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.
- package/LICENSE +21 -21
- package/README.md +504 -497
- package/README.zh-CN.md +427 -422
- package/bin/pi-web-ui.mjs +1809 -1881
- package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
- package/deploy/nginx-subpath.conf +88 -88
- package/deploy/pi-web-ui-task.xml +54 -54
- package/deploy/pi-web-ui.service +31 -31
- package/dist/server/agent-service.js +318 -88
- package/dist/server/attachments.js +37 -28
- package/dist/server/bg-servers.js +6 -1
- package/dist/server/client-state.js +15 -7
- package/dist/server/control-socket.js +1 -5
- package/dist/server/dsh/dsh-agent-service.js +454 -118
- package/dist/server/dsh/dsh-client.js +1 -3
- package/dist/server/dsh/dsh-sessions.js +4 -14
- package/dist/server/dsh/runtime/cordis.yml +1 -1
- package/dist/server/dsh/runtime/goal-rpc.mjs +645 -662
- package/dist/server/dsh/runtime/launcher.mjs +8 -18
- package/dist/server/dsh/runtime/override.patch.yml +9 -9
- package/dist/server/dsh/runtime/runtime-root.mjs +7 -11
- package/dist/server/ensure-bash.js +2 -5
- package/dist/server/files-service.js +42 -36
- package/dist/server/goal-service.js +59 -30
- package/dist/server/index.js +17 -10
- package/dist/server/mcp-bridge.js +11 -5
- package/dist/server/model-admin.js +78 -72
- package/dist/server/plugin-facilities.js +2 -4
- package/dist/server/plugin-updater.js +1 -1
- package/dist/server/plugins.js +26 -8
- package/dist/server/process-utils.js +4 -4
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/scm.js +27 -21
- package/dist/server/serialize.js +1 -3
- package/dist/server/settings-service.js +63 -21
- package/dist/server/slash-commands.js +29 -3
- package/dist/server/subagent-templates.js +249 -0
- package/dist/server/subagents.js +150 -0
- package/dist/server/terminals.js +101 -40
- package/dist/server/text-sniff.js +4 -24
- package/dist/server/update-check.js +2 -4
- package/dist/server/uploads.js +1 -3
- package/dist/server/vision-bridge.js +2 -7
- package/dist/server/wait-subscription-scan.js +102 -21
- package/dist/server/webui-context.js +1 -1
- package/extensions/webui.ts +190 -192
- package/package.json +109 -103
- package/web/dist/assets/TerminalPanel-B9vcI7-_.js +2 -0
- package/web/dist/assets/index-B8Wv29p5.css +10 -0
- package/web/dist/assets/index-CWUWD0rq.js +323 -0
- package/web/dist/favicon.svg +8 -8
- package/web/dist/index.html +15 -15
- package/web/public/favicon.svg +8 -8
- package/web/dist/assets/TerminalPanel-BkCWDsIG.js +0 -2
- package/web/dist/assets/index-BRVKed7W.js +0 -321
- 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 {
|
|
15
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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({
|
|
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 =
|
|
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,报告更新状态。 */
|
package/dist/server/plugins.js
CHANGED
|
@@ -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
|
-
|
|
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, {
|
|
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 = {
|
|
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) ||
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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
|
}
|