pi-web-ui 0.90.1 → 0.92.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 (54) hide show
  1. package/CHANGELOG.md +81 -1
  2. package/bin/pi-web-ui.mjs +21 -2
  3. package/dist/server/agent-service.js +1206 -106
  4. package/dist/server/claim-files-tool.js +116 -0
  5. package/dist/server/claim-store.js +301 -0
  6. package/dist/server/client-state.js +119 -5
  7. package/dist/server/compaction-markers.js +287 -0
  8. package/dist/server/conversation-read-tool.js +369 -27
  9. package/dist/server/conversation-touches.js +357 -0
  10. package/dist/server/delegate-task.js +12 -1
  11. package/dist/server/dsh/dsh-agent-service.js +28 -1
  12. package/dist/server/file-transfer-routes.js +4 -1
  13. package/dist/server/icon-svg.js +35 -0
  14. package/dist/server/index.js +152 -34
  15. package/dist/server/model-admin.js +124 -2
  16. package/dist/server/model-enrich.js +478 -0
  17. package/dist/server/plugin-catalog.js +5 -0
  18. package/dist/server/plugin-facilities.js +42 -11
  19. package/dist/server/plugin-schedule.js +27 -2
  20. package/dist/server/plugins.js +112 -18
  21. package/dist/server/present-files-tool.js +303 -0
  22. package/dist/server/prompt-composer.js +1 -1
  23. package/dist/server/protocol-version.js +1 -1
  24. package/dist/server/read-tool.js +91 -0
  25. package/dist/server/schedule-agent-tool.js +23 -5
  26. package/dist/server/scheduler-tasks.js +60 -1
  27. package/dist/server/scm-commitmsg.js +140 -0
  28. package/dist/server/scm.js +42 -0
  29. package/dist/server/serialize.js +21 -1
  30. package/dist/server/settings-service.js +47 -0
  31. package/dist/server/skill-tool.js +120 -0
  32. package/dist/server/soft-cap.js +68 -0
  33. package/dist/server/subagent-templates.js +6 -0
  34. package/dist/server/subagents.js +157 -16
  35. package/dist/server/text-sniff.js +15 -0
  36. package/dist/server/themes.js +17 -2
  37. package/dist/server/tool-manager.js +13 -1
  38. package/package.json +2 -1
  39. package/plugin-sdk/README.md +1 -0
  40. package/plugins/catalog.json +20 -0
  41. package/themes/catppuccin-latte.css +136 -0
  42. package/themes/catppuccin.css +140 -0
  43. package/themes/nord.css +130 -0
  44. package/themes/one-dark.css +131 -0
  45. package/themes/paper.css +12 -12
  46. package/themes/solarized-light.css +131 -0
  47. package/themes/tokyo-night.css +140 -0
  48. package/web/dist/assets/{TerminalPanel-CkxM4Byl.js → TerminalPanel-RACpk6AX.js} +1 -1
  49. package/web/dist/assets/es-DIDZppIF.js +2 -0
  50. package/web/dist/assets/index-BgrnP9Jz.js +364 -0
  51. package/web/dist/assets/index-Cy8dSTZt.css +1 -0
  52. package/web/dist/index.html +2 -2
  53. package/web/dist/assets/index-DJIV4A-t.js +0 -355
  54. package/web/dist/assets/index-DLdcRBs_.css +0 -1
package/CHANGELOG.md CHANGED
@@ -10,6 +10,77 @@
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+ ## [0.92.0] — 2026-09-20
14
+
15
+ ### Added
16
+
17
+ - **插件设置的 `select` 候选值可由宿主现算(`optionsFrom`)** —— manifest `settings` 里写 `"type": "select", "optionsFrom": "models" | "thinkingLevels"` 即可让宿主在浏览器侧现算候选值:模型列已配置鉴权的模型(值 `provider/id`,标签同设置面板的模型选择器)、思考强度列 SDK 档位(`off`…`max`,文案走 `thinking.<值>`);两者自动带一个空值选项 = 跟随全局默认,插件不用自己维护会过期的静态表。服务端不校验这类值(清单在浏览器侧、随配置变化),只留 200 字符长度护栏,非法值由用的时候(如 `host.chat` 切模型)报错;当前存值不在清单里(模型被删/手改过 storage.json)时也保留,不被下拉静默吃掉。
18
+ - **自定义模型提供商「补参数」支持随时中断与实时进度反馈** —— 模型配置面板中点击「补参数」后,新增实时进度显示(下载 OpenRouter / models.dev 目录、抓取依据网页、逐个匹配参数 N/M 与百分比),并提供「✕ 取消」按钮;点击取消后服务端立即截断网络连接与批处理,并自动保留中断前已匹配的模型行,避免弱网时无响应或无法停止。
19
+ - **微信通道(wechat-ilink)设置里的「模型」「思考强度」改下拉选择** —— 旧版是手打 `provider/id` 文本框,打错要到微信里跑完一轮才发现(切换失败)。现在从清单里选,空 = 跟随全局默认。
20
+ - **read 工具可直接读目录** —— 模型把目录路径交给 read 时不再报 `EISDIR`,改为列出目录条目(一行一项、目录带 `/` 后缀,`limit` 此时是条目上限),看目录不必再走 bash 的 `ls`。实现是覆盖内置 read(同名 customTool),文件/图片/不存在的路径行为与原来完全一致;设置 → 工具页新增「read 读目录」开关(默认开),关掉即恢复内置行为。仅 pi 引擎生效(DSH 引擎的工具来自预设,无此覆盖面)。
21
+ - **浏览器扩展(page-picker):点一次图标就能看见「让 AI 操作本页」** —— AI 授权入口原来只挂在拾取**确认条**里(必须先在页面上点一个元素它才出现,「只想授权」的人白点一下)。现在拾取态底部常驻一条细条:直接显示本页授权状态(未授权 / 已授权 / 「AI 操作页面」总开关关着 / 查不到后台),并给出「让 AI 操作本页…」「与另一页配对…」「退出」;在扩展设置页点完「授权该页面」回到那个页面,细条自己变成「已授权 · 模型可操作本页」(扩展监听授权表变化,不用重新点图标、不用刷新)。细条只有按钮可点,其余区域点击照旧穿透到页面元素 —— 不影响拾取手感。
22
+ - **AI 可以主动把文件「拿给你看」(`present_files`)** —— 新工具让模型把产物直接推到对话里成卡片:图片、视频、音频**在消息内直接显示/播放**(不折叠、不用点),文本/代码/Markdown/HTML 给开头摘录 + 「预览」按钮开文件预览弹窗(行号、选区、加进对话都在那边),不能内联的(PDF/二进制)只给下载与本地打开;每一行都带「预览 / 本地打开(用默认应用打开文件)/ 在文件夹中显示 / 下载 / 复制路径」,其中「本地打开」「在文件夹中显示」与右栏文件树右键菜单**同一套协议**(服务器跑在别的机器上时由服务端明确提示不支持,不是默默没反应);路径不存在时卡片直接标红说明,不会给你一个点不动的东西。模型还能把某个文件标成「先看这个」,配合设置 → 消息显示新增的「自动打开 AI 展示的预览」开关(默认关)就能自动把预览窗弹出来(只对刚发生的卡片生效,翻旧会话不会突然弹窗)。工具目录 24→25(默认开,**设置 → 工具页有独立开关**,可随时关掉)。仅 pi 引擎(DSH 引擎的工具来自预设,无此覆盖面)。
23
+
24
+ - **插件可以「没有独立视图、但每次进页都常驻加载」(manifest `preload`)** —— 有的插件根本不需要自己的 tab(界面全在浮窗 / 输入框按钮 / 消息卡片里),但它的客户端代码必须一直在跑(到点提醒的长轮询、快捷键、常驻浮窗的状态恢复)。以前这种插件只有两条路:要么写 `view: false`(宿主**不**预加载,只有点它的按钮才 import → 刷新页面后提醒/浮窗全停摆),要么被迫留一个没人用的视图 tab。现在 manifest 写 `"view": false, "preload": true` 即可:宿主每次进页都预加载它的 bundle(顶层代码跑完即算加载成功,**可以没有 `default.mount`**),但仍不给它视图 tab。笔记插件是第一个用户(见下面 Changed)。
25
+ - **SCM 提交树过滤 + 提交信息历史** —— 提交树列表头新增过滤框:按主题 / 作者 / hash / 分支·标签实时过滤(空格分隔多关键字 AND 语义,计数徽标显示「命中/总数」,切工作区自动重置);提交输入框支持 ↑/↓ 回溯最近用过的提交信息(shell 风格:↑ 记住当前草稿并翻回历史,↓ 逐级退回,localStorage 存最近 20 条,去重置顶、隐私模式静默降级)。纯前端零协议改动;过滤与回溯的纯函数带单测(`scm-history-filter` / `scm-commit-history`)。
26
+ - **顶栏插件改成单个 🧩 入口(插件视图默认不再占顶栏)** —— 以前装了视图的插件每个都在顶栏占一个 tab,插件一多就被挤进「⋯」,装得越多越难找。现在顶栏只留一个 🧩:点开是全部已装插件的面板,每行带图钉(钉住的插件视图 tab 才回到顶栏直流),行首把手可拖拽排序、聚焦后也能用 ↑/↓ 调序(只改插件视图彼此的相对次序,其它条目的排序偏好原样保留),点行本身直接切到该插件视图,底部「管理插件」直达设置 → 插件 → 市场。纯渲染器插件(`view: false`)与报错插件照样列出(后者行内显示原因),但不给图钉、行不可点。**升级注意**:插件视图默认「未钉住」= 不再出现在顶栏,之前装过插件的用户需要在 🧩 面板里挨个钉回来;钉住状态与设置「界面布局」页的勾选框是同一套存储与语义,两边改都生效。顺带两处修正:插件视图不再被历史排序偏好挤到对话/终端之前(独立区段排在 Git 之后),以及「设置」入口被固定保护 —— 不能被插件 arrange 或布局偏好隐藏(布局页该行勾选框置灰),空间不够时它也优先留下、不让位给「⋯」(`fitTopbar` 常驻项先占预算)。
27
+
28
+ ### Fixed
29
+
30
+ - **设置「工具」页漏挂的两个工具开关(`skill` / `present_files`)补齐** —— 该页的工具行是手写的,终端组/子代理组按名单循环渲染,成“其他”组逐个手写,因此工具只进 `AGENT_TOOL_CATALOG` 目录、忘了写行的时候,会出现「目录里有、设置里找不到」:工具默认开着且关不掉。现在两个开关都在「其他」组里,并新增静态守卫单测(`tests/unit/settings-tool-rows.test.ts`):目录里每个工具必须能被设置页渲染(循环组或显式行),今后再漏会直接 CI 报红。
31
+
32
+ ### Changed
33
+
34
+ - **笔记插件:去掉独立视图 tab,浮窗成为唯一界面(插件 0.2.0)** —— 以前它有两个入口:顶栏按钮开浮窗 + 🧩 插件面板里的「笔记」行开一个完整视图 tab(浮窗头部还多一个 `⤢` 按钮指向它),同一件事三个入口。现在只留一个:顶栏 **📌**(图标从「日历打勾」换成图钉,与浮窗头部同一枚)开合浮窗,浮窗里工作。同时修了浮窗里四处布局毛病:① 正文区不再按内容撑高(实测 440px 的窗口里列表高 600px,日历会画到底栏和设置层上去),现在自己收缩成可滚动的一块;② 设置从「挤在窗口底部的第三块」(一开就把正文压成一条缝、还套了两层容器)改成**盖在正文之上的覆盖层**(自带标题栏 + ✕,内部滚动,正文不被压缩);③ 设置里「语言」标签不再被下拉框挤成竖排两个字;④ 底栏在窄窗里从三行压成一行(提醒那句长了给省略号),日历格子在窄窗里也矮了一档。**升级注意**:已装插件需要更新一次(`pi-web-ui install --name notes --force` 或重装),且改完 manifest 后要触发一次重扫(设置 → 插件 → 重新扫描,或重启服务)才会生效。
35
+ - **插件声明式设置表单改成单列行式布局** —— 旧版是 `auto-fit` 网格 + `space-between`:窄列时长标签被逐字挤成**竖排**(如「允许的用户默认工作空间」一个字一行),勾选框被甩到行最右端、与标签断开,输入框/下拉/数字框宽度也各自为政。现在统一为「标签固定左列(不压缩、超长省略号 + 悬浮看全名)+ 控件右列(文本/下拉 420px、数字 120px、勾选框贴标签)」,行间细分隔线;`hint` 从只挂 `title` tooltip 改为**常显在标签下的小字**(最多两行);窄窗口(≤720px)标签与控件上下堆叠。纯渲染层改动,manifest `settings` schema、`plugin_settings` 协议与既有 class 名(`.plugin-settings-field/-save/-reset/-form`)均未变。
36
+
37
+ <!-- auto-i18n:start -->
38
+ ### i18n
39
+
40
+ - 前端新增 key(45):`setGlobalDefault`、`clearGlobalDefault`、`globalDefaultBadge`、`themeGroupClassics`、`themeGroupBuiltin`、`scmHistoryFilterPlaceholder`、`scmHistoryFilterTip`、`scmHistoryFilterEmpty`、`scmCommitHistoryTip`、`antigravityTemplateTitle`、`antigravityTemplateDesc`、`antigravityFillOpenAI`、`antigravityFillAnthropic`、`enrichModels`、`enrichModelsHint`、`enrichHintPh`、`enrichingModels`、`enrichModelsCancel`、`enrichModelsAbort`、`enrichCancelling`、`enrichModelsCancelled`、`enrichModelsOk`、`enrichModelsErr`、`enrichModelsNeedIds`、`readDirEnabled`、`readDirEnabledDesc`、`pluginMenuTitle`、`pluginMenuPin`、`pluginMenuUnpin`、`pluginMenuReorder`、`pluginMenuReorderHint`、`pluginMenuManage`、`pluginMenuEmpty`、`pluginMenuNoView`、`uiLayoutRequired`、`pluginSettingsInherit`、`presentOpenLocal`、`presentMissing`、`presentEmpty`、`presentAutoOpen`、`presentAutoOpenDesc`、`presentFilesEnabledDesc`、`presentFilesOffHint`、`skillEnabledDesc`、`skillOffHint`
41
+ - 服务端新增 key(45):`claimfiles.no.store`、`claimfiles.list.empty`、`claimfiles.list.ttl`、`claimfiles.list.head`、`claimfiles.release.all`、`claimfiles.bad.paths`、`claimfiles.bad.outside`、`claimfiles.claim.ok`、`claimfiles.claim.conflict`、`claimfiles.claim.noop`、`claimfiles.release.paths`、`claimfiles.bad.action`、`convread.read.query.head`、`convread.read.chat.note`、`convread.list.bad.kind`、`convread.list.running.more`、`convread.list.history.more`、`convread.list.head.running`、`convread.list.head.history`、`convread.list.head.all`、`convread.read.bad.view`、`convread.read.query.empty`、`convread.files.empty`、`convread.files.more`、`convread.files.head`、`convread.files.claims`、`convread.status.head`、`convread.status.last.tool`、`convread.status.no.tool`、`convread.status.last.say`、`convread.status.no.say`、`convread.status.touched`、`convread.status.touched.none`、`convread.status.waiting`、`convread.status.claims`、`models.enrich.empty`、`models.enrich.cancelled`、`present.files.result.head`、`present.files.result.kindDir`、`present.files.result.missing`、`present.files.result.tail`、`present.files.result.allMissing`、`present.files.result.disabled`、`present.files.result.noItems`、`read.dir.header`
42
+ - 服务端文案变更(1):`convread.bad.action`
43
+ <!-- auto-i18n:end -->
44
+
45
+ ## [0.91.0] — 2026-09-19
46
+
47
+ ### Added
48
+
49
+ - **消息一键复制三件套**(#228)—— 消息 hover 工具条新增「复制纯文本 / 复制 Markdown / 复制为图片」:纯文本走轻量去标记(标题/加粗/链接/表格/代码围栏只去标记留内容);Markdown 取原始源码;图片用 html-to-image(按需加载)把气泡导出 2x PNG 写剪贴板(工具条/复制键/流式光标自动排除)。三个都是 `message.actions` 槽位条目(`host:msg-copy-text/markdown/image`),布局页可隐藏/调序。
50
+ - **上下文压缩软上限 Soft Cap**(#229)—— 设置「对话」页可设全局压缩阈值(tokens)+ 按模型覆盖(`provider/id`,如 `xai/grok-4 → 190000`):会话 tokens 到线即触发已有压缩流程,不再堆到物理上限(防 Grok 类阶梯计费翻倍与长上下文降智)。实现为 `compaction.reserveTokens = window - cap` 的 SDK SettingsManager 覆盖(与重试次数同一 live 机制,reload/建会话/换模型后重放,关闭时回填 SDK 默认 16384);底栏上下文条按 `cap/window` 画琥珀色标记线 + hover 显示阈值。pi 引擎独有(DSH 运行时无此概念,保持关闭)。
51
+ - **SCM「AI 生成」提交信息**(#233)—— SCM 面板提交输入行旁新增生成按钮:服务端汇总暂存区 + 工作区 diff(各截 12000 字符并标注截断)+ numstat + 近 15 条提交主题拼提示词,风格跟随仓库近期提交;走 `completeSimple` 一次性补全(不进对话上下文、不打断流式回复),60s 超时;无模型/非仓库/无改动/失败一律恰好应答一次,按钮不卡转圈。提示词可在设置 → 提示词「AI 提交信息」区块追加/替换(不进预设)。
52
+ - **技能全文按名加载工具 `skill`** —— 名录只渲染 name/description,模型不再拼 `location` 路径调 read,需要正文时调 `skill({name})` 精确命中(单文件 8KB 封顶,禁用集由宿主过滤);工具目录 23→24(默认开)。
53
+ - **插件通道对齐定时任务能力**(#226)—— `host.chat` 新增 `cwd` / `conversationId` / `model` / `thinkingLevel` 四个可选参数:`cwd` 显式 pin 工作空间(不存在即拒绝,Windows 下系统目录如 System32 直接拒绝,防后台启动时 cwd 飘到 system32 高危执行);`conversationId` 命中运行中对话时走 steer 语义投递(网页端实时可见,miss 则回落无头执行);`model` / `thinkingLevel` 投递前预切,失败即拒绝不回落。`wechat-ilink` 跟进:设置里可配默认工作空间、模型、思考强度与「投递到网页当前会话」开关。
54
+
55
+ ### Changed
56
+
57
+ - **插件运行相位 + 坏 manifest 占位行 + 顶栏缺省收起** —— 设置面板插件清单新增运行相位(failed/disabled/active/idle,汇总条 + 行内圆点 + 重扫按钮);坏 manifest 目录不再静默跳过,清单里出占位行标红给原因(`error` + `view:false`,不激活);顶栏低频条目(浏览器操作/声音/语言/主题/版本/GitHub)缺省收进「⋯」,布局页可勾回。
58
+
59
+ ### Fixed
60
+
61
+ - **插件图标 SVG 清洗误杀** —— 白名单比较改大小写不敏感(`viewBox` 曾被整条剥掉致坐标系映射失效);补 `ry`/`points`/`fill-opacity`/`stroke-opacity` 等缺属性(run-trace 的 FiActivity 整图标不可见即 `points` 被剥);`script`/`style`/`title`/`desc`/`foreignObject` 整棵删除(子节点外来命名空间,展平会漏进 SVG);catalog 补 10 个插件 `iconSvg`。
62
+ - **全部组件更新面板列表项防压扁**(#224)—— 补 `flex-shrink: 0` + 悬停暴露错误详情。
63
+ - **插件设置保存后重启即丢(storage 旧快照回写)** —— `PluginStorage` 首次 `load()` 后**缓存永不失效**,而 `storage.json` 有两个写入者:插件自己,以及设置面板的 `saveSettingsValues`(直写磁盘的 `settings` 键,不经过该缓存)。长轮询插件每隔几秒就 `storage.set("cursor", …)` 一次,于是把整份旧快照回写,把面板刚保存的 `settings` 抹成 `undefined` —— 表现为「面板里改了设置、当次会话生效、重启后全部回落默认」。现改为写前重读磁盘(`set`/`delete` 无条件重读,不受 mtime 粒度影响),两个写入者各自保留自己的键;读路径仍走缓存。
64
+ - **`host.chat` / 定时任务的模型切换静默失败**(#226 的「失败即拒绝」未真正生效)—— `ClientSession.setModel` 为兼容 UI 把异常吞成 notice(面板要看到原因,调用方是 fire-and-forget),**永不 reject**,于是插件与定时任务里 `try { await cs.setModel(m) } catch` 的 catch 永不触发:模型 ID 打错或没配供应商密钥时,会静默按旧模型跑完整轮次(账单与效果都和用户预期不符)。新增 `switchModelOrThrow`(切完复核当前模型,不符即抛)供无头路径使用;UI 路径的 `setModel` 语义不变。
65
+ - **子代理 8 项修复** —— `steer`/`stop` 对不存在的 runId 不再谎报成功(先查快照,找不到回未找到 + 指引查 `subagent_list`);`wait_all` 收口长输出改留头 10 行 + 留尾 30 行(旧实现只取前 30 行,结论在尾部会被丢掉);子代理数量上限 16 个(每客户端全局计,超限抛错由工具转友好文本,AI 可改串行/等收口后重试);`spawn`/`delegate` 的启动失败(上限/runtime 创建失败/坏 cwd)转返回文本不再直抛工具异常;相对 `cwd` 按派发者目录解析(旧实现相对 server 进程 cwd 落到别处);跟随模型/思考强度/项目密钥改读真正的派发者会话(旧实现读派发瞬间 active,后台派发会跟错);模板 replace 在 SDK 提示词边界串对不上时前置拼接兜底(旧实现静默回退默认 persona);模板非空扩展白名单不再漏进插件/MCP 工具(工厂期不注册 + `refreshPluginTools` 不回补);快照补上 `prompt`(截断 2000 字符)与 `canceled` 终态;`replace + 空提示词` 模板保存期直接拦截(只想限白名单请用 append)。
66
+ - **升级后主题 CSS 全部 404**(#223)—— 0.90.1 的 Express 4→5 升级后,`sendFile`/`download` 默认 `dotfiles=ignore`,绝对路径含隐藏目录段(如 `~/.pi-web`、`~/.local`、`~/.nvm`)的文件一律被判 404。已对主题 CSS、插件 bundle、文件预览/下载、打包下载、首页等全部绝对路径发送点显式放行(路径本身仍由各路由的 id 白名单/工作区 containment 校验把关),并加冒烟回归 `theme-dotfile-test`。
67
+ - **插件 client bundle 在默认 `~/.pi-web` 下 404**(#230)—— 经实测验证为已修复问题的重复报告:#223 的泛化修复已覆盖插件路由(`dotfiles: "allow"` + `splatParam` 数组兼容),单文件/多级 splat 200、越界 `../..` 404 拦截,直接关闭无代码改动。
68
+ - **定时任务压缩/重启后静默转无头**(#231)—— `schedule_task` 只绑内存对话 id(`c1/c2…`,各客户端从 0 计数、重启/切走即失效),压缩或重启后触发必然误判 closed/gone,巡检报告静默落进后台历史、前台毫无动静。现创建时同时快照落盘会话文件(压缩/重启后稳定):触发先按会话文件重认同一会话(含换新 id 自动重绑定,下次直达);原句柄断开时先回落同项目活跃对话并广播提示;同项目无存活对话才无头执行且明确广播去向。同时 id 唤醒加 `cwd` 护栏(跨项目同 id 必然撞车,不校验会把报告投进无关项目)。
69
+ - **重复压缩标记导致会话打不开**(#235)—— 多次上下文压缩后转录里攒下多个同名 `pi-web-ui-compaction-done` 标记;若标记恰为文件末行,重启后新消息的 parent 会记成该共享 id,SDK 的 last-wins 索引把它解析到最后一个标记(其 parent 前指同子树)→ 回溯成环,`getBranch` 死循环直到 `RangeError: Invalid array length`,整个会话报 `Failed to initialize session`。现标记 id 每次唯一(`clearCompactionPending` 改按 `customType` 定位本次 pending),四个会话打开点(首屏恢复/切项目/打开历史/强制重置)遇到坏转录自动修一次再试:残留 pending 移除(子节点旁路到 pending 的 parent,marker 对上下文零贡献,仍弹"可 /compact 重试")、重复 id 改名+引用改指最近的前序同名、残余环截断;改前同目录留 `.bak` 备份(已存在不覆盖,保最早现场)。
70
+ - **插件子目录文件 404,插件面板白屏**(#225)—— 0.90.1 的 Express 4→5 迁移把通配路由改成命名 `*splat`,但多段路径在 Express 5 里是**数组**(`["a","b.mjs"]`),直接 `String()` 会拼成 `"a,b.mjs"`:插件 vendor 分包/CSS、嵌套文件 HTTP 预览、插件子路径 API(`/plugins/*`、`/plugins-api/*`、`/api/preview/*` 三处)全挂。已加 `splatParam` 统一拼回 `/`(下游越界/包含校验不变),回归进 `plugin-test`(vendor 嵌套)/`plugin-http-test`(多段 API)/新增 `preview-http-test`。另:切到 bundle 没加载出来的插件视图不再静默空白——给「加载中/失败原因 + 重试」占位(`PluginViewFallback`,重试带 `&r=` 击穿 ESM 模块表的失败缓存),真机 E2E 验证过。
71
+
72
+ <!-- auto-i18n:start -->
73
+
74
+ ### i18n
75
+
76
+ - 前端新增 key(36):`brand`、`pluginViewLoading`、`pluginViewLoadFailed`、`pluginViewLoadFailedHint`、`pluginViewRetry`、`softCapTokens`、`softCapHint`、`softCapOff`、`softCapByModel`、`softCapByModelHint`、`softCapModelId`、`softCapAdd`、`softCapRemove`、`softCapMarker`、`copyText`、`copyMarkdown`、`copyImage`、`copyFailed`、`scmGenMsg`、`scmGenMsgRunning`、`scmGenMsgTip`、`scmGenMsgFail`、`parallelReminderEnabled`、`parallelReminderEnabledDesc`、`parallelReminderOffHint`、`scmCommitMsgSettingsTitle`、`scmCommitMsgSettingsDesc`、`scmCommitMsgPromptPlaceholder`、`scmCommitMsgSettingsHint`、`uiLayoutTopbarText`、`pluginPhaseActive`、`pluginPhaseDisabled`、`pluginPhaseFailed`、`pluginPhaseIdle`、`pluginRescan`、`pluginRescanHint`
77
+ - 前端删除 key(2):`brandLogo`、`brandName`
78
+ - 服务端新增 key(21):`agent.subagent.limit.reached`、`scm.commitmsg.no.model`、`scm.commitmsg.no.changes`、`scm.commitmsg.model.terminated`、`scm.commitmsg.empty`、`scm.commitmsg.not.repo`、`scm.commitmsg.timeout`、`delegate.start.failed`、`plugins.manifest.broken`、`prompt.skills.intro.use.skill`、`skill.catalog.empty`、`skill.catalog.title`、`skill.catalog.hint`、`skill.not.found`、`skill.no.filepath`、`skill.file.unreadable`、`skill.file.empty`、`skill.file.failed`、`subagents.spawn.failed`、`subagents.steer.not.found`、`subagents.stop.not.found`
79
+ - 服务端文案变更(1):`subagents.wait.empty`
80
+ - 服务端删除 key(1):`prompt.skills.intro.use.read`
81
+
82
+ <!-- auto-i18n:end -->
83
+
13
84
  ## [0.90.1] — 2026-09-18
14
85
 
15
86
  ### Added
@@ -29,11 +100,13 @@
29
100
  - **Windows 关机跳过 `pty.kill`**(#215 跟进)—— 根治 ConPTY 关停死锁。
30
101
 
31
102
  <!-- auto-i18n:start -->
103
+
32
104
  ### i18n
33
105
 
34
106
  - 前端新增 key(3):`takeoverConversation`、`takeoverHasQuestion`、`waitingQuestionBadge`
35
107
  - 前端中文变更(1):`elsewhereTip`
36
108
  - 前端英文变更(1):`elsewhereTip`
109
+
37
110
  <!-- auto-i18n:end -->
38
111
 
39
112
  ## [0.90.0] — 2026-09-18
@@ -64,10 +137,12 @@
64
137
  - **设置重载失败打日志**,不再静默吞错。
65
138
 
66
139
  <!-- auto-i18n:start -->
140
+
67
141
  ### i18n
68
142
 
69
143
  - 前端新增 key(27):`brandLogo`、`brandName`、`manageProjects`、`projectPickerTitle`、`newProject`、`projectName`、`createAndOpenProject`、`invalidProjectName`、`openProject`、`fileCompress`、`fileExtract`、`fileCompressDownload`、`fileUploadFolder`、`fileChooseFolder`、`fileExtractDestination`、`fileConflictPolicy`、`fileConflictSkip`、`fileConflictOverwrite`、`fileConflictError`、`fileArchiveLimits`、`fileFolderUploadHint`、`fileTransferBusy`、`fileTransferFailed`、`conversationReadEnabledDesc`、`conversationReadOffHint`、`scheduleTaskEnabledDesc`、`scheduleTaskOffHint`
70
144
  - 服务端新增 key(9):`sched.not.wired`、`sched.task.bad.schedule`、`sched.task.interval.too.short`、`sched.task.empty.prompt`、`sched.task.no.cwd`、`sched.list.empty`、`sched.cancel.empty.id`、`sched.cancel.not.found`、`sched.cancel.ok`
145
+
71
146
  <!-- auto-i18n:end -->
72
147
 
73
148
  ## [0.89.0] — 2026-09-17
@@ -103,10 +178,12 @@
103
178
  - **Windows 下 `/webui` 启动不再闪一下控制台窗口**(#176,社区)—— spawn 补 `windowsHide`,`detached` 只在非 Windows 下设;POSIX 行为不变。
104
179
 
105
180
  <!-- auto-i18n:start -->
181
+
106
182
  ### i18n
107
183
 
108
184
  - 前端新增 key(89):`composerResize`、`updateDesktopNote`、`updateDesktopCheck`、`updateDesktopChecking`、`updateDesktopAvailable`、`updateDesktopDownload`、`updateDesktopDownloading`、`updateDesktopDownloaded`、`updateDesktopInstall`、`updateDesktopManual`、`updateDesktopError`、`updateDesktopNoBridge`、`kindGitExtension`、`fileReveal`、`fileOpenDefault`、`refreshBuiltinCatalog`、`refreshBuiltinHint`、`refreshBuiltinBusy`、`refreshBuiltinOk`、`refreshBuiltinFail`、`appendModel`、`appendModelTitle`、`appendModelIdPh`、`appendModelNamePh`、`appendModelAdd`、`appendModelBusy`、`appendModelCancel`、`appendModelOk`、`appendModelFail`、`appendModelApiTitle`、`appendModelApiAuto`、`appendModelBaseUrlPh`、`toolsSectionPlugin`、`toolsPluginHint`、`pluginToolsSection`、`pluginToolsEmpty`、`pluginToolOffHint`、`pluginListTab`、`settingsScheduler`、`schedulerDesc`、`schedulerEmpty`、`schedulerNew`、`schedulerEdit`、`schedulerDelete`、`schedulerRunNow`、`schedulerRunning`、`schedulerEnable`、`schedulerDisable`、`schedulerEnabled`、`schedulerDisabled`、`schedulerNameLabel`、`schedulerNamePlaceholder`、`schedulerDescPlaceholder`、`schedulerCwdLabel`、`schedulerCwdPlaceholder`、`schedulerUseCurrentCwd`、`schedulerKindLabel`、`schedulerKindCron`、`schedulerKindInterval`、`schedulerCronPlaceholder`、`schedulerPresetDaily`、`schedulerPresetHourly`、`schedulerPresetHalfHour`、`schedulerPresetWorkday`、`schedulerPresetMonday`、`schedulerPresetCustom`、`schedulerIntervalMinutes`、`schedulerPromptLabel`、`schedulerPromptPlaceholder`、`schedulerModelLabel`、`schedulerThinkingLabel`、`schedulerCatchUp`、`schedulerCatchUpHint`、`schedulerNextFire`、`schedulerLastRun`、`schedulerNeverRun`、`schedulerHistory`、`schedulerManualBadge`、`schedulerRunOk`、`schedulerRunFail`、`schedulerConfirmDelete`、`schedulerSave`、`schedulerCancelEdit`、`copyConversationId`、`copyConversationPath`、`quoteConversation`、`quoteConversationShort`、`attachConversation`、`attachConversationShort`
109
185
  - 服务端新增 key(6):`convread.list.bad.scope`、`convread.read.bad.args`、`convread.read.id.not.found`、`convread.read.path.not.found`、`convread.bad.action`、`dsh.provider.builtin.refresh.unsupported`
186
+
110
187
  <!-- auto-i18n:end -->
111
188
 
112
189
  ## [0.88.0] — 2026-09-16
@@ -1046,7 +1123,10 @@ when?, children?}`,也收 `topbar` / `settings` 这类简写别名);宿主
1046
1123
  - 0.35.1(2026-08-27):编辑重问保留附件(#18)+ 全窗口拖放(#19)。
1047
1124
  - 0.29.0(2026-08-23):全局搜索弹窗(Ctrl+K)+ 消息列表惰性窗口化。
1048
1125
 
1049
- [Unreleased]: https://github.com/xing-shuyin/pi-web-ui/compare/v0.90.0...main
1126
+ [Unreleased]: https://github.com/xing-shuyin/pi-web-ui/compare/v0.92.0...main
1127
+ [0.92.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.92.0
1128
+ [0.91.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.91.0
1129
+ [0.90.1]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.90.1
1050
1130
  [0.90.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.90.0
1051
1131
  [0.89.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.89.0
1052
1132
  [0.88.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.88.0
package/bin/pi-web-ui.mjs CHANGED
@@ -1000,6 +1000,17 @@ function systemdQuote(value) {
1000
1000
  return JSON.stringify(String(value)).replace(/%/g, "%%");
1001
1001
  }
1002
1002
 
1003
+ /**
1004
+ * systemd paths (WorkingDirectory=) must NOT be quoted: unlike Environment= and
1005
+ * ExecStart=, the path directives do not strip surrounding double quotes, so
1006
+ * WorkingDirectory="/home/me/work" makes systemd fail with "path is not absolute".
1007
+ * The whole (trimmed) value is the path, so inner spaces need no escaping —
1008
+ * only % has to be doubled to survive specifier expansion.
1009
+ */
1010
+ function systemdPath(value) {
1011
+ return String(value).replace(/%/g, "%%");
1012
+ }
1013
+
1003
1014
  /** Build the systemd unit file. */
1004
1015
  function buildUnit(cwd, env) {
1005
1016
  const envLines = Object.entries(env)
@@ -1017,7 +1028,7 @@ After=network.target
1017
1028
  [Service]
1018
1029
  Type=simple
1019
1030
  User=${process.env.SUDO_USER ?? userInfo().username}
1020
- WorkingDirectory=${systemdQuote(cwd)}
1031
+ WorkingDirectory=${systemdPath(cwd)}
1021
1032
  ${envLines}
1022
1033
  ${capabilities}ExecStart=${JSON.stringify(NODE)} ${JSON.stringify(SERVER_ENTRY)}
1023
1034
  Restart=always
@@ -1147,7 +1158,15 @@ function installSystemd(opts) {
1147
1158
  return;
1148
1159
  }
1149
1160
  // Generated units can contain credentials. install also corrects an existing unit's mode.
1150
- runSystemdRoot("install", ["-m", "600", "/dev/stdin", unitPath], content);
1161
+ // Stage through a temp file: `install /dev/stdin` fails on some hosts (WSL) with
1162
+ // ENXIO when stdin is a pipe rather than a tty.
1163
+ const staged = join(tmpdir(), `pi-web-ui-${name}-${process.pid}.service`);
1164
+ writeFileSync(staged, content, { mode: 0o600 });
1165
+ try {
1166
+ runSystemdRoot("install", ["-m", "600", staged, unitPath]);
1167
+ } finally {
1168
+ rmSync(staged, { force: true });
1169
+ }
1151
1170
  runSystemdRoot("systemctl", ["daemon-reload"]);
1152
1171
  runSystemdRoot("systemctl", ["enable", `${name}.service`]);
1153
1172
  // enable --now does not restart an already-active unit after configuration changes.