@trydying/opencode-feishu-notifier 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/package.json +5 -20
- package/src/config.ts +172 -0
- package/src/context/progress.ts +174 -0
- package/src/context/project.ts +202 -0
- package/src/feishu/client.ts +202 -0
- package/src/feishu/messages.ts +322 -0
- package/src/feishu/templates.ts +539 -0
- package/src/hooks.ts +40 -0
- package/src/index.ts +144 -0
- package/src/types.ts +102 -0
- package/dist/chunk-DGUM43GV.js +0 -11
- package/dist/chunk-DGUM43GV.js.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -550
- package/dist/index.js.map +0 -1
- package/dist/templates-PQN4V7CV.js +0 -564
- package/dist/templates-PQN4V7CV.js.map +0 -1
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MessageContext,
|
|
3
|
+
MessageTemplate,
|
|
4
|
+
ReasonConfig,
|
|
5
|
+
ReasonConfigMap,
|
|
6
|
+
} from "../types";
|
|
7
|
+
import type { NotificationType } from "./messages";
|
|
8
|
+
|
|
9
|
+
import { createProgressInfo, formatProgressInfo } from "../context/progress";
|
|
10
|
+
import { extractProjectContext } from "../context/project";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 事件原因配置映射
|
|
14
|
+
*/
|
|
15
|
+
const REASON_CONFIGS = {
|
|
16
|
+
session_idle: {
|
|
17
|
+
category: "闲暇等待",
|
|
18
|
+
description: "已完成当前任务,等待下一步指示",
|
|
19
|
+
requiresAction: true,
|
|
20
|
+
emoji: "💤",
|
|
21
|
+
},
|
|
22
|
+
permission_required: {
|
|
23
|
+
category: "需要权限",
|
|
24
|
+
description: "需要访问文件权限才能继续",
|
|
25
|
+
requiresAction: true,
|
|
26
|
+
emoji: "🔐",
|
|
27
|
+
},
|
|
28
|
+
question_asked: {
|
|
29
|
+
category: "需要选择",
|
|
30
|
+
description: "提供了多个方案,需要你选择",
|
|
31
|
+
requiresAction: true,
|
|
32
|
+
emoji: "❓",
|
|
33
|
+
},
|
|
34
|
+
interaction_required: {
|
|
35
|
+
category: "需要输入",
|
|
36
|
+
description: "需要你提供额外信息",
|
|
37
|
+
requiresAction: true,
|
|
38
|
+
emoji: "✏️",
|
|
39
|
+
},
|
|
40
|
+
command_args_required: {
|
|
41
|
+
category: "参数缺失",
|
|
42
|
+
description: "命令需要额外参数才能执行",
|
|
43
|
+
requiresAction: true,
|
|
44
|
+
emoji: "⚙️",
|
|
45
|
+
},
|
|
46
|
+
confirmation_required: {
|
|
47
|
+
category: "需要确认",
|
|
48
|
+
description: "需要你确认是否继续操作",
|
|
49
|
+
requiresAction: true,
|
|
50
|
+
emoji: "✅",
|
|
51
|
+
},
|
|
52
|
+
setup_test: {
|
|
53
|
+
category: "测试通知",
|
|
54
|
+
description: "飞书通知功能测试",
|
|
55
|
+
requiresAction: false,
|
|
56
|
+
emoji: "🧪",
|
|
57
|
+
},
|
|
58
|
+
} as const satisfies ReasonConfigMap;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 获取事件类型的中文标题
|
|
62
|
+
*/
|
|
63
|
+
function getEventTitle(eventType: NotificationType): string {
|
|
64
|
+
const titles: Record<NotificationType, string> = {
|
|
65
|
+
interaction_required: "需要交互",
|
|
66
|
+
permission_required: "需要权限",
|
|
67
|
+
command_args_required: "缺少参数",
|
|
68
|
+
confirmation_required: "需要确认",
|
|
69
|
+
session_idle: "任务完成",
|
|
70
|
+
question_asked: "请做选择",
|
|
71
|
+
setup_test: "测试通知",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return titles[eventType];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 权限类型中文映射
|
|
79
|
+
*/
|
|
80
|
+
const PERMISSION_TYPE_LABELS: Record<string, string> = {
|
|
81
|
+
read: "读取",
|
|
82
|
+
write: "写入",
|
|
83
|
+
execute: "执行",
|
|
84
|
+
edit: "编辑",
|
|
85
|
+
bash: "命令行",
|
|
86
|
+
webfetch: "网络请求",
|
|
87
|
+
external_directory: "外部目录",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 从事件负载中提取具体操作说明
|
|
92
|
+
* 根据不同事件类型提取对应的详细信息
|
|
93
|
+
*/
|
|
94
|
+
function extractActionDetails(
|
|
95
|
+
eventPayload?: unknown,
|
|
96
|
+
originalEventType?: string
|
|
97
|
+
): string[] {
|
|
98
|
+
const details: string[] = [];
|
|
99
|
+
|
|
100
|
+
if (!eventPayload) {
|
|
101
|
+
return details;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (typeof eventPayload !== "object" || eventPayload === null) {
|
|
105
|
+
return details;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const payload = eventPayload as Record<string, unknown>;
|
|
109
|
+
|
|
110
|
+
// ========== permission.updated / permission.asked ==========
|
|
111
|
+
if (
|
|
112
|
+
originalEventType === "permission.updated" ||
|
|
113
|
+
originalEventType === "permission.asked"
|
|
114
|
+
) {
|
|
115
|
+
const permType = payload.type as string | undefined;
|
|
116
|
+
const pattern = payload.pattern as string | string[] | undefined;
|
|
117
|
+
const title = payload.title as string | undefined;
|
|
118
|
+
|
|
119
|
+
if (title) {
|
|
120
|
+
details.push(`- ${title}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (permType) {
|
|
124
|
+
const typeLabel = PERMISSION_TYPE_LABELS[permType] || permType;
|
|
125
|
+
details.push(`- 权限类型: ${typeLabel}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (pattern) {
|
|
129
|
+
if (Array.isArray(pattern)) {
|
|
130
|
+
details.push(`- 涉及路径:`);
|
|
131
|
+
pattern.forEach((p) => {
|
|
132
|
+
details.push(` - ${p}`);
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
details.push(`- 涉及路径: ${pattern}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return details;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ========== tui.prompt.append ==========
|
|
143
|
+
if (originalEventType === "tui.prompt.append") {
|
|
144
|
+
const text = payload.text as string | undefined;
|
|
145
|
+
if (text) {
|
|
146
|
+
details.push(`- 提示内容: ${text}`);
|
|
147
|
+
}
|
|
148
|
+
return details;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ========== tui.command.execute ==========
|
|
152
|
+
if (originalEventType === "tui.command.execute") {
|
|
153
|
+
const command = payload.command as string | undefined;
|
|
154
|
+
if (command) {
|
|
155
|
+
details.push(`- 命令: ${command}`);
|
|
156
|
+
}
|
|
157
|
+
return details;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ========== tui.toast.show ==========
|
|
161
|
+
if (originalEventType === "tui.toast.show") {
|
|
162
|
+
const title = payload.title as string | undefined;
|
|
163
|
+
const message = payload.message as string | undefined;
|
|
164
|
+
|
|
165
|
+
if (title) {
|
|
166
|
+
details.push(`- 标题: ${title}`);
|
|
167
|
+
}
|
|
168
|
+
if (message) {
|
|
169
|
+
details.push(`- 内容: ${message}`);
|
|
170
|
+
}
|
|
171
|
+
return details;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ========== session.status ==========
|
|
175
|
+
if (originalEventType === "session.status") {
|
|
176
|
+
const status = payload.status as Record<string, unknown> | undefined;
|
|
177
|
+
if (status) {
|
|
178
|
+
const statusType = status.type as string | undefined;
|
|
179
|
+
if (statusType === "idle") {
|
|
180
|
+
details.push(`- 状态: 空闲,等待指令`);
|
|
181
|
+
} else if (statusType === "busy") {
|
|
182
|
+
details.push(`- 状态: 忙碌中`);
|
|
183
|
+
} else if (statusType === "retry") {
|
|
184
|
+
const attempt = status.attempt as number | undefined;
|
|
185
|
+
const message = status.message as string | undefined;
|
|
186
|
+
details.push(`- 状态: 重试中 (第 ${attempt || "?"} 次)`);
|
|
187
|
+
if (message) {
|
|
188
|
+
details.push(`- 原因: ${message}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return details;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ========== question.asked (通用选项处理) ==========
|
|
196
|
+
if (payload.options && Array.isArray(payload.options)) {
|
|
197
|
+
const options = payload.options as Array<{
|
|
198
|
+
label?: string;
|
|
199
|
+
description?: string;
|
|
200
|
+
}>;
|
|
201
|
+
if (options.length > 0) {
|
|
202
|
+
details.push(`可选方案:`);
|
|
203
|
+
options.forEach((option, index) => {
|
|
204
|
+
const label = option.label || `选项 ${index + 1}`;
|
|
205
|
+
const desc = option.description ? ` - ${option.description}` : "";
|
|
206
|
+
details.push(` ${index + 1}. ${label}${desc}`);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return details;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ========== 通用字段处理 ==========
|
|
213
|
+
|
|
214
|
+
if (payload.prompt && typeof payload.prompt === "string") {
|
|
215
|
+
details.push(`- 提示: ${payload.prompt}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (payload.message && typeof payload.message === "string") {
|
|
219
|
+
details.push(`- 消息: ${payload.message}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (payload.title && typeof payload.title === "string" && details.length === 0) {
|
|
223
|
+
details.push(`- ${payload.title}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (payload.action && typeof payload.action === "string") {
|
|
227
|
+
details.push(`- 操作: ${payload.action}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (payload.args && Array.isArray(payload.args)) {
|
|
231
|
+
const args = payload.args as string[];
|
|
232
|
+
if (args.length > 0) {
|
|
233
|
+
details.push(`- 参数:`);
|
|
234
|
+
args.forEach((arg) => {
|
|
235
|
+
details.push(` - --${arg}`);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return details;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 构建头部区域 - 醒目的标题行
|
|
245
|
+
*/
|
|
246
|
+
function buildHeader(context: MessageContext): string {
|
|
247
|
+
const { eventType } = context;
|
|
248
|
+
const config = REASON_CONFIGS[eventType];
|
|
249
|
+
const title = getEventTitle(eventType);
|
|
250
|
+
|
|
251
|
+
return `${config.emoji} **${title}**`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 构建环境信息区域
|
|
256
|
+
*/
|
|
257
|
+
function buildEnvironment(context: MessageContext): string {
|
|
258
|
+
const { project, sessionTitle, sessionID, agentName } = context;
|
|
259
|
+
const lines: string[] = [];
|
|
260
|
+
|
|
261
|
+
lines.push("**🖥️ 环境**");
|
|
262
|
+
|
|
263
|
+
if (project.hostname) {
|
|
264
|
+
lines.push(`- 主机: ${project.hostname}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lines.push(`- 项目: ${project.projectName}`);
|
|
268
|
+
|
|
269
|
+
if (project.branch) {
|
|
270
|
+
lines.push(`- 分支: ${project.branch}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (sessionTitle || sessionID) {
|
|
274
|
+
const sessionLabel = sessionTitle || sessionID || "";
|
|
275
|
+
lines.push(`- 会话: ${sessionLabel}`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (agentName) {
|
|
279
|
+
lines.push(`- Agent: ${agentName}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return lines.join("\n");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* 构建原因说明区域
|
|
287
|
+
*/
|
|
288
|
+
function buildReason(context: MessageContext): string {
|
|
289
|
+
const { eventType, eventPayload, originalEventType } = context;
|
|
290
|
+
const config = REASON_CONFIGS[eventType];
|
|
291
|
+
const lines: string[] = [];
|
|
292
|
+
|
|
293
|
+
lines.push("**💡 说明**");
|
|
294
|
+
lines.push(config.description);
|
|
295
|
+
|
|
296
|
+
// 添加具体操作说明
|
|
297
|
+
const actionDetails = extractActionDetails(eventPayload, originalEventType);
|
|
298
|
+
if (actionDetails.length > 0) {
|
|
299
|
+
lines.push("");
|
|
300
|
+
actionDetails.forEach((detail) => lines.push(detail));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 对于需要确认的操作,添加警告
|
|
304
|
+
if (eventType === "confirmation_required") {
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push("⚠️ 请谨慎确认此操作");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return lines.join("\n");
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* 构建工作目录信息
|
|
314
|
+
*/
|
|
315
|
+
function buildWorkdir(context: MessageContext): string {
|
|
316
|
+
const { project } = context;
|
|
317
|
+
const lines: string[] = [];
|
|
318
|
+
|
|
319
|
+
lines.push("**📂 路径**");
|
|
320
|
+
lines.push(`- 目录: ${project.workingDir}`);
|
|
321
|
+
|
|
322
|
+
if (project.isGitRepo && project.repoUrl) {
|
|
323
|
+
lines.push(`- 仓库: ${project.repoUrl}`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return lines.join("\n");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* 构建时间戳
|
|
331
|
+
*/
|
|
332
|
+
function buildTimestamp(): string {
|
|
333
|
+
const now = new Date();
|
|
334
|
+
const timeStr = now.toLocaleString("zh-CN", {
|
|
335
|
+
month: "2-digit",
|
|
336
|
+
day: "2-digit",
|
|
337
|
+
hour: "2-digit",
|
|
338
|
+
minute: "2-digit",
|
|
339
|
+
});
|
|
340
|
+
return `⏰ ${timeStr}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* 美观消息模板实现
|
|
345
|
+
*/
|
|
346
|
+
export class BeautifulMessageTemplate implements MessageTemplate {
|
|
347
|
+
buildTitle(context: MessageContext): string {
|
|
348
|
+
return buildHeader(context);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
buildReason(context: MessageContext): string {
|
|
352
|
+
return buildReason(context);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
buildProgress(context: MessageContext): string {
|
|
356
|
+
return buildEnvironment(context);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
buildFullMessage(context: MessageContext): string {
|
|
360
|
+
const header = buildHeader(context);
|
|
361
|
+
const environment = buildEnvironment(context);
|
|
362
|
+
const reason = buildReason(context);
|
|
363
|
+
const workdir = buildWorkdir(context);
|
|
364
|
+
const timestamp = buildTimestamp();
|
|
365
|
+
|
|
366
|
+
// 组装完整消息
|
|
367
|
+
const sections = [
|
|
368
|
+
header,
|
|
369
|
+
"",
|
|
370
|
+
environment,
|
|
371
|
+
"",
|
|
372
|
+
reason,
|
|
373
|
+
"",
|
|
374
|
+
workdir,
|
|
375
|
+
"",
|
|
376
|
+
timestamp,
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
return sections.join("\n");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* 默认消息模板实现(保留向后兼容)
|
|
385
|
+
*/
|
|
386
|
+
export class DefaultMessageTemplate implements MessageTemplate {
|
|
387
|
+
buildTitle(context: MessageContext): string {
|
|
388
|
+
const { project, eventType } = context;
|
|
389
|
+
const eventTitle = getEventTitle(eventType);
|
|
390
|
+
|
|
391
|
+
let title = `📦 [${project.projectName}]`;
|
|
392
|
+
|
|
393
|
+
if (project.branch) {
|
|
394
|
+
title += ` ${project.branch}`;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (project.hostname) {
|
|
398
|
+
title += ` @${project.hostname}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
title += ` | ${eventTitle}`;
|
|
402
|
+
|
|
403
|
+
return title;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
buildReason(context: MessageContext): string {
|
|
407
|
+
const { eventType, eventPayload, originalEventType } = context;
|
|
408
|
+
const config = REASON_CONFIGS[eventType];
|
|
409
|
+
|
|
410
|
+
const lines: string[] = [];
|
|
411
|
+
lines.push(`🔔 原因:${config.category}`);
|
|
412
|
+
lines.push(config.description);
|
|
413
|
+
|
|
414
|
+
const actionDetails = extractActionDetails(eventPayload, originalEventType);
|
|
415
|
+
if (actionDetails.length > 0) {
|
|
416
|
+
lines.push("");
|
|
417
|
+
actionDetails.forEach((detail) => lines.push(detail));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (eventType === "confirmation_required") {
|
|
421
|
+
lines.push("");
|
|
422
|
+
lines.push("⚠️ 此操作可能需要谨慎确认。");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return lines.join("\n");
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
buildProgress(context: MessageContext): string {
|
|
429
|
+
const { project, progress, sessionID, sessionTitle, agentName } = context;
|
|
430
|
+
|
|
431
|
+
const lines: string[] = [];
|
|
432
|
+
lines.push("📊 进度摘要");
|
|
433
|
+
|
|
434
|
+
lines.push(`• 工作目录:${project.workingDir}`);
|
|
435
|
+
|
|
436
|
+
if (sessionTitle || sessionID) {
|
|
437
|
+
const label = sessionTitle ?? sessionID ?? "";
|
|
438
|
+
const suffix = sessionTitle && sessionID ? ` (${sessionID})` : "";
|
|
439
|
+
lines.push(`• 会话:${label}${suffix}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (agentName) {
|
|
443
|
+
lines.push(`• Agent:${agentName}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const progressText = formatProgressInfo(progress);
|
|
447
|
+
if (progressText) {
|
|
448
|
+
const progressLines = progressText.split("\n");
|
|
449
|
+
progressLines.forEach((line: string) => {
|
|
450
|
+
if (line.trim()) {
|
|
451
|
+
lines.push(line);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (project.isGitRepo && project.repoUrl) {
|
|
457
|
+
lines.push(`• 仓库地址:${project.repoUrl}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return lines.join("\n");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
buildFullMessage(context: MessageContext): string {
|
|
464
|
+
const title = this.buildTitle(context);
|
|
465
|
+
const reason = this.buildReason(context);
|
|
466
|
+
const progress = this.buildProgress(context);
|
|
467
|
+
|
|
468
|
+
return `${title}\n\n${reason}\n\n${progress}`;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* 创建消息模板实例(默认使用美观模板)
|
|
474
|
+
*/
|
|
475
|
+
export function createMessageTemplate(): MessageTemplate {
|
|
476
|
+
return new BeautifulMessageTemplate();
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* 根据事件类型获取原因配置
|
|
481
|
+
*/
|
|
482
|
+
export function getReasonConfig(eventType: NotificationType): ReasonConfig {
|
|
483
|
+
return REASON_CONFIGS[eventType] as ReasonConfig;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* 构建完整的消息上下文
|
|
488
|
+
*/
|
|
489
|
+
export async function buildMessageContext(
|
|
490
|
+
eventType: NotificationType,
|
|
491
|
+
eventPayload?: unknown,
|
|
492
|
+
originalEventType?: string,
|
|
493
|
+
directory?: string,
|
|
494
|
+
sessionContext?: {
|
|
495
|
+
sessionID?: string;
|
|
496
|
+
sessionTitle?: string;
|
|
497
|
+
agentName?: string;
|
|
498
|
+
}
|
|
499
|
+
): Promise<MessageContext> {
|
|
500
|
+
const project = await extractProjectContext(directory || process.cwd());
|
|
501
|
+
const progress = createProgressInfo(eventPayload, directory || process.cwd());
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
project,
|
|
505
|
+
progress,
|
|
506
|
+
eventType,
|
|
507
|
+
eventPayload,
|
|
508
|
+
originalEventType,
|
|
509
|
+
sessionID: sessionContext?.sessionID,
|
|
510
|
+
sessionTitle: sessionContext?.sessionTitle,
|
|
511
|
+
agentName: sessionContext?.agentName,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* 快速构建消息(简化接口)
|
|
517
|
+
*/
|
|
518
|
+
export async function buildStructuredMessage(
|
|
519
|
+
eventType: NotificationType,
|
|
520
|
+
eventPayload?: unknown,
|
|
521
|
+
originalEventType?: string,
|
|
522
|
+
directory?: string,
|
|
523
|
+
sessionContext?: {
|
|
524
|
+
sessionID?: string;
|
|
525
|
+
sessionTitle?: string;
|
|
526
|
+
agentName?: string;
|
|
527
|
+
}
|
|
528
|
+
): Promise<string> {
|
|
529
|
+
const context = await buildMessageContext(
|
|
530
|
+
eventType,
|
|
531
|
+
eventPayload,
|
|
532
|
+
originalEventType,
|
|
533
|
+
directory,
|
|
534
|
+
sessionContext
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const template = createMessageTemplate();
|
|
538
|
+
return template.buildFullMessage(context);
|
|
539
|
+
}
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { NotificationType } from "./feishu/messages"
|
|
2
|
+
|
|
3
|
+
export function mapEventToNotification(eventType: string): NotificationType | null {
|
|
4
|
+
switch (eventType) {
|
|
5
|
+
case "permission.asked":
|
|
6
|
+
return "permission_required"
|
|
7
|
+
case "question.asked":
|
|
8
|
+
return "question_asked"
|
|
9
|
+
default:
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type CompletionCandidateEvent = {
|
|
15
|
+
type: string
|
|
16
|
+
properties?: Record<string, unknown>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 消息完成事件(包含错误后终止)统一映射为完成通知。
|
|
21
|
+
*/
|
|
22
|
+
export function mapCompletionEventToNotification(
|
|
23
|
+
event: CompletionCandidateEvent
|
|
24
|
+
): NotificationType | null {
|
|
25
|
+
const status = event.properties?.status as { type?: string } | undefined
|
|
26
|
+
if (event.type === "session.status" && status?.type === "idle") {
|
|
27
|
+
return "session_idle"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 兼容不同版本 OpenCode 可能出现的完成/终止事件名
|
|
31
|
+
if (
|
|
32
|
+
event.type === "message.completed" ||
|
|
33
|
+
event.type === "message.failed" ||
|
|
34
|
+
event.type === "message.errored"
|
|
35
|
+
) {
|
|
36
|
+
return "session_idle"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
import { loadConfigWithSource } from "./config";
|
|
3
|
+
import { sendTextMessage, sendRichTextMessage } from "./feishu/client";
|
|
4
|
+
import { buildNotification, recordEventContext } from "./feishu/messages";
|
|
5
|
+
import { mapCompletionEventToNotification, mapEventToNotification } from "./hooks";
|
|
6
|
+
|
|
7
|
+
const serviceName = "opencode-feishu-notifier";
|
|
8
|
+
|
|
9
|
+
const FeishuNotifierPlugin: Plugin = async ({ client, directory }) => {
|
|
10
|
+
let configCache: ReturnType<typeof loadConfigWithSource> | null = null;
|
|
11
|
+
let configError: Error | null = null;
|
|
12
|
+
|
|
13
|
+
const log = (
|
|
14
|
+
level: "debug" | "info" | "warn" | "error",
|
|
15
|
+
message: string,
|
|
16
|
+
extra?: Record<string, unknown>
|
|
17
|
+
) => {
|
|
18
|
+
const payload = {
|
|
19
|
+
body: {
|
|
20
|
+
service: serviceName,
|
|
21
|
+
level,
|
|
22
|
+
message,
|
|
23
|
+
extra,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
void client.app.log(payload).catch(() => undefined);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const logDebug = (message: string, extra?: Record<string, unknown>) => {
|
|
30
|
+
log("debug", message, extra);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const logInfo = (message: string, extra?: Record<string, unknown>) => {
|
|
34
|
+
log("info", message, extra);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const logError = (message: string, extra?: Record<string, unknown>) => {
|
|
38
|
+
log("error", message, extra);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
logInfo("Feishu notifier plugin loading", { directory });
|
|
42
|
+
|
|
43
|
+
const ensureConfig = () => {
|
|
44
|
+
if (configCache || configError) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
configCache = loadConfigWithSource({ directory });
|
|
50
|
+
logInfo("Feishu notifier plugin initialized", { sources: configCache.sources.map(s => s.type) });
|
|
51
|
+
logDebug("Loaded Feishu config", { sources: configCache.sources });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
configError = error instanceof Error ? error : new Error(String(error));
|
|
54
|
+
logError("Feishu config error", { error: configError.message });
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
ensureConfig();
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
event: async ({ event }) => {
|
|
62
|
+
recordEventContext(event);
|
|
63
|
+
logDebug("Event received", { eventType: event.type });
|
|
64
|
+
|
|
65
|
+
let notificationType = mapEventToNotification(event.type);
|
|
66
|
+
notificationType = notificationType ?? mapCompletionEventToNotification(event);
|
|
67
|
+
|
|
68
|
+
if (!notificationType) {
|
|
69
|
+
logDebug("Event ignored", { eventType: event.type });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
logDebug("Event mapped to notification", {
|
|
73
|
+
eventType: event.type,
|
|
74
|
+
notificationType,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
ensureConfig();
|
|
78
|
+
if (configError) {
|
|
79
|
+
logError("Feishu config error (cached)", { error: configError.message });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!configCache) {
|
|
83
|
+
logError("Feishu config not loaded");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const { text, title, richContent } = await buildNotification(
|
|
88
|
+
notificationType,
|
|
89
|
+
event,
|
|
90
|
+
directory,
|
|
91
|
+
{ session: client.session }
|
|
92
|
+
);
|
|
93
|
+
logDebug("Sending Feishu notification", {
|
|
94
|
+
eventType: event.type,
|
|
95
|
+
notificationType,
|
|
96
|
+
directory,
|
|
97
|
+
hasRichContent: !!richContent,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
let response;
|
|
102
|
+
if (richContent) {
|
|
103
|
+
// 尝试发送富文本消息
|
|
104
|
+
try {
|
|
105
|
+
logDebug("Attempting to send rich text message", {
|
|
106
|
+
richContentType: typeof richContent,
|
|
107
|
+
hasPost: !!richContent.post,
|
|
108
|
+
hasZhCn: !!(richContent.post?.zh_cn),
|
|
109
|
+
titleLength: richContent.post?.zh_cn?.title?.length ?? 0,
|
|
110
|
+
contentLength: richContent.post?.zh_cn?.content?.length ?? 0,
|
|
111
|
+
});
|
|
112
|
+
response = await sendRichTextMessage(configCache.config, text, title, richContent);
|
|
113
|
+
logDebug("Feishu rich notification sent", {
|
|
114
|
+
messageId: response.data?.message_id ?? null,
|
|
115
|
+
});
|
|
116
|
+
} catch (richError) {
|
|
117
|
+
// 富文本消息失败,回退到纯文本
|
|
118
|
+
logDebug("Rich text message failed, falling back to text", {
|
|
119
|
+
error: richError instanceof Error ? richError.message : String(richError),
|
|
120
|
+
stack: richError instanceof Error ? richError.stack : undefined,
|
|
121
|
+
name: richError instanceof Error ? richError.name : undefined,
|
|
122
|
+
});
|
|
123
|
+
response = await sendTextMessage(configCache.config, text);
|
|
124
|
+
logDebug("Feishu text notification sent (fallback)", {
|
|
125
|
+
messageId: response.data?.message_id ?? null,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// 回退到纯文本
|
|
130
|
+
response = await sendTextMessage(configCache.config, text);
|
|
131
|
+
logDebug("Feishu text notification sent", {
|
|
132
|
+
messageId: response.data?.message_id ?? null,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logError("Failed to send Feishu notification", {
|
|
137
|
+
error: String(error),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default FeishuNotifierPlugin;
|