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