pi-web-ui 0.64.5 → 0.64.6
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/README.md +2 -2
- package/dist/server/agent-service.js +95 -5
- package/dist/server/bg-servers.js +111 -26
- package/dist/server/client-state.js +2 -0
- package/dist/server/dsh/dsh-agent-service.js +2 -0
- package/dist/server/index.js +1 -0
- package/dist/server/process-utils.js +41 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/settings-service.js +33 -0
- package/dist/server/subagent-templates.js +14 -1
- package/dist/server/subagents.js +107 -11
- package/package.json +110 -110
- package/web/dist/assets/{TerminalPanel-7xM8bK8t.js → TerminalPanel-CoPFNu1d.js} +1 -1
- package/web/dist/assets/{abnfDiagram-VCTEODGH-dzrhTpxN.js → abnfDiagram-VCTEODGH-Dltz1N0G.js} +1 -1
- package/web/dist/assets/{arc-CvEtLzuM.js → arc-QOm2xRll.js} +1 -1
- package/web/dist/assets/{architectureDiagram-5GKGNRK7-1d4TEc2z.js → architectureDiagram-5GKGNRK7-L3XUBFbb.js} +1 -1
- package/web/dist/assets/{blockDiagram-I7D4REHJ-B8HBpC0d.js → blockDiagram-I7D4REHJ-CJmza4yx.js} +1 -1
- package/web/dist/assets/{c4Diagram-7LVT6UL2-CNGfPB3f.js → c4Diagram-7LVT6UL2-CwgA5WEW.js} +1 -1
- package/web/dist/assets/channel-VncP7yyT.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-CSVK1bvg.js → chunk-2Q5K7J3B-DoqSiCVF.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-BxIozqQs.js → chunk-5VM5RSS4-CO14GhQ1.js} +1 -1
- package/web/dist/assets/{chunk-F27PBJKO-QnkX6p93.js → chunk-F27PBJKO-kRFf9SDx.js} +1 -1
- package/web/dist/assets/{chunk-IMKFNOWR-kBTZpeLx.js → chunk-IMKFNOWR-Cm6JOqm0.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-CcF10PqG.js → chunk-JWPE2WC7-DTpnGTdB.js} +1 -1
- package/web/dist/assets/{chunk-POPQ4Y6H-Ch9HWJm9.js → chunk-POPQ4Y6H-CdSt_7xq.js} +1 -1
- package/web/dist/assets/{chunk-SVP7TREG-D-6N0S8z.js → chunk-SVP7TREG-IQRTD_4U.js} +1 -1
- package/web/dist/assets/{chunk-TICWLB2K-CSDXk9NS.js → chunk-TICWLB2K-Duj1rt_5.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-C7tgRoeO.js → chunk-XXDRQBXY-BFf3gcbL.js} +1 -1
- package/web/dist/assets/classDiagram-ZZMXUADV-BWCNDr-F.js +1 -0
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-BWCNDr-F.js +1 -0
- package/web/dist/assets/{cose-bilkent-JH36ORCC-V45ICdWW.js → cose-bilkent-JH36ORCC-D5k0Z1dx.js} +1 -1
- package/web/dist/assets/{cynefin-OW5HDTMX-DkjrprMv.js → cynefin-OW5HDTMX-OsV-x0h-.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-CSFzGNbH.js → cynefinDiagram-5FMLGOSQ-DM7XHm4s.js} +1 -1
- package/web/dist/assets/{dagre-GXQ25YYZ-B2tQ52ot.js → dagre-GXQ25YYZ-B0bIbgMh.js} +1 -1
- package/web/dist/assets/{diagram-S7CK7UJ4-CAeTly-S.js → diagram-S7CK7UJ4-lMVsMjfI.js} +1 -1
- package/web/dist/assets/{diagram-UQ7AKVKN-D6_khPwH.js → diagram-UQ7AKVKN-BJDbkaYf.js} +1 -1
- package/web/dist/assets/{diagram-VSXAHHWV-DDaGr2Z2.js → diagram-VSXAHHWV-1zUfVr-L.js} +1 -1
- package/web/dist/assets/{diagram-VX7I27RA-8nsSxNoG.js → diagram-VX7I27RA-B-Qy3dth.js} +1 -1
- package/web/dist/assets/{diagram-Z3DM3KII-CQFF2G65.js → diagram-Z3DM3KII-DbKcoZFP.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-PWID7BFC-CmR6-1Mj.js → ebnfDiagram-PWID7BFC-CuMZBzDi.js} +1 -1
- package/web/dist/assets/{erDiagram-RLTQ6QDP-CCnQ4eXZ.js → erDiagram-RLTQ6QDP-Dkw_Uhg2.js} +1 -1
- package/web/dist/assets/{flowDiagram-HODETNUW-bpunOdNf.js → flowDiagram-HODETNUW-CwDYMy38.js} +1 -1
- package/web/dist/assets/{ganttDiagram-EL5Y4UJY-Do5WVm61.js → ganttDiagram-EL5Y4UJY-B2s3myqg.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-DRBgLSBf.js → gitGraphDiagram-WWUBYQGX-CJ3rUTG5.js} +1 -1
- package/web/dist/assets/{index-D2iRpLNi.js → index-30RM921r.js} +19 -19
- package/web/dist/assets/index-B_lYcsGw.css +10 -0
- package/web/dist/assets/{infoDiagram-27XIBGKW-9xdIyGW9.js → infoDiagram-27XIBGKW-DuIoOHyL.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-5VMMS53U-C1HHSxnn.js → ishikawaDiagram-5VMMS53U-Fm14e_CH.js} +1 -1
- package/web/dist/assets/{journeyDiagram-3NMN7TZE-BTroj2Jo.js → journeyDiagram-3NMN7TZE-BuokOq_3.js} +1 -1
- package/web/dist/assets/{kanban-definition-UXKFOSKX-ByvuSwPg.js → kanban-definition-UXKFOSKX-D2FQMRnh.js} +1 -1
- package/web/dist/assets/{layout-CJG7qpZa.js → layout-CZUvkkNF.js} +1 -1
- package/web/dist/assets/{linear-CIQgXZYV.js → linear-tIFeS1QT.js} +1 -1
- package/web/dist/assets/{mermaid.core-CkrYNntI.js → mermaid.core-BI5_3AOv.js} +5 -5
- package/web/dist/assets/{mindmap-definition-YA3MSWOX-C2wu2sJd.js → mindmap-definition-YA3MSWOX-D5RmpgpB.js} +1 -1
- package/web/dist/assets/{pegDiagram-XKGWAZYB-pyyiTfDO.js → pegDiagram-XKGWAZYB-DpZxaRI3.js} +1 -1
- package/web/dist/assets/{pieDiagram-E7YTZNPT-BwJv35J9.js → pieDiagram-E7YTZNPT-Co7MADQz.js} +1 -1
- package/web/dist/assets/{quadrantDiagram-AXDQQJYC-1Q0wSsql.js → quadrantDiagram-AXDQQJYC-BsRraVQ-.js} +1 -1
- package/web/dist/assets/{railroadDiagram-O6MQD6OU-DWFZGo9j.js → railroadDiagram-O6MQD6OU-DW2pw5p_.js} +1 -1
- package/web/dist/assets/{requirementDiagram-BXWQKSXE-CNgNvQO7.js → requirementDiagram-BXWQKSXE-DeaEY9Dk.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-P5KCCOFB-DWmOe-31.js → sankeyDiagram-P5KCCOFB-BlKg1xhH.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-CvFRnceu.js → sequenceDiagram-WJ2MYXX4-C0ffpRmx.js} +1 -1
- package/web/dist/assets/{sizeCapture-INFHLROL-DNyzv4qr.js → sizeCapture-INFHLROL-DHFDdkAo.js} +1 -1
- package/web/dist/assets/{stateDiagram-D77RDMKH-CUOwyJAj.js → stateDiagram-D77RDMKH-CwKStGSS.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-B0qJoPpx.js +1 -0
- package/web/dist/assets/{swimlanes-42K2YHIH-B2tqXq5C.js → swimlanes-42K2YHIH-MB6Uskqk.js} +1 -1
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-IYPIFoU6.js +8 -0
- package/web/dist/assets/{timeline-definition-24CTP7MA-BC349pHg.js → timeline-definition-24CTP7MA-CkcuHQ1A.js} +1 -1
- package/web/dist/assets/{vennDiagram-4TSXK5OY-Dp_8OJ1R.js → vennDiagram-4TSXK5OY-DRiGv5ga.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-VM6X3IG4-aZq6zGi4.js → wardleyDiagram-VM6X3IG4-Cwldlf89.js} +1 -1
- package/web/dist/assets/{xychartDiagram-S5SC5T6Z-Cyq-4VPK.js → xychartDiagram-S5SC5T6Z-Biqu8RpV.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/channel-DXmrQIC_.js +0 -1
- package/web/dist/assets/classDiagram-ZZMXUADV-sfceoHNh.js +0 -1
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-sfceoHNh.js +0 -1
- package/web/dist/assets/index-BVwF4g4j.css +0 -10
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-CqyXAKwd.js +0 -1
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-DzTxUWmy.js +0 -8
package/README.md
CHANGED
|
@@ -70,8 +70,8 @@ theme switching, and a full settings panel — tuned for daily development.
|
|
|
70
70
|
|
|
71
71
|
### 🤖 Subagents & templates
|
|
72
72
|
|
|
73
|
-
- **First-party subagents** — spawn independent background conversations for parallel exploration / implementation / review (`subagent_spawn`);
|
|
74
|
-
- **Subagent templates** — configure reusable presets in Settings → Subagent templates: a role system prompt (append or replace)
|
|
73
|
+
- **First-party subagents** — spawn independent background conversations for parallel exploration / implementation / review (`subagent_spawn`, with optional `model` override or a template's model); collect results without polling via `subagent_wait_all` (blocks until every subagent finishes, then summarizes results/errors). Manage them like a chat right in the left panel: view live output, inject follow-ups (steer), abort, dismiss — failed runs surface a red dot in the running list and an error notice in the main chat. In-memory sessions — they never touch the history / resume list, and can be nested.
|
|
74
|
+
- **Subagent templates** — configure reusable presets in Settings → Subagent templates: a role system prompt (append or replace), skills & extensions whitelists, and an optional per-template model. The AI picks one via the `subagent_templates` tool and `subagent_spawn(template="…")`, or spawns without one (default = follow the main conversation's current model, or the global default subagent model set in the same panel). Disabled templates stay in the panel for re-enabling but become invisible to the AI tools (can't be listed or picked). Templates are shared globally across browser clients (`<dataDir>/subagent-templates.json`). Six built-in templates (review / implement / research / scout / audit / delegate, adapted from the pi-subagents community projects) seed the list on first run — marked 「Built-in」, editable and deletable like any other.
|
|
75
75
|
|
|
76
76
|
### 🖼️ Files, images & attachments
|
|
77
77
|
|
|
@@ -589,8 +589,12 @@ export class ClientSession {
|
|
|
589
589
|
}
|
|
590
590
|
}
|
|
591
591
|
/** 创建子代理 conversation(inMemory runtime + 独立 terminals),listed 入左栏,
|
|
592
|
-
* 并在其上触发一次完整回合。返回 convId(= 工具 runId)。
|
|
593
|
-
|
|
592
|
+
* 并在其上触发一次完整回合。返回 convId(= 工具 runId)。
|
|
593
|
+
*
|
|
594
|
+
* `model`(可选):"provider/id",显式指定子代理模型。不传时由调用方决定是否
|
|
595
|
+
* 回退到模板模型 / 设置面板默认模型;null = 跟随主对话当前模型(默认行为,
|
|
596
|
+
* runtime 重建时会继承共享 ModelRuntime 的当前默认)。 */
|
|
597
|
+
async spawnSubagentConversation(prompt, type, cwd, apply, model) {
|
|
594
598
|
const conversationId = `sa-${randomUUID().slice(0, 8)}`;
|
|
595
599
|
const terminals = this.makeTerminalManager(conversationId, cwd);
|
|
596
600
|
const runtime = await createAgentSessionRuntime(this.makeRuntimeFactory(terminals, apply), {
|
|
@@ -614,6 +618,41 @@ export class ClientSession {
|
|
|
614
618
|
catch {
|
|
615
619
|
// 绑定失败不阻断运行。
|
|
616
620
|
}
|
|
621
|
+
// 指定模型(显式 model 参数 → 模板 model → 设置面板默认)时,在首回合前
|
|
622
|
+
// 给子代理会话换模型;全都不给 = 跟随主对话:把发起会话当前的模型也
|
|
623
|
+
// 显式搬过来(新 runtime 的默认模型未必等于主对话刚选的模型)。
|
|
624
|
+
const resolvedModel = model ?? (apply?.model?.trim() || null) ?? (this.settingsSvc.current.subagentDefaultModel || null);
|
|
625
|
+
const followModel = resolvedModel ? resolvedModel : this.session.model ? `${this.session.model.provider}/${this.session.model.id}` : null;
|
|
626
|
+
if (followModel) {
|
|
627
|
+
const slash = followModel.indexOf("/");
|
|
628
|
+
const m = slash > 0 && slash < followModel.length - 1
|
|
629
|
+
? this.sharedModelRuntime?.getModel(followModel.slice(0, slash), followModel.slice(slash + 1))
|
|
630
|
+
: undefined;
|
|
631
|
+
if (m) {
|
|
632
|
+
try {
|
|
633
|
+
// 先恢复该 provider 的项目密钥(setModel 的鉴权检查要用),再换模型。
|
|
634
|
+
await this.restoreKeyForModel(followModel, cwd);
|
|
635
|
+
await conv.session.setModel(m);
|
|
636
|
+
}
|
|
637
|
+
catch (err) {
|
|
638
|
+
// 换模型失败不阻断运行——沿用默认模型继续。
|
|
639
|
+
this.emit({
|
|
640
|
+
type: "notice",
|
|
641
|
+
level: "warning",
|
|
642
|
+
text: `子代理模型切换失败(将按默认模型运行):${followModel}(${err.message})`,
|
|
643
|
+
textEn: `Failed to set subagent model, running with default: ${followModel} (${err.message})`,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
this.emit({
|
|
649
|
+
type: "notice",
|
|
650
|
+
level: "warning",
|
|
651
|
+
text: `子代理模型不存在,将按默认模型运行:${followModel}`,
|
|
652
|
+
textEn: `Subagent model not found, running with default: ${followModel}`,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
617
656
|
// 触发回合(后台执行;失败转识为通知)。
|
|
618
657
|
void conv.session.sendUserMessage(prompt).catch((err) => {
|
|
619
658
|
this.emit({
|
|
@@ -648,6 +687,7 @@ export class ClientSession {
|
|
|
648
687
|
catch {
|
|
649
688
|
// session being replaced — report defaults
|
|
650
689
|
}
|
|
690
|
+
const { error, canceled } = this.subagentRunOutcome(conv);
|
|
651
691
|
return {
|
|
652
692
|
convId: conv.id,
|
|
653
693
|
type: conv.subagentType ?? "general",
|
|
@@ -655,11 +695,38 @@ export class ClientSession {
|
|
|
655
695
|
prompt: "",
|
|
656
696
|
state,
|
|
657
697
|
streaming,
|
|
698
|
+
error,
|
|
699
|
+
canceled,
|
|
658
700
|
messageCount,
|
|
659
701
|
model: conv.session.model?.id,
|
|
660
702
|
output: conv.session.getLastAssistantText() ?? "",
|
|
661
703
|
};
|
|
662
704
|
}
|
|
705
|
+
/** 子代理最近一次运行的结局:最后一条 assistant 消息的 errorMessage / stopReason。
|
|
706
|
+
* 报错 > 中止 > 正常,三者互斥;无 assistant 消息时返回空。 */
|
|
707
|
+
subagentRunOutcome(conv) {
|
|
708
|
+
try {
|
|
709
|
+
const msgs = conv.session.agent.state.messages;
|
|
710
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
711
|
+
const m = msgs[i];
|
|
712
|
+
if (m.role !== "assistant")
|
|
713
|
+
continue;
|
|
714
|
+
const err = m.errorMessage;
|
|
715
|
+
if (typeof err === "string" && err.trim()) {
|
|
716
|
+
return { error: err.trim() };
|
|
717
|
+
}
|
|
718
|
+
const stop = m.stopReason;
|
|
719
|
+
if (stop === "aborted" || stop === "cancelled") {
|
|
720
|
+
return { canceled: true };
|
|
721
|
+
}
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
catch {
|
|
726
|
+
// session being replaced — treat as no outcome yet
|
|
727
|
+
}
|
|
728
|
+
return {};
|
|
729
|
+
}
|
|
663
730
|
emitTerminal(conversationId, msg) {
|
|
664
731
|
// Background conversations keep collecting output in their own PTY buffer.
|
|
665
732
|
// Do not stream it into the active xterm; push the retained window on switch.
|
|
@@ -738,13 +805,14 @@ export class ClientSession {
|
|
|
738
805
|
* 切换查看 / 输入补充(steer)/ 中止(abort)/ 移出全部复用现有对话机制。
|
|
739
806
|
*/
|
|
740
807
|
subagentHost = {
|
|
741
|
-
spawnSubagent: (prompt, type, cwd, templateName) => {
|
|
808
|
+
spawnSubagent: (prompt, type, cwd, templateName, model) => {
|
|
742
809
|
// 模板:存在且启用时应用;传了名字但不可用 → 抛错让工具转给 AI。
|
|
743
810
|
const tpl = templateName ? this.subagentTemplates.get(templateName) : undefined;
|
|
744
811
|
if (templateName && (!tpl || !tpl.enabled)) {
|
|
745
812
|
throw new Error(`子代理模板不可用:${templateName}(不存在或已停用)`);
|
|
746
813
|
}
|
|
747
|
-
|
|
814
|
+
// 模型优先级:显式 model 参数 > 模板自带模型 > 设置面板默认模型;都不给 = 跟随主对话。
|
|
815
|
+
return this.spawnSubagentConversation(prompt, type, cwd, tpl, model);
|
|
748
816
|
},
|
|
749
817
|
getSubagent: (convId) => this.getSubagentSnapshot(convId),
|
|
750
818
|
listSubagents: () => this.listSubagentSnapshots(),
|
|
@@ -764,7 +832,7 @@ export class ClientSession {
|
|
|
764
832
|
listTemplates: () => this.subagentTemplates
|
|
765
833
|
.list()
|
|
766
834
|
.filter((t) => t.enabled)
|
|
767
|
-
.map((t) => ({ name: t.name, description: t.description })),
|
|
835
|
+
.map((t) => ({ name: t.name, description: t.description, model: t.model })),
|
|
768
836
|
isTemplateUsable: (name) => {
|
|
769
837
|
const t = this.subagentTemplates.get(name);
|
|
770
838
|
return !!t && t.enabled;
|
|
@@ -1456,6 +1524,26 @@ export class ClientSession {
|
|
|
1456
1524
|
}
|
|
1457
1525
|
break;
|
|
1458
1526
|
}
|
|
1527
|
+
// 子代理运行报错(provider 400 / 超时等)→ 通知主对话,让用户/AI 知道
|
|
1528
|
+
// 拿回的结果可能是空或无意义的(否则子代理只是安静地停在「done」,
|
|
1529
|
+
// 主对话永远收不到失败信号)。错误文本变化时允许再次通知(去重)。
|
|
1530
|
+
if (conv.isSubagent) {
|
|
1531
|
+
const { error } = this.subagentRunOutcome(conv);
|
|
1532
|
+
if (error && error !== conv.subagentErrorNotified) {
|
|
1533
|
+
conv.subagentError = error;
|
|
1534
|
+
conv.subagentErrorNotified = error;
|
|
1535
|
+
this.emit({
|
|
1536
|
+
type: "notice",
|
|
1537
|
+
level: "error",
|
|
1538
|
+
text: `子代理 ${conv.id.slice(0, 8)}(${conv.subagentType ?? "general"})运行失败:${error}`,
|
|
1539
|
+
textEn: `Subagent ${conv.id.slice(0, 8)} (${conv.subagentType ?? "general"}) failed: ${error}`,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
else if (error) {
|
|
1543
|
+
conv.subagentError = error;
|
|
1544
|
+
}
|
|
1545
|
+
this.emitConversations();
|
|
1546
|
+
}
|
|
1459
1547
|
// Goal review hook lives in GoalService.onAgentEnd(conv, false).
|
|
1460
1548
|
this.goalSvc.onAgentEnd(conv, false);
|
|
1461
1549
|
// Deferred settings reload: settings (system prompt / skills /
|
|
@@ -2907,6 +2995,8 @@ export class ClientSession {
|
|
|
2907
2995
|
messageCount,
|
|
2908
2996
|
isStreaming,
|
|
2909
2997
|
isSubagent: !!conv.isSubagent,
|
|
2998
|
+
// 子代理带 error 标记:左栏红点提示(普通对话不参与)。
|
|
2999
|
+
...(conv.isSubagent ? this.subagentRunOutcome(conv) : {}),
|
|
2910
3000
|
});
|
|
2911
3001
|
}
|
|
2912
3002
|
this.emit({
|
|
@@ -1,7 +1,68 @@
|
|
|
1
|
-
import { killPidTree, lookupProcessName, lookupProcessCommandLine, snapshotListeningPorts } from "./process-utils.js";
|
|
1
|
+
import { killPidTree, lookupProcessName, lookupProcessCommandLine, snapshotListeningPorts, snapshotProcessParents, } from "./process-utils.js";
|
|
2
2
|
const BG_REFRESH_INTERVAL_MS = 30_000;
|
|
3
3
|
/** bash 结束后等这么久再拍「后」快照——给后台服务绑定端口的时间。 */
|
|
4
4
|
const BG_BIND_WAIT_MS = 1500;
|
|
5
|
+
/**
|
|
6
|
+
* 基本不可能由 AI 启动的常驻桌面软件进程名(小写)。命中即跳过,避免面板被
|
|
7
|
+
* 微信/QQ 等本地软件的动态监听端口污染。注意 Chrome 不进黑名单:AI 会用
|
|
8
|
+
* Playwright 拉浏览器做模拟,它靠下方父链回溯判定(Playwright 起的 chrome
|
|
9
|
+
* 能回溯到服务器进程,自己开的 chrome 父链是 explorer,分得开)。
|
|
10
|
+
*/
|
|
11
|
+
export const NON_AGENT_PROCESS_NAMES = new Set([
|
|
12
|
+
"wechat.exe",
|
|
13
|
+
"weixin.exe",
|
|
14
|
+
"wechatappex.exe",
|
|
15
|
+
"qq.exe",
|
|
16
|
+
"tim.exe",
|
|
17
|
+
"telegram.exe",
|
|
18
|
+
"dingtalk.exe",
|
|
19
|
+
"explorer.exe",
|
|
20
|
+
"searchhost.exe",
|
|
21
|
+
"searchapp.exe",
|
|
22
|
+
"svchost.exe",
|
|
23
|
+
"winlogon.exe",
|
|
24
|
+
"dwm.exe",
|
|
25
|
+
"csrss.exe",
|
|
26
|
+
"conhost.exe",
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* 判定 bash 后新出现的监听端口进程是否该记入后台任务列表(即「AI 启动的」)。
|
|
30
|
+
* 判定规则(查不到就保守记录,宁多勿漏):
|
|
31
|
+
* 1. 进程名命中黑名单(如 WeChat.exe,AI 不会去启动微信)→ 跳过;
|
|
32
|
+
* 2. 进程树可查时沿父链向上回溯:
|
|
33
|
+
* - 撞上服务器进程(serverPid,AI 的任何 bash/execFile 都从它 spawn)→ 记录;
|
|
34
|
+
* - 撞上本次 diff 出的其他新 pid(bash 留下的中间层,如 concurrently/npm
|
|
35
|
+
* 一类进程,它自己不监听端口所以没进 diff,但子进程监听)→ 记录;
|
|
36
|
+
* - 完整回溯到系统根(pid 0/1)都未命中 → 桌面软件自启(如自己开的 Chrome,
|
|
37
|
+
* 父链 explorer→…→0),跳过;
|
|
38
|
+
* 3. 父链断链(父进程已退出/reparent,快照里查不到级联)→ 保守记录。
|
|
39
|
+
* @param parents 全量 pid→ppid 映射,undefined = 查询失败(跳过父链判定,只留黑名单)。
|
|
40
|
+
* @param newPids 本次 bash 前后 diff 出的全部新监听 pid。
|
|
41
|
+
*/
|
|
42
|
+
export function shouldTrackBackgroundServer(pid, parents, newPids, serverPid, name) {
|
|
43
|
+
if (name && NON_AGENT_PROCESS_NAMES.has(name.toLowerCase()))
|
|
44
|
+
return false;
|
|
45
|
+
if (!parents)
|
|
46
|
+
return true; // 进程树查不到 → 保守记录
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
let cur = pid;
|
|
49
|
+
while (cur !== undefined && !seen.has(cur)) {
|
|
50
|
+
seen.add(cur);
|
|
51
|
+
if (cur === serverPid)
|
|
52
|
+
return true;
|
|
53
|
+
if (cur !== pid && newPids.has(cur))
|
|
54
|
+
return true;
|
|
55
|
+
const next = parents.get(cur);
|
|
56
|
+
if (next === undefined) {
|
|
57
|
+
// 到达系统根(0/1)→ 完整链,桌面软件;否则是断链 → 保守记录
|
|
58
|
+
return cur <= 1 ? false : true;
|
|
59
|
+
}
|
|
60
|
+
if (next === cur)
|
|
61
|
+
return false; // 自引用根,同样视为完整链终点
|
|
62
|
+
cur = next;
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
5
66
|
export class BgServerTracker {
|
|
6
67
|
opts;
|
|
7
68
|
servers = new Map();
|
|
@@ -38,38 +99,62 @@ export class BgServerTracker {
|
|
|
38
99
|
return;
|
|
39
100
|
await new Promise((r) => setTimeout(r, BG_BIND_WAIT_MS));
|
|
40
101
|
const after = await snapshotListeningPorts();
|
|
41
|
-
|
|
102
|
+
const fresh = [];
|
|
42
103
|
for (const [port, pid] of after) {
|
|
43
104
|
if (!before.has(port) && !this.servers.has(port)) {
|
|
44
|
-
|
|
45
|
-
added = true;
|
|
46
|
-
// Best-effort process name + full command line so the panel shows
|
|
47
|
-
// something readable (name) AND what is actually running (command).
|
|
48
|
-
void lookupProcessName(pid).then((name) => {
|
|
49
|
-
const cur = this.servers.get(port);
|
|
50
|
-
if (cur && cur.pid === pid && name) {
|
|
51
|
-
cur.name = name;
|
|
52
|
-
this.push();
|
|
53
|
-
}
|
|
54
|
-
});
|
|
55
|
-
void lookupProcessCommandLine(pid).then((command) => {
|
|
56
|
-
const cur = this.servers.get(port);
|
|
57
|
-
if (cur && cur.pid === pid && command) {
|
|
58
|
-
cur.command = command;
|
|
59
|
-
this.push();
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
this.opts.emit({
|
|
63
|
-
type: "notice",
|
|
64
|
-
level: "info",
|
|
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`,
|
|
67
|
-
});
|
|
105
|
+
fresh.push({ port, pid });
|
|
68
106
|
}
|
|
69
107
|
}
|
|
108
|
+
if (fresh.length === 0)
|
|
109
|
+
return;
|
|
110
|
+
// 剔除桌面软件误报(微信等)后,剩下的才是 AI 启动的后台服务。
|
|
111
|
+
const keep = await this.filterAgentSpawned(fresh);
|
|
112
|
+
let added = false;
|
|
113
|
+
for (const { port, pid } of keep) {
|
|
114
|
+
if (this.servers.has(port))
|
|
115
|
+
continue;
|
|
116
|
+
this.servers.set(port, { pid, since: Date.now() });
|
|
117
|
+
added = true;
|
|
118
|
+
// Best-effort process name + full command line so the panel shows
|
|
119
|
+
// something readable (name) AND what is actually running (command).
|
|
120
|
+
void lookupProcessName(pid).then((name) => {
|
|
121
|
+
const cur = this.servers.get(port);
|
|
122
|
+
if (cur && cur.pid === pid && name) {
|
|
123
|
+
cur.name = name;
|
|
124
|
+
this.push();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
void lookupProcessCommandLine(pid).then((command) => {
|
|
128
|
+
const cur = this.servers.get(port);
|
|
129
|
+
if (cur && cur.pid === pid && command) {
|
|
130
|
+
cur.command = command;
|
|
131
|
+
this.push();
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
this.opts.emit({
|
|
135
|
+
type: "notice",
|
|
136
|
+
level: "info",
|
|
137
|
+
text: `检测到 AI 启动的后台服务:端口 ${port}(pid ${pid})——可在顶栏「后台任务」里单独停止或全部关闭`,
|
|
138
|
+
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`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
70
141
|
if (added)
|
|
71
142
|
this.push();
|
|
72
143
|
}
|
|
144
|
+
/** 并行拉进程树与各进程名,用黑名单 + 父链回溯剔除桌面软件误报。 */
|
|
145
|
+
async filterAgentSpawned(fresh) {
|
|
146
|
+
const parents = await snapshotProcessParents();
|
|
147
|
+
const names = await Promise.all(fresh.map((f) => lookupProcessName(f.pid)));
|
|
148
|
+
const newPids = new Set(fresh.map((f) => f.pid));
|
|
149
|
+
const out = [];
|
|
150
|
+
for (let i = 0; i < fresh.length; i++) {
|
|
151
|
+
const { port, pid } = fresh[i];
|
|
152
|
+
if (shouldTrackBackgroundServer(pid, parents, newPids, process.pid, names[i])) {
|
|
153
|
+
out.push({ port, pid });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
73
158
|
/** The current background-server list, oldest first. 合并插件任务。 */
|
|
74
159
|
list() {
|
|
75
160
|
const out = [...this.servers.entries()]
|
|
@@ -193,6 +193,7 @@ export class ClientStateStore {
|
|
|
193
193
|
visionBridgeModel: s?.settings?.visionBridgeModel ?? null,
|
|
194
194
|
visionBridgePromptMode: s?.settings?.visionBridgePromptMode === "replace" ? "replace" : "append",
|
|
195
195
|
visionBridgePrompt: s?.settings?.visionBridgePrompt ?? "",
|
|
196
|
+
subagentDefaultModel: s?.settings?.subagentDefaultModel ?? null,
|
|
196
197
|
reviewPrompt: s?.settings?.reviewPrompt ?? "",
|
|
197
198
|
reviewDisabledSkills: s?.settings?.reviewDisabledSkills ?? [],
|
|
198
199
|
disabledPlugins: s?.settings?.disabledPlugins ?? [],
|
|
@@ -215,6 +216,7 @@ export class ClientStateStore {
|
|
|
215
216
|
toolsWrap: settings.toolsWrap ?? cur.toolsWrap ?? true,
|
|
216
217
|
visionBridgeEnabled: settings.visionBridgeEnabled ?? cur.visionBridgeEnabled ?? true,
|
|
217
218
|
visionBridgeModel: settings.visionBridgeModel ?? cur.visionBridgeModel ?? null,
|
|
219
|
+
subagentDefaultModel: settings.subagentDefaultModel ?? cur.subagentDefaultModel ?? null,
|
|
218
220
|
visionBridgePromptMode: settings.visionBridgePromptMode ?? cur.visionBridgePromptMode ?? "append",
|
|
219
221
|
visionBridgePrompt: settings.visionBridgePrompt ?? cur.visionBridgePrompt ?? "",
|
|
220
222
|
reviewPrompt: settings.reviewPrompt ?? cur.reviewPrompt ?? "",
|
package/dist/server/index.js
CHANGED
|
@@ -823,6 +823,7 @@ wss.on("connection", (ws) => {
|
|
|
823
823
|
visionBridgeModel: msg.visionBridgeModel,
|
|
824
824
|
visionBridgePromptMode: msg.visionBridgePromptMode,
|
|
825
825
|
visionBridgePrompt: msg.visionBridgePrompt,
|
|
826
|
+
subagentDefaultModel: msg.subagentDefaultModel,
|
|
826
827
|
reviewPrompt: msg.reviewPrompt,
|
|
827
828
|
reviewDisabledSkills: msg.reviewDisabledSkills,
|
|
828
829
|
markersEnabled: msg.markersEnabled,
|
|
@@ -45,6 +45,47 @@ export async function snapshotListeningPorts() {
|
|
|
45
45
|
}
|
|
46
46
|
return m;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Snapshot every process's parent pid → Map<pid, ppid>. Windows: one
|
|
50
|
+
* PowerShell CIM pass (avoids spawning PowerShell once per pid); POSIX:
|
|
51
|
+
* one `ps -Ao pid=,ppid=` pass. Returns undefined when the whole query
|
|
52
|
+
* fails — callers should treat that as "cannot verify, stay conservative".
|
|
53
|
+
*/
|
|
54
|
+
export async function snapshotProcessParents() {
|
|
55
|
+
try {
|
|
56
|
+
const { execFile } = await import("node:child_process");
|
|
57
|
+
const m = new Map();
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
const out = await new Promise((resolve, reject) => execFile("powershell.exe", [
|
|
60
|
+
"-NoProfile",
|
|
61
|
+
"-NonInteractive",
|
|
62
|
+
"-Command",
|
|
63
|
+
'Get-CimInstance Win32_Process | ForEach-Object { "$($_.ProcessId):$($_.ParentProcessId)" }',
|
|
64
|
+
], { windowsHide: true, timeout: 10000 }, (err, stdout) => (err ? reject(err) : resolve(stdout))));
|
|
65
|
+
for (const line of out.split(/\r?\n/)) {
|
|
66
|
+
const [pid, ppid] = line.trim().split(":").map(Number);
|
|
67
|
+
if (Number.isFinite(pid) && Number.isFinite(ppid))
|
|
68
|
+
m.set(pid, ppid);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const out = await new Promise((resolve, reject) => execFile("ps", ["-Ao", "pid=,ppid="], { timeout: 5000 }, (err, stdout) => err ? reject(err) : resolve(stdout)));
|
|
73
|
+
for (const line of out.split(/\r?\n/)) {
|
|
74
|
+
const p = line.trim().split(/\s+/);
|
|
75
|
+
if (p.length >= 2) {
|
|
76
|
+
const pid = Number(p[0]);
|
|
77
|
+
const ppid = Number(p[1]);
|
|
78
|
+
if (Number.isFinite(pid) && Number.isFinite(ppid))
|
|
79
|
+
m.set(pid, ppid);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return m;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
48
89
|
/** Kill a pid and its whole process tree (cross-platform). */
|
|
49
90
|
export function killPidTree(pid) {
|
|
50
91
|
try {
|
|
@@ -219,6 +219,8 @@ export class SettingsService {
|
|
|
219
219
|
}),
|
|
220
220
|
subagentTemplates: this.templates.list(),
|
|
221
221
|
subagentDefaultTemplates: DEFAULT_TEMPLATES.map((t) => t.name),
|
|
222
|
+
subagentDefaultModel: this.settings.subagentDefaultModel ?? null,
|
|
223
|
+
subagentModels: this.collectSubagentModels(),
|
|
222
224
|
},
|
|
223
225
|
});
|
|
224
226
|
}
|
|
@@ -236,6 +238,30 @@ export class SettingsService {
|
|
|
236
238
|
return [];
|
|
237
239
|
}
|
|
238
240
|
}
|
|
241
|
+
/** 已配置鉴权的全部模型(子代理模型选择器用;跟随视觉桥的收集方式但不限视觉)。 */
|
|
242
|
+
collectSubagentModels() {
|
|
243
|
+
try {
|
|
244
|
+
const runtime = this.host.getSession().modelRuntime;
|
|
245
|
+
const out = [];
|
|
246
|
+
for (const p of runtime.getProviders()) {
|
|
247
|
+
if (!runtime.hasConfiguredAuth(p.id))
|
|
248
|
+
continue;
|
|
249
|
+
for (const m of runtime.getModels(p.id)) {
|
|
250
|
+
out.push({
|
|
251
|
+
provider: p.id,
|
|
252
|
+
id: m.id,
|
|
253
|
+
label: `${m.name ?? m.id} (${p.id})`,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// 稳定排序:provider 名 → 模型名。
|
|
258
|
+
return out.sort((a, b) => (a.provider === b.provider ? a.label.localeCompare(b.label) : a.provider.localeCompare(b.provider)));
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Session not ready yet — the picker stays empty until next push.
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
239
265
|
/** Persist + apply a partial settings update (prompt text/mode, toggles). */
|
|
240
266
|
async set(partial) {
|
|
241
267
|
const needsReload = partial.promptMode !== undefined ||
|
|
@@ -291,6 +317,11 @@ export class SettingsService {
|
|
|
291
317
|
if (partial.reviewDisabledSkills !== undefined) {
|
|
292
318
|
this.settings.reviewDisabledSkills = partial.reviewDisabledSkills;
|
|
293
319
|
}
|
|
320
|
+
if (partial.subagentDefaultModel !== undefined) {
|
|
321
|
+
// 空串归一为 null(跟随主对话);其余剥空白后存格式 provider/id。
|
|
322
|
+
const m = partial.subagentDefaultModel?.trim() ?? "";
|
|
323
|
+
this.settings.subagentDefaultModel = m ? m : null;
|
|
324
|
+
}
|
|
294
325
|
this.host.stateStore.saveSettings(this.host.clientId, this.settings);
|
|
295
326
|
this.push();
|
|
296
327
|
if (needsReload)
|
|
@@ -360,6 +391,8 @@ export class SettingsService {
|
|
|
360
391
|
visionBridgeModel: this.settings.visionBridgeModel,
|
|
361
392
|
visionBridgePromptMode: this.settings.visionBridgePromptMode,
|
|
362
393
|
visionBridgePrompt: this.settings.visionBridgePrompt,
|
|
394
|
+
// 子代理默认模型也不进预设——保留当前值。
|
|
395
|
+
subagentDefaultModel: this.settings.subagentDefaultModel,
|
|
363
396
|
};
|
|
364
397
|
this.host.stateStore.saveSettings(this.host.clientId, this.settings);
|
|
365
398
|
this.push();
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* subagent-templates.ts — 子代理模板库(全局共享,<dataDir>/subagent-templates.json)。
|
|
3
3
|
*
|
|
4
4
|
* 模板 = 派生子代理时套用的预设:角色系统提示词(replace/append 同主设置语义)+
|
|
5
|
-
* 技能白名单 +
|
|
5
|
+
* 技能白名单 + 扩展白名单 + 可选模型。白名单空数组 = 该维度不限定,子代理跟随主会话设置。
|
|
6
|
+
* `model` 为 "provider/id"(与 subagent_spawn 的 model 参数、设置面板子代理默认模型
|
|
7
|
+
* 同格式);空字符串 = 跟随主对话当前模型。
|
|
6
8
|
* 带 `enabled: false` 的模板停用:设置面板仍可见、可重新启用,但 AI 工具
|
|
7
9
|
* (subagent_templates / subagent_spawn)查询不到它、也不能选择它——「关闭 =
|
|
8
10
|
* 对 AI 不可见」。
|
|
@@ -14,6 +16,8 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
|
14
16
|
import { dirname } from "node:path";
|
|
15
17
|
/** 名字去空白折叠后非空且 ≤ 60 字符(工具参数可读,允许中文)。 */
|
|
16
18
|
const NAME_MAX = 60;
|
|
19
|
+
/** 模型 id 上限("provider/id",含斜杠与自定义模型目录 id)。 */
|
|
20
|
+
const MODEL_MAX = 200;
|
|
17
21
|
/**
|
|
18
22
|
* 内置默认模板(第一次运行时种子进列表;用户改动后以 <dataDir> 文件为准)。
|
|
19
23
|
* 文案改编自 pi-subagents 社区项目(tintinweb / nicobailon)的角色提示词,
|
|
@@ -21,6 +25,7 @@ const NAME_MAX = 60;
|
|
|
21
25
|
* 白名单留空 = 技能/扩展跟随主会话设置,开箱即用。
|
|
22
26
|
*/
|
|
23
27
|
export const DEFAULT_TEMPLATES = [
|
|
28
|
+
// 全部内置模板默认 model 为空字符串 = 跟随主对话模型(面板改模板时可指定专属模型)。
|
|
24
29
|
{
|
|
25
30
|
name: "review",
|
|
26
31
|
description: "代码 / 计划 / 方案 / PR 审查:有证据的 P0-P2 发现与合并结论",
|
|
@@ -46,6 +51,7 @@ export const DEFAULT_TEMPLATES = [
|
|
|
46
51
|
"\n只报告由目标改动引起或可达的具体当前问题,并为每条提供源码证明、测试/复现或契约冲突。P0 = 阻止合并;P1 = 发布前应修;P2 = 仅报告。没有符合条件的问题时,明确写「No issues found.」。",
|
|
47
52
|
enabledSkills: [],
|
|
48
53
|
enabledExtensions: [],
|
|
54
|
+
model: "",
|
|
49
55
|
enabled: true,
|
|
50
56
|
},
|
|
51
57
|
{
|
|
@@ -71,6 +77,7 @@ export const DEFAULT_TEMPLATES = [
|
|
|
71
77
|
"实现内容:X。\n改动文件:Y。\n验证:Z。\n未决风险/问题:R。\n建议下一步:N。",
|
|
72
78
|
enabledSkills: [],
|
|
73
79
|
enabledExtensions: [],
|
|
80
|
+
model: "",
|
|
74
81
|
enabled: true,
|
|
75
82
|
},
|
|
76
83
|
{
|
|
@@ -94,6 +101,7 @@ export const DEFAULT_TEMPLATES = [
|
|
|
94
101
|
"## Gaps\n未能自信回答的部分与建议下一步。",
|
|
95
102
|
enabledSkills: [],
|
|
96
103
|
enabledExtensions: [],
|
|
104
|
+
model: "",
|
|
97
105
|
enabled: true,
|
|
98
106
|
},
|
|
99
107
|
{
|
|
@@ -119,6 +127,7 @@ export const DEFAULT_TEMPLATES = [
|
|
|
119
127
|
"## Start Here\n另一个 agent 应最先打开的文件及原因。",
|
|
120
128
|
enabledSkills: [],
|
|
121
129
|
enabledExtensions: [],
|
|
130
|
+
model: "",
|
|
122
131
|
enabled: true,
|
|
123
132
|
},
|
|
124
133
|
{
|
|
@@ -135,6 +144,7 @@ export const DEFAULT_TEMPLATES = [
|
|
|
135
144
|
"只报告能由证据支持的发现,不要臆测。没有发现问题时明确说「未发现安全问题」。保持简洁。",
|
|
136
145
|
enabledSkills: [],
|
|
137
146
|
enabledExtensions: [],
|
|
147
|
+
model: "",
|
|
138
148
|
enabled: true,
|
|
139
149
|
},
|
|
140
150
|
{
|
|
@@ -148,6 +158,7 @@ export const DEFAULT_TEMPLATES = [
|
|
|
148
158
|
"- 简洁、具体,引用证据(路径、命令输出、行号)。",
|
|
149
159
|
enabledSkills: [],
|
|
150
160
|
enabledExtensions: [],
|
|
161
|
+
model: "",
|
|
151
162
|
enabled: true,
|
|
152
163
|
},
|
|
153
164
|
];
|
|
@@ -170,6 +181,8 @@ function normalize(raw) {
|
|
|
170
181
|
enabledExtensions: Array.isArray(o.enabledExtensions)
|
|
171
182
|
? o.enabledExtensions.filter((x) => typeof x === "string")
|
|
172
183
|
: [],
|
|
184
|
+
// 空字符串 = 跟随主对话;其余剥掉首尾空白,超长当脏数据丢弃。
|
|
185
|
+
model: typeof o.model === "string" ? o.model.trim() : "",
|
|
173
186
|
enabled: o.enabled !== false,
|
|
174
187
|
};
|
|
175
188
|
}
|