evolclaw 3.1.0 → 3.1.1
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/dist/agents/claude-runner.js +40 -3
- package/dist/aun/msg/p2p.js +38 -0
- package/dist/channels/aun.js +80 -27
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -2
- package/dist/channels/qqbot.js +1 -0
- package/dist/channels/wechat.js +1 -0
- package/dist/channels/wecom.js +1 -0
- package/dist/cli/index.js +147 -29
- package/dist/cli/init.js +3 -4
- package/dist/cli/watch-msg.js +107 -30
- package/dist/config-store.js +45 -0
- package/dist/core/command-handler.js +86 -82
- package/dist/core/message/im-renderer.js +43 -4
- package/dist/core/message/message-bridge.js +4 -0
- package/dist/core/message/message-log.js +6 -1
- package/dist/core/message/message-processor.js +50 -34
- package/dist/core/relation/peer-identity.js +161 -0
- package/dist/core/session/session-manager.js +7 -3
- 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 +49 -3
- package/dist/utils/error-utils.js +17 -13
- package/dist/utils/stats.js +216 -2
- package/kits/docs/evolclaw/MSG_PRIVATE.md +53 -6
- package/kits/rules/06-channel.md +30 -0
- package/package.json +2 -2
|
@@ -398,6 +398,25 @@ export class AgentRunner {
|
|
|
398
398
|
// 尝试发送交互卡片
|
|
399
399
|
let cardSent = false;
|
|
400
400
|
if (permCtx.adapter?.send) {
|
|
401
|
+
// 发送计划内容:找 plans 目录中最新修改的 .md 文件
|
|
402
|
+
if (sendPrompt) {
|
|
403
|
+
try {
|
|
404
|
+
const plansDir = path.join(process.env.HOME || '/root', '.claude', 'plans');
|
|
405
|
+
const files = fs.readdirSync(plansDir)
|
|
406
|
+
.filter((f) => f.endsWith('.md'))
|
|
407
|
+
.map((f) => ({ name: f, mtime: fs.statSync(path.join(plansDir, f)).mtimeMs }))
|
|
408
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
409
|
+
if (files.length > 0) {
|
|
410
|
+
const planContent = fs.readFileSync(path.join(plansDir, files[0].name), 'utf-8');
|
|
411
|
+
if (planContent.trim()) {
|
|
412
|
+
await sendPrompt(`📋 **计划内容**\n\n${planContent}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// 读取失败不影响后续审批流程
|
|
418
|
+
}
|
|
419
|
+
}
|
|
401
420
|
const requestId = `plan-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
402
421
|
const interaction = {
|
|
403
422
|
type: 'interaction',
|
|
@@ -497,6 +516,8 @@ export class AgentRunner {
|
|
|
497
516
|
let lastSessionId;
|
|
498
517
|
// tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
|
|
499
518
|
const toolUseNames = new Map();
|
|
519
|
+
let turnCount = 0;
|
|
520
|
+
const seenMessageIds = new Set();
|
|
500
521
|
for await (const event of sdkStream) {
|
|
501
522
|
// 提取 session_id(任意 SDK 事件都可能携带)
|
|
502
523
|
if (event.session_id && event.session_id !== lastSessionId) {
|
|
@@ -523,15 +544,31 @@ export class AgentRunner {
|
|
|
523
544
|
}
|
|
524
545
|
// assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
|
|
525
546
|
if (event.type === 'assistant' && event.message?.content) {
|
|
547
|
+
const msgId = event.message.id;
|
|
548
|
+
if (!msgId || !seenMessageIds.has(msgId)) {
|
|
549
|
+
if (msgId)
|
|
550
|
+
seenMessageIds.add(msgId);
|
|
551
|
+
turnCount++;
|
|
552
|
+
}
|
|
553
|
+
// 统计本轮 base agent 全部输出字符数(text + tool_use input)
|
|
554
|
+
let turnOutputChars = 0;
|
|
555
|
+
for (const content of event.message.content) {
|
|
556
|
+
if (content.type === 'tool_use') {
|
|
557
|
+
const inputStr = typeof content.input === 'string' ? content.input : JSON.stringify(content.input || '');
|
|
558
|
+
turnOutputChars += inputStr.length;
|
|
559
|
+
}
|
|
560
|
+
else if (content.type === 'text' && content.text) {
|
|
561
|
+
turnOutputChars += content.text.length;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
526
564
|
for (const content of event.message.content) {
|
|
527
565
|
if (content.type === 'tool_use') {
|
|
528
|
-
// 记录 id → name 映射,供后续 tool_result 使用
|
|
529
566
|
if (content.id)
|
|
530
567
|
toolUseNames.set(content.id, content.name);
|
|
531
|
-
yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id };
|
|
568
|
+
yield { type: 'tool_use', name: content.name, input: content.input, callId: content.id, turn: turnCount, outputTokens: turnOutputChars };
|
|
532
569
|
}
|
|
533
570
|
else if (content.type === 'text' && content.text) {
|
|
534
|
-
yield { type: 'text', text: content.text };
|
|
571
|
+
yield { type: 'text', text: content.text, outputTokens: turnOutputChars, turn: turnCount };
|
|
535
572
|
}
|
|
536
573
|
}
|
|
537
574
|
}
|
package/dist/aun/msg/p2p.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import { createShortConnection } from '../rpc/index.js';
|
|
2
3
|
import { uploadFileAndBuildPayload } from './upload.js';
|
|
4
|
+
import { appendMessageLog, buildOutboundEntry } from '../../core/message/message-log.js';
|
|
5
|
+
import { chatDirPath } from '../../core/session/session-fs-store.js';
|
|
6
|
+
import { resolvePaths } from '../../paths.js';
|
|
3
7
|
export async function msgSend(args) {
|
|
4
8
|
const conn = await createShortConnection(args.from, { aunPath: args.aunPath, slotId: args.slotId });
|
|
5
9
|
try {
|
|
10
|
+
// 1. 解析对端身份(30天缓存)
|
|
11
|
+
const { agentsDir } = resolvePaths();
|
|
12
|
+
const selfAgentDir = path.join(agentsDir, args.from);
|
|
13
|
+
const { PeerIdentityCache } = await import('../../core/relation/peer-identity.js');
|
|
14
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', args.to, selfAgentDir, conn, false);
|
|
15
|
+
// 2. 决定 chatmode(遵循来源1-3)
|
|
16
|
+
// 私聊:非 human 对端 → proactive,human 对端 → interactive
|
|
17
|
+
const chatmode = peerIdentity.isAgent ? 'proactive' : 'interactive';
|
|
18
|
+
// 3. 构建 payload
|
|
6
19
|
let payload;
|
|
7
20
|
switch (args.body.mode) {
|
|
8
21
|
case 'text':
|
|
@@ -29,10 +42,35 @@ export async function msgSend(args) {
|
|
|
29
42
|
break;
|
|
30
43
|
}
|
|
31
44
|
}
|
|
45
|
+
// 4. 写入 payload.chatmode
|
|
46
|
+
payload.chatmode = chatmode;
|
|
32
47
|
const sendParams = { to: args.to, payload };
|
|
33
48
|
// Default: plaintext. Set encrypt: true to enable E2EE.
|
|
34
49
|
sendParams.encrypt = args.encrypt === true;
|
|
35
50
|
const result = await conn.call('message.send', sendParams);
|
|
51
|
+
// 5. 写出方向 jsonl(与 daemon 一致格式,标记 source=cli)
|
|
52
|
+
if (result?.message_id) {
|
|
53
|
+
try {
|
|
54
|
+
const sessionsDir = resolvePaths().sessionsDir;
|
|
55
|
+
const chatDir = chatDirPath(sessionsDir, 'aun', args.to, args.from);
|
|
56
|
+
const textContent = args.body.mode === 'text' ? args.body.text
|
|
57
|
+
: args.body.mode === 'link' ? `[link] ${args.body.url}`
|
|
58
|
+
: args.body.mode === 'file' ? `[file] ${args.body.filePath}`
|
|
59
|
+
: `[payload]`;
|
|
60
|
+
appendMessageLog(chatDir, buildOutboundEntry({
|
|
61
|
+
from: args.from,
|
|
62
|
+
to: args.to,
|
|
63
|
+
chatType: 'private',
|
|
64
|
+
msgId: result.message_id,
|
|
65
|
+
content: textContent,
|
|
66
|
+
encrypt: args.encrypt === true,
|
|
67
|
+
chatmode, // 使用解析出的 chatmode
|
|
68
|
+
msgType: 'text',
|
|
69
|
+
source: 'cli',
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
catch { }
|
|
73
|
+
}
|
|
36
74
|
return {
|
|
37
75
|
ok: true,
|
|
38
76
|
message_id: result?.message_id,
|
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, 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,8 +408,8 @@ export class AUNChannel {
|
|
|
404
408
|
}
|
|
405
409
|
return out;
|
|
406
410
|
}
|
|
407
|
-
buildGroupReplyContext(taskId, senderAid, encrypted, messageId) {
|
|
408
|
-
const replyContext = { metadata: { encrypted } };
|
|
411
|
+
buildGroupReplyContext(taskId, senderAid, encrypted, messageId, chatmode) {
|
|
412
|
+
const replyContext = { metadata: { encrypted, chatmode } };
|
|
409
413
|
if (taskId)
|
|
410
414
|
replyContext.threadId = taskId;
|
|
411
415
|
replyContext.peerId = senderAid;
|
|
@@ -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',
|
|
@@ -947,12 +952,13 @@ 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 peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, this.agentDir, this.client, false);
|
|
951
957
|
const shortAid = this.getShortAid(fromAid);
|
|
952
|
-
const displayName =
|
|
958
|
+
const displayName = peerIdentity.name || shortAid;
|
|
953
959
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
954
960
|
const p2pPayloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
955
|
-
logger.info(`${this.logPrefix()} P2P dispatch decision: mid=${messageId} from=${shortAid}(${displayName}) peerType=${
|
|
961
|
+
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
962
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
957
963
|
if (p2pPayloadType === 'action_card_reply')
|
|
958
964
|
return;
|
|
@@ -964,7 +970,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
964
970
|
text: JSON.stringify(payload),
|
|
965
971
|
chatType: 'private', messageId, seq,
|
|
966
972
|
peerName: displayName || undefined,
|
|
967
|
-
peerType:
|
|
973
|
+
peerType: peerIdentity.type,
|
|
968
974
|
});
|
|
969
975
|
return;
|
|
970
976
|
}
|
|
@@ -974,11 +980,12 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
974
980
|
logger.info(`${this.logPrefix()} P2P dropped (type deny): type=${p2pPayloadType} from=${shortAid}(${displayName}) mid=${messageId}`);
|
|
975
981
|
return;
|
|
976
982
|
}
|
|
977
|
-
|
|
983
|
+
const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
984
|
+
logger.info(`${this.logPrefix()} P2P dispatched: from=${shortAid}(${displayName}) mid=${messageId} encrypt=${msgEncrypted} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
|
|
978
985
|
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
986
|
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 } };
|
|
987
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode);
|
|
988
|
+
const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode } };
|
|
982
989
|
if (taskId)
|
|
983
990
|
replyContext.threadId = taskId;
|
|
984
991
|
this.dispatchMessage({
|
|
@@ -991,7 +998,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
991
998
|
taskId,
|
|
992
999
|
mentions,
|
|
993
1000
|
peerName: displayName || undefined,
|
|
994
|
-
peerType:
|
|
1001
|
+
peerType: peerIdentity.type,
|
|
995
1002
|
replyContext,
|
|
996
1003
|
});
|
|
997
1004
|
}
|
|
@@ -1028,6 +1035,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1028
1035
|
if (/echo/i.test(firstLineFast) && firstLineFast.trim().length <= 10 && !hasEvolClawTrace) {
|
|
1029
1036
|
this.acknowledgeImmediately(messageId, seq);
|
|
1030
1037
|
const msgEncryptedFast = !!(msg.e2ee);
|
|
1038
|
+
const msgChatmodeFast = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1031
1039
|
const peerInfo = this.peerInfoCached(senderAid);
|
|
1032
1040
|
const shortAid = this.getShortAid(senderAid);
|
|
1033
1041
|
const displayName = peerInfo?.name || shortAid;
|
|
@@ -1043,7 +1051,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1043
1051
|
peerName: displayName,
|
|
1044
1052
|
peerType: peerInfo?.type || 'unknown',
|
|
1045
1053
|
seq,
|
|
1046
|
-
replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId),
|
|
1054
|
+
replyContext: this.buildGroupReplyContext(undefined, senderAid, msgEncryptedFast, messageId, msgChatmodeFast),
|
|
1047
1055
|
createdAt,
|
|
1048
1056
|
});
|
|
1049
1057
|
return;
|
|
@@ -1104,10 +1112,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1104
1112
|
};
|
|
1105
1113
|
let echoText = text;
|
|
1106
1114
|
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'}`;
|
|
1115
|
+
const msgChatmodeEcho = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1107
1116
|
this.pendingEchoMessages.set(messageId, {
|
|
1108
1117
|
text: echoText,
|
|
1109
1118
|
channelId: groupId,
|
|
1110
|
-
context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId),
|
|
1119
|
+
context: this.buildGroupReplyContext(undefined, senderAid, msgEncrypted, messageId, msgChatmodeEcho),
|
|
1111
1120
|
receiveTs: Date.now(),
|
|
1112
1121
|
});
|
|
1113
1122
|
// 继续走正常 Agent 流程(下面的代码会 dispatch)
|
|
@@ -1159,9 +1168,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1159
1168
|
finalText = parts.join('\n\n');
|
|
1160
1169
|
}
|
|
1161
1170
|
}
|
|
1162
|
-
const
|
|
1171
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, this.agentDir, this.client, false);
|
|
1163
1172
|
const shortAid = this.getShortAid(senderAid);
|
|
1164
|
-
const displayName =
|
|
1173
|
+
const displayName = peerIdentity.name || shortAid;
|
|
1165
1174
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
1166
1175
|
const payloadType = (payload && typeof payload === 'object') ? payload.type ?? '' : '';
|
|
1167
1176
|
const textMentionSelf = this._aid ? this.hasExplicitMention(text, this._aid) : false;
|
|
@@ -1173,26 +1182,27 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1173
1182
|
: mentionedSelf
|
|
1174
1183
|
? (structMentionSelf ? 'mention.self(struct)' : 'mention.self(text)')
|
|
1175
1184
|
: `${dispatchMode}.no-mention`;
|
|
1176
|
-
logger.info(`${this.logPrefix()} Group dispatch decision: mid=${messageId} group=${groupId} sender=${shortAid}(${displayName}) peerType=${
|
|
1185
|
+
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
1186
|
// action_card_reply 已在 extractTextPayload 中消费,不分发给 agent
|
|
1178
1187
|
if (payloadType === 'action_card_reply')
|
|
1179
1188
|
return;
|
|
1180
|
-
|
|
1189
|
+
const msgChatmode = (payload && typeof payload === 'object') ? payload.chatmode : undefined;
|
|
1190
|
+
logger.info(`${this.logPrefix()} Group dispatched: group=${groupId} sender=${shortAid}(${displayName}) mode=${dispatchMode} mid=${messageId} chatmode=${msgChatmode ?? 'none'} text=${finalText.slice(0, 60)}`);
|
|
1181
1191
|
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');
|
|
1192
|
+
this.aidStatsCollector?.recordInbound(this.config.aid, senderAid, Buffer.byteLength(finalText, 'utf-8'), finalText, payloadType === 'event', msgEncrypted, msgChatmode);
|
|
1183
1193
|
this.dispatchMessage({
|
|
1184
1194
|
channelId: groupId,
|
|
1185
1195
|
groupId,
|
|
1186
1196
|
userId: senderAid,
|
|
1187
1197
|
peerName: displayName || undefined,
|
|
1188
|
-
peerType:
|
|
1198
|
+
peerType: peerIdentity.type,
|
|
1189
1199
|
text: finalText,
|
|
1190
1200
|
chatType: 'group',
|
|
1191
1201
|
messageId,
|
|
1192
1202
|
seq,
|
|
1193
1203
|
taskId,
|
|
1194
1204
|
mentions,
|
|
1195
|
-
replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted, messageId),
|
|
1205
|
+
replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted, messageId, msgChatmode),
|
|
1196
1206
|
});
|
|
1197
1207
|
}
|
|
1198
1208
|
dispatchMessage(event) {
|
|
@@ -1624,9 +1634,6 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1624
1634
|
await this.flushPendingEcho(channelId);
|
|
1625
1635
|
}
|
|
1626
1636
|
let finalText = text;
|
|
1627
|
-
if (context?.title && (this.sentCount.get(channelId) || 0) > 0) {
|
|
1628
|
-
finalText = '最终回复\n' + text;
|
|
1629
|
-
}
|
|
1630
1637
|
this.sentCount.set(channelId, (this.sentCount.get(channelId) || 0) + 1);
|
|
1631
1638
|
if (this.isGroupId(channelId) && context?.peerId) {
|
|
1632
1639
|
if (!finalText.includes(`@${context.peerId}`)) {
|
|
@@ -1738,7 +1745,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1738
1745
|
else {
|
|
1739
1746
|
logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1740
1747
|
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);
|
|
1748
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
|
|
1749
|
+
this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true);
|
|
1742
1750
|
}
|
|
1743
1751
|
}
|
|
1744
1752
|
else {
|
|
@@ -1750,7 +1758,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1750
1758
|
else {
|
|
1751
1759
|
logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
|
|
1752
1760
|
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);
|
|
1761
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, targetAid, Buffer.byteLength(finalText, 'utf-8'), finalText, false, encrypt, context?.metadata?.chatmode);
|
|
1762
|
+
this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false);
|
|
1754
1763
|
}
|
|
1755
1764
|
}
|
|
1756
1765
|
return true;
|
|
@@ -1792,6 +1801,33 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1792
1801
|
}
|
|
1793
1802
|
}
|
|
1794
1803
|
}
|
|
1804
|
+
/** 出站消息写入 messages.jsonl(message.send/group.send/thought.put 成功后调用) */
|
|
1805
|
+
appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text') {
|
|
1806
|
+
try {
|
|
1807
|
+
const sessionsDir = resolvePaths().sessionsDir;
|
|
1808
|
+
const selfId = this.config.aid;
|
|
1809
|
+
const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfId);
|
|
1810
|
+
const chatmode = context?.metadata?.chatmode;
|
|
1811
|
+
appendMessageLog(chatDir, buildOutboundEntry({
|
|
1812
|
+
from: selfId,
|
|
1813
|
+
to: channelId,
|
|
1814
|
+
chatType: isGroup ? 'group' : 'private',
|
|
1815
|
+
groupId: isGroup ? channelId : null,
|
|
1816
|
+
msgId,
|
|
1817
|
+
content: text,
|
|
1818
|
+
replyTo: null,
|
|
1819
|
+
agent: null,
|
|
1820
|
+
model: null,
|
|
1821
|
+
durationMs: null,
|
|
1822
|
+
encrypt,
|
|
1823
|
+
chatmode,
|
|
1824
|
+
msgType,
|
|
1825
|
+
}));
|
|
1826
|
+
}
|
|
1827
|
+
catch (e) {
|
|
1828
|
+
logger.debug(`${this.logPrefix()} appendOutboundJsonl failed: ${e}`);
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1795
1831
|
/**
|
|
1796
1832
|
* 发送 thought 内容(Proactive 模式可观测)
|
|
1797
1833
|
* 群聊:调用 group.thought.put
|
|
@@ -1818,17 +1854,28 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1818
1854
|
try {
|
|
1819
1855
|
const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
|
|
1820
1856
|
const stage = payload?.stage ?? `items=${itemCount}`;
|
|
1857
|
+
// 提取 thought 文本(取最后一项的 text 或 content 字段)
|
|
1858
|
+
const items = payload?.items;
|
|
1859
|
+
let thoughtText;
|
|
1860
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
1861
|
+
const lastItem = items[items.length - 1];
|
|
1862
|
+
thoughtText = lastItem?.text || lastItem?.content || (typeof lastItem === 'string' ? lastItem : undefined);
|
|
1863
|
+
}
|
|
1821
1864
|
if (this.isGroupId(channelId)) {
|
|
1822
1865
|
params.group_id = targetId;
|
|
1823
1866
|
const putRes = await this.callAndTrace('group.thought.put', params);
|
|
1824
1867
|
const tid = putRes?.thought_id;
|
|
1825
1868
|
logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
1869
|
+
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
1870
|
+
// thought jsonl 写入已改为按 LLM 调用次数统计(在 complete 事件处写入),此处不再写
|
|
1826
1871
|
}
|
|
1827
1872
|
else {
|
|
1828
1873
|
params.to = targetId;
|
|
1829
1874
|
const putRes = await this.callAndTrace('message.thought.put', params);
|
|
1830
1875
|
const tid = putRes?.thought_id;
|
|
1831
1876
|
logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
1877
|
+
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
1878
|
+
// thought jsonl 写入已改为按 LLM 调用次数统计(在 complete 事件处写入),此处不再写
|
|
1832
1879
|
}
|
|
1833
1880
|
}
|
|
1834
1881
|
catch (e) {
|
|
@@ -2085,7 +2132,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2085
2132
|
// to avoid duplicate "已送达" at the sender CLI
|
|
2086
2133
|
this.messageSeqMap.delete(messageId);
|
|
2087
2134
|
}
|
|
2088
|
-
sendProcessingStatus(channelId, status, sessionId, taskId, context) {
|
|
2135
|
+
sendProcessingStatus(channelId, status, sessionId, taskId, context, extraMeta) {
|
|
2089
2136
|
if (status === 'start')
|
|
2090
2137
|
this.sentCount.delete(channelId); // 新任务开始,重置计数
|
|
2091
2138
|
if (!this.client || !this.connected)
|
|
@@ -2098,6 +2145,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2098
2145
|
error: 'error',
|
|
2099
2146
|
timeout: 'timeout',
|
|
2100
2147
|
queued: 'queued',
|
|
2148
|
+
progress: 'progress',
|
|
2101
2149
|
};
|
|
2102
2150
|
const statusPayload = {
|
|
2103
2151
|
type: 'status',
|
|
@@ -2105,6 +2153,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2105
2153
|
task_id: taskId,
|
|
2106
2154
|
session_id: sessionId,
|
|
2107
2155
|
severity,
|
|
2156
|
+
...(extraMeta && Object.keys(extraMeta).length > 0 && { metadata: extraMeta }),
|
|
2108
2157
|
};
|
|
2109
2158
|
if (context?.threadId)
|
|
2110
2159
|
statusPayload.thread_id = context.threadId;
|
|
@@ -2155,7 +2204,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2155
2204
|
const chatmode = context?.metadata?.chatmode ?? '?';
|
|
2156
2205
|
const initiator = statusPayload.initiator ?? '';
|
|
2157
2206
|
const refMsgId = statusPayload.ref_message_id ?? '';
|
|
2158
|
-
|
|
2207
|
+
const metaStr = statusPayload.metadata ? ` meta=${JSON.stringify(statusPayload.metadata)}` : '';
|
|
2208
|
+
logger.info(`${this.logPrefix()} task.${status} task=${taskId} session=${sessionId} chatmode=${chatmode} target=${targetLabel} initiator=${initiator} ref_msg=${refMsgId}${metaStr}`);
|
|
2159
2209
|
}
|
|
2160
2210
|
sendCustomPayload(channelId, payload) {
|
|
2161
2211
|
if (!this.client || !this.connected)
|
|
@@ -2421,6 +2471,9 @@ export class AUNChannelPlugin {
|
|
|
2421
2471
|
}
|
|
2422
2472
|
return;
|
|
2423
2473
|
}
|
|
2474
|
+
case 'status.progress':
|
|
2475
|
+
channel.sendProcessingStatus(channelId, 'progress', envelope.taskId, envelope.taskId, ctx, payload.metadata);
|
|
2476
|
+
return;
|
|
2424
2477
|
case 'status.started':
|
|
2425
2478
|
channel.sendProcessingStatus(channelId, 'start', envelope.taskId, envelope.taskId, ctx);
|
|
2426
2479
|
return;
|
package/dist/channels/feishu.js
CHANGED
|
@@ -219,7 +219,7 @@ export class FeishuChannel {
|
|
|
219
219
|
const imageData = await this.downloadAndSaveImage(imageKey, msg.chat_id, msg.message_id, projectPath);
|
|
220
220
|
if (imageData) {
|
|
221
221
|
const allImages = [...quotedImages, imageData];
|
|
222
|
-
const prompt = quotedText + '
|
|
222
|
+
const prompt = quotedText + '用户发送了一张图片,请结合上下文理解用户意图并回应。';
|
|
223
223
|
await this.messageHandler({ channelId: msg.chat_id, content: prompt, images: allImages, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
|
|
224
224
|
}
|
|
225
225
|
else {
|
|
@@ -573,7 +573,14 @@ export class FeishuChannel {
|
|
|
573
573
|
const truncated = content.slice(0, 28000) + '\n\n⚠️ 消息过长,已截断';
|
|
574
574
|
return this.sendMessage(chatId, truncated, options);
|
|
575
575
|
}
|
|
576
|
-
|
|
576
|
+
const respData = error?.response?.data;
|
|
577
|
+
const code = respData?.code;
|
|
578
|
+
logger.error('[Feishu] Failed to send message:', respData ? JSON.stringify(respData) : error?.message ?? error);
|
|
579
|
+
// post 格式相关错误(400/230001):降级为纯文本重试
|
|
580
|
+
if (!options?.forceText && (error?.response?.status === 400 || code === 230001)) {
|
|
581
|
+
logger.warn('[Feishu] Retrying as plain text (forceText)');
|
|
582
|
+
return this.sendMessage(chatId, content, { ...options, forceText: true });
|
|
583
|
+
}
|
|
577
584
|
throw error;
|
|
578
585
|
}
|
|
579
586
|
}
|
|
@@ -1357,6 +1364,7 @@ export class FeishuChannelPlugin {
|
|
|
1357
1364
|
case 'status.interrupted':
|
|
1358
1365
|
case 'status.error':
|
|
1359
1366
|
case 'status.timeout':
|
|
1367
|
+
case 'status.progress':
|
|
1360
1368
|
// Feishu 通过 acknowledge (✓ 表情) 表达状态,由 channel 自行处理
|
|
1361
1369
|
return;
|
|
1362
1370
|
case 'interaction':
|
package/dist/channels/qqbot.js
CHANGED
package/dist/channels/wechat.js
CHANGED