evolclaw 3.1.1 → 3.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ 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, agentDir as agentDirPath } from '../paths.js';
9
+ import { resolvePaths, getPackageRoot, agentMdPath as agentMdPathFn, agentDir as agentDirPath } from '../paths.js';
10
10
  import { saveToUploads, sanitizeFileName } from '../utils/media-cache.js';
11
11
  import { appendAidEvent } from '../utils/instance-registry.js';
12
12
  import { appendMessageLog, buildOutboundEntry } from '../core/message/message-log.js';
@@ -408,10 +408,10 @@ export class AUNChannel {
408
408
  }
409
409
  return out;
410
410
  }
411
- buildGroupReplyContext(taskId, senderAid, encrypted, messageId, chatmode) {
411
+ buildGroupReplyContext(threadId, senderAid, encrypted, messageId, chatmode) {
412
412
  const replyContext = { metadata: { encrypted, chatmode } };
413
- if (taskId)
414
- replyContext.threadId = taskId;
413
+ if (threadId)
414
+ replyContext.threadId = threadId;
415
415
  replyContext.peerId = senderAid;
416
416
  if (messageId)
417
417
  replyContext.replyToMessageId = messageId;
@@ -731,8 +731,8 @@ export class AUNChannel {
731
731
  logger.info(`${this.logPrefix()} No owner configured, skipping welcome message (will retry after auto-bind)`);
732
732
  return;
733
733
  }
734
- const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
735
- const existingAgentMd = fs.existsSync(agentMdPath) ? fs.readFileSync(agentMdPath, 'utf-8') : '';
734
+ const agentMdLocalPath = agentMdPathFn(aidName);
735
+ const existingAgentMd = fs.existsSync(agentMdLocalPath) ? fs.readFileSync(agentMdLocalPath, 'utf-8') : '';
736
736
  const existingFrontmatterMatch = existingAgentMd.match(/^---\n([\s\S]*?)\n---/);
737
737
  const existingFrontmatter = existingFrontmatterMatch?.[1] ?? '';
738
738
  // Fetch owner's agent.md to derive name and validate type
@@ -778,8 +778,8 @@ tags:
778
778
  EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
779
779
  `;
780
780
  // Write locally
781
- fs.mkdirSync(path.dirname(agentMdPath), { recursive: true });
782
- fs.writeFileSync(agentMdPath, newAgentMd, 'utf-8');
781
+ fs.mkdirSync(path.dirname(agentMdLocalPath), { recursive: true });
782
+ fs.writeFileSync(agentMdLocalPath, newAgentMd, 'utf-8');
783
783
  logger.info(`${this.logPrefix()} Updated agent.md for ${aidName}`);
784
784
  // Publish to AUN network via auth.uploadAgentMd
785
785
  try {
@@ -908,7 +908,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
908
908
  const fromAid = msg.from ?? '';
909
909
  const payload = msg.payload ?? '';
910
910
  const text = this.extractTextPayload(payload, fromAid);
911
- const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
911
+ const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
912
912
  const messageId = msg.message_id ?? '';
913
913
  const seq = msg.seq;
914
914
  // 回声过滤:自己发出的消息会被 gateway fanout 回来,
@@ -953,7 +953,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
953
953
  // device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
954
954
  const chatId = fromAid;
955
955
  // 解析对端身份(30天缓存)
956
- const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, this.agentDir, this.client, false);
956
+ const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
957
+ const peerIdentity = await PeerIdentityCache.resolve('aun', fromAid, selfAgentDir, this.client, false);
957
958
  const shortAid = this.getShortAid(fromAid);
958
959
  const displayName = peerIdentity.name || shortAid;
959
960
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
@@ -986,8 +987,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
986
987
  const isSystemP2P = p2pPayloadType === 'event';
987
988
  this.aidStatsCollector?.recordInbound(this.config.aid, fromAid, Buffer.byteLength(finalText, 'utf-8'), finalText, isSystemP2P, msgEncrypted, msgChatmode);
988
989
  const replyContext = { metadata: { encrypted: msgEncrypted, chatmode: msgChatmode } };
989
- if (taskId)
990
- replyContext.threadId = taskId;
990
+ if (threadId)
991
+ replyContext.threadId = threadId;
991
992
  this.dispatchMessage({
992
993
  channelId: chatId,
993
994
  userId: fromAid,
@@ -995,7 +996,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
995
996
  chatType: 'private',
996
997
  messageId,
997
998
  seq,
998
- taskId,
999
+ threadId,
999
1000
  mentions,
1000
1001
  peerName: displayName || undefined,
1001
1002
  peerType: peerIdentity.type,
@@ -1010,7 +1011,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1010
1011
  const senderAid = msg.sender_aid ?? '';
1011
1012
  const payload = msg.payload ?? '';
1012
1013
  const text = this.extractTextPayload(payload, groupId, senderAid);
1013
- const taskId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1014
+ const threadId = typeof payload === 'object' && payload !== null ? payload.thread_id : undefined;
1014
1015
  const messageId = msg.message_id ?? '';
1015
1016
  const seq = msg.seq;
1016
1017
  // Extract structured mentions from payload (e.g. payload.mentions: ["evolai.agentid.pub"])
@@ -1168,7 +1169,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1168
1169
  finalText = parts.join('\n\n');
1169
1170
  }
1170
1171
  }
1171
- const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, this.agentDir, this.client, false);
1172
+ const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
1173
+ const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.client, false);
1172
1174
  const shortAid = this.getShortAid(senderAid);
1173
1175
  const displayName = peerIdentity.name || shortAid;
1174
1176
  // 详细 dispatch 决策日志:记录消息为何被路由到 agent
@@ -1200,9 +1202,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1200
1202
  chatType: 'group',
1201
1203
  messageId,
1202
1204
  seq,
1203
- taskId,
1205
+ threadId,
1204
1206
  mentions,
1205
- replyContext: this.buildGroupReplyContext(taskId, senderAid, msgEncrypted, messageId, msgChatmode),
1207
+ replyContext: this.buildGroupReplyContext(threadId, senderAid, msgEncrypted, messageId, msgChatmode),
1206
1208
  });
1207
1209
  }
1208
1210
  dispatchMessage(event) {
@@ -1252,8 +1254,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1252
1254
  // Use caller-supplied replyContext (group path builds mentionUserIds);
1253
1255
  // fall back to simple threadId-only context for private messages
1254
1256
  let replyContext = event.replyContext;
1255
- if (!replyContext && event.taskId) {
1256
- replyContext = { threadId: event.taskId };
1257
+ if (!replyContext && event.threadId) {
1258
+ replyContext = { threadId: event.threadId };
1257
1259
  }
1258
1260
  this.messageHandler({
1259
1261
  channelId: event.channelId || '',
@@ -1266,7 +1268,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1266
1268
  peerName: event.peerName,
1267
1269
  peerType: event.peerType,
1268
1270
  messageId: event.messageId,
1269
- threadId: event.taskId,
1271
+ threadId: event.threadId,
1270
1272
  mentions: mentionObjects,
1271
1273
  replyContext,
1272
1274
  }).catch(err => {
@@ -1707,6 +1709,8 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1707
1709
  const channelId = entry.channelId;
1708
1710
  const finalText = entry.text;
1709
1711
  const context = entry.context;
1712
+ // 从 context.metadata.source 读取 source,默认为 'daemon'
1713
+ const source = context?.metadata?.source ?? 'daemon';
1710
1714
  const payload = { type: 'text', text: finalText };
1711
1715
  if (this.isGroupId(channelId)) {
1712
1716
  const extracted = this.extractMentionAidsFromText(finalText);
@@ -1720,7 +1724,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1720
1724
  if (context?.metadata?.chatmode)
1721
1725
  payload.chatmode = context.metadata.chatmode;
1722
1726
  // 诊断日志:记录 payload 构造结果(含 task_id / thread_id / chatmode)
1723
- logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} textLen=${finalText.length}`);
1727
+ logger.info(`${this.logPrefix()} deliverTextEntry: channelId=${channelId} thread_id=${payload.thread_id ?? 'none'} task_id=${payload.task_id ?? 'none'} chatmode=${payload.chatmode ?? 'none'} source=${source} textLen=${finalText.length}`);
1724
1728
  const isGroup = this.isGroupId(channelId);
1725
1729
  const targetAid = channelId;
1726
1730
  const encryptTarget = isGroup ? channelId : targetAid;
@@ -1746,7 +1750,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1746
1750
  logger.info(`${this.logPrefix()} group.send ok: group=${channelId} mid=${mid} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1747
1751
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: channelId, msgId: mid, kind: 'text', len: finalText.length, groupId: channelId });
1748
1752
  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);
1753
+ this.appendOutboundJsonl(channelId, finalText, mid, encrypt, context, true, 'text', source);
1750
1754
  }
1751
1755
  }
1752
1756
  else {
@@ -1759,7 +1763,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1759
1763
  logger.info(`${this.logPrefix()} message.send ok: to=${this.peerLabel(targetAid)} mid=${result.message_id} encrypt=${encrypt} text=${finalText.slice(0, 60)}`);
1760
1764
  appendAidEvent({ ts: Date.now(), iso: new Date().toISOString(), event: 'message_out', aid: this.config.aid, to: targetAid, msgId: result.message_id, kind: 'text', len: finalText.length });
1761
1765
  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);
1766
+ this.appendOutboundJsonl(targetAid, finalText, result.message_id, encrypt, context, false, 'text', source);
1763
1767
  }
1764
1768
  }
1765
1769
  return true;
@@ -1802,7 +1806,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1802
1806
  }
1803
1807
  }
1804
1808
  /** 出站消息写入 messages.jsonl(message.send/group.send/thought.put 成功后调用) */
1805
- appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text') {
1809
+ appendOutboundJsonl(channelId, text, msgId, encrypt, context, isGroup, msgType = 'text', source = 'daemon') {
1806
1810
  try {
1807
1811
  const sessionsDir = resolvePaths().sessionsDir;
1808
1812
  const selfId = this.config.aid;
@@ -1822,6 +1826,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1822
1826
  encrypt,
1823
1827
  chatmode,
1824
1828
  msgType,
1829
+ source,
1825
1830
  }));
1826
1831
  }
1827
1832
  catch (e) {
@@ -1854,12 +1859,24 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1854
1859
  try {
1855
1860
  const itemCount = Array.isArray(payload?.items) ? payload.items.length : 0;
1856
1861
  const stage = payload?.stage ?? `items=${itemCount}`;
1857
- // 提取 thought 文本(取最后一项的 text content 字段)
1862
+ // 提取 thought 文本(只对 kind=text item 写 jsonl,过滤 tool_use/tool_result 等结构化项)
1858
1863
  const items = payload?.items;
1859
1864
  let thoughtText;
1860
1865
  if (Array.isArray(items) && items.length > 0) {
1861
1866
  const lastItem = items[items.length - 1];
1862
- thoughtText = lastItem?.text || lastItem?.content || (typeof lastItem === 'string' ? lastItem : undefined);
1867
+ // 优先 text 字段(kind=text item),否则 content
1868
+ if (lastItem?.kind === 'text' && lastItem.text) {
1869
+ thoughtText = lastItem.text;
1870
+ }
1871
+ else if (lastItem?.text) {
1872
+ thoughtText = lastItem.text;
1873
+ }
1874
+ else if (lastItem?.content) {
1875
+ thoughtText = lastItem.content;
1876
+ }
1877
+ else if (typeof lastItem === 'string') {
1878
+ thoughtText = lastItem;
1879
+ }
1863
1880
  }
1864
1881
  if (this.isGroupId(channelId)) {
1865
1882
  params.group_id = targetId;
@@ -1867,7 +1884,10 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1867
1884
  const tid = putRes?.thought_id;
1868
1885
  logger.info(`${this.logPrefix()} thought.put ok group=${targetId} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1869
1886
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
1870
- // thought jsonl 写入已改为按 LLM 调用次数统计(在 complete 事件处写入),此处不再写
1887
+ // 文本类 thought 写入 jsonl(只对有 text item,过滤 tool 等结构化项)
1888
+ if (thoughtText) {
1889
+ this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, true, 'thought', 'daemon');
1890
+ }
1871
1891
  }
1872
1892
  else {
1873
1893
  params.to = targetId;
@@ -1875,7 +1895,9 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
1875
1895
  const tid = putRes?.thought_id;
1876
1896
  logger.info(`${this.logPrefix()} thought.put ok p2p=${this.peerLabel(targetId)} task=${taskId} stage=${stage} encrypt=${encrypt} tid=${tid ?? '?'}`);
1877
1897
  this.eventBus?.publish?.({ type: 'message:thought-put', agentName: this.config.aid, channelId, taskId, text: thoughtText });
1878
- // thought jsonl 写入已改为按 LLM 调用次数统计(在 complete 事件处写入),此处不再写
1898
+ if (thoughtText) {
1899
+ this.appendOutboundJsonl(channelId, thoughtText, tid ?? `thought-${Date.now()}`, encrypt, context, false, 'thought', 'daemon');
1900
+ }
1879
1901
  }
1880
1902
  }
1881
1903
  catch (e) {
@@ -2303,13 +2325,13 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
2303
2325
  loadSelfName(aid) {
2304
2326
  try {
2305
2327
  const aidName = aid.startsWith('@') ? aid.slice(1) : aid;
2306
- const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aidName, 'agent.md');
2307
- if (!fs.existsSync(agentMdPath)) {
2328
+ const mdPath = agentMdPathFn(aidName);
2329
+ if (!fs.existsSync(mdPath)) {
2308
2330
  // 异步拉取,不阻塞连接流程
2309
2331
  this.fetchAndCacheSelfName(aidName);
2310
2332
  return undefined;
2311
2333
  }
2312
- const content = fs.readFileSync(agentMdPath, 'utf-8');
2334
+ const content = fs.readFileSync(mdPath, 'utf-8');
2313
2335
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
2314
2336
  if (!fmMatch)
2315
2337
  return undefined;
@@ -2420,6 +2442,7 @@ export class AUNChannelPlugin {
2420
2442
  });
2421
2443
  const adapter = {
2422
2444
  channelName: inst.name,
2445
+ channelKey: inst.name, // channelName 实际上就是 channelKey
2423
2446
  capabilities: { file: true, image: true, interaction: true, markdown: true, thought: true, status: true },
2424
2447
  send: async (envelope, payload) => {
2425
2448
  const ctx = envelope.replyContext;
@@ -2464,10 +2487,8 @@ export class AUNChannelPlugin {
2464
2487
  await channel.sendThought(channelId, envelope.taskId, aunPayload, ctx);
2465
2488
  }
2466
2489
  else {
2467
- await Promise.all([
2468
- channel.sendThought(channelId, envelope.taskId, aunPayload, ctx),
2469
- channel.sendStructured(channelId, aunPayload, ctx),
2470
- ]);
2490
+ // interactive 模式不发 thought.put,只写入消息历史
2491
+ await channel.sendStructured(channelId, aunPayload, ctx);
2471
2492
  }
2472
2493
  return;
2473
2494
  }
@@ -447,6 +447,7 @@ export class DingtalkChannelPlugin {
447
447
  });
448
448
  const adapter = {
449
449
  channelName: inst.name,
450
+ channelKey: inst.name,
450
451
  capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
451
452
  send: async (envelope, payload) => {
452
453
  const ctx = envelope.replyContext;
@@ -329,12 +329,11 @@ export class FeishuChannel {
329
329
  }
330
330
  logger.info(`[Feishu] CommandCard trigger: command=${value._command}, operator=${operatorId}`);
331
331
  if (this.messageHandler) {
332
- // Feishu chatId 前缀:oc_ = group chat,ou_ = private user open_id
333
- const chatType = typeof chatId === 'string' && chatId.startsWith('oc_') ? 'group' : 'private';
332
+ // 卡片回调不传 chatType——oc_ 前缀不区分群聊/单聊,
333
+ // ensureSession 从已有 session 中继承正确的 chatType
334
334
  await this.messageHandler({
335
335
  channelId: chatId,
336
336
  content: value._command,
337
- chatType,
338
337
  peerId: operatorId,
339
338
  messageId: `card-trigger-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
340
339
  source: 'card-trigger',
@@ -1327,6 +1326,7 @@ export class FeishuChannelPlugin {
1327
1326
  });
1328
1327
  const adapter = {
1329
1328
  channelName: inst.name,
1329
+ channelKey: inst.name,
1330
1330
  capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true },
1331
1331
  send: async (envelope, payload) => {
1332
1332
  const ctx = envelope.replyContext;
@@ -1424,7 +1424,8 @@ export class FeishuChannelPlugin {
1424
1424
  registerBridge(bridge, channelType) {
1425
1425
  bridge.register(adapter.channelName, (handler) => channel.onMessage(async ({ channelId: chatId, content, images, peerId, peerName, messageId, mentions, threadId, rootId, chatType, source }) => {
1426
1426
  await handler({
1427
- channel: adapter.channelName, channelType, channelId: chatId, content, images, chatType,
1427
+ channel: adapter.channelName, channelType, channelId: chatId, content, images,
1428
+ chatType: chatType || 'private',
1428
1429
  peerId: peerId || '', peerName, messageId, mentions, threadId,
1429
1430
  replyContext: threadId ? { replyToMessageId: rootId ?? threadId, replyInThread: true } : undefined,
1430
1431
  source,
@@ -334,6 +334,7 @@ export class QQBotChannelPlugin {
334
334
  });
335
335
  const adapter = {
336
336
  channelName: inst.name,
337
+ channelKey: inst.name,
337
338
  capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
338
339
  send: async (envelope, payload) => {
339
340
  const ctx = envelope.replyContext;
@@ -730,6 +730,7 @@ export class WechatChannelPlugin {
730
730
  });
731
731
  const adapter = {
732
732
  channelName: inst.name,
733
+ channelKey: inst.name,
733
734
  capabilities: { file: false, image: false, interaction: false, markdown: false, thought: false, status: true },
734
735
  send: async (envelope, payload) => {
735
736
  const channelId = envelope.channelId;
@@ -490,6 +490,7 @@ export class WecomChannelPlugin {
490
490
  });
491
491
  const adapter = {
492
492
  channelName: inst.name,
493
+ channelKey: inst.name,
493
494
  capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
494
495
  send: async (envelope, payload) => {
495
496
  const ctx = envelope.replyContext;
package/dist/cli/agent.js CHANGED
@@ -239,22 +239,30 @@ export async function agentShow(aid) {
239
239
  }
240
240
  export async function agentCreateInteractive(opts = {}) {
241
241
  const p = resolvePaths();
242
- const rl = readline.createInterface({
242
+ const ownRl = !opts.rl;
243
+ const rl = opts.rl ?? readline.createInterface({
243
244
  input: opts.stdin || process.stdin,
244
245
  output: opts.stdout || process.stdout,
245
246
  });
246
247
  const ask = (q) => new Promise(r => rl.question(q, r));
247
248
  try {
249
+ const { isValidAid, aidCreate } = await import('../aun/aid/index.js');
248
250
  const aidPrompt = opts.suggestedName
249
251
  ? `AID [${opts.suggestedName}]: `
250
252
  : 'AID (e.g. mybot.agentid.pub): ';
251
- const aidInput = (await ask(aidPrompt)).trim();
252
- const aid = aidInput || opts.suggestedName;
253
- if (!aid)
254
- return { ok: false, error: 'AID is required.' };
255
- const { isValidAid, aidCreate } = await import('../aun/aid/index.js');
256
- if (!isValidAid(aid)) {
257
- return { ok: false, error: `Invalid AID "${aid}": must be a valid multi-level domain (e.g. mybot.agentid.pub)` };
253
+ let aid = '';
254
+ while (!aid) {
255
+ const aidInput = (await ask(aidPrompt)).trim();
256
+ const candidate = aidInput || opts.suggestedName;
257
+ if (!candidate) {
258
+ console.log(' ⚠ AID is required.');
259
+ continue;
260
+ }
261
+ if (!isValidAid(candidate)) {
262
+ console.log(` ⚠ Invalid AID "${candidate}": must be a valid multi-level domain (e.g. mybot.agentid.pub)`);
263
+ continue;
264
+ }
265
+ aid = candidate;
258
266
  }
259
267
  const agentDirPath = path.join(p.agentsDir, aid);
260
268
  const configExists = fs.existsSync(path.join(agentDirPath, 'config.json'));
@@ -311,26 +319,48 @@ export async function agentCreateInteractive(opts = {}) {
311
319
  return { ok: false, error: `No baseagent CLI detected on PATH. Install one of: ${BASEAGENT_CANDIDATES.join('/')}` };
312
320
  }
313
321
  const defaultBa = pickDefaultBaseagent(available);
314
- let baseagent = null;
315
- while (baseagent === null) {
316
- const input = (await ask(`Baseagent (${available.join('/')}) [${defaultBa}]: `)).trim() || defaultBa;
317
- if (!BASEAGENT_CANDIDATES.includes(input)) {
318
- console.log(` Invalid choice. Options: ${BASEAGENT_CANDIDATES.join('/')}`);
319
- continue;
322
+ let baseagent;
323
+ if (available.length === 1) {
324
+ console.log(` Baseagent: ${defaultBa}`);
325
+ baseagent = defaultBa;
326
+ }
327
+ else {
328
+ let chosen = null;
329
+ while (chosen === null) {
330
+ const input = (await ask(`Baseagent (${available.join('/')}) [${defaultBa}]: `)).trim() || defaultBa;
331
+ if (!BASEAGENT_CANDIDATES.includes(input)) {
332
+ console.log(` Invalid choice. Options: ${BASEAGENT_CANDIDATES.join('/')}`);
333
+ continue;
334
+ }
335
+ if (!available.includes(input)) {
336
+ console.log(` ${input} not detected on PATH. Available: ${available.join('/')}`);
337
+ continue;
338
+ }
339
+ chosen = input;
340
+ }
341
+ baseagent = chosen;
342
+ }
343
+ // Owner
344
+ let owner;
345
+ while (true) {
346
+ const ownerInput = (await ask('Owner AID (leave empty for auto-bind on first message): ')).trim();
347
+ if (!ownerInput) {
348
+ owner = undefined;
349
+ break;
320
350
  }
321
- if (!available.includes(input)) {
322
- console.log(` ${input} not detected on PATH. Available: ${available.join('/')}`);
351
+ if (!isValidAid(ownerInput)) {
352
+ console.log(` ⚠ Invalid Owner AID "${ownerInput}": must be a valid multi-level domain (e.g. alice.agentid.pub)`);
323
353
  continue;
324
354
  }
325
- baseagent = input;
355
+ owner = ownerInput;
356
+ break;
326
357
  }
327
- // Owner
328
- const owner = (await ask('Owner AID (leave empty for auto-bind on first message): ')).trim() || undefined;
329
358
  // Name + description for agent.md
330
359
  const defaultName = aid.split('.')[0];
331
360
  const agentName = (await ask(`Display name [${defaultName}]: `)).trim() || defaultName;
332
361
  const agentDescription = (await ask('Description (optional): ')).trim() || '';
333
- rl.close();
362
+ if (ownRl)
363
+ rl.close();
334
364
  const agentConfig = {
335
365
  $schema_version: CONFIG_SCHEMA_VERSION,
336
366
  aid,
@@ -359,31 +389,64 @@ export async function agentCreateInteractive(opts = {}) {
359
389
  const agentMdPath = path.join(aunPath, 'AIDs', aid, 'agent.md');
360
390
  fs.mkdirSync(path.dirname(agentMdPath), { recursive: true });
361
391
  fs.writeFileSync(agentMdPath, content, 'utf-8');
362
- try {
363
- await agentmdPut(content, { aid, aunPath });
364
- agentmdUploaded = true;
392
+ // Upload with retry (3 attempts, 2s delay between retries)
393
+ const MAX_ATTEMPTS = 3;
394
+ const RETRY_DELAY_MS = 2000;
395
+ let lastError;
396
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
397
+ try {
398
+ if (attempt > 1) {
399
+ process.stdout.write(` ↻ agent.md 上传重试 (${attempt}/${MAX_ATTEMPTS})...\n`);
400
+ await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
401
+ }
402
+ await agentmdPut(content, { aid, aunPath });
403
+ agentmdUploaded = true;
404
+ break;
405
+ }
406
+ catch (e) {
407
+ lastError = e;
408
+ }
365
409
  }
366
- catch (e) {
367
- console.warn(` ⚠ agent.md upload failed: ${e?.message || e}`);
410
+ if (!agentmdUploaded) {
411
+ console.warn(` ⚠ agent.md upload failed: ${lastError?.message || lastError}`);
368
412
  console.warn(` → Retry later with: evolclaw aid agentmd put ${aid}`);
369
413
  }
414
+ // Yield to allow the SDK WebSocket to fully close before IPC
415
+ await new Promise(r => setTimeout(r, 0));
370
416
  }
371
417
  catch (e) {
372
418
  console.warn(` ⚠ agent.md generation failed: ${e?.message || e}`);
373
419
  }
420
+ // Attempt hot-load via IPC (if daemon is running)
421
+ let hotLoaded = false;
422
+ let hotLoadError;
423
+ try {
424
+ const ipcResult = await ipcQuery(p.socket, { type: 'evolagent.load', aid });
425
+ if (ipcResult?.ok) {
426
+ hotLoaded = true;
427
+ }
428
+ else if (ipcResult) {
429
+ hotLoadError = ipcResult.error;
430
+ }
431
+ }
432
+ catch { /* daemon not running */ }
374
433
  return {
375
434
  ok: true,
376
435
  aid,
377
436
  configPath: toPosix(path.join(agentDirPath, 'config.json')),
378
437
  aidCreated,
379
438
  agentmdUploaded,
439
+ hotLoaded,
440
+ hotLoadError,
380
441
  };
381
442
  }
382
443
  finally {
383
- try {
384
- rl.close();
444
+ if (ownRl) {
445
+ try {
446
+ rl.close();
447
+ }
448
+ catch { /* ignore */ }
385
449
  }
386
- catch { /* ignore */ }
387
450
  }
388
451
  }
389
452
  export async function agentCreateNonInteractive(opts) {
@@ -482,24 +545,51 @@ export async function agentCreateNonInteractive(opts) {
482
545
  const agentMdPath = path.join(aunPath, 'AIDs', opts.aid, 'agent.md');
483
546
  fs.mkdirSync(path.dirname(agentMdPath), { recursive: true });
484
547
  fs.writeFileSync(agentMdPath, content, 'utf-8');
485
- try {
486
- await agentmdPut(content, { aid: opts.aid, aunPath });
487
- agentmdUploaded = true;
548
+ const MAX_ATTEMPTS = 3;
549
+ const RETRY_DELAY_MS = 2000;
550
+ let lastError;
551
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
552
+ try {
553
+ if (attempt > 1)
554
+ await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
555
+ await agentmdPut(content, { aid: opts.aid, aunPath });
556
+ agentmdUploaded = true;
557
+ break;
558
+ }
559
+ catch (e) {
560
+ lastError = e;
561
+ }
488
562
  }
489
- catch (e) {
490
- console.warn(`⚠ agent.md upload failed: ${e?.message || e}`);
563
+ if (!agentmdUploaded) {
564
+ console.warn(`⚠ agent.md upload failed: ${lastError?.message || lastError}`);
491
565
  console.warn(` Retry later with: evolclaw aid agentmd put ${opts.aid}`);
492
566
  }
567
+ await new Promise(r => setTimeout(r, 0));
493
568
  }
494
569
  catch (e) {
495
570
  console.warn(`⚠ agent.md generation failed: ${e?.message || e}`);
496
571
  }
572
+ // Attempt hot-load via IPC (if daemon is running)
573
+ let hotLoaded = false;
574
+ let hotLoadError;
575
+ try {
576
+ const ipcResult = await ipcQuery(p.socket, { type: 'evolagent.load', aid: opts.aid });
577
+ if (ipcResult?.ok) {
578
+ hotLoaded = true;
579
+ }
580
+ else if (ipcResult) {
581
+ hotLoadError = ipcResult.error;
582
+ }
583
+ }
584
+ catch { /* daemon not running */ }
497
585
  return {
498
586
  ok: true,
499
587
  aid: opts.aid,
500
588
  configPath: toPosix(path.join(agentDirPath, 'config.json')),
501
589
  aidCreated,
502
590
  agentmdUploaded,
591
+ hotLoaded,
592
+ hotLoadError,
503
593
  };
504
594
  }
505
595
  // ==================== agentSyncAids ====================
@@ -665,7 +755,12 @@ export async function agentSet(aid, key, rawValue) {
665
755
  }
666
756
  const value = parseJsonValue(rawValue);
667
757
  setNestedValue(config, key, value);
668
- saveAgent(config);
758
+ try {
759
+ saveAgent(config);
760
+ }
761
+ catch (e) {
762
+ return { ok: false, error: e?.message || String(e) };
763
+ }
669
764
  // Try hot-reload
670
765
  let reloaded = false;
671
766
  try {
@@ -733,7 +828,7 @@ export async function agentChannelUpsert(opts) {
733
828
  return {
734
829
  ok: true,
735
830
  aid: opts.aid,
736
- channelKey: `${opts.aid}#${opts.channel.type}#${opts.channel.name}`,
831
+ channelKey: `${opts.channel.type}#${encodeURIComponent(opts.aid)}#${opts.channel.name}`,
737
832
  reloaded,
738
833
  };
739
834
  }