evolclaw 3.1.4 → 3.1.6
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 +60 -0
- package/dist/agents/claude-runner.js +398 -161
- package/dist/agents/kit-renderer.js +191 -25
- package/dist/aun/aid/agentmd.js +75 -103
- package/dist/aun/aid/client.js +1 -29
- package/dist/aun/aid/identity.js +105 -64
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/msg/group.js +2 -2
- package/dist/aun/msg/p2p.js +26 -2
- package/dist/aun/rpc/connection.js +23 -30
- package/dist/channels/aun.js +174 -99
- package/dist/channels/dingtalk.js +2 -1
- package/dist/channels/feishu.js +301 -199
- package/dist/channels/qqbot.js +2 -1
- package/dist/channels/wechat.js +2 -1
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent.js +21 -16
- package/dist/cli/bench.js +41 -28
- package/dist/cli/help.js +8 -0
- package/dist/cli/index.js +176 -87
- package/dist/cli/init-channel.js +5 -1
- package/dist/cli/init.js +37 -21
- package/dist/cli/link-rules.js +1 -7
- package/dist/cli/model.js +549 -0
- package/dist/cli/net-check.js +133 -50
- package/dist/cli/watch-msg.js +7 -7
- package/dist/cli/watch-web/debug-log.js +18 -0
- package/dist/cli/watch-web/server.js +306 -0
- package/dist/cli/watch-web/sources/aid.js +63 -0
- package/dist/cli/watch-web/sources/msg.js +70 -0
- package/dist/cli/watch-web/sources/session.js +638 -0
- package/dist/cli/watch-web/sources/types.js +10 -0
- package/dist/cli/watch-web/static/app.js +546 -0
- package/dist/cli/watch-web/static/index.html +54 -0
- package/dist/cli/watch-web/static/style.css +247 -0
- package/dist/config-store.js +1 -22
- package/dist/core/channel-loader.js +7 -4
- package/dist/core/command-handler.js +261 -133
- package/dist/core/evolagent-registry.js +1 -1
- package/dist/core/evolagent.js +4 -22
- package/dist/core/interaction-router.js +59 -0
- package/dist/core/message/im-renderer.js +9 -20
- package/dist/core/message/message-bridge.js +13 -9
- package/dist/core/message/message-log.js +2 -2
- package/dist/core/message/message-processor.js +211 -123
- package/dist/core/message/stream-idle-monitor.js +21 -0
- package/dist/core/model/model-catalog.js +215 -0
- package/dist/core/model/model-scope.js +250 -0
- package/dist/core/relation/peer-identity.js +58 -55
- package/dist/core/relation/peer-key.js +16 -0
- package/dist/core/session/session-fs-store.js +34 -55
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +308 -251
- package/dist/core/session/session-mapper.js +9 -4
- package/dist/core/trigger/manager.js +3 -3
- package/dist/core/trigger/parser.js +4 -4
- package/dist/core/trigger/scheduler.js +22 -7
- package/dist/index.js +61 -7
- package/dist/ipc.js +23 -1
- package/dist/utils/error-utils.js +6 -0
- package/dist/utils/process-introspect.js +7 -5
- package/kits/docs/GUIDE.md +2 -2
- package/kits/docs/INDEX.md +8 -8
- package/kits/docs/channels/aun.md +56 -17
- package/kits/docs/channels/feishu.md +41 -12
- package/kits/docs/context-assembly.md +182 -0
- package/kits/docs/evolclaw/INDEX.md +43 -0
- package/kits/docs/evolclaw/agent.md +49 -0
- package/kits/docs/evolclaw/aid.md +49 -0
- package/kits/docs/evolclaw/ctl.md +46 -0
- package/kits/docs/evolclaw/group.md +89 -0
- package/kits/docs/evolclaw/model.md +51 -0
- package/kits/docs/evolclaw/msg.md +91 -0
- package/kits/docs/evolclaw/rpc.md +35 -0
- package/kits/docs/evolclaw/storage.md +49 -0
- package/kits/docs/venues/aun-group.md +10 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +23 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.json +81 -36
- package/kits/rules/01-overview.md +20 -10
- package/kits/rules/06-channel.md +34 -27
- package/kits/templates/system-fragments/baseagent.md +7 -1
- package/kits/templates/system-fragments/channel.md +7 -5
- package/kits/templates/system-fragments/commands.md +19 -0
- package/kits/templates/system-fragments/session.md +19 -3
- package/kits/templates/system-fragments/venue.md +24 -0
- package/package.json +10 -5
- package/dist/aun/aid/lifecycle-log.js +0 -33
- package/dist/utils/aid-lifecycle-log.js +0 -33
- package/kits/docs/evolclaw/AGENT_CMD.md +0 -31
- package/kits/docs/evolclaw/MSG_GROUP.md +0 -30
- package/kits/docs/evolclaw/MSG_PRIVATE.md +0 -72
- package/kits/docs/evolclaw/tools.md +0 -25
package/dist/channels/aun.js
CHANGED
|
@@ -12,8 +12,8 @@ import { appendAidEvent } from '../utils/instance-registry.js';
|
|
|
12
12
|
import { appendMessageLog, buildOutboundEntry } from '../core/message/message-log.js';
|
|
13
13
|
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
14
14
|
import { appendAidLifecycle } from '../aun/aid/identity.js';
|
|
15
|
-
import {
|
|
16
|
-
import { loadAgent, saveAgent
|
|
15
|
+
import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
|
|
16
|
+
import { loadAgent, saveAgent } from '../config-store.js';
|
|
17
17
|
import { getProcessStartTime } from '../utils/process-introspect.js';
|
|
18
18
|
import * as outbox from '../aun/outbox.js';
|
|
19
19
|
import { guessMime, formatSize } from '../utils/media-cache.js';
|
|
@@ -61,6 +61,9 @@ function getEvolclawVersion() {
|
|
|
61
61
|
export class AUNChannel {
|
|
62
62
|
config;
|
|
63
63
|
client = null;
|
|
64
|
+
store = null;
|
|
65
|
+
/** 实际连接的网关 URL(来自 authenticate() 返回值 / connection.state 事件),替代旧 (client as any)._gatewayUrl。 */
|
|
66
|
+
gatewayUrl = '';
|
|
64
67
|
projectPathProvider;
|
|
65
68
|
messageHandler;
|
|
66
69
|
recallHandler;
|
|
@@ -115,11 +118,6 @@ export class AUNChannel {
|
|
|
115
118
|
*/
|
|
116
119
|
async callAndTrace(method, params, opts) {
|
|
117
120
|
this.trace('OUT', method, params);
|
|
118
|
-
// [DIAG-STALE] 记录调用瞬间 SDK 内部 _state,证明是否在 reconnecting 中误发
|
|
119
|
-
const sdkStateBefore = this.client?._state ?? 'no-client';
|
|
120
|
-
if (sdkStateBefore !== 'connected') {
|
|
121
|
-
logger.warn(`[AUN][DIAG-STALE] callAndTrace ${method} on non-connected SDK: sdk_state=${sdkStateBefore} evolclaw_connected=${this.connected}`);
|
|
122
|
-
}
|
|
123
121
|
try {
|
|
124
122
|
const result = await this.client.call(method, params);
|
|
125
123
|
if (!opts?.silentOk) {
|
|
@@ -137,9 +135,6 @@ export class AUNChannel {
|
|
|
137
135
|
code: e?.code,
|
|
138
136
|
name: e?.name,
|
|
139
137
|
});
|
|
140
|
-
// [DIAG-STALE] 失败时再记录一次 SDK _state,看错误类型是否为 ConnectionError
|
|
141
|
-
const sdkStateAfter = this.client?._state ?? 'no-client';
|
|
142
|
-
logger.warn(`[AUN][DIAG-STALE] callAndTrace ${method} FAILED: err_name=${e?.name ?? '?'} err_code=${e?.code ?? '?'} sdk_state_before=${sdkStateBefore} sdk_state_after=${sdkStateAfter} evolclaw_connected=${this.connected}`);
|
|
143
138
|
logger.warn(`${this.logPrefix()} rpc ${method} failed: ${e?.name ?? ''}(${e?.code ?? ''}) ${e?.message ?? e}`);
|
|
144
139
|
throw e;
|
|
145
140
|
}
|
|
@@ -545,12 +540,17 @@ export class AUNChannel {
|
|
|
545
540
|
}
|
|
546
541
|
this.client = null;
|
|
547
542
|
}
|
|
543
|
+
if (this.store) {
|
|
544
|
+
try {
|
|
545
|
+
this.store.close();
|
|
546
|
+
}
|
|
547
|
+
catch { /* ignore */ }
|
|
548
|
+
this.store = null;
|
|
549
|
+
}
|
|
548
550
|
this.connected = false;
|
|
549
551
|
const aunPath = this.config.keystorePath || resolveRoot();
|
|
550
552
|
const aidName = this.config.aid;
|
|
551
|
-
|
|
552
|
-
|| process.env.AUN_ENCRYPTION_SEED
|
|
553
|
-
|| 'evol';
|
|
553
|
+
// encryptionSeed 由 getAidStore 内部解析(config / env / 'evol')
|
|
554
554
|
// Migration from ~/.aun is handled by ensureDataDirs() at startup with a marker file.
|
|
555
555
|
// Gateway URL 解析:优先用配置的 gatewayUrl,否则通过 well-known 自动发现
|
|
556
556
|
let gateway = this.config.gatewayUrl || '';
|
|
@@ -571,16 +571,18 @@ export class AUNChannel {
|
|
|
571
571
|
throw new Error('Cannot resolve gateway URL from AID');
|
|
572
572
|
}
|
|
573
573
|
logger.info(`${this.logPrefix()} Initializing: aid=${aidName}, gateway=${gateway}, aun_path=${aunPath}`);
|
|
574
|
-
//
|
|
575
|
-
//
|
|
576
|
-
const
|
|
574
|
+
// 构造 AIDStore(slot=evolclaw daemon,与 cli/netcheck 共享 evolclaw 隔离键)
|
|
575
|
+
// encryptionSeed / rootCaPath 由 getAidStore 内部注入
|
|
576
|
+
const store = await getAidStore({
|
|
577
|
+
slotId: SLOT.daemon,
|
|
577
578
|
aunPath,
|
|
578
|
-
|
|
579
|
-
aunSdkLog: this.config.aunSdkLog ?? true,
|
|
579
|
+
debug: this.config.aunSdkLog ?? false,
|
|
580
580
|
});
|
|
581
|
+
this.store = store;
|
|
582
|
+
const client = await loadClient(store, aidName);
|
|
581
583
|
this.client = client;
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
+
// 记录应用层发现的 gateway 作为初始值(authenticate 后会用权威值覆盖)
|
|
585
|
+
this.gatewayUrl = gateway;
|
|
584
586
|
// Register event handlers before connecting
|
|
585
587
|
client.on('message.received', (data) => {
|
|
586
588
|
this.trace('IN', 'message.received', data);
|
|
@@ -629,30 +631,16 @@ export class AUNChannel {
|
|
|
629
631
|
const d = data;
|
|
630
632
|
logger.warn(`${this.logPrefix()} Group message undecryptable: group=${d.group_id} from=${d.from} mid=${d.message_id} err=${d._decrypt_error}`);
|
|
631
633
|
});
|
|
632
|
-
// Authenticate
|
|
633
|
-
// Workaround: SDK 0.3.x _loadIdentityOrRaise doesn't set identity.aid from requested aid,
|
|
634
|
-
// causing gateway "missing aid" error. Patch to backfill aid on loaded identity.
|
|
635
|
-
const authFlow = client._auth;
|
|
636
|
-
if (authFlow && typeof authFlow._loadIdentityOrRaise === 'function') {
|
|
637
|
-
const origLoad = authFlow._loadIdentityOrRaise.bind(authFlow);
|
|
638
|
-
authFlow._loadIdentityOrRaise = (aid) => {
|
|
639
|
-
const identity = origLoad(aid);
|
|
640
|
-
if (identity && !identity.aid)
|
|
641
|
-
identity.aid = aid ?? authFlow._aid;
|
|
642
|
-
return identity;
|
|
643
|
-
};
|
|
644
|
-
}
|
|
645
|
-
let accessToken;
|
|
634
|
+
// Authenticate(拿权威 gateway 用于日志/状态;connect 内部也会复用 token)
|
|
646
635
|
try {
|
|
647
636
|
logger.info(`${this.logPrefix()} Authenticating as ${aidName}...`);
|
|
648
637
|
this.trace('OUT', 'auth.authenticate', { aid: aidName });
|
|
649
|
-
const auth = await client.
|
|
650
|
-
this.trace('OUT', 'auth.authenticate.ok', { aid:
|
|
651
|
-
this.trace('IN', 'auth.result', { aid:
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
client.
|
|
655
|
-
logger.info(`${this.logPrefix()} Authenticated as ${auth.aid ?? '?'}, gateway=${resolvedGateway}`);
|
|
638
|
+
const auth = await client.authenticate();
|
|
639
|
+
this.trace('OUT', 'auth.authenticate.ok', { aid: client.aid, gateway: auth?.gateway, hasToken: !!auth?.access_token });
|
|
640
|
+
this.trace('IN', 'auth.result', { aid: client.aid, gateway: auth?.gateway, hasToken: !!auth?.access_token });
|
|
641
|
+
const resolvedGateway = String(auth?.gateway ?? gateway);
|
|
642
|
+
this.gatewayUrl = resolvedGateway;
|
|
643
|
+
logger.info(`${this.logPrefix()} Authenticated as ${client.aid ?? '?'}, gateway=${resolvedGateway}`);
|
|
656
644
|
}
|
|
657
645
|
catch (e) {
|
|
658
646
|
const errMsg = e.message || String(e);
|
|
@@ -661,15 +649,9 @@ export class AUNChannel {
|
|
|
661
649
|
logger.error(`${this.logPrefix()} Authentication failed (${errName}): ${errMsg}`);
|
|
662
650
|
if (e.stack)
|
|
663
651
|
logger.debug(`${this.logPrefix()} Auth stack: ${e.stack}`);
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
logger.error(`${this.logPrefix()} No accessToken fallback available, scheduling retry`);
|
|
668
|
-
this.setAidStatus('failed', { lastError: `${errName}: ${errMsg}`.slice(0, 80) });
|
|
669
|
-
this.scheduleReconnect();
|
|
670
|
-
throw new Error('Authentication failed and no accessToken fallback available');
|
|
671
|
-
}
|
|
672
|
-
logger.warn(`${this.logPrefix()} Using accessToken fallback`);
|
|
652
|
+
this.setAidStatus('failed', { lastError: `${errName}: ${errMsg}`.slice(0, 80) });
|
|
653
|
+
this.scheduleReconnect();
|
|
654
|
+
throw new Error(`Authentication failed: ${errName}: ${errMsg}`);
|
|
673
655
|
}
|
|
674
656
|
// Connect (SDK auto_reconnect handles transient failures)
|
|
675
657
|
try {
|
|
@@ -678,11 +660,18 @@ export class AUNChannel {
|
|
|
678
660
|
agentName: this.config.agentName,
|
|
679
661
|
channelName: this.config.channelName,
|
|
680
662
|
});
|
|
681
|
-
this.trace('OUT', 'client.connect', { gateway:
|
|
682
|
-
await client.connect({
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
663
|
+
this.trace('OUT', 'client.connect', { gateway: this.gatewayUrl, extra_info: extraInfo });
|
|
664
|
+
await client.connect({
|
|
665
|
+
// connection_kind 默认 long;slot 已由 AID 携带(evolclaw daemon)
|
|
666
|
+
// extra_info:互踢诊断名片(0.4.3 公开 connect 已支持透传)
|
|
667
|
+
extra_info: extraInfo,
|
|
668
|
+
// max_attempts=0 = 无限重试(与 Go/Python 对齐),交由 SDK 自己跑指数退避
|
|
669
|
+
// initial_delay=1s,max_delay=300s(5min 封顶)
|
|
670
|
+
auto_reconnect: true,
|
|
671
|
+
retry_max_attempts: 0,
|
|
672
|
+
retry_initial_delay: 1.0,
|
|
673
|
+
retry_max_delay: 300.0,
|
|
674
|
+
});
|
|
686
675
|
this.trace('OUT', 'client.connect.ok', { aid: client.aid });
|
|
687
676
|
this._aid = this.client.aid ?? undefined;
|
|
688
677
|
const deviceId = this.client._device_id ?? '';
|
|
@@ -692,7 +681,7 @@ export class AUNChannel {
|
|
|
692
681
|
this.aidStatsCollector.setSelfName(this.config.aid, this._selfName);
|
|
693
682
|
this.connected = true;
|
|
694
683
|
this.connectedAt = Date.now();
|
|
695
|
-
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.
|
|
684
|
+
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
|
|
696
685
|
// Workaround: SDK e2ee uses _identity.cert for sender_cert_fingerprint;
|
|
697
686
|
// if cert is missing, it falls back to public key SPKI fingerprint which
|
|
698
687
|
// causes peer cert lookup failures. Backfill from keystore if needed.
|
|
@@ -705,8 +694,8 @@ export class AUNChannel {
|
|
|
705
694
|
}
|
|
706
695
|
}
|
|
707
696
|
logger.info(`${this.logPrefix()} Connected as ${this._aid}`);
|
|
708
|
-
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.
|
|
709
|
-
appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.
|
|
697
|
+
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
|
|
698
|
+
appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'connected', aid: this.config.aid, gateway: this.gatewayUrl });
|
|
710
699
|
// Send welcome message to owner after first connection
|
|
711
700
|
await this.sendWelcomeMessage();
|
|
712
701
|
}
|
|
@@ -785,13 +774,10 @@ tags:
|
|
|
785
774
|
|
|
786
775
|
EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
787
776
|
`;
|
|
788
|
-
// Write locally
|
|
789
|
-
fs.mkdirSync(path.dirname(agentMdLocalPath), { recursive: true });
|
|
790
|
-
fs.writeFileSync(agentMdLocalPath, newAgentMd, 'utf-8');
|
|
791
|
-
logger.info(`${this.logPrefix()} Updated agent.md for ${aidName}`);
|
|
792
|
-
// Publish to AUN network via publishAgentMd (auto-sign)
|
|
777
|
+
// Write locally and publish to AUN network (auto-sign)
|
|
793
778
|
try {
|
|
794
|
-
await
|
|
779
|
+
const { agentmdPut } = await import('../aun/aid/agentmd.js');
|
|
780
|
+
await agentmdPut(newAgentMd, { aid: aidName, store: this.store });
|
|
795
781
|
logger.info(`${this.logPrefix()} Published agent.md to AUN network`);
|
|
796
782
|
}
|
|
797
783
|
catch (e) {
|
|
@@ -804,18 +790,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
804
790
|
|
|
805
791
|
📋 **日常使用方法**:
|
|
806
792
|
|
|
807
|
-
1.
|
|
808
|
-
2.
|
|
809
|
-
3.
|
|
810
|
-
4. **查看状态**:发送 \`/status\` 查看当前会话状态
|
|
811
|
-
5. **会话管理**:发送 \`/session\` 查看和切换会话
|
|
793
|
+
1. **查看帮助**:发送 \`/help\` 查看所有可用命令
|
|
794
|
+
2. **查看状态**:发送 \`/status\` 查看当前会话状态
|
|
795
|
+
3. **会话管理**:发送 \`/session\` 查看和切换会话
|
|
812
796
|
|
|
813
797
|
💡 **提示**:
|
|
814
798
|
- 直接发送消息即可与 Claude/Codex 对话
|
|
815
|
-
-
|
|
799
|
+
- 支持多会话管理,每个会话独立上下文
|
|
816
800
|
- 所有命令以 \`/\` 开头
|
|
817
801
|
|
|
818
|
-
|
|
802
|
+
现在就可以开始工作了!`;
|
|
819
803
|
// First contact with Owner races against Owner's async cert fetch from
|
|
820
804
|
// gateway PKI; a 3s pause lets the cert propagate. persist_required asks
|
|
821
805
|
// the gateway to durably store the message so Owner can recover it via
|
|
@@ -832,6 +816,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
832
816
|
persist_required: true,
|
|
833
817
|
});
|
|
834
818
|
logger.info(`${this.logPrefix()} Welcome message sent to owner: ${owner}`);
|
|
819
|
+
// Send binding credential for Evol App to persist locally
|
|
820
|
+
await this.sendBindingCredential(owner, agentDisplayName, agentConfig.active_baseagent || 'claude').catch(e => logger.warn(`${this.logPrefix()} Binding credential failed: ${e}`));
|
|
835
821
|
// Mark agent as initialized in config.json (replaces old agent.md frontmatter flag)
|
|
836
822
|
try {
|
|
837
823
|
const fresh = loadAgent(aidName);
|
|
@@ -849,7 +835,57 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
849
835
|
logger.warn(`${this.logPrefix()} Failed to send welcome message: ${e}`);
|
|
850
836
|
}
|
|
851
837
|
}
|
|
838
|
+
async sendBindingCredential(owner, name, baseagent) {
|
|
839
|
+
if (!this.client)
|
|
840
|
+
return;
|
|
841
|
+
await this.callAndTrace('message.send', {
|
|
842
|
+
to: owner,
|
|
843
|
+
payload: { type: 'binding', aid: this.config.aid, name, owner, baseagent },
|
|
844
|
+
encrypt: true,
|
|
845
|
+
persist_required: true,
|
|
846
|
+
});
|
|
847
|
+
logger.info(`${this.logPrefix()} Binding credential sent to owner: ${owner}`);
|
|
848
|
+
}
|
|
852
849
|
// ── Event handlers ──────────────────────────────────────────
|
|
850
|
+
/**
|
|
851
|
+
* 判断附件是否为图片,返回 MIME 类型(非图片返回空)。
|
|
852
|
+
* 多重检测:附件元数据字段 → 文件名后缀 → 文件 magic bytes。
|
|
853
|
+
*/
|
|
854
|
+
detectImageMime(att, filePath) {
|
|
855
|
+
const extToMime = {
|
|
856
|
+
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
857
|
+
'.gif': 'image/gif', '.webp': 'image/webp',
|
|
858
|
+
};
|
|
859
|
+
// 1. 附件元数据字段(content_type / mime_type / mimeType)
|
|
860
|
+
const metaCt = (att?.content_type || att?.mime_type || att?.mimeType || '');
|
|
861
|
+
if (typeof metaCt === 'string' && metaCt.startsWith('image/'))
|
|
862
|
+
return metaCt;
|
|
863
|
+
// 2. 文件名后缀
|
|
864
|
+
const name = (att?.filename || att?.object_key || filePath || '').toLowerCase();
|
|
865
|
+
for (const [ext, mime] of Object.entries(extToMime)) {
|
|
866
|
+
if (name.endsWith(ext))
|
|
867
|
+
return mime;
|
|
868
|
+
}
|
|
869
|
+
// 3. magic bytes
|
|
870
|
+
try {
|
|
871
|
+
const { openSync, readSync, closeSync } = require('node:fs');
|
|
872
|
+
const fd = openSync(filePath, 'r');
|
|
873
|
+
const head = Buffer.alloc(12);
|
|
874
|
+
readSync(fd, head, 0, 12, 0);
|
|
875
|
+
closeSync(fd);
|
|
876
|
+
if (head[0] === 0x89 && head[1] === 0x50 && head[2] === 0x4e && head[3] === 0x47)
|
|
877
|
+
return 'image/png';
|
|
878
|
+
if (head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff)
|
|
879
|
+
return 'image/jpeg';
|
|
880
|
+
if (head[0] === 0x47 && head[1] === 0x49 && head[2] === 0x46)
|
|
881
|
+
return 'image/gif';
|
|
882
|
+
if (head[0] === 0x52 && head[1] === 0x49 && head[2] === 0x46 && head[3] === 0x46 &&
|
|
883
|
+
head[8] === 0x57 && head[9] === 0x45 && head[10] === 0x42 && head[11] === 0x50)
|
|
884
|
+
return 'image/webp';
|
|
885
|
+
}
|
|
886
|
+
catch { /* not readable, skip */ }
|
|
887
|
+
return '';
|
|
888
|
+
}
|
|
853
889
|
async downloadAttachment(att, channelId) {
|
|
854
890
|
const ownerAid = att.owner_aid || this._aid || '';
|
|
855
891
|
const objectKey = att.object_key;
|
|
@@ -858,7 +894,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
858
894
|
return null;
|
|
859
895
|
}
|
|
860
896
|
const filename = att.filename || objectKey.split('/').pop() || 'unknown';
|
|
861
|
-
|
|
897
|
+
// 安全:始终通过受信任的 ticket 路径获取下载 URL。
|
|
898
|
+
// 不信任 att.url(来自对端消息 payload,可被构造为内网/元数据地址,SSRF)。
|
|
899
|
+
let downloadUrl = '';
|
|
862
900
|
try {
|
|
863
901
|
const ticket = await this.callAndTrace('storage.create_download_ticket', {
|
|
864
902
|
owner_aid: ownerAid,
|
|
@@ -938,12 +976,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
938
976
|
// Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
|
|
939
977
|
const rawAttachments = this.collectAllAttachments(payload);
|
|
940
978
|
let finalText = text;
|
|
979
|
+
const inboundImages = [];
|
|
941
980
|
if (rawAttachments.length > 0 && this.client) {
|
|
942
981
|
const fileParts = [];
|
|
943
982
|
for (const att of rawAttachments) {
|
|
944
983
|
const filePath = await this.downloadAttachment(att, fromAid);
|
|
945
984
|
if (filePath) {
|
|
946
985
|
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
986
|
+
const mime = this.detectImageMime(att, filePath);
|
|
987
|
+
if (mime) {
|
|
988
|
+
try {
|
|
989
|
+
const { readFileSync } = await import('node:fs');
|
|
990
|
+
inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
|
|
991
|
+
}
|
|
992
|
+
catch { /* fallback to file path */ }
|
|
993
|
+
}
|
|
947
994
|
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
948
995
|
}
|
|
949
996
|
}
|
|
@@ -952,16 +999,18 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
952
999
|
if (text)
|
|
953
1000
|
parts.push(text);
|
|
954
1001
|
parts.push(...fileParts);
|
|
955
|
-
|
|
1002
|
+
if (inboundImages.length === 0)
|
|
1003
|
+
parts.push('请使用 Read 工具读取文件内容。');
|
|
956
1004
|
finalText = parts.join('\n\n');
|
|
957
1005
|
}
|
|
1006
|
+
logger.info(`${this.logPrefix()} [img-debug] private attachments=${rawAttachments.length} images=${inboundImages.length}`);
|
|
958
1007
|
}
|
|
959
1008
|
// 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
|
|
960
1009
|
// device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
|
|
961
1010
|
const chatId = fromAid;
|
|
962
1011
|
// 解析对端身份(30天缓存)
|
|
963
1012
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
964
|
-
const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.
|
|
1013
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.store, false);
|
|
965
1014
|
const shortAid = this.getShortAid(fromAid);
|
|
966
1015
|
const displayName = peerIdentity.name || shortAid;
|
|
967
1016
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
@@ -1007,7 +1056,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1007
1056
|
mentions,
|
|
1008
1057
|
peerName: displayName || undefined,
|
|
1009
1058
|
peerType: peerIdentity.type,
|
|
1059
|
+
sameDevice: msg.same_device === true || undefined,
|
|
1060
|
+
sameNetwork: msg.same_network === true || undefined,
|
|
1061
|
+
sameEgressIp: msg.same_egress_ip === true || undefined,
|
|
1010
1062
|
replyContext,
|
|
1063
|
+
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
1011
1064
|
});
|
|
1012
1065
|
}
|
|
1013
1066
|
async handleIncomingGroupMessage(data) {
|
|
@@ -1158,12 +1211,21 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1158
1211
|
: mentionedSelf && this._aid ? [this._aid] : [];
|
|
1159
1212
|
// Process attachments
|
|
1160
1213
|
let finalText = strippedText;
|
|
1214
|
+
const inboundImages = [];
|
|
1161
1215
|
if (hasAttachments && this.client) {
|
|
1162
1216
|
const fileParts = [];
|
|
1163
1217
|
for (const att of rawAttachments) {
|
|
1164
1218
|
const filePath = await this.downloadAttachment(att, groupId);
|
|
1165
1219
|
if (filePath) {
|
|
1166
1220
|
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
1221
|
+
const mime = this.detectImageMime(att, filePath);
|
|
1222
|
+
if (mime) {
|
|
1223
|
+
try {
|
|
1224
|
+
const { readFileSync } = await import('node:fs');
|
|
1225
|
+
inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
|
|
1226
|
+
}
|
|
1227
|
+
catch { /* fallback to file path */ }
|
|
1228
|
+
}
|
|
1167
1229
|
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
1168
1230
|
}
|
|
1169
1231
|
}
|
|
@@ -1172,12 +1234,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1172
1234
|
if (strippedText)
|
|
1173
1235
|
parts.push(strippedText);
|
|
1174
1236
|
parts.push(...fileParts);
|
|
1175
|
-
|
|
1237
|
+
if (inboundImages.length === 0)
|
|
1238
|
+
parts.push('请使用 Read 工具读取文件内容。');
|
|
1176
1239
|
finalText = parts.join('\n\n');
|
|
1177
1240
|
}
|
|
1178
1241
|
}
|
|
1179
1242
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
1180
|
-
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.
|
|
1243
|
+
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
|
|
1181
1244
|
const shortAid = this.getShortAid(senderAid);
|
|
1182
1245
|
const displayName = peerIdentity.name || shortAid;
|
|
1183
1246
|
// 详细 dispatch 决策日志:记录消息为何被路由到 agent
|
|
@@ -1205,6 +1268,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1205
1268
|
userId: senderAid,
|
|
1206
1269
|
peerName: displayName || undefined,
|
|
1207
1270
|
peerType: peerIdentity.type,
|
|
1271
|
+
sameDevice: msg.same_device === true || undefined,
|
|
1272
|
+
sameNetwork: msg.same_network === true || undefined,
|
|
1273
|
+
sameEgressIp: msg.same_egress_ip === true || undefined,
|
|
1208
1274
|
text: finalText,
|
|
1209
1275
|
chatType: 'group',
|
|
1210
1276
|
messageId,
|
|
@@ -1212,6 +1278,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1212
1278
|
threadId,
|
|
1213
1279
|
mentions,
|
|
1214
1280
|
replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
|
|
1281
|
+
images: inboundImages.length > 0 ? inboundImages : undefined,
|
|
1215
1282
|
});
|
|
1216
1283
|
}
|
|
1217
1284
|
dispatchMessage(event) {
|
|
@@ -1268,16 +1335,20 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1268
1335
|
channelId: event.channelId || '',
|
|
1269
1336
|
channelType: 'aun',
|
|
1270
1337
|
content: event.text || '',
|
|
1271
|
-
|
|
1338
|
+
selfAID: this._aid,
|
|
1272
1339
|
groupId: event.groupId,
|
|
1273
1340
|
chatType: event.chatType,
|
|
1274
1341
|
peerId: event.userId || event.channelId || '',
|
|
1275
1342
|
peerName: event.peerName,
|
|
1276
1343
|
peerType: event.peerType,
|
|
1344
|
+
sameDevice: event.sameDevice,
|
|
1345
|
+
sameNetwork: event.sameNetwork,
|
|
1346
|
+
sameEgressIp: event.sameEgressIp,
|
|
1277
1347
|
messageId: event.messageId,
|
|
1278
1348
|
threadId: event.threadId,
|
|
1279
1349
|
mentions: mentionObjects,
|
|
1280
1350
|
replyContext,
|
|
1351
|
+
images: event.images,
|
|
1281
1352
|
}).catch(err => {
|
|
1282
1353
|
logger.error(`${this.logPrefix()} Message handler error:`, err);
|
|
1283
1354
|
});
|
|
@@ -1351,17 +1422,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1351
1422
|
if (!data || typeof data !== 'object')
|
|
1352
1423
|
return;
|
|
1353
1424
|
const state = data.state ?? '';
|
|
1354
|
-
// [DIAG-STALE] 记录状态切换瞬间 evolclaw 的 connected 标志和 SDK 的内部 _state,
|
|
1355
|
-
// 用于证明"reconnecting 时 connected 保持 true,导致 sendMessage 误放行"的假设
|
|
1356
|
-
const sdkState = this.client?._state ?? 'no-client';
|
|
1357
|
-
const connectedBefore = this.connected;
|
|
1358
|
-
logger.info(`[AUN][DIAG-STALE] connection.state event: state=${state} attempt=${data.attempt ?? '-'} | connected_before=${connectedBefore} sdk_state=${sdkState}`);
|
|
1359
1425
|
if (state === 'connected') {
|
|
1360
1426
|
this.connected = true;
|
|
1361
1427
|
this.connectedAt = Date.now();
|
|
1362
1428
|
this.lastReconnectLogTime = 0;
|
|
1363
1429
|
this.lastReconnectLogAttempt = 0;
|
|
1364
|
-
|
|
1430
|
+
// connection.state 事件 payload 带实际连接的 gateway,更新本地缓存
|
|
1431
|
+
const evtGateway = data.gateway;
|
|
1432
|
+
if (typeof evtGateway === 'string' && evtGateway)
|
|
1433
|
+
this.gatewayUrl = evtGateway;
|
|
1434
|
+
this.setAidStatus('connected', { lastConnectedAt: Date.now(), lastError: undefined, gatewayUrl: this.gatewayUrl });
|
|
1365
1435
|
this.trace('IN', 'connection.state', data);
|
|
1366
1436
|
logger.info(`${this.logPrefix()} Connected`);
|
|
1367
1437
|
// 不在这里清 flapCount —— 短命连接一上来就会触发本分支,
|
|
@@ -1816,11 +1886,11 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1816
1886
|
appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text', source = 'daemon') {
|
|
1817
1887
|
try {
|
|
1818
1888
|
const sessionsDir = resolvePaths().sessionsDir;
|
|
1819
|
-
const
|
|
1820
|
-
const chatDir = chatDirPath(sessionsDir, 'aun', channelId,
|
|
1889
|
+
const selfAID = this.config.aid;
|
|
1890
|
+
const chatDir = chatDirPath(sessionsDir, 'aun', channelId, selfAID);
|
|
1821
1891
|
const chatmode = context?.metadata?.chatmode;
|
|
1822
1892
|
appendMessageLog(chatDir, buildOutboundEntry({
|
|
1823
|
-
from:
|
|
1893
|
+
from: selfAID,
|
|
1824
1894
|
to: channelId,
|
|
1825
1895
|
chatType: isGroup ? 'group' : 'private',
|
|
1826
1896
|
groupId: isGroup ? channelId : null,
|
|
@@ -1891,8 +1961,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1891
1961
|
const tid = putRes?.thought_id;
|
|
1892
1962
|
logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
1893
1963
|
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
1894
|
-
// 文本类 thought 写入 jsonl(只对有 text 的 item,过滤 tool 等结构化项)
|
|
1895
1964
|
if (thoughtText) {
|
|
1965
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, channelId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
|
|
1896
1966
|
this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
|
|
1897
1967
|
}
|
|
1898
1968
|
}
|
|
@@ -1903,6 +1973,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1903
1973
|
logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
|
|
1904
1974
|
this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
|
|
1905
1975
|
if (thoughtText) {
|
|
1976
|
+
this.aidStatsCollector?.recordOutbound(this.config.aid, targetId, Buffer.byteLength(thoughtText, 'utf-8'), thoughtText, false, encrypt, context?.metadata?.chatmode ?? 'proactive');
|
|
1906
1977
|
this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
|
|
1907
1978
|
}
|
|
1908
1979
|
}
|
|
@@ -2280,6 +2351,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2280
2351
|
}
|
|
2281
2352
|
this.client = null;
|
|
2282
2353
|
}
|
|
2354
|
+
if (this.store) {
|
|
2355
|
+
try {
|
|
2356
|
+
this.store.close();
|
|
2357
|
+
}
|
|
2358
|
+
catch { /* ignore */ }
|
|
2359
|
+
this.store = null;
|
|
2360
|
+
}
|
|
2283
2361
|
this.connected = false;
|
|
2284
2362
|
appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
|
|
2285
2363
|
appendAidLifecycle({ ts: Date.now(), iso: new Date().toISOString(), event: 'disconnected', aid: this.config.aid, reason: 'intentional' });
|
|
@@ -2381,7 +2459,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2381
2459
|
return { type: null };
|
|
2382
2460
|
try {
|
|
2383
2461
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
2384
|
-
const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.
|
|
2462
|
+
const identity = await PeerIdentityCache.resolve('aun', aid, selfAgentDir, this.store, false);
|
|
2385
2463
|
const type = identity.type === 'human' ? 'human' : 'ai';
|
|
2386
2464
|
const name = identity.name || undefined;
|
|
2387
2465
|
const info = { type, name };
|
|
@@ -2405,19 +2483,16 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
2405
2483
|
void this.fetchPeerInfo(aid).catch(() => { });
|
|
2406
2484
|
}
|
|
2407
2485
|
async uploadAgentMd(content) {
|
|
2408
|
-
if (!this.
|
|
2486
|
+
if (!this.store)
|
|
2409
2487
|
throw new Error('not connected');
|
|
2410
|
-
const {
|
|
2411
|
-
|
|
2412
|
-
fs.mkdirSync(path.dirname(localPath), { recursive: true });
|
|
2413
|
-
fs.writeFileSync(localPath, content, 'utf-8');
|
|
2414
|
-
await this.client.publishAgentMd();
|
|
2488
|
+
const { agentmdPut } = await import('../aun/aid/agentmd.js');
|
|
2489
|
+
await agentmdPut(content, { aid: this.config.aid, store: this.store });
|
|
2415
2490
|
}
|
|
2416
2491
|
async downloadAgentMd(aid) {
|
|
2417
|
-
if (!this.
|
|
2492
|
+
if (!this.store)
|
|
2418
2493
|
throw new Error('not connected');
|
|
2419
2494
|
const { agentmdSync } = await import('../aun/aid/agentmd.js');
|
|
2420
|
-
const result = await agentmdSync(aid, {
|
|
2495
|
+
const result = await agentmdSync(aid, { store: this.store ?? undefined });
|
|
2421
2496
|
return result.content ?? '';
|
|
2422
2497
|
}
|
|
2423
2498
|
}
|
|
@@ -2445,7 +2520,6 @@ export class AUNChannelPlugin {
|
|
|
2445
2520
|
gatewayUrl: inst.gatewayUrl,
|
|
2446
2521
|
accessToken: inst.accessToken,
|
|
2447
2522
|
flushDelay: inst.flushDelay,
|
|
2448
|
-
encryptionSeed: inst.encryptionSeed,
|
|
2449
2523
|
owner: inst.owner,
|
|
2450
2524
|
agentName: inst.agentName,
|
|
2451
2525
|
channelName: inst.name,
|
|
@@ -2455,7 +2529,7 @@ export class AUNChannelPlugin {
|
|
|
2455
2529
|
const adapter = {
|
|
2456
2530
|
channelName: inst.name,
|
|
2457
2531
|
channelKey: inst.name, // channelName 实际上就是 channelKey
|
|
2458
|
-
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true },
|
|
2532
|
+
capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true, thread: true },
|
|
2459
2533
|
send: async (envelope, payload) => {
|
|
2460
2534
|
const ctx = envelope.replyContext;
|
|
2461
2535
|
const channelId = envelope.channelId;
|
|
@@ -2636,7 +2710,7 @@ export class AUNChannelPlugin {
|
|
|
2636
2710
|
channel: adapter.channelName,
|
|
2637
2711
|
channelType,
|
|
2638
2712
|
channelId: opts.channelId,
|
|
2639
|
-
|
|
2713
|
+
selfAID: opts.selfAID,
|
|
2640
2714
|
groupId: opts.groupId,
|
|
2641
2715
|
content: opts.content,
|
|
2642
2716
|
chatType: opts.chatType || 'private',
|
|
@@ -2648,6 +2722,7 @@ export class AUNChannelPlugin {
|
|
|
2648
2722
|
threadId: opts.threadId,
|
|
2649
2723
|
replyContext: opts.replyContext,
|
|
2650
2724
|
source: opts.source,
|
|
2725
|
+
images: opts.images,
|
|
2651
2726
|
});
|
|
2652
2727
|
}), (channelId, text, replyContext) => channel.sendMessage(channelId, text, replyContext), adapter, channelType);
|
|
2653
2728
|
},
|
|
@@ -448,7 +448,7 @@ export class DingtalkChannelPlugin {
|
|
|
448
448
|
const adapter = {
|
|
449
449
|
channelName: inst.name,
|
|
450
450
|
channelKey: inst.name,
|
|
451
|
-
capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
|
|
451
|
+
capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false, thread: false },
|
|
452
452
|
send: async (envelope, payload) => {
|
|
453
453
|
const ctx = envelope.replyContext;
|
|
454
454
|
const channelId = envelope.channelId;
|
|
@@ -540,6 +540,7 @@ export class DingtalkChannelPlugin {
|
|
|
540
540
|
channel: adapter.channelName,
|
|
541
541
|
channelType,
|
|
542
542
|
channelId: event.channelId,
|
|
543
|
+
selfAID: inst.agentName,
|
|
543
544
|
content: event.content,
|
|
544
545
|
images: event.images,
|
|
545
546
|
chatType: event.chatType || 'private',
|