pi-web-ui 0.97.0 → 0.99.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 +126 -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 +645 -147
- 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 +30 -5
- package/dist/server/goal-review-gate.js +7 -20
- package/dist/server/goal-service.js +77 -10
- 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 +2 -3
- package/dist/server/patch-tool.js +78 -28
- package/dist/server/permission-preset.js +25 -0
- package/dist/server/plan-manager.js +196 -10
- 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 +24 -12
- 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 -40
- 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-NerrJlTS.js} +1 -1
- package/web/dist/assets/index-BdMaGCcs.css +1 -0
- package/web/dist/assets/index-ByCBN4Kt.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。
|
|
@@ -522,7 +523,28 @@ export function makeEvalTool(opts) {
|
|
|
522
523
|
}),
|
|
523
524
|
execute: async (_id, params) => {
|
|
524
525
|
const lang = getLang();
|
|
525
|
-
|
|
526
|
+
let language = params.language ?? "py";
|
|
527
|
+
if (language === "python")
|
|
528
|
+
language = "py"; // 常见笔误别名(issue #462)
|
|
529
|
+
if (language !== "py" && language !== "js" && language !== "ts") {
|
|
530
|
+
// schema 已声明 enum(["py","js","ts"])(#417),但那只影响提示不拦运行时;
|
|
531
|
+
// getKernel 的 normLang 只映射 ts,其余任意值会原样建出名为它的 Node 内核,
|
|
532
|
+
// Python 代码被 node 执行报 JS 语法错且模型无从知道是键写错。
|
|
533
|
+
const text = pick(lang, `错误:language 仅支持 py / js / ts,收到「${language}」。`, `Error: language must be one of py / js / ts; got "${language}".`);
|
|
534
|
+
return {
|
|
535
|
+
content: [{ type: "text", text }],
|
|
536
|
+
details: {
|
|
537
|
+
language,
|
|
538
|
+
durationMs: 0,
|
|
539
|
+
ok: false,
|
|
540
|
+
stdout: "",
|
|
541
|
+
stderr: "",
|
|
542
|
+
result: null,
|
|
543
|
+
error: text,
|
|
544
|
+
reset: params.reset ?? false,
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
}
|
|
526
548
|
const timeoutSec = Math.min(Math.max(1, params.timeout ?? DEFAULT_TIMEOUT_SECONDS), MAX_TIMEOUT_SECONDS);
|
|
527
549
|
const timeoutMs = timeoutSec * 1000;
|
|
528
550
|
const startTime = Date.now();
|
|
@@ -569,8 +591,11 @@ export function makeEvalTool(opts) {
|
|
|
569
591
|
}
|
|
570
592
|
catch (err) {
|
|
571
593
|
const durationMs = Date.now() - startTime;
|
|
572
|
-
|
|
573
|
-
|
|
594
|
+
let errMsg = err.message;
|
|
595
|
+
if (errMsg.includes("Python executable not found in PATH")) {
|
|
596
|
+
errMsg = pick(lang, "未在系统 PATH 中找到 Python 可执行程序,请安装 Python 或使用 language: 'js'。", "Python executable not found in PATH. Please install Python or use language: 'js'.");
|
|
597
|
+
}
|
|
598
|
+
const text = `${pick(lang, `[eval:${language} 出错 (${durationMs}ms)]`, `[eval:${language} error (${durationMs}ms)]`)}\n${errMsg}`;
|
|
574
599
|
return {
|
|
575
600
|
content: [{ type: "text", text }],
|
|
576
601
|
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
|
/** 引号感知的命令切分(按 | ; && || 换行拆段;含 $( / 反引号时整条视为一段交白名单判
|