pi-web-ui 0.97.0 → 0.98.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 +97 -1
- package/README.md +3 -3
- package/README.zh-CN.md +3 -3
- package/bin/pi-web-ui.mjs +64 -23
- package/dist/server/agent-service.js +637 -145
- package/dist/server/approval-rules.js +5 -10
- package/dist/server/atomic-file.js +40 -0
- package/dist/server/attachments.js +22 -0
- package/dist/server/claim-store.js +5 -11
- package/dist/server/client-state.js +220 -23
- package/dist/server/compact-context-tool.js +3 -1
- package/dist/server/compacted-history.js +7 -5
- package/dist/server/composer-drafts.js +4 -6
- package/dist/server/control-socket.js +0 -40
- package/dist/server/delegate-mode.js +6 -12
- package/dist/server/dsh/dsh-agent-service.js +32 -30
- package/dist/server/dsh/dsh-client.js +45 -9
- package/dist/server/dsh/dsh-serialize.js +12 -10
- package/dist/server/dsh/dsh-sessions.js +20 -6
- package/dist/server/dsh/runtime/goal-rpc.mjs +14 -1
- package/dist/server/dsh/runtime/runtime-root.mjs +1 -1
- package/dist/server/ensure-bash.js +3 -5
- package/dist/server/eval-tool.js +8 -4
- package/dist/server/goal-review-gate.js +7 -20
- package/dist/server/goal-service.js +49 -5
- package/dist/server/index.js +83 -15
- package/dist/server/launch-origin.js +0 -4
- package/dist/server/locales.js +3 -5
- package/dist/server/lsp-tool.js +110 -14
- package/dist/server/markers/builtins/notify.js +4 -2
- package/dist/server/markers/builtins/rename.js +0 -1
- package/dist/server/markers/builtins/todo.js +6 -26
- package/dist/server/markers/marker.js +0 -15
- package/dist/server/markers/registry.js +0 -3
- package/dist/server/markers/store.js +0 -3
- package/dist/server/mcp-hot-reload.js +12 -5
- package/dist/server/model-admin.js +19 -18
- package/dist/server/model-enrich.js +0 -3
- package/dist/server/patch-tool.js +21 -6
- package/dist/server/permission-preset.js +25 -0
- package/dist/server/plan-manager.js +150 -5
- package/dist/server/plan-mode.js +58 -20
- package/dist/server/plugin-api-catalog.js +2 -0
- package/dist/server/plugin-catalog.js +6 -6
- package/dist/server/plugin-dom.js +3 -4
- package/dist/server/plugin-facilities.js +3 -5
- package/dist/server/plugin-grants.js +5 -8
- package/dist/server/plugin-permissions.js +4 -6
- package/dist/server/plugin-schedule.js +4 -7
- package/dist/server/plugin-updater.js +151 -38
- package/dist/server/plugins.js +16 -2
- package/dist/server/prompt-composer.js +12 -3
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/resolve-global-sdk.js +8 -0
- package/dist/server/schedule-agent-tool.js +5 -1
- package/dist/server/scheduler-tasks.js +46 -27
- package/dist/server/sdk-origin.js +27 -0
- package/dist/server/serialize.js +22 -9
- package/dist/server/slash-commands.js +16 -0
- package/dist/server/subagent-templates.js +4 -10
- package/dist/server/subagents.js +22 -11
- package/dist/server/terminals.js +11 -6
- package/dist/server/text-quote.js +59 -0
- package/dist/server/tool-manager.js +12 -3
- package/dist/server/tool-overrides.js +24 -0
- package/dist/server/update-check.js +69 -25
- package/extensions/webui.ts +15 -1
- package/package.json +31 -19
- package/plugin-sdk/client-utils.mjs +92 -0
- package/plugin-sdk/index.d.ts +21 -0
- package/plugin-sdk/index.mjs +2 -0
- package/plugins/catalog.json +7 -0
- package/web/dist/assets/{TerminalPanel-CITBqQrb.js → TerminalPanel-C88r3r3Q.js} +1 -1
- package/web/dist/assets/index-2jDtjHvW.css +1 -0
- package/web/dist/assets/index-BjdK7s1M.js +368 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +2 -1
- package/web/public/sw.js +2 -1
- package/web/dist/assets/index-DwToli0J.css +0 -1
- package/web/dist/assets/index-oI7UKOqJ.js +0 -368
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* 判定全部是纯函数(无 IO),单测见 tests/unit/delegate-mode.test.ts。
|
|
17
17
|
*/
|
|
18
|
-
import { bashCommandIsReadOnly } from "./plan-mode.js";
|
|
18
|
+
import { bashCommandIsReadOnly, SESSION_DISPATCH_TOOLS } from "./plan-mode.js";
|
|
19
19
|
/** 审查者模式下确定禁止的工具:与计划模式同名单(写 / 改 / 增删 / git 写 / 形态变换)。 */
|
|
20
20
|
const BLOCKED_TOOLS = new Set([
|
|
21
21
|
"write",
|
|
@@ -47,6 +47,8 @@ const BLOCKED_TOOLS = new Set([
|
|
|
47
47
|
"truncate",
|
|
48
48
|
"rename",
|
|
49
49
|
"git",
|
|
50
|
+
// 代码求值(eval 沙箱可写真实文件系统 —— 主对话只审阅不施工,#436)
|
|
51
|
+
"eval",
|
|
50
52
|
"format",
|
|
51
53
|
"prettier",
|
|
52
54
|
"lint_fix",
|
|
@@ -57,18 +59,10 @@ const BLOCKED_TOOLS = new Set([
|
|
|
57
59
|
* 模型再自己 spawn 一个只会绕开「一个常驻执行对话」这条语义线(多份上下文、
|
|
58
60
|
* 多份配额、左栏一堆对话),而且落盘对话占项目会话名额。
|
|
59
61
|
* 与计划模式禁它的理由不同:那边是「别绕开只读约束」,这边是「别绕开统一派活」。
|
|
62
|
+
* 名单 = 三闸门共用的真实注册派发工具(SESSION_DISPATCH_TOOLS,#436 前是一堆
|
|
63
|
+
* 幽灵名)+ subagent 整工具(这里不分 action,审查者对话一概不派不问)。
|
|
60
64
|
*/
|
|
61
|
-
const DISPATCH_TOOLS = new Set([
|
|
62
|
-
"spawn",
|
|
63
|
-
"subagent_spawn",
|
|
64
|
-
"subagent",
|
|
65
|
-
"delegate_task",
|
|
66
|
-
"spawn_subagent",
|
|
67
|
-
"schedule",
|
|
68
|
-
"set_goal",
|
|
69
|
-
"start_goal_wizard",
|
|
70
|
-
"set_plan_mode",
|
|
71
|
-
]);
|
|
65
|
+
const DISPATCH_TOOLS = new Set([...SESSION_DISPATCH_TOOLS, "subagent"]);
|
|
72
66
|
/** bash 家族(要逐条判命令是否只读)。 */
|
|
73
67
|
const BASH_TOOLS = new Set(["bash", "shell", "sh", "zsh", "terminal", "terminal_bash", "run_command", "exec"]);
|
|
74
68
|
/** 从工具参数里取命令文本(各工具字段名不同)。 */
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
* 引擎无关模块复用:FilesService(文件树/预览)、scm.ts(git 查询)、
|
|
25
25
|
* BgServerTracker(后台任务)、TerminalManager(PTY)、uploads.ts。
|
|
26
26
|
*/
|
|
27
|
-
import { existsSync, mkdirSync, readFileSync, statSync
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
|
|
28
28
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
29
|
+
import { writeJsonAtomicSync } from "../atomic-file.js";
|
|
29
30
|
import { randomUUID } from "node:crypto";
|
|
30
31
|
import { homedir } from "node:os";
|
|
31
32
|
import { BgServerTracker } from "../bg-servers.js";
|
|
@@ -45,6 +46,7 @@ import { previewKind } from "../text-sniff.js";
|
|
|
45
46
|
import { removeQueuedByIndexOrText } from "../queue-utils.js";
|
|
46
47
|
import { launchOrigin, toServiceInfo } from "../launch-origin.js";
|
|
47
48
|
import { DshRuntime, loadDeepSeekKey } from "./dsh-client.js";
|
|
49
|
+
import { formatTextQuote, readTextQuote, messageTextWithQuotes } from "../text-quote.js";
|
|
48
50
|
import { DshStreamAccumulator, assistantMessageEventToUiMessage, toolResultEventToUiMessage, userMessageEventToUiMessage, } from "./dsh-serialize.js";
|
|
49
51
|
import { firstUserText, findSessionFilesForCwd, readSessionLog, replayEventsToMessages, sessionLogPermission, sessionLogPreset, } from "./dsh-sessions.js";
|
|
50
52
|
import { generatePresetClones, PRESET_DEFAULT_ID } from "./preset-clones.js";
|
|
@@ -902,9 +904,8 @@ export class DshClientSession {
|
|
|
902
904
|
break;
|
|
903
905
|
const msg = userMessageEventToUiMessage(ev.data);
|
|
904
906
|
// 重复文本(DSH 有时重放同一用户消息)→ 去重。
|
|
905
|
-
const text = msg
|
|
906
|
-
if (text &&
|
|
907
|
-
conv.messages.some((m) => m.role === "user" && m.content.map((c) => ("text" in c ? c.text : "")).join("") === text)) {
|
|
907
|
+
const text = messageTextWithQuotes(msg);
|
|
908
|
+
if (text && conv.messages.some((m) => m.role === "user" && messageTextWithQuotes(m) === text)) {
|
|
908
909
|
break;
|
|
909
910
|
}
|
|
910
911
|
this.appendMessage(conv, msg);
|
|
@@ -1579,7 +1580,7 @@ export class DshClientSession {
|
|
|
1579
1580
|
// 磁盘回放会话(switch_session)没有 live runtime session —— DSH 的
|
|
1580
1581
|
// JSON-RPC 面不支持恢复(id collision),自动 fork 新会话继续:把历史
|
|
1581
1582
|
// 作为上下文注入首条 prompt,前端提示。
|
|
1582
|
-
if (conv.fromDisk && text.trim()) {
|
|
1583
|
+
if (conv.fromDisk && (text.trim() || attachments?.some((a) => a.mode === "quote" && readTextQuote(a.quote)))) {
|
|
1583
1584
|
const histText = this.histToContext(conv);
|
|
1584
1585
|
conv = this.forkConversation(conv);
|
|
1585
1586
|
if (histText.trim()) {
|
|
@@ -1688,6 +1689,12 @@ export class DshClientSession {
|
|
|
1688
1689
|
: []),
|
|
1689
1690
|
],
|
|
1690
1691
|
timestamp: Date.now(),
|
|
1692
|
+
details: {
|
|
1693
|
+
quotes: (attachments ?? [])
|
|
1694
|
+
.filter((a) => a.mode === "quote")
|
|
1695
|
+
.map((a) => readTextQuote(a.quote))
|
|
1696
|
+
.filter((q) => q !== null),
|
|
1697
|
+
},
|
|
1691
1698
|
};
|
|
1692
1699
|
this.appendMessage(conv, optimistic);
|
|
1693
1700
|
this.flushSnapshot();
|
|
@@ -1721,7 +1728,7 @@ export class DshClientSession {
|
|
|
1721
1728
|
histToContext(conv) {
|
|
1722
1729
|
return conv.messages
|
|
1723
1730
|
.map((m) => {
|
|
1724
|
-
const blocks = m
|
|
1731
|
+
const blocks = messageTextWithQuotes(m);
|
|
1725
1732
|
return blocks ? `[${m.role === "assistant" ? "AI" : m.role}] ${blocks}` : "";
|
|
1726
1733
|
})
|
|
1727
1734
|
.filter(Boolean)
|
|
@@ -1756,10 +1763,7 @@ export class DshClientSession {
|
|
|
1756
1763
|
for (let i = conv.messages.length - 1; i >= 0; i--) {
|
|
1757
1764
|
const m = conv.messages[i];
|
|
1758
1765
|
if (m.role === "user") {
|
|
1759
|
-
lastUser = m.
|
|
1760
|
-
.map((c) => ("text" in c ? c.text : ""))
|
|
1761
|
-
.join("")
|
|
1762
|
-
.trim();
|
|
1766
|
+
lastUser = messageTextWithQuotes(m).trim();
|
|
1763
1767
|
if (lastUser)
|
|
1764
1768
|
break;
|
|
1765
1769
|
}
|
|
@@ -1794,6 +1798,19 @@ export class DshClientSession {
|
|
|
1794
1798
|
if (!Array.isArray(attachments))
|
|
1795
1799
|
return blocks;
|
|
1796
1800
|
for (const a of attachments) {
|
|
1801
|
+
if (a.mode === "quote") {
|
|
1802
|
+
const quote = readTextQuote(a.quote);
|
|
1803
|
+
if (quote)
|
|
1804
|
+
blocks.push({ type: "text", text: formatTextQuote(quote) });
|
|
1805
|
+
else
|
|
1806
|
+
this.emit({
|
|
1807
|
+
type: "notice",
|
|
1808
|
+
level: "warning",
|
|
1809
|
+
text: "引用内容无效,请重新选择文字",
|
|
1810
|
+
textEn: "Select the text again to add a quote.",
|
|
1811
|
+
});
|
|
1812
|
+
continue;
|
|
1813
|
+
}
|
|
1797
1814
|
const resolved = a.path ? workspacePath(this.cwd, a.path) : null;
|
|
1798
1815
|
if (a.imageData) {
|
|
1799
1816
|
// 视觉桥:base64 图片 → attachment store → 真 image 块(模型可看图)。
|
|
@@ -2306,7 +2323,7 @@ export class DshClientSession {
|
|
|
2306
2323
|
// 项目 / 文件 / SCM / 搜索
|
|
2307
2324
|
// -----------------------------------------------------------------------
|
|
2308
2325
|
async pushProjects() {
|
|
2309
|
-
const recent = this.stateStore.getRecentProjects(this.clientId);
|
|
2326
|
+
const recent = await this.stateStore.getRecentProjects(this.clientId);
|
|
2310
2327
|
const removedKeys = new Set(this.stateStore.getRemovedProjects(this.clientId).map(normalizePathKey));
|
|
2311
2328
|
const projects = new Map();
|
|
2312
2329
|
for (const p of recent) {
|
|
@@ -2580,11 +2597,6 @@ export class DshClientSession {
|
|
|
2580
2597
|
}
|
|
2581
2598
|
this.flushSnapshot(true);
|
|
2582
2599
|
}
|
|
2583
|
-
async cycleModel() {
|
|
2584
|
-
const idx = DSH_MODELS.findIndex((m) => m.id === this.model);
|
|
2585
|
-
const next = DSH_MODELS[(idx + 1) % DSH_MODELS.length];
|
|
2586
|
-
await this.setModel(next.id);
|
|
2587
|
-
}
|
|
2588
2600
|
setThinking(level) {
|
|
2589
2601
|
if (level !== "high") {
|
|
2590
2602
|
this.emit({
|
|
@@ -2598,15 +2610,6 @@ export class DshClientSession {
|
|
|
2598
2610
|
this.thinkingLevel = level;
|
|
2599
2611
|
this.flushSnapshot();
|
|
2600
2612
|
}
|
|
2601
|
-
cycleThinking() {
|
|
2602
|
-
// DSH 固定 high。
|
|
2603
|
-
this.emit({
|
|
2604
|
-
type: "notice",
|
|
2605
|
-
level: "info",
|
|
2606
|
-
text: "DeepSeek V4 仅支持高思考强度",
|
|
2607
|
-
textEn: "DeepSeek V4 only supports high thinking intensity",
|
|
2608
|
-
});
|
|
2609
|
-
}
|
|
2610
2613
|
// -----------------------------------------------------------------------
|
|
2611
2614
|
// 设置 / 系统提示词
|
|
2612
2615
|
// -----------------------------------------------------------------------
|
|
@@ -3779,8 +3782,7 @@ export class DshClientSession {
|
|
|
3779
3782
|
/* new file */
|
|
3780
3783
|
}
|
|
3781
3784
|
auth[this.normalizeDshProvider(provider)] = { type: "api_key", key };
|
|
3782
|
-
|
|
3783
|
-
writeFileSync(authPath, JSON.stringify(auth, null, 2) + "\n");
|
|
3785
|
+
writeJsonAtomicSync(authPath, auth);
|
|
3784
3786
|
this.emit({
|
|
3785
3787
|
type: "notice",
|
|
3786
3788
|
level: "info",
|
|
@@ -3810,7 +3812,7 @@ export class DshClientSession {
|
|
|
3810
3812
|
return;
|
|
3811
3813
|
}
|
|
3812
3814
|
delete auth[pid];
|
|
3813
|
-
|
|
3815
|
+
writeJsonAtomicSync(authPath, auth);
|
|
3814
3816
|
this.emit({
|
|
3815
3817
|
type: "notice",
|
|
3816
3818
|
level: "info",
|
|
@@ -3989,11 +3991,11 @@ export class DshClientSession {
|
|
|
3989
3991
|
const fresh = this.addConversation(newSessionId, this.cwd, false);
|
|
3990
3992
|
// 回放编辑点之前的消息(作为会话初始上下文:DSH 无 seed 机制,v1 用
|
|
3991
3993
|
// 简化——直接把历史作为一条提示词说明附上)。
|
|
3992
|
-
const head = conv.messages.slice(0, idx
|
|
3994
|
+
const head = conv.messages.slice(0, idx);
|
|
3993
3995
|
// 把编辑前的对话内容写进新会话的 prompt(尽力保留上下文)。
|
|
3994
3996
|
const contextNote = head
|
|
3995
3997
|
.map((m) => {
|
|
3996
|
-
const blocks = m
|
|
3998
|
+
const blocks = messageTextWithQuotes(m);
|
|
3997
3999
|
return `[${m.role}] ${blocks}`;
|
|
3998
4000
|
})
|
|
3999
4001
|
.join("\n");
|
|
@@ -78,6 +78,7 @@ export class DshRuntime {
|
|
|
78
78
|
notificationHandler = null;
|
|
79
79
|
stderrTail = "";
|
|
80
80
|
closed = false;
|
|
81
|
+
disposed = false;
|
|
81
82
|
initialized = false;
|
|
82
83
|
/** PI_WEB_DSH_DEBUG=1 时把 RPC 帧/生命周期事件打到 stderr(诊断用,默认关)。 */
|
|
83
84
|
debugEnabled = process.env.PI_WEB_DSH_DEBUG === "1";
|
|
@@ -122,6 +123,9 @@ export class DshRuntime {
|
|
|
122
123
|
}
|
|
123
124
|
/** 启动子进程 + initialize 握手(幂等;并发调用共享同一个启动任务)。 */
|
|
124
125
|
start() {
|
|
126
|
+
if (this.disposed) {
|
|
127
|
+
return Promise.reject(new DshTransportError(bilingual("client has been disposed", "客户端已销毁")));
|
|
128
|
+
}
|
|
125
129
|
if (this.alive && this.initialized)
|
|
126
130
|
return Promise.resolve();
|
|
127
131
|
if (!this.startPromise) {
|
|
@@ -270,7 +274,10 @@ export class DshRuntime {
|
|
|
270
274
|
if (!proc || !proc.stdin || proc.stdin.destroyed) {
|
|
271
275
|
throw new DshTransportError(bilingual("runtime not started", "runtime 未启动"));
|
|
272
276
|
}
|
|
273
|
-
proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
277
|
+
const ok = proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
278
|
+
if (!ok) {
|
|
279
|
+
this.debug("stdin buffer full, backpressure triggered");
|
|
280
|
+
}
|
|
274
281
|
}
|
|
275
282
|
/**
|
|
276
283
|
* 向一个会话排队 prompt。返回持久化收据 messageId(运行时接受即 resolve)。
|
|
@@ -411,8 +418,9 @@ export class DshRuntime {
|
|
|
411
418
|
}
|
|
412
419
|
/** 优雅关闭:shutdown 握手 → stdin EOF → SIGTERM → SIGKILL 阶梯。 */
|
|
413
420
|
async close() {
|
|
414
|
-
if (this.
|
|
421
|
+
if (this.disposed)
|
|
415
422
|
return;
|
|
423
|
+
this.disposed = true;
|
|
416
424
|
this.closed = true;
|
|
417
425
|
const proc = this.proc;
|
|
418
426
|
if (!proc || proc.exitCode !== null) {
|
|
@@ -487,16 +495,44 @@ export class DshRuntime {
|
|
|
487
495
|
this.failPending(new DshTransportError(bilingual("runtime killed (interrupt)", "运行时已被终止(中断)")));
|
|
488
496
|
try {
|
|
489
497
|
if (process.platform === "win32") {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
498
|
+
await new Promise((resolve) => {
|
|
499
|
+
let settled = false;
|
|
500
|
+
const done = () => {
|
|
501
|
+
if (!settled) {
|
|
502
|
+
settled = true;
|
|
503
|
+
clearTimeout(timer);
|
|
504
|
+
resolve();
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
const timer = setTimeout(done, 1500);
|
|
495
508
|
try {
|
|
496
|
-
|
|
509
|
+
const killer = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
510
|
+
stdio: "ignore",
|
|
511
|
+
windowsHide: true,
|
|
512
|
+
});
|
|
513
|
+
killer.on("close", (code) => {
|
|
514
|
+
if (code !== 0) {
|
|
515
|
+
try {
|
|
516
|
+
proc.kill("SIGKILL");
|
|
517
|
+
}
|
|
518
|
+
catch { }
|
|
519
|
+
}
|
|
520
|
+
done();
|
|
521
|
+
});
|
|
522
|
+
killer.on("error", () => {
|
|
523
|
+
try {
|
|
524
|
+
proc.kill("SIGKILL");
|
|
525
|
+
}
|
|
526
|
+
catch { }
|
|
527
|
+
done();
|
|
528
|
+
});
|
|
497
529
|
}
|
|
498
530
|
catch {
|
|
499
|
-
|
|
531
|
+
try {
|
|
532
|
+
proc.kill("SIGKILL");
|
|
533
|
+
}
|
|
534
|
+
catch { }
|
|
535
|
+
done();
|
|
500
536
|
}
|
|
501
537
|
});
|
|
502
538
|
}
|
|
@@ -23,15 +23,8 @@
|
|
|
23
23
|
* - streamingMessage:由 assistant/chunk 增量累积(reasoning→thinking、
|
|
24
24
|
* text→text、tool-call→toolCall),id 用 stream-<会话seq> 稳定跨快照。
|
|
25
25
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const ARGS_CAP = 20_000;
|
|
29
|
-
function truncate(s, cap) {
|
|
30
|
-
const str = typeof s === "string" ? s : String(s ?? "");
|
|
31
|
-
if (str.length <= cap)
|
|
32
|
-
return { text: str, truncated: false };
|
|
33
|
-
return { text: `${str.slice(0, cap)}\n\n… [truncated]`, truncated: true };
|
|
34
|
-
}
|
|
26
|
+
import { parseTextQuote } from "../text-quote.js";
|
|
27
|
+
import { ARGS_CAP, TEXT_CAP, TOOL_OUTPUT_CAP, truncate } from "../serialize.js";
|
|
35
28
|
/** DSH user-role content blocks → UiContentBlock[]。 */
|
|
36
29
|
export function serializeUserBlocks(blocks) {
|
|
37
30
|
if (!Array.isArray(blocks) || blocks.length === 0) {
|
|
@@ -102,10 +95,19 @@ export function toolResultEventToUiMessage(data) {
|
|
|
102
95
|
}
|
|
103
96
|
/** user/message 事件 data → UiMessage。 */
|
|
104
97
|
export function userMessageEventToUiMessage(data) {
|
|
98
|
+
const quotes = [];
|
|
99
|
+
const content = (data.content ?? []).filter((block) => {
|
|
100
|
+
const quote = block.type === "text" ? parseTextQuote(block.text ?? "") : null;
|
|
101
|
+
if (!quote)
|
|
102
|
+
return true;
|
|
103
|
+
quotes.push(quote);
|
|
104
|
+
return false;
|
|
105
|
+
});
|
|
105
106
|
return {
|
|
106
107
|
id: `u-${data.id ?? "?"}`,
|
|
107
108
|
role: "user",
|
|
108
|
-
content: serializeUserBlocks(
|
|
109
|
+
content: serializeUserBlocks(content),
|
|
110
|
+
...(quotes.length ? { details: { quotes } } : {}),
|
|
109
111
|
timestamp: data.time,
|
|
110
112
|
};
|
|
111
113
|
}
|
|
@@ -84,14 +84,28 @@ function zstdDecompressAll(buf) {
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
let out = "";
|
|
87
|
-
|
|
87
|
+
let i = 0;
|
|
88
|
+
while (i < starts.length) {
|
|
88
89
|
const from = starts[i];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
let nextIdx = i + 1;
|
|
91
|
+
let decompressed = null;
|
|
92
|
+
// 贪心尝试:若数据体内部误命中魔数导致切帧错误,向后试探合并到下一个魔数直至解压成功(issue #467)
|
|
93
|
+
while (nextIdx <= starts.length) {
|
|
94
|
+
const to = nextIdx < starts.length ? starts[nextIdx] : buf.length;
|
|
95
|
+
try {
|
|
96
|
+
decompressed = zstdDecompressSync(buf.subarray(from, to));
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
nextIdx++;
|
|
101
|
+
}
|
|
92
102
|
}
|
|
93
|
-
|
|
94
|
-
|
|
103
|
+
if (decompressed) {
|
|
104
|
+
out += decompressed.toString("utf8");
|
|
105
|
+
i = nextIdx;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
i++;
|
|
95
109
|
}
|
|
96
110
|
}
|
|
97
111
|
return out || buf.toString("utf8");
|
|
@@ -870,10 +870,19 @@ class DshGoalJsonRpcServer extends HarnessSdkJsonRpcServer {
|
|
|
870
870
|
return new Promise((resolve, reject) => {
|
|
871
871
|
const timer = setTimeout(() => {
|
|
872
872
|
const queued = this.questionBridge.queue.findIndex((q) => q.qid === qid);
|
|
873
|
-
if (queued >= 0)
|
|
873
|
+
if (queued >= 0) {
|
|
874
|
+
this.questionBridge.queue.splice(queued, 1);
|
|
875
|
+
// #459:排队条目超时也必须 settle——此前只 splice 不 reject,
|
|
876
|
+
// 该 ask 的 Promise 永不落定,agent-loop 卡死在工具调用上。
|
|
877
|
+
reject(new Error("提问超时(等待回答过久)"));
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
874
880
|
if (this.questionBridge.pending?.qid === qid) {
|
|
875
881
|
this.questionBridge.pending = null;
|
|
876
882
|
reject(new Error("提问超时(等待回答过久)"));
|
|
883
|
+
// #459:pending 腾出后必须派发下一个排队提问——否则队列无人派发,
|
|
884
|
+
// 后续条目只能等各自超时且永不 settle,整条提问桥永久停摆。
|
|
885
|
+
this.dispatchNextQuestion();
|
|
877
886
|
}
|
|
878
887
|
}, QUESTION_TIMEOUT_MS);
|
|
879
888
|
timer.unref?.();
|
|
@@ -939,6 +948,10 @@ class DshGoalJsonRpcServer extends HarnessSdkJsonRpcServer {
|
|
|
939
948
|
}
|
|
940
949
|
const bytes = new Uint8Array(Buffer.from(params.data, "base64"));
|
|
941
950
|
if (bytes.length === 0) throw new TypeError("attachment/save: empty image data");
|
|
951
|
+
const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024; // 20MB 上限
|
|
952
|
+
if (bytes.length > MAX_ATTACHMENT_BYTES) {
|
|
953
|
+
throw new RangeError(`attachment/save: image data exceeds 20MB limit (${bytes.length} bytes)`);
|
|
954
|
+
}
|
|
942
955
|
const ref = await this.ctx.attachments.saveImage({
|
|
943
956
|
data: bytes,
|
|
944
957
|
mediaType,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Shared runtime-tree resolution for the pi-web-ui DSH runtime.
|
|
2
|
-
// Used by launcher.mjs (boot-time) and probe-mixed.mjs (self-check).
|
|
2
|
+
// Used by launcher.mjs (boot-time) and dev/dsh-probes/probe-mixed.mjs (self-check).
|
|
3
3
|
//
|
|
4
4
|
// A "runtime tree" is a node_modules root that directly contains the dsh
|
|
5
5
|
// runtime packages: `@deepseek-ai/dsh-base` (with its cordis.patch.yml
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* Download is fire-and-forget at server start and never throws: on failure
|
|
12
12
|
* the terminal simply falls back to $COMSPEC (cmd.exe) as before.
|
|
13
13
|
*/
|
|
14
|
-
import { copyFileSync, existsSync, mkdirSync
|
|
14
|
+
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
|
+
import { writeAtomicSync } from "./atomic-file.js";
|
|
17
18
|
/** Official busybox-w32 64-bit Unicode build (Win10 1903+ / Win11). */
|
|
18
19
|
const BUSYBOX_URL = "https://frippery.org/files/busybox/busybox64u.exe";
|
|
19
20
|
/** busybox.exe is ~660KB; anything far smaller is an error page, not a binary. */
|
|
@@ -55,7 +56,6 @@ export async function ensureWindowsBash() {
|
|
|
55
56
|
return null; // Git Bash preferred — nothing to install.
|
|
56
57
|
const dir = windowsBashDir();
|
|
57
58
|
mkdirSync(dir, { recursive: true });
|
|
58
|
-
const tmp = join(dir, `busybox-${process.pid}.tmp`);
|
|
59
59
|
try {
|
|
60
60
|
const res = await fetch(BUSYBOX_URL, {
|
|
61
61
|
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
@@ -65,13 +65,11 @@ export async function ensureWindowsBash() {
|
|
|
65
65
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
66
66
|
if (buf.length < MIN_SIZE)
|
|
67
67
|
return null; // error/HTML page, not the exe.
|
|
68
|
-
|
|
69
|
-
renameSync(tmp, join(dir, "busybox.exe"));
|
|
68
|
+
writeAtomicSync(join(dir, "busybox.exe"), buf);
|
|
70
69
|
copyFileSync(join(dir, "busybox.exe"), target);
|
|
71
70
|
return target;
|
|
72
71
|
}
|
|
73
72
|
catch {
|
|
74
|
-
rmSync(tmp, { force: true });
|
|
75
73
|
return null;
|
|
76
74
|
}
|
|
77
75
|
}
|
package/dist/server/eval-tool.js
CHANGED
|
@@ -287,8 +287,9 @@ class EvalKernel {
|
|
|
287
287
|
this.proc = spawn(pyCmd, ["-u", "-c", PYTHON_DRIVER_SCRIPT], spawnOpts);
|
|
288
288
|
}
|
|
289
289
|
else {
|
|
290
|
-
// js / ts:独立 node -e
|
|
291
|
-
|
|
290
|
+
// js / ts:独立 node -e 驱动,ts 时带 --experimental-strip-types 支持 TypeScript 原生剥离执行。
|
|
291
|
+
const nodeArgs = this.language === "ts" ? ["--experimental-strip-types", "-e", NODE_DRIVER_SCRIPT] : ["-e", NODE_DRIVER_SCRIPT];
|
|
292
|
+
this.proc = spawn(process.execPath, nodeArgs, spawnOpts);
|
|
292
293
|
}
|
|
293
294
|
// stderr 必须持续排空:驱动只把用户的 print/console 劫进 StringIO,但
|
|
294
295
|
// os.write(2, …) / 原生扩展 / eval 里再 spawn 的子进程仍写真实 fd 2。
|
|
@@ -569,8 +570,11 @@ export function makeEvalTool(opts) {
|
|
|
569
570
|
}
|
|
570
571
|
catch (err) {
|
|
571
572
|
const durationMs = Date.now() - startTime;
|
|
572
|
-
|
|
573
|
-
|
|
573
|
+
let errMsg = err.message;
|
|
574
|
+
if (errMsg.includes("Python executable not found in PATH")) {
|
|
575
|
+
errMsg = pick(lang, "未在系统 PATH 中找到 Python 可执行程序,请安装 Python 或使用 language: 'js'。", "Python executable not found in PATH. Please install Python or use language: 'js'.");
|
|
576
|
+
}
|
|
577
|
+
const text = `${pick(lang, `[eval:${language} 出错 (${durationMs}ms)]`, `[eval:${language} error (${durationMs}ms)]`)}\n${errMsg}`;
|
|
574
578
|
return {
|
|
575
579
|
content: [{ type: "text", text }],
|
|
576
580
|
details: {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* 同样被包裹;但之后经 refreshPluginTools 动态补入已有会话的不在覆盖面。
|
|
17
17
|
* 自研插件若绕过 wrapper 直接写文件同样拦不住 —— 只读是硬闸门,不是安全边界。
|
|
18
18
|
*/
|
|
19
|
-
import { bashCommandIsReadOnly } from "./plan-mode.js";
|
|
19
|
+
import { bashCommandIsReadOnly, SESSION_DISPATCH_TOOLS } from "./plan-mode.js";
|
|
20
20
|
/** 审查回合禁写的工具:与计划/审查者模式同名单(写 / 改 / 增删 / git 写 / 形态变换)。 */
|
|
21
21
|
const WRITE_TOOLS = new Set([
|
|
22
22
|
// 文件写入与编辑(第一方 + SDK 内置 + 编辑器类扩展的常见名)
|
|
@@ -51,6 +51,8 @@ const WRITE_TOOLS = new Set([
|
|
|
51
51
|
"rename",
|
|
52
52
|
// 版本控制写(git add/commit/checkout/reset…;只读子命令走 bash 白名单)
|
|
53
53
|
"git",
|
|
54
|
+
// 代码求值(eval 沙箱可写真实文件系统 —— 审查回合只读核实,#436)
|
|
55
|
+
"eval",
|
|
54
56
|
// 形态变换(会重写整个文件树)
|
|
55
57
|
"format",
|
|
56
58
|
"prettier",
|
|
@@ -60,27 +62,12 @@ const WRITE_TOOLS = new Set([
|
|
|
60
62
|
/**
|
|
61
63
|
* 审查回合禁掉的「派发类」工具:轮次由服务端控制(D6)—— 派生 / 等待 / 指挥
|
|
62
64
|
* 别的对话、排程、改模式 / 开新目标都会绕开「一轮 = 派活 + 判定」的口径。
|
|
63
|
-
*
|
|
65
|
+
* 名单 = 三闸门共用的真实注册派发工具(SESSION_DISPATCH_TOOLS,#436 前是一堆
|
|
66
|
+
* 幽灵名)。subagent 不在此列:上面的按 action 判定已拦掉全部写向 action,
|
|
67
|
+
* 只读的 `conversation_read` / subagent get_result/list/templates 放行 ——
|
|
64
68
|
* 审查者可以按需看执行者的转录(契约要求看实际状态,而不是信描述)。
|
|
65
69
|
*/
|
|
66
|
-
const DISPATCH_TOOLS =
|
|
67
|
-
"spawn",
|
|
68
|
-
"spawn_agent",
|
|
69
|
-
"subagent_spawn",
|
|
70
|
-
"spawn_subagent",
|
|
71
|
-
"subagent_steer",
|
|
72
|
-
"subagent_stop",
|
|
73
|
-
"subagent_wait_all",
|
|
74
|
-
"subagent_handoff",
|
|
75
|
-
"delegate_task",
|
|
76
|
-
"schedule",
|
|
77
|
-
"schedule_agent",
|
|
78
|
-
"host_schedule",
|
|
79
|
-
"create_goal",
|
|
80
|
-
"set_goal",
|
|
81
|
-
"start_goal_wizard",
|
|
82
|
-
"set_plan_mode",
|
|
83
|
-
]);
|
|
70
|
+
const DISPATCH_TOOLS = SESSION_DISPATCH_TOOLS;
|
|
84
71
|
/** bash 家族(要逐条判命令是否只读;与审查者模式同名单)。 */
|
|
85
72
|
const BASH_TOOLS = new Set(["bash", "shell", "sh", "zsh", "terminal", "terminal_bash", "run_command", "exec"]);
|
|
86
73
|
/** 引号感知的命令切分(按 | ; && || 换行拆段;含 $( / 反引号时整条视为一段交白名单判
|
|
@@ -817,10 +817,15 @@ export class GoalService {
|
|
|
817
817
|
aborted = true;
|
|
818
818
|
};
|
|
819
819
|
ac.signal.addEventListener("abort", onAbort, { once: true });
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
820
|
+
let ans;
|
|
821
|
+
try {
|
|
822
|
+
const choose = isChoice ? ctx.ui.select(qTitle, params.options) : ctx.ui.input(qTitle);
|
|
823
|
+
ans = (await choose);
|
|
824
|
+
}
|
|
825
|
+
finally {
|
|
826
|
+
ac.signal.removeEventListener("abort", onAbort);
|
|
827
|
+
clearIdle();
|
|
828
|
+
}
|
|
824
829
|
if (aborted || ac.signal.aborted) {
|
|
825
830
|
return {
|
|
826
831
|
content: [
|
|
@@ -1398,7 +1403,7 @@ export class GoalService {
|
|
|
1398
1403
|
conv.goal.roles = {};
|
|
1399
1404
|
return exec?.convId;
|
|
1400
1405
|
}
|
|
1401
|
-
stopDelegated(conv) {
|
|
1406
|
+
async stopDelegated(conv) {
|
|
1402
1407
|
// 排队的启动请求一并作废:setGoal 会在后面按新代次重新排,clearGoal 则不需要。
|
|
1403
1408
|
this.delegatedPending.delete(conv.id);
|
|
1404
1409
|
conv.awaitingVerdict = undefined;
|
|
@@ -1411,6 +1416,30 @@ export class GoalService {
|
|
|
1411
1416
|
settle("gone");
|
|
1412
1417
|
}
|
|
1413
1418
|
const execId = this.detachRoleExec(conv);
|
|
1419
|
+
if (execId) {
|
|
1420
|
+
try {
|
|
1421
|
+
await this.host.stopRoleAgent?.(execId);
|
|
1422
|
+
}
|
|
1423
|
+
catch { }
|
|
1424
|
+
try {
|
|
1425
|
+
// issue #464: 必须等待执行者完全 dismiss 移出会话表,避免立即重设目标时误报容量超限
|
|
1426
|
+
await this.host.dismissRoleAgent?.(execId);
|
|
1427
|
+
}
|
|
1428
|
+
catch { }
|
|
1429
|
+
}
|
|
1430
|
+
conv.goal.phase = "idle";
|
|
1431
|
+
}
|
|
1432
|
+
/** 会话过户时清理目标审查态并唤醒等待者(issue #457)。 */
|
|
1433
|
+
notifyTakeover(conv) {
|
|
1434
|
+
this.delegatedPending.delete(conv.id);
|
|
1435
|
+
conv.awaitingVerdict = undefined;
|
|
1436
|
+
void this.flushDeferredPrompts(conv);
|
|
1437
|
+
const settle = this.verdictWaiters.get(conv.id);
|
|
1438
|
+
if (settle) {
|
|
1439
|
+
this.verdictWaiters.delete(conv.id);
|
|
1440
|
+
settle("gone");
|
|
1441
|
+
}
|
|
1442
|
+
const execId = this.detachRoleExec(conv);
|
|
1414
1443
|
if (execId) {
|
|
1415
1444
|
void this.host.stopRoleAgent?.(execId).catch(() => { });
|
|
1416
1445
|
void this.host.dismissRoleAgent?.(execId).catch(() => { });
|
|
@@ -1581,6 +1610,14 @@ export class GoalService {
|
|
|
1581
1610
|
// 落盘对话在左栏/历史里没有「子代理」微标,用带前缀的标题保持可辨识。
|
|
1582
1611
|
title: this.roleConvTitle(goalText),
|
|
1583
1612
|
});
|
|
1613
|
+
// #490/案1:spawn 的长 await 期间目标可能被重设/清除(goalGeneration 已变,
|
|
1614
|
+
// stopDelegated 当时无可停执行者)——老循环不得把带着老目标首轮 prompt 的
|
|
1615
|
+
// 执行者写回 conv.roleExec,否则新循环会把它当现役执行者 steer(上下文被
|
|
1616
|
+
// 老目标污染,新目标首轮产出混入老目标工作)。就地停掉并让老循环按代次退出。
|
|
1617
|
+
if (conv.goalGeneration !== goalGeneration) {
|
|
1618
|
+
void host.stopRoleAgent?.(convId).catch(() => { });
|
|
1619
|
+
return undefined;
|
|
1620
|
+
}
|
|
1584
1621
|
conv.roleExec = { convId, generation: (conv.roleExec?.generation ?? 0) + 1 };
|
|
1585
1622
|
g.roles = { ...g.roles, executor: { convId, spawned: true } };
|
|
1586
1623
|
this.beginRoundVitals(conv, convId);
|
|
@@ -1762,6 +1799,13 @@ export class GoalService {
|
|
|
1762
1799
|
phase: "start",
|
|
1763
1800
|
round,
|
|
1764
1801
|
});
|
|
1802
|
+
// #490/案2:pushReviewCard 的 await 窗口里 setGoal/clearGoal 可能已清掉
|
|
1803
|
+
// awaitingVerdict(此刻 verdictWaiters 尚未登记,stopDelegated 的 settle 落空)
|
|
1804
|
+
// ——不复检就会继续登记 waiter 并把老目标的审查指令投进主对话:主对话白烧
|
|
1805
|
+
// 一轮审查、老循环干等 20 分钟超时,新目标的 pending 循环全程无法启动。
|
|
1806
|
+
if (!conv.awaitingVerdict || conv.awaitingVerdict.round !== round) {
|
|
1807
|
+
return "gone";
|
|
1808
|
+
}
|
|
1765
1809
|
const verdict = await new Promise((resolve) => {
|
|
1766
1810
|
let settled = false;
|
|
1767
1811
|
let timer;
|