evolclaw 2.1.2 → 2.3.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.
- package/README.md +59 -30
- package/data/evolclaw.sample.json +15 -4
- package/dist/agents/claude-runner.js +685 -0
- package/dist/agents/codex-runner.js +315 -0
- package/dist/agents/gemini-runner.js +425 -0
- package/dist/channels/aun.js +580 -10
- package/dist/channels/feishu.js +888 -135
- package/dist/channels/wechat.js +127 -21
- package/dist/cli.js +519 -136
- package/dist/config.js +277 -25
- package/dist/core/agent-loader.js +39 -0
- package/dist/core/channel-loader.js +67 -0
- package/dist/core/command-handler.js +1537 -392
- package/dist/core/event-bus.js +32 -0
- package/dist/core/interaction-router.js +68 -0
- package/dist/core/message/message-bridge.js +216 -0
- package/dist/core/message/message-processor.js +1028 -0
- package/dist/core/message/message-queue.js +240 -0
- package/dist/core/message/stream-debouncer.js +122 -0
- package/dist/{utils → core/message}/stream-flusher.js +73 -13
- package/dist/{utils → core/message}/stream-idle-monitor.js +1 -1
- package/dist/core/permission.js +259 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +144 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +261 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +177 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-manager.js +1072 -0
- package/dist/index.js +402 -252
- package/dist/ipc.js +106 -0
- package/dist/paths.js +1 -0
- package/dist/types.js +3 -0
- package/dist/utils/{platform.js → cross-platform.js} +38 -1
- package/dist/utils/error-utils.js +130 -5
- package/dist/utils/init-channel.js +649 -0
- package/dist/utils/init.js +190 -53
- package/dist/utils/logger.js +8 -3
- package/dist/utils/media-cache.js +207 -0
- package/dist/utils/migrate-project.js +122 -0
- package/dist/utils/rich-content-renderer.js +228 -0
- package/dist/utils/stats-collector.js +102 -0
- package/package.json +4 -2
- package/dist/core/agent-runner.js +0 -348
- package/dist/core/message-processor.js +0 -604
- package/dist/core/message-queue.js +0 -116
- package/dist/core/message-stream.js +0 -59
- package/dist/core/session-manager.js +0 -664
- package/dist/index.js.bak +0 -340
- package/dist/utils/init-feishu.js +0 -261
- package/dist/utils/init-wechat.js +0 -170
- package/dist/utils/markdown-to-feishu.js +0 -94
- package/dist/utils/permission.js +0 -43
- package/dist/utils/session-file-health.js +0 -68
- /package/dist/core/{message-cache.js → message/message-cache.js} +0 -0
package/dist/channels/feishu.js
CHANGED
|
@@ -2,31 +2,36 @@ import * as lark from '@larksuiteoapi/node-sdk';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import imageType from 'image-type';
|
|
5
|
-
import {
|
|
5
|
+
import { sanitizeFileName, saveToUploads, validateImage } from '../utils/media-cache.js';
|
|
6
6
|
import { logger } from '../utils/logger.js';
|
|
7
|
-
import {
|
|
7
|
+
import { hasRichContent, renderAllRichContent, checkDependencies } from '../utils/rich-content-renderer.js';
|
|
8
8
|
export class FeishuChannel {
|
|
9
9
|
config;
|
|
10
10
|
client = null;
|
|
11
11
|
wsClient = null;
|
|
12
12
|
messageHandler;
|
|
13
13
|
projectPathProvider;
|
|
14
|
-
db;
|
|
15
14
|
cleanupInterval;
|
|
16
|
-
|
|
15
|
+
seenMessages = new Map(); // messageId -> timestamp
|
|
16
|
+
seenThreads = new Set(); // 已见的 thread_id,用于判断话题创建消息
|
|
17
|
+
userNameCache = new Map(); // userId -> userName
|
|
18
|
+
recallHandler;
|
|
19
|
+
interactionCallback;
|
|
20
|
+
connected = false;
|
|
21
|
+
enableRichContent;
|
|
17
22
|
constructor(config) {
|
|
18
23
|
this.config = config;
|
|
19
|
-
this.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
this.enableRichContent = config.enableRichContent ?? false; // 默认关闭
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 预填充已知的 thread_id(重启后从数据库恢复,避免误判话题创建)
|
|
28
|
+
*/
|
|
29
|
+
preloadThreads(threadIds) {
|
|
30
|
+
for (const id of threadIds)
|
|
31
|
+
this.seenThreads.add(id);
|
|
32
|
+
if (threadIds.length > 0) {
|
|
33
|
+
logger.info(`[Feishu] Preloaded ${threadIds.length} known thread(s)`);
|
|
34
|
+
}
|
|
30
35
|
}
|
|
31
36
|
async connect() {
|
|
32
37
|
// 检查配置有效性
|
|
@@ -51,10 +56,11 @@ export class FeishuChannel {
|
|
|
51
56
|
return;
|
|
52
57
|
}
|
|
53
58
|
this.markSeen(msg.message_id);
|
|
54
|
-
this.addAckReaction(msg.message_id);
|
|
55
59
|
if (!this.messageHandler)
|
|
56
60
|
return;
|
|
57
|
-
//
|
|
61
|
+
// 提取 chatType(从 SDK 事件直接获取)
|
|
62
|
+
const chatType = msg.chat_type === 'group' ? 'group' : 'private';
|
|
63
|
+
// 话题消息检测日志
|
|
58
64
|
if (msg.thread_id) {
|
|
59
65
|
logger.info('[Feishu] Thread message, thread_id:', msg.thread_id, 'root_id:', msg.root_id);
|
|
60
66
|
}
|
|
@@ -65,24 +71,37 @@ export class FeishuChannel {
|
|
|
65
71
|
key: m.key
|
|
66
72
|
})).filter((m) => m.userId && m.userId !== this.config.appId);
|
|
67
73
|
// 提取发送者信息
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
const peerId = data.sender?.sender_id?.open_id;
|
|
75
|
+
// 尝试从 mentions 中查找发送者名字(群聊中可能包含)
|
|
76
|
+
let peerName;
|
|
77
|
+
if (mentions.length > 0) {
|
|
78
|
+
const senderMention = mentions.find((m) => m.userId === peerId);
|
|
79
|
+
peerName = senderMention?.name;
|
|
72
80
|
}
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
// 如果 mentions 中没有,尝试调用 API 获取
|
|
82
|
+
if (!peerName && peerId) {
|
|
83
|
+
try {
|
|
84
|
+
peerName = await this.getUserName(peerId);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger.debug('[Feishu] getUserName error:', err);
|
|
88
|
+
}
|
|
75
89
|
}
|
|
76
90
|
try {
|
|
77
91
|
// 提取话题信息
|
|
78
92
|
const threadId = msg.thread_id || undefined;
|
|
79
93
|
const rootId = msg.root_id || undefined;
|
|
80
|
-
//
|
|
94
|
+
// 处理引用消息(话题内后续消息跳过,避免每条都拼接引用前缀)
|
|
81
95
|
let quotedText = '';
|
|
82
96
|
let quotedImages = [];
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
// 引用消息处理:
|
|
98
|
+
// - 非话题:直接回复某条消息时,拉取被引用的消息内容
|
|
99
|
+
// - 话题首条:创建话题时,拉取根消息内容作为上下文
|
|
100
|
+
// - 话题后续:不拉取(上下文由 session 维护)
|
|
101
|
+
const isThreadCreation = !!(msg.thread_id && msg.parent_id && !this.seenThreads.has(msg.thread_id));
|
|
102
|
+
if (msg.thread_id)
|
|
103
|
+
this.seenThreads.add(msg.thread_id);
|
|
104
|
+
if (msg.parent_id && (!msg.thread_id || isThreadCreation) && this.client) {
|
|
86
105
|
try {
|
|
87
106
|
const res = await this.client.im.message.get({
|
|
88
107
|
path: { message_id: msg.parent_id }
|
|
@@ -94,7 +113,7 @@ export class FeishuChannel {
|
|
|
94
113
|
const quotedContent = res.data.items[0].body.content;
|
|
95
114
|
if (quotedMsgType === 'text') {
|
|
96
115
|
const parsed = JSON.parse(quotedContent);
|
|
97
|
-
quotedText = `> ${parsed.text}\n\n`;
|
|
116
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> ${parsed.text}\n> ================\n\n`;
|
|
98
117
|
}
|
|
99
118
|
else if (quotedMsgType === 'post') {
|
|
100
119
|
const parsed = JSON.parse(quotedContent);
|
|
@@ -110,7 +129,7 @@ export class FeishuChannel {
|
|
|
110
129
|
text += '\n';
|
|
111
130
|
}
|
|
112
131
|
}
|
|
113
|
-
quotedText = `> ${text.trim()}\n\n`;
|
|
132
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> ${text.trim()}\n> ================\n\n`;
|
|
114
133
|
}
|
|
115
134
|
else if (quotedMsgType === 'image') {
|
|
116
135
|
const parsed = JSON.parse(quotedContent);
|
|
@@ -121,10 +140,10 @@ export class FeishuChannel {
|
|
|
121
140
|
const imageData = await this.downloadAndSaveImage(imageKey, msg.chat_id, msg.parent_id, projectPath);
|
|
122
141
|
if (imageData) {
|
|
123
142
|
quotedImages.push(imageData);
|
|
124
|
-
quotedText = `> [引用的图片]\n\n`;
|
|
143
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [引用的图片]\n> ================\n\n`;
|
|
125
144
|
}
|
|
126
145
|
else {
|
|
127
|
-
quotedText = `> [图片消息]\n\n`;
|
|
146
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [图片消息]\n> ================\n\n`;
|
|
128
147
|
}
|
|
129
148
|
}
|
|
130
149
|
else if (quotedMsgType === 'file') {
|
|
@@ -136,14 +155,14 @@ export class FeishuChannel {
|
|
|
136
155
|
: process.cwd();
|
|
137
156
|
const quotedFilePath = await this.downloadFile(quotedFileKey, quotedFileName, msg.parent_id, projectPath);
|
|
138
157
|
if (quotedFilePath) {
|
|
139
|
-
quotedText = `> [引用的文件:${quotedFileName}]\n> 文件已保存到:${quotedFilePath}\n\n`;
|
|
158
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [引用的文件:${quotedFileName}]\n> 文件已保存到:${quotedFilePath}\n> ================\n\n`;
|
|
140
159
|
}
|
|
141
160
|
else {
|
|
142
|
-
quotedText = `> [文件消息]\n\n`;
|
|
161
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [文件消息]\n> ================\n\n`;
|
|
143
162
|
}
|
|
144
163
|
}
|
|
145
164
|
else {
|
|
146
|
-
quotedText = `> [${quotedMsgType}消息]\n\n`;
|
|
165
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [${quotedMsgType}消息]\n> ================\n\n`;
|
|
147
166
|
}
|
|
148
167
|
}
|
|
149
168
|
catch (err) {
|
|
@@ -154,9 +173,11 @@ export class FeishuChannel {
|
|
|
154
173
|
if (msg.message_type === 'text') {
|
|
155
174
|
const parsed = JSON.parse(msg.content);
|
|
156
175
|
// 优先使用 text_without_at_bot(去除机器人 @),否则使用 text
|
|
157
|
-
|
|
176
|
+
let content = (parsed.text_without_at_bot || parsed.text || '').trim();
|
|
177
|
+
// 清理残留的 mention 占位符(@_user_N 代表机器人)
|
|
178
|
+
content = content.replace(/@_user_\d+/g, '').trim();
|
|
158
179
|
const finalContent = quotedText + content;
|
|
159
|
-
await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: quotedImages.length > 0 ? quotedImages : undefined,
|
|
180
|
+
await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: quotedImages.length > 0 ? quotedImages : undefined, peerId, peerName, messageId: msg.message_id, mentions: mentions.length > 0 ? mentions : undefined, threadId, rootId, chatType });
|
|
160
181
|
}
|
|
161
182
|
// 处理图片消息
|
|
162
183
|
else if (msg.message_type === 'image') {
|
|
@@ -170,11 +191,11 @@ export class FeishuChannel {
|
|
|
170
191
|
if (imageData) {
|
|
171
192
|
const allImages = [...quotedImages, imageData];
|
|
172
193
|
const prompt = quotedText + '用户发送了一张图片,请分析这张图片的内容。';
|
|
173
|
-
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: allImages,
|
|
194
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: allImages, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
174
195
|
}
|
|
175
196
|
else {
|
|
176
197
|
const prompt = quotedText + '[图片下载失败] 应用可能缺少 im:message 或 im:message:readonly 权限';
|
|
177
|
-
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined,
|
|
198
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
178
199
|
}
|
|
179
200
|
}
|
|
180
201
|
// 处理文件消息
|
|
@@ -189,24 +210,34 @@ export class FeishuChannel {
|
|
|
189
210
|
const filePath = await this.downloadFile(fileKey, fileName, msg.message_id, projectPath);
|
|
190
211
|
if (filePath) {
|
|
191
212
|
const prompt = quotedText + `用户发送了文件:${fileName}\n文件已保存到:${filePath}\n请使用 Read 工具读取并分析文件内容。`;
|
|
192
|
-
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined,
|
|
213
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
193
214
|
}
|
|
194
215
|
else {
|
|
195
216
|
const prompt = quotedText + '[文件下载失败] 应用可能缺少 im:resource 权限';
|
|
196
|
-
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined,
|
|
217
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
197
218
|
}
|
|
198
219
|
}
|
|
199
220
|
// 处理富文本消息
|
|
200
221
|
else if (msg.message_type === 'post') {
|
|
201
222
|
const parsed = JSON.parse(msg.content);
|
|
202
223
|
let text = '';
|
|
224
|
+
const postImages = [];
|
|
203
225
|
const title = parsed.zh_cn?.title || parsed.en_us?.title || parsed.title;
|
|
204
226
|
const content = parsed.zh_cn?.content || parsed.en_us?.content || parsed.content;
|
|
205
227
|
if (content) {
|
|
228
|
+
const projectPath = this.projectPathProvider
|
|
229
|
+
? await this.projectPathProvider(msg.chat_id)
|
|
230
|
+
: process.cwd();
|
|
206
231
|
for (const line of content) {
|
|
207
232
|
for (const elem of line) {
|
|
208
|
-
if (elem.
|
|
233
|
+
if (elem.tag === 'img' && elem.image_key) {
|
|
234
|
+
const imageData = await this.downloadAndSaveImage(elem.image_key, msg.chat_id, msg.message_id, projectPath);
|
|
235
|
+
if (imageData)
|
|
236
|
+
postImages.push(imageData);
|
|
237
|
+
}
|
|
238
|
+
else if (elem.text) {
|
|
209
239
|
text += elem.text;
|
|
240
|
+
}
|
|
210
241
|
}
|
|
211
242
|
text += '\n';
|
|
212
243
|
}
|
|
@@ -215,27 +246,75 @@ export class FeishuChannel {
|
|
|
215
246
|
if (title)
|
|
216
247
|
finalContent = `${title}\n${finalContent}`;
|
|
217
248
|
finalContent = quotedText + finalContent;
|
|
218
|
-
|
|
249
|
+
const allImages = [...quotedImages, ...postImages];
|
|
250
|
+
await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: allImages.length > 0 ? allImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
219
251
|
}
|
|
220
252
|
// 处理其他类型消息
|
|
221
253
|
else {
|
|
222
254
|
logger.debug('[Feishu] Unsupported message type:', msg.message_type);
|
|
223
255
|
const prompt = quotedText + `[不支持的消息类型: ${msg.message_type}]`;
|
|
224
|
-
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined,
|
|
256
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
225
257
|
}
|
|
226
258
|
}
|
|
227
259
|
catch (error) {
|
|
228
260
|
logger.error('[Feishu] Failed to process message:', error);
|
|
229
261
|
}
|
|
230
262
|
},
|
|
263
|
+
'im.message.recalled_v1': async (data) => {
|
|
264
|
+
const messageId = data?.message_id;
|
|
265
|
+
if (messageId) {
|
|
266
|
+
logger.info('[Feishu] Message recalled:', messageId);
|
|
267
|
+
this.recallHandler?.(messageId);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
231
270
|
'im.message.message_read_v1': async () => { },
|
|
232
|
-
'im.message.reaction.created_v1': async () => { }
|
|
271
|
+
'im.message.reaction.created_v1': async () => { },
|
|
272
|
+
'card.action.trigger': async (data) => {
|
|
273
|
+
try {
|
|
274
|
+
const action = data?.action;
|
|
275
|
+
if (!action?.value)
|
|
276
|
+
return;
|
|
277
|
+
const value = action.value;
|
|
278
|
+
const requestId = value._request_id;
|
|
279
|
+
if (!requestId) {
|
|
280
|
+
logger.debug('[Feishu] Card action without _request_id, ignoring');
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Legacy field change (non-form select_static with _field_key): ignore silently
|
|
284
|
+
if (value._field_key) {
|
|
285
|
+
logger.debug(`[Feishu] Legacy field change: requestId=${requestId}, field=${value._field_key}`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
// Form submit: `action.form_value` contains all field values from form container
|
|
289
|
+
const formValues = action.form_value || {};
|
|
290
|
+
const response = {
|
|
291
|
+
type: 'interaction.response',
|
|
292
|
+
id: requestId,
|
|
293
|
+
action: value._action || 'submit',
|
|
294
|
+
values: { ...formValues, ...value },
|
|
295
|
+
operatorId: data.operator?.open_id,
|
|
296
|
+
};
|
|
297
|
+
// Remove internal fields from values
|
|
298
|
+
delete response.values._request_id;
|
|
299
|
+
delete response.values._action;
|
|
300
|
+
delete response.values._card_title;
|
|
301
|
+
logger.info(`[Feishu] Card action: requestId=${requestId}, action=${response.action}, values=${JSON.stringify(response.values)}`);
|
|
302
|
+
this.interactionCallback?.(response);
|
|
303
|
+
// Return updated card (buttons disabled + result shown)
|
|
304
|
+
const cardTitle = value._card_title || '操作';
|
|
305
|
+
return this.buildResolvedCard(cardTitle, response);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
logger.error('[Feishu] Failed to handle card action:', err);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
233
311
|
});
|
|
234
312
|
this.wsClient = new lark.WSClient({
|
|
235
313
|
appId: this.config.appId,
|
|
236
314
|
appSecret: this.config.appSecret,
|
|
237
315
|
});
|
|
238
316
|
await this.wsClient.start({ eventDispatcher });
|
|
317
|
+
this.connected = true;
|
|
239
318
|
this.startCleanupTask();
|
|
240
319
|
}
|
|
241
320
|
catch (error) {
|
|
@@ -248,43 +327,36 @@ export class FeishuChannel {
|
|
|
248
327
|
onMessage(handler) {
|
|
249
328
|
this.messageHandler = handler;
|
|
250
329
|
}
|
|
330
|
+
onRecall(handler) {
|
|
331
|
+
this.recallHandler = handler;
|
|
332
|
+
}
|
|
333
|
+
onInteraction(callback) {
|
|
334
|
+
this.interactionCallback = callback;
|
|
335
|
+
}
|
|
251
336
|
onProjectPathRequest(provider) {
|
|
252
337
|
this.projectPathProvider = provider;
|
|
253
338
|
}
|
|
254
|
-
async
|
|
255
|
-
|
|
339
|
+
async getUserName(userId) {
|
|
340
|
+
if (!userId || !this.client)
|
|
341
|
+
return undefined;
|
|
256
342
|
// 检查缓存
|
|
257
|
-
if (this.
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
261
|
-
// 检查数据库
|
|
262
|
-
const row = this.db.prepare('SELECT chat_mode FROM chat_types WHERE chat_id = ?').get(chatId);
|
|
263
|
-
if (row) {
|
|
264
|
-
logger.info(`[Feishu] getChatMode from db: ${row.chat_mode}`);
|
|
265
|
-
this.chatTypeCache.set(chatId, row.chat_mode);
|
|
266
|
-
return row.chat_mode;
|
|
267
|
-
}
|
|
268
|
-
// 调用 API 获取
|
|
269
|
-
if (!this.client)
|
|
270
|
-
return 'p2p';
|
|
343
|
+
if (this.userNameCache.has(userId)) {
|
|
344
|
+
return this.userNameCache.get(userId);
|
|
345
|
+
}
|
|
271
346
|
try {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
347
|
+
const res = await this.client.contact.user.get({
|
|
348
|
+
path: { user_id: userId },
|
|
349
|
+
params: { user_id_type: 'open_id' }
|
|
350
|
+
});
|
|
351
|
+
const userName = res.data?.user?.name;
|
|
352
|
+
if (userName) {
|
|
353
|
+
this.userNameCache.set(userId, userName);
|
|
354
|
+
return userName;
|
|
355
|
+
}
|
|
280
356
|
}
|
|
281
|
-
catch (
|
|
282
|
-
logger.
|
|
283
|
-
return 'p2p';
|
|
357
|
+
catch (err) {
|
|
358
|
+
logger.debug('[Feishu] Failed to get user name, code:', err?.code, 'msg:', err?.message);
|
|
284
359
|
}
|
|
285
|
-
}
|
|
286
|
-
async getUserName(_userId) {
|
|
287
|
-
// TODO: 需要开通 contact:contact.base:readonly 权限后启用
|
|
288
360
|
return undefined;
|
|
289
361
|
}
|
|
290
362
|
async sendMessage(chatId, content, options) {
|
|
@@ -294,29 +366,89 @@ export class FeishuChannel {
|
|
|
294
366
|
logger.warn('[Feishu] Attempted to send empty message, skipping');
|
|
295
367
|
return;
|
|
296
368
|
}
|
|
369
|
+
// 飞书消息内容限制约 30KB(text)/ 150KB(post),安全阈值 28000 字符
|
|
370
|
+
// 超长消息自动拆分,按段落边界分割
|
|
371
|
+
const MAX_CONTENT_LENGTH = 28000;
|
|
372
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
373
|
+
logger.info(`[Feishu] Message too long (${content.length} chars), splitting into parts`);
|
|
374
|
+
const parts = splitLongMessage(content, MAX_CONTENT_LENGTH);
|
|
375
|
+
for (let i = 0; i < parts.length; i++) {
|
|
376
|
+
// 首条消息保留 reply 选项,后续消息不再 reply
|
|
377
|
+
const partOptions = i === 0 ? options : { ...options, replyToMessageId: undefined };
|
|
378
|
+
await this.sendMessage(chatId, parts[i], partOptions);
|
|
379
|
+
}
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
297
382
|
logger.debug(`[Feishu] sendMessage called, chatId: ${chatId}, content length: ${content.length}`);
|
|
298
383
|
try {
|
|
384
|
+
// 检测富内容并渲染(受 enableRichContent 开关控制,且依赖必须可用)
|
|
385
|
+
const richItems = (this.enableRichContent && checkDependencies() && hasRichContent(content))
|
|
386
|
+
? await renderAllRichContent(content)
|
|
387
|
+
: [];
|
|
388
|
+
// 上传所有图片获取 image_key,建立位置映射
|
|
389
|
+
const richItemsWithKeys = [];
|
|
390
|
+
for (const item of richItems) {
|
|
391
|
+
try {
|
|
392
|
+
const uploadResponse = await this.client.im.image.create({
|
|
393
|
+
data: { image_type: 'message', image: Buffer.from(item.png) }
|
|
394
|
+
});
|
|
395
|
+
if (uploadResponse?.image_key) {
|
|
396
|
+
richItemsWithKeys.push({ start: item.start, end: item.end, imageKey: uploadResponse.image_key });
|
|
397
|
+
logger.debug(`[Feishu] Uploaded ${item.type} image, image_key:`, uploadResponse.image_key);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
catch (err) {
|
|
401
|
+
logger.warn(`[Feishu] Failed to upload ${item.type} image:`, err);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
299
404
|
const useMarkdown = !options?.forceText && hasMarkdownSyntax(content);
|
|
300
405
|
const hasMention = !!(options?.mentionUserIds && options.mentionUserIds.length > 0);
|
|
301
|
-
|
|
302
|
-
|
|
406
|
+
const hasRichImages = richItemsWithKeys.length > 0;
|
|
407
|
+
// 消息类型决策:有 Markdown / @ / 富内容图片 → post,否则 text
|
|
408
|
+
const msgType = (useMarkdown || hasMention || hasRichImages) ? 'post' : 'text';
|
|
303
409
|
let msgContent;
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
410
|
+
if (msgType === 'post') {
|
|
411
|
+
let postData;
|
|
412
|
+
if (hasRichImages) {
|
|
413
|
+
// 有富内容图片:按位置分段文本并插入图片
|
|
414
|
+
postData = { zh_cn: { title: options?.title || '', content: [] } };
|
|
415
|
+
const sorted = [...richItemsWithKeys].sort((a, b) => a.start - b.start);
|
|
416
|
+
let lastEnd = 0;
|
|
417
|
+
for (const item of sorted) {
|
|
418
|
+
// 插入图片前的文本段
|
|
419
|
+
if (item.start > lastEnd) {
|
|
420
|
+
const textSegment = content.slice(lastEnd, item.start).trim();
|
|
421
|
+
if (textSegment) {
|
|
422
|
+
postData.zh_cn.content.push([{ tag: 'text', text: textSegment }]);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// 插入图片
|
|
426
|
+
postData.zh_cn.content.push([{ tag: 'img', image_key: item.imageKey }]);
|
|
427
|
+
lastEnd = item.end;
|
|
428
|
+
}
|
|
429
|
+
// 插入最后一段文本
|
|
430
|
+
if (lastEnd < content.length) {
|
|
431
|
+
const textSegment = content.slice(lastEnd).trim();
|
|
432
|
+
if (textSegment) {
|
|
433
|
+
postData.zh_cn.content.push([{ tag: 'text', text: textSegment }]);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
// 无富内容图片:使用原有逻辑
|
|
439
|
+
postData = useMarkdown
|
|
440
|
+
? markdownToFeishuPost(content, options?.title)
|
|
441
|
+
: { zh_cn: { title: options?.title || '', content: [[{ tag: 'text', text: content }]] } };
|
|
442
|
+
}
|
|
309
443
|
// 在第一行开头插入所有 @ 标签
|
|
310
|
-
if (postData.zh_cn.content.length > 0) {
|
|
444
|
+
if (hasMention && postData.zh_cn.content.length > 0) {
|
|
311
445
|
const atTags = options.mentionUserIds.map(uid => ({ tag: 'at', user_id: uid }));
|
|
312
446
|
postData.zh_cn.content[0].unshift(...atTags);
|
|
313
447
|
}
|
|
314
448
|
msgContent = JSON.stringify(postData);
|
|
315
449
|
}
|
|
316
450
|
else {
|
|
317
|
-
msgContent =
|
|
318
|
-
? JSON.stringify(markdownToFeishuPost(content, options?.title))
|
|
319
|
-
: JSON.stringify({ text: content });
|
|
451
|
+
msgContent = JSON.stringify({ text: content });
|
|
320
452
|
}
|
|
321
453
|
if (options?.replyToMessageId) {
|
|
322
454
|
const replyData = { msg_type: msgType, content: msgContent };
|
|
@@ -330,11 +462,16 @@ export class FeishuChannel {
|
|
|
330
462
|
}
|
|
331
463
|
else {
|
|
332
464
|
await this.client.im.message.create({
|
|
333
|
-
params: { receive_id_type: 'chat_id' },
|
|
465
|
+
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
334
466
|
data: { receive_id: chatId, msg_type: msgType, content: msgContent }
|
|
335
467
|
});
|
|
336
468
|
}
|
|
337
|
-
|
|
469
|
+
if (hasRichImages) {
|
|
470
|
+
logger.info(`[Feishu] Sent message with ${richItemsWithKeys.length} embedded images`);
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
logger.debug(`[Feishu] Sent message as ${useMarkdown ? 'post (Markdown)' : 'text'}`);
|
|
474
|
+
}
|
|
338
475
|
}
|
|
339
476
|
catch (error) {
|
|
340
477
|
// 230011: 消息已被撤回,降级为普通消息重试
|
|
@@ -342,14 +479,31 @@ export class FeishuChannel {
|
|
|
342
479
|
logger.warn('[Feishu] Message withdrawn (230011), retrying without reply');
|
|
343
480
|
return this.sendMessage(chatId, content, { ...options, replyToMessageId: undefined });
|
|
344
481
|
}
|
|
482
|
+
// 230025: 消息内容超长,截断后重试
|
|
483
|
+
if (error.response?.data?.code === 230025) {
|
|
484
|
+
logger.warn(`[Feishu] Message too long (230025, ${content.length} chars), truncating`);
|
|
485
|
+
const truncated = content.slice(0, 28000) + '\n\n⚠️ 消息过长,已截断';
|
|
486
|
+
return this.sendMessage(chatId, truncated, options);
|
|
487
|
+
}
|
|
345
488
|
logger.error('[Feishu] Failed to send message:', error);
|
|
346
489
|
throw error;
|
|
347
490
|
}
|
|
348
491
|
}
|
|
349
|
-
async sendFile(chatId, filePath) {
|
|
492
|
+
async sendFile(chatId, filePath, options) {
|
|
350
493
|
if (!this.client)
|
|
351
494
|
return;
|
|
352
495
|
try {
|
|
496
|
+
// 检测是否为图片,是则走 sendImage(内联预览)而非文件卡片
|
|
497
|
+
const header = Buffer.alloc(12);
|
|
498
|
+
const fd = fs.openSync(filePath, 'r');
|
|
499
|
+
fs.readSync(fd, header, 0, 12, 0);
|
|
500
|
+
fs.closeSync(fd);
|
|
501
|
+
const imgType = await imageType(header);
|
|
502
|
+
if (imgType) {
|
|
503
|
+
logger.info(`[Feishu] Detected image (${imgType.mime}), sending as inline image:`, filePath);
|
|
504
|
+
const buf = fs.readFileSync(filePath);
|
|
505
|
+
return this.sendImage(chatId, buf, options);
|
|
506
|
+
}
|
|
353
507
|
logger.info('[Feishu] Uploading file:', filePath);
|
|
354
508
|
const fileStream = fs.createReadStream(filePath);
|
|
355
509
|
const fileName = path.basename(filePath);
|
|
@@ -365,32 +519,104 @@ export class FeishuChannel {
|
|
|
365
519
|
return;
|
|
366
520
|
}
|
|
367
521
|
const fileKey = uploadResponse.file_key;
|
|
522
|
+
const msgContent = JSON.stringify({ file_key: fileKey });
|
|
368
523
|
logger.info('[Feishu] File uploaded, file_key:', fileKey);
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
msg_type: 'file',
|
|
374
|
-
content: JSON.stringify({ file_key: fileKey })
|
|
524
|
+
if (options?.replyToMessageId) {
|
|
525
|
+
const replyData = { msg_type: 'file', content: msgContent };
|
|
526
|
+
if (options.replyInThread) {
|
|
527
|
+
replyData.reply_in_thread = true;
|
|
375
528
|
}
|
|
376
|
-
|
|
529
|
+
await this.client.im.message.reply({
|
|
530
|
+
path: { message_id: options.replyToMessageId },
|
|
531
|
+
data: replyData
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
await this.client.im.message.create({
|
|
536
|
+
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
537
|
+
data: {
|
|
538
|
+
receive_id: chatId,
|
|
539
|
+
msg_type: 'file',
|
|
540
|
+
content: msgContent
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
}
|
|
377
544
|
logger.info('[Feishu] File message sent successfully');
|
|
378
545
|
}
|
|
379
546
|
catch (error) {
|
|
547
|
+
// 230011: 消息已被撤回,降级为普通消息重试
|
|
548
|
+
if (error.response?.data?.code === 230011 && options?.replyToMessageId) {
|
|
549
|
+
logger.warn('[Feishu] Message withdrawn (230011), retrying file send without reply');
|
|
550
|
+
return this.sendFile(chatId, filePath);
|
|
551
|
+
}
|
|
380
552
|
logger.error('[Feishu] Failed to send file:', error);
|
|
381
553
|
throw error;
|
|
382
554
|
}
|
|
383
555
|
}
|
|
384
|
-
|
|
556
|
+
async sendImage(chatId, png, options) {
|
|
557
|
+
if (!this.client)
|
|
558
|
+
return;
|
|
385
559
|
try {
|
|
386
|
-
const
|
|
387
|
-
|
|
560
|
+
const uploadResponse = await this.client.im.image.create({
|
|
561
|
+
data: {
|
|
562
|
+
image_type: 'message',
|
|
563
|
+
image: Buffer.from(png),
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
const imageKey = uploadResponse?.image_key;
|
|
567
|
+
if (!imageKey) {
|
|
568
|
+
logger.error('[Feishu] Image upload failed: no image_key returned');
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
logger.debug('[Feishu] Image uploaded, image_key:', imageKey);
|
|
572
|
+
const msgContent = JSON.stringify({ image_key: imageKey });
|
|
573
|
+
if (options?.replyToMessageId) {
|
|
574
|
+
const replyData = { msg_type: 'image', content: msgContent };
|
|
575
|
+
if (options.replyInThread)
|
|
576
|
+
replyData.reply_in_thread = true;
|
|
577
|
+
await this.client.im.message.reply({
|
|
578
|
+
path: { message_id: options.replyToMessageId },
|
|
579
|
+
data: replyData
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
await this.client.im.message.create({
|
|
584
|
+
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
585
|
+
data: { receive_id: chatId, msg_type: 'image', content: msgContent }
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
logger.debug('[Feishu] Image message sent successfully');
|
|
388
589
|
}
|
|
389
|
-
catch {
|
|
390
|
-
|
|
590
|
+
catch (error) {
|
|
591
|
+
logger.error('[Feishu] Failed to send image:', error);
|
|
592
|
+
throw error;
|
|
391
593
|
}
|
|
392
594
|
}
|
|
595
|
+
isDuplicate(msgId) {
|
|
596
|
+
return this.seenMessages.has(msgId);
|
|
597
|
+
}
|
|
598
|
+
markSeen(msgId) {
|
|
599
|
+
this.seenMessages.set(msgId, Date.now());
|
|
600
|
+
}
|
|
601
|
+
startCleanupTask() {
|
|
602
|
+
this.cleanupInterval = setInterval(() => {
|
|
603
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
604
|
+
let cleaned = 0;
|
|
605
|
+
for (const [id, ts] of this.seenMessages) {
|
|
606
|
+
if (ts < cutoff) {
|
|
607
|
+
this.seenMessages.delete(id);
|
|
608
|
+
cleaned++;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (cleaned > 0)
|
|
612
|
+
logger.info(`[Feishu] Cleaned ${cleaned} old message IDs`);
|
|
613
|
+
// seenThreads 无时间戳,仅限容量(话题持久存在,不按时间清理)
|
|
614
|
+
if (this.seenThreads.size > 1000)
|
|
615
|
+
this.seenThreads.clear();
|
|
616
|
+
}, 60 * 60 * 1000);
|
|
617
|
+
}
|
|
393
618
|
async disconnect() {
|
|
619
|
+
this.connected = false;
|
|
394
620
|
if (this.cleanupInterval) {
|
|
395
621
|
clearInterval(this.cleanupInterval);
|
|
396
622
|
this.cleanupInterval = undefined;
|
|
@@ -401,6 +627,23 @@ export class FeishuChannel {
|
|
|
401
627
|
}
|
|
402
628
|
this.client = null;
|
|
403
629
|
}
|
|
630
|
+
/** Get current connection status */
|
|
631
|
+
getStatus() {
|
|
632
|
+
return { connected: this.connected };
|
|
633
|
+
}
|
|
634
|
+
/** Reconnect: disconnect then connect again */
|
|
635
|
+
async reconnect() {
|
|
636
|
+
if (this.connected) {
|
|
637
|
+
await this.disconnect();
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
await this.connect();
|
|
641
|
+
return '重连成功';
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
return `重连失败: ${err instanceof Error ? err.message : String(err)}`;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
404
647
|
async downloadAndSaveImage(imageKey, chatId, messageId, projectPath) {
|
|
405
648
|
if (!this.client)
|
|
406
649
|
return null;
|
|
@@ -428,28 +671,17 @@ export class FeishuChannel {
|
|
|
428
671
|
logger.warn('[Feishu] Empty response from image download');
|
|
429
672
|
return null;
|
|
430
673
|
}
|
|
431
|
-
//
|
|
432
|
-
const
|
|
433
|
-
if (
|
|
434
|
-
logger.warn(
|
|
435
|
-
return null;
|
|
436
|
-
}
|
|
437
|
-
// 白名单验证:只允许常见的图片格式
|
|
438
|
-
const allowedMimes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
|
|
439
|
-
if (!allowedMimes.includes(type.mime)) {
|
|
440
|
-
logger.warn('[Feishu] Unsupported image type:', type.mime);
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
|
-
// 大小限制:10MB
|
|
444
|
-
if (buffer.length > 10 * 1024 * 1024) {
|
|
445
|
-
logger.warn('[Feishu] Image too large:', buffer.length, 'bytes');
|
|
674
|
+
// 统一图片验证(类型白名单 + 大小限制)
|
|
675
|
+
const result = await validateImage(buffer);
|
|
676
|
+
if (result.mime === null) {
|
|
677
|
+
logger.warn(`[Feishu] Image validation failed: ${result.reason}`);
|
|
446
678
|
return null;
|
|
447
679
|
}
|
|
448
680
|
const base64Data = buffer.toString('base64');
|
|
449
|
-
logger.debug('[Feishu] Image downloaded successfully, type:',
|
|
681
|
+
logger.debug('[Feishu] Image downloaded successfully, type:', result.mime, 'size:', base64Data.length);
|
|
450
682
|
return {
|
|
451
683
|
data: base64Data,
|
|
452
|
-
mimeType:
|
|
684
|
+
mimeType: result.mime
|
|
453
685
|
};
|
|
454
686
|
}
|
|
455
687
|
logger.error('[Feishu] Image download failed: no valid method');
|
|
@@ -490,11 +722,7 @@ export class FeishuChannel {
|
|
|
490
722
|
logger.warn('[Feishu] Empty response from file download');
|
|
491
723
|
return null;
|
|
492
724
|
}
|
|
493
|
-
const
|
|
494
|
-
ensureDir(uploadsDir);
|
|
495
|
-
const filePath = path.join(uploadsDir, fileName);
|
|
496
|
-
fs.writeFileSync(filePath, buffer);
|
|
497
|
-
logger.info('[Feishu] File downloaded successfully:', filePath, 'size:', buffer.length);
|
|
725
|
+
const { filePath } = saveToUploads(buffer, sanitizeFileName(fileName), projectPath);
|
|
498
726
|
return filePath;
|
|
499
727
|
}
|
|
500
728
|
logger.error('[Feishu] File download failed: no valid method');
|
|
@@ -505,12 +733,101 @@ export class FeishuChannel {
|
|
|
505
733
|
return null;
|
|
506
734
|
}
|
|
507
735
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
736
|
+
async sendInteraction(chatId, interaction, options) {
|
|
737
|
+
if (!this.client)
|
|
738
|
+
return false;
|
|
739
|
+
const card = buildInteractionCard(interaction);
|
|
740
|
+
if (!card)
|
|
741
|
+
return false;
|
|
742
|
+
try {
|
|
743
|
+
let messageId;
|
|
744
|
+
if (options?.replyToMessageId) {
|
|
745
|
+
const replyData = {
|
|
746
|
+
msg_type: 'interactive',
|
|
747
|
+
content: JSON.stringify(card),
|
|
748
|
+
};
|
|
749
|
+
if (options.replyInThread)
|
|
750
|
+
replyData.reply_in_thread = true;
|
|
751
|
+
const res = await this.client.im.message.reply({
|
|
752
|
+
path: { message_id: options.replyToMessageId },
|
|
753
|
+
data: replyData,
|
|
754
|
+
});
|
|
755
|
+
messageId = res?.data?.message_id;
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
const res = await this.client.im.message.create({
|
|
759
|
+
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
760
|
+
data: {
|
|
761
|
+
receive_id: chatId,
|
|
762
|
+
msg_type: 'interactive',
|
|
763
|
+
content: JSON.stringify(card),
|
|
764
|
+
},
|
|
765
|
+
});
|
|
766
|
+
messageId = res?.data?.message_id;
|
|
767
|
+
}
|
|
768
|
+
logger.info(`[Feishu] Sent interaction card: ${interaction.id}, messageId=${messageId}`);
|
|
769
|
+
return messageId || false;
|
|
770
|
+
}
|
|
771
|
+
catch (error) {
|
|
772
|
+
const detail = error?.response?.data || error?.message || error;
|
|
773
|
+
logger.error(`[Feishu] Failed to send interaction card (id=${interaction.id}, replyTo=${options?.replyToMessageId || 'none'}):`, detail);
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
511
776
|
}
|
|
512
|
-
|
|
513
|
-
|
|
777
|
+
async patchInteractionCard(messageId, card) {
|
|
778
|
+
if (!this.client)
|
|
779
|
+
return;
|
|
780
|
+
try {
|
|
781
|
+
await this.client.im.message.patch({
|
|
782
|
+
path: { message_id: messageId },
|
|
783
|
+
data: { content: JSON.stringify(card) },
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
logger.warn(`[Feishu] Failed to patch card ${messageId}:`, error?.response?.data || error?.message);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
buildResolvedCard(cardTitle, response) {
|
|
791
|
+
const action = response.action;
|
|
792
|
+
const labelMap = {
|
|
793
|
+
'allow': '✅ 已允许',
|
|
794
|
+
'always': '🔓 已设为始终允许',
|
|
795
|
+
'deny': '❌ 已拒绝',
|
|
796
|
+
'cancel': '取消',
|
|
797
|
+
'submit': '✅ 已提交',
|
|
798
|
+
};
|
|
799
|
+
const statusText = labelMap[action] || `✅ ${action}`;
|
|
800
|
+
const now = new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
|
|
801
|
+
// Build summary of selected values
|
|
802
|
+
const elements = [];
|
|
803
|
+
if (response.values && action === 'submit') {
|
|
804
|
+
const entries = Object.entries(response.values).filter(([k]) => !k.startsWith('_'));
|
|
805
|
+
if (entries.length > 0) {
|
|
806
|
+
const lines = entries.map(([k, v]) => {
|
|
807
|
+
const display = Array.isArray(v) ? v.join(', ') : String(v);
|
|
808
|
+
return `**${k}**: ${display}`;
|
|
809
|
+
});
|
|
810
|
+
elements.push({ tag: 'markdown', content: lines.join('\n') });
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
elements.push({ tag: 'markdown', content: `操作时间:${now}` });
|
|
814
|
+
return {
|
|
815
|
+
toast: {
|
|
816
|
+
type: 'success',
|
|
817
|
+
content: statusText,
|
|
818
|
+
},
|
|
819
|
+
card: {
|
|
820
|
+
type: 'raw',
|
|
821
|
+
data: {
|
|
822
|
+
config: { wide_screen_mode: true },
|
|
823
|
+
header: {
|
|
824
|
+
template: action === 'deny' ? 'red' : 'green',
|
|
825
|
+
title: { tag: 'plain_text', content: `${cardTitle} — ${statusText}` },
|
|
826
|
+
},
|
|
827
|
+
elements,
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
};
|
|
514
831
|
}
|
|
515
832
|
addAckReaction(messageId) {
|
|
516
833
|
if (!this.client)
|
|
@@ -522,13 +839,449 @@ export class FeishuChannel {
|
|
|
522
839
|
}
|
|
523
840
|
}).catch(() => { });
|
|
524
841
|
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
842
|
+
}
|
|
843
|
+
// ── 交互卡片构建工具 ──
|
|
844
|
+
export function buildInteractionCard(interaction) {
|
|
845
|
+
const { kind } = interaction;
|
|
846
|
+
if (kind.kind === 'action') {
|
|
847
|
+
return buildActionCard(interaction.id, kind);
|
|
848
|
+
}
|
|
849
|
+
if (kind.kind === 'form') {
|
|
850
|
+
return buildFormCard(interaction.id, kind);
|
|
851
|
+
}
|
|
852
|
+
// menu kind: not rendered as card (handled via menu.response JSON)
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
export function buildActionCard(requestId, action) {
|
|
856
|
+
const elements = [];
|
|
857
|
+
// Body text
|
|
858
|
+
if (action.body) {
|
|
859
|
+
elements.push({ tag: 'markdown', content: action.body });
|
|
860
|
+
}
|
|
861
|
+
// Buttons row
|
|
862
|
+
const buttons = action.buttons.map(btn => {
|
|
863
|
+
const buttonEl = {
|
|
864
|
+
tag: 'button',
|
|
865
|
+
text: { tag: 'plain_text', content: btn.label },
|
|
866
|
+
type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
|
|
867
|
+
value: {
|
|
868
|
+
_request_id: requestId,
|
|
869
|
+
_action: btn.key,
|
|
870
|
+
_card_title: action.title,
|
|
871
|
+
},
|
|
872
|
+
};
|
|
873
|
+
if (btn.confirm) {
|
|
874
|
+
buttonEl.confirm = {
|
|
875
|
+
title: { tag: 'plain_text', content: btn.confirm.title },
|
|
876
|
+
text: { tag: 'plain_text', content: btn.confirm.body },
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
return buttonEl;
|
|
880
|
+
});
|
|
881
|
+
elements.push({
|
|
882
|
+
tag: 'action',
|
|
883
|
+
actions: buttons,
|
|
884
|
+
});
|
|
885
|
+
return {
|
|
886
|
+
config: { wide_screen_mode: true },
|
|
887
|
+
header: {
|
|
888
|
+
template: 'blue',
|
|
889
|
+
title: { tag: 'plain_text', content: action.title },
|
|
890
|
+
},
|
|
891
|
+
elements,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
export function buildFormCard(requestId, form) {
|
|
895
|
+
// Use Feishu card v2 form container: all fields wrapped in a `form` tag.
|
|
896
|
+
// On submit, callback receives `action.form_value` with all field values keyed by `name`.
|
|
897
|
+
// This eliminates per-field callbacks when selecting dropdown options.
|
|
898
|
+
const formElements = [];
|
|
899
|
+
// Body text
|
|
900
|
+
if (form.body) {
|
|
901
|
+
formElements.push({ tag: 'markdown', content: form.body });
|
|
902
|
+
}
|
|
903
|
+
// Fields — inside form, components use `name` (not `value`) for identification
|
|
904
|
+
for (const field of form.fields) {
|
|
905
|
+
formElements.push(buildFormFieldElement(field));
|
|
906
|
+
if (field.hint) {
|
|
907
|
+
formElements.push({
|
|
908
|
+
tag: 'note',
|
|
909
|
+
elements: [{ tag: 'plain_text', content: field.hint }],
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
// Submit button (inside form, uses form_action_type)
|
|
914
|
+
const submitBtn = {
|
|
915
|
+
tag: 'button',
|
|
916
|
+
text: { tag: 'plain_text', content: form.submitLabel || '确认' },
|
|
917
|
+
type: form.submitStyle === 'danger' ? 'danger' : 'primary',
|
|
918
|
+
form_action_type: 'submit',
|
|
919
|
+
name: 'submit',
|
|
920
|
+
value: {
|
|
921
|
+
_request_id: requestId,
|
|
922
|
+
_action: 'submit',
|
|
923
|
+
_card_title: form.title,
|
|
924
|
+
},
|
|
925
|
+
};
|
|
926
|
+
if (form.submitConfirm) {
|
|
927
|
+
submitBtn.confirm = {
|
|
928
|
+
title: { tag: 'plain_text', content: form.submitConfirm.title },
|
|
929
|
+
text: { tag: 'plain_text', content: form.submitConfirm.body },
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
const actions = [submitBtn];
|
|
933
|
+
if (form.cancelable !== false) {
|
|
934
|
+
actions.push({
|
|
935
|
+
tag: 'button',
|
|
936
|
+
text: { tag: 'plain_text', content: '取消' },
|
|
937
|
+
type: 'default',
|
|
938
|
+
// Cancel is NOT form_action_type — it's a regular button that triggers callback directly
|
|
939
|
+
value: {
|
|
940
|
+
_request_id: requestId,
|
|
941
|
+
_action: 'cancel',
|
|
942
|
+
_card_title: form.title,
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
formElements.push({ tag: 'action', actions });
|
|
947
|
+
return {
|
|
948
|
+
schema: '2.0',
|
|
949
|
+
config: { update_multi: true },
|
|
950
|
+
header: {
|
|
951
|
+
template: 'blue',
|
|
952
|
+
title: { tag: 'plain_text', content: form.title },
|
|
953
|
+
},
|
|
954
|
+
body: {
|
|
955
|
+
elements: [
|
|
956
|
+
{
|
|
957
|
+
tag: 'form',
|
|
958
|
+
name: requestId,
|
|
959
|
+
elements: formElements,
|
|
960
|
+
},
|
|
961
|
+
],
|
|
962
|
+
},
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
/** Build a field element for use inside a form container (uses `name` for identification) */
|
|
966
|
+
export function buildFormFieldElement(field) {
|
|
967
|
+
switch (field.type) {
|
|
968
|
+
case 'select': {
|
|
969
|
+
const options = field.options.map(opt => ({
|
|
970
|
+
text: { tag: 'plain_text', content: opt.label },
|
|
971
|
+
value: opt.value,
|
|
972
|
+
}));
|
|
973
|
+
const selectedOpt = field.options.find(opt => opt.selected);
|
|
974
|
+
return {
|
|
975
|
+
tag: 'select_static',
|
|
976
|
+
name: field.key,
|
|
977
|
+
placeholder: { tag: 'plain_text', content: field.placeholder || `选择${field.label}` },
|
|
978
|
+
options,
|
|
979
|
+
...(selectedOpt ? { initial_option: selectedOpt.value } : {}),
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
case 'text': {
|
|
983
|
+
return {
|
|
984
|
+
tag: 'input',
|
|
985
|
+
name: field.key,
|
|
986
|
+
placeholder: { tag: 'plain_text', content: field.placeholder || `输入${field.label}` },
|
|
987
|
+
...(field.defaultValue != null ? { default_value: String(field.defaultValue) } : {}),
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
case 'toggle': {
|
|
991
|
+
const checked = field.defaultValue ?? false;
|
|
992
|
+
return {
|
|
993
|
+
tag: 'select_static',
|
|
994
|
+
name: field.key,
|
|
995
|
+
placeholder: { tag: 'plain_text', content: field.label },
|
|
996
|
+
options: [
|
|
997
|
+
{ text: { tag: 'plain_text', content: '开启' }, value: 'true' },
|
|
998
|
+
{ text: { tag: 'plain_text', content: '关闭' }, value: 'false' },
|
|
999
|
+
],
|
|
1000
|
+
initial_option: checked ? 'true' : 'false',
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
case 'multi-select': {
|
|
1004
|
+
const options = field.options.map(opt => ({
|
|
1005
|
+
text: { tag: 'plain_text', content: opt.label },
|
|
1006
|
+
value: opt.value,
|
|
1007
|
+
}));
|
|
1008
|
+
const selectedValues = field.options.filter(opt => opt.selected).map(opt => opt.value);
|
|
1009
|
+
return {
|
|
1010
|
+
tag: 'multi_select_static',
|
|
1011
|
+
name: field.key,
|
|
1012
|
+
placeholder: { tag: 'plain_text', content: `选择${field.label}` },
|
|
1013
|
+
options,
|
|
1014
|
+
...(selectedValues.length > 0 ? { initial_options: selectedValues } : {}),
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
default:
|
|
1018
|
+
return { tag: 'markdown', content: `[不支持的字段类型: ${field.type}]` };
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
export function buildFieldElement(requestId, field) {
|
|
1022
|
+
switch (field.type) {
|
|
1023
|
+
case 'select': {
|
|
1024
|
+
const options = field.options.map(opt => ({
|
|
1025
|
+
text: { tag: 'plain_text', content: opt.label },
|
|
1026
|
+
value: opt.value,
|
|
1027
|
+
}));
|
|
1028
|
+
const selectedOpt = field.options.find(opt => opt.selected);
|
|
1029
|
+
return {
|
|
1030
|
+
tag: 'action',
|
|
1031
|
+
actions: [{
|
|
1032
|
+
tag: 'select_static',
|
|
1033
|
+
placeholder: { tag: 'plain_text', content: field.placeholder || `选择${field.label}` },
|
|
1034
|
+
options,
|
|
1035
|
+
...(selectedOpt ? { initial_option: selectedOpt.value } : {}),
|
|
1036
|
+
value: {
|
|
1037
|
+
_request_id: requestId,
|
|
1038
|
+
_field_key: field.key,
|
|
1039
|
+
},
|
|
1040
|
+
}],
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
case 'text': {
|
|
1044
|
+
// Feishu cards don't have a native text input component.
|
|
1045
|
+
// Use a note element as placeholder label; actual input via form submit.
|
|
1046
|
+
return {
|
|
1047
|
+
tag: 'note',
|
|
1048
|
+
elements: [
|
|
1049
|
+
{ tag: 'plain_text', content: `${field.label}: ${field.placeholder || '(请在提交时输入)'}` },
|
|
1050
|
+
],
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
case 'toggle': {
|
|
1054
|
+
const checked = field.defaultValue ?? false;
|
|
1055
|
+
return {
|
|
1056
|
+
tag: 'action',
|
|
1057
|
+
actions: [{
|
|
1058
|
+
tag: 'select_static',
|
|
1059
|
+
placeholder: { tag: 'plain_text', content: field.label },
|
|
1060
|
+
options: [
|
|
1061
|
+
{ text: { tag: 'plain_text', content: '开启' }, value: 'true' },
|
|
1062
|
+
{ text: { tag: 'plain_text', content: '关闭' }, value: 'false' },
|
|
1063
|
+
],
|
|
1064
|
+
initial_option: checked ? 'true' : 'false',
|
|
1065
|
+
value: {
|
|
1066
|
+
_request_id: requestId,
|
|
1067
|
+
_field_key: field.key,
|
|
1068
|
+
},
|
|
1069
|
+
}],
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
case 'multi-select': {
|
|
1073
|
+
// Feishu cards: use multi_select_static (checker)
|
|
1074
|
+
const options = field.options.map(opt => ({
|
|
1075
|
+
text: { tag: 'plain_text', content: opt.label },
|
|
1076
|
+
value: opt.value,
|
|
1077
|
+
}));
|
|
1078
|
+
const selectedValues = field.options.filter(opt => opt.selected).map(opt => opt.value);
|
|
1079
|
+
return {
|
|
1080
|
+
tag: 'action',
|
|
1081
|
+
actions: [{
|
|
1082
|
+
tag: 'multi_select_static',
|
|
1083
|
+
placeholder: { tag: 'plain_text', content: `选择${field.label}` },
|
|
1084
|
+
options,
|
|
1085
|
+
...(selectedValues.length > 0 ? { initial_options: selectedValues } : {}),
|
|
1086
|
+
value: {
|
|
1087
|
+
_request_id: requestId,
|
|
1088
|
+
_field_key: field.key,
|
|
1089
|
+
},
|
|
1090
|
+
}],
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
default:
|
|
1094
|
+
return { tag: 'markdown', content: `[不支持的字段类型: ${field.type}]` };
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
function displayWidth(str) {
|
|
1098
|
+
let width = 0;
|
|
1099
|
+
for (const ch of str) {
|
|
1100
|
+
const code = ch.codePointAt(0);
|
|
1101
|
+
if ((code >= 0x4E00 && code <= 0x9FFF) ||
|
|
1102
|
+
(code >= 0x3400 && code <= 0x4DBF) ||
|
|
1103
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
1104
|
+
(code >= 0xFF01 && code <= 0xFF60) ||
|
|
1105
|
+
(code >= 0x3000 && code <= 0x303F)) {
|
|
1106
|
+
width += 2;
|
|
1107
|
+
}
|
|
1108
|
+
else {
|
|
1109
|
+
width += 1;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return width;
|
|
1113
|
+
}
|
|
1114
|
+
function padToWidth(str, targetWidth) {
|
|
1115
|
+
const current = displayWidth(str);
|
|
1116
|
+
const padding = Math.max(0, targetWidth - current);
|
|
1117
|
+
return str + ' '.repeat(padding);
|
|
1118
|
+
}
|
|
1119
|
+
function convertTablesToText(text) {
|
|
1120
|
+
const tableRegex = /^(\|.+\|)\n(\|[\s:|-]+\|)\n((?:\|.+\|\n?)+)/gm;
|
|
1121
|
+
return text.replace(tableRegex, (_match, headerLine, _sep, bodyBlock) => {
|
|
1122
|
+
const parseRow = (line) => line.split('|').slice(1, -1).map((c) => c.trim());
|
|
1123
|
+
const headers = parseRow(headerLine);
|
|
1124
|
+
const rows = bodyBlock.trim().split('\n').map(parseRow);
|
|
1125
|
+
const colWidths = headers.map((h, i) => {
|
|
1126
|
+
const cellWidths = rows.map(r => displayWidth(r[i] || ''));
|
|
1127
|
+
return Math.max(displayWidth(h), ...cellWidths);
|
|
1128
|
+
});
|
|
1129
|
+
const headerStr = headers.map((h, i) => padToWidth(h, colWidths[i])).join(' ');
|
|
1130
|
+
const sepStr = colWidths.map(w => '-'.repeat(w)).join(' ');
|
|
1131
|
+
const rowStrs = rows.map(r => headers.map((_, i) => padToWidth(r[i] || '', colWidths[i])).join(' '));
|
|
1132
|
+
return '```\n' + [headerStr, sepStr, ...rowStrs].join('\n') + '\n```';
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* 按段落边界拆分超长消息
|
|
1137
|
+
* 优先在 \n\n 处分割,其次 \n,最后强制截断
|
|
1138
|
+
*/
|
|
1139
|
+
function splitLongMessage(content, maxLength) {
|
|
1140
|
+
const parts = [];
|
|
1141
|
+
let remaining = content;
|
|
1142
|
+
while (remaining.length > maxLength) {
|
|
1143
|
+
let splitAt = remaining.lastIndexOf('\n\n', maxLength);
|
|
1144
|
+
if (splitAt <= 0)
|
|
1145
|
+
splitAt = remaining.lastIndexOf('\n', maxLength);
|
|
1146
|
+
if (splitAt <= 0)
|
|
1147
|
+
splitAt = maxLength;
|
|
1148
|
+
parts.push(remaining.slice(0, splitAt).trimEnd());
|
|
1149
|
+
remaining = remaining.slice(splitAt).trimStart();
|
|
1150
|
+
}
|
|
1151
|
+
if (remaining)
|
|
1152
|
+
parts.push(remaining);
|
|
1153
|
+
return parts;
|
|
1154
|
+
}
|
|
1155
|
+
export function markdownToFeishuPost(markdown, defaultTitle) {
|
|
1156
|
+
const match = markdown.match(/^# (.+)$/m);
|
|
1157
|
+
const title = match?.[1] ?? defaultTitle ?? '';
|
|
1158
|
+
let body = match ? markdown.replace(/^# .+\n?/, '') : markdown;
|
|
1159
|
+
body = convertTablesToText(body);
|
|
1160
|
+
return {
|
|
1161
|
+
zh_cn: {
|
|
1162
|
+
title,
|
|
1163
|
+
content: [[{ tag: 'md', text: body.trim() }]]
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* 将 Markdown 内容转为飞书消息卡片格式(interactive msg_type)
|
|
1169
|
+
* 飞书卡片的 markdown 组件支持完整 Markdown 渲染(代码块、表格、列表等)
|
|
1170
|
+
* 当前消息类型决策统一走 post + md tag,此函数为 interactive 卡片场景预留。
|
|
1171
|
+
*/
|
|
1172
|
+
export function markdownToFeishuCard(markdown, defaultTitle) {
|
|
1173
|
+
const match = markdown.match(/^# (.+)$/m);
|
|
1174
|
+
const title = match?.[1] ?? defaultTitle;
|
|
1175
|
+
let body = match ? markdown.replace(/^# .+\n?/, '') : markdown;
|
|
1176
|
+
body = convertTablesToText(body).trim();
|
|
1177
|
+
const card = {
|
|
1178
|
+
config: { wide_screen_mode: true },
|
|
1179
|
+
elements: [
|
|
1180
|
+
{ tag: 'markdown', content: body }
|
|
1181
|
+
]
|
|
1182
|
+
};
|
|
1183
|
+
if (title) {
|
|
1184
|
+
card.header = {
|
|
1185
|
+
title: { tag: 'plain_text', content: title }
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
return card;
|
|
1189
|
+
}
|
|
1190
|
+
export function hasMarkdownSyntax(text) {
|
|
1191
|
+
const markdownPatterns = [
|
|
1192
|
+
/^#{1,6}\s/m, /\*\*.*?\*\*/, /\*.*?\*/, /__.*?__/, /_.*?_/, /~~.*?~~/,
|
|
1193
|
+
/`.*?`/, /```[\s\S]*?```/, /\[.*?\]\(.*?\)/, /^[\s]*[-*+]\s/m,
|
|
1194
|
+
/^[\s]*\d+\.\s/m, /^\|.+\|$/m
|
|
1195
|
+
];
|
|
1196
|
+
return markdownPatterns.some(pattern => pattern.test(text));
|
|
1197
|
+
}
|
|
1198
|
+
import { normalizeChannelInstances } from '../config.js';
|
|
1199
|
+
export class FeishuChannelPlugin {
|
|
1200
|
+
name = 'feishu';
|
|
1201
|
+
isEnabled(config) {
|
|
1202
|
+
const raw = config.channels?.feishu;
|
|
1203
|
+
if (!raw)
|
|
1204
|
+
return false;
|
|
1205
|
+
if (Array.isArray(raw)) {
|
|
1206
|
+
return raw.some(inst => inst.enabled !== false && inst.appId && inst.appSecret);
|
|
1207
|
+
}
|
|
1208
|
+
if (raw.enabled === false)
|
|
1209
|
+
return false;
|
|
1210
|
+
return !!(raw.appId && raw.appSecret);
|
|
1211
|
+
}
|
|
1212
|
+
async createChannels(config) {
|
|
1213
|
+
const instances = normalizeChannelInstances(config.channels?.feishu, 'feishu');
|
|
1214
|
+
const result = [];
|
|
1215
|
+
for (const inst of instances) {
|
|
1216
|
+
if (inst.enabled === false || !inst.appId || !inst.appSecret)
|
|
1217
|
+
continue;
|
|
1218
|
+
const channel = new FeishuChannel({
|
|
1219
|
+
appId: inst.appId,
|
|
1220
|
+
appSecret: inst.appSecret,
|
|
1221
|
+
enableRichContent: config.enableRichContent,
|
|
1222
|
+
});
|
|
1223
|
+
const adapter = {
|
|
1224
|
+
channelName: inst.name,
|
|
1225
|
+
sendText: (id, text, context) => channel.sendMessage(id, text, context),
|
|
1226
|
+
sendFile: (id, filePath, context) => channel.sendFile(id, filePath, context),
|
|
1227
|
+
sendImage: (id, png, context) => channel.sendImage(id, png, context),
|
|
1228
|
+
acknowledge: (messageId) => { channel.addAckReaction(messageId); return Promise.resolve(); },
|
|
1229
|
+
sendInteraction: (id, interaction, context) => channel.sendInteraction(id, interaction, context),
|
|
1230
|
+
patchInteractionCard: (messageId, card) => channel.patchInteractionCard(messageId, card),
|
|
1231
|
+
onInteraction: (callback) => channel.onInteraction(callback),
|
|
1232
|
+
};
|
|
1233
|
+
const policy = {
|
|
1234
|
+
canSwitchProject: (chatType, identity) => identity === 'owner',
|
|
1235
|
+
canListProjects: (chatType, identity) => identity === 'owner',
|
|
1236
|
+
canCreateSession: (chatType, identity) => true,
|
|
1237
|
+
canDeleteSession: (chatType, identity) => true,
|
|
1238
|
+
canImportCliSession: (chatType, identity) => identity === 'owner',
|
|
1239
|
+
messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
|
|
1240
|
+
showMiddleResult: (chatType, identity) => {
|
|
1241
|
+
const mode = inst.showActivities ?? config.showActivities ?? 'all';
|
|
1242
|
+
if (mode === 'none')
|
|
1243
|
+
return false;
|
|
1244
|
+
if (mode === 'dm-only')
|
|
1245
|
+
return chatType === 'private';
|
|
1246
|
+
if (mode === 'owner-dm-only')
|
|
1247
|
+
return chatType === 'private' && identity === 'owner';
|
|
1248
|
+
return true;
|
|
1249
|
+
},
|
|
1250
|
+
showIdleMonitor: (chatType, identity) => {
|
|
1251
|
+
const mode = inst.showActivities ?? config.showActivities ?? 'all';
|
|
1252
|
+
if (mode === 'none')
|
|
1253
|
+
return false;
|
|
1254
|
+
if (mode === 'dm-only')
|
|
1255
|
+
return chatType === 'private';
|
|
1256
|
+
if (mode === 'owner-dm-only')
|
|
1257
|
+
return chatType === 'private' && identity === 'owner';
|
|
1258
|
+
return true;
|
|
1259
|
+
},
|
|
1260
|
+
accumulateErrors: (chatType, identity) => true,
|
|
1261
|
+
};
|
|
1262
|
+
const options = {
|
|
1263
|
+
fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
|
|
1264
|
+
supportsImages: true,
|
|
1265
|
+
flushDelay: inst.flushDelay,
|
|
1266
|
+
};
|
|
1267
|
+
result.push({
|
|
1268
|
+
channelType: 'feishu',
|
|
1269
|
+
adapter,
|
|
1270
|
+
channel,
|
|
1271
|
+
policy,
|
|
1272
|
+
options,
|
|
1273
|
+
connect: () => channel.connect(),
|
|
1274
|
+
disconnect: () => channel.disconnect(),
|
|
1275
|
+
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
return result;
|
|
1279
|
+
}
|
|
1280
|
+
async createChannel(config) {
|
|
1281
|
+
const instances = await this.createChannels(config);
|
|
1282
|
+
if (instances.length === 0) {
|
|
1283
|
+
throw new Error('Feishu config missing');
|
|
1284
|
+
}
|
|
1285
|
+
return instances[0];
|
|
533
1286
|
}
|
|
534
1287
|
}
|