@ai-setting/roy-agent-core 1.7.44 → 1.7.47

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 (59) hide show
  1. package/dist/config/index.js +4 -4
  2. package/dist/env/agent/index.js +7 -7
  3. package/dist/env/commands/index.js +4 -4
  4. package/dist/env/debug/index.js +4 -4
  5. package/dist/env/event-source/index.js +8 -7
  6. package/dist/env/hook/index.js +2 -2
  7. package/dist/env/index.js +24 -23
  8. package/dist/env/llm/index.js +15 -5
  9. package/dist/env/log-trace/index.js +4 -4
  10. package/dist/env/mcp/index.js +4 -4
  11. package/dist/env/memory/index.js +4 -4
  12. package/dist/env/plugin/index.js +8 -6
  13. package/dist/env/prompt/index.js +4 -4
  14. package/dist/env/session/index.js +7 -7
  15. package/dist/env/session/storage/index.js +3 -3
  16. package/dist/env/skill/index.js +4 -4
  17. package/dist/env/task/delegate/index.js +2 -2
  18. package/dist/env/task/index.js +6 -6
  19. package/dist/env/task/plugins/index.js +2 -2
  20. package/dist/env/task/storage/index.js +1 -1
  21. package/dist/env/tool/index.js +4 -4
  22. package/dist/env/workflow/engine/index.js +3 -3
  23. package/dist/env/workflow/index.js +11 -11
  24. package/dist/env/workflow/tools/index.js +8 -8
  25. package/dist/index.js +37 -30
  26. package/dist/shared/@ai-setting/{roy-agent-core-cysnmvcy.js → roy-agent-core-0r90298w.js} +1 -1
  27. package/dist/shared/@ai-setting/{roy-agent-core-qfktfbrr.js → roy-agent-core-1rm32m5t.js} +3 -3
  28. package/dist/shared/@ai-setting/{roy-agent-core-a236mps6.js → roy-agent-core-3j4by8rj.js} +35 -2
  29. package/dist/shared/@ai-setting/{roy-agent-core-wybj8emq.js → roy-agent-core-4m02hnqf.js} +1 -1
  30. package/dist/shared/@ai-setting/{roy-agent-core-jawzpf5k.js → roy-agent-core-4nnv0dze.js} +1 -1
  31. package/dist/shared/@ai-setting/{roy-agent-core-cw89yj9d.js → roy-agent-core-54wzmvnb.js} +1 -1
  32. package/dist/shared/@ai-setting/{roy-agent-core-jjw7xcxv.js → roy-agent-core-55hkeft7.js} +2 -2
  33. package/dist/shared/@ai-setting/{roy-agent-core-rmn29nrh.js → roy-agent-core-64f22h1g.js} +1 -11
  34. package/dist/shared/@ai-setting/{roy-agent-core-eakjnbaj.js → roy-agent-core-7p5zf8ay.js} +4 -4
  35. package/dist/shared/@ai-setting/{roy-agent-core-qhw8jp6j.js → roy-agent-core-83mk8wrn.js} +2 -2
  36. package/dist/shared/@ai-setting/roy-agent-core-ajtsk20z.js +2396 -0
  37. package/dist/shared/@ai-setting/{roy-agent-core-pc8x07p0.js → roy-agent-core-amp8qckb.js} +1 -1
  38. package/dist/shared/@ai-setting/roy-agent-core-az13yzmc.js +11 -0
  39. package/dist/shared/@ai-setting/{roy-agent-core-2f2nqx7g.js → roy-agent-core-b09eqhhz.js} +86 -60
  40. package/dist/shared/@ai-setting/{roy-agent-core-0gmnp1jt.js → roy-agent-core-bjrtwfta.js} +41 -6
  41. package/dist/shared/@ai-setting/{roy-agent-core-ts56bz5e.js → roy-agent-core-d94ddn0r.js} +3 -3
  42. package/dist/shared/@ai-setting/{roy-agent-core-rczz7e81.js → roy-agent-core-dabnevwe.js} +1 -1
  43. package/dist/shared/@ai-setting/{roy-agent-core-9t675te0.js → roy-agent-core-ead21w5c.js} +2 -2
  44. package/dist/shared/@ai-setting/{roy-agent-core-c95tpapc.js → roy-agent-core-gy1zfe0d.js} +2 -2
  45. package/dist/shared/@ai-setting/{roy-agent-core-5tzc2spk.js → roy-agent-core-h93v6p6y.js} +2 -2
  46. package/dist/shared/@ai-setting/{roy-agent-core-f18jk3cw.js → roy-agent-core-hxazjj4p.js} +1 -1
  47. package/dist/shared/@ai-setting/{roy-agent-core-3h7ajma5.js → roy-agent-core-jfptxs4f.js} +1 -1
  48. package/dist/shared/@ai-setting/{roy-agent-core-4m0t4y9w.js → roy-agent-core-nvcqp6p9.js} +1 -1
  49. package/dist/shared/@ai-setting/{roy-agent-core-jadrj0bz.js → roy-agent-core-p1p9mr8f.js} +13 -0
  50. package/dist/shared/@ai-setting/{roy-agent-core-65q93n1k.js → roy-agent-core-pk1b3kvf.js} +1 -1
  51. package/dist/shared/@ai-setting/{roy-agent-core-n1ceexfz.js → roy-agent-core-qvaqmj3f.js} +1 -1
  52. package/dist/shared/@ai-setting/{roy-agent-core-6mnwqcqf.js → roy-agent-core-v5vs1jby.js} +1 -1
  53. package/dist/shared/@ai-setting/{roy-agent-core-z6kv2mdp.js → roy-agent-core-y1fp2mgf.js} +18 -35
  54. package/dist/shared/@ai-setting/{roy-agent-core-asxe1nt4.js → roy-agent-core-ygnva439.js} +2 -2
  55. package/dist/shared/@ai-setting/{roy-agent-core-8xgk89y4.js → roy-agent-core-yjqj3taz.js} +4 -4
  56. package/dist/shared/@ai-setting/{roy-agent-core-w3tq0j5c.js → roy-agent-core-zrgaemma.js} +1 -1
  57. package/dist/shared/@ai-setting/{roy-agent-core-2ahddbdf.js → roy-agent-core-zy3hqfwg.js} +251 -53
  58. package/package.json +1 -1
  59. package/dist/shared/@ai-setting/roy-agent-core-0zgcfcnq.js +0 -1720
@@ -0,0 +1,2396 @@
1
+ import {
2
+ getSharedEventStore
3
+ } from "./roy-agent-core-xyys5m1a.js";
4
+ import {
5
+ TaskHookPoints
6
+ } from "./roy-agent-core-1pg0fepg.js";
7
+ import {
8
+ WORKFLOW_EXTRACT_PROMPT
9
+ } from "./roy-agent-core-223v204d.js";
10
+ import {
11
+ validateWorkflowDefinition
12
+ } from "./roy-agent-core-6vbgswjw.js";
13
+ import {
14
+ enqueueToAddFakeUserMessage
15
+ } from "./roy-agent-core-4cj6hram.js";
16
+ import {
17
+ BasePlugin
18
+ } from "./roy-agent-core-az13yzmc.js";
19
+ import {
20
+ init_context
21
+ } from "./roy-agent-core-wn59p0zw.js";
22
+ import {
23
+ getEnvContextOrEmpty
24
+ } from "./roy-agent-core-zbb3sk72.js";
25
+ import {
26
+ TracedAs,
27
+ init_decorator,
28
+ wrapFunction
29
+ } from "./roy-agent-core-a9pbda78.js";
30
+ import {
31
+ createLogger,
32
+ init_logger
33
+ } from "./roy-agent-core-8seq65bp.js";
34
+ import {
35
+ __legacyDecorateClassTS,
36
+ __require
37
+ } from "./roy-agent-core-fs0mn2jk.js";
38
+
39
+ // src/env/task/plugins/task-tag-plugin.ts
40
+ init_logger();
41
+
42
+ // src/env/task/plugins/workflow-extractor-agent.ts
43
+ var WORKFLOW_EXTRACTOR_PROMPT = WORKFLOW_EXTRACT_PROMPT;
44
+ function formatExtractorInput(params) {
45
+ const lines = [];
46
+ lines.push("# 任务信息");
47
+ lines.push(`- 标题: ${params.task.title}`);
48
+ if (params.task.description) {
49
+ lines.push(`- 描述: ${params.task.description}`);
50
+ }
51
+ if (params.task.goals_and_expected_deliverables) {
52
+ lines.push(`- 目标: ${params.task.goals_and_expected_deliverables}`);
53
+ }
54
+ lines.push("");
55
+ if (params.operations.length > 0) {
56
+ lines.push("# 操作记录");
57
+ for (const op of params.operations) {
58
+ lines.push(`- [${op.actionType}] ${op.actionTitle}`);
59
+ if (op.actionDescription) {
60
+ lines.push(` ${op.actionDescription.slice(0, 200)}`);
61
+ }
62
+ }
63
+ lines.push("");
64
+ }
65
+ lines.push("# Session 消息历史");
66
+ lines.push("(以下是相关的对话历史,请从中提取 Workflow)");
67
+ for (const msg of params.messages.slice(-50)) {
68
+ const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
69
+ const truncated = content.slice(0, 500);
70
+ lines.push(`**${msg.role}**: ${truncated}${content.length > 500 ? "..." : ""}`);
71
+ }
72
+ lines.push("");
73
+ lines.push("## 任务");
74
+ lines.push("请分析以上信息,构建一个可复用的 Workflow。");
75
+ lines.push("使用迭代式流程:生成 YAML → validate 验证 → 修正错误 → 重复直到通过验证。");
76
+ return lines.join(`
77
+ `);
78
+ }
79
+ function formatExtractorInputFromDescription(params) {
80
+ const lines = [];
81
+ lines.push("# Workflow 描述");
82
+ lines.push(params.description);
83
+ lines.push("");
84
+ lines.push("## 任务");
85
+ lines.push("请根据以上描述,构建一个可复用的 Workflow。");
86
+ lines.push("使用迭代式流程:生成 YAML → validate 验证 → 修正错误 → 重复直到通过验证。");
87
+ return lines.join(`
88
+ `);
89
+ }
90
+ async function parseExtractorOutput(output) {
91
+ const yamlMatch = output.match(/```yaml\n?([\s\S]*?)\n?```/) || output.match(/```\n?([\s\S]*?)\n?```/) || output.match(/(\|[\s\S]*?)(?:\n\n|$)/);
92
+ const yamlContent = yamlMatch?.[1] || output;
93
+ try {
94
+ const YAML = await import("yaml");
95
+ const parsed = YAML.parse(yamlContent);
96
+ if (!parsed.name || !parsed.nodes || !Array.isArray(parsed.nodes)) {
97
+ return null;
98
+ }
99
+ if (parsed.nodes.length < 2) {
100
+ return null;
101
+ }
102
+ parsed.version = parsed.version || "1.0";
103
+ return parsed;
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ // src/env/task/plugins/task-tag-plugin.ts
110
+ var logger = createLogger("plugin:task-tag");
111
+
112
+ class TaskTagPlugin extends BasePlugin {
113
+ name = "task-tag";
114
+ version = "1.0.0";
115
+ description = "Inject similar task information into delegate prompt and extract workflow on task completion";
116
+ env;
117
+ taskComponent = null;
118
+ agentComponent = null;
119
+ tagService = null;
120
+ constructor() {
121
+ super();
122
+ }
123
+ init(env) {
124
+ this.env = env;
125
+ this.taskComponent = env.getComponent("task");
126
+ if (this.taskComponent && typeof this.taskComponent.getTagService === "function") {
127
+ this.tagService = this.taskComponent.getTagService();
128
+ }
129
+ this.agentComponent = env.getComponent("agent");
130
+ env.registerHook({
131
+ point: TaskHookPoints.DELEGATE_BEFORE,
132
+ priority: 20,
133
+ handler: this.onDelegateBefore.bind(this)
134
+ });
135
+ env.registerHook({
136
+ point: TaskHookPoints.OPERATION_AFTER_CREATE,
137
+ priority: 10,
138
+ handler: this.onOperationAfterCreate.bind(this)
139
+ });
140
+ logger.info("[TaskTagPlugin] Initialized");
141
+ }
142
+ dispose() {
143
+ this.env = null;
144
+ this.taskComponent = null;
145
+ this.agentComponent = null;
146
+ this.tagService = null;
147
+ logger.info("[TaskTagPlugin] Disposed");
148
+ }
149
+ async onDelegateBefore(ctx) {
150
+ const data = ctx.data;
151
+ const { taskId, prompt } = data;
152
+ if (!taskId || !this.taskComponent) {
153
+ return;
154
+ }
155
+ try {
156
+ const currentTask = await this.taskComponent.getTask(taskId);
157
+ if (!currentTask)
158
+ return;
159
+ const keywords = this.extractKeywords(currentTask);
160
+ if (keywords.length === 0)
161
+ return;
162
+ const similarTasks = await this.tagService?.findSimilarTasksByKeywords?.(keywords, {
163
+ limit: 5,
164
+ excludeTaskId: taskId
165
+ }) ?? [];
166
+ if (similarTasks.length === 0)
167
+ return;
168
+ const hint = this.formatSimilarTasks(similarTasks);
169
+ data.prompt = `${hint}
170
+
171
+ ---
172
+
173
+ ## 用户指令
174
+
175
+ ${prompt}`;
176
+ logger.info(`[TaskTagPlugin] Injected ${similarTasks.length} similar tasks`);
177
+ } catch (error) {
178
+ logger.warn(`[TaskTagPlugin] Similar tasks injection failed: ${error}`);
179
+ }
180
+ }
181
+ extractKeywords(task) {
182
+ const keywords = [];
183
+ if (task.tags?.length) {
184
+ keywords.push(...task.tags);
185
+ }
186
+ if (task.title) {
187
+ const words = task.title.split(/[\s\-_,,。、#]+/).filter((w) => w.length > 2 && !/^\d+$/.test(w)).slice(0, 5);
188
+ keywords.push(...words);
189
+ }
190
+ return keywords;
191
+ }
192
+ formatSimilarTasks(tasks) {
193
+ const lines = [
194
+ "## \uD83D\uDD0D 类似任务参考",
195
+ "",
196
+ "_提示:可通过 `task_get <id>` 获取详情,`task_operation_list <id>` 获取操作记录_",
197
+ ""
198
+ ];
199
+ for (const task of tasks) {
200
+ lines.push(`### Task #${task.id}`);
201
+ lines.push(`**Title**: ${task.title}`);
202
+ if (task.description) {
203
+ const desc = task.description.slice(0, 100);
204
+ lines.push(`**Description**: ${desc}${task.description.length > 100 ? "..." : ""}`);
205
+ }
206
+ if (task.goals_and_expected_deliverables) {
207
+ const goal = task.goals_and_expected_deliverables.slice(0, 100);
208
+ lines.push(`**Goal**: ${goal}${task.goals_and_expected_deliverables.length > 100 ? "..." : ""}`);
209
+ }
210
+ lines.push("");
211
+ }
212
+ return lines.join(`
213
+ `);
214
+ }
215
+ async onOperationAfterCreate(ctx) {
216
+ const data = ctx.data;
217
+ const operation = data?.operation ?? data?.data?.operation;
218
+ if (!operation || operation?.actionType !== "completed")
219
+ return;
220
+ await this.extractWorkflow(operation.taskId, operation.sessionId);
221
+ }
222
+ async extractWorkflow(taskId, sessionId) {
223
+ try {
224
+ const task = await this.taskComponent?.getTask(taskId);
225
+ if (!task)
226
+ return;
227
+ const operations = await this.taskComponent?.listOperations({ taskId }) ?? [];
228
+ const allSessionIds = [...new Set([sessionId, ...operations.map((op) => op.sessionId).filter(Boolean)])];
229
+ const sessionComponent = this.env.getComponent("session");
230
+ const allMessages = [];
231
+ for (const sid of allSessionIds) {
232
+ try {
233
+ const ctx = await sessionComponent?.getContext(sid, { fullHistory: false, minUserMessages: 200 });
234
+ if (ctx?.messages)
235
+ allMessages.push(...ctx.messages);
236
+ } catch {}
237
+ }
238
+ if (!this.agentComponent)
239
+ return;
240
+ const input = formatExtractorInput({ task, operations, messages: allMessages });
241
+ const result = await this.agentComponent.run("workflow-extract", input);
242
+ const workflowDef = await parseExtractorOutput(result.finalText || "");
243
+ if (!workflowDef)
244
+ return;
245
+ const validation = this.validateWorkflowDefinition(workflowDef);
246
+ if (!validation.valid) {
247
+ logger.warn(`[TaskTagPlugin] Skipping workflow creation due to validation failure: ` + `${validation.errors.join("; ")}`);
248
+ await this.taskComponent?.createOperation({
249
+ taskId,
250
+ sessionId,
251
+ actionType: "workflow_extraction_failed",
252
+ actionTitle: `Workflow extraction failed validation: ${workflowDef.name}`,
253
+ actionDescription: validation.errors.join("; ")
254
+ });
255
+ return;
256
+ }
257
+ const workflowRepo = await this.getWorkflowRepository();
258
+ if (!workflowRepo)
259
+ return;
260
+ let version = workflowDef.version || "1.0";
261
+ const existingWf = workflowRepo.getByName(workflowDef.name);
262
+ if (existingWf) {
263
+ const parts = existingWf.version?.split(".") ?? ["1", "0"];
264
+ version = `${parts[0]}.${parseInt(parts[1] || "0") + 1}`;
265
+ }
266
+ const workflow = workflowRepo.create({
267
+ name: workflowDef.name,
268
+ version,
269
+ description: workflowDef.description,
270
+ definition: workflowDef,
271
+ config: workflowDef.config || {},
272
+ metadata: { author: "workflow-extract", created_at: new Date().toISOString() },
273
+ tags: [`task:${taskId}`]
274
+ });
275
+ await this.taskComponent?.createOperation({
276
+ taskId,
277
+ sessionId,
278
+ actionType: "workflow_extracted",
279
+ actionTitle: `Workflow extracted: ${workflow.name}`,
280
+ actionDescription: workflow.id
281
+ });
282
+ logger.info(`[TaskTagPlugin] Workflow extracted: ${workflow.id}`);
283
+ } catch (error) {
284
+ logger.warn(`[TaskTagPlugin] Workflow extraction failed: ${error}`);
285
+ }
286
+ }
287
+ validateWorkflowDefinition(workflowDef) {
288
+ return validateWorkflowDefinition(workflowDef);
289
+ }
290
+ _workflowRepo = null;
291
+ async getWorkflowRepository() {
292
+ const workflowComponent = this.env.getComponent("workflow");
293
+ if (workflowComponent?.workflowService?.workflowRepository) {
294
+ return workflowComponent.workflowService.workflowRepository;
295
+ }
296
+ if (!this._workflowRepo) {
297
+ try {
298
+ const sqlite = await import("./roy-agent-core-9cqpdrnk.js");
299
+ this.ensureDataDir();
300
+ const db = sqlite.getDatabase();
301
+ sqlite.initializeTables();
302
+ const { WorkflowRepository } = await import("./roy-agent-core-5j4awbgc.js");
303
+ this._workflowRepo = new WorkflowRepository(db);
304
+ return this._workflowRepo;
305
+ } catch {
306
+ return null;
307
+ }
308
+ }
309
+ return this._workflowRepo;
310
+ }
311
+ ensureDataDir() {
312
+ const { join } = __require("path");
313
+ const dataDir = join(process.env.XDG_DATA_HOME || join(process.env.HOME || "", ".local/share"), "roy-agent");
314
+ __require("fs").mkdirSync(dataDir, { recursive: true });
315
+ }
316
+ }
317
+ // src/env/task/plugins/auto-task-plugin.ts
318
+ init_logger();
319
+ init_decorator();
320
+ var logger2 = createLogger("AutoTaskPlugin");
321
+ function createAutoTaskPlugin(config) {
322
+ const threshold = config?.threshold ?? 20;
323
+ const agentName = config?.agentName ?? "default";
324
+ let lastProcessedTraceId = "";
325
+ async function executeImpl(ctx) {
326
+ try {
327
+ logger2.info(`[AutoTaskPlugin:ENTER] agentName=${ctx.agent.name} iteration=${ctx.iteration} threshold=${threshold}`);
328
+ if (ctx.agent.name !== agentName)
329
+ return { continue: true };
330
+ if (ctx.iteration !== threshold)
331
+ return { continue: true };
332
+ const traceId = ctx.context?.traceId ?? ctx.context?.sessionId ?? "unknown";
333
+ if (traceId === lastProcessedTraceId) {
334
+ logger2.debug(`[AutoTaskPlugin] skip - traceId=${traceId.slice(0, 20)}... already processed`);
335
+ return { continue: true };
336
+ }
337
+ lastProcessedTraceId = traceId;
338
+ logger2.debug(`[AutoTaskPlugin] new query detected (traceId=${traceId.slice(0, 20)}...)`);
339
+ const reminderContent = `⚠️ **系统提醒**: 你已经调用了 ${ctx.iteration} 次工具,达到 iteration 阈值(默认 ${threshold})。
340
+ ` + `当前请求较为复杂,建议通过 \`delegate_task\` 工具将整个任务委托给 \`roy\` sub-agent 处理。` + `注意,需要让roy为这个任务调用task create工具来创建任务进行跟踪,这个是必走程序哈。`;
341
+ if (!ctx.toolResult) {
342
+ logger2.warn(`[AutoTaskPlugin] toolResult missing, skip meta-protocol (iteration=${ctx.iteration})`);
343
+ return { continue: true };
344
+ }
345
+ enqueueToAddFakeUserMessage(ctx.toolResult, {
346
+ role: "user",
347
+ content: reminderContent
348
+ }, "delegate");
349
+ logger2.info(`[AutoTaskPlugin] user reminder meta-protocol set (session=${(ctx.context?.sessionId ?? "unknown").slice(0, 12)}..., iteration=${ctx.iteration})`);
350
+ return { continue: true };
351
+ } catch (err) {
352
+ logger2.error(`[AutoTaskPlugin] hook failed: ${err instanceof Error ? err.message : String(err)}`);
353
+ return { continue: true };
354
+ }
355
+ }
356
+ const execute = wrapFunction(executeImpl, "plugin.auto-task.launch-agent", {
357
+ recordParams: false,
358
+ recordResult: true,
359
+ log: true
360
+ });
361
+ return {
362
+ name: "AutoTaskPlugin",
363
+ version: "5.2.0",
364
+ description: "ReAct iteration 达到阈值时通过共享 meta-protocol helper 注入 user 消息(与 MaxIterReminder 共用字段、确定性合并)",
365
+ hooks: [
366
+ { point: "agent:after.tool", priority: 50 }
367
+ ],
368
+ execute
369
+ };
370
+ }
371
+ // src/env/task/plugins/lark-cli-task-notify-plugin.ts
372
+ init_decorator();
373
+ init_logger();
374
+ init_context();
375
+
376
+ // src/env/task/plugins/lark-card-patch-notifier.ts
377
+ init_logger();
378
+ init_decorator();
379
+ import { spawn } from "node:child_process";
380
+ var logger3 = createLogger("lark-cli:card-patch-notifier");
381
+ var HEADER_TEMPLATE = {
382
+ running: "blue",
383
+ completed: "green",
384
+ failed: "red"
385
+ };
386
+ var STATUS_PHRASE = {
387
+ running: "实时同步中",
388
+ completed: "\uD83C\uDF89 全部完成!",
389
+ failed: "\uD83D\uDEA8 任务失败"
390
+ };
391
+ var SPINNER_EMOJIS = ["⏳", "\uD83D\uDD04", "\uD83D\uDD03", "⌛", "⏰", "\uD83D\uDD01"];
392
+ var PROGRESS_BAR_CELLS = 20;
393
+ function formatTimestampForDisplay(iso) {
394
+ if (!iso)
395
+ return "";
396
+ const d = new Date(iso);
397
+ if (isNaN(d.getTime())) {
398
+ return iso;
399
+ }
400
+ return new Intl.DateTimeFormat("en-GB", {
401
+ timeZone: "Asia/Shanghai",
402
+ hour: "2-digit",
403
+ minute: "2-digit",
404
+ second: "2-digit",
405
+ hour12: false
406
+ }).format(d);
407
+ }
408
+ function sanitizeInlineText(text, maxLen = 300) {
409
+ if (!text)
410
+ return "";
411
+ const flat = text.replace(/\r\n?/g, `
412
+ `).replace(/^#{1,6}\s+/gm, "").replace(/^\s*[-*+]\s+/gm, "· ").replace(/^\s*\d+\.\s+/gm, "· ").replace(/\*\*/g, "").replace(/`/g, "").replace(/\n+/g, " · ").replace(/\s{2,}/g, " ").replace(/(?:\s*·\s*){2,}/g, " · ").replace(/^(\s*·\s*)+/, "").replace(/(\s*·\s*)+$/, "").trim();
413
+ return flat.length <= maxLen ? flat : `${flat.slice(0, maxLen)}…`;
414
+ }
415
+ function formatDateTimeForDisplay(value) {
416
+ const d = new Date(value);
417
+ if (isNaN(d.getTime()))
418
+ return value;
419
+ const parts = new Intl.DateTimeFormat("en-CA", {
420
+ timeZone: "Asia/Shanghai",
421
+ year: "numeric",
422
+ month: "2-digit",
423
+ day: "2-digit",
424
+ hour: "2-digit",
425
+ minute: "2-digit",
426
+ hour12: false
427
+ }).formatToParts(d);
428
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
429
+ return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}`;
430
+ }
431
+ function looksLikeIsoDateTime(v) {
432
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(v);
433
+ }
434
+ function renderProgressBar(progress) {
435
+ const clamped = Math.max(0, Math.min(100, progress));
436
+ const filled = Math.round(clamped / 100 * PROGRESS_BAR_CELLS);
437
+ const bar = "▓".repeat(filled) + "░".repeat(PROGRESS_BAR_CELLS - filled);
438
+ return `${bar} ${Math.round(clamped)}%`;
439
+ }
440
+ function humanizeValue(value, maxLen = 200) {
441
+ const truncate = (t) => t.length <= maxLen ? t : `${t.slice(0, maxLen)}…`;
442
+ if (value === null || value === undefined)
443
+ return "";
444
+ if (typeof value === "string") {
445
+ if (looksLikeIsoDateTime(value))
446
+ return formatDateTimeForDisplay(value);
447
+ return truncate(sanitizeInlineText(value, maxLen));
448
+ }
449
+ if (typeof value === "number" || typeof value === "boolean") {
450
+ return String(value);
451
+ }
452
+ if (Array.isArray(value)) {
453
+ return truncate(value.map((v) => humanizeValue(v, 80)).filter((t) => t.length > 0).join("、"));
454
+ }
455
+ if (typeof value === "object") {
456
+ const entries = Object.entries(value).map(([k, v]) => {
457
+ const rendered = humanizeValue(v, 80);
458
+ return rendered ? `${k}: ${rendered}` : "";
459
+ }).filter((t) => t.length > 0);
460
+ return truncate(entries.join(" · "));
461
+ }
462
+ return "";
463
+ }
464
+ var CONTEXT_LABELS = {
465
+ worktree_path: "\uD83C\uDF3F Worktree",
466
+ branch: "\uD83C\uDF3F 分支",
467
+ created_at: "\uD83D\uDD50 创建时间",
468
+ prior_context: "\uD83D\uDCDC 背景",
469
+ project_path: "\uD83D\uDCC2 项目",
470
+ type: "\uD83C\uDFF7️ 类型",
471
+ feature: "\uD83C\uDFF7️ 特性",
472
+ goal: "\uD83C\uDFAF 目标",
473
+ feishu_chat_id: "\uD83D\uDCAC 飞书群",
474
+ trigger_message_id: "✉️ 触发消息",
475
+ trigger_sender_id: "\uD83D\uDC64 触发人",
476
+ related_task: "\uD83D\uDD17 关联任务",
477
+ repo: "\uD83D\uDCE6 仓库",
478
+ commit: "\uD83D\uDD16 Commit",
479
+ session_id: "\uD83E\uDDF5 Session",
480
+ worktree: "\uD83C\uDF3F Worktree",
481
+ larkPerTaskChatId: "\uD83D\uDDC2️ 任务群聊",
482
+ larkPerTaskChatProfile: "\uD83D\uDDC2️ 群聊 Profile"
483
+ };
484
+ var CONTEXT_SKIP_KEYS = new Set([
485
+ "larkPerTaskChatTaskId",
486
+ "larkPerTaskChatCreatedAt"
487
+ ]);
488
+ function renderTaskInfoLines(meta) {
489
+ const lines = [];
490
+ if (meta.id !== undefined)
491
+ lines.push(`- **\uD83C\uDD94 任务 ID**:#${meta.id}`);
492
+ if (meta.title) {
493
+ lines.push(`- **\uD83D\uDCCC 标题**:${sanitizeInlineText(meta.title, 120)}`);
494
+ }
495
+ if (meta.description) {
496
+ lines.push(`- **\uD83D\uDCDD 描述**:${sanitizeInlineText(meta.description, 300)}`);
497
+ }
498
+ if (meta.goals) {
499
+ lines.push(`- **\uD83C\uDFAF 目标**:${sanitizeInlineText(meta.goals, 300)}`);
500
+ }
501
+ if (meta.parentTaskId)
502
+ lines.push(`- **\uD83D\uDD17 父任务**:#${meta.parentTaskId}`);
503
+ if (meta.priority || meta.type) {
504
+ lines.push(`- **⚡ 优先级 / 类型**:${meta.priority ?? "—"} / ${meta.type ?? "—"}`);
505
+ }
506
+ if (meta.currentStatus) {
507
+ lines.push(`- **\uD83D\uDCF6 当前状态**:${sanitizeInlineText(meta.currentStatus, 200)}`);
508
+ }
509
+ const project = realValue(meta.projectPath);
510
+ if (project)
511
+ lines.push(`- **\uD83D\uDCC2 项目**:\`${sanitizeInlineText(project, 160)}\``);
512
+ const contextLines = renderContextAsLines(meta.context);
513
+ lines.push(...contextLines);
514
+ return lines;
515
+ }
516
+ function realValue(v) {
517
+ if (!v)
518
+ return;
519
+ const t = v.trim();
520
+ if (t.length === 0 || t === "unknown" || t === "undefined" || t === "null") {
521
+ return;
522
+ }
523
+ return t;
524
+ }
525
+ function renderContextAsLines(context) {
526
+ const raw = realValue(context);
527
+ if (!raw)
528
+ return [];
529
+ let parsed;
530
+ try {
531
+ parsed = JSON.parse(raw);
532
+ } catch {
533
+ return [];
534
+ }
535
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
536
+ return [];
537
+ const obj = parsed;
538
+ const lines = [];
539
+ const knownKeys = Object.keys(obj).filter((k) => CONTEXT_LABELS[k] && !CONTEXT_SKIP_KEYS.has(k));
540
+ const otherKeys = Object.keys(obj).filter((k) => !CONTEXT_LABELS[k] && !CONTEXT_SKIP_KEYS.has(k));
541
+ for (const k of knownKeys) {
542
+ const rendered = humanizeValue(obj[k], 160);
543
+ if (rendered)
544
+ lines.push(`- **${CONTEXT_LABELS[k]}**:${rendered}`);
545
+ }
546
+ for (const k of otherKeys) {
547
+ const rendered = humanizeValue(obj[k], 160);
548
+ if (rendered)
549
+ lines.push(`- **${k}**:${rendered}`);
550
+ }
551
+ return lines;
552
+ }
553
+ function renderMilestones(milestones) {
554
+ if (milestones.length === 0) {
555
+ return "_(暂无里程碑)_";
556
+ }
557
+ return milestones.map((m) => {
558
+ const ts = formatTimestampForDisplay(m.timestamp) || "—";
559
+ const head = `- **${ts}** ${sanitizeInlineText(m.title, 120)}`;
560
+ return m.description ? `${head}
561
+ - ${sanitizeInlineText(m.description, 300)}` : head;
562
+ }).join(`
563
+ `);
564
+ }
565
+ function renderProcessSteps(steps) {
566
+ if (steps.length === 0) {
567
+ return "_(暂无过程记录)_";
568
+ }
569
+ return steps.map((s) => {
570
+ const startTime = formatTimestampForDisplay(s.timestamp);
571
+ const endTime = formatTimestampForDisplay(s.endTimestamp) || "现在";
572
+ const range = startTime ? `${startTime} → ${endTime}` : endTime;
573
+ const title = s.category ? `${s.title}(${s.category})` : s.title;
574
+ const head = `- **${range}** ${sanitizeInlineText(title, 120)}`;
575
+ return s.description ? `${head}
576
+ - ${sanitizeInlineText(s.description, 300)}` : head;
577
+ }).join(`
578
+ `);
579
+ }
580
+ function milestoneKey(m) {
581
+ return `${m.title}|${m.timestamp ?? ""}|${m.description ?? ""}`;
582
+ }
583
+ function processStepKey(s) {
584
+ return `${s.title}|${s.timestamp ?? ""}|${s.endTimestamp ?? ""}|${s.description ?? ""}`;
585
+ }
586
+ function buildTaskCard(opts) {
587
+ const status = opts.status ?? "running";
588
+ const milestones = opts.milestones ?? [];
589
+ const processSteps = opts.processSteps ?? [];
590
+ const updateCount = opts.updateCount ?? 0;
591
+ const spinner = opts.spinner ?? "";
592
+ const elements = [];
593
+ const infoLines = renderTaskInfoLines(opts.meta);
594
+ if (infoLines.length > 0) {
595
+ elements.push({ tag: "markdown", content: infoLines.join(`
596
+ `) });
597
+ elements.push({ tag: "hr" });
598
+ }
599
+ elements.push({ tag: "markdown", content: `## \uD83C\uDFAF 里程碑序列` });
600
+ elements.push({ tag: "markdown", content: renderMilestones(milestones) });
601
+ elements.push({ tag: "hr" });
602
+ elements.push({ tag: "markdown", content: `## \uD83D\uDD27 过程信息` });
603
+ elements.push({ tag: "markdown", content: renderProcessSteps(processSteps) });
604
+ elements.push({ tag: "hr" });
605
+ const effectiveProgress = status === "completed" ? 100 : opts.progress;
606
+ elements.push({
607
+ tag: "markdown",
608
+ content: effectiveProgress !== undefined ? `**\uD83D\uDCCA 进度**:${renderProgressBar(effectiveProgress)}` : `**\uD83D\uDCCA 进度**:进行中(第 ${updateCount} 次更新,尚未上报百分比)`
609
+ });
610
+ const taskLabel = opts.meta.id !== undefined ? `#${opts.meta.id}` : "任务";
611
+ return {
612
+ schema: "2.0",
613
+ header: {
614
+ title: { tag: "plain_text", content: `\uD83D\uDCCA Task 实时更新(${taskLabel})` },
615
+ subtitle: {
616
+ tag: "plain_text",
617
+ content: `spinner: '${spinner}' · 第 ${updateCount} 次更新 | ` + STATUS_PHRASE[status]
618
+ },
619
+ template: HEADER_TEMPLATE[status]
620
+ },
621
+ body: { elements }
622
+ };
623
+ }
624
+ function buildChatAppLink(chatId, base = DEFAULT_APP_LINK_BASE) {
625
+ return `${base}/client/chat/open?openChatId=${chatId}`;
626
+ }
627
+ var DEFAULT_APP_LINK_BASE = "https://applink.feishu.cn";
628
+ function buildRedirectCard(opts) {
629
+ const taskLabel = opts.taskId !== undefined ? `#${opts.taskId}` : "本任务";
630
+ const title = opts.taskTitle ? ` ${sanitizeInlineText(opts.taskTitle, 120)}` : "";
631
+ const link = buildChatAppLink(opts.targetChatId);
632
+ const groupName = opts.targetChatName ? sanitizeInlineText(opts.targetChatName, 60) : "专属群";
633
+ return {
634
+ schema: "2.0",
635
+ header: {
636
+ title: { tag: "plain_text", content: "\uD83D\uDCC2 任务卡片已改投专属群" },
637
+ subtitle: { tag: "plain_text", content: "这里不是本任务的群,进展请看专属群" },
638
+ template: "grey"
639
+ },
640
+ body: {
641
+ elements: [
642
+ {
643
+ tag: "markdown",
644
+ content: [
645
+ `- **任务**:${taskLabel}${title}`,
646
+ `- **专属群**:[${groupName}](${link})`,
647
+ `- **群 ID**:\`${opts.targetChatId}\`(搜群名也能进)`,
648
+ opts.reason ? `- **原因**:${sanitizeInlineText(opts.reason, 280)}` : ""
649
+ ].filter(Boolean).join(`
650
+ `)
651
+ }
652
+ ]
653
+ }
654
+ };
655
+ }
656
+
657
+ class MutexImpl {
658
+ chain = Promise.resolve();
659
+ async run(fn) {
660
+ const prev = this.chain;
661
+ const next = prev.then(() => fn());
662
+ this.chain = next.catch(() => {});
663
+ return next;
664
+ }
665
+ }
666
+
667
+ class LarkCardPatchNotifier {
668
+ options;
669
+ mutexes = new Map;
670
+ states = new Map;
671
+ redirectsSent = new Set;
672
+ constructor(options = {}) {
673
+ this.options = {
674
+ pinOnStart: true,
675
+ unpinOnFinish: true,
676
+ ...options
677
+ };
678
+ }
679
+ mutexFor(messageId) {
680
+ let m = this.mutexes.get(messageId);
681
+ if (!m) {
682
+ m = new MutexImpl;
683
+ this.mutexes.set(messageId, m);
684
+ }
685
+ return m;
686
+ }
687
+ async startTask(opts) {
688
+ const now = new Date().toISOString();
689
+ const spinner = SPINNER_EMOJIS[0];
690
+ const milestones = opts.milestones?.length ? opts.milestones : [{ title: "Task 创建", timestamp: now }];
691
+ const card = buildTaskCard({
692
+ status: "running",
693
+ meta: opts.meta,
694
+ milestones,
695
+ processSteps: [],
696
+ updateCount: 1,
697
+ spinner
698
+ });
699
+ const stdout = await this.runLarkCli(this.buildSendCardArgs({ chatId: opts.chatId, profile: opts.profile, card }), "send");
700
+ const messageId = this.extractMessageId(stdout);
701
+ if (!messageId) {
702
+ throw new Error(`lark-cli +messages-send did not return message_id: ${stdout.slice(0, 500)}`);
703
+ }
704
+ this.states.set(messageId, {
705
+ chatId: opts.chatId,
706
+ profile: opts.profile,
707
+ meta: opts.meta,
708
+ startedAt: now,
709
+ updateCount: 1,
710
+ spinnerIndex: 0,
711
+ milestones,
712
+ processSteps: []
713
+ });
714
+ let pinned = false;
715
+ let pinError;
716
+ if (this.options.pinOnStart) {
717
+ try {
718
+ await this.runLarkCli(this.buildPinArgs({ messageId, profile: opts.profile }), "pin");
719
+ pinned = true;
720
+ } catch (pinErr) {
721
+ pinError = pinErr.message;
722
+ logger3.warn(`Pin failed (non-fatal) for message ${messageId}: ${pinError}`);
723
+ }
724
+ }
725
+ return { messageId, pinned, pinError };
726
+ }
727
+ async sendRedirectNotice(opts) {
728
+ const redirectKey = opts.taskId !== undefined ? `${opts.taskId}:${opts.targetChatId}` : undefined;
729
+ if (redirectKey) {
730
+ if (this.redirectsSent.has(redirectKey)) {
731
+ logger3.debug(`sendRedirectNotice: 已发过改投卡片(${redirectKey})→ 跳过重复发送`);
732
+ return false;
733
+ }
734
+ this.redirectsSent.add(redirectKey);
735
+ }
736
+ const card = buildRedirectCard({
737
+ taskId: opts.taskId,
738
+ taskTitle: opts.taskTitle,
739
+ targetChatId: opts.targetChatId,
740
+ targetChatName: opts.targetChatName,
741
+ reason: opts.reason
742
+ });
743
+ try {
744
+ await this.runLarkCli(this.buildSendCardArgs({
745
+ chatId: opts.chatId,
746
+ profile: opts.profile,
747
+ card
748
+ }), "send-redirect");
749
+ return true;
750
+ } catch (err) {
751
+ if (redirectKey)
752
+ this.redirectsSent.delete(redirectKey);
753
+ logger3.warn(`sendRedirectNotice failed (non-fatal) for chat ${opts.chatId}: ${err.message}`);
754
+ return false;
755
+ }
756
+ }
757
+ async updateTask(opts) {
758
+ await this.mutexFor(opts.messageId).run(async () => {
759
+ const state = this.states.get(opts.messageId) ?? {
760
+ chatId: opts.chatId ?? "",
761
+ profile: opts.profile,
762
+ meta: opts.meta ?? { title: "任务" },
763
+ startedAt: new Date().toISOString(),
764
+ updateCount: 0,
765
+ spinnerIndex: 0,
766
+ milestones: [],
767
+ processSteps: []
768
+ };
769
+ const mergedMilestones = mergeUnique(state.milestones, opts.milestones ?? [], milestoneKey);
770
+ const mergedSteps = mergeUnique(state.processSteps, opts.processSteps ?? [], processStepKey);
771
+ const updateCount = state.updateCount + 1;
772
+ const spinner = SPINNER_EMOJIS[updateCount % SPINNER_EMOJIS.length];
773
+ const card = buildTaskCard({
774
+ status: opts.status,
775
+ meta: mergeTaskMeta(state.meta, opts.meta),
776
+ progress: opts.progress ?? state.progress,
777
+ milestones: mergedMilestones,
778
+ processSteps: mergedSteps,
779
+ updateCount,
780
+ spinner
781
+ });
782
+ await this.runLarkCli(this.buildPatchCardArgs({
783
+ messageId: opts.messageId,
784
+ profile: opts.profile ?? state.profile,
785
+ card
786
+ }), "patch");
787
+ this.states.set(opts.messageId, {
788
+ ...state,
789
+ chatId: opts.chatId ?? state.chatId,
790
+ profile: opts.profile ?? state.profile,
791
+ meta: mergeTaskMeta(state.meta, opts.meta),
792
+ updateCount,
793
+ spinnerIndex: spinnerIndexFor(updateCount),
794
+ milestones: mergedMilestones,
795
+ processSteps: mergedSteps,
796
+ progress: opts.progress ?? state.progress
797
+ });
798
+ });
799
+ }
800
+ async finishTask(opts) {
801
+ await this.mutexFor(opts.messageId).run(async () => {
802
+ const state = this.states.get(opts.messageId);
803
+ const now = new Date().toISOString();
804
+ const updateCount = (state?.updateCount ?? 0) + 1;
805
+ const spinner = SPINNER_EMOJIS[updateCount % SPINNER_EMOJIS.length];
806
+ const finalMilestone = {
807
+ title: opts.status === "completed" ? "Task 完成" : "Task 失败",
808
+ timestamp: now,
809
+ description: opts.finalStep
810
+ };
811
+ const card = buildTaskCard({
812
+ status: opts.status,
813
+ meta: mergeTaskMeta(state?.meta, opts.meta),
814
+ progress: opts.status === "completed" ? 100 : opts.progress ?? state?.progress,
815
+ milestones: mergeUnique(state?.milestones ?? [], [finalMilestone], milestoneKey),
816
+ processSteps: state?.processSteps ?? [],
817
+ updateCount,
818
+ spinner
819
+ });
820
+ await this.runLarkCli(this.buildPatchCardArgs({
821
+ messageId: opts.messageId,
822
+ profile: opts.profile ?? state?.profile,
823
+ card
824
+ }), "patch");
825
+ });
826
+ if (this.options.unpinOnFinish) {
827
+ const state = this.states.get(opts.messageId);
828
+ try {
829
+ await this.runLarkCli(this.buildUnpinArgs({
830
+ messageId: opts.messageId,
831
+ profile: opts.profile ?? state?.profile
832
+ }), "unpin");
833
+ } catch (unpinErr) {
834
+ logger3.warn(`Unpin failed (non-fatal) for ${opts.messageId}: ${unpinErr.message}`);
835
+ }
836
+ }
837
+ this.states.delete(opts.messageId);
838
+ this.mutexes.delete(opts.messageId);
839
+ }
840
+ trackedCardCount() {
841
+ return this.states.size;
842
+ }
843
+ buildSendCardArgs(opts) {
844
+ const args = [
845
+ "im",
846
+ "+messages-send",
847
+ "--chat-id",
848
+ opts.chatId,
849
+ "--msg-type",
850
+ "interactive",
851
+ "--content",
852
+ JSON.stringify(opts.card),
853
+ "--as",
854
+ "bot"
855
+ ];
856
+ if (opts.profile) {
857
+ args.unshift("--profile", opts.profile);
858
+ }
859
+ return args;
860
+ }
861
+ buildPatchCardArgs(opts) {
862
+ const data = JSON.stringify({ content: JSON.stringify(opts.card) });
863
+ const args = [
864
+ "im",
865
+ "messages",
866
+ "patch",
867
+ "--message-id",
868
+ opts.messageId,
869
+ "--data",
870
+ data,
871
+ "--as",
872
+ "bot"
873
+ ];
874
+ if (opts.profile) {
875
+ args.unshift("--profile", opts.profile);
876
+ }
877
+ return args;
878
+ }
879
+ buildPinArgs(opts) {
880
+ const args = [
881
+ "im",
882
+ "pins",
883
+ "create",
884
+ "--data",
885
+ JSON.stringify({ message_id: opts.messageId }),
886
+ "--as",
887
+ "bot"
888
+ ];
889
+ if (opts.profile) {
890
+ args.unshift("--profile", opts.profile);
891
+ }
892
+ return args;
893
+ }
894
+ buildUnpinArgs(opts) {
895
+ const args = [
896
+ "im",
897
+ "pins",
898
+ "delete",
899
+ "--message-id",
900
+ opts.messageId,
901
+ "--yes",
902
+ "--as",
903
+ "bot"
904
+ ];
905
+ if (opts.profile) {
906
+ args.unshift("--profile", opts.profile);
907
+ }
908
+ return args;
909
+ }
910
+ runLarkCli(argv, op) {
911
+ return new Promise((resolve, reject) => {
912
+ if (process.env.ROY_HOOK_DEBUG === "1") {
913
+ console.error(`[DEBUG LarkCardPatchNotifier.runLarkCli] op=${op} args=${JSON.stringify(argv).slice(0, 300)}`);
914
+ }
915
+ const proc = spawn("lark-cli", argv, {
916
+ shell: false,
917
+ stdio: ["pipe", "pipe", "pipe"]
918
+ });
919
+ let stdout = "";
920
+ let stderr = "";
921
+ proc.stdout?.on("data", (data) => {
922
+ stdout += data.toString();
923
+ });
924
+ proc.stderr?.on("data", (data) => {
925
+ stderr += data.toString();
926
+ });
927
+ proc.on("close", (code) => {
928
+ if (process.env.ROY_HOOK_DEBUG === "1") {
929
+ console.error(`[DEBUG LarkCardPatchNotifier.runLarkCli] op=${op} CLOSE code=${code} stdoutLen=${stdout.length} stderrLen=${stderr.length}`);
930
+ }
931
+ if (code === 0) {
932
+ resolve(stdout);
933
+ } else {
934
+ reject(new Error(`lark-cli ${op} exited with code ${code}: ${sanitize(stderr || stdout)}`));
935
+ }
936
+ });
937
+ proc.on("error", (error) => {
938
+ reject(error);
939
+ });
940
+ });
941
+ }
942
+ extractMessageId(stdout) {
943
+ try {
944
+ const obj = JSON.parse(stdout);
945
+ const data = obj?.data ?? obj;
946
+ const id = data?.message_id ?? data?.messageId ?? obj?.message_id ?? obj?.messageId;
947
+ return typeof id === "string" ? id : undefined;
948
+ } catch {
949
+ return;
950
+ }
951
+ }
952
+ }
953
+ __legacyDecorateClassTS([
954
+ TracedAs("lark-card.startTask", {
955
+ recordParams: true,
956
+ recordResult: true,
957
+ flattenParamFilter: true,
958
+ paramFilter: (args) => ({
959
+ chatId: args[0]?.chatId,
960
+ profile: args[0]?.profile,
961
+ taskId: args[0]?.taskId
962
+ }),
963
+ resultFilter: (r) => ({
964
+ messageId: r?.messageId,
965
+ pinned: r?.pinned
966
+ })
967
+ })
968
+ ], LarkCardPatchNotifier.prototype, "startTask", null);
969
+ __legacyDecorateClassTS([
970
+ TracedAs("lark-card.updateTask", {
971
+ recordParams: true,
972
+ recordResult: false,
973
+ flattenParamFilter: true,
974
+ paramFilter: (args) => ({
975
+ messageId: args[0]?.messageId,
976
+ profile: args[0]?.profile,
977
+ status: args[0]?.status,
978
+ taskId: args[0]?.taskId,
979
+ progress: args[0]?.progress,
980
+ newMilestones: args[0]?.milestones?.length ?? 0,
981
+ newProcessSteps: args[0]?.processSteps?.length ?? 0
982
+ })
983
+ })
984
+ ], LarkCardPatchNotifier.prototype, "updateTask", null);
985
+ __legacyDecorateClassTS([
986
+ TracedAs("lark-card.finishTask", {
987
+ recordParams: true,
988
+ recordResult: false,
989
+ flattenParamFilter: true,
990
+ paramFilter: (args) => ({
991
+ messageId: args[0]?.messageId,
992
+ profile: args[0]?.profile,
993
+ status: args[0]?.status,
994
+ taskId: args[0]?.taskId
995
+ })
996
+ })
997
+ ], LarkCardPatchNotifier.prototype, "finishTask", null);
998
+ __legacyDecorateClassTS([
999
+ TracedAs("lark-card.runLarkCli", {
1000
+ recordParams: true,
1001
+ recordResult: true,
1002
+ flattenParamFilter: true,
1003
+ paramFilter: (args) => {
1004
+ const argv = args[0] ?? [];
1005
+ const op = args[1] ?? "unknown";
1006
+ return {
1007
+ op,
1008
+ argv: argv.join(" ").slice(0, 300),
1009
+ hasProfile: argv.includes("--profile"),
1010
+ msgType: valueAfter(argv, "--msg-type"),
1011
+ contentLength: (valueAfter(argv, "--content") ?? "").length,
1012
+ dataLength: (valueAfter(argv, "--data") ?? "").length
1013
+ };
1014
+ },
1015
+ resultFilter: (r) => ({
1016
+ stdoutLength: (r ?? "").length
1017
+ })
1018
+ })
1019
+ ], LarkCardPatchNotifier.prototype, "runLarkCli", null);
1020
+ function mergeTaskMeta(base, incoming) {
1021
+ const merged = { ...base ?? {} };
1022
+ if (!incoming)
1023
+ return merged;
1024
+ for (const [k, v] of Object.entries(incoming)) {
1025
+ if (v === undefined || v === null || v === "")
1026
+ continue;
1027
+ merged[k] = v;
1028
+ }
1029
+ return merged;
1030
+ }
1031
+ function mergeUnique(base, incoming, keyOf) {
1032
+ const seen = new Set(base.map(keyOf));
1033
+ const merged = [...base];
1034
+ for (const item of incoming) {
1035
+ const k = keyOf(item);
1036
+ if (seen.has(k))
1037
+ continue;
1038
+ seen.add(k);
1039
+ merged.push(item);
1040
+ }
1041
+ return merged;
1042
+ }
1043
+ function spinnerIndexFor(updateCount) {
1044
+ return updateCount % SPINNER_EMOJIS.length;
1045
+ }
1046
+ function valueAfter(argv, flag) {
1047
+ const idx = argv.indexOf(flag);
1048
+ if (idx < 0 || idx + 1 >= argv.length)
1049
+ return;
1050
+ return argv[idx + 1];
1051
+ }
1052
+ function sanitize(text) {
1053
+ const trimmed = text.trim();
1054
+ if (trimmed.length <= 500)
1055
+ return trimmed;
1056
+ return `${trimmed.slice(0, 500)}…`;
1057
+ }
1058
+
1059
+ // src/env/task/plugins/card-chat-routing.ts
1060
+ var GROUP_NAME_TASK_ID_RE = /(?:Task\s*#?\s*|#)(\d{1,7})\b/i;
1061
+ var DEDICATED_CHAT_ID_FIELD_RE = /^(larkPerTaskChatId|[a-z0-9_]*(followup|group|per_?task)[a-z0-9_]*chat[_]?id)$/i;
1062
+ var CONVERSATION_CHAT_ID_FIELD_RE = /^(feishu_chat_id|chat_id|message_?chat_?id)$/i;
1063
+ var CTX_FIELD_LARK_PER_TASK_CHAT_ID = "larkPerTaskChatId";
1064
+ function extractPerTaskChatId(contextStr) {
1065
+ if (!contextStr)
1066
+ return;
1067
+ try {
1068
+ const obj = JSON.parse(contextStr);
1069
+ const v = obj?.[CTX_FIELD_LARK_PER_TASK_CHAT_ID];
1070
+ return typeof v === "string" && v.length > 0 ? v : undefined;
1071
+ } catch {
1072
+ return;
1073
+ }
1074
+ }
1075
+ function extractPerTaskChatProfile(contextStr) {
1076
+ if (!contextStr)
1077
+ return;
1078
+ try {
1079
+ const obj = JSON.parse(contextStr);
1080
+ const v = obj?.["larkPerTaskChatProfile"];
1081
+ return typeof v === "string" && v.length > 0 ? v : undefined;
1082
+ } catch {
1083
+ return;
1084
+ }
1085
+ }
1086
+ function extractRecordedChatNames(contextStr) {
1087
+ if (!contextStr)
1088
+ return [];
1089
+ const out = [];
1090
+ try {
1091
+ const obj = JSON.parse(contextStr);
1092
+ for (const [k, v] of Object.entries(obj ?? {})) {
1093
+ if (typeof v === "string" && /chat_?name$/i.test(k) && v.length > 0) {
1094
+ out.push(v);
1095
+ }
1096
+ }
1097
+ } catch {
1098
+ return [];
1099
+ }
1100
+ return out;
1101
+ }
1102
+ function analyzeContextOwnership(contextStr, chatId) {
1103
+ const sources = [];
1104
+ const impliedTaskIds = [];
1105
+ if (!contextStr)
1106
+ return { owned: false, sources, impliedTaskIds };
1107
+ let obj;
1108
+ try {
1109
+ const parsed = JSON.parse(contextStr);
1110
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1111
+ obj = parsed;
1112
+ }
1113
+ } catch {
1114
+ obj = undefined;
1115
+ }
1116
+ if (!obj)
1117
+ return { owned: false, sources, impliedTaskIds };
1118
+ for (const [k, v] of Object.entries(obj)) {
1119
+ if (typeof v !== "string" || v.length === 0)
1120
+ continue;
1121
+ const isDedicatedField = DEDICATED_CHAT_ID_FIELD_RE.test(k);
1122
+ const isConversationField = CONVERSATION_CHAT_ID_FIELD_RE.test(k);
1123
+ if (isDedicatedField && !isConversationField && v === chatId) {
1124
+ sources.push(k);
1125
+ continue;
1126
+ }
1127
+ if (/chat_?name$/i.test(k)) {
1128
+ const m = v.match(GROUP_NAME_TASK_ID_RE);
1129
+ if (m) {
1130
+ const id = Number(m[1]);
1131
+ if (Number.isFinite(id) && id > 0)
1132
+ impliedTaskIds.push(id);
1133
+ }
1134
+ }
1135
+ }
1136
+ return { owned: sources.length > 0, sources, impliedTaskIds };
1137
+ }
1138
+ function mergeChatOwnership(chatId, candidates, extra = {}) {
1139
+ const ownerTaskIds = new Set;
1140
+ const sources = [];
1141
+ let chatName = extra.chatName;
1142
+ if (typeof extra.boundTaskId === "number" && extra.boundTaskId > 0) {
1143
+ ownerTaskIds.add(extra.boundTaskId);
1144
+ sources.push("chat-binding");
1145
+ }
1146
+ for (const c of candidates) {
1147
+ const r = analyzeContextOwnership(c.context, chatId);
1148
+ if (r.owned) {
1149
+ ownerTaskIds.add(c.id);
1150
+ sources.push(`#${c.id}:${r.sources.join("+")}`);
1151
+ }
1152
+ if (!chatName) {
1153
+ const names = extractRecordedChatNames(c.context);
1154
+ const matched = names.find((n) => n.includes(chatId) || GROUP_NAME_TASK_ID_RE.test(n));
1155
+ if (matched)
1156
+ chatName = matched;
1157
+ }
1158
+ for (const implied of r.impliedTaskIds) {
1159
+ if (implied === c.id) {
1160
+ ownerTaskIds.add(c.id);
1161
+ sources.push(`#${c.id}:chat-name`);
1162
+ }
1163
+ }
1164
+ }
1165
+ if (chatName) {
1166
+ const m = chatName.match(GROUP_NAME_TASK_ID_RE);
1167
+ if (m) {
1168
+ const id = Number(m[1]);
1169
+ if (Number.isFinite(id) && id > 0) {
1170
+ ownerTaskIds.add(id);
1171
+ sources.push("chat-name");
1172
+ }
1173
+ }
1174
+ }
1175
+ return {
1176
+ chatId,
1177
+ ownerTaskIds: [...ownerTaskIds],
1178
+ sources,
1179
+ chatName,
1180
+ dedicated: ownerTaskIds.size > 0
1181
+ };
1182
+ }
1183
+ function decideCardChatTarget(input) {
1184
+ const {
1185
+ lineageIds,
1186
+ lineageChat,
1187
+ triggerChatId,
1188
+ triggerProfile,
1189
+ triggerOwnership
1190
+ } = input;
1191
+ const foreign = !!triggerChatId && !!triggerOwnership?.dedicated && triggerOwnership.ownerTaskIds.length > 0 && !triggerOwnership.ownerTaskIds.some((id) => lineageIds.includes(id));
1192
+ if (triggerChatId && !foreign) {
1193
+ return {
1194
+ chatId: triggerChatId,
1195
+ profile: triggerProfile,
1196
+ source: "trigger-chat",
1197
+ reason: triggerOwnership?.dedicated ? `触发群属于自己的 lineage(owner=${triggerOwnership.ownerTaskIds.join(",")}),直接用` : "触发群(普通会话群),直接用"
1198
+ };
1199
+ }
1200
+ if (lineageChat) {
1201
+ return {
1202
+ chatId: lineageChat.chatId,
1203
+ profile: lineageChat.profile,
1204
+ source: lineageChat.isSelf ? "own-group" : "ancestor-group",
1205
+ reroutedFrom: foreign ? {
1206
+ chatId: triggerChatId,
1207
+ ownerTaskIds: triggerOwnership.ownerTaskIds,
1208
+ sources: triggerOwnership.sources
1209
+ } : undefined,
1210
+ reason: foreign ? `触发群 ${triggerChatId} 是 task ${triggerOwnership.ownerTaskIds.join(",")} 的专属群,改投 lineage 群 ${lineageChat.chatId}` : `无触发群,用 lineage 群 ${lineageChat.chatId}`
1211
+ };
1212
+ }
1213
+ if (!triggerChatId) {
1214
+ return {
1215
+ chatId: undefined,
1216
+ source: "none",
1217
+ reason: "既没有触发群也没有 lineage 群(无法决定投递目标)"
1218
+ };
1219
+ }
1220
+ return {
1221
+ chatId: undefined,
1222
+ source: "new-group",
1223
+ profile: triggerProfile,
1224
+ reroutedFrom: foreign ? {
1225
+ chatId: triggerChatId,
1226
+ ownerTaskIds: triggerOwnership.ownerTaskIds,
1227
+ sources: triggerOwnership.sources
1228
+ } : undefined,
1229
+ reason: foreign ? `触发群 ${triggerChatId} 是 task ${triggerOwnership.ownerTaskIds.join(",")} 的专属群,且本 task 无群 → 需要新建` : "无触发群且本 task 无群 → 需要新建"
1230
+ };
1231
+ }
1232
+
1233
+ // src/env/task/plugins/lark-cli-task-notify-plugin.ts
1234
+ var logger4 = createLogger("lark-cli:task-notify");
1235
+ var CTX_FIELD_CHAT_ID = "larkPerTaskChatId";
1236
+ var CTX_FIELD_CHAT_PROFILE = "larkPerTaskChatProfile";
1237
+ function parseLarkPerTaskChatId(ctxStr) {
1238
+ if (!ctxStr)
1239
+ return;
1240
+ try {
1241
+ const obj = JSON.parse(ctxStr);
1242
+ const id = obj[CTX_FIELD_CHAT_ID];
1243
+ return typeof id === "string" ? id : undefined;
1244
+ } catch {
1245
+ return;
1246
+ }
1247
+ }
1248
+ function parseLarkPerTaskChatProfile(ctxStr) {
1249
+ if (!ctxStr)
1250
+ return;
1251
+ try {
1252
+ const obj = JSON.parse(ctxStr);
1253
+ const profile = obj[CTX_FIELD_CHAT_PROFILE];
1254
+ return typeof profile === "string" ? profile : undefined;
1255
+ } catch {
1256
+ return;
1257
+ }
1258
+ }
1259
+ var NotifyType;
1260
+ ((NotifyType2) => {
1261
+ NotifyType2["TASK_CREATED"] = "task_created";
1262
+ NotifyType2["TASK_COMPLETED"] = "task_completed";
1263
+ NotifyType2["TASK_UPDATED"] = "task_updated";
1264
+ NotifyType2["TASK_FAILED"] = "task_failed";
1265
+ NotifyType2["OPERATION_CREATED"] = "operation_created";
1266
+ })(NotifyType ||= {});
1267
+
1268
+ class LarkCliTaskNotifyHook {
1269
+ name;
1270
+ priority;
1271
+ description;
1272
+ pluginName;
1273
+ sourceId;
1274
+ config;
1275
+ cardNotifier;
1276
+ cardMessageIds = new Map;
1277
+ lastNotifyTimestamps = new Map;
1278
+ cardRoutingPorts;
1279
+ static CARD_START_FAILED_SENTINEL = "__card_start_failed__";
1280
+ static CARD_START_INFLIGHT_SENTINEL = "__card_start_inflight__";
1281
+ constructor(config = {}) {
1282
+ this.name = config.pluginName || "LarkCliTaskNotifyHook";
1283
+ this.priority = config.priority ?? 5;
1284
+ this.description = "LarkCli 任务通知 Hook(卡片模式)";
1285
+ this.pluginName = config.pluginName;
1286
+ this.sourceId = config.sourceId;
1287
+ this.config = {
1288
+ enabled: true,
1289
+ priority: 5,
1290
+ notifyTypes: [
1291
+ "task_created" /* TASK_CREATED */,
1292
+ "task_updated" /* TASK_UPDATED */,
1293
+ "task_completed" /* TASK_COMPLETED */,
1294
+ "operation_created" /* OPERATION_CREATED */
1295
+ ],
1296
+ ...config
1297
+ };
1298
+ if (this.config.cardMode !== false) {
1299
+ this.cardNotifier = config.cardNotifier ?? new LarkCardPatchNotifier;
1300
+ }
1301
+ }
1302
+ setCardRoutingPorts(ports) {
1303
+ this.cardRoutingPorts = ports;
1304
+ logger4.debug(`CardRoutingPorts injected: lookup=${typeof ports.lookupChatOwnership}, ` + `getTask=${typeof ports.getTask}, ensureGroup=${typeof ports.ensureTaskGroupChat}`);
1305
+ }
1306
+ hasCardRoutingPorts() {
1307
+ return this.cardRoutingPorts !== undefined;
1308
+ }
1309
+ async execute(ctx) {
1310
+ if (!this.config.enabled) {
1311
+ return;
1312
+ }
1313
+ const env = getEnvContextOrEmpty();
1314
+ if (this.pluginName && this.sourceId) {
1315
+ const key = `${this.sourceId}:${this.pluginName}`;
1316
+ if (env.pluginEnabled && env.pluginEnabled[key] !== true) {
1317
+ logger4.debug(`Plugin not enabled for source: ${key}`);
1318
+ return;
1319
+ }
1320
+ }
1321
+ switch (ctx.hookPoint) {
1322
+ case TaskHookPoints.AFTER_CREATE:
1323
+ await this.onAfterCreate(ctx);
1324
+ break;
1325
+ case TaskHookPoints.AFTER_UPDATE:
1326
+ await this.onAfterUpdate(ctx);
1327
+ break;
1328
+ case TaskHookPoints.AFTER_COMPLETE:
1329
+ await this.onAfterComplete(ctx);
1330
+ break;
1331
+ case TaskHookPoints.OPERATION_AFTER_CREATE:
1332
+ await this.onAfterOperationCreate(ctx);
1333
+ break;
1334
+ }
1335
+ }
1336
+ async onAfterCreate(ctx) {
1337
+ const { task } = ctx.data;
1338
+ if (!task || !this.shouldNotify("task_created" /* TASK_CREATED */))
1339
+ return;
1340
+ if (!this.isCardModeEnabled())
1341
+ return;
1342
+ await this.sendCardStart(task);
1343
+ }
1344
+ async onAfterUpdate(ctx) {
1345
+ const data = ctx.data;
1346
+ if (!data?.task)
1347
+ return;
1348
+ const { task, changes } = data;
1349
+ let notifyType;
1350
+ if (changes?.status === "completed" || changes?.progress === 100) {
1351
+ notifyType = "task_completed" /* TASK_COMPLETED */;
1352
+ } else if (changes?.status) {
1353
+ notifyType = "task_updated" /* TASK_UPDATED */;
1354
+ } else if (task.status === "completed" || task.progress === 100) {
1355
+ notifyType = "task_completed" /* TASK_COMPLETED */;
1356
+ } else if (changes && Object.keys(changes).length > 0) {
1357
+ notifyType = "task_updated" /* TASK_UPDATED */;
1358
+ } else {
1359
+ return;
1360
+ }
1361
+ if (!this.shouldNotify(notifyType))
1362
+ return;
1363
+ if (!this.isCardModeEnabled())
1364
+ return;
1365
+ if (notifyType === "task_completed" /* TASK_COMPLETED */ || task.status === "completed") {
1366
+ await this.sendCardFinish(task);
1367
+ } else {
1368
+ await this.sendCardUpdate(task, changes);
1369
+ }
1370
+ }
1371
+ async onAfterComplete(ctx) {
1372
+ const data = ctx.data;
1373
+ const task = data?.task ?? data;
1374
+ if (!task || !this.shouldNotify("task_completed" /* TASK_COMPLETED */))
1375
+ return;
1376
+ if (!this.isCardModeEnabled())
1377
+ return;
1378
+ await this.sendCardFinish(task);
1379
+ }
1380
+ async onAfterOperationCreate(ctx) {
1381
+ const { operation } = ctx.data;
1382
+ if (!operation)
1383
+ return;
1384
+ if (!this.shouldNotify("operation_created" /* OPERATION_CREATED */))
1385
+ return;
1386
+ if (this.shouldThrottleOperation(operation))
1387
+ return;
1388
+ if (!this.isCardModeEnabled())
1389
+ return;
1390
+ const messageId = this.cardMessageIds.get(operation.taskId);
1391
+ if (!messageId || messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL) {
1392
+ logger4.debug(`onAfterOperationCreate: no card for task #${operation.taskId}, skip`);
1393
+ return;
1394
+ }
1395
+ const { milestones, processSteps } = this.deriveTimelineFromOperation(operation);
1396
+ if (milestones.length === 0 && processSteps.length === 0)
1397
+ return;
1398
+ const env = getEnvContextOrEmpty();
1399
+ const profile = env.replyChannel?.profile;
1400
+ try {
1401
+ await this.cardNotifier.updateTask({
1402
+ messageId,
1403
+ profile,
1404
+ status: "running",
1405
+ milestones,
1406
+ processSteps
1407
+ });
1408
+ } catch (err) {
1409
+ logger4.error(`sendCardOperation failed for task #${operation.taskId}: ${err.message}`);
1410
+ }
1411
+ }
1412
+ isCardModeEnabled() {
1413
+ if (this.config.cardMode === false || !this.cardNotifier) {
1414
+ logger4.debug("cardMode=false (legacy opt-out), skip task notification (no text fallback)");
1415
+ return false;
1416
+ }
1417
+ return true;
1418
+ }
1419
+ async resolveCardTarget(task) {
1420
+ const env = getEnvContextOrEmpty();
1421
+ const triggerChatId = env.replyChannel?.chatId;
1422
+ const triggerProfile = env.replyChannel?.profile;
1423
+ const lineage = await this.collectLineage(task);
1424
+ const lineageIds = lineage.map((t) => t.id);
1425
+ const selfChatId = extractPerTaskChatId(task.context);
1426
+ let lineageChat;
1427
+ for (const node of lineage) {
1428
+ const chatId = extractPerTaskChatId(node.context);
1429
+ if (chatId) {
1430
+ lineageChat = {
1431
+ chatId,
1432
+ profile: extractPerTaskChatProfile(node.context),
1433
+ isSelf: node.id === task.id
1434
+ };
1435
+ break;
1436
+ }
1437
+ }
1438
+ if (!lineageChat && selfChatId) {
1439
+ lineageChat = {
1440
+ chatId: selfChatId,
1441
+ profile: extractPerTaskChatProfile(task.context),
1442
+ isSelf: true
1443
+ };
1444
+ }
1445
+ let triggerOwnership;
1446
+ if (triggerChatId && this.cardRoutingPorts?.lookupChatOwnership) {
1447
+ try {
1448
+ triggerOwnership = await this.cardRoutingPorts.lookupChatOwnership(triggerChatId);
1449
+ } catch (err) {
1450
+ logger4.warn(`lookupChatOwnership failed for ${triggerChatId} (degrade to trigger chat): ${err.message}`);
1451
+ }
1452
+ }
1453
+ const decision = decideCardChatTarget({
1454
+ taskId: task.id,
1455
+ lineageIds,
1456
+ lineageChat,
1457
+ triggerChatId,
1458
+ triggerProfile,
1459
+ triggerOwnership
1460
+ });
1461
+ if (decision.source === "new-group") {
1462
+ if (this.cardRoutingPorts?.ensureTaskGroupChat) {
1463
+ try {
1464
+ const created = await this.cardRoutingPorts.ensureTaskGroupChat(task);
1465
+ if (created?.chatId) {
1466
+ return {
1467
+ ...decision,
1468
+ chatId: created.chatId,
1469
+ profile: created.profile ?? decision.profile,
1470
+ chatName: created.name,
1471
+ reason: `${decision.reason};已新建专属群 ${created.chatId}`
1472
+ };
1473
+ }
1474
+ } catch (err) {
1475
+ logger4.warn(`ensureTaskGroupChat failed for task #${task.id}: ${err.message}`);
1476
+ }
1477
+ }
1478
+ return {
1479
+ ...decision,
1480
+ chatId: triggerChatId,
1481
+ profile: triggerProfile,
1482
+ source: triggerChatId ? "trigger-chat" : "none",
1483
+ reroutedFrom: undefined,
1484
+ reason: `${decision.reason};但建群失败/不可用 → 退回触发群`
1485
+ };
1486
+ }
1487
+ return decision;
1488
+ }
1489
+ async collectLineage(task) {
1490
+ const out = [
1491
+ { id: task.id, context: task.context }
1492
+ ];
1493
+ let parentId = typeof task.parent_task_id === "number" && task.parent_task_id > 0 ? task.parent_task_id : undefined;
1494
+ let depth = 0;
1495
+ while (parentId && depth < 10 && this.cardRoutingPorts?.getTask) {
1496
+ try {
1497
+ const parent = await this.cardRoutingPorts.getTask(parentId);
1498
+ if (!parent)
1499
+ break;
1500
+ out.push({ id: parentId, context: parent.context });
1501
+ parentId = typeof parent.parent_task_id === "number" && parent.parent_task_id > 0 ? parent.parent_task_id : undefined;
1502
+ } catch {
1503
+ break;
1504
+ }
1505
+ depth++;
1506
+ }
1507
+ return out;
1508
+ }
1509
+ async sendCardStart(task) {
1510
+ if (!this.cardNotifier)
1511
+ return;
1512
+ const claim = this.cardMessageIds.get(task.id);
1513
+ if (claim) {
1514
+ logger4.debug(`sendCardStart: task #${task.id} 已有首卡(${claim})→ 跳过重复发卡`);
1515
+ return;
1516
+ }
1517
+ this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL);
1518
+ let target;
1519
+ try {
1520
+ target = await this.resolveCardTarget(task);
1521
+ } catch (err) {
1522
+ this.cardMessageIds.delete(task.id);
1523
+ throw err;
1524
+ }
1525
+ const chatId = target.chatId;
1526
+ const profile = target.profile;
1527
+ if (!chatId) {
1528
+ this.cardMessageIds.delete(task.id);
1529
+ logger4.debug(`sendCardStart: no chatId for task #${task.id}, skip`);
1530
+ return;
1531
+ }
1532
+ try {
1533
+ const { messageId, pinned } = await this.cardNotifier.startTask({
1534
+ chatId,
1535
+ profile,
1536
+ meta: this.buildTaskMeta(task),
1537
+ milestones: [
1538
+ {
1539
+ title: "Task 创建",
1540
+ timestamp: new Date().toISOString(),
1541
+ description: task.current_status || undefined
1542
+ }
1543
+ ]
1544
+ });
1545
+ this.cardMessageIds.set(task.id, messageId);
1546
+ logger4.debug(`sendCardStart: task #${task.id} → message ${messageId} (pinned=${pinned}) ` + `chat=${chatId} source=${target.source} reason=${target.reason}`);
1547
+ if (target.reroutedFrom) {
1548
+ await this.cardNotifier.sendRedirectNotice({
1549
+ chatId: target.reroutedFrom.chatId,
1550
+ profile: target.profile,
1551
+ taskId: task.id,
1552
+ taskTitle: task.title,
1553
+ targetChatId: chatId,
1554
+ targetChatName: target.chatName,
1555
+ reason: target.reason
1556
+ });
1557
+ }
1558
+ } catch (err) {
1559
+ this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL);
1560
+ logger4.error(`sendCardStart failed for task #${task.id}: ${err.message}`);
1561
+ }
1562
+ }
1563
+ async sendCardUpdate(task, changes) {
1564
+ if (!this.cardNotifier)
1565
+ return;
1566
+ let messageId = this.cardMessageIds.get(task.id);
1567
+ const target = messageId ? undefined : await this.resolveCardTarget(task);
1568
+ const chatId = target?.chatId ?? this.resolveChatId(task, getEnvContextOrEmpty());
1569
+ const profile = target?.profile ?? this.resolveProfile(task, getEnvContextOrEmpty());
1570
+ if (!chatId) {
1571
+ logger4.debug(`sendCardUpdate: no chatId for task #${task.id}, skip`);
1572
+ return;
1573
+ }
1574
+ if (messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL) {
1575
+ logger4.debug(`sendCardUpdate: startTask failed previously for task #${task.id}, skip`);
1576
+ return;
1577
+ }
1578
+ if (messageId === LarkCliTaskNotifyHook.CARD_START_INFLIGHT_SENTINEL) {
1579
+ logger4.debug(`sendCardUpdate: task #${task.id} 首卡正在发送中(in-flight)→ 跳过本次 update`);
1580
+ return;
1581
+ }
1582
+ if (!messageId) {
1583
+ try {
1584
+ const res = await this.cardNotifier.startTask({
1585
+ chatId,
1586
+ profile,
1587
+ meta: this.buildTaskMeta(task)
1588
+ });
1589
+ messageId = res.messageId;
1590
+ this.cardMessageIds.set(task.id, messageId);
1591
+ } catch (err) {
1592
+ this.cardMessageIds.set(task.id, LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL);
1593
+ logger4.error(`sendCardUpdate fallback start failed: ${err.message}`);
1594
+ return;
1595
+ }
1596
+ }
1597
+ const changeKeys = Object.keys(changes ?? {});
1598
+ const processSteps = changeKeys.length > 0 ? [
1599
+ {
1600
+ title: "状态更新",
1601
+ timestamp: new Date().toISOString(),
1602
+ description: changeKeys.map((k) => `${k}=${humanizeValue(changes[k], 80)}`).join(" · ")
1603
+ }
1604
+ ] : [];
1605
+ try {
1606
+ await this.cardNotifier.updateTask({
1607
+ messageId,
1608
+ chatId,
1609
+ profile,
1610
+ status: "running",
1611
+ meta: this.buildTaskMeta(task),
1612
+ progress: typeof task.progress === "number" && task.progress > 0 ? task.progress : undefined,
1613
+ milestones: [],
1614
+ processSteps
1615
+ });
1616
+ } catch (err) {
1617
+ logger4.error(`sendCardUpdate failed: ${err.message}`);
1618
+ }
1619
+ }
1620
+ async sendCardFinish(task) {
1621
+ if (!this.cardNotifier)
1622
+ return;
1623
+ const messageId = this.cardMessageIds.get(task.id);
1624
+ const ctx = getEnvContextOrEmpty();
1625
+ const profile = this.resolveProfile(task, ctx);
1626
+ if (!messageId || messageId === LarkCliTaskNotifyHook.CARD_START_FAILED_SENTINEL) {
1627
+ logger4.debug(`sendCardFinish: no card messageId for task #${task.id}, skip`);
1628
+ return;
1629
+ }
1630
+ const status = String(task.status) === "failed" || String(task.status) === "cancelled" || String(task.status) === "timeout" ? "failed" : "completed";
1631
+ try {
1632
+ await this.cardNotifier.finishTask({
1633
+ messageId,
1634
+ profile,
1635
+ status,
1636
+ meta: this.buildTaskMeta(task),
1637
+ finalStep: task.current_status || undefined
1638
+ });
1639
+ this.cardMessageIds.delete(task.id);
1640
+ } catch (err) {
1641
+ logger4.error(`sendCardFinish failed: ${err.message}`);
1642
+ }
1643
+ }
1644
+ resolveChatId(task, env) {
1645
+ const envChat = env.replyChannel?.chatId;
1646
+ if (envChat)
1647
+ return envChat;
1648
+ const ctxStr = task.context;
1649
+ if (typeof ctxStr === "string") {
1650
+ const ctxChat = parseLarkPerTaskChatId(ctxStr);
1651
+ if (ctxChat)
1652
+ return ctxChat;
1653
+ }
1654
+ return;
1655
+ }
1656
+ resolveProfile(task, env) {
1657
+ const envProfile = env.replyChannel?.profile;
1658
+ if (envProfile)
1659
+ return envProfile;
1660
+ const ctxStr = task.context;
1661
+ if (typeof ctxStr === "string") {
1662
+ const ctxProfile = parseLarkPerTaskChatProfile(ctxStr);
1663
+ if (ctxProfile)
1664
+ return ctxProfile;
1665
+ }
1666
+ return;
1667
+ }
1668
+ buildTaskMeta(task) {
1669
+ return {
1670
+ id: task.id,
1671
+ title: task.title,
1672
+ description: task.description,
1673
+ goals: task.goals_and_expected_deliverables,
1674
+ parentTaskId: typeof task.parent_task_id === "number" && task.parent_task_id > 0 ? task.parent_task_id : undefined,
1675
+ priority: task.priority,
1676
+ type: task.type,
1677
+ projectPath: task.project_path,
1678
+ context: task.context,
1679
+ currentStatus: task.current_status
1680
+ };
1681
+ }
1682
+ deriveTimelineFromOperation(operation) {
1683
+ const raw = operation;
1684
+ const title = operation.milestoneTitle ?? operation.actionTitle ?? raw.action_title ?? raw.description ?? raw.type ?? undefined;
1685
+ const detail = operation.milestoneDescription ?? operation.actionDescription ?? raw.action_description ?? undefined;
1686
+ const timestamp = operation.timestamp ?? new Date().toISOString();
1687
+ const milestones = [];
1688
+ const processSteps = [];
1689
+ if (operation.milestoneTitle || operation.milestoneType || title) {
1690
+ milestones.push({
1691
+ title: title ?? String(operation.milestoneType ?? "里程碑"),
1692
+ timestamp,
1693
+ description: detail
1694
+ });
1695
+ }
1696
+ if (operation.processDescription || detail) {
1697
+ processSteps.push({
1698
+ title: title ?? "过程",
1699
+ timestamp,
1700
+ description: operation.processDescription ?? detail
1701
+ });
1702
+ }
1703
+ const currentStep = operation.processDescription ?? detail ?? title ?? undefined;
1704
+ return { milestones, processSteps, currentStep };
1705
+ }
1706
+ shouldThrottleOperation(operation) {
1707
+ const throttleMs = this.config.operationThrottleMs ?? 5000;
1708
+ if (throttleMs <= 0)
1709
+ return false;
1710
+ const key = `op:${operation.taskId}`;
1711
+ const now = Date.now();
1712
+ const lastTime = this.lastNotifyTimestamps.get(key);
1713
+ if (lastTime && now - lastTime < throttleMs) {
1714
+ logger4.debug(`[throttle] skip OPERATION_CREATED patch for task #${operation.taskId} (${now - lastTime}ms ago)`);
1715
+ return true;
1716
+ }
1717
+ this.lastNotifyTimestamps.set(key, now);
1718
+ return false;
1719
+ }
1720
+ shouldNotify(type) {
1721
+ const { notifyTypes } = this.config;
1722
+ if (notifyTypes && notifyTypes.length === 0)
1723
+ return true;
1724
+ if (notifyTypes)
1725
+ return notifyTypes.includes(type);
1726
+ return type === "task_completed" /* TASK_COMPLETED */;
1727
+ }
1728
+ }
1729
+ __legacyDecorateClassTS([
1730
+ TracedAs("lark-cli-task-notify.execute")
1731
+ ], LarkCliTaskNotifyHook.prototype, "execute", null);
1732
+ __legacyDecorateClassTS([
1733
+ TracedAs("lark-cli-task-notify.resolveCardTarget", {
1734
+ recordParams: true,
1735
+ recordResult: true,
1736
+ flattenParamFilter: true,
1737
+ paramFilter: (args) => ({
1738
+ taskId: args[0]?.id,
1739
+ triggerChatId: undefined
1740
+ }),
1741
+ resultFilter: (r) => ({
1742
+ source: r?.source,
1743
+ chatId: r?.chatId,
1744
+ chatName: r?.chatName,
1745
+ reroutedFrom: r?.reroutedFrom?.chatId,
1746
+ rerouteOwners: r?.reroutedFrom?.ownerTaskIds?.join(","),
1747
+ reason: r?.reason?.slice(0, 200)
1748
+ })
1749
+ })
1750
+ ], LarkCliTaskNotifyHook.prototype, "resolveCardTarget", null);
1751
+ function createLarkCliTaskNotifyHook(config = {}) {
1752
+ return new LarkCliTaskNotifyHook(config);
1753
+ }
1754
+ // src/env/task/plugins/per-task-group-chat-plugin.ts
1755
+ init_decorator();
1756
+ init_logger();
1757
+ init_context();
1758
+ import { spawn as spawn2 } from "child_process";
1759
+ var logger5 = createLogger("lark-cli:per-task-group");
1760
+ function isPluginDebugEnabled() {
1761
+ return process.env.ROY_TASK_NOTIFY_DEBUG === "1" || process.env.ROY_TRACE_DEBUG === "1";
1762
+ }
1763
+ var CTX_FIELD_CHAT_ID2 = "larkPerTaskChatId";
1764
+ var CTX_FIELD_CHAT_PROFILE2 = "larkPerTaskChatProfile";
1765
+ var CTX_FIELD_CHAT_CREATED_AT = "larkPerTaskChatCreatedAt";
1766
+ var CTX_FIELD_CHAT_TASK_ID = "larkPerTaskChatTaskId";
1767
+
1768
+ class PerTaskGroupChatPlugin {
1769
+ name;
1770
+ priority;
1771
+ pluginName;
1772
+ sourceId;
1773
+ config;
1774
+ lastNotifyTimestamps = new Map;
1775
+ taskUpdater;
1776
+ taskGetter;
1777
+ sessionComponent;
1778
+ cardNotifier;
1779
+ cardMessageIds = new Map;
1780
+ static CARD_START_FAILED_SENTINEL = "__card_start_failed__";
1781
+ groupChatInflight = new Map;
1782
+ constructor(config = {}) {
1783
+ this.config = {
1784
+ enabled: config.enabled ?? true,
1785
+ priority: config.priority ?? 100,
1786
+ pluginName: config.pluginName ?? "PerTaskGroupChatPlugin",
1787
+ sourceId: config.sourceId ?? "",
1788
+ nameTemplate: config.nameTemplate ?? "Task #{id} - #{title}",
1789
+ descriptionTemplate: config.descriptionTemplate ?? "roy-agent task #{id} - #{project_path}",
1790
+ botAppIds: config.botAppIds ?? [],
1791
+ inviteUserIds: config.inviteUserIds ?? ["sender"],
1792
+ defaultInviteUserIds: config.defaultInviteUserIds ?? [],
1793
+ fallbackToMain: config.fallbackToMain ?? true,
1794
+ operationThrottleMs: config.operationThrottleMs ?? 5000,
1795
+ disableSubTaskSkip: config.disableSubTaskSkip ?? false,
1796
+ requireReplyChannel: config.requireReplyChannel ?? true,
1797
+ fallbackProfile: config.fallbackProfile ?? "",
1798
+ sendWelcomeMessage: config.sendWelcomeMessage ?? true,
1799
+ resolveToTriggerChatFallback: config.resolveToTriggerChatFallback ?? false,
1800
+ cardInitialMessage: config.cardInitialMessage !== false,
1801
+ cardNotifier: config.cardNotifier ?? new LarkCardPatchNotifier
1802
+ };
1803
+ this.cardNotifier = this.config.cardNotifier;
1804
+ this.name = this.config.pluginName;
1805
+ this.priority = this.config.priority;
1806
+ this.pluginName = this.config.pluginName;
1807
+ this.sourceId = this.config.sourceId;
1808
+ }
1809
+ async ensureGroupChat(task, replyChannelOverride) {
1810
+ const renderedName = this.renderTemplate(this.config.nameTemplate, task);
1811
+ const existingChatId = this.getTaskChatId(task);
1812
+ if (existingChatId) {
1813
+ logger5.debug(`ensureGroupChat[${this.pluginName}]: task #${task.id} 已有专属群 ${existingChatId} → 复用(不重复建群)`);
1814
+ return {
1815
+ chatId: existingChatId,
1816
+ profile: this.parseProfileFromContext(task.context),
1817
+ name: renderedName
1818
+ };
1819
+ }
1820
+ const inflight = this.groupChatInflight.get(task.id);
1821
+ if (inflight) {
1822
+ logger5.debug(`ensureGroupChat[${this.pluginName}]: task #${task.id} 建群已在进行中 → 复用同一 promise`);
1823
+ return inflight;
1824
+ }
1825
+ const pending = this.doEnsureGroupChat(task, renderedName, replyChannelOverride);
1826
+ this.groupChatInflight.set(task.id, pending);
1827
+ try {
1828
+ return await pending;
1829
+ } finally {
1830
+ this.groupChatInflight.delete(task.id);
1831
+ }
1832
+ }
1833
+ async doEnsureGroupChat(task, name, replyChannelOverride) {
1834
+ const replyChannel = replyChannelOverride ?? getEnvContextOrEmpty().replyChannel;
1835
+ if (!replyChannel?.chatId && this.config.requireReplyChannel) {
1836
+ logger5.warn(`ensureGroupChat[${this.pluginName}]: no replyChannel.chatId for task #${task.id} ` + `(requireReplyChannel=true) → 无法建群`);
1837
+ return;
1838
+ }
1839
+ const description = this.renderTemplate(this.config.descriptionTemplate, task);
1840
+ const profile = replyChannel?.profile || (this.config.fallbackProfile || undefined);
1841
+ const botAppIds = this.resolveBotAppIds(replyChannel ?? {});
1842
+ const userIds = this.resolveUserIds(replyChannel);
1843
+ try {
1844
+ const chatId = await this.createGroupChat({
1845
+ name,
1846
+ description,
1847
+ profile,
1848
+ botAppIds,
1849
+ userIds
1850
+ });
1851
+ await this.writeChatBinding(task, chatId, profile, "creating");
1852
+ await this.attachChatIdToTask(task, chatId, profile);
1853
+ await this.writeChatBinding(task, chatId, profile, "ready");
1854
+ logger5.info(`ensureGroupChat[${this.pluginName}]: created group ${chatId} for task #${task.id}`);
1855
+ return { chatId, profile, name };
1856
+ } catch (error) {
1857
+ logger5.error(`ensureGroupChat[${this.pluginName}]: failed for task #${task.id}: ${error}`);
1858
+ return;
1859
+ }
1860
+ }
1861
+ setTaskUpdater(updater) {
1862
+ this.taskUpdater = updater;
1863
+ logger5.debug(`TaskUpdater injected for plugin ${this.name} (sourceId=${this.sourceId})`);
1864
+ }
1865
+ hasTaskUpdater() {
1866
+ return this.taskUpdater !== undefined;
1867
+ }
1868
+ setTaskGetter(getter) {
1869
+ this.taskGetter = getter;
1870
+ logger5.debug(`TaskGetter injected for plugin ${this.name} (sourceId=${this.sourceId})`);
1871
+ }
1872
+ hasTaskGetter() {
1873
+ return this.taskGetter !== undefined;
1874
+ }
1875
+ setSessionComponent(sessionComponent) {
1876
+ this.sessionComponent = sessionComponent;
1877
+ logger5.debug(`SessionComponent injected for plugin ${this.name} (sourceId=${this.sourceId})`);
1878
+ }
1879
+ hasSessionComponent() {
1880
+ return this.sessionComponent !== undefined;
1881
+ }
1882
+ async execute(ctx) {
1883
+ if (process.env.ROY_HOOK_DEBUG === "1") {
1884
+ console.error(`[DEBUG per-task-group-chat.execute] ENTER hookPoint=${ctx.hookPoint} pluginName=${this.pluginName} sourceId=${this.sourceId}`);
1885
+ }
1886
+ if (!this.config.enabled) {
1887
+ if (process.env.ROY_HOOK_DEBUG === "1") {
1888
+ console.error(`[DEBUG per-task-group-chat.execute] → SKIP (config.enabled=false)`);
1889
+ }
1890
+ logger5.debug(`execute: plugin disabled (pluginName=${this.pluginName})`);
1891
+ return;
1892
+ }
1893
+ if (this.pluginName && this.sourceId) {
1894
+ const env = getEnvContextOrEmpty();
1895
+ const key = `${this.sourceId}:${this.pluginName}`;
1896
+ if (process.env.ROY_HOOK_DEBUG === "1") {
1897
+ console.error(`[DEBUG per-task-group-chat.execute] check pluginEnabled key=${key} env.pluginEnabled=${JSON.stringify(env.pluginEnabled)}`);
1898
+ }
1899
+ if (env.pluginEnabled && env.pluginEnabled[key] !== true) {
1900
+ if (process.env.ROY_HOOK_DEBUG === "1") {
1901
+ console.error(`[DEBUG per-task-group-chat.execute] → SKIP (plugin NOT enabled for ${key})`);
1902
+ }
1903
+ logger5.debug(`execute[${this.pluginName}]: plugin NOT enabled (key=${key}, ` + `env.pluginEnabled=${JSON.stringify(env.pluginEnabled)}, ` + `hookPoint=${ctx.hookPoint}). ` + `→ skip. 修复:roy-agent eventsource update <id> 必须包含 PerTaskGroupChatPlugin`);
1904
+ return;
1905
+ }
1906
+ const enabledState = env.pluginEnabled ? env.pluginEnabled[key] === true ? "✅ enabled" : "❌ NOT enabled" : "⚠️ env.pluginEnabled 未设置 (env 没创建 / 插件未注册到 eventsource)";
1907
+ logger5.debug(`execute[${this.pluginName}]: ${enabledState} (key=${key}, ` + `hookPoint=${ctx.hookPoint}, taskUpdaterInjected=${this.hasTaskUpdater()})`);
1908
+ } else {
1909
+ logger5.debug(`execute[${this.pluginName}]: missing pluginName/sourceId metadata ` + `(pluginName=${this.pluginName}, sourceId=${this.sourceId}), ` + `skipping pluginEnabled check. hookPoint=${ctx.hookPoint}`);
1910
+ }
1911
+ if (process.env.ROY_HOOK_DEBUG === "1") {
1912
+ console.error(`[DEBUG per-task-group-chat.execute] → PASSED pluginEnabled check, will dispatch hookPoint=${ctx.hookPoint}`);
1913
+ }
1914
+ switch (ctx.hookPoint) {
1915
+ case TaskHookPoints.AFTER_CREATE:
1916
+ await this.onAfterCreate(ctx);
1917
+ break;
1918
+ case TaskHookPoints.AFTER_UPDATE:
1919
+ await this.onAfterUpdate(ctx);
1920
+ break;
1921
+ case TaskHookPoints.OPERATION_AFTER_CREATE:
1922
+ await this.onAfterOperationCreate(ctx);
1923
+ break;
1924
+ case TaskHookPoints.AFTER_COMPLETE:
1925
+ await this.onAfterComplete(ctx);
1926
+ break;
1927
+ default:
1928
+ logger5.debug(`execute: unhandled hookPoint=${ctx.hookPoint}, skip`);
1929
+ }
1930
+ }
1931
+ async onAfterCreate(ctx) {
1932
+ const data = ctx.data;
1933
+ if (!data?.task) {
1934
+ logger5.debug("onAfterCreate: task is missing, skip");
1935
+ return;
1936
+ }
1937
+ const { task } = data;
1938
+ if (process.env.ROY_HOOK_DEBUG === "1") {
1939
+ const env = getEnvContextOrEmpty();
1940
+ console.error(`[DEBUG per-task-group-chat.onAfterCreate] task=#${task.id} title="${task.title}" parent_task_id=${task.parent_task_id ?? "none"}`);
1941
+ console.error(`[DEBUG per-task-group-chat.onAfterCreate] env.replyChannel=${JSON.stringify(env.replyChannel)}`);
1942
+ console.error(`[DEBUG per-task-group-chat.onAfterCreate] env.messageId=${env.messageId}`);
1943
+ console.error(`[DEBUG per-task-group-chat.onAfterCreate] env.sourceId=${env.sourceId}`);
1944
+ }
1945
+ logger5.debug(`onAfterCreate[${this.pluginName}]: enter task=${task.id} "${task.title}" ` + `parent_task_id=${task.parent_task_id ?? "none"} ` + `requireReplyChannel=${this.config.requireReplyChannel} ` + `disableSubTaskSkip=${this.config.disableSubTaskSkip} ` + `sendWelcomeMessage=${this.config.sendWelcomeMessage}`);
1946
+ const replyChannel = this.getReplyChannelFromCtx(ctx);
1947
+ if (!replyChannel?.chatId && this.config.requireReplyChannel) {
1948
+ logger5.debug(`onAfterCreate[${this.pluginName}]: no replyChannel.chatId for task #${task.id}, skip. ` + `修复:eventsource 收到 im.message.receive_v1 事件时 replyChannel 应有值;` + `或在 plugin config 设 requireReplyChannel=false 强制创建群聊(profile 降级到 fallbackProfile/无)`);
1949
+ return;
1950
+ }
1951
+ if (!replyChannel?.chatId && !this.config.requireReplyChannel) {
1952
+ logger5.debug(`onAfterCreate[${this.pluginName}]: no replyChannel but requireReplyChannel=false, ` + `proceeding with profile fallback (fallbackProfile="${this.config.fallbackProfile}")`);
1953
+ }
1954
+ if (this.isSubTask(task) && !this.config.disableSubTaskSkip) {
1955
+ logger5.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task, ` + `skip group chat creation. 关闭方式:config.disableSubTaskSkip=true`);
1956
+ return;
1957
+ }
1958
+ if (this.isSubTask(task) && this.config.disableSubTaskSkip) {
1959
+ logger5.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task but ` + `disableSubTaskSkip=true, proceeding to create group chat`);
1960
+ }
1961
+ try {
1962
+ const created = await this.ensureGroupChat(task, replyChannel);
1963
+ if (!created?.chatId) {
1964
+ logger5.error(`onAfterCreate[${this.pluginName}]: ensureGroupChat 未返回 chatId for task #${task.id}`);
1965
+ if (!this.config.fallbackToMain) {
1966
+ throw new Error(`per-task group chat unavailable for task #${task.id}`);
1967
+ }
1968
+ return;
1969
+ }
1970
+ logger5.info(`onAfterCreate[${this.pluginName}]: task #${task.id} → group ${created.chatId} ` + `(initial card owned by LarkCliTaskNotifyHook)`);
1971
+ } catch (error) {
1972
+ logger5.error(`onAfterCreate[${this.pluginName}]: group chat failed for task #${task.id}: ${error}`);
1973
+ if (!this.config.fallbackToMain)
1974
+ throw error;
1975
+ }
1976
+ }
1977
+ async writeChatBinding(task, chatId, profile, status, errorMessage) {
1978
+ if (!this.sessionComponent) {
1979
+ if (isPluginDebugEnabled()) {
1980
+ logger5.debug(`writeChatBinding[${this.pluginName}]: no sessionComponent injected, ` + `skip chat-task binding for chat ${chatId} (status=${status})`);
1981
+ }
1982
+ return;
1983
+ }
1984
+ try {
1985
+ await this.sessionComponent.upsertChatTaskBinding({
1986
+ chatId,
1987
+ taskId: task.id,
1988
+ profile,
1989
+ status,
1990
+ errorMessage: errorMessage ?? null
1991
+ });
1992
+ } catch (bindErr) {
1993
+ logger5.warn(`writeChatBinding[${this.pluginName}]: failed to write chat-task binding for ` + `chat ${chatId} / task #${task.id} (status=${status}): ${bindErr}. ` + `群消息 handler 将无法自动注入 parentTaskId。`);
1994
+ }
1995
+ }
1996
+ async onAfterUpdate(ctx) {
1997
+ const data = ctx.data;
1998
+ if (!data?.task) {
1999
+ logger5.debug("onAfterUpdate: task is missing, skip");
2000
+ return;
2001
+ }
2002
+ logger5.debug(`onAfterUpdate[${this.pluginName}]: task #${data.task.id} 卡片由 ` + `LarkCliTaskNotifyHook 负责(本插件不再 patch),skip`);
2003
+ }
2004
+ async onAfterOperationCreate(ctx) {
2005
+ const data = ctx.data;
2006
+ if (!data?.operation) {
2007
+ logger5.debug("onAfterOperationCreate: operation is missing, skip");
2008
+ return;
2009
+ }
2010
+ logger5.debug(`onAfterOperationCreate[${this.pluginName}]: operation #${data.operation.id} ` + `卡片由 LarkCliTaskNotifyHook 负责,skip`);
2011
+ }
2012
+ async onAfterComplete(ctx) {
2013
+ const data = ctx.data;
2014
+ if (!data?.task) {
2015
+ logger5.debug("onAfterComplete: task is missing, skip");
2016
+ return;
2017
+ }
2018
+ logger5.debug(`onAfterComplete[${this.pluginName}]: task #${data.task.id} 终态卡片由 ` + `LarkCliTaskNotifyHook 负责,skip`);
2019
+ }
2020
+ createGroupChat(opts) {
2021
+ const args = this.buildCreateGroupChatArgs(opts);
2022
+ if (isPluginDebugEnabled()) {
2023
+ logger5.debug(`Creating group chat: lark-cli ${args.join(" ")}`);
2024
+ }
2025
+ return new Promise((resolve, reject) => {
2026
+ const proc = spawn2("lark-cli", args, {
2027
+ shell: false,
2028
+ stdio: ["pipe", "pipe", "pipe"]
2029
+ });
2030
+ let stdout = "";
2031
+ let stderr = "";
2032
+ proc.stdout?.on("data", (data) => {
2033
+ stdout += data.toString();
2034
+ });
2035
+ proc.stderr?.on("data", (data) => {
2036
+ stderr += data.toString();
2037
+ });
2038
+ proc.on("close", (code) => {
2039
+ if (code !== 0) {
2040
+ reject(new Error(`lark-cli +chat-create exited with code ${code}: ${stderr}`));
2041
+ return;
2042
+ }
2043
+ const chatId = this.parseChatId(stdout);
2044
+ if (!chatId) {
2045
+ reject(new Error(`lark-cli +chat-create did not return chat_id: ${stdout}`));
2046
+ return;
2047
+ }
2048
+ resolve(chatId);
2049
+ });
2050
+ proc.on("error", (error) => {
2051
+ reject(error);
2052
+ });
2053
+ });
2054
+ }
2055
+ buildCreateGroupChatArgs(opts) {
2056
+ const nameTrimmed = this.truncate(opts.name, 60);
2057
+ const descriptionTrimmed = this.truncate(opts.description, 100);
2058
+ const args = [
2059
+ "im",
2060
+ "+chat-create",
2061
+ "--name",
2062
+ nameTrimmed,
2063
+ "--description",
2064
+ descriptionTrimmed
2065
+ ];
2066
+ if (opts.profile) {
2067
+ args.push("--as", "bot");
2068
+ }
2069
+ if (opts.botAppIds.length > 0) {
2070
+ args.push("--bots", opts.botAppIds.join(","));
2071
+ args.push("--set-bot-manager");
2072
+ }
2073
+ if (opts.userIds && opts.userIds.length > 0) {
2074
+ args.push("--users", opts.userIds.join(","));
2075
+ }
2076
+ if (opts.profile) {
2077
+ args.unshift("--profile", opts.profile);
2078
+ }
2079
+ return args;
2080
+ }
2081
+ async attachChatIdToTask(task, chatId, profile) {
2082
+ const contextStr = task.context ?? "{}";
2083
+ let parsed = {};
2084
+ try {
2085
+ const obj = JSON.parse(contextStr);
2086
+ if (obj && typeof obj === "object" && !Array.isArray(obj)) {
2087
+ parsed = obj;
2088
+ }
2089
+ } catch {
2090
+ parsed = {};
2091
+ }
2092
+ parsed[CTX_FIELD_CHAT_ID2] = chatId;
2093
+ parsed[CTX_FIELD_CHAT_PROFILE2] = profile;
2094
+ parsed[CTX_FIELD_CHAT_CREATED_AT] = new Date().toISOString();
2095
+ parsed[CTX_FIELD_CHAT_TASK_ID] = task.id;
2096
+ const newContext = JSON.stringify(parsed);
2097
+ if (!this.taskUpdater) {
2098
+ logger5.warn(`attachChatIdToTask[${this.pluginName}]: ⚠️ no taskUpdater injected, ` + `chat_id ${chatId} NOT persisted to db for task #${task.id}. ` + `后果:下次 AFTER_UPDATE 从 DB 加载 task 时 getTaskChatId() 返回 undefined,` + `所有 update/complete/operation 通知 silently 丢失。` + `修复:event-source-handlers.ts:registerPluginHooks 应自动注入 setTaskUpdater。`);
2099
+ task.context = newContext;
2100
+ return;
2101
+ }
2102
+ try {
2103
+ await this.taskUpdater(task.id, newContext);
2104
+ task.context = newContext;
2105
+ logger5.debug(`attachChatIdToTask[${this.pluginName}]: ✅ persisted chat_id ${chatId} ` + `to task #${task.id} context`);
2106
+ } catch (error) {
2107
+ logger5.error(`Failed to persist chat_id to task #${task.id}: ${error}`);
2108
+ task.context = newContext;
2109
+ }
2110
+ }
2111
+ getReplyChannelFromCtx(ctx) {
2112
+ const metadata = ctx.metadata;
2113
+ const alsEnv = getEnvContextOrEmpty();
2114
+ if (alsEnv.replyChannel?.chatId) {
2115
+ return alsEnv.replyChannel;
2116
+ }
2117
+ if (!metadata)
2118
+ return;
2119
+ const envCtx = metadata.envContext;
2120
+ if (envCtx?.replyChannel?.chatId) {
2121
+ return envCtx.replyChannel;
2122
+ }
2123
+ const direct = metadata.replyChannel;
2124
+ if (direct?.chatId) {
2125
+ return direct;
2126
+ }
2127
+ return;
2128
+ }
2129
+ parseChatIdFromContext(ctxStr) {
2130
+ if (!ctxStr)
2131
+ return;
2132
+ try {
2133
+ const obj = JSON.parse(ctxStr);
2134
+ const id = obj[CTX_FIELD_CHAT_ID2];
2135
+ return typeof id === "string" ? id : undefined;
2136
+ } catch {
2137
+ return;
2138
+ }
2139
+ }
2140
+ parseProfileFromContext(ctxStr) {
2141
+ if (!ctxStr)
2142
+ return;
2143
+ try {
2144
+ const obj = JSON.parse(ctxStr);
2145
+ const profile = obj[CTX_FIELD_CHAT_PROFILE2];
2146
+ return typeof profile === "string" ? profile : undefined;
2147
+ } catch {
2148
+ return;
2149
+ }
2150
+ }
2151
+ getTaskChatId(task) {
2152
+ return this.parseChatIdFromContext(task.context);
2153
+ }
2154
+ getTaskChatProfile(task) {
2155
+ return this.parseProfileFromContext(task.context);
2156
+ }
2157
+ async resolveEffectiveChat(task) {
2158
+ const ownChatId = this.getTaskChatId(task);
2159
+ if (ownChatId) {
2160
+ return { chatId: ownChatId, profile: this.getTaskChatProfile(task) };
2161
+ }
2162
+ if (!this.taskGetter)
2163
+ return {};
2164
+ const seen = new Set;
2165
+ let id = task.parent_task_id;
2166
+ while (typeof id === "number" && id > 0 && !seen.has(id) && seen.size < 8) {
2167
+ seen.add(id);
2168
+ let parent;
2169
+ try {
2170
+ parent = await this.taskGetter(id);
2171
+ } catch {
2172
+ return {};
2173
+ }
2174
+ if (!parent)
2175
+ return {};
2176
+ const chatId = this.parseChatIdFromContext(parent.context);
2177
+ if (chatId) {
2178
+ return { chatId, profile: this.parseProfileFromContext(parent.context) };
2179
+ }
2180
+ id = parent.parent_task_id ?? undefined;
2181
+ }
2182
+ if (this.config.resolveToTriggerChatFallback) {
2183
+ const store = getSharedEventStore();
2184
+ const triggerEventId = getEnvContextOrEmpty()?.envEvent?.metadata?.triggerEventId;
2185
+ if (store && typeof triggerEventId === "string" && triggerEventId.length > 0) {
2186
+ try {
2187
+ const triggerEvent = await store.getEventById(triggerEventId);
2188
+ const fallbackChatId = triggerEvent?.metadata?.chatId ?? triggerEvent?.metadata?.replyChannel?.chatId;
2189
+ if (fallbackChatId) {
2190
+ return {
2191
+ chatId: fallbackChatId,
2192
+ profile: triggerEvent?.metadata?.profile ?? triggerEvent?.metadata?.replyChannel?.profile
2193
+ };
2194
+ }
2195
+ } catch {}
2196
+ }
2197
+ }
2198
+ return {};
2199
+ }
2200
+ resolveBotAppIds(replyChannel) {
2201
+ if (this.config.botAppIds.length > 0) {
2202
+ logger5.debug(`resolveBotAppIds[${this.pluginName}]: using config.botAppIds=${JSON.stringify(this.config.botAppIds)}`);
2203
+ return this.config.botAppIds;
2204
+ }
2205
+ const appId = replyChannel ? replyChannel.appId : undefined;
2206
+ if (appId) {
2207
+ logger5.debug(`resolveBotAppIds[${this.pluginName}]: using replyChannel.appId=${appId} (auto-detected from event header.app_id)`);
2208
+ return [appId];
2209
+ }
2210
+ logger5.warn(`resolveBotAppIds[${this.pluginName}]: ⚠️ no botAppIds found, creating group WITHOUT --bots. ` + `Bot will NOT be invited automatically. ` + `可能原因:(1) eventsource 没有收到 im.message.receive_v1 事件(replyChannel.appId 缺失);` + `(2) 配置了 requireReplyChannel=false 模式;` + `(3) buildReplyChannel 没透传 appId(应在 v1.10+ 修复)。` + `修复:手动设置 config.botAppIds: ["cli_xxx"] 或确认 eventsource 处理了 lark event。`);
2211
+ return [];
2212
+ }
2213
+ resolveUserIds(replyChannel) {
2214
+ const ids = new Set;
2215
+ if (this.config.defaultInviteUserIds && this.config.defaultInviteUserIds.length > 0) {
2216
+ for (const id of this.config.defaultInviteUserIds) {
2217
+ if (id && id !== "sender" && id.startsWith("ou_")) {
2218
+ ids.add(id);
2219
+ } else if (id && id !== "sender") {
2220
+ logger5.debug(`resolveUserIds[${this.pluginName}]: skip non-open_id in defaultInviteUserIds: "${id}"`);
2221
+ }
2222
+ }
2223
+ }
2224
+ if (this.config.inviteUserIds && this.config.inviteUserIds.length > 0) {
2225
+ for (const id of this.config.inviteUserIds) {
2226
+ if (id !== "sender" && id)
2227
+ ids.add(id);
2228
+ }
2229
+ }
2230
+ if (this.config.inviteUserIds?.includes("sender")) {
2231
+ const senderId = replyChannel?.userId || replyChannel?.params?.userId || getEnvContextOrEmpty().userId;
2232
+ if (senderId && senderId.startsWith("ou_")) {
2233
+ ids.add(senderId);
2234
+ }
2235
+ }
2236
+ if (ids.size === 0 && this.config.inviteUserIds?.includes("sender")) {
2237
+ const alsUserId = getEnvContextOrEmpty().userId;
2238
+ if (alsUserId && alsUserId.startsWith("ou_")) {
2239
+ ids.add(alsUserId);
2240
+ }
2241
+ }
2242
+ if (isPluginDebugEnabled()) {
2243
+ logger5.debug(`resolveUserIds[${this.pluginName}]: ` + `defaultInviteUserIds=${JSON.stringify(this.config.defaultInviteUserIds)} ` + `inviteUserIds=${JSON.stringify(this.config.inviteUserIds)} ` + `replyChannel.userId=${replyChannel?.userId ?? "(none)"} ` + `→ final userIds=${JSON.stringify(Array.from(ids))}`);
2244
+ }
2245
+ return Array.from(ids);
2246
+ }
2247
+ isSubTask(task) {
2248
+ const parentId = task.parent_task_id;
2249
+ return typeof parentId === "number" && parentId > 0;
2250
+ }
2251
+ hasMeaningfulChange(changes) {
2252
+ if (!changes || Object.keys(changes).length === 0)
2253
+ return false;
2254
+ const keys = Object.keys(changes);
2255
+ return keys.some((k) => [
2256
+ "status",
2257
+ "progress",
2258
+ "priority",
2259
+ "title",
2260
+ "description",
2261
+ "current_status",
2262
+ "tags",
2263
+ "goals_and_expected_deliverables"
2264
+ ].includes(k));
2265
+ }
2266
+ shouldThrottleOperation(operation) {
2267
+ if (this.config.operationThrottleMs <= 0)
2268
+ return false;
2269
+ const taskId = operation.taskId ?? operation.task_id ?? 0;
2270
+ if (taskId === 0)
2271
+ return false;
2272
+ const now = Date.now();
2273
+ const last = this.lastNotifyTimestamps.get(taskId) ?? 0;
2274
+ if (now - last < this.config.operationThrottleMs) {
2275
+ return true;
2276
+ }
2277
+ this.lastNotifyTimestamps.set(taskId, now);
2278
+ return false;
2279
+ }
2280
+ renderTemplate(template, task) {
2281
+ const id = String(task.id);
2282
+ const title = this.truncate(task.title ?? "", 60);
2283
+ const projectPath = task.project_path ?? "";
2284
+ return template.replace(/#\{id\}/g, id).replace(/(^|[^{]){id\}/g, `$1${id}`).replace(/#\{title\}/g, title).replace(/(^|[^{]){title\}/g, `$1${title}`).replace(/#\{project_path\}/g, projectPath).replace(/(^|[^{]){project_path\}/g, `$1${projectPath}`);
2285
+ }
2286
+ truncate(text, maxLen) {
2287
+ if (!text)
2288
+ return "";
2289
+ if (text.length <= maxLen)
2290
+ return text;
2291
+ return text.substring(0, Math.max(0, maxLen - 3)) + "...";
2292
+ }
2293
+ parseChatId(stdout) {
2294
+ if (!stdout)
2295
+ return;
2296
+ try {
2297
+ const obj = JSON.parse(stdout.trim());
2298
+ const data = obj.data;
2299
+ if (data?.chat_id)
2300
+ return data.chat_id;
2301
+ if (data?.chatId)
2302
+ return data.chatId;
2303
+ } catch {}
2304
+ const match = stdout.match(/oc_[a-zA-Z0-9]+/);
2305
+ return match ? match[0] : undefined;
2306
+ }
2307
+ }
2308
+ __legacyDecorateClassTS([
2309
+ TracedAs("per-task-group-chat.ensureGroupChat", {
2310
+ recordParams: true,
2311
+ recordResult: true,
2312
+ flattenParamFilter: true,
2313
+ paramFilter: (args) => ({ taskId: args[0]?.id }),
2314
+ resultFilter: (r) => ({ chatId: r?.chatId })
2315
+ })
2316
+ ], PerTaskGroupChatPlugin.prototype, "ensureGroupChat", null);
2317
+ __legacyDecorateClassTS([
2318
+ TracedAs("per-task-group-chat.execute")
2319
+ ], PerTaskGroupChatPlugin.prototype, "execute", null);
2320
+ __legacyDecorateClassTS([
2321
+ TracedAs("per-task-group-chat.onAfterCreate", { recordParams: false })
2322
+ ], PerTaskGroupChatPlugin.prototype, "onAfterCreate", null);
2323
+ __legacyDecorateClassTS([
2324
+ TracedAs("per-task-group-chat.writeChatBinding", {
2325
+ recordParams: true,
2326
+ recordResult: false,
2327
+ flattenParamFilter: true,
2328
+ paramFilter: (args) => ({
2329
+ taskId: args[0]?.id,
2330
+ chatId: args[1],
2331
+ status: args[3]
2332
+ })
2333
+ })
2334
+ ], PerTaskGroupChatPlugin.prototype, "writeChatBinding", null);
2335
+ __legacyDecorateClassTS([
2336
+ TracedAs("per-task-group-chat.onAfterUpdate", { recordParams: false })
2337
+ ], PerTaskGroupChatPlugin.prototype, "onAfterUpdate", null);
2338
+ __legacyDecorateClassTS([
2339
+ TracedAs("per-task-group-chat.onAfterOperationCreate", { recordParams: false })
2340
+ ], PerTaskGroupChatPlugin.prototype, "onAfterOperationCreate", null);
2341
+ __legacyDecorateClassTS([
2342
+ TracedAs("per-task-group-chat.onAfterComplete", { recordParams: false })
2343
+ ], PerTaskGroupChatPlugin.prototype, "onAfterComplete", null);
2344
+ __legacyDecorateClassTS([
2345
+ TracedAs("per-task-group-chat.createGroupChat", {
2346
+ recordParams: true,
2347
+ recordResult: true,
2348
+ flattenParamFilter: true,
2349
+ paramFilter: (args) => ({
2350
+ profile: args[0]?.profile,
2351
+ botAppIdsCount: args[0]?.botAppIds?.length,
2352
+ userIdsCount: args[0]?.userIds?.length ?? 0,
2353
+ userIds: args[0]?.userIds,
2354
+ name: args[0]?.name,
2355
+ description: args[0]?.description
2356
+ }),
2357
+ resultFilter: (r) => ({
2358
+ chatId: r,
2359
+ ok: !!r
2360
+ })
2361
+ })
2362
+ ], PerTaskGroupChatPlugin.prototype, "createGroupChat", null);
2363
+ __legacyDecorateClassTS([
2364
+ TracedAs("per-task-group-chat.buildCreateGroupChatArgs", {
2365
+ recordParams: false,
2366
+ recordResult: true,
2367
+ flattenParamFilter: true,
2368
+ paramFilter: (args) => ({
2369
+ hasProfile: !!args[0]?.profile,
2370
+ botAppIdsCount: args[0]?.botAppIds?.length,
2371
+ hasUsers: !!args[0]?.userIds?.length
2372
+ }),
2373
+ resultFilter: (r) => ({
2374
+ argsSummary: r.join(" "),
2375
+ hasAsBot: r.includes("--as") && r[r.indexOf("--as") + 1] === "bot",
2376
+ hasBots: r.includes("--bots"),
2377
+ hasUsers: r.includes("--users")
2378
+ })
2379
+ })
2380
+ ], PerTaskGroupChatPlugin.prototype, "buildCreateGroupChatArgs", null);
2381
+ __legacyDecorateClassTS([
2382
+ TracedAs("per-task-group-chat.attachChatIdToTask", {
2383
+ recordParams: false,
2384
+ flattenParamFilter: true,
2385
+ paramFilter: (args) => ({
2386
+ taskId: args[0]?.id,
2387
+ chatId: args[1],
2388
+ profile: args[2],
2389
+ taskUpdaterInjected: !!args[0] && !!args[0].context
2390
+ })
2391
+ })
2392
+ ], PerTaskGroupChatPlugin.prototype, "attachChatIdToTask", null);
2393
+ function createPerTaskGroupChatPlugin(config = {}) {
2394
+ return new PerTaskGroupChatPlugin(config);
2395
+ }
2396
+ export { createAutoTaskPlugin, WORKFLOW_EXTRACTOR_PROMPT, formatExtractorInput, formatExtractorInputFromDescription, parseExtractorOutput, TaskTagPlugin, mergeChatOwnership, NotifyType, LarkCliTaskNotifyHook, createLarkCliTaskNotifyHook, PerTaskGroupChatPlugin, createPerTaskGroupChatPlugin };