feat(plugin): add AI Navigator plugin with 34 AI platform entries#951
feat(plugin): add AI Navigator plugin with 34 AI platform entries#951StarrySerendipity wants to merge 1 commit into
Conversation
漫步(Walkthrough)新增 AI-Navigator 插件模块,提供基于目录的网站导航 UI 和操作。启动时注册静态 HTML UI,返回平台总数和分类名称。包含模糊平台解析器用于容错匹配,支持打开平台 URL、列表展示、获取平台详情等功能喵。 变更(Changes)
序列图(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. 刷新统计&卡片显示
代码审查工作量评估(Estimated Code Review Effort)🎯 4 (Complex) | ⏱️ ~45 minutes 推荐审核者(Suggested Reviewers)
诗歌(Poem)
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ 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. Comment |
There was a problem hiding this comment.
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这种看起来像会过期的查询参数;还有yuanbao、pika、runway、midjourney等等的 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
📒 Files selected for processing (3)
plugin/plugins/ai_navigator/__init__.pyplugin/plugins/ai_navigator/plugin.tomlplugin/plugins/ai_navigator/static/index.html
| 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 | ||
|
|
There was a problem hiding this comment.
别名打分这两行重复加分了喵~
第 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)。
| function init() { | ||
| const today = new Date().toDateString(); | ||
| if (lastVisitDate !== today) { | ||
| todayVisits = 0; | ||
| lastVisitDate = today; | ||
| localStorage.setItem('aiLastVisitDate', today); | ||
| localStorage.setItem('aiTodayVisis', '0'); | ||
| } |
There was a problem hiding this comment.
笨蛋开发者,这里写错 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.
| 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.
| 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(); | ||
| }); |
There was a problem hiding this comment.
搜索过滤时拖拽会把顺序搞乱喵!
zoneCards 是从 DOM 里取的、已经被 searchQuery 过滤过的卡片集合,而 categoryPlatforms 是 platforms 里该分类的全集。把过滤后的 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.
Summary by CodeRabbit
新功能