team-anya 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +38 -0
  2. package/apps/server/dist/broker/cc-broker.js +267 -0
  3. package/apps/server/dist/cli.js +296 -0
  4. package/apps/server/dist/config.js +78 -0
  5. package/apps/server/dist/daemon.js +51 -0
  6. package/apps/server/dist/franky/context-builder.js +161 -0
  7. package/apps/server/dist/franky/franky-mcp-server.js +110 -0
  8. package/apps/server/dist/franky/franky-orchestrator.js +629 -0
  9. package/apps/server/dist/franky/index.js +5 -0
  10. package/apps/server/dist/franky/topic-router.js +16 -0
  11. package/apps/server/dist/gateway/chat-sync.js +135 -0
  12. package/apps/server/dist/gateway/command-router.js +116 -0
  13. package/apps/server/dist/gateway/commands/cancel.js +32 -0
  14. package/apps/server/dist/gateway/commands/help.js +16 -0
  15. package/apps/server/dist/gateway/commands/index.js +26 -0
  16. package/apps/server/dist/gateway/commands/restart.js +43 -0
  17. package/apps/server/dist/gateway/commands/status.js +34 -0
  18. package/apps/server/dist/gateway/commands/tasks.js +33 -0
  19. package/apps/server/dist/gateway/feishu-sender.js +508 -0
  20. package/apps/server/dist/gateway/feishu-ws.js +353 -0
  21. package/apps/server/dist/gateway/health-monitor.js +154 -0
  22. package/apps/server/dist/gateway/http.js +1064 -0
  23. package/apps/server/dist/gateway/media-downloader.js +182 -0
  24. package/apps/server/dist/gateway/message-events.js +10 -0
  25. package/apps/server/dist/gateway/message-intake.js +72 -0
  26. package/apps/server/dist/gateway/message-queue.js +118 -0
  27. package/apps/server/dist/gateway/session-reader.js +142 -0
  28. package/apps/server/dist/gateway/ws-push.js +115 -0
  29. package/apps/server/dist/loid/brain.js +121 -0
  30. package/apps/server/dist/loid/clarifier.js +162 -0
  31. package/apps/server/dist/loid/context-builder.js +462 -0
  32. package/apps/server/dist/loid/mcp-server.js +119 -0
  33. package/apps/server/dist/loid/memory-settler.js +189 -0
  34. package/apps/server/dist/loid/opportunity-manager.js +148 -0
  35. package/apps/server/dist/loid/profile-updater.js +179 -0
  36. package/apps/server/dist/loid/project-registry.js +192 -0
  37. package/apps/server/dist/loid/reporter.js +148 -0
  38. package/apps/server/dist/loid/schemas.js +117 -0
  39. package/apps/server/dist/loid/self-calibrator.js +314 -0
  40. package/apps/server/dist/loid/session-manager.js +472 -0
  41. package/apps/server/dist/loid/session.js +276 -0
  42. package/apps/server/dist/main.js +528 -0
  43. package/apps/server/dist/tracing/index.js +2 -0
  44. package/apps/server/dist/tracing/trace-context.js +92 -0
  45. package/apps/server/dist/types/message.js +2 -0
  46. package/apps/server/dist/yor/yor-mcp-server.js +107 -0
  47. package/apps/server/dist/yor/yor-orchestrator.js +248 -0
  48. package/apps/web/dist/assets/index-BiiEB0qZ.css +1 -0
  49. package/apps/web/dist/assets/index-Dnb9LGZd.js +798 -0
  50. package/apps/web/dist/index.html +13 -0
  51. package/package.json +42 -0
  52. package/packages/cc-client/dist/claude-code-backend.js +792 -0
  53. package/packages/cc-client/dist/index.js +2 -0
  54. package/packages/cc-client/package.json +11 -0
  55. package/packages/core/dist/constants.js +60 -0
  56. package/packages/core/dist/errors.js +35 -0
  57. package/packages/core/dist/index.js +9 -0
  58. package/packages/core/dist/office-init.js +190 -0
  59. package/packages/core/dist/repo-cache.js +70 -0
  60. package/packages/core/dist/scope/checker.js +114 -0
  61. package/packages/core/dist/scope/defaults.js +55 -0
  62. package/packages/core/dist/scope/index.js +3 -0
  63. package/packages/core/dist/state-machine.js +86 -0
  64. package/packages/core/dist/types/audit.js +12 -0
  65. package/packages/core/dist/types/backend.js +2 -0
  66. package/packages/core/dist/types/commitment.js +17 -0
  67. package/packages/core/dist/types/communication.js +18 -0
  68. package/packages/core/dist/types/index.js +9 -0
  69. package/packages/core/dist/types/opportunity.js +27 -0
  70. package/packages/core/dist/types/org.js +26 -0
  71. package/packages/core/dist/types/task.js +46 -0
  72. package/packages/core/dist/types/workspace.js +39 -0
  73. package/packages/core/dist/workspace-manager.js +314 -0
  74. package/packages/core/package.json +10 -0
  75. package/packages/db/dist/client.js +69 -0
  76. package/packages/db/dist/index.js +756 -0
  77. package/packages/db/dist/schema/audit-events.js +13 -0
  78. package/packages/db/dist/schema/cc-sessions.js +14 -0
  79. package/packages/db/dist/schema/chats.js +35 -0
  80. package/packages/db/dist/schema/commitments.js +18 -0
  81. package/packages/db/dist/schema/communication-events.js +14 -0
  82. package/packages/db/dist/schema/index.js +14 -0
  83. package/packages/db/dist/schema/message-log.js +20 -0
  84. package/packages/db/dist/schema/opportunities.js +23 -0
  85. package/packages/db/dist/schema/org.js +36 -0
  86. package/packages/db/dist/schema/projects.js +23 -0
  87. package/packages/db/dist/schema/tasks.js +51 -0
  88. package/packages/db/dist/schema/topics.js +22 -0
  89. package/packages/db/dist/schema/trace-spans.js +19 -0
  90. package/packages/db/dist/schema/workspaces.js +15 -0
  91. package/packages/db/package.json +12 -0
  92. package/packages/db/src/migrations/0000_baseline.sql +251 -0
  93. package/packages/db/src/migrations/0001_workspaces.sql +19 -0
  94. package/packages/db/src/migrations/0002_workspace_parent.sql +1 -0
  95. package/packages/db/src/migrations/0003_chat_context.sql +3 -0
  96. package/packages/db/src/migrations/meta/_journal.json +34 -0
  97. package/packages/mcp-tools/dist/index.js +41 -0
  98. package/packages/mcp-tools/dist/layer1/audit-append.js +38 -0
  99. package/packages/mcp-tools/dist/layer1/audit-query.js +51 -0
  100. package/packages/mcp-tools/dist/layer1/memory-brief.js +168 -0
  101. package/packages/mcp-tools/dist/layer1/memory-context.js +124 -0
  102. package/packages/mcp-tools/dist/layer1/memory-digest.js +126 -0
  103. package/packages/mcp-tools/dist/layer1/memory-forget.js +108 -0
  104. package/packages/mcp-tools/dist/layer1/memory-learn.js +63 -0
  105. package/packages/mcp-tools/dist/layer1/memory-recall.js +287 -0
  106. package/packages/mcp-tools/dist/layer1/memory-reflect.js +80 -0
  107. package/packages/mcp-tools/dist/layer1/memory-remember.js +119 -0
  108. package/packages/mcp-tools/dist/layer1/memory-search.js +263 -0
  109. package/packages/mcp-tools/dist/layer1/memory-write.js +21 -0
  110. package/packages/mcp-tools/dist/layer1/org-lookup.js +47 -0
  111. package/packages/mcp-tools/dist/layer1/project-get.js +28 -0
  112. package/packages/mcp-tools/dist/layer1/project-list.js +20 -0
  113. package/packages/mcp-tools/dist/layer1/report-daily.js +68 -0
  114. package/packages/mcp-tools/dist/layer1/task-get.js +29 -0
  115. package/packages/mcp-tools/dist/layer1/task-update.js +34 -0
  116. package/packages/mcp-tools/dist/layer2/franky/topic-checkpoint.js +43 -0
  117. package/packages/mcp-tools/dist/layer2/franky/topic-escalate.js +19 -0
  118. package/packages/mcp-tools/dist/layer2/loid/decision-log.js +15 -0
  119. package/packages/mcp-tools/dist/layer2/loid/decision-no-action.js +15 -0
  120. package/packages/mcp-tools/dist/layer2/loid/delivery-create-pr.js +30 -0
  121. package/packages/mcp-tools/dist/layer2/loid/delivery-share.js +12 -0
  122. package/packages/mcp-tools/dist/layer2/loid/delivery-submit.js +77 -0
  123. package/packages/mcp-tools/dist/layer2/loid/delivery-upload.js +18 -0
  124. package/packages/mcp-tools/dist/layer2/loid/project-remove.js +16 -0
  125. package/packages/mcp-tools/dist/layer2/loid/project-upsert.js +33 -0
  126. package/packages/mcp-tools/dist/layer2/loid/task-dispatch.js +206 -0
  127. package/packages/mcp-tools/dist/layer2/loid/task-escalate-to-topic.js +170 -0
  128. package/packages/mcp-tools/dist/layer2/loid/task-lookup.js +45 -0
  129. package/packages/mcp-tools/dist/layer2/loid/topic-close.js +22 -0
  130. package/packages/mcp-tools/dist/layer2/loid/topic-create.js +60 -0
  131. package/packages/mcp-tools/dist/layer2/loid/yor-approve.js +8 -0
  132. package/packages/mcp-tools/dist/layer2/loid/yor-kill.js +7 -0
  133. package/packages/mcp-tools/dist/layer2/loid/yor-rework.js +7 -0
  134. package/packages/mcp-tools/dist/layer2/loid/yor-spawn.js +28 -0
  135. package/packages/mcp-tools/dist/layer2/loid/yor-status.js +8 -0
  136. package/packages/mcp-tools/dist/layer2/yor/task-block.js +11 -0
  137. package/packages/mcp-tools/dist/layer2/yor/task-deliver.js +35 -0
  138. package/packages/mcp-tools/dist/layer2/yor/task-progress.js +21 -0
  139. package/packages/mcp-tools/dist/layer3/adapters/feishu-adapter.js +203 -0
  140. package/packages/mcp-tools/dist/layer3/adapters/types.js +28 -0
  141. package/packages/mcp-tools/dist/layer3/channel-receive.js +11 -0
  142. package/packages/mcp-tools/dist/layer3/channel-send.js +75 -0
  143. package/packages/mcp-tools/dist/layer3/file-upload.js +44 -0
  144. package/packages/mcp-tools/dist/registry.js +911 -0
  145. package/packages/mcp-tools/package.json +13 -0
@@ -0,0 +1,508 @@
1
+ import * as Lark from '@larksuiteoapi/node-sdk';
2
+ import { insertMessageLog, getMessageLogBySourceRef } from '@team-anya/db';
3
+ import { messageEvents } from './message-events.js';
4
+ // ── markdownToFeishuCard ──
5
+ /**
6
+ * 将 markdown 内容转换为飞书交互式卡片格式
7
+ */
8
+ export function markdownToFeishuCard(content, title) {
9
+ return {
10
+ schema: '2.0',
11
+ config: { wide_screen_mode: true },
12
+ header: {
13
+ title: { content: title, tag: 'plain_text' },
14
+ },
15
+ body: {
16
+ elements: [
17
+ { tag: 'markdown', content },
18
+ ],
19
+ },
20
+ };
21
+ }
22
+ // ── FeishuSender(基于官方 SDK)──
23
+ /**
24
+ * 飞书消息发送器
25
+ *
26
+ * 使用 @larksuiteoapi/node-sdk 的 Client,SDK 内部自动处理 token 获取和缓存。
27
+ *
28
+ * 出站场景:
29
+ * - sendText / sendReply: 日常对话(纯文本,像正常同事说话)
30
+ * - sendBlockedAlert: 任务阻塞提醒
31
+ * - sendDailyReport: 日报推送(卡片,内容较长)
32
+ * - sendTaskDone: 任务完成通知(纯文本)
33
+ */
34
+ export class FeishuSender {
35
+ client;
36
+ db;
37
+ constructor(config) {
38
+ this.client = new Lark.Client({
39
+ appId: config.appId,
40
+ appSecret: config.appSecret,
41
+ appType: Lark.AppType.SelfBuild,
42
+ domain: Lark.Domain.Feishu,
43
+ });
44
+ this.db = config.db ?? null;
45
+ }
46
+ /**
47
+ * 写入 outbound message_log 并 emit 事件
48
+ */
49
+ logOutbound(params) {
50
+ if (!this.db)
51
+ return;
52
+ try {
53
+ const chatId = params.receiveIdType === 'chat_id' ? params.receiver : null;
54
+ const chatType = params.receiveIdType === 'chat_id' ? 'group'
55
+ : params.receiveIdType === 'open_id' ? 'p2p' : null;
56
+ // 构建 metadata(话题回复标记等)
57
+ const meta = {};
58
+ if (params.replyToMessageId)
59
+ meta.parent_message_id = params.replyToMessageId;
60
+ // 优先:使用飞书 API 响应中的 thread_id(权威数据源)
61
+ if (params.threadId) {
62
+ meta.thread_id = params.threadId;
63
+ }
64
+ // 兜底 1:反查父消息的 thread_id
65
+ if (!meta.thread_id && params.replyToMessageId && this.db) {
66
+ const parent = getMessageLogBySourceRef(this.db, params.replyToMessageId);
67
+ if (parent?.metadata) {
68
+ try {
69
+ const parentMeta = JSON.parse(parent.metadata);
70
+ if (parentMeta.thread_id)
71
+ meta.thread_id = parentMeta.thread_id;
72
+ }
73
+ catch { /* ignore */ }
74
+ }
75
+ }
76
+ // 兜底 2:创建新话题时父消息还没有 thread_id,用显式标记
77
+ if (!meta.thread_id && params.replyInThread) {
78
+ meta.thread_id = params.replyToMessageId;
79
+ }
80
+ const metadataStr = Object.keys(meta).length > 0 ? JSON.stringify(meta) : null;
81
+ const entry = insertMessageLog(this.db, {
82
+ direction: 'outbound',
83
+ source_type: 'feishu',
84
+ source_ref: params.sourceRef ?? null,
85
+ sender: 'anya',
86
+ receiver: params.receiver,
87
+ chat_id: chatId,
88
+ chat_type: chatType,
89
+ content: params.content,
90
+ message_type: params.message_type,
91
+ related_task_id: params.related_task_id ?? null,
92
+ metadata: metadataStr,
93
+ });
94
+ messageEvents.emit('message', {
95
+ id: entry.id,
96
+ direction: 'outbound',
97
+ source_type: entry.source_type,
98
+ sender: entry.sender,
99
+ receiver: entry.receiver,
100
+ content: entry.content,
101
+ message_type: entry.message_type,
102
+ related_task_id: entry.related_task_id,
103
+ chat_id: entry.chat_id ?? null,
104
+ chat_type: entry.chat_type,
105
+ created_at: entry.created_at,
106
+ });
107
+ }
108
+ catch {
109
+ // 日志写入失败不应阻断消息发送
110
+ }
111
+ }
112
+ // ── 消息发送 ──
113
+ /** 从飞书 API 响应中提取 thread_id */
114
+ static extractThreadId(data) {
115
+ return data?.thread_id ?? undefined;
116
+ }
117
+ async sendCard(params) {
118
+ const content = JSON.stringify(params.card);
119
+ let messageId;
120
+ let threadId;
121
+ // 回复消息
122
+ if (params.replyToMessageId) {
123
+ const response = await this.client.im.message.reply({
124
+ path: { message_id: params.replyToMessageId },
125
+ data: {
126
+ content,
127
+ msg_type: 'interactive',
128
+ },
129
+ });
130
+ if (response.code !== 0) {
131
+ throw new Error(`飞书回复失败: ${response.msg || `code ${response.code}`}`);
132
+ }
133
+ messageId = response.data?.message_id ?? 'unknown';
134
+ threadId = FeishuSender.extractThreadId(response.data);
135
+ }
136
+ else {
137
+ // 发送新消息
138
+ const response = await this.client.im.message.create({
139
+ params: { receive_id_type: params.receiveIdType },
140
+ data: {
141
+ receive_id: params.receiveId,
142
+ content,
143
+ msg_type: 'interactive',
144
+ },
145
+ });
146
+ if (response.code !== 0) {
147
+ throw new Error(`飞书发送失败: ${response.msg || `code ${response.code}`}`);
148
+ }
149
+ messageId = response.data?.message_id ?? 'unknown';
150
+ threadId = FeishuSender.extractThreadId(response.data);
151
+ }
152
+ this.logOutbound({
153
+ receiver: params.receiveId,
154
+ content: params.log?.contentOverride ?? content.slice(0, 500),
155
+ message_type: params.log?.messageType ?? 'interactive',
156
+ sourceRef: messageId,
157
+ receiveIdType: params.receiveIdType,
158
+ related_task_id: params.log?.relatedTaskId,
159
+ replyToMessageId: params.replyToMessageId,
160
+ threadId,
161
+ replyInThread: params.log?.replyInThread,
162
+ });
163
+ return messageId;
164
+ }
165
+ // ── 发送纯文本 ──
166
+ async sendText(params) {
167
+ const response = await this.client.im.message.create({
168
+ params: { receive_id_type: params.receiveIdType },
169
+ data: {
170
+ receive_id: params.receiveId,
171
+ content: JSON.stringify({ text: params.text }),
172
+ msg_type: 'text',
173
+ },
174
+ });
175
+ if (response.code !== 0) {
176
+ throw new Error(`飞书发送失败: ${response.msg || `code ${response.code}`}`);
177
+ }
178
+ const messageId = response.data?.message_id ?? 'unknown';
179
+ const threadId = FeishuSender.extractThreadId(response.data);
180
+ this.logOutbound({
181
+ receiver: params.receiveId,
182
+ content: params.text,
183
+ message_type: params.log?.messageType ?? 'message',
184
+ sourceRef: messageId,
185
+ receiveIdType: params.receiveIdType,
186
+ related_task_id: params.log?.relatedTaskId,
187
+ replyToMessageId: params.log?.replyToMessageId,
188
+ threadId,
189
+ replyInThread: params.log?.replyInThread,
190
+ });
191
+ return messageId;
192
+ }
193
+ // ── 回复消息(纯文本,像正常同事说话)──
194
+ async sendReply(params) {
195
+ const response = await this.client.im.message.reply({
196
+ path: { message_id: params.replyToMessageId },
197
+ data: {
198
+ content: JSON.stringify({ text: params.text }),
199
+ msg_type: 'text',
200
+ ...(params.replyInThread ? { reply_in_thread: true } : {}),
201
+ },
202
+ });
203
+ if (response.code !== 0) {
204
+ throw new Error(`飞书回复失败: ${response.msg || `code ${response.code}`}`);
205
+ }
206
+ const messageId = response.data?.message_id ?? 'unknown';
207
+ const threadId = FeishuSender.extractThreadId(response.data);
208
+ this.logOutbound({
209
+ receiver: params.chatId ?? params.replyToMessageId,
210
+ content: params.text,
211
+ message_type: params.log?.messageType ?? 'message',
212
+ sourceRef: messageId,
213
+ receiveIdType: params.chatId ? 'chat_id' : undefined,
214
+ related_task_id: params.log?.relatedTaskId,
215
+ replyToMessageId: params.replyToMessageId,
216
+ threadId,
217
+ replyInThread: params.replyInThread,
218
+ });
219
+ return messageId;
220
+ }
221
+ /**
222
+ * 获取回复消息的 thread_id(创建话题后从响应中提取)
223
+ */
224
+ async sendReplyInThread(params) {
225
+ const response = await this.client.im.message.reply({
226
+ path: { message_id: params.replyToMessageId },
227
+ data: {
228
+ content: JSON.stringify({ text: params.text }),
229
+ msg_type: 'text',
230
+ reply_in_thread: true,
231
+ },
232
+ });
233
+ if (response.code !== 0) {
234
+ throw new Error(`飞书创建话题失败: ${response.msg || `code ${response.code}`}`);
235
+ }
236
+ const messageId = response.data?.message_id ?? 'unknown';
237
+ const threadId = FeishuSender.extractThreadId(response.data) ?? null;
238
+ this.logOutbound({
239
+ receiver: params.chatId ?? params.replyToMessageId,
240
+ content: params.text,
241
+ message_type: 'message',
242
+ sourceRef: messageId,
243
+ receiveIdType: params.chatId ? 'chat_id' : undefined,
244
+ replyToMessageId: params.replyToMessageId,
245
+ threadId: threadId ?? undefined,
246
+ replyInThread: true,
247
+ });
248
+ return { messageId, threadId };
249
+ }
250
+ // ── 图片上传与发送 ──
251
+ /**
252
+ * 上传图片到飞书(获取 image_key)
253
+ *
254
+ * 调用 POST /im/v1/images 接口。
255
+ * 支持格式: png, jpg, jpeg, gif, bmp, webp, ico, tiff, tif
256
+ */
257
+ async uploadImage(filePath) {
258
+ const fs = await import('node:fs');
259
+ const fileStream = fs.createReadStream(filePath);
260
+ const response = await this.client.im.image.create({
261
+ data: {
262
+ image_type: 'message',
263
+ image: fileStream,
264
+ },
265
+ });
266
+ const result = response;
267
+ if (result.code != null && result.code !== 0) {
268
+ throw new Error(`飞书图片上传失败: ${result.msg || `code ${result.code}`}`);
269
+ }
270
+ const imageKey = result.data?.image_key ?? result.image_key;
271
+ if (!imageKey) {
272
+ throw new Error('飞书图片上传成功但未返回 image_key');
273
+ }
274
+ return imageKey;
275
+ }
276
+ /**
277
+ * 发送图片消息
278
+ */
279
+ async sendImage(params) {
280
+ const content = JSON.stringify({ image_key: params.imageKey });
281
+ let messageId;
282
+ let threadId;
283
+ if (params.replyToMessageId) {
284
+ const response = await this.client.im.message.reply({
285
+ path: { message_id: params.replyToMessageId },
286
+ data: {
287
+ content,
288
+ msg_type: 'image',
289
+ },
290
+ });
291
+ if (response.code !== 0) {
292
+ throw new Error(`飞书图片回复失败: ${response.msg || `code ${response.code}`}`);
293
+ }
294
+ messageId = response.data?.message_id ?? 'unknown';
295
+ threadId = FeishuSender.extractThreadId(response.data);
296
+ }
297
+ else {
298
+ const response = await this.client.im.message.create({
299
+ params: { receive_id_type: params.receiveIdType },
300
+ data: {
301
+ receive_id: params.receiveId,
302
+ content,
303
+ msg_type: 'image',
304
+ },
305
+ });
306
+ if (response.code !== 0) {
307
+ throw new Error(`飞书图片发送失败: ${response.msg || `code ${response.code}`}`);
308
+ }
309
+ messageId = response.data?.message_id ?? 'unknown';
310
+ threadId = FeishuSender.extractThreadId(response.data);
311
+ }
312
+ this.logOutbound({
313
+ receiver: params.receiveId,
314
+ content: `[图片] ${params.imageKey}`,
315
+ message_type: 'image',
316
+ sourceRef: messageId,
317
+ receiveIdType: params.receiveIdType,
318
+ replyToMessageId: params.replyToMessageId,
319
+ threadId,
320
+ });
321
+ return messageId;
322
+ }
323
+ // ── 文件上传与发送 ──
324
+ /**
325
+ * 上传文件到飞书(获取 file_key)
326
+ *
327
+ * 调用 POST /im/v1/files 接口。
328
+ */
329
+ async uploadFile(params) {
330
+ const fs = await import('node:fs');
331
+ const path = await import('node:path');
332
+ const fileStream = fs.createReadStream(params.filePath);
333
+ const finalName = params.fileName || path.basename(params.filePath);
334
+ const fileType = params.fileType || 'stream';
335
+ const response = await this.client.im.file.create({
336
+ data: {
337
+ file_type: fileType,
338
+ file_name: finalName,
339
+ file: fileStream,
340
+ ...(params.duration != null ? { duration: params.duration } : {}),
341
+ },
342
+ });
343
+ // im.file.create 返回 { file_key?: string } 或含错误信息
344
+ const result = response;
345
+ if (result.code != null && result.code !== 0) {
346
+ throw new Error(`飞书文件上传失败: ${result.msg || `code ${result.code}`}`);
347
+ }
348
+ const fileKey = result.data?.file_key ?? result.file_key;
349
+ if (!fileKey) {
350
+ throw new Error('飞书文件上传成功但未返回 file_key');
351
+ }
352
+ return fileKey;
353
+ }
354
+ /**
355
+ * 发送文件消息
356
+ *
357
+ * @param msgType - 消息类型:'file'(默认)或 'audio'(opus 语音)
358
+ */
359
+ async sendFile(params) {
360
+ const msgType = params.msgType ?? 'file';
361
+ const content = JSON.stringify({ file_key: params.fileKey });
362
+ let messageId;
363
+ let threadId;
364
+ if (params.replyToMessageId) {
365
+ const response = await this.client.im.message.reply({
366
+ path: { message_id: params.replyToMessageId },
367
+ data: {
368
+ content,
369
+ msg_type: msgType,
370
+ },
371
+ });
372
+ if (response.code !== 0) {
373
+ throw new Error(`飞书${msgType === 'audio' ? '语音' : '文件'}回复失败: ${response.msg || `code ${response.code}`}`);
374
+ }
375
+ messageId = response.data?.message_id ?? 'unknown';
376
+ threadId = FeishuSender.extractThreadId(response.data);
377
+ }
378
+ else {
379
+ const response = await this.client.im.message.create({
380
+ params: { receive_id_type: params.receiveIdType },
381
+ data: {
382
+ receive_id: params.receiveId,
383
+ content,
384
+ msg_type: msgType,
385
+ },
386
+ });
387
+ if (response.code !== 0) {
388
+ throw new Error(`飞书${msgType === 'audio' ? '语音' : '文件'}发送失败: ${response.msg || `code ${response.code}`}`);
389
+ }
390
+ messageId = response.data?.message_id ?? 'unknown';
391
+ threadId = FeishuSender.extractThreadId(response.data);
392
+ }
393
+ this.logOutbound({
394
+ receiver: params.receiveId,
395
+ content: `[${msgType === 'audio' ? '语音' : '文件'}] ${params.fileKey}`,
396
+ message_type: msgType,
397
+ sourceRef: messageId,
398
+ receiveIdType: params.receiveIdType,
399
+ replyToMessageId: params.replyToMessageId,
400
+ threadId,
401
+ });
402
+ return messageId;
403
+ }
404
+ // ── 表情回复(Reaction)──
405
+ /** "处理中"指示 emoji,收到消息时添加,回复后移除 */
406
+ static TYPING_EMOJI = 'OneSecond';
407
+ /** 待清除的 typing reaction,key 为 chatId */
408
+ pendingTypingReactions = new Map();
409
+ /**
410
+ * 给消息添加"处理中"表情,并记录到 pending 映射
411
+ *
412
+ * 收到消息后立即调用,大模型回复或出错时通过 clearTypingReaction 移除。
413
+ */
414
+ async addTypingReaction(chatId, messageId) {
415
+ const reactionId = await this.addReaction(messageId, FeishuSender.TYPING_EMOJI);
416
+ if (reactionId) {
417
+ this.pendingTypingReactions.set(chatId, { messageId, reactionId });
418
+ }
419
+ }
420
+ /**
421
+ * 清除指定群聊的"处理中"表情
422
+ *
423
+ * 在大模型回复、冷启动回复、或出错回复时调用。
424
+ */
425
+ async clearTypingReaction(chatId) {
426
+ const pending = this.pendingTypingReactions.get(chatId);
427
+ if (!pending)
428
+ return;
429
+ this.pendingTypingReactions.delete(chatId);
430
+ await this.deleteReaction(pending.messageId, pending.reactionId);
431
+ }
432
+ /**
433
+ * 给消息添加表情回复
434
+ *
435
+ * 调用飞书 POST /im/v1/messages/{message_id}/reactions
436
+ */
437
+ async addReaction(messageId, emojiType) {
438
+ try {
439
+ const response = await this.client.im.messageReaction.create({
440
+ path: { message_id: messageId },
441
+ data: {
442
+ reaction_type: { emoji_type: emojiType },
443
+ },
444
+ });
445
+ if (response.code !== 0) {
446
+ console.warn(`[feishu-sender] 添加表情回复失败: ${response.msg || `code ${response.code}`}`);
447
+ return null;
448
+ }
449
+ return response.data?.reaction_id ?? null;
450
+ }
451
+ catch (err) {
452
+ console.warn('[feishu-sender] 添加表情回复异常:', err);
453
+ return null;
454
+ }
455
+ }
456
+ /**
457
+ * 删除消息上的表情回复
458
+ *
459
+ * 调用飞书 DELETE /im/v1/messages/{message_id}/reactions/{reaction_id}
460
+ */
461
+ async deleteReaction(messageId, reactionId) {
462
+ try {
463
+ const response = await this.client.im.messageReaction.delete({
464
+ path: { message_id: messageId, reaction_id: reactionId },
465
+ });
466
+ if (response.code !== 0) {
467
+ console.warn(`[feishu-sender] 删除表情回复失败: ${response.msg || `code ${response.code}`}`);
468
+ }
469
+ }
470
+ catch (err) {
471
+ console.warn('[feishu-sender] 删除表情回复异常:', err);
472
+ }
473
+ }
474
+ // ── 出站场景 ──
475
+ async sendBlockedAlert(params) {
476
+ // 用自然语言,不用制式模板
477
+ const text = `${params.taskId} 卡住了——${params.reason}`;
478
+ const results = await Promise.allSettled(params.targetUserIds.map(userId => this.sendText({
479
+ receiveIdType: 'open_id',
480
+ receiveId: userId,
481
+ text,
482
+ log: { messageType: 'alert', relatedTaskId: params.taskId },
483
+ })));
484
+ return results;
485
+ }
486
+ async sendDailyReport(params) {
487
+ // 日报内容较长,保留卡片格式便于阅读
488
+ const card = markdownToFeishuCard(params.reportContent, `${params.date} 工作小结`);
489
+ await this.sendCard({
490
+ receiveIdType: 'chat_id',
491
+ receiveId: params.chatId,
492
+ card,
493
+ log: { messageType: 'report', contentOverride: params.reportContent },
494
+ });
495
+ }
496
+ async sendTaskDone(params) {
497
+ // 用自然语言,像同事在群里说"搞定了"
498
+ const prPart = params.prUrl ? `,PR: ${params.prUrl}` : '';
499
+ const text = `${params.taskId} 搞定了——${params.summary}${prPart}`;
500
+ await this.sendText({
501
+ receiveIdType: 'chat_id',
502
+ receiveId: params.chatId,
503
+ text,
504
+ log: { messageType: 'task_done', relatedTaskId: params.taskId },
505
+ });
506
+ }
507
+ }
508
+ //# sourceMappingURL=feishu-sender.js.map