pi-web-ui 0.62.0 → 0.63.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.
Files changed (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +504 -497
  3. package/README.zh-CN.md +427 -422
  4. package/bin/pi-web-ui.mjs +1809 -1881
  5. package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
  6. package/deploy/nginx-subpath.conf +88 -88
  7. package/deploy/pi-web-ui-task.xml +54 -54
  8. package/deploy/pi-web-ui.service +31 -31
  9. package/dist/server/agent-service.js +318 -88
  10. package/dist/server/attachments.js +37 -28
  11. package/dist/server/bg-servers.js +6 -1
  12. package/dist/server/client-state.js +15 -7
  13. package/dist/server/control-socket.js +1 -5
  14. package/dist/server/dsh/dsh-agent-service.js +454 -118
  15. package/dist/server/dsh/dsh-client.js +1 -3
  16. package/dist/server/dsh/dsh-sessions.js +4 -14
  17. package/dist/server/dsh/runtime/cordis.yml +1 -1
  18. package/dist/server/dsh/runtime/goal-rpc.mjs +645 -662
  19. package/dist/server/dsh/runtime/launcher.mjs +8 -18
  20. package/dist/server/dsh/runtime/override.patch.yml +9 -9
  21. package/dist/server/dsh/runtime/runtime-root.mjs +7 -11
  22. package/dist/server/ensure-bash.js +2 -5
  23. package/dist/server/files-service.js +42 -36
  24. package/dist/server/goal-service.js +59 -30
  25. package/dist/server/index.js +17 -10
  26. package/dist/server/mcp-bridge.js +11 -5
  27. package/dist/server/model-admin.js +78 -72
  28. package/dist/server/plugin-facilities.js +2 -4
  29. package/dist/server/plugin-updater.js +1 -1
  30. package/dist/server/plugins.js +26 -8
  31. package/dist/server/process-utils.js +4 -4
  32. package/dist/server/protocol-version.js +1 -1
  33. package/dist/server/scm.js +27 -21
  34. package/dist/server/serialize.js +1 -3
  35. package/dist/server/settings-service.js +63 -21
  36. package/dist/server/slash-commands.js +29 -3
  37. package/dist/server/subagent-templates.js +249 -0
  38. package/dist/server/subagents.js +150 -0
  39. package/dist/server/terminals.js +101 -40
  40. package/dist/server/text-sniff.js +4 -24
  41. package/dist/server/update-check.js +2 -4
  42. package/dist/server/uploads.js +1 -3
  43. package/dist/server/vision-bridge.js +2 -7
  44. package/dist/server/wait-subscription-scan.js +102 -21
  45. package/dist/server/webui-context.js +1 -1
  46. package/extensions/webui.ts +190 -192
  47. package/package.json +109 -103
  48. package/web/dist/assets/TerminalPanel-B9vcI7-_.js +2 -0
  49. package/web/dist/assets/index-B8Wv29p5.css +10 -0
  50. package/web/dist/assets/index-CWUWD0rq.js +323 -0
  51. package/web/dist/favicon.svg +8 -8
  52. package/web/dist/index.html +15 -15
  53. package/web/public/favicon.svg +8 -8
  54. package/web/dist/assets/TerminalPanel-BkCWDsIG.js +0 -2
  55. package/web/dist/assets/index-BRVKed7W.js +0 -321
  56. package/web/dist/assets/index-ORTpR6ZQ.css +0 -10
@@ -159,12 +159,15 @@ export class GoalService {
159
159
  goal.feedback = undefined;
160
160
  goal.wizard.active = false;
161
161
  goal.wizard.status = "";
162
+ goal.wizard.statusEn = "";
162
163
  goal.status = "目标已设,等待生成…";
164
+ goal.statusEn = "Goal set, waiting to generate…";
163
165
  this.emitGoalStatus();
164
166
  this.host.emit({
165
167
  type: "notice",
166
168
  level: "info",
167
169
  text: `🎯 已设目标:${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
170
+ textEn: `🎯 Goal set: ${text.slice(0, 80)}${text.length > 80 ? "…" : ""}`,
168
171
  });
169
172
  // Auto-start generation right after setting the goal (unless this setGoal is
170
173
  // the wizard's internal one, which kicks off itself). This makes the direct
@@ -172,7 +175,9 @@ export class GoalService {
172
175
  if (opts?.autoStart !== false) {
173
176
  try {
174
177
  const s = conv.session;
175
- await s.sendUserMessage(`【目标已设定】\n\n${text}\n\n请现在开始实现这个目标。`, { deliverAs: s.isStreaming ? "steer" : "followUp" });
178
+ await s.sendUserMessage(`【目标已设定】\n\n${text}\n\n请现在开始实现这个目标。`, {
179
+ deliverAs: s.isStreaming ? "steer" : "followUp",
180
+ });
176
181
  }
177
182
  catch {
178
183
  // Best-effort; the user can still prompt manually.
@@ -204,6 +209,7 @@ export class GoalService {
204
209
  type: "notice",
205
210
  level: "warning",
206
211
  text: "已有目标调研进行中,请等它完成…",
212
+ textEn: "A goal survey is already running — wait for it to finish…",
207
213
  });
208
214
  return;
209
215
  }
@@ -212,6 +218,7 @@ export class GoalService {
212
218
  type: "notice",
213
219
  level: "warning",
214
220
  text: "正在审查中,无法开始目标调研,请稍等…",
221
+ textEn: "A review is running; cannot start a goal survey yet…",
215
222
  });
216
223
  return;
217
224
  }
@@ -250,7 +257,9 @@ export class GoalService {
250
257
  wgoal.wizard.step = 0;
251
258
  wgoal.wizard.maxSteps = maxSteps;
252
259
  wgoal.wizard.status = "调研中…";
260
+ wgoal.wizard.statusEn = "Scoping…";
253
261
  wgoal.status = "目标调研中…";
262
+ wgoal.statusEn = "Scoping the goal…";
254
263
  this.emitGoalStatus();
255
264
  // Idle-timeout: cancel the wizard if no question is answered within the
256
265
  // window (a stale dialog with no user response must not run forever). A
@@ -288,14 +297,13 @@ export class GoalService {
288
297
  type: "notice",
289
298
  level: "info",
290
299
  text: `🔍 正在围绕需求展开调研:${draft.slice(0, 60)}${draft.length > 60 ? "…" : ""}`,
300
+ textEn: `🔍 Surveying the requirement: ${draft.slice(0, 60)}${draft.length > 60 ? "…" : ""}`,
291
301
  });
292
302
  // The main conversation to show wizard progress cards in.
293
303
  const mainSession = wizardConversation.session;
294
304
  let refinedGoal = "";
295
305
  try {
296
- const wmSpec = opts?.wizardModel
297
- ? this.resolveReviewModel(opts.wizardModel)
298
- : null; // reuse the honest "provider/id" parser
306
+ const wmSpec = opts?.wizardModel ? this.resolveReviewModel(opts.wizardModel) : null; // reuse the honest "provider/id" parser
299
307
  const services = await createAgentSessionServices({
300
308
  cwd: wizardConversation.cwd,
301
309
  agentDir: this.host.agentDir,
@@ -345,6 +353,7 @@ export class GoalService {
345
353
  // the user sees the wizard working even before answering.
346
354
  wgoal.wizard.step = qStep;
347
355
  wgoal.wizard.status = `调研中:请回答第 ${qStep} 题`;
356
+ wgoal.wizard.statusEn = `Scoping: please answer question ${qStep}`;
348
357
  this.emitGoalStatus();
349
358
  try {
350
359
  armIdle();
@@ -384,7 +393,10 @@ export class GoalService {
384
393
  };
385
394
  }
386
395
  // Record the answer in the flow too (instant append, main session idle).
387
- await this.pushWizardCard(mainSession, `↳ 您的回答:${ans}`, { question: params.question, answer: String(ans) });
396
+ await this.pushWizardCard(mainSession, `↳ 您的回答:${ans}`, {
397
+ question: params.question,
398
+ answer: String(ans),
399
+ });
388
400
  return {
389
401
  content: [{ type: "text", text: `用户回答:${ans}` }],
390
402
  details: {},
@@ -445,6 +457,7 @@ export class GoalService {
445
457
  type: "notice",
446
458
  level: "error",
447
459
  text: `目标调研失败:${err.message}`,
460
+ textEn: `Goal survey failed: ${err.message}`,
448
461
  });
449
462
  }
450
463
  finally {
@@ -456,6 +469,7 @@ export class GoalService {
456
469
  wgoal.wizard.active = false;
457
470
  wgoal.wizard.step = 0;
458
471
  wgoal.wizard.status = "";
472
+ wgoal.wizard.statusEn = "";
459
473
  this.wizardSession = null;
460
474
  this.emitGoalStatus();
461
475
  }
@@ -465,6 +479,7 @@ export class GoalService {
465
479
  type: "notice",
466
480
  level: "info",
467
481
  text: `目标调研已取消${ac.signal.reason ? `:${String(ac.signal.reason?.message ?? ac.signal.reason)}` : ""}`,
482
+ textEn: `Goal survey cancelled${ac.signal.reason ? `: ${String(ac.signal.reason?.message ?? ac.signal.reason)}` : ""}`,
468
483
  });
469
484
  this.wizardAbort = null;
470
485
  return;
@@ -474,6 +489,7 @@ export class GoalService {
474
489
  type: "notice",
475
490
  level: "warning",
476
491
  text: "调研未产出有效目标,请重试",
492
+ textEn: "The survey produced no usable goal — retry",
477
493
  });
478
494
  return;
479
495
  }
@@ -482,6 +498,7 @@ export class GoalService {
482
498
  type: "notice",
483
499
  level: "info",
484
500
  text: "已切换对话,目标调研结果已丢弃",
501
+ textEn: "Switched conversations; the survey result was discarded",
485
502
  });
486
503
  return;
487
504
  }
@@ -503,12 +520,15 @@ export class GoalService {
503
520
  type: "notice",
504
521
  level: "info",
505
522
  text: `🎯 调研完成,目标已设为:${refinedGoal.slice(0, 80)}${refinedGoal.length > 80 ? "…" : ""}`,
523
+ textEn: `🎯 Survey done, goal set: ${refinedGoal.slice(0, 80)}${refinedGoal.length > 80 ? "…" : ""}`,
506
524
  });
507
525
  // Kick the main agent into generating right away (no manual "开始吧").
508
526
  // The kick-off is a user message so it appears in the flow and triggers a
509
527
  // normal turn; the finishing agent_end then runs the review loop.
510
528
  try {
511
- await mainSession.sendUserMessage(`【目标已设定】\n\n${wgoal.goal}\n\n请现在开始实现这个目标。`, { deliverAs: mainSession.isStreaming ? "steer" : "followUp" });
529
+ await mainSession.sendUserMessage(`【目标已设定】\n\n${wgoal.goal}\n\n请现在开始实现这个目标。`, {
530
+ deliverAs: mainSession.isStreaming ? "steer" : "followUp",
531
+ });
512
532
  }
513
533
  catch {
514
534
  // Generation kick-off is best-effort; the user can still prompt manually.
@@ -553,7 +573,9 @@ export class GoalService {
553
573
  goal.feedback = undefined;
554
574
  goal.wizard.active = false;
555
575
  goal.wizard.status = "";
576
+ goal.wizard.statusEn = "";
556
577
  goal.status = "";
578
+ goal.statusEn = "";
557
579
  this.emitGoalStatus();
558
580
  // Abort a running wizard for real (✗ in the goal bar while scoping).
559
581
  if (this.wizardOwnerId === this.host.activeConvId()) {
@@ -587,6 +609,7 @@ export class GoalService {
587
609
  g.verdict = "pending";
588
610
  g.feedback = undefined;
589
611
  g.status = "已手动停止,目标审查已中止";
612
+ g.statusEn = "Stopped manually, goal review aborted";
590
613
  this.emitGoalStatus();
591
614
  return "⏹ 已手动停止,目标审查已中止(想继续可重新设定目标)";
592
615
  }
@@ -595,11 +618,7 @@ export class GoalService {
595
618
  // Goal review hook: after the run finished normally, if a goal is
596
619
  // active (and it belonged to the ACTIVE conversation) and we're not
597
620
  // already mid-review, spawn the isolated reviewer.
598
- if (g.goal &&
599
- g.conversationId === conv.id &&
600
- !g.reviewing &&
601
- !conv.wizardRunning &&
602
- !this.host.isDisposed()) {
621
+ if (g.goal && g.conversationId === conv.id && !g.reviewing && !conv.wizardRunning && !this.host.isDisposed()) {
603
622
  void this.runGoalReview(conv);
604
623
  }
605
624
  return null;
@@ -626,9 +645,7 @@ export class GoalService {
626
645
  gitDiff.length > 0 ? gitDiff : "(no staged/committed changes detected)", // eslint-disable-line max-len
627
646
  ``,
628
647
  `This is review round ${round}${maxRounds > 0 ? ` of up to ${maxRounds}` : " (no round cap — keep revising until it passes)"}.`, // eslint-disable-line max-len
629
- ...(customPrompt.trim()
630
- ? [``, `# Additional reviewer instructions`, customPrompt.trim()]
631
- : []),
648
+ ...(customPrompt.trim() ? [``, `# Additional reviewer instructions`, customPrompt.trim()] : []),
632
649
  ``,
633
650
  `Decide: does the work satisfy the goal? If yes, respond with ONLY a JSON object with this exact shape (no markdown fences, no extra text):`, // eslint-disable-line max-len
634
651
  `{"verdict":"pass","feedback":"<one short sentence: what was satisfied>"}`, // eslint-disable-line max-len
@@ -666,10 +683,10 @@ export class GoalService {
666
683
  discardStaleGoalReview(conv, goalGeneration, reviewGeneration) {
667
684
  if (conv.goalReviewGeneration !== reviewGeneration)
668
685
  return;
669
- if (conv.goalGeneration === goalGeneration &&
670
- conv.goal.conversationId === conv.id) {
686
+ if (conv.goalGeneration === goalGeneration && conv.goal.conversationId === conv.id) {
671
687
  conv.goal.reviewing = false;
672
688
  conv.goal.status = "审查已中止,目标已更新或取消";
689
+ conv.goal.statusEn = "Review aborted, goal updated or cleared";
673
690
  this.emitGoalStatus();
674
691
  }
675
692
  }
@@ -680,11 +697,7 @@ export class GoalService {
680
697
  const mainConv = this.host.getConv(conv.id) ?? conv;
681
698
  const mainSession = mainConv.session;
682
699
  const g = conv.goal;
683
- if (!g.goal ||
684
- g.conversationId !== conv.id ||
685
- g.reviewing ||
686
- conv.wizardRunning ||
687
- this.host.isDisposed())
700
+ if (!g.goal || g.conversationId !== conv.id || g.reviewing || conv.wizardRunning || this.host.isDisposed())
688
701
  return;
689
702
  const goalGeneration = conv.goalGeneration;
690
703
  const reviewGeneration = ++conv.goalReviewGeneration;
@@ -701,6 +714,7 @@ export class GoalService {
701
714
  const budget = g.locked ? (g.maxRounds > 0 ? g.maxRounds : Infinity) : 1;
702
715
  if (g.locked && g.maxRounds > 0 && g.round >= budget) {
703
716
  g.status = `已达最大轮数(${budget}),停止审查`;
717
+ g.statusEn = `Max rounds reached (${budget}), stopping review`;
704
718
  g.reviewing = false;
705
719
  this.emitGoalStatus();
706
720
  return;
@@ -710,6 +724,7 @@ export class GoalService {
710
724
  g.verdict = "pending";
711
725
  g.feedback = undefined;
712
726
  g.status = `审查中(第 ${g.round} 轮)…`;
727
+ g.statusEn = `Reviewing (round ${g.round})…`;
713
728
  this.emitGoalStatus();
714
729
  // Collect the review inputs.
715
730
  let finalText = "";
@@ -800,11 +815,13 @@ export class GoalService {
800
815
  const budgetForCard = g.locked ? (Number.isFinite(budget) ? budget : 0) : 1;
801
816
  const verdict = reviewerVerdict;
802
817
  const feedback = reviewerFeedback;
803
- /** Format "round/cap" for user-facing strings; cap 0 → 不限. */
804
- const capFmt = (cap) => cap > 0 ? `第 ${round}/${cap} 轮` : `第 ${round} 轮(不限)`;
818
+ /** Rounds label for the goalStatus* keys — pre-rendered zh + en. */
819
+ const roundsZh = budgetForCard > 0 ? `第 ${round}/${budgetForCard} 轮` : `第 ${round} 轮(不限)`;
820
+ const roundsEn = budgetForCard > 0 ? `Round ${round}/${budgetForCard}` : `Round ${round} (unlimited)`;
805
821
  if (verdict === "pass") {
806
822
  g.status = "✅ 已通过目标审查";
807
- this.host.emit({ type: "notice", level: "info", text: "✅ 目标已通过审查" });
823
+ g.statusEn = "✅ Goal review passed";
824
+ this.host.emit({ type: "notice", level: "info", text: "✅ 目标已通过审查", textEn: "✅ Goal passed review" });
808
825
  g.conversationId = null;
809
826
  g.goal = null; // a passed goal is done and cleared
810
827
  this.emitGoalStatus();
@@ -825,11 +842,13 @@ export class GoalService {
825
842
  // For unlimited (budget=0) isLastRound is always false → keeps revising.
826
843
  const isLastRound = !g.locked ? true : g.maxRounds > 0 && g.round >= g.maxRounds;
827
844
  if (!isLastRound) {
828
- g.status = `本轮不通过,正在把意见交给 agent 修改(${capFmt(budgetForCard)})…`;
845
+ g.status = `本轮不通过,正在把意见交给 agent 修改(${roundsZh})…`;
846
+ g.statusEn = `Round failed, sending feedback to the agent (${roundsEn})…`;
829
847
  this.host.emit({
830
848
  type: "notice",
831
849
  level: "warning",
832
850
  text: `目标审查第 ${g.round}/${budgetForCard > 0 ? budgetForCard : "不限"} 轮未通过,把意见交给 agent 修改…`,
851
+ textEn: `Goal review round ${g.round}/${budgetForCard > 0 ? budgetForCard : "unlimited"} failed; sending feedback to the agent…`,
833
852
  });
834
853
  // Inject the reviewer's feedback into the main session to revise (this IS
835
854
  // the fail review result, as an ordinary user message — no separate card).
@@ -842,6 +861,7 @@ export class GoalService {
842
861
  }
843
862
  catch (err) {
844
863
  g.status = `意见注入失败:${err.message}`;
864
+ g.statusEn = `Feedback injection failed: ${err.message}`;
845
865
  }
846
866
  this.emitGoalStatus();
847
867
  this.host.flushSnapshot();
@@ -850,17 +870,26 @@ export class GoalService {
850
870
  // Rounds exhausted (finite cap reached / single-shot failed). Deliver the
851
871
  // fail result as an ordinary user message (no separate card), like the pass
852
872
  // and revise paths — the review result always lands in the conversation.
853
- g.status =
854
- g.locked && g.maxRounds > 0
855
- ? `已达最大轮数(${g.maxRounds}),目标仍未通过`
856
- : `目标未通过(${capFmt(budgetForCard)})`;
873
+ if (g.locked && g.maxRounds > 0) {
874
+ g.status = `已达最大轮数(${g.maxRounds}),目标仍未通过`;
875
+ g.statusEn = `Max rounds reached (${g.maxRounds}), goal still failing`;
876
+ }
877
+ else {
878
+ g.status = `目标未通过(${roundsZh})`;
879
+ g.statusEn = `Goal failed (${roundsEn})`;
880
+ }
857
881
  try {
858
882
  await mainSession.sendUserMessage(`❌ 目标未通过审查(第 ${round}/${budgetForCard > 0 ? budgetForCard : "不限"} 轮)。\n\n目标:${goalText}\n\n审查意见:${feedback}`, { deliverAs: mainSession.isStreaming ? "steer" : "followUp" });
859
883
  }
860
884
  catch {
861
885
  // Best-effort.
862
886
  }
863
- this.host.emit({ type: "notice", level: "warning", text: "目标未通过审查(已达最大轮数)" });
887
+ this.host.emit({
888
+ type: "notice",
889
+ level: "warning",
890
+ text: "目标未通过审查(已达最大轮数)",
891
+ textEn: "Goal failed review (max rounds reached)",
892
+ });
864
893
  g.conversationId = null;
865
894
  g.goal = null; // loop exhausted — clear the active goal
866
895
  this.emitGoalStatus();
@@ -29,7 +29,7 @@ import compression from "compression";
29
29
  import { WebSocket, WebSocketServer } from "ws";
30
30
  import { VERSION, getAgentDir } from "@earendil-works/pi-coding-agent";
31
31
  import { PROTOCOL_VERSION } from "./protocol-version.js";
32
- import { AgentService, workspacePath, QuiesceRejectedError, } from "./agent-service.js";
32
+ import { AgentService, workspacePath, QuiesceRejectedError } from "./agent-service.js";
33
33
  import { previewKind } from "./text-sniff.js";
34
34
  import { startControlServer } from "./control-socket.js";
35
35
  import { scheduleUploadCleanup } from "./uploads.js";
@@ -201,11 +201,7 @@ function resolvePkgRoot() {
201
201
  // 可选:显式指定 pkgRoot(如部署在自定义目录时),否则按候选路径探测。
202
202
  if (process.env.PI_WEB_PKG_ROOT)
203
203
  return process.env.PI_WEB_PKG_ROOT;
204
- const candidates = [
205
- resolve(here, ".."),
206
- resolve(here, "..", ".."),
207
- resolve(here, "..", "..", ".."),
208
- ];
204
+ const candidates = [resolve(here, ".."), resolve(here, "..", ".."), resolve(here, "..", "..", "..")];
209
205
  for (const c of candidates) {
210
206
  if (existsSync(join(c, "package.json")))
211
207
  return c;
@@ -261,7 +257,9 @@ app.get("/plugins/:id/client/*", (req, res) => {
261
257
  res.setHeader("Cache-Control", "no-cache"); // 开发期改文件即生效
262
258
  res.sendFile(abs, (err) => {
263
259
  if (err && !res.headersSent)
264
- res.status(err.statusCode === 404 ? 404 : 500).end("not found");
260
+ res
261
+ .status(err.statusCode === 404 ? 404 : 500)
262
+ .end("not found");
265
263
  });
266
264
  });
267
265
  /** Set in the env of the replacement child spawned by a self-update restart. */
@@ -296,8 +294,7 @@ else if (process.env[RESTART_CHILD_ENV]) {
296
294
  // complete (Windows: locked files / rollback can leave the global package
297
295
  // without web/dist). Fail loudly with a repair hint instead of serving a
298
296
  // UI-less 404 with no explanation.
299
- console.error("✖ 更新后的安装不完整(缺少 web/dist/index.html)。\n" +
300
- " 请手动执行 npm i -g pi-web-ui@latest 修复后重新启动。");
297
+ console.error("✖ 更新后的安装不完整(缺少 web/dist/index.html)。\n" + " 请手动执行 npm i -g pi-web-ui@latest 修复后重新启动。");
301
298
  process.exit(1);
302
299
  }
303
300
  const httpServer = createServer(app);
@@ -607,6 +604,9 @@ wss.on("connection", (ws) => {
607
604
  case "delete_session":
608
605
  void cs.deleteSession(msg.path);
609
606
  break;
607
+ case "dismiss_conversation":
608
+ void cs.dismissConversation(msg.id);
609
+ break;
610
610
  case "switch_session":
611
611
  void cs.switchSession(msg.path);
612
612
  break;
@@ -712,7 +712,7 @@ wss.on("connection", (ws) => {
712
712
  case "terminal_create": {
713
713
  const tm = cs.getTerminalManager(msg.conversationId);
714
714
  if (tm)
715
- tm.create(msg.terminalId, msg.cwd, msg.cols, msg.rows, cs.getTerminalCwd(msg.conversationId), msg.title);
715
+ tm.create(msg.terminalId, msg.cwd, msg.cols, msg.rows, cs.getTerminalCwd(msg.conversationId), msg.title, msg.locale ? { locale: msg.locale } : undefined);
716
716
  break;
717
717
  }
718
718
  case "terminal_input":
@@ -811,6 +811,12 @@ wss.on("connection", (ws) => {
811
811
  case "save_preset":
812
812
  void cs.savePreset(msg.name);
813
813
  break;
814
+ case "save_subagent_template":
815
+ void cs.saveSubagentTemplate(msg.template);
816
+ break;
817
+ case "delete_subagent_template":
818
+ void cs.deleteSubagentTemplate(msg.name);
819
+ break;
814
820
  case "apply_preset":
815
821
  void cs.applyPreset(msg.name);
816
822
  break;
@@ -883,6 +889,7 @@ wss.on("connection", (ws) => {
883
889
  type: "notice",
884
890
  level: "error",
885
891
  text: `会话初始化失败:${err.message}`,
892
+ textEn: `Failed to initialize session: ${err.message}`,
886
893
  });
887
894
  });
888
895
  return;
@@ -39,7 +39,7 @@ export class McpClient {
39
39
  this.log = log ?? (() => { });
40
40
  }
41
41
  /** 启动子进程 + 握手 + 拉取工具列表。 */
42
- async start(timeoutMs = 8000) {
42
+ async start(_timeoutMs = 8000) {
43
43
  if (this.child)
44
44
  return;
45
45
  const { command, args = [], cwd, env } = this.spec;
@@ -71,7 +71,7 @@ export class McpClient {
71
71
  this.send({ jsonrpc: "2.0", method: "notifications/initialized" });
72
72
  // 仍以协商协议版本调用 tools(多数服务器对新版本容忍,这里用协商结果)
73
73
  void version;
74
- const listed = (await this.request("tools/list", {}) ?? {});
74
+ const listed = ((await this.request("tools/list", {})) ?? {});
75
75
  this.tools = Array.isArray(listed.tools) ? listed.tools : [];
76
76
  this.log(`[mcp:${this.name}] ready, ${this.tools.length} tools`);
77
77
  }
@@ -83,13 +83,19 @@ export class McpClient {
83
83
  async call(name, args, timeoutMs = 60000) {
84
84
  const res = (await this.request("tools/call", { name, arguments: args }, timeoutMs));
85
85
  if (res?.isError) {
86
- const msg = (res.content ?? []).map((c) => c.text ?? "").join("\n").trim() || "MCP 工具错误";
86
+ const msg = (res.content ?? [])
87
+ .map((c) => c.text ?? "")
88
+ .join("\n")
89
+ .trim() || "MCP 工具错误";
87
90
  throw new Error(msg);
88
91
  }
89
92
  // 结构化结果优先,其次文本内容。
90
93
  if (res?.structuredContent !== undefined)
91
94
  return res.structuredContent;
92
- const text = (res.content ?? []).map((c) => c.text ?? "").filter((x) => x).join("\n");
95
+ const text = (res.content ?? [])
96
+ .map((c) => c.text ?? "")
97
+ .filter((x) => x)
98
+ .join("\n");
93
99
  return { content: text, isError: !!res.isError };
94
100
  }
95
101
  /** 关闭:kill 子进程,拒绝所有在途请求。 */
@@ -114,7 +120,7 @@ export class McpClient {
114
120
  stdin.write(JSON.stringify(msg) + "\n");
115
121
  }
116
122
  request(method, params, timeoutMs = 8000) {
117
- const id = (rpcSeq++);
123
+ const id = rpcSeq++;
118
124
  const outId = String(id);
119
125
  return new Promise((resolve, reject) => {
120
126
  const timer = setTimeout(() => {