evolcore 0.0.2 → 0.0.3
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/CHANGELOG.md +44 -793
- package/dist/agents/claude-runner.js +197 -17
- package/dist/agents/codex-runner.js +46 -3
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +21 -4
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/dingtalk.js +979 -149
- package/dist/channels/feishu.js +130 -54
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +755 -163
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +88 -33
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +2 -2
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +92 -97
- package/dist/cli/init.js +63 -26
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +55 -15
- package/dist/cli/version.js +6 -1
- package/dist/config/builtin-role-templates.js +22 -10
- package/dist/config/builtin-roles.js +7 -1
- package/dist/config/config-manager.js +221 -17
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +156 -303
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/peer-role-resolver.js +133 -54
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config-store.js +0 -2
- package/dist/core/auth/authorization-audit.js +10 -1
- package/dist/core/auth/operation-authorizer.js +2 -0
- package/dist/core/auth/operation-catalog.js +56 -0
- package/dist/core/command/command-handler.js +105 -10
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +114 -16
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-handler.js +28 -18
- package/dist/core/daemon-file-cache.js +12 -6
- package/dist/core/event-catalog.js +70 -0
- package/dist/core/evolagent-registry.js +0 -1
- package/dist/core/evolagent.js +20 -9
- package/dist/core/message/im-renderer.js +2 -0
- package/dist/core/message/message-bridge.js +79 -8
- package/dist/core/message/message-queue.js +10 -0
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/response-engine.js +269 -25
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/tool-policy.js +47 -15
- package/dist/core/protected-paths.js +2 -0
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +61 -5
- package/dist/index.js +62 -6
- package/dist/ipc.js +34 -5
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +77 -2
- package/dist/trigger/patch.js +8 -1
- package/dist/trigger/scheduler.js +3 -1
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/contact.md +57 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group.md +12 -3
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +9 -3
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +17 -2
- package/kits/eck_manifest.json +12 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/commands.md +3 -1
- package/package.json +4 -4
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
package/dist/channels/feishu.js
CHANGED
|
@@ -6,6 +6,7 @@ import { logger } from '../utils/logger.js';
|
|
|
6
6
|
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
7
7
|
import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
|
|
8
8
|
import { chatDirPath, decodeDirSegment } from '../core/session/session-fs-store.js';
|
|
9
|
+
import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
|
|
9
10
|
export class FeishuChannel {
|
|
10
11
|
config;
|
|
11
12
|
agentAid;
|
|
@@ -125,7 +126,7 @@ export class FeishuChannel {
|
|
|
125
126
|
try {
|
|
126
127
|
// 首次交互欢迎消息(使用共享帮助函数)
|
|
127
128
|
if (peerId) {
|
|
128
|
-
await sendWelcomeIfNeeded(this.welcomeManager, peerId, msg.chat_id, (id, text) => this.sendMessage(id, text), 'Feishu');
|
|
129
|
+
await sendWelcomeIfNeeded(this.welcomeManager, peerId, msg.chat_id, async (id, text) => { await this.sendMessage(id, text); }, 'Feishu');
|
|
129
130
|
}
|
|
130
131
|
// 提取话题信息
|
|
131
132
|
const threadId = msg.thread_id || undefined;
|
|
@@ -471,11 +472,11 @@ export class FeishuChannel {
|
|
|
471
472
|
}
|
|
472
473
|
async sendMessage(chatId, content, options) {
|
|
473
474
|
if (!this.client)
|
|
474
|
-
|
|
475
|
+
throw new Error('Feishu client is not connected');
|
|
475
476
|
content = exposeLocalMarkdownLinkTargetsForFeishu(content);
|
|
476
477
|
if (!content || content.trim() === '') {
|
|
477
478
|
logger.warn('[Feishu] Attempted to send empty message, skipping');
|
|
478
|
-
return;
|
|
479
|
+
return [];
|
|
479
480
|
}
|
|
480
481
|
// 飞书消息内容限制约 30KB(text)/ 150KB(post),安全阈值 28000 字符
|
|
481
482
|
// 超长消息自动拆分,按段落边界分割
|
|
@@ -483,12 +484,13 @@ export class FeishuChannel {
|
|
|
483
484
|
if (content.length > MAX_CONTENT_LENGTH) {
|
|
484
485
|
logger.info(`[Feishu] Message too long (${content.length} chars), splitting into parts`);
|
|
485
486
|
const parts = splitLongMessage(content, MAX_CONTENT_LENGTH);
|
|
487
|
+
const messageIds = [];
|
|
486
488
|
for (let i = 0; i < parts.length; i++) {
|
|
487
489
|
// 首条消息保留 reply 选项,后续消息不再 reply
|
|
488
490
|
const partOptions = i === 0 ? options : { ...options, replyToMessageId: undefined };
|
|
489
|
-
await this.sendMessage(chatId, parts[i], partOptions);
|
|
491
|
+
messageIds.push(...await this.sendMessage(chatId, parts[i], partOptions));
|
|
490
492
|
}
|
|
491
|
-
return;
|
|
493
|
+
return messageIds;
|
|
492
494
|
}
|
|
493
495
|
logger.debug(`[Feishu] sendMessage called, chatId: ${chatId}, content length: ${content.length}`);
|
|
494
496
|
try {
|
|
@@ -511,6 +513,7 @@ export class FeishuChannel {
|
|
|
511
513
|
else {
|
|
512
514
|
msgContent = JSON.stringify({ text: content });
|
|
513
515
|
}
|
|
516
|
+
let messageId;
|
|
514
517
|
if (options?.replyToMessageId) {
|
|
515
518
|
const replyData = { msg_type: msgType, content: msgContent };
|
|
516
519
|
if (options.replyInThread) {
|
|
@@ -520,6 +523,7 @@ export class FeishuChannel {
|
|
|
520
523
|
path: { message_id: options.replyToMessageId },
|
|
521
524
|
data: replyData
|
|
522
525
|
});
|
|
526
|
+
messageId = replyRes?.data?.message_id;
|
|
523
527
|
if (options.replyInThread && options.onThreadCreated) {
|
|
524
528
|
const newThreadId = replyRes?.data?.thread_id;
|
|
525
529
|
if (newThreadId)
|
|
@@ -527,12 +531,16 @@ export class FeishuChannel {
|
|
|
527
531
|
}
|
|
528
532
|
}
|
|
529
533
|
else {
|
|
530
|
-
await this.client.im.message.create({
|
|
534
|
+
const createRes = await this.client.im.message.create({
|
|
531
535
|
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
532
536
|
data: { receive_id: chatId, msg_type: msgType, content: msgContent }
|
|
533
537
|
});
|
|
538
|
+
messageId = createRes?.data?.message_id;
|
|
534
539
|
}
|
|
540
|
+
if (!messageId)
|
|
541
|
+
throw new Error('Feishu send succeeded without message_id');
|
|
535
542
|
logger.debug(`[Feishu] Sent message as ${useMarkdown ? 'post (Markdown)' : 'text'}`);
|
|
543
|
+
return [messageId];
|
|
536
544
|
}
|
|
537
545
|
catch (error) {
|
|
538
546
|
// 230011: 消息已被撤回,降级为普通消息重试
|
|
@@ -560,7 +568,7 @@ export class FeishuChannel {
|
|
|
560
568
|
}
|
|
561
569
|
async sendFile(chatId, filePath, options) {
|
|
562
570
|
if (!this.client)
|
|
563
|
-
|
|
571
|
+
throw new Error('Feishu client is not connected');
|
|
564
572
|
try {
|
|
565
573
|
// 检测是否为图片,是则走 sendImage(内联预览)而非文件卡片
|
|
566
574
|
// 读取足够字节供 file-type 解析(ZIP-based 格式如 PPTX 需要更多字节)
|
|
@@ -585,24 +593,25 @@ export class FeishuChannel {
|
|
|
585
593
|
}
|
|
586
594
|
});
|
|
587
595
|
if (!uploadResponse || !uploadResponse.file_key) {
|
|
588
|
-
|
|
589
|
-
return;
|
|
596
|
+
throw new Error('Feishu file upload failed: no file_key returned');
|
|
590
597
|
}
|
|
591
598
|
const fileKey = uploadResponse.file_key;
|
|
592
599
|
const msgContent = JSON.stringify({ file_key: fileKey });
|
|
593
600
|
logger.info('[Feishu] File uploaded, file_key:', fileKey);
|
|
601
|
+
let messageId;
|
|
594
602
|
if (options?.replyToMessageId) {
|
|
595
603
|
const replyData = { msg_type: 'file', content: msgContent };
|
|
596
604
|
if (options.replyInThread) {
|
|
597
605
|
replyData.reply_in_thread = true;
|
|
598
606
|
}
|
|
599
|
-
await this.client.im.message.reply({
|
|
607
|
+
const replyRes = await this.client.im.message.reply({
|
|
600
608
|
path: { message_id: options.replyToMessageId },
|
|
601
609
|
data: replyData
|
|
602
610
|
});
|
|
611
|
+
messageId = replyRes?.data?.message_id;
|
|
603
612
|
}
|
|
604
613
|
else {
|
|
605
|
-
await this.client.im.message.create({
|
|
614
|
+
const createRes = await this.client.im.message.create({
|
|
606
615
|
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
607
616
|
data: {
|
|
608
617
|
receive_id: chatId,
|
|
@@ -610,8 +619,12 @@ export class FeishuChannel {
|
|
|
610
619
|
content: msgContent
|
|
611
620
|
}
|
|
612
621
|
});
|
|
622
|
+
messageId = createRes?.data?.message_id;
|
|
613
623
|
}
|
|
624
|
+
if (!messageId)
|
|
625
|
+
throw new Error('Feishu file send succeeded without message_id');
|
|
614
626
|
logger.info('[Feishu] File message sent successfully');
|
|
627
|
+
return [messageId];
|
|
615
628
|
}
|
|
616
629
|
catch (error) {
|
|
617
630
|
// 230011/99992354: reply target invalid, retry without reply
|
|
@@ -626,7 +639,7 @@ export class FeishuChannel {
|
|
|
626
639
|
}
|
|
627
640
|
async sendImage(chatId, png, options) {
|
|
628
641
|
if (!this.client)
|
|
629
|
-
|
|
642
|
+
throw new Error('Feishu client is not connected');
|
|
630
643
|
try {
|
|
631
644
|
const uploadResponse = await this.client.im.image.create({
|
|
632
645
|
data: {
|
|
@@ -636,27 +649,32 @@ export class FeishuChannel {
|
|
|
636
649
|
});
|
|
637
650
|
const imageKey = uploadResponse?.image_key;
|
|
638
651
|
if (!imageKey) {
|
|
639
|
-
|
|
640
|
-
return;
|
|
652
|
+
throw new Error('Feishu image upload failed: no image_key returned');
|
|
641
653
|
}
|
|
642
654
|
logger.debug('[Feishu] Image uploaded, image_key:', imageKey);
|
|
643
655
|
const msgContent = JSON.stringify({ image_key: imageKey });
|
|
656
|
+
let messageId;
|
|
644
657
|
if (options?.replyToMessageId) {
|
|
645
658
|
const replyData = { msg_type: 'image', content: msgContent };
|
|
646
659
|
if (options.replyInThread)
|
|
647
660
|
replyData.reply_in_thread = true;
|
|
648
|
-
await this.client.im.message.reply({
|
|
661
|
+
const replyRes = await this.client.im.message.reply({
|
|
649
662
|
path: { message_id: options.replyToMessageId },
|
|
650
663
|
data: replyData
|
|
651
664
|
});
|
|
665
|
+
messageId = replyRes?.data?.message_id;
|
|
652
666
|
}
|
|
653
667
|
else {
|
|
654
|
-
await this.client.im.message.create({
|
|
668
|
+
const createRes = await this.client.im.message.create({
|
|
655
669
|
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
656
670
|
data: { receive_id: chatId, msg_type: 'image', content: msgContent }
|
|
657
671
|
});
|
|
672
|
+
messageId = createRes?.data?.message_id;
|
|
658
673
|
}
|
|
674
|
+
if (!messageId)
|
|
675
|
+
throw new Error('Feishu image send succeeded without message_id');
|
|
659
676
|
logger.debug('[Feishu] Image message sent successfully');
|
|
677
|
+
return [messageId];
|
|
660
678
|
}
|
|
661
679
|
catch (error) {
|
|
662
680
|
// 99992354: reply target invalid — image cannot easily retry, just log
|
|
@@ -1273,6 +1291,10 @@ export class FeishuChannel {
|
|
|
1273
1291
|
}
|
|
1274
1292
|
// messageId → Pin reaction create promise(等待 reaction_id 落地,供 promoteAck 删除)
|
|
1275
1293
|
pinReactions = new Map();
|
|
1294
|
+
// messageId → StatusInFlight reaction create promise(终态时替换)
|
|
1295
|
+
inFlightReactions = new Map();
|
|
1296
|
+
// taskId → 触发该任务的全部原始入站 messageId(批量合并时可有多条)
|
|
1297
|
+
taskAckMessages = new Map();
|
|
1276
1298
|
/** 收到消息时添加 Pin 表情(表示"已收到,排队中") */
|
|
1277
1299
|
addPinReaction(messageId) {
|
|
1278
1300
|
if (!this.client)
|
|
@@ -1284,22 +1306,67 @@ export class FeishuChannel {
|
|
|
1284
1306
|
.catch(() => undefined);
|
|
1285
1307
|
this.pinReactions.set(messageId, p);
|
|
1286
1308
|
}
|
|
1287
|
-
/** Runner 开始执行时:添加
|
|
1288
|
-
async promoteAckReaction(messageId) {
|
|
1309
|
+
/** Runner 开始执行时:添加 StatusInFlight,然后移除 Pin(视觉无闪烁) */
|
|
1310
|
+
async promoteAckReaction(messageId, taskId, sourceMessageIds) {
|
|
1311
|
+
const messageIds = [...new Set([...(sourceMessageIds ?? []), messageId].filter(Boolean))];
|
|
1312
|
+
if (taskId)
|
|
1313
|
+
this.taskAckMessages.set(taskId, messageIds);
|
|
1289
1314
|
if (!this.client)
|
|
1290
1315
|
return;
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
this.
|
|
1301
|
-
|
|
1302
|
-
|
|
1316
|
+
const promotions = messageIds.map(id => {
|
|
1317
|
+
// 先加 StatusInFlight,再删 Pin——用户看到的是 Pin→Pin+InFlight→InFlight。
|
|
1318
|
+
const inFlightPending = this.client.im.messageReaction.create({
|
|
1319
|
+
path: { message_id: id },
|
|
1320
|
+
data: { reaction_type: { emoji_type: 'StatusInFlight' } },
|
|
1321
|
+
}).then((res) => res?.data?.reaction_id)
|
|
1322
|
+
.catch(() => undefined);
|
|
1323
|
+
this.inFlightReactions.set(id, inFlightPending);
|
|
1324
|
+
const pending = this.pinReactions.get(id);
|
|
1325
|
+
this.pinReactions.delete(id);
|
|
1326
|
+
return { id, pending };
|
|
1327
|
+
});
|
|
1328
|
+
// Every in-flight reaction is registered before this first await, so a
|
|
1329
|
+
// fast terminal event cannot race ahead and strand a merged-batch status.
|
|
1330
|
+
for (const { id, pending } of promotions) {
|
|
1331
|
+
const reactionId = await pending;
|
|
1332
|
+
if (reactionId) {
|
|
1333
|
+
await this.client.im.messageReaction.delete({
|
|
1334
|
+
path: { message_id: id, reaction_id: reactionId },
|
|
1335
|
+
}).catch(() => { });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
/** 任务终态:用 CheckMark/CrossMark 替换 StatusInFlight。 */
|
|
1340
|
+
async finalizeAckReaction(taskId, terminalEmoji) {
|
|
1341
|
+
const messageIds = this.taskAckMessages.get(taskId);
|
|
1342
|
+
if (!messageIds)
|
|
1343
|
+
return;
|
|
1344
|
+
if (!this.client) {
|
|
1345
|
+
this.taskAckMessages.delete(taskId);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
this.taskAckMessages.delete(taskId);
|
|
1349
|
+
for (const messageId of messageIds) {
|
|
1350
|
+
const inFlightPending = this.inFlightReactions.get(messageId);
|
|
1351
|
+
// 先加终态表情,再删处理中表情,避免视觉空窗。
|
|
1352
|
+
const addTerminal = () => this.client.im.messageReaction.create({
|
|
1353
|
+
path: { message_id: messageId },
|
|
1354
|
+
data: { reaction_type: { emoji_type: terminalEmoji } },
|
|
1355
|
+
}).then((res) => !!res?.data?.reaction_id).catch(() => false);
|
|
1356
|
+
let terminalAdded = await addTerminal();
|
|
1357
|
+
if (!terminalAdded)
|
|
1358
|
+
terminalAdded = await addTerminal();
|
|
1359
|
+
if (!terminalAdded) {
|
|
1360
|
+
logger.warn(`[Feishu] Failed to add terminal reaction for ${messageId}; preserving processing reaction`);
|
|
1361
|
+
continue;
|
|
1362
|
+
}
|
|
1363
|
+
this.inFlightReactions.delete(messageId);
|
|
1364
|
+
const inFlightReactionId = await inFlightPending;
|
|
1365
|
+
if (inFlightReactionId) {
|
|
1366
|
+
await this.client.im.messageReaction.delete({
|
|
1367
|
+
path: { message_id: messageId, reaction_id: inFlightReactionId },
|
|
1368
|
+
}).catch(() => { });
|
|
1369
|
+
}
|
|
1303
1370
|
}
|
|
1304
1371
|
}
|
|
1305
1372
|
}
|
|
@@ -1707,46 +1774,53 @@ export class FeishuChannelPlugin {
|
|
|
1707
1774
|
sendCtx.title = '✅ 最终回复:';
|
|
1708
1775
|
if (replyCtx?.metadata?.onThreadCreated)
|
|
1709
1776
|
sendCtx.onThreadCreated = replyCtx.metadata.onThreadCreated;
|
|
1710
|
-
await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
1711
|
-
return
|
|
1777
|
+
const ids = await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
1778
|
+
return ids.length > 0
|
|
1779
|
+
? sentReceipt(envelope, ids, replyCtx?.threadId)
|
|
1780
|
+
: suppressedReceipt(envelope, 'empty_message');
|
|
1712
1781
|
}
|
|
1713
|
-
case 'result.file':
|
|
1714
|
-
|
|
1715
|
-
return;
|
|
1716
|
-
case 'result.image':
|
|
1717
|
-
await channel.sendImage(channelId, payload.data, replyCtx);
|
|
1718
|
-
return;
|
|
1782
|
+
case 'result.file': return sentReceipt(envelope, await channel.sendFile(channelId, payload.filePath, replyCtx), replyCtx?.threadId);
|
|
1783
|
+
case 'result.image': return sentReceipt(envelope, await channel.sendImage(channelId, payload.data, replyCtx), replyCtx?.threadId);
|
|
1719
1784
|
case 'activity.batch': {
|
|
1720
1785
|
const filtered = payload.items.filter((i) => !(i.kind === 'tool_result' && i.ok));
|
|
1721
1786
|
const text = formatItemsAsText(filtered);
|
|
1722
1787
|
if (text)
|
|
1723
|
-
await channel.sendMessage(channelId, text, replyCtx);
|
|
1724
|
-
return;
|
|
1788
|
+
return sentReceipt(envelope, await channel.sendMessage(channelId, text, replyCtx), replyCtx?.threadId);
|
|
1789
|
+
return suppressedReceipt(envelope, 'empty_activity');
|
|
1725
1790
|
}
|
|
1726
1791
|
case 'status.started':
|
|
1792
|
+
case 'status.queued':
|
|
1793
|
+
case 'status.progress':
|
|
1794
|
+
return suppressedReceipt(envelope, 'reaction_lifecycle');
|
|
1727
1795
|
case 'status.completed':
|
|
1796
|
+
await channel.finalizeAckReaction(envelope.taskId, 'CheckMark');
|
|
1797
|
+
return suppressedReceipt(envelope, 'reaction_lifecycle');
|
|
1728
1798
|
case 'status.interrupted':
|
|
1729
1799
|
case 'status.timeout':
|
|
1730
|
-
|
|
1800
|
+
await channel.finalizeAckReaction(envelope.taskId, 'CrossMark');
|
|
1801
|
+
return suppressedReceipt(envelope, 'reaction_lifecycle');
|
|
1731
1802
|
case 'status.requires_action':
|
|
1732
|
-
await channel.sendMessage(channelId, '等待 owner 审批', replyCtx);
|
|
1733
|
-
return;
|
|
1803
|
+
return sentReceipt(envelope, await channel.sendMessage(channelId, '等待 owner 审批', replyCtx), replyCtx?.threadId);
|
|
1734
1804
|
case 'status.error':
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1805
|
+
await channel.finalizeAckReaction(envelope.taskId, 'CrossMark');
|
|
1806
|
+
if (payload.metadata?.message) {
|
|
1807
|
+
return sentReceipt(envelope, await channel.sendMessage(channelId, payload.metadata.message, replyCtx), replyCtx?.threadId);
|
|
1808
|
+
}
|
|
1809
|
+
return suppressedReceipt(envelope, 'reaction_lifecycle');
|
|
1738
1810
|
case 'interaction': {
|
|
1739
1811
|
const sent = await channel.sendInteraction(channelId, payload.interaction, replyCtx);
|
|
1740
1812
|
if (!sent)
|
|
1741
1813
|
throw new Error('sendInteraction returned false');
|
|
1742
|
-
return;
|
|
1814
|
+
return sentReceipt(envelope, [sent], replyCtx?.threadId);
|
|
1743
1815
|
}
|
|
1744
|
-
case 'custom': return;
|
|
1745
|
-
default:
|
|
1816
|
+
case 'custom': return suppressedReceipt(envelope, 'unsupported_custom_payload');
|
|
1817
|
+
default:
|
|
1818
|
+
logger.warn(`[Feishu] Unhandled payload kind: ${payload.kind}`);
|
|
1819
|
+
return suppressedReceipt(envelope, 'unhandled_payload');
|
|
1746
1820
|
}
|
|
1747
1821
|
},
|
|
1748
1822
|
acknowledge: (messageId) => { channel.addPinReaction(messageId); return Promise.resolve(); },
|
|
1749
|
-
promoteAck: (messageId) => channel.promoteAckReaction(messageId),
|
|
1823
|
+
promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds)),
|
|
1750
1824
|
onInteraction: (callback) => channel.onInteraction(callback),
|
|
1751
1825
|
onInteractionInvalidated: (callback) => channel.onInteractionInvalidated(callback),
|
|
1752
1826
|
invalidateInteraction: (interactionId, reason) => channel.invalidateInteraction(interactionId, reason),
|
|
@@ -1779,10 +1853,12 @@ export class FeishuChannelPlugin {
|
|
|
1779
1853
|
replyContext: threadId ? { threadId, replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
|
|
1780
1854
|
source,
|
|
1781
1855
|
});
|
|
1782
|
-
}), (channelId, text, replyContext) =>
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1856
|
+
}), async (channelId, text, replyContext) => {
|
|
1857
|
+
await channel.sendMessage(channelId, text, {
|
|
1858
|
+
replyToMessageId: replyContext?.replyToMessageId,
|
|
1859
|
+
replyInThread: replyContext?.replyInThread,
|
|
1860
|
+
});
|
|
1861
|
+
}, adapter, channelType);
|
|
1786
1862
|
},
|
|
1787
1863
|
};
|
|
1788
1864
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
function truncate(value, max) {
|
|
2
|
+
if (!value)
|
|
3
|
+
return undefined;
|
|
4
|
+
const chars = [...value.trim()];
|
|
5
|
+
return chars.length <= max ? value.trim() : `${chars.slice(0, Math.max(1, max - 1)).join('')}...`;
|
|
6
|
+
}
|
|
7
|
+
function buttonStyle(style) {
|
|
8
|
+
if (style === 'primary')
|
|
9
|
+
return 2;
|
|
10
|
+
if (style === 'danger')
|
|
11
|
+
return 3;
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
export function buildWecomInteractionCard(interaction, taskId) {
|
|
15
|
+
const bodyParts = [];
|
|
16
|
+
if (interaction.kind.body)
|
|
17
|
+
bodyParts.push(interaction.kind.body);
|
|
18
|
+
if (interaction.kind.kind === 'action' && interaction.kind.checkers?.length) {
|
|
19
|
+
bodyParts.push(interaction.kind.checkers.map((item, index) => `${index + 1}. ${item.label}`).join('\n'));
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
card_type: 'button_interaction',
|
|
23
|
+
task_id: taskId,
|
|
24
|
+
main_title: {
|
|
25
|
+
title: truncate(interaction.kind.title, 26),
|
|
26
|
+
desc: truncate(bodyParts.join('\n\n'), 120),
|
|
27
|
+
},
|
|
28
|
+
button_list: interaction.kind.buttons
|
|
29
|
+
.map((button, index) => ({ button, index }))
|
|
30
|
+
.filter(({ button }) => !('disabled' in button) || !button.disabled)
|
|
31
|
+
.slice(0, 6)
|
|
32
|
+
.map(({ button, index }) => ({
|
|
33
|
+
text: truncate(button.label, 10) || `选项 ${index + 1}`,
|
|
34
|
+
style: buttonStyle(button.style),
|
|
35
|
+
key: interaction.kind.kind === 'command-card' ? `cmd_${index}` : `act_${index}`,
|
|
36
|
+
})),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function parseWecomCardResponse(record, event, operatorId) {
|
|
40
|
+
const eventKey = String(event.event_key ?? '').trim();
|
|
41
|
+
const match = /^(cmd|act)_(\d+)$/.exec(eventKey);
|
|
42
|
+
if (!match)
|
|
43
|
+
return null;
|
|
44
|
+
const index = Number(match[2]);
|
|
45
|
+
if (record.interaction.kind.kind === 'command-card') {
|
|
46
|
+
const button = record.interaction.kind.buttons[index];
|
|
47
|
+
if (!button || button.disabled)
|
|
48
|
+
return null;
|
|
49
|
+
return { command: button.command };
|
|
50
|
+
}
|
|
51
|
+
const button = record.interaction.kind.buttons[index];
|
|
52
|
+
if (!button)
|
|
53
|
+
return null;
|
|
54
|
+
const values = {};
|
|
55
|
+
const selectedItems = event.selected_items?.selected_item;
|
|
56
|
+
if (Array.isArray(selectedItems)) {
|
|
57
|
+
for (const selected of selectedItems) {
|
|
58
|
+
const questionKey = String(selected?.question_key ?? '').trim();
|
|
59
|
+
const optionIds = selected?.option_ids?.option_id;
|
|
60
|
+
if (questionKey && Array.isArray(optionIds))
|
|
61
|
+
values[questionKey] = optionIds.filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
response: {
|
|
66
|
+
type: 'interaction.response',
|
|
67
|
+
id: record.interaction.id,
|
|
68
|
+
action: button.key,
|
|
69
|
+
values: Object.keys(values).length > 0 ? values : undefined,
|
|
70
|
+
operatorId,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export function buildWecomTerminalCard(record, status, detail) {
|
|
75
|
+
const statusText = status === 'resolved'
|
|
76
|
+
? `已处理${detail ? `:${detail}` : ''}`
|
|
77
|
+
: status === 'rejected'
|
|
78
|
+
? '仅卡片发起者可操作'
|
|
79
|
+
: status === 'unavailable'
|
|
80
|
+
? '处理服务暂不可用,请重新发起'
|
|
81
|
+
: `卡片已失效${detail ? `:${detail}` : ''}`;
|
|
82
|
+
return {
|
|
83
|
+
card_type: 'text_notice',
|
|
84
|
+
task_id: record.taskId,
|
|
85
|
+
main_title: {
|
|
86
|
+
title: truncate(record.interaction.kind.title, 26),
|
|
87
|
+
desc: truncate(statusText, 120),
|
|
88
|
+
},
|
|
89
|
+
sub_title_text: truncate(record.interaction.kind.body, 120),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function buildWecomUnknownCard(taskId) {
|
|
93
|
+
return {
|
|
94
|
+
card_type: 'text_notice',
|
|
95
|
+
task_id: taskId,
|
|
96
|
+
main_title: {
|
|
97
|
+
title: '卡片已失效',
|
|
98
|
+
desc: '本地状态不存在,请重新发起操作',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
const WECOM_QR_GENERATE_URL = 'https://work.weixin.qq.com/ai/qc/generate';
|
|
3
|
+
const WECOM_QR_QUERY_URL = 'https://work.weixin.qq.com/ai/qc/query_result';
|
|
4
|
+
const WECOM_QR_PAGE_URL = 'https://work.weixin.qq.com/ai/qc/gen';
|
|
5
|
+
function platformCode() {
|
|
6
|
+
if (os.platform() === 'darwin')
|
|
7
|
+
return 1;
|
|
8
|
+
if (os.platform() === 'win32')
|
|
9
|
+
return 2;
|
|
10
|
+
if (os.platform() === 'linux')
|
|
11
|
+
return 3;
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
async function fetchJson(url, fetchImpl, signal) {
|
|
15
|
+
const timeoutSignal = AbortSignal.timeout(15_000);
|
|
16
|
+
const response = await fetchImpl(url, {
|
|
17
|
+
headers: { Accept: 'application/json' },
|
|
18
|
+
signal: signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal,
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok)
|
|
21
|
+
throw new Error(`企微扫码服务请求失败: HTTP ${response.status}`);
|
|
22
|
+
return response.json();
|
|
23
|
+
}
|
|
24
|
+
export async function createWecomQrSession(fetchImpl = fetch) {
|
|
25
|
+
const url = new URL(WECOM_QR_GENERATE_URL);
|
|
26
|
+
url.searchParams.set('source', 'wecom-cli');
|
|
27
|
+
url.searchParams.set('plat', String(platformCode()));
|
|
28
|
+
const payload = await fetchJson(url, fetchImpl);
|
|
29
|
+
const scode = String(payload?.data?.scode ?? '').trim();
|
|
30
|
+
const authUrl = String(payload?.data?.auth_url ?? '').trim();
|
|
31
|
+
if (!scode || !authUrl)
|
|
32
|
+
throw new Error('企微扫码服务未返回 scode 或授权地址');
|
|
33
|
+
const browserUrl = new URL(WECOM_QR_PAGE_URL);
|
|
34
|
+
browserUrl.searchParams.set('source', 'wecom-cli');
|
|
35
|
+
browserUrl.searchParams.set('scode', scode);
|
|
36
|
+
return { scode, authUrl, browserUrl: browserUrl.toString() };
|
|
37
|
+
}
|
|
38
|
+
export async function pollWecomQrCredentials(scode, opts) {
|
|
39
|
+
const fetchImpl = opts?.fetchImpl ?? fetch;
|
|
40
|
+
const timeoutMs = opts?.timeoutMs ?? 5 * 60 * 1000;
|
|
41
|
+
const intervalMs = opts?.intervalMs ?? 3_000;
|
|
42
|
+
const deadline = Date.now() + timeoutMs;
|
|
43
|
+
const url = new URL(WECOM_QR_QUERY_URL);
|
|
44
|
+
url.searchParams.set('scode', scode);
|
|
45
|
+
let previousStatus = '';
|
|
46
|
+
while (Date.now() < deadline) {
|
|
47
|
+
if (opts?.signal?.aborted)
|
|
48
|
+
throw new Error('企微扫码已取消');
|
|
49
|
+
const payload = await fetchJson(url, fetchImpl, opts?.signal);
|
|
50
|
+
const status = String(payload?.data?.status ?? 'waiting');
|
|
51
|
+
if (status !== previousStatus) {
|
|
52
|
+
opts?.onStatus?.(status);
|
|
53
|
+
previousStatus = status;
|
|
54
|
+
}
|
|
55
|
+
if (status === 'success') {
|
|
56
|
+
const botId = String(payload?.data?.bot_info?.botid ?? '').trim();
|
|
57
|
+
const secret = String(payload?.data?.bot_info?.secret ?? '').trim();
|
|
58
|
+
if (!botId || !secret)
|
|
59
|
+
throw new Error('扫码成功,但企微未返回 Bot ID 或 Secret');
|
|
60
|
+
return { botId, secret };
|
|
61
|
+
}
|
|
62
|
+
if (status === 'expired' || status === 'cancelled' || status === 'rejected') {
|
|
63
|
+
throw new Error(`企微扫码未完成: ${status}`);
|
|
64
|
+
}
|
|
65
|
+
await new Promise((resolve, reject) => {
|
|
66
|
+
const finish = () => {
|
|
67
|
+
opts?.signal?.removeEventListener('abort', onAbort);
|
|
68
|
+
resolve();
|
|
69
|
+
};
|
|
70
|
+
const timer = setTimeout(finish, intervalMs);
|
|
71
|
+
const onAbort = () => {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
opts?.signal?.removeEventListener('abort', onAbort);
|
|
74
|
+
reject(new Error('企微扫码已取消'));
|
|
75
|
+
};
|
|
76
|
+
opts?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
77
|
+
if (opts?.signal?.aborted)
|
|
78
|
+
onAbort();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
throw new Error('企微扫码超时,请重新生成二维码');
|
|
82
|
+
}
|