pi-web-ui 0.91.0 → 0.93.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 +90 -1
- package/README.md +69 -14
- package/README.zh-CN.md +36 -10
- package/bin/pi-web-ui.mjs +39 -8
- package/dist/server/agent-service.js +938 -163
- package/dist/server/auth-cookie.js +26 -0
- package/dist/server/claim-files-tool.js +115 -0
- package/dist/server/claim-store.js +301 -0
- package/dist/server/client-state.js +114 -6
- package/dist/server/conversation-read-tool.js +371 -28
- package/dist/server/conversation-touches.js +357 -0
- package/dist/server/delegate-task.js +10 -3
- package/dist/server/dsh/dsh-agent-service.js +50 -1
- package/dist/server/files-service.js +78 -26
- package/dist/server/http-proxy.js +72 -0
- package/dist/server/index.js +94 -11
- package/dist/server/model-admin.js +124 -2
- package/dist/server/model-enrich.js +478 -0
- package/dist/server/patch-node-pty.js +11 -0
- package/dist/server/plugin-schedule.js +27 -2
- package/dist/server/plugins.js +66 -10
- package/dist/server/present-files-tool.js +303 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/read-tool.js +125 -0
- package/dist/server/resolve-global-sdk.js +72 -0
- package/dist/server/sdk-origin.js +64 -0
- package/dist/server/serialize.js +80 -22
- package/dist/server/settings-service.js +19 -1
- package/dist/server/skill-tool.js +2 -1
- package/dist/server/subagents.js +30 -13
- package/dist/server/terminals.js +38 -18
- package/dist/server/text-sniff.js +15 -0
- package/dist/server/themes.js +32 -2
- package/dist/server/tool-info.js +89 -0
- package/dist/server/tool-manager.js +110 -11
- package/package.json +2 -2
- package/plugin-sdk/README.md +1 -0
- package/plugins/catalog.json +20 -0
- package/themes/ayu-light.css +136 -0
- package/themes/catppuccin-latte.css +140 -0
- package/themes/catppuccin.css +144 -0
- package/themes/codex.css +136 -0
- package/themes/cyberpunk.css +4 -0
- package/themes/dazzle.css +4 -0
- package/themes/everforest-light.css +136 -0
- package/themes/geist.css +136 -0
- package/themes/gruvbox-light.css +136 -0
- package/themes/kanagawa-lotus.css +136 -0
- package/themes/md-preview.css +4 -0
- package/themes/mist.css +4 -0
- package/themes/nord.css +134 -0
- package/themes/one-dark.css +135 -0
- package/themes/paper.css +16 -12
- package/themes/rose-pine-dawn.css +136 -0
- package/themes/sakura.css +4 -0
- package/themes/solarized-light.css +135 -0
- package/themes/tokyo-night.css +144 -0
- package/themes/white.css +4 -0
- package/web/dist/assets/{TerminalPanel-GtDQ1h85.js → TerminalPanel-C1A23aI-.js} +1 -1
- package/web/dist/assets/index-B48QWf0I.js +364 -0
- package/web/dist/assets/index-DGL5OInG.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-CdsKltOS.css +0 -1
- package/web/dist/assets/index-OoULNF70.js +0 -363
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// conversation-touches.ts — 会话「触碰文件集」提取(纯函数,零 node 依赖)
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// 背景:同项目多对话并行时,服务端完全知道「谁碰过哪些文件」(实时消息与落盘
|
|
5
|
+
// 转录里都有工具调用记录),但既没算也没给 AI,导致 AI 只能靠猜冲突。本模块
|
|
6
|
+
// 只实现一次触碰集解析器,三处复用:并行提醒(agent-service.ts)、
|
|
7
|
+
// conversation_read 的 action:"files" 与 action:"status"。
|
|
8
|
+
//
|
|
9
|
+
// 输入形状(两条路都已核实,见下「数据来源」):
|
|
10
|
+
// - 实时路:session.agent.state.messages(SDK AgentMessage),assistant 消息
|
|
11
|
+
// 的 content 里是 {type:"toolCall", id, name, arguments} 块;
|
|
12
|
+
// - 历史路:转录 jsonl 的 message.content,原样保留同一形状(parseTranscript
|
|
13
|
+
// Lines 透传 content)。
|
|
14
|
+
// 注意:textOfContent 只在「渲染文本」里丢了参数(仍渲染 `[tool call: name]`),
|
|
15
|
+
// content 结构里的 arguments 一直都在。所以 toolCalls 字段是 additive 的便捷
|
|
16
|
+
// 访问点(一处解析、多处复用,并行 toolCall 也能表示),不是新数据源;取不到
|
|
17
|
+
// 时回落扫 content,再取不到就给空集 —— 绝不伪造。
|
|
18
|
+
//
|
|
19
|
+
// 提取规则(只算写,不算读 —— 这是准确性的关键):
|
|
20
|
+
// 写工具白名单:edit / write / edit_soft(参数 path;兼容 file_path/filePath
|
|
21
|
+
// 别名)。白名单之外的一切工具调用一律忽略,所以 read/ls/glob/grep 等读操作
|
|
22
|
+
// 天然不算 —— 不需要也不维护「读工具黑名单」。
|
|
23
|
+
// bash 高置信度写入迹象(拿不准就丢,宁可漏报不要假阳性):
|
|
24
|
+
// - 重定向 `>` / `>>` 的目标词(含 `2> file` 这类 fd 前缀;排除 `>&N`、
|
|
25
|
+
// /dev/null、NUL、&N;heredoc `<<`/`<<<` 不算写入);
|
|
26
|
+
// - rm / rmdir / mkdir / touch 的非 flag 参数;
|
|
27
|
+
// - cp / mv 只取最后一个非 flag 参数(目标端;源端是读/被删,不算写)。
|
|
28
|
+
// 解析是轻量分词器(引号/转义感知,`;`/`&&`/`||`/`|`/`&`/`(`/`)` 切分命令
|
|
29
|
+
// 段),不是完整 shell 解析器。
|
|
30
|
+
//
|
|
31
|
+
// 已知局限(如实记录,调用方展示时不要夸大):
|
|
32
|
+
// 1. 压缩(compaction)后旧消息被摘要替代,压缩前的触碰会丢失 —— 触碰集是
|
|
33
|
+
// 「当前可见转录」的下界,不是全集。
|
|
34
|
+
// 2. 相对路径按字面记录(edit 常给绝对路径、bash 常给相对路径),跨会话交集
|
|
35
|
+
// 可能因此漏报;但绝不做后缀模糊匹配(同名不同目录会假阳性,比漏报更糟)。
|
|
36
|
+
// 3. 路径归一化只做:去首尾空白、反斜杠转斜杠、去 `./` 前缀。Windows 大小写
|
|
37
|
+
// 不敏感不做归一。
|
|
38
|
+
// 4. powershell/cmd 风格写入(Set-Content/Out-File/del/copy)、git apply、
|
|
39
|
+
// tee 落盘、xargs/timeout/nice 等包装词之后的子命令 —— 一律不提取
|
|
40
|
+
// (sudo/env/command/time/nohup/doas 这几个常见包装词能跳过)。
|
|
41
|
+
// 5. 引号内的 `>`(如 echo "a > b")不提取;但 `eval`/`sh -c` 套娃串内的重定
|
|
42
|
+
// 向会被当成普通命令解析 —— 置信度下降,属已知噪声源。
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/** 工具调用的参数键:SDK 用 arguments,兼容 input/args 两种历史写法。 */
|
|
45
|
+
function argsOfBlock(blk) {
|
|
46
|
+
if (typeof blk.arguments !== "undefined")
|
|
47
|
+
return blk.arguments;
|
|
48
|
+
if (typeof blk.input !== "undefined")
|
|
49
|
+
return blk.input;
|
|
50
|
+
return blk.args;
|
|
51
|
+
}
|
|
52
|
+
/** content 块数组里工具调用的 (name, args) 对(非 toolCall 块跳过)。 */
|
|
53
|
+
export function toolCallRefsOfContent(content) {
|
|
54
|
+
if (!Array.isArray(content))
|
|
55
|
+
return [];
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const b of content) {
|
|
58
|
+
if (!b || typeof b !== "object")
|
|
59
|
+
continue;
|
|
60
|
+
const blk = b;
|
|
61
|
+
if (blk.type !== "toolCall" || typeof blk.name !== "string" || !blk.name)
|
|
62
|
+
continue;
|
|
63
|
+
const args = argsOfBlock(blk);
|
|
64
|
+
if (typeof args === "undefined")
|
|
65
|
+
out.push({ name: blk.name });
|
|
66
|
+
else
|
|
67
|
+
out.push({ name: blk.name, args });
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** 一条消息的全部工具调用:优先已解析的 toolCalls 字段,回落扫 content。 */
|
|
72
|
+
export function toolCallsOf(m) {
|
|
73
|
+
if (!m || typeof m !== "object")
|
|
74
|
+
return [];
|
|
75
|
+
if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
76
|
+
return m.toolCalls.filter((t) => !!t && typeof t === "object" && typeof t.name === "string");
|
|
77
|
+
}
|
|
78
|
+
return toolCallRefsOfContent(m.content);
|
|
79
|
+
}
|
|
80
|
+
/** 写工具白名单:只有这些工具的目标路径才算「写」。 */
|
|
81
|
+
const WRITE_TOOL_NAMES = new Set(["edit", "write", "edit_soft"]);
|
|
82
|
+
function pathOfWriteArgs(args) {
|
|
83
|
+
if (!args || typeof args !== "object")
|
|
84
|
+
return undefined;
|
|
85
|
+
const a = args;
|
|
86
|
+
for (const k of ["path", "file_path", "filePath"]) {
|
|
87
|
+
const v = a[k];
|
|
88
|
+
if (typeof v === "string" && v.trim())
|
|
89
|
+
return v.trim();
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/** 路径轻归一(见文件头局限 3):只做无损、低风险的规范化。 */
|
|
94
|
+
export function normalizeTouchPath(p) {
|
|
95
|
+
let s = p.trim().replace(/\\/g, "/");
|
|
96
|
+
while (s.startsWith("./"))
|
|
97
|
+
s = s.slice(2);
|
|
98
|
+
return s;
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// bash 命令解析(高置信度写入迹象,见文件头规则)
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
/** shell 分词:引号/反斜杠转义感知,引号剥掉(`echo "a > b"` 的 > 不会误报)。 */
|
|
104
|
+
function tokenizeShell(cmd) {
|
|
105
|
+
const toks = [];
|
|
106
|
+
let cur = "";
|
|
107
|
+
let quote = null;
|
|
108
|
+
let i = 0;
|
|
109
|
+
const push = () => {
|
|
110
|
+
if (cur !== "") {
|
|
111
|
+
toks.push(cur);
|
|
112
|
+
cur = "";
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
while (i < cmd.length) {
|
|
116
|
+
const ch = cmd[i];
|
|
117
|
+
if (quote) {
|
|
118
|
+
if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
|
|
119
|
+
cur += cmd[i + 1];
|
|
120
|
+
i += 2;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (ch === quote)
|
|
124
|
+
quote = null;
|
|
125
|
+
else
|
|
126
|
+
cur += ch;
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (ch === "\\" && i + 1 < cmd.length) {
|
|
131
|
+
cur += cmd[i + 1];
|
|
132
|
+
i += 2;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === "'" || ch === '"') {
|
|
136
|
+
quote = ch;
|
|
137
|
+
i++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (ch === " " || ch === "\t" || ch === "\r" || ch === "\n") {
|
|
141
|
+
push();
|
|
142
|
+
i++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// 多字符操作符先行:>>、<<、&&、||、2>、1>> 等。
|
|
146
|
+
const three = cmd.slice(i, i + 3);
|
|
147
|
+
const two = cmd.slice(i, i + 2);
|
|
148
|
+
if (/^\d>>/.test(three) || three === "<<<") {
|
|
149
|
+
push();
|
|
150
|
+
toks.push(three);
|
|
151
|
+
i += 3;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (two === ">>" || two === "<<" || two === "&&" || two === "||" || /^\d>$/.test(two) || /^\d</.test(two)) {
|
|
155
|
+
push();
|
|
156
|
+
toks.push(two);
|
|
157
|
+
i += 2;
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (ch === ">" || ch === "<" || ch === ";" || ch === "|" || ch === "&" || ch === "(" || ch === ")") {
|
|
161
|
+
push();
|
|
162
|
+
toks.push(ch);
|
|
163
|
+
i++;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
cur += ch;
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
push();
|
|
170
|
+
return toks;
|
|
171
|
+
}
|
|
172
|
+
const SEGMENT_SEPS = new Set([";", "&&", "||", "|", "&", "(", ")"]);
|
|
173
|
+
/** 命令段首可跳过的包装词(之后仍是命令位置)。 */
|
|
174
|
+
const WRAPPER_WORDS = new Set(["sudo", "env", "command", "time", "nohup", "doas"]);
|
|
175
|
+
const MULTI_TARGET_WRITES = new Set(["rm", "rmdir", "mkdir", "touch"]);
|
|
176
|
+
/** 只算目标端(最后一个非 flag 参数)的命令。 */
|
|
177
|
+
const DEST_ONLY_WRITES = new Set(["cp", "mv"]);
|
|
178
|
+
/** 重定向目标黑名单:不是文件。 */
|
|
179
|
+
function isNonFileTarget(t) {
|
|
180
|
+
if (t === "/dev/null" || t.toLowerCase() === "nul")
|
|
181
|
+
return true;
|
|
182
|
+
if (/^&\d*$/.test(t))
|
|
183
|
+
return true;
|
|
184
|
+
if (t === "-")
|
|
185
|
+
return true;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
/** bash 命令 → 高置信度写入路径(去重前,调用方计数)。 */
|
|
189
|
+
export function bashWriteTargets(command) {
|
|
190
|
+
if (typeof command !== "string" || !command.trim())
|
|
191
|
+
return [];
|
|
192
|
+
const toks = tokenizeShell(command);
|
|
193
|
+
const out = [];
|
|
194
|
+
// 命令段内状态:cmd = 当前命令词(null = 还在找命令词),args = 该段非 flag 参数。
|
|
195
|
+
let cmd = null;
|
|
196
|
+
let pendingMulti = [];
|
|
197
|
+
let pendingDest = null;
|
|
198
|
+
let afterDoubleDash = false;
|
|
199
|
+
let skipNext = false;
|
|
200
|
+
const flush = () => {
|
|
201
|
+
out.push(...pendingMulti);
|
|
202
|
+
if (pendingDest !== null)
|
|
203
|
+
out.push(pendingDest);
|
|
204
|
+
pendingMulti = [];
|
|
205
|
+
pendingDest = null;
|
|
206
|
+
};
|
|
207
|
+
for (let i = 0; i < toks.length; i++) {
|
|
208
|
+
const t = toks[i];
|
|
209
|
+
if (skipNext) {
|
|
210
|
+
skipNext = false;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (SEGMENT_SEPS.has(t)) {
|
|
214
|
+
flush();
|
|
215
|
+
cmd = null;
|
|
216
|
+
afterDoubleDash = false;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
// 重定向:`[fd]>[>] 目标`;heredoc 一律跳过。分隔符不能当目标吃掉
|
|
220
|
+
// (`echo a > ; rm x` 里 `;` 必须保留,否则后半段命令会错位)。
|
|
221
|
+
if (/^(\d*)(>>?)$/.test(t) || t === "<<" || t === "<<<" || /^(\d*)<<<?$/.test(t)) {
|
|
222
|
+
const next = toks[i + 1];
|
|
223
|
+
if (typeof next !== "undefined" && !SEGMENT_SEPS.has(next)) {
|
|
224
|
+
if (t.includes(">") && !isNonFileTarget(next))
|
|
225
|
+
out.push(next);
|
|
226
|
+
skipNext = true;
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (t === "--") {
|
|
231
|
+
afterDoubleDash = true;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (cmd === null) {
|
|
235
|
+
if (WRAPPER_WORDS.has(t))
|
|
236
|
+
continue;
|
|
237
|
+
if (t.includes("=") && !t.startsWith("-"))
|
|
238
|
+
continue; // VAR=val 前缀赋值
|
|
239
|
+
cmd = t;
|
|
240
|
+
if (cmd === "xargs")
|
|
241
|
+
cmd = "\0xargs-opaque"; // xargs 后的是子命令+参数,别当文件(局限 4)
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (cmd === "\0xargs-opaque")
|
|
245
|
+
continue;
|
|
246
|
+
if (!afterDoubleDash && t.startsWith("-") && t.length > 1)
|
|
247
|
+
continue; // flag
|
|
248
|
+
if (MULTI_TARGET_WRITES.has(cmd))
|
|
249
|
+
pendingMulti.push(t);
|
|
250
|
+
else if (DEST_ONLY_WRITES.has(cmd))
|
|
251
|
+
pendingDest = t; // 只留最后一个
|
|
252
|
+
}
|
|
253
|
+
flush();
|
|
254
|
+
return out.filter((p) => p !== "" && !isNonFileTarget(p));
|
|
255
|
+
}
|
|
256
|
+
/** 转录消息 → 触碰文件集(按次数降序,次数相同按最后时间降序)。 */
|
|
257
|
+
export function extractTouches(messages, opts) {
|
|
258
|
+
if (!Array.isArray(messages) || messages.length === 0)
|
|
259
|
+
return [];
|
|
260
|
+
const max = Math.max(1, Math.floor(opts?.maxMessages ?? 500));
|
|
261
|
+
const slice = messages.slice(Math.max(0, messages.length - max));
|
|
262
|
+
const acc = new Map();
|
|
263
|
+
const bump = (raw, ts) => {
|
|
264
|
+
const path = normalizeTouchPath(raw);
|
|
265
|
+
if (!path)
|
|
266
|
+
return;
|
|
267
|
+
const e = acc.get(path);
|
|
268
|
+
if (e) {
|
|
269
|
+
e.count++;
|
|
270
|
+
if (ts > e.lastTs)
|
|
271
|
+
e.lastTs = ts;
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
acc.set(path, { count: 1, lastTs: ts });
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
for (const m of slice) {
|
|
278
|
+
if (!m || typeof m !== "object")
|
|
279
|
+
continue;
|
|
280
|
+
const ts = typeof m.timestamp === "number" && Number.isFinite(m.timestamp) ? m.timestamp : 0;
|
|
281
|
+
for (const call of toolCallsOf(m)) {
|
|
282
|
+
const name = call.name;
|
|
283
|
+
if (WRITE_TOOL_NAMES.has(name)) {
|
|
284
|
+
const p = pathOfWriteArgs(call.args);
|
|
285
|
+
if (p)
|
|
286
|
+
bump(p, ts);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (name === "bash" && call.args && typeof call.args === "object") {
|
|
290
|
+
const c = call.args.command;
|
|
291
|
+
if (typeof c === "string") {
|
|
292
|
+
for (const p of bashWriteTargets(c))
|
|
293
|
+
bump(p, ts);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// `!` 命令 / bashExecution 转录消息:命令在顶层 command 字段。
|
|
298
|
+
if (m.role === "bashExecution" && typeof m.command === "string") {
|
|
299
|
+
for (const p of bashWriteTargets(m.command))
|
|
300
|
+
bump(p, ts);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return [...acc.entries()]
|
|
304
|
+
.map(([path, e]) => ({ path, count: e.count, lastTs: e.lastTs }))
|
|
305
|
+
.sort((a, b) => b.count - a.count || b.lastTs - a.lastTs);
|
|
306
|
+
}
|
|
307
|
+
/** 两个触碰集求并(sidecar 合并用):sidecar 是历史快照、live 是当前全量,
|
|
308
|
+
* 两者消息集重叠,所以 count 取大不相加 —— 相加会把同一批触碰算两遍。
|
|
309
|
+
* 压缩后次数只能保证是下界(presence 与 lastTs 精确,交集计算不受影响)。 */
|
|
310
|
+
export function unionTouchLists(a, b) {
|
|
311
|
+
const acc = new Map();
|
|
312
|
+
for (const f of [...(Array.isArray(a) ? a : []), ...(Array.isArray(b) ? b : [])]) {
|
|
313
|
+
if (!f || typeof f.path !== "string")
|
|
314
|
+
continue;
|
|
315
|
+
const e = acc.get(f.path);
|
|
316
|
+
const count = typeof f.count === "number" && Number.isFinite(f.count) ? f.count : 0;
|
|
317
|
+
const lastTs = typeof f.lastTs === "number" && Number.isFinite(f.lastTs) ? f.lastTs : 0;
|
|
318
|
+
if (e) {
|
|
319
|
+
if (count > e.count)
|
|
320
|
+
e.count = count;
|
|
321
|
+
if (lastTs > e.lastTs)
|
|
322
|
+
e.lastTs = lastTs;
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
acc.set(f.path, { count, lastTs });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return [...acc.entries()]
|
|
329
|
+
.map(([path, e]) => ({ path, count: e.count, lastTs: e.lastTs }))
|
|
330
|
+
.sort((x, y) => y.count - x.count || y.lastTs - x.lastTs);
|
|
331
|
+
}
|
|
332
|
+
/** 两个触碰集的交集:取 a 中同时出现在 b 里的条目(计数/时间沿用 a 的)。 */
|
|
333
|
+
export function intersectTouches(a, b) {
|
|
334
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || b.length === 0)
|
|
335
|
+
return [];
|
|
336
|
+
const inB = new Set(b.map((f) => f.path));
|
|
337
|
+
return a.filter((f) => inB.has(f.path));
|
|
338
|
+
}
|
|
339
|
+
/** 单条触碰格式化(供提醒用):条目超长时优先保路径完整 —— 路径永不截断
|
|
340
|
+
* (半截路径不可用,还可能误导 AI 去碰错文件),只丢 `×N` 后缀。 */
|
|
341
|
+
export function formatTouchEntry(f, perItemCap = 60) {
|
|
342
|
+
const full = f.count > 1 ? `${f.path} ×${f.count}` : f.path;
|
|
343
|
+
if (full.length <= perItemCap)
|
|
344
|
+
return full;
|
|
345
|
+
return f.path;
|
|
346
|
+
}
|
|
347
|
+
/** 触碰集紧凑格式化:前 N 条完整路径 + `… (+K)` 计数(静默丢弃是大忌)。 */
|
|
348
|
+
export function formatTouchesCompact(files, opts) {
|
|
349
|
+
if (!Array.isArray(files) || files.length === 0)
|
|
350
|
+
return "";
|
|
351
|
+
const maxItems = Math.max(1, Math.floor(opts?.maxItems ?? 3));
|
|
352
|
+
const cap = Math.max(10, Math.floor(opts?.perItemCap ?? 60));
|
|
353
|
+
const shown = files.slice(0, maxItems).map((f) => formatTouchEntry(f, cap));
|
|
354
|
+
if (files.length > maxItems)
|
|
355
|
+
shown.push(`… (+${files.length - maxItems})`);
|
|
356
|
+
return shown.join("、");
|
|
357
|
+
}
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
import { Type } from "typebox";
|
|
18
18
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { bilingual, pick } from "./i18n.js";
|
|
20
|
+
import { DELEGATE_TASK_TOOL_NAME } from "./tool-manager.js";
|
|
20
21
|
import { subagentTitle } from "./subagents.js";
|
|
21
|
-
/**
|
|
22
|
-
export const DELEGATE_TOOL_NAME =
|
|
22
|
+
/** 工具名(唯一值在 tool-manager.ts;本别名保留:前端派单卡片与旧导入沿用它)。 */
|
|
23
|
+
export const DELEGATE_TOOL_NAME = DELEGATE_TASK_TOOL_NAME;
|
|
23
24
|
/** 各段最小长度(trim 后字符数):TASK 必须具体,OUTCOME 必须可验收,其余段不许空着。 */
|
|
24
25
|
const MIN_TASK = 20;
|
|
25
26
|
const MIN_OUTCOME = 10;
|
|
@@ -40,6 +41,7 @@ export function normalizeDelegation(params) {
|
|
|
40
41
|
must_not_do: str(p.must_not_do),
|
|
41
42
|
context: str(p.context),
|
|
42
43
|
model: str(p.model).trim() || undefined,
|
|
44
|
+
persist: typeof p.persist === "boolean" ? p.persist : undefined,
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
@@ -122,6 +124,11 @@ const delegateSchema = Type.Object({
|
|
|
122
124
|
model: Type.Optional(Type.String({
|
|
123
125
|
description: bilingual('Optional model "provider/id" for this delegation; omit = template model → panel default → follow the main conversation model.', "可选:本次派单的子代理模型(provider/id);不传 = 模板模型 → 面板默认 → 跟随主对话模型。"),
|
|
124
126
|
})),
|
|
127
|
+
persist: Type.Optional(Type.Boolean({
|
|
128
|
+
description: bilingual("Optional: persist this conversation to disk as a regular session (saved in history, resumable). " +
|
|
129
|
+
"Default false (lightweight in-memory subagent). Use true for tasks that need long-term retention.", "可选:是否将该对话持久化落盘为普通对话(保存在历史会话中,可随时回顾与继续)。" +
|
|
130
|
+
"默认 false(轻量内存子代理)。需要长期留存的任务建议设为 true。"),
|
|
131
|
+
})),
|
|
125
132
|
});
|
|
126
133
|
/** 结构化派单工具:校验六段 → 拼装标准 prompt → 走 host.spawnSubagent 真子代理。 */
|
|
127
134
|
export function makeDelegateTaskTool(host, lang) {
|
|
@@ -163,7 +170,7 @@ export function makeDelegateTaskTool(host, lang) {
|
|
|
163
170
|
// 创建失败都经由 host 抛错),转成返回文本让 AI 能读到原因并修正重试。
|
|
164
171
|
let convId;
|
|
165
172
|
try {
|
|
166
|
-
convId = await host.spawnSubagent(prompt, "delegate", ctx.cwd, input.agent, input.model);
|
|
173
|
+
convId = await host.spawnSubagent(prompt, "delegate", ctx.cwd, input.agent, input.model, undefined, input.persist);
|
|
167
174
|
}
|
|
168
175
|
catch (err) {
|
|
169
176
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -29,12 +29,14 @@ import { basename, dirname, join, resolve, sep } from "node:path";
|
|
|
29
29
|
import { randomUUID } from "node:crypto";
|
|
30
30
|
import { homedir } from "node:os";
|
|
31
31
|
import { BgServerTracker } from "../bg-servers.js";
|
|
32
|
-
import { ClientStateStore, DEFAULT_RETRY_MAX_ATTEMPTS } from "../client-state.js";
|
|
32
|
+
import { ClientStateStore, DEFAULT_RETRY_MAX_ATTEMPTS, normalizeToolWatchdogTimeoutMs } from "../client-state.js";
|
|
33
33
|
import { normalizeUiLayout } from "../client-state.js";
|
|
34
34
|
import { FilesService, workspacePath, desktopDirWire } from "../files-service.js";
|
|
35
35
|
import { QuiesceRejectedError } from "../agent-service.js";
|
|
36
36
|
import { NATIVE_COMMANDS, parseSlash } from "../slash-commands.js";
|
|
37
37
|
import { bilingual, pick, resolveServerLang } from "../i18n.js";
|
|
38
|
+
// 工具定义说明的归一化(工具卡右键 → 「显示工具详细信息」,见 getToolInfo)。
|
|
39
|
+
import { normalizeToolInfo, unsupportedToolInfo } from "../tool-info.js";
|
|
38
40
|
import { TerminalManager, loadCommands, saveCommandsFile } from "../terminals.js";
|
|
39
41
|
import { saveUpload } from "../uploads.js";
|
|
40
42
|
import { checkAll as checkAllUpdates, collectTargets, resolveNpmRegistry } from "../update-check.js";
|
|
@@ -122,12 +124,14 @@ const DEFAULT_SETTINGS = {
|
|
|
122
124
|
terminalToolsEnabled: false,
|
|
123
125
|
terminalBash: false,
|
|
124
126
|
terminalBashIdleMs: 15_000,
|
|
127
|
+
toolWatchdogTimeoutMs: 20 * 60_000,
|
|
125
128
|
editSoftEnabled: false,
|
|
126
129
|
questionnaireEnabled: true,
|
|
127
130
|
goalModeEnabled: true,
|
|
128
131
|
parallelReminderEnabled: true,
|
|
129
132
|
thinkingWrap: false,
|
|
130
133
|
toolsWrap: true,
|
|
134
|
+
toolImagesEnabled: true,
|
|
131
135
|
disabledPlugins: [],
|
|
132
136
|
uiLayout: {},
|
|
133
137
|
reviewPrompt: "",
|
|
@@ -267,12 +271,14 @@ export class DshClientSession {
|
|
|
267
271
|
terminalToolsEnabled: savedSettings.terminalToolsEnabled,
|
|
268
272
|
terminalBash: savedSettings.terminalBash,
|
|
269
273
|
terminalBashIdleMs: savedSettings.terminalBashIdleMs,
|
|
274
|
+
toolWatchdogTimeoutMs: savedSettings.toolWatchdogTimeoutMs ?? 20 * 60_000,
|
|
270
275
|
editSoftEnabled: savedSettings.editSoftEnabled,
|
|
271
276
|
questionnaireEnabled: savedSettings.questionnaireEnabled ?? true,
|
|
272
277
|
goalModeEnabled: savedSettings.goalModeEnabled ?? true,
|
|
273
278
|
parallelReminderEnabled: savedSettings.parallelReminderEnabled ?? true,
|
|
274
279
|
thinkingWrap: savedSettings.thinkingWrap,
|
|
275
280
|
toolsWrap: savedSettings.toolsWrap,
|
|
281
|
+
toolImagesEnabled: savedSettings.toolImagesEnabled ?? true,
|
|
276
282
|
disabledPlugins: savedSettings.disabledPlugins ?? [],
|
|
277
283
|
uiLayout: normalizeUiLayout(savedSettings.uiLayout),
|
|
278
284
|
reviewPrompt: savedSettings.reviewPrompt,
|
|
@@ -563,16 +569,22 @@ export class DshClientSession {
|
|
|
563
569
|
// 记下待答问卷:`question_pending` 只推给「当时在线」的连接,刷新页面
|
|
564
570
|
// /WS 重连后靠快照(UiState.pendingQuestion)把对话框恢复出来。
|
|
565
571
|
// DSH 的提问桥是 runtime 级的(无 conversationId),故不分对话。
|
|
572
|
+
const dshConv = this.convs.get(this.activeId);
|
|
573
|
+
const conversationTitle = dshConv?.title;
|
|
566
574
|
this.pendingQuestion = {
|
|
567
575
|
id: params0.id,
|
|
568
576
|
...(typeof params0.deadline === "number" ? { deadline: params0.deadline } : {}),
|
|
569
577
|
questions: mapped,
|
|
578
|
+
...(this.activeId ? { conversationId: this.activeId } : {}),
|
|
579
|
+
...(conversationTitle ? { conversationTitle } : {}),
|
|
570
580
|
};
|
|
571
581
|
this.emit({
|
|
572
582
|
type: "question_pending",
|
|
573
583
|
id: params0.id,
|
|
574
584
|
...(typeof params0.deadline === "number" ? { deadline: params0.deadline } : {}),
|
|
575
585
|
questions: mapped,
|
|
586
|
+
...(this.activeId ? { conversationId: this.activeId } : {}),
|
|
587
|
+
...(conversationTitle ? { conversationTitle } : {}),
|
|
576
588
|
});
|
|
577
589
|
}
|
|
578
590
|
else if (method === "tools.call.request") {
|
|
@@ -658,6 +670,28 @@ export class DshClientSession {
|
|
|
658
670
|
console.error(`[dsh] syncPluginTools 失败 (client=${this.clientId}):`, err);
|
|
659
671
|
}
|
|
660
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* 取一条工具的**定义说明**(工具卡右键 → 「显示工具详细信息」)→ `tool_info`。
|
|
675
|
+
*
|
|
676
|
+
* DSH 侧的定义来自运行时(`tools/list`:DSH 原生工具 + 桥接进来的插件工具)。
|
|
677
|
+
* 拿不到(运行时没起来 / RPC 报错)回 `unsupported`,让前端说「当前引擎不支持」
|
|
678
|
+
* 而不是「未找到工具定义」—— 后者会让用户以为工具名写错了。
|
|
679
|
+
*/
|
|
680
|
+
async getToolInfo(name) {
|
|
681
|
+
if (!this.runtime.alive) {
|
|
682
|
+
this.emit({ type: "tool_info", ...unsupportedToolInfo(name) });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const res = await this.runtime.listTools();
|
|
687
|
+
const found = (res.tools ?? []).find((t) => t.name === name);
|
|
688
|
+
this.emit({ type: "tool_info", ...normalizeToolInfo(name, found) });
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
console.error(`[dsh] listTools 失败 (client=${this.clientId}):`, err);
|
|
692
|
+
this.emit({ type: "tool_info", ...unsupportedToolInfo(name) });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
661
695
|
/** 从运行时拉取技能清单 → 缓存 → 重推 settings_state(enabled 由 disabledSkills 推导)。 */
|
|
662
696
|
async refreshSkillsFromRuntime() {
|
|
663
697
|
if (!this.runtime.alive)
|
|
@@ -2590,6 +2624,9 @@ export class DshClientSession {
|
|
|
2590
2624
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
2591
2625
|
terminalBash: this.settings.terminalBash,
|
|
2592
2626
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2627
|
+
toolWatchdogTimeoutMs: this.settings.toolWatchdogTimeoutMs,
|
|
2628
|
+
// DSH 引擎无 customTool 注册面(工具来自 shipped preset),read 目录覆盖面不存在。
|
|
2629
|
+
readDirEnabled: true,
|
|
2593
2630
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2594
2631
|
// DSH 无独立重试配置(pi 引擎才暴露),保持默认。
|
|
2595
2632
|
retryMaxAttempts: DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
@@ -2601,6 +2638,7 @@ export class DshClientSession {
|
|
|
2601
2638
|
parallelReminderEnabled: this.settings.parallelReminderEnabled,
|
|
2602
2639
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2603
2640
|
toolsWrap: this.settings.toolsWrap,
|
|
2641
|
+
toolImagesEnabled: this.settings.toolImagesEnabled,
|
|
2604
2642
|
// DSH 无 skill 全文注入概念,给空保协议完整。
|
|
2605
2643
|
skillsFullText: [],
|
|
2606
2644
|
visionBridgeEnabled: false,
|
|
@@ -2657,6 +2695,8 @@ export class DshClientSession {
|
|
|
2657
2695
|
this.settings.terminalBash = partial.terminalBash;
|
|
2658
2696
|
if (partial.terminalBashIdleMs !== undefined)
|
|
2659
2697
|
this.settings.terminalBashIdleMs = partial.terminalBashIdleMs;
|
|
2698
|
+
if (partial.toolWatchdogTimeoutMs !== undefined)
|
|
2699
|
+
this.settings.toolWatchdogTimeoutMs = normalizeToolWatchdogTimeoutMs(partial.toolWatchdogTimeoutMs);
|
|
2660
2700
|
if (partial.editSoftEnabled !== undefined)
|
|
2661
2701
|
this.settings.editSoftEnabled = partial.editSoftEnabled;
|
|
2662
2702
|
if (partial.questionnaireEnabled !== undefined)
|
|
@@ -2669,6 +2709,8 @@ export class DshClientSession {
|
|
|
2669
2709
|
this.settings.thinkingWrap = partial.thinkingWrap;
|
|
2670
2710
|
if (partial.toolsWrap !== undefined)
|
|
2671
2711
|
this.settings.toolsWrap = partial.toolsWrap;
|
|
2712
|
+
if (partial.toolImagesEnabled !== undefined)
|
|
2713
|
+
this.settings.toolImagesEnabled = partial.toolImagesEnabled;
|
|
2672
2714
|
if (partial.disabledPlugins !== undefined)
|
|
2673
2715
|
this.settings.disabledPlugins = partial.disabledPlugins;
|
|
2674
2716
|
if (partial.uiLayout !== undefined)
|
|
@@ -2704,6 +2746,7 @@ export class DshClientSession {
|
|
|
2704
2746
|
parallelReminderEnabled: this.settings.parallelReminderEnabled,
|
|
2705
2747
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2706
2748
|
toolsWrap: this.settings.toolsWrap,
|
|
2749
|
+
toolImagesEnabled: this.settings.toolImagesEnabled,
|
|
2707
2750
|
disabledPlugins: this.settings.disabledPlugins,
|
|
2708
2751
|
uiLayout: normalizeUiLayout(this.settings.uiLayout),
|
|
2709
2752
|
reviewPrompt: this.settings.reviewPrompt,
|
|
@@ -3069,6 +3112,7 @@ export class DshClientSession {
|
|
|
3069
3112
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
3070
3113
|
thinkingWrap: this.settings.thinkingWrap,
|
|
3071
3114
|
toolsWrap: this.settings.toolsWrap,
|
|
3115
|
+
toolImagesEnabled: this.settings.toolImagesEnabled,
|
|
3072
3116
|
disabledPlugins: this.settings.disabledPlugins,
|
|
3073
3117
|
uiLayout: normalizeUiLayout(this.settings.uiLayout),
|
|
3074
3118
|
reviewPrompt: this.settings.reviewPrompt,
|
|
@@ -3880,6 +3924,11 @@ export class DshClientSession {
|
|
|
3880
3924
|
this.emit({ type: "notice", level: "error", text: error, textEn: errorEn });
|
|
3881
3925
|
this.emit({ type: "clone_provider_result", reqId, ok: false, error });
|
|
3882
3926
|
}
|
|
3927
|
+
async enrichModels(reqId, _ids, _hints) {
|
|
3928
|
+
const error = pick(this.getLang(), "DSH 引擎不支持自定义 provider", "The DSH engine does not support custom providers", "dsh.provider.custom.unsupported");
|
|
3929
|
+
this.emit({ type: "enrich_models_result", reqId, ok: false, error });
|
|
3930
|
+
}
|
|
3931
|
+
abortEnrichModels(_reqId) { }
|
|
3883
3932
|
// -----------------------------------------------------------------------
|
|
3884
3933
|
// 其他
|
|
3885
3934
|
// -----------------------------------------------------------------------
|