lightclawbot 1.2.6-beta.0 → 1.2.7

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.
Files changed (98) hide show
  1. package/dist/src/gateway.js +50 -6
  2. package/dist/src/group/constants/index.js +20 -0
  3. package/dist/src/group/inbound/index.js +254 -0
  4. package/dist/src/group/index.js +15 -0
  5. package/dist/src/group/orchestrator/execution/agent-runner.js +299 -0
  6. package/dist/src/group/orchestrator/execution/index.js +7 -0
  7. package/dist/src/group/orchestrator/execution/prompt-builder.js +288 -0
  8. package/dist/src/group/orchestrator/execution/soul-resolver.js +38 -0
  9. package/dist/src/group/orchestrator/execution/types.js +7 -0
  10. package/dist/src/group/orchestrator/index.js +14 -0
  11. package/dist/src/group/orchestrator/lifecycle/conversation-state.js +162 -0
  12. package/dist/src/group/orchestrator/lifecycle/index.js +7 -0
  13. package/dist/src/group/orchestrator/lifecycle/ledger-writer.js +96 -0
  14. package/dist/src/group/orchestrator/lifecycle/run-registry.js +174 -0
  15. package/dist/src/group/orchestrator/orchestrator.js +265 -0
  16. package/dist/src/group/orchestrator/planning/index.js +13 -0
  17. package/dist/src/group/orchestrator/planning/plan-validator.js +233 -0
  18. package/dist/src/group/orchestrator/planning/planning-parser.js +207 -0
  19. package/dist/src/group/orchestrator/planning/subtask-executor.js +345 -0
  20. package/dist/src/group/orchestrator/planning/summarizer-runner.js +224 -0
  21. package/dist/src/group/orchestrator/routes/index.js +9 -0
  22. package/dist/src/group/orchestrator/routes/leader-dispatch.js +229 -0
  23. package/dist/src/group/orchestrator/routes/leader-orchestration-route.js +179 -0
  24. package/dist/src/group/orchestrator/routes/leader-planning.js +92 -0
  25. package/dist/src/group/orchestrator/routes/leader-self-answer.js +223 -0
  26. package/dist/src/group/orchestrator/routes/mention-concurrent-route.js +226 -0
  27. package/dist/src/group/orchestrator/routes/route-helpers.js +186 -0
  28. package/dist/src/group/orchestrator/routes/types.js +8 -0
  29. package/dist/src/group/services/group-cleanup-service.js +183 -0
  30. package/dist/src/group/services/group-creation-service.js +122 -0
  31. package/dist/src/group/services/group-deletion-service.js +111 -0
  32. package/dist/src/group/services/group-history-service.js +73 -0
  33. package/dist/src/group/services/group-member-service.js +169 -0
  34. package/dist/src/group/services/group-query-service.js +133 -0
  35. package/dist/src/group/services/group-update-service.js +144 -0
  36. package/dist/src/group/services/index.js +20 -0
  37. package/dist/src/group/storage/concurrency-manager.js +119 -0
  38. package/dist/src/group/storage/group-storage-core.js +227 -0
  39. package/dist/src/group/storage/index.js +12 -0
  40. package/dist/src/group/storage/message-reader.js +213 -0
  41. package/dist/src/group/storage/message-writer.js +229 -0
  42. package/dist/src/group/storage/slice-manager.js +165 -0
  43. package/dist/src/group/types/common.js +5 -0
  44. package/dist/src/group/types/index.js +5 -0
  45. package/dist/src/group/types/message.js +5 -0
  46. package/dist/src/group/types/orchestrator.js +5 -0
  47. package/dist/src/group/types/storage.js +5 -0
  48. package/dist/src/group/utils/id-generator.js +15 -0
  49. package/dist/src/group/utils/index.js +12 -0
  50. package/dist/src/group/utils/mime.js +36 -0
  51. package/dist/src/group/utils/normalize.js +32 -0
  52. package/dist/src/group/utils/run-helpers.js +36 -0
  53. package/dist/src/outbound.js +12 -19
  54. package/dist/src/shared.js +4 -3
  55. package/dist/src/socket/events/agents-request.js +147 -0
  56. package/dist/src/socket/events/chat-request.js +67 -0
  57. package/dist/src/socket/events/file-download.js +121 -0
  58. package/dist/src/socket/events/group-abort.js +59 -0
  59. package/dist/src/socket/events/group-history.js +59 -0
  60. package/dist/src/socket/events/group-member.js +83 -0
  61. package/dist/src/socket/events/group-request.js +91 -0
  62. package/dist/src/socket/events/history-request.js +95 -0
  63. package/dist/src/socket/events/index.js +39 -0
  64. package/dist/src/socket/events/message-private.js +82 -0
  65. package/dist/src/socket/handlers.js +53 -517
  66. package/dist/src/socket/native-socket.js +21 -20
  67. package/dist/src/socket/registry.js +6 -3
  68. package/dist/src/socket/reliable-emitter.js +16 -13
  69. package/dist/src/socket/service/chat-common.js +36 -0
  70. package/dist/src/socket/service/chat-create.js +75 -0
  71. package/dist/src/socket/service/chat-delete.js +94 -0
  72. package/dist/src/socket/service/chat-list.js +82 -0
  73. package/dist/src/socket/service/chat-update.js +83 -0
  74. package/dist/src/socket/service/group-abort.js +104 -0
  75. package/dist/src/socket/service/group-history.js +140 -0
  76. package/dist/src/socket/service/group-member.js +209 -0
  77. package/dist/src/socket/service/group.js +233 -0
  78. package/dist/src/socket/service/history.js +102 -0
  79. package/dist/src/socket/service/index.js +14 -0
  80. package/dist/src/socket/types/index.js +7 -0
  81. package/dist/src/socket/types/request.js +8 -0
  82. package/dist/src/socket/types/service.js +8 -0
  83. package/dist/src/socket/utils/agent-soul.js +95 -0
  84. package/dist/src/socket/utils/index.js +8 -0
  85. package/dist/src/socket/utils/message.js +83 -0
  86. package/dist/src/socket/utils/validate.js +42 -0
  87. package/dist/src/streaming/index.js +1 -0
  88. package/dist/src/streaming/stream-reply-sink.js +270 -14
  89. package/dist/src/streaming/types.js +20 -1
  90. package/dist/src/{download-tool.js → tools/download-tool.js} +41 -35
  91. package/dist/src/tools/group-history-tool.js +172 -0
  92. package/dist/src/{upload-tool.js → tools/upload-tool.js} +2 -2
  93. package/dist/src/tools.js +4 -3
  94. package/dist/src/utils/index.js +1 -0
  95. package/dist/src/utils/logger.js +38 -0
  96. package/openclaw.plugin.json +2 -1
  97. package/package.json +1 -1
  98. package/dist/src/socket/chat.js +0 -257
@@ -0,0 +1,265 @@
1
+ /**
2
+ * @file 群聊调度器(Orchestrator)
3
+ * @description
4
+ * 群聊消息处理的总入口,负责:
5
+ * 1. 根据用户消息中的 mentions 信息选择执行路径(Leader 决策 / Mention 并发)
6
+ * 2. 装配并注入各子模块依赖(AgentRunner、SoulResolver、PromptBuilder 等)
7
+ * 3. 管理会话生命周期(注册、abort、清理)
8
+ * 4. 发布调度过程中的关键事件(用于 metrics / 日志追踪)
9
+ *
10
+ * 设计原则:
11
+ * - 不写账本:账本写入由上游 inbound 层完成
12
+ * - 不解析 mentions:mentions 规范化由 inbound 层完成
13
+ * - 不直接调用 SDK:Agent 执行委托给 AgentRunner
14
+ * - 单例模式:每个 gateway / account 维护一份 Orchestrator 实例
15
+ */
16
+ import { ConversationStateMachine } from './lifecycle/conversation-state.js';
17
+ import { RunRegistry } from './lifecycle/run-registry.js';
18
+ import { LedgerWriter } from './lifecycle/ledger-writer.js';
19
+ import { AgentRunner } from './execution/agent-runner.js';
20
+ import { SoulResolver } from './execution/soul-resolver.js';
21
+ import { PromptBuilder } from './execution/prompt-builder.js';
22
+ import { LeaderOrchestrationRoute } from './routes/leader-orchestration-route.js';
23
+ import { MentionConcurrentRoute } from './routes/mention-concurrent-route.js';
24
+ import { CHANNEL_KEY } from '../../config.js';
25
+ import { getModuleLogger } from '../../utils/logger.js';
26
+ /**
27
+ * 群聊调度器主体
28
+ *
29
+ * @remarks
30
+ * 每个 gateway / account 维护一份实例。
31
+ * 职责边界:路径选择 + 子模块装配 + 生命周期管理 + 事件发布。
32
+ * 具体的 Agent 执行逻辑委托给 Route 实现。
33
+ *
34
+ * 路径策略:
35
+ * - mentions 非空 → MentionConcurrentRoute(并发调用被 @ 的 Agent)
36
+ * - mentions 为空 → LeaderOrchestrationRoute(默认 Agent 自决直答或拆活派发)
37
+ */
38
+ export class Orchestrator {
39
+ /** Run 注册表:管理所有活跃会话的 AbortController,支持级联 abort */
40
+ runRegistry;
41
+ /** 群账本写入器:负责将 Agent 回复写入持久化账本 */
42
+ ledger;
43
+ /** 路径共享依赖集合:所有 Route 实例共享的基础设施引用 */
44
+ routeDeps;
45
+ /** Leader 决策路径:默认 Agent 自决直答或拆活派发给其他成员 */
46
+ leaderRoute;
47
+ /** Mention 并发路径:用户 @ 了特定成员时,并发调用被 @ 的 Agent */
48
+ mentionRoute;
49
+ /** 模块日志器 */
50
+ log;
51
+ /** 事件订阅回调(可选) */
52
+ onEvent;
53
+ /**
54
+ * 构造 Orchestrator 实例
55
+ *
56
+ * @param options - 构造参数,包含 accountId、storage、emitter 等依赖
57
+ *
58
+ * @remarks
59
+ * 构造过程中会初始化以下子模块:
60
+ * - RunRegistry:会话级 abort 管理
61
+ * - LedgerWriter:账本写入
62
+ * - AgentRunner:Agent 执行器(内部通过 getLightclawRuntime() 获取 runtime)
63
+ * - SoulResolver:成员身份/能力卡解析
64
+ * - PromptBuilder:系统提示词构建
65
+ * - LeaderOrchestrationRoute / MentionConcurrentRoute:两条执行路径
66
+ */
67
+ constructor(options) {
68
+ const { accountId, storage, emitter, onEvent } = options;
69
+ this.log = getModuleLogger('group.orchestrator');
70
+ this.onEvent = onEvent;
71
+ // ── 基础设施层 ──
72
+ this.runRegistry = new RunRegistry();
73
+ // 群账本写入器:负责将 Agent 回复写入持久化账本
74
+ this.ledger = new LedgerWriter(storage);
75
+ // ── 执行层 ──
76
+ // AgentRunner 内部通过 getLightclawRuntime() 获取 PluginRuntime,无需外部传入
77
+ const runner = new AgentRunner({ accountId });
78
+ // SoulResolver:根据 agentId 解析成员的能力描述(soul card)
79
+ const soulResolver = new SoulResolver();
80
+ // PromptBuilder:基于群元数据和成员能力卡构建系统提示词
81
+ const promptBuilder = new PromptBuilder();
82
+ // ── 路径共享 deps:所有 Route 实例共享的依赖集合 ──
83
+ this.routeDeps = {
84
+ runner,
85
+ soulResolver,
86
+ promptBuilder,
87
+ runRegistry: this.runRegistry,
88
+ ledger: this.ledger,
89
+ emitter,
90
+ };
91
+ // Leader 决策路径:默认 Agent 自决直答或拆活派发给其他成员
92
+ this.leaderRoute = new LeaderOrchestrationRoute(this.routeDeps);
93
+ // Mention 并发路径:用户 @ 了特定成员时,调用被 @ 的 Agent
94
+ this.mentionRoute = new MentionConcurrentRoute(this.routeDeps);
95
+ }
96
+ /**
97
+ * 调度入口:处理一次完整的群聊回合
98
+ *
99
+ * @param ctx - 调度上下文,包含 conversationId、groupId、用户消息、mentions 等
100
+ * @returns 路径执行结果,包含最终状态、各 run 信息和可能的错误
101
+ *
102
+ * @remarks
103
+ * 执行流程:
104
+ * 1. 创建会话状态机,在 RunRegistry 中注册会话
105
+ * 2. 发布 conversation_started 事件
106
+ * 3. 根据 mentions 选择路径(Leader / Mention)
107
+ * 4. 执行选中的路径,获取结果
108
+ * 5. 异常兜底:路径执行失败时标记 FAILED 状态
109
+ * 6. 收尾:从 RunRegistry 中清理会话,发布终态事件
110
+ */
111
+ async dispatch(ctx) {
112
+ const tag = `[${CHANNEL_KEY}][conv=${ctx.conversationId}][group=${ctx.groupId}]`;
113
+ // 创建会话状态机(跟踪 IDLE → RUNNING → DONE/FAILED/ABORTED 状态流转)
114
+ const state = new ConversationStateMachine(ctx.conversationId);
115
+ // 在注册表中确保会话存在(幂等操作,返回 AbortSignal)
116
+ this.runRegistry.ensureConversation(ctx.conversationId);
117
+ this.emitEvent({
118
+ type: 'conversation_started',
119
+ conversationId: ctx.conversationId,
120
+ ts: Date.now(),
121
+ });
122
+ // ── 路径选择:根据 mentions 决定走哪条路径 ──
123
+ const route = this.selectRoute(ctx);
124
+ this.emitEvent({
125
+ type: 'route_decided',
126
+ conversationId: ctx.conversationId,
127
+ ts: Date.now(),
128
+ payload: { route, mentions: ctx.mentions },
129
+ });
130
+ this.log.info(`${tag} route=${route} mentions=[${ctx.mentions.join(',')}]`);
131
+ // ── 推送 thinking_started 帧:通知前端 Agent 已进入思考阶段 ──
132
+ // 前端收到后可精确展示"哪个 Agent 在思考",替代纯前端的猜测逻辑
133
+ this.emitThinkingStarted(ctx, route);
134
+ let result;
135
+ try {
136
+ switch (route) {
137
+ case 'leader_orchestration':
138
+ // Leader 路径:默认 Agent 先规划,再决定直答或派活
139
+ result = await this.leaderRoute.run({ ctx, state });
140
+ break;
141
+ case 'mention_concurrent':
142
+ // Mention 路径:并发调用所有被 @ 的 Agent
143
+ result = await this.mentionRoute.run({ ctx, state });
144
+ break;
145
+ }
146
+ }
147
+ catch (err) {
148
+ // 兜底:路径内部未捕获的异常统一标记为 FAILED
149
+ const e = err instanceof Error ? err : new Error(String(err));
150
+ this.log.error(`${tag} 路径执行失败: ${e.message}`);
151
+ state.forceTerminal('FAILED');
152
+ result = { route, finalStatus: 'FAILED', runs: [], error: e };
153
+ }
154
+ // 清理已完成的会话,释放资源
155
+ this.runRegistry.disposeConversation(ctx.conversationId);
156
+ // 发布 conversation_finished / conversation_aborted / conversation_failed 事件
157
+ this.emitFinishEvent(ctx, result);
158
+ return result;
159
+ }
160
+ /**
161
+ * 级联中止指定会话的所有 Agent run
162
+ *
163
+ * @param conversationId - 要中止的会话 ID
164
+ * @returns 实际被中止的 runId 列表
165
+ */
166
+ abort(conversationId) {
167
+ return this.runRegistry.abortConversation(conversationId);
168
+ }
169
+ /**
170
+ * 路径选择策略
171
+ *
172
+ * @param ctx - 调度上下文
173
+ * @returns 选中的路径标识
174
+ *
175
+ * @remarks
176
+ * 当前策略:
177
+ * - mentions 非空 → mention_concurrent(用户明确指定了目标 Agent)
178
+ * - mentions 为空 → leader_orchestration(由默认 Agent 自主决策)
179
+ */
180
+ selectRoute(ctx) {
181
+ return ctx.mentions.length > 0 ? 'mention_concurrent' : 'leader_orchestration';
182
+ }
183
+ /**
184
+ * 安全发布事件
185
+ *
186
+ * @param event - 要发布的调度事件
187
+ *
188
+ * @remarks
189
+ * 监听器异常会被捕获并记录警告日志,不会影响主调度流程。
190
+ */
191
+ emitEvent(event) {
192
+ if (!this.onEvent)
193
+ return;
194
+ try {
195
+ this.onEvent(event);
196
+ }
197
+ catch (err) {
198
+ this.log.warn?.(`[${CHANNEL_KEY}][Orchestrator] 事件监听器异常: ${err}`);
199
+ }
200
+ }
201
+ /**
202
+ * 向前端推送 thinking_started 帧
203
+ *
204
+ * @remarks
205
+ * 在路径选择完成后、实际 Agent 执行前推送,通知前端进入"思考中"阶段。
206
+ * 前端收到后可精确展示对应 Agent 的思考动画,替代纯前端的猜测逻辑。
207
+ *
208
+ * 帧格式遵循 message:private 协议,streamStatus='thinking',
209
+ * 前端 useGroupChat 中识别此状态后更新 thinking 状态和 agentId。
210
+ *
211
+ * @param ctx - 调度上下文
212
+ * @param route - 选中的路径标识
213
+ */
214
+ emitThinkingStarted(ctx, route) {
215
+ try {
216
+ // 确定思考中的 agentId:
217
+ // - leader_orchestration 路径:使用 defaultAgentId
218
+ // - mention_concurrent 路径:使用第一个被 @ 的 agentId
219
+ const agentId = route === 'leader_orchestration' ? ctx.meta.defaultAgentId : ctx.mentions[0] || ctx.meta.defaultAgentId;
220
+ this.routeDeps.emitter.emit({
221
+ msgId: `thinking:${ctx.conversationId}`,
222
+ from: this.routeDeps.emitter.botClientId,
223
+ to: ctx.userId,
224
+ content: '',
225
+ timestamp: Date.now(),
226
+ chatKind: 'group',
227
+ extra: {
228
+ groupId: ctx.groupId,
229
+ agentId,
230
+ conversationId: ctx.conversationId,
231
+ streamStatus: 'thinking',
232
+ },
233
+ });
234
+ }
235
+ catch (err) {
236
+ // thinking 帧推送失败不影响主流程,仅记录警告
237
+ this.log.warn?.(`[${CHANNEL_KEY}][Orchestrator] thinking_started 推送失败: ${err}`);
238
+ }
239
+ }
240
+ /**
241
+ * 根据路径执行结果发布终态事件
242
+ *
243
+ * @param ctx - 调度上下文
244
+ * @param result - 路径执行结果
245
+ *
246
+ * @remarks
247
+ * 终态映射:
248
+ * - DONE → conversation_finished
249
+ * - ABORTED → conversation_aborted
250
+ * - 其他 → conversation_failed
251
+ */
252
+ emitFinishEvent(ctx, result) {
253
+ const type = result.finalStatus === 'DONE'
254
+ ? 'conversation_finished'
255
+ : result.finalStatus === 'ABORTED'
256
+ ? 'conversation_aborted'
257
+ : 'conversation_failed';
258
+ this.emitEvent({
259
+ type,
260
+ conversationId: ctx.conversationId,
261
+ ts: Date.now(),
262
+ payload: { route: result.route, runCount: result.runs.length, error: result.error?.message },
263
+ });
264
+ }
265
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * planning 模块统一出口
3
+ *
4
+ * 主 Agent 拆活相关:
5
+ * - PlanValidator(plan 合法性校验)
6
+ * - SubtaskExecutor(子任务 DAG 执行)
7
+ * - SummarizerRunner(汇总阶段执行)
8
+ * - PlanningParser(Planning 决策解析)
9
+ */
10
+ export { validatePlan, } from './plan-validator.js';
11
+ export { SubtaskExecutor, } from './subtask-executor.js';
12
+ export { SummarizerRunner, } from './summarizer-runner.js';
13
+ export { parsePlanningDecision, extractPlanFromDecision, } from './planning-parser.js';
@@ -0,0 +1,233 @@
1
+ /**
2
+ * LightClaw — Plan 校验器
3
+ *
4
+ * 对主 Agent 产出的拆分计划做硬约束校验:
5
+ * 1. 总数 1 ≤ N ≤ maxTasks;
6
+ * 2. 每个 task 的 id 非空且唯一;
7
+ * 3. to 必须 ∈ 群成员且不能是主 Agent 自己;
8
+ * 4. dependsOn 引用合法且无环;
9
+ * 5. task 描述非空。
10
+ *
11
+ * 校验通过返回标准化后的 plan,失败返回错误列表。
12
+ */
13
+ /**
14
+ * 校验并标准化 Plan
15
+ *
16
+ * @param raw - 原始 plan 数据
17
+ * @param vctx - 校验上下文
18
+ */
19
+ export function validatePlan(raw, vctx) {
20
+ const maxTasks = vctx.maxTasks ?? 5;
21
+ // ── 顶层结构校验 ──
22
+ const structErr = validateStructure(raw, maxTasks);
23
+ if (structErr) {
24
+ return { ok: false, errors: [structErr] };
25
+ }
26
+ const tasksRaw = raw.tasks;
27
+ const memberSet = new Set(vctx.members);
28
+ const idSet = new Set();
29
+ const errors = [];
30
+ const normalized = [];
31
+ // ── 逐项字段校验 + 标准化 ──
32
+ for (let i = 0; i < tasksRaw.length; i++) {
33
+ const result = validateTaskItem(tasksRaw[i], i, memberSet, idSet, vctx.leaderAgentId);
34
+ errors.push(...result.errors);
35
+ if (result.item) {
36
+ normalized.push(result.item);
37
+ }
38
+ }
39
+ if (errors.length > 0) {
40
+ return { ok: false, errors };
41
+ }
42
+ // ── dependsOn 引用合法性 ──
43
+ for (const item of normalized) {
44
+ if (!item.dependsOn)
45
+ continue;
46
+ for (const dep of item.dependsOn) {
47
+ if (dep === item.id) {
48
+ errors.push(`任务 "${item.id}" 不能依赖自身`);
49
+ }
50
+ else if (!idSet.has(dep)) {
51
+ errors.push(`任务 "${item.id}" 依赖了不存在的 id "${dep}"`);
52
+ }
53
+ }
54
+ }
55
+ if (errors.length > 0) {
56
+ return { ok: false, errors };
57
+ }
58
+ // ── 循环依赖检测 ──
59
+ const cycle = detectCycle(normalized);
60
+ if (cycle) {
61
+ return {
62
+ ok: false,
63
+ errors: [`计划中存在循环依赖: ${cycle.join(' -> ')}`],
64
+ };
65
+ }
66
+ return { ok: true, plan: { tasks: normalized } };
67
+ }
68
+ /**
69
+ * 顶层结构校验,返回错误信息或 null
70
+ */
71
+ function validateStructure(raw, maxTasks) {
72
+ if (!raw || typeof raw !== 'object') {
73
+ return 'plan 必须是一个对象';
74
+ }
75
+ const tasksRaw = raw.tasks;
76
+ if (!Array.isArray(tasksRaw)) {
77
+ return 'plan.tasks 必须是一个数组';
78
+ }
79
+ if (tasksRaw.length === 0) {
80
+ return 'plan.tasks 不能为空';
81
+ }
82
+ if (tasksRaw.length > maxTasks) {
83
+ return `plan.tasks 长度 ${tasksRaw.length} 超过最大限制 ${maxTasks}`;
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * 校验并标准化单个 task 项
89
+ */
90
+ function validateTaskItem(t, index, memberSet, idSet, leaderAgentId) {
91
+ const prefix = `tasks[${index}]`;
92
+ const errors = [];
93
+ if (!t || typeof t !== 'object') {
94
+ return { errors: [`${prefix} 不是一个对象`], item: null };
95
+ }
96
+ const item = t;
97
+ // id
98
+ const id = typeof item.id === 'string' ? item.id.trim() : '';
99
+ if (!id) {
100
+ errors.push(`${prefix}.id 必须是非空字符串`);
101
+ }
102
+ else if (idSet.has(id)) {
103
+ errors.push(`${prefix}.id 重复: "${id}"`);
104
+ }
105
+ else {
106
+ idSet.add(id);
107
+ }
108
+ // to
109
+ const to = typeof item.to === 'string' ? item.to.trim() : '';
110
+ if (!to) {
111
+ errors.push(`${prefix}.to 必须是非空字符串`);
112
+ }
113
+ else if (!memberSet.has(to)) {
114
+ errors.push(`${prefix}.to "${to}" 不是群成员`);
115
+ }
116
+ else if (to === leaderAgentId) {
117
+ errors.push(`${prefix}.to "${to}" 是主 Agent 自身(禁止派给自己)`);
118
+ }
119
+ // task
120
+ const task = typeof item.task === 'string' ? item.task.trim() : '';
121
+ if (!task) {
122
+ errors.push(`${prefix}.task 必须是非空字符串`);
123
+ }
124
+ // dependsOn
125
+ let dependsOn;
126
+ if (item.dependsOn != null) {
127
+ if (!Array.isArray(item.dependsOn)) {
128
+ errors.push(`${prefix}.dependsOn 必须是字符串数组`);
129
+ }
130
+ else {
131
+ dependsOn = [];
132
+ for (let j = 0; j < item.dependsOn.length; j++) {
133
+ const d = item.dependsOn[j];
134
+ if (typeof d !== 'string' || !d.trim()) {
135
+ errors.push(`${prefix}.dependsOn[${j}] 必须是非空字符串`);
136
+ }
137
+ else {
138
+ dependsOn.push(d.trim());
139
+ }
140
+ }
141
+ }
142
+ }
143
+ return { errors, item: { id, to, task, dependsOn } };
144
+ }
145
+ /**
146
+ * 循环依赖检测(Kahn 拓扑排序)
147
+ *
148
+ * @returns 若存在环则返回环路径,否则 undefined
149
+ */
150
+ function detectCycle(tasks) {
151
+ const indeg = new Map();
152
+ const graph = new Map();
153
+ for (const t of tasks) {
154
+ indeg.set(t.id, 0);
155
+ graph.set(t.id, []);
156
+ }
157
+ for (const t of tasks) {
158
+ for (const dep of t.dependsOn ?? []) {
159
+ graph.get(dep).push(t.id);
160
+ indeg.set(t.id, (indeg.get(t.id) ?? 0) + 1);
161
+ }
162
+ }
163
+ // Kahn(使用索引模拟队列,避免 shift() 的 O(n) 开销)
164
+ const queue = [];
165
+ for (const [id, deg] of indeg) {
166
+ if (deg === 0)
167
+ queue.push(id);
168
+ }
169
+ let head = 0;
170
+ let visited = 0;
171
+ while (head < queue.length) {
172
+ const cur = queue[head++];
173
+ visited += 1;
174
+ for (const next of graph.get(cur)) {
175
+ const d = indeg.get(next) - 1;
176
+ indeg.set(next, d);
177
+ if (d === 0)
178
+ queue.push(next);
179
+ }
180
+ }
181
+ if (visited === tasks.length)
182
+ return undefined;
183
+ // 收集环中节点,DFS 还原路径
184
+ const inCycle = new Set();
185
+ for (const [id, d] of indeg) {
186
+ if (d > 0)
187
+ inCycle.add(id);
188
+ }
189
+ return reconstructCycle(graph, inCycle);
190
+ }
191
+ /**
192
+ * 在环子图上 DFS 还原一条可视化环路径
193
+ */
194
+ function reconstructCycle(graph, inCycle) {
195
+ const start = inCycle.values().next().value;
196
+ const stack = [start];
197
+ const onStack = new Set([start]);
198
+ // 记录每个节点已探索到的邻居索引,避免重复访问导致死循环
199
+ const edgeIdx = new Map();
200
+ edgeIdx.set(start, 0);
201
+ while (stack.length > 0) {
202
+ const top = stack[stack.length - 1];
203
+ const neighbors = graph.get(top) ?? [];
204
+ let idx = edgeIdx.get(top) ?? 0;
205
+ let found = false;
206
+ // 从上次探索位置继续寻找环内邻居
207
+ while (idx < neighbors.length) {
208
+ const next = neighbors[idx];
209
+ idx++;
210
+ if (!inCycle.has(next))
211
+ continue;
212
+ edgeIdx.set(top, idx);
213
+ if (onStack.has(next)) {
214
+ // 找到环
215
+ const cycleStart = stack.indexOf(next);
216
+ return [...stack.slice(cycleStart), next];
217
+ }
218
+ stack.push(next);
219
+ onStack.add(next);
220
+ edgeIdx.set(next, 0);
221
+ found = true;
222
+ break;
223
+ }
224
+ if (!found) {
225
+ // 当前节点所有邻居已探索完毕,回溯
226
+ edgeIdx.set(top, idx);
227
+ stack.pop();
228
+ onStack.delete(top);
229
+ }
230
+ }
231
+ // 兜底:理论不可达
232
+ return [...inCycle];
233
+ }