Conversation
- 隔离:utilityProcess + vm.createContext 双层沙箱;心跳检测、崩溃自动重启(指数退避 3 次) - HostApi:request(Electron net.fetch + URL 白名单)/ storage(每插件 KV 原子写)/ log / register / on - lx 兼容:window.lx 垫片 + gz_ 前缀自动解压,沿用头部 JSDoc 元数据约定 - 路由:search / musicUrl 已端到端;lyric / pic / meta HostApi 已预留 - 渲染端:设置页新增「插件管理」分类,支持原生文件选择框导入、启用开关、卸载确认、状态徽章、lx/源标识 - SettingsItem 支持 fullWidth 模式以承载独占布局的自定义组件 - 打包:electron-vite 配置增补 sandbox.worker 独立 entry - 详见 docs/plugins-mvp.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
该 PR 旨在为 SPlayer-Next 引入插件系统(含主进程沙箱、IPC、渲染端管理 UI),并新增/重构多平台音源 API(Netease / QQMusic / Kugou)调用链路与部分类型结构。
Changes:
- 新增插件系统:主进程 registry/router/sandbox/host/net/storage/loader + preload/IPC + 设置页插件管理 UI 与文档。
- 新增统一音源 API IPC(
apis:call/apis:clearSession)及渲染端代理(src/apis/*),并引入 Netease cookies 落库(SQLiteaccount_sessions)。 - 重构歌词类型(
LyricSource/LyricData等)与外部歌词列表结构,补充相关 i18n/样式与窗口/托盘行为调整。
Reviewed changes
Copilot reviewed 121 out of 125 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.node.json | 移除 electron/server 编译包含与路径别名。 |
| src/utils/url.ts | 新增外链判断/打开工具函数。 |
| src/utils/lyric/parseTimeline.ts | 更新 YRC 注释文案表述。 |
| src/utils/lyric/parse.ts | 调整外部歌词最优索引选择的入参类型。 |
| src/types/settings-schema.ts | 为设置项新增 fullWidth 支持。 |
| src/styles/global.css | 调整可拖拽行为与滚动条宽度。 |
| src/stores/user.ts | 新增用户登录态 store(Netease)。 |
| src/stores/plugins.ts | 新增插件管理 Pinia store(列表/订阅/安装/启用/卸载)。 |
| src/stores/media.ts | 将歌词来源类型切换为 LyricData 并适配字段名。 |
| src/settings/schema.ts | 新增“插件管理”设置分类与自定义面板组件。 |
| src/services/lyricLoader.ts | 歌词加载逻辑适配 LyricData。 |
| src/pages/Home.vue | 增加 Netease 搜索测试相关 UI/逻辑。 |
| src/layouts/components/NavHeader.vue | 调整标题栏按钮属性与拖拽区域 class 分布。 |
| src/i18n/locales/zh-CN.json | 新增插件管理相关文案。 |
| src/i18n/locales/en-US.json | 新增插件管理相关文案(英文)。 |
| src/components/ui/SToast.vue | 调整 toast 容器 z-index。 |
| src/components/settings/custom/PluginManager.vue | 新增插件管理面板(导入/启用/卸载/更新提示)。 |
| src/components/settings/SettingsItem.vue | 支持 fullWidth 的 custom 设置项渲染分支。 |
| src/apis/qqmusic.ts | 新增 QQMusic 渲染端 API 代理(Proxy → IPC)。 |
| src/apis/netease.ts | 新增 Netease 渲染端 API 代理与清会话方法。 |
| src/apis/kugou.ts | 新增 Kugou 渲染端 API 代理(Proxy → IPC)。 |
| shared/types/settings.ts | 系统配置新增 plugins: PluginsConfig。 |
| shared/types/plugin.ts | 新增插件系统跨进程类型契约与渲染端 PluginsApi。 |
| shared/types/player.ts | 扩展 TrackSource=plugin、补充 plugin 字段、调整 externalLyrics 结构。 |
| shared/types/platform.ts | 新增平台类型(netease/qqmusic/kugou)。 |
| shared/types/nowPlaying.ts | NowPlaying payload/snapshot 的歌词来源类型改为 LyricData。 |
| shared/types/lyrics.ts | 重构歌词来源为 LyricSource 字面量 + LyricData。 |
| shared/types/apis.ts | 新增统一音源 API 的类型(ApiPlatform/ApisApi/ApiCallResponse)。 |
| shared/defaults/settings.ts | 默认系统配置补齐插件默认配置。 |
| shared/defaults/plugin-api.ts | 新增 Host API level、超时、限制与错误码、默认插件配置。 |
| electron/preload/index.ts | 暴露 window.api.plugins 与 window.api.apis。 |
| electron/preload/index.d.ts | 增加 plugins/apis 的 preload 类型声明。 |
| electron/main/window/index.ts | 调整歌词窗口恢复逻辑与 isWin 引用位置。 |
| electron/main/services/tray.ts | 托盘菜单与任务栏歌词同步逻辑适配 isWin 常量。 |
| electron/main/services/nowPlaying.ts | NowPlaying 的歌词来源类型改为 LyricData。 |
| electron/main/plugins/storage.ts | 新增每插件隔离 KV 存储(落盘+原子写)。 |
| electron/main/plugins/sandbox.ts | 新增 utilityProcess 沙箱控制器(心跳/超时/消息路由)。 |
| electron/main/plugins/router.ts | 新增插件动作路由(当前实现 musicUrl)。 |
| electron/main/plugins/registry.ts | 新增插件注册表(扫描/启停/重启/状态广播/安装卸载)。 |
| electron/main/plugins/net.ts | 新增宿主网络代理(net.fetch + 白名单 + 超时)。 |
| electron/main/plugins/lx-shim.ts | 新增 lx user_api 兼容垫片。 |
| electron/main/plugins/loader.ts | 新增脚本加载器(gz_ 解压、JSDoc 元数据解析、ID 生成)。 |
| electron/main/plugins/host.ts | 新增 Host API dispatch(request/storage 等)。 |
| electron/main/ipc/window.ts | 任务栏歌词窗口 IPC 按平台(Windows)条件注册。 |
| electron/main/ipc/plugin.ts | 新增插件系统 IPC(list/install/uninstall/resolveUrl/status 广播等)。 |
| electron/main/ipc/library.ts | artist avatar 获取入口从 @server/* 切到 @main/apis/musicbrainz。 |
| electron/main/ipc/index.ts | 注册新增的 plugin/apis IPC handlers。 |
| electron/main/ipc/config.ts | taskbarLyric 的副作用/广播在非 Windows 下跳过。 |
| electron/main/ipc/apis.ts | 新增统一音源 API IPC 分发与清会话入口。 |
| electron/main/database/sessions.ts | 新增第三方音源账号 cookies 的 SQLite 存取。 |
| electron/main/database/index.ts | 初始化 DB 时新增 account_sessions 表。 |
| electron/main/core/index.ts | App 启动初始化插件系统;退出时做平台条件化写入与 shutdown。 |
| electron/main/apis/qqmusic/modules/song_list.ts | 新增 QQMusic 歌单详情模块。 |
| electron/main/apis/qqmusic/modules/song_info.ts | 新增 QQMusic 单曲详情模块。 |
| electron/main/apis/qqmusic/modules/search.ts | 新增 QQMusic 搜索模块。 |
| electron/main/apis/qqmusic/modules/match.ts | 新增 QQMusic 模糊匹配歌词模块(search+lyric)。 |
| electron/main/apis/qqmusic/modules/lyric.ts | 新增 QQMusic 歌词模块(解密 QRC/LRC/译/罗马音)。 |
| electron/main/apis/qqmusic/modules/leaderboard.ts | 新增 QQMusic 排行榜模块。 |
| electron/main/apis/qqmusic/modules/index.ts | QQMusic 模块注册表。 |
| electron/main/apis/qqmusic/modules/hot_search.ts | 新增 QQMusic 热搜模块。 |
| electron/main/apis/qqmusic/index.ts | QQMusic 主进程服务入口(含内存缓存)。 |
| electron/main/apis/qqmusic/core/types.ts | QQMusic 模块函数签名类型。 |
| electron/main/apis/qqmusic/core/request.ts | QQMusic 请求层(session 缓存/初始化)。 |
| electron/main/apis/qqmusic/core/qrc.ts | QRC 解密与解压工具。 |
| electron/main/apis/qqmusic/core/config.ts | QQMusic 通用常量与 comm 伪装参数。 |
| electron/main/apis/netease/modules/user_subcount.ts | 新增 Netease 用户收藏计数模块。 |
| electron/main/apis/netease/modules/user_record.ts | 新增 Netease 听歌排行模块。 |
| electron/main/apis/netease/modules/user_playlist.ts | 新增 Netease 用户歌单列表模块。 |
| electron/main/apis/netease/modules/user_level.ts | 新增 Netease 用户等级模块。 |
| electron/main/apis/netease/modules/user_follows.ts | 新增 Netease 关注列表模块。 |
| electron/main/apis/netease/modules/user_followeds.ts | 新增 Netease 粉丝列表模块。 |
| electron/main/apis/netease/modules/user_detail_new.ts | 新增 Netease 用户详情(eapi)模块。 |
| electron/main/apis/netease/modules/user_detail.ts | 新增 Netease 用户详情(旧版)模块。 |
| electron/main/apis/netease/modules/user_cloud.ts | 新增 Netease 云盘列表模块。 |
| electron/main/apis/netease/modules/user_account.ts | 新增 Netease 当前账号模块。 |
| electron/main/apis/netease/modules/search_suggest_pc.ts | 新增 Netease PC 搜索建议模块。 |
| electron/main/apis/netease/modules/search_suggest.ts | 新增 Netease 搜索建议模块。 |
| electron/main/apis/netease/modules/search_multimatch.ts | 新增 Netease 多类型搜索建议模块。 |
| electron/main/apis/netease/modules/search_match.ts | 新增 Netease 本地歌曲匹配模块。 |
| electron/main/apis/netease/modules/search_hot_detail.ts | 新增 Netease 热搜详情模块。 |
| electron/main/apis/netease/modules/search_hot.ts | 新增 Netease 热搜简版模块。 |
| electron/main/apis/netease/modules/search_default.ts | 新增 Netease 默认搜索词模块。 |
| electron/main/apis/netease/modules/search.ts | 新增 Netease 普通搜索模块(含 voice 特例)。 |
| electron/main/apis/netease/modules/register_anonimous.ts | 新增 Netease 匿名注册模块。 |
| electron/main/apis/netease/modules/logout.ts | 新增 Netease 登出模块。 |
| electron/main/apis/netease/modules/login_status.ts | 新增 Netease 登录状态模块。 |
| electron/main/apis/netease/modules/login_refresh.ts | 新增 Netease 刷新登录态模块。 |
| electron/main/apis/netease/modules/login_qr_key.ts | 新增 Netease 二维码 key 模块。 |
| electron/main/apis/netease/modules/login_qr_create.ts | 新增 Netease 二维码 URL 生成模块。 |
| electron/main/apis/netease/modules/login_qr_check.ts | 新增 Netease 二维码轮询模块。 |
| electron/main/apis/netease/modules/login_cellphone.ts | 新增 Netease 手机号登录模块。 |
| electron/main/apis/netease/modules/login.ts | 新增 Netease 邮箱登录模块。 |
| electron/main/apis/netease/modules/index.ts | Netease 模块注册表。 |
| electron/main/apis/netease/modules/cloudsearch.ts | 新增 Netease cloudsearch 模块。 |
| electron/main/apis/netease/modules/captcha_verify.ts | 新增 Netease 短信验证码校验模块。 |
| electron/main/apis/netease/modules/captcha_sent.ts | 新增 Netease 发送短信验证码模块。 |
| electron/main/apis/netease/index.ts | Netease 主进程服务入口(cookies 落库 + 内存 cache)。 |
| electron/main/apis/netease/core/types.ts | Netease 模块函数签名类型。 |
| electron/main/apis/netease/core/request.ts | Netease 请求层(加密/UA/cookie/解密/状态归一化)。 |
| electron/main/apis/netease/core/option.ts | Netease 请求 options 工厂。 |
| electron/main/apis/netease/core/device.ts | Netease 进程级 deviceId/匿名 token 管理。 |
| electron/main/apis/netease/core/crypto.ts | Netease weapi/linuxapi/eapi 加解密实现。 |
| electron/main/apis/netease/core/cookie.ts | Netease cookie 字符串/对象互转。 |
| electron/main/apis/netease/core/config.ts | Netease 常量/UA/OS 伪装与加密相关配置。 |
| electron/main/apis/netease/core/cache.ts | Netease 内存响应缓存(LRU/TTL)。 |
| electron/main/apis/musicbrainz.ts | 新增 MusicBrainz/TheAudioDB 歌手头像抓取与缓存。 |
| electron/main/apis/kugou/modules/search.ts | 新增 Kugou 搜索模块(结果规范化)。 |
| electron/main/apis/kugou/modules/lyric.ts | 新增 Kugou 歌词模块(krc/lrc 解码)。 |
| electron/main/apis/kugou/modules/index.ts | Kugou 模块注册表。 |
| electron/main/apis/kugou/index.ts | Kugou 主进程服务入口(含内存缓存)。 |
| electron/main/apis/kugou/core/types.ts | Kugou 模块函数签名类型。 |
| electron/main/apis/kugou/core/request.ts | Kugou 请求层(GET + retry + 错误码处理)。 |
| electron/main/apis/kugou/core/krc.ts | KRC 解密与格式化工具。 |
| electron/main/apis/kugou/core/config.ts | Kugou 常量与工具(headers、实体解码、时长转换)。 |
| electron.vite.config.ts | main build 增加 sandbox.worker 入口并移除 @server alias。 |
| docs/plugins-usage.md | 新增插件使用指南文档。 |
| docs/plugins-mvp.md | 新增插件系统 MVP 进度/接续文档。 |
| demo/audio-architecture.md | 更新文档中 YRC 文案为 Netease。 |
| components.d.ts | 更新自动组件类型声明(新增图标与 PluginManager)。 |
| CLAUDE.md | 更新类型系统说明与文件指向。 |
| .github/copilot-instructions.md | 更新路径别名说明,移除 @server。 |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| export const openExternal = (url?: string | null): void => { | ||
| if (!isExternalUrl(url)) return; | ||
| window.open(url, "_blank"); |
There was a problem hiding this comment.
openExternal 直接使用 window.open(url, "_blank") 会让新窗口拿到 window.opener,存在被外链页面反向控制当前窗口的风险。建议至少加上 noopener,noreferrer(并将 opener 置空),或在 Electron 环境下改为通过主进程 shell.openExternal 打开外链。
|
|
||
| - 仅支持 HTTP/HTTPS | ||
| - 单次下载上限 **9 MB** | ||
| - 请求超时 15 秒,最多跟随 3 次重定向 |
There was a problem hiding this comment.
文档写“最多跟随 3 次重定向”,但当前 electron/main/ipc/plugin.ts 里用的是 net.fetch(..., { redirect: 'follow' }),实现上并未限制重定向次数(取决于底层 fetch 默认策略)。建议要么在实现里显式限制 redirect 次数,要么把文档改成与实现一致,避免误导用户。
| - 请求超时 15 秒,最多跟随 3 次重定向 | |
| - 请求超时 15 秒,重定向策略以当前运行时实现为准 |
| ``` | ||
| {userData}/ | ||
| ├── plugins/ | ||
| │ ├── scripts/{id}.js 脚本源码 | ||
| │ ├── manifest.json 已安装列表 | ||
| │ ├── data/{id}.json 插件私有 KV 存储(splayer.storage 写入的) | ||
| │ └── logs/{id}.log 插件日志(splayer.log) | ||
| ``` |
There was a problem hiding this comment.
文档列出了 {userData}/plugins/logs/{id}.log 并声称卸载会清理日志,但当前实现里没有看到对 plugins/logs 的写入/清理逻辑(registry.uninstall 只删 scripts 与 data)。建议补齐日志落盘与清理实现,或先从文档中移除 logs 目录描述,避免与实际行为不一致。
| case "fatal": | ||
| this.events.onFatal(msg.error); | ||
| done(Object.assign(new Error(msg.error.message), { code: msg.error.code })); | ||
| return; |
There was a problem hiding this comment.
收到 worker 的 fatal 消息后这里只 done(reject),但没有 kill() 子进程;而 sandbox.worker.ts 在发送 fatal 后也不会自动退出,容易留下“已 fatal 但仍存活”的 utilityProcess,造成资源泄漏/心跳逻辑异常。建议在 case 'fatal' 里主动 this.kill()(并确保只执行一次),保证子进程被回收。
| const ctrl = new AbortController(); | ||
| inflight.set(msg.requestId, ctrl); | ||
| try { | ||
| const data = await handler(msg.params); | ||
| inflight.delete(msg.requestId); | ||
| if (ctrl.signal.aborted) { | ||
| send({ |
There was a problem hiding this comment.
cancel 分支里调用了 AbortController.abort(),但在 call 分支中创建的 ctrl 并没有传递给 handler(也没有注入到 sandbox 全局让脚本可感知),所以取消/超时只能在 handler 结束后“丢弃结果”,无法真正中断耗时任务。建议把 signal 通过第二参数传给 handler(或在 params 中附带)并在 lx shim / splayer.on 的类型上约定可选的 signal。
| let body: BodyInit | undefined; | ||
| if (opts.body != null) { | ||
| if (typeof opts.body === "string") body = opts.body; | ||
| else if (opts.body instanceof ArrayBuffer) body = opts.body; | ||
| else body = (opts.body as Uint8Array).buffer as ArrayBuffer; | ||
| } |
There was a problem hiding this comment.
构造请求 body 时把 Uint8Array 转成了 opts.body.buffer。如果插件传入的是 subarray()(存在 byteOffset/byteLength),直接取 .buffer 会把整块底层 ArrayBuffer 都发出去,导致请求体包含多余字节。建议直接把 Uint8Array 作为 BodyInit 传给 fetch,或用 opts.body.slice().buffer/Buffer.from(opts.body) 保留正确长度。
| /** 恢复歌词相关窗口 */ | ||
| export const restoreLyricWindows = (): void => { | ||
| if (!(store.get("system.rememberWindowState") ?? true)) return; | ||
| if (store.get("windowStates.desktopLyric.visible")) createDesktopLyricWindow(); | ||
| if (store.get("windowStates.dynamicIsland.visible")) createDynamicIslandWindow(); | ||
| if (isWin && store.get("windowStates.taskbarLyric.visible")) { |
There was a problem hiding this comment.
restoreLyricWindows() 里移除了对 system.rememberWindowState 的判断,导致用户关闭“记忆窗口状态”后依然会按 windowStates.*.visible 恢复歌词窗口,和设置项语义不一致。建议恢复原来的 guard:当 rememberWindowState=false 时直接 return(或至少不要自动恢复歌词窗口)。
| export const resolveUrl = async (args: PluginResolveUrlArgs): Promise<MusicUrlRes> => { | ||
| const rt = pluginRegistry.getRuntime(args.pluginId); | ||
| if (!rt) { | ||
| throw Object.assign(new Error(`plugin ${args.pluginId} not found`), { | ||
| code: PluginErrorCodes.NOT_FOUND, | ||
| }); | ||
| } | ||
| const check = supportsAction(rt, args.source, "musicUrl"); | ||
| if (!check.ok) { | ||
| throw Object.assign( | ||
| new Error(`plugin ${args.pluginId} does not support musicUrl on source ${args.source}`), | ||
| { code: PluginErrorCodes.ACTION_UNSUPPORTED }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
resolveUrl() 在插件未 ready / 已禁用时也会走 supportsAction(),从而抛出 ACTION_UNSUPPORTED;实际更合理的错误应是 PLUGIN_NOT_READY 或 PLUGIN_DISABLED(并且当前实现会让“加载中”状态误报为“不支持动作”)。建议在 resolveUrl 里先判断 rt.enabled/rt.status.state,分别抛对应错误码;只有 state==='ready' 时才做 sources/actions 校验。
| } catch (err) { | ||
| send({ | ||
| kind: "fatal", | ||
| error: { | ||
| code: "PLUGIN_SCRIPT_ERROR", | ||
| message: err instanceof Error ? `${err.message}\n${err.stack ?? ""}` : String(err), | ||
| }, | ||
| }); | ||
| return; |
There was a problem hiding this comment.
脚本执行失败时发送了 kind: 'fatal' 但随后只是 return,进程不会退出。主进程侧如果没及时 kill,就会残留一个空转的 sandbox 进程。建议在发送 fatal 后显式 process.exit(1)(或抛出让 uncaughtException 兜底),确保 fatal=进程终止。
| import { netease } from "@/apis/netease"; | ||
| import type { ThemeSource } from "@/types/theme"; | ||
|
|
||
| const theme = useThemeStore(); | ||
|
|
||
| /** Netease 搜索测试 */ | ||
| interface NeteaseSearchSong { | ||
| id: number; | ||
| name: string; | ||
| artists?: { name: string }[]; | ||
| album?: { name: string }; | ||
| duration?: number; | ||
| } | ||
| interface NeteaseSearchBody { | ||
| code: number; | ||
| result?: { songs?: NeteaseSearchSong[]; songCount?: number }; | ||
| message?: string; | ||
| } | ||
| const searchKeyword = ref("告白气球"); | ||
| const searchLoading = ref(false); | ||
| const searchError = ref(""); | ||
| const searchSongs = ref<NeteaseSearchSong[]>([]); | ||
| const searchCount = ref(0); | ||
|
|
||
| const handleSearch = async (): Promise<void> => { | ||
| const kw = searchKeyword.value.trim(); | ||
| if (!kw) return; | ||
| searchLoading.value = true; | ||
| searchError.value = ""; | ||
| try { | ||
| const body = await netease.search<NeteaseSearchBody>({ keywords: kw, limit: 10 }); | ||
| if (body.code !== 200) { | ||
| searchError.value = body.message ?? `code=${body.code}`; | ||
| searchSongs.value = []; | ||
| searchCount.value = 0; | ||
| return; | ||
| } | ||
| searchSongs.value = body.result?.songs ?? []; | ||
| searchCount.value = body.result?.songCount ?? 0; | ||
| } catch (err) { | ||
| searchError.value = err instanceof Error ? err.message : String(err); | ||
| searchSongs.value = []; | ||
| searchCount.value = 0; | ||
| } finally { | ||
| searchLoading.value = false; | ||
| } | ||
| }; | ||
|
|
||
| const formatArtists = (song: NeteaseSearchSong): string => | ||
| song.artists?.map((a) => a.name).join(" / ") ?? ""; | ||
|
|
There was a problem hiding this comment.
这里新增的“Netease 搜索测试”逻辑和 UI 属于调试/演示代码,会直接进入生产 Home 页面(包含额外的接口类型、状态、模板块),与 PR 标题“feat: Plugins”不匹配且会增加维护与发布风险。建议移除该测试区块,或至少用 import.meta.env.DEV/单独路由开关隔离到开发环境。
…ble types' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
…ariable' Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 123 out of 127 changed files in this pull request and generated 5 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
electron/main/window/index.ts:56
- 这里移除了
system.rememberWindowState的判断,导致即使用户关闭了“记忆窗口状态”,启动时仍会按windowStates.*.visible自动恢复歌词窗口,属于配置失效的行为回归。建议恢复原有判断:当 rememberWindowState=false 时直接 return,或同步清空 windowStates.visible 以避免下次误恢复。
| init(): void { | ||
| ensureDirs(); | ||
| const stored = readStored(); | ||
| const enabledMap = store.get("plugins.enabled") as Record<string, boolean>; | ||
|
|
||
| // 首先加载 stored manifest | ||
| for (const [id, manifest] of Object.entries(stored.plugins)) { | ||
| const scriptPath = path.join(scriptsDir(), manifest.fileName); | ||
| let source = ""; | ||
| try { | ||
| source = fs.readFileSync(scriptPath, "utf-8"); | ||
| // 重新解压(防止脚本外部被替换为 gz_) | ||
| const { source: s } = loadScript(source, false, manifest.fileName); | ||
| source = s; | ||
| } catch (err) { | ||
| coreLog.warn(`[plugin] failed to read ${manifest.fileName}:`, err); | ||
| continue; | ||
| } | ||
| const enabled = enabledMap[id] ?? true; | ||
| this.runtimes.set(id, { |
There was a problem hiding this comment.
init() 里直接把 store.get("plugins.enabled") 断言成 Record<string, boolean>,如果老用户配置里还没有 plugins 字段,这里可能为 undefined,随后访问 enabledMap[id] 会在启动时直接抛错。建议对 get 结果做兜底:const enabledMap = (store.get("plugins.enabled") as Record<string, boolean> | undefined) ?? {},或在 store 初始化/迁移时补齐默认 plugins 配置。
| onExit: (isCrash) => { | ||
| if (!isCrash) return; | ||
| rt.restartAttempts++; | ||
| if (rt.restartAttempts > RESTART_MAX_ATTEMPTS) { | ||
| this.setStatus(rt, { | ||
| state: "error", | ||
| error: { | ||
| code: PluginErrorCodes.WORKER_CRASHED, | ||
| message: "plugin crashed too many times", | ||
| }, | ||
| }); | ||
| return; | ||
| } | ||
| const delayMs = [2_000, 8_000, 30_000][rt.restartAttempts - 1] ?? 30_000; | ||
| setTimeout(() => { | ||
| if (rt.enabled) this.start(rt).catch(() => {}); | ||
| }, delayMs); | ||
| }, |
There was a problem hiding this comment.
崩溃自动重启的 setTimeout 仅判断 rt.enabled,但 uninstall() 并不会先把 rt.enabled 置为 false;如果插件崩溃→已排队重启→用户在延迟窗口内卸载,定时器仍可能触发并把已卸载插件重新 start(rt 仍被闭包引用)。建议在回调里额外判断 runtime 是否仍在 registry 中(例如 this.runtimes.has(rt.manifest.id)),或在 uninstall() 里先 rt.enabled=false 并记录/清理该 rt 的重启定时器。
| /** | ||
| * 插件系统 IPC | ||
| * | ||
| * 渲染端通过 `window.api.plugins.*` 调用以下 channel: | ||
| * - plugin:list / install / pickAndInstall / installFromUrl / uninstall / setEnabled | ||
| * - plugin:search / resolveUrl | ||
| * 并订阅 `plugin:status` 广播以更新 UI。 |
There was a problem hiding this comment.
文件头注释声明支持 plugin:search channel,但当前实际只注册了 list/install/pickAndInstall/installFromUrl/uninstall/setEnabled/resolveUrl(没有 plugin:search)。建议删除注释中的 plugin:search,或补齐对应 IPC handler,避免文档与实现不一致导致调用方误用。
| import { netease } from "@/apis/netease"; | ||
| import type { ThemeSource } from "@/types/theme"; | ||
|
|
||
| const theme = useThemeStore(); | ||
|
|
||
| /** Netease 搜索测试 */ | ||
| interface NeteaseSearchSong { | ||
| id: number; | ||
| name: string; | ||
| artists?: { name: string }[]; | ||
| album?: { name: string }; | ||
| duration?: number; | ||
| } | ||
| interface NeteaseSearchBody { | ||
| code: number; | ||
| result?: { songs?: NeteaseSearchSong[]; songCount?: number }; | ||
| message?: string; | ||
| } | ||
| const searchKeyword = ref("告白气球"); | ||
| const searchLoading = ref(false); | ||
| const searchError = ref(""); | ||
| const searchSongs = ref<NeteaseSearchSong[]>([]); | ||
| const searchCount = ref(0); | ||
|
|
||
| const handleSearch = async (): Promise<void> => { | ||
| const kw = searchKeyword.value.trim(); | ||
| if (!kw) return; | ||
| searchLoading.value = true; | ||
| searchError.value = ""; | ||
| try { | ||
| const body = await netease.search<NeteaseSearchBody>({ keywords: kw, limit: 10 }); | ||
| if (body.code !== 200) { | ||
| searchError.value = body.message ?? `code=${body.code}`; | ||
| searchSongs.value = []; | ||
| searchCount.value = 0; | ||
| return; | ||
| } | ||
| searchSongs.value = body.result?.songs ?? []; | ||
| searchCount.value = body.result?.songCount ?? 0; | ||
| } catch (err) { | ||
| searchError.value = err instanceof Error ? err.message : String(err); | ||
| searchSongs.value = []; | ||
| searchCount.value = 0; | ||
| } finally { | ||
| searchLoading.value = false; | ||
| } |
There was a problem hiding this comment.
这里新增了“Netease 搜索测试”整块 UI 与请求逻辑,看起来是调试/验收用代码,但会随正式构建一起发布并暴露内部接口调用能力,且会让 Home 页承担额外网络请求与 UI 噪音。建议在合入前移除该测试区,或至少用开发环境开关(如 import.meta.env.DEV)/隐藏入口包起来,避免影响生产用户。
| apis: { | ||
| // 调用任意平台的任意接口 | ||
| call: (platform: string, name: string, params?: Record<string, unknown>) => | ||
| ipcRenderer.invoke("apis:call", platform, name, params ?? {}), | ||
| // 清空指定平台的登录态 | ||
| clearSession: (platform: string) => ipcRenderer.invoke("apis:clearSession", platform), | ||
| }, |
There was a problem hiding this comment.
preload 侧 apis.call/clearSession 把 platform 参数标成 string,但共享类型里 ApisApi 的平台是 ApiPlatform("netease" | "qqmusic" | "kugou")。这会弱化类型约束,也让渲染端更容易传入非法平台字符串。建议把这里的签名改为使用 ApiPlatform,并让返回值类型显式对齐 ApiCallResponse。
No description provided.