evolclaw 3.1.3 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/assets/.env.template +4 -0
  3. package/assets/config.json.template +6 -0
  4. package/assets/wechat-group-qr.jpeg +0 -0
  5. package/dist/agents/kit-renderer.js +35 -21
  6. package/dist/aun/aid/agentmd.js +25 -54
  7. package/dist/aun/aid/client.js +22 -7
  8. package/dist/aun/aid/identity.js +314 -28
  9. package/dist/aun/aid/index.js +1 -1
  10. package/dist/aun/rpc/connection.js +8 -13
  11. package/dist/channels/aun.js +31 -72
  12. package/dist/cli/agent.js +15 -22
  13. package/dist/cli/bench.js +8 -14
  14. package/dist/cli/help.js +23 -0
  15. package/dist/cli/index.js +371 -36
  16. package/dist/cli/init-channel.js +2 -3
  17. package/dist/cli/link-rules.js +2 -1
  18. package/dist/cli/net-check.js +10 -11
  19. package/dist/core/command-handler.js +6 -7
  20. package/dist/core/message/message-processor.js +19 -18
  21. package/dist/core/relation/peer-identity.js +64 -21
  22. package/dist/core/session/session-manager.js +6 -2
  23. package/dist/core/trigger/manager.js +37 -0
  24. package/dist/index.js +4 -1
  25. package/dist/paths.js +87 -16
  26. package/dist/utils/npm-ops.js +18 -11
  27. package/kits/eck_manifest.json +8 -8
  28. package/kits/rules/05-venue.md +2 -2
  29. package/kits/templates/system-fragments/baseagent.md +7 -1
  30. package/kits/templates/system-fragments/channel.md +4 -1
  31. package/kits/templates/system-fragments/identity.md +4 -4
  32. package/kits/templates/system-fragments/relation.md +8 -5
  33. package/kits/templates/system-fragments/session.md +20 -0
  34. package/kits/templates/system-fragments/venue.md +4 -1
  35. package/package.json +4 -2
  36. package/dist/net-check.js +0 -640
  37. package/dist/watch-msg.js +0 -544
  38. package/kits/templates/system-fragments/eckruntime.md +0 -14
@@ -1,12 +1,14 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import net from 'net';
5
4
  import tls from 'tls';
6
5
  import dns from 'dns/promises';
7
6
  import https from 'https';
8
7
  // @ts-ignore
9
8
  import { WebSocket } from 'ws';
9
+ import { aunPath as defaultAunPath } from '../paths.js';
10
+ import { createAunClient } from '../aun/aid/client.js';
11
+ import { isHelpFlag } from './help.js';
10
12
  const GREEN = '\x1b[32m';
11
13
  const RED = '\x1b[31m';
12
14
  const YELLOW = '\x1b[33m';
@@ -217,10 +219,9 @@ async function runCheck(aid, formatJson) {
217
219
  let accessToken;
218
220
  try {
219
221
  const start = Date.now();
220
- const aunPath = process.env.AUN_HOME || path.join(os.homedir(), '.aun');
221
- const { AUNClient } = await import('@agentunion/fastaun');
222
+ const aunPath = process.env.AUN_HOME || defaultAunPath();
222
223
  const result = await suppressSdkOutput(async () => {
223
- const client = new AUNClient({ aun_path: aunPath, debug: false });
224
+ const client = await createAunClient({ aunPath });
224
225
  await client.auth.createAid({ aid });
225
226
  const authResult = await client.auth.authenticate({ aid });
226
227
  await client.close().catch(() => { });
@@ -268,10 +269,9 @@ async function runCheck(aid, formatJson) {
268
269
  // ── Step 9: RPC 调用 (meta.ping) ──
269
270
  try {
270
271
  const start = Date.now();
271
- const aunPath = process.env.AUN_HOME || path.join(os.homedir(), '.aun');
272
- const { AUNClient } = await import('@agentunion/fastaun');
272
+ const aunPath = process.env.AUN_HOME || defaultAunPath();
273
273
  const sendResult = await suppressSdkOutput(async () => {
274
- const client = new AUNClient({ aun_path: aunPath, debug: false });
274
+ const client = await createAunClient({ aunPath });
275
275
  await client.auth.createAid({ aid });
276
276
  const authResult = await client.auth.authenticate({ aid });
277
277
  const at = authResult?.access_token || client._access_token;
@@ -291,8 +291,7 @@ async function runCheck(aid, formatJson) {
291
291
  // CLI 模拟 app 发送 echo[nc],向多个目标测试链路
292
292
  try {
293
293
  const echoStart = Date.now();
294
- const aunPath = process.env.AUN_HOME || path.join(os.homedir(), '.aun');
295
- const { AUNClient } = await import('@agentunion/fastaun');
294
+ const aunPath = process.env.AUN_HOME || defaultAunPath();
296
295
  // 选择 6 个测试目标,按消息数量、域、有无证书均衡选择
297
296
  const allAids = await getAidList();
298
297
  const myDomain = aid.split('.').slice(1).join('.');
@@ -421,7 +420,7 @@ async function runCheck(aid, formatJson) {
421
420
  const label = targetLabel(target);
422
421
  try {
423
422
  const replyText = await suppressSdkOutput(async () => {
424
- const client = new AUNClient({ aun_path: aunPath, debug: false });
423
+ const client = await createAunClient({ aunPath });
425
424
  await client.auth.createAid({ aid });
426
425
  const authResult = await client.auth.authenticate({ aid });
427
426
  const at = authResult?.access_token || client._access_token;
@@ -564,7 +563,7 @@ function shuffle(arr) {
564
563
  export async function cmdNet(args) {
565
564
  const sub = args[0];
566
565
  const formatJson = args.includes('--format') && args.includes('json');
567
- if (sub === 'help' || sub === '--help' || sub === '-h') {
566
+ if (isHelpFlag(sub)) {
568
567
  console.log(`用法: evolclaw net check [<aid>] [--format json]
569
568
 
570
569
  检查 AUN 网络链路连通性(10 步逐层诊断)。
@@ -410,8 +410,9 @@ export class CommandHandler {
410
410
  return { session };
411
411
  }
412
412
  const ct = chatType === 'group' ? 'group' : chatType === 'private' ? 'private' : undefined;
413
+ const channelType = this.resolveChannelType(channel);
413
414
  const session = await this.sessionManager.getActiveSession(channel, channelId)
414
- ?? await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, ct);
415
+ ?? await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, ct, undefined, undefined, channelType);
415
416
  // 如果 session 已存在但 chatType 跟传入的不一致,更新
416
417
  if (ct && session.chatType !== ct) {
417
418
  await this.sessionManager.updateSession(session.id, { chatType: ct });
@@ -2185,16 +2186,14 @@ export class CommandHandler {
2185
2186
  // 尝试获取活跃会话(话题时直接查找话题 session)
2186
2187
  let session;
2187
2188
  if (threadId) {
2188
- session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId);
2189
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), threadId, undefined, undefined, undefined, chatType, undefined, undefined, this.resolveChannelType(channel));
2189
2190
  }
2190
2191
  else {
2191
2192
  session = await this.sessionManager.getActiveSession(channel, channelId);
2192
2193
  }
2193
- // 对于需要会话的命令,如果没有会话则使用默认项目创建临时会话
2194
- if (!session && (normalizedContent.startsWith('/new') ||
2195
- normalizedContent === '/pwd' ||
2196
- normalizedContent === '/status')) {
2197
- session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel));
2194
+ // 如果没有会话,自动创建(所有后续命令都需要 session)
2195
+ if (!session) {
2196
+ session = await this.sessionManager.getOrCreateSession(channel, channelId, this.getEffectiveDefaultPath(channel), undefined, undefined, undefined, undefined, chatType, undefined, undefined, this.resolveChannelType(channel));
2198
2197
  }
2199
2198
  // /status 命令:显示会话状态
2200
2199
  if (normalizedContent === '/status') {
@@ -95,10 +95,11 @@ export class MessageProcessor {
95
95
  return [...this.agentMap.keys()];
96
96
  }
97
97
  /** 判断是否为后台会话(仅主会话参与判断,话题会话独立) */
98
- async isBackgroundSession(session, channel, channelId) {
98
+ isBackgroundSession(session, _channel, _channelId) {
99
99
  if (session.threadId)
100
100
  return false;
101
- const active = await this.sessionManager.getActiveSession(channel, channelId);
101
+ // 使用 session 自身的 channelType 精确定位 active.json,避免扫描误匹配
102
+ const active = this.sessionManager.getActiveSessionSync(session.channel, session.channelId, session.channelType, session.selfId);
102
103
  return active ? session.id !== active.id : false;
103
104
  }
104
105
  constructor(agentRunnerOrMap, sessionManager, globalSettings, messageCache, eventBus, commandHandler, primaryRunnerKey) {
@@ -245,7 +246,7 @@ export class MessageProcessor {
245
246
  monitor?.recordEvent(eventType || 'unknown', toolName);
246
247
  };
247
248
  // Cache background status to avoid async call inside setInterval
248
- const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
249
+ const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
249
250
  const timeoutPromise = new Promise((_, reject) => {
250
251
  rejectFn = reject;
251
252
  if (!monitorEnabled)
@@ -382,7 +383,7 @@ export class MessageProcessor {
382
383
  replyContext: taskReplyContext(),
383
384
  });
384
385
  try {
385
- const isBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
386
+ const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
386
387
  // 记录收到消息
387
388
  logger.message({
388
389
  msgId: messageId,
@@ -439,7 +440,7 @@ export class MessageProcessor {
439
440
  // (不支持 thought 的 channel 静默丢弃,避免降级为普通消息)
440
441
  if (isProactive && payload.kind === 'activity.batch' && !adapter.capabilities?.thought)
441
442
  return;
442
- const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
443
+ const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
443
444
  if (isCurrentlyBackground)
444
445
  return;
445
446
  const opts = {};
@@ -516,18 +517,13 @@ export class MessageProcessor {
516
517
  const selfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
517
518
  const selfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
518
519
  const peerName = message.peerName || session.metadata?.peerName;
519
- // 文件发送能力
520
- let currentCanSend = false;
521
- if (!isProactive) {
522
- currentCanSend = !!(channelInfo.adapter.capabilities?.file);
523
- }
524
520
  // 通道能力
525
521
  const capParts = [];
526
522
  if (options?.supportsImages)
527
523
  capParts.push('图片输入');
528
524
  if (channelInfo.adapter.capabilities?.image)
529
525
  capParts.push('图片输出');
530
- if (channelInfo.adapter.capabilities?.file)
526
+ if (!isProactive && channelInfo.adapter.capabilities?.file)
531
527
  capParts.push('文件发送');
532
528
  // Personal layer
533
529
  const owningAgent = this.agentRegistry?.resolveByChannel(channelKey);
@@ -543,6 +539,7 @@ export class MessageProcessor {
543
539
  ? `${currentChannelType}#${encodeURIComponent(peerIdRaw)}`
544
540
  : undefined;
545
541
  const normalizedBaseagent = normalizeBaseagent(agent.name);
542
+ const agentModel = (typeof agent.getModel === 'function') ? agent.getModel() : undefined;
546
543
  // Kit renderer: 组装上下文
547
544
  const kitCtx = {
548
545
  vars: {
@@ -557,19 +554,23 @@ export class MessageProcessor {
557
554
  peerKey,
558
555
  peerName: peerName || undefined,
559
556
  peerRole: session.identity?.role || 'unknown',
557
+ peerType: message.peerType || undefined,
560
558
  groupId: session.metadata?.groupId || undefined,
561
- scene: session.chatType ? (session.chatType === 'group' ? 'group' : 'private') : 'coding',
562
559
  chatType: session.chatType || null,
563
560
  channel: currentChannelType || null,
564
561
  venueUid: undefined,
562
+ capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
565
563
  project: path.basename(absoluteProjectPath),
564
+ sessionId: session.id,
566
565
  sessionName: session.name || undefined,
567
- chatmode: isProactive ? 'proactive' : 'interactive',
566
+ sessionCreatedAt: session.createdAt ? new Date(session.createdAt).toISOString() : undefined,
567
+ threadId: session.threadId || undefined,
568
+ chatMode: isProactive ? 'proactive' : 'interactive',
568
569
  readonly: session.metadata?.permissionMode === 'readonly',
569
- canSendFile: !isProactive && currentCanSend,
570
- capabilities: capParts.length > 0 ? capParts.join('、') : undefined,
571
570
  baseAgent: normalizedBaseagent.canonical,
572
571
  baseAgentName: normalizedBaseagent.displayName,
572
+ baseAgentModel: agentModel || undefined,
573
+ agentSessionId: session.agentSessionId || undefined,
573
574
  },
574
575
  sessionId: session.id,
575
576
  };
@@ -752,7 +753,7 @@ export class MessageProcessor {
752
753
  if (finalReplyText) {
753
754
  if (isProactive && !streamResult.hasReceivedText && /^Unknown skill:\s+\S+/i.test(finalReplyText.trim())) {
754
755
  // Proactive 模式 + SDK 本地兜底:直接发送绕过 silent renderer
755
- const isCurrentlyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
756
+ const isCurrentlyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
756
757
  if (!isCurrentlyBackground) {
757
758
  await adapter.send({ ...envelope, replyContext: capturedReplyContext }, { kind: 'result.text', text: finalReplyText, isFinal: true });
758
759
  logger.info(`[MessageProcessor] proactive SDK fallback replied task=${taskId} text="${finalReplyText.slice(0, 60)}"`);
@@ -868,7 +869,7 @@ export class MessageProcessor {
868
869
  // 写入消息记录(出方向)已下沉到 aun.ts:deliverTextEntry,
869
870
  // 所有 message.send 成功后统一写入 messages.jsonl,此处不再重复写入。
870
871
  }
871
- const isFinallyBackground = await this.isBackgroundSession(session, message.channel, message.channelId);
872
+ const isFinallyBackground = this.isBackgroundSession(session, message.channel, message.channelId);
872
873
  if (isFinallyBackground && session.sessionMode !== 'autonomous') {
873
874
  const projectName = path.basename(session.projectPath);
874
875
  const count = this.messageCache.getCount(session.id);
@@ -1083,7 +1084,7 @@ export class MessageProcessor {
1083
1084
  });
1084
1085
  continue;
1085
1086
  }
1086
- const isCurrentlyBackground = await this.isBackgroundSession(session, session.channel, session.channelId);
1087
+ const isCurrentlyBackground = this.isBackgroundSession(session, session.channel, session.channelId);
1087
1088
  // === 前台任务:正常处理所有事件 ===
1088
1089
  if (!isCurrentlyBackground) {
1089
1090
  // 流式文本
@@ -2,11 +2,11 @@
2
2
  * PeerIdentityCache - 对端身份缓存管理
3
3
  *
4
4
  * 职责:
5
- * 1. 从对端的 agent.md 确定身份(human / agent
6
- * 2. 缓存到关系层文件(30天时效)
5
+ * 1. 通过 agentmdSync 标准流程获取对端 agent.md(check fetch if changed
6
+ * 2. 仅在 agent.md 内容变化时重写 peer-identity.json
7
7
  * 3. 支持入站和出站消息的身份查询
8
8
  *
9
- * 信源:对端的 agent.md(通过 AUN SDK 下载并验签)
9
+ * 信源:对端的 agent.md(通过 AUN SDK checkAgentMd + fetchAgentMd)
10
10
  * 判定规则:type !== 'human' → agent
11
11
  * 缓存位置:$AGENT_DIR/relations/<channel>#<urlEncode(peerId)>/peer-identity.json
12
12
  */
@@ -14,6 +14,7 @@ import * as fs from 'fs';
14
14
  import * as path from 'path';
15
15
  import * as crypto from 'crypto';
16
16
  import { logger } from '../../utils/logger.js';
17
+ import { agentMdPath } from '../../paths.js';
17
18
  /**
18
19
  * 对端身份缓存管理器
19
20
  */
@@ -54,29 +55,26 @@ export class PeerIdentityCache {
54
55
  }
55
56
  /**
56
57
  * 从 agent.md 更新身份信息
57
- * @param agentMd 已验签的 agent.md 内容
58
58
  */
59
59
  static updateFromAgentMd(channel, peerId, agentDir, agentMd, verifiedAt) {
60
- // 解析 type 和 name
61
60
  const typeMatch = agentMd.match(/^type:\s*["']?(\w+)["']?/m);
62
61
  const nameMatch = agentMd.match(/^name:\s*["']?(.+?)["']?\s*$/m);
63
62
  const type = typeMatch?.[1] || 'unknown';
64
63
  const isAgent = type !== 'human';
65
64
  const name = nameMatch?.[1]?.trim();
66
- // 计算 hash
67
65
  const agentMdHash = 'sha256:' + crypto.createHash('sha256').update(agentMd, 'utf-8').digest('hex');
68
- // 构建身份信息
66
+ const now = Date.now();
69
67
  const identity = {
70
68
  aid: peerId,
71
69
  type,
72
70
  isAgent,
73
71
  name,
74
72
  agentMdHash,
73
+ agentMdUpdatedAt: now,
75
74
  verifiedAt,
76
- lastCheckedAt: Date.now(),
75
+ lastCheckedAt: now,
77
76
  source: 'agentmd',
78
77
  };
79
- // 写入文件
80
78
  const filePath = this.getFilePath(channel, peerId, agentDir);
81
79
  try {
82
80
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -88,6 +86,18 @@ export class PeerIdentityCache {
88
86
  }
89
87
  return identity;
90
88
  }
89
+ /**
90
+ * 仅更新 lastCheckedAt(内容未变时的轻量操作)
91
+ */
92
+ static touchLastChecked(channel, peerId, agentDir, cached) {
93
+ const updated = { ...cached, lastCheckedAt: Date.now() };
94
+ const filePath = this.getFilePath(channel, peerId, agentDir);
95
+ try {
96
+ fs.writeFileSync(filePath, JSON.stringify(updated, null, 2), 'utf-8');
97
+ }
98
+ catch { /* ignore */ }
99
+ return updated;
100
+ }
91
101
  /**
92
102
  * 标记为 unknown(验签失败或无 agent.md)
93
103
  */
@@ -95,8 +105,9 @@ export class PeerIdentityCache {
95
105
  const identity = {
96
106
  aid: peerId,
97
107
  type: 'unknown',
98
- isAgent: true, // 验签失败 → 当做 agent(安全策略)
108
+ isAgent: true,
99
109
  agentMdHash: '',
110
+ agentMdUpdatedAt: 0,
100
111
  verifiedAt: 0,
101
112
  lastCheckedAt: Date.now(),
102
113
  source: 'unknown',
@@ -113,17 +124,17 @@ export class PeerIdentityCache {
113
124
  return identity;
114
125
  }
115
126
  /**
116
- * 完整流程:检查缓存需要刷新则下载 agent.md 更新缓存
127
+ * 完整流程:缓存检查agentmdSync(check+fetch)→ changed 决定是否重写
117
128
  *
118
129
  * @param channel 渠道类型(如 'aun')
119
130
  * @param peerId 对端 ID(AUN 是 AID)
120
131
  * @param agentDir agent 数据根目录
121
- * @param aunClient AUN SDK client(需要有 fetchAgentMd 方法)
132
+ * @param aunClient AUN SDK client(需要有 checkAgentMd / fetchAgentMd 方法)
122
133
  * @param forceRefresh 强制刷新(忽略缓存时效)
123
134
  * @returns PeerIdentity
124
135
  */
125
136
  static async resolve(channel, peerId, agentDir, aunClient, forceRefresh = false) {
126
- // 1. 检查缓存
137
+ // 1. 缓存检查
127
138
  if (!forceRefresh && !this.needsRefresh(channel, peerId, agentDir)) {
128
139
  const cached = this.get(channel, peerId, agentDir);
129
140
  if (cached) {
@@ -131,17 +142,49 @@ export class PeerIdentityCache {
131
142
  return cached;
132
143
  }
133
144
  }
134
- // 2. 下载并验签 agent.md(SDK 自动验签)
145
+ // 2. 标准流程:checkAgentMd fetchAgentMd(如果有变化)
135
146
  try {
136
- logger.debug(`[PeerIdentityCache] Fetching agent.md: ${channel}#${peerId}`);
137
- const result = await aunClient.fetchAgentMd(peerId);
138
- const agentMd = result.content;
139
- // 3. 更新缓存
140
- return this.updateFromAgentMd(channel, peerId, agentDir, agentMd, Date.now());
147
+ logger.debug(`[PeerIdentityCache] Syncing agent.md: ${channel}#${peerId}`);
148
+ const state = await aunClient.checkAgentMd(peerId, 30);
149
+ let content;
150
+ if (state.in_sync && state.local_found) {
151
+ // 本地已是最新,读本地文件
152
+ const localPath = agentMdPath(peerId);
153
+ try {
154
+ content = fs.readFileSync(localPath, 'utf-8');
155
+ }
156
+ catch { /* ignore */ }
157
+ }
158
+ if (!content) {
159
+ // 需要下载(不同步或本地不存在)
160
+ const info = await aunClient.fetchAgentMd(peerId);
161
+ content = info.content;
162
+ }
163
+ // 3. 比较 hash,仅在变化时重写 peer-identity.json
164
+ const newHash = 'sha256:' + crypto.createHash('sha256').update(content, 'utf-8').digest('hex');
165
+ const cached = this.get(channel, peerId, agentDir);
166
+ if (cached && cached.agentMdHash === newHash && cached.source === 'agentmd') {
167
+ return this.touchLastChecked(channel, peerId, agentDir, cached);
168
+ }
169
+ return this.updateFromAgentMd(channel, peerId, agentDir, content, Date.now());
141
170
  }
142
171
  catch (err) {
143
- // 验签失败或下载失败 标记为 unknown,当做 agent
144
- logger.warn(`[PeerIdentityCache] Failed to fetch agent.md: ${channel}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
172
+ // 4. 网络失败,fallback 本地文件
173
+ const localPath = agentMdPath(peerId);
174
+ try {
175
+ if (fs.existsSync(localPath)) {
176
+ const localContent = fs.readFileSync(localPath, 'utf-8');
177
+ logger.info(`[PeerIdentityCache] Network failed, using local agent.md for ${peerId}`);
178
+ const localHash = 'sha256:' + crypto.createHash('sha256').update(localContent, 'utf-8').digest('hex');
179
+ const cached = this.get(channel, peerId, agentDir);
180
+ if (cached && cached.agentMdHash === localHash && cached.source === 'agentmd') {
181
+ return this.touchLastChecked(channel, peerId, agentDir, cached);
182
+ }
183
+ return this.updateFromAgentMd(channel, peerId, agentDir, localContent, cached?.verifiedAt ?? 0);
184
+ }
185
+ }
186
+ catch { /* ignore fs errors */ }
187
+ logger.warn(`[PeerIdentityCache] Failed to resolve: ${channel}#${peerId} err=${err instanceof Error ? err.message : String(err)}`);
145
188
  return this.markUnknown(channel, peerId, agentDir);
146
189
  }
147
190
  }
@@ -36,8 +36,8 @@ export class SessionManager {
36
36
  // 来源2:群聊强制 proactive
37
37
  if (ct === 'group')
38
38
  return 'proactive';
39
- // 来源3:非 human 对端(ai/bot)强制 proactive,无视 agent 的默认 chatmode 配置
40
- if (peerType && peerType !== 'human' && peerType !== 'unknown')
39
+ // 来源3:非 human 对端强制 proactive,无视 agent 的默认 chatmode 配置
40
+ if (peerType && peerType !== 'human')
41
41
  return 'proactive';
42
42
  // 来源1:agent 配置默认值
43
43
  const resolved = this.sessionModeResolver?.(channel, ct, peerType);
@@ -850,6 +850,10 @@ export class SessionManager {
850
850
  async getActiveSession(channel, channelId) {
851
851
  return this.readActive(channel, channelId);
852
852
  }
853
+ /** 同步版 getActiveSession,支持精确路径定位(避免扫描) */
854
+ getActiveSessionSync(channel, channelId, channelType, selfId) {
855
+ return this.readActive(channel, channelId, channelType, selfId);
856
+ }
853
857
  async getThreadSession(channel, channelId, threadId) {
854
858
  let chatDir;
855
859
  try {
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { atomicWriteJson, appendJsonl } from '../session/session-fs-store.js';
4
+ import { formatChannelKey } from '../channel-loader.js';
4
5
  import { logger } from '../../utils/logger.js';
5
6
  export class TriggerManager {
6
7
  aid;
@@ -22,6 +23,26 @@ export class TriggerManager {
22
23
  const raw = fs.readFileSync(this.triggersPath, 'utf8');
23
24
  const data = JSON.parse(raw);
24
25
  this.triggers = new Map(Object.entries(data.triggers ?? {}));
26
+ // Migrate old channel key format: "selfPeerId#type#name" → "type#selfPeerId#name"
27
+ let migrated = false;
28
+ for (const trigger of this.triggers.values()) {
29
+ const fixed = this.migrateChannelKey(trigger.targetChannel);
30
+ if (fixed !== trigger.targetChannel) {
31
+ trigger.targetChannel = fixed;
32
+ migrated = true;
33
+ }
34
+ if (trigger.createdByChannel) {
35
+ const fixedBy = this.migrateChannelKey(trigger.createdByChannel);
36
+ if (fixedBy !== trigger.createdByChannel) {
37
+ trigger.createdByChannel = fixedBy;
38
+ migrated = true;
39
+ }
40
+ }
41
+ }
42
+ if (migrated) {
43
+ logger.info(`[TriggerManager] Migrated old channel key format`);
44
+ this.save();
45
+ }
25
46
  return [...this.triggers.values()];
26
47
  }
27
48
  catch (e) {
@@ -30,6 +51,22 @@ export class TriggerManager {
30
51
  return [];
31
52
  }
32
53
  }
54
+ /**
55
+ * Detect and fix old format "selfPeerId#type#name" → current "type#selfPeerId#name".
56
+ * Old format: first segment contains '.' (AID like "evolai.agentid.pub").
57
+ */
58
+ migrateChannelKey(key) {
59
+ if (!key || !key.includes('#'))
60
+ return key;
61
+ const parts = key.split('#');
62
+ if (parts.length !== 3)
63
+ return key;
64
+ const [first, second, third] = parts;
65
+ if (first.includes('.') && !second.includes('.')) {
66
+ return formatChannelKey({ type: second, selfPeerId: first, name: third });
67
+ }
68
+ return key;
69
+ }
33
70
  save() {
34
71
  const data = {
35
72
  version: 1,
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { ensureDataDirs, resolvePaths, getPackageRoot, agentMdPath } from './pat
5
5
  import { resolveAnthropicConfig } from './agents/resolve.js';
6
6
  import { loadDefaults, autoMigrateIfNeeded, migrateIdentitiesIfNeeded } from './config-store.js';
7
7
  import { CONFIG_SCHEMA_VERSION } from './types.js';
8
+ import dotenv from 'dotenv';
8
9
  import { SessionManager } from './core/session/session-manager.js';
9
10
  import { ClaudeAgentPlugin } from './agents/claude-runner.js';
10
11
  import { CodexAgentPlugin } from './agents/codex-runner.js';
@@ -183,6 +184,8 @@ async function main() {
183
184
  logger.info(`EvolClaw v${readEvolclawVersion()} starting... (fastaun v${readFastaunVersion()})`);
184
185
  // 确保数据目录存在
185
186
  ensureDataDirs();
187
+ // 加载 $EVOLCLAW_HOME/.env 到 process.env(不覆盖已存在的变量)
188
+ dotenv.config({ path: path.join(resolvePaths().root, '.env') });
186
189
  // ── 单实例保护(pre-check + post-write self-check)──
187
190
  // pre-check:发现已有活 main 直接退出,避免起任何副作用
188
191
  {
@@ -313,7 +316,7 @@ async function main() {
313
316
  // 优先级:群聊 > nothuman > private
314
317
  if (chatType === 'group')
315
318
  return cm.group;
316
- if (peerType && peerType !== 'human' && peerType !== 'unknown')
319
+ if (peerType && peerType !== 'human')
317
320
  return cm.nothuman;
318
321
  return cm.private;
319
322
  });
package/dist/paths.js CHANGED
@@ -46,6 +46,14 @@ export function resolvePaths() {
46
46
  };
47
47
  }
48
48
  // ── AID 路径(agent.md 存放在 $EVOLCLAW_HOME/AIDs/<aid>/)──
49
+ /**
50
+ * AUN SDK 数据根路径(密钥/证书/AgentMDs 等)。
51
+ * 与 evolclaw 数据共用 $EVOLCLAW_HOME 根,避免散落到 ~/.aun。
52
+ * 启动时 SDK 通过 aun_path 选项指向这里;agent.md 子目录通过 setAgentMdPath(aidsDir()) 单独设置。
53
+ */
54
+ export function aunPath() {
55
+ return resolveRoot();
56
+ }
49
57
  export function aidsDir() {
50
58
  return path.join(resolveRoot(), 'AIDs');
51
59
  }
@@ -109,30 +117,93 @@ export function ensureDataDirs() {
109
117
  fs.mkdirSync(p.eckDir, { recursive: true });
110
118
  fs.mkdirSync(eckDebugDir(), { recursive: true });
111
119
  fs.mkdirSync(aidsDir(), { recursive: true });
112
- migrateAgentMdFromAun();
120
+ seedConfigTemplates();
121
+ migrateFromAun();
113
122
  }
123
+ const MIGRATION_MARKER = '.migrated-from-aun';
114
124
  /**
115
- * One-time migration: copy agent.md from ~/.aun/AIDs/<aid>/ to $EVOLCLAW_HOME/AIDs/<aid>/
116
- * if the new location doesn't have it yet.
125
+ * 一次性迁移:把 ~/.aun 下的 SDK 数据搬到 $EVOLCLAW_HOME 下。
126
+ * 通过标记文件判断是否已迁移,完成后写入标记,后续版本可删除此函数。
117
127
  */
118
- function migrateAgentMdFromAun() {
119
- const aunAidsDir = path.join(os.homedir(), '.aun', 'AIDs');
120
- const ecAids = aidsDir();
121
- if (!fs.existsSync(aunAidsDir) || aunAidsDir === ecAids)
128
+ function migrateFromAun() {
129
+ const newRoot = resolveRoot();
130
+ const markerPath = path.join(newRoot, MIGRATION_MARKER);
131
+ if (fs.existsSync(markerPath))
132
+ return;
133
+ const oldRoot = path.join(os.homedir(), '.aun');
134
+ if (!fs.existsSync(oldRoot) || oldRoot === newRoot) {
135
+ try {
136
+ fs.writeFileSync(markerPath, new Date().toISOString(), 'utf-8');
137
+ }
138
+ catch { }
122
139
  return;
140
+ }
141
+ const copyFileIfMissing = (src, dst) => {
142
+ if (!fs.existsSync(src) || fs.existsSync(dst))
143
+ return;
144
+ fs.mkdirSync(path.dirname(dst), { recursive: true });
145
+ fs.copyFileSync(src, dst);
146
+ };
147
+ const copyDirIfMissing = (src, dst) => {
148
+ if (!fs.existsSync(src))
149
+ return;
150
+ if (!fs.statSync(src).isDirectory())
151
+ return;
152
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
153
+ const s = path.join(src, entry.name);
154
+ const d = path.join(dst, entry.name);
155
+ if (entry.isDirectory()) {
156
+ copyDirIfMissing(s, d);
157
+ }
158
+ else if (entry.isFile()) {
159
+ copyFileIfMissing(s, d);
160
+ }
161
+ }
162
+ };
123
163
  try {
124
- for (const entry of fs.readdirSync(aunAidsDir, { withFileTypes: true })) {
125
- if (!entry.isDirectory())
126
- continue;
127
- const oldMd = path.join(aunAidsDir, entry.name, 'agent.md');
128
- const newMd = path.join(ecAids, entry.name, 'agent.md');
129
- if (fs.existsSync(oldMd) && !fs.existsSync(newMd)) {
130
- fs.mkdirSync(path.join(ecAids, entry.name), { recursive: true });
131
- fs.copyFileSync(oldMd, newMd);
164
+ const oldAids = path.join(oldRoot, 'AIDs');
165
+ const newAids = aidsDir();
166
+ if (fs.existsSync(oldAids)) {
167
+ for (const entry of fs.readdirSync(oldAids, { withFileTypes: true })) {
168
+ if (!entry.isDirectory())
169
+ continue;
170
+ copyDirIfMissing(path.join(oldAids, entry.name), path.join(newAids, entry.name));
132
171
  }
133
172
  }
173
+ copyDirIfMissing(path.join(oldRoot, 'CA'), path.join(newRoot, 'CA'));
174
+ for (const f of ['.seed', '.device_id']) {
175
+ copyFileIfMissing(path.join(oldRoot, f), path.join(newRoot, f));
176
+ }
177
+ }
178
+ catch { /* best-effort */ }
179
+ try {
180
+ fs.writeFileSync(markerPath, new Date().toISOString(), 'utf-8');
181
+ }
182
+ catch { }
183
+ }
184
+ /**
185
+ * 首次运行时从 assets/ 模板拷贝 config.json 和 .env 到 $EVOLCLAW_HOME。
186
+ * 已存在则跳过,不覆盖用户修改。
187
+ */
188
+ function seedConfigTemplates() {
189
+ const root = resolveRoot();
190
+ const assetsDir = path.join(getPackageRoot(), 'assets');
191
+ const templates = [
192
+ { src: 'config.json.template', dst: 'config.json' },
193
+ { src: '.env.template', dst: '.env' },
194
+ ];
195
+ for (const { src, dst } of templates) {
196
+ const dstPath = path.join(root, dst);
197
+ if (fs.existsSync(dstPath))
198
+ continue;
199
+ const srcPath = path.join(assetsDir, src);
200
+ if (!fs.existsSync(srcPath))
201
+ continue;
202
+ try {
203
+ fs.copyFileSync(srcPath, dstPath);
204
+ }
205
+ catch { /* best-effort */ }
134
206
  }
135
- catch { /* best-effort migration */ }
136
207
  }
137
208
  // ── kits 路径(始终从包内读取,不复制到 EVOLCLAW_HOME)──
138
209
  export function kitsDir() {
@@ -84,19 +84,26 @@ export function getLocalVersion() {
84
84
  }
85
85
  /**
86
86
  * 查询 npm registry 上指定包的最新版本。
87
- * 超时 15 秒,失败返回 null
87
+ * 使用 HTTP fetch 直接查 registry API,不依赖 npm CLI
88
+ * 超时 10 秒,失败返回 null。
88
89
  */
89
- export function checkLatestVersion(pkg = 'evolclaw') {
90
- return new Promise((resolve) => {
91
- execFile('npm', ['view', pkg, 'version'], { timeout: 15000 }, (err, stdout) => {
92
- if (err) {
93
- resolve(null);
94
- return;
95
- }
96
- const ver = stdout.trim();
97
- resolve(ver || null);
90
+ export async function checkLatestVersion(pkg = 'evolclaw') {
91
+ try {
92
+ const controller = new AbortController();
93
+ const timer = setTimeout(() => controller.abort(), 10000);
94
+ const res = await fetch(`https://registry.npmjs.org/${pkg}/latest`, {
95
+ signal: controller.signal,
96
+ headers: { 'Accept': 'application/json' },
98
97
  });
99
- });
98
+ clearTimeout(timer);
99
+ if (!res.ok)
100
+ return null;
101
+ const data = await res.json();
102
+ return data.version || null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
100
107
  }
101
108
  /**
102
109
  * 完整升级流程:检查 → 比较 → 安装(失败重试一次)