evolclaw 3.1.0 → 3.1.2
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 +407 -0
- package/README.md +1 -1
- package/SKILLS.md +311 -0
- package/dist/agents/claude-runner.js +40 -3
- package/dist/aun/aid/agentmd.js +7 -6
- package/dist/aun/aid/client.js +5 -11
- package/dist/aun/aid/identity.js +32 -13
- package/dist/aun/msg/group.js +1 -0
- package/dist/aun/msg/p2p.js +51 -0
- package/dist/aun/msg/upload.js +57 -18
- package/dist/channels/aun.js +124 -50
- package/dist/channels/dingtalk.js +2 -0
- package/dist/channels/feishu.js +15 -6
- package/dist/channels/qqbot.js +2 -0
- package/dist/channels/wechat.js +2 -0
- package/dist/channels/wecom.js +2 -0
- package/dist/cli/agent.js +130 -35
- package/dist/cli/index.js +221 -48
- package/dist/cli/init-channel.js +4 -2
- package/dist/cli/init.js +44 -23
- package/dist/cli/watch-msg.js +109 -30
- package/dist/config-store.js +67 -1
- package/dist/core/channel-loader.js +4 -4
- package/dist/core/command-handler.js +95 -84
- package/dist/core/evolagent-registry.js +45 -9
- package/dist/core/evolagent.js +4 -4
- package/dist/core/message/im-renderer.js +47 -8
- package/dist/core/message/message-bridge.js +30 -1
- package/dist/core/message/message-log.js +6 -1
- package/dist/core/message/message-processor.js +29 -35
- package/dist/core/relation/peer-identity.js +161 -0
- package/dist/core/session/session-fs-store.js +23 -0
- package/dist/core/session/session-manager.js +11 -4
- package/dist/core/trigger/manager.js +16 -0
- package/dist/core/trigger/parser.js +110 -0
- package/dist/core/trigger/scheduler.js +6 -0
- package/dist/index.js +64 -20
- package/dist/paths.js +35 -0
- package/dist/utils/cross-platform.js +2 -1
- package/dist/utils/error-utils.js +17 -13
- package/dist/utils/stats.js +216 -2
- package/kits/docs/INDEX.md +6 -0
- package/kits/docs/evolclaw/MSG_PRIVATE.md +53 -6
- package/kits/rules/06-channel.md +30 -0
- package/package.json +6 -3
package/dist/aun/msg/upload.js
CHANGED
|
@@ -3,21 +3,19 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { guessMime, formatSize } from '../../utils/media-cache.js';
|
|
5
5
|
import { inferPayloadType, isValidPayloadType } from './payload-type.js';
|
|
6
|
-
/** 小文件阈值:≤64KB 走 storage.put_object 内联 base64;>64KB 走 create_upload_session + HTTP PUT。 */
|
|
7
|
-
const INLINE_UPLOAD_LIMIT = 64 * 1024;
|
|
8
6
|
/** 单次上传最大大小(与 daemon sendFile 一致)。 */
|
|
9
7
|
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
8
|
+
/** server 端 inline 上限错误的识别特征。优先匹配错误消息里的“inline 上限”字样。 */
|
|
9
|
+
function isInlineLimitError(err) {
|
|
10
|
+
const msg = err?.message ?? '';
|
|
11
|
+
return /inline\s*上限|inline limit|create_upload_session/i.test(msg);
|
|
12
|
+
}
|
|
10
13
|
/**
|
|
11
14
|
* 上传本地文件并构造发送用的 payload。
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 3. 按 as / 扩展名 确定 payload.type
|
|
17
|
-
* 4. 构造 payload(含 attachments 引用)
|
|
18
|
-
*
|
|
19
|
-
* 不做 outbox 持久化、不做 E2EE 加密兜底——这些是 daemon 的职责。
|
|
20
|
-
* CLI 短连接场景假定网络稳定,失败抛异常给调用方处理。
|
|
16
|
+
* 策略:先无脑 storage.put_object(内联 base64)。server 报“超过 inline 上限”
|
|
17
|
+
* 时降级到 storage.create_upload_session + HTTP PUT + storage.complete_upload,
|
|
18
|
+
* 此时通过 onProgress 上报字节级进度。
|
|
21
19
|
*/
|
|
22
20
|
export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts) {
|
|
23
21
|
const absPath = path.resolve(filePath);
|
|
@@ -29,14 +27,19 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
|
|
|
29
27
|
throw new Error(`文件为空: ${absPath}`);
|
|
30
28
|
}
|
|
31
29
|
if (stat.size > MAX_FILE_SIZE) {
|
|
32
|
-
throw new Error(`文件过大 (${formatSize(stat.size)},
|
|
30
|
+
throw new Error(`文件过大 (${formatSize(stat.size)}, 上限 ${formatSize(MAX_FILE_SIZE)}): ${absPath}`);
|
|
33
31
|
}
|
|
34
32
|
const filename = path.basename(absPath);
|
|
35
33
|
const fileData = fs.readFileSync(absPath);
|
|
36
34
|
const sha256 = crypto.createHash('sha256').update(fileData).digest('hex');
|
|
37
35
|
const contentType = opts?.contentType ?? guessMime(filename);
|
|
38
36
|
const objectKey = `shared/${crypto.randomUUID()}/${filename}`;
|
|
39
|
-
|
|
37
|
+
const total = stat.size;
|
|
38
|
+
const report = (phase, bytes) => opts?.onProgress?.({ phase, bytes, total });
|
|
39
|
+
// 1. 先按 inline 走
|
|
40
|
+
let inlineRejected = false;
|
|
41
|
+
try {
|
|
42
|
+
report('inline', 0);
|
|
40
43
|
await conn.call('storage.put_object', {
|
|
41
44
|
object_key: objectKey,
|
|
42
45
|
content: fileData.toString('base64'),
|
|
@@ -44,32 +47,68 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
|
|
|
44
47
|
is_private: false,
|
|
45
48
|
overwrite: true,
|
|
46
49
|
});
|
|
50
|
+
report('inline', total);
|
|
47
51
|
}
|
|
48
|
-
|
|
52
|
+
catch (e) {
|
|
53
|
+
if (!isInlineLimitError(e))
|
|
54
|
+
throw e;
|
|
55
|
+
inlineRejected = true;
|
|
56
|
+
}
|
|
57
|
+
// 2. inline 被拒:降级到 session + HTTP PUT
|
|
58
|
+
if (inlineRejected) {
|
|
59
|
+
report('session-create', 0);
|
|
49
60
|
const session = await conn.call('storage.create_upload_session', {
|
|
50
61
|
object_key: objectKey,
|
|
51
|
-
size_bytes:
|
|
62
|
+
size_bytes: total,
|
|
52
63
|
content_type: contentType,
|
|
53
64
|
});
|
|
54
65
|
const uploadUrl = session?.upload_url;
|
|
55
66
|
if (!uploadUrl)
|
|
56
67
|
throw new Error('storage.create_upload_session 未返回 upload_url');
|
|
57
|
-
|
|
68
|
+
// 简单 Buffer 上传 + 周期性进度(不用流,避免某些 storage 网关对 chunked / duplex 不友好)
|
|
69
|
+
report('http-put', 0);
|
|
70
|
+
const PROGRESS_TICK_MS = 250;
|
|
71
|
+
let lastBytes = 0;
|
|
72
|
+
const tickerStart = Date.now();
|
|
73
|
+
// 简单的“估算”进度:实际上一次性发,但在等待响应期间按 elapsed 模拟字节数,让用户看到在跑
|
|
74
|
+
const ticker = setInterval(() => {
|
|
75
|
+
const elapsed = Date.now() - tickerStart;
|
|
76
|
+
// 假设 2MB/s 估算;不超过 99%
|
|
77
|
+
const estimated = Math.min(total - 1, Math.floor((elapsed / 1000) * 2 * 1024 * 1024));
|
|
78
|
+
if (estimated > lastBytes) {
|
|
79
|
+
lastBytes = estimated;
|
|
80
|
+
report('http-put', estimated);
|
|
81
|
+
}
|
|
82
|
+
}, PROGRESS_TICK_MS);
|
|
83
|
+
let uploadResp;
|
|
84
|
+
try {
|
|
85
|
+
uploadResp = await fetch(uploadUrl, {
|
|
86
|
+
method: 'PUT',
|
|
87
|
+
body: new Blob([new Uint8Array(fileData)], { type: contentType }),
|
|
88
|
+
headers: { 'Content-Type': contentType },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
clearInterval(ticker);
|
|
93
|
+
}
|
|
58
94
|
if (!uploadResp.ok)
|
|
59
95
|
throw new Error(`HTTP 上传失败: ${uploadResp.status}`);
|
|
96
|
+
report('http-put', total);
|
|
97
|
+
report('session-complete', total);
|
|
60
98
|
await conn.call('storage.complete_upload', {
|
|
61
99
|
object_key: objectKey,
|
|
62
100
|
sha256,
|
|
63
101
|
content_type: contentType,
|
|
64
102
|
is_private: false,
|
|
65
|
-
size_bytes:
|
|
103
|
+
size_bytes: total,
|
|
66
104
|
});
|
|
67
105
|
}
|
|
106
|
+
report('done', total);
|
|
68
107
|
const attachment = {
|
|
69
108
|
owner_aid: ownerAid,
|
|
70
109
|
object_key: objectKey,
|
|
71
110
|
filename,
|
|
72
|
-
size_bytes:
|
|
111
|
+
size_bytes: total,
|
|
73
112
|
sha256,
|
|
74
113
|
content_type: contentType,
|
|
75
114
|
};
|
|
@@ -91,7 +130,7 @@ export async function uploadFileAndBuildPayload(conn, ownerAid, filePath, opts)
|
|
|
91
130
|
if (opts?.text)
|
|
92
131
|
payload.text = opts.text;
|
|
93
132
|
else if (type === 'file')
|
|
94
|
-
payload.text = `📎 ${filename} (${formatSize(
|
|
133
|
+
payload.text = `📎 ${filename} (${formatSize(total)})`;
|
|
95
134
|
if (type === 'voice' && opts?.transcript)
|
|
96
135
|
payload.transcript = opts.transcript;
|
|
97
136
|
return { payload, type, attachment };
|
package/dist/channels/aun.js
CHANGED
|
@@ -6,14 +6,17 @@ import os from 'os';
|
|
|
6
6
|
import { logger, localTimestamp } from '../utils/logger.js';
|
|
7
7
|
import { LogWriter } from '../utils/log-writer.js';
|
|
8
8
|
import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
|
|
9
|
-
import { resolvePaths, getPackageRoot } from '../paths.js';
|
|
9
|
+
import { resolvePaths, getPackageRoot, agentMdPath as agentMdPathFn, agentDir as agentDirPath } from '../paths.js';
|
|
10
10
|
import { saveToUploads, sanitizeFileName } from '../utils/media-cache.js';
|
|
11
11
|
import { appendAidEvent } from '../utils/instance-registry.js';
|
|
12
|
+
import { appendMessageLog, buildOutboundEntry } from '../core/message/message-log.js';
|
|
13
|
+
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
12
14
|
import { appendAidLifecycle } from '../aun/aid/identity.js';
|
|
13
15
|
import { loadAgent, saveAgent } from '../config-store.js';
|
|
14
16
|
import { getProcessStartTime } from '../utils/process-introspect.js';
|
|
15
17
|
import * as outbox from '../aun/outbox.js';
|
|
16
18
|
import { guessMime, formatSize } from '../utils/media-cache.js';
|
|
19
|
+
import { PeerIdentityCache } from '../core/relation/peer-identity.js';
|
|
17
20
|
/**
|
|
18
21
|
* 构造 connect extra_info:自描述本进程身份。
|
|
19
22
|
*
|
|
@@ -67,6 +70,7 @@ export class AUNChannel {
|
|
|
67
70
|
queuedHandler = null;
|
|
68
71
|
pendingEchoMessages = new Map();
|
|
69
72
|
isEchoSending = false;
|
|
73
|
+
agentDir;
|
|
70
74
|
trace(dir, event, data) {
|
|
71
75
|
if (!this.config.aunTrace)
|
|
72
76
|
return;
|
|
@@ -404,10 +408,10 @@ export class AUNChannel {
|
|
|
404
408
|
}
|
|
405
409
|
return out;
|
|
406
410
|
}
|
|
407
|
-
buildGroupReplyContext(
|
|
408
|
-
const replyContext = { metadata: { encrypted } };
|
|
409
|
-
if (
|
|
410
|
-
replyContext.threadId =
|
|
411
|
+
buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode) {
|
|
412
|
+
const replyContext = { metadata: { encrypted, chatmode } };
|
|
413
|
+
if (threadId)
|
|
414
|
+
replyContext.threadId = threadId;
|
|
411
415
|
replyContext.peerId = senderAid;
|
|
412
416
|
if (messageId)
|
|
413
417
|
replyContext.replyToMessageId = messageId;
|
|
@@ -470,6 +474,7 @@ export class AUNChannel {
|
|
|
470
474
|
aidStatsCollector;
|
|
471
475
|
constructor(config) {
|
|
472
476
|
this.config = config;
|
|
477
|
+
this.agentDir = agentDirPath(config.aid);
|
|
473
478
|
if (config.aunTrace) {
|
|
474
479
|
this.traceWriter = new LogWriter({
|
|
475
480
|
baseName: 'aun',
|
|
@@ -726,8 +731,8 @@ export class AUNChannel {
|
|
|
726
731
|
logger.info(`${this.logPrefix()} No owner configured, skipping welcome message (will retry after auto-bind)`);
|
|
727
732
|
return;
|
|
728
733
|
}
|
|
729
|
-
const
|
|
730
|
-
const existingAgentMd = fs.existsSync(
|
|
734
|
+
const agentMdLocalPath = agentMdPathFn(aidName);
|
|
735
|
+
const existingAgentMd = fs.existsSync(agentMdLocalPath) ? fs.readFileSync(agentMdLocalPath, 'utf-8') : '';
|
|
731
736
|
const existingFrontmatterMatch = existingAgentMd.match(/^---\n([\s\S]*?)\n---/);
|
|
732
737
|
const existingFrontmatter = existingFrontmatterMatch?.[1] ?? '';
|
|
733
738
|
// Fetch owner's agent.md to derive name and validate type
|
|
@@ -773,8 +778,8 @@ tags:
|
|
|
773
778
|
EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
774
779
|
`;
|
|
775
780
|
// Write locally
|
|
776
|
-
fs.mkdirSync(path.dirname(
|
|
777
|
-
fs.writeFileSync(
|
|
781
|
+
fs.mkdirSync(path.dirname(agentMdLocalPath), { recursive: true });
|
|
782
|
+
fs.writeFileSync(agentMdLocalPath, newAgentMd, 'utf-8');
|
|
778
783
|
logger.info(`${this.logPrefix()} Updated agent.md for ${aidName}`);
|
|
779
784
|
// Publish to AUN network via auth.uploadAgentMd
|
|
780
785
|
try {
|
|
@@ -903,7 +908,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
903
908
|
const fromAid = msg.from ?? '';
|
|
904
909
|
const payload = msg.payload ?? '';
|
|
905
910
|
const text = this.extractTextPayload(payload, fromAid);
|
|
906
|
-
const
|
|
911
|
+
const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
907
912
|
const messageId = msg.message_id ?? '';
|
|
908
913
|
const seq = msg.seq;
|
|
909
914
|
// 回声过滤:自己发出的消息会被 gateway fanout 回来,
|
|
@@ -947,12 +952,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
947
952
|
// 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
|
|
948
953
|
// device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
|
|
949
954
|
const chatId = fromAid;
|
|
950
|
-
|
|
955
|
+
// 解析对端身份(30天缓存)
|
|
956
|
+
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
957
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.client, false);
|
|
951
958
|
const shortAid = this.getShortAid(fromAid);
|
|
952
|
-
const displayName =
|
|
959
|
+
const displayName = peerIdentity.name || shortAid;
|
|
953
960
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
954
961
|
const p2pPayloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
955
|
-
logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${
|
|
962
|
+
logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${p2pPayloadType} chatId=${chatId} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
|
|
956
963
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
957
964
|
if (p2pPayloadType === 'action_card_reply')
|
|
958
965
|
return;
|
|
@@ -964,7 +971,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
964
971
|
text: JSON.stringify(payload),
|
|
965
972
|
chatType: 'private', messageId, seq,
|
|
966
973
|
peerName: displayName || undefined,
|
|
967
|
-
peerType:
|
|
974
|
+
peerType: peerIdentity.type,
|
|
968
975
|
});
|
|
969
976
|
return;
|
|
970
977
|
}
|
|
@@ -974,13 +981,14 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
974
981
|
logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
|
|
975
982
|
return;
|
|
976
983
|
}
|
|
977
|
-
|
|
984
|
+
const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
985
|
+
logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
|
|
978
986
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: fromAid, msgId: messageId, kind: 'text', len: finalText.length });
|
|
979
987
|
const isSystemP2P = p2pPayloadType === 'event';
|
|
980
|
-
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P);
|
|
981
|
-
const replyContext = { metadata: { encrypted: msgEncrypted } };
|
|
982
|
-
if (
|
|
983
|
-
replyContext.threadId =
|
|
988
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode);
|
|
989
|
+
const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode } };
|
|
990
|
+
if (threadId)
|
|
991
|
+
replyContext.threadId = threadId;
|
|
984
992
|
this.dispatchMessage({
|
|
985
993
|
channelId: chatId,
|
|
986
994
|
userId: fromAid,
|
|
@@ -988,10 +996,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
988
996
|
chatType: 'private',
|
|
989
997
|
messageId,
|
|
990
998
|
seq,
|
|
991
|
-
|
|
999
|
+
threadId,
|
|
992
1000
|
mentions,
|
|
993
1001
|
peerName: displayName || undefined,
|
|
994
|
-
peerType:
|
|
1002
|
+
peerType: peerIdentity.type,
|
|
995
1003
|
replyContext,
|
|
996
1004
|
});
|
|
997
1005
|
}
|
|
@@ -1003,7 +1011,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1003
1011
|
const senderAid = msg.sender_aid ?? '';
|
|
1004
1012
|
const payload = msg.payload ?? '';
|
|
1005
1013
|
const text = this.extractTextPayload(payload, groupId, senderAid);
|
|
1006
|
-
const
|
|
1014
|
+
const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
|
|
1007
1015
|
const messageId = msg.message_id ?? '';
|
|
1008
1016
|
const seq = msg.seq;
|
|
1009
1017
|
// Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
|
|
@@ -1028,6 +1036,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1028
1036
|
if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
|
|
1029
1037
|
this.acknowledgeImmediately(messageId, seq);
|
|
1030
1038
|
const msgEncryptedFast = !!(msg.e2ee);
|
|
1039
|
+
const msgChatmodeFast = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1031
1040
|
const peerInfo = this.peerInfoCached(senderAid);
|
|
1032
1041
|
const shortAid = this.getShortAid(senderAid);
|
|
1033
1042
|
const displayName = peerInfo?.name || shortAid;
|
|
@@ -1043,7 +1052,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1043
1052
|
peerName: displayName,
|
|
1044
1053
|
peerType: peerInfo?.type || 'unknown',
|
|
1045
1054
|
seq,
|
|
1046
|
-
replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId),
|
|
1055
|
+
replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId, msgChatmodeFast),
|
|
1047
1056
|
createdAt,
|
|
1048
1057
|
});
|
|
1049
1058
|
return;
|
|
@@ -1104,10 +1113,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1104
1113
|
};
|
|
1105
1114
|
let echoText = text;
|
|
1106
1115
|
echoText += `\n${echoTs()} [EvolClaw.receive] from=${senderAid} mid=${messageId} chat=group self=${this._aid || 'unknown'} conn_uptime=${this.connectedAt ? Math.round((Date.now() - this.connectedAt) / 1000) + 's' : 'unknown'}`;
|
|
1116
|
+
const msgChatmodeEcho = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1107
1117
|
this.pendingEchoMessages.set(messageId, {
|
|
1108
1118
|
text: echoText,
|
|
1109
1119
|
channelId: groupId,
|
|
1110
|
-
context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId),
|
|
1120
|
+
context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho),
|
|
1111
1121
|
receiveTs: Date.now(),
|
|
1112
1122
|
});
|
|
1113
1123
|
// 继续走正常 Agent 流程(下面的代码会 dispatch)
|
|
@@ -1159,9 +1169,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1159
1169
|
finalText = parts.join('\n\n');
|
|
1160
1170
|
}
|
|
1161
1171
|
}
|
|
1162
|
-
const
|
|
1172
|
+
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
1173
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.client, false);
|
|
1163
1174
|
const shortAid = this.getShortAid(senderAid);
|
|
1164
|
-
const displayName =
|
|
1175
|
+
const displayName = peerIdentity.name || shortAid;
|
|
1165
1176
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
1166
1177
|
const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
1167
1178
|
const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
|
|
@@ -1173,26 +1184,27 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1173
1184
|
: mentionedSelf
|
|
1174
1185
|
? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
|
|
1175
1186
|
: `${dispatchMode}.no-mention`;
|
|
1176
|
-
logger.info(`${this.logPrefix()} Group dispatch decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${
|
|
1187
|
+
logger.info(`${this.logPrefix()} Group dispatch decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${peerIdentity.type} payloadType=${payloadType} dispatchMode=${dispatchMode} reason=${reason} structMentions=${JSON.stringify(payloadMentions)} textMentionSelf=${textMentionSelf} textMentionAll=${textMentionAll} structMentionSelf=${structMentionSelf} structMentionAll=${structMentionAll} encrypt=${msgEncrypted} textPreview=${JSON.stringify(text.slice(0, 80))}`);
|
|
1177
1188
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
1178
1189
|
if (payloadType === 'action_card_reply')
|
|
1179
1190
|
return;
|
|
1180
|
-
|
|
1191
|
+
const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1192
|
+
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
|
|
1181
1193
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_in', aid: this.config.aid, from: senderAid, msgId: messageId, kind: 'text', len: finalText.length, groupId });
|
|
1182
|
-
this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event');
|
|
1194
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode);
|
|
1183
1195
|
this.dispatchMessage({
|
|
1184
1196
|
channelId: groupId,
|
|
1185
1197
|
groupId,
|
|
1186
1198
|
userId: senderAid,
|
|
1187
1199
|
peerName: displayName || undefined,
|
|
1188
|
-
peerType:
|
|
1200
|
+
peerType: peerIdentity.type,
|
|
1189
1201
|
text: finalText,
|
|
1190
1202
|
chatType: 'group',
|
|
1191
1203
|
messageId,
|
|
1192
1204
|
seq,
|
|
1193
|
-
|
|
1205
|
+
threadId,
|
|
1194
1206
|
mentions,
|
|
1195
|
-
replyContext: this.buildGroupReplyContext(
|
|
1207
|
+
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
|
|
1196
1208
|
});
|
|
1197
1209
|
}
|
|
1198
1210
|
dispatchMessage(event) {
|
|
@@ -1242,8 +1254,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1242
1254
|
// Use caller-supplied replyContext (group path builds mentionUserIds);
|
|
1243
1255
|
// fall back to simple threadId-only context for private messages
|
|
1244
1256
|
let replyContext = event.replyContext;
|
|
1245
|
-
if (!replyContext && event.
|
|
1246
|
-
replyContext = { threadId: event.
|
|
1257
|
+
if (!replyContext && event.threadId) {
|
|
1258
|
+
replyContext = { threadId: event.threadId };
|
|
1247
1259
|
}
|
|
1248
1260
|
this.messageHandler({
|
|
1249
1261
|
channelId: event.channelId || '',
|
|
@@ -1256,7 +1268,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1256
1268
|
peerName: event.peerName,
|
|
1257
1269
|
peerType: event.peerType,
|
|
1258
1270
|
messageId: event.messageId,
|
|
1259
|
-
threadId: event.
|
|
1271
|
+
threadId: event.threadId,
|
|
1260
1272
|
mentions: mentionObjects,
|
|
1261
1273
|
replyContext,
|
|
1262
1274
|
}).catch(err => {
|
|
@@ -1624,9 +1636,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1624
1636
|
await this.flushPendingEcho(channelId);
|
|
1625
1637
|
}
|
|
1626
1638
|
let finalText = text;
|
|
1627
|
-
if (context?.title && (this.sentCount.get(channelId) || 0) > 0) {
|
|
1628
|
-
finalText = '最终回复\n' + text;
|
|
1629
|
-
}
|
|
1630
1639
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
1631
1640
|
if (this.isGroupId(channelId) && context?.peerId) {
|
|
1632
1641
|
if (!finalText.includes(`@${context.peerId}`)) {
|
|
@@ -1700,6 +1709,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1700
1709
|
const channelId = entry.channelId;
|
|
1701
1710
|
const finalText = entry.text;
|
|
1702
1711
|
const context = entry.context;
|
|
1712
|
+
// 从 context.metadata.source 读取 source,默认为 'daemon'
|
|
1713
|
+
const source = context?.metadata?.source ?? 'daemon';
|
|
1703
1714
|
const payload = { type: 'text', text: finalText };
|
|
1704
1715
|
if (this.isGroupId(channelId)) {
|
|
1705
1716
|
const extracted = this.extractMentionAidsFromText(finalText);
|
|
@@ -1713,7 +1724,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1713
1724
|
if (context?.metadata?.chatmode)
|
|
1714
1725
|
payload.chatmode = context.metadata.chatmode;
|
|
1715
1726
|
// 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
|
|
1716
|
-
logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} textLen=${finalText.length}`);
|
|
1727
|
+
logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
|
|
1717
1728
|
const isGroup = this.isGroupId(channelId);
|
|
1718
1729
|
const targetAid = channelId;
|
|
1719
1730
|
const encryptTarget = isGroup ? channelId : targetAid;
|
|
@@ -1738,7 +1749,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1738
1749
|
else {
|
|
1739
1750
|
logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1740
1751
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
|
|
1741
|
-
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText);
|
|
1752
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
|
|
1753
|
+
this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
|
|
1742
1754
|
}
|
|
1743
1755
|
}
|
|
1744
1756
|
else {
|
|
@@ -1750,7 +1762,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1750
1762
|
else {
|
|
1751
1763
|
logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1752
1764
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: result.message_id, kind: 'text', len: finalText.length });
|
|
1753
|
-
this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText);
|
|
1765
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
|
|
1766
|
+
this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
|
|
1754
1767
|
}
|
|
1755
1768
|
}
|
|
1756
1769
|
return true;
|
|
@@ -1792,6 +1805,34 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1792
1805
|
}
|
|
1793
1806
|
}
|
|
1794
1807
|
}
|
|
1808
|
+
/** 出站消息写入 messages.jsonl(message.send/group.send/thought.put 成功后调用) */
|
|
1809
|
+
appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text', source = 'daemon') {
|
|
1810
|
+
try {
|
|
1811
|
+
const sessionsDir = resolvePaths().sessionsDir;
|
|
1812
|
+
const selfId = this.config.aid;
|
|
1813
|
+
const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfId);
|
|
1814
|
+
const chatmode = context?.metadata?.chatmode;
|
|
1815
|
+
appendMessageLog(chatDir, buildOutboundEntry({
|
|
1816
|
+
from: selfId,
|
|
1817
|
+
to: channelId,
|
|
1818
|
+
chatType: isGroup ? 'group' : 'private',
|
|
1819
|
+
groupId: isGroup ? channelId : null,
|
|
1820
|
+
msgId,
|
|
1821
|
+
content: text,
|
|
1822
|
+
replyTo: null,
|
|
1823
|
+
agent: null,
|
|
1824
|
+
model: null,
|
|
1825
|
+
durationMs: null,
|
|
1826
|
+
encrypt,
|
|
1827
|
+
chatmode,
|
|
1828
|
+
msgType,
|
|
1829
|
+
source,
|
|
1830
|
+
}));
|
|
1831
|
+
}
|
|
1832
|
+
catch (e) {
|
|
1833
|
+
logger.debug(`${this.logPrefix()} appendOutboundJsonl failed: ${e}`);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1795
1836
|
/**
|
|
1796
1837
|
* 发送 thought 内容(Proactive 模式可观测)
|
|
1797
1838
|
* 群聊:调用 group.thought.put
|
|
@@ -1818,17 +1859,45 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1818
1859
|
try {
|
|
1819
1860
|
const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
|
|
1820
1861
|
const stage = payload?.stage ?? `items=${itemCount}`;
|
|
1862
|
+
// 提取 thought 文本(只对 kind=text 的 item 写 jsonl,过滤 tool_use/tool_result 等结构化项)
|
|
1863
|
+
const items = payload?.items;
|
|
1864
|
+
let thoughtText;
|
|
1865
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
1866
|
+
const lastItem = items[items.length - 1];
|
|
1867
|
+
// 优先 text 字段(kind=text 的 item),否则 content
|
|
1868
|
+
if (lastItem?.kind === 'text' && lastItem.text) {
|
|
1869
|
+
thoughtText = lastItem.text;
|
|
1870
|
+
}
|
|
1871
|
+
else if (lastItem?.text) {
|
|
1872
|
+
thoughtText = lastItem.text;
|
|
1873
|
+
}
|
|
1874
|
+
else if (lastItem?.content) {
|
|
1875
|
+
thoughtText = lastItem.content;
|
|
1876
|
+
}
|
|
1877
|
+
else if (typeof lastItem === 'string') {
|
|
1878
|
+
thoughtText = lastItem;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1821
1881
|
if (this.isGroupId(channelId)) {
|
|
1822
1882
|
params.group_id = targetId;
|
|
1823
1883
|
const putRes = await this.callAndTrace('group.thought.put', params);
|
|
1824
1884
|
const tid = putRes?.thought_id;
|
|
1825
1885
|
logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
1886
|
+
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
1887
|
+
// 文本类 thought 写入 jsonl(只对有 text 的 item,过滤 tool 等结构化项)
|
|
1888
|
+
if (thoughtText) {
|
|
1889
|
+
this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
|
|
1890
|
+
}
|
|
1826
1891
|
}
|
|
1827
1892
|
else {
|
|
1828
1893
|
params.to = targetId;
|
|
1829
1894
|
const putRes = await this.callAndTrace('message.thought.put', params);
|
|
1830
1895
|
const tid = putRes?.thought_id;
|
|
1831
1896
|
logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
1897
|
+
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
1898
|
+
if (thoughtText) {
|
|
1899
|
+
this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
|
|
1900
|
+
}
|
|
1832
1901
|
}
|
|
1833
1902
|
}
|
|
1834
1903
|
catch (e) {
|
|
@@ -2085,7 +2154,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2085
2154
|
// to avoid duplicate "已送达" at the sender CLI
|
|
2086
2155
|
this.messageSeqMap.delete(messageId);
|
|
2087
2156
|
}
|
|
2088
|
-
sendProcessingStatus(channelId, status, sessionId, taskId, context) {
|
|
2157
|
+
sendProcessingStatus(channelId, status, sessionId, taskId, context, extraMeta) {
|
|
2089
2158
|
if (status === 'start')
|
|
2090
2159
|
this.sentCount.delete(channelId); // 新任务开始,重置计数
|
|
2091
2160
|
if (!this.client || !this.connected)
|
|
@@ -2098,6 +2167,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2098
2167
|
error: 'error',
|
|
2099
2168
|
timeout: 'timeout',
|
|
2100
2169
|
queued: 'queued',
|
|
2170
|
+
progress: 'progress',
|
|
2101
2171
|
};
|
|
2102
2172
|
const statusPayload = {
|
|
2103
2173
|
type: 'status',
|
|
@@ -2105,6 +2175,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2105
2175
|
task_id: taskId,
|
|
2106
2176
|
session_id: sessionId,
|
|
2107
2177
|
severity,
|
|
2178
|
+
...(extraMeta && Object.keys(extraMeta).length > 0 && { metadata: extraMeta }),
|
|
2108
2179
|
};
|
|
2109
2180
|
if (context?.threadId)
|
|
2110
2181
|
statusPayload.thread_id = context.threadId;
|
|
@@ -2155,7 +2226,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2155
2226
|
const chatmode = context?.metadata?.chatmode ?? '?';
|
|
2156
2227
|
const initiator = statusPayload.initiator ?? '';
|
|
2157
2228
|
const refMsgId = statusPayload.ref_message_id ?? '';
|
|
2158
|
-
|
|
2229
|
+
const metaStr = statusPayload.metadata ? ` meta=${JSON.stringify(statusPayload.metadata)}` : '';
|
|
2230
|
+
logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel} initiator=${initiator} ref_msg=${refMsgId}${metaStr}`);
|
|
2159
2231
|
}
|
|
2160
2232
|
sendCustomPayload(channelId, payload) {
|
|
2161
2233
|
if (!this.client || !this.connected)
|
|
@@ -2253,13 +2325,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2253
2325
|
loadSelfName(aid) {
|
|
2254
2326
|
try {
|
|
2255
2327
|
const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
|
|
2256
|
-
const
|
|
2257
|
-
if (!fs.existsSync(
|
|
2328
|
+
const mdPath = agentMdPathFn(aidName);
|
|
2329
|
+
if (!fs.existsSync(mdPath)) {
|
|
2258
2330
|
// 异步拉取,不阻塞连接流程
|
|
2259
2331
|
this.fetchAndCacheSelfName(aidName);
|
|
2260
2332
|
return undefined;
|
|
2261
2333
|
}
|
|
2262
|
-
const content = fs.readFileSync(
|
|
2334
|
+
const content = fs.readFileSync(mdPath, 'utf-8');
|
|
2263
2335
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2264
2336
|
if (!fmMatch)
|
|
2265
2337
|
return undefined;
|
|
@@ -2370,6 +2442,7 @@ export class AUNChannelPlugin {
|
|
|
2370
2442
|
});
|
|
2371
2443
|
const adapter = {
|
|
2372
2444
|
channelName: inst.name,
|
|
2445
|
+
channelKey: inst.name, // channelName 实际上就是 channelKey
|
|
2373
2446
|
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true },
|
|
2374
2447
|
send: async (envelope, payload) => {
|
|
2375
2448
|
const ctx = envelope.replyContext;
|
|
@@ -2414,13 +2487,14 @@ export class AUNChannelPlugin {
|
|
|
2414
2487
|
await channel.sendThought(channelId, envelope.taskId, aunPayload, ctx);
|
|
2415
2488
|
}
|
|
2416
2489
|
else {
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
channel.sendStructured(channelId, aunPayload, ctx),
|
|
2420
|
-
]);
|
|
2490
|
+
// interactive 模式不发 thought.put,只写入消息历史
|
|
2491
|
+
await channel.sendStructured(channelId, aunPayload, ctx);
|
|
2421
2492
|
}
|
|
2422
2493
|
return;
|
|
2423
2494
|
}
|
|
2495
|
+
case 'status.progress':
|
|
2496
|
+
channel.sendProcessingStatus(channelId, 'progress', envelope.taskId, envelope.taskId, ctx, payload.metadata);
|
|
2497
|
+
return;
|
|
2424
2498
|
case 'status.started':
|
|
2425
2499
|
channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx);
|
|
2426
2500
|
return;
|
|
@@ -447,6 +447,7 @@ export class DingtalkChannelPlugin {
|
|
|
447
447
|
});
|
|
448
448
|
const adapter = {
|
|
449
449
|
channelName: inst.name,
|
|
450
|
+
channelKey: inst.name,
|
|
450
451
|
capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
|
|
451
452
|
send: async (envelope, payload) => {
|
|
452
453
|
const ctx = envelope.replyContext;
|
|
@@ -482,6 +483,7 @@ export class DingtalkChannelPlugin {
|
|
|
482
483
|
case 'status.interrupted':
|
|
483
484
|
case 'status.error':
|
|
484
485
|
case 'status.timeout':
|
|
486
|
+
case 'status.progress':
|
|
485
487
|
case 'custom':
|
|
486
488
|
return;
|
|
487
489
|
default:
|