pi-web-ui 0.64.4 → 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 +31 -21
- package/dist/server/dsh/dsh-sessions.js +12 -2
- package/dist/server/files-service.js +51 -28
- 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-D6-kd9NX.js → TerminalPanel-CoPFNu1d.js} +1 -1
- package/web/dist/assets/{abnfDiagram-VCTEODGH-Ciapjs2h.js → abnfDiagram-VCTEODGH-Dltz1N0G.js} +1 -1
- package/web/dist/assets/{arc-DeXTszP5.js → arc-QOm2xRll.js} +1 -1
- package/web/dist/assets/{architectureDiagram-5GKGNRK7-CT2YpHJP.js → architectureDiagram-5GKGNRK7-L3XUBFbb.js} +1 -1
- package/web/dist/assets/{blockDiagram-I7D4REHJ-CUbGhz2G.js → blockDiagram-I7D4REHJ-CJmza4yx.js} +1 -1
- package/web/dist/assets/{c4Diagram-7LVT6UL2-B3tmW0uO.js → c4Diagram-7LVT6UL2-CwgA5WEW.js} +1 -1
- package/web/dist/assets/channel-VncP7yyT.js +1 -0
- package/web/dist/assets/{chunk-2Q5K7J3B-B5eVe3ZU.js → chunk-2Q5K7J3B-DoqSiCVF.js} +1 -1
- package/web/dist/assets/{chunk-5VM5RSS4-zHW5GxYE.js → chunk-5VM5RSS4-CO14GhQ1.js} +1 -1
- package/web/dist/assets/{chunk-F27PBJKO-D9YBCGH8.js → chunk-F27PBJKO-kRFf9SDx.js} +1 -1
- package/web/dist/assets/{chunk-IMKFNOWR-DFERzrJy.js → chunk-IMKFNOWR-Cm6JOqm0.js} +1 -1
- package/web/dist/assets/{chunk-JWPE2WC7-VCholTr0.js → chunk-JWPE2WC7-DTpnGTdB.js} +1 -1
- package/web/dist/assets/{chunk-POPQ4Y6H-DgLTPLwv.js → chunk-POPQ4Y6H-CdSt_7xq.js} +1 -1
- package/web/dist/assets/{chunk-SVP7TREG-B_43mnQv.js → chunk-SVP7TREG-IQRTD_4U.js} +1 -1
- package/web/dist/assets/{chunk-TICWLB2K-DexHBk9f.js → chunk-TICWLB2K-Duj1rt_5.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-B4NDVbVt.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-D4MfizXL.js → cose-bilkent-JH36ORCC-D5k0Z1dx.js} +1 -1
- package/web/dist/assets/{cynefin-OW5HDTMX-BoCpr6Nu.js → cynefin-OW5HDTMX-OsV-x0h-.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-wCccvWLd.js → cynefinDiagram-5FMLGOSQ-DM7XHm4s.js} +1 -1
- package/web/dist/assets/{dagre-GXQ25YYZ-hZXj_BO7.js → dagre-GXQ25YYZ-B0bIbgMh.js} +1 -1
- package/web/dist/assets/{diagram-S7CK7UJ4-DgqjaR7A.js → diagram-S7CK7UJ4-lMVsMjfI.js} +1 -1
- package/web/dist/assets/{diagram-UQ7AKVKN-D2yHlsqg.js → diagram-UQ7AKVKN-BJDbkaYf.js} +1 -1
- package/web/dist/assets/{diagram-VSXAHHWV-B2ppn0d7.js → diagram-VSXAHHWV-1zUfVr-L.js} +1 -1
- package/web/dist/assets/{diagram-VX7I27RA-DGl4RYFm.js → diagram-VX7I27RA-B-Qy3dth.js} +1 -1
- package/web/dist/assets/{diagram-Z3DM3KII-Bu_MuKt2.js → diagram-Z3DM3KII-DbKcoZFP.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-PWID7BFC-B462IWhy.js → ebnfDiagram-PWID7BFC-CuMZBzDi.js} +1 -1
- package/web/dist/assets/{erDiagram-RLTQ6QDP-4N5KTVz0.js → erDiagram-RLTQ6QDP-Dkw_Uhg2.js} +1 -1
- package/web/dist/assets/{flowDiagram-HODETNUW-D5YGm0jY.js → flowDiagram-HODETNUW-CwDYMy38.js} +1 -1
- package/web/dist/assets/{ganttDiagram-EL5Y4UJY-DtPjqo-N.js → ganttDiagram-EL5Y4UJY-B2s3myqg.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-DDi56hwo.js → gitGraphDiagram-WWUBYQGX-CJ3rUTG5.js} +1 -1
- package/web/dist/assets/{index-Bfi5_bc1.js → index-30RM921r.js} +25 -25
- package/web/dist/assets/index-B_lYcsGw.css +10 -0
- package/web/dist/assets/{infoDiagram-27XIBGKW-FRX_QzzT.js → infoDiagram-27XIBGKW-DuIoOHyL.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-5VMMS53U-BYvNCcn0.js → ishikawaDiagram-5VMMS53U-Fm14e_CH.js} +1 -1
- package/web/dist/assets/{journeyDiagram-3NMN7TZE-HbYFhz4W.js → journeyDiagram-3NMN7TZE-BuokOq_3.js} +1 -1
- package/web/dist/assets/{kanban-definition-UXKFOSKX-BJFVJp0b.js → kanban-definition-UXKFOSKX-D2FQMRnh.js} +1 -1
- package/web/dist/assets/{layout-DJnxm5X9.js → layout-CZUvkkNF.js} +1 -1
- package/web/dist/assets/{linear-M6L-f6ON.js → linear-tIFeS1QT.js} +1 -1
- package/web/dist/assets/{mermaid.core-BPgLSPyb.js → mermaid.core-BI5_3AOv.js} +5 -5
- package/web/dist/assets/{mindmap-definition-YA3MSWOX-CT34wBkW.js → mindmap-definition-YA3MSWOX-D5RmpgpB.js} +1 -1
- package/web/dist/assets/{pegDiagram-XKGWAZYB-y7gUs6p-.js → pegDiagram-XKGWAZYB-DpZxaRI3.js} +1 -1
- package/web/dist/assets/{pieDiagram-E7YTZNPT-8Ksrkk_P.js → pieDiagram-E7YTZNPT-Co7MADQz.js} +1 -1
- package/web/dist/assets/{quadrantDiagram-AXDQQJYC-B1JgOvVm.js → quadrantDiagram-AXDQQJYC-BsRraVQ-.js} +1 -1
- package/web/dist/assets/{railroadDiagram-O6MQD6OU-kYgyfD-Z.js → railroadDiagram-O6MQD6OU-DW2pw5p_.js} +1 -1
- package/web/dist/assets/{requirementDiagram-BXWQKSXE-DsU0dCxH.js → requirementDiagram-BXWQKSXE-DeaEY9Dk.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-P5KCCOFB-Bn3K22Ux.js → sankeyDiagram-P5KCCOFB-BlKg1xhH.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-Ovxqv6CH.js → sequenceDiagram-WJ2MYXX4-C0ffpRmx.js} +1 -1
- package/web/dist/assets/{sizeCapture-INFHLROL-DIkQtrVp.js → sizeCapture-INFHLROL-DHFDdkAo.js} +1 -1
- package/web/dist/assets/{stateDiagram-D77RDMKH-C4ql6zER.js → stateDiagram-D77RDMKH-CwKStGSS.js} +1 -1
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-B0qJoPpx.js +1 -0
- package/web/dist/assets/{swimlanes-42K2YHIH-2hpivmJO.js → swimlanes-42K2YHIH-MB6Uskqk.js} +1 -1
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-IYPIFoU6.js +8 -0
- package/web/dist/assets/{timeline-definition-24CTP7MA-BuH_ON1U.js → timeline-definition-24CTP7MA-CkcuHQ1A.js} +1 -1
- package/web/dist/assets/{vennDiagram-4TSXK5OY-D9E14XfU.js → vennDiagram-4TSXK5OY-DRiGv5ga.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-VM6X3IG4-D6h0zd6l.js → wardleyDiagram-VM6X3IG4-Cwldlf89.js} +1 -1
- package/web/dist/assets/{xychartDiagram-S5SC5T6Z-C7uuvJqC.js → xychartDiagram-S5SC5T6Z-Biqu8RpV.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/dist/assets/channel-C-ah19CA.js +0 -1
- package/web/dist/assets/classDiagram-ZZMXUADV-BjgHzd13.js +0 -1
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-BjgHzd13.js +0 -1
- package/web/dist/assets/index-BVwF4g4j.css +0 -10
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-CM0zTC3x.js +0 -1
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-BqO7jiba.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 ?? "",
|
|
@@ -1554,29 +1554,37 @@ export class DshClientSession {
|
|
|
1554
1554
|
await this.pushSessions();
|
|
1555
1555
|
}
|
|
1556
1556
|
async pushSessions() {
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1557
|
+
try {
|
|
1558
|
+
// 目录/会话文件被删除、改名或不可读都不许把异常抛出去:本方法以
|
|
1559
|
+
// fire-and-forget(void …)方式调用,未处理的 rejection 会杀死服务进程
|
|
1560
|
+
// (issue #74 同类)。失败降级为空列表,面板显示“暂无历史”。
|
|
1561
|
+
const files = findSessionFilesForCwd(this.sessionRoot, this.cwd);
|
|
1562
|
+
const summaries = [];
|
|
1563
|
+
for (const file of files) {
|
|
1564
|
+
try {
|
|
1565
|
+
const sessionId = basename(dirname(file));
|
|
1566
|
+
// 审查会话(review-*)是内部工作会话,不进历史列表。
|
|
1567
|
+
if (sessionId.startsWith("review-"))
|
|
1568
|
+
continue;
|
|
1569
|
+
const { events } = readSessionLog(file);
|
|
1570
|
+
summaries.push({
|
|
1571
|
+
path: file,
|
|
1572
|
+
name: sessionId,
|
|
1573
|
+
firstMessage: firstUserText(events),
|
|
1574
|
+
messageCount: events.filter((e) => e.type === "user/message" || e.type === "assistant/message" || e.type === "tool/result").length,
|
|
1575
|
+
modified: statSync(file).mtimeMs,
|
|
1576
|
+
source: "web",
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
catch {
|
|
1580
|
+
/* skip unreadable */
|
|
1581
|
+
}
|
|
1577
1582
|
}
|
|
1583
|
+
this.emit({ type: "sessions", sessions: summaries });
|
|
1584
|
+
}
|
|
1585
|
+
catch {
|
|
1586
|
+
this.emit({ type: "sessions", sessions: [] });
|
|
1578
1587
|
}
|
|
1579
|
-
this.emit({ type: "sessions", sessions: summaries });
|
|
1580
1588
|
}
|
|
1581
1589
|
async renameSession(_path, _name) {
|
|
1582
1590
|
this.emit({
|
|
@@ -2067,6 +2075,8 @@ export class DshClientSession {
|
|
|
2067
2075
|
markers: [],
|
|
2068
2076
|
subagentTemplates: [],
|
|
2069
2077
|
subagentDefaultTemplates: [],
|
|
2078
|
+
subagentDefaultModel: null,
|
|
2079
|
+
subagentModels: [],
|
|
2070
2080
|
};
|
|
2071
2081
|
this.emit({ type: "settings_state", settings });
|
|
2072
2082
|
}
|
|
@@ -145,7 +145,17 @@ export function findSessionFiles(root) {
|
|
|
145
145
|
}
|
|
146
146
|
};
|
|
147
147
|
walk(root);
|
|
148
|
-
return out.sort(
|
|
148
|
+
return out.sort(sessionFileNewest);
|
|
149
|
+
}
|
|
150
|
+
/** 排序比较器:文件在扫描与排序之间被删除/改名时 statSync 会抛 ENOENT
|
|
151
|
+
* (issue #74 同类:列表刷新不能因一次正常的删除把服务进程打死)。 */
|
|
152
|
+
function sessionFileNewest(a, b) {
|
|
153
|
+
try {
|
|
154
|
+
return statSync(b).mtimeMs - statSync(a).mtimeMs;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
149
159
|
}
|
|
150
160
|
/** 一个工作区的会话文件:官方布局(root/--<cwd>--)+ 旧版 per-cwd 布局。 */
|
|
151
161
|
export function findSessionFilesForCwd(sessionRoot, cwd) {
|
|
@@ -153,7 +163,7 @@ export function findSessionFilesForCwd(sessionRoot, cwd) {
|
|
|
153
163
|
const out = [];
|
|
154
164
|
for (const d of dirs)
|
|
155
165
|
out.push(...findSessionFiles(d));
|
|
156
|
-
return out.sort(
|
|
166
|
+
return out.sort(sessionFileNewest);
|
|
157
167
|
}
|
|
158
168
|
/** 第一个用户文本(会话标题素材)。 */
|
|
159
169
|
export function firstUserText(events) {
|
|
@@ -100,8 +100,10 @@ export function workspacePath(root, raw) {
|
|
|
100
100
|
* Read a directory for the file panel. The two platforms intentionally use
|
|
101
101
|
* different strategies — do NOT unify them:
|
|
102
102
|
*
|
|
103
|
-
* darwin/linux (posix):
|
|
104
|
-
*
|
|
103
|
+
* darwin/linux (posix): hide build/dependency noise, small cap; a listing
|
|
104
|
+
* failure (deleted/renamed dir → ENOENT/ENOTDIR, permission → EACCES/EPERM)
|
|
105
|
+
* degrades to an empty listing plus a notice chosen by errorCode — the server
|
|
106
|
+
* must NEVER crash on a vanished directory (issue #74).
|
|
105
107
|
*
|
|
106
108
|
* win32: stability and completeness first, preview second. ACL-protected
|
|
107
109
|
* system dirs (C:\$Recycle.Bin, Program Files internals, OneDrive placeholders)
|
|
@@ -121,11 +123,14 @@ async function readDirForUI(abs, rel) {
|
|
|
121
123
|
dirents = await fs.readdir(abs, { withFileTypes: true });
|
|
122
124
|
}
|
|
123
125
|
catch (err) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
126
|
+
// 任何 readdir 失败都降级为“空列表 + notice”,绝不向上抛:listFiles 以
|
|
127
|
+
// fire-and-forget(void …)调用,未处理的 rejection 会成为 unhandled
|
|
128
|
+
// rejection 直接杀掉整个服务进程(issue #74:目录被删除/改名后刷新即崩)。
|
|
129
|
+
// 缺失(ENOENT/ENOTDIR)、权限拒绝(EACCES/EPERM)、系统 ACL 目录全部归此,
|
|
130
|
+
// 文案由 listFiles 按 errorCode 分类:缺失 = 软提示“目录不存在”,
|
|
131
|
+
// 权限/其余 = posix 硬错误 notice / win32 软提示(ACL 系统目录是常态)。
|
|
132
|
+
const e = err;
|
|
133
|
+
return { entries: [], truncated: false, error: e.message, errorCode: e.code };
|
|
129
134
|
}
|
|
130
135
|
const out = [];
|
|
131
136
|
for (const d of dirents) {
|
|
@@ -204,6 +209,35 @@ export class FilesService {
|
|
|
204
209
|
}
|
|
205
210
|
return [{ name: "/", path: "/", type: "dir" }];
|
|
206
211
|
}
|
|
212
|
+
/** 目录列表失败的 notice:缺失路径(ENOENT/ENOTDIR)是删除/改名等正常场景,
|
|
213
|
+
* 软提示“目录不存在”;其余(权限拒绝等)保留原平台语义——win32 软提示
|
|
214
|
+
* (ACL 系统目录常见),posix 硬错误(error 级 notice,取代曾经的 throw)。 */
|
|
215
|
+
emitListError(path, error, code) {
|
|
216
|
+
if (code === "ENOENT") {
|
|
217
|
+
this.host.emit({
|
|
218
|
+
type: "notice",
|
|
219
|
+
level: "warning",
|
|
220
|
+
text: `目录不存在:${path}`,
|
|
221
|
+
textEn: `Directory not found: ${path}`,
|
|
222
|
+
});
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (code === "ENOTDIR") {
|
|
226
|
+
this.host.emit({
|
|
227
|
+
type: "notice",
|
|
228
|
+
level: "warning",
|
|
229
|
+
text: `不是目录:${path}`,
|
|
230
|
+
textEn: `Not a directory: ${path}`,
|
|
231
|
+
});
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
this.host.emit({
|
|
235
|
+
type: "notice",
|
|
236
|
+
level: IS_WIN32 ? "warning" : "error",
|
|
237
|
+
text: `目录不可读:${error}`,
|
|
238
|
+
textEn: `Directory is not readable: ${error}`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
207
241
|
async listFiles(relPath) {
|
|
208
242
|
const { resolve, sep, relative } = await import("node:path");
|
|
209
243
|
const root = resolve(this.host.getCwd());
|
|
@@ -226,7 +260,7 @@ export class FilesService {
|
|
|
226
260
|
if (isAbsoluteWirePath(raw)) {
|
|
227
261
|
const wire = normWirePath(raw);
|
|
228
262
|
const abs = wireToAbs(wire);
|
|
229
|
-
const { entries, truncated, error } = await readDirForUI(abs, wire);
|
|
263
|
+
const { entries, truncated, error, errorCode } = await readDirForUI(abs, wire);
|
|
230
264
|
this.host.emit({
|
|
231
265
|
type: "files",
|
|
232
266
|
path: wire,
|
|
@@ -235,14 +269,8 @@ export class FilesService {
|
|
|
235
269
|
truncated,
|
|
236
270
|
absolute: true,
|
|
237
271
|
});
|
|
238
|
-
if (error)
|
|
239
|
-
this.
|
|
240
|
-
type: "notice",
|
|
241
|
-
level: "warning",
|
|
242
|
-
text: `目录不可读:${error}`,
|
|
243
|
-
textEn: `Directory is not readable: ${error}`,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
272
|
+
if (error)
|
|
273
|
+
this.emitListError(wire, error, errorCode);
|
|
246
274
|
return;
|
|
247
275
|
}
|
|
248
276
|
// ---- 工作区相对视图(原有行为) ----
|
|
@@ -260,19 +288,14 @@ export class FilesService {
|
|
|
260
288
|
// Normalize to forward slashes: the wire protocol and the frontend
|
|
261
289
|
// always use "/", but relative() returns "\\" on Windows.
|
|
262
290
|
const rel = rawRel.split(sep).join("/");
|
|
263
|
-
const { entries, truncated, error } = await readDirForUI(target, rel);
|
|
264
|
-
// Watch the listed directory
|
|
265
|
-
// dir
|
|
266
|
-
this.watchDir(target, rel);
|
|
291
|
+
const { entries, truncated, error, errorCode } = await readDirForUI(target, rel);
|
|
292
|
+
// Watch the listed directory only after a successful read — a missing
|
|
293
|
+
// dir must not create a watcher on a phantom path (issue #74).
|
|
267
294
|
if (error) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
level: "warning",
|
|
273
|
-
text: `目录不可读:${error}`,
|
|
274
|
-
textEn: `Directory is not readable: ${error}`,
|
|
275
|
-
});
|
|
295
|
+
this.emitListError(rel === "" ? root : rel, error, errorCode);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this.watchDir(target, rel);
|
|
276
299
|
}
|
|
277
300
|
this.host.emit({
|
|
278
301
|
type: "files",
|
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 {
|