pi-web-ui 0.94.1 → 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 +91 -2
- package/README.md +5 -6
- package/README.zh-CN.md +4 -5
- package/bin/pi-web-ui.mjs +910 -237
- package/dist/server/agent-service.js +2655 -231
- package/dist/server/approval-rules.js +596 -0
- package/dist/server/attachment-store.js +129 -0
- package/dist/server/attachments.js +92 -174
- 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 +67 -14
- package/dist/server/compact-context-tool.js +126 -0
- package/dist/server/composer-drafts.js +9 -0
- package/dist/server/context-budget.js +317 -0
- package/dist/server/control-socket.js +57 -28
- package/dist/server/conversation-read-tool.js +7 -17
- package/dist/server/dangling-tools.js +229 -0
- package/dist/server/delegate-task.js +25 -28
- package/dist/server/dsh/dsh-agent-service.js +69 -66
- package/dist/server/edit-soft-tool.js +84 -16
- package/dist/server/eval-tool.js +588 -0
- package/dist/server/file-archives.js +16 -5
- package/dist/server/files-service.js +65 -18
- package/dist/server/goal-service.js +371 -77
- package/dist/server/hashline-engine.js +703 -0
- package/dist/server/host-guard.js +94 -0
- package/dist/server/host-metrics.js +26 -2
- package/dist/server/i18n.js +3 -3
- package/dist/server/index.js +456 -80
- package/dist/server/lsp-tool.js +1371 -0
- package/dist/server/mcp-bridge.js +47 -3
- package/dist/server/model-admin.js +112 -32
- package/dist/server/office-parse.js +375 -0
- package/dist/server/patch-tool.js +85 -0
- package/dist/server/permission-preset.js +25 -0
- package/dist/server/plan-manager.js +114 -0
- package/dist/server/plugin-api-catalog.js +296 -0
- 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-install-spec.js +196 -0
- package/dist/server/plugin-installer.js +73 -0
- package/dist/server/plugin-llm.js +71 -65
- package/dist/server/plugin-manifest-validate.js +305 -0
- package/dist/server/plugin-project.js +117 -13
- package/dist/server/plugin-tool-guard.js +120 -0
- package/dist/server/plugin-updater.js +115 -12
- package/dist/server/plugins.js +950 -205
- package/dist/server/present-files-tool.js +9 -12
- package/dist/server/process-utils.js +16 -6
- 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/scheduler-tasks.js +6 -0
- package/dist/server/sdk-origin.js +18 -2
- package/dist/server/serialize.js +111 -14
- package/dist/server/settings-service.js +112 -3
- package/dist/server/skill-tool.js +5 -7
- package/dist/server/subagent-templates.js +51 -0
- package/dist/server/subagents.js +83 -66
- package/dist/server/terminals.js +248 -81
- package/dist/server/tool-approval.js +84 -0
- package/dist/server/tool-manager.js +237 -12
- 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/workspace-snapshot.js +113 -0
- 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 +5 -3
- package/plugin-sdk/README.md +25 -0
- package/plugin-sdk/index.d.ts +31 -38
- package/plugin-sdk/index.mjs +35 -19
- package/plugins/catalog.json +40 -0
- package/themes/aetheris.css +457 -0
- package/themes/ayu-light.css +6 -6
- package/themes/catppuccin-latte.css +6 -6
- package/themes/claude-code-dark.css +144 -0
- package/themes/codex.css +6 -6
- package/themes/everforest-light.css +6 -6
- package/themes/geist.css +6 -6
- package/themes/gruvbox-light.css +6 -6
- package/themes/kanagawa-lotus.css +6 -6
- package/themes/rose-pine-dawn.css +6 -6
- package/themes/solarized-light.css +6 -6
- package/themes/vs-code-dark.css +146 -0
- package/themes/zhupi-dark.css +658 -0
- package/themes/zhupi.css +711 -0
- package/web/dist/assets/{TerminalPanel-MVoxpJOA.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-B3S9MxnN.css +0 -1
- package/web/dist/assets/index-DVLrHI2E.js +0 -364
package/dist/server/serialize.js
CHANGED
|
@@ -31,21 +31,21 @@ function truncate(s, cap) {
|
|
|
31
31
|
*/
|
|
32
32
|
function imageBlockToUi(b, cap = Number.POSITIVE_INFINITY) {
|
|
33
33
|
const img = b;
|
|
34
|
+
const src = img.source;
|
|
35
|
+
if (typeof src?.url === "string" && src.url)
|
|
36
|
+
return { type: "image", dataUrl: src.url };
|
|
34
37
|
if (typeof img.data === "string" && img.data.length > 0) {
|
|
35
38
|
const dataUrl = `data:${img.mimeType ?? "image/png"};base64,${img.data}`;
|
|
36
39
|
if (dataUrl.length > cap)
|
|
37
40
|
return undefined;
|
|
38
41
|
return { type: "image", dataUrl, mimeType: img.mimeType };
|
|
39
42
|
}
|
|
40
|
-
const src = img.source;
|
|
41
43
|
if (src?.type === "base64" && src.data) {
|
|
42
44
|
const dataUrl = `data:${src.mediaType ?? "image/png"};base64,${src.data}`;
|
|
43
45
|
if (dataUrl.length > cap)
|
|
44
46
|
return undefined;
|
|
45
47
|
return { type: "image", dataUrl, mimeType: src.mediaType };
|
|
46
48
|
}
|
|
47
|
-
if (typeof src?.url === "string" && src.url)
|
|
48
|
-
return { type: "image", dataUrl: src.url };
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
51
51
|
function serializeUserContent(content) {
|
|
@@ -65,7 +65,11 @@ function serializeAssistantContent(content) {
|
|
|
65
65
|
return { type: "text", text, truncated };
|
|
66
66
|
}
|
|
67
67
|
if (b.type === "thinking") {
|
|
68
|
-
|
|
68
|
+
// thinking 也走 TEXT_CAP:思维链没有长度保证(长任务能刷出远超正文的
|
|
69
|
+
// 体量),不截断会把快照推送撑爆。UiThinkingBlock 没有 truncated 字段
|
|
70
|
+
// (protocol 不动),截断语义靠 truncate 自带的 "… [truncated]" 尾标。
|
|
71
|
+
const { text } = truncate(b.thinking, TEXT_CAP);
|
|
72
|
+
return { type: "thinking", thinking: text };
|
|
69
73
|
}
|
|
70
74
|
if (b.type === "toolCall") {
|
|
71
75
|
if (b.arguments === undefined) {
|
|
@@ -107,18 +111,98 @@ export function stripTransientRetryErrors(messages, retryActive) {
|
|
|
107
111
|
}
|
|
108
112
|
return end === messages.length ? messages : messages.slice(0, end);
|
|
109
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Single source for rendered message ids. Both serializeMessage (下发) and
|
|
116
|
+
* resolveMessageEntry (解析) must derive ids through this function — recomputing
|
|
117
|
+
* the format anywhere else is how the two sides drifted apart and fork/rollback
|
|
118
|
+
* on assistant bubbles stopped resolving (issue #381).
|
|
119
|
+
*/
|
|
120
|
+
export function uiMessageId(m, seq) {
|
|
121
|
+
switch (m.role) {
|
|
122
|
+
case "user":
|
|
123
|
+
return `u-${m.timestamp}-${seq}`;
|
|
124
|
+
case "assistant":
|
|
125
|
+
return `a-${m.timestamp}-${seq}`;
|
|
126
|
+
case "toolResult":
|
|
127
|
+
return `t-${m.toolCallId}`;
|
|
128
|
+
case "bashExecution":
|
|
129
|
+
return `b-${m.timestamp}-${seq}`;
|
|
130
|
+
case "custom":
|
|
131
|
+
return `c-${m.timestamp}-${seq}`;
|
|
132
|
+
case "branchSummary":
|
|
133
|
+
return `bs-${m.timestamp}-${seq}`;
|
|
134
|
+
case "compactionSummary":
|
|
135
|
+
return `cs-${m.timestamp}-${seq}`;
|
|
136
|
+
default:
|
|
137
|
+
return `x-${seq}`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Find the session entry a rendered message id points at, by re-deriving each
|
|
142
|
+
* entry's rendered id through uiMessageId() — the same function serializeMessage
|
|
143
|
+
* used to hand ids to the browser. `seqOf` supplies the per-message seq; the
|
|
144
|
+
* caller injects its counter there (agent-service passes uiMessageKey().n, which
|
|
145
|
+
* is exactly the counter serialization drew from — issue #381).
|
|
146
|
+
*/
|
|
147
|
+
export function findEntryByUiId(entries, messageId, seqOf) {
|
|
148
|
+
const userSeqByTs = new Map();
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
if (entry.id === messageId)
|
|
151
|
+
return entry;
|
|
152
|
+
if (entry.type === "message" && entry.message) {
|
|
153
|
+
const m = entry.message;
|
|
154
|
+
let seq;
|
|
155
|
+
if (m.role === "user") {
|
|
156
|
+
// User ids count messages sharing a timestamp — mirrors the
|
|
157
|
+
// special-case in serializeCachedFor() (see its comment).
|
|
158
|
+
const ts = m.timestamp ?? 0;
|
|
159
|
+
seq = (userSeqByTs.get(ts) ?? 0) + 1;
|
|
160
|
+
userSeqByTs.set(ts, seq);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
seq = seqOf(m);
|
|
164
|
+
}
|
|
165
|
+
if (uiMessageId(m, seq) === messageId)
|
|
166
|
+
return entry;
|
|
167
|
+
}
|
|
168
|
+
else if (entry.type === "custom_message" && entry.display !== false) {
|
|
169
|
+
// Custom messages carry content/timestamp on the entry itself (no
|
|
170
|
+
// message object); rebuild the shape uiMessageId()/seqOf() key on —
|
|
171
|
+
// the same conversion createCustomMessage() uses on reload.
|
|
172
|
+
const m = {
|
|
173
|
+
role: "custom",
|
|
174
|
+
content: entry.content,
|
|
175
|
+
timestamp: entry.timestamp ? new Date(entry.timestamp).getTime() : 0,
|
|
176
|
+
};
|
|
177
|
+
if (uiMessageId(m, seqOf(m)) === messageId)
|
|
178
|
+
return entry;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
110
183
|
export function serializeMessage(m, seq) {
|
|
184
|
+
// SDK 的 system 消息是 prompt sections 的内部差量
|
|
185
|
+
// (content 空串 + sections 结构化内存)、compaction 的
|
|
186
|
+
// systemMessage 等——从来不面向用户。不过滤的话
|
|
187
|
+
// 会掉进 default 分支被序列化成 content: []
|
|
188
|
+
// 的空气泡、前端顶着 system 标题白显示一条
|
|
189
|
+
// (对话结束后底部冒出的空 SYSTEM 气泡就是它)。
|
|
190
|
+
// LLM 上下文不受影响——这里只决定浏览器看到什么。
|
|
191
|
+
if (m.role === "system") {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const id = uiMessageId(m, seq);
|
|
111
195
|
switch (m.role) {
|
|
112
196
|
case "user":
|
|
113
197
|
return {
|
|
114
|
-
id
|
|
198
|
+
id,
|
|
115
199
|
role: "user",
|
|
116
200
|
content: serializeUserContent(m.content),
|
|
117
201
|
timestamp: m.timestamp,
|
|
118
202
|
};
|
|
119
203
|
case "assistant":
|
|
120
204
|
return {
|
|
121
|
-
id
|
|
205
|
+
id,
|
|
122
206
|
role: "assistant",
|
|
123
207
|
content: serializeAssistantContent(m.content),
|
|
124
208
|
timestamp: m.timestamp,
|
|
@@ -152,7 +236,7 @@ export function serializeMessage(m, seq) {
|
|
|
152
236
|
const { text, truncated } = truncate(raw, TOOL_OUTPUT_CAP);
|
|
153
237
|
const content = raw || images.length === 0 ? [{ type: "text", text, truncated }, ...images] : [...images];
|
|
154
238
|
const msg = {
|
|
155
|
-
id
|
|
239
|
+
id,
|
|
156
240
|
role: "toolResult",
|
|
157
241
|
content,
|
|
158
242
|
toolCallId: m.toolCallId,
|
|
@@ -177,7 +261,7 @@ export function serializeMessage(m, seq) {
|
|
|
177
261
|
case "bashExecution": {
|
|
178
262
|
const { text, truncated } = truncate(m.output, TOOL_OUTPUT_CAP);
|
|
179
263
|
return {
|
|
180
|
-
id
|
|
264
|
+
id,
|
|
181
265
|
role: "bashExecution",
|
|
182
266
|
content: [
|
|
183
267
|
{
|
|
@@ -199,19 +283,32 @@ export function serializeMessage(m, seq) {
|
|
|
199
283
|
return null;
|
|
200
284
|
}
|
|
201
285
|
const content = serializeUserContent(m.content);
|
|
202
|
-
|
|
203
|
-
id
|
|
286
|
+
const msg = {
|
|
287
|
+
id,
|
|
204
288
|
role: "custom",
|
|
205
289
|
content,
|
|
206
290
|
customType: m.customType,
|
|
207
|
-
details: m.details,
|
|
208
291
|
timestamp: m.timestamp,
|
|
209
292
|
};
|
|
293
|
+
// custom details 与 toolResult 的 details 同一闸门(TOOL_DETAILS_CAP):
|
|
294
|
+
// details 随每 60ms 一发的快照推送,扩展塞进来的大对象不能无节制;
|
|
295
|
+
// 超限/序列化失败整丢(截断后的 JSON 不可解析,前端还得写容错)。
|
|
296
|
+
const rawDetails = m.details;
|
|
297
|
+
if (rawDetails !== undefined) {
|
|
298
|
+
try {
|
|
299
|
+
if (JSON.stringify(rawDetails).length <= TOOL_DETAILS_CAP)
|
|
300
|
+
msg.details = rawDetails;
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// 循环引用等序列化不了的值:details 是附加信息,丢掉不影响消息本体。
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return msg;
|
|
210
307
|
}
|
|
211
308
|
case "branchSummary": {
|
|
212
309
|
const { text, truncated } = truncate(m.summary, TEXT_CAP);
|
|
213
310
|
return {
|
|
214
|
-
id
|
|
311
|
+
id,
|
|
215
312
|
role: "branchSummary",
|
|
216
313
|
content: [{ type: "text", text, truncated }],
|
|
217
314
|
timestamp: m.timestamp,
|
|
@@ -220,7 +317,7 @@ export function serializeMessage(m, seq) {
|
|
|
220
317
|
case "compactionSummary": {
|
|
221
318
|
const { text, truncated } = truncate(m.summary, TEXT_CAP);
|
|
222
319
|
return {
|
|
223
|
-
id
|
|
320
|
+
id,
|
|
224
321
|
role: "compactionSummary",
|
|
225
322
|
content: [{ type: "text", text, truncated }],
|
|
226
323
|
timestamp: m.timestamp,
|
|
@@ -229,7 +326,7 @@ export function serializeMessage(m, seq) {
|
|
|
229
326
|
}
|
|
230
327
|
default:
|
|
231
328
|
return {
|
|
232
|
-
id
|
|
329
|
+
id,
|
|
233
330
|
role: String(m.role ?? "unknown"),
|
|
234
331
|
content: [],
|
|
235
332
|
timestamp: m.timestamp,
|
|
@@ -18,6 +18,7 @@ import { deriveLegacy, foldLegacyIntoDisabled, normalizeDisabledAgentTools } fro
|
|
|
18
18
|
export class SettingsService {
|
|
19
19
|
host;
|
|
20
20
|
templates;
|
|
21
|
+
approvalRules;
|
|
21
22
|
settings;
|
|
22
23
|
presets;
|
|
23
24
|
knownSkills = new Map();
|
|
@@ -26,9 +27,12 @@ export class SettingsService {
|
|
|
26
27
|
pendingReload = false;
|
|
27
28
|
constructor(host,
|
|
28
29
|
/** 全局子代理模板库(所有客户端共享;模板改动无需 reload runtime)。 */
|
|
29
|
-
templates
|
|
30
|
+
templates,
|
|
31
|
+
/** 全局审批规则库(所有客户端共享;修改实时生效无需 reload runtime)。 */
|
|
32
|
+
approvalRules) {
|
|
30
33
|
this.host = host;
|
|
31
34
|
this.templates = templates;
|
|
35
|
+
this.approvalRules = approvalRules;
|
|
32
36
|
this.settings = host.stateStore.getSettings(host.clientId);
|
|
33
37
|
this.presets = host.stateStore.getPresets(host.clientId);
|
|
34
38
|
}
|
|
@@ -289,8 +293,12 @@ export class SettingsService {
|
|
|
289
293
|
terminalToolsEnabled: legacyTools.terminalToolsEnabled,
|
|
290
294
|
terminalBash: this.settings.terminalBash,
|
|
291
295
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
296
|
+
terminalBashMaxForegroundMs: this.settings.terminalBashMaxForegroundMs,
|
|
292
297
|
toolWatchdogTimeoutMs: this.settings.toolWatchdogTimeoutMs,
|
|
293
298
|
readDirEnabled: this.settings.readDirEnabled !== false,
|
|
299
|
+
toolApprovalEnabled: this.settings.toolApprovalEnabled !== false,
|
|
300
|
+
approvalPolicy: this.host.getApprovalPolicy?.() ?? { allowAll: false, categories: [] },
|
|
301
|
+
approvalRules: this.approvalRules?.list() ?? [],
|
|
294
302
|
editSoftEnabled: legacyTools.editSoftEnabled,
|
|
295
303
|
questionnaireEnabled: legacyTools.questionnaireEnabled,
|
|
296
304
|
parallelReminderEnabled: this.settings.parallelReminderEnabled ?? true,
|
|
@@ -461,6 +469,9 @@ export class SettingsService {
|
|
|
461
469
|
if (partial.terminalBashIdleMs !== undefined) {
|
|
462
470
|
this.settings.terminalBashIdleMs = Math.max(0, Math.floor(partial.terminalBashIdleMs) || 0);
|
|
463
471
|
}
|
|
472
|
+
if (partial.terminalBashMaxForegroundMs !== undefined) {
|
|
473
|
+
this.settings.terminalBashMaxForegroundMs = Math.max(0, Math.floor(partial.terminalBashMaxForegroundMs) || 0);
|
|
474
|
+
}
|
|
464
475
|
if (partial.toolWatchdogTimeoutMs !== undefined) {
|
|
465
476
|
this.settings.toolWatchdogTimeoutMs = normalizeToolWatchdogTimeoutMs(partial.toolWatchdogTimeoutMs);
|
|
466
477
|
}
|
|
@@ -468,6 +479,10 @@ export class SettingsService {
|
|
|
468
479
|
if (partial.readDirEnabled !== undefined) {
|
|
469
480
|
this.settings.readDirEnabled = partial.readDirEnabled;
|
|
470
481
|
}
|
|
482
|
+
// 工具执行审批总开关:审批入口每次实时读取(askApproval 顶部门禁),无需 reload。
|
|
483
|
+
if (partial.toolApprovalEnabled !== undefined) {
|
|
484
|
+
this.settings.toolApprovalEnabled = partial.toolApprovalEnabled;
|
|
485
|
+
}
|
|
471
486
|
// 目标模式总开关:运行时无需重载(goal bar / 服务端入口实时读取)。
|
|
472
487
|
if (partial.goalModeEnabled !== undefined) {
|
|
473
488
|
this.settings.goalModeEnabled = partial.goalModeEnabled;
|
|
@@ -554,13 +569,15 @@ export class SettingsService {
|
|
|
554
569
|
if (partial.quickPhrasesEnabled !== undefined) {
|
|
555
570
|
this.settings.quickPhrasesEnabled = partial.quickPhrasesEnabled;
|
|
556
571
|
}
|
|
557
|
-
this.host.stateStore.saveSettings(this.host.clientId, this.settings);
|
|
558
|
-
this.push();
|
|
559
572
|
// 统一工具开关 live 生效(ActiveSet 加减;失败静默,下次创建/reload 重放)。
|
|
573
|
+
// 必须在 this.push() 前生效,保证 promptSnapshot 收集到的是最新的活跃工具集与完整提示词!
|
|
560
574
|
if (toolGatingChanged)
|
|
561
575
|
this.host.applyToolGating();
|
|
562
576
|
if (needsReload)
|
|
563
577
|
await this.applyRuntime();
|
|
578
|
+
this.host.stateStore.saveSettings(this.host.clientId, this.settings);
|
|
579
|
+
this.push();
|
|
580
|
+
this.host.flushSnapshot();
|
|
564
581
|
}
|
|
565
582
|
/** Save the CURRENT settings as a named preset (overwrites if exists). */
|
|
566
583
|
async savePreset(name) {
|
|
@@ -587,6 +604,7 @@ export class SettingsService {
|
|
|
587
604
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
588
605
|
terminalBash: this.settings.terminalBash,
|
|
589
606
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
607
|
+
terminalBashMaxForegroundMs: this.settings.terminalBashMaxForegroundMs,
|
|
590
608
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
591
609
|
retryMaxAttempts: this.settings.retryMaxAttempts,
|
|
592
610
|
softCapTokens: this.settings.softCapTokens,
|
|
@@ -636,8 +654,11 @@ export class SettingsService {
|
|
|
636
654
|
// 终端接管偏好随预设走;旧预设缺字段时保留当前值。
|
|
637
655
|
terminalBash: p.terminalBash ?? this.settings.terminalBash,
|
|
638
656
|
terminalBashIdleMs: p.terminalBashIdleMs ?? this.settings.terminalBashIdleMs,
|
|
657
|
+
terminalBashMaxForegroundMs: p.terminalBashMaxForegroundMs ?? this.settings.terminalBashMaxForegroundMs,
|
|
639
658
|
// read 读目录是纯运行行为开关,不进预设——保留当前值。
|
|
640
659
|
readDirEnabled: this.settings.readDirEnabled !== false,
|
|
660
|
+
// 工具审批总开关同样是纯运行开关,不进预设——保留当前值。
|
|
661
|
+
toolApprovalEnabled: this.settings.toolApprovalEnabled !== false,
|
|
641
662
|
// toolWatchdogTimeoutMs 是纯运行行为参数,不进预设——保留当前值。
|
|
642
663
|
toolWatchdogTimeoutMs: this.settings.toolWatchdogTimeoutMs,
|
|
643
664
|
editSoftEnabled: presetLegacy.editSoftEnabled,
|
|
@@ -725,6 +746,94 @@ export class SettingsService {
|
|
|
725
746
|
textEn: `Subagent template deleted: ${name}`,
|
|
726
747
|
});
|
|
727
748
|
}
|
|
749
|
+
/** Upsert 一条审批规则(全局共享)。 */
|
|
750
|
+
async saveApprovalRule(rule) {
|
|
751
|
+
if (!this.approvalRules)
|
|
752
|
+
return;
|
|
753
|
+
const err = this.approvalRules.upsert(rule);
|
|
754
|
+
if (err) {
|
|
755
|
+
this.host.emit({
|
|
756
|
+
type: "notice",
|
|
757
|
+
level: "error",
|
|
758
|
+
text: `审批规则保存失败:${err}`,
|
|
759
|
+
textEn: `Failed to save approval rule: ${err}`,
|
|
760
|
+
});
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
this.push();
|
|
764
|
+
this.host.emit({
|
|
765
|
+
type: "notice",
|
|
766
|
+
level: "info",
|
|
767
|
+
text: `审批规则已保存:${rule.label}`,
|
|
768
|
+
textEn: `Approval rule saved: ${rule.labelEn || rule.label}`,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
/** 批量更新审批规则列表(重排或批量保存,全局共享)。 */
|
|
772
|
+
async saveApprovalRules(rules) {
|
|
773
|
+
if (!this.approvalRules)
|
|
774
|
+
return;
|
|
775
|
+
const err = this.approvalRules.saveAll(rules);
|
|
776
|
+
if (err) {
|
|
777
|
+
this.host.emit({
|
|
778
|
+
type: "notice",
|
|
779
|
+
level: "error",
|
|
780
|
+
text: `审批规则列表保存失败:${err}`,
|
|
781
|
+
textEn: `Failed to save approval rules: ${err}`,
|
|
782
|
+
});
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
this.push();
|
|
786
|
+
this.host.emit({
|
|
787
|
+
type: "notice",
|
|
788
|
+
level: "info",
|
|
789
|
+
text: "审批规则列表已更新",
|
|
790
|
+
textEn: "Approval rules updated",
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
/** 删除一条自定义审批规则。 */
|
|
794
|
+
async deleteApprovalRule(id) {
|
|
795
|
+
if (!this.approvalRules)
|
|
796
|
+
return;
|
|
797
|
+
const ok = this.approvalRules.remove(id);
|
|
798
|
+
if (!ok) {
|
|
799
|
+
this.host.emit({
|
|
800
|
+
type: "notice",
|
|
801
|
+
level: "error",
|
|
802
|
+
text: "审批规则删除失败(内置规则不可删除)",
|
|
803
|
+
textEn: "Failed to delete approval rule (built-in rules cannot be deleted)",
|
|
804
|
+
});
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
this.push();
|
|
808
|
+
this.host.emit({
|
|
809
|
+
type: "notice",
|
|
810
|
+
level: "info",
|
|
811
|
+
text: "审批规则已删除",
|
|
812
|
+
textEn: "Approval rule deleted",
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
/** 恢复某条内置审批规则到系统默认设定。 */
|
|
816
|
+
async resetBuiltinApprovalRule(id) {
|
|
817
|
+
if (!this.approvalRules)
|
|
818
|
+
return;
|
|
819
|
+
const ok = this.approvalRules.resetBuiltin(id);
|
|
820
|
+
if (!ok) {
|
|
821
|
+
this.host.emit({
|
|
822
|
+
type: "notice",
|
|
823
|
+
level: "error",
|
|
824
|
+
text: "恢复默认失败:未找到对应内置规则",
|
|
825
|
+
textEn: "Failed to reset: built-in rule not found",
|
|
826
|
+
});
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
this.push();
|
|
830
|
+
this.host.emit({
|
|
831
|
+
type: "notice",
|
|
832
|
+
level: "info",
|
|
833
|
+
text: "内置规则已恢复默认",
|
|
834
|
+
textEn: "Built-in rule reset to default",
|
|
835
|
+
});
|
|
836
|
+
}
|
|
728
837
|
/**
|
|
729
838
|
* Make settings changes effective in the running runtime. The resource-loader
|
|
730
839
|
* overrides read this.settings at call time, so a reload re-applies them.
|
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
// 查不到被禁用的技能;
|
|
15
15
|
// - 与 SDK 同名工具撞名时 customTools 按 name 覆盖(bash 本体是先例)。
|
|
16
16
|
//
|
|
17
|
-
//
|
|
17
|
+
// 文案约定:工具 definition(description/promptSnippet/promptGuidelines)为纯英文;per-call
|
|
18
18
|
// 返回文本按 lang 取 pick(lang, zh, en, key),缺表回落英文内联。
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
import { statSync, readFileSync } from "node:fs";
|
|
21
21
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
22
22
|
import { Type } from "typebox";
|
|
23
|
-
import {
|
|
23
|
+
import { pick } from "./i18n.js";
|
|
24
24
|
import { SKILL_TOOL_NAME } from "./tool-manager.js";
|
|
25
25
|
import { decodeText } from "./text-sniff.js";
|
|
26
26
|
/** 单文件正文上限(与 agent-service fillSkillContents 同口径:8KB)。 */
|
|
@@ -61,12 +61,10 @@ export function makeSkillTool(host, lang) {
|
|
|
61
61
|
return defineTool({
|
|
62
62
|
name: SKILL_TOOL_NAME,
|
|
63
63
|
label: "Load a skill",
|
|
64
|
-
description:
|
|
64
|
+
description: "Load a skill's full text by its exact name (see the <available_skills> catalog in the system prompt). " +
|
|
65
65
|
"Prefer this over reading the skill file with the read tool — no path guessing needed. " +
|
|
66
|
-
"Call without a name to list the current catalog.",
|
|
67
|
-
|
|
68
|
-
"不给名则返回当前名录。"),
|
|
69
|
-
promptSnippet: bilingual("load a skill's full text by name (skill tool, preferred over read)", "按名加载技能全文(skill 工具,优先于 read)"),
|
|
66
|
+
"Call without a name to list the current catalog.",
|
|
67
|
+
promptSnippet: "load a skill's full text by name (skill tool, preferred over read)",
|
|
70
68
|
parameters: Type.Object({
|
|
71
69
|
name: Type.Optional(Type.String({
|
|
72
70
|
description: "Exact skill name from the catalog. Omit to list the current catalog.",
|
|
@@ -42,6 +42,53 @@ export function pickTemplatePrompt(t, lang) {
|
|
|
42
42
|
}
|
|
43
43
|
/** 名字去空白折叠后非空且 ≤ 60 字符(工具参数可读,允许中文)。 */
|
|
44
44
|
const NAME_MAX = 60;
|
|
45
|
+
/**
|
|
46
|
+
* 模板体积上限(保存期拒绝,不静默截断):模板会整体进入设置面板快照并被
|
|
47
|
+
* subagent_templates 工具列给 AI,systemPrompt 还会拼进子代理上下文,无上限
|
|
48
|
+
* 的单条模板能把快照推送与子代理首响都拖爆。对齐常见上下文预算的保守值。
|
|
49
|
+
*/
|
|
50
|
+
export const TEMPLATE_LIMITS = {
|
|
51
|
+
/** 模板系统提示词上限(systemPrompt / systemPromptEn 各自计)。 */
|
|
52
|
+
systemPrompt: 32_768,
|
|
53
|
+
/** 模板简介上限(description / descriptionEn 各自计)。 */
|
|
54
|
+
description: 2_000,
|
|
55
|
+
/** 技能/扩展白名单各自最多条目数。 */
|
|
56
|
+
whitelistEntries: 64,
|
|
57
|
+
/** 白名单单条字符上限。 */
|
|
58
|
+
whitelistEntryLength: 200,
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* 保存期体积校验:超限返回明确错误文本(让设置面板能原样提示),null = 通过。
|
|
62
|
+
* 只拦新保存,不拦读盘老数据(向后兼容,与 replace 空提示词的拦截口径一致);
|
|
63
|
+
* 拒绝而非截断——静默截断会让用户以为保存的是完整内容。
|
|
64
|
+
*/
|
|
65
|
+
export function validateTemplateLimits(t) {
|
|
66
|
+
if (t.systemPrompt.length > TEMPLATE_LIMITS.systemPrompt) {
|
|
67
|
+
return `模板 "${t.name}" 的系统提示词超长(${t.systemPrompt.length} > ${TEMPLATE_LIMITS.systemPrompt} 字符),请精简后再保存`;
|
|
68
|
+
}
|
|
69
|
+
if ((t.systemPromptEn ?? "").length > TEMPLATE_LIMITS.systemPrompt) {
|
|
70
|
+
return `模板 "${t.name}" 的英文系统提示词超长(${(t.systemPromptEn ?? "").length} > ${TEMPLATE_LIMITS.systemPrompt} 字符),请精简后再保存`;
|
|
71
|
+
}
|
|
72
|
+
if (t.description.length > TEMPLATE_LIMITS.description) {
|
|
73
|
+
return `模板 "${t.name}" 的简介超长(${t.description.length} > ${TEMPLATE_LIMITS.description} 字符),请精简后再保存`;
|
|
74
|
+
}
|
|
75
|
+
if ((t.descriptionEn ?? "").length > TEMPLATE_LIMITS.description) {
|
|
76
|
+
return `模板 "${t.name}" 的英文简介超长(${(t.descriptionEn ?? "").length} > ${TEMPLATE_LIMITS.description} 字符),请精简后再保存`;
|
|
77
|
+
}
|
|
78
|
+
for (const [label, list] of [
|
|
79
|
+
["技能白名单", t.enabledSkills],
|
|
80
|
+
["扩展白名单", t.enabledExtensions],
|
|
81
|
+
]) {
|
|
82
|
+
if (list.length > TEMPLATE_LIMITS.whitelistEntries) {
|
|
83
|
+
return `模板 "${t.name}" 的${label}条目过多(${list.length} > ${TEMPLATE_LIMITS.whitelistEntries} 条)`;
|
|
84
|
+
}
|
|
85
|
+
const over = list.find((entry) => entry.length > TEMPLATE_LIMITS.whitelistEntryLength);
|
|
86
|
+
if (over) {
|
|
87
|
+
return `模板 "${t.name}" 的${label}有超长条目(${over.length} > ${TEMPLATE_LIMITS.whitelistEntryLength} 字符)`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
45
92
|
/**
|
|
46
93
|
* 内置默认模板(第一次运行时种子进列表;用户改动后以 <dataDir> 文件为准)。
|
|
47
94
|
* 文案改编自 pi-subagents 社区项目(tintinweb / nicobailon)的角色提示词,
|
|
@@ -629,6 +676,10 @@ export class SubagentTemplatesStore {
|
|
|
629
676
|
if (t.promptMode === "replace" && !t.systemPrompt.trim() && !(t.systemPromptEn ?? "").trim()) {
|
|
630
677
|
return `模板 "${t.name}" 用了 replace 模式但系统提示词为空:replace 会整体替换子代理 persona,空提示词等于模板没生效。请填写提示词(中文或英文至少一个),只想限定技能/扩展白名单请用 append 模式`;
|
|
631
678
|
}
|
|
679
|
+
// 体积上限同样只在保存期拦:超限明确报错,绝不静默截断。
|
|
680
|
+
const limitError = validateTemplateLimits(t);
|
|
681
|
+
if (limitError)
|
|
682
|
+
return limitError;
|
|
632
683
|
const list = this.load();
|
|
633
684
|
const i = list.findIndex((x) => x.name === t.name);
|
|
634
685
|
if (i >= 0)
|