pi-web-ui 0.97.0 → 0.98.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/CHANGELOG.md +97 -1
  2. package/README.md +3 -3
  3. package/README.zh-CN.md +3 -3
  4. package/bin/pi-web-ui.mjs +64 -23
  5. package/dist/server/agent-service.js +637 -145
  6. package/dist/server/approval-rules.js +5 -10
  7. package/dist/server/atomic-file.js +40 -0
  8. package/dist/server/attachments.js +22 -0
  9. package/dist/server/claim-store.js +5 -11
  10. package/dist/server/client-state.js +220 -23
  11. package/dist/server/compact-context-tool.js +3 -1
  12. package/dist/server/compacted-history.js +7 -5
  13. package/dist/server/composer-drafts.js +4 -6
  14. package/dist/server/control-socket.js +0 -40
  15. package/dist/server/delegate-mode.js +6 -12
  16. package/dist/server/dsh/dsh-agent-service.js +32 -30
  17. package/dist/server/dsh/dsh-client.js +45 -9
  18. package/dist/server/dsh/dsh-serialize.js +12 -10
  19. package/dist/server/dsh/dsh-sessions.js +20 -6
  20. package/dist/server/dsh/runtime/goal-rpc.mjs +14 -1
  21. package/dist/server/dsh/runtime/runtime-root.mjs +1 -1
  22. package/dist/server/ensure-bash.js +3 -5
  23. package/dist/server/eval-tool.js +8 -4
  24. package/dist/server/goal-review-gate.js +7 -20
  25. package/dist/server/goal-service.js +49 -5
  26. package/dist/server/index.js +83 -15
  27. package/dist/server/launch-origin.js +0 -4
  28. package/dist/server/locales.js +3 -5
  29. package/dist/server/lsp-tool.js +110 -14
  30. package/dist/server/markers/builtins/notify.js +4 -2
  31. package/dist/server/markers/builtins/rename.js +0 -1
  32. package/dist/server/markers/builtins/todo.js +6 -26
  33. package/dist/server/markers/marker.js +0 -15
  34. package/dist/server/markers/registry.js +0 -3
  35. package/dist/server/markers/store.js +0 -3
  36. package/dist/server/mcp-hot-reload.js +12 -5
  37. package/dist/server/model-admin.js +19 -18
  38. package/dist/server/model-enrich.js +0 -3
  39. package/dist/server/patch-tool.js +21 -6
  40. package/dist/server/permission-preset.js +25 -0
  41. package/dist/server/plan-manager.js +150 -5
  42. package/dist/server/plan-mode.js +58 -20
  43. package/dist/server/plugin-api-catalog.js +2 -0
  44. package/dist/server/plugin-catalog.js +6 -6
  45. package/dist/server/plugin-dom.js +3 -4
  46. package/dist/server/plugin-facilities.js +3 -5
  47. package/dist/server/plugin-grants.js +5 -8
  48. package/dist/server/plugin-permissions.js +4 -6
  49. package/dist/server/plugin-schedule.js +4 -7
  50. package/dist/server/plugin-updater.js +151 -38
  51. package/dist/server/plugins.js +16 -2
  52. package/dist/server/prompt-composer.js +12 -3
  53. package/dist/server/protocol-version.js +1 -1
  54. package/dist/server/resolve-global-sdk.js +8 -0
  55. package/dist/server/schedule-agent-tool.js +5 -1
  56. package/dist/server/scheduler-tasks.js +46 -27
  57. package/dist/server/sdk-origin.js +27 -0
  58. package/dist/server/serialize.js +22 -9
  59. package/dist/server/slash-commands.js +16 -0
  60. package/dist/server/subagent-templates.js +4 -10
  61. package/dist/server/subagents.js +22 -11
  62. package/dist/server/terminals.js +11 -6
  63. package/dist/server/text-quote.js +59 -0
  64. package/dist/server/tool-manager.js +12 -3
  65. package/dist/server/tool-overrides.js +24 -0
  66. package/dist/server/update-check.js +69 -25
  67. package/extensions/webui.ts +15 -1
  68. package/package.json +31 -19
  69. package/plugin-sdk/client-utils.mjs +92 -0
  70. package/plugin-sdk/index.d.ts +21 -0
  71. package/plugin-sdk/index.mjs +2 -0
  72. package/plugins/catalog.json +7 -0
  73. package/web/dist/assets/{TerminalPanel-CITBqQrb.js → TerminalPanel-C88r3r3Q.js} +1 -1
  74. package/web/dist/assets/index-2jDtjHvW.css +1 -0
  75. package/web/dist/assets/index-BjdK7s1M.js +368 -0
  76. package/web/dist/index.html +2 -2
  77. package/web/dist/sw.js +2 -1
  78. package/web/public/sw.js +2 -1
  79. package/web/dist/assets/index-DwToli0J.css +0 -1
  80. package/web/dist/assets/index-oI7UKOqJ.js +0 -368
package/CHANGELOG.md CHANGED
@@ -10,6 +10,101 @@
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ 暂无未发布内容。
14
+
15
+ ## [0.98.0] — 2026-10-03
16
+
17
+ ### Added
18
+
19
+ - **聊天内容引用(#430)** —— 选中消息里的文字或代码,点浮动按钮即可把原文作为引用卡片加进输入框:支持多条引用、展开查看、逐条移除与重复去重,发送后能在历史消息里回看,编辑重问时保留。标准引擎与 DSH 引擎的消息序列化、草稿恢复、流式提问都已适配。回归:`quote-selection.test.ts`、`text-quote.test.ts`、`dsh-edit-quotes.test.ts`、`quote-selection-ui-test.mjs`。
20
+ - **侧边图标停靠栏改为布局内槽位 + 拖拽式图标编辑模式(#443)** —— `SideDock` 不再是 `position:fixed` 贴边浮层,而是 `.layout` 里的 flex 子项:面板与主区主动向它让出宽度,不再压住面板里的按钮;该侧没有图标时整条不渲染(连宽度都不占)。想要旧行为可在「设置 → 界面布局 → 侧边图标悬浮显示」(`uiLayout.sideDockFloat`)打开。新增**图标编辑模式**(顶栏「⋯」→「编辑图标」):用 pointer 事件把顶栏、底栏、左右侧边栏四个栏与「待放回」托盘里的条目直接拖进同一份 `uiLayout`(order/align/slots/hidden),与设置页布局互通;「⋯」溢出菜单因此改为顶栏有任意条目时常驻(否则进不去编辑面板)。回归:`ui-layout-edit.test.ts`、`host-icon.test.ts`、`ui-layout-ui-test`(真 Chrome)。
21
+ - **顶栏/底栏条目可移动到上下左右 + 全屏预览增强** —— 条目右键菜单新增「移至顶部 / 底部 / 左侧 / 右侧」,左右侧边停靠栏可折叠展开,偏好持久化;文件全屏预览同步增强。
22
+ - **子代理 `wait_all` 阻塞上限可配(#449)** —— 上限从硬编码 60 秒提升到 0.8×看门狗(默认约 16 分钟),可用 `PI_WEB_SUBAGENT_WAIT_CAP_SECONDS` 覆盖:长子代理任务不再被迫每 60 秒重发一次 `wait_all` 烧轮次。
23
+ - **任务计划跨重启/跨刷新持久化** —— 计划不再只活在内存:按 `sessionId` 落盘到 `<dataDir>/plans.json`,并随会话转录(`plan/update` custom entry)一起走;重建会话时按「落盘文件 → 转录 → 历史 `plan_update` 工具调用」三重兼容顺序回放,旧会话也能找回看板。回归:`plan-state.test.ts`。
24
+ - **任务看板折叠态精简为单行状态条** —— 折叠时只留「标题 + 进度徽标 + 当前步骤」,进度以底边 2px 细线呈现(零额外高度);「净室执行 / 复制 Markdown / 清空」收进展开态,「开始实施」在两种状态都可达。
25
+ - **`ask_user_question` 提问框可折叠(#480)** —— 模型提问卡支持折叠/展开,长问卷不再长期占满消息区。
26
+
27
+ ### Fixed
28
+
29
+ - **系统提示词不再吞掉其他扩展的增补(#455)** —— 自定义了提示词模板/覆盖、或使用非 standard 预设、或禁用了工具时,内置的提示词组装曾整体替换该轮系统提示词,把别的扩展前置/后置注入的内容(如 `<invoked_skill>` 块)一起丢掉。现在会先把扩展的首尾增补摘出来,渲染完再原样套回去;子代理模板 replace 同样不再丢掉前置增补。
30
+ - **计划模式只读闸门堵住成体系旁路(#436)** —— 六处旁路一次性收口:`terminal_input`/`terminal_key` 从计划模式工具集里整条剔除(PTY 行编辑缓冲在 shell 进程里,无法在服务端可靠镜像,只查缓冲会同时误伤与漏放);`eval` 补进计划 / 审查者 / 目标审查三道闸门的封禁名单(它是能直接落盘的写路径);`BYPASS_TOOLS` / `DISPATCH_TOOLS` 里那批**根本不存在**的工具名(`spawn`/`subagent_spawn`/`schedule_agent`/`set_plan_mode` 等)换成真实注册名,并抽成共享的 `SESSION_DISPATCH_TOOLS`(`delegate_task` + 真实存在的 `schedule_task`),三处不再各自漂移;`subagent` 只放行 `get_result`/`list`/`templates`,`steer`/`stop`/`wait_all`/`handoff` 与未知/缺失 action 一律按旁路拒绝(`steer` 最狠:能把写指令注进一个 `planMode=false`、工具齐全的会话);打开历史会话 / 派生分支 / 冷启动续会话三条注册路径补上 `applyToolGating` 重放,计划模式不再「闸门生效但写工具仍在模型视野里」。回归:`plan-mode` 相关单测。
31
+ - **计划模式有了可靠退出路径(#435)** —— 看板为空时整个 UI 曾无出口(开关已从输入框移除,唯一出口是看板里的「开始实施」)。现在空计划也能从目标条/看板退出计划模式,且状态随转录持久化后仍能正确恢复。
32
+ - **长驻服务内存单调上涨(#440)** —— `sessionFileCache` 原本是无上限的静态 Map,值里还带着整份转录文本(`allMessagesText`,长会话可达数 MB),删掉/改名/轮转出去的转录永远留在内存里。现在列表缓存改成 LRU 上限 512 条且只存元数据;全文搜索改为按需加载到独立小缓存(256 条、30 秒 TTL、单条 256K 字符上限),删除/改名失效与 200 候选的列表语义保持不变。
33
+ - **`client-state.json` 膨胀与界面冻结(#441)** —— 死 `clientId` 键(来自 sessionStorage,每开一个标签页就多一个)此前永不清理,文件与 `save()` 的同步序列化开销线性增长;`getRecentProjects()` 又会在截断到 30 条**之前**对每条合并路径做同步 `existsSync`,Windows 上一个断连网络盘就能把事件循环卡住数秒、冻住所有客户端的 WS/HTTP。现在加载时做保守清扫(30 天不活跃或超出 50 条上限才淘汰,`__settings__` 永不淘汰),探测改成先排序截断再用 `fsPromises.access` 并发探活,`discoverRecentProjectsFromDisk` 的三处同步调用一并异步化。
34
+ - **扁平布局下历史会话列表串项目(#438)** —— 设了 `PI_CODING_AGENT_SESSION_DIR` 时 pi 把各项目转录平铺在会话根目录(cwd 是文件内字段),但快路径从未按 cwd 过滤,导致 A 项目的历史面板里混进 B/C 项目的会话、点一下整个工作区就被切走。现在按全仓统一的 `normalizePathKey` 过滤,并且 200 条名额在**过滤之后**分配——别的项目不再把当前项目挤出自己的列表。
35
+ - **会话缓存首帧种子不校验 cwd(#439)** —— localStorage 里的会话缓存与快照守卫不看 cwd,刷新或切项目时会先显示上一个项目的会话;删除/重命名也不跨标签页失效。
36
+ - **LSP 截断口径如实上报(#447)** —— `cascade` 超 25 个引用方时静默截断且谎报完整,现在文本头与 `details` 都报真实总数并补「还有 N 个未分析」尾注;`documentSymbol` 改为按**展平后**的符号数截断(与文本大纲口径一致,截断的父节点连子树一起去掉),并加序列化体积预算,超大 `.d.ts` / protobuf 文件不再因为整条 `details` 超 64KB 被整块丢弃。
37
+ - **WS 半开连接与背压(#460)** —— 补上 ping/pong 半开连接回收;heartbeat / notice / 调度广播不再绕过 `bufferedAmount` 背压直写死 socket。
38
+ - **迟到的异步副作用复活(#461)** —— 反激活或连接失败后,迟到的异步回调曾把已该结束的东西重新拉起来:vscode-editor 留下僵尸 `connecting`、db-client 工具复活、webmail 泄漏 IMAP 连接、dsh-client 在 close 之后重启运行时。
39
+ - **`lsp`/`patch` 文案未接多语言(#463)** —— 工具返回文案恒英文、scheduler 校验错误中文直通,统一改走 `pick()`。
40
+ - **DSH 视觉附件链路无上限(#467)** —— 视觉附件链路补体量上限与 stdin 背压;`dsh-sessions` 的 zstd 魔数切帧误命中导致的静默丢事件一并修掉。
41
+ - **`ChatInput` 发送按钮白名单漏附件类型(#493)** —— `canSubmit` 漏了 `reference`/`lines`/`page` 附件:纯引用消息发送按钮恒灰但回车照发,流式胶囊也不出现。
42
+ - **后台问卷横幅「切入会话」永久消失(#494)** —— 它复用了 `onClose`,把 `questionId` 记进「已关闭」集合,未作答切走后就再也收不到常驻提醒。
43
+ - **插件目录授权并发覆盖(#495)** —— confirm 没有 busy 守卫,并发授权第二个覆盖第一个,先到的 Promise 永不 settle、悬挂整条 `openSession` 链。
44
+ - **@ 补全与全局搜索抢通道(#496)** —— 两者共用 `chat.fileSearch` 但 `reqId` 编号空间重叠,并发时结果交叉串 feed。
45
+ - **@ 补全浮层词元坐标失效(#442)** —— 光标移动或程序化改文后旧坐标仍被使用,接受补全会把正文拼接写坏。
46
+ - **纯附件提问「直接重问」无效(#443)** —— 只贴图不写字的提问上按钮可见但点击静默无效。
47
+ - **PWA 旧缓存永不淘汰(#504)** —— Service Worker 静态缓存名硬编码 `v1` 且 activate 只清异名缓存,升级后旧 hash 资产永久堆积。
48
+ - **受限网络下安装白等 7.5 分钟(#529)** —— 插件安装的探测/安装阶段补超时与快速失败。
49
+ - **语音输入两处(#445, #446)** —— auto 引擎下本地「超 8 分钟 413 / 忙 429」的前置拦截直接抛出、绕过远端兜底;Whisper 安装轮询器用全局 overlay 判活,可跨浮层存活并在完成回调里无条件 `startRecorderFlow`,劫持或双重开启录音。
50
+ - **vscode-editor「ssh config 自动加载」全链路失效(#429)** —— `loadFile` 作用域错误(ReferenceError 被静默吞掉),并加固了日志。
51
+ - **插件目录非中文语言下恒中文(#505, #432)** —— `catalog.json` 补 `nameEn` 字段并接进 schema 白名单与市场渲染,非 zh 语言不再恒显示中文名。
52
+ - **语言包「中文假翻译」(#502)** —— de/es/fr/it/ko/ru 六个包里各约 130 条的值是中文原文,而 pack 值优先于 en 回落,等于系统性地把中文当成译文发出去;逐条修正并补上文档(#478:README 工具清单、`wait_all` 描述、6 个环境变量、serverStrings 的 ja/pt 缺口)。
53
+ - **db-client 两处硬伤(#458)** —— 三家 SQL 长连接没有 `error` 监听,一个连接错误就能崩掉主进程;Mongo 的保存/删除 100% 抛 `ObjectId` ReferenceError(从未可用)。回归:`tests/unit`。
54
+ - **DSH 提问桥超时后队列永久停摆(#459)** —— `goal-rpc` 的 pending 超时后排队中的提问 Promise 永不 settle,agent 卡死在工具调用上;超时路径现在统一 settle 并派发下一个。
55
+ - **legado 规则引擎启动超时后永久瘫痪(#466)** —— worker 启动超时后 `ready` 永久 rejected,桥再也起不来;现在会复位 worker。`/store`、`/proxy` 的 `readRawBody` 补 10MB 上限。
56
+ - **`forceReset` 后事件订阅不重挂 / 幽灵重建(#484, #485)** —— 重建非活跃会话(子代理/角色对话)后事件订阅没挂回去,表现为快照冻结、看门狗失聪、`turnEndWaiters` 死等;强行关闭与 `forceReset` 交错还会把已移除会话幽灵重建,泄漏无主 runtime 与扩展宿主子进程。
57
+ - **模型列表刷新回滚并发编辑(#486)** —— `refreshProviderModels` 的读-改-写没有互斥,probe 的网络窗口里用户对同一服务商的编辑会被旧快照静默回滚;现在以盘上最新条目为合并基准。
58
+ - **abort/移除会话不结算挂起审批(#487)** —— `pendingQuestions`/`pendingApprovals` 里的悬挂条目会让失联检测被永久豁免,弹窗还会跨轮残留;现在就地结算。
59
+ - **`wait_all` 把「已移出」当「仍在运行」(#488)** —— 等待集合里含被移出的子代理时必然白等满 cap(#449 的放大);现在视为终态。
60
+ - **插件 reload 让剩余守卫整体跳过(#489)** —— reload/uninstall 原地清空 `loaded` Map,`evaluateToolPre` 的迭代器静默终止,剩余插件的 pre 守卫整体 fail-open;改为对快照迭代。
61
+ - **目标重设污染新目标(#490)** —— `dispatchExecutor` spawn 后没有代次复检就写回老执行者,`deliverAndWait` 登记 `verdictWaiter` 前也有 `await` 窗口;两处都补上复检。
62
+ - **可崩全服的 `unhandledRejection`(#491)** —— dispatch 的 `queue_remove` 是唯一缺 `void` 的 async 调用,且 `removeQueued` 内 `clearQueue` 无 try;现已消除该点。
63
+ - **新对话窗口静默吞消息(#492)** —— 新对话 displacement 与 runtime 创建窗口重叠时 `prompt` 被 abort 静默丢弃(无 notice、草稿已清),现在补「消息未送达」回执。
64
+ - **`mcp.json` 读失败被当空配置(#497)** —— EBUSY/EACCES 被当成空配置,会把在跑的 MCP 服务器全杀掉并推「热加载成功 0 个」的误导通知;现在按坏配置口径保留在跑的服务器,仅 ENOENT 视为删除。
65
+ - **markers 三处(#499)** —— `todo dep` 英文反馈方向颠倒、`new` 主题含逗号被静默截断、notify guidance 宣称协议里并不存在的 `success` 级别。
66
+ - **session-migrate 可伪造官方会话(#500)** —— fallback 导入没有来源标记,能伪造出看起来像官方的会话记录,且插件内直写会话库绕过了 outside-workspace 审批;现在补 `[imported-from]` 提示与会话头来源元数据。
67
+ - **内置预览页可被注入(#501)** —— live-preview 的 Markdown 链接/图片替换没有 scheme 白名单,`javascript:` href 可注入同源预览页。
68
+ - **直启路径端口校验(#506)** —— `--port abc` 曾静默绑随机端口并打印 `http://localhost:NaN`,现在 fail-fast。
69
+ - **只读 `bash` 白名单的 `find` 旁路(#483)** —— 白名单命中不等于只读:`find` 的 `-exec`/`-delete`/`-fprintf` 族现在按写副作用拒绝,一条常规 `find` 语法不再能绕过计划模式的三道只读闸门。
70
+ - **@ 引用正文写相对路径(#532)** —— 引用正文改用相对路径,同名文件/文件夹在输入框与聊天记录里可区分;退格整块删除与 chip 的 ✕ 反查口径同步对齐。
71
+ - **Windows 更新面板缺 `pi-core` 行(#533)** —— `readPiCoreVersionFromDisk()` 只认 npm 的 POSIX 布局,Windows 下 cmd-shim 是普通脚本、`realpathSync` 原样返回,向上找 `package.json` 永远找不到;现在先试 `<bin 同级>/node_modules/<pkg>/package.json`,探测不到时也保留该行并诚实标注「无法检测」而不是假装不存在。
72
+ - **Windows `server install` 生成的 VBS 无效(#534)** —— PowerShell 7 的 `C:\Program Files\PowerShell\7\pwsh.exe` 含空格却没加引号,Windows Script Host 报 `80070002`,登录自启与桌面快捷方式都起不来。
73
+ - **已钉住的对话跨服务重启持久化(#433)** —— 钉住状态持久化至 `client-state.json`,服务重启后自动恢复常驻运行列表,历史会话列表同步展示 📌 置顶标记并支持右键切换钉住状态。
74
+ - **计划模式净室执行(Clean Handoff)原子化(#434)** —— 修复净室执行前端四发消息由于服务端异步分发导致的竞态,提供原子协议通道 `plan_clean_handoff`,确保旧会话计划闸门关闭、新隔离会话原子就绪并承接计划步骤与目标后再发起实施。
75
+ - **压缩历史展开区只读防护与序号对齐(#437)** —— 压缩历史展开视窗右键菜单禁用派生分支、回滚与生成长图等写/非会话操作,序列化复用全局序号生成器,杜绝序号重计与碰撞。
76
+ - **PlanBoard 任务看板并发编辑防范(#444)** —— 看板步骤状态切换、内容修改、增量新增与删除改走单步骤级增量协议(`plan_step_update`、`plan_step_delete`、`plan_step_add`),解决快速连点与模型流式更新互相整组清改问题,前端即时响应 `plan_updated` 广播。
77
+ - **多轮压缩历史折叠卡片链式展示与错误本地化(#448)** —— 多次压缩的会话按分支链完整展示前序历史压缩卡片,各卡片按链式切分边界独立展开早期折叠内容;优化压缩记录未找到时的错误提示并接入多语言。
78
+ - **第三方 subagent 扩展同名工具透传支持(#481)** —— 修复内置 `subagent` 工具与第三方扩展(如 `pi-subagents`)同名冲突问题;当第三方扩展注册了同名工具且内置 `subagent` 被关闭/禁用时,自动透传扩展工具,不再强制顶替或剔除。
79
+ - **作为 Pi 扩展包安装时宿主 SDK 识别与加载器告警修复(#482)** —— 声明可选 `peerDependencies` 解决扩展加载器告警;`/webui` 子进程显式透传宿主 SDK 路径并加载 `resolve-global-sdk` 钩子,使服务准确跟随宿主 SDK 版本。
80
+ - **恢复模型管理中的第二把密钥与刷新模型功能(#456)** —— 恢复模型管理面板中克隆内置提供商添加第二把密钥与刷新已存提供商模型列表的功能与按钮。
81
+ - **防止会话交接记录与展开历史无界内存增长(#465)** —— `subagentHandoffs` 增加上限容量保护,前端被折叠压缩历史展开缓存改用 LRU 淘汰机制,防止长时间会话内存泄漏。
82
+ - **目标审查循环状态机健壮性强化(#464, #457)** —— `goal_ask` 超时定时器保证在 `finally` 中清理,避免残留定时器误杀后续调研;`stopDelegated` 异步等待执行者完全移出,消除容量误判;会话过户时主动唤醒等待者并清理目标审查阻塞态。
83
+ - **工具参数 TypeBox 约束与运行时一致性校准(#462)** —— `eval` 工具在执行 TypeScript 代码时启用原生类型剥离(`--experimental-strip-types`);`lsp`、`terminals`、`compact_context`、`patch` 工具参数范围严格钳制与上下限校准。
84
+ - **插件更新检查不再误报** —— 版本号相同就绝不报更新(即使 monorepo 仓库 HEAD SHA 变化);子目录源插件抓不到远端清单时明确报「无法检查(未能获取远端插件清单)」,不再拿仓库根 SHA 当版本号;随包(`pkgRoot`)插件版本可离线直读。CLI `pi-web-ui check-updates` 同步改为按版本号(而非 SHA)展示「已装 → 远端」差距。安装/卸载插件完成后立即重算更新状态,不必等下次轮询。
85
+ - **已结束的对话过户到另一处后可直接继续(#484)** —— 过户只换页面持有者、不重建 runtime,但新页面的会话订阅此前没挂回去,导致过户后发消息收不到回复(必须刷新页面)。现在过户尾部显式重挂事件订阅。回归:`idle-takeover-test`。
86
+
87
+ ### Changed
88
+
89
+ - **清掉 103 个没人引用的 i18n key(#472)** —— 前端字典里积了一批没人读的翻译(旧模型管理 UI 残留 54 个、旧语言选择键 10 个、提示词/工具开关重构残留等),白白拖着 8 个语言包一起翻译与审阅。现新增守卫 `tests/unit/i18n-dead-keys.test.ts`(随 `npx vitest run` 进 CI):字典里出现全仓零引用的 key 就直接红,并列出清单。模板拼接的动态 key(`thinking.*` / `promptTok_*`)已显式白名单。
90
+ - **服务端与前端死码清理(#468, #473)** —— 服务端:删掉协议死链(`cycle_model`/`cycle_thinking`、上行 `subagent_handoff`)、`sendControlCommand` 死副本、16 个零引用导出与一批死设置类型;`plugin_job_cancel` 反向补上 UI 入口(设置页作业状态行加「取消」),插件安装/更新这类长任务终于能中止。前端:删掉 11 个只被自己单测养活的 hook/util(约 1000 行)、7 个全死导出、`ui-slots` 三条「可拖可隐藏但对渲染毫无影响」的假条目(`host:lp-projects/lp-running/lp-history`),184 个仅本文件使用的导出去掉 `export`。
91
+ - **基础工具层统一、目录选择器同构合并、复制与格式化组件抽离(#470, #474, #475, #476, #477)** —— 服务端路径归一化 6 套语义分派、手抄 15+ 处的原子写、三份 TTL memoize 收敛为一套;`FooterBar` 与 `ProjectPicker` 复刻的两套目录浏览器(约 250 行同构)合并;`SettingsModal` 6 个逐行同构的提示词覆盖区块与两套 list+modal 编辑器合并;前端复制反馈 8 处内联、`formatSize` 6 份、可折叠区块头 3 份抽成共享组件;11 个插件里逐字复刻的 `esc`/`hostApi`/`apiBase` 等 client utils 下沉到 plugin-sdk 共享层。
92
+ - **构建期依赖归类与产物守卫(#468)** —— 16 个只被 `web/src` 用、随 vite 打进 `web/dist` 的包移入 devDependencies(npm 用户的安装体积与供应链面纯收益);探针从 `server/dsh/` 挪到 `dev/dsh-probes/`(`dev/` 不进 npm 包);新增 `.gitattributes` 与 CI 步骤「重新生成插件 bundle / vendor 后 `git diff --exit-code`」,改了 `src/` 忘了重新生成会被当场拦下。
93
+ - **CI 与 lint 覆盖补口(#479, #503)** —— `lint`/`lint:fix` 纳入 `bin/`(4400 行主入口不再零 lint 防护);`check:protocol` 补 git 启发式:`protocol.ts` 相对上一个 tag 有 diff 而 `PROTOCOL_VERSION` 未 bump 即报错。
94
+ - **`release-notes.mjs --write-changelog` 替换串改函数形式(#498, #515)** —— i18n 文案含 `$&`、`` $` `` 等替换模式时不再静默写坏 `CHANGELOG.md`。
95
+
96
+ <!-- auto-i18n:start -->
97
+ ### i18n
98
+
99
+ - 前端新增 key(26):`openInNewTab`、`clickToCloseImage`、`compactedHistoryNotFound`、`uiLayoutSideDockFloat`、`uiIconEdit`、`uiIconEditTitle`、`uiIconEditHint`、`uiIconEditTray`、`uiIconEditDropHere`、`uiLayoutSidebarLeft`、`uiLayoutSidebarRight`、`uiLayoutPosition`、`uiLayoutPosTop`、`uiLayoutPosBottom`、`uiLayoutPosLeft`、`uiLayoutPosRight`、`moveToTop`、`moveToBottom`、`moveToLeft`、`moveToRight`、`sideDockCollapse`、`sideDockExpand`、`quoteSelection`、`quoteText`、`quoteSource`、`removeQuote`
100
+ - 前端删除 key(103):`langZh`、`langEn`、`langIt`、`langJa`、`langKo`、`langFr`、`langDe`、`langEs`、`langRu`、`langPt`、`manageModelsTitle`、`setGlobalDefault`、`clearGlobalDefault`、`globalDefaultBadge`、`slashHelpHint`、`renameSessionConfirm`、`dismissConversationWithSubagents`、`dismissConversationWithSubagentsMixed`、`toolApprovalCommand`、`toolApprovalReason`、`approvalRuleResetConfirm`、`approvalRuleDeleteConfirm`、`planBoardSteps`、`planBoardProgress`、`planBoardNoPlan`、`editHint`、`updateTip`、`pluginAllUpToDate`、`fileSaved`、`dshVisionHiddenNote`、`questionNavTip`、`planModeTipOn`、`scmQueryFailed`、`scmTooManyFailures`、`editProvider`、`builtinProviders`、`hintKeyOnly`、`providerAuthHint`、`keyReady`、`replaceKey`、`replaceKeyTitle`、`cloneProviderTitle`、`pasteKey`、`providerIdPlaceholder`、`baseUrlExamplePh`、`saveAllBatch`、`advancedEdit`、`batchCreateProviders`、`secondKeyTitle`、`noBaseUrlShort`、`providerNameLabel`、`providerNameHint`、`apiKeyLabel`、`secondKeyPlaceholder`、`batchDesc`、`batchKeyLabel`、`modelsCountShort`、`customProviders`、`customDesc`、`noCustomProviders`、`refreshBuiltinCatalog`、`appendModelTitle`、`appendModelIdPh`、`appendModelNamePh`、`appendModelAdd`、`appendModelBusy`、`appendModelCancel`、`appendModelApiTitle`、`appendModelApiAuto`、`appendModelBaseUrlPh`、`modelsCount`、`addProvider`、`modelsTitle`、`modelIdReq`、`textImage`、`maxOutput`、`removeModel`、`addModel`、`fetchModelsErr`、`antigravityTemplateTitle`、`antigravityTemplateDesc`、`antigravityFillOpenAI`、`antigravityFillAnthropic`、`enrichHintPh`、`enrichModelsCancelled`、`goalBarActive`、`goalBarStatusPending`、`goalWizardAnswer`、`promptHistoryCleared`、`settingsPromptMode`、`promptAppendHint`、`promptReplaceHint`、`promptPlaceholder`、`promptReadonlyLockedBadge`、`reviewPromptHint`、`settingsTerminalTools`、`terminalToolsOffHint`、`settingsEditTools`、`visionBridgePromptAppendHint`、`visionBridgePromptReplaceHint`、`dshPresetCurrent`、`schedulerNameLabel`、`schedulerPromptLabel`
101
+ - 前端中文变更(3):`planBoardTitle`、`planImplementBtn`、`planCleanHandoffBtn`
102
+ - 前端英文变更(4):`planBoardTitle`、`planImplementBtn`、`planCleanHandoffBtn`、`browserControlExample2`
103
+ - 服务端新增 key(1):`pluginupdate.subpath.manifest.failed`
104
+ - 服务端文案变更(2):`markers.todo.dep.blocks.updated`、`subagents.wait.pending`
105
+ - 服务端删除 key(1):`markers.todo.list.empty`
106
+
107
+
13
108
  ## [0.97.0] — 2026-09-26
14
109
 
15
110
  ### Added
@@ -1358,7 +1453,8 @@ when?, children?}`,也收 `topbar` / `settings` 这类简写别名);宿主
1358
1453
  - 0.35.1(2026-08-27):编辑重问保留附件(#18)+ 全窗口拖放(#19)。
1359
1454
  - 0.29.0(2026-08-23):全局搜索弹窗(Ctrl+K)+ 消息列表惰性窗口化。
1360
1455
 
1361
- [Unreleased]: https://github.com/xing-shuyin/pi-web-ui/compare/v0.97.0...main
1456
+ [Unreleased]: https://github.com/xing-shuyin/pi-web-ui/compare/v0.98.0...main
1457
+ [0.98.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.98.0
1362
1458
  [0.97.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.97.0
1363
1459
  [0.96.1]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.96.1
1364
1460
  [0.96.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.96.0
package/README.md CHANGED
@@ -103,8 +103,8 @@ QQ群 1126050727
103
103
 
104
104
  ### 🤖 Subagents & templates
105
105
 
106
- - **First-party subagents** — spawn independent background conversations for parallel exploration / implementation / review (`subagent_spawn`, with optional `model` override or a template's model); collect results without polling via `subagent_wait_all` (blocks until every subagent finishes, then summarizes results/errors). Manage them like a chat right in the left panel: view live output, inject follow-ups (steer), abort, dismiss — failed runs surface a red dot in the running list and an error notice in the main chat. In-memory sessions — they never touch the history / resume list, and can be nested.
107
- - **Subagent templates** — configure reusable presets in Settings → Subagent templates: a role system prompt (append or replace), skills & extensions whitelists, an optional per-template model, and an optional thinking level. The AI picks one via the `subagent_templates` tool and `subagent_spawn(template="…")`, or spawns without one (default = follow the main conversation's current model **and thinking level**, or the global default subagent model set in the same panel). A template that sets them pins that exact combination (unsupported thinking levels are clamped by the SDK to the nearest one the model supports). Disabled templates stay in the panel for re-enabling but become invisible to the AI tools (can't be listed or picked). Templates are shared globally across browser clients (`<dataDir>/subagent-templates.json`). Thirteen built-in templates seed the list on first run — marked 「Built-in」, editable and deletable like any other: review / implement / research / scout / audit / delegate (adapted from the pi-subagents community projects) and oracle / librarian / explore / metis / momus / multimodal-looker / sisyphus-junior (ported from oh-my-pi's built-in agents).
106
+ - **First-party subagents** — spawn independent background conversations for parallel exploration / implementation / review (unified `subagent` tool with `action="spawn"`, with optional `model` override or a template's model); collect results without polling via `subagent` with `action="wait_all"` (blocks up to 60s, capped by watchdog × 0.8 with a 5s lower bound, returning unfinished subagent lists to guide subsequent waits). Manage them like a chat right in the left panel: view live output, inject follow-ups (steer), abort, dismiss — failed runs surface a red dot in the running list and an error notice in the main chat. In-memory sessions — they never touch the history / resume list, and can be nested.
107
+ - **Subagent templates** — configure reusable presets in Settings → Subagent templates: a role system prompt (append or replace), skills & extensions whitelists, an optional per-template model, and an optional thinking level. The AI picks one via the `subagent` tool (`action="templates"`) and `subagent(action="spawn", template="…")`, or spawns without one (default = follow the main conversation's current model **and thinking level**, or the global default subagent model set in the same panel). A template that sets them pins that exact combination (unsupported thinking levels are clamped by the SDK to the nearest one the model supports). Disabled templates stay in the panel for re-enabling but become invisible to the AI tools (can't be listed or picked). Templates are shared globally across browser clients (`<dataDir>/subagent-templates.json`). Thirteen built-in templates seed the list on first run — marked 「Built-in」, editable and deletable like any other: review / implement / research / scout / audit / delegate (adapted from the pi-subagents community projects) and oracle / librarian / explore / metis / momus / multimodal-looker / sisyphus-junior (ported from oh-my-pi's built-in agents).
108
108
 
109
109
  ### 🖼️ Files, images & attachments
110
110
 
@@ -151,7 +151,7 @@ QQ群 1126050727
151
151
 
152
152
  ### 🧩 Agent tools & inline markers
153
153
 
154
- - **Tool switches** — Settings → Tools lists every optional agent tool as its own switch: the 7 terminal tools (default **off**), the 7 `subagent_*` tools (default on), and the other 11 — `edit_soft` and `browser_page` (default off), `delegate_task`, `ask_user_question`, `todo_list`, `conversation_read`, `present_files`, `skill`, `schedule_task`, `schedule_list` and `schedule_cancel` (default on). Toggling is live (no reload) and the tools stay registered so they can come back; `bash` and the SDK's own `edit`/`read` are deliberately outside the catalog and cannot be disabled.
154
+ - **Tool switches** — Settings → Tools lists every optional agent tool as its own switch: the unified `subagent` tool (default on), and 17 other tools — `edit_soft`, `browser_page`, `eval`, and `lsp` (default off), plus `delegate_task`, `ask_user_question`, `todo_list`, `conversation_read`, `present_files`, `skill`, `schedule_task`, `schedule_list`, `schedule_cancel`, `claim_files`, `plan_update`, `patch`, and `compact_context` (default on). Toggling is live (no reload) and the tools stay registered so they can come back; core built-in tools (`bash`, `read`, `write`, `edit`) are managed in their own Core Tools section.
155
155
  - **Inline markers** — instead of a tool round-trip the AI writes state changes straight into its reply: `[[todo:new:<subject>]]` / `[[todo:set:<id>,in_progress]]` / `[[todo:remove:<id>]]` / `[[todo:dep:<id>,blocks=<id>]]` for the task list, `[[notify:<level>:<message>]]` for a non-interruptive notice, and `[[conv:rename:<title>]]` to retitle the chat. Markers are applied as soon as a reply bubble is final, a bad marker comes back as a browser notice, and the task list also renders as a live widget under the file tree (`N/M done` with ✓ / ◐ / ○) that follows the active conversation and survives a reload — it is stored in that conversation's own session branch. Settings → Tools has a master switch plus one switch per marker (these are global, shared by all browsers).
156
156
  - **`edit_soft`** — a looser `edit` (default off) for when indentation or whitespace makes the built-in tool fail: exact substring first, then trimmed line-core matching, `newText` written verbatim with the file's line endings/BOM preserved, and a diff + unified patch in the result. It also tolerates sloppy input (a JSON string, a bare object, legacy top-level `oldText`/`newText`).
157
157
  - **`delegate_task`** — hands a specialist template a six-section brief (TASK / EXPECTED OUTCOME / REQUIRED TOOLS / MUST DO / MUST NOT DO / CONTEXT) validated on the server: a missing template, a task under 20 characters or any empty section is rejected, and the error tells the model which templates it may use. Cards render the brief as labelled sections, and a finished delegation gets a button that jumps to the subagent's conversation.
package/README.zh-CN.md CHANGED
@@ -64,8 +64,8 @@ QQ群 1126050727
64
64
 
65
65
  **子代理与模板**
66
66
 
67
- - **第一方子代理** —— 后台派发独立对话并行做调研 / 实现 / 审查(`subagent_spawn`);与普通对话一样在左栏管理:实时查看输出、补充(steer)、中止、移出。内存会话——不进历史 / resume 列表,可嵌套派发。
68
- - **子代理模板** —— 设置面板「子代理模板」里配置可复用预设:角色系统提示词(追加或整体替换)+ 技能/扩展白名单 + 可选模型 + 可选思考强度。AI 用 `subagent_templates` 工具查询清单、`subagent_spawn(template="…")` 选用,也可以不传模板按主会话默认配置运行。模型与思考强度都留空 = 跟随主对话当前设置(与「跟随主对话」的模型回落同语义);模板指定了就固定用那个组合(模型不支持的思考档位会自动收敛)。停用的模板保留在面板可随时重新启用,但对 AI 工具不可见(查不到、不能选)。模板全局共享(`<dataDir>/subagent-templates.json`,所有浏览器客户端一致)。首次运行自带 13 个内置模板 —— 前 6 个(review / implement / research / scout / audit / delegate)改编自 pi-subagents 社区项目,另 7 个(oracle / librarian / explore / metis / momus / multimodal-looker / sisyphus-junior)移植自 oh-my-pi 内置 agent —— 面板标「默认」徽标,可像普通模板一样修改或删除。
67
+ - **第一方子代理** —— 后台派发独立对话并行做调研 / 实现 / 审查(统一的 `subagent` 工具,通过 `action="spawn"` 派发);支持 `action="wait_all"` 批量等待(阻塞上限为 60 秒且不低于 5 秒,受看门狗 × 0.8 约束,超时返回未完成名单引导后续等待);与普通对话一样在左栏管理:实时查看输出、补充(steer)、中止、移出。内存会话——不进历史 / resume 列表,可嵌套派发。
68
+ - **子代理模板** —— 设置面板「子代理模板」里配置可复用预设:角色系统提示词(追加或整体替换)+ 技能/扩展白名单 + 可选模型 + 可选思考强度。AI 用 `subagent(action="templates")` 查询清单、`subagent(action="spawn", template="…")` 选用,也可以不传模板按主会话默认配置运行。模型与思考强度都留空 = 跟随主对话当前设置(与「跟随主对话」的模型回落同语义);模板指定了就固定用那个组合(模型不支持的思考档位会自动收敛)。停用的模板保留在面板可随时重新启用,但对 AI 工具不可见(查不到、不能选)。模板全局共享(`<dataDir>/subagent-templates.json`,所有浏览器客户端一致)。首次运行自带 13 个内置模板 —— 前 6 个(review / implement / research / scout / audit / delegate)改编自 pi-subagents 社区项目,另 7 个(oracle / librarian / explore / metis / momus / multimodal-looker / sisyphus-junior)移植自 oh-my-pi 内置 agent —— 面板标「默认」徽标,可像普通模板一样修改或删除。
69
69
 
70
70
  **文件、图片与附件**
71
71
 
@@ -112,7 +112,7 @@ QQ群 1126050727
112
112
 
113
113
  **代理工具与内联标记**
114
114
 
115
- - **工具开关** —— 设置 →「工具」把所有可选工具逐个列出:7 个终端工具(默认**关**)、7 个 `subagent_*` 工具(默认开),其余 11 个 —— `edit_soft` 与 `browser_page` 默认关,`delegate_task`/`ask_user_question`/`todo_list`/`conversation_read`/`present_files`/`skill`/`schedule_task`/`schedule_list`/`schedule_cancel` 默认开。开关即时生效、不重启,工具只是被禁用仍保留注册以便随时开回;`bash` 与 SDK 自带的 `edit`/`read` 有意不可关。
115
+ - **工具开关** —— 设置 →「工具」把所有可选工具逐个列出:统一的 `subagent` 工具(默认开)、以及另外 17 个工具 —— `edit_soft`、`browser_page`、`eval` 与 `lsp` 默认关,`delegate_task` / `ask_user_question` / `todo_list` / `conversation_read` / `present_files` / `skill` / `schedule_task` / `schedule_list` / `schedule_cancel` / `claim_files` / `plan_update` / `patch` / `compact_context` 默认开。开关即时生效、不重启,工具只是被禁用仍保留注册以便随时开回;核心工具 `bash` 与 SDK 自带的 `read` / `write` / `edit` 走设置页「核心工具」独立区域管理。
116
116
  - **内联标记** —— 状态改变不需要工具往返,AI 直接把标记写进回复:任务列表用 `[[todo:new:<主题>]]` / `[[todo:set:<id>,in_progress]]` / `[[todo:remove:<id>]]` / `[[todo:dep:<id>,blocks=<id>]]`,不打断的提醒用 `[[notify:<级别>:<内容>]]`,改对话标题用 `[[conv:rename:<标题>]]`。气泡定稿即执行,标记写错会以浏览器提示回显;任务列表同时以常驻 widget 显示在右栏文件树下方(`N/M done` + ✓/◐/○),跟随当前对话,且因为存在该对话自己的会话分支里,刷新后仍在。设置 →「工具」另有总开关与逐标记开关(这两项全局共享)。
117
117
  - **`edit_soft`** —— 更宽松的 `edit`(默认关):缩进/空白导致内置工具失败时用它,先精确子串、再按去空白逐行核心匹配,`newText` 原样写入并保留文件换行符/BOM,结果带 diff 与 unified patch。
118
118
  - **`delegate_task`** —— 强制六段派单(TASK / EXPECTED OUTCOME / REQUIRED TOOLS / MUST DO / MUST NOT DO / CONTEXT)并在服务端校验:模板不可用、任务少于 20 字或任一段为空都会被打回,并把可用模板清单回给模型。卡片按六段结构化展示,跑完后可一键跳到对应子代理对话。
package/bin/pi-web-ui.mjs CHANGED
@@ -682,7 +682,9 @@ function buildWinShortcutPs1(env, cwd, taskName, url, logPath, pidPath) {
682
682
  * this launcher never creates one at all.
683
683
  */
684
684
  function buildWinHiddenVbs(ps1Path) {
685
- const cmd = `${winPowershell()} -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "${ps1Path}"`;
685
+ // 可执行文件必须带引号:winPowershell() 常返回 `C:\Program Files\PowerShell\7\pwsh.exe`,
686
+ // 含空格且未加引号时 WScript.Shell.Run 找不到文件,直接报 80070002(系统找不到指定的文件)。
687
+ const cmd = `"${winPowershell()}" -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File "${ps1Path}"`;
686
688
  // VBScript 字符串没有 \" 转义(也没有 \uXXXX),内嵌引号必须写成 "";
687
689
  // 不能用 JSON.stringify —— 它输出 \" 会在 VBScript 里提前结束字符串(语句未结束 800A0401),
688
690
  // 且会把非 ASCII 路径转成 \uXXXX 字面量(wscript 不识别,中文用户名直接变成乱码路径)。
@@ -1943,19 +1945,40 @@ function parsePluginSource(rawSpec) {
1943
1945
  return { owner, repo, ref, subpath, cloneUrl: `https://github.com/${owner}/${repo}.git` };
1944
1946
  }
1945
1947
 
1948
+ /** 清理临时目录下残留超过 1 小时的 pi-web-ui-plugin-* 安装目录。 */
1949
+ function cleanStalePluginTmpDirs() {
1950
+ try {
1951
+ const base = tmpdir();
1952
+ const entries = readdirSync(base);
1953
+ const now = Date.now();
1954
+ for (const name of entries) {
1955
+ if (name.startsWith("pi-web-ui-plugin-")) {
1956
+ try {
1957
+ const p = join(base, name);
1958
+ const st = statSync(p);
1959
+ if (now - st.mtimeMs > 3600_000) {
1960
+ rmSync(p, { recursive: true, force: true });
1961
+ }
1962
+ } catch {}
1963
+ }
1964
+ }
1965
+ } catch {}
1966
+ }
1967
+
1946
1968
  /** 把仓库拉到 tmpDir 并返回检出根目录。优先 git clone --depth 1,失败回退 codeload tarball + 系统 tar。 */
1947
1969
  async function acquireRepo(src, tmpDir) {
1948
1970
  const dst = join(tmpDir, "src");
1949
1971
  const hasGit = spawnSync("git", ["--version"], { stdio: "ignore", timeout: 10_000 }).status === 0;
1950
1972
  if (hasGit) {
1951
- const args = ["clone", "--depth", "1", "--single-branch"];
1973
+ const gitTimeout = Number(process.env.PI_WEB_GIT_TIMEOUT_MS) || 45_000;
1974
+ const args = ["-c", "connect.timeout=15", "clone", "--depth", "1", "--single-branch"];
1952
1975
  if (src.ref) args.push("--branch", src.ref);
1953
1976
  args.push(src.cloneUrl, dst);
1954
1977
  console.log(`· git clone --depth 1 ${src.cloneUrl}${src.ref ? ` (${src.ref})` : ""}`);
1955
1978
  const res = spawnSync("git", args, {
1956
1979
  stdio: "inherit",
1957
1980
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never" },
1958
- timeout: 300_000,
1981
+ timeout: gitTimeout,
1959
1982
  });
1960
1983
  if (res.status === 0 && existsSync(dst)) return dst;
1961
1984
  console.log(
@@ -1969,26 +1992,28 @@ async function acquireRepo(src, tmpDir) {
1969
1992
  // 注意:这里不用 fail()/process.exit —— async 上下文里还有未关闭的 socket 时
1970
1993
  // 直接退出会触发 Windows libuv "UV_HANDLE_CLOSING" 断言崩溃;改为 throw,
1971
1994
  // 由 pluginInstallCmd 捕获后设 exitCode 让事件循环自然排空。
1972
- let res;
1995
+ let buf;
1973
1996
  try {
1974
- res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) });
1997
+ const res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120_000) });
1998
+ if (!res.ok) {
1999
+ throw new Error(
2000
+ (ZH ? `HTTP 状态码 ${res.status}` : `HTTP status ${res.status}`) +
2001
+ (res.status === 404
2002
+ ? ZH
2003
+ ? "\n 仓库/分支不存在,或为私有仓库(私有仓库请先在本机配置好 git 凭据再重试,会优先走 git clone)。"
2004
+ : "\n Repository/branch does not exist, or is private (for private repos, configure git credentials locally first and retry; git clone will be tried first)."
2005
+ : ""),
2006
+ );
2007
+ }
2008
+ buf = Buffer.from(await res.arrayBuffer());
1975
2009
  } catch (err) {
1976
2010
  throw new Error(
1977
2011
  ZH
1978
- ? `下载失败:${err?.message ?? err}\n 请检查网络/代理后重试。`
1979
- : `Download failed: ${err?.message ?? err}\n Check your network/proxy and retry.`,
2012
+ ? `下载失败 (${url}):${err?.message ?? err}\n 请检查网络/代理后重试(可配置 HTTP_PROXY / HTTPS_PROXY 环境变量,或为 git/npm 配置代理)。`
2013
+ : `Download failed (${url}): ${err?.message ?? err}\n Check your network/proxy and retry (consider configuring HTTP_PROXY/HTTPS_PROXY or git/npm proxy).`,
1980
2014
  );
1981
2015
  }
1982
- if (!res.ok)
1983
- throw new Error(
1984
- (ZH ? `下载失败 HTTP ${res.status}:${url}` : `Download failed HTTP ${res.status}: ${url}`) +
1985
- (res.status === 404
1986
- ? ZH
1987
- ? "\n 仓库/分支不存在,或为私有仓库(私有仓库请先在本机配置好 git 凭据再重试,会优先走 git clone)。"
1988
- : "\n Repository/branch does not exist, or is private (for private repos, configure git credentials locally first and retry; git clone will be tried first)."
1989
- : ""),
1990
- );
1991
- writeFileSync(join(tmpDir, "src.tar.gz"), Buffer.from(await res.arrayBuffer()));
2016
+ writeFileSync(join(tmpDir, "src.tar.gz"), buf);
1992
2017
  const extractTo = join(tmpDir, "tar");
1993
2018
  mkdirSync(extractTo, { recursive: true });
1994
2019
  // 相对路径解压:win32 的 GNU tar 会把 "C:\..." 里的 C: 当远程主机(Cannot connect to C:)
@@ -2209,6 +2234,7 @@ async function confirmSourceBuild(plan) {
2209
2234
  * 失败抛 Error(目录模式逐条 try/catch 继续下一条,单源模式由调用方转 fail)。
2210
2235
  */
2211
2236
  async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir, isCatalog = false }) {
2237
+ cleanStalePluginTmpDirs();
2212
2238
  const pluginsDir = join(dataDir, "plugins");
2213
2239
  const localCandidate = resolve(rawSpec.replace(/^file:\/\//, ""));
2214
2240
  const isLocal = existsSync(localCandidate);
@@ -2360,7 +2386,11 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir,
2360
2386
  }
2361
2387
  return { id, target, manifest, buildMode: decision.mode };
2362
2388
  } finally {
2363
- rmSync(tmp, { recursive: true, force: true });
2389
+ try {
2390
+ rmSync(tmp, { recursive: true, force: true });
2391
+ } catch {
2392
+ /* Windows 等平台杀毒/进程占用可能抛 EBUSY,不应翻转已成功的安装 */
2393
+ }
2364
2394
  }
2365
2395
  }
2366
2396
 
@@ -3287,10 +3317,16 @@ async function checkUpdatesCmd(dataDir) {
3287
3317
  for (const r of rows) {
3288
3318
  const label = r.name && r.name !== r.id ? `${r.id}(${r.name})` : r.id;
3289
3319
  if (r.updatable) {
3320
+ const cur = r.version ? (r.version.startsWith("v") ? r.version : `v${r.version}`) : (r.localSha ?? "未知");
3321
+ const lat = r.latestVersion
3322
+ ? r.latestVersion.startsWith("v")
3323
+ ? r.latestVersion
3324
+ : `v${r.latestVersion}`
3325
+ : (r.remoteSha ?? "");
3290
3326
  console.log(
3291
3327
  ZH
3292
- ? ` 🔄 ${label}${r.version ? ` v${r.version}` : ""} 可更新(已装 ${r.localSha ?? "未知"} → 远端 ${r.remoteSha})`
3293
- : ` 🔄 ${label}${r.version ? ` v${r.version}` : ""} Updatable (installed ${r.localSha ?? "unknown"} → remote ${r.remoteSha})`,
3328
+ ? ` 🔄 ${label}${r.version ? ` v${r.version}` : ""} 可更新(已装 ${cur} → 远端 ${lat})`
3329
+ : ` 🔄 ${label}${r.version ? ` v${r.version}` : ""} Updatable (installed ${cur} → remote ${lat})`,
3294
3330
  );
3295
3331
  console.log(
3296
3332
  ZH
@@ -3298,11 +3334,16 @@ async function checkUpdatesCmd(dataDir) {
3298
3334
  : ` Update: pi-web-ui install ${r.source} --name ${r.id} --force`,
3299
3335
  );
3300
3336
  any = true;
3301
- } else if (r.remoteSha) {
3337
+ } else if (r.latestVersion || r.remoteSha) {
3338
+ const detail = r.latestVersion
3339
+ ? r.latestVersion.startsWith("v")
3340
+ ? r.latestVersion
3341
+ : `v${r.latestVersion}`
3342
+ : (r.remoteSha ?? "");
3302
3343
  console.log(
3303
3344
  ZH
3304
- ? ` ✓ ${label}${r.version ? ` v${r.version}` : ""} 已是最新(${r.remoteSha})`
3305
- : ` ✓ ${label}${r.version ? ` v${r.version}` : ""} Up to date (${r.remoteSha})`,
3345
+ ? ` ✓ ${label}${r.version ? ` v${r.version}` : ""} 已是最新(${detail})`
3346
+ : ` ✓ ${label}${r.version ? ` v${r.version}` : ""} Up to date (${detail})`,
3306
3347
  );
3307
3348
  } else {
3308
3349
  console.log(