pi-web-ui 0.96.1 → 0.97.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 +48 -2
- package/README.md +26 -0
- package/README.zh-CN.md +25 -0
- package/bin/pi-web-ui.mjs +11 -2
- package/dist/server/agent-service.js +1195 -120
- package/dist/server/client-state.js +89 -1
- package/dist/server/compacted-history.js +146 -0
- package/dist/server/delegate-mode.js +149 -0
- package/dist/server/delegate-task.js +10 -10
- package/dist/server/dsh/dsh-agent-service.js +35 -2
- package/dist/server/dsh/dsh-client.js +3 -1
- package/dist/server/eval-tool.js +3 -1
- package/dist/server/goal-review-gate.js +263 -0
- package/dist/server/goal-service.js +1225 -445
- package/dist/server/index.js +35 -1
- package/dist/server/lsp-tool.js +13 -10
- package/dist/server/markers/builtins/todo.js +0 -2
- package/dist/server/mcp-bridge.js +13 -0
- package/dist/server/patch-tool.js +1 -0
- package/dist/server/patch-turn-end-boundary.js +77 -0
- package/dist/server/permission-preset.js +44 -0
- package/dist/server/plan-mode.js +376 -0
- package/dist/server/plugin-facilities.js +36 -3
- package/dist/server/plugins.js +6 -3
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/serialize.js +11 -0
- package/dist/server/settings-service.js +18 -6
- package/dist/server/subagents.js +360 -408
- package/dist/server/text-sniff.js +15 -9
- package/dist/server/tool-manager.js +48 -4
- package/dist/server/update-check.js +11 -3
- package/dist/server/wait-subscription-scan.js +5 -1
- package/package.json +3 -3
- package/themes/zhupi-dark.css +5 -30
- package/themes/zhupi.css +5 -30
- package/web/dist/assets/{TerminalPanel-O3GyFIGG.js → TerminalPanel-CITBqQrb.js} +1 -1
- package/web/dist/assets/index-DwToli0J.css +1 -0
- package/web/dist/assets/index-oI7UKOqJ.js +368 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-B7Zp23Vc.css +0 -1
- package/web/dist/assets/index-ByfoaDwp.js +0 -366
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* server/plan-mode.ts
|
|
3
|
+
*
|
|
4
|
+
* 计划模式(Plan Mode):**只规划不实施** 的纯函数闸门与判定。
|
|
5
|
+
*
|
|
6
|
+
* 形态:会话级开关(`set_plan_mode`)→ 打开后本对话只允许「只读调研」,
|
|
7
|
+
* 产出走 `plan_update` 步骤看板 + 计划正文,实施由用户确认后另起一轮。
|
|
8
|
+
*
|
|
9
|
+
* 为什么要硬闸门而不只靠提示词:模型不遵守软约束是常态(尤其长任务里
|
|
10
|
+
* 「顺手改一下」)。所以计划模式在**工具执行前**再挡一道:写类工具直接拒、
|
|
11
|
+
* bash 只放行只读命令,拒了就把原因当工具结果回给模型,让它换只读路径。
|
|
12
|
+
*
|
|
13
|
+
* 判定全部是纯函数(无 IO),单测见 tests/unit/plan-mode.test.ts。
|
|
14
|
+
*/
|
|
15
|
+
/** 计划模式下确定禁止的工具:写文件 / 改文件 / 建删 / 版本控制写。 */
|
|
16
|
+
const BLOCKED_TOOLS = new Set([
|
|
17
|
+
// 文件写入与编辑(第一方 + SDK 内置 + 编辑器类扩展的常见名)
|
|
18
|
+
"write",
|
|
19
|
+
"create",
|
|
20
|
+
"create_file",
|
|
21
|
+
"write_file",
|
|
22
|
+
"edit",
|
|
23
|
+
"edit_file",
|
|
24
|
+
"edit_soft",
|
|
25
|
+
"apply_patch",
|
|
26
|
+
"patch",
|
|
27
|
+
"patch_tool",
|
|
28
|
+
"str_replace",
|
|
29
|
+
"str_replace_editor",
|
|
30
|
+
"multi_edit",
|
|
31
|
+
"notebook_edit",
|
|
32
|
+
// 文件系统增删改
|
|
33
|
+
"mv",
|
|
34
|
+
"move",
|
|
35
|
+
"cp",
|
|
36
|
+
"copy",
|
|
37
|
+
"mkdir",
|
|
38
|
+
"rm",
|
|
39
|
+
"rmdir",
|
|
40
|
+
"delete",
|
|
41
|
+
"delete_file",
|
|
42
|
+
"touch",
|
|
43
|
+
"chmod",
|
|
44
|
+
"chown",
|
|
45
|
+
"truncate",
|
|
46
|
+
"rename",
|
|
47
|
+
// 版本控制写(git add/commit/checkout/reset…;git 的只读子命令走 bash 白名单)
|
|
48
|
+
"git",
|
|
49
|
+
// 形态变换(会重写整个文件树)
|
|
50
|
+
"format",
|
|
51
|
+
"prettier",
|
|
52
|
+
"lint_fix",
|
|
53
|
+
"autofix",
|
|
54
|
+
]);
|
|
55
|
+
/**
|
|
56
|
+
* 计划模式下也禁掉的「旁路工具」:它们能在**别的会话**里干活,而闸门是
|
|
57
|
+
* 会话级的 —— 子代理/排程对话的 planMode 是 false,等于绕过只读约束。
|
|
58
|
+
* 调研靠本对话的 read/grep/只读 bash 足够,需要并行调研时先退出计划模式。
|
|
59
|
+
*/
|
|
60
|
+
const BYPASS_TOOLS = new Set([
|
|
61
|
+
"spawn",
|
|
62
|
+
"spawn_agent",
|
|
63
|
+
"subagent_spawn",
|
|
64
|
+
"delegate_task",
|
|
65
|
+
"schedule_agent",
|
|
66
|
+
"host_schedule",
|
|
67
|
+
"create_goal",
|
|
68
|
+
"set_goal",
|
|
69
|
+
"start_goal_wizard",
|
|
70
|
+
"create_conversation",
|
|
71
|
+
"fork_conversation",
|
|
72
|
+
]);
|
|
73
|
+
/** 计划模式下从模型视野中彻底隐藏的写类工具与旁路工具全集。 */
|
|
74
|
+
export const PLAN_MODE_BLOCKED_TOOL_NAMES = new Set([...BLOCKED_TOOLS, ...BYPASS_TOOLS, "subagent"]);
|
|
75
|
+
/** bash 类工具名(terminalBash 开关分流后可能是这几个)。 */
|
|
76
|
+
const BASH_TOOLS = new Set(["bash", "bash_execute", "run_command", "shell", "terminal", "terminal_exec"]);
|
|
77
|
+
/** 计划模式放行的只读命令(basename,无参数)。刻意保守:宁可拒一次让模型换命令。 */
|
|
78
|
+
const READ_ONLY_COMMANDS = new Set([
|
|
79
|
+
// 目录 / 文件查看
|
|
80
|
+
"ls",
|
|
81
|
+
"ll",
|
|
82
|
+
"pwd",
|
|
83
|
+
"cat",
|
|
84
|
+
"bat",
|
|
85
|
+
"head",
|
|
86
|
+
"tail",
|
|
87
|
+
"wc",
|
|
88
|
+
"file",
|
|
89
|
+
"stat",
|
|
90
|
+
"du",
|
|
91
|
+
"df",
|
|
92
|
+
"tree",
|
|
93
|
+
"find",
|
|
94
|
+
"fd",
|
|
95
|
+
"realpath",
|
|
96
|
+
"readlink",
|
|
97
|
+
"basename",
|
|
98
|
+
"dirname",
|
|
99
|
+
"which",
|
|
100
|
+
"type",
|
|
101
|
+
// 检索与文本处理(只读变体;sed 的 -i 在段内另行拒绝)
|
|
102
|
+
"grep",
|
|
103
|
+
"egrep",
|
|
104
|
+
"fgrep",
|
|
105
|
+
"rg",
|
|
106
|
+
"ag",
|
|
107
|
+
"ack",
|
|
108
|
+
"sed",
|
|
109
|
+
"awk",
|
|
110
|
+
"cut",
|
|
111
|
+
"tr",
|
|
112
|
+
"sort",
|
|
113
|
+
"uniq",
|
|
114
|
+
"jq",
|
|
115
|
+
"yq",
|
|
116
|
+
"diff",
|
|
117
|
+
"cmp",
|
|
118
|
+
// 环境 / 版本
|
|
119
|
+
"env",
|
|
120
|
+
"printenv",
|
|
121
|
+
"date",
|
|
122
|
+
"uname",
|
|
123
|
+
"whoami",
|
|
124
|
+
"node",
|
|
125
|
+
"npm",
|
|
126
|
+
"python",
|
|
127
|
+
"python3",
|
|
128
|
+
// 校验和(只读)
|
|
129
|
+
"md5sum",
|
|
130
|
+
"sha256sum",
|
|
131
|
+
]);
|
|
132
|
+
/** git 的只读子命令(写子命令一律禁)。 */
|
|
133
|
+
const READ_ONLY_GIT_SUBCOMMANDS = new Set([
|
|
134
|
+
"status",
|
|
135
|
+
"diff",
|
|
136
|
+
"log",
|
|
137
|
+
"show",
|
|
138
|
+
"blame",
|
|
139
|
+
"rev-parse",
|
|
140
|
+
"rev-list",
|
|
141
|
+
"ls-files",
|
|
142
|
+
"ls-tree",
|
|
143
|
+
"describe",
|
|
144
|
+
"shortlog",
|
|
145
|
+
"grep",
|
|
146
|
+
"cat-file",
|
|
147
|
+
"whatchanged",
|
|
148
|
+
]);
|
|
149
|
+
/** 解释器的「只读例外」:只有这些参数组合放行,其余(-e/--eval/run script/子命令)拒。 */
|
|
150
|
+
const INTERPRETER_SAFE_ARGS = new Set(["--version", "-v", "-V", "--help", "-h", "--revision"]);
|
|
151
|
+
/** 段内出现即判写:重定向、命令替换、后台、heredoc。 */
|
|
152
|
+
const WRITE_SHELL_TOKENS = /[><`]|&\s*$/;
|
|
153
|
+
/** 引号感知的命令切分:把一条命令按 ; && || | 换行 拆成独立段。 */
|
|
154
|
+
function splitShellSegments(cmd) {
|
|
155
|
+
const out = [];
|
|
156
|
+
let cur = "";
|
|
157
|
+
let quote = null;
|
|
158
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
159
|
+
const ch = cmd[i];
|
|
160
|
+
if (quote) {
|
|
161
|
+
cur += ch;
|
|
162
|
+
if (ch === quote && cmd[i - 1] !== "\\")
|
|
163
|
+
quote = null;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (ch === '"' || ch === "'") {
|
|
167
|
+
quote = ch;
|
|
168
|
+
cur += ch;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
// 命令替换 $( … ) 一律视为写(能跑任意代码),整条拒。
|
|
172
|
+
if (ch === "$" && cmd[i + 1] === "(")
|
|
173
|
+
return [""];
|
|
174
|
+
if (ch === ";" || ch === "\n") {
|
|
175
|
+
out.push(cur);
|
|
176
|
+
cur = "";
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if ((ch === "&" || ch === "|") && cmd[i + 1] === (ch === "&" ? "&" : "|")) {
|
|
180
|
+
out.push(cur);
|
|
181
|
+
cur = "";
|
|
182
|
+
i++;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (ch === "|") {
|
|
186
|
+
out.push(cur);
|
|
187
|
+
cur = "";
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
cur += ch;
|
|
191
|
+
}
|
|
192
|
+
out.push(cur);
|
|
193
|
+
return out.map((s) => s.trim()).filter((s) => s !== "");
|
|
194
|
+
}
|
|
195
|
+
/** 取一段里的命令 token:跳过 `FOO=bar` 前缀、`/usr/bin/env`、路径前缀。 */
|
|
196
|
+
function commandToken(segment) {
|
|
197
|
+
const tokens = segment.split(/\s+/);
|
|
198
|
+
let i = 0;
|
|
199
|
+
while (i < tokens.length) {
|
|
200
|
+
const tok = tokens[i];
|
|
201
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(tok)) {
|
|
202
|
+
i++;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (tok === "env" || tok === "command" || tok === "nohup" || tok === "time") {
|
|
206
|
+
i++;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
return tok;
|
|
210
|
+
}
|
|
211
|
+
return "";
|
|
212
|
+
}
|
|
213
|
+
function basenameOf(token) {
|
|
214
|
+
const idx = token.lastIndexOf("/");
|
|
215
|
+
return idx >= 0 ? token.slice(idx + 1) : token;
|
|
216
|
+
}
|
|
217
|
+
/** 单段是否只读(不含管道/重定向/命令替换,命令在白名单内)。 */
|
|
218
|
+
function segmentIsReadOnly(segment) {
|
|
219
|
+
if (WRITE_SHELL_TOKENS.test(segment))
|
|
220
|
+
return false;
|
|
221
|
+
// `>` 重定向在别处已覆盖;这里补 `2>&1` 之外的 fd 复制与 `<<<`/`2>`。
|
|
222
|
+
if (/<<|&\s*\d|\d\s*>/.test(segment))
|
|
223
|
+
return false;
|
|
224
|
+
const cmd = commandToken(segment);
|
|
225
|
+
if (!cmd)
|
|
226
|
+
return false;
|
|
227
|
+
const name = basenameOf(cmd);
|
|
228
|
+
const rest = segment.slice(segment.indexOf(cmd) + cmd.length).trim();
|
|
229
|
+
const args = rest.split(/\s+/).filter(Boolean);
|
|
230
|
+
if (name === "git") {
|
|
231
|
+
const sub = args.find((a) => !a.startsWith("-"));
|
|
232
|
+
return sub !== undefined && READ_ONLY_GIT_SUBCOMMANDS.has(sub);
|
|
233
|
+
}
|
|
234
|
+
if (!READ_ONLY_COMMANDS.has(name))
|
|
235
|
+
return false;
|
|
236
|
+
// sed -i / awk 的 system() 之类写路径。
|
|
237
|
+
if (name === "sed" && args.some((a) => /^-[a-z]*i/.test(a) || a === "--in-place"))
|
|
238
|
+
return false;
|
|
239
|
+
if (name === "awk" && /system\s*\(/.test(rest))
|
|
240
|
+
return false;
|
|
241
|
+
// 解释器:只放行 --version/--help 这类无副作用调用。
|
|
242
|
+
if (name === "node" || name === "python" || name === "python3" || name === "npm") {
|
|
243
|
+
return args.length > 0 && args.every((a) => INTERPRETER_SAFE_ARGS.has(a));
|
|
244
|
+
}
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* bash 命令是否只读(纯函数)。规则:按 `;` `&&` `||` `|` 切段,**每段**都要
|
|
249
|
+
* 只读;段内禁重定向 / 命令替换 / 后台 / heredoc;命令在白名单内,
|
|
250
|
+
* git 只放只读子命令,解释器只放行 --version/--help。
|
|
251
|
+
*/
|
|
252
|
+
export function bashCommandIsReadOnly(cmd) {
|
|
253
|
+
const text = (cmd ?? "").trim();
|
|
254
|
+
if (!text)
|
|
255
|
+
return false;
|
|
256
|
+
const segments = splitShellSegments(text);
|
|
257
|
+
if (segments.length === 0)
|
|
258
|
+
return false;
|
|
259
|
+
return segments.every(segmentIsReadOnly);
|
|
260
|
+
}
|
|
261
|
+
/** 从工具参数里取命令文本(各工具字段名不同)。 */
|
|
262
|
+
function commandOf(params) {
|
|
263
|
+
if (!params || typeof params !== "object")
|
|
264
|
+
return "";
|
|
265
|
+
const p = params;
|
|
266
|
+
for (const key of ["command", "cmd", "script", "input"]) {
|
|
267
|
+
const v = p[key];
|
|
268
|
+
if (typeof v === "string" && v.trim() !== "")
|
|
269
|
+
return v;
|
|
270
|
+
}
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* 计划模式闸门(纯函数):命中即拒绝,并把可操作的替代路径告诉模型。
|
|
275
|
+
* - 写类工具 → 拒(提示:把要做的事写进计划,用户确认后另起一轮实施)
|
|
276
|
+
* - bash 非常规命令 → 拒(提示:用 read/grep/只读 git,或给出计划)
|
|
277
|
+
* - 旁路工具(spawn/delegate/目标模式/排程)→ 拒(会绕过本会话只读约束)
|
|
278
|
+
*/
|
|
279
|
+
export function planModeDenial(toolName, params) {
|
|
280
|
+
const name = (toolName ?? "").trim();
|
|
281
|
+
if (!name)
|
|
282
|
+
return undefined;
|
|
283
|
+
if (name === "subagent") {
|
|
284
|
+
const action = typeof params === "object" && params !== null ? params.action : undefined;
|
|
285
|
+
const act = typeof action === "string" ? action.trim().toLowerCase() : "";
|
|
286
|
+
if (!act || act === "spawn") {
|
|
287
|
+
return {
|
|
288
|
+
kind: "bypass-tool",
|
|
289
|
+
reason: `计划模式:不能调用 subagent(action="spawn")(它会在别的会话里执行,绕过本会话的只读约束)。请在本对话内用只读工具完成调研。`,
|
|
290
|
+
reasonEn: `Plan mode: subagent(action="spawn") is unavailable (it runs outside this conversation and would bypass the read-only constraint). Research with read-only tools here.`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (BYPASS_TOOLS.has(name)) {
|
|
295
|
+
return {
|
|
296
|
+
kind: "bypass-tool",
|
|
297
|
+
reason: `计划模式:不能调用 ${name}(它会在别的会话里执行,绕过本会话的只读约束)。请在本对话内用只读工具完成调研。`,
|
|
298
|
+
reasonEn: `Plan mode: ${name} is unavailable (it runs outside this conversation and would bypass the read-only constraint). Research with read-only tools here.`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (BASH_TOOLS.has(name)) {
|
|
302
|
+
const cmd = commandOf(params);
|
|
303
|
+
if (cmd.trim() === "") {
|
|
304
|
+
return {
|
|
305
|
+
kind: "bash",
|
|
306
|
+
reason: "计划模式:终端命令不可用。请用 read/grep/只读 git 完成调研。",
|
|
307
|
+
reasonEn: "Plan mode: shell commands are unavailable. Use read/grep and read-only git instead.",
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
if (bashCommandIsReadOnly(cmd))
|
|
311
|
+
return undefined;
|
|
312
|
+
return {
|
|
313
|
+
kind: "bash",
|
|
314
|
+
reason: `计划模式:只允许只读命令,命令被拒:\`${cmd.slice(0, 200)}\`。请改用 read/grep/git status|diff|log 等只读手段;需要做的改动写进计划,等用户确认后实施。`,
|
|
315
|
+
reasonEn: `Plan mode: only read-only commands are allowed; rejected: \`${cmd.slice(0, 200)}\`. Use read/grep or read-only git (status/diff/log). Put proposed changes in the plan and wait for the user's go-ahead.`,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (BLOCKED_TOOLS.has(name)) {
|
|
319
|
+
return {
|
|
320
|
+
kind: "write-tool",
|
|
321
|
+
reason: `计划模式:只规划不实施,${name} 被拒。请把这一步写进计划(plan_update 步骤 + 计划正文),等用户确认后在新一轮里实施。`,
|
|
322
|
+
reasonEn: `Plan mode: planning only, no implementation — ${name} was rejected. Record it as a plan step (plan_update) and wait for the user to approve before implementing.`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* 计划模式系统提示词段(英文:进入模型上下文,按工具提示词卫生口径只写英文)。
|
|
329
|
+
*
|
|
330
|
+
* 头两条是这份提示词真正的载荷:**禁代码倾倒**(模型在本模式下写不出文件,
|
|
331
|
+
* 于是会把整份实现当正文吐出来 —— 那既烧 token 又没落地,是计划模式最大的
|
|
332
|
+
* 失败模式)与 **小需求走短计划**(一句话需求不必先跑一轮 plan_update 看板)。
|
|
333
|
+
*/
|
|
334
|
+
export const PLAN_MODE_SYSTEM_PROMPT = [
|
|
335
|
+
"# Plan mode (planning only — do NOT implement)",
|
|
336
|
+
"The user turned on plan mode for this conversation. Your job in this mode is to produce a concrete, executable plan — never to make the changes.",
|
|
337
|
+
"## Hard rules",
|
|
338
|
+
"- Read-only research only: read, grep/rg, ls, and read-only bash (git status/diff/log/show, cat, wc, head, tail, jq). Anything that writes is blocked by the server and the rejection comes back as a tool error.",
|
|
339
|
+
"- Do not create, edit, move or delete files, do not run installs, builds, tests that write artifacts, and do not spawn subagents or start goal mode (those run outside this conversation).",
|
|
340
|
+
"## Output shape (this is the part that matters most)",
|
|
341
|
+
'- NEVER write the implementation. No full file bodies, no complete functions, no "here is the finished code", no working version of anything — it cannot be applied from this conversation and it wastes the user\'s tokens.',
|
|
342
|
+
"- Describe the work instead: which files to create or change, the structure (modules / functions / state), the key decisions and their reasons, the risks and unknowns, how it is verified, and the first concrete step.",
|
|
343
|
+
"- Code is allowed only as short illustrative snippets (about 20 lines each, at most two per plan) for genuinely tricky parts — a non-obvious algorithm or an API signature. No file dumps.",
|
|
344
|
+
"- Keep the whole plan short: a screen or two. Depth beats length; if a decision is still open, list it as a question instead of exploring it in prose.",
|
|
345
|
+
"## Plan board",
|
|
346
|
+
'- For work with 3+ steps or real unknowns, call plan_update early with decision-ready steps (all status "pending") and refine it as you learn more.',
|
|
347
|
+
'- For a small, unambiguous, greenfield request (e.g. "write a single-page snake game", "add a settings toggle", "rename this helper"), skip plan_update entirely and answer with the short plan: files to touch, structure, acceptance checks, first step.',
|
|
348
|
+
"## Finishing",
|
|
349
|
+
"- End with the plan and stop. Ask the user to confirm; implementation happens in the next turn after they turn plan mode off (or press the plan board's start-implementing action).",
|
|
350
|
+
"- If the request is too ambiguous to plan, ask 1-3 targeted questions instead of guessing.",
|
|
351
|
+
].join("\n");
|
|
352
|
+
/**
|
|
353
|
+
* 按设置面板偏好拼出生效的计划模式提示词(契约同 buildCommitMsgPrompt):
|
|
354
|
+
* append 把自定义文字接在内置默认之后(空自定义 = 纯默认);replace 用自定义
|
|
355
|
+
* 文字整体替换,但空自定义仍回落内置默认(绝不发空提示词)。
|
|
356
|
+
*/
|
|
357
|
+
export function buildPlanModePrompt(mode, custom) {
|
|
358
|
+
const text = custom?.trim() ?? "";
|
|
359
|
+
if (mode === "replace" && text)
|
|
360
|
+
return text;
|
|
361
|
+
if (text)
|
|
362
|
+
return `${PLAN_MODE_SYSTEM_PROMPT}\n\n${text}`;
|
|
363
|
+
return PLAN_MODE_SYSTEM_PROMPT;
|
|
364
|
+
}
|
|
365
|
+
/** 拒给用户的界面文案(notice,zh + en 成对)。 */
|
|
366
|
+
export function planModeNoticeText(enabled) {
|
|
367
|
+
return enabled
|
|
368
|
+
? {
|
|
369
|
+
text: "📋 计划模式:只调研与出计划,不实施(写操作会被服务端拒绝)",
|
|
370
|
+
textEn: "📋 Plan mode on: research and plan only — writes are blocked by the server",
|
|
371
|
+
}
|
|
372
|
+
: {
|
|
373
|
+
text: "计划模式已关闭:恢复正常对话",
|
|
374
|
+
textEn: "Plan mode off: back to normal",
|
|
375
|
+
};
|
|
376
|
+
}
|
|
@@ -303,14 +303,47 @@ export function depName(spec) {
|
|
|
303
303
|
function str(v) {
|
|
304
304
|
return typeof v === "string" ? v.trim() : "";
|
|
305
305
|
}
|
|
306
|
-
/**
|
|
306
|
+
/** 纯包名(`foo` / `@scope/foo`):能按 `node_modules/<name>/package.json` 判盘。
|
|
307
|
+
* `node:path`、URL、相对/绝对路径、git spec 一律 false(交给 resolve 判)。 */
|
|
308
|
+
function isPlainPackageName(name) {
|
|
309
|
+
return /^(?:@[a-z\d](?:[a-z\d._-]*[a-z\d])?\/)?[a-z\d](?:[a-z\d._-]*[a-z\d])?$/i.test(name);
|
|
310
|
+
}
|
|
311
|
+
/** 从 fromDir 沿目录树向上找 `node_modules/<name>/package.json` 是否已落盘。 */
|
|
312
|
+
function depOnDisk(fromDir, name) {
|
|
313
|
+
let dir = fromDir;
|
|
314
|
+
for (;;) {
|
|
315
|
+
try {
|
|
316
|
+
if (existsSync(join(dir, "node_modules", ...name.split("/"), "package.json")))
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
/* 权限/坏路径:当作这一层没有,继续往上找 */
|
|
321
|
+
}
|
|
322
|
+
const up = dirname(dir);
|
|
323
|
+
if (!up || up === dir)
|
|
324
|
+
return false;
|
|
325
|
+
dir = up;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** 从插件目录出发能否解析到这个模块(模拟插件自身 import() 的查找链)。
|
|
329
|
+
*
|
|
330
|
+
* resolve 失败后再看一眼「文件是否已落盘」,这不是冗余判据而是**修 bug**(issue #383):
|
|
331
|
+
* Node 的 CJS 解析会把 `node_modules/<pkg>/package.json` 不存在的**负结果**缓存在
|
|
332
|
+
* 进程内存里(packageJsonReader 的路径缓存)。于是「探测缺失 → npm install 成功 →
|
|
333
|
+
* 复查」这组三连里,最后一次复查永远命中那条负缓存,恒抛
|
|
334
|
+
* `Cannot find module`,用户看到的是「装成功了却报 npm install 没跑通」,
|
|
335
|
+
* 且非重启整个进程不能恢复。落盘判据不碰 CJS 缓存,天然免疫。
|
|
336
|
+
*
|
|
337
|
+
* 顺序保持「先 resolve 后判盘」:解析成功仍然是最强的信号(含 exports 映射等
|
|
338
|
+
* 盘上判据覆盖不到的形状),判盘只是给解析的假阴性兜底,不会放宽真缺失。 */
|
|
307
339
|
export function isDepAvailable(pluginDir, spec) {
|
|
340
|
+
const name = depName(spec);
|
|
308
341
|
try {
|
|
309
|
-
createRequire(join(pluginDir, "index.mjs")).resolve(
|
|
342
|
+
createRequire(join(pluginDir, "index.mjs")).resolve(name);
|
|
310
343
|
return true;
|
|
311
344
|
}
|
|
312
345
|
catch {
|
|
313
|
-
return
|
|
346
|
+
return isPlainPackageName(name) && depOnDisk(pluginDir, name);
|
|
314
347
|
}
|
|
315
348
|
}
|
|
316
349
|
const depInstallLocks = new Map();
|
package/dist/server/plugins.js
CHANGED
|
@@ -1763,11 +1763,14 @@ export class PluginManager {
|
|
|
1763
1763
|
}
|
|
1764
1764
|
return out;
|
|
1765
1765
|
}
|
|
1766
|
-
/** 当前全部插件注册的 AI 工具(扁平化,按插件 id
|
|
1766
|
+
/** 当前全部插件注册的 AI 工具(扁平化,按插件 id 稳定排序,附带所属 pluginId)。 */
|
|
1767
1767
|
getAgentTools() {
|
|
1768
1768
|
const out = [];
|
|
1769
|
-
for (const table of [...this.agentTools.
|
|
1770
|
-
|
|
1769
|
+
for (const [pid, table] of [...this.agentTools.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1770
|
+
for (const tool of table.values()) {
|
|
1771
|
+
out.push({ ...tool, pluginId: pid });
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1771
1774
|
return out;
|
|
1772
1775
|
}
|
|
1773
1776
|
/** 注册一个供 AI 调用的工具;重名拒绝并返回空操作注销函数。
|
package/dist/server/serialize.js
CHANGED
|
@@ -177,6 +177,17 @@ export function findEntryByUiId(entries, messageId, seqOf) {
|
|
|
177
177
|
if (uiMessageId(m, seqOf(m)) === messageId)
|
|
178
178
|
return entry;
|
|
179
179
|
}
|
|
180
|
+
else if (entry.type === "compaction") {
|
|
181
|
+
const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : 0;
|
|
182
|
+
const m = {
|
|
183
|
+
role: "compactionSummary",
|
|
184
|
+
summary: entry.summary ?? "",
|
|
185
|
+
tokensBefore: entry.tokensBefore,
|
|
186
|
+
timestamp: ts,
|
|
187
|
+
};
|
|
188
|
+
if (uiMessageId(m, seqOf(m)) === messageId)
|
|
189
|
+
return entry;
|
|
190
|
+
}
|
|
180
191
|
}
|
|
181
192
|
return null;
|
|
182
193
|
}
|
|
@@ -13,6 +13,7 @@ import { extensionKey, normalizeDisabledPluginTools, normalizeRetryMaxAttempts,
|
|
|
13
13
|
import { normalizeSoftCapByModel, normalizeSoftCapTokens } from "./soft-cap.js";
|
|
14
14
|
import { findVisionModels, SYSTEM_PROMPT } from "./vision-bridge.js";
|
|
15
15
|
import { COMMITMSG_SYSTEM_PROMPT } from "./scm-commitmsg.js";
|
|
16
|
+
import { PLAN_MODE_SYSTEM_PROMPT } from "./plan-mode.js";
|
|
16
17
|
import { DEFAULT_TEMPLATES } from "./subagent-templates.js";
|
|
17
18
|
import { deriveLegacy, foldLegacyIntoDisabled, normalizeDisabledAgentTools } from "./tool-manager.js";
|
|
18
19
|
export class SettingsService {
|
|
@@ -54,12 +55,6 @@ export class SettingsService {
|
|
|
54
55
|
}
|
|
55
56
|
return false;
|
|
56
57
|
}
|
|
57
|
-
get reviewPrefs() {
|
|
58
|
-
return {
|
|
59
|
-
reviewPrompt: this.settings.reviewPrompt,
|
|
60
|
-
reviewDisabledSkills: this.settings.reviewDisabledSkills,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
58
|
hasPendingReload() {
|
|
64
59
|
return this.pendingReload;
|
|
65
60
|
}
|
|
@@ -314,6 +309,8 @@ export class SettingsService {
|
|
|
314
309
|
visionBridgePrompt: this.settings.visionBridgePrompt,
|
|
315
310
|
scmCommitMsgPromptMode: this.settings.scmCommitMsgPromptMode,
|
|
316
311
|
scmCommitMsgPrompt: this.settings.scmCommitMsgPrompt,
|
|
312
|
+
planModePromptMode: this.settings.planModePromptMode,
|
|
313
|
+
planModePrompt: this.settings.planModePrompt,
|
|
317
314
|
reviewPrompt: this.settings.reviewPrompt,
|
|
318
315
|
reviewDisabledSkills: [...this.settings.reviewDisabledSkills],
|
|
319
316
|
disabledPlugins: [...(this.settings.disabledPlugins ?? [])],
|
|
@@ -327,6 +324,7 @@ export class SettingsService {
|
|
|
327
324
|
toolsSchema: promptSnap.toolsSchema,
|
|
328
325
|
visionBridgeDefaultPrompt: SYSTEM_PROMPT,
|
|
329
326
|
scmCommitMsgDefaultPrompt: COMMITMSG_SYSTEM_PROMPT,
|
|
327
|
+
planModeDefaultPrompt: PLAN_MODE_SYSTEM_PROMPT,
|
|
330
328
|
visionModels: this.collectVisionModels(),
|
|
331
329
|
disabledSkills: [...this.settings.disabledSkills],
|
|
332
330
|
disabledExtensions: [...this.settings.disabledExtensions],
|
|
@@ -404,6 +402,7 @@ export class SettingsService {
|
|
|
404
402
|
// 统一工具开关 live 生效(ActiveSet 加减,无需 reload),见末尾 applyToolGating。
|
|
405
403
|
const toolGatingChanged = partial.disabledAgentTools !== undefined ||
|
|
406
404
|
partial.disabledPluginTools !== undefined ||
|
|
405
|
+
partial.disabledPlugins !== undefined ||
|
|
407
406
|
partial.terminalToolsEnabled !== undefined ||
|
|
408
407
|
partial.editSoftEnabled !== undefined ||
|
|
409
408
|
partial.questionnaireEnabled !== undefined;
|
|
@@ -484,8 +483,12 @@ export class SettingsService {
|
|
|
484
483
|
this.settings.toolApprovalEnabled = partial.toolApprovalEnabled;
|
|
485
484
|
}
|
|
486
485
|
// 目标模式总开关:运行时无需重载(goal bar / 服务端入口实时读取)。
|
|
486
|
+
// on→off 时立即停掉在飞的目标/调研 —— 不停的话循环照跑照派,开关名存实亡。
|
|
487
487
|
if (partial.goalModeEnabled !== undefined) {
|
|
488
|
+
const wasOn = this.settings.goalModeEnabled !== false;
|
|
488
489
|
this.settings.goalModeEnabled = partial.goalModeEnabled;
|
|
490
|
+
if (wasOn && partial.goalModeEnabled === false)
|
|
491
|
+
this.host.onGoalModeDisabled?.();
|
|
489
492
|
}
|
|
490
493
|
// 同项目并行提醒开关:同上,发送入口逐轮实时读取,无需 reload。
|
|
491
494
|
if (partial.parallelReminderEnabled !== undefined) {
|
|
@@ -529,6 +532,12 @@ export class SettingsService {
|
|
|
529
532
|
if (partial.scmCommitMsgPrompt !== undefined) {
|
|
530
533
|
this.settings.scmCommitMsgPrompt = partial.scmCommitMsgPrompt;
|
|
531
534
|
}
|
|
535
|
+
if (partial.planModePromptMode !== undefined) {
|
|
536
|
+
this.settings.planModePromptMode = partial.planModePromptMode;
|
|
537
|
+
}
|
|
538
|
+
if (partial.planModePrompt !== undefined) {
|
|
539
|
+
this.settings.planModePrompt = partial.planModePrompt;
|
|
540
|
+
}
|
|
532
541
|
if (partial.reviewPrompt !== undefined) {
|
|
533
542
|
this.settings.reviewPrompt = partial.reviewPrompt;
|
|
534
543
|
}
|
|
@@ -693,6 +702,9 @@ export class SettingsService {
|
|
|
693
702
|
// 「AI 提交信息」提示词同样不进预设——保留当前值。
|
|
694
703
|
scmCommitMsgPromptMode: this.settings.scmCommitMsgPromptMode,
|
|
695
704
|
scmCommitMsgPrompt: this.settings.scmCommitMsgPrompt,
|
|
705
|
+
// 计划模式提示词同样不进预设——保留当前值。
|
|
706
|
+
planModePromptMode: this.settings.planModePromptMode,
|
|
707
|
+
planModePrompt: this.settings.planModePrompt,
|
|
696
708
|
// 子代理默认模型也不进预设——保留当前值。
|
|
697
709
|
subagentDefaultModel: this.settings.subagentDefaultModel,
|
|
698
710
|
// 快捷短语是纯 UI 偏好,不进预设——保留当前值。
|