@ai-setting/roy-agent-core 1.7.30 → 1.7.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/env/event-source/index.js +2 -2
- package/dist/env/index.js +4 -4
- package/dist/env/task/index.js +1 -1
- package/dist/env/task/plugins/index.js +5 -1
- package/dist/index.js +4 -4
- package/dist/shared/@ai-setting/{roy-agent-core-91rzfw8d.js → roy-agent-core-4csc7ars.js} +126 -31
- package/dist/shared/@ai-setting/{roy-agent-core-62mfe44h.js → roy-agent-core-4t6m600m.js} +1 -1
- package/dist/shared/@ai-setting/roy-agent-core-9z6gcy77.js +1520 -0
- package/dist/shared/@ai-setting/{roy-agent-core-pp140n9r.js → roy-agent-core-m09a4xwh.js} +2 -1
- package/package.json +1 -1
- package/dist/shared/@ai-setting/roy-agent-core-h4h6n9vk.js +0 -801
|
@@ -0,0 +1,1520 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TaskHookPoints
|
|
3
|
+
} from "./roy-agent-core-1pg0fepg.js";
|
|
4
|
+
import {
|
|
5
|
+
WORKFLOW_EXTRACT_PROMPT
|
|
6
|
+
} from "./roy-agent-core-skqt40yy.js";
|
|
7
|
+
import {
|
|
8
|
+
validateWorkflowDefinition
|
|
9
|
+
} from "./roy-agent-core-1df0grva.js";
|
|
10
|
+
import {
|
|
11
|
+
enqueueToAddFakeUserMessage
|
|
12
|
+
} from "./roy-agent-core-jbc1t62x.js";
|
|
13
|
+
import {
|
|
14
|
+
BasePlugin
|
|
15
|
+
} from "./roy-agent-core-az13yzmc.js";
|
|
16
|
+
import {
|
|
17
|
+
init_context
|
|
18
|
+
} from "./roy-agent-core-m2pxg5d8.js";
|
|
19
|
+
import {
|
|
20
|
+
getEnvContextOrEmpty,
|
|
21
|
+
getEventFromEnvContext,
|
|
22
|
+
getLarkMessageId,
|
|
23
|
+
isLarkEvent
|
|
24
|
+
} from "./roy-agent-core-7p6rny5m.js";
|
|
25
|
+
import {
|
|
26
|
+
TracedAs,
|
|
27
|
+
init_decorator,
|
|
28
|
+
wrapFunction
|
|
29
|
+
} from "./roy-agent-core-38rtrpk3.js";
|
|
30
|
+
import {
|
|
31
|
+
createLogger,
|
|
32
|
+
init_logger
|
|
33
|
+
} from "./roy-agent-core-e6dahpp2.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-89zsa89z.js");
|
|
299
|
+
this.ensureDataDir();
|
|
300
|
+
const db = sqlite.getDatabase();
|
|
301
|
+
sqlite.initializeTables();
|
|
302
|
+
const { WorkflowRepository } = await import("./roy-agent-core-pz8jvvtn.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
|
+
import { spawn } from "child_process";
|
|
376
|
+
var logger3 = createLogger("lark-cli:task-notify");
|
|
377
|
+
function isTaskNotifyDebugEnabled() {
|
|
378
|
+
return process.env.ROY_TASK_NOTIFY_DEBUG === "1" || process.env.ROY_TRACE_DEBUG === "1";
|
|
379
|
+
}
|
|
380
|
+
function extractLarkFields(env) {
|
|
381
|
+
const event = getEventFromEnvContext(env);
|
|
382
|
+
if (!isLarkEvent(event)) {
|
|
383
|
+
return { isLark: false };
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
isLark: true,
|
|
387
|
+
source: event.source,
|
|
388
|
+
messageId: getLarkMessageId(event) ?? env.replyChannel?.messageId,
|
|
389
|
+
chatId: env.replyChannel?.chatId,
|
|
390
|
+
userId: env.replyChannel?.userId
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
var NotifyType;
|
|
394
|
+
((NotifyType2) => {
|
|
395
|
+
NotifyType2["TASK_CREATED"] = "task_created";
|
|
396
|
+
NotifyType2["TASK_COMPLETED"] = "task_completed";
|
|
397
|
+
NotifyType2["TASK_UPDATED"] = "task_updated";
|
|
398
|
+
NotifyType2["TASK_FAILED"] = "task_failed";
|
|
399
|
+
NotifyType2["OPERATION_CREATED"] = "operation_created";
|
|
400
|
+
})(NotifyType ||= {});
|
|
401
|
+
|
|
402
|
+
class LarkCliTaskNotifyHook {
|
|
403
|
+
name;
|
|
404
|
+
priority;
|
|
405
|
+
description;
|
|
406
|
+
pluginName;
|
|
407
|
+
sourceId;
|
|
408
|
+
config;
|
|
409
|
+
lastNotifyTimestamps = new Map;
|
|
410
|
+
constructor(config = {}) {
|
|
411
|
+
this.name = config.pluginName || "LarkCliTaskNotifyHook";
|
|
412
|
+
this.priority = config.priority ?? 5;
|
|
413
|
+
this.description = "LarkCli 任务通知 Hook";
|
|
414
|
+
this.pluginName = config.pluginName;
|
|
415
|
+
this.sourceId = config.sourceId;
|
|
416
|
+
this.config = {
|
|
417
|
+
enabled: true,
|
|
418
|
+
priority: 5,
|
|
419
|
+
notifyTypes: [
|
|
420
|
+
"task_created" /* TASK_CREATED */,
|
|
421
|
+
"task_updated" /* TASK_UPDATED */,
|
|
422
|
+
"task_completed" /* TASK_COMPLETED */,
|
|
423
|
+
"operation_created" /* OPERATION_CREATED */
|
|
424
|
+
],
|
|
425
|
+
...config
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
async execute(ctx) {
|
|
429
|
+
if (!this.config.enabled) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const env = getEnvContextOrEmpty();
|
|
433
|
+
if (this.pluginName && this.sourceId) {
|
|
434
|
+
const key = `${this.sourceId}:${this.pluginName}`;
|
|
435
|
+
if (env.pluginEnabled && env.pluginEnabled[key] !== true) {
|
|
436
|
+
logger3.debug(`Plugin not enabled for source: ${key}`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
switch (ctx.hookPoint) {
|
|
441
|
+
case TaskHookPoints.AFTER_CREATE:
|
|
442
|
+
await this.onAfterCreate(ctx);
|
|
443
|
+
break;
|
|
444
|
+
case TaskHookPoints.AFTER_UPDATE:
|
|
445
|
+
await this.onAfterUpdate(ctx);
|
|
446
|
+
break;
|
|
447
|
+
case TaskHookPoints.OPERATION_AFTER_CREATE:
|
|
448
|
+
await this.onAfterOperationCreate(ctx);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async onAfterCreate(ctx) {
|
|
453
|
+
const { task } = ctx.data;
|
|
454
|
+
if (!this.shouldNotify("task_created" /* TASK_CREATED */))
|
|
455
|
+
return;
|
|
456
|
+
await this.sendNotify({
|
|
457
|
+
type: "task_created" /* TASK_CREATED */,
|
|
458
|
+
title: `\uD83D\uDCDD 任务已创建`,
|
|
459
|
+
content: this.formatTaskCreated(task)
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async onAfterUpdate(ctx) {
|
|
463
|
+
const data = ctx.data;
|
|
464
|
+
if (!data) {
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
const { task, changes } = data;
|
|
468
|
+
if (!task) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
let notifyType;
|
|
472
|
+
let notifyChanges = changes || {};
|
|
473
|
+
if (changes?.status === "completed" || changes?.progress === 100) {
|
|
474
|
+
notifyType = "task_completed" /* TASK_COMPLETED */;
|
|
475
|
+
} else if (changes?.status) {
|
|
476
|
+
notifyType = "task_updated" /* TASK_UPDATED */;
|
|
477
|
+
} else if (task.status === "completed" || task.progress === 100) {
|
|
478
|
+
notifyType = "task_completed" /* TASK_COMPLETED */;
|
|
479
|
+
notifyChanges = { status: task.status, progress: task.progress };
|
|
480
|
+
} else if (changes && Object.keys(changes).length > 0) {
|
|
481
|
+
notifyType = "task_updated" /* TASK_UPDATED */;
|
|
482
|
+
} else {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!this.shouldNotify(notifyType)) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
await this.sendNotify({
|
|
489
|
+
type: notifyType,
|
|
490
|
+
title: this.getNotifyTitle(notifyType),
|
|
491
|
+
content: this.formatTaskUpdate(task, notifyChanges),
|
|
492
|
+
_hookContext: ctx
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
async onAfterOperationCreate(ctx) {
|
|
496
|
+
const { operation } = ctx.data;
|
|
497
|
+
if (!operation) {
|
|
498
|
+
logger3.debug("onAfterOperationCreate: operation is missing, skip");
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (!this.shouldNotify("operation_created" /* OPERATION_CREATED */)) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (this.shouldThrottleOperation(operation)) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
await this.sendNotify({
|
|
508
|
+
type: "operation_created" /* OPERATION_CREATED */,
|
|
509
|
+
title: this.getNotifyTitle("operation_created" /* OPERATION_CREATED */),
|
|
510
|
+
content: await this.formatOperationCreated(operation),
|
|
511
|
+
_hookContext: ctx
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async sendNotify(options) {
|
|
515
|
+
const { type, title, content, _hookContext } = options;
|
|
516
|
+
const env = getEnvContextOrEmpty();
|
|
517
|
+
let replyChannel = env.replyChannel;
|
|
518
|
+
const envContextFromMetadata = env.metadata?.envContext;
|
|
519
|
+
if (!replyChannel && envContextFromMetadata) {
|
|
520
|
+
replyChannel = envContextFromMetadata.replyChannel;
|
|
521
|
+
}
|
|
522
|
+
if (!replyChannel && _hookContext?.metadata?.envContext) {
|
|
523
|
+
replyChannel = _hookContext.metadata.envContext.replyChannel;
|
|
524
|
+
}
|
|
525
|
+
const larkFields = extractLarkFields(env);
|
|
526
|
+
if (!larkFields.isLark && env.event) {
|
|
527
|
+
logger3.debug(`Skipping notification for non-lark event source: ${larkFields.source}`);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const chatId = replyChannel?.chatId;
|
|
531
|
+
const messageId = replyChannel?.messageId;
|
|
532
|
+
if (!chatId && !messageId) {
|
|
533
|
+
logger3.debug(`No replyChannel available for ${type}`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
const message = `${title}
|
|
537
|
+
|
|
538
|
+
${content}`;
|
|
539
|
+
try {
|
|
540
|
+
if (isTaskNotifyDebugEnabled()) {
|
|
541
|
+
logger3.debug(`Sending message - messageId: ${messageId}`);
|
|
542
|
+
}
|
|
543
|
+
const profile = replyChannel?.profile;
|
|
544
|
+
await this.sendLarkMessage({
|
|
545
|
+
chatId,
|
|
546
|
+
messageId,
|
|
547
|
+
content: message,
|
|
548
|
+
profile
|
|
549
|
+
});
|
|
550
|
+
logger3.info(`Notification sent: ${type}`);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
logger3.error(`Failed to send notification: ${error}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
sendLarkMessage(options) {
|
|
556
|
+
return new Promise((resolve, reject) => {
|
|
557
|
+
const { chatId, messageId, content, profile } = options;
|
|
558
|
+
let args;
|
|
559
|
+
if (messageId) {
|
|
560
|
+
args = [
|
|
561
|
+
"im",
|
|
562
|
+
"+messages-reply",
|
|
563
|
+
"--message-id",
|
|
564
|
+
messageId,
|
|
565
|
+
"--markdown",
|
|
566
|
+
content,
|
|
567
|
+
"--as",
|
|
568
|
+
"bot"
|
|
569
|
+
];
|
|
570
|
+
} else {
|
|
571
|
+
args = [
|
|
572
|
+
"im",
|
|
573
|
+
"+messages-send",
|
|
574
|
+
"--chat-id",
|
|
575
|
+
chatId,
|
|
576
|
+
"--markdown",
|
|
577
|
+
content
|
|
578
|
+
];
|
|
579
|
+
}
|
|
580
|
+
if (profile) {
|
|
581
|
+
args.unshift("--profile", profile);
|
|
582
|
+
}
|
|
583
|
+
const proc = spawn("lark-cli", args, {
|
|
584
|
+
shell: false,
|
|
585
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
586
|
+
});
|
|
587
|
+
let stdout = "";
|
|
588
|
+
let stderr = "";
|
|
589
|
+
proc.stdout?.on("data", (data) => {
|
|
590
|
+
stdout += data.toString();
|
|
591
|
+
});
|
|
592
|
+
proc.stderr?.on("data", (data) => {
|
|
593
|
+
stderr += data.toString();
|
|
594
|
+
});
|
|
595
|
+
proc.on("close", (code) => {
|
|
596
|
+
if (code === 0) {
|
|
597
|
+
logger3.debug(`lark-cli output: ${stdout}`);
|
|
598
|
+
resolve();
|
|
599
|
+
} else {
|
|
600
|
+
reject(new Error(`lark-cli exited with code ${code}: ${stderr}`));
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
proc.on("error", (error) => {
|
|
604
|
+
reject(error);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
truncate(text, maxLen) {
|
|
609
|
+
if (text.length <= maxLen)
|
|
610
|
+
return text;
|
|
611
|
+
return text.substring(0, maxLen) + "...";
|
|
612
|
+
}
|
|
613
|
+
appendSpatiotemporalContextLines(lines, task, options = {}) {
|
|
614
|
+
if (task?.project_path) {
|
|
615
|
+
lines.push(`- \uD83D\uDCC2 项目路径: \`${task.project_path}\``);
|
|
616
|
+
}
|
|
617
|
+
const ctxStr = task?.context;
|
|
618
|
+
let parsedCtx = null;
|
|
619
|
+
if (typeof ctxStr === "string" && ctxStr.length > 0) {
|
|
620
|
+
try {
|
|
621
|
+
const obj = JSON.parse(ctxStr);
|
|
622
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
623
|
+
parsedCtx = obj;
|
|
624
|
+
}
|
|
625
|
+
} catch {}
|
|
626
|
+
}
|
|
627
|
+
if (!parsedCtx) {
|
|
628
|
+
if (options.appendFullContext && typeof ctxStr === "string") {
|
|
629
|
+
lines.push(`- \uD83D\uDCCB 完整 context: \`${this.truncate(ctxStr, 200)}\``);
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (parsedCtx.worktree_path && typeof parsedCtx.worktree_path === "string") {
|
|
634
|
+
lines.push(`- \uD83C\uDF3F Worktree: \`${parsedCtx.worktree_path}\``);
|
|
635
|
+
}
|
|
636
|
+
if (parsedCtx.created_at && typeof parsedCtx.created_at === "string") {
|
|
637
|
+
lines.push(`- ⏰ 时间上下文: ${parsedCtx.created_at}`);
|
|
638
|
+
}
|
|
639
|
+
if (parsedCtx.prior_context && typeof parsedCtx.prior_context === "object") {
|
|
640
|
+
const priorStr = this.formatPriorContext(parsedCtx.prior_context);
|
|
641
|
+
if (priorStr) {
|
|
642
|
+
lines.push(`- \uD83D\uDCDC 背景: ${priorStr}`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (options.appendFullContext) {
|
|
646
|
+
lines.push(`- \uD83D\uDCCB 完整 context: \`${this.truncate(JSON.stringify(parsedCtx), 300)}\``);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
formatPriorContext(prior) {
|
|
650
|
+
if (!prior || typeof prior !== "object")
|
|
651
|
+
return "";
|
|
652
|
+
const entries = [];
|
|
653
|
+
for (const [k, v] of Object.entries(prior)) {
|
|
654
|
+
if (v === null || v === undefined)
|
|
655
|
+
continue;
|
|
656
|
+
const s = typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
657
|
+
if (s === "" || s === "undefined")
|
|
658
|
+
continue;
|
|
659
|
+
entries.push(`${k}=${s}`);
|
|
660
|
+
}
|
|
661
|
+
return entries.join(", ");
|
|
662
|
+
}
|
|
663
|
+
shouldNotify(type) {
|
|
664
|
+
const { notifyTypes } = this.config;
|
|
665
|
+
if (notifyTypes && notifyTypes.length === 0)
|
|
666
|
+
return true;
|
|
667
|
+
if (notifyTypes)
|
|
668
|
+
return notifyTypes.includes(type);
|
|
669
|
+
return type === "task_completed" /* TASK_COMPLETED */;
|
|
670
|
+
}
|
|
671
|
+
getNotifyTitle(type) {
|
|
672
|
+
const template = this.config.templates?.[type];
|
|
673
|
+
if (template)
|
|
674
|
+
return template;
|
|
675
|
+
switch (type) {
|
|
676
|
+
case "task_created" /* TASK_CREATED */:
|
|
677
|
+
return "\uD83D\uDCDD 任务已创建";
|
|
678
|
+
case "task_completed" /* TASK_COMPLETED */:
|
|
679
|
+
return "✅ 任务已完成";
|
|
680
|
+
case "task_updated" /* TASK_UPDATED */:
|
|
681
|
+
return "\uD83D\uDCCB 任务已更新";
|
|
682
|
+
case "task_failed" /* TASK_FAILED */:
|
|
683
|
+
return "❌ 任务执行失败";
|
|
684
|
+
case "operation_created" /* OPERATION_CREATED */:
|
|
685
|
+
return "\uD83D\uDCCC 操作记录";
|
|
686
|
+
default:
|
|
687
|
+
return "\uD83D\uDCEC 任务通知";
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
formatTaskCreated(task) {
|
|
691
|
+
const lines = [];
|
|
692
|
+
if (task.title) {
|
|
693
|
+
lines.push(`- **标题**: ${task.title}`);
|
|
694
|
+
}
|
|
695
|
+
if (task.description) {
|
|
696
|
+
lines.push(`- **描述**: ${this.truncate(task.description, 100)}`);
|
|
697
|
+
}
|
|
698
|
+
if (task.priority) {
|
|
699
|
+
lines.push(`- **优先级**: ${task.priority}`);
|
|
700
|
+
}
|
|
701
|
+
this.appendSpatiotemporalContextLines(lines, task);
|
|
702
|
+
return lines.join(`
|
|
703
|
+
`);
|
|
704
|
+
}
|
|
705
|
+
formatTaskUpdate(task, changes) {
|
|
706
|
+
const lines = [];
|
|
707
|
+
if (task.title) {
|
|
708
|
+
lines.push(`- **标题**: ${task.title}`);
|
|
709
|
+
}
|
|
710
|
+
lines.push(`- **ID**: ${task.id}`);
|
|
711
|
+
if (changes?.status) {
|
|
712
|
+
lines.push(`- **状态**: ${changes.status}`);
|
|
713
|
+
}
|
|
714
|
+
if (changes?.progress !== undefined) {
|
|
715
|
+
lines.push(`- **进度**: ${changes.progress}%`);
|
|
716
|
+
}
|
|
717
|
+
if (changes?.current_status) {
|
|
718
|
+
lines.push(`- **当前状态**: ${changes.current_status}`);
|
|
719
|
+
}
|
|
720
|
+
this.appendSpatiotemporalContextLines(lines, task);
|
|
721
|
+
const changeKeys = Object.keys(changes || {}).filter((k) => !["status", "progress", "current_status"].includes(k));
|
|
722
|
+
if (changeKeys.length > 0) {
|
|
723
|
+
lines.push(`- **变更**: ${changeKeys.join(", ")}`);
|
|
724
|
+
}
|
|
725
|
+
return lines.join(`
|
|
726
|
+
`);
|
|
727
|
+
}
|
|
728
|
+
shouldThrottleOperation(operation) {
|
|
729
|
+
const throttleMs = this.config.operationThrottleMs ?? 5000;
|
|
730
|
+
if (throttleMs <= 0) {
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
const key = `op:${operation.taskId}`;
|
|
734
|
+
const now = Date.now();
|
|
735
|
+
const lastTime = this.lastNotifyTimestamps.get(key);
|
|
736
|
+
if (lastTime && now - lastTime < throttleMs) {
|
|
737
|
+
logger3.debug(`[throttle] Skipping OPERATION_CREATED notify for task #${operation.taskId}, last notified ${now - lastTime}ms ago`);
|
|
738
|
+
return true;
|
|
739
|
+
}
|
|
740
|
+
this.lastNotifyTimestamps.set(key, now);
|
|
741
|
+
return false;
|
|
742
|
+
}
|
|
743
|
+
async formatOperationCreated(operation, task) {
|
|
744
|
+
const resolvedTask = task ?? await this.resolveTaskForOperation(operation);
|
|
745
|
+
const lines = [];
|
|
746
|
+
if (resolvedTask?.title) {
|
|
747
|
+
lines.push(`- **任务**: ${resolvedTask.title}`);
|
|
748
|
+
}
|
|
749
|
+
if (operation.taskId !== undefined && operation.taskId !== null) {
|
|
750
|
+
lines.push(`- **任务 ID**: #${operation.taskId}`);
|
|
751
|
+
}
|
|
752
|
+
if (operation.milestoneType) {
|
|
753
|
+
lines.push(`- **里程碑类型**: ${operation.milestoneType}`);
|
|
754
|
+
}
|
|
755
|
+
if (operation.milestoneTitle) {
|
|
756
|
+
lines.push(`- **标题**: ${operation.milestoneTitle}`);
|
|
757
|
+
}
|
|
758
|
+
if (operation.milestoneDescription) {
|
|
759
|
+
lines.push(`- **里程碑描述**: ${this.truncate(String(operation.milestoneDescription), 200)}`);
|
|
760
|
+
}
|
|
761
|
+
if (operation.processDescription) {
|
|
762
|
+
lines.push(`- **过程概要**: ${this.truncate(String(operation.processDescription), 200)}`);
|
|
763
|
+
}
|
|
764
|
+
this.appendSpatiotemporalContextLines(lines, resolvedTask, {
|
|
765
|
+
appendFullContext: true
|
|
766
|
+
});
|
|
767
|
+
if (operation.sessionId) {
|
|
768
|
+
lines.push(`- **Session**: \`${operation.sessionId}\``);
|
|
769
|
+
}
|
|
770
|
+
return lines.join(`
|
|
771
|
+
`);
|
|
772
|
+
}
|
|
773
|
+
async resolveTaskForOperation(operation) {
|
|
774
|
+
if (operation.taskId === undefined || operation.taskId === null) {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const fetch = this.config.fetchTaskById;
|
|
778
|
+
if (!fetch) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
try {
|
|
782
|
+
return await fetch(operation.taskId);
|
|
783
|
+
} catch (error) {
|
|
784
|
+
logger3.warn(`fetchTaskById failed for task #${operation.taskId}: ${error}`);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
__legacyDecorateClassTS([
|
|
790
|
+
TracedAs("lark-cli-task-notify.execute")
|
|
791
|
+
], LarkCliTaskNotifyHook.prototype, "execute", null);
|
|
792
|
+
__legacyDecorateClassTS([
|
|
793
|
+
TracedAs("lark-cli-task-notify.sendNotify")
|
|
794
|
+
], LarkCliTaskNotifyHook.prototype, "sendNotify", null);
|
|
795
|
+
__legacyDecorateClassTS([
|
|
796
|
+
TracedAs("lark-cli-task-notify.sendLarkMessage")
|
|
797
|
+
], LarkCliTaskNotifyHook.prototype, "sendLarkMessage", null);
|
|
798
|
+
function createLarkCliTaskNotifyHook(config = {}) {
|
|
799
|
+
return new LarkCliTaskNotifyHook(config);
|
|
800
|
+
}
|
|
801
|
+
// src/env/task/plugins/per-task-group-chat-plugin.ts
|
|
802
|
+
init_decorator();
|
|
803
|
+
init_logger();
|
|
804
|
+
init_context();
|
|
805
|
+
import { spawn as spawn2 } from "child_process";
|
|
806
|
+
var logger4 = createLogger("lark-cli:per-task-group");
|
|
807
|
+
function isPluginDebugEnabled() {
|
|
808
|
+
return process.env.ROY_TASK_NOTIFY_DEBUG === "1" || process.env.ROY_TRACE_DEBUG === "1";
|
|
809
|
+
}
|
|
810
|
+
var CTX_FIELD_CHAT_ID = "larkPerTaskChatId";
|
|
811
|
+
var CTX_FIELD_CHAT_PROFILE = "larkPerTaskChatProfile";
|
|
812
|
+
var CTX_FIELD_CHAT_CREATED_AT = "larkPerTaskChatCreatedAt";
|
|
813
|
+
var CTX_FIELD_CHAT_TASK_ID = "larkPerTaskChatTaskId";
|
|
814
|
+
|
|
815
|
+
class PerTaskGroupChatPlugin {
|
|
816
|
+
name;
|
|
817
|
+
priority;
|
|
818
|
+
pluginName;
|
|
819
|
+
sourceId;
|
|
820
|
+
config;
|
|
821
|
+
lastNotifyTimestamps = new Map;
|
|
822
|
+
taskUpdater;
|
|
823
|
+
constructor(config = {}) {
|
|
824
|
+
this.config = {
|
|
825
|
+
enabled: config.enabled ?? true,
|
|
826
|
+
priority: config.priority ?? 100,
|
|
827
|
+
pluginName: config.pluginName ?? "PerTaskGroupChatPlugin",
|
|
828
|
+
sourceId: config.sourceId ?? "",
|
|
829
|
+
nameTemplate: config.nameTemplate ?? "Task #{id} - #{title}",
|
|
830
|
+
descriptionTemplate: config.descriptionTemplate ?? "roy-agent task #{id} - #{project_path}",
|
|
831
|
+
botAppIds: config.botAppIds ?? [],
|
|
832
|
+
inviteUserIds: config.inviteUserIds ?? ["sender"],
|
|
833
|
+
defaultInviteUserIds: config.defaultInviteUserIds ?? [],
|
|
834
|
+
fallbackToMain: config.fallbackToMain ?? true,
|
|
835
|
+
operationThrottleMs: config.operationThrottleMs ?? 5000,
|
|
836
|
+
disableSubTaskSkip: config.disableSubTaskSkip ?? false,
|
|
837
|
+
requireReplyChannel: config.requireReplyChannel ?? true,
|
|
838
|
+
fallbackProfile: config.fallbackProfile ?? "",
|
|
839
|
+
sendWelcomeMessage: config.sendWelcomeMessage ?? true
|
|
840
|
+
};
|
|
841
|
+
this.name = this.config.pluginName;
|
|
842
|
+
this.priority = this.config.priority;
|
|
843
|
+
this.pluginName = this.config.pluginName;
|
|
844
|
+
this.sourceId = this.config.sourceId;
|
|
845
|
+
}
|
|
846
|
+
setTaskUpdater(updater) {
|
|
847
|
+
this.taskUpdater = updater;
|
|
848
|
+
logger4.debug(`TaskUpdater injected for plugin ${this.name} (sourceId=${this.sourceId})`);
|
|
849
|
+
}
|
|
850
|
+
hasTaskUpdater() {
|
|
851
|
+
return this.taskUpdater !== undefined;
|
|
852
|
+
}
|
|
853
|
+
async execute(ctx) {
|
|
854
|
+
if (!this.config.enabled) {
|
|
855
|
+
logger4.debug(`execute: plugin disabled (pluginName=${this.pluginName})`);
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
if (this.pluginName && this.sourceId) {
|
|
859
|
+
const env = getEnvContextOrEmpty();
|
|
860
|
+
const key = `${this.sourceId}:${this.pluginName}`;
|
|
861
|
+
if (env.pluginEnabled && env.pluginEnabled[key] !== true) {
|
|
862
|
+
logger4.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`);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const enabledState = env.pluginEnabled ? env.pluginEnabled[key] === true ? "✅ enabled" : "❌ NOT enabled" : "⚠️ env.pluginEnabled 未设置 (env 没创建 / 插件未注册到 eventsource)";
|
|
866
|
+
logger4.debug(`execute[${this.pluginName}]: ${enabledState} (key=${key}, ` + `hookPoint=${ctx.hookPoint}, taskUpdaterInjected=${this.hasTaskUpdater()})`);
|
|
867
|
+
} else {
|
|
868
|
+
logger4.debug(`execute[${this.pluginName}]: missing pluginName/sourceId metadata ` + `(pluginName=${this.pluginName}, sourceId=${this.sourceId}), ` + `skipping pluginEnabled check. hookPoint=${ctx.hookPoint}`);
|
|
869
|
+
}
|
|
870
|
+
switch (ctx.hookPoint) {
|
|
871
|
+
case TaskHookPoints.AFTER_CREATE:
|
|
872
|
+
await this.onAfterCreate(ctx);
|
|
873
|
+
break;
|
|
874
|
+
case TaskHookPoints.AFTER_UPDATE:
|
|
875
|
+
await this.onAfterUpdate(ctx);
|
|
876
|
+
break;
|
|
877
|
+
case TaskHookPoints.OPERATION_AFTER_CREATE:
|
|
878
|
+
await this.onAfterOperationCreate(ctx);
|
|
879
|
+
break;
|
|
880
|
+
case TaskHookPoints.AFTER_COMPLETE:
|
|
881
|
+
await this.onAfterComplete(ctx);
|
|
882
|
+
break;
|
|
883
|
+
default:
|
|
884
|
+
logger4.debug(`execute: unhandled hookPoint=${ctx.hookPoint}, skip`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
async onAfterCreate(ctx) {
|
|
888
|
+
const data = ctx.data;
|
|
889
|
+
if (!data?.task) {
|
|
890
|
+
logger4.debug("onAfterCreate: task is missing, skip");
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const { task } = data;
|
|
894
|
+
logger4.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}`);
|
|
895
|
+
const replyChannel = this.getReplyChannelFromCtx(ctx);
|
|
896
|
+
if (!replyChannel?.chatId && this.config.requireReplyChannel) {
|
|
897
|
+
logger4.debug(`onAfterCreate[${this.pluginName}]: no replyChannel.chatId for task #${task.id}, skip. ` + `修复:eventsource 收到 im.message.receive_v1 事件时 replyChannel 应有值;` + `或在 plugin config 设 requireReplyChannel=false 强制创建群聊(profile 降级到 fallbackProfile/无)`);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (!replyChannel?.chatId && !this.config.requireReplyChannel) {
|
|
901
|
+
logger4.debug(`onAfterCreate[${this.pluginName}]: no replyChannel but requireReplyChannel=false, ` + `proceeding with profile fallback (fallbackProfile="${this.config.fallbackProfile}")`);
|
|
902
|
+
}
|
|
903
|
+
if (this.isSubTask(task) && !this.config.disableSubTaskSkip) {
|
|
904
|
+
logger4.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task, ` + `skip group chat creation. 关闭方式:config.disableSubTaskSkip=true`);
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
if (this.isSubTask(task) && this.config.disableSubTaskSkip) {
|
|
908
|
+
logger4.debug(`onAfterCreate[${this.pluginName}]: task #${task.id} is sub-task but ` + `disableSubTaskSkip=true, proceeding to create group chat`);
|
|
909
|
+
}
|
|
910
|
+
const name = this.renderTemplate(this.config.nameTemplate, task);
|
|
911
|
+
const description = this.renderTemplate(this.config.descriptionTemplate, task);
|
|
912
|
+
const profile = replyChannel?.profile || (this.config.fallbackProfile || undefined);
|
|
913
|
+
const botAppIds = this.resolveBotAppIds(replyChannel ?? {});
|
|
914
|
+
const userIds = this.resolveUserIds(replyChannel);
|
|
915
|
+
logger4.debug(`onAfterCreate[${this.pluginName}]: creating group chat for task #${task.id} ` + `name="${name}" profile="${profile}" botAppIds=${JSON.stringify(botAppIds)} ` + `userIds=${JSON.stringify(userIds)}`);
|
|
916
|
+
try {
|
|
917
|
+
const chatId = await this.createGroupChat({
|
|
918
|
+
name,
|
|
919
|
+
description,
|
|
920
|
+
profile,
|
|
921
|
+
botAppIds,
|
|
922
|
+
userIds
|
|
923
|
+
});
|
|
924
|
+
await this.attachChatIdToTask(task, chatId, profile);
|
|
925
|
+
if (this.config.sendWelcomeMessage) {
|
|
926
|
+
try {
|
|
927
|
+
await this.sendToChat({
|
|
928
|
+
chatId,
|
|
929
|
+
profile,
|
|
930
|
+
content: this.formatWelcomeMessage(task, chatId)
|
|
931
|
+
});
|
|
932
|
+
logger4.debug(`onAfterCreate[${this.pluginName}]: sent welcome message to new chat ${chatId}`);
|
|
933
|
+
} catch (welcomeErr) {
|
|
934
|
+
logger4.warn(`onAfterCreate[${this.pluginName}]: failed to send welcome message to ${chatId}: ${welcomeErr}`);
|
|
935
|
+
}
|
|
936
|
+
} else {
|
|
937
|
+
logger4.debug(`onAfterCreate[${this.pluginName}]: skip welcome message (sendWelcomeMessage=false)`);
|
|
938
|
+
}
|
|
939
|
+
if (replyChannel?.chatId) {
|
|
940
|
+
await this.sendToMain(replyChannel, {
|
|
941
|
+
title: `\uD83D\uDCC2 Task #${task.id} 群聊已创建`,
|
|
942
|
+
content: [
|
|
943
|
+
`- **任务**: ${this.truncate(task.title ?? "", 60)}`,
|
|
944
|
+
`- **群聊链接**: https://applink.larkoffice.com/client/chat/chatDetail?chat_id=${chatId}`,
|
|
945
|
+
`- **任务 ID**: #${task.id}`
|
|
946
|
+
].join(`
|
|
947
|
+
`),
|
|
948
|
+
profile
|
|
949
|
+
});
|
|
950
|
+
} else {
|
|
951
|
+
logger4.debug(`onAfterCreate[${this.pluginName}]: skip "群聊已创建" main-chat notification ` + `(no replyChannel.chatId)`);
|
|
952
|
+
}
|
|
953
|
+
logger4.info(`Created per-task group chat ${chatId} for task #${task.id}`);
|
|
954
|
+
} catch (error) {
|
|
955
|
+
logger4.error(`Failed to create per-task group chat for task #${task.id}: ${error}`);
|
|
956
|
+
if (!this.config.fallbackToMain)
|
|
957
|
+
throw error;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
async onAfterUpdate(ctx) {
|
|
961
|
+
const data = ctx.data;
|
|
962
|
+
if (!data?.task) {
|
|
963
|
+
logger4.debug("onAfterUpdate: task is missing, skip");
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
const { task, changes } = data;
|
|
967
|
+
const chatId = this.getTaskChatId(task);
|
|
968
|
+
if (!chatId) {
|
|
969
|
+
logger4.debug(`onAfterUpdate[${this.pluginName}]: task #${task.id} has no per-task chat_id, skip. ` + `诊断:检查 task.context JSON 是否含 larkPerTaskChatId 字段 ` + `(sqlite: SELECT context FROM task WHERE id=${task.id})`);
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
const replyChannel = this.getReplyChannelFromCtx(ctx);
|
|
973
|
+
const profile = replyChannel?.profile ?? this.getTaskChatProfile(task) ?? undefined;
|
|
974
|
+
if (!this.hasMeaningfulChange(changes)) {
|
|
975
|
+
logger4.debug(`onAfterUpdate[${this.pluginName}]: no meaningful change for task #${task.id}, skip`);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
logger4.debug(`onAfterUpdate[${this.pluginName}]: send update for task #${task.id} ` + `to chat_id=${chatId} profile="${profile}" changes=${JSON.stringify(changes)}`);
|
|
979
|
+
await this.sendToChat({
|
|
980
|
+
chatId,
|
|
981
|
+
profile,
|
|
982
|
+
content: this.formatUpdate(task, changes)
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
async onAfterOperationCreate(ctx) {
|
|
986
|
+
const data = ctx.data;
|
|
987
|
+
if (!data?.operation) {
|
|
988
|
+
logger4.debug("onAfterOperationCreate: operation is missing, skip");
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const { operation } = data;
|
|
992
|
+
if (this.shouldThrottleOperation(operation)) {
|
|
993
|
+
logger4.debug(`onAfterOperationCreate[${this.pluginName}]: operation #${operation.id} ` + `throttled for task #${operation.taskId ?? operation.task_id}`);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const taskId = operation.taskId ?? operation.task_id;
|
|
997
|
+
if (!taskId) {
|
|
998
|
+
logger4.debug("onAfterOperationCreate: no taskId in operation, skip");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const taskFromCtx = ctx.metadata?.task;
|
|
1002
|
+
const chatId = taskFromCtx ? this.getTaskChatId(taskFromCtx) : undefined;
|
|
1003
|
+
if (!chatId || !taskFromCtx) {
|
|
1004
|
+
logger4.debug(`onAfterOperationCreate[${this.pluginName}]: no per-task chat_id for task #${taskId}, skip. ` + `诊断:ctx.metadata.task 是否包含完整 task 对象?task.context.larkPerTaskChatId 是否存在?`);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const replyChannel = this.getReplyChannelFromCtx(ctx);
|
|
1008
|
+
const profile = replyChannel?.profile ?? this.getTaskChatProfile(taskFromCtx) ?? undefined;
|
|
1009
|
+
logger4.debug(`onAfterOperationCreate[${this.pluginName}]: send operation #${operation.id} ` + `for task #${taskId} to chat_id=${chatId}`);
|
|
1010
|
+
await this.sendToChat({
|
|
1011
|
+
chatId,
|
|
1012
|
+
profile,
|
|
1013
|
+
content: this.formatOperation(operation)
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
async onAfterComplete(ctx) {
|
|
1017
|
+
const data = ctx.data;
|
|
1018
|
+
if (!data?.task) {
|
|
1019
|
+
logger4.debug("onAfterComplete: task is missing, skip");
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const { task, terminalStatus } = data;
|
|
1023
|
+
const chatId = this.getTaskChatId(task);
|
|
1024
|
+
if (!chatId) {
|
|
1025
|
+
logger4.debug(`onAfterComplete[${this.pluginName}]: task #${task.id} has no per-task chat_id, skip`);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const replyChannel = this.getReplyChannelFromCtx(ctx);
|
|
1029
|
+
const profile = replyChannel?.profile ?? this.getTaskChatProfile(task) ?? undefined;
|
|
1030
|
+
const emoji = terminalStatus === "completed" ? "✅" : terminalStatus === "failed" ? "❌" : terminalStatus === "cancelled" ? "\uD83D\uDEAB" : terminalStatus === "timeout" ? "⏰" : "\uD83C\uDFC1";
|
|
1031
|
+
logger4.debug(`onAfterComplete[${this.pluginName}]: send complete notification for task #${task.id} ` + `status=${terminalStatus} to chat_id=${chatId}`);
|
|
1032
|
+
await this.sendToChat({
|
|
1033
|
+
chatId,
|
|
1034
|
+
profile,
|
|
1035
|
+
content: `${emoji} **Task #${task.id} ${terminalStatus.toUpperCase()}**
|
|
1036
|
+
|
|
1037
|
+
` + `- **任务**: ${this.truncate(task.title, 80)}
|
|
1038
|
+
` + `- **最终状态**: ${terminalStatus}
|
|
1039
|
+
` + `- **进度**: ${task.progress ?? "?"}%
|
|
1040
|
+
` + `- **结束时间**: ${new Date().toISOString()}`
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
createGroupChat(opts) {
|
|
1044
|
+
const args = this.buildCreateGroupChatArgs(opts);
|
|
1045
|
+
if (isPluginDebugEnabled()) {
|
|
1046
|
+
logger4.debug(`Creating group chat: lark-cli ${args.join(" ")}`);
|
|
1047
|
+
}
|
|
1048
|
+
return new Promise((resolve, reject) => {
|
|
1049
|
+
const proc = spawn2("lark-cli", args, {
|
|
1050
|
+
shell: false,
|
|
1051
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1052
|
+
});
|
|
1053
|
+
let stdout = "";
|
|
1054
|
+
let stderr = "";
|
|
1055
|
+
proc.stdout?.on("data", (data) => {
|
|
1056
|
+
stdout += data.toString();
|
|
1057
|
+
});
|
|
1058
|
+
proc.stderr?.on("data", (data) => {
|
|
1059
|
+
stderr += data.toString();
|
|
1060
|
+
});
|
|
1061
|
+
proc.on("close", (code) => {
|
|
1062
|
+
if (code !== 0) {
|
|
1063
|
+
reject(new Error(`lark-cli +chat-create exited with code ${code}: ${stderr}`));
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const chatId = this.parseChatId(stdout);
|
|
1067
|
+
if (!chatId) {
|
|
1068
|
+
reject(new Error(`lark-cli +chat-create did not return chat_id: ${stdout}`));
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
resolve(chatId);
|
|
1072
|
+
});
|
|
1073
|
+
proc.on("error", (error) => {
|
|
1074
|
+
reject(error);
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
buildCreateGroupChatArgs(opts) {
|
|
1079
|
+
const nameTrimmed = this.truncate(opts.name, 60);
|
|
1080
|
+
const descriptionTrimmed = this.truncate(opts.description, 100);
|
|
1081
|
+
const args = [
|
|
1082
|
+
"im",
|
|
1083
|
+
"+chat-create",
|
|
1084
|
+
"--name",
|
|
1085
|
+
nameTrimmed,
|
|
1086
|
+
"--description",
|
|
1087
|
+
descriptionTrimmed
|
|
1088
|
+
];
|
|
1089
|
+
if (opts.profile) {
|
|
1090
|
+
args.push("--as", "bot");
|
|
1091
|
+
}
|
|
1092
|
+
if (opts.botAppIds.length > 0) {
|
|
1093
|
+
args.push("--bots", opts.botAppIds.join(","));
|
|
1094
|
+
args.push("--set-bot-manager");
|
|
1095
|
+
}
|
|
1096
|
+
if (opts.userIds && opts.userIds.length > 0) {
|
|
1097
|
+
args.push("--users", opts.userIds.join(","));
|
|
1098
|
+
}
|
|
1099
|
+
if (opts.profile) {
|
|
1100
|
+
args.unshift("--profile", opts.profile);
|
|
1101
|
+
}
|
|
1102
|
+
return args;
|
|
1103
|
+
}
|
|
1104
|
+
async attachChatIdToTask(task, chatId, profile) {
|
|
1105
|
+
const contextStr = task.context ?? "{}";
|
|
1106
|
+
let parsed = {};
|
|
1107
|
+
try {
|
|
1108
|
+
const obj = JSON.parse(contextStr);
|
|
1109
|
+
if (obj && typeof obj === "object" && !Array.isArray(obj)) {
|
|
1110
|
+
parsed = obj;
|
|
1111
|
+
}
|
|
1112
|
+
} catch {
|
|
1113
|
+
parsed = {};
|
|
1114
|
+
}
|
|
1115
|
+
parsed[CTX_FIELD_CHAT_ID] = chatId;
|
|
1116
|
+
parsed[CTX_FIELD_CHAT_PROFILE] = profile;
|
|
1117
|
+
parsed[CTX_FIELD_CHAT_CREATED_AT] = new Date().toISOString();
|
|
1118
|
+
parsed[CTX_FIELD_CHAT_TASK_ID] = task.id;
|
|
1119
|
+
const newContext = JSON.stringify(parsed);
|
|
1120
|
+
if (!this.taskUpdater) {
|
|
1121
|
+
logger4.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。`);
|
|
1122
|
+
task.context = newContext;
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
try {
|
|
1126
|
+
await this.taskUpdater(task.id, newContext);
|
|
1127
|
+
task.context = newContext;
|
|
1128
|
+
logger4.debug(`attachChatIdToTask[${this.pluginName}]: ✅ persisted chat_id ${chatId} ` + `to task #${task.id} context`);
|
|
1129
|
+
} catch (error) {
|
|
1130
|
+
logger4.error(`Failed to persist chat_id to task #${task.id}: ${error}`);
|
|
1131
|
+
task.context = newContext;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
buildSendToChatArgs(opts) {
|
|
1135
|
+
const args = [
|
|
1136
|
+
"im",
|
|
1137
|
+
"+messages-send",
|
|
1138
|
+
"--chat-id",
|
|
1139
|
+
opts.chatId,
|
|
1140
|
+
"--markdown",
|
|
1141
|
+
opts.content,
|
|
1142
|
+
"--as",
|
|
1143
|
+
"bot"
|
|
1144
|
+
];
|
|
1145
|
+
if (opts.profile) {
|
|
1146
|
+
args.unshift("--profile", opts.profile);
|
|
1147
|
+
}
|
|
1148
|
+
return args;
|
|
1149
|
+
}
|
|
1150
|
+
sendToChat(opts) {
|
|
1151
|
+
return new Promise((resolve, reject) => {
|
|
1152
|
+
const args = this.buildSendToChatArgs(opts);
|
|
1153
|
+
if (isPluginDebugEnabled()) {
|
|
1154
|
+
logger4.debug(`Sending to chat ${opts.chatId}: lark-cli ${args.join(" ")}`);
|
|
1155
|
+
}
|
|
1156
|
+
const proc = spawn2("lark-cli", args, {
|
|
1157
|
+
shell: false,
|
|
1158
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1159
|
+
});
|
|
1160
|
+
let stderr = "";
|
|
1161
|
+
proc.stderr?.on("data", (data) => {
|
|
1162
|
+
stderr += data.toString();
|
|
1163
|
+
});
|
|
1164
|
+
proc.on("close", (code) => {
|
|
1165
|
+
if (code === 0) {
|
|
1166
|
+
resolve();
|
|
1167
|
+
} else {
|
|
1168
|
+
reject(new Error(`lark-cli +messages-send exited with code ${code}: ${stderr}`));
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
proc.on("error", (error) => {
|
|
1172
|
+
reject(error);
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
sendToMain(replyChannel, opts) {
|
|
1177
|
+
return new Promise((resolve, reject) => {
|
|
1178
|
+
const message = `${opts.title}
|
|
1179
|
+
|
|
1180
|
+
${opts.content}`;
|
|
1181
|
+
const args = replyChannel.messageId ? ["im", "+messages-reply", "--message-id", replyChannel.messageId, "--markdown", message, "--as", "bot"] : ["im", "+messages-send", "--chat-id", replyChannel.chatId, "--markdown", message];
|
|
1182
|
+
if (opts.profile) {
|
|
1183
|
+
args.unshift("--profile", opts.profile);
|
|
1184
|
+
}
|
|
1185
|
+
const proc = spawn2("lark-cli", args, {
|
|
1186
|
+
shell: false,
|
|
1187
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1188
|
+
});
|
|
1189
|
+
let stderr = "";
|
|
1190
|
+
proc.stderr?.on("data", (data) => {
|
|
1191
|
+
stderr += data.toString();
|
|
1192
|
+
});
|
|
1193
|
+
proc.on("close", (code) => {
|
|
1194
|
+
if (code === 0) {
|
|
1195
|
+
resolve();
|
|
1196
|
+
} else {
|
|
1197
|
+
reject(new Error(`lark-cli send-to-main exited with code ${code}: ${stderr}`));
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
proc.on("error", (error) => {
|
|
1201
|
+
reject(error);
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
getReplyChannelFromCtx(ctx) {
|
|
1206
|
+
const metadata = ctx.metadata;
|
|
1207
|
+
const alsEnv = getEnvContextOrEmpty();
|
|
1208
|
+
if (alsEnv.replyChannel?.chatId) {
|
|
1209
|
+
return alsEnv.replyChannel;
|
|
1210
|
+
}
|
|
1211
|
+
if (!metadata)
|
|
1212
|
+
return;
|
|
1213
|
+
const envCtx = metadata.envContext;
|
|
1214
|
+
if (envCtx?.replyChannel?.chatId) {
|
|
1215
|
+
return envCtx.replyChannel;
|
|
1216
|
+
}
|
|
1217
|
+
const direct = metadata.replyChannel;
|
|
1218
|
+
if (direct?.chatId) {
|
|
1219
|
+
return direct;
|
|
1220
|
+
}
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
getTaskChatId(task) {
|
|
1224
|
+
const ctxStr = task.context;
|
|
1225
|
+
if (!ctxStr)
|
|
1226
|
+
return;
|
|
1227
|
+
try {
|
|
1228
|
+
const obj = JSON.parse(ctxStr);
|
|
1229
|
+
const id = obj[CTX_FIELD_CHAT_ID];
|
|
1230
|
+
return typeof id === "string" ? id : undefined;
|
|
1231
|
+
} catch {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
getTaskChatProfile(task) {
|
|
1236
|
+
const ctxStr = task.context;
|
|
1237
|
+
if (!ctxStr)
|
|
1238
|
+
return;
|
|
1239
|
+
try {
|
|
1240
|
+
const obj = JSON.parse(ctxStr);
|
|
1241
|
+
const profile = obj[CTX_FIELD_CHAT_PROFILE];
|
|
1242
|
+
return typeof profile === "string" ? profile : undefined;
|
|
1243
|
+
} catch {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
resolveBotAppIds(replyChannel) {
|
|
1248
|
+
if (this.config.botAppIds.length > 0) {
|
|
1249
|
+
logger4.debug(`resolveBotAppIds[${this.pluginName}]: using config.botAppIds=${JSON.stringify(this.config.botAppIds)}`);
|
|
1250
|
+
return this.config.botAppIds;
|
|
1251
|
+
}
|
|
1252
|
+
const appId = replyChannel ? replyChannel.appId : undefined;
|
|
1253
|
+
if (appId) {
|
|
1254
|
+
logger4.debug(`resolveBotAppIds[${this.pluginName}]: using replyChannel.appId=${appId} (auto-detected from event header.app_id)`);
|
|
1255
|
+
return [appId];
|
|
1256
|
+
}
|
|
1257
|
+
logger4.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。`);
|
|
1258
|
+
return [];
|
|
1259
|
+
}
|
|
1260
|
+
resolveUserIds(replyChannel) {
|
|
1261
|
+
const ids = new Set;
|
|
1262
|
+
if (this.config.defaultInviteUserIds && this.config.defaultInviteUserIds.length > 0) {
|
|
1263
|
+
for (const id of this.config.defaultInviteUserIds) {
|
|
1264
|
+
if (id && id !== "sender" && id.startsWith("ou_")) {
|
|
1265
|
+
ids.add(id);
|
|
1266
|
+
} else if (id && id !== "sender") {
|
|
1267
|
+
logger4.debug(`resolveUserIds[${this.pluginName}]: skip non-open_id in defaultInviteUserIds: "${id}"`);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (this.config.inviteUserIds && this.config.inviteUserIds.length > 0) {
|
|
1272
|
+
for (const id of this.config.inviteUserIds) {
|
|
1273
|
+
if (id !== "sender" && id)
|
|
1274
|
+
ids.add(id);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (this.config.inviteUserIds?.includes("sender")) {
|
|
1278
|
+
const senderId = replyChannel?.userId || replyChannel?.params?.userId || getEnvContextOrEmpty().userId;
|
|
1279
|
+
if (senderId && senderId.startsWith("ou_")) {
|
|
1280
|
+
ids.add(senderId);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
if (ids.size === 0 && this.config.inviteUserIds?.includes("sender")) {
|
|
1284
|
+
const alsUserId = getEnvContextOrEmpty().userId;
|
|
1285
|
+
if (alsUserId && alsUserId.startsWith("ou_")) {
|
|
1286
|
+
ids.add(alsUserId);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
if (isPluginDebugEnabled()) {
|
|
1290
|
+
logger4.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))}`);
|
|
1291
|
+
}
|
|
1292
|
+
return Array.from(ids);
|
|
1293
|
+
}
|
|
1294
|
+
isSubTask(task) {
|
|
1295
|
+
const parentId = task.parent_task_id;
|
|
1296
|
+
return typeof parentId === "number" && parentId > 0;
|
|
1297
|
+
}
|
|
1298
|
+
hasMeaningfulChange(changes) {
|
|
1299
|
+
if (!changes || Object.keys(changes).length === 0)
|
|
1300
|
+
return false;
|
|
1301
|
+
const keys = Object.keys(changes);
|
|
1302
|
+
return keys.some((k) => [
|
|
1303
|
+
"status",
|
|
1304
|
+
"progress",
|
|
1305
|
+
"priority",
|
|
1306
|
+
"title",
|
|
1307
|
+
"description",
|
|
1308
|
+
"current_status",
|
|
1309
|
+
"tags",
|
|
1310
|
+
"goals_and_expected_deliverables"
|
|
1311
|
+
].includes(k));
|
|
1312
|
+
}
|
|
1313
|
+
shouldThrottleOperation(operation) {
|
|
1314
|
+
if (this.config.operationThrottleMs <= 0)
|
|
1315
|
+
return false;
|
|
1316
|
+
const taskId = operation.taskId ?? operation.task_id ?? 0;
|
|
1317
|
+
if (taskId === 0)
|
|
1318
|
+
return false;
|
|
1319
|
+
const now = Date.now();
|
|
1320
|
+
const last = this.lastNotifyTimestamps.get(taskId) ?? 0;
|
|
1321
|
+
if (now - last < this.config.operationThrottleMs) {
|
|
1322
|
+
return true;
|
|
1323
|
+
}
|
|
1324
|
+
this.lastNotifyTimestamps.set(taskId, now);
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
1327
|
+
renderTemplate(template, task) {
|
|
1328
|
+
const id = String(task.id);
|
|
1329
|
+
const title = this.truncate(task.title ?? "", 60);
|
|
1330
|
+
const projectPath = task.project_path ?? "";
|
|
1331
|
+
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}`);
|
|
1332
|
+
}
|
|
1333
|
+
truncate(text, maxLen) {
|
|
1334
|
+
if (!text)
|
|
1335
|
+
return "";
|
|
1336
|
+
if (text.length <= maxLen)
|
|
1337
|
+
return text;
|
|
1338
|
+
return text.substring(0, Math.max(0, maxLen - 3)) + "...";
|
|
1339
|
+
}
|
|
1340
|
+
parseChatId(stdout) {
|
|
1341
|
+
if (!stdout)
|
|
1342
|
+
return;
|
|
1343
|
+
try {
|
|
1344
|
+
const obj = JSON.parse(stdout.trim());
|
|
1345
|
+
const data = obj.data;
|
|
1346
|
+
if (data?.chat_id)
|
|
1347
|
+
return data.chat_id;
|
|
1348
|
+
if (data?.chatId)
|
|
1349
|
+
return data.chatId;
|
|
1350
|
+
} catch {}
|
|
1351
|
+
const match = stdout.match(/oc_[a-zA-Z0-9]+/);
|
|
1352
|
+
return match ? match[0] : undefined;
|
|
1353
|
+
}
|
|
1354
|
+
formatUpdate(task, changes) {
|
|
1355
|
+
const lines = [];
|
|
1356
|
+
lines.push(`\uD83D\uDD04 **Task #${task.id} 更新**`);
|
|
1357
|
+
lines.push("");
|
|
1358
|
+
lines.push(`- **任务**: ${this.truncate(task.title ?? "", 80)}`);
|
|
1359
|
+
if (changes?.status) {
|
|
1360
|
+
lines.push(`- **状态变更**: ${String(changes.status)}`);
|
|
1361
|
+
}
|
|
1362
|
+
if (changes?.progress !== undefined) {
|
|
1363
|
+
lines.push(`- **进度变更**: ${String(changes.progress)}%`);
|
|
1364
|
+
}
|
|
1365
|
+
if (changes?.priority) {
|
|
1366
|
+
lines.push(`- **优先级变更**: ${String(changes.priority)}`);
|
|
1367
|
+
}
|
|
1368
|
+
if (changes?.current_status) {
|
|
1369
|
+
lines.push(`- **状态描述变更**: ${this.truncate(String(changes.current_status), 200)}`);
|
|
1370
|
+
}
|
|
1371
|
+
if (changes?.tags) {
|
|
1372
|
+
const tags = Array.isArray(changes.tags) ? changes.tags.join(", ") : String(changes.tags);
|
|
1373
|
+
lines.push(`- **标签变更**: ${tags}`);
|
|
1374
|
+
}
|
|
1375
|
+
if (changes?.goals_and_expected_deliverables) {
|
|
1376
|
+
lines.push(`- **目标变更**: ${this.truncate(String(changes.goals_and_expected_deliverables), 200)}`);
|
|
1377
|
+
}
|
|
1378
|
+
if (task.progress !== undefined) {
|
|
1379
|
+
lines.push(`- **当前进度**: ${task.progress}%`);
|
|
1380
|
+
}
|
|
1381
|
+
if (task.status) {
|
|
1382
|
+
lines.push(`- **当前状态**: ${task.status}`);
|
|
1383
|
+
}
|
|
1384
|
+
if (task.current_status) {
|
|
1385
|
+
lines.push(`- **当前状态描述**: ${this.truncate(task.current_status, 200)}`);
|
|
1386
|
+
}
|
|
1387
|
+
return lines.join(`
|
|
1388
|
+
`);
|
|
1389
|
+
}
|
|
1390
|
+
formatOperation(operation) {
|
|
1391
|
+
const title = operation.milestoneTitle ?? operation.action_title ?? "操作记录";
|
|
1392
|
+
const desc = operation.milestoneDescription ?? operation.action_description ?? "";
|
|
1393
|
+
return [
|
|
1394
|
+
`\uD83D\uDCCC **${this.truncate(title, 80)}**`,
|
|
1395
|
+
"",
|
|
1396
|
+
desc ? this.truncate(desc, 300) : ""
|
|
1397
|
+
].filter(Boolean).join(`
|
|
1398
|
+
`);
|
|
1399
|
+
}
|
|
1400
|
+
formatWelcomeMessage(task, chatId) {
|
|
1401
|
+
const lines = [];
|
|
1402
|
+
lines.push(`\uD83D\uDCCC **Task #${task.id} 已创建**`);
|
|
1403
|
+
lines.push("");
|
|
1404
|
+
lines.push(`此群聊用于追踪任务进度、记录里程碑、讨论细节。任务状态更新、进度变化、完成摘要都会发到这里。`);
|
|
1405
|
+
lines.push("");
|
|
1406
|
+
lines.push(`**任务信息**`);
|
|
1407
|
+
lines.push(`- **任务 ID**: #${task.id}`);
|
|
1408
|
+
lines.push(`- **标题**: ${this.truncate(task.title ?? "", 100)}`);
|
|
1409
|
+
if (task.description) {
|
|
1410
|
+
lines.push(`- **描述**: ${this.truncate(task.description, 200)}`);
|
|
1411
|
+
}
|
|
1412
|
+
if (task.priority) {
|
|
1413
|
+
lines.push(`- **优先级**: ${task.priority}`);
|
|
1414
|
+
}
|
|
1415
|
+
if (task.status) {
|
|
1416
|
+
lines.push(`- **状态**: ${task.status}`);
|
|
1417
|
+
}
|
|
1418
|
+
if (task.project_path) {
|
|
1419
|
+
lines.push(`- **项目**: \`${task.project_path}\``);
|
|
1420
|
+
}
|
|
1421
|
+
lines.push(`- **创建时间**: ${new Date().toISOString()}`);
|
|
1422
|
+
lines.push("");
|
|
1423
|
+
lines.push(`\uD83D\uDCA1 在此群聊中发言会作为该 task 的输入。如需查看完整链接:\`https://applink.larkoffice.com/client/chat/chatDetail?chat_id=${chatId}\``);
|
|
1424
|
+
return lines.join(`
|
|
1425
|
+
`);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
__legacyDecorateClassTS([
|
|
1429
|
+
TracedAs("per-task-group-chat.execute")
|
|
1430
|
+
], PerTaskGroupChatPlugin.prototype, "execute", null);
|
|
1431
|
+
__legacyDecorateClassTS([
|
|
1432
|
+
TracedAs("per-task-group-chat.onAfterCreate", { recordParams: false })
|
|
1433
|
+
], PerTaskGroupChatPlugin.prototype, "onAfterCreate", null);
|
|
1434
|
+
__legacyDecorateClassTS([
|
|
1435
|
+
TracedAs("per-task-group-chat.onAfterUpdate", { recordParams: false })
|
|
1436
|
+
], PerTaskGroupChatPlugin.prototype, "onAfterUpdate", null);
|
|
1437
|
+
__legacyDecorateClassTS([
|
|
1438
|
+
TracedAs("per-task-group-chat.onAfterOperationCreate", { recordParams: false })
|
|
1439
|
+
], PerTaskGroupChatPlugin.prototype, "onAfterOperationCreate", null);
|
|
1440
|
+
__legacyDecorateClassTS([
|
|
1441
|
+
TracedAs("per-task-group-chat.onAfterComplete", { recordParams: false })
|
|
1442
|
+
], PerTaskGroupChatPlugin.prototype, "onAfterComplete", null);
|
|
1443
|
+
__legacyDecorateClassTS([
|
|
1444
|
+
TracedAs("per-task-group-chat.createGroupChat", {
|
|
1445
|
+
recordParams: true,
|
|
1446
|
+
recordResult: true,
|
|
1447
|
+
flattenParamFilter: true,
|
|
1448
|
+
paramFilter: (args) => ({
|
|
1449
|
+
profile: args[0]?.profile,
|
|
1450
|
+
botAppIdsCount: args[0]?.botAppIds?.length,
|
|
1451
|
+
userIdsCount: args[0]?.userIds?.length ?? 0,
|
|
1452
|
+
userIds: args[0]?.userIds,
|
|
1453
|
+
name: args[0]?.name,
|
|
1454
|
+
description: args[0]?.description
|
|
1455
|
+
}),
|
|
1456
|
+
resultFilter: (r) => ({
|
|
1457
|
+
chatId: r,
|
|
1458
|
+
ok: !!r
|
|
1459
|
+
})
|
|
1460
|
+
})
|
|
1461
|
+
], PerTaskGroupChatPlugin.prototype, "createGroupChat", null);
|
|
1462
|
+
__legacyDecorateClassTS([
|
|
1463
|
+
TracedAs("per-task-group-chat.buildCreateGroupChatArgs", {
|
|
1464
|
+
recordParams: false,
|
|
1465
|
+
recordResult: true,
|
|
1466
|
+
flattenParamFilter: true,
|
|
1467
|
+
paramFilter: (args) => ({
|
|
1468
|
+
hasProfile: !!args[0]?.profile,
|
|
1469
|
+
botAppIdsCount: args[0]?.botAppIds?.length,
|
|
1470
|
+
hasUsers: !!args[0]?.userIds?.length
|
|
1471
|
+
}),
|
|
1472
|
+
resultFilter: (r) => ({
|
|
1473
|
+
argsSummary: r.join(" "),
|
|
1474
|
+
hasAsBot: r.includes("--as") && r[r.indexOf("--as") + 1] === "bot",
|
|
1475
|
+
hasBots: r.includes("--bots"),
|
|
1476
|
+
hasUsers: r.includes("--users")
|
|
1477
|
+
})
|
|
1478
|
+
})
|
|
1479
|
+
], PerTaskGroupChatPlugin.prototype, "buildCreateGroupChatArgs", null);
|
|
1480
|
+
__legacyDecorateClassTS([
|
|
1481
|
+
TracedAs("per-task-group-chat.attachChatIdToTask", {
|
|
1482
|
+
recordParams: false,
|
|
1483
|
+
flattenParamFilter: true,
|
|
1484
|
+
paramFilter: (args) => ({
|
|
1485
|
+
taskId: args[0]?.id,
|
|
1486
|
+
chatId: args[1],
|
|
1487
|
+
profile: args[2],
|
|
1488
|
+
taskUpdaterInjected: !!args[0] && !!args[0].context
|
|
1489
|
+
})
|
|
1490
|
+
})
|
|
1491
|
+
], PerTaskGroupChatPlugin.prototype, "attachChatIdToTask", null);
|
|
1492
|
+
__legacyDecorateClassTS([
|
|
1493
|
+
TracedAs("per-task-group-chat.sendToChat", {
|
|
1494
|
+
recordParams: true,
|
|
1495
|
+
recordError: true,
|
|
1496
|
+
flattenParamFilter: true,
|
|
1497
|
+
paramFilter: (args) => ({
|
|
1498
|
+
chatId: args[0]?.chatId,
|
|
1499
|
+
profile: args[0]?.profile,
|
|
1500
|
+
contentLength: args[0]?.content?.length
|
|
1501
|
+
})
|
|
1502
|
+
})
|
|
1503
|
+
], PerTaskGroupChatPlugin.prototype, "sendToChat", null);
|
|
1504
|
+
__legacyDecorateClassTS([
|
|
1505
|
+
TracedAs("per-task-group-chat.sendToMain", {
|
|
1506
|
+
recordParams: true,
|
|
1507
|
+
recordError: true,
|
|
1508
|
+
flattenParamFilter: true,
|
|
1509
|
+
paramFilter: (args) => ({
|
|
1510
|
+
mode: args[0]?.messageId ? "reply" : "send",
|
|
1511
|
+
chatId: args[0]?.messageId ? undefined : args[0]?.chatId,
|
|
1512
|
+
profile: args[1]?.profile,
|
|
1513
|
+
title: args[1]?.title
|
|
1514
|
+
})
|
|
1515
|
+
})
|
|
1516
|
+
], PerTaskGroupChatPlugin.prototype, "sendToMain", null);
|
|
1517
|
+
function createPerTaskGroupChatPlugin(config = {}) {
|
|
1518
|
+
return new PerTaskGroupChatPlugin(config);
|
|
1519
|
+
}
|
|
1520
|
+
export { createAutoTaskPlugin, WORKFLOW_EXTRACTOR_PROMPT, formatExtractorInput, formatExtractorInputFromDescription, parseExtractorOutput, TaskTagPlugin, NotifyType, LarkCliTaskNotifyHook, createLarkCliTaskNotifyHook, PerTaskGroupChatPlugin, createPerTaskGroupChatPlugin };
|