evolclaw 2.8.3 → 3.1.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 +21 -12
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent-normalize.js +19 -0
- package/dist/agents/claude-runner.js +108 -46
- package/dist/agents/codex-runner.js +13 -14
- package/dist/agents/gemini-runner.js +15 -17
- package/dist/agents/kit-renderer.js +281 -0
- package/dist/agents/resolve.js +134 -0
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +159 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/lifecycle-log.js +33 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +293 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +147 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +98 -0
- package/dist/aun/outbox.js +138 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +34 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +35 -0
- package/dist/channels/aun.js +1340 -349
- package/dist/channels/dingtalk.js +59 -5
- package/dist/channels/feishu.js +381 -32
- package/dist/channels/qqbot.js +68 -12
- package/dist/channels/wechat.js +63 -4
- package/dist/channels/wecom.js +59 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/bench.js +1219 -0
- package/dist/cli/index.js +4513 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/cli/link-rules.js +245 -0
- package/dist/cli/net-check.js +640 -0
- package/dist/cli/watch-msg.js +589 -0
- package/dist/config-store.js +645 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +176 -12
- package/dist/core/command-handler.js +883 -848
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +202 -238
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +486 -0
- package/dist/core/message/items-formatter.js +68 -0
- package/dist/core/message/message-bridge.js +109 -56
- package/dist/core/message/message-log.js +93 -0
- package/dist/core/message/message-processor.js +430 -212
- package/dist/core/message/message-queue.js +13 -6
- package/dist/core/permission.js +116 -11
- package/dist/core/session/adapters/codex-session-file-adapter.js +24 -2
- package/dist/core/session/session-fs-store.js +230 -0
- package/dist/core/session/session-manager.js +740 -777
- package/dist/core/session/session-mapper.js +87 -0
- package/dist/core/trigger/manager.js +122 -0
- package/dist/core/trigger/parser.js +128 -0
- package/dist/core/trigger/scheduler.js +224 -0
- package/dist/data/error-dict.json +118 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +560 -283
- package/dist/ipc.js +49 -0
- package/dist/net-check.js +640 -0
- package/dist/paths.js +73 -9
- package/dist/types.js +8 -2
- package/dist/utils/aid-lifecycle-log.js +33 -0
- package/dist/utils/atomic-write.js +89 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +17 -26
- package/dist/utils/error-utils.js +10 -2
- package/dist/utils/instance-registry.js +434 -0
- package/dist/utils/log-writer.js +217 -0
- package/dist/utils/logger.js +34 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/npm-ops.js +163 -0
- package/dist/utils/process-introspect.js +122 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +544 -0
- package/evolclaw-install-aun.md +127 -47
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +52 -0
- package/kits/docs/aun/CHEATSHEET.md +17 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +25 -0
- package/kits/docs/channels/feishu.md +27 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolclaw/AGENT_CMD.md +31 -0
- package/kits/docs/evolclaw/MSG_GROUP.md +30 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +25 -0
- package/kits/docs/evolclaw/self-summary.md +29 -0
- package/kits/docs/evolclaw/tools.md +25 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +27 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +20 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/eck_manifest.json +95 -0
- package/kits/rules/01-overview.md +120 -0
- package/kits/rules/02-navigation.md +75 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +49 -0
- package/kits/rules/05-venue.md +45 -0
- package/kits/rules/06-channel.md +43 -0
- package/kits/templates/system-fragments/baseagent.md +2 -0
- package/kits/templates/system-fragments/channel.md +10 -0
- package/kits/templates/system-fragments/identity.md +12 -0
- package/kits/templates/system-fragments/relation.md +9 -0
- package/kits/templates/system-fragments/runtime.md +19 -0
- package/kits/templates/system-fragments/venue.md +5 -0
- package/package.json +10 -6
- package/data/evolclaw.sample.json +0 -60
- package/dist/agents/templates.js +0 -122
- package/dist/channels/aun-ops.js +0 -275
- package/dist/cli.js +0 -2178
- package/dist/config.js +0 -591
- package/dist/core/agent-registry.js +0 -450
- package/dist/core/evolagent-schema.js +0 -72
- package/dist/core/message/stream-flusher.js +0 -238
- package/dist/core/message/thought-emitter.js +0 -162
- package/dist/core/reload-hooks.js +0 -87
- package/dist/prompts/templates.js +0 -122
- package/dist/templates/prompts.md +0 -104
- package/dist/templates/skills.md +0 -66
- package/dist/utils/channel-fingerprint.js +0 -59
- package/dist/utils/error-dict.js +0 -63
- package/dist/utils/format.js +0 -32
- package/dist/utils/init.js +0 -645
- package/dist/utils/migrate-project.js +0 -122
- package/dist/utils/reload-hooks.js +0 -87
- package/dist/utils/stats-collector.js +0 -99
- package/dist/utils/upgrade.js +0 -100
package/dist/channels/feishu.js
CHANGED
|
@@ -4,6 +4,7 @@ import imageType from 'image-type';
|
|
|
4
4
|
import { sanitizeFileName, saveToUploads, validateImage } from '../utils/media-cache.js';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
import { hasRichContent, renderAllRichContent, checkDependencies } from '../utils/rich-content-renderer.js';
|
|
7
|
+
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
7
8
|
export class FeishuChannel {
|
|
8
9
|
config;
|
|
9
10
|
client = null;
|
|
@@ -18,6 +19,8 @@ export class FeishuChannel {
|
|
|
18
19
|
interactionCallback;
|
|
19
20
|
connected = false;
|
|
20
21
|
enableRichContent;
|
|
22
|
+
// chatId → 该会话内仍 pending 的交互卡片 messageId 集合,用于作废
|
|
23
|
+
pendingCardsByChat = new Map();
|
|
21
24
|
constructor(config) {
|
|
22
25
|
this.config = config;
|
|
23
26
|
this.enableRichContent = config.enableRichContent ?? false; // 默认关闭
|
|
@@ -40,7 +43,9 @@ export class FeishuChannel {
|
|
|
40
43
|
if (this.config.appId.startsWith('YOUR_') || this.config.appSecret.startsWith('YOUR_')) {
|
|
41
44
|
throw new Error('Feishu credentials not configured (placeholder values detected)');
|
|
42
45
|
}
|
|
43
|
-
|
|
46
|
+
// 加载持久化的已处理消息 ID,防止重启后 Feishu 重推同一条消息
|
|
47
|
+
this.loadSeenMessages();
|
|
48
|
+
const { requireOptional } = await import('../utils/npm-ops.js');
|
|
44
49
|
const lark = await requireOptional('@larksuiteoapi/node-sdk');
|
|
45
50
|
try {
|
|
46
51
|
this.client = new lark.Client({
|
|
@@ -57,6 +62,18 @@ export class FeishuChannel {
|
|
|
57
62
|
return;
|
|
58
63
|
}
|
|
59
64
|
this.markSeen(msg.message_id);
|
|
65
|
+
// 丢弃飞书服务端积压超过 5 分钟才下发的消息:上游观察到 65 分钟级延迟下发的
|
|
66
|
+
// 历史消息(含 /restart 这类破坏性命令),无差别接收会导致非预期重启。
|
|
67
|
+
// create_time 是 ms 字符串。
|
|
68
|
+
{
|
|
69
|
+
const createTimeMs = Number(msg.create_time ?? 0);
|
|
70
|
+
const ageMs = Date.now() - createTimeMs;
|
|
71
|
+
const STALE_THRESHOLD_MS = 5 * 60 * 1000;
|
|
72
|
+
if (createTimeMs > 0 && ageMs > STALE_THRESHOLD_MS) {
|
|
73
|
+
logger.warn(`[Feishu] Dropping stale message: id=${msg.message_id} type=${msg.message_type} age=${Math.round(ageMs / 1000)}s create_time=${createTimeMs}`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
60
77
|
if (!this.messageHandler)
|
|
61
78
|
return;
|
|
62
79
|
// 提取 chatType(从 SDK 事件直接获取)
|
|
@@ -65,8 +82,6 @@ export class FeishuChannel {
|
|
|
65
82
|
if (msg.thread_id) {
|
|
66
83
|
logger.info('[Feishu] Thread message, thread_id:', msg.thread_id, 'root_id:', msg.root_id);
|
|
67
84
|
}
|
|
68
|
-
// [DEBUG] 临时:记录所有消息的 root_id/thread_id,用于排查图片回复带引用问题
|
|
69
|
-
logger.info('[Feishu][DEBUG] msg_type:', msg.message_type, 'root_id:', msg.root_id ?? '(empty)', 'thread_id:', msg.thread_id ?? '(empty)', 'parent_id:', msg.parent_id ?? '(empty)');
|
|
70
85
|
// 提取 @ 提及列表(排除机器人自身)
|
|
71
86
|
const mentions = (msg.mentions || []).map((m) => ({
|
|
72
87
|
userId: m.id?.open_id || '',
|
|
@@ -164,6 +179,16 @@ export class FeishuChannel {
|
|
|
164
179
|
quotedText = `> 以下是引用的原消息\n> ================\n> [文件消息]\n> ================\n\n`;
|
|
165
180
|
}
|
|
166
181
|
}
|
|
182
|
+
else if (quotedMsgType === 'merge_forward') {
|
|
183
|
+
const { text: mergedText, images: mergedImages } = await this.extractMergeForwardContent(msg.parent_id, msg.chat_id);
|
|
184
|
+
if (mergedText) {
|
|
185
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [合并转发消息]\n> ================\n\n${mergedText}\n\n`;
|
|
186
|
+
quotedImages.push(...mergedImages);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
quotedText = `> 以下是引用的原消息\n> ================\n> [合并转发消息]\n> ================\n\n`;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
167
192
|
else {
|
|
168
193
|
quotedText = `> 以下是引用的原消息\n> ================\n> [${quotedMsgType}消息]\n> ================\n\n`;
|
|
169
194
|
}
|
|
@@ -172,6 +197,7 @@ export class FeishuChannel {
|
|
|
172
197
|
logger.warn({ err }, '[Feishu] Failed to fetch quoted message');
|
|
173
198
|
}
|
|
174
199
|
}
|
|
200
|
+
logger.info(`[Feishu] Incoming message_type=${msg.message_type} content=${msg.content?.substring(0, 200)}`);
|
|
175
201
|
// 处理文本消息
|
|
176
202
|
if (msg.message_type === 'text') {
|
|
177
203
|
const parsed = JSON.parse(msg.content);
|
|
@@ -252,6 +278,19 @@ export class FeishuChannel {
|
|
|
252
278
|
const allImages = [...quotedImages, ...postImages];
|
|
253
279
|
await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: allImages.length > 0 ? allImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
254
280
|
}
|
|
281
|
+
// 处理合并转发消息
|
|
282
|
+
else if (msg.message_type === 'merge_forward') {
|
|
283
|
+
const { text: mergedText, images: mergedImages } = await this.extractMergeForwardContent(msg.message_id, msg.chat_id);
|
|
284
|
+
if (mergedText) {
|
|
285
|
+
const finalContent = quotedText + mergedText;
|
|
286
|
+
const allImages = [...quotedImages, ...mergedImages];
|
|
287
|
+
await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: allImages.length > 0 ? allImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
const prompt = quotedText + '[合并转发消息解析失败]';
|
|
291
|
+
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: quotedImages.length > 0 ? quotedImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
255
294
|
// 处理其他类型消息
|
|
256
295
|
else {
|
|
257
296
|
logger.debug('[Feishu] Unsupported message type:', msg.message_type);
|
|
@@ -278,11 +317,49 @@ export class FeishuChannel {
|
|
|
278
317
|
if (!action?.value)
|
|
279
318
|
return;
|
|
280
319
|
const value = action.value;
|
|
320
|
+
const operatorId = data.operator?.open_id;
|
|
321
|
+
const chatId = data.context?.open_chat_id || data.open_chat_id;
|
|
322
|
+
const cardMessageId = data.open_message_id || data.context?.open_message_id;
|
|
323
|
+
// ── CommandCard 分支:按钮直接触发命令 ──
|
|
324
|
+
if (value._command) {
|
|
325
|
+
if (value._initiator && operatorId && operatorId !== value._initiator) {
|
|
326
|
+
return {
|
|
327
|
+
toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
logger.info(`[Feishu] CommandCard trigger: command=${value._command}, operator=${operatorId}`);
|
|
331
|
+
if (this.messageHandler) {
|
|
332
|
+
// Feishu chatId 前缀:oc_ = group chat,ou_ = private user open_id
|
|
333
|
+
const chatType = typeof chatId === 'string' && chatId.startsWith('oc_') ? 'group' : 'private';
|
|
334
|
+
await this.messageHandler({
|
|
335
|
+
channelId: chatId,
|
|
336
|
+
content: value._command,
|
|
337
|
+
chatType,
|
|
338
|
+
peerId: operatorId,
|
|
339
|
+
messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
340
|
+
source: 'card-trigger',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// 点击后从作废集合移除——已 resolved 的卡片不再被新卡片到来时 PATCH
|
|
344
|
+
if (chatId && cardMessageId)
|
|
345
|
+
this.untrackPendingCard(chatId, cardMessageId);
|
|
346
|
+
const cardTitle = value._card_title || '操作';
|
|
347
|
+
const btnLabel = value._btn_label || value._command;
|
|
348
|
+
const cardBody = value._card_body || '';
|
|
349
|
+
return this.buildResolvedCard(cardTitle, { type: 'interaction.response', id: '', action: value._command, operatorId }, cardBody, btnLabel);
|
|
350
|
+
}
|
|
351
|
+
// ── ActionInteraction 分支 ──
|
|
281
352
|
const requestId = value._request_id;
|
|
282
353
|
if (!requestId) {
|
|
283
|
-
logger.debug('[Feishu] Card action without _request_id, ignoring');
|
|
354
|
+
logger.debug('[Feishu] Card action without _request_id or _command, ignoring');
|
|
284
355
|
return;
|
|
285
356
|
}
|
|
357
|
+
// initiator 校验
|
|
358
|
+
if (value._initiator && operatorId && operatorId !== value._initiator) {
|
|
359
|
+
return {
|
|
360
|
+
toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
|
|
361
|
+
};
|
|
362
|
+
}
|
|
286
363
|
// Legacy field change (non-form select_static with _field_key): ignore silently
|
|
287
364
|
if (value._field_key) {
|
|
288
365
|
logger.debug(`[Feishu] Legacy field change: requestId=${requestId}, field=${value._field_key}`);
|
|
@@ -295,11 +372,12 @@ export class FeishuChannel {
|
|
|
295
372
|
id: requestId,
|
|
296
373
|
action: value._action || 'submit',
|
|
297
374
|
values: { ...formValues, ...value },
|
|
298
|
-
operatorId
|
|
375
|
+
operatorId,
|
|
299
376
|
};
|
|
300
377
|
// Remove internal fields from values
|
|
301
378
|
delete response.values._request_id;
|
|
302
379
|
delete response.values._action;
|
|
380
|
+
delete response.values._initiator;
|
|
303
381
|
delete response.values._card_title;
|
|
304
382
|
const cardBody = value._card_body || '';
|
|
305
383
|
delete response.values._card_body;
|
|
@@ -307,6 +385,9 @@ export class FeishuChannel {
|
|
|
307
385
|
delete response.values._btn_label;
|
|
308
386
|
logger.info(`[Feishu] Card action: requestId=${requestId}, action=${response.action}, values=${JSON.stringify(response.values)}`);
|
|
309
387
|
this.interactionCallback?.(response);
|
|
388
|
+
// 点击后从作废集合移除——已 resolved 的卡片不再被新卡片到来时 PATCH
|
|
389
|
+
if (chatId && cardMessageId)
|
|
390
|
+
this.untrackPendingCard(chatId, cardMessageId);
|
|
310
391
|
// Return updated card (buttons disabled + result shown)
|
|
311
392
|
const cardTitle = value._card_title || '操作';
|
|
312
393
|
return this.buildResolvedCard(cardTitle, response, cardBody, btnLabel);
|
|
@@ -604,7 +685,35 @@ export class FeishuChannel {
|
|
|
604
685
|
return this.seenMessages.has(msgId);
|
|
605
686
|
}
|
|
606
687
|
markSeen(msgId) {
|
|
607
|
-
|
|
688
|
+
const now = Date.now();
|
|
689
|
+
this.seenMessages.set(msgId, now);
|
|
690
|
+
// 持久化到文件,供重启后去重
|
|
691
|
+
if (this.config.seenMsgFile) {
|
|
692
|
+
try {
|
|
693
|
+
fs.appendFileSync(this.config.seenMsgFile, JSON.stringify({ id: msgId, ts: now }) + '\n');
|
|
694
|
+
}
|
|
695
|
+
catch { }
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
loadSeenMessages() {
|
|
699
|
+
if (!this.config.seenMsgFile)
|
|
700
|
+
return;
|
|
701
|
+
try {
|
|
702
|
+
if (!fs.existsSync(this.config.seenMsgFile))
|
|
703
|
+
return;
|
|
704
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
705
|
+
const lines = fs.readFileSync(this.config.seenMsgFile, 'utf-8').split('\n').filter(Boolean);
|
|
706
|
+
for (const line of lines) {
|
|
707
|
+
try {
|
|
708
|
+
const { id, ts } = JSON.parse(line);
|
|
709
|
+
if (ts > cutoff)
|
|
710
|
+
this.seenMessages.set(id, ts);
|
|
711
|
+
}
|
|
712
|
+
catch { }
|
|
713
|
+
}
|
|
714
|
+
logger.info(`[Feishu] Loaded ${this.seenMessages.size} seen message ID(s) from disk`);
|
|
715
|
+
}
|
|
716
|
+
catch { }
|
|
608
717
|
}
|
|
609
718
|
startCleanupTask() {
|
|
610
719
|
this.cleanupInterval = setInterval(() => {
|
|
@@ -621,6 +730,16 @@ export class FeishuChannel {
|
|
|
621
730
|
// seenThreads 无时间戳,仅限容量(话题持久存在,不按时间清理)
|
|
622
731
|
if (this.seenThreads.size > 1000)
|
|
623
732
|
this.seenThreads.clear();
|
|
733
|
+
// 重写文件,去掉过期条目
|
|
734
|
+
if (this.config.seenMsgFile && this.seenMessages.size > 0) {
|
|
735
|
+
try {
|
|
736
|
+
const lines = [...this.seenMessages.entries()]
|
|
737
|
+
.map(([id, ts]) => JSON.stringify({ id, ts }))
|
|
738
|
+
.join('\n') + '\n';
|
|
739
|
+
fs.writeFileSync(this.config.seenMsgFile, lines);
|
|
740
|
+
}
|
|
741
|
+
catch { }
|
|
742
|
+
}
|
|
624
743
|
}, 60 * 60 * 1000);
|
|
625
744
|
}
|
|
626
745
|
async disconnect() {
|
|
@@ -705,6 +824,94 @@ export class FeishuChannel {
|
|
|
705
824
|
return null;
|
|
706
825
|
}
|
|
707
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* 提取合并转发消息的子消息内容。
|
|
829
|
+
* 调用 im.message.get 获取子消息列表,逐条解析 text/image/post/file 类型。
|
|
830
|
+
*/
|
|
831
|
+
async extractMergeForwardContent(messageId, chatId) {
|
|
832
|
+
const empty = { text: '', images: [] };
|
|
833
|
+
if (!this.client)
|
|
834
|
+
return empty;
|
|
835
|
+
try {
|
|
836
|
+
const res = await this.client.im.message.get({
|
|
837
|
+
path: { message_id: messageId }
|
|
838
|
+
});
|
|
839
|
+
const items = res.data?.items;
|
|
840
|
+
if (!items || items.length === 0) {
|
|
841
|
+
logger.warn('[Feishu] merge_forward: no sub-messages found');
|
|
842
|
+
return empty;
|
|
843
|
+
}
|
|
844
|
+
logger.info(`[Feishu] merge_forward: ${items.length} sub-messages`);
|
|
845
|
+
const projectPath = this.projectPathProvider
|
|
846
|
+
? await this.projectPathProvider(chatId)
|
|
847
|
+
: process.cwd();
|
|
848
|
+
const textParts = [];
|
|
849
|
+
const images = [];
|
|
850
|
+
const MAX_IMAGES = 10;
|
|
851
|
+
textParts.push('以下是用户转发的合并消息:\n---');
|
|
852
|
+
for (const item of items) {
|
|
853
|
+
const msgType = item.msg_type;
|
|
854
|
+
const content = item.body?.content;
|
|
855
|
+
if (!content)
|
|
856
|
+
continue;
|
|
857
|
+
try {
|
|
858
|
+
if (msgType === 'text') {
|
|
859
|
+
const parsed = JSON.parse(content);
|
|
860
|
+
textParts.push(parsed.text || '');
|
|
861
|
+
}
|
|
862
|
+
else if (msgType === 'post') {
|
|
863
|
+
const parsed = JSON.parse(content);
|
|
864
|
+
let text = '';
|
|
865
|
+
const postContent = parsed.zh_cn?.content || parsed.en_us?.content || parsed.content;
|
|
866
|
+
if (postContent) {
|
|
867
|
+
for (const line of postContent) {
|
|
868
|
+
for (const elem of line) {
|
|
869
|
+
if (elem.tag === 'img' && elem.image_key && item.message_id && images.length < MAX_IMAGES) {
|
|
870
|
+
const imageData = await this.downloadAndSaveImage(elem.image_key, chatId, item.message_id, projectPath);
|
|
871
|
+
if (imageData)
|
|
872
|
+
images.push(imageData);
|
|
873
|
+
}
|
|
874
|
+
else if (elem.text) {
|
|
875
|
+
text += elem.text;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
text += '\n';
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
const title = parsed.zh_cn?.title || parsed.en_us?.title || parsed.title;
|
|
882
|
+
textParts.push(title ? `${title}\n${text.trim()}` : text.trim());
|
|
883
|
+
}
|
|
884
|
+
else if (msgType === 'image' && item.message_id) {
|
|
885
|
+
const parsed = JSON.parse(content);
|
|
886
|
+
if (parsed.image_key && images.length < MAX_IMAGES) {
|
|
887
|
+
const imageData = await this.downloadAndSaveImage(parsed.image_key, chatId, item.message_id, projectPath);
|
|
888
|
+
if (imageData) {
|
|
889
|
+
images.push(imageData);
|
|
890
|
+
textParts.push('[图片]');
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
else if (msgType === 'file') {
|
|
895
|
+
const parsed = JSON.parse(content);
|
|
896
|
+
textParts.push(`[文件: ${parsed.file_name || 'unknown'}]`);
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
textParts.push(`[${msgType}]`);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
catch (parseErr) {
|
|
903
|
+
logger.debug('[Feishu] merge_forward: failed to parse sub-message:', parseErr);
|
|
904
|
+
textParts.push(`[${msgType}: 解析失败]`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
textParts.push('---');
|
|
908
|
+
return { text: textParts.join('\n'), images };
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
logger.error('[Feishu] Failed to extract merge_forward content:', error);
|
|
912
|
+
return empty;
|
|
913
|
+
}
|
|
914
|
+
}
|
|
708
915
|
async downloadFile(fileKey, fileName, messageId, projectPath) {
|
|
709
916
|
if (!this.client)
|
|
710
917
|
return null;
|
|
@@ -741,12 +948,63 @@ export class FeishuChannel {
|
|
|
741
948
|
return null;
|
|
742
949
|
}
|
|
743
950
|
}
|
|
951
|
+
/** 跟踪 pending 交互卡片,等待后续作废 */
|
|
952
|
+
trackPendingCard(chatId, messageId) {
|
|
953
|
+
let set = this.pendingCardsByChat.get(chatId);
|
|
954
|
+
if (!set) {
|
|
955
|
+
set = new Set();
|
|
956
|
+
this.pendingCardsByChat.set(chatId, set);
|
|
957
|
+
}
|
|
958
|
+
set.add(messageId);
|
|
959
|
+
}
|
|
960
|
+
/** 卡片已 resolved(用户点击了按钮,飞书已用回调返回值替换卡片),从作废集合移除 */
|
|
961
|
+
untrackPendingCard(chatId, messageId) {
|
|
962
|
+
const set = this.pendingCardsByChat.get(chatId);
|
|
963
|
+
if (!set)
|
|
964
|
+
return;
|
|
965
|
+
set.delete(messageId);
|
|
966
|
+
if (set.size === 0)
|
|
967
|
+
this.pendingCardsByChat.delete(chatId);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* 作废 chatId 下所有未被点击的旧卡片:PATCH 为"已过期"灰色卡片。
|
|
971
|
+
* 卡片需在 config 中声明 update_multi: true 才能被 PATCH。
|
|
972
|
+
*/
|
|
973
|
+
async invalidatePendingCards(chatId) {
|
|
974
|
+
const set = this.pendingCardsByChat.get(chatId);
|
|
975
|
+
if (!set || set.size === 0)
|
|
976
|
+
return;
|
|
977
|
+
const expiredCard = {
|
|
978
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
979
|
+
header: {
|
|
980
|
+
template: 'grey',
|
|
981
|
+
title: { tag: 'plain_text', content: '已过期' },
|
|
982
|
+
},
|
|
983
|
+
elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
|
|
984
|
+
};
|
|
985
|
+
const ids = Array.from(set);
|
|
986
|
+
this.pendingCardsByChat.delete(chatId);
|
|
987
|
+
await Promise.all(ids.map(async (msgId) => {
|
|
988
|
+
try {
|
|
989
|
+
await this.client.im.message.patch({
|
|
990
|
+
path: { message_id: msgId },
|
|
991
|
+
data: { content: JSON.stringify(expiredCard) },
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
catch (err) {
|
|
995
|
+
const detail = err?.response?.data ?? err?.message ?? err;
|
|
996
|
+
logger.debug(`[Feishu] Patch expired card failed (msgId=${msgId}): ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`);
|
|
997
|
+
}
|
|
998
|
+
}));
|
|
999
|
+
}
|
|
744
1000
|
async sendInteraction(chatId, interaction, options) {
|
|
745
1001
|
if (!this.client)
|
|
746
1002
|
return false;
|
|
747
1003
|
const card = buildInteractionCard(interaction);
|
|
748
1004
|
if (!card)
|
|
749
1005
|
return false;
|
|
1006
|
+
// 在新卡发送前作废旧卡(PATCH 为"已过期"),避免历史卡片仍可点击
|
|
1007
|
+
await this.invalidatePendingCards(chatId);
|
|
750
1008
|
try {
|
|
751
1009
|
let messageId;
|
|
752
1010
|
if (options?.replyToMessageId) {
|
|
@@ -774,6 +1032,8 @@ export class FeishuChannel {
|
|
|
774
1032
|
messageId = res?.data?.message_id;
|
|
775
1033
|
}
|
|
776
1034
|
logger.info(`[Feishu] Sent interaction card: ${interaction.id}, messageId=${messageId}`);
|
|
1035
|
+
if (messageId)
|
|
1036
|
+
this.trackPendingCard(chatId, messageId);
|
|
777
1037
|
return messageId || false;
|
|
778
1038
|
}
|
|
779
1039
|
catch (error) {
|
|
@@ -790,19 +1050,6 @@ export class FeishuChannel {
|
|
|
790
1050
|
return false;
|
|
791
1051
|
}
|
|
792
1052
|
}
|
|
793
|
-
async patchInteractionCard(messageId, card) {
|
|
794
|
-
if (!this.client)
|
|
795
|
-
return;
|
|
796
|
-
try {
|
|
797
|
-
await this.client.im.message.patch({
|
|
798
|
-
path: { message_id: messageId },
|
|
799
|
-
data: { content: JSON.stringify(card) },
|
|
800
|
-
});
|
|
801
|
-
}
|
|
802
|
-
catch (error) {
|
|
803
|
-
logger.warn(`[Feishu] Failed to patch card ${messageId}:`, error?.response?.data || error?.message);
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
1053
|
buildResolvedCard(cardTitle, response, cardBody, btnLabel) {
|
|
807
1054
|
const action = response.action;
|
|
808
1055
|
const labelMap = {
|
|
@@ -812,7 +1059,6 @@ export class FeishuChannel {
|
|
|
812
1059
|
'cancel': '取消',
|
|
813
1060
|
};
|
|
814
1061
|
const statusText = labelMap[action] || (btnLabel ? `✅ ${btnLabel}` : `✅ ${action}`);
|
|
815
|
-
// Build elements: original body only
|
|
816
1062
|
const elements = [];
|
|
817
1063
|
if (cardBody) {
|
|
818
1064
|
elements.push({ tag: 'markdown', content: cardBody });
|
|
@@ -825,7 +1071,7 @@ export class FeishuChannel {
|
|
|
825
1071
|
card: {
|
|
826
1072
|
type: 'raw',
|
|
827
1073
|
data: {
|
|
828
|
-
config: { wide_screen_mode: true },
|
|
1074
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
829
1075
|
header: {
|
|
830
1076
|
template: action === 'deny' ? 'red' : 'green',
|
|
831
1077
|
title: { tag: 'plain_text', content: `${cardTitle} — ${statusText}` },
|
|
@@ -849,12 +1095,57 @@ export class FeishuChannel {
|
|
|
849
1095
|
// ── 交互卡片构建工具 ──
|
|
850
1096
|
export function buildInteractionCard(interaction) {
|
|
851
1097
|
const { kind } = interaction;
|
|
1098
|
+
if (kind.kind === 'command-card') {
|
|
1099
|
+
return buildCommandCardFeishu(kind, interaction.initiatorId);
|
|
1100
|
+
}
|
|
852
1101
|
if (kind.kind === 'action') {
|
|
853
|
-
return buildActionCard(interaction.id, kind);
|
|
1102
|
+
return buildActionCard(interaction.id, kind, interaction.initiatorId);
|
|
854
1103
|
}
|
|
855
1104
|
return null;
|
|
856
1105
|
}
|
|
857
|
-
|
|
1106
|
+
function buildCommandCardFeishu(card, initiatorId) {
|
|
1107
|
+
const elements = [];
|
|
1108
|
+
if (card.body) {
|
|
1109
|
+
elements.push({ tag: 'markdown', content: card.body });
|
|
1110
|
+
}
|
|
1111
|
+
// Build full card body for resolved state: original body + button labels
|
|
1112
|
+
const btnLabels = card.buttons.map(btn => btn.label).join(' · ');
|
|
1113
|
+
const fullCardBody = [card.body, btnLabels].filter(Boolean).join('\n\n');
|
|
1114
|
+
const buttons = card.buttons.map(btn => {
|
|
1115
|
+
const buttonEl = {
|
|
1116
|
+
tag: 'button',
|
|
1117
|
+
text: { tag: 'plain_text', content: btn.label },
|
|
1118
|
+
type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
|
|
1119
|
+
value: {
|
|
1120
|
+
_command: btn.command,
|
|
1121
|
+
_initiator: initiatorId,
|
|
1122
|
+
_card_title: card.title,
|
|
1123
|
+
_card_body: fullCardBody,
|
|
1124
|
+
_btn_label: btn.label,
|
|
1125
|
+
},
|
|
1126
|
+
};
|
|
1127
|
+
if (btn.disabled) {
|
|
1128
|
+
buttonEl.disabled = true;
|
|
1129
|
+
}
|
|
1130
|
+
if (btn.confirm) {
|
|
1131
|
+
buttonEl.confirm = {
|
|
1132
|
+
title: { tag: 'plain_text', content: btn.confirm.title },
|
|
1133
|
+
text: { tag: 'plain_text', content: btn.confirm.body },
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
return buttonEl;
|
|
1137
|
+
});
|
|
1138
|
+
elements.push({ tag: 'action', actions: buttons });
|
|
1139
|
+
return {
|
|
1140
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
1141
|
+
header: {
|
|
1142
|
+
template: 'blue',
|
|
1143
|
+
title: { tag: 'plain_text', content: card.title },
|
|
1144
|
+
},
|
|
1145
|
+
elements,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
export function buildActionCard(requestId, action, initiatorId) {
|
|
858
1149
|
const elements = [];
|
|
859
1150
|
// Body text
|
|
860
1151
|
if (action.body) {
|
|
@@ -872,6 +1163,7 @@ export function buildActionCard(requestId, action) {
|
|
|
872
1163
|
value: {
|
|
873
1164
|
_request_id: requestId,
|
|
874
1165
|
_action: btn.key,
|
|
1166
|
+
_initiator: initiatorId,
|
|
875
1167
|
_card_title: action.title,
|
|
876
1168
|
_card_body: fullCardBody,
|
|
877
1169
|
_btn_label: btn.label,
|
|
@@ -890,7 +1182,7 @@ export function buildActionCard(requestId, action) {
|
|
|
890
1182
|
actions: buttons,
|
|
891
1183
|
});
|
|
892
1184
|
return {
|
|
893
|
-
config: { wide_screen_mode: true },
|
|
1185
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
894
1186
|
header: {
|
|
895
1187
|
template: 'blue',
|
|
896
1188
|
title: { tag: 'plain_text', content: action.title },
|
|
@@ -999,7 +1291,8 @@ export function hasMarkdownSyntax(text) {
|
|
|
999
1291
|
];
|
|
1000
1292
|
return markdownPatterns.some(pattern => pattern.test(text));
|
|
1001
1293
|
}
|
|
1002
|
-
import { normalizeChannelInstances, getChannelShowActivities } from '../
|
|
1294
|
+
import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
|
|
1295
|
+
import { resolvePaths } from '../paths.js';
|
|
1003
1296
|
export class FeishuChannelPlugin {
|
|
1004
1297
|
name = 'feishu';
|
|
1005
1298
|
isEnabled(config) {
|
|
@@ -1023,16 +1316,59 @@ export class FeishuChannelPlugin {
|
|
|
1023
1316
|
appId: inst.appId,
|
|
1024
1317
|
appSecret: inst.appSecret,
|
|
1025
1318
|
enableRichContent: config.enableRichContent,
|
|
1319
|
+
seenMsgFile: path.join(resolvePaths().dataDir, `feishu-seen-${inst.name}.jsonl`),
|
|
1026
1320
|
});
|
|
1027
1321
|
const adapter = {
|
|
1028
1322
|
channelName: inst.name,
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1323
|
+
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true },
|
|
1324
|
+
send: async (envelope, payload) => {
|
|
1325
|
+
const ctx = envelope.replyContext;
|
|
1326
|
+
const channelId = envelope.channelId;
|
|
1327
|
+
switch (payload.kind) {
|
|
1328
|
+
case 'result.text':
|
|
1329
|
+
case 'command.result':
|
|
1330
|
+
case 'command.error':
|
|
1331
|
+
case 'system.notice':
|
|
1332
|
+
case 'system.error':
|
|
1333
|
+
case 'result.error': {
|
|
1334
|
+
const sendCtx = { ...(ctx ?? {}) };
|
|
1335
|
+
if (payload.kind === 'result.text' && payload.isFinal)
|
|
1336
|
+
sendCtx.title = '✅ 最终回复:';
|
|
1337
|
+
await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
case 'result.file':
|
|
1341
|
+
await channel.sendFile(channelId, payload.filePath, ctx);
|
|
1342
|
+
return;
|
|
1343
|
+
case 'result.image':
|
|
1344
|
+
await channel.sendImage(channelId, payload.data, ctx);
|
|
1345
|
+
return;
|
|
1346
|
+
case 'activity.batch': {
|
|
1347
|
+
// Feishu 不发送成功的 tool_result(信息密度低,刷屏)
|
|
1348
|
+
const filtered = payload.items.filter((i) => !(i.kind === 'tool_result' && i.ok));
|
|
1349
|
+
const text = formatItemsAsText(filtered);
|
|
1350
|
+
if (text) {
|
|
1351
|
+
await channel.sendMessage(channelId, text, ctx);
|
|
1352
|
+
}
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
case 'status.started':
|
|
1356
|
+
case 'status.completed':
|
|
1357
|
+
case 'status.interrupted':
|
|
1358
|
+
case 'status.error':
|
|
1359
|
+
case 'status.timeout':
|
|
1360
|
+
// Feishu 通过 acknowledge (✓ 表情) 表达状态,由 channel 自行处理
|
|
1361
|
+
return;
|
|
1362
|
+
case 'interaction':
|
|
1363
|
+
await channel.sendInteraction(channelId, payload.interaction, ctx);
|
|
1364
|
+
return;
|
|
1365
|
+
case 'custom':
|
|
1366
|
+
// Feishu 不支持自定义 payload
|
|
1367
|
+
return;
|
|
1368
|
+
default:
|
|
1369
|
+
logger.warn(`[Feishu] Unhandled payload kind: ${payload.kind}`);
|
|
1370
|
+
}
|
|
1371
|
+
}, acknowledge: (messageId) => { channel.addAckReaction(messageId); return Promise.resolve(); }, onInteraction: (callback) => channel.onInteraction(callback),
|
|
1036
1372
|
};
|
|
1037
1373
|
const policy = {
|
|
1038
1374
|
canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
@@ -1077,6 +1413,19 @@ export class FeishuChannelPlugin {
|
|
|
1077
1413
|
connect: () => channel.connect(),
|
|
1078
1414
|
disconnect: () => channel.disconnect(),
|
|
1079
1415
|
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
1416
|
+
registerBridge(bridge, channelType) {
|
|
1417
|
+
bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
|
|
1418
|
+
await handler({
|
|
1419
|
+
channel: adapter.channelName, channelType, channelId: chatId, content, images, chatType,
|
|
1420
|
+
peerId: peerId || '', peerName, messageId, mentions, threadId,
|
|
1421
|
+
replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
|
|
1422
|
+
source,
|
|
1423
|
+
});
|
|
1424
|
+
}), (channelId, text, replyContext) => channel.sendMessage(channelId, text, {
|
|
1425
|
+
replyToMessageId: replyContext?.replyToMessageId,
|
|
1426
|
+
replyInThread: replyContext?.replyInThread,
|
|
1427
|
+
}), adapter, channelType);
|
|
1428
|
+
},
|
|
1080
1429
|
});
|
|
1081
1430
|
}
|
|
1082
1431
|
return result;
|