pi-web-ui 0.95.0 → 0.96.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.
- package/CHANGELOG.md +42 -1
- package/bin/pi-web-ui.mjs +159 -31
- package/dist/server/agent-service.js +880 -240
- package/dist/server/approval-rules.js +56 -10
- package/dist/server/attachment-store.js +21 -4
- package/dist/server/attachments.js +30 -3
- package/dist/server/bg-servers.js +45 -7
- package/dist/server/claim-files-tool.js +4 -8
- package/dist/server/claim-store.js +4 -2
- package/dist/server/client-state.js +63 -14
- package/dist/server/compact-context-tool.js +6 -5
- package/dist/server/composer-drafts.js +9 -0
- package/dist/server/control-socket.js +57 -28
- package/dist/server/conversation-read-tool.js +7 -17
- package/dist/server/dangling-tools.js +72 -5
- package/dist/server/delegate-task.js +25 -28
- package/dist/server/dsh/dsh-agent-service.js +19 -6
- package/dist/server/edit-soft-tool.js +14 -11
- package/dist/server/eval-tool.js +67 -14
- package/dist/server/file-archives.js +16 -5
- package/dist/server/files-service.js +30 -11
- package/dist/server/goal-service.js +141 -19
- package/dist/server/hashline-engine.js +59 -21
- package/dist/server/host-guard.js +94 -0
- package/dist/server/i18n.js +3 -3
- package/dist/server/index.js +306 -78
- package/dist/server/lsp-tool.js +752 -60
- package/dist/server/mcp-bridge.js +47 -3
- package/dist/server/model-admin.js +94 -34
- package/dist/server/office-parse.js +96 -30
- package/dist/server/patch-tool.js +9 -15
- package/dist/server/permission-preset.js +25 -0
- package/dist/server/plan-manager.js +11 -1
- package/dist/server/plugin-catalog-sync.js +36 -19
- package/dist/server/plugin-catalog.js +11 -4
- package/dist/server/plugin-facilities.js +92 -21
- package/dist/server/plugin-llm.js +71 -65
- package/dist/server/plugin-project.js +70 -2
- package/dist/server/plugin-updater.js +115 -12
- package/dist/server/plugins.js +205 -44
- package/dist/server/present-files-tool.js +9 -12
- package/dist/server/process-utils.js +4 -0
- package/dist/server/prompt-composer.js +9 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/read-tool.js +69 -30
- package/dist/server/resolve-global-sdk.js +30 -16
- package/dist/server/schedule-agent-tool.js +12 -15
- package/dist/server/sdk-origin.js +18 -2
- package/dist/server/serialize.js +108 -21
- package/dist/server/settings-service.js +4 -2
- package/dist/server/skill-tool.js +5 -7
- package/dist/server/subagent-templates.js +51 -0
- package/dist/server/subagents.js +48 -72
- package/dist/server/terminals.js +181 -69
- package/dist/server/tool-manager.js +24 -7
- package/dist/server/tool-overrides.js +59 -0
- package/dist/server/update-check.js +28 -1
- package/dist/server/uploads.js +17 -2
- package/dist/server/wait-subscription-scan.js +18 -21
- package/dist/server/ws-client-id.js +29 -0
- package/dist/server/ws-pending-queue.js +60 -0
- package/extensions/webui.ts +60 -2
- package/package.json +4 -2
- package/plugins/catalog.json +20 -0
- package/themes/aetheris.css +457 -0
- package/themes/zhupi-dark.css +658 -0
- package/themes/zhupi.css +711 -0
- package/web/dist/assets/{TerminalPanel-DlGBF2MD.js → TerminalPanel-DDChcYOh.js} +1 -1
- package/web/dist/assets/index-Do9RgJC3.js +366 -0
- package/web/dist/assets/index-DryOsILO.css +1 -0
- package/web/dist/assets/{markdown-eUQn_o9D.js → markdown-DXwnfD9T.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/index-BXnCQL9p.js +0 -364
- package/web/dist/assets/index-RpIGFyi_.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,41 @@
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.96.0] — 2026-09-26
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **内置 LSP 工具新增四个语义动作(#331 Phase 1)** —— `documentSymbol`(分层符号大纲,带行跨度与 300 条防洪截断)、`read_symbol`(按符号名/点分路径精准读取实现体,双遍扫描精确匹配优先、400 行截断、未命中时自愈提示可用符号)、`workspaceSymbol`(工作区全局符号搜索,100 条上限,可不传 `path` 自动探测主文件路由语言服务)、`cascade`(编辑影响级联:查引用方文件并聚合其编译诊断,改坏签名当轮即暴露)。提示词开销保持 ~350 tokens;设置页工具说明中英文同步。
|
|
18
|
+
- **系统提示词与工具 schema token 占用实时估算** —— 设置面板「系统提示词」支持直接查看当前会话实际生效的完整系统提示词与工具 schema 定义,并展示粗略 token 估算值与上下文总占用;工作区与活跃会话切换时主动同步最新设置快照,避免提示词上下文陈旧。
|
|
19
|
+
- **核心内置工具独立开关与运行时门控** —— 设置页新增「核心工具」配置区,支持独立开启/禁用 `bash`、`read`、`edit`、`write` 原生核心工具;工具禁用状态随会话即时生效,并在系统提示词与 tools schema 中同步过滤与剔除,杜绝无效工具调用。
|
|
20
|
+
- **新增内置主题** —— 新增三款内置现代与古典主题:赛博科幻暗色 HUD 风格「以太座舱 (Aetheris HUD)」、古籍朱印浅色风「朱批 (Vermilion Manuscript)」与深色夜读风「朱批·夜 (Vermilion Night)」。
|
|
21
|
+
- **消息气泡复制菜单与直接重问增强** —— 消息底栏复制按钮升级为下拉菜单,支持一键复制 Markdown、纯文本或生成长图 PNG;新增「直接重问」快捷按钮,无需重新聚焦编辑框即可立即复用上一条指令;消息气泡增加右键上下文菜单,支持快速复制、编辑/直接重问、派生分支、回滚到此与语音朗读。
|
|
22
|
+
- **TTS 语音朗读与流式生成提示音** —— 助手消息气泡支持单条 TTS 语音朗读与停止;新增流式开始与完成提示音,后台生成完毕及时提醒。
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **非全功能预设(如代码开发/极简/纯对话)下提示词泄露已禁用工具指南修复** —— `before_agent_start` 提示词组装现在严格按会话当前真正处于活跃状态的工具集合(`activeTools`)过滤 Guidelines 与 Snippets,防止未启用的工具(如 `plan_update`、`ask_user_question`、`subagent_*` 等)的提示词指南被无差别注入到给模型的系统提示词中;彻底解决在「代码开发」预设下 AI 因收到动代码前调用 `plan_update` 的硬性指令而误调用未激活工具导致报错无效的问题;会话创建(`makeRuntimeFactory`)与重载(`reloadSession`)全生命周期对齐当前会话预设门控。
|
|
27
|
+
- **助手气泡 Fork / 回滚无法解析修复(#381)** —— 统一 `server/serialize.ts` 的 `uiMessageId` 与 `findEntryByUiId` 算法为单一事实源,彻底修复从助手气泡触发分支派生(Fork)或回滚到此消息时因 ID 生成算法漂移导致的解析失败问题。
|
|
28
|
+
- **悬空工具调用修复收紧分支与上线检查(#332)** —— `healDanglingToolCallFile` 与 `findDanglingToolCalls` 现在严格过滤 `stopReason` 为 `error` 或 `aborted` 的 assistant(不上线幽灵调用,避免在文件尾补合成结果构造出孤儿 `role: "tool"` 导致 DeepSeek/OpenAI 400 报错),且文件落盘修复仅沿活跃分支 `lastId` 向上回溯当前尾部生效 assistant,彻底跳过老分支遗留的悬空调用,防止跨分支污染。
|
|
29
|
+
- **微信通道插件问题修复(#345)** —— 微信回包自动剥离内部控制标记(如 `[[plan:...]]`、`[[todo:...]]`、`[[conv:...]]`、`[[notify:...]]` 等),仅向微信发送用户可见正文;按微信用户 ID 隔离 `accountId`(`wx_${hash}`),为每个用户分配独立的伪客户端与会话,防止上下文串扰与关闭冲突;增加 `earlyRuns` 机制妥善承接极快完成的运行事件,消除回包竞态。
|
|
30
|
+
- **SoL-Pi 节能看板弹窗展示优化** —— 扩展运行与配置区域默认采用折叠组件收拢,避免未折叠时挤占弹窗主视区。
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- **输入框 `@` 提及支持技能(skills)自动补全** —— 在消息文本任意位置键入 `@` 或 `@skill:` 即可弹出技能候选列表(名称匹配优先于描述匹配,支持中英文双语描述检索与 `@page` 页面置顶防挤占);点选后自动在光标处插入 `@skill:<name>` 词元,与 Pi 运行时的技能提升扩展无缝联动。
|
|
35
|
+
- **收敛型澄清提问与决策就绪型计划规范(#330)** —— `ask_user_question` 现在单次严格限制 1~3 个问题(优先 1 个,超过 3 个直接报错阻断,防止问卷轰炸),选项 schema 收紧为 2~4 个互斥选项且推荐方案置顶,选项 description 要求一句话说明影响与权衡;`plan_update` 提示词升级为「决策就绪型」规划:动代码前先在步骤中落实排查发现(Discovery)、受影响文件清单(File Touch List)与风险回滚预案(Rollback),并随执行实时流转步骤状态。目标向导(`goal_ask` / wizardPrompt)与 DSH 澄清提示词同步对齐收敛型交互。
|
|
36
|
+
|
|
37
|
+
<!-- auto-i18n:start -->
|
|
38
|
+
### i18n
|
|
39
|
+
|
|
40
|
+
- 前端新增 key(49):`reaskDirectly`、`reaskDirectlyTip`、`kindPlugin`、`pluginCheckUpdates`、`pluginCheckUpdatesHint`、`pluginUpdateAvailableBadge`、`pluginUpdateAvailableDetail`、`pluginAllUpToDate`、`piCoreSplitRun`、`piSdkSplitNote`、`piSdkBundledNote`、`installGlobalEngineBtn`、`installGlobalEngineTabTitle`、`saveResultUnknown`、`questionNoneAvailable`、`notifyApprovalTitle`、`notifyApprovalBody`、`notifyApprovalBodyTool`、`sound.approval`、`sound.approval.desc`、`settingsSoundVoice`、`ttsHeader`、`ttsEnable`、`ttsEnableDesc`、`ttsAnnounce`、`ttsAnnounceDesc`、`ttsReadReplies`、`ttsReadRepliesDesc`、`ttsRate`、`ttsVoice`、`ttsVoiceAuto`、`ttsVoiceOnline`、`ttsUnavailable`、`ttsPreviewLine`、`ttsAnnounceDone`、`ttsAnnounceQuestion`、`ttsAnnounceError`、`ttsAnnounceApproval`、`speakMsg`、`stopSpeakingMsg`、`apiKeySavedHint`、`settingsViewPromptTokens`、`settingsPromptContextTotal`、`toolsSectionCore`、`toolsCoreHint`、`toolCoreBashDesc`、`toolCoreReadDesc`、`toolCoreEditDesc`、`toolCoreWriteDesc`
|
|
41
|
+
- 前端中文变更(2):`settingsViewToolsSchema`、`lspToolEnabledDesc`
|
|
42
|
+
- 前端英文变更(2):`settingsViewToolsSchema`、`lspToolEnabledDesc`
|
|
43
|
+
- 服务端新增 key(3):`plugincatalog.sync.doc.invalid`、`terminals.bash.nosentinel.note`、`terminals.command.blocked`
|
|
44
|
+
- 服务端文案变更(1):`terminals.bash.timeout`
|
|
45
|
+
- 服务端删除 key(2):`plugincatalog.sync.source.invalid`、`plugincatalog.sync.read.failed`
|
|
46
|
+
<!-- auto-i18n:end -->
|
|
47
|
+
|
|
13
48
|
## [0.95.0] — 2026-09-24
|
|
14
49
|
|
|
15
50
|
### Added
|
|
@@ -30,9 +65,11 @@
|
|
|
30
65
|
- **受控的持久代码求值沙箱工具 `eval`(opt-in,默认关)** — 新第一方 customTool:在隔离子进程中执行 Python(`py`)或 JavaScript/TypeScript(`js`/`ts`),变量与导入跨调用保持,顶层表达式自动求值回显(省掉以往 `write` 临时脚本 → `bash` 跑 → 删文件的三步流程)。设计要点:① 默认关(`AGENT_TOOL_CATALOG` 里 `defaultOn: false`),关掉 AI 不知道有它,杜绝「什么问题都塞进内核」的工具挤占;② 每个会话一个独立内核进程 + 独立临时目录(cwd 不落在项目里,项目路径经 `PROJECT_DIR` 变量显式引用),关对话 / 停服务即回收进程树(Windows `taskkill /F /T`,Unix `SIGKILL` 进程组),不留孤儿;③ 单请求默认 15s、上限 120s 硬超时,超时杀进程树后内核自动重启,不会把会话拖死;④ 驱动协议串行排队,并行 `eval` 调用不会互相覆盖 resolver;⑤ stderr 持续排空,避免原生扩展写满管道缓冲造成假超时。DSH 引擎无 customTool 注册面,不接。
|
|
31
66
|
|
|
32
67
|
### Changed
|
|
68
|
+
|
|
33
69
|
- **插件 manifest 校验失败即拒(P1-6)** — 坏 manifest 不再带病启动:`id` 非法/与目录名不一致、`apiVersion` 非法、未知能力拼写、`engines`/`permissions`/`ui` 坏形状、v2 不声明 `permissions`、有 `ui` 声明却无 `ui` 能力,都会拒绝激活(scan 置红 + 诊断随清单下发,activate 重判)。以前其中两类(v2 无能力声明、只有别的能力却写 `ui`)是"激活成功但 ui 静默忽略",现在是明确拒绝;纯警告(坏文本字段/坏数组字段/截断)仍不阻断。未来版本(`apiVersion` 大于宿主)仍走版本门出"请升级",校验层不抢错。
|
|
34
70
|
|
|
35
71
|
### Fixed
|
|
72
|
+
|
|
36
73
|
- **目标调研向导:切会话不再丢弃调研成果,且消息流里有「原始目标草案」卡片(#292)** — 两处修复:① `setGoal` 新增 `targetConvId`,调研收敛后把目标写到**发起调研的那个对话**(原来是硬读 `activeConv` 再一刀切丢:「已切换对话,目标调研结果已丢弃」——用户在向导问答期间切去看代码/文档是常态,多轮问答瞬间白做);发起会话已关闭时响亮拒绝而不是静默丢。完成通知随之改成点名会话(`🎯 会话「title」目标调研完成,目标已设为…`),取消/无结果时也告诉用户草案还在哪儿。② 调研开始先往发起对话推一张只读「🎯 原始目标草案」卡片,排在所有提问之前——调研被超时/取消打断后,用户至少能把自己最初写的那段需求读回来、复制重试,而不是面对一片空白。
|
|
37
74
|
- **子代理(in-memory 会话)的扩展错误不再按轮数刷屏(#298)** — `SessionManager.inMemory(cwd)` 建出来的子代理会话取不到会话目录(`getSessionDir()` 返回空串),而 SDK 的 `ExtensionRunner.emitContext()` 在**每次 provider 请求**前都会跑一遍扩展的 `context` hook,于是「会话目录依赖型」扩展(如 SoL-Pi 的 `runtimeRoot()`)每轮都抛同一个错。原 `bindExtensions` 的 `onError` 把错误原样广播成 notice:不去重、不带会话归属、不落服务端日志,一个子代理跑 N 轮就弹 N 条,且看不出是哪个会话出的问题。现改为共享的 `makeExtensionErrorReporter()`:① 同一会话内「扩展 + 事件 + 错误文本」只提示一次;② 子代理的 notice 带 `子代理 <conversationId>:` 前缀(与同函数内其它子代理通知口径一致);③ 全量错误(含 extensionPath / event / stack)始终 `console.error` 落服务端日志。主对话(持久会话)行为不变,只是多了去重与日志。
|
|
38
75
|
- **插件市场仅同步列表时保留已激活插件实例(#296,感谢 @StarryJia)** — 启动预同步和手动目录同步不再重载插件,避免重新激活时重复广播当前工作目录。
|
|
@@ -45,6 +82,7 @@
|
|
|
45
82
|
- **任务执行看板与对话列等宽** — Plan Mode 的任务执行看板(`PlanBoard`)此前写死左右各 16px 外边距,没走 `.main` 的列 token(`--chat-inset`):桌面 / 宽屏聊天列下比消息列与输入框宽一截、手机上又比它们窄一点,左右边缘始终对不齐。现改为同一条列 token(`.plan-board` 落进 `styles.css`,与 `.goalbar` / 问卷面板同口径),任何视口宽度与「宽屏聊天列」开关下都与输入框严格齐平。回归:`tests/chat-column-align-test.mjs` 新增看板条目。
|
|
46
83
|
|
|
47
84
|
<!-- auto-i18n:start -->
|
|
85
|
+
|
|
48
86
|
### i18n
|
|
49
87
|
|
|
50
88
|
- 前端新增 key(104):`newChatEphemeral`、`newChatEphemeralTip`、`ephemeralBadge`、`ephemeralBannerText`、`elsewhereActions`、`takeoverConfirm`、`saveEphemeral`、`forkSession`、`forkSessionTip`、`rollbackSession`、`rollbackSessionTip`、`rollbackConfirm`、`rollbackRestoreWorkspace`、`rollbackRestoreWorkspaceTip`、`toolApprovalTitle`、`toolApprovalApprove`、`toolApprovalDeny`、`toolApprovalEditAndRun`、`toolApprovalRiskAlert`、`toolApprovalCommand`、`toolApprovalParams`、`toolApprovalEditPlaceholder`、`toolApprovalReason`、`toolApprovalCategory`、`toolApprovalAllowCategory`、`toolApprovalAllowCategoryHint`、`toolApprovalAllowConversation`、`toolApprovalAllowConversationHint`、`toolApprovalEnabled`、`toolApprovalEnabledDesc`、`toolApprovalPolicyTitle`、`toolApprovalPolicyAllowAll`、`toolApprovalPolicyRevoke`、`toolApprovalPolicyHint`、`settingsApprovalRules`、`settingsApprovalRulesDesc`、`manageApprovalRules`、`approvalRuleNew`、`approvalRuleEdit`、`approvalRuleDelete`、`approvalRuleReset`、`approvalRuleResetConfirm`、`approvalRuleDeleteConfirm`、`approvalRuleActionAsk`、`approvalRuleActionDeny`、`approvalRuleActionAllow`、`approvalRuleTools`、`approvalRuleToolsTip`、`approvalRuleField`、`approvalRuleFieldCommand`、`approvalRuleFieldPath`、`approvalRuleFieldParams`、`approvalRuleMatch`、`approvalRuleMatchRegex`、`approvalRuleMatchGlob`、`approvalRuleMatchContains`、`approvalRuleMatchPrefix`、`approvalRuleMatchOutsideWs`、`approvalRuleValue`、`approvalRuleValueTip`、`approvalRuleLabel`、`approvalRuleLabelEn`、`approvalRuleReason`、`approvalRuleReasonEn`、`approvalRuleEnabled`、`approvalRuleBuiltin`、`approvalRuleEmpty`、`approvalRuleMoveUp`、`approvalRuleMoveDown`、`planBoardTitle`、`planBoardSteps`、`planBoardProgress`、`planBoardNoPlan`、`planBoardCompleted`、`planBoardInProgress`、`planBoardPending`、`planBoardFailed`、`clear`、`confirm`、`forkBadge`、`forkBadgeTip`、`goalBarBlocked`、`toolsPresetBanner`、`toolsBackToStandard`、`toolsBlockedByPreset`、`skillsHiddenByPreset`、`terminalBashMaxForegroundMs`、`terminalBashMaxForegroundMsDesc`、`pluginInspectAlreadyInstalled`、`pluginInspectInvalid`、`pluginInspectNotPlugin`、`pluginInspectNetwork`、`uiLayoutDiagTitle`、`uiLayoutDiagHint`、`planUpdateEnabledDesc`、`planUpdateOffHint`、`compactContextEnabledDesc`、`compactContextOffHint`、`evalEnabledDesc`、`evalOffHint`、`patchToolEnabledDesc`、`patchToolOffHint`、`lspToolEnabledDesc`、`lspToolOffHint`
|
|
@@ -54,6 +92,7 @@
|
|
|
54
92
|
- 服务端新增 key(14):`editsoft.fragment.not.supported`、`goal.wizard.draft.card`、`goal.autonomous.pass`、`goal.review.blocked`、`goal.autonomous.continue`、`goal.review.blocked_msg`、`plugins.requires.cycle`、`plugins.manifest.invalid`、`plugins.requires.cascade`、`subagents.handoff.self`、`subagents.handoff.not.found`、`subagents.handoff.success`、`subagents.handoff.failed`、`terminals.bash.background.elapsed`
|
|
55
93
|
- 服务端文案变更(1):`terminals.bash.background.running`
|
|
56
94
|
- 服务端删除 key(2):`dsh.attach.file.large`、`dsh.attach.file.ref.fallback`
|
|
95
|
+
|
|
57
96
|
<!-- auto-i18n:end -->
|
|
58
97
|
|
|
59
98
|
## [0.94.1] — 2026-09-22
|
|
@@ -1254,7 +1293,9 @@ when?, children?}`,也收 `topbar` / `settings` 这类简写别名);宿主
|
|
|
1254
1293
|
- 0.35.1(2026-08-27):编辑重问保留附件(#18)+ 全窗口拖放(#19)。
|
|
1255
1294
|
- 0.29.0(2026-08-23):全局搜索弹窗(Ctrl+K)+ 消息列表惰性窗口化。
|
|
1256
1295
|
|
|
1257
|
-
[Unreleased]: https://github.com/xing-shuyin/pi-web-ui/compare/v0.
|
|
1296
|
+
[Unreleased]: https://github.com/xing-shuyin/pi-web-ui/compare/v0.96.0...main
|
|
1297
|
+
[0.96.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.96.0
|
|
1298
|
+
[0.95.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.95.0
|
|
1258
1299
|
[0.94.1]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.94.1
|
|
1259
1300
|
[0.94.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.94.0
|
|
1260
1301
|
[0.93.0]: https://github.com/xing-shuyin/pi-web-ui/releases/tag/v0.93.0
|
package/bin/pi-web-ui.mjs
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { spawn, spawnSync } from "node:child_process";
|
|
28
28
|
import { createConnection } from "node:net";
|
|
29
29
|
import { get as httpGet } from "node:http";
|
|
30
|
+
import { createInterface } from "node:readline/promises";
|
|
30
31
|
import {
|
|
31
32
|
ensureBackup as ensurePluginBackup,
|
|
32
33
|
restoreBackup as restorePluginBackup,
|
|
@@ -55,9 +56,9 @@ import { fileURLToPath } from "node:url";
|
|
|
55
56
|
const BIN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
56
57
|
/** <pkg>/dist/server/index.js — the actual server entry. */
|
|
57
58
|
const SERVER_ENTRY = join(BIN_DIR, "..", "dist", "server", "index.js");
|
|
58
|
-
/**
|
|
59
|
+
/** 「机器上有更新的 pi 副本就跟随它」解析钩子(issue #260;#321 起默认启用)。
|
|
59
60
|
* 必须早于任何 SDK 静态 import 加载 —— 所以每条启动路径都把它当 `--import` 传进去;
|
|
60
|
-
*
|
|
61
|
+
* PI_WEB_SDK=bundled 显式钉死自带副本(见 server/resolve-global-sdk.ts)。 */
|
|
61
62
|
const SDK_HOOK = join(BIN_DIR, "..", "dist", "server", "resolve-global-sdk.js");
|
|
62
63
|
/** dist 可能是旧构建(没有这个钩子文件)—— 只有文件在才注入:`--import <missing>` 会让
|
|
63
64
|
* CLI/服务直接起不来,那比少个开关严重得多。 */
|
|
@@ -108,8 +109,9 @@ server 选项:
|
|
|
108
109
|
--engine <pi|dsh> 智能体引擎(默认 $PI_WEB_ENGINE 或 pi)
|
|
109
110
|
--host <addr> 监听地址(默认 $PI_WEB_HOST 或 127.0.0.1;0.0.0.0 供局域网/容器)
|
|
110
111
|
--agent-dir <dir> pi 配置目录(默认 $PI_CODING_AGENT_DIR 或 ~/.pi/agent)
|
|
111
|
-
--name <name> 服务名(默认 pi-web-ui
|
|
112
|
-
为 com.xingshuyin.pi-web-ui
|
|
112
|
+
--name <name> 服务名(默认 pi-web-ui;仅限字母/数字/-/_,字母或数字开头,
|
|
113
|
+
最长 64 字符。macOS 的 launchd label 为 com.xingshuyin.pi-web-ui,
|
|
114
|
+
自定义名时为 com.<name>.server)
|
|
113
115
|
--print 只打印将生成的配置文件,不实际安装
|
|
114
116
|
|
|
115
117
|
平台: macOS → launchd 用户代理 · Linux → systemd · Windows → 登录自启 Run 键
|
|
@@ -128,7 +130,8 @@ server 选项:
|
|
|
128
130
|
install 选项: --name <id> 自定义插件目录名(默认取仓库名)
|
|
129
131
|
--data-dir <dir> 数据目录(默认 ~/.pi-web)
|
|
130
132
|
--force 目标已存在时覆盖
|
|
131
|
-
--build
|
|
133
|
+
--build 强制源码构建(隔离目录编译,见下;不加 --build 时若插件只有
|
|
134
|
+
源码,交互终端会询问是否构建并需输入 y 确认,非交互环境跳过)
|
|
132
135
|
--no-build 即使只有源码也不构建(产物缺失的插件装上后不加载)
|
|
133
136
|
--catalog <url> 目录同步模式:读目录文档 → 写可安装列表 → 逐条安装
|
|
134
137
|
--replace 配合 --catalog:整体替换列表(默认按 id 合并)
|
|
@@ -161,7 +164,8 @@ Server options:
|
|
|
161
164
|
--engine <pi|dsh> Agent engine (default $PI_WEB_ENGINE or pi)
|
|
162
165
|
--host <addr> Listen address (default $PI_WEB_HOST or 127.0.0.1; 0.0.0.0 for LAN/containers)
|
|
163
166
|
--agent-dir <dir> pi config directory (default $PI_CODING_AGENT_DIR or ~/.pi/agent)
|
|
164
|
-
--name <name> Service name (default pi-web-ui;
|
|
167
|
+
--name <name> Service name (default pi-web-ui; alphanumeric, "-" and "_" only,
|
|
168
|
+
starting with a letter or digit, max 64 chars. macOS launchd label is
|
|
165
169
|
com.xingshuyin.pi-web-ui, or com.<name>.server for custom names)
|
|
166
170
|
--print Only print generated config files (no actual install)
|
|
167
171
|
|
|
@@ -181,7 +185,9 @@ UI plugins (installed into <data-dir>/plugins/; refresh browser to activate whil
|
|
|
181
185
|
install options: --name <id> Custom plugin directory name (default: repo name)
|
|
182
186
|
--data-dir <dir> Data directory (default: ~/.pi-web)
|
|
183
187
|
--force Overwrite if target already exists
|
|
184
|
-
--build Force a source build (isolated build, see below)
|
|
188
|
+
--build Force a source build (isolated build, see below). Without it,
|
|
189
|
+
a source-only plugin asks for confirmation on a TTY (answer y to
|
|
190
|
+
build) and skips the build in non-interactive environments
|
|
185
191
|
--no-build Never build, even for source-only plugins
|
|
186
192
|
--catalog <url> Catalog mode: read a catalog document, write the
|
|
187
193
|
installable list, then install every entry
|
|
@@ -519,8 +525,16 @@ function winIcoPath() {
|
|
|
519
525
|
return join(winServiceDir(), APP_ICO_NAME);
|
|
520
526
|
}
|
|
521
527
|
|
|
522
|
-
/** Full path to Windows PowerShell
|
|
528
|
+
/** Full path to Windows PowerShell. Prefers pwsh.exe (PowerShell 7) when it is
|
|
529
|
+
* resolvable on PATH: some machines ship Windows PowerShell 5.1 as a broken
|
|
530
|
+
* stub whose launch fails silently, so probe for a working shell first and
|
|
531
|
+
* fall back to the built-in path. */
|
|
523
532
|
function winPowershell() {
|
|
533
|
+
const which = spawnSync("where.exe", ["pwsh.exe"], { encoding: "utf8" });
|
|
534
|
+
if (which.status === 0) {
|
|
535
|
+
const first = (which.stdout ?? "").trim().split(/\r?\n/)[0];
|
|
536
|
+
if (first) return first;
|
|
537
|
+
}
|
|
524
538
|
return join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
525
539
|
}
|
|
526
540
|
|
|
@@ -535,12 +549,13 @@ function winWscript() {
|
|
|
535
549
|
* Resolve the real node binary. fnm/volta/nvm shims (e.g. fnm_multishells)
|
|
536
550
|
* point into temp dirs that vanish when the installing shell exits — the
|
|
537
551
|
* baked-in launcher scripts must use the stable real path instead.
|
|
552
|
+
* Also applies resolveNode() so Bun-hosted pi never bakes in pi.exe.
|
|
538
553
|
*/
|
|
539
554
|
function realNode() {
|
|
540
555
|
try {
|
|
541
|
-
return realpathSync(
|
|
556
|
+
return realpathSync(resolveNode());
|
|
542
557
|
} catch {
|
|
543
|
-
return
|
|
558
|
+
return resolveNode();
|
|
544
559
|
}
|
|
545
560
|
}
|
|
546
561
|
|
|
@@ -682,7 +697,11 @@ function buildWinHiddenVbs(ps1Path) {
|
|
|
682
697
|
function installWinShortcut(opts) {
|
|
683
698
|
const { name, port, cwd, dataDir, engine, host, agentDir } = serviceOptions(opts);
|
|
684
699
|
const env = serviceEnv(port, cwd, dataDir, engine, host, agentDir);
|
|
685
|
-
|
|
700
|
+
// The server binds 127.0.0.1 by default; PowerShell 7's Invoke-WebRequest
|
|
701
|
+
// resolves `localhost` to ::1 first and hangs until TimeoutSec when nothing
|
|
702
|
+
// listens on IPv6, making the health probe misfire ("server not running").
|
|
703
|
+
// Pin the probe (and the opened URL) to the IPv4 loopback the server uses.
|
|
704
|
+
const url = `http://127.0.0.1:${port}`;
|
|
686
705
|
const ps1Path = winShortcutPs1Path(name);
|
|
687
706
|
const ps1 = buildWinShortcutPs1(env, cwd, name, url, winLogPath(name), winPidFilePath(name));
|
|
688
707
|
if (opts.print) {
|
|
@@ -986,19 +1005,53 @@ function launchWinService(vbsPath) {
|
|
|
986
1005
|
child.unref();
|
|
987
1006
|
}
|
|
988
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* PID 文件记录的是看门狗 PowerShell 的 $PID(winPowershell() 可能选 pwsh.exe),
|
|
1010
|
+
* node 是它的子进程 —— 这三者是本服务实例的合理映像名。taskkill /T /F 是树级强杀,
|
|
1011
|
+
* PID 若已退出被系统复用就会误杀无辜进程,所以杀前必须核对映像名。
|
|
1012
|
+
*/
|
|
1013
|
+
const WIN_SERVICE_IMAGE_RE = /^(powershell|pwsh|node)\.exe$/i;
|
|
1014
|
+
|
|
1015
|
+
/** tasklist 查询 PID 对应的进程映像名;查询失败/无此进程返回 null(无法核实)。 */
|
|
1016
|
+
function winPidImageName(pid) {
|
|
1017
|
+
const res = spawnSync("tasklist", ["/FI", `PID eq ${pid}`, "/FO", "CSV", "/NH"], { encoding: "utf8" });
|
|
1018
|
+
if (res.status !== 0) return null;
|
|
1019
|
+
const line = (res.stdout ?? "").split(/\r?\n/).find((l) => l.trim());
|
|
1020
|
+
if (!line) return null; // tasklist 的 "INFO: 无任务" 输出没有 CSV 行
|
|
1021
|
+
return (line.match(/^"([^"]+)"/) ?? [])[1] ?? null;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
989
1024
|
/** Kill a running Windows instance via its PID file (whole tree). */
|
|
990
1025
|
function stopWinInstance(name) {
|
|
991
1026
|
const pid = winReadPid(name);
|
|
992
1027
|
if (pid) {
|
|
993
1028
|
if (pidAlive(pid)) {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1029
|
+
// PID 可能已退出并被系统复用给别的进程:杀前核对映像名,不符则警告并跳过,
|
|
1030
|
+
// 绝不 taskkill /T /F 盲杀。macOS/Linux 无按 PID 杀的路径(launchctl/systemctl
|
|
1031
|
+
// 按服务单元管理),只有 Windows 需要这道核对。
|
|
1032
|
+
const image = winPidImageName(pid);
|
|
1033
|
+
if (image === null) {
|
|
1034
|
+
console.warn(
|
|
1035
|
+
ZH
|
|
1036
|
+
? `⚠ 无法核实 PID ${pid} 的进程映像名(tasklist 不可用或进程已消失),跳过强制结束以防误杀`
|
|
1037
|
+
: `⚠ Cannot verify the image name of PID ${pid} (tasklist unavailable or process gone), skipping force-kill to avoid killing an unrelated process`,
|
|
1038
|
+
);
|
|
1039
|
+
} else if (!WIN_SERVICE_IMAGE_RE.test(image)) {
|
|
1040
|
+
console.warn(
|
|
1041
|
+
ZH
|
|
1042
|
+
? `⚠ PID ${pid} 现在是 ${image},不是本服务的进程(PID 可能已被复用),跳过强制结束`
|
|
1043
|
+
: `⚠ PID ${pid} is now ${image}, not a pi-web-ui process (PID may have been reused), skipping force-kill`,
|
|
1044
|
+
);
|
|
1045
|
+
} else {
|
|
1046
|
+
run("taskkill", ["/PID", String(pid), "/T", "/F"], {
|
|
1047
|
+
ignoreError: true,
|
|
1048
|
+
silent: true,
|
|
1049
|
+
});
|
|
1050
|
+
// taskkill /F 异步生效:等到进程树真正退出(restart 需要避免端口竞争)
|
|
1051
|
+
const deadline = Date.now() + 5000;
|
|
1052
|
+
while (Date.now() < deadline && pidAlive(pid)) {
|
|
1053
|
+
spawnSync("ping", ["-n", "2", "127.0.0.1"], { stdio: "ignore" });
|
|
1054
|
+
}
|
|
1002
1055
|
}
|
|
1003
1056
|
}
|
|
1004
1057
|
rmSync(winPidFilePath(name), { force: true });
|
|
@@ -1110,9 +1163,27 @@ function effectivePort(opts) {
|
|
|
1110
1163
|
return String(opts.port ?? process.env.PI_WEB_PORT ?? "8787");
|
|
1111
1164
|
}
|
|
1112
1165
|
|
|
1166
|
+
/**
|
|
1167
|
+
* 服务名(--name)统一校验。name 会被原样拼进 systemd unit 文件名
|
|
1168
|
+
* (/etc/systemd/system/<name>.service)、Windows 启动脚本/PID 文件路径
|
|
1169
|
+
* (%APPDATA%\pi-web-ui\<name>.ps1) 与 HKCU Run 键值名 —— 不限字符集时,
|
|
1170
|
+
* 路径分隔符/引号/空格都能进这些拼接点(路径穿越或 unit 内容注入)。
|
|
1171
|
+
* 收紧为:字母或数字开头,仅字母/数字/-/_,最长 64 字符。
|
|
1172
|
+
*/
|
|
1173
|
+
function serviceName(opts) {
|
|
1174
|
+
const name = opts.name ?? "pi-web-ui";
|
|
1175
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(name))
|
|
1176
|
+
fail(
|
|
1177
|
+
ZH
|
|
1178
|
+
? `无效服务名 "${name}"(仅限字母/数字/-/_,字母或数字开头,最长 64 字符)`
|
|
1179
|
+
: `Invalid service name "${name}" (alphanumeric, "-" and "_" only; must start with a letter or digit; max 64 chars)`,
|
|
1180
|
+
);
|
|
1181
|
+
return name;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1113
1184
|
/** Shared option normalization for install. */
|
|
1114
1185
|
function serviceOptions(opts) {
|
|
1115
|
-
const name = opts
|
|
1186
|
+
const name = serviceName(opts);
|
|
1116
1187
|
const port = effectivePort(opts);
|
|
1117
1188
|
if (!/^\d{1,5}$/.test(port) || Number(port) < 1 || Number(port) > 65535) {
|
|
1118
1189
|
fail(ZH ? `无效端口: ${port}` : `Invalid port: ${port}`);
|
|
@@ -1267,7 +1338,7 @@ function installSystemd(opts) {
|
|
|
1267
1338
|
}
|
|
1268
1339
|
|
|
1269
1340
|
function uninstallLaunchd(opts) {
|
|
1270
|
-
const name = opts
|
|
1341
|
+
const name = serviceName(opts);
|
|
1271
1342
|
const label = serviceLabel(name);
|
|
1272
1343
|
const plist = launchAgentPlist(name);
|
|
1273
1344
|
run("launchctl", ["bootout", `gui/${uid()}/${label}`], {
|
|
@@ -1285,7 +1356,7 @@ function uninstallLaunchd(opts) {
|
|
|
1285
1356
|
}
|
|
1286
1357
|
|
|
1287
1358
|
function uninstallSystemd(opts) {
|
|
1288
|
-
const name = opts
|
|
1359
|
+
const name = serviceName(opts);
|
|
1289
1360
|
ensureRootForSystemctl();
|
|
1290
1361
|
run("systemctl", ["disable", "--now", `${name}.service`], {
|
|
1291
1362
|
ignoreError: true,
|
|
@@ -1368,7 +1439,7 @@ function installWindows(opts) {
|
|
|
1368
1439
|
}
|
|
1369
1440
|
|
|
1370
1441
|
function uninstallWindows(opts) {
|
|
1371
|
-
const name = opts
|
|
1442
|
+
const name = serviceName(opts);
|
|
1372
1443
|
winRunKeyDelete(name);
|
|
1373
1444
|
// 历史计划任务(旧版本 install 可能注册过)
|
|
1374
1445
|
if (winTaskExists(name)) {
|
|
@@ -1503,7 +1574,7 @@ async function setQuiesce(opts, on) {
|
|
|
1503
1574
|
}
|
|
1504
1575
|
|
|
1505
1576
|
function controlService(action, opts) {
|
|
1506
|
-
const name = opts
|
|
1577
|
+
const name = serviceName(opts);
|
|
1507
1578
|
|
|
1508
1579
|
if (isMac) {
|
|
1509
1580
|
const label = serviceLabel(name);
|
|
@@ -2072,11 +2143,13 @@ function buildPluginSource(pluginRoot, tmpDir, manifest) {
|
|
|
2072
2143
|
* 构建决策(issue #165:--build 自动推断)。
|
|
2073
2144
|
*
|
|
2074
2145
|
* 只有源码、没有产物(index.mjs 与 client/entry.mjs 都缺)且存在可解析的构建声明 =
|
|
2075
|
-
*
|
|
2076
|
-
*
|
|
2146
|
+
* “不构建这次安装必死”,此前会直接自动构建;但构建命令来自远端 manifest/package.json,
|
|
2147
|
+
* 经 shell:true 执行属于任意命令执行面,非交互环境下静默跑等于替用户放行。
|
|
2148
|
+
* 收紧后(审计修复):mode=auto 只表示“有构建声明且必要”,是否真正执行由
|
|
2149
|
+
* installOnePlugin 二次把关 —— 交互 TTY 且用户输入 y 才跑,否则跳过并警告。
|
|
2077
2150
|
* --no-build 保留旧的“装个空目录”行为(脚本化镜像/检查用),并明确打印跳过原因。
|
|
2078
2151
|
*/
|
|
2079
|
-
function decideBuildAction({ pluginRoot, manifest, build, noBuild }) {
|
|
2152
|
+
function decideBuildAction({ pluginRoot, manifest, build, noBuild, isCatalog = false }) {
|
|
2080
2153
|
if (build && noBuild)
|
|
2081
2154
|
throw new Error(
|
|
2082
2155
|
ZH ? "--build 与 --no-build 不能同时用(二选一)" : "--build and --no-build cannot be used together (pick one)",
|
|
@@ -2085,12 +2158,14 @@ function decideBuildAction({ pluginRoot, manifest, build, noBuild }) {
|
|
|
2085
2158
|
const artifactsMissing =
|
|
2086
2159
|
!existsSync(join(pluginRoot, "index.mjs")) && !existsSync(join(pluginRoot, "client", "entry.mjs"));
|
|
2087
2160
|
if (build) {
|
|
2088
|
-
if (!plan)
|
|
2161
|
+
if (!plan) {
|
|
2162
|
+
if (isCatalog) return { mode: "none", plan };
|
|
2089
2163
|
throw new Error(
|
|
2090
2164
|
ZH
|
|
2091
2165
|
? "插件没有声明构建方式:请在 manifest.json 里加 build.command(或 package.json 的 scripts.build),或去掉 --build"
|
|
2092
2166
|
: "Plugin has no build declaration: add build.command to manifest.json (or scripts.build in package.json), or remove --build",
|
|
2093
2167
|
);
|
|
2168
|
+
}
|
|
2094
2169
|
return { mode: "explicit", plan };
|
|
2095
2170
|
}
|
|
2096
2171
|
if (plan && artifactsMissing) {
|
|
@@ -2100,12 +2175,31 @@ function decideBuildAction({ pluginRoot, manifest, build, noBuild }) {
|
|
|
2100
2175
|
return { mode: "none", plan };
|
|
2101
2176
|
}
|
|
2102
2177
|
|
|
2178
|
+
/**
|
|
2179
|
+
* 交互确认:插件声明的构建命令将经 shell 在本机执行(install + command 都来自
|
|
2180
|
+
* 远端 manifest/package.json),必须用户显式输入 y 才跑。仅 TTY 下会被调用。
|
|
2181
|
+
*/
|
|
2182
|
+
async function confirmSourceBuild(plan) {
|
|
2183
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2184
|
+
try {
|
|
2185
|
+
const answer = await rl.question(
|
|
2186
|
+
ZH
|
|
2187
|
+
? `⚠ 插件声明了以下构建命令,将在本机以 shell 执行:\n 1) ${plan.install}\n 2) ${plan.command}\n 确认执行?输入 y 继续,其他任意键跳过构建: `
|
|
2188
|
+
: `⚠ The plugin declares the following build commands, which will run through a shell on this machine:\n 1) ${plan.install}\n 2) ${plan.command}\n Proceed? Enter y to run, anything else to skip the build: `,
|
|
2189
|
+
);
|
|
2190
|
+
const normalized = answer.trim().toLowerCase();
|
|
2191
|
+
return normalized === "y" || normalized === "yes";
|
|
2192
|
+
} finally {
|
|
2193
|
+
rl.close();
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2103
2197
|
/**
|
|
2104
2198
|
* 装一个插件(单源模式与目录模式共用):拉取/定位 → 读 manifest → 构建决策 →
|
|
2105
2199
|
* 覆盖(备份+保留 config.json)→ 落盘 → 记录来源/sha。
|
|
2106
2200
|
* 失败抛 Error(目录模式逐条 try/catch 继续下一条,单源模式由调用方转 fail)。
|
|
2107
2201
|
*/
|
|
2108
|
-
async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir }) {
|
|
2202
|
+
async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir, isCatalog = false }) {
|
|
2109
2203
|
const pluginsDir = join(dataDir, "plugins");
|
|
2110
2204
|
const localCandidate = resolve(rawSpec.replace(/^file:\/\//, ""));
|
|
2111
2205
|
const isLocal = existsSync(localCandidate);
|
|
@@ -2132,9 +2226,36 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
|
|
|
2132
2226
|
}
|
|
2133
2227
|
// 构建决策(issue #150 的 --build + issue #165 的自动推断):构建在临时目录里完成,
|
|
2134
2228
|
// 成功后才进入覆盖流程——构建失败 = 目标目录完全没被动过(上一版插件照常可用)。
|
|
2135
|
-
const decision = decideBuildAction({
|
|
2229
|
+
const decision = decideBuildAction({
|
|
2230
|
+
pluginRoot,
|
|
2231
|
+
manifest,
|
|
2232
|
+
build: build === true,
|
|
2233
|
+
noBuild: noBuild === true,
|
|
2234
|
+
isCatalog,
|
|
2235
|
+
});
|
|
2236
|
+
// 授权把关(审计修复):--build 显式传参行为不变;mode=auto 的构建命令来自
|
|
2237
|
+
// 远端 manifest/package.json 且经 shell:true 执行 —— 非交互环境绝不自动跑
|
|
2238
|
+
// (跳过并警告),交互 TTY 也要用户显式输入 y 才执行。
|
|
2239
|
+
let authorized = decision.mode === "explicit";
|
|
2240
|
+
if (decision.mode === "auto") {
|
|
2241
|
+
if (!process.stdin.isTTY) {
|
|
2242
|
+
console.warn(
|
|
2243
|
+
ZH
|
|
2244
|
+
? "⚠ 插件声明了构建命令但未授权执行,已跳过构建 —— 传 --build 或手动构建后再安装"
|
|
2245
|
+
: "⚠ Plugin declares build commands but they were not authorized; skipping the build — pass --build or build manually before installing",
|
|
2246
|
+
);
|
|
2247
|
+
} else {
|
|
2248
|
+
authorized = await confirmSourceBuild(decision.plan);
|
|
2249
|
+
if (!authorized)
|
|
2250
|
+
console.warn(
|
|
2251
|
+
ZH
|
|
2252
|
+
? "⚠ 未确认执行构建命令,已跳过构建 —— 需要时传 --build 或手动构建后再安装"
|
|
2253
|
+
: "⚠ Build commands not confirmed; skipping the build — pass --build or build manually if needed",
|
|
2254
|
+
);
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2136
2257
|
let installRoot = pluginRoot;
|
|
2137
|
-
if (
|
|
2258
|
+
if (authorized) {
|
|
2138
2259
|
console.log(
|
|
2139
2260
|
ZH
|
|
2140
2261
|
? `· 源码构建(${decision.mode === "auto" ? "自动推断:有构建声明但无产物" : "--build"}):先 ${decision.plan.install},再 ${decision.plan.command}`
|
|
@@ -2151,6 +2272,12 @@ async function installOnePlugin({ rawSpec, name, force, build, noBuild, dataDir
|
|
|
2151
2272
|
? "· 跳过构建(--no-build):目录里没有产物,装上后该插件不会被加载"
|
|
2152
2273
|
: "· Skipping build (--no-build): no artifacts in directory, plugin will not load after install",
|
|
2153
2274
|
);
|
|
2275
|
+
} else if (decision.mode === "auto") {
|
|
2276
|
+
console.log(
|
|
2277
|
+
ZH
|
|
2278
|
+
? "· 已跳过构建:目录里没有产物,装上后该插件不会被加载(授权后重装可补产物)"
|
|
2279
|
+
: "· Build skipped: no artifacts in directory, plugin will not load after install (re-install with authorization to build)",
|
|
2280
|
+
);
|
|
2154
2281
|
}
|
|
2155
2282
|
// 默认 id:子目录名 > 仓库名 > 本地目录名
|
|
2156
2283
|
const sourceName = src?.subpath ? src.subpath.split("/").pop() : (src?.repo ?? localCandidate.split(/[\\/]/).pop());
|
|
@@ -2430,6 +2557,7 @@ async function installCatalogCmd(opts) {
|
|
|
2430
2557
|
build: opts.build === true,
|
|
2431
2558
|
noBuild: opts.noBuild === true,
|
|
2432
2559
|
dataDir,
|
|
2560
|
+
isCatalog: true,
|
|
2433
2561
|
});
|
|
2434
2562
|
console.log(ZH ? `✔ ${e.id} 安装成功` : `✔ ${e.id} installed`);
|
|
2435
2563
|
okCount++;
|