evolclaw 2.8.3 → 3.0.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/dist/agents/claude-runner.js +102 -38
- package/dist/agents/codex-runner.js +11 -14
- package/dist/agents/gemini-runner.js +10 -12
- package/dist/agents/resolve.js +134 -0
- package/dist/agents/templates.js +3 -3
- package/dist/aun/aid/agentmd.js +186 -0
- package/dist/aun/aid/client.js +134 -0
- package/dist/aun/aid/identity.js +131 -0
- package/dist/aun/aid/index.js +3 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/msg/group.js +291 -0
- package/dist/aun/msg/index.js +4 -0
- package/dist/aun/msg/p2p.js +144 -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 +1051 -288
- package/dist/channels/dingtalk.js +58 -5
- package/dist/channels/feishu.js +266 -30
- package/dist/channels/qqbot.js +67 -12
- package/dist/channels/wechat.js +61 -4
- package/dist/channels/wecom.js +58 -5
- package/dist/cli/agent.js +800 -0
- package/dist/cli/index.js +4253 -0
- package/dist/{utils → cli}/init-channel.js +211 -621
- package/dist/cli/init.js +178 -0
- package/dist/config-store.js +613 -0
- package/dist/core/{agent-loader.js → baseagent-loader.js} +6 -12
- package/dist/core/channel-loader.js +162 -11
- package/dist/core/command-handler.js +858 -847
- package/dist/core/evolagent-registry.js +191 -371
- package/dist/core/evolagent.js +203 -234
- package/dist/core/interaction-router.js +52 -5
- package/dist/core/message/im-renderer.js +480 -0
- package/dist/core/message/items-formatter.js +61 -0
- package/dist/core/message/message-bridge.js +104 -56
- package/dist/core/message/message-log.js +91 -0
- package/dist/core/message/message-processor.js +309 -142
- package/dist/core/message/message-queue.js +3 -3
- package/dist/core/permission.js +21 -8
- 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 +704 -775
- 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/{templates → data}/prompts.md +34 -1
- package/dist/index.js +431 -275
- package/dist/ipc.js +49 -0
- package/dist/paths.js +82 -9
- package/dist/types.js +8 -2
- package/dist/utils/atomic-write.js +79 -0
- package/dist/utils/channel-helpers.js +46 -0
- package/dist/utils/cross-platform.js +0 -18
- package/dist/utils/instance-registry.js +433 -0
- package/dist/utils/log-writer.js +216 -0
- package/dist/utils/logger.js +24 -77
- package/dist/utils/media-cache.js +23 -0
- package/dist/utils/{upgrade.js → npm-ops.js} +52 -21
- package/dist/utils/process-introspect.js +144 -0
- package/dist/utils/stats.js +192 -0
- package/dist/watch-msg.js +529 -0
- package/evolclaw-install-aun.md +114 -46
- package/kits/aun/meta.md +25 -0
- package/kits/aun/role.md +25 -0
- package/kits/channels/aun.md +25 -0
- package/kits/evolclaw/commands.md +31 -0
- package/kits/evolclaw/identity-tools.md +26 -0
- package/kits/evolclaw/self-summary.md +29 -0
- package/kits/evolclaw/tools.md +25 -0
- package/kits/templates/group.md +20 -0
- package/kits/templates/private.md +9 -0
- package/kits/templates/system-fragments/personal-context.md +3 -0
- package/kits/templates/system-fragments/self-intro.md +5 -0
- package/kits/templates/system-fragments/speaker-intro.md +5 -0
- package/kits/templates/system-fragments/venue-intro.md +5 -0
- package/package.json +7 -5
- package/data/evolclaw.sample.json +0 -60
- 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/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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { logger } from '../utils/logger.js';
|
|
2
|
-
import { requireOptional } from '../utils/
|
|
3
|
-
import { normalizeChannelInstances, getChannelShowActivities } from '../
|
|
2
|
+
import { requireOptional } from '../utils/npm-ops.js';
|
|
3
|
+
import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
|
|
4
|
+
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
4
5
|
// ── Webhook SSRF validation ────────────────────────────────────────────────────
|
|
5
6
|
const WEBHOOK_RE = /^https:\/\/(api|oapi)\.dingtalk\.com\//;
|
|
6
7
|
// ── DingtalkChannel ────────────────────────────────────────────────────────────
|
|
@@ -446,9 +447,46 @@ export class DingtalkChannelPlugin {
|
|
|
446
447
|
});
|
|
447
448
|
const adapter = {
|
|
448
449
|
channelName: inst.name,
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
450
|
+
capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
|
|
451
|
+
send: async (envelope, payload) => {
|
|
452
|
+
const ctx = envelope.replyContext;
|
|
453
|
+
const channelId = envelope.channelId;
|
|
454
|
+
switch (payload.kind) {
|
|
455
|
+
case 'result.text':
|
|
456
|
+
case 'command.result':
|
|
457
|
+
case 'command.error':
|
|
458
|
+
case 'system.notice':
|
|
459
|
+
case 'system.error':
|
|
460
|
+
case 'result.error':
|
|
461
|
+
await channel.sendMessage(channelId, payload.text);
|
|
462
|
+
return;
|
|
463
|
+
case 'result.file':
|
|
464
|
+
await channel.sendFile(channelId, payload.filePath);
|
|
465
|
+
return;
|
|
466
|
+
case 'result.image':
|
|
467
|
+
await channel.sendImage(channelId, payload.data);
|
|
468
|
+
return;
|
|
469
|
+
case 'activity.batch': {
|
|
470
|
+
const text = formatItemsAsText(payload.items);
|
|
471
|
+
if (text)
|
|
472
|
+
await channel.sendMessage(channelId, text);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
case 'interaction':
|
|
476
|
+
if (payload.fallbackText)
|
|
477
|
+
await channel.sendMessage(channelId, payload.fallbackText);
|
|
478
|
+
return;
|
|
479
|
+
case 'status.started':
|
|
480
|
+
case 'status.completed':
|
|
481
|
+
case 'status.interrupted':
|
|
482
|
+
case 'status.error':
|
|
483
|
+
case 'status.timeout':
|
|
484
|
+
case 'custom':
|
|
485
|
+
return;
|
|
486
|
+
default:
|
|
487
|
+
logger.warn(`[DingTalk] Unhandled payload kind: ${payload.kind}`);
|
|
488
|
+
}
|
|
489
|
+
},
|
|
452
490
|
};
|
|
453
491
|
const policy = {
|
|
454
492
|
canSwitchProject: (_chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
@@ -493,6 +531,21 @@ export class DingtalkChannelPlugin {
|
|
|
493
531
|
connect: () => channel.connect(),
|
|
494
532
|
disconnect: () => channel.disconnect(),
|
|
495
533
|
onProjectPathRequest: () => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
534
|
+
registerBridge(bridge, channelType) {
|
|
535
|
+
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (event) => {
|
|
536
|
+
await handler({
|
|
537
|
+
channel: adapter.channelName,
|
|
538
|
+
channelType,
|
|
539
|
+
channelId: event.channelId,
|
|
540
|
+
content: event.content,
|
|
541
|
+
images: event.images,
|
|
542
|
+
chatType: event.chatType || 'private',
|
|
543
|
+
peerId: event.peerId || '',
|
|
544
|
+
peerName: event.peerName,
|
|
545
|
+
messageId: event.messageId,
|
|
546
|
+
});
|
|
547
|
+
}), (channelId, text) => channel.sendMessage(channelId, text), adapter, channelType);
|
|
548
|
+
},
|
|
496
549
|
});
|
|
497
550
|
}
|
|
498
551
|
return result;
|
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 事件直接获取)
|
|
@@ -278,11 +295,49 @@ export class FeishuChannel {
|
|
|
278
295
|
if (!action?.value)
|
|
279
296
|
return;
|
|
280
297
|
const value = action.value;
|
|
298
|
+
const operatorId = data.operator?.open_id;
|
|
299
|
+
const chatId = data.context?.open_chat_id || data.open_chat_id;
|
|
300
|
+
const cardMessageId = data.open_message_id || data.context?.open_message_id;
|
|
301
|
+
// ── CommandCard 分支:按钮直接触发命令 ──
|
|
302
|
+
if (value._command) {
|
|
303
|
+
if (value._initiator && operatorId && operatorId !== value._initiator) {
|
|
304
|
+
return {
|
|
305
|
+
toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
logger.info(`[Feishu] CommandCard trigger: command=${value._command}, operator=${operatorId}`);
|
|
309
|
+
if (this.messageHandler) {
|
|
310
|
+
// Feishu chatId 前缀:oc_ = group chat,ou_ = private user open_id
|
|
311
|
+
const chatType = typeof chatId === 'string' && chatId.startsWith('oc_') ? 'group' : 'private';
|
|
312
|
+
await this.messageHandler({
|
|
313
|
+
channelId: chatId,
|
|
314
|
+
content: value._command,
|
|
315
|
+
chatType,
|
|
316
|
+
peerId: operatorId,
|
|
317
|
+
messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
318
|
+
source: 'card-trigger',
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
// 点击后从作废集合移除——已 resolved 的卡片不再被新卡片到来时 PATCH
|
|
322
|
+
if (chatId && cardMessageId)
|
|
323
|
+
this.untrackPendingCard(chatId, cardMessageId);
|
|
324
|
+
const cardTitle = value._card_title || '操作';
|
|
325
|
+
const btnLabel = value._btn_label || value._command;
|
|
326
|
+
const cardBody = value._card_body || '';
|
|
327
|
+
return this.buildResolvedCard(cardTitle, { type: 'interaction.response', id: '', action: value._command, operatorId }, cardBody, btnLabel);
|
|
328
|
+
}
|
|
329
|
+
// ── ActionInteraction 分支 ──
|
|
281
330
|
const requestId = value._request_id;
|
|
282
331
|
if (!requestId) {
|
|
283
|
-
logger.debug('[Feishu] Card action without _request_id, ignoring');
|
|
332
|
+
logger.debug('[Feishu] Card action without _request_id or _command, ignoring');
|
|
284
333
|
return;
|
|
285
334
|
}
|
|
335
|
+
// initiator 校验
|
|
336
|
+
if (value._initiator && operatorId && operatorId !== value._initiator) {
|
|
337
|
+
return {
|
|
338
|
+
toast: { type: 'warning', content: '⚠️ 仅卡片发起者可操作' },
|
|
339
|
+
};
|
|
340
|
+
}
|
|
286
341
|
// Legacy field change (non-form select_static with _field_key): ignore silently
|
|
287
342
|
if (value._field_key) {
|
|
288
343
|
logger.debug(`[Feishu] Legacy field change: requestId=${requestId}, field=${value._field_key}`);
|
|
@@ -295,11 +350,12 @@ export class FeishuChannel {
|
|
|
295
350
|
id: requestId,
|
|
296
351
|
action: value._action || 'submit',
|
|
297
352
|
values: { ...formValues, ...value },
|
|
298
|
-
operatorId
|
|
353
|
+
operatorId,
|
|
299
354
|
};
|
|
300
355
|
// Remove internal fields from values
|
|
301
356
|
delete response.values._request_id;
|
|
302
357
|
delete response.values._action;
|
|
358
|
+
delete response.values._initiator;
|
|
303
359
|
delete response.values._card_title;
|
|
304
360
|
const cardBody = value._card_body || '';
|
|
305
361
|
delete response.values._card_body;
|
|
@@ -307,6 +363,9 @@ export class FeishuChannel {
|
|
|
307
363
|
delete response.values._btn_label;
|
|
308
364
|
logger.info(`[Feishu] Card action: requestId=${requestId}, action=${response.action}, values=${JSON.stringify(response.values)}`);
|
|
309
365
|
this.interactionCallback?.(response);
|
|
366
|
+
// 点击后从作废集合移除——已 resolved 的卡片不再被新卡片到来时 PATCH
|
|
367
|
+
if (chatId && cardMessageId)
|
|
368
|
+
this.untrackPendingCard(chatId, cardMessageId);
|
|
310
369
|
// Return updated card (buttons disabled + result shown)
|
|
311
370
|
const cardTitle = value._card_title || '操作';
|
|
312
371
|
return this.buildResolvedCard(cardTitle, response, cardBody, btnLabel);
|
|
@@ -604,7 +663,35 @@ export class FeishuChannel {
|
|
|
604
663
|
return this.seenMessages.has(msgId);
|
|
605
664
|
}
|
|
606
665
|
markSeen(msgId) {
|
|
607
|
-
|
|
666
|
+
const now = Date.now();
|
|
667
|
+
this.seenMessages.set(msgId, now);
|
|
668
|
+
// 持久化到文件,供重启后去重
|
|
669
|
+
if (this.config.seenMsgFile) {
|
|
670
|
+
try {
|
|
671
|
+
fs.appendFileSync(this.config.seenMsgFile, JSON.stringify({ id: msgId, ts: now }) + '\n');
|
|
672
|
+
}
|
|
673
|
+
catch { }
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
loadSeenMessages() {
|
|
677
|
+
if (!this.config.seenMsgFile)
|
|
678
|
+
return;
|
|
679
|
+
try {
|
|
680
|
+
if (!fs.existsSync(this.config.seenMsgFile))
|
|
681
|
+
return;
|
|
682
|
+
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
|
683
|
+
const lines = fs.readFileSync(this.config.seenMsgFile, 'utf-8').split('\n').filter(Boolean);
|
|
684
|
+
for (const line of lines) {
|
|
685
|
+
try {
|
|
686
|
+
const { id, ts } = JSON.parse(line);
|
|
687
|
+
if (ts > cutoff)
|
|
688
|
+
this.seenMessages.set(id, ts);
|
|
689
|
+
}
|
|
690
|
+
catch { }
|
|
691
|
+
}
|
|
692
|
+
logger.info(`[Feishu] Loaded ${this.seenMessages.size} seen message ID(s) from disk`);
|
|
693
|
+
}
|
|
694
|
+
catch { }
|
|
608
695
|
}
|
|
609
696
|
startCleanupTask() {
|
|
610
697
|
this.cleanupInterval = setInterval(() => {
|
|
@@ -621,6 +708,16 @@ export class FeishuChannel {
|
|
|
621
708
|
// seenThreads 无时间戳,仅限容量(话题持久存在,不按时间清理)
|
|
622
709
|
if (this.seenThreads.size > 1000)
|
|
623
710
|
this.seenThreads.clear();
|
|
711
|
+
// 重写文件,去掉过期条目
|
|
712
|
+
if (this.config.seenMsgFile && this.seenMessages.size > 0) {
|
|
713
|
+
try {
|
|
714
|
+
const lines = [...this.seenMessages.entries()]
|
|
715
|
+
.map(([id, ts]) => JSON.stringify({ id, ts }))
|
|
716
|
+
.join('\n') + '\n';
|
|
717
|
+
fs.writeFileSync(this.config.seenMsgFile, lines);
|
|
718
|
+
}
|
|
719
|
+
catch { }
|
|
720
|
+
}
|
|
624
721
|
}, 60 * 60 * 1000);
|
|
625
722
|
}
|
|
626
723
|
async disconnect() {
|
|
@@ -741,12 +838,63 @@ export class FeishuChannel {
|
|
|
741
838
|
return null;
|
|
742
839
|
}
|
|
743
840
|
}
|
|
841
|
+
/** 跟踪 pending 交互卡片,等待后续作废 */
|
|
842
|
+
trackPendingCard(chatId, messageId) {
|
|
843
|
+
let set = this.pendingCardsByChat.get(chatId);
|
|
844
|
+
if (!set) {
|
|
845
|
+
set = new Set();
|
|
846
|
+
this.pendingCardsByChat.set(chatId, set);
|
|
847
|
+
}
|
|
848
|
+
set.add(messageId);
|
|
849
|
+
}
|
|
850
|
+
/** 卡片已 resolved(用户点击了按钮,飞书已用回调返回值替换卡片),从作废集合移除 */
|
|
851
|
+
untrackPendingCard(chatId, messageId) {
|
|
852
|
+
const set = this.pendingCardsByChat.get(chatId);
|
|
853
|
+
if (!set)
|
|
854
|
+
return;
|
|
855
|
+
set.delete(messageId);
|
|
856
|
+
if (set.size === 0)
|
|
857
|
+
this.pendingCardsByChat.delete(chatId);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* 作废 chatId 下所有未被点击的旧卡片:PATCH 为"已过期"灰色卡片。
|
|
861
|
+
* 卡片需在 config 中声明 update_multi: true 才能被 PATCH。
|
|
862
|
+
*/
|
|
863
|
+
async invalidatePendingCards(chatId) {
|
|
864
|
+
const set = this.pendingCardsByChat.get(chatId);
|
|
865
|
+
if (!set || set.size === 0)
|
|
866
|
+
return;
|
|
867
|
+
const expiredCard = {
|
|
868
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
869
|
+
header: {
|
|
870
|
+
template: 'grey',
|
|
871
|
+
title: { tag: 'plain_text', content: '已过期' },
|
|
872
|
+
},
|
|
873
|
+
elements: [{ tag: 'markdown', content: '此卡片已过期,请查看最新卡片。' }],
|
|
874
|
+
};
|
|
875
|
+
const ids = Array.from(set);
|
|
876
|
+
this.pendingCardsByChat.delete(chatId);
|
|
877
|
+
await Promise.all(ids.map(async (msgId) => {
|
|
878
|
+
try {
|
|
879
|
+
await this.client.im.message.patch({
|
|
880
|
+
path: { message_id: msgId },
|
|
881
|
+
data: { content: JSON.stringify(expiredCard) },
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
catch (err) {
|
|
885
|
+
const detail = err?.response?.data ?? err?.message ?? err;
|
|
886
|
+
logger.debug(`[Feishu] Patch expired card failed (msgId=${msgId}): ${typeof detail === 'string' ? detail : JSON.stringify(detail)}`);
|
|
887
|
+
}
|
|
888
|
+
}));
|
|
889
|
+
}
|
|
744
890
|
async sendInteraction(chatId, interaction, options) {
|
|
745
891
|
if (!this.client)
|
|
746
892
|
return false;
|
|
747
893
|
const card = buildInteractionCard(interaction);
|
|
748
894
|
if (!card)
|
|
749
895
|
return false;
|
|
896
|
+
// 在新卡发送前作废旧卡(PATCH 为"已过期"),避免历史卡片仍可点击
|
|
897
|
+
await this.invalidatePendingCards(chatId);
|
|
750
898
|
try {
|
|
751
899
|
let messageId;
|
|
752
900
|
if (options?.replyToMessageId) {
|
|
@@ -774,6 +922,8 @@ export class FeishuChannel {
|
|
|
774
922
|
messageId = res?.data?.message_id;
|
|
775
923
|
}
|
|
776
924
|
logger.info(`[Feishu] Sent interaction card: ${interaction.id}, messageId=${messageId}`);
|
|
925
|
+
if (messageId)
|
|
926
|
+
this.trackPendingCard(chatId, messageId);
|
|
777
927
|
return messageId || false;
|
|
778
928
|
}
|
|
779
929
|
catch (error) {
|
|
@@ -790,19 +940,6 @@ export class FeishuChannel {
|
|
|
790
940
|
return false;
|
|
791
941
|
}
|
|
792
942
|
}
|
|
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
943
|
buildResolvedCard(cardTitle, response, cardBody, btnLabel) {
|
|
807
944
|
const action = response.action;
|
|
808
945
|
const labelMap = {
|
|
@@ -812,7 +949,6 @@ export class FeishuChannel {
|
|
|
812
949
|
'cancel': '取消',
|
|
813
950
|
};
|
|
814
951
|
const statusText = labelMap[action] || (btnLabel ? `✅ ${btnLabel}` : `✅ ${action}`);
|
|
815
|
-
// Build elements: original body only
|
|
816
952
|
const elements = [];
|
|
817
953
|
if (cardBody) {
|
|
818
954
|
elements.push({ tag: 'markdown', content: cardBody });
|
|
@@ -825,7 +961,7 @@ export class FeishuChannel {
|
|
|
825
961
|
card: {
|
|
826
962
|
type: 'raw',
|
|
827
963
|
data: {
|
|
828
|
-
config: { wide_screen_mode: true },
|
|
964
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
829
965
|
header: {
|
|
830
966
|
template: action === 'deny' ? 'red' : 'green',
|
|
831
967
|
title: { tag: 'plain_text', content: `${cardTitle} — ${statusText}` },
|
|
@@ -849,12 +985,57 @@ export class FeishuChannel {
|
|
|
849
985
|
// ── 交互卡片构建工具 ──
|
|
850
986
|
export function buildInteractionCard(interaction) {
|
|
851
987
|
const { kind } = interaction;
|
|
988
|
+
if (kind.kind === 'command-card') {
|
|
989
|
+
return buildCommandCardFeishu(kind, interaction.initiatorId);
|
|
990
|
+
}
|
|
852
991
|
if (kind.kind === 'action') {
|
|
853
|
-
return buildActionCard(interaction.id, kind);
|
|
992
|
+
return buildActionCard(interaction.id, kind, interaction.initiatorId);
|
|
854
993
|
}
|
|
855
994
|
return null;
|
|
856
995
|
}
|
|
857
|
-
|
|
996
|
+
function buildCommandCardFeishu(card, initiatorId) {
|
|
997
|
+
const elements = [];
|
|
998
|
+
if (card.body) {
|
|
999
|
+
elements.push({ tag: 'markdown', content: card.body });
|
|
1000
|
+
}
|
|
1001
|
+
// Build full card body for resolved state: original body + button labels
|
|
1002
|
+
const btnLabels = card.buttons.map(btn => btn.label).join(' · ');
|
|
1003
|
+
const fullCardBody = [card.body, btnLabels].filter(Boolean).join('\n\n');
|
|
1004
|
+
const buttons = card.buttons.map(btn => {
|
|
1005
|
+
const buttonEl = {
|
|
1006
|
+
tag: 'button',
|
|
1007
|
+
text: { tag: 'plain_text', content: btn.label },
|
|
1008
|
+
type: btn.style === 'danger' ? 'danger' : btn.style === 'primary' ? 'primary' : 'default',
|
|
1009
|
+
value: {
|
|
1010
|
+
_command: btn.command,
|
|
1011
|
+
_initiator: initiatorId,
|
|
1012
|
+
_card_title: card.title,
|
|
1013
|
+
_card_body: fullCardBody,
|
|
1014
|
+
_btn_label: btn.label,
|
|
1015
|
+
},
|
|
1016
|
+
};
|
|
1017
|
+
if (btn.disabled) {
|
|
1018
|
+
buttonEl.disabled = true;
|
|
1019
|
+
}
|
|
1020
|
+
if (btn.confirm) {
|
|
1021
|
+
buttonEl.confirm = {
|
|
1022
|
+
title: { tag: 'plain_text', content: btn.confirm.title },
|
|
1023
|
+
text: { tag: 'plain_text', content: btn.confirm.body },
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
return buttonEl;
|
|
1027
|
+
});
|
|
1028
|
+
elements.push({ tag: 'action', actions: buttons });
|
|
1029
|
+
return {
|
|
1030
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
1031
|
+
header: {
|
|
1032
|
+
template: 'blue',
|
|
1033
|
+
title: { tag: 'plain_text', content: card.title },
|
|
1034
|
+
},
|
|
1035
|
+
elements,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
export function buildActionCard(requestId, action, initiatorId) {
|
|
858
1039
|
const elements = [];
|
|
859
1040
|
// Body text
|
|
860
1041
|
if (action.body) {
|
|
@@ -872,6 +1053,7 @@ export function buildActionCard(requestId, action) {
|
|
|
872
1053
|
value: {
|
|
873
1054
|
_request_id: requestId,
|
|
874
1055
|
_action: btn.key,
|
|
1056
|
+
_initiator: initiatorId,
|
|
875
1057
|
_card_title: action.title,
|
|
876
1058
|
_card_body: fullCardBody,
|
|
877
1059
|
_btn_label: btn.label,
|
|
@@ -890,7 +1072,7 @@ export function buildActionCard(requestId, action) {
|
|
|
890
1072
|
actions: buttons,
|
|
891
1073
|
});
|
|
892
1074
|
return {
|
|
893
|
-
config: { wide_screen_mode: true },
|
|
1075
|
+
config: { wide_screen_mode: true, update_multi: true },
|
|
894
1076
|
header: {
|
|
895
1077
|
template: 'blue',
|
|
896
1078
|
title: { tag: 'plain_text', content: action.title },
|
|
@@ -999,7 +1181,8 @@ export function hasMarkdownSyntax(text) {
|
|
|
999
1181
|
];
|
|
1000
1182
|
return markdownPatterns.some(pattern => pattern.test(text));
|
|
1001
1183
|
}
|
|
1002
|
-
import { normalizeChannelInstances, getChannelShowActivities } from '../
|
|
1184
|
+
import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
|
|
1185
|
+
import { resolvePaths } from '../paths.js';
|
|
1003
1186
|
export class FeishuChannelPlugin {
|
|
1004
1187
|
name = 'feishu';
|
|
1005
1188
|
isEnabled(config) {
|
|
@@ -1023,16 +1206,56 @@ export class FeishuChannelPlugin {
|
|
|
1023
1206
|
appId: inst.appId,
|
|
1024
1207
|
appSecret: inst.appSecret,
|
|
1025
1208
|
enableRichContent: config.enableRichContent,
|
|
1209
|
+
seenMsgFile: path.join(resolvePaths().dataDir, `feishu-seen-${inst.name}.jsonl`),
|
|
1026
1210
|
});
|
|
1027
1211
|
const adapter = {
|
|
1028
1212
|
channelName: inst.name,
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1213
|
+
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true },
|
|
1214
|
+
send: async (envelope, payload) => {
|
|
1215
|
+
const ctx = envelope.replyContext;
|
|
1216
|
+
const channelId = envelope.channelId;
|
|
1217
|
+
switch (payload.kind) {
|
|
1218
|
+
case 'result.text':
|
|
1219
|
+
case 'command.result':
|
|
1220
|
+
case 'command.error':
|
|
1221
|
+
case 'system.notice':
|
|
1222
|
+
case 'system.error':
|
|
1223
|
+
case 'result.error': {
|
|
1224
|
+
const sendCtx = { ...(ctx ?? {}) };
|
|
1225
|
+
if (payload.kind === 'result.text' && payload.isFinal)
|
|
1226
|
+
sendCtx.title = '✓ 最终回复:';
|
|
1227
|
+
await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
case 'result.file':
|
|
1231
|
+
await channel.sendFile(channelId, payload.filePath, ctx);
|
|
1232
|
+
return;
|
|
1233
|
+
case 'result.image':
|
|
1234
|
+
await channel.sendImage(channelId, payload.data, ctx);
|
|
1235
|
+
return;
|
|
1236
|
+
case 'activity.batch': {
|
|
1237
|
+
const text = formatItemsAsText(payload.items);
|
|
1238
|
+
if (text)
|
|
1239
|
+
await channel.sendMessage(channelId, text, ctx);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
case 'status.started':
|
|
1243
|
+
case 'status.completed':
|
|
1244
|
+
case 'status.interrupted':
|
|
1245
|
+
case 'status.error':
|
|
1246
|
+
case 'status.timeout':
|
|
1247
|
+
// Feishu 通过 acknowledge (✓ 表情) 表达状态,由 channel 自行处理
|
|
1248
|
+
return;
|
|
1249
|
+
case 'interaction':
|
|
1250
|
+
await channel.sendInteraction(channelId, payload.interaction, ctx);
|
|
1251
|
+
return;
|
|
1252
|
+
case 'custom':
|
|
1253
|
+
// Feishu 不支持自定义 payload
|
|
1254
|
+
return;
|
|
1255
|
+
default:
|
|
1256
|
+
logger.warn(`[Feishu] Unhandled payload kind: ${payload.kind}`);
|
|
1257
|
+
}
|
|
1258
|
+
}, acknowledge: (messageId) => { channel.addAckReaction(messageId); return Promise.resolve(); }, onInteraction: (callback) => channel.onInteraction(callback),
|
|
1036
1259
|
};
|
|
1037
1260
|
const policy = {
|
|
1038
1261
|
canSwitchProject: (chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
@@ -1077,6 +1300,19 @@ export class FeishuChannelPlugin {
|
|
|
1077
1300
|
connect: () => channel.connect(),
|
|
1078
1301
|
disconnect: () => channel.disconnect(),
|
|
1079
1302
|
onProjectPathRequest: (channelId) => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
1303
|
+
registerBridge(bridge, channelType) {
|
|
1304
|
+
bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
|
|
1305
|
+
await handler({
|
|
1306
|
+
channel: adapter.channelName, channelType, channelId: chatId, content, images, chatType,
|
|
1307
|
+
peerId: peerId || '', peerName, messageId, mentions, threadId,
|
|
1308
|
+
replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
|
|
1309
|
+
source,
|
|
1310
|
+
});
|
|
1311
|
+
}), (channelId, text, replyContext) => channel.sendMessage(channelId, text, {
|
|
1312
|
+
replyToMessageId: replyContext?.replyToMessageId,
|
|
1313
|
+
replyInThread: replyContext?.replyInThread,
|
|
1314
|
+
}), adapter, channelType);
|
|
1315
|
+
},
|
|
1080
1316
|
});
|
|
1081
1317
|
}
|
|
1082
1318
|
return result;
|
package/dist/channels/qqbot.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { logger } from '../utils/logger.js';
|
|
2
2
|
import { markdownToPlainText } from '../utils/rich-content-renderer.js';
|
|
3
|
-
import { requireOptional } from '../utils/
|
|
4
|
-
import { normalizeChannelInstances, getChannelShowActivities } from '../
|
|
3
|
+
import { requireOptional } from '../utils/npm-ops.js';
|
|
4
|
+
import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
|
|
5
|
+
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
5
6
|
// ── QQBotChannel ────────────────────────────────────────────────────────────
|
|
6
7
|
export class QQBotChannel {
|
|
7
8
|
config;
|
|
@@ -245,11 +246,11 @@ export class QQBotChannel {
|
|
|
245
246
|
async sendImage(chatId, png) {
|
|
246
247
|
if (!this.client)
|
|
247
248
|
return;
|
|
249
|
+
const fs = await import('fs');
|
|
250
|
+
const path = await import('path');
|
|
251
|
+
const os = await import('os');
|
|
252
|
+
const tmpPath = path.join(os.tmpdir(), `evolclaw-qqbot-${Date.now()}.png`);
|
|
248
253
|
try {
|
|
249
|
-
const fs = await import('fs');
|
|
250
|
-
const path = await import('path');
|
|
251
|
-
const os = await import('os');
|
|
252
|
-
const tmpPath = path.join(os.tmpdir(), `evolclaw-qqbot-${Date.now()}.png`);
|
|
253
254
|
fs.writeFileSync(tmpPath, png);
|
|
254
255
|
const chatType = this.chatTypeCache.get(chatId);
|
|
255
256
|
const msgId = this.msgIdCache.get(chatId);
|
|
@@ -260,14 +261,16 @@ export class QQBotChannel {
|
|
|
260
261
|
else {
|
|
261
262
|
await this.client.sendPrivateImage(chatId, `file://${tmpPath}`, msgId);
|
|
262
263
|
}
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
logger.error(`[QQBot] sendImage failed for ${chatId}:`, error?.message || error);
|
|
267
|
+
}
|
|
268
|
+
finally {
|
|
263
269
|
try {
|
|
264
270
|
fs.unlinkSync(tmpPath);
|
|
265
271
|
}
|
|
266
272
|
catch { /* ignore */ }
|
|
267
273
|
}
|
|
268
|
-
catch (error) {
|
|
269
|
-
logger.error(`[QQBot] sendImage failed for ${chatId}:`, error?.message || error);
|
|
270
|
-
}
|
|
271
274
|
}
|
|
272
275
|
// ── Outbound: file ─────────────────────────────────────────────────────────
|
|
273
276
|
async sendFile(chatId, filePath) {
|
|
@@ -331,9 +334,46 @@ export class QQBotChannelPlugin {
|
|
|
331
334
|
});
|
|
332
335
|
const adapter = {
|
|
333
336
|
channelName: inst.name,
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
+
capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
|
|
338
|
+
send: async (envelope, payload) => {
|
|
339
|
+
const ctx = envelope.replyContext;
|
|
340
|
+
const channelId = envelope.channelId;
|
|
341
|
+
switch (payload.kind) {
|
|
342
|
+
case 'result.text':
|
|
343
|
+
case 'command.result':
|
|
344
|
+
case 'command.error':
|
|
345
|
+
case 'system.notice':
|
|
346
|
+
case 'system.error':
|
|
347
|
+
case 'result.error':
|
|
348
|
+
await channel.sendMessage(channelId, payload.text);
|
|
349
|
+
return;
|
|
350
|
+
case 'result.file':
|
|
351
|
+
await channel.sendFile(channelId, payload.filePath);
|
|
352
|
+
return;
|
|
353
|
+
case 'result.image':
|
|
354
|
+
await channel.sendImage(channelId, payload.data);
|
|
355
|
+
return;
|
|
356
|
+
case 'activity.batch': {
|
|
357
|
+
const text = formatItemsAsText(payload.items);
|
|
358
|
+
if (text)
|
|
359
|
+
await channel.sendMessage(channelId, text);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
case 'interaction':
|
|
363
|
+
if (payload.fallbackText)
|
|
364
|
+
await channel.sendMessage(channelId, payload.fallbackText);
|
|
365
|
+
return;
|
|
366
|
+
case 'status.started':
|
|
367
|
+
case 'status.completed':
|
|
368
|
+
case 'status.interrupted':
|
|
369
|
+
case 'status.error':
|
|
370
|
+
case 'status.timeout':
|
|
371
|
+
case 'custom':
|
|
372
|
+
return;
|
|
373
|
+
default:
|
|
374
|
+
logger.warn(`[QQBot] Unhandled payload kind: ${payload.kind}`);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
337
377
|
};
|
|
338
378
|
const policy = {
|
|
339
379
|
canSwitchProject: (_chatType, identity) => identity === 'owner' || identity === 'admin',
|
|
@@ -378,6 +418,21 @@ export class QQBotChannelPlugin {
|
|
|
378
418
|
connect: () => channel.connect(),
|
|
379
419
|
disconnect: () => channel.disconnect(),
|
|
380
420
|
onProjectPathRequest: () => Promise.resolve(config.projects?.defaultPath || process.cwd()),
|
|
421
|
+
registerBridge(bridge, channelType) {
|
|
422
|
+
bridge.register(adapter.channelName, (handler) => channel.onMessage(async (event) => {
|
|
423
|
+
handler({
|
|
424
|
+
channel: adapter.channelName,
|
|
425
|
+
channelType,
|
|
426
|
+
channelId: event.channelId,
|
|
427
|
+
content: event.content,
|
|
428
|
+
images: event.images,
|
|
429
|
+
chatType: event.chatType || 'private',
|
|
430
|
+
peerId: event.peerId || '',
|
|
431
|
+
peerName: event.peerName,
|
|
432
|
+
messageId: event.messageId,
|
|
433
|
+
});
|
|
434
|
+
}), (channelId, text) => channel.sendMessage(channelId, text), adapter, channelType);
|
|
435
|
+
},
|
|
381
436
|
});
|
|
382
437
|
}
|
|
383
438
|
return result;
|