Skip to content

feat(plugin): add AI Navigator plugin with 34 AI platform entries#951

Open
StarrySerendipity wants to merge 1 commit into
Project-N-E-K-O:mainfrom
StarrySerendipity:feature/ai-navigator-plugin
Open

feat(plugin): add AI Navigator plugin with 34 AI platform entries#951
StarrySerendipity wants to merge 1 commit into
Project-N-E-K-O:mainfrom
StarrySerendipity:feature/ai-navigator-plugin

Conversation

@StarrySerendipity
Copy link
Copy Markdown

@StarrySerendipity StarrySerendipity commented Apr 25, 2026

Summary by CodeRabbit

新功能

  • 添加 AI 导航插件,提供智能导航界面
  • 支持平台搜索、分类浏览和快速打开功能
  • 记录访问历史,支持拖拽重排应用卡片
  • 实时展示平台统计与访问计数

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

漫步(Walkthrough)

新增 AI-Navigator 插件模块,提供基于目录的网站导航 UI 和操作。启动时注册静态 HTML UI,返回平台总数和分类名称。包含模糊平台解析器用于容错匹配,支持打开平台 URL、列表展示、获取平台详情等功能喵。

变更(Changes)

Cohort / File(s) Summary
AI-Navigator Plugin Core
plugin/plugins/ai_navigator/__init__.py
实现插件主体逻辑,包含 AiNavigatorPlugin 类与生命周期方法、模糊平台解析、异步打开浏览器、列表和获取平台信息等四个入口方法喵。
Plugin Manifest
plugin/plugins/ai_navigator/plugin.toml
声明插件身份、入口点、SDK 兼容性要求和运行时配置,禁用商店发布喵。
Navigation UI
plugin/plugins/ai_navigator/static/index.html
中文单页应用,含动画背景、搜索过滤、实时计数器、最近访问栏、五个分类区域的平台卡片、本地存储状态管理和拖拽排序功能喵。

序列图(Sequence Diagram)

sequenceDiagram
    participant User as 用户
    participant UI as UI界面
    participant Plugin as AI-Navigator<br/>Plugin
    participant Browser as 系统浏览器

    User->>UI: 1. 访问导航页面
    UI->>Plugin: 2. on_startup() 初始化
    Plugin-->>UI: 3. 返回平台列表&元数据
    UI->>UI: 4. 渲染分类卡片<br/>恢复localStorage状态
    
    User->>UI: 5. 搜索或点击平台
    UI->>UI: 6. 模糊匹配查询
    UI->>UI: 7. 更新访问计数&历史
    
    User->>UI: 8. 点击打开平台
    UI->>Plugin: 9. open_platform(name)
    Plugin->>Plugin: 10. 解析平台信息
    Plugin->>Browser: 11. asyncio.to_thread<br/>启动系统浏览器
    Browser-->>User: 12. 打开平台URL
    Plugin-->>UI: 13. 返回成功/失败
    UI->>UI: 14. 刷新统计&卡片显示
Loading

代码审查工作量评估(Estimated Code Review Effort)

🎯 4 (Complex) | ⏱️ ~45 minutes

推荐审核者(Suggested Reviewers)

  • wehos

诗歌(Poem)

智能导航中心闪闪发光✨
分类平台任你徜徉
模糊搜索贼聪慧
拖拽排序萌萌哒
本地存储不遗忘~喵💕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR标题准确反映了主要变更——添加了AI Navigator插件及其34个AI平台条目,简洁明了。
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
plugin/plugins/ai_navigator/static/index.html (1)

579-614: 前后端的平台目录抄了两份,迟早会漂移喵~

这里硬编码的 platforms 数组和 plugin/plugins/ai_navigator/__init__.py 里的 PLATFORMS 是同一份数据的两个副本,字段还不完全一致(前端多 icon/gradient/tags,后端是 aliases)。已经看到一些差异了,比如 chatglm 后端 URL 是 https://chatglm.cn/main/alltoolsdetail,这边却带着 ?t=1776803740258&lang=zh 这种看起来像会过期的查询参数;还有 yuanbaopikarunwaymidjourney 等等的 URL 在两边也不一样喵。

后续维护时只改一边就会出现"语音能打开但 UI 打不开"之类的尴尬情况。建议让后端通过一个接口(比如 /plugin/ai_navigator/api/platforms)把目录吐给前端,前端只保留 icon/gradient 之类纯展示字段,统一以后端为唯一数据源喵~

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/plugins/ai_navigator/static/index.html` around lines 579 - 614, The
frontend hardcodes a duplicate platforms array (const platforms) that drifts
from the backend PLATFORMS in plugin/plugins/ai_navigator/__init__.py; refactor
so the backend is the single source of truth by exposing an API endpoint (e.g.
/plugin/ai_navigator/api/platforms) that returns the canonical PLATFORMS, then
update the frontend to fetch that endpoint and only keep presentation-only
fields (icon/gradient/tags) locally while relying on the backend for
url/name/description/region/aliases; update code references to the frontend
"platforms" variable to use the fetched data and remove the duplicated static
array.
plugin/plugins/ai_navigator/__init__.py (1)

83-93: 应该用标准库 webbrowser 喵~

Python 自带 webbrowser.open(url, new=2),跨平台且自动处理各系统差异,不用像现在一样分 win32 / darwin / 其它,也没有 subprocess.Popen 那种子进程托管的副作用喵。虽然现在 URL 来自 PLATFORMS 写死的列表和本地 localhost 比较安全,但用标准库方式更优雅、更可维护呢~

建议的改动
-import sys
-import subprocess
+import webbrowser
@@
 def _open_url_in_browser(url: str) -> None:
     """在默认浏览器打开 URL(同步调用,仅供 asyncio.to_thread 使用)"""
     try:
-        if sys.platform == "win32":
-            subprocess.Popen(["cmd", "/c", "start", "", url], shell=False)
-        elif sys.platform == "darwin":
-            subprocess.Popen(["open", url])
-        else:
-            subprocess.Popen(["xdg-open", url])
+        if not webbrowser.open(url, new=2):
+            raise RuntimeError(f"webbrowser.open returned False for {url}")
     except Exception as e:
         raise RuntimeError(f"无法在浏览器中打开 {url}: {e}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/plugins/ai_navigator/__init__.py` around lines 83 - 93, The
_open_url_in_browser function uses subprocess calls per-platform which is
fragile and unnecessary; replace its implementation to use the standard library
webbrowser module (call webbrowser.open(url, new=2)) to open URLs
cross-platform, remove the manual sys.platform branches and subprocess.Popen
usage, and preserve the error handling by catching exceptions from
webbrowser.open and raising a RuntimeError that includes the URL and original
exception message.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugin/plugins/ai_navigator/__init__.py`:
- Around line 61-72: 别名匹配逻辑重复给分:在循环里对 paliases 的两处条件 alias_clean and q_clean in
alias_clean (当前 +50) 与 q_clean and alias_clean and len(q_clean) >= 2 and q_clean
in alias_clean (当前 +45) 是重叠的。请保留一处带长度限制的匹配,具体做法是把第一个别名匹配条件 alias_clean and
q_clean in alias_clean 改为同时检查长度(即加上 len(q_clean) >= 2),并删除重复的后者 (q_clean and
alias_clean and len(q_clean) >= 2 and q_clean in alias_clean) 及其对应的 +45
加分,确保短查询不会触发 clean 匹配(symbols: paliases, alias_clean, q_clean, score)。

In `@plugin/plugins/ai_navigator/static/index.html`:
- Around line 630-637: In init(), when resetting the visit counter you set
localStorage with a misspelled key 'aiTodayVisis' and also redundantly write to
storage even though todayVisits = 0 in memory and is persisted later; remove the
mistaken localStorage.setItem('aiTodayVisis', '0') line (or if you prefer to
persist immediately, replace the key with the correct 'aiTodayVisits') so that
lastVisitDate, todayVisits and the proper 'aiTodayVisits' localStorage key stay
consistent.
- Around line 905-924: The drag-drop uses DOM-derived indexes
(zoneCards/fromIndex/toIndex) which are filtered by searchQuery, causing wrong
splices into the full categoryPlatforms array; change the drop logic to locate
items by id instead: read draggedCard.dataset.id and targetCard.dataset.id, then
compute fromIndex = categoryPlatforms.findIndex(p => p.id === draggedId) and
toIndex = categoryPlatforms.findIndex(p => p.id === targetId) (or disable
dragging when search is active), then perform the splice on categoryPlatforms
only if both indexes are valid and different, and finally call
savePlatformOrder() and renderZones(); keep references to zoneCards,
categoryPlatforms, platforms, draggedCard, targetCard, savePlatformOrder and
renderZones to locate where to modify.

---

Nitpick comments:
In `@plugin/plugins/ai_navigator/__init__.py`:
- Around line 83-93: The _open_url_in_browser function uses subprocess calls
per-platform which is fragile and unnecessary; replace its implementation to use
the standard library webbrowser module (call webbrowser.open(url, new=2)) to
open URLs cross-platform, remove the manual sys.platform branches and
subprocess.Popen usage, and preserve the error handling by catching exceptions
from webbrowser.open and raising a RuntimeError that includes the URL and
original exception message.

In `@plugin/plugins/ai_navigator/static/index.html`:
- Around line 579-614: The frontend hardcodes a duplicate platforms array (const
platforms) that drifts from the backend PLATFORMS in
plugin/plugins/ai_navigator/__init__.py; refactor so the backend is the single
source of truth by exposing an API endpoint (e.g.
/plugin/ai_navigator/api/platforms) that returns the canonical PLATFORMS, then
update the frontend to fetch that endpoint and only keep presentation-only
fields (icon/gradient/tags) locally while relying on the backend for
url/name/description/region/aliases; update code references to the frontend
"platforms" variable to use the fetched data and remove the duplicated static
array.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 155fbb66-9511-4476-8919-55593e658474

📥 Commits

Reviewing files that changed from the base of the PR and between 7019c4b and 6701669.

📒 Files selected for processing (3)
  • plugin/plugins/ai_navigator/__init__.py
  • plugin/plugins/ai_navigator/plugin.toml
  • plugin/plugins/ai_navigator/static/index.html

Comment on lines +61 to +72
for alias in paliases:
alias_clean = alias.replace("-", "").replace("_", "").replace(" ", "").replace(".", "")
if q == alias or q in alias:
score += 60
if alias_clean and q_clean in alias_clean:
score += 50
if q_clean and alias_clean and len(q_clean) >= 2 and q_clean in alias_clean:
score += 45

if q_clean in pname_clean and len(q_clean) >= 2:
score += 30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

别名打分这两行重复加分了喵~

第 67 行的条件 q_clean and alias_clean and len(q_clean) >= 2 and q_clean in alias_clean 是第 65 行 alias_clean and q_clean in alias_clean严格子集(只是多一个 len(q_clean) >= 2)。也就是说当 q_clean 长度 ≥ 2 时,这条别名会同时命中两次,累加 +50 +45 = +95,把别名匹配的权重莫名其妙拉到比 q in pid (+80) 还高,本笑生看一眼就觉得不对劲喵~

如果是想"短查询不走 clean 匹配"以避免误命中(比如 ai 命中所有带 ai 的别名),那应该把第 65 行改成 len(q_clean) >= 2 才对,并删掉重复的第 67–68 行喵。

🐛 建议改法
         for alias in paliases:
             alias_clean = alias.replace("-", "").replace("_", "").replace(" ", "").replace(".", "")
             if q == alias or q in alias:
                 score += 60
-            if alias_clean and q_clean in alias_clean:
+            if alias_clean and q_clean and len(q_clean) >= 2 and q_clean in alias_clean:
                 score += 50
-            if q_clean and alias_clean and len(q_clean) >= 2 and q_clean in alias_clean:
-                score += 45
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/plugins/ai_navigator/__init__.py` around lines 61 - 72,
别名匹配逻辑重复给分:在循环里对 paliases 的两处条件 alias_clean and q_clean in alias_clean (当前 +50)
与 q_clean and alias_clean and len(q_clean) >= 2 and q_clean in alias_clean (当前
+45) 是重叠的。请保留一处带长度限制的匹配,具体做法是把第一个别名匹配条件 alias_clean and q_clean in alias_clean
改为同时检查长度(即加上 len(q_clean) >= 2),并删除重复的后者 (q_clean and alias_clean and
len(q_clean) >= 2 and q_clean in alias_clean) 及其对应的 +45 加分,确保短查询不会触发 clean
匹配(symbols: paliases, alias_clean, q_clean, score)。

Comment on lines +630 to +637
function init() {
const today = new Date().toDateString();
if (lastVisitDate !== today) {
todayVisits = 0;
lastVisitDate = today;
localStorage.setItem('aiLastVisitDate', today);
localStorage.setItem('aiTodayVisis', '0');
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

笨蛋开发者,这里写错 key 了喵!

第 636 行 localStorage.setItem('aiTodayVisis', '0') —— 少了一个 t,应该是 aiTodayVisits 才对喵。结果就是日期翻篇时,真正被读取的 aiTodayVisits 那个 key 根本没被清零,下次启动 todayVisits 还是会从昨天的旧值开始累加,直到下一次 openPlatform 触发覆盖为止喵~

而且写的这一行其实多此一举 —— 内存里 todayVisits = 0 就够了,反正下次访问时第 809 行会重新写入正确的 key,本笑生建议直接删掉这行,省得继续往 localStorage 里塞垃圾 key 喵。

🐛 修复建议
             if (lastVisitDate !== today) {
                 todayVisits = 0;
                 lastVisitDate = today;
                 localStorage.setItem('aiLastVisitDate', today);
-                localStorage.setItem('aiTodayVisis', '0');
+                localStorage.setItem('aiTodayVisits', '0');
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function init() {
const today = new Date().toDateString();
if (lastVisitDate !== today) {
todayVisits = 0;
lastVisitDate = today;
localStorage.setItem('aiLastVisitDate', today);
localStorage.setItem('aiTodayVisis', '0');
}
function init() {
const today = new Date().toDateString();
if (lastVisitDate !== today) {
todayVisits = 0;
lastVisitDate = today;
localStorage.setItem('aiLastVisitDate', today);
localStorage.setItem('aiTodayVisits', '0');
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/plugins/ai_navigator/static/index.html` around lines 630 - 637, In
init(), when resetting the visit counter you set localStorage with a misspelled
key 'aiTodayVisis' and also redundantly write to storage even though todayVisits
= 0 in memory and is persisted later; remove the mistaken
localStorage.setItem('aiTodayVisis', '0') line (or if you prefer to persist
immediately, replace the key with the correct 'aiTodayVisits') so that
lastVisitDate, todayVisits and the proper 'aiTodayVisits' localStorage key stay
consistent.

Comment on lines +905 to +924
const targetId = targetCard.dataset.id;
const category = sourceZone.dataset.category;

const zoneCards = Array.from(sourceZone.querySelectorAll('.app-card'));
const fromIndex = zoneCards.indexOf(draggedCard);
const toIndex = zoneCards.indexOf(targetCard);

if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return;

const categoryPlatforms = platforms.filter(p => p.category === category);
const [moved] = categoryPlatforms.splice(fromIndex, 1);
categoryPlatforms.splice(toIndex, 0, moved);

const otherPlatforms = platforms.filter(p => p.category !== category);
platforms.length = 0;
platforms.push(...otherPlatforms, ...categoryPlatforms);

savePlatformOrder();
renderZones();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

搜索过滤时拖拽会把顺序搞乱喵!

zoneCards 是从 DOM 里取的、已经被 searchQuery 过滤过的卡片集合,而 categoryPlatformsplatforms 里该分类的全集。把过滤后的 fromIndex/toIndex 直接用来 splice 全集,就会搬错条目、把无关平台的位置也连带打乱喵~搜索一开就坏给你看哦笨蛋。

建议要么按"被拖动的实际平台 id"在全集里定位,要么干脆在搜索激活时禁用拖拽喵。

🐛 修复建议(按 id 定位真实下标)
-                const zoneCards = Array.from(sourceZone.querySelectorAll('.app-card'));
-                const fromIndex = zoneCards.indexOf(draggedCard);
-                const toIndex = zoneCards.indexOf(targetCard);
-
-                if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return;
-
-                const categoryPlatforms = platforms.filter(p => p.category === category);
-                const [moved] = categoryPlatforms.splice(fromIndex, 1);
-                categoryPlatforms.splice(toIndex, 0, moved);
+                const categoryPlatforms = platforms.filter(p => p.category === category);
+                const fromIndex = categoryPlatforms.findIndex(p => p.id === draggedId);
+                const toIndex = categoryPlatforms.findIndex(p => p.id === targetId);
+                if (fromIndex === -1 || toIndex === -1 || fromIndex === toIndex) return;
+                const [moved] = categoryPlatforms.splice(fromIndex, 1);
+                categoryPlatforms.splice(toIndex, 0, moved);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin/plugins/ai_navigator/static/index.html` around lines 905 - 924, The
drag-drop uses DOM-derived indexes (zoneCards/fromIndex/toIndex) which are
filtered by searchQuery, causing wrong splices into the full categoryPlatforms
array; change the drop logic to locate items by id instead: read
draggedCard.dataset.id and targetCard.dataset.id, then compute fromIndex =
categoryPlatforms.findIndex(p => p.id === draggedId) and toIndex =
categoryPlatforms.findIndex(p => p.id === targetId) (or disable dragging when
search is active), then perform the splice on categoryPlatforms only if both
indexes are valid and different, and finally call savePlatformOrder() and
renderZones(); keep references to zoneCards, categoryPlatforms, platforms,
draggedCard, targetCard, savePlatformOrder and renderZones to locate where to
modify.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant