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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { countLines, decodeText, looksLikeText, sniffImageMime
|
|
1
|
+
import { countLines, decodeText, looksLikeText, sniffImageMime } from "./text-sniff.js";
|
|
2
2
|
import { saveUpload, uploadsRoot } from "./uploads.js";
|
|
3
|
-
import { buildVisionBridgePrompt, findVisionModels, transcribeImages
|
|
3
|
+
import { buildVisionBridgePrompt, findVisionModels, transcribeImages } from "./vision-bridge.js";
|
|
4
4
|
/** 跨快照的视觉转写缓存:批次 hash(名称 + base64 头 + 提示词)→ 转写文本。
|
|
5
5
|
* 编辑重问重发相同图片不再重复耗视觉 token。进程级共享即可。 */
|
|
6
6
|
const visionBridgeCache = new Map();
|
|
@@ -17,21 +17,13 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
17
17
|
if (!attachments || attachments.length === 0)
|
|
18
18
|
return [];
|
|
19
19
|
const fs = await import("node:fs/promises");
|
|
20
|
-
const { resolve, sep, relative, extname,
|
|
20
|
+
const { resolve, sep, relative, extname, basename } = await import("node:path");
|
|
21
21
|
const root = resolve(ctx.cwd);
|
|
22
22
|
const MAX_ATTACHMENT_BYTES = 200 * 1024;
|
|
23
23
|
// Files at or below this size are inlined; larger files are referenced by
|
|
24
24
|
// path only (the model reads them on demand — saves tokens for small edits).
|
|
25
25
|
const MAX_INLINE_BYTES = Number(process.env.PI_WEB_INLINE_FILE_MAX ?? 12 * 1024);
|
|
26
|
-
const IMAGE_EXT = new Set([
|
|
27
|
-
".png",
|
|
28
|
-
".jpg",
|
|
29
|
-
".jpeg",
|
|
30
|
-
".gif",
|
|
31
|
-
".webp",
|
|
32
|
-
".bmp",
|
|
33
|
-
".svg",
|
|
34
|
-
]);
|
|
26
|
+
const IMAGE_EXT = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"]);
|
|
35
27
|
const MIME = {
|
|
36
28
|
".png": "image/png",
|
|
37
29
|
".jpg": "image/jpeg",
|
|
@@ -113,9 +105,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
113
105
|
for (const [idx, att] of attachments.entries()) {
|
|
114
106
|
if (att.imageData) {
|
|
115
107
|
const raw = att.imageData.replace(/^data:[^;]*;base64,/, "");
|
|
116
|
-
const mimeType = att.mimeType?.startsWith("image/")
|
|
117
|
-
? att.mimeType
|
|
118
|
-
: "image/png";
|
|
108
|
+
const mimeType = att.mimeType?.startsWith("image/") ? att.mimeType : "image/png";
|
|
119
109
|
const bytes = Buffer.byteLength(raw, "base64");
|
|
120
110
|
// Only images that would actually be sent (non-empty, under the cap).
|
|
121
111
|
if (bytes > 0 && bytes <= 2 * 1024 * 1024) {
|
|
@@ -162,6 +152,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
162
152
|
type: "notice",
|
|
163
153
|
level: "warning",
|
|
164
154
|
text: `当前模型(${mainModel?.name ?? mainModel?.id ?? "未知"})不支持识图,且视觉桥已在设置中关闭:图片将原样发送、可能被忽略。`,
|
|
155
|
+
textEn: `Current model (${mainModel?.name ?? mainModel?.id ?? "unknown"}) cannot see images and the vision bridge is off in settings: images will be sent as-is and may be ignored.`,
|
|
165
156
|
});
|
|
166
157
|
}
|
|
167
158
|
else {
|
|
@@ -188,6 +179,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
188
179
|
type: "notice",
|
|
189
180
|
level: "warning",
|
|
190
181
|
text: `当前模型(${mainModel?.name ?? mainModel?.id ?? "未知"})不支持识图,且未找到可用的视觉模型:图片将原样发送、可能被忽略。在模型配置里添加任意支持图片的模型(如 qwen-vl、GLM-4V、Gemini)即可自动启用视觉桥转写。`,
|
|
182
|
+
textEn: `Current model (${mainModel?.name ?? mainModel?.id ?? "unknown"}) cannot see images and no vision model is available: images will be sent as-is and may be ignored. Add any vision-capable model (e.g. qwen-vl, GLM-4V, Gemini) in model settings to enable vision-bridge transcription.`,
|
|
191
183
|
});
|
|
192
184
|
}
|
|
193
185
|
else {
|
|
@@ -196,9 +188,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
196
188
|
// The active transcription prompt is part of the key: changing
|
|
197
189
|
// the custom prompt must invalidate cached transcripts made with
|
|
198
190
|
// the old prompt.
|
|
199
|
-
const batchHash = bridgedImages
|
|
200
|
-
.map((b) => `${b.att.name ?? "img"}:${b.raw.slice(0, 48)}`)
|
|
201
|
-
.join("|") +
|
|
191
|
+
const batchHash = bridgedImages.map((b) => `${b.att.name ?? "img"}:${b.raw.slice(0, 48)}`).join("|") +
|
|
202
192
|
"::" +
|
|
203
193
|
buildVisionBridgePrompt(ctx.settings.visionBridgePromptMode, ctx.settings.visionBridgePrompt);
|
|
204
194
|
let transcript = visionBridgeCache.get(batchHash);
|
|
@@ -207,6 +197,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
207
197
|
type: "notice",
|
|
208
198
|
level: "info",
|
|
209
199
|
text: `当前模型不支持识图,正在用视觉桥(${chosen.label})转写 ${bridgedImages.length} 张图片…`,
|
|
200
|
+
textEn: `Current model cannot see images; transcribing ${bridgedImages.length} image(s) via the vision bridge (${chosen.label})…`,
|
|
210
201
|
});
|
|
211
202
|
try {
|
|
212
203
|
const chosenModel = ctx.session.modelRuntime.getModel(chosen.provider, chosen.id);
|
|
@@ -223,6 +214,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
223
214
|
type: "notice",
|
|
224
215
|
level: "info",
|
|
225
216
|
text: `✅ 图片已由视觉桥转写完成(${chosen.label})`,
|
|
217
|
+
textEn: `✅ Images transcribed by the vision bridge (${chosen.label})`,
|
|
226
218
|
});
|
|
227
219
|
}
|
|
228
220
|
catch (err) {
|
|
@@ -231,6 +223,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
231
223
|
type: "notice",
|
|
232
224
|
level: "error",
|
|
233
225
|
text: `图片转写失败(${chosen.label}):${err.message}。图片将原样发送、可能被忽略。`,
|
|
226
|
+
textEn: `Image transcription failed (${chosen.label}): ${err.message}. Images will be sent as-is and may be ignored.`,
|
|
234
227
|
});
|
|
235
228
|
}
|
|
236
229
|
}
|
|
@@ -254,6 +247,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
254
247
|
type: "notice",
|
|
255
248
|
level: "error",
|
|
256
249
|
text: `图片数据为空,已跳过`,
|
|
250
|
+
textEn: `Image data is empty, skipped`,
|
|
257
251
|
});
|
|
258
252
|
continue;
|
|
259
253
|
}
|
|
@@ -262,6 +256,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
262
256
|
type: "notice",
|
|
263
257
|
level: "warning",
|
|
264
258
|
text: `图片过大已跳过(>2MB):${att.name ?? "粘贴图片"}`,
|
|
259
|
+
textEn: `Image too large, skipped (>2MB): ${att.name ?? "pasted image"}`,
|
|
265
260
|
});
|
|
266
261
|
continue;
|
|
267
262
|
}
|
|
@@ -321,6 +316,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
321
316
|
type: "notice",
|
|
322
317
|
level: "error",
|
|
323
318
|
text: `文件数据为空,已跳过`,
|
|
319
|
+
textEn: `File data is empty, skipped`,
|
|
324
320
|
});
|
|
325
321
|
continue;
|
|
326
322
|
}
|
|
@@ -329,6 +325,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
329
325
|
type: "notice",
|
|
330
326
|
level: "warning",
|
|
331
327
|
text: `文件过大已跳过(>20MB):${att.name ?? "上传文件"}`,
|
|
328
|
+
textEn: `File too large, skipped (>20MB): ${att.name ?? "uploaded file"}`,
|
|
332
329
|
});
|
|
333
330
|
continue;
|
|
334
331
|
}
|
|
@@ -356,13 +353,13 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
356
353
|
const relToRoot = relative(rootDir, abs);
|
|
357
354
|
const inClientDir = !relToRoot.startsWith("..") &&
|
|
358
355
|
!relToRoot.includes(`${sep}..`) &&
|
|
359
|
-
(relToRoot === ctx.clientId ||
|
|
360
|
-
relToRoot.startsWith(`${ctx.clientId}${sep}`));
|
|
356
|
+
(relToRoot === ctx.clientId || relToRoot.startsWith(`${ctx.clientId}${sep}`));
|
|
361
357
|
if (!inClientDir) {
|
|
362
358
|
ctx.emit({
|
|
363
359
|
type: "notice",
|
|
364
360
|
level: "warning",
|
|
365
361
|
text: `无法恢复已上传文件(路径不在本客户端上传目录):${att.name ?? att.uploadPath}`,
|
|
362
|
+
textEn: `Cannot restore uploaded file (outside this client upload dir): ${att.name ?? att.uploadPath}`,
|
|
366
363
|
});
|
|
367
364
|
continue;
|
|
368
365
|
}
|
|
@@ -375,6 +372,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
375
372
|
type: "notice",
|
|
376
373
|
level: "warning",
|
|
377
374
|
text: `无法恢复已上传文件(已被清理或不可读):${att.name ?? att.uploadPath}`,
|
|
375
|
+
textEn: `Cannot restore uploaded file (cleaned up or unreadable): ${att.name ?? att.uploadPath}`,
|
|
378
376
|
});
|
|
379
377
|
continue;
|
|
380
378
|
}
|
|
@@ -390,6 +388,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
390
388
|
type: "notice",
|
|
391
389
|
level: "warning",
|
|
392
390
|
text: `附件路径超出工作区:${att.path}`,
|
|
391
|
+
textEn: `Attachment path is outside the workspace: ${att.path}`,
|
|
393
392
|
});
|
|
394
393
|
continue;
|
|
395
394
|
}
|
|
@@ -405,6 +404,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
405
404
|
type: "notice",
|
|
406
405
|
level: "error",
|
|
407
406
|
text: `附件不存在:${att.path}`,
|
|
407
|
+
textEn: `Attachment does not exist: ${att.path}`,
|
|
408
408
|
});
|
|
409
409
|
continue;
|
|
410
410
|
}
|
|
@@ -432,6 +432,7 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
432
432
|
type: "notice",
|
|
433
433
|
level: "warning",
|
|
434
434
|
text: `跳过非文件附件:${att.path}`,
|
|
435
|
+
textEn: `Skipped non-file attachment: ${att.path}`,
|
|
435
436
|
});
|
|
436
437
|
continue;
|
|
437
438
|
}
|
|
@@ -448,17 +449,19 @@ export async function buildAttachmentMessages(ctx, attachments) {
|
|
|
448
449
|
content: [
|
|
449
450
|
{
|
|
450
451
|
type: "text",
|
|
451
|
-
text: `
|
|
452
|
-
<vision-bridge>
|
|
453
|
-
${transcript}
|
|
452
|
+
text: `
|
|
453
|
+
<vision-bridge>
|
|
454
|
+
${transcript}
|
|
454
455
|
</vision-bridge>`,
|
|
455
456
|
},
|
|
456
457
|
...(pathImg
|
|
457
|
-
?
|
|
458
|
+
? [
|
|
459
|
+
{
|
|
458
460
|
type: "image",
|
|
459
461
|
data: pathImg.raw,
|
|
460
462
|
mimeType: pathImg.mimeType,
|
|
461
|
-
}
|
|
463
|
+
},
|
|
464
|
+
]
|
|
462
465
|
: []),
|
|
463
466
|
],
|
|
464
467
|
display: true,
|
|
@@ -498,6 +501,7 @@ ${transcript}
|
|
|
498
501
|
type: "notice",
|
|
499
502
|
level: "warning",
|
|
500
503
|
text: `图片附件过大已跳过(>200KB):${att.path}`,
|
|
504
|
+
textEn: `Image attachment too large, skipped (>200KB): ${att.path}`,
|
|
501
505
|
});
|
|
502
506
|
continue;
|
|
503
507
|
}
|
|
@@ -505,9 +509,7 @@ ${transcript}
|
|
|
505
509
|
out.push({
|
|
506
510
|
message: {
|
|
507
511
|
customType: "file",
|
|
508
|
-
content: [
|
|
509
|
-
{ type: "image", data, mimeType: MIME[ext] ?? "image/png" },
|
|
510
|
-
],
|
|
512
|
+
content: [{ type: "image", data, mimeType: MIME[ext] ?? "image/png" }],
|
|
511
513
|
display: true,
|
|
512
514
|
details: { name, path: rel, mode: "image", size: stat.size },
|
|
513
515
|
},
|
|
@@ -564,6 +566,7 @@ ${transcript}
|
|
|
564
566
|
type: "notice",
|
|
565
567
|
level: "warning",
|
|
566
568
|
text: `行范围无效,已改为仅引用:${att.path}`,
|
|
569
|
+
textEn: `Invalid line range, switched to reference-only: ${att.path}`,
|
|
567
570
|
});
|
|
568
571
|
out.push(makeReference());
|
|
569
572
|
continue;
|
|
@@ -573,6 +576,7 @@ ${transcript}
|
|
|
573
576
|
type: "notice",
|
|
574
577
|
level: "warning",
|
|
575
578
|
text: `文件过大,已改为仅引用:${att.path}`,
|
|
579
|
+
textEn: `File too large, switched to reference-only: ${att.path}`,
|
|
576
580
|
});
|
|
577
581
|
out.push(makeReference());
|
|
578
582
|
continue;
|
|
@@ -583,6 +587,7 @@ ${transcript}
|
|
|
583
587
|
type: "notice",
|
|
584
588
|
level: "warning",
|
|
585
589
|
text: `二进制文件已改为仅引用:${att.path}`,
|
|
590
|
+
textEn: `Binary file, switched to reference-only: ${att.path}`,
|
|
586
591
|
});
|
|
587
592
|
out.push(makeReference());
|
|
588
593
|
continue;
|
|
@@ -599,6 +604,7 @@ ${transcript}
|
|
|
599
604
|
type: "notice",
|
|
600
605
|
level: "warning",
|
|
601
606
|
text: `选中行超出文件范围,已改为仅引用:${att.path}`,
|
|
607
|
+
textEn: `Selected lines out of range, switched to reference-only: ${att.path}`,
|
|
602
608
|
});
|
|
603
609
|
out.push(makeReference());
|
|
604
610
|
continue;
|
|
@@ -634,6 +640,7 @@ ${transcript}
|
|
|
634
640
|
type: "notice",
|
|
635
641
|
level: "warning",
|
|
636
642
|
text: `文件过大,已改为仅引用:${att.path}`,
|
|
643
|
+
textEn: `File too large, switched to reference-only: ${att.path}`,
|
|
637
644
|
});
|
|
638
645
|
out.push(makeReference());
|
|
639
646
|
continue;
|
|
@@ -644,6 +651,7 @@ ${transcript}
|
|
|
644
651
|
type: "notice",
|
|
645
652
|
level: "warning",
|
|
646
653
|
text: `二进制文件已改为仅引用:${att.path}`,
|
|
654
|
+
textEn: `Binary file, switched to reference-only: ${att.path}`,
|
|
647
655
|
});
|
|
648
656
|
out.push(makeReference());
|
|
649
657
|
continue;
|
|
@@ -662,6 +670,7 @@ ${transcript}
|
|
|
662
670
|
type: "notice",
|
|
663
671
|
level: "warning",
|
|
664
672
|
text: `二进制文件已跳过(仅引用路径):${att.path}`,
|
|
673
|
+
textEn: `Binary file skipped (path referenced only): ${att.path}`,
|
|
665
674
|
});
|
|
666
675
|
out.push(makeReference());
|
|
667
676
|
continue;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { killPidTree, lookupProcessName, lookupProcessCommandLine, snapshotListeningPorts
|
|
1
|
+
import { killPidTree, lookupProcessName, lookupProcessCommandLine, snapshotListeningPorts } from "./process-utils.js";
|
|
2
2
|
const BG_REFRESH_INTERVAL_MS = 30_000;
|
|
3
3
|
/** bash 结束后等这么久再拍「后」快照——给后台服务绑定端口的时间。 */
|
|
4
4
|
const BG_BIND_WAIT_MS = 1500;
|
|
@@ -63,6 +63,7 @@ export class BgServerTracker {
|
|
|
63
63
|
type: "notice",
|
|
64
64
|
level: "info",
|
|
65
65
|
text: `检测到 AI 启动的后台服务:端口 ${port}(pid ${pid})——可在顶栏「后台任务」里单独停止或全部关闭`,
|
|
66
|
+
textEn: `Detected an AI-started background service: port ${port} (pid ${pid}) — stop it individually or all at once under Background tasks in the top bar`,
|
|
66
67
|
});
|
|
67
68
|
}
|
|
68
69
|
}
|
|
@@ -97,6 +98,7 @@ export class BgServerTracker {
|
|
|
97
98
|
return;
|
|
98
99
|
const now = await snapshotListeningPorts();
|
|
99
100
|
let changed = false;
|
|
101
|
+
// eslint-disable-next-line unicorn/no-useless-spread -- snapshot: handlers may unsubscribe mid-emit
|
|
100
102
|
for (const [port, v] of [...this.servers]) {
|
|
101
103
|
if (now.get(port) !== v.pid) {
|
|
102
104
|
this.servers.delete(port);
|
|
@@ -119,6 +121,7 @@ export class BgServerTracker {
|
|
|
119
121
|
type: "notice",
|
|
120
122
|
level: "info",
|
|
121
123
|
text: `端口 ${port} 不在后台任务列表中`,
|
|
124
|
+
textEn: `Port ${port} is not in the background task list`,
|
|
122
125
|
});
|
|
123
126
|
this.opts.flushSnapshot();
|
|
124
127
|
return false;
|
|
@@ -130,6 +133,7 @@ export class BgServerTracker {
|
|
|
130
133
|
type: "notice",
|
|
131
134
|
level: "info",
|
|
132
135
|
text: `已停止后台任务:端口 ${port}(pid ${entry.pid})`,
|
|
136
|
+
textEn: `Stopped background task: port ${port} (pid ${entry.pid})`,
|
|
133
137
|
});
|
|
134
138
|
this.opts.flushSnapshot();
|
|
135
139
|
return true;
|
|
@@ -139,6 +143,7 @@ export class BgServerTracker {
|
|
|
139
143
|
if (this.servers.size === 0)
|
|
140
144
|
return [];
|
|
141
145
|
const killed = [];
|
|
146
|
+
// eslint-disable-next-line unicorn/no-useless-spread -- snapshot: handlers may unsubscribe mid-emit
|
|
142
147
|
for (const [port, { pid }] of [...this.servers]) {
|
|
143
148
|
killPidTree(pid);
|
|
144
149
|
killed.push(String(port));
|
|
@@ -46,6 +46,15 @@ export function isExtensionDisabled(e, disabled) {
|
|
|
46
46
|
const keys = extensionKeyCandidates(e);
|
|
47
47
|
return disabled.some((d) => keys.includes(d));
|
|
48
48
|
}
|
|
49
|
+
/** Whether an extension is covered by an ENABLED whitelist (any identity
|
|
50
|
+
* match). Empty whitelist = not whitelisting = everything allowed. Used by
|
|
51
|
+
* subagent templates(白名单语义:模板勾选 = 子代理只加载这些扩展)。 */
|
|
52
|
+
export function isExtensionEnabled(e, enabled) {
|
|
53
|
+
if (enabled.length === 0)
|
|
54
|
+
return true;
|
|
55
|
+
const keys = extensionKeyCandidates(e);
|
|
56
|
+
return enabled.some((d) => keys.includes(d));
|
|
57
|
+
}
|
|
49
58
|
/**
|
|
50
59
|
* Persists which workspace each browser client last used + which workspaces it
|
|
51
60
|
* has opened, so a server restart / page reload restores the same project and
|
|
@@ -58,6 +67,10 @@ export class ClientStateStore {
|
|
|
58
67
|
constructor(filePath) {
|
|
59
68
|
this.filePath = filePath;
|
|
60
69
|
}
|
|
70
|
+
/** <dataDir>(client-state.json 的上一级)——共享配置(子代理模板库等)落在这里。 */
|
|
71
|
+
get dataDir() {
|
|
72
|
+
return dirname(this.filePath);
|
|
73
|
+
}
|
|
61
74
|
load() {
|
|
62
75
|
if (this.cache)
|
|
63
76
|
return this.cache;
|
|
@@ -93,10 +106,7 @@ export class ClientStateStore {
|
|
|
93
106
|
const state = (all[clientId] ??= { projects: [] });
|
|
94
107
|
state.lastCwd = cwd;
|
|
95
108
|
const now = Date.now();
|
|
96
|
-
state.projects = [
|
|
97
|
-
{ path: cwd, lastUsed: now },
|
|
98
|
-
...state.projects.filter((p) => p.path !== cwd),
|
|
99
|
-
].slice(0, 30);
|
|
109
|
+
state.projects = [{ path: cwd, lastUsed: now }, ...state.projects.filter((p) => p.path !== cwd)].slice(0, 30);
|
|
100
110
|
// Opening the workspace again clears its removal tombstone.
|
|
101
111
|
if (state.removedProjects?.length) {
|
|
102
112
|
state.removedProjects = state.removedProjects.filter((p) => p !== cwd);
|
|
@@ -205,9 +215,7 @@ export class ClientStateStore {
|
|
|
205
215
|
toolsWrap: settings.toolsWrap ?? cur.toolsWrap ?? true,
|
|
206
216
|
visionBridgeEnabled: settings.visionBridgeEnabled ?? cur.visionBridgeEnabled ?? true,
|
|
207
217
|
visionBridgeModel: settings.visionBridgeModel ?? cur.visionBridgeModel ?? null,
|
|
208
|
-
visionBridgePromptMode: settings.visionBridgePromptMode ??
|
|
209
|
-
cur.visionBridgePromptMode ??
|
|
210
|
-
"append",
|
|
218
|
+
visionBridgePromptMode: settings.visionBridgePromptMode ?? cur.visionBridgePromptMode ?? "append",
|
|
211
219
|
visionBridgePrompt: settings.visionBridgePrompt ?? cur.visionBridgePrompt ?? "",
|
|
212
220
|
reviewPrompt: settings.reviewPrompt ?? cur.reviewPrompt ?? "",
|
|
213
221
|
reviewDisabledSkills: settings.reviewDisabledSkills ?? cur.reviewDisabledSkills ?? [],
|
|
@@ -25,16 +25,13 @@ const CONTROL_IDLE_TIMEOUT_MS = 5_000;
|
|
|
25
25
|
const CONTROL_CLIENT_TIMEOUT_MS = 3_000;
|
|
26
26
|
/** Socket path (POSIX) or pipe name (Windows). */
|
|
27
27
|
export function controlPath(dataDir, port) {
|
|
28
|
-
return process.platform === "win32"
|
|
29
|
-
? `\\\\.\\pipe\\pi-web-ui-${port}`
|
|
30
|
-
: join(dataDir, "pi-web-ui.sock");
|
|
28
|
+
return process.platform === "win32" ? `\\\\.\\pipe\\pi-web-ui-${port}` : join(dataDir, "pi-web-ui.sock");
|
|
31
29
|
}
|
|
32
30
|
/** Start the control socket; returns a stop function. */
|
|
33
31
|
export function startControlServer(opts) {
|
|
34
32
|
const { service, dataDir, port } = opts;
|
|
35
33
|
const path = controlPath(dataDir, port);
|
|
36
34
|
let server;
|
|
37
|
-
let stop = false;
|
|
38
35
|
if (process.platform === "win32") {
|
|
39
36
|
server = createServer(handleConnection);
|
|
40
37
|
}
|
|
@@ -126,7 +123,6 @@ export function startControlServer(opts) {
|
|
|
126
123
|
});
|
|
127
124
|
}
|
|
128
125
|
return () => {
|
|
129
|
-
stop = true;
|
|
130
126
|
server.close();
|
|
131
127
|
try {
|
|
132
128
|
if (process.platform !== "win32" && existsSync(path))
|