@co0ontty/wand 4.83.1 → 4.84.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/README.md +4 -0
- package/dist/agent-dispatch.js +5 -1
- package/dist/ai-team-availability.d.ts +1 -1
- package/dist/ai-team-prompts.js +13 -4
- package/dist/ai-team-runner.d.ts +80 -5
- package/dist/ai-team-runner.js +332 -55
- package/dist/ai-team-types.d.ts +92 -5
- package/dist/ai-team-types.js +9 -2
- package/dist/build-info.json +3 -3
- package/dist/cli-api.d.ts +1 -0
- package/dist/cli-api.js +3 -0
- package/dist/cli.js +18 -1
- package/dist/commit-task-archive.d.ts +3 -1
- package/dist/commit-task-archive.js +21 -10
- package/dist/config.d.ts +1 -1
- package/dist/config.js +73 -0
- package/dist/conversation-mentions.d.ts +16 -0
- package/dist/conversation-mentions.js +32 -0
- package/dist/conversation-service.d.ts +90 -0
- package/dist/conversation-service.js +759 -0
- package/dist/conversation-session-preview.d.ts +9 -0
- package/dist/conversation-session-preview.js +47 -0
- package/dist/conversation-task-preview.d.ts +5 -0
- package/dist/conversation-task-preview.js +8 -0
- package/dist/conversation-types.d.ts +80 -0
- package/dist/conversation-types.js +7 -0
- package/dist/core-extension-host.d.ts +27 -0
- package/dist/core-extension-host.js +138 -0
- package/dist/core-runner.d.ts +86 -0
- package/dist/core-runner.js +757 -0
- package/dist/core-status-cli.d.ts +9 -0
- package/dist/core-status-cli.js +280 -0
- package/dist/core-turn-tracker.d.ts +12 -0
- package/dist/core-turn-tracker.js +36 -0
- package/dist/decision-runner.d.ts +6 -0
- package/dist/decision-runner.js +3 -1
- package/dist/decision-tool.d.ts +2 -0
- package/dist/decision-tool.js +49 -22
- package/dist/env-utils.d.ts +7 -3
- package/dist/env-utils.js +30 -5
- package/dist/git-quick-commit.d.ts +5 -0
- package/dist/git-quick-commit.js +9 -3
- package/dist/git-utils.js +2 -1
- package/dist/harness-engine.d.ts +110 -0
- package/dist/harness-engine.js +331 -0
- package/dist/mission-diff.js +2 -1
- package/dist/model-auto-assign.d.ts +50 -0
- package/dist/model-auto-assign.js +163 -0
- package/dist/model-group-runner.d.ts +4 -0
- package/dist/model-group-runner.js +72 -0
- package/dist/model-groups.d.ts +38 -0
- package/dist/model-groups.js +138 -0
- package/dist/models.d.ts +12 -0
- package/dist/models.js +102 -8
- package/dist/npm-update-utils.js +2 -1
- package/dist/openrouter-free-cli.d.ts +25 -0
- package/dist/openrouter-free-cli.js +109 -0
- package/dist/openrouter-free-models.d.ts +85 -0
- package/dist/openrouter-free-models.js +458 -0
- package/dist/openrouter-free-selection.d.ts +5 -0
- package/dist/openrouter-free-selection.js +7 -0
- package/dist/path-repair.d.ts +10 -0
- package/dist/path-repair.js +50 -5
- package/dist/pi-auto-resources.d.ts +12 -0
- package/dist/pi-auto-resources.js +151 -0
- package/dist/pi-execution-types.d.ts +48 -0
- package/dist/pi-execution-types.js +1 -0
- package/dist/pi-execution.d.ts +9 -0
- package/dist/pi-execution.js +187 -0
- package/dist/pi-extension-resources.d.ts +13 -0
- package/dist/pi-extension-resources.js +71 -0
- package/dist/pi-openrouter-free-bridge.d.ts +2 -0
- package/dist/pi-openrouter-free-bridge.js +80 -0
- package/dist/pi-resource-bridge.d.ts +2 -0
- package/dist/pi-resource-bridge.js +73 -0
- package/dist/pi-resource-catalog.d.ts +18 -0
- package/dist/pi-resource-catalog.js +82 -0
- package/dist/pi-resource-recommendation.d.ts +29 -0
- package/dist/pi-resource-recommendation.js +169 -0
- package/dist/pi-resource-run.d.ts +8 -0
- package/dist/pi-resource-run.js +28 -0
- package/dist/pi-session-settings.d.ts +157 -0
- package/dist/pi-session-settings.js +166 -0
- package/dist/platform-action-catalog.d.ts +24 -0
- package/dist/platform-action-catalog.js +141 -0
- package/dist/platform-action-service.d.ts +37 -0
- package/dist/platform-action-service.js +138 -0
- package/dist/platform-action-types.d.ts +125 -0
- package/dist/platform-action-types.js +7 -0
- package/dist/process-manager.d.ts +21 -1
- package/dist/process-manager.js +168 -15
- package/dist/provider-catalog.js +1 -1
- package/dist/provider-cli-updater.js +29 -8
- package/dist/provider-history-scanner.d.ts +9 -0
- package/dist/provider-history-scanner.js +107 -1
- package/dist/retention.d.ts +19 -7
- package/dist/retention.js +171 -28
- package/dist/server-ai-team-routes.js +6 -2
- package/dist/server-conversation-routes.d.ts +5 -0
- package/dist/server-conversation-routes.js +91 -0
- package/dist/server-employee-routes.d.ts +2 -0
- package/dist/server-employee-routes.js +85 -11
- package/dist/server-pi-recommendation-routes.d.ts +13 -0
- package/dist/server-pi-recommendation-routes.js +67 -0
- package/dist/server-resume-routes.js +1 -1
- package/dist/server-session-routes.d.ts +4 -3
- package/dist/server-session-routes.js +182 -37
- package/dist/server-settings-routes.d.ts +5 -0
- package/dist/server-settings-routes.js +46 -0
- package/dist/server-task-routes.js +49 -4
- package/dist/server-team-dispatch-routes.d.ts +19 -0
- package/dist/server-team-dispatch-routes.js +71 -0
- package/dist/server-update-routes.d.ts +1 -1
- package/dist/server-update-routes.js +10 -5
- package/dist/server-workspace-routes.js +44 -24
- package/dist/server.d.ts +1 -0
- package/dist/server.js +148 -30
- package/dist/session-ai-context.d.ts +5 -1
- package/dist/session-ai-context.js +9 -4
- package/dist/session-registry.d.ts +4 -0
- package/dist/session-registry.js +47 -13
- package/dist/session-transport.js +8 -1
- package/dist/settings-web-access.d.ts +15 -0
- package/dist/settings-web-access.js +37 -0
- package/dist/silicon-employee-defaults.d.ts +29 -0
- package/dist/silicon-employee-defaults.js +41 -0
- package/dist/silicon-employee-dispatch.d.ts +5 -1
- package/dist/silicon-employee-dispatch.js +10 -4
- package/dist/silicon-employee-draft.d.ts +21 -4
- package/dist/silicon-employee-draft.js +103 -16
- package/dist/storage.d.ts +73 -2
- package/dist/storage.js +362 -26
- package/dist/structured-claude-protocol.js +1 -1
- package/dist/structured-client-protocol.js +11 -33
- package/dist/structured-content.d.ts +23 -0
- package/dist/structured-content.js +72 -0
- package/dist/structured-failure.d.ts +16 -0
- package/dist/structured-failure.js +36 -0
- package/dist/structured-pi-adapter.d.ts +8 -3
- package/dist/structured-pi-adapter.js +131 -30
- package/dist/structured-provider-common.d.ts +6 -0
- package/dist/structured-provider-common.js +23 -1
- package/dist/structured-runner.d.ts +45 -0
- package/dist/structured-session-manager.d.ts +78 -4
- package/dist/structured-session-manager.js +513 -129
- package/dist/structured-thinking.d.ts +15 -0
- package/dist/structured-thinking.js +46 -0
- package/dist/subagent-dispatch.d.ts +56 -0
- package/dist/subagent-dispatch.js +102 -0
- package/dist/system-employee.d.ts +4 -3
- package/dist/system-employee.js +7 -4
- package/dist/task-retention.d.ts +23 -0
- package/dist/task-retention.js +69 -0
- package/dist/task-types.d.ts +4 -0
- package/dist/task-types.js +3 -0
- package/dist/team-dispatch.d.ts +101 -0
- package/dist/team-dispatch.js +269 -0
- package/dist/tool-preview.js +7 -1
- package/dist/tui/commands.js +50 -1
- package/dist/tui/ipc-protocol.d.ts +1 -1
- package/dist/tui/ipc-server.d.ts +4 -0
- package/dist/tui/ipc-server.js +23 -0
- package/dist/turn-heartbeat.d.ts +35 -0
- package/dist/turn-heartbeat.js +64 -0
- package/dist/types.d.ts +147 -1
- package/dist/user-profile.d.ts +33 -0
- package/dist/user-profile.js +70 -0
- package/dist/web-ui/chat-message-meta.d.ts +11 -0
- package/dist/web-ui/chat-message-meta.js +47 -0
- package/dist/web-ui/content/ai-teams.js +13 -13
- package/dist/web-ui/content/scripts.js +795 -95
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +1 -6808
- package/dist/web-ui/content/vendor/qrcode/qrcode.bundle.js +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +5 -5
- package/dist/web-ui/index.d.ts +1 -1
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/markdown.d.ts +2 -0
- package/dist/web-ui/markdown.js +111 -0
- package/dist/web-ui/page.d.ts +4 -0
- package/dist/web-ui/page.js +8 -0
- package/dist/web-ui/react/local-preview/controller.d.ts +30 -0
- package/dist/web-ui/react/local-preview/controller.js +159 -0
- package/dist/web-ui/running-activity.d.ts +63 -0
- package/dist/web-ui/running-activity.js +134 -0
- package/dist/web-ui/styles.js +2 -11
- package/package.json +7 -2
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AI_TEAM_DEFAULT_MAX_STEPS, AI_TEAM_MAX_MEMBERS, AI_TEAM_MIN_MEMBERS, isSystemSiliconEmployee, memberAgents, } from "./ai-team-types.js";
|
|
3
|
+
import { DECISION_MAX_OPTIONS } from "./decision-types.js";
|
|
4
|
+
import { defaultMilestoneIdForWrite, scopedMilestoneId } from "./milestone-scope.js";
|
|
5
|
+
import { provisionalTaskTitleFromDescription } from "./task-title.js";
|
|
6
|
+
/** 团队页「直接开工」自动建卡的标记,与手写卡区分。 */
|
|
7
|
+
export const TEAM_DISPATCH_LABEL = "team_direct";
|
|
8
|
+
/** 临时派工的候选集:一次决策调用最多评估这么多员工(= 每题最多选项数)。 */
|
|
9
|
+
export const DISPATCH_CANDIDATES_PER_CALL = DECISION_MAX_OPTIONS;
|
|
10
|
+
/** 建议名单默认上限:够完成一件事,又不至于把团队开成大会。 */
|
|
11
|
+
export const DISPATCH_MAX_MEMBERS_DEFAULT = 3;
|
|
12
|
+
/** 一次规划最多发多少次判断(每批 8 人):保证响应时间与决策 worker 队列不被目录规模拖垮。 */
|
|
13
|
+
export const DISPATCH_MAX_CALLS = 8;
|
|
14
|
+
/** 入选门槛:低于这个概率视为“不参与”。概率只是参考,不是正确率。 */
|
|
15
|
+
export const DISPATCH_MIN_PROBABILITY_DEFAULT = 0.5;
|
|
16
|
+
/** 开工说明与状态的上限,与团队直接开工同口径。 */
|
|
17
|
+
export const DISPATCH_NOTE_MAX = 4000;
|
|
18
|
+
const DISPATCH_INSTRUCTION_MAX = 500;
|
|
19
|
+
const DISPATCH_DUTY_MAX = 120;
|
|
20
|
+
function collapse(value, max) {
|
|
21
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
22
|
+
return flat.length > max ? `${flat.slice(0, Math.max(1, max - 1))}…` : flat;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 可派工的员工目录。
|
|
26
|
+
* 系统运维(Wand 自己的生成式 AI 执行者)不是干活的人,永远不进候选集;
|
|
27
|
+
* 归档员工不参与派工,但历史会话仍可打开。
|
|
28
|
+
*/
|
|
29
|
+
export function listTeamDispatchCandidates(storage) {
|
|
30
|
+
return storage.listSiliconEmployees()
|
|
31
|
+
.filter((employee) => !employee.archivedAt && !isSystemSiliconEmployee(employee))
|
|
32
|
+
.map((employee) => candidateOf(employee));
|
|
33
|
+
}
|
|
34
|
+
function candidateOf(employee) {
|
|
35
|
+
return {
|
|
36
|
+
employeeId: employee.id,
|
|
37
|
+
name: employee.name,
|
|
38
|
+
duty: collapse(employee.duty ?? "", DISPATCH_DUTY_MAX),
|
|
39
|
+
tags: employee.tags ?? [],
|
|
40
|
+
avatar: employee.avatar ?? "",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function chunksOf(items, size) {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
for (let index = 0; index < items.length; index += size)
|
|
46
|
+
chunks.push(items.slice(index, index + size));
|
|
47
|
+
return chunks;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 用本地决策模型给任务挑参与员工:**只产出建议名单**,不自动开工。
|
|
51
|
+
* 每批最多 8 名候选、一名一题(是非判断),批次之间顺序调用(决策服务本身是单 worker)。
|
|
52
|
+
*/
|
|
53
|
+
export async function planTeamDispatch(request) {
|
|
54
|
+
const note = collapse(request.note ?? "", DISPATCH_NOTE_MAX);
|
|
55
|
+
if (!note)
|
|
56
|
+
throw new Error("请先写清这次要做什么(开工说明不能为空)。");
|
|
57
|
+
const maxMembers = clampInteger(request.maxMembers, DISPATCH_MAX_MEMBERS_DEFAULT, AI_TEAM_MIN_MEMBERS, AI_TEAM_MAX_MEMBERS);
|
|
58
|
+
const threshold = clampProbability(request.threshold, DISPATCH_MIN_PROBABILITY_DEFAULT);
|
|
59
|
+
const candidates = listTeamDispatchCandidates(request.storage);
|
|
60
|
+
const considered = candidates.length;
|
|
61
|
+
if (considered === 0) {
|
|
62
|
+
return emptyPlan({ considered, omitted: 0, threshold, maxMembers, note: "员工目录里还没有可派工的员工:先在「AI 团队 → 硅基员工」里建一个。" });
|
|
63
|
+
}
|
|
64
|
+
const batches = chunksOf(candidates, DISPATCH_CANDIDATES_PER_CALL);
|
|
65
|
+
const covered = batches.slice(0, DISPATCH_MAX_CALLS);
|
|
66
|
+
const omitted = Math.max(0, candidates.length - covered.length * DISPATCH_CANDIDATES_PER_CALL);
|
|
67
|
+
const scored = [];
|
|
68
|
+
let calls = 0;
|
|
69
|
+
let model = null;
|
|
70
|
+
let inputTokens = 0;
|
|
71
|
+
for (const [batchIndex, batch] of covered.entries()) {
|
|
72
|
+
const questions = {};
|
|
73
|
+
for (const [index, candidate] of batch.entries())
|
|
74
|
+
questions[`c${batchIndex}_${index}`] = {
|
|
75
|
+
type: "noul",
|
|
76
|
+
instructions: dispatchQuestion(candidate, note),
|
|
77
|
+
};
|
|
78
|
+
const result = await request.evaluate({ state: dispatchState(note), questions }, "team-dispatch", request.signal);
|
|
79
|
+
calls += 1;
|
|
80
|
+
model = model ?? result.model;
|
|
81
|
+
inputTokens += result.usage?.input_tokens ?? 0;
|
|
82
|
+
for (const [index, candidate] of batch.entries()) {
|
|
83
|
+
const answer = result.answers?.[`c${batchIndex}_${index}`];
|
|
84
|
+
const probability = typeof answer?.noul === "number" && Number.isFinite(answer.noul) ? answer.noul : 0;
|
|
85
|
+
if (probability < threshold)
|
|
86
|
+
continue;
|
|
87
|
+
scored.push({ ...candidate, probability, isLeader: false });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
scored.sort((a, b) => b.probability - a.probability || a.name.localeCompare(b.name, "zh-Hans-CN"));
|
|
91
|
+
const members = scored.slice(0, maxMembers);
|
|
92
|
+
const bench = scored.slice(maxMembers);
|
|
93
|
+
if (members[0])
|
|
94
|
+
members[0] = { ...members[0], isLeader: true };
|
|
95
|
+
return {
|
|
96
|
+
members,
|
|
97
|
+
bench,
|
|
98
|
+
considered,
|
|
99
|
+
omitted,
|
|
100
|
+
threshold,
|
|
101
|
+
maxMembers,
|
|
102
|
+
note: planNote({ members, bench, considered, omitted, threshold, calls }),
|
|
103
|
+
decision: { calls, model, inputTokens },
|
|
104
|
+
experimental: true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function dispatchQuestion(candidate, note) {
|
|
108
|
+
const tags = candidate.tags.length ? `;标签:${candidate.tags.join("、")}` : "";
|
|
109
|
+
const role = candidate.duty ? `;职责:${candidate.duty}` : "";
|
|
110
|
+
return collapse(`任务:${note}\n判断员工「${candidate.name}」是否应该参与这个任务${role}${tags}。只按能力与职责匹配判断,不要因为他已经做过别的事就默认参与。`, DISPATCH_INSTRUCTION_MAX);
|
|
111
|
+
}
|
|
112
|
+
function dispatchState(note) {
|
|
113
|
+
return collapse(`从员工目录里为一个任务挑选参与者。任务描述:${note}`, 16_000);
|
|
114
|
+
}
|
|
115
|
+
function planNote(input) {
|
|
116
|
+
if (input.members.length === 0) {
|
|
117
|
+
return `在已评估的候选里没有员工的参与概率超过 ${Math.round(input.threshold * 100)}%;可以放低门槛、补全员工职责/标签,或直接指定员工。`;
|
|
118
|
+
}
|
|
119
|
+
if (input.members.length < AI_TEAM_MIN_MEMBERS) {
|
|
120
|
+
return `只有 1 名员工的参与概率超过 ${Math.round(input.threshold * 100)}%,而团队开工至少需要 ${AI_TEAM_MIN_MEMBERS} 人:放低门槛,或从备选里再点一名。`;
|
|
121
|
+
}
|
|
122
|
+
const parts = [`从 ${input.considered} 名候选里建议 ${input.members.length} 人(${input.calls} 次本地判断)。`];
|
|
123
|
+
if (input.omitted > 0)
|
|
124
|
+
parts.push(`另有 ${input.omitted} 人未评估(一次规划最多评估 ${DISPATCH_MAX_CALLS * DISPATCH_CANDIDATES_PER_CALL} 人),可直接指定。`);
|
|
125
|
+
if (input.bench.length > 0)
|
|
126
|
+
parts.push(`另有 ${input.bench.length} 人达到门槛但超出人数上限,可作为备选。`);
|
|
127
|
+
parts.push("概率只是参考,不是正确率;确认后才会开工。");
|
|
128
|
+
return parts.join("");
|
|
129
|
+
}
|
|
130
|
+
function emptyPlan(input) {
|
|
131
|
+
return {
|
|
132
|
+
members: [],
|
|
133
|
+
bench: [],
|
|
134
|
+
considered: input.considered,
|
|
135
|
+
omitted: input.omitted,
|
|
136
|
+
threshold: input.threshold,
|
|
137
|
+
maxMembers: input.maxMembers,
|
|
138
|
+
note: input.note,
|
|
139
|
+
decision: { calls: 0, model: null, inputTokens: 0 },
|
|
140
|
+
experimental: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function clampInteger(value, fallback, min, max) {
|
|
144
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
145
|
+
if (!Number.isFinite(parsed))
|
|
146
|
+
return fallback;
|
|
147
|
+
return Math.min(max, Math.max(min, Math.floor(parsed)));
|
|
148
|
+
}
|
|
149
|
+
function clampProbability(value, fallback) {
|
|
150
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
151
|
+
if (!Number.isFinite(parsed))
|
|
152
|
+
return fallback;
|
|
153
|
+
return Math.min(0.95, Math.max(0.05, parsed));
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* 确认开工:用建议名单建一个临时团队并起 run。
|
|
157
|
+
* 名单由调用方回传(服务端重新校验员工身份),所以“建议”和“开工”之间不需要服务端存计划。
|
|
158
|
+
*/
|
|
159
|
+
export async function startTeamDispatch(request) {
|
|
160
|
+
const note = collapse(request.note ?? "", DISPATCH_NOTE_MAX);
|
|
161
|
+
if (!note)
|
|
162
|
+
throw new Error("请先写清这次要做什么(开工说明不能为空)。");
|
|
163
|
+
if (request.members.length < AI_TEAM_MIN_MEMBERS)
|
|
164
|
+
throw new Error(`团队开工至少需要 ${AI_TEAM_MIN_MEMBERS} 名员工(含负责人)。`);
|
|
165
|
+
if (request.members.length > AI_TEAM_MAX_MEMBERS)
|
|
166
|
+
throw new Error(`临时派工最多 ${AI_TEAM_MAX_MEMBERS} 名员工。`);
|
|
167
|
+
const workspace = request.storage.getWorkspace(request.workspaceId);
|
|
168
|
+
if (!workspace)
|
|
169
|
+
throw new Error("请选择工作项目。");
|
|
170
|
+
if (workspace.kind === "global")
|
|
171
|
+
throw new Error("临时派工不能在全局暂存工作区运行,请先选择或创建一个项目。");
|
|
172
|
+
if (!workspace.cwd?.trim())
|
|
173
|
+
throw new Error("这个项目还没有工作目录,请先选择一个项目。");
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
const requestedLeader = request.members.findIndex((member) => member.isLeader);
|
|
176
|
+
if (request.members.filter((member) => member.isLeader).length > 1)
|
|
177
|
+
throw new Error("只能指定一名负责人。");
|
|
178
|
+
const leaderAt = requestedLeader >= 0 ? requestedLeader : 0;
|
|
179
|
+
const members = [];
|
|
180
|
+
for (const [index, requested] of request.members.entries()) {
|
|
181
|
+
const employeeId = requested.employeeId?.trim();
|
|
182
|
+
if (!employeeId || seen.has(employeeId))
|
|
183
|
+
throw new Error("名单里有重复或缺失的员工。");
|
|
184
|
+
seen.add(employeeId);
|
|
185
|
+
const employee = request.storage.getSiliconEmployee(employeeId);
|
|
186
|
+
if (!employee)
|
|
187
|
+
throw new Error("名单里的员工不存在,请重新生成建议。");
|
|
188
|
+
if (employee.archivedAt)
|
|
189
|
+
throw new Error(`员工「${employee.name}」已归档,不能参与派工。`);
|
|
190
|
+
if (isSystemSiliconEmployee(employee))
|
|
191
|
+
throw new Error("系统运维员工不参与任务派工。");
|
|
192
|
+
const agents = memberAgents({ agents: employee.agents });
|
|
193
|
+
if (agents.length === 0)
|
|
194
|
+
throw new Error(`员工「${employee.name}」没有可用的执行候选,请先在员工设置里补上。`);
|
|
195
|
+
members.push({
|
|
196
|
+
id: `m_${randomUUID().replace(/-/g, "").slice(0, 8)}`,
|
|
197
|
+
employeeId: employee.id,
|
|
198
|
+
name: employee.name,
|
|
199
|
+
duty: collapse(employee.duty ?? "", DISPATCH_DUTY_MAX),
|
|
200
|
+
agents,
|
|
201
|
+
agent: agents[0],
|
|
202
|
+
role: "any",
|
|
203
|
+
isLeader: index === leaderAt,
|
|
204
|
+
avatar: employee.avatar ?? "",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
if (request.members.length < AI_TEAM_MIN_MEMBERS) {
|
|
208
|
+
throw new Error(`团队开工至少需要 ${AI_TEAM_MIN_MEMBERS} 名员工(含负责人)。`);
|
|
209
|
+
}
|
|
210
|
+
const title = provisionalTaskTitleFromDescription(note) || "临时派工";
|
|
211
|
+
const now = new Date().toISOString();
|
|
212
|
+
const team = {
|
|
213
|
+
id: `t_dispatch_${randomUUID().replace(/-/g, "").slice(0, 12)}`,
|
|
214
|
+
name: collapse(request.name?.trim() || `临时派工 · ${title}`, 120),
|
|
215
|
+
description: note,
|
|
216
|
+
instructions: [
|
|
217
|
+
"本团队由本地决策模型按任务描述临时组建,只在这一次任务里协作。",
|
|
218
|
+
"分工与职责以成员说明为准;开工说明是本轮的唯一目标。",
|
|
219
|
+
`成员:${members.map((member) => `${member.name}(${member.duty || "未填写职责"})`).join(";")}`,
|
|
220
|
+
].join("\n"),
|
|
221
|
+
members,
|
|
222
|
+
requirePlanApproval: false,
|
|
223
|
+
maxSteps: AI_TEAM_DEFAULT_MAX_STEPS,
|
|
224
|
+
createdAt: now,
|
|
225
|
+
updatedAt: now,
|
|
226
|
+
};
|
|
227
|
+
request.storage.saveAiTeam(team);
|
|
228
|
+
const leader = members.find((member) => member.isLeader) ?? members[0];
|
|
229
|
+
const milestoneId = defaultMilestoneIdForWrite(request.storage);
|
|
230
|
+
const task = request.storage.createWandTask({
|
|
231
|
+
workspaceId: workspace.id,
|
|
232
|
+
title,
|
|
233
|
+
description: note,
|
|
234
|
+
status: "doing",
|
|
235
|
+
labels: [TEAM_DISPATCH_LABEL],
|
|
236
|
+
milestoneId: scopedMilestoneId(request.storage, milestoneId, workspace.id) ?? milestoneId,
|
|
237
|
+
agent: leader.agents[0] ?? null,
|
|
238
|
+
executionSubject: { type: "team", id: team.id },
|
|
239
|
+
});
|
|
240
|
+
try {
|
|
241
|
+
const run = await request.runner.start({ teamId: team.id, taskId: task.id, note });
|
|
242
|
+
return { teamId: team.id, taskId: task.id, run };
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
try {
|
|
246
|
+
request.storage.deleteWandTask(task.id);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// 回滚失败不回滚错误本身:原始错误更重要。
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
request.storage.deleteAiTeam(team.id);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// 同上:临时团队即使残留也不会被误用(run 没起来)。
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** 决策服务可用性:不可用时明确报错,绝不编造名单。 */
|
|
261
|
+
export function assertDispatchDecisionReady(decisions) {
|
|
262
|
+
const status = decisions.status();
|
|
263
|
+
if (!status.enabled)
|
|
264
|
+
throw new Error("本地决策未启用(localDecision.enabled=false),无法用决策模型选人。");
|
|
265
|
+
if (!status.supported)
|
|
266
|
+
throw new Error("本地决策在当前平台不可用(首期只支持 macOS arm64),无法用决策模型选人。");
|
|
267
|
+
if (!status.configured)
|
|
268
|
+
throw new Error("本地决策运行环境未配置(pythonPath / modelPath),无法用决策模型选人。");
|
|
269
|
+
}
|
package/dist/tool-preview.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentHasStructuredImage, isStructuredImagePart } from "./structured-content.js";
|
|
1
2
|
export const TOOL_PREVIEW_LIMIT = 180;
|
|
2
3
|
/** A small, literal excerpt for a collapsed card, never generated by a model. */
|
|
3
4
|
export function compactPreviewText(value) {
|
|
@@ -112,8 +113,13 @@ export function toolResultPreview(result) {
|
|
|
112
113
|
const raw = typeof result.content === "string" ? result.content : result.content
|
|
113
114
|
.filter(part => part.type === "text" && typeof part.text === "string")
|
|
114
115
|
.slice(0, 3).map(part => part.text).join("\n");
|
|
115
|
-
if (!raw.trim())
|
|
116
|
+
if (!raw.trim()) {
|
|
117
|
+
if (Array.isArray(result.content) && contentHasStructuredImage(result.content)) {
|
|
118
|
+
const count = result.content.filter(isStructuredImagePart).length;
|
|
119
|
+
return compactPreviewText(count > 1 ? `返回 ${count} 张图片` : "返回 1 张图片");
|
|
120
|
+
}
|
|
116
121
|
return "";
|
|
122
|
+
}
|
|
117
123
|
let text = raw;
|
|
118
124
|
let exit = "";
|
|
119
125
|
if (raw.length <= 65_536 && /^\s*[{[]/.test(raw)) {
|
package/dist/tui/commands.js
CHANGED
|
@@ -942,6 +942,48 @@ function homeForUser(userName, fallback) {
|
|
|
942
942
|
}
|
|
943
943
|
return fallback;
|
|
944
944
|
}
|
|
945
|
+
function shellFromConfig(configPath) {
|
|
946
|
+
try {
|
|
947
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
948
|
+
const shell = typeof parsed.shell === "string" ? parsed.shell.trim() : "";
|
|
949
|
+
return shell || null;
|
|
950
|
+
}
|
|
951
|
+
catch {
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function shellForUser(userName, fallback) {
|
|
956
|
+
if (userName && userName !== "root") {
|
|
957
|
+
if (process.platform === "darwin") {
|
|
958
|
+
const resolved = spawnSync("dscl", [".", "-read", `/Users/${userName}`, "UserShell"], {
|
|
959
|
+
encoding: "utf8",
|
|
960
|
+
timeout: 3000,
|
|
961
|
+
});
|
|
962
|
+
const match = (resolved.stdout || "").match(/UserShell:\s*(.+)/);
|
|
963
|
+
if (resolved.status === 0 && match?.[1])
|
|
964
|
+
return match[1].trim();
|
|
965
|
+
}
|
|
966
|
+
if (process.platform === "linux") {
|
|
967
|
+
const resolved = spawnSync("getent", ["passwd", userName], { encoding: "utf8", timeout: 3000 });
|
|
968
|
+
const shell = (resolved.stdout || "").split(":")[6];
|
|
969
|
+
if (resolved.status === 0 && shell)
|
|
970
|
+
return shell.trim();
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
try {
|
|
974
|
+
const current = os.userInfo().shell?.trim();
|
|
975
|
+
if (current)
|
|
976
|
+
return current;
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
// userInfo() can throw in stripped-down service environments.
|
|
980
|
+
}
|
|
981
|
+
return process.env.SHELL?.trim() || fallback;
|
|
982
|
+
}
|
|
983
|
+
function serviceShellFor(ctx, runUser) {
|
|
984
|
+
return shellFromConfig(ctx.configPath)
|
|
985
|
+
?? shellForUser(runUser, process.platform === "darwin" ? "/bin/zsh" : "/bin/bash");
|
|
986
|
+
}
|
|
945
987
|
function statUid(targetPath) {
|
|
946
988
|
return readFileStat(targetPath).uid;
|
|
947
989
|
}
|
|
@@ -960,6 +1002,7 @@ function installSystemdService(ctx, scope) {
|
|
|
960
1002
|
const runUser = scope === "system" ? ownerUserNameForPath(ctx.configPath) : currentUserName();
|
|
961
1003
|
const fallbackHome = process.env.HOME || os.homedir();
|
|
962
1004
|
const runHome = scope === "system" ? homeForUser(runUser, fallbackHome) : fallbackHome;
|
|
1005
|
+
const runShell = serviceShellFor(ctx, runUser);
|
|
963
1006
|
// 关键:把调用 `wand service:install` 时的真实 PATH 写进 unit。
|
|
964
1007
|
// 否则 systemd 默认 PATH 极简(system scope 之前写死 `nodeBin:/usr/local/...`,
|
|
965
1008
|
// user scope 干脆没写),spawn 出的 claude/codex 子进程会撞 "command not found"
|
|
@@ -977,6 +1020,7 @@ function installSystemdService(ctx, scope) {
|
|
|
977
1020
|
`ExecStart=${nodeBin} ${wandBin} web -c ${ctx.configPath}`,
|
|
978
1021
|
`Environment=WAND_NO_TUI=1`,
|
|
979
1022
|
`Environment=PATH=${servicePath}`,
|
|
1023
|
+
`Environment=SHELL=${runShell}`,
|
|
980
1024
|
"Restart=always",
|
|
981
1025
|
"RestartSec=3",
|
|
982
1026
|
"StandardOutput=journal",
|
|
@@ -1019,6 +1063,7 @@ function installSystemdService(ctx, scope) {
|
|
|
1019
1063
|
"Type=simple",
|
|
1020
1064
|
`ExecStart=${nodeBin} ${wandBin} terminald -c ${ctx.configPath}`,
|
|
1021
1065
|
`Environment=PATH=${servicePath}`,
|
|
1066
|
+
`Environment=SHELL=${runShell}`,
|
|
1022
1067
|
"Restart=always",
|
|
1023
1068
|
"RestartSec=3",
|
|
1024
1069
|
"StandardOutput=journal",
|
|
@@ -1060,6 +1105,7 @@ function installSystemdService(ctx, scope) {
|
|
|
1060
1105
|
`scope: ${scope}`,
|
|
1061
1106
|
`unit: ${unitPath}`,
|
|
1062
1107
|
`terminal unit: ${terminalUnitPath}`,
|
|
1108
|
+
`shell: ${runShell}`,
|
|
1063
1109
|
...daemonNotes,
|
|
1064
1110
|
"daemons:",
|
|
1065
1111
|
describeDaemonEndpoints(ctx.configPath),
|
|
@@ -1122,6 +1168,7 @@ function installLaunchdService(ctx, scope) {
|
|
|
1122
1168
|
const runUser = scope === "system" ? ownerUserNameForPath(ctx.configPath) : currentUserName();
|
|
1123
1169
|
const fallbackHome = process.env.HOME || os.homedir();
|
|
1124
1170
|
const runHome = scope === "system" ? homeForUser(runUser, fallbackHome) : fallbackHome;
|
|
1171
|
+
const runShell = serviceShellFor(ctx, runUser);
|
|
1125
1172
|
// 与 systemd 同理:launchd 默认 PATH 极简,spawn 出的 claude 会找不到。
|
|
1126
1173
|
const servicePath = buildServicePath(nodeBinDir, runHome);
|
|
1127
1174
|
// launchd 默认把 stdout/stderr 丢进 /dev/null:线上出问题时连“daemon 活着但端点没了”
|
|
@@ -1151,6 +1198,7 @@ ${userNameField} <key>WorkingDirectory</key><string>${runHome}</string>
|
|
|
1151
1198
|
<dict>
|
|
1152
1199
|
<key>WAND_NO_TUI</key><string>1</string>
|
|
1153
1200
|
<key>PATH</key><string>${servicePath}</string>
|
|
1201
|
+
<key>SHELL</key><string>${runShell}</string>
|
|
1154
1202
|
<key>HOME</key><string>${runHome}</string>
|
|
1155
1203
|
</dict>
|
|
1156
1204
|
<key>RunAtLoad</key><true/>
|
|
@@ -1175,6 +1223,7 @@ ${userNameField} <key>WorkingDirectory</key><string>${runHome}</string>
|
|
|
1175
1223
|
<key>EnvironmentVariables</key>
|
|
1176
1224
|
<dict>
|
|
1177
1225
|
<key>PATH</key><string>${servicePath}</string>
|
|
1226
|
+
<key>SHELL</key><string>${runShell}</string>
|
|
1178
1227
|
<key>HOME</key><string>${runHome}</string>
|
|
1179
1228
|
</dict>
|
|
1180
1229
|
<key>RunAtLoad</key><true/>
|
|
@@ -1232,7 +1281,7 @@ ${logFields("terminald.log", "terminald-error.log")} <key>KeepAlive</key><true/
|
|
|
1232
1281
|
message: `已注册 launchd ${scope === "user" ? "用户代理" : "系统守护"}: ${plistPath}` +
|
|
1233
1282
|
(daemonNotes.length > 0 ? `(已清理 ${daemonNotes.length} 个僵尸 daemon)` : "") +
|
|
1234
1283
|
(staleDaemonNote(ctx.configPath) ?? ""),
|
|
1235
|
-
detail: [terminalDetail, started.detail, ...daemonNotes, "daemons:", describeDaemonEndpoints(ctx.configPath)]
|
|
1284
|
+
detail: [`shell: ${runShell}`, terminalDetail, started.detail, ...daemonNotes, "daemons:", describeDaemonEndpoints(ctx.configPath)]
|
|
1236
1285
|
.filter(Boolean)
|
|
1237
1286
|
.join("\n"),
|
|
1238
1287
|
};
|
|
@@ -31,7 +31,7 @@ export interface IpcSnapshotData {
|
|
|
31
31
|
}
|
|
32
32
|
export interface IpcRequest {
|
|
33
33
|
id: string;
|
|
34
|
-
cmd: "snapshot" | "ping" | "shutdown";
|
|
34
|
+
cmd: "snapshot" | "ping" | "shutdown" | "core-status" | "core-drain";
|
|
35
35
|
}
|
|
36
36
|
export interface IpcResponseOk<T = unknown> {
|
|
37
37
|
id: string;
|
package/dist/tui/ipc-server.d.ts
CHANGED
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
* Windows 不支持(socketPath 返回空字符串),调用方应跳过 startIpcServer。
|
|
5
5
|
*/
|
|
6
6
|
import { IpcSnapshotData } from "./ipc-protocol.js";
|
|
7
|
+
import type { CoreStatus } from "../core-status-cli.js";
|
|
7
8
|
export interface IpcServerDeps {
|
|
8
9
|
socketPath: string;
|
|
9
10
|
/** 由 TUI 上层组装。同步函数,避免在 socket callback 里做异步阻塞。 */
|
|
10
11
|
snapshotProvider: () => IpcSnapshotData;
|
|
11
12
|
/** attach 客户端发起 shutdown 时调用;返回 promise,resolve 后服务端退出。 */
|
|
12
13
|
onShutdown?: () => void | Promise<void>;
|
|
14
|
+
coreStatusProvider?: () => CoreStatus;
|
|
15
|
+
/** Connection-scoped admission barrier; disconnect cancels the pending restart. */
|
|
16
|
+
beginCoreDrain?: () => () => void;
|
|
13
17
|
}
|
|
14
18
|
export interface IpcServerHandle {
|
|
15
19
|
close(): Promise<void>;
|
package/dist/tui/ipc-server.js
CHANGED
|
@@ -19,7 +19,16 @@ export function startIpcServer(deps) {
|
|
|
19
19
|
}
|
|
20
20
|
catch { /* noop */ }
|
|
21
21
|
}
|
|
22
|
+
const connections = new Map();
|
|
22
23
|
const server = net.createServer((conn) => {
|
|
24
|
+
connections.set(conn, null);
|
|
25
|
+
conn.on("close", () => {
|
|
26
|
+
const release = connections.get(conn);
|
|
27
|
+
connections.delete(conn);
|
|
28
|
+
// Server shutdown must not resume queues just before manager disposal.
|
|
29
|
+
if (!closed)
|
|
30
|
+
release?.();
|
|
31
|
+
});
|
|
23
32
|
let buf = "";
|
|
24
33
|
const decoder = new StringDecoder("utf8");
|
|
25
34
|
conn.on("data", (chunk) => {
|
|
@@ -58,6 +67,18 @@ export function startIpcServer(deps) {
|
|
|
58
67
|
conn.write(JSON.stringify(resp) + "\n");
|
|
59
68
|
return;
|
|
60
69
|
}
|
|
70
|
+
case "core-status":
|
|
71
|
+
case "core-drain": {
|
|
72
|
+
if (!deps.coreStatusProvider || (req.cmd === "core-drain" && !deps.beginCoreDrain)) {
|
|
73
|
+
throw new Error(`unknown cmd: ${req.cmd}`);
|
|
74
|
+
}
|
|
75
|
+
if (req.cmd === "core-drain" && !connections.get(conn)) {
|
|
76
|
+
connections.set(conn, deps.beginCoreDrain());
|
|
77
|
+
}
|
|
78
|
+
const resp = { id: req.id, ok: true, data: deps.coreStatusProvider() };
|
|
79
|
+
conn.write(JSON.stringify(resp) + "\n");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
61
82
|
case "shutdown": {
|
|
62
83
|
const ack = { id: req.id, ok: true, data: { accepted: true } };
|
|
63
84
|
conn.write(JSON.stringify(ack) + "\n");
|
|
@@ -103,6 +124,8 @@ export function startIpcServer(deps) {
|
|
|
103
124
|
close: async () => {
|
|
104
125
|
closed = true;
|
|
105
126
|
stopSocketKeepalive();
|
|
127
|
+
for (const conn of connections.keys())
|
|
128
|
+
conn.destroy();
|
|
106
129
|
await new Promise((resolve) => {
|
|
107
130
|
server.close(() => resolve());
|
|
108
131
|
// 兜底:1s 内强制 resolve
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回合静默期心跳:一条回合真的还在跑(structured `inFlight` / PTY `ptyBusy`),但服务端
|
|
3
|
+
* 连续这么久没产生任何事件时,用**现有的 status 事件类型**重发一次权威快照(带上最新的
|
|
4
|
+
* turn 锚点),给客户端「仍在运行、只是这一句很长」的证据。
|
|
5
|
+
*
|
|
6
|
+
* 这里不新增协议、不新增事件类型,也不做无界轮询:
|
|
7
|
+
* - timer 是会话级的,只在回合在飞期间存在;
|
|
8
|
+
* - `observe()` 在回合开始和每次观测到活动时重新计时(对齐 process-manager 里
|
|
9
|
+
* `refreshPtyTurn()` 的既有做法);
|
|
10
|
+
* - `cancel()` 在回合结束 / 会话停止、删除时立即撤销;
|
|
11
|
+
* - `dispose()` 在服务关闭时清干净,不允许任何 timer 跨进程存活;
|
|
12
|
+
* - 触发一次后由 `notify` 的返回值决定是否续下一个窗口——回合不再在飞就自然停。
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* 静默阈值 30s:大致是用户开始怀疑「是不是卡死了」的时间尺度,同时保证一轮最多触发
|
|
16
|
+
* 两次心跳就把「还在跑」传达出去。注意这与 `ws-broadcast` 的 HEARTBEAT_INTERVAL_MS
|
|
17
|
+
* 不是一回事——那个只探测 socket 连接活性,不携带会话事实。
|
|
18
|
+
*/
|
|
19
|
+
export declare const TURN_QUIET_HEARTBEAT_MS = 30000;
|
|
20
|
+
export declare class TurnQuietHeartbeat {
|
|
21
|
+
/** 重发快照;返回 true 表示这一轮仍在飞,需要继续下一个静默窗口。 */
|
|
22
|
+
private readonly notify;
|
|
23
|
+
private readonly delayMs;
|
|
24
|
+
private readonly timers;
|
|
25
|
+
private disposed;
|
|
26
|
+
constructor(
|
|
27
|
+
/** 重发快照;返回 true 表示这一轮仍在飞,需要继续下一个静默窗口。 */
|
|
28
|
+
notify: (sessionId: string) => boolean, delayMs?: number);
|
|
29
|
+
/** 观测到活动(回合开始 / 输出 chunk / 状态变化):把静默窗口往后推。 */
|
|
30
|
+
observe(sessionId: string): void;
|
|
31
|
+
/** 回合收敛或会话被移除:立即撤销这个会话的静默窗口。 */
|
|
32
|
+
cancel(sessionId: string): void;
|
|
33
|
+
/** 服务关闭:一次性丢掉所有在飞的窗口,之后不再接受新的 observe。 */
|
|
34
|
+
dispose(): void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回合静默期心跳:一条回合真的还在跑(structured `inFlight` / PTY `ptyBusy`),但服务端
|
|
3
|
+
* 连续这么久没产生任何事件时,用**现有的 status 事件类型**重发一次权威快照(带上最新的
|
|
4
|
+
* turn 锚点),给客户端「仍在运行、只是这一句很长」的证据。
|
|
5
|
+
*
|
|
6
|
+
* 这里不新增协议、不新增事件类型,也不做无界轮询:
|
|
7
|
+
* - timer 是会话级的,只在回合在飞期间存在;
|
|
8
|
+
* - `observe()` 在回合开始和每次观测到活动时重新计时(对齐 process-manager 里
|
|
9
|
+
* `refreshPtyTurn()` 的既有做法);
|
|
10
|
+
* - `cancel()` 在回合结束 / 会话停止、删除时立即撤销;
|
|
11
|
+
* - `dispose()` 在服务关闭时清干净,不允许任何 timer 跨进程存活;
|
|
12
|
+
* - 触发一次后由 `notify` 的返回值决定是否续下一个窗口——回合不再在飞就自然停。
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* 静默阈值 30s:大致是用户开始怀疑「是不是卡死了」的时间尺度,同时保证一轮最多触发
|
|
16
|
+
* 两次心跳就把「还在跑」传达出去。注意这与 `ws-broadcast` 的 HEARTBEAT_INTERVAL_MS
|
|
17
|
+
* 不是一回事——那个只探测 socket 连接活性,不携带会话事实。
|
|
18
|
+
*/
|
|
19
|
+
export const TURN_QUIET_HEARTBEAT_MS = 30_000;
|
|
20
|
+
export class TurnQuietHeartbeat {
|
|
21
|
+
notify;
|
|
22
|
+
delayMs;
|
|
23
|
+
timers = new Map();
|
|
24
|
+
disposed = false;
|
|
25
|
+
constructor(
|
|
26
|
+
/** 重发快照;返回 true 表示这一轮仍在飞,需要继续下一个静默窗口。 */
|
|
27
|
+
notify, delayMs = TURN_QUIET_HEARTBEAT_MS) {
|
|
28
|
+
this.notify = notify;
|
|
29
|
+
this.delayMs = delayMs;
|
|
30
|
+
}
|
|
31
|
+
/** 观测到活动(回合开始 / 输出 chunk / 状态变化):把静默窗口往后推。 */
|
|
32
|
+
observe(sessionId) {
|
|
33
|
+
if (this.disposed)
|
|
34
|
+
return;
|
|
35
|
+
const existing = this.timers.get(sessionId);
|
|
36
|
+
if (existing)
|
|
37
|
+
clearTimeout(existing);
|
|
38
|
+
const timer = setTimeout(() => {
|
|
39
|
+
this.timers.delete(sessionId);
|
|
40
|
+
if (this.disposed)
|
|
41
|
+
return;
|
|
42
|
+
if (this.notify(sessionId))
|
|
43
|
+
this.observe(sessionId);
|
|
44
|
+
}, this.delayMs);
|
|
45
|
+
// 心跳不能让空闲进程 / 测试用例挂在 event loop 上。
|
|
46
|
+
timer.unref?.();
|
|
47
|
+
this.timers.set(sessionId, timer);
|
|
48
|
+
}
|
|
49
|
+
/** 回合收敛或会话被移除:立即撤销这个会话的静默窗口。 */
|
|
50
|
+
cancel(sessionId) {
|
|
51
|
+
const existing = this.timers.get(sessionId);
|
|
52
|
+
if (!existing)
|
|
53
|
+
return;
|
|
54
|
+
clearTimeout(existing);
|
|
55
|
+
this.timers.delete(sessionId);
|
|
56
|
+
}
|
|
57
|
+
/** 服务关闭:一次性丢掉所有在飞的窗口,之后不再接受新的 observe。 */
|
|
58
|
+
dispose() {
|
|
59
|
+
this.disposed = true;
|
|
60
|
+
for (const timer of this.timers.values())
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
this.timers.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|