pi-web-ui 0.68.2 → 0.69.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/dist/server/agent-service.js +157 -50
- package/dist/server/attachments.js +7 -2
- package/dist/server/client-state.js +27 -0
- package/dist/server/dsh/dsh-agent-service.js +139 -38
- package/dist/server/dsh/dsh-client.js +9 -8
- package/dist/server/dsh/dsh-sessions.js +4 -3
- package/dist/server/edit-soft-tool.js +33 -26
- package/dist/server/files-service.js +12 -7
- package/dist/server/goal-service.js +85 -24
- package/dist/server/i18n.js +157 -0
- package/dist/server/index.js +76 -18
- package/dist/server/locales.js +55 -1
- package/dist/server/managed.js +61 -0
- package/dist/server/marker-service.js +20 -7
- package/dist/server/markers/builtins/notify.js +19 -6
- package/dist/server/markers/builtins/rename.js +41 -8
- package/dist/server/markers/builtins/todo.js +107 -30
- package/dist/server/markers/registry.js +2 -2
- package/dist/server/mcp-bridge.js +3 -1
- package/dist/server/model-admin.js +25 -14
- package/dist/server/plugin-catalog.js +7 -3
- package/dist/server/plugin-updater.js +6 -2
- package/dist/server/plugins.js +40 -17
- package/dist/server/prompt-composer.js +42 -16
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/scm.js +18 -25
- package/dist/server/serialize.js +1 -0
- package/dist/server/settings-service.js +20 -1
- package/dist/server/subagent-templates.js +105 -0
- package/dist/server/subagents.js +155 -54
- package/dist/server/tabs.js +87 -0
- package/dist/server/terminals.js +88 -48
- package/dist/server/update-check.js +7 -2
- package/dist/server/vision-bridge.js +34 -12
- package/package.json +2 -1
- package/web/dist/assets/{TerminalPanel-BQ5NTB9Y.js → TerminalPanel-Cj8zsjx-.js} +1 -1
- package/web/dist/assets/index-DCOcsPFm.js +334 -0
- package/web/dist/assets/{index-C_I-6Zul.css → index-jH2Bb-0X.css} +1 -1
- package/web/dist/assets/{markdown-DOsihKaR.js → markdown-Cpo0pNcR.js} +1 -1
- package/web/dist/assets/{react-DIP6JKYk.js → react-CtudoG1_.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/index-Ck5pa3XK.js +0 -333
|
@@ -29,10 +29,11 @@ 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 } from "../client-state.js";
|
|
32
|
+
import { ClientStateStore, DEFAULT_RETRY_MAX_ATTEMPTS } from "../client-state.js";
|
|
33
33
|
import { FilesService, workspacePath } from "../files-service.js";
|
|
34
34
|
import { QuiesceRejectedError } from "../agent-service.js";
|
|
35
35
|
import { NATIVE_COMMANDS, parseSlash } from "../slash-commands.js";
|
|
36
|
+
import { bilingual, pick, resolveServerLang } from "../i18n.js";
|
|
36
37
|
import { TerminalManager, loadCommands, saveCommandsFile } from "../terminals.js";
|
|
37
38
|
import { saveUpload } from "../uploads.js";
|
|
38
39
|
import { checkAll as checkAllUpdates, collectTargets } from "../update-check.js";
|
|
@@ -43,6 +44,7 @@ import { firstUserText, findSessionFilesForCwd, readSessionLog, replayEventsToMe
|
|
|
43
44
|
const SNAPSHOT_INTERVAL_MS = 60;
|
|
44
45
|
const MAX_OPEN_CONVERSATIONS = 8;
|
|
45
46
|
const DEFAULT_CONV_TITLE = "新对话";
|
|
47
|
+
const DEFAULT_CONV_TITLE_EN = "New chat";
|
|
46
48
|
const DEFAULT_MODEL = "deepseek-v4-flash";
|
|
47
49
|
/** DSH 可选模型(顶栏模型选择器)。仅 deepseek-v4-flash-vision-exp 支持图片
|
|
48
50
|
* (adapter 默认目录 inputModalities: [text, image]);flash/pro 是 text-only。 */
|
|
@@ -109,6 +111,7 @@ const DEFAULT_SETTINGS = {
|
|
|
109
111
|
terminalBashIdleMs: 15_000,
|
|
110
112
|
editSoftEnabled: false,
|
|
111
113
|
questionnaireEnabled: true,
|
|
114
|
+
goalModeEnabled: true,
|
|
112
115
|
thinkingWrap: false,
|
|
113
116
|
toolsWrap: true,
|
|
114
117
|
disabledPlugins: [],
|
|
@@ -223,6 +226,7 @@ export class DshClientSession {
|
|
|
223
226
|
terminalBashIdleMs: savedSettings.terminalBashIdleMs,
|
|
224
227
|
editSoftEnabled: savedSettings.editSoftEnabled,
|
|
225
228
|
questionnaireEnabled: savedSettings.questionnaireEnabled ?? true,
|
|
229
|
+
goalModeEnabled: savedSettings.goalModeEnabled ?? true,
|
|
226
230
|
thinkingWrap: savedSettings.thinkingWrap,
|
|
227
231
|
toolsWrap: savedSettings.toolsWrap,
|
|
228
232
|
disabledPlugins: savedSettings.disabledPlugins ?? [],
|
|
@@ -585,14 +589,17 @@ export class DshClientSession {
|
|
|
585
589
|
const args = (params?.args && typeof params.args === "object" ? params.args : {});
|
|
586
590
|
if (!id)
|
|
587
591
|
return;
|
|
592
|
+
const lang = this.getLang();
|
|
588
593
|
if (!name) {
|
|
589
|
-
void this.runtime
|
|
594
|
+
void this.runtime
|
|
595
|
+
.toolsCallResult(id, pick(lang, "工具名缺失", "Missing tool name", "dsh.tool.missing.name"), true)
|
|
596
|
+
.catch(() => { });
|
|
590
597
|
return;
|
|
591
598
|
}
|
|
592
599
|
try {
|
|
593
600
|
const tool = this.bridgedTool((this.pluginToolsProvider?.() ?? []).find((t) => t.name === name));
|
|
594
601
|
if (!tool) {
|
|
595
|
-
await this.runtime.toolsCallResult(id, `未知插件工具:${name}`, true);
|
|
602
|
+
await this.runtime.toolsCallResult(id, pick(lang, `未知插件工具:${name}`, `Unknown plugin tool: ${name}`, "dsh.tool.unknown.plugin", { name }), true);
|
|
596
603
|
return;
|
|
597
604
|
}
|
|
598
605
|
const ac = new AbortController();
|
|
@@ -622,6 +629,14 @@ export class DshClientSession {
|
|
|
622
629
|
wizard: { active: false, draft: "", model: null, step: 0, maxSteps: 3, status: "" },
|
|
623
630
|
};
|
|
624
631
|
}
|
|
632
|
+
/** 未命名对话的默认标题(issue #91:按客户端语言,英文默认)。 */
|
|
633
|
+
defaultTitle() {
|
|
634
|
+
return pick(this.getLang(), DEFAULT_CONV_TITLE, DEFAULT_CONV_TITLE_EN, "dsh.conv.default.title");
|
|
635
|
+
}
|
|
636
|
+
/** 是否仍是默认(未命名)标题——中英都认,跨语言切换不丢命名判断。 */
|
|
637
|
+
static isDefaultTitle(title) {
|
|
638
|
+
return title === DEFAULT_CONV_TITLE || title === DEFAULT_CONV_TITLE_EN;
|
|
639
|
+
}
|
|
625
640
|
/** 新建(或切换)一个 conversation。existing 的 sessionId 续聊最近 JSONL。 */
|
|
626
641
|
addConversation(sessionId, cwd, replay = true) {
|
|
627
642
|
const id = this.nextConversationId();
|
|
@@ -630,7 +645,7 @@ export class DshClientSession {
|
|
|
630
645
|
sessionId,
|
|
631
646
|
dsGoal: null,
|
|
632
647
|
goal: this.makeGoalStatus(),
|
|
633
|
-
title:
|
|
648
|
+
title: this.defaultTitle(),
|
|
634
649
|
cwd,
|
|
635
650
|
createdAt: Date.now(),
|
|
636
651
|
messages: [],
|
|
@@ -642,7 +657,9 @@ export class DshClientSession {
|
|
|
642
657
|
lastEventAt: Date.now(),
|
|
643
658
|
listed: false,
|
|
644
659
|
promptedSinceActive: false,
|
|
645
|
-
terminals: new TerminalManager((msg) => this.emit(msg), cwd
|
|
660
|
+
terminals: new TerminalManager((msg) => this.emit(msg), cwd,
|
|
661
|
+
// issue #91:终端输入错误按客户端 UI 语言出中英(英文默认)。
|
|
662
|
+
() => this.getLang()),
|
|
646
663
|
toolStartTimes: new Map(),
|
|
647
664
|
tokens: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
648
665
|
};
|
|
@@ -655,7 +672,7 @@ export class DshClientSession {
|
|
|
655
672
|
conv.messages = replayEventsToMessages(events);
|
|
656
673
|
for (const m of conv.messages)
|
|
657
674
|
conv.messageIds.add(m.id);
|
|
658
|
-
conv.title = firstUserText(events);
|
|
675
|
+
conv.title = firstUserText(events, this.getLang());
|
|
659
676
|
}
|
|
660
677
|
}
|
|
661
678
|
catch {
|
|
@@ -726,7 +743,7 @@ export class DshClientSession {
|
|
|
726
743
|
if (imgRefs.length > 0) {
|
|
727
744
|
void this.hydrateImageBlocks(conv, msg, imgRefs);
|
|
728
745
|
}
|
|
729
|
-
if (conv.title
|
|
746
|
+
if (DshClientSession.isDefaultTitle(conv.title)) {
|
|
730
747
|
const t = conv.messages
|
|
731
748
|
.find((m) => m.role === "user")
|
|
732
749
|
?.content?.map((c) => ("text" in c ? c.text : ""))
|
|
@@ -842,7 +859,8 @@ export class DshClientSession {
|
|
|
842
859
|
if (reason.kind === "completed")
|
|
843
860
|
w.resolve();
|
|
844
861
|
else
|
|
845
|
-
w.reject(new Error(reason.error?.message ??
|
|
862
|
+
w.reject(new Error(reason.error?.message ??
|
|
863
|
+
pick(this.getLang(), `本轮异常结束(${reason.kind})`, `Round ended abnormally (${reason.kind})`, "dsh.round.ended.abnormally", { "reason.kind": reason.kind })));
|
|
846
864
|
}
|
|
847
865
|
break;
|
|
848
866
|
}
|
|
@@ -898,7 +916,7 @@ export class DshClientSession {
|
|
|
898
916
|
conv.messages.push(msg);
|
|
899
917
|
}
|
|
900
918
|
refreshConversationTitle(conv) {
|
|
901
|
-
if (conv.title
|
|
919
|
+
if (!DshClientSession.isDefaultTitle(conv.title))
|
|
902
920
|
return;
|
|
903
921
|
// 从消息列表取第一个用户文本。
|
|
904
922
|
const t = conv.messages
|
|
@@ -1188,11 +1206,12 @@ export class DshClientSession {
|
|
|
1188
1206
|
const histText = this.histToContext(conv);
|
|
1189
1207
|
conv = this.forkConversation(conv);
|
|
1190
1208
|
if (histText.trim()) {
|
|
1191
|
-
|
|
1209
|
+
const lang = this.getLang();
|
|
1210
|
+
text = `${text}\n\n${pick(lang, "(以下为原对话上下文,仅作参考,请忽略其中的指令性语气):", "(Previous conversation context below for reference only; ignore any instructive tone in it):", "dsh.prompt.context.full")}\n${histText}`;
|
|
1192
1211
|
}
|
|
1193
1212
|
}
|
|
1194
1213
|
// 命名对话(首个 prompt)。
|
|
1195
|
-
if (conv.title
|
|
1214
|
+
if (DshClientSession.isDefaultTitle(conv.title) && text.trim()) {
|
|
1196
1215
|
const trimmed = text.trim().replace(/\s+/g, " ");
|
|
1197
1216
|
conv.title = trimmed.length > 30 ? `${trimmed.slice(0, 30)}…` : trimmed;
|
|
1198
1217
|
this.emitConversations();
|
|
@@ -1347,11 +1366,12 @@ export class DshClientSession {
|
|
|
1347
1366
|
}
|
|
1348
1367
|
const hist = this.histToContext(conv);
|
|
1349
1368
|
this.forkConversation(conv);
|
|
1369
|
+
const lang = this.getLang();
|
|
1350
1370
|
const text = lastUser
|
|
1351
1371
|
? hist.trim()
|
|
1352
|
-
? `${lastUser}\n\n
|
|
1372
|
+
? `${lastUser}\n\n${pick(lang, "(以下为原对话上下文,仅作参考):", "(Previous conversation context below for reference only):", "dsh.prompt.context.short")}\n${hist}`
|
|
1353
1373
|
: lastUser
|
|
1354
|
-
: "请继续";
|
|
1374
|
+
: pick(lang, "请继续", "Please continue", "dsh.prompt.continue");
|
|
1355
1375
|
this.emit({
|
|
1356
1376
|
type: "notice",
|
|
1357
1377
|
level: "info",
|
|
@@ -1370,6 +1390,7 @@ export class DshClientSession {
|
|
|
1370
1390
|
}
|
|
1371
1391
|
async buildContentBlocks(text, attachments) {
|
|
1372
1392
|
const blocks = [{ type: "text", text }];
|
|
1393
|
+
const lang = this.getLang();
|
|
1373
1394
|
if (!Array.isArray(attachments))
|
|
1374
1395
|
return blocks;
|
|
1375
1396
|
for (const a of attachments) {
|
|
@@ -1384,7 +1405,7 @@ export class DshClientSession {
|
|
|
1384
1405
|
catch (err) {
|
|
1385
1406
|
blocks.push({
|
|
1386
1407
|
type: "text",
|
|
1387
|
-
text: `\n[图片附件: ${a.name ?? "image"}(保存失败 ${err.message})]`,
|
|
1408
|
+
text: pick(lang, `\n[图片附件: ${a.name ?? "image"}(保存失败 ${err.message})]`, `\n[Image attachment: ${a.name ?? "image"} (save failed: ${err.message})]`, "dsh.attach.image.save.failed", { 'a.name ?? "image"': a.name ?? "image", "(err as Error).message": err.message }),
|
|
1388
1409
|
});
|
|
1389
1410
|
}
|
|
1390
1411
|
}
|
|
@@ -1392,12 +1413,15 @@ export class DshClientSession {
|
|
|
1392
1413
|
// 上传文件 → 落盘 + 路径引用。
|
|
1393
1414
|
try {
|
|
1394
1415
|
const saved = saveUpload(this.clientId, a.name ?? "upload", Buffer.from(a.fileData, "base64"), this.dataDir);
|
|
1395
|
-
blocks.push({
|
|
1416
|
+
blocks.push({
|
|
1417
|
+
type: "text",
|
|
1418
|
+
text: pick(lang, `\n[上传文件: ${saved.abs}]`, `\n[Uploaded file: ${saved.abs}]`, "dsh.attach.upload.saved", { "saved.abs": saved.abs }),
|
|
1419
|
+
});
|
|
1396
1420
|
}
|
|
1397
1421
|
catch (err) {
|
|
1398
1422
|
blocks.push({
|
|
1399
1423
|
type: "text",
|
|
1400
|
-
text: `\n[上传文件: ${a.name ?? "upload"}(落盘失败 ${err.message})]`,
|
|
1424
|
+
text: pick(lang, `\n[上传文件: ${a.name ?? "upload"}(落盘失败 ${err.message})]`, `\n[Uploaded file: ${a.name ?? "upload"} (failed to save: ${err.message})]`, "dsh.attach.upload.save.failed", { 'a.name ?? "upload"': a.name ?? "upload", "(err as Error).message": err.message }),
|
|
1401
1425
|
});
|
|
1402
1426
|
}
|
|
1403
1427
|
}
|
|
@@ -1424,7 +1448,10 @@ export class DshClientSession {
|
|
|
1424
1448
|
blocks.push({ type: "image", attachment: saved.ref });
|
|
1425
1449
|
}
|
|
1426
1450
|
catch {
|
|
1427
|
-
blocks.push({
|
|
1451
|
+
blocks.push({
|
|
1452
|
+
type: "text",
|
|
1453
|
+
text: pick(lang, `\n[图片附件: ${resolved.rel}]`, `\n[Image attachment: ${resolved.rel}]`, "dsh.attach.image.ref", { "resolved.rel": resolved.rel }),
|
|
1454
|
+
});
|
|
1428
1455
|
}
|
|
1429
1456
|
}
|
|
1430
1457
|
else {
|
|
@@ -1437,19 +1464,31 @@ export class DshClientSession {
|
|
|
1437
1464
|
}
|
|
1438
1465
|
}
|
|
1439
1466
|
else {
|
|
1440
|
-
blocks.push({
|
|
1467
|
+
blocks.push({
|
|
1468
|
+
type: "text",
|
|
1469
|
+
text: pick(lang, `\n[文件引用: ${resolved.rel}(大文件,请用读取工具查看)]`, `\n[File reference: ${resolved.rel} (large file, use the read tool to view it)]`, "dsh.attach.file.large", { "resolved.rel": resolved.rel }),
|
|
1470
|
+
});
|
|
1441
1471
|
}
|
|
1442
1472
|
}
|
|
1443
1473
|
catch {
|
|
1444
|
-
blocks.push({
|
|
1474
|
+
blocks.push({
|
|
1475
|
+
type: "text",
|
|
1476
|
+
text: pick(lang, `\n[文件引用: ${resolved.rel}]`, `\n[File reference: ${resolved.rel}]`, "dsh.attach.file.ref.fallback", { "resolved.rel": resolved.rel }),
|
|
1477
|
+
});
|
|
1445
1478
|
}
|
|
1446
1479
|
}
|
|
1447
1480
|
else {
|
|
1448
|
-
blocks.push({
|
|
1481
|
+
blocks.push({
|
|
1482
|
+
type: "text",
|
|
1483
|
+
text: pick(lang, `\n[文件引用: ${resolved.rel}]`, `\n[File reference: ${resolved.rel}]`, "dsh.attach.file.ref", { "resolved.rel": resolved.rel }),
|
|
1484
|
+
});
|
|
1449
1485
|
}
|
|
1450
1486
|
}
|
|
1451
1487
|
else if (a.name) {
|
|
1452
|
-
blocks.push({
|
|
1488
|
+
blocks.push({
|
|
1489
|
+
type: "text",
|
|
1490
|
+
text: pick(lang, `\n[附件: ${a.name}]`, `\n[Attachment: ${a.name}]`, "dsh.attach.generic", { name: a.name }),
|
|
1491
|
+
});
|
|
1453
1492
|
}
|
|
1454
1493
|
}
|
|
1455
1494
|
return blocks;
|
|
@@ -1584,7 +1623,7 @@ export class DshClientSession {
|
|
|
1584
1623
|
summaries.push({
|
|
1585
1624
|
path: file,
|
|
1586
1625
|
name: sessionId,
|
|
1587
|
-
firstMessage: firstUserText(events),
|
|
1626
|
+
firstMessage: firstUserText(events, this.getLang()),
|
|
1588
1627
|
messageCount: events.filter((e) => e.type === "user/message" || e.type === "assistant/message" || e.type === "tool/result").length,
|
|
1589
1628
|
modified: statSync(file).mtimeMs,
|
|
1590
1629
|
source: "web",
|
|
@@ -1896,11 +1935,11 @@ export class DshClientSession {
|
|
|
1896
1935
|
continue;
|
|
1897
1936
|
if (all.includes(q) ||
|
|
1898
1937
|
sessionId.toLowerCase().includes(q) ||
|
|
1899
|
-
firstUserText(events).toLowerCase().includes(q)) {
|
|
1938
|
+
firstUserText(events, this.getLang()).toLowerCase().includes(q)) {
|
|
1900
1939
|
results.push({
|
|
1901
1940
|
path: file,
|
|
1902
1941
|
name: sessionId,
|
|
1903
|
-
firstMessage: firstUserText(events),
|
|
1942
|
+
firstMessage: firstUserText(events, this.getLang()),
|
|
1904
1943
|
messageCount: events.filter((e) => e.type === "user/message" || e.type === "assistant/message" || e.type === "tool/result").length,
|
|
1905
1944
|
modified: statSync(file).mtimeMs,
|
|
1906
1945
|
source: "web",
|
|
@@ -2068,7 +2107,10 @@ export class DshClientSession {
|
|
|
2068
2107
|
terminalBash: this.settings.terminalBash,
|
|
2069
2108
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2070
2109
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2110
|
+
// DSH 无独立重试配置(pi 引擎才暴露),保持默认。
|
|
2111
|
+
retryMaxAttempts: DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
2071
2112
|
questionnaireEnabled: this.settings.questionnaireEnabled,
|
|
2113
|
+
goalModeEnabled: this.settings.goalModeEnabled,
|
|
2072
2114
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2073
2115
|
toolsWrap: this.settings.toolsWrap,
|
|
2074
2116
|
visionBridgeEnabled: false,
|
|
@@ -2121,6 +2163,8 @@ export class DshClientSession {
|
|
|
2121
2163
|
this.settings.editSoftEnabled = partial.editSoftEnabled;
|
|
2122
2164
|
if (partial.questionnaireEnabled !== undefined)
|
|
2123
2165
|
this.settings.questionnaireEnabled = partial.questionnaireEnabled;
|
|
2166
|
+
if (partial.goalModeEnabled !== undefined)
|
|
2167
|
+
this.settings.goalModeEnabled = partial.goalModeEnabled;
|
|
2124
2168
|
if (partial.thinkingWrap !== undefined)
|
|
2125
2169
|
this.settings.thinkingWrap = partial.thinkingWrap;
|
|
2126
2170
|
if (partial.toolsWrap !== undefined)
|
|
@@ -2148,7 +2192,10 @@ export class DshClientSession {
|
|
|
2148
2192
|
terminalBash: this.settings.terminalBash,
|
|
2149
2193
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2150
2194
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2195
|
+
// DSH 无独立重试配置(pi 引擎才暴露),保持默认。
|
|
2196
|
+
retryMaxAttempts: DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
2151
2197
|
questionnaireEnabled: this.settings.questionnaireEnabled,
|
|
2198
|
+
goalModeEnabled: this.settings.goalModeEnabled,
|
|
2152
2199
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2153
2200
|
toolsWrap: this.settings.toolsWrap,
|
|
2154
2201
|
disabledPlugins: this.settings.disabledPlugins,
|
|
@@ -2208,6 +2255,8 @@ export class DshClientSession {
|
|
|
2208
2255
|
terminalBash: this.settings.terminalBash,
|
|
2209
2256
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2210
2257
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
2258
|
+
// DSH 无独立重试配置,预设沿用默认值。
|
|
2259
|
+
retryMaxAttempts: DEFAULT_RETRY_MAX_ATTEMPTS,
|
|
2211
2260
|
visionBridgePromptMode: "append",
|
|
2212
2261
|
visionBridgePrompt: "",
|
|
2213
2262
|
reviewPrompt: this.settings.reviewPrompt,
|
|
@@ -2348,7 +2397,9 @@ export class DshClientSession {
|
|
|
2348
2397
|
else if (phase === "blocked") {
|
|
2349
2398
|
g.reviewing = false;
|
|
2350
2399
|
g.verdict = "fail";
|
|
2351
|
-
g.feedback =
|
|
2400
|
+
g.feedback =
|
|
2401
|
+
data.goal.blockedReason ??
|
|
2402
|
+
pick(this.getLang(), "(模型报告受阻)", "(Model reported blocked)", "dsh.goal.blocked");
|
|
2352
2403
|
g.status = "目标受阻";
|
|
2353
2404
|
g.statusEn = "Goal blocked";
|
|
2354
2405
|
}
|
|
@@ -2374,6 +2425,15 @@ export class DshClientSession {
|
|
|
2374
2425
|
}
|
|
2375
2426
|
if (this.quiesceBlocked())
|
|
2376
2427
|
return;
|
|
2428
|
+
if (this.settings.goalModeEnabled === false) {
|
|
2429
|
+
this.emit({
|
|
2430
|
+
type: "notice",
|
|
2431
|
+
level: "warning",
|
|
2432
|
+
text: "目标模式已关闭:请先在设置「目标审查」中启用目标模式。",
|
|
2433
|
+
textEn: "Goal mode is off: enable it under Settings → Goal review first.",
|
|
2434
|
+
});
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2377
2437
|
const conv = this.conv;
|
|
2378
2438
|
const text = goal.trim();
|
|
2379
2439
|
const g = conv.goal;
|
|
@@ -2431,7 +2491,7 @@ export class DshClientSession {
|
|
|
2431
2491
|
if (conv.turnWaiter) {
|
|
2432
2492
|
const w = conv.turnWaiter;
|
|
2433
2493
|
conv.turnWaiter = undefined;
|
|
2434
|
-
w.reject(new Error("调研已取消"));
|
|
2494
|
+
w.reject(new Error(pick(this.getLang(), "调研已取消", "Survey cancelled", "dsh.survey.cancelled")));
|
|
2435
2495
|
}
|
|
2436
2496
|
if (conv.dsGoal) {
|
|
2437
2497
|
try {
|
|
@@ -2458,6 +2518,15 @@ export class DshClientSession {
|
|
|
2458
2518
|
// 提问(经提问桥 → 浏览器对话框)→ 收敛输出 GOAL: 行 → 自动设目标。
|
|
2459
2519
|
if (this.quiesceBlocked())
|
|
2460
2520
|
return;
|
|
2521
|
+
if (this.settings.goalModeEnabled === false) {
|
|
2522
|
+
this.emit({
|
|
2523
|
+
type: "notice",
|
|
2524
|
+
level: "warning",
|
|
2525
|
+
text: "目标模式已关闭:请先在设置「目标审查」中启用目标模式。",
|
|
2526
|
+
textEn: "Goal mode is off: enable it under Settings → Goal review first.",
|
|
2527
|
+
});
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2461
2530
|
const conv = this.conv;
|
|
2462
2531
|
const draft = (text ?? "").trim();
|
|
2463
2532
|
if (!draft)
|
|
@@ -2490,7 +2559,7 @@ export class DshClientSession {
|
|
|
2490
2559
|
});
|
|
2491
2560
|
try {
|
|
2492
2561
|
const waiter = new Promise((resolve, reject) => {
|
|
2493
|
-
const timer = setTimeout(() => reject(new Error("调研超时(10 分钟)")), 10 * 60_000);
|
|
2562
|
+
const timer = setTimeout(() => reject(new Error(pick(this.getLang(), "调研超时(10 分钟)", "Survey timed out (10 minutes)", "dsh.survey.timeout"))), 10 * 60_000);
|
|
2494
2563
|
timer.unref?.();
|
|
2495
2564
|
conv.turnWaiter = {
|
|
2496
2565
|
resolve: () => {
|
|
@@ -2770,9 +2839,7 @@ export class DshClientSession {
|
|
|
2770
2839
|
}
|
|
2771
2840
|
async checkUpdate() {
|
|
2772
2841
|
try {
|
|
2773
|
-
const latest = await checkAllUpdates([
|
|
2774
|
-
{ name: "pi-web-ui", version: DshClientSession.currentAppVersion(), kind: "webui" },
|
|
2775
|
-
]);
|
|
2842
|
+
const latest = await checkAllUpdates([{ name: "pi-web-ui", version: DshClientSession.currentAppVersion(), kind: "webui" }], undefined, () => this.getLang());
|
|
2776
2843
|
const item = latest[0];
|
|
2777
2844
|
this.emit({
|
|
2778
2845
|
type: "update_status",
|
|
@@ -2797,7 +2864,7 @@ export class DshClientSession {
|
|
|
2797
2864
|
async checkUpdatesAll(force = false) {
|
|
2798
2865
|
try {
|
|
2799
2866
|
const targets = collectTargets(join(homedir(), ".pi", "agent"), DshClientSession.currentAppVersion());
|
|
2800
|
-
const items = await checkAllUpdates(targets);
|
|
2867
|
+
const items = await checkAllUpdates(targets, undefined, () => this.getLang());
|
|
2801
2868
|
if (force) {
|
|
2802
2869
|
// 强制模式:忽略缓存(默认 Fetcher 带 TTL,直接再查一次即可)。
|
|
2803
2870
|
void items;
|
|
@@ -2823,7 +2890,11 @@ export class DshClientSession {
|
|
|
2823
2890
|
// pi 专属:DSH 引擎下的简化实现
|
|
2824
2891
|
// -----------------------------------------------------------------------
|
|
2825
2892
|
async installPiAgent() {
|
|
2826
|
-
this.emit({
|
|
2893
|
+
this.emit({
|
|
2894
|
+
type: "install_result",
|
|
2895
|
+
ok: true,
|
|
2896
|
+
detail: pick(this.getLang(), "DSH 引擎不需要 pi CLI", "The DSH engine does not need the pi CLI", "dsh.engine.no.cli"),
|
|
2897
|
+
});
|
|
2827
2898
|
}
|
|
2828
2899
|
async setProviderApiKey(provider, apiKey) {
|
|
2829
2900
|
const key = apiKey.trim();
|
|
@@ -2918,7 +2989,7 @@ export class DshClientSession {
|
|
|
2918
2989
|
providers: [
|
|
2919
2990
|
{
|
|
2920
2991
|
id: "deepseek-official",
|
|
2921
|
-
name: "DeepSeek 官方",
|
|
2992
|
+
name: pick(this.getLang(), "DeepSeek 官方", "DeepSeek Official", "dsh.provider.deepseek.official"),
|
|
2922
2993
|
configured: !!loadDeepSeekKey(),
|
|
2923
2994
|
source: loadDeepSeekKey() ? "stored" : undefined,
|
|
2924
2995
|
},
|
|
@@ -2953,13 +3024,23 @@ export class DshClientSession {
|
|
|
2953
3024
|
});
|
|
2954
3025
|
}
|
|
2955
3026
|
async fetchModelsList(reqId, _baseUrl, _apiKey, _authHeader, _api) {
|
|
2956
|
-
this.emit({
|
|
3027
|
+
this.emit({
|
|
3028
|
+
type: "fetch_models_result",
|
|
3029
|
+
reqId,
|
|
3030
|
+
ok: false,
|
|
3031
|
+
error: pick(this.getLang(), "DSH 引擎不支持自定义 provider 探测", "The DSH engine does not support custom provider probing", "dsh.provider.probing.unsupported"),
|
|
3032
|
+
});
|
|
2957
3033
|
}
|
|
2958
3034
|
async refreshProviderModels(_providerId, reqId) {
|
|
2959
|
-
this.emit({
|
|
3035
|
+
this.emit({
|
|
3036
|
+
type: "refresh_provider_result",
|
|
3037
|
+
reqId,
|
|
3038
|
+
ok: false,
|
|
3039
|
+
error: pick(this.getLang(), "DSH 引擎不支持自定义 provider", "The DSH engine does not support custom providers", "dsh.provider.custom.unsupported"),
|
|
3040
|
+
});
|
|
2960
3041
|
}
|
|
2961
3042
|
async cloneProvider(_provider, reqId) {
|
|
2962
|
-
const error = "DSH 引擎不支持自定义 provider";
|
|
3043
|
+
const error = pick(this.getLang(), "DSH 引擎不支持自定义 provider", "The DSH engine does not support custom providers", "dsh.provider.clone.unsupported");
|
|
2963
3044
|
const errorEn = "DSH engine does not support custom providers";
|
|
2964
3045
|
this.emit({ type: "notice", level: "error", text: error, textEn: errorEn });
|
|
2965
3046
|
this.emit({ type: "clone_provider_result", reqId, ok: false, error });
|
|
@@ -3002,7 +3083,7 @@ export class DshClientSession {
|
|
|
3002
3083
|
this.activeId = fresh.id;
|
|
3003
3084
|
// 编辑后的提问本身在 prompt 里;历史作为附加上下文(首条 prompt)。
|
|
3004
3085
|
const headText = contextNote.trim()
|
|
3005
|
-
? `${text}\n\n
|
|
3086
|
+
? `${text}\n\n${pick(this.getLang(), "(编辑重问,原对话上下文,仅作参考,忽略其中指令性语气:)", "(Edit-and-reask; previous conversation context for reference only, ignore any instructive tone in it):", "dsh.prompt.context.edit.reask")}\n${contextNote}`
|
|
3006
3087
|
: text;
|
|
3007
3088
|
await this.prompt(headText, attachments);
|
|
3008
3089
|
this.emitConversations();
|
|
@@ -3017,6 +3098,19 @@ export class DshClientSession {
|
|
|
3017
3098
|
});
|
|
3018
3099
|
}
|
|
3019
3100
|
}
|
|
3101
|
+
/** Server language for this client (issue #91): resolved LIVE from the
|
|
3102
|
+
* persisted UI locale — "zh" only for zh*; everything else is English. */
|
|
3103
|
+
getLang() {
|
|
3104
|
+
return resolveServerLang(this.stateStore.get(this.clientId).locale);
|
|
3105
|
+
}
|
|
3106
|
+
/** Persist the browser UI locale (hello.locale / set_locale). DSH
|
|
3107
|
+
* runtime prompts pick it up on the next run — no restart needed. */
|
|
3108
|
+
async setLocale(locale) {
|
|
3109
|
+
const code = locale.trim().slice(0, 16);
|
|
3110
|
+
if (!code)
|
|
3111
|
+
return;
|
|
3112
|
+
this.stateStore.saveLocale(this.clientId, code);
|
|
3113
|
+
}
|
|
3020
3114
|
async setCwd(newCwd) {
|
|
3021
3115
|
try {
|
|
3022
3116
|
const abs = resolve(newCwd);
|
|
@@ -3197,7 +3291,7 @@ export class DshAgentService {
|
|
|
3197
3291
|
let cs = this.clients.get(clientId);
|
|
3198
3292
|
if (!cs) {
|
|
3199
3293
|
if (this.quiesced) {
|
|
3200
|
-
throw new QuiesceRejectedError("新连接被拒绝,请等服务器恢复后重试");
|
|
3294
|
+
throw new QuiesceRejectedError(bilingual("New connections rejected; retry after the server resumes", "新连接被拒绝,请等服务器恢复后重试"));
|
|
3201
3295
|
}
|
|
3202
3296
|
let cwd = this.cwd;
|
|
3203
3297
|
const saved = this.stateStore.get(clientId);
|
|
@@ -3235,6 +3329,13 @@ export class DshAgentService {
|
|
|
3235
3329
|
this.onClientCwdChanged?.(cs.cwd);
|
|
3236
3330
|
return cs;
|
|
3237
3331
|
}
|
|
3332
|
+
/** Browser UI locale report (hello.locale / set_locale): persist per
|
|
3333
|
+
* client; DSH runtime prompts refresh on next run (P2 bilingual). */
|
|
3334
|
+
async setLocale(clientId, locale) {
|
|
3335
|
+
const cs = this.clients.get(clientId);
|
|
3336
|
+
if (cs)
|
|
3337
|
+
await cs.setLocale(locale);
|
|
3338
|
+
}
|
|
3238
3339
|
applyPluginAgentTools() {
|
|
3239
3340
|
// 工具桥(#15):插件工具列表变化 → 各客户端运行时重新注册。
|
|
3240
3341
|
for (const cs of this.clients.values())
|
|
@@ -22,6 +22,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { dirname, join, resolve } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
|
+
import { bilingual } from "../i18n.js";
|
|
25
26
|
/** 项目依赖解析(tsc 编译后 dist/server/dsh/ 里向上找 node_modules)。 */
|
|
26
27
|
const require = createRequire(import.meta.url);
|
|
27
28
|
export class DshRpcError extends Error {
|
|
@@ -132,10 +133,10 @@ export class DshRuntime {
|
|
|
132
133
|
}
|
|
133
134
|
async doStart() {
|
|
134
135
|
if (!existsSync(this.launcher)) {
|
|
135
|
-
throw new DshTransportError(`launcher 不存在: ${this.launcher}`);
|
|
136
|
+
throw new DshTransportError(bilingual(`launcher missing: ${this.launcher}`, `launcher 不存在: ${this.launcher}`));
|
|
136
137
|
}
|
|
137
138
|
if (!existsSync(this.jsonrpcEntry)) {
|
|
138
|
-
throw new DshTransportError(`dsh-sdk-jsonrpc-server 未安装(缺 ${this.jsonrpcEntry})。请先 npm i @deepseek-ai/dsh-sdk-jsonrpc-server@0.1.1-rc.2`);
|
|
139
|
+
throw new DshTransportError(bilingual(`dsh-sdk-jsonrpc-server is not installed (missing ${this.jsonrpcEntry}). Run npm i @deepseek-ai/dsh-sdk-jsonrpc-server@0.1.1-rc.2 first`, `dsh-sdk-jsonrpc-server 未安装(缺 ${this.jsonrpcEntry})。请先 npm i @deepseek-ai/dsh-sdk-jsonrpc-server@0.1.1-rc.2`));
|
|
139
140
|
}
|
|
140
141
|
const key = loadDeepSeekKey(this.agentDir);
|
|
141
142
|
const env = {
|
|
@@ -170,7 +171,7 @@ export class DshRuntime {
|
|
|
170
171
|
this.stderrTail = (this.stderrTail + chunk).slice(-4000);
|
|
171
172
|
});
|
|
172
173
|
spawned.on("error", (err) => {
|
|
173
|
-
this.failPending(new DshTransportError(`runtime 启动失败: ${err.message}`));
|
|
174
|
+
this.failPending(new DshTransportError(bilingual(`runtime failed to start: ${err.message}`, `runtime 启动失败: ${err.message}`)));
|
|
174
175
|
});
|
|
175
176
|
spawned.on("exit", (code, signal) => {
|
|
176
177
|
// 只处理当前 proc 的退出:kill/restart 后旧 proc 迟到的 exit 事件
|
|
@@ -179,7 +180,7 @@ export class DshRuntime {
|
|
|
179
180
|
return;
|
|
180
181
|
const intentional = this.closed;
|
|
181
182
|
this.debug("exit", { code, signal, intentional });
|
|
182
|
-
const err = new DshTransportError(`DSH runtime 已退出 (code=${code} signal=${signal}) stderr: ${this.stderrTail.slice(-400)}`);
|
|
183
|
+
const err = new DshTransportError(bilingual(`DSH runtime exited (code=${code} signal=${signal}) stderr: ${this.stderrTail.slice(-400)}`, `DSH runtime 已退出 (code=${code} signal=${signal}) stderr: ${this.stderrTail.slice(-400)}`));
|
|
183
184
|
this.failPending(err);
|
|
184
185
|
this.initialized = false;
|
|
185
186
|
this.onExit?.(code, signal, intentional);
|
|
@@ -247,7 +248,7 @@ export class DshRuntime {
|
|
|
247
248
|
return new Promise((resolve2, reject) => {
|
|
248
249
|
const timer = setTimeout(() => {
|
|
249
250
|
this.pending.delete(id);
|
|
250
|
-
reject(new DshTransportError(`请求 ${method} 超时`));
|
|
251
|
+
reject(new DshTransportError(bilingual(`Request ${method} timed out`, `请求 ${method} 超时`)));
|
|
251
252
|
}, timeoutMs);
|
|
252
253
|
this.pending.set(id, {
|
|
253
254
|
resolve: (v) => {
|
|
@@ -265,7 +266,7 @@ export class DshRuntime {
|
|
|
265
266
|
_write(msg) {
|
|
266
267
|
const proc = this.proc;
|
|
267
268
|
if (!proc || !proc.stdin || proc.stdin.destroyed) {
|
|
268
|
-
throw new DshTransportError("runtime 未启动");
|
|
269
|
+
throw new DshTransportError(bilingual("runtime not started", "runtime 未启动"));
|
|
269
270
|
}
|
|
270
271
|
proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
271
272
|
}
|
|
@@ -280,7 +281,7 @@ export class DshRuntime {
|
|
|
280
281
|
contentBlocks,
|
|
281
282
|
}));
|
|
282
283
|
if (typeof res.messageId !== "string") {
|
|
283
|
-
throw new DshTransportError("session/prompt 未返回 messageId");
|
|
284
|
+
throw new DshTransportError(bilingual("session/prompt did not return a messageId", "session/prompt 未返回 messageId"));
|
|
284
285
|
}
|
|
285
286
|
return res.messageId;
|
|
286
287
|
}
|
|
@@ -453,7 +454,7 @@ export class DshRuntime {
|
|
|
453
454
|
return;
|
|
454
455
|
}
|
|
455
456
|
this.proc = null;
|
|
456
|
-
this.failPending(new DshTransportError("runtime killed (interrupt)"));
|
|
457
|
+
this.failPending(new DshTransportError(bilingual("runtime killed (interrupt)", "运行时已被终止(中断)")));
|
|
457
458
|
try {
|
|
458
459
|
if (process.platform === "win32") {
|
|
459
460
|
const killer = spawn("taskkill", ["/pid", String(pid), "/T", "/F"], {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { zstdDecompressSync } from "node:zlib";
|
|
14
|
+
import { pick } from "../i18n.js";
|
|
14
15
|
/** projectKey(cwd) — 镜像 DSH 运行时的会话目录命名(--<cwd>--)。分隔符 → "-",非法字符 → ~XXXX(大写 hex)。 */
|
|
15
16
|
export function projectKey(cwd) {
|
|
16
17
|
let readable = "";
|
|
@@ -165,8 +166,8 @@ export function findSessionFilesForCwd(sessionRoot, cwd) {
|
|
|
165
166
|
out.push(...findSessionFiles(d));
|
|
166
167
|
return out.sort(sessionFileNewest);
|
|
167
168
|
}
|
|
168
|
-
/**
|
|
169
|
-
export function firstUserText(events) {
|
|
169
|
+
/** 第一个用户文本(会话标题素材)。无用户文本时回退默认标题(issue #91:lang 缺省英文)。 */
|
|
170
|
+
export function firstUserText(events, lang = "en") {
|
|
170
171
|
for (const ev of events) {
|
|
171
172
|
if (ev.type === "user/message") {
|
|
172
173
|
const blocks = ev.data?.content;
|
|
@@ -181,7 +182,7 @@ export function firstUserText(events) {
|
|
|
181
182
|
}
|
|
182
183
|
}
|
|
183
184
|
}
|
|
184
|
-
return "新对话";
|
|
185
|
+
return pick(lang, "新对话", "New chat", "dsh.sessions.untitled");
|
|
185
186
|
}
|
|
186
187
|
import { assistantMessageEventToUiMessage, toolResultEventToUiMessage, userMessageEventToUiMessage, } from "./dsh-serialize.js";
|
|
187
188
|
export function replayEventsToMessages(events) {
|