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
@@ -11,8 +11,8 @@
11
11
  * 经 ModelAdminHost 与 ClientSession 解耦(同 settings/goal/slash 服务模式)。
12
12
  * UI 文案直接中文(服务端 notice 约定)。apiKey/headers 绝不下发浏览器。
13
13
  */
14
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
- import { dirname, join } from "node:path";
14
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
16
  /** Strip // and /* *\/ comments without touching string literals (URLs contain //). */
17
17
  function stripJsonComments(src) {
18
18
  let out = "";
@@ -64,9 +64,7 @@ function boolMeta(v) {
64
64
  return typeof v === "boolean" ? v : undefined;
65
65
  }
66
66
  function strArrMeta(v) {
67
- return Array.isArray(v)
68
- ? v.filter((x) => typeof x === "string")
69
- : undefined;
67
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : undefined;
70
68
  }
71
69
  /** Best-effort extraction of model metadata from an OpenAI-compatible
72
70
  * /models `data[]` item. Most endpoints only return `{ id }` — the extra
@@ -77,11 +75,8 @@ function parseOpenAiModel(m) {
77
75
  const r = (m ?? {});
78
76
  const id = typeof r.id === "string" ? r.id : "";
79
77
  const name = (typeof r.name === "string" && r.name.trim() ? r.name : undefined) ??
80
- (typeof r.display_name === "string" && r.display_name.trim()
81
- ? r.display_name
82
- : undefined);
83
- const modalities = strArrMeta(r.modalities) ??
84
- strArrMeta(r.input_modalities);
78
+ (typeof r.display_name === "string" && r.display_name.trim() ? r.display_name : undefined);
79
+ const modalities = strArrMeta(r.modalities) ?? strArrMeta(r.input_modalities);
85
80
  const vision = modalities?.includes("image") === true ||
86
81
  boolMeta(r.supports_vision) === true ||
87
82
  boolMeta(r.vision) === true ||
@@ -89,13 +84,8 @@ function parseOpenAiModel(m) {
89
84
  const reasoning = boolMeta(r.reasoning) === true ||
90
85
  boolMeta(r.supports_reasoning) === true ||
91
86
  modalities?.includes("reasoning") === true;
92
- const contextWindow = numMeta(r.context_window) ??
93
- numMeta(r.context_length) ??
94
- numMeta(r.max_model_len) ??
95
- numMeta(r.max_context_length);
96
- const maxTokens = numMeta(r.max_tokens) ??
97
- numMeta(r.max_output_tokens) ??
98
- numMeta(r.max_completion_tokens);
87
+ const contextWindow = numMeta(r.context_window) ?? numMeta(r.context_length) ?? numMeta(r.max_model_len) ?? numMeta(r.max_context_length);
88
+ const maxTokens = numMeta(r.max_tokens) ?? numMeta(r.max_output_tokens) ?? numMeta(r.max_completion_tokens);
99
89
  return {
100
90
  id,
101
91
  ...(name ? { name } : {}),
@@ -116,12 +106,8 @@ function parseGoogleModel(m) {
116
106
  return {
117
107
  id,
118
108
  ...(displayName && displayName !== id ? { name: displayName } : {}),
119
- ...(numMeta(r.inputTokenLimit)
120
- ? { contextWindow: numMeta(r.inputTokenLimit) }
121
- : {}),
122
- ...(numMeta(r.outputTokenLimit)
123
- ? { maxTokens: numMeta(r.outputTokenLimit) }
124
- : {}),
109
+ ...(numMeta(r.inputTokenLimit) ? { contextWindow: numMeta(r.inputTokenLimit) } : {}),
110
+ ...(numMeta(r.outputTokenLimit) ? { maxTokens: numMeta(r.outputTokenLimit) } : {}),
125
111
  };
126
112
  }
127
113
  export class ModelAdminService {
@@ -146,14 +132,10 @@ export class ModelAdminService {
146
132
  const parsed = JSON.parse(readFileSync(this.providerKeysPath(), "utf8"));
147
133
  const out = {};
148
134
  for (const [pid, entry] of Object.entries(parsed)) {
149
- const keys = Array.isArray(entry?.keys)
150
- ? entry.keys.filter((k) => k?.name && k?.apiKey)
151
- : [];
135
+ const keys = Array.isArray(entry?.keys) ? entry.keys.filter((k) => k?.name && k?.apiKey) : [];
152
136
  if (!pid || keys.length === 0)
153
137
  continue;
154
- const activeKeyName = entry.activeKeyName && keys.some((k) => k.name === entry.activeKeyName)
155
- ? entry.activeKeyName
156
- : keys[0].name;
138
+ const activeKeyName = entry.activeKeyName && keys.some((k) => k.name === entry.activeKeyName) ? entry.activeKeyName : keys[0].name;
157
139
  out[pid] = { activeKeyName, keys };
158
140
  }
159
141
  return out;
@@ -267,11 +249,11 @@ export class ModelAdminService {
267
249
  const pid = provider.trim();
268
250
  const key = apiKey.trim();
269
251
  if (!pid) {
270
- this.host.emit({ type: "notice", level: "error", text: "请填写服务商 ID" });
252
+ this.host.emit({ type: "notice", level: "error", text: "请填写服务商 ID", textEn: "Enter a provider ID" });
271
253
  return;
272
254
  }
273
255
  if (!key) {
274
- this.host.emit({ type: "notice", level: "error", text: "请填写 API 密钥" });
256
+ this.host.emit({ type: "notice", level: "error", text: "请填写 API 密钥", textEn: "Enter an API key" });
275
257
  return;
276
258
  }
277
259
  try {
@@ -301,6 +283,7 @@ export class ModelAdminService {
301
283
  type: "notice",
302
284
  level: "info",
303
285
  text: `✅ 已保存 ${pid} 的密钥「${name}」并刷新模型列表`,
286
+ textEn: `✅ Saved key "${name}" for ${pid} and refreshed the model list`,
304
287
  });
305
288
  await this.host.pushModels();
306
289
  await this.listProviders();
@@ -311,6 +294,7 @@ export class ModelAdminService {
311
294
  type: "notice",
312
295
  level: "error",
313
296
  text: `保存 API 密钥失败:${err.message}`,
297
+ textEn: `Failed to save API key: ${err.message}`,
314
298
  });
315
299
  }
316
300
  this.host.flushSnapshot();
@@ -323,11 +307,11 @@ export class ModelAdminService {
323
307
  const pid = provider.trim();
324
308
  const key = apiKey.trim();
325
309
  if (!pid) {
326
- this.host.emit({ type: "notice", level: "error", text: "请填写服务商 ID" });
310
+ this.host.emit({ type: "notice", level: "error", text: "请填写服务商 ID", textEn: "Enter a provider ID" });
327
311
  return;
328
312
  }
329
313
  if (!key) {
330
- this.host.emit({ type: "notice", level: "error", text: "请填写 API 密钥" });
314
+ this.host.emit({ type: "notice", level: "error", text: "请填写 API 密钥", textEn: "Enter an API key" });
331
315
  return;
332
316
  }
333
317
  try {
@@ -345,6 +329,7 @@ export class ModelAdminService {
345
329
  type: "notice",
346
330
  level: "info",
347
331
  text: `${pid} 已存在该密钥`,
332
+ textEn: `${pid} already has this key`,
348
333
  });
349
334
  return;
350
335
  }
@@ -361,6 +346,7 @@ export class ModelAdminService {
361
346
  type: "notice",
362
347
  level: "info",
363
348
  text: `🔑 已添加 ${pid} 的密钥「${keyName}」并设为当前`,
349
+ textEn: `🔑 Added key "${keyName}" for ${pid} and set it active`,
364
350
  });
365
351
  }
366
352
  else {
@@ -368,6 +354,7 @@ export class ModelAdminService {
368
354
  type: "notice",
369
355
  level: "info",
370
356
  text: `🔑 已添加 ${pid} 的密钥「${keyName}」,点击模型时可切换使用`,
357
+ textEn: `🔑 Added key "${keyName}" for ${pid}; click a model to switch to it`,
371
358
  });
372
359
  }
373
360
  await this.host.pushModels();
@@ -379,6 +366,7 @@ export class ModelAdminService {
379
366
  type: "notice",
380
367
  level: "error",
381
368
  text: `添加密钥失败:${err.message}`,
369
+ textEn: `Failed to add key: ${err.message}`,
382
370
  });
383
371
  }
384
372
  this.host.flushSnapshot();
@@ -393,11 +381,21 @@ export class ModelAdminService {
393
381
  const entry = data[pid];
394
382
  const target = entry?.keys.find((k) => k.name === targetName);
395
383
  if (!target) {
396
- this.host.emit({ type: "notice", level: "error", text: `${pid} 的密钥「${targetName}」不存在` });
384
+ this.host.emit({
385
+ type: "notice",
386
+ level: "error",
387
+ text: `${pid} 的密钥「${targetName}」不存在`,
388
+ textEn: `Key "${targetName}" for ${pid} does not exist`,
389
+ });
397
390
  return;
398
391
  }
399
392
  if (entry.activeKeyName === targetName) {
400
- this.host.emit({ type: "notice", level: "info", text: `「${targetName}」已是当前密钥` });
393
+ this.host.emit({
394
+ type: "notice",
395
+ level: "info",
396
+ text: `「${targetName}」已是当前密钥`,
397
+ textEn: `"${targetName}" is already the active key`,
398
+ });
401
399
  return;
402
400
  }
403
401
  entry.activeKeyName = targetName;
@@ -407,6 +405,7 @@ export class ModelAdminService {
407
405
  type: "notice",
408
406
  level: "info",
409
407
  text: `⚡ 已切换到 ${pid} 的「${targetName}」`,
408
+ textEn: `⚡ Switched to "${targetName}" for ${pid}`,
410
409
  });
411
410
  await this.host.pushModels();
412
411
  await this.listProviders();
@@ -417,6 +416,7 @@ export class ModelAdminService {
417
416
  type: "notice",
418
417
  level: "error",
419
418
  text: `切换密钥失败:${err.message}`,
419
+ textEn: `Failed to switch key: ${err.message}`,
420
420
  });
421
421
  }
422
422
  this.host.flushSnapshot();
@@ -430,7 +430,12 @@ export class ModelAdminService {
430
430
  const data = this.readProviderKeys();
431
431
  const entry = data[pid];
432
432
  if (!entry || !entry.keys.some((k) => k.name === targetName)) {
433
- this.host.emit({ type: "notice", level: "error", text: `${pid} 的密钥「${targetName}」不存在` });
433
+ this.host.emit({
434
+ type: "notice",
435
+ level: "error",
436
+ text: `${pid} 的密钥「${targetName}」不存在`,
437
+ textEn: `Key "${targetName}" for ${pid} does not exist`,
438
+ });
434
439
  return;
435
440
  }
436
441
  const wasActive = entry.activeKeyName === targetName;
@@ -458,6 +463,7 @@ export class ModelAdminService {
458
463
  type: "notice",
459
464
  level: "info",
460
465
  text: `🗑 已移除 ${pid} 的密钥「${targetName}」,该服务商回到未配置状态`,
466
+ textEn: `🗑 Removed key "${targetName}" for ${pid}; provider is now unconfigured`,
461
467
  });
462
468
  }
463
469
  else {
@@ -475,6 +481,9 @@ export class ModelAdminService {
475
481
  text: wasActive
476
482
  ? `🗑 已移除「${targetName}」,已切换到 ${entry.keys[0].name}`
477
483
  : `🗑 已移除 ${pid} 的密钥「${targetName}」`,
484
+ textEn: wasActive
485
+ ? `🗑 Removed "${targetName}", switched to ${entry.keys[0].name}`
486
+ : `🗑 Removed key "${targetName}" for ${pid}`,
478
487
  });
479
488
  }
480
489
  await this.host.pushModels();
@@ -486,6 +495,7 @@ export class ModelAdminService {
486
495
  type: "notice",
487
496
  level: "error",
488
497
  text: `移除密钥失败:${err.message}`,
498
+ textEn: `Failed to remove key: ${err.message}`,
489
499
  });
490
500
  }
491
501
  this.host.flushSnapshot();
@@ -500,7 +510,7 @@ export class ModelAdminService {
500
510
  async clearProviderApiKey(provider) {
501
511
  const pid = provider.trim();
502
512
  if (!pid) {
503
- this.host.emit({ type: "notice", level: "error", text: "请填写服务商 ID" });
513
+ this.host.emit({ type: "notice", level: "error", text: "请填写服务商 ID", textEn: "Enter a provider ID" });
504
514
  return;
505
515
  }
506
516
  try {
@@ -520,6 +530,7 @@ export class ModelAdminService {
520
530
  type: "notice",
521
531
  level: "info",
522
532
  text: `${pid} 没有已保存的密钥`,
533
+ textEn: `${pid} has no saved key`,
523
534
  });
524
535
  return;
525
536
  }
@@ -538,6 +549,7 @@ export class ModelAdminService {
538
549
  type: "notice",
539
550
  level: "info",
540
551
  text: `🗑 已清除 ${pid} 的密钥,该服务商回到未配置状态`,
552
+ textEn: `🗑 Cleared keys for ${pid}; provider is now unconfigured`,
541
553
  });
542
554
  await this.host.pushModels();
543
555
  await this.listProviders();
@@ -548,6 +560,7 @@ export class ModelAdminService {
548
560
  type: "notice",
549
561
  level: "error",
550
562
  text: `清除密钥失败:${err.message}`,
563
+ textEn: `Failed to clear key: ${err.message}`,
551
564
  });
552
565
  }
553
566
  this.host.flushSnapshot();
@@ -588,9 +601,7 @@ export class ModelAdminService {
588
601
  id: m.id,
589
602
  ...(m.name && m.name !== m.id ? { name: m.name } : {}),
590
603
  ...(m.reasoning ? { reasoning: true } : {}),
591
- ...(m.input?.includes("image")
592
- ? { input: ["text", "image"] }
593
- : {}),
604
+ ...(m.input?.includes("image") ? { input: ["text", "image"] } : {}),
594
605
  ...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
595
606
  ...(m.maxTokens ? { maxTokens: m.maxTokens } : {}),
596
607
  },
@@ -623,10 +634,7 @@ export class ModelAdminService {
623
634
  if (!keptMap.has(m.entry.id))
624
635
  keptMap.set(m.entry.id, m.entry);
625
636
  const kept = [...keptMap.values()].sort((a, b) => a.id.localeCompare(b.id));
626
- const taken = new Set([
627
- ...Object.keys(this.readModelsConfig().providers),
628
- ...mr.getRegisteredProviderIds(),
629
- ]);
637
+ const taken = new Set([...Object.keys(this.readModelsConfig().providers), ...mr.getRegisteredProviderIds()]);
630
638
  let newId = `${pid}-2`;
631
639
  for (let n = 2; taken.has(newId); n++)
632
640
  newId = `${pid}-${n}`;
@@ -678,6 +686,7 @@ export class ModelAdminService {
678
686
  type: "notice",
679
687
  level: "error",
680
688
  text: `获取服务商列表失败:${err.message}`,
689
+ textEn: `Failed to fetch provider list: ${err.message}`,
681
690
  });
682
691
  return;
683
692
  }
@@ -686,6 +695,7 @@ export class ModelAdminService {
686
695
  type: "notice",
687
696
  level: "warning",
688
697
  text: "服务商列表为空——pi 运行时未注册任何提供商",
698
+ textEn: "Provider list is empty — the pi runtime registered no providers",
689
699
  });
690
700
  }
691
701
  this.host.emit({ type: "providers_status", providers });
@@ -787,9 +797,7 @@ export class ModelAdminService {
787
797
  return typeof v === "boolean" ? v : undefined;
788
798
  }
789
799
  static strArrMeta(v) {
790
- return Array.isArray(v)
791
- ? v.filter((x) => typeof x === "string")
792
- : undefined;
800
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : undefined;
793
801
  }
794
802
  /** Best-effort extraction of model metadata from an OpenAI-compatible
795
803
  * /models `data[]` item. Most endpoints only return `{ id }` — the extra
@@ -800,11 +808,8 @@ export class ModelAdminService {
800
808
  const r = (m ?? {});
801
809
  const id = typeof r.id === "string" ? r.id : "";
802
810
  const name = (typeof r.name === "string" && r.name.trim() ? r.name : undefined) ??
803
- (typeof r.display_name === "string" && r.display_name.trim()
804
- ? r.display_name
805
- : undefined);
806
- const modalities = strArrMeta(r.modalities) ??
807
- strArrMeta(r.input_modalities);
811
+ (typeof r.display_name === "string" && r.display_name.trim() ? r.display_name : undefined);
812
+ const modalities = strArrMeta(r.modalities) ?? strArrMeta(r.input_modalities);
808
813
  const vision = modalities?.includes("image") === true ||
809
814
  boolMeta(r.supports_vision) === true ||
810
815
  boolMeta(r.vision) === true ||
@@ -816,9 +821,7 @@ export class ModelAdminService {
816
821
  numMeta(r.context_length) ??
817
822
  numMeta(r.max_model_len) ??
818
823
  numMeta(r.max_context_length);
819
- const maxTokens = numMeta(r.max_tokens) ??
820
- numMeta(r.max_output_tokens) ??
821
- numMeta(r.max_completion_tokens);
824
+ const maxTokens = numMeta(r.max_tokens) ?? numMeta(r.max_output_tokens) ?? numMeta(r.max_completion_tokens);
822
825
  return {
823
826
  id,
824
827
  ...(name ? { name } : {}),
@@ -839,12 +842,8 @@ export class ModelAdminService {
839
842
  return {
840
843
  id,
841
844
  ...(displayName && displayName !== id ? { name: displayName } : {}),
842
- ...(numMeta(r.inputTokenLimit)
843
- ? { contextWindow: numMeta(r.inputTokenLimit) }
844
- : {}),
845
- ...(numMeta(r.outputTokenLimit)
846
- ? { maxTokens: numMeta(r.outputTokenLimit) }
847
- : {}),
845
+ ...(numMeta(r.inputTokenLimit) ? { contextWindow: numMeta(r.inputTokenLimit) } : {}),
846
+ ...(numMeta(r.outputTokenLimit) ? { maxTokens: numMeta(r.outputTokenLimit) } : {}),
848
847
  };
849
848
  }
850
849
  /** Probe a custom provider's OpenAI-compatible /models endpoint (server-side
@@ -882,7 +881,7 @@ export class ModelAdminService {
882
881
  throw new Error("baseUrl 仅支持 http/https");
883
882
  }
884
883
  const headers = {
885
- ...(extraHeaders ?? {}),
884
+ ...extraHeaders,
886
885
  };
887
886
  // Per-api auth conventions (mirror pi's built-in provider configs):
888
887
  // openai-*: Authorization: Bearer <key>
@@ -942,15 +941,11 @@ export class ModelAdminService {
942
941
  const data = Array.isArray(json.data) ? json.data : null;
943
942
  if (data) {
944
943
  // OpenAI-compatible: { data: [{ id, context_window, modalities, … }] }
945
- models = data
946
- .map((m) => parseOpenAiModel(m))
947
- .filter((m) => m.id);
944
+ models = data.map((m) => parseOpenAiModel(m)).filter((m) => m.id);
948
945
  }
949
946
  else if (Array.isArray(json.models)) {
950
947
  // Google: { models: [{ name: "models/…", displayName, … }] }
951
- models = json.models
952
- .map((m) => parseGoogleModel(m))
953
- .filter((m) => m.id);
948
+ models = json.models.map((m) => parseGoogleModel(m)).filter((m) => m.id);
954
949
  }
955
950
  }
956
951
  catch {
@@ -984,6 +979,7 @@ export class ModelAdminService {
984
979
  type: "notice",
985
980
  level: "warning",
986
981
  text: `服务商 ${pid} 不存在或未配置 baseUrl,无法刷新`,
982
+ textEn: `Provider ${pid} does not exist or has no baseUrl; cannot refresh`,
987
983
  });
988
984
  return done(false, { error: "provider missing or no baseUrl" });
989
985
  }
@@ -1027,6 +1023,7 @@ export class ModelAdminService {
1027
1023
  type: "notice",
1028
1024
  level: "error",
1029
1025
  text: `刷新模型列表失败:${err.message}`,
1026
+ textEn: `Failed to refresh model list: ${err.message}`,
1030
1027
  });
1031
1028
  return done(false, { error: err.message });
1032
1029
  }
@@ -1039,6 +1036,7 @@ export class ModelAdminService {
1039
1036
  type: "notice",
1040
1037
  level: "error",
1041
1038
  text: "服务商 ID 无效(仅字母/数字/._-)",
1039
+ textEn: "Invalid provider ID (letters/digits/._- only)",
1042
1040
  });
1043
1041
  return;
1044
1042
  }
@@ -1053,7 +1051,12 @@ export class ModelAdminService {
1053
1051
  ...(m.maxTokens ? { maxTokens: Number(m.maxTokens) } : {}),
1054
1052
  }));
1055
1053
  if (models.length === 0) {
1056
- this.host.emit({ type: "notice", level: "error", text: "至少需要一个模型" });
1054
+ this.host.emit({
1055
+ type: "notice",
1056
+ level: "error",
1057
+ text: "至少需要一个模型",
1058
+ textEn: "At least one model is required",
1059
+ });
1057
1060
  return;
1058
1061
  }
1059
1062
  try {
@@ -1067,9 +1070,7 @@ export class ModelAdminService {
1067
1070
  ...(config.baseUrl?.trim() ? { baseUrl: config.baseUrl.trim() } : {}),
1068
1071
  ...(config.apiKey?.trim() ? { apiKey: config.apiKey.trim() } : {}),
1069
1072
  ...(config.authHeader ? { authHeader: true } : {}),
1070
- ...(prevHeaders && Object.keys(prevHeaders).length > 0
1071
- ? { headers: prevHeaders }
1072
- : {}),
1073
+ ...(prevHeaders && Object.keys(prevHeaders).length > 0 ? { headers: prevHeaders } : {}),
1073
1074
  models,
1074
1075
  };
1075
1076
  mkdirSync(this.host.agentDir, { recursive: true });
@@ -1101,6 +1102,7 @@ export class ModelAdminService {
1101
1102
  type: "notice",
1102
1103
  level: "info",
1103
1104
  text: `✅ 已保存服务商 ${pid}(${models.length} 个模型)并刷新模型列表`,
1105
+ textEn: `✅ Saved provider ${pid} (${models.length} models) and refreshed the model list`,
1104
1106
  });
1105
1107
  }
1106
1108
  catch (err) {
@@ -1108,6 +1110,7 @@ export class ModelAdminService {
1108
1110
  type: "notice",
1109
1111
  level: "error",
1110
1112
  text: `保存模型配置失败:${err.message}`,
1113
+ textEn: `Failed to save model config: ${err.message}`,
1111
1114
  });
1112
1115
  }
1113
1116
  this.host.flushSnapshot();
@@ -1121,6 +1124,7 @@ export class ModelAdminService {
1121
1124
  type: "notice",
1122
1125
  level: "info",
1123
1126
  text: `服务商 ${providerId} 不存在`,
1127
+ textEn: `Provider ${providerId} does not exist`,
1124
1128
  });
1125
1129
  return;
1126
1130
  }
@@ -1134,6 +1138,7 @@ export class ModelAdminService {
1134
1138
  type: "notice",
1135
1139
  level: "info",
1136
1140
  text: `🗑 已删除服务商 ${providerId}`,
1141
+ textEn: `🗑 Deleted provider ${providerId}`,
1137
1142
  });
1138
1143
  }
1139
1144
  catch (err) {
@@ -1141,6 +1146,7 @@ export class ModelAdminService {
1141
1146
  type: "notice",
1142
1147
  level: "error",
1143
1148
  text: `删除模型配置失败:${err.message}`,
1149
+ textEn: `Failed to delete model config: ${err.message}`,
1144
1150
  });
1145
1151
  }
1146
1152
  this.host.flushSnapshot();
@@ -17,7 +17,7 @@
17
17
  */
18
18
  import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
19
19
  import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
20
- import { readFile as fspReadFile, readdir as fspReaddir, rm as fspRm, mkdir as fspMkdir, writeFile as fspWriteFile } from "node:fs/promises";
20
+ import { readFile as fspReadFile, readdir as fspReaddir, rm as fspRm, mkdir as fspMkdir, writeFile as fspWriteFile, } from "node:fs/promises";
21
21
  import { dirname, join, resolve } from "node:path";
22
22
  import { createRequire } from "node:module";
23
23
  import { spawnSync } from "node:child_process";
@@ -136,9 +136,7 @@ export class PluginSecrets {
136
136
  try {
137
137
  const parsed = JSON.parse(readFileSync(this.file, "utf8"));
138
138
  this.store =
139
- parsed && parsed.v === 1 && parsed.items && typeof parsed.items === "object"
140
- ? parsed
141
- : { v: 1, items: {} };
139
+ parsed && parsed.v === 1 && parsed.items && typeof parsed.items === "object" ? parsed : { v: 1, items: {} };
142
140
  }
143
141
  catch {
144
142
  this.store = { v: 1, items: {} };
@@ -146,7 +146,7 @@ export async function resolveRemoteSha(spec, exec = execGit) {
146
146
  if (!res.ok)
147
147
  return null;
148
148
  // 行格式: <sha>\tHEAD(可能多行——取第一行)
149
- const sha = (res.stdout.match(/^([0-9a-f]{40,64})\s+HEAD/m)?.[1]) ?? null;
149
+ const sha = res.stdout.match(/^([0-9a-f]{40,64})\s+HEAD/m)?.[1] ?? null;
150
150
  return sha ? sha.slice(0, 12) : null;
151
151
  }
152
152
  /** 扫描全部已装插件,对比本地 sha 与远端 sha,报告更新状态。 */
@@ -56,9 +56,7 @@ function parseSettingsSchema(raw) {
56
56
  ...(o.default !== undefined ? { default: o.default } : {}),
57
57
  ...(typeof o.min === "number" ? { min: o.min } : {}),
58
58
  ...(typeof o.max === "number" ? { max: o.max } : {}),
59
- ...(Array.isArray(o.options)
60
- ? { options: o.options.filter((x) => typeof x === "string") }
61
- : {}),
59
+ ...(Array.isArray(o.options) ? { options: o.options.filter((x) => typeof x === "string") } : {}),
62
60
  ...(typeof o.hint === "string" ? { hint: o.hint } : {}),
63
61
  };
64
62
  out.push(field);
@@ -369,7 +367,7 @@ export class PluginManager {
369
367
  }
370
368
  /** 服务端热重载:反激活全部 → 清缓存 → 重扫重激活 → epoch+1。
371
369
  * 返回新目录清单(含激活结果)。重激活后的插件实例是新模块,
372
- * 内存状态为初始值——逐个客户端触发 onAttach 让它们重推自身状态。 */
370
+ * 内存状态为初始值——逐个客户端触发 onAttach 让它们重推自身状态。 */
373
371
  async reload() {
374
372
  this.dispose();
375
373
  this.attempted.clear();
@@ -524,6 +522,7 @@ export class PluginManager {
524
522
  await this.activate(info);
525
523
  }
526
524
  // 已被删除的插件:调用 deactivate 并移出缓存
525
+ // eslint-disable-next-line unicorn/no-useless-spread -- snapshot: handlers may unsubscribe mid-emit
527
526
  for (const [id, p] of [...this.loaded]) {
528
527
  if (!found.some((f) => f.id === id)) {
529
528
  this.deactivateEntry(id, p);
@@ -539,6 +538,7 @@ export class PluginManager {
539
538
  catch (err) {
540
539
  console.error(`[plugin:${id}] deactivate failed:`, err);
541
540
  }
541
+ // eslint-disable-next-line unicorn/no-useless-spread -- snapshot: handlers may unsubscribe mid-emit
542
542
  for (const off of [...(p.agentToolUnsubscribers ?? [])]) {
543
543
  try {
544
544
  off();
@@ -658,7 +658,14 @@ export class PluginManager {
658
658
  if (apiVersion > PLUGIN_API_VERSION) {
659
659
  const msg = `插件要求宿主 API v${apiVersion},当前宿主 v${PLUGIN_API_VERSION} —— 请升级 pi-web-ui`;
660
660
  console.error(`[plugin:${info.id}] ${msg}`);
661
- this.loaded.set(info.id, { info: { ...info, error: msg }, toolHandlers, attachHandlers, cwdHandlers, httpRoutes, settingsHandlers: new Set() });
661
+ this.loaded.set(info.id, {
662
+ info: { ...info, error: msg },
663
+ toolHandlers,
664
+ attachHandlers,
665
+ cwdHandlers,
666
+ httpRoutes,
667
+ settingsHandlers: new Set(),
668
+ });
662
669
  return;
663
670
  }
664
671
  // 能力声明:写了 permissions → 严格模式(受控宿主 API 按声明族强制执行);
@@ -666,7 +673,15 @@ export class PluginManager {
666
673
  const permsDeclared = (info.permissions ?? []).slice();
667
674
  const strict = permsDeclared.length > 0 || apiVersion >= 2;
668
675
  const permFamilies = new Set(permsDeclared.map((x) => x.split(":")[0]));
669
- const p = { info, toolHandlers, attachHandlers, cwdHandlers, commandUnsubscribers: unregisterCommands, httpRoutes, settingsHandlers };
676
+ const p = {
677
+ info,
678
+ toolHandlers,
679
+ attachHandlers,
680
+ cwdHandlers,
681
+ commandUnsubscribers: unregisterCommands,
682
+ httpRoutes,
683
+ settingsHandlers,
684
+ };
670
685
  p.permsDeclared = permsDeclared;
671
686
  p.permFamilies = permFamilies;
672
687
  p.legacyWarned = false;
@@ -690,7 +705,7 @@ export class PluginManager {
690
705
  console.error(`[plugin:${info.id}] 缺少能力声明 "${family}"(manifest.permissions)——请求被拒`);
691
706
  return false;
692
707
  };
693
- const self = this; // 对象字面量 getter 里不能用插件宿主的 this
708
+ const self = this; // 对象字面量 getter 里不能用插件宿主的 this (oxlint no-this-alias: 誤報, getter closure 需要 host)
694
709
  const host = {
695
710
  broadcast: (payload) => this.broadcast(info.id, payload),
696
711
  notify: (level, text) => this.notifyAll(level, text),
@@ -728,7 +743,10 @@ export class PluginManager {
728
743
  if (!can("http"))
729
744
  return () => { };
730
745
  const m = String(method ?? "GET").toUpperCase();
731
- if (!["GET", "POST", "PUT", "DELETE"].includes(m) || typeof path !== "string" || !path.startsWith("/") || typeof handler !== "function") {
746
+ if (!["GET", "POST", "PUT", "DELETE"].includes(m) ||
747
+ typeof path !== "string" ||
748
+ !path.startsWith("/") ||
749
+ typeof handler !== "function") {
732
750
  console.error(`[plugin:${info.id}] route: 非法参数(method=${method} path=${path}),忽略`);
733
751
  return () => { };
734
752
  }
@@ -13,7 +13,7 @@ export async function snapshotListeningPorts() {
13
13
  try {
14
14
  const { execFile } = await import("node:child_process");
15
15
  if (process.platform === "win32") {
16
- const out = await new Promise((resolve, reject) => execFile("netstat", ["-ano", "-p", "tcp"], { windowsHide: true, timeout: 8000 }, (err, stdout) => (err ? reject(err) : resolve(stdout))));
16
+ const out = await new Promise((resolve, reject) => execFile("netstat", ["-ano", "-p", "tcp"], { windowsHide: true, timeout: 8000 }, (err, stdout) => err ? reject(err) : resolve(stdout)));
17
17
  for (const line of out.split(/\r?\n/)) {
18
18
  const p = line.trim().split(/\s+/);
19
19
  // TCP 0.0.0.0:5173 0.0.0.0:0 LISTENING 12345
@@ -26,7 +26,7 @@ export async function snapshotListeningPorts() {
26
26
  }
27
27
  }
28
28
  else {
29
- const out = await new Promise((resolve, reject) => execFile("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n"], { timeout: 8000 }, (err, stdout) => (err ? reject(err) : resolve(stdout))));
29
+ const out = await new Promise((resolve, reject) => execFile("lsof", ["-iTCP", "-sTCP:LISTEN", "-P", "-n"], { timeout: 8000 }, (err, stdout) => err ? reject(err) : resolve(stdout)));
30
30
  for (const line of out.split(/\r?\n/).slice(1)) {
31
31
  const p = line.trim().split(/\s+/);
32
32
  if (p.length >= 9) {
@@ -83,7 +83,7 @@ export async function lookupProcessCommandLine(pid) {
83
83
  const line = out.trim();
84
84
  return line || undefined;
85
85
  }
86
- const out = await new Promise((resolve, reject) => execFile("ps", ["-o", "command=", "-p", String(pid)], { timeout: 4000 }, (err, stdout) => (err ? reject(err) : resolve(stdout))));
86
+ const out = await new Promise((resolve, reject) => execFile("ps", ["-o", "command=", "-p", String(pid)], { timeout: 4000 }, (err, stdout) => err ? reject(err) : resolve(stdout)));
87
87
  const line = out.trim();
88
88
  return line || undefined;
89
89
  }
@@ -102,7 +102,7 @@ export async function lookupProcessName(pid) {
102
102
  const m = out.match(/"([^"]+)"/);
103
103
  return m ? m[1] : undefined;
104
104
  }
105
- const out = await new Promise((resolve, reject) => execFile("ps", ["-o", "comm=", "-p", String(pid)], { timeout: 4000 }, (err, stdout) => (err ? reject(err) : resolve(stdout))));
105
+ const out = await new Promise((resolve, reject) => execFile("ps", ["-o", "comm=", "-p", String(pid)], { timeout: 4000 }, (err, stdout) => err ? reject(err) : resolve(stdout)));
106
106
  const name = out.trim();
107
107
  return name || undefined;
108
108
  }
@@ -8,4 +8,4 @@
8
8
  * its own copy in web/src/protocol-version.ts; scripts/check-protocol-sync.mjs
9
9
  * verifies the two never drift.
10
10
  */
11
- export const PROTOCOL_VERSION = 10;
11
+ export const PROTOCOL_VERSION = 11;