pi-web-ui 0.63.4 → 0.64.1

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.
@@ -201,7 +201,10 @@ function makeMarkersListTool(getActiveId, markerSvc) {
201
201
  const text = markerSvc.describe(convId, "todo", !!p.includeDeleted);
202
202
  const state = markerSvc.getRawState(convId, "todo");
203
203
  const visible = (state?.tasks ?? []).filter((t) => p.includeDeleted || t.status !== "deleted");
204
- return { content: [{ type: "text", text }], details: { action: "list", todos: visible, nextId: state?.nextId } };
204
+ return {
205
+ content: [{ type: "text", text }],
206
+ details: { action: "list", todos: visible, nextId: state?.nextId },
207
+ };
205
208
  },
206
209
  };
207
210
  }
@@ -592,7 +595,7 @@ export class ClientSession {
592
595
  try {
593
596
  await conv.session.bindExtensions({
594
597
  mode: "rpc",
595
- onError: (err) => this.emit({ type: "notice", level: "error", text: err.error }),
598
+ onError: (err) => this.emit({ type: "notice", level: "error", text: err.error, textEn: err.error }),
596
599
  });
597
600
  }
598
601
  catch {
@@ -604,6 +607,7 @@ export class ClientSession {
604
607
  type: "notice",
605
608
  level: "error",
606
609
  text: `子代理 ${conversationId} 启动失败: ${err instanceof Error ? err.message : String(err)}`,
610
+ textEn: `Subagent ${conversationId} failed to start: ${err instanceof Error ? err.message : String(err)}`,
607
611
  });
608
612
  });
609
613
  this.emitConversations();
@@ -818,7 +822,9 @@ export class ClientSession {
818
822
  getActiveConversationId: () => this.activeId,
819
823
  getSessionManager: (id) => {
820
824
  const c = this.convs.get(id);
821
- return c ? c.session.sessionManager : undefined;
825
+ return c
826
+ ? c.session.sessionManager
827
+ : undefined;
822
828
  },
823
829
  renameConversation: (convId, title) => {
824
830
  // 复用现有重命名路径(内存标题 + 磁盘 session_info)
@@ -905,6 +911,7 @@ export class ClientSession {
905
911
  type: "notice",
906
912
  level: d.type,
907
913
  text: d.message,
914
+ textEn: d.message,
908
915
  });
909
916
  }
910
917
  }
@@ -1175,7 +1182,7 @@ export class ClientSession {
1175
1182
  mode: "rpc",
1176
1183
  uiContext: this.webUi,
1177
1184
  onError: (err) => {
1178
- this.emit({ type: "notice", level: "error", text: err.error });
1185
+ this.emit({ type: "notice", level: "error", text: err.error, textEn: err.error });
1179
1186
  },
1180
1187
  });
1181
1188
  conv.unsubscribe = conv.session.subscribe((event) => this.onEvent(conv, event));
@@ -1432,7 +1439,7 @@ export class ClientSession {
1432
1439
  if (aborted) {
1433
1440
  const stopNotice = this.goalSvc.onAgentEnd(conv, true);
1434
1441
  if (stopNotice) {
1435
- this.emit({ type: "notice", level: "warning", text: stopNotice });
1442
+ this.emit({ type: "notice", level: "warning", text: stopNotice.text, textEn: stopNotice.textEn });
1436
1443
  }
1437
1444
  break;
1438
1445
  }
@@ -2012,7 +2019,7 @@ export class ClientSession {
2012
2019
  const result = await def.run(args, { clientId: this.clientId });
2013
2020
  // 字符串返回值 → 通知条回显给发起人;富展示用 broadcast/sendTo。
2014
2021
  if (typeof result === "string" && result.trim()) {
2015
- this.emit({ type: "notice", level: "info", text: result });
2022
+ this.emit({ type: "notice", level: "info", text: result, textEn: result });
2016
2023
  }
2017
2024
  }
2018
2025
  catch (err) {
@@ -2513,8 +2520,8 @@ export class ClientSession {
2513
2520
  this.bg.push();
2514
2521
  }
2515
2522
  /** 插件设置保存结果等需要从 index.ts 发 notice 时用(emit 是私有的)。 */
2516
- emitNotice(level, text) {
2517
- this.emit({ type: "notice", level, text });
2523
+ emitNotice(level, text, textEn) {
2524
+ this.emit({ type: "notice", level, text, textEn });
2518
2525
  }
2519
2526
  /** Kill ONE background server (by port); returns whether anything was killed. */
2520
2527
  /** Kill ONE background server (by port) OR a plugin task (by taskId). */
@@ -2644,7 +2651,12 @@ export class ClientSession {
2644
2651
  });
2645
2652
  conv.runtime = runtime;
2646
2653
  conv.session = runtime.session;
2647
- this.emit({ type: "notice", level: "warning", text: reason });
2654
+ this.emit({
2655
+ type: "notice",
2656
+ level: "warning",
2657
+ text: reason,
2658
+ textEn: `${reason} (forced reset: run did not terminate)`,
2659
+ });
2648
2660
  await this.bindSession();
2649
2661
  this.emitConversations();
2650
2662
  void this.pushSlashCommands();
@@ -3036,6 +3048,9 @@ export class ClientSession {
3036
3048
  text: stillRunning
3037
3049
  ? "对话仍在后台运行,已停止删除;请等待其结束后再删除"
3038
3050
  : "未能切换到其他对话,已取消删除本次操作",
3051
+ textEn: stillRunning
3052
+ ? "Conversation is still running in the background; delete aborted — wait for it to finish and retry"
3053
+ : "Could not switch to another conversation; delete cancelled",
3039
3054
  });
3040
3055
  return;
3041
3056
  }
@@ -3142,11 +3157,21 @@ export class ClientSession {
3142
3157
  async dismissConversation(id) {
3143
3158
  const conv = this.convs.get(id);
3144
3159
  if (!conv) {
3145
- this.emit({ type: "notice", level: "warning", text: "该对话不存在或已关闭" });
3160
+ this.emit({
3161
+ type: "notice",
3162
+ level: "warning",
3163
+ text: "该对话不存在或已关闭",
3164
+ textEn: "This conversation does not exist or is already closed",
3165
+ });
3146
3166
  return;
3147
3167
  }
3148
3168
  if (id === this.activeId) {
3149
- this.emit({ type: "notice", level: "warning", text: "当前对话不能直接移出,请先切换到其他对话" });
3169
+ this.emit({
3170
+ type: "notice",
3171
+ level: "warning",
3172
+ text: "当前对话不能直接移出,请先切换到其他对话",
3173
+ textEn: "The active conversation cannot be removed directly — switch to another conversation first",
3174
+ });
3150
3175
  return;
3151
3176
  }
3152
3177
  if (!conv.listed) {
@@ -3171,6 +3196,7 @@ export class ClientSession {
3171
3196
  type: "notice",
3172
3197
  level: "warning",
3173
3198
  text: `对话「${conv.title}」仍在运行中,请先等待结束或点击停止后再移出`,
3199
+ textEn: `Conversation "${conv.title}" is still running — wait for it to finish or press Stop before removing`,
3174
3200
  });
3175
3201
  }
3176
3202
  else if (conv.terminals.list().length > 0) {
@@ -3178,6 +3204,7 @@ export class ClientSession {
3178
3204
  type: "notice",
3179
3205
  level: "warning",
3180
3206
  text: `对话「${conv.title}」还有未关闭的终端,请先关闭终端后再移出`,
3207
+ textEn: `Conversation "${conv.title}" still has open terminals — close them before removing`,
3181
3208
  });
3182
3209
  }
3183
3210
  else {
@@ -3185,6 +3212,7 @@ export class ClientSession {
3185
3212
  type: "notice",
3186
3213
  level: "warning",
3187
3214
  text: `对话「${conv.title}」暂时无法移出(存在待处理的后台任务/审查)`,
3215
+ textEn: `Conversation "${conv.title}" cannot be removed right now (pending background task/review)`,
3188
3216
  });
3189
3217
  }
3190
3218
  return;
@@ -3484,6 +3512,9 @@ export class ClientSession {
3484
3512
  async uploadFile(relDir, name, data) {
3485
3513
  return this.files.uploadFile(relDir, name, data);
3486
3514
  }
3515
+ async makeDir(relPath) {
3516
+ return this.files.makeDir(relPath);
3517
+ }
3487
3518
  async cycleModel() {
3488
3519
  try {
3489
3520
  const result = await this.session.cycleModel();
@@ -3565,7 +3596,7 @@ export class ClientSession {
3565
3596
  this.removeConversation(displaced.id);
3566
3597
  for (const d of newRuntime.diagnostics) {
3567
3598
  if (d.type !== "info") {
3568
- this.emit({ type: "notice", level: d.type, text: d.message });
3599
+ this.emit({ type: "notice", level: d.type, text: d.message, textEn: d.message });
3569
3600
  }
3570
3601
  }
3571
3602
  await this.bindSession();
@@ -3725,17 +3756,17 @@ export class ClientSession {
3725
3756
  }
3726
3757
  /** Push the user command list (.pi/commands.json) to the client. */
3727
3758
  async listCommands() {
3728
- const { commands, path, warning } = await loadCommands(this.cwd);
3759
+ const { commands, path, warning, warningEn } = await loadCommands(this.cwd);
3729
3760
  if (warning) {
3730
- this.emit({ type: "notice", level: "warning", text: warning });
3761
+ this.emit({ type: "notice", level: "warning", text: warning, textEn: warningEn });
3731
3762
  }
3732
3763
  this.emit({ type: "commands", commands, path });
3733
3764
  }
3734
3765
  /** Persist the user command list (.pi/commands.json). */
3735
3766
  async saveCommands(commands) {
3736
- const { path, error } = await saveCommandsFile(this.cwd, commands);
3767
+ const { path, error, errorEn } = await saveCommandsFile(this.cwd, commands);
3737
3768
  if (error) {
3738
- this.emit({ type: "notice", level: "error", text: error });
3769
+ this.emit({ type: "notice", level: "error", text: error, textEn: errorEn });
3739
3770
  return;
3740
3771
  }
3741
3772
  this.emit({ type: "commands", commands, path });
@@ -1029,8 +1029,8 @@ export class DshClientSession {
1029
1029
  for (const sink of [...this.sinks])
1030
1030
  sink(msg);
1031
1031
  }
1032
- emitNotice(level, text) {
1033
- this.emit({ type: "notice", level, text });
1032
+ emitNotice(level, text, textEn) {
1033
+ this.emit({ type: "notice", level, text, textEn });
1034
1034
  }
1035
1035
  pushTerminals() {
1036
1036
  for (const conv of this.convs.values()) {
@@ -1308,6 +1308,9 @@ export class DshClientSession {
1308
1308
  text: hadGoal
1309
1309
  ? "已新建分支继续对话(DSH 引擎不支持原地续聊旧会话);原目标已随旧会话存档,如需继续请重新设置目标"
1310
1310
  : "已新建分支继续对话(DSH 引擎不支持原地续聊旧会话)",
1311
+ textEn: hadGoal
1312
+ ? "Started a branch to continue (DSH engine cannot resume an old session in place); the old goal was archived with it — set a new goal to continue"
1313
+ : "Started a branch to continue (DSH engine cannot resume an old session in place)",
1311
1314
  });
1312
1315
  return fork;
1313
1316
  }
@@ -1622,11 +1625,21 @@ export class DshClientSession {
1622
1625
  async dismissConversation(id) {
1623
1626
  const conv = this.convs.get(id);
1624
1627
  if (!conv) {
1625
- this.emit({ type: "notice", level: "warning", text: "该对话不存在或已关闭" });
1628
+ this.emit({
1629
+ type: "notice",
1630
+ level: "warning",
1631
+ text: "该对话不存在或已关闭",
1632
+ textEn: "This conversation does not exist or is already closed",
1633
+ });
1626
1634
  return;
1627
1635
  }
1628
1636
  if (id === this.activeId) {
1629
- this.emit({ type: "notice", level: "warning", text: "当前对话不能直接移出,请先切换到其他对话" });
1637
+ this.emit({
1638
+ type: "notice",
1639
+ level: "warning",
1640
+ text: "当前对话不能直接移出,请先切换到其他对话",
1641
+ textEn: "The active conversation cannot be removed directly — switch to another conversation first",
1642
+ });
1630
1643
  return;
1631
1644
  }
1632
1645
  if (!conv.listed) {
@@ -1638,11 +1651,17 @@ export class DshClientSession {
1638
1651
  type: "notice",
1639
1652
  level: "warning",
1640
1653
  text: `对话「${conv.title}」仍在运行中,请先等待结束或停止后再移出`,
1654
+ textEn: `Conversation "${conv.title}" is still running — wait for it to finish or press Stop before removing`,
1641
1655
  });
1642
1656
  return;
1643
1657
  }
1644
1658
  if (conv.terminals.list().length > 0) {
1645
- this.emit({ type: "notice", level: "warning", text: `对话「${conv.title}」还有未关闭的终端` });
1659
+ this.emit({
1660
+ type: "notice",
1661
+ level: "warning",
1662
+ text: `对话「${conv.title}」还有未关闭的终端,请先关闭终端后再移出`,
1663
+ textEn: `Conversation "${conv.title}" still has open terminals — close them before removing`,
1664
+ });
1646
1665
  return;
1647
1666
  }
1648
1667
  this.removeConversation(id);
@@ -1895,6 +1914,9 @@ export class DshClientSession {
1895
1914
  async completePath(input) {
1896
1915
  await this.files.completePath(input);
1897
1916
  }
1917
+ async makeDir(input) {
1918
+ await this.files.makeDir(input);
1919
+ }
1898
1920
  // -----------------------------------------------------------------------
1899
1921
  // 模型 / 思考
1900
1922
  // -----------------------------------------------------------------------
@@ -2665,7 +2687,7 @@ export class DshClientSession {
2665
2687
  try {
2666
2688
  const result = await def.run(args, { clientId: this.clientId });
2667
2689
  if (typeof result === "string" && result.trim()) {
2668
- this.emit({ type: "notice", level: "info", text: result });
2690
+ this.emit({ type: "notice", level: "info", text: result, textEn: result });
2669
2691
  }
2670
2692
  }
2671
2693
  catch (err) {
@@ -2884,7 +2906,8 @@ export class DshClientSession {
2884
2906
  }
2885
2907
  async cloneProvider(_provider, reqId) {
2886
2908
  const error = "DSH 引擎不支持自定义 provider";
2887
- this.emit({ type: "notice", level: "error", text: error });
2909
+ const errorEn = "DSH engine does not support custom providers";
2910
+ this.emit({ type: "notice", level: "error", text: error, textEn: errorEn });
2888
2911
  this.emit({ type: "clone_provider_result", reqId, ok: false, error });
2889
2912
  }
2890
2913
  // -----------------------------------------------------------------------
@@ -657,14 +657,14 @@ export class FilesService {
657
657
  * for the target dir (the recursive watcher may not cover it on posix).
658
658
  */
659
659
  async uploadFile(relDir, name, data) {
660
- const emitErr = (text) => this.host.emit({ type: "notice", level: "error", text });
660
+ const emitErr = (text, textEn) => this.host.emit({ type: "notice", level: "error", text, textEn });
661
661
  try {
662
662
  const root = this.host.getCwd();
663
663
  let wp;
664
664
  if (relDir) {
665
665
  wp = workspacePath(resolve(root), relDir);
666
666
  if (!wp) {
667
- emitErr(`路径超出工作区:${relDir}`);
667
+ emitErr(`路径超出工作区:${relDir}`, `Path outside workspace: ${relDir}`);
668
668
  return;
669
669
  }
670
670
  }
@@ -678,16 +678,16 @@ export class FilesService {
678
678
  const abs = resolve(wp.abs, safe);
679
679
  const rawRel = relative(root, abs);
680
680
  if (rawRel.startsWith("..") || rawRel.includes(`${sep}..`)) {
681
- emitErr(`文件名不合法:${name}`);
681
+ emitErr(`文件名不合法:${name}`, `Invalid file name: ${name}`);
682
682
  return;
683
683
  }
684
684
  const buf = Buffer.from(data, "base64");
685
685
  if (buf.length === 0) {
686
- emitErr(`空文件:${name}`);
686
+ emitErr(`空文件:${name}`, `Empty file: ${name}`);
687
687
  return;
688
688
  }
689
689
  if (buf.length > MAX_UPLOAD_BYTES) {
690
- emitErr(`文件过大:${name}(上限 ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)}MB)`);
690
+ emitErr(`文件过大:${name}(上限 ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)}MB)`, `File too large: ${name} (max ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)}MB)`);
691
691
  return;
692
692
  }
693
693
  mkdirSync(wp.abs, { recursive: true });
@@ -707,7 +707,49 @@ export class FilesService {
707
707
  });
708
708
  }
709
709
  catch (err) {
710
- emitErr(`上传文件失败:${err.message}`);
710
+ emitErr(`上传文件失败:${err.message}`, `Upload failed: ${err.message}`);
711
+ }
712
+ }
713
+ /**
714
+ * Create a folder. Accepts absolute, ~-prefixed or session-relative paths
715
+ * (same expansion rules as completePath — the cwd picker may browse outside
716
+ * the session root, and set_cwd itself accepts any directory). Answers
717
+ * with a notice; the picker refreshes its listing on its own.
718
+ */
719
+ async makeDir(input) {
720
+ try {
721
+ const fs = await import("node:fs/promises");
722
+ const { resolve, sep, isAbsolute } = await import("node:path");
723
+ const { homedir } = await import("node:os");
724
+ const home = homedir();
725
+ let expanded = input.trim();
726
+ if (!expanded)
727
+ throw new Error("路径为空");
728
+ if (expanded === "~" || expanded === "~\\") {
729
+ expanded = home;
730
+ }
731
+ else if (expanded.startsWith("~/") || expanded.startsWith("~\\")) {
732
+ expanded = home + sep + expanded.slice(2);
733
+ }
734
+ else if (!isAbsolute(expanded)) {
735
+ expanded = resolve(this.host.getCwd(), expanded);
736
+ }
737
+ const abs = resolve(expanded);
738
+ await fs.mkdir(abs, { recursive: true });
739
+ this.host.emit({
740
+ type: "notice",
741
+ level: "info",
742
+ text: `已创建文件夹:${abs}`,
743
+ textEn: `Folder created: ${abs}`,
744
+ });
745
+ }
746
+ catch (err) {
747
+ this.host.emit({
748
+ type: "notice",
749
+ level: "error",
750
+ text: `创建文件夹失败:${err.message}`,
751
+ textEn: `Failed to create folder: ${err.message}`,
752
+ });
711
753
  }
712
754
  }
713
755
  /**
@@ -611,7 +611,10 @@ export class GoalService {
611
611
  g.status = "已手动停止,目标审查已中止";
612
612
  g.statusEn = "Stopped manually, goal review aborted";
613
613
  this.emitGoalStatus();
614
- return "⏹ 已手动停止,目标审查已中止(想继续可重新设定目标)";
614
+ return {
615
+ text: "⏹ 已手动停止,目标审查已中止(想继续可重新设定目标)",
616
+ textEn: "⏹ Stopped manually, goal review aborted (set a new goal to continue)",
617
+ };
615
618
  }
616
619
  return null;
617
620
  }
@@ -664,6 +664,9 @@ wss.on("connection", (ws) => {
664
664
  case "complete_path":
665
665
  void cs.completePath(msg.path);
666
666
  break;
667
+ case "make_dir":
668
+ void cs.makeDir(msg.path);
669
+ break;
667
670
  case "check_update":
668
671
  void cs.checkUpdate();
669
672
  break;
@@ -800,10 +803,10 @@ wss.on("connection", (ws) => {
800
803
  case "plugin_settings": {
801
804
  const r = pluginMgr.savePluginSettings(msg.pluginId, msg.values ?? {});
802
805
  if (r.error) {
803
- cs?.emitNotice("error", `插件设置保存失败:${r.error}`);
806
+ cs?.emitNotice("error", `插件设置保存失败:${r.error}`, `Failed to save plugin settings: ${r.error}`);
804
807
  }
805
808
  else {
806
- cs?.emitNotice("info", "插件设置已保存");
809
+ cs?.emitNotice("info", "插件设置已保存", "Plugin settings saved");
807
810
  }
808
811
  break;
809
812
  }
@@ -33,7 +33,8 @@ export class MarkerService {
33
33
  return false;
34
34
  if ((name === "rename" || name === "title") && this.settings.disabledMarkers.includes("conv"))
35
35
  return false;
36
- if (name === "conv" && (this.settings.disabledMarkers.includes("rename") || this.settings.disabledMarkers.includes("title")))
36
+ if (name === "conv" &&
37
+ (this.settings.disabledMarkers.includes("rename") || this.settings.disabledMarkers.includes("title")))
37
38
  return false;
38
39
  return !this.settings.disabledMarkers.includes(name);
39
40
  }
@@ -154,8 +155,8 @@ export class MarkerService {
154
155
  const state = getOrInit(token.tool);
155
156
  const ctx = {
156
157
  conversationId,
157
- notify: (msg, level) => {
158
- this.host.emit({ type: "notice", level: level ?? "info", text: msg });
158
+ notify: (msg, level, msgEn) => {
159
+ this.host.emit({ type: "notice", level: level ?? "info", text: msg, textEn: msgEn });
159
160
  },
160
161
  renameConversation: (title) => {
161
162
  this.host.renameConversation(conversationId, title);
@@ -182,7 +183,12 @@ export class MarkerService {
182
183
  dirty.add(token.tool);
183
184
  }
184
185
  else if (result.error) {
185
- this.host.emit({ type: "notice", level: "warning", text: `[${token.tool}] ${result.error}` });
186
+ this.host.emit({
187
+ type: "notice",
188
+ level: "warning",
189
+ text: `[${token.tool}] ${result.error}`,
190
+ textEn: `[${token.tool}] ${result.error}`,
191
+ });
186
192
  }
187
193
  }
188
194
  for (const ns of dirty) {
@@ -43,7 +43,7 @@ export const renameMarker = {
43
43
  return { applied: false, error: "当前环境不支持重命名" };
44
44
  try {
45
45
  ctx.renameConversation(title);
46
- ctx.notify(`已重命名为:${title}`, "info");
46
+ ctx.notify(`已重命名为:${title}`, "info", `Renamed to: ${title}`);
47
47
  return { applied: true, feedback: `renamed to "${title}"` };
48
48
  }
49
49
  catch (e) {
@@ -56,9 +56,7 @@ export const renameMarker = {
56
56
  /** 别名:[[rename:set:标题]] 等同 [[conv:rename:标题]],方便模型直觉书写。 */
57
57
  export const renameAliasMarker = {
58
58
  name: "rename",
59
- guidance: [
60
- "- [[rename:set:<新标题>]] 同 [[conv:rename:<新标题>]]:重命名当前对话。",
61
- ],
59
+ guidance: ["- [[rename:set:<新标题>]] 同 [[conv:rename:<新标题>]]:重命名当前对话。"],
62
60
  async apply(token, ctx) {
63
61
  // 兼容任意 op:只要能取到标题就重命名
64
62
  const title = extractTitle(token) || token.op?.trim() || "";
@@ -71,7 +69,7 @@ export const renameAliasMarker = {
71
69
  return { applied: false, error: "当前环境不支持重命名" };
72
70
  try {
73
71
  ctx.renameConversation(trimmed);
74
- ctx.notify(`已重命名为:${trimmed}`, "info");
72
+ ctx.notify(`已重命名为:${trimmed}`, "info", `Renamed to: ${trimmed}`);
75
73
  return { applied: true, feedback: `renamed to "${trimmed}"` };
76
74
  }
77
75
  catch (e) {
@@ -94,7 +92,7 @@ export const titleAliasMarker = {
94
92
  if (!ctx.renameConversation)
95
93
  return { applied: false, error: "当前环境不支持重命名" };
96
94
  ctx.renameConversation(title.slice(0, 80));
97
- ctx.notify(`已重命名为:${title.slice(0, 80)}`, "info");
95
+ ctx.notify(`已重命名为:${title.slice(0, 80)}`, "info", `Renamed to: ${title.slice(0, 80)}`);
98
96
  return { applied: true, feedback: `renamed` };
99
97
  },
100
98
  overlay: undefined,
@@ -21,7 +21,9 @@ function findService(state, id) {
21
21
  return state.services.find((s) => s.id === id);
22
22
  }
23
23
  function lineOf(s) {
24
- const meta = [s.pid !== undefined ? `pid ${s.pid}` : "", s.port !== undefined ? `:${s.port}` : ""].filter(Boolean).join(" ");
24
+ const meta = [s.pid !== undefined ? `pid ${s.pid}` : "", s.port !== undefined ? `:${s.port}` : ""]
25
+ .filter(Boolean)
26
+ .join(" ");
25
27
  return `${s.stopped ? "[x]" : "[ ]"} #${s.id}: ${s.name}${meta ? ` (${meta})` : ""}${s.command ? ` — ${s.command}` : ""}`;
26
28
  }
27
29
  export function describeServices(state) {
@@ -53,7 +55,9 @@ export const servicesMarker = {
53
55
  stopped: false,
54
56
  };
55
57
  state.services.push(svc);
56
- const meta = [svc.pid !== undefined ? `pid ${svc.pid}` : "", svc.port !== undefined ? `:${svc.port}` : ""].filter(Boolean).join(" ");
58
+ const meta = [svc.pid !== undefined ? `pid ${svc.pid}` : "", svc.port !== undefined ? `:${svc.port}` : ""]
59
+ .filter(Boolean)
60
+ .join(" ");
57
61
  return { applied: true, feedback: `Registered #${svc.id}: ${name}${meta ? ` (${meta})` : ""}` };
58
62
  }
59
63
  case "stop": {
@@ -16,10 +16,14 @@ function parseId(raw) {
16
16
  }
17
17
  function formatStatus(s) {
18
18
  switch (s) {
19
- case "pending": return "pending";
20
- case "in_progress": return "in_progress";
21
- case "completed": return "completed";
22
- default: return s;
19
+ case "pending":
20
+ return "pending";
21
+ case "in_progress":
22
+ return "in_progress";
23
+ case "completed":
24
+ return "completed";
25
+ default:
26
+ return s;
23
27
  }
24
28
  }
25
29
  export function describeTodos(state, includeDeleted = false) {
@@ -575,19 +575,19 @@ export class ModelAdminService {
575
575
  */
576
576
  async cloneProvider(providerId, reqId) {
577
577
  const pid = providerId.trim();
578
- const fail = (error) => {
579
- this.host.emit({ type: "notice", level: "error", text: error });
578
+ const fail = (error, errorEn) => {
579
+ this.host.emit({ type: "notice", level: "error", text: error, textEn: errorEn });
580
580
  this.host.emit({ type: "clone_provider_result", reqId, ok: false, error });
581
581
  };
582
582
  try {
583
583
  if (!pid) {
584
- fail("请填写服务商 ID");
584
+ fail("请填写服务商 ID", "Enter a provider ID");
585
585
  return;
586
586
  }
587
587
  const mr = this.host.modelRuntime();
588
588
  const p = mr.getProvider(pid);
589
589
  if (!p) {
590
- fail(`供应商 ${pid} 不存在`);
590
+ fail(`供应商 ${pid} 不存在`, `Provider ${pid} does not exist`);
591
591
  return;
592
592
  }
593
593
  const noBaseUrl = !p.baseUrl;
@@ -617,7 +617,7 @@ export class ModelAdminService {
617
617
  models = readModels();
618
618
  }
619
619
  if (models.length === 0) {
620
- fail(`${pid} 的模型列表为空,无法复制(请稍后重试)`);
620
+ fail(`${pid} 的模型列表为空,无法复制(请稍后重试)`, `Model list for ${pid} is empty, cannot clone (retry later)`);
621
621
  return;
622
622
  }
623
623
  // 供应商级 api 取占比最高,模型保留全量去重(避免 muse-spark 被过滤)
@@ -652,11 +652,14 @@ export class ModelAdminService {
652
652
  text: noBaseUrl
653
653
  ? `📋 已复制 ${pid} → ${newId}(${kept.length} 个模型),该供应商无远程 baseUrl,已生成模板请手动填写 baseUrl 和新的 API 密钥后保存`
654
654
  : `📋 已复制 ${pid} → ${newId}(${kept.length} 个模型),请填入新的 API 密钥后保存`,
655
+ textEn: noBaseUrl
656
+ ? `📋 Cloned ${pid} → ${newId} (${kept.length} models); this provider has no remote baseUrl — template generated, fill in baseUrl and a new API key, then save`
657
+ : `📋 Cloned ${pid} → ${newId} (${kept.length} models); fill in the new API key, then save`,
655
658
  });
656
659
  this.host.emit({ type: "clone_provider_result", reqId, ok: true, config, configs: [config] });
657
660
  }
658
661
  catch (err) {
659
- fail(`复制服务商失败:${err.message}`);
662
+ fail(`复制服务商失败:${err.message}`, `Failed to clone provider: ${err.message}`);
660
663
  }
661
664
  this.host.flushSnapshot();
662
665
  }
@@ -1015,6 +1018,9 @@ export class ModelAdminService {
1015
1018
  text: added > 0
1016
1019
  ? `🔄 已刷新 ${pid}:新增 ${added} 个模型,共 ${merged.length} 个`
1017
1020
  : `🔄 已刷新 ${pid}:无新增模型(共 ${merged.length} 个)`,
1021
+ textEn: added > 0
1022
+ ? `🔄 Refreshed ${pid}: ${added} new models, ${merged.length} total`
1023
+ : `🔄 Refreshed ${pid}: no new models (${merged.length} total)`,
1018
1024
  });
1019
1025
  return done(true, { added, total: merged.length });
1020
1026
  }
@@ -308,7 +308,7 @@ export class PluginManager {
308
308
  if (prev === key)
309
309
  return; // 同版本能力清单,不再打扰
310
310
  const list = perms.length ? perms.join(", ") : "无";
311
- this.notifyAll(perms.length ? "warning" : "info", `插件「${info.name}」已激活(${prev ? "能力清单变更" : "首次安装"};声明能力:${list})——请确认来源可信`);
311
+ this.notifyAll(perms.length ? "warning" : "info", `插件「${info.name}」已激活(${prev ? "能力清单变更" : "首次安装"};声明能力:${list})——请确认来源可信`, `Plugin "${info.name}" activated (${prev ? "capability list changed" : "first install"}; declared: ${list}) — verify the source is trusted`);
312
312
  writeFileSync(markerFile, JSON.stringify({ v: 1, key, perms }), "utf8");
313
313
  }
314
314
  catch (err) {
@@ -344,8 +344,8 @@ export class PluginManager {
344
344
  this.deliverAll({ type: "plugin_data", pluginId, payload });
345
345
  }
346
346
  /** 系统通知:发给所有 socket(复用 notice 消息,前端 toast 展示)。 */
347
- notifyAll(level, text) {
348
- this.deliverAll({ type: "notice", level, text });
347
+ notifyAll(level, text, textEn) {
348
+ this.deliverAll({ type: "notice", level, text, textEn });
349
349
  }
350
350
  /** 给指定客户端定向发一条插件消息;找不到该 socket 时静默忽略。 */
351
351
  sendTo(clientId, pluginId, payload) {
@@ -708,7 +708,7 @@ export class PluginManager {
708
708
  const self = this; // 对象字面量 getter 里不能用插件宿主的 this (oxlint no-this-alias: 誤報, getter closure 需要 host)
709
709
  const host = {
710
710
  broadcast: (payload) => this.broadcast(info.id, payload),
711
- notify: (level, text) => this.notifyAll(level, text),
711
+ notify: (level, text, textEn) => this.notifyAll(level, text, textEn),
712
712
  sendTo: (clientId, payload) => this.sendTo(clientId, info.id, payload),
713
713
  onMessage: (h) => {
714
714
  handlers.add(h);
@@ -212,7 +212,11 @@ export class SettingsService {
212
212
  presets: this.presets.map((p) => ({ ...p })),
213
213
  ...(this.host.getMarkerState
214
214
  ? this.host.getMarkerState()
215
- : { markersEnabled: true, disabledMarkers: [], markers: [] }),
215
+ : {
216
+ markersEnabled: true,
217
+ disabledMarkers: [],
218
+ markers: [],
219
+ }),
216
220
  subagentTemplates: this.templates.list(),
217
221
  subagentDefaultTemplates: DEFAULT_TEMPLATES.map((t) => t.name),
218
222
  },
@@ -174,6 +174,9 @@ export class SlashCommandsService {
174
174
  text: current
175
175
  ? `当前模型:${current.name}(${current.provider}/${current.id})。用法:/model <名称>`
176
176
  : `用法:/model <名称>`,
177
+ textEn: current
178
+ ? `Current model: ${current.name} (${current.provider}/${current.id}). Usage: /model <name>`
179
+ : `Usage: /model <name>`,
177
180
  });
178
181
  return true;
179
182
  }