evolcore 0.0.2 → 0.0.4
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 +93 -778
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- 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 +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- 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 +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- 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/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +285 -94
- package/dist/ipc.js +114 -8
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- 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/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- 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/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- 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 +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- 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
|
@@ -4,8 +4,28 @@ import imageType from 'image-type';
|
|
|
4
4
|
import { sanitizeFileName, saveToUploads, bufferToInboundImage } from '../utils/media-cache.js';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
7
|
+
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
7
8
|
import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
|
|
8
9
|
import { chatDirPath, decodeDirSegment } from '../core/session/session-fs-store.js';
|
|
10
|
+
import { sentReceipt, suppressedReceipt } from '../core/message/send-receipt.js';
|
|
11
|
+
import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
|
|
12
|
+
const feishuContactBinds = new ContactBindCodeRegistry({
|
|
13
|
+
channelType: 'feishu',
|
|
14
|
+
displayName: '飞书',
|
|
15
|
+
initCommand: 'ec init feishu',
|
|
16
|
+
});
|
|
17
|
+
export function registerPendingFeishuContactBind(req) {
|
|
18
|
+
return feishuContactBinds.register(req);
|
|
19
|
+
}
|
|
20
|
+
export function handlePendingFeishuContactBindMessage(ctx) {
|
|
21
|
+
return handleExplicitContactBindCommand(feishuContactBinds, ctx);
|
|
22
|
+
}
|
|
23
|
+
export function getPendingFeishuContactBind(selfAid, channelName, primaryId) {
|
|
24
|
+
return feishuContactBinds.get(selfAid, channelName, primaryId);
|
|
25
|
+
}
|
|
26
|
+
export function clearPendingFeishuContactBinds() {
|
|
27
|
+
feishuContactBinds.clear();
|
|
28
|
+
}
|
|
9
29
|
export class FeishuChannel {
|
|
10
30
|
config;
|
|
11
31
|
agentAid;
|
|
@@ -125,7 +145,7 @@ export class FeishuChannel {
|
|
|
125
145
|
try {
|
|
126
146
|
// 首次交互欢迎消息(使用共享帮助函数)
|
|
127
147
|
if (peerId) {
|
|
128
|
-
await sendWelcomeIfNeeded(this.welcomeManager, peerId, msg.chat_id, (id, text) => this.sendMessage(id, text), 'Feishu');
|
|
148
|
+
await sendWelcomeIfNeeded(this.welcomeManager, peerId, msg.chat_id, async (id, text) => { await this.sendMessage(id, text); }, 'Feishu');
|
|
129
149
|
}
|
|
130
150
|
// 提取话题信息
|
|
131
151
|
const threadId = msg.thread_id || undefined;
|
|
@@ -471,11 +491,11 @@ export class FeishuChannel {
|
|
|
471
491
|
}
|
|
472
492
|
async sendMessage(chatId, content, options) {
|
|
473
493
|
if (!this.client)
|
|
474
|
-
|
|
494
|
+
throw new Error('Feishu client is not connected');
|
|
475
495
|
content = exposeLocalMarkdownLinkTargetsForFeishu(content);
|
|
476
496
|
if (!content || content.trim() === '') {
|
|
477
497
|
logger.warn('[Feishu] Attempted to send empty message, skipping');
|
|
478
|
-
return;
|
|
498
|
+
return [];
|
|
479
499
|
}
|
|
480
500
|
// 飞书消息内容限制约 30KB(text)/ 150KB(post),安全阈值 28000 字符
|
|
481
501
|
// 超长消息自动拆分,按段落边界分割
|
|
@@ -483,12 +503,13 @@ export class FeishuChannel {
|
|
|
483
503
|
if (content.length > MAX_CONTENT_LENGTH) {
|
|
484
504
|
logger.info(`[Feishu] Message too long (${content.length} chars), splitting into parts`);
|
|
485
505
|
const parts = splitLongMessage(content, MAX_CONTENT_LENGTH);
|
|
506
|
+
const messageIds = [];
|
|
486
507
|
for (let i = 0; i < parts.length; i++) {
|
|
487
508
|
// 首条消息保留 reply 选项,后续消息不再 reply
|
|
488
509
|
const partOptions = i === 0 ? options : { ...options, replyToMessageId: undefined };
|
|
489
|
-
await this.sendMessage(chatId, parts[i], partOptions);
|
|
510
|
+
messageIds.push(...await this.sendMessage(chatId, parts[i], partOptions));
|
|
490
511
|
}
|
|
491
|
-
return;
|
|
512
|
+
return messageIds;
|
|
492
513
|
}
|
|
493
514
|
logger.debug(`[Feishu] sendMessage called, chatId: ${chatId}, content length: ${content.length}`);
|
|
494
515
|
try {
|
|
@@ -511,6 +532,7 @@ export class FeishuChannel {
|
|
|
511
532
|
else {
|
|
512
533
|
msgContent = JSON.stringify({ text: content });
|
|
513
534
|
}
|
|
535
|
+
let messageId;
|
|
514
536
|
if (options?.replyToMessageId) {
|
|
515
537
|
const replyData = { msg_type: msgType, content: msgContent };
|
|
516
538
|
if (options.replyInThread) {
|
|
@@ -520,6 +542,7 @@ export class FeishuChannel {
|
|
|
520
542
|
path: { message_id: options.replyToMessageId },
|
|
521
543
|
data: replyData
|
|
522
544
|
});
|
|
545
|
+
messageId = replyRes?.data?.message_id;
|
|
523
546
|
if (options.replyInThread && options.onThreadCreated) {
|
|
524
547
|
const newThreadId = replyRes?.data?.thread_id;
|
|
525
548
|
if (newThreadId)
|
|
@@ -527,12 +550,16 @@ export class FeishuChannel {
|
|
|
527
550
|
}
|
|
528
551
|
}
|
|
529
552
|
else {
|
|
530
|
-
await this.client.im.message.create({
|
|
553
|
+
const createRes = await this.client.im.message.create({
|
|
531
554
|
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
532
555
|
data: { receive_id: chatId, msg_type: msgType, content: msgContent }
|
|
533
556
|
});
|
|
557
|
+
messageId = createRes?.data?.message_id;
|
|
534
558
|
}
|
|
559
|
+
if (!messageId)
|
|
560
|
+
throw new Error('Feishu send succeeded without message_id');
|
|
535
561
|
logger.debug(`[Feishu] Sent message as ${useMarkdown ? 'post (Markdown)' : 'text'}`);
|
|
562
|
+
return [messageId];
|
|
536
563
|
}
|
|
537
564
|
catch (error) {
|
|
538
565
|
// 230011: 消息已被撤回,降级为普通消息重试
|
|
@@ -560,7 +587,7 @@ export class FeishuChannel {
|
|
|
560
587
|
}
|
|
561
588
|
async sendFile(chatId, filePath, options) {
|
|
562
589
|
if (!this.client)
|
|
563
|
-
|
|
590
|
+
throw new Error('Feishu client is not connected');
|
|
564
591
|
try {
|
|
565
592
|
// 检测是否为图片,是则走 sendImage(内联预览)而非文件卡片
|
|
566
593
|
// 读取足够字节供 file-type 解析(ZIP-based 格式如 PPTX 需要更多字节)
|
|
@@ -585,24 +612,25 @@ export class FeishuChannel {
|
|
|
585
612
|
}
|
|
586
613
|
});
|
|
587
614
|
if (!uploadResponse || !uploadResponse.file_key) {
|
|
588
|
-
|
|
589
|
-
return;
|
|
615
|
+
throw new Error('Feishu file upload failed: no file_key returned');
|
|
590
616
|
}
|
|
591
617
|
const fileKey = uploadResponse.file_key;
|
|
592
618
|
const msgContent = JSON.stringify({ file_key: fileKey });
|
|
593
619
|
logger.info('[Feishu] File uploaded, file_key:', fileKey);
|
|
620
|
+
let messageId;
|
|
594
621
|
if (options?.replyToMessageId) {
|
|
595
622
|
const replyData = { msg_type: 'file', content: msgContent };
|
|
596
623
|
if (options.replyInThread) {
|
|
597
624
|
replyData.reply_in_thread = true;
|
|
598
625
|
}
|
|
599
|
-
await this.client.im.message.reply({
|
|
626
|
+
const replyRes = await this.client.im.message.reply({
|
|
600
627
|
path: { message_id: options.replyToMessageId },
|
|
601
628
|
data: replyData
|
|
602
629
|
});
|
|
630
|
+
messageId = replyRes?.data?.message_id;
|
|
603
631
|
}
|
|
604
632
|
else {
|
|
605
|
-
await this.client.im.message.create({
|
|
633
|
+
const createRes = await this.client.im.message.create({
|
|
606
634
|
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
607
635
|
data: {
|
|
608
636
|
receive_id: chatId,
|
|
@@ -610,8 +638,12 @@ export class FeishuChannel {
|
|
|
610
638
|
content: msgContent
|
|
611
639
|
}
|
|
612
640
|
});
|
|
641
|
+
messageId = createRes?.data?.message_id;
|
|
613
642
|
}
|
|
643
|
+
if (!messageId)
|
|
644
|
+
throw new Error('Feishu file send succeeded without message_id');
|
|
614
645
|
logger.info('[Feishu] File message sent successfully');
|
|
646
|
+
return [messageId];
|
|
615
647
|
}
|
|
616
648
|
catch (error) {
|
|
617
649
|
// 230011/99992354: reply target invalid, retry without reply
|
|
@@ -626,7 +658,7 @@ export class FeishuChannel {
|
|
|
626
658
|
}
|
|
627
659
|
async sendImage(chatId, png, options) {
|
|
628
660
|
if (!this.client)
|
|
629
|
-
|
|
661
|
+
throw new Error('Feishu client is not connected');
|
|
630
662
|
try {
|
|
631
663
|
const uploadResponse = await this.client.im.image.create({
|
|
632
664
|
data: {
|
|
@@ -636,27 +668,32 @@ export class FeishuChannel {
|
|
|
636
668
|
});
|
|
637
669
|
const imageKey = uploadResponse?.image_key;
|
|
638
670
|
if (!imageKey) {
|
|
639
|
-
|
|
640
|
-
return;
|
|
671
|
+
throw new Error('Feishu image upload failed: no image_key returned');
|
|
641
672
|
}
|
|
642
673
|
logger.debug('[Feishu] Image uploaded, image_key:', imageKey);
|
|
643
674
|
const msgContent = JSON.stringify({ image_key: imageKey });
|
|
675
|
+
let messageId;
|
|
644
676
|
if (options?.replyToMessageId) {
|
|
645
677
|
const replyData = { msg_type: 'image', content: msgContent };
|
|
646
678
|
if (options.replyInThread)
|
|
647
679
|
replyData.reply_in_thread = true;
|
|
648
|
-
await this.client.im.message.reply({
|
|
680
|
+
const replyRes = await this.client.im.message.reply({
|
|
649
681
|
path: { message_id: options.replyToMessageId },
|
|
650
682
|
data: replyData
|
|
651
683
|
});
|
|
684
|
+
messageId = replyRes?.data?.message_id;
|
|
652
685
|
}
|
|
653
686
|
else {
|
|
654
|
-
await this.client.im.message.create({
|
|
687
|
+
const createRes = await this.client.im.message.create({
|
|
655
688
|
params: { receive_id_type: chatId.startsWith('ou_') ? 'open_id' : chatId.startsWith('on_') ? 'union_id' : 'chat_id' },
|
|
656
689
|
data: { receive_id: chatId, msg_type: 'image', content: msgContent }
|
|
657
690
|
});
|
|
691
|
+
messageId = createRes?.data?.message_id;
|
|
658
692
|
}
|
|
693
|
+
if (!messageId)
|
|
694
|
+
throw new Error('Feishu image send succeeded without message_id');
|
|
659
695
|
logger.debug('[Feishu] Image message sent successfully');
|
|
696
|
+
return [messageId];
|
|
660
697
|
}
|
|
661
698
|
catch (error) {
|
|
662
699
|
// 99992354: reply target invalid — image cannot easily retry, just log
|
|
@@ -1273,6 +1310,10 @@ export class FeishuChannel {
|
|
|
1273
1310
|
}
|
|
1274
1311
|
// messageId → Pin reaction create promise(等待 reaction_id 落地,供 promoteAck 删除)
|
|
1275
1312
|
pinReactions = new Map();
|
|
1313
|
+
// messageId → StatusInFlight reaction create promise(终态时替换)
|
|
1314
|
+
inFlightReactions = new Map();
|
|
1315
|
+
// taskId → 触发该任务的全部原始入站 messageId(批量合并时可有多条)
|
|
1316
|
+
taskAckMessages = new Map();
|
|
1276
1317
|
/** 收到消息时添加 Pin 表情(表示"已收到,排队中") */
|
|
1277
1318
|
addPinReaction(messageId) {
|
|
1278
1319
|
if (!this.client)
|
|
@@ -1284,22 +1325,94 @@ export class FeishuChannel {
|
|
|
1284
1325
|
.catch(() => undefined);
|
|
1285
1326
|
this.pinReactions.set(messageId, p);
|
|
1286
1327
|
}
|
|
1287
|
-
/** Runner 开始执行时:添加
|
|
1288
|
-
async promoteAckReaction(messageId) {
|
|
1328
|
+
/** Runner 开始执行时:添加 StatusInFlight,然后移除 Pin(视觉无闪烁) */
|
|
1329
|
+
async promoteAckReaction(messageId, taskId, sourceMessageIds, restartResume = false) {
|
|
1330
|
+
const messageIds = [...new Set([...(sourceMessageIds ?? []), messageId].filter(Boolean))];
|
|
1331
|
+
if (taskId)
|
|
1332
|
+
this.taskAckMessages.set(taskId, messageIds);
|
|
1289
1333
|
if (!this.client)
|
|
1290
1334
|
return;
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
this.
|
|
1301
|
-
|
|
1302
|
-
|
|
1335
|
+
const promotions = messageIds.map(id => {
|
|
1336
|
+
// 先加 StatusInFlight,再删 Pin——用户看到的是 Pin→Pin+InFlight→InFlight。
|
|
1337
|
+
const inFlightPending = this.client.im.messageReaction.create({
|
|
1338
|
+
path: { message_id: id },
|
|
1339
|
+
data: { reaction_type: { emoji_type: 'StatusInFlight' } },
|
|
1340
|
+
}).then((res) => res?.data?.reaction_id)
|
|
1341
|
+
.catch(() => undefined);
|
|
1342
|
+
this.inFlightReactions.set(id, inFlightPending);
|
|
1343
|
+
const pending = this.pinReactions.get(id);
|
|
1344
|
+
this.pinReactions.delete(id);
|
|
1345
|
+
return { id, pending };
|
|
1346
|
+
});
|
|
1347
|
+
// Every in-flight reaction is registered before this first await, so a
|
|
1348
|
+
// fast terminal event cannot race ahead and strand a merged-batch status.
|
|
1349
|
+
for (const { id, pending } of promotions) {
|
|
1350
|
+
if (restartResume)
|
|
1351
|
+
await this.clearRestartCrossMark(id);
|
|
1352
|
+
const reactionId = await pending;
|
|
1353
|
+
if (reactionId) {
|
|
1354
|
+
await this.client.im.messageReaction.delete({
|
|
1355
|
+
path: { message_id: id, reaction_id: reactionId },
|
|
1356
|
+
}).catch(() => { });
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* A daemon restart terminates the old task with CrossMark. The reaction IDs
|
|
1362
|
+
* are process-local, so a resumed process must discover and remove its own
|
|
1363
|
+
* stale terminal reaction before it can show the new task state.
|
|
1364
|
+
*/
|
|
1365
|
+
async clearRestartCrossMark(messageId) {
|
|
1366
|
+
const reactions = this.client?.im?.messageReaction;
|
|
1367
|
+
if (!reactions?.list || !reactions?.delete)
|
|
1368
|
+
return;
|
|
1369
|
+
try {
|
|
1370
|
+
const response = await reactions.list({
|
|
1371
|
+
path: { message_id: messageId },
|
|
1372
|
+
params: { reaction_type: 'CrossMark', page_size: 50 },
|
|
1373
|
+
});
|
|
1374
|
+
const staleReactionIds = (response?.data?.items ?? [])
|
|
1375
|
+
.filter((reaction) => (reaction?.reaction_type?.emoji_type === 'CrossMark'
|
|
1376
|
+
&& reaction?.operator?.operator_type === 'app'
|
|
1377
|
+
&& typeof reaction?.reaction_id === 'string'))
|
|
1378
|
+
.map((reaction) => reaction.reaction_id);
|
|
1379
|
+
await Promise.all(staleReactionIds.map(reactionId => (reactions.delete({ path: { message_id: messageId, reaction_id: reactionId } }).catch(() => { }))));
|
|
1380
|
+
}
|
|
1381
|
+
catch {
|
|
1382
|
+
// A missing reaction-list permission must not block the resumed task.
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
/** 任务终态:用 CheckMark/CrossMark 替换 StatusInFlight。 */
|
|
1386
|
+
async finalizeAckReaction(taskId, terminalEmoji) {
|
|
1387
|
+
const messageIds = this.taskAckMessages.get(taskId);
|
|
1388
|
+
if (!messageIds)
|
|
1389
|
+
return;
|
|
1390
|
+
if (!this.client) {
|
|
1391
|
+
this.taskAckMessages.delete(taskId);
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
this.taskAckMessages.delete(taskId);
|
|
1395
|
+
for (const messageId of messageIds) {
|
|
1396
|
+
const inFlightPending = this.inFlightReactions.get(messageId);
|
|
1397
|
+
// 先加终态表情,再删处理中表情,避免视觉空窗。
|
|
1398
|
+
const addTerminal = () => this.client.im.messageReaction.create({
|
|
1399
|
+
path: { message_id: messageId },
|
|
1400
|
+
data: { reaction_type: { emoji_type: terminalEmoji } },
|
|
1401
|
+
}).then((res) => !!res?.data?.reaction_id).catch(() => false);
|
|
1402
|
+
let terminalAdded = await addTerminal();
|
|
1403
|
+
if (!terminalAdded)
|
|
1404
|
+
terminalAdded = await addTerminal();
|
|
1405
|
+
if (!terminalAdded) {
|
|
1406
|
+
logger.warn(`[Feishu] Failed to add terminal reaction for ${messageId}; preserving processing reaction`);
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
this.inFlightReactions.delete(messageId);
|
|
1410
|
+
const inFlightReactionId = await inFlightPending;
|
|
1411
|
+
if (inFlightReactionId) {
|
|
1412
|
+
await this.client.im.messageReaction.delete({
|
|
1413
|
+
path: { message_id: messageId, reaction_id: inFlightReactionId },
|
|
1414
|
+
}).catch(() => { });
|
|
1415
|
+
}
|
|
1303
1416
|
}
|
|
1304
1417
|
}
|
|
1305
1418
|
}
|
|
@@ -1707,46 +1820,53 @@ export class FeishuChannelPlugin {
|
|
|
1707
1820
|
sendCtx.title = '✅ 最终回复:';
|
|
1708
1821
|
if (replyCtx?.metadata?.onThreadCreated)
|
|
1709
1822
|
sendCtx.onThreadCreated = replyCtx.metadata.onThreadCreated;
|
|
1710
|
-
await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
1711
|
-
return
|
|
1823
|
+
const ids = await channel.sendMessage(channelId, payload.text, sendCtx);
|
|
1824
|
+
return ids.length > 0
|
|
1825
|
+
? sentReceipt(envelope, ids, replyCtx?.threadId)
|
|
1826
|
+
: suppressedReceipt(envelope, 'empty_message');
|
|
1712
1827
|
}
|
|
1713
|
-
case 'result.file':
|
|
1714
|
-
|
|
1715
|
-
return;
|
|
1716
|
-
case 'result.image':
|
|
1717
|
-
await channel.sendImage(channelId, payload.data, replyCtx);
|
|
1718
|
-
return;
|
|
1828
|
+
case 'result.file': return sentReceipt(envelope, await channel.sendFile(channelId, payload.filePath, replyCtx), replyCtx?.threadId);
|
|
1829
|
+
case 'result.image': return sentReceipt(envelope, await channel.sendImage(channelId, payload.data, replyCtx), replyCtx?.threadId);
|
|
1719
1830
|
case 'activity.batch': {
|
|
1720
1831
|
const filtered = payload.items.filter((i) => !(i.kind === 'tool_result' && i.ok));
|
|
1721
1832
|
const text = formatItemsAsText(filtered);
|
|
1722
1833
|
if (text)
|
|
1723
|
-
await channel.sendMessage(channelId, text, replyCtx);
|
|
1724
|
-
return;
|
|
1834
|
+
return sentReceipt(envelope, await channel.sendMessage(channelId, text, replyCtx), replyCtx?.threadId);
|
|
1835
|
+
return suppressedReceipt(envelope, 'empty_activity');
|
|
1725
1836
|
}
|
|
1726
1837
|
case 'status.started':
|
|
1838
|
+
case 'status.queued':
|
|
1839
|
+
case 'status.progress':
|
|
1840
|
+
return suppressedReceipt(envelope, 'reaction_lifecycle');
|
|
1727
1841
|
case 'status.completed':
|
|
1842
|
+
await channel.finalizeAckReaction(envelope.taskId, 'CheckMark');
|
|
1843
|
+
return suppressedReceipt(envelope, 'reaction_lifecycle');
|
|
1728
1844
|
case 'status.interrupted':
|
|
1729
1845
|
case 'status.timeout':
|
|
1730
|
-
|
|
1846
|
+
await channel.finalizeAckReaction(envelope.taskId, 'CrossMark');
|
|
1847
|
+
return suppressedReceipt(envelope, 'reaction_lifecycle');
|
|
1731
1848
|
case 'status.requires_action':
|
|
1732
|
-
await channel.sendMessage(channelId, '等待 owner 审批', replyCtx);
|
|
1733
|
-
return;
|
|
1849
|
+
return sentReceipt(envelope, await channel.sendMessage(channelId, '等待 owner 审批', replyCtx), replyCtx?.threadId);
|
|
1734
1850
|
case 'status.error':
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1851
|
+
await channel.finalizeAckReaction(envelope.taskId, 'CrossMark');
|
|
1852
|
+
if (payload.metadata?.message) {
|
|
1853
|
+
return sentReceipt(envelope, await channel.sendMessage(channelId, payload.metadata.message, replyCtx), replyCtx?.threadId);
|
|
1854
|
+
}
|
|
1855
|
+
return suppressedReceipt(envelope, 'reaction_lifecycle');
|
|
1738
1856
|
case 'interaction': {
|
|
1739
1857
|
const sent = await channel.sendInteraction(channelId, payload.interaction, replyCtx);
|
|
1740
1858
|
if (!sent)
|
|
1741
1859
|
throw new Error('sendInteraction returned false');
|
|
1742
|
-
return;
|
|
1860
|
+
return sentReceipt(envelope, [sent], replyCtx?.threadId);
|
|
1743
1861
|
}
|
|
1744
|
-
case 'custom': return;
|
|
1745
|
-
default:
|
|
1862
|
+
case 'custom': return suppressedReceipt(envelope, 'unsupported_custom_payload');
|
|
1863
|
+
default:
|
|
1864
|
+
logger.warn(`[Feishu] Unhandled payload kind: ${payload.kind}`);
|
|
1865
|
+
return suppressedReceipt(envelope, 'unhandled_payload');
|
|
1746
1866
|
}
|
|
1747
1867
|
},
|
|
1748
1868
|
acknowledge: (messageId) => { channel.addPinReaction(messageId); return Promise.resolve(); },
|
|
1749
|
-
promoteAck: (messageId) => channel.promoteAckReaction(messageId),
|
|
1869
|
+
promoteAck: (messageId, context) => (channel.promoteAckReaction(messageId, context?.taskId, context?.messageIds, context?.restartResume)),
|
|
1750
1870
|
onInteraction: (callback) => channel.onInteraction(callback),
|
|
1751
1871
|
onInteractionInvalidated: (callback) => channel.onInteractionInvalidated(callback),
|
|
1752
1872
|
invalidateInteraction: (interactionId, reason) => channel.invalidateInteraction(interactionId, reason),
|
|
@@ -1766,23 +1886,31 @@ export class FeishuChannelPlugin {
|
|
|
1766
1886
|
return {
|
|
1767
1887
|
channelType: 'feishu', adapter, channel,
|
|
1768
1888
|
policy,
|
|
1769
|
-
options: { fileMarkerPattern:
|
|
1889
|
+
options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
|
|
1770
1890
|
connect: () => channel.connect(),
|
|
1771
1891
|
disconnect: () => channel.disconnect(),
|
|
1772
1892
|
onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
|
|
1773
1893
|
registerBridge(bridge, channelType) {
|
|
1774
1894
|
bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, mentionAids, threadId, rootId, chatType, source }) => {
|
|
1895
|
+
const replyContext = peerId || threadId
|
|
1896
|
+
? {
|
|
1897
|
+
...(peerId ? { peerId } : {}),
|
|
1898
|
+
...(threadId ? { threadId, replyToMessageId: rootId ?? threadId, replyInThread: true } : {}),
|
|
1899
|
+
}
|
|
1900
|
+
: undefined;
|
|
1775
1901
|
await handler({
|
|
1776
1902
|
channel: adapter.channelName, channelType, channelId: chatId, content, images,
|
|
1777
1903
|
selfAID: ctx.agentName, chatType: chatType || 'private',
|
|
1778
1904
|
peerId: peerId || '', peerName, messageId, mentions, mentionAids, threadId,
|
|
1779
|
-
replyContext
|
|
1905
|
+
replyContext,
|
|
1780
1906
|
source,
|
|
1781
1907
|
});
|
|
1782
|
-
}), (channelId, text, replyContext) =>
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1908
|
+
}), async (channelId, text, replyContext) => {
|
|
1909
|
+
await channel.sendMessage(channelId, text, {
|
|
1910
|
+
replyToMessageId: replyContext?.replyToMessageId,
|
|
1911
|
+
replyInThread: replyContext?.replyInThread,
|
|
1912
|
+
});
|
|
1913
|
+
}, adapter, channelType);
|
|
1786
1914
|
},
|
|
1787
1915
|
};
|
|
1788
1916
|
}
|
package/dist/channels/qqbot.js
CHANGED
|
@@ -3,7 +3,26 @@ import { markdownToPlainText } from '../utils/markdown-to-plain-text.js';
|
|
|
3
3
|
import { requireOptional } from '../utils/npm-ops.js';
|
|
4
4
|
import { middleOutputModePolicy, resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
|
|
5
5
|
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
6
|
+
import { createSendFileMarkerPattern } from '../core/message/file-markers.js';
|
|
6
7
|
import { initWelcomeManager, sendWelcomeIfNeeded } from '../utils/welcome.js';
|
|
8
|
+
import { ContactBindCodeRegistry, handleExplicitContactBindCommand, } from '../config/contact-bind-code.js';
|
|
9
|
+
const qqbotContactBinds = new ContactBindCodeRegistry({
|
|
10
|
+
channelType: 'qqbot',
|
|
11
|
+
displayName: 'QQ',
|
|
12
|
+
initCommand: 'ec init qqbot',
|
|
13
|
+
});
|
|
14
|
+
export function registerPendingQQBotContactBind(req) {
|
|
15
|
+
return qqbotContactBinds.register(req);
|
|
16
|
+
}
|
|
17
|
+
export function handlePendingQQBotContactBindMessage(ctx) {
|
|
18
|
+
return handleExplicitContactBindCommand(qqbotContactBinds, ctx);
|
|
19
|
+
}
|
|
20
|
+
export function getPendingQQBotContactBind(selfAid, channelName, primaryId) {
|
|
21
|
+
return qqbotContactBinds.get(selfAid, channelName, primaryId);
|
|
22
|
+
}
|
|
23
|
+
export function clearPendingQQBotContactBinds() {
|
|
24
|
+
qqbotContactBinds.clear();
|
|
25
|
+
}
|
|
7
26
|
// ── QQBotChannel ────────────────────────────────────────────────────────────
|
|
8
27
|
export class QQBotChannel {
|
|
9
28
|
agentAid;
|
|
@@ -93,6 +112,9 @@ export class QQBotChannel {
|
|
|
93
112
|
}
|
|
94
113
|
logger.info('[QQBot] Disconnected');
|
|
95
114
|
}
|
|
115
|
+
getStatus() {
|
|
116
|
+
return { connected: this.connected };
|
|
117
|
+
}
|
|
96
118
|
onMessage(handler) {
|
|
97
119
|
this.messageHandler = handler;
|
|
98
120
|
}
|
|
@@ -390,7 +412,7 @@ export class QQBotChannelPlugin {
|
|
|
390
412
|
return {
|
|
391
413
|
channelType: 'qqbot', adapter, channel,
|
|
392
414
|
policy,
|
|
393
|
-
options: { fileMarkerPattern:
|
|
415
|
+
options: { fileMarkerPattern: createSendFileMarkerPattern(), supportsImages: true, flushDelay: inst.flushDelay },
|
|
394
416
|
connect: () => channel.connect(),
|
|
395
417
|
disconnect: () => channel.disconnect(),
|
|
396
418
|
onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
|