evolclaw 2.7.3 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -316,11 +316,17 @@ export class MessageProcessor {
316
316
  });
317
317
  const imageInfo = message.images && message.images.length > 0 ? ` [${message.images.length} image(s)]` : '';
318
318
  const modeInfo = isBackground ? ' [\u540e\u53f0]' : '';
319
- logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}`);
320
- logger.info(`[MessageProcessor] session=${session.id} task=${taskId} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
319
+ const e2eeInfo = message.replyContext?.metadata?.encrypted != null ? ` encrypt=${message.replyContext.metadata.encrypted}` : '';
320
+ logger.info(`[${message.channel}] ${message.channelId}: ${message.content}${imageInfo}${modeInfo}${e2eeInfo}`);
321
+ // 构建 peer 标识(优先 peerName,退化到 peerId / channelId)
322
+ const peerName = session.metadata?.peerName ?? message.peerName;
323
+ const peerId = session.metadata?.peerId ?? message.peerId ?? message.channelId;
324
+ const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
325
+ const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
326
+ logger.info(`[MessageProcessor] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} sessionMode=${session.sessionMode} agentId=${session.agentId} msgChatType=${message.chatType ?? 'n/a'}`);
321
327
  // 记录开始处理
322
328
  this.eventBus.publish({ type: 'message:processing', sessionId: session.id });
323
- adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, this.getReplyContext(message));
329
+ adapter.sendProcessingStatus?.(message.channelId, 'start', session.id, taskId, taskReplyContext());
324
330
  logger.message({
325
331
  msgId: messageId,
326
332
  sessionId: session.id,
@@ -363,7 +369,7 @@ export class MessageProcessor {
363
369
  // Proactive 模式可观测:创建 ThoughtEmitter,将静默的流式事件转发为 thought
364
370
  // selector: context = { type: 'task', id: taskId }
365
371
  if (isProactive && adapter.putThought) {
366
- thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId, chatmode);
372
+ thoughtEmitter = new ThoughtEmitter(adapter, message.channelId, taskId, chatmode, this.getReplyContext(message));
367
373
  }
368
374
  // 调用 AgentRunner(含上下文过长自动 compact 重试)
369
375
  // 捕获当前消息的上下文(闭包),避免后续消息处理时串台
@@ -389,7 +395,10 @@ export class MessageProcessor {
389
395
  // 设置 per-session 权限模式(默认 bypass,所有角色统一)
390
396
  agent.setMode(session.metadata?.permissionMode ?? DEFAULT_PERMISSION_MODE);
391
397
  // 标记会话为处理中(实时持久化,重启后可恢复)
392
- this.sessionManager.markProcessing(session.id);
398
+ this.sessionManager.markProcessing(session.id, taskId);
399
+ if (message.replyContext?.metadata?.encrypted != null) {
400
+ this.sessionManager.setSessionEncrypt(session.id, !!(message.replyContext.metadata.encrypted));
401
+ }
393
402
  logger.info(`[MessageProcessor] session ${session.id} marked as processing task=${taskId}`);
394
403
  // 检查是否因新消息自动中断 — 包装 prompt 让 Agent 知道上下文
395
404
  const prevInterruptReason = this.interruptedSessions.get(session.id);
@@ -527,8 +536,13 @@ export class MessageProcessor {
527
536
  for (const match of fileMatches) {
528
537
  // 兼容旧格式 (1组) 和新格式 (2组)
529
538
  const hasChannelGroup = match.length >= 3;
530
- const targetSpec = hasChannelGroup ? (match[1] ?? undefined) : undefined;
531
- const filePath = (hasChannelGroup ? match[2] : match[1]).trim();
539
+ let targetSpec = hasChannelGroup ? (match[1] ?? undefined) : undefined;
540
+ let filePath = (hasChannelGroup ? match[2] : match[1]).trim();
541
+ // 白名单校验:targetSpec 必须是已注册通道,否则视为路径的一部分(如 Windows 盘符 C:)
542
+ if (targetSpec && !this.channels.has(targetSpec) && !this.channelTypeMap.has(targetSpec)) {
543
+ filePath = `${targetSpec}:${filePath}`;
544
+ targetSpec = undefined;
545
+ }
532
546
  if (this.isPlaceholderPath(filePath)) {
533
547
  logger.info(`[${adapter.channelName}] Skipped placeholder file marker: [SEND_FILE:${filePath}]`);
534
548
  continue;
@@ -635,7 +649,7 @@ export class MessageProcessor {
635
649
  const errorSummary = streamResult.errors?.join('; ') || '任务执行失败';
636
650
  const rawSubtype = streamResult.subtype || 'agent_error';
637
651
  const errorType = prefixErrorType(ERROR_PREFIX.AGENT, rawSubtype);
638
- adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, taskId, this.getReplyContext(message));
652
+ adapter.sendProcessingStatus?.(message.channelId, 'error', session.id, taskId, taskReplyContext());
639
653
  this.eventBus.publish({
640
654
  type: 'message:error',
641
655
  sessionId: session.id,
@@ -663,7 +677,7 @@ export class MessageProcessor {
663
677
  }
664
678
  else {
665
679
  // 真正的成功
666
- adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, taskId, this.getReplyContext(message));
680
+ adapter.sendProcessingStatus?.(message.channelId, interruptReason ? 'interrupted' : 'done', session.id, taskId, taskReplyContext());
667
681
  await this.sessionManager.recordSuccess(session.id);
668
682
  this.eventBus.publish({
669
683
  type: 'message:completed',
@@ -718,7 +732,7 @@ export class MessageProcessor {
718
732
  // 用户主动中断(新消息打断 或 /stop 命令)时静默,不发送中断/错误提示
719
733
  if (!isUserInterrupt) {
720
734
  try {
721
- adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, taskId, this.getReplyContext(message));
735
+ adapter.sendProcessingStatus?.(message.channelId, procStatus, session.id, taskId, taskReplyContext());
722
736
  }
723
737
  catch { }
724
738
  }
@@ -14,8 +14,9 @@ export class ThoughtEmitter {
14
14
  channelId;
15
15
  taskId;
16
16
  chatmode;
17
+ replyContext;
17
18
  hasEmittedText = false;
18
- constructor(adapter, channelId, taskId, chatmode = 'proactive') {
19
+ constructor(adapter, channelId, taskId, chatmode = 'proactive', replyContext) {
19
20
  if (!taskId) {
20
21
  throw new Error('[ThoughtEmitter] taskId is required at construction');
21
22
  }
@@ -23,6 +24,7 @@ export class ThoughtEmitter {
23
24
  this.channelId = channelId;
24
25
  this.taskId = taskId;
25
26
  this.chatmode = chatmode;
27
+ this.replyContext = replyContext;
26
28
  logger.info(`[ThoughtEmitter] created channel=${channelId} task=${taskId} chatmode=${chatmode}`);
27
29
  }
28
30
  async emit(event) {
@@ -45,7 +47,7 @@ export class ThoughtEmitter {
45
47
  payload.task_id = this.taskId;
46
48
  payload.chatmode = this.chatmode;
47
49
  try {
48
- await this.adapter.putThought(this.channelId, this.taskId, payload);
50
+ await this.adapter.putThought(this.channelId, this.taskId, payload, this.replyContext);
49
51
  }
50
52
  catch (err) {
51
53
  logger.debug(`[ThoughtEmitter] putThought failed: ${err.message}`);
@@ -13,6 +13,7 @@ export class SessionManager {
13
13
  adminResolver;
14
14
  sessionModeResolver;
15
15
  fileAdapters = new Map();
16
+ sessionEncryptState = new Map();
16
17
  constructor(dbPath = resolvePaths().db, eventBus, ownerResolver, adminResolver) {
17
18
  ensureDir(path.dirname(dbPath));
18
19
  this.db = new DatabaseSync(dbPath);
@@ -428,11 +429,21 @@ export class SessionManager {
428
429
  }
429
430
  /**
430
431
  * 标记会话为处理中(实时写 DB,crash 也能恢复)
432
+ * processing_state 格式: "timestamp:taskId"
431
433
  */
432
- markProcessing(sessionId) {
434
+ markProcessing(sessionId, taskId) {
433
435
  const now = Date.now();
436
+ const state = taskId ? `${now}:${taskId}` : String(now);
434
437
  this.db.prepare(`UPDATE sessions SET processing_state = ?, updated_at = ? WHERE id = ?`)
435
- .run(String(now), now, sessionId);
438
+ .run(state, now, sessionId);
439
+ }
440
+ /** 从 processing_state 解析当前活跃 taskId */
441
+ getActiveTaskId(sessionId) {
442
+ const row = this.db.prepare(`SELECT processing_state FROM sessions WHERE id = ?`).get(sessionId);
443
+ if (!row?.processing_state)
444
+ return undefined;
445
+ const colonIdx = row.processing_state.indexOf(':');
446
+ return colonIdx > 0 ? row.processing_state.slice(colonIdx + 1) : undefined;
436
447
  }
437
448
  /**
438
449
  * 清除会话处理中状态
@@ -440,6 +451,13 @@ export class SessionManager {
440
451
  clearProcessing(sessionId) {
441
452
  this.db.prepare(`UPDATE sessions SET processing_state = NULL, updated_at = ? WHERE id = ?`)
442
453
  .run(Date.now(), sessionId);
454
+ this.sessionEncryptState.delete(sessionId);
455
+ }
456
+ setSessionEncrypt(sessionId, encrypted) {
457
+ this.sessionEncryptState.set(sessionId, encrypted);
458
+ }
459
+ getSessionEncrypt(sessionId) {
460
+ return this.sessionEncryptState.get(sessionId);
443
461
  }
444
462
  /**
445
463
  * 获取所有处于 processing 状态的会话(用于重启后恢复)
@@ -453,7 +471,8 @@ export class SessionManager {
453
471
  const now = Date.now();
454
472
  const result = [];
455
473
  for (const row of rows) {
456
- const ts = parseInt(row.processing_state, 10);
474
+ const colonIdx = row.processing_state.indexOf(':');
475
+ const ts = parseInt(colonIdx > 0 ? row.processing_state.slice(0, colonIdx) : row.processing_state, 10);
457
476
  if (!isNaN(ts) && (now - ts) < maxAgeMs) {
458
477
  result.push(this.rowToSession(row));
459
478
  }
package/dist/index.js CHANGED
@@ -289,6 +289,14 @@ async function main() {
289
289
  });
290
290
  });
291
291
  }
292
+ // proactive 模式入站白名单:注入 sessionMode 查询器
293
+ if (typeof inst.channel.setSessionModeResolver === 'function') {
294
+ const chName = inst.adapter.channelName;
295
+ inst.channel.setSessionModeResolver(async (channelId) => {
296
+ const session = await sessionManager.getActiveSession(chName, channelId);
297
+ return session?.sessionMode;
298
+ });
299
+ }
292
300
  }
293
301
  if (channelType === 'dingtalk') {
294
302
  msgBridge.register(inst.adapter.channelName, (handler) => inst.channel.onMessage(async (event) => {
@@ -340,18 +348,6 @@ async function main() {
340
348
  for (const inst of channelInstances) {
341
349
  registerChannelInstance(inst);
342
350
  }
343
- // ── 设置热加载回调 ──
344
- cmdHandler.setHotLoadChannel(async (inst) => {
345
- registerChannelInstance(inst);
346
- channelInstances.push(inst);
347
- await inst.connect();
348
- eventBus.publish({
349
- type: 'channel:connected',
350
- channel: (inst.channelType || inst.adapter.channelName).toLowerCase(),
351
- channelName: inst.adapter.channelName,
352
- timestamp: Date.now(),
353
- });
354
- });
355
351
  // ── 连接所有渠道 ──
356
352
  const connected = await channelLoader.connectAll(channelInstances);
357
353
  // 预填充 Feishu 已知 thread_id(重启后避免误判话题创建)
package/dist/paths.js CHANGED
@@ -32,6 +32,7 @@ export function resolvePaths() {
32
32
  pid: path.join(root, 'logs', 'evolclaw.pid'),
33
33
  dataDir: path.join(root, 'data'),
34
34
  logs: path.join(root, 'logs'),
35
+ agentsDir: path.join(root, 'agents'),
35
36
  lineStats: path.join(root, 'logs', 'line-stats.log'),
36
37
  readySignal: path.join(root, 'logs', 'ready.signal'),
37
38
  selfHealLog: path.join(root, 'logs', 'self-heal.md'),
@@ -49,6 +50,7 @@ export function ensureDataDirs() {
49
50
  const p = resolvePaths();
50
51
  fs.mkdirSync(p.dataDir, { recursive: true });
51
52
  fs.mkdirSync(p.logs, { recursive: true });
53
+ fs.mkdirSync(p.agentsDir, { recursive: true });
52
54
  }
53
55
  export function getPackageRoot() {
54
56
  // import.meta.dirname is available in Node.js 21.2+ and always returns
@@ -22,6 +22,7 @@
22
22
  调用 Bash 工具执行命令 :evolclaw ctl send "<消息内容>"
23
23
  发送文件: evolclaw ctl file <路径>
24
24
  可多次调用发送多条消息 ,如果不想回复停止调用即可。
25
+ 禁止使用 AskUserQuestion 和 ExitPlanMode 工具——proactive 模式下应由你主动用 ctl send 与用户沟通。
25
26
 
26
27
 
27
28
  ---
@@ -10,13 +10,13 @@ import readline from 'readline';
10
10
  import path from 'path';
11
11
  import os from 'os';
12
12
  import crypto from 'crypto';
13
- import { fileURLToPath } from 'url';
14
- import { execFile, execFileSync } from 'child_process';
13
+ import { execFile } from 'child_process';
15
14
  import { promisify } from 'util';
16
15
  import { resolvePaths } from '../paths.js';
17
16
  import { normalizeChannelInstances } from '../config.js';
18
17
  import { selectInstance } from './init.js';
19
18
  import { isWindows } from './cross-platform.js';
19
+ import { AUN_CORE_SDK_PKG, MIN_AUN_CORE_SDK, resolveAunCoreSdkPkg, isAunSdkVersionOk, isValidAid, aidCreate, agentmdPut, buildInitialAgentMd, } from '../channels/aun-ops.js';
20
20
  const execFileAsync = promisify(execFile);
21
21
  export async function npmInstallGlobal(pkg) {
22
22
  const npmCmd = isWindows ? 'npm.cmd' : 'npm';
@@ -580,91 +580,9 @@ export async function cmdInitWechat() {
580
580
  process.exit(1);
581
581
  }
582
582
  // ==================== AUN ====================
583
- // 最低 @agentunion/fastaun 版本要求
584
- const MIN_AUN_CORE_SDK = [0, 2, 14];
585
- const AUN_CORE_SDK_PKG = '@agentunion/fastaun';
586
- function compareVersion(a, min) {
587
- const parts = a.split('.').map(n => parseInt(n, 10));
588
- if (parts.length < 3 || parts.some(isNaN))
589
- return false;
590
- if (parts[0] !== min[0])
591
- return parts[0] > min[0];
592
- if (parts[1] !== min[1])
593
- return parts[1] > min[1];
594
- return parts[2] >= min[2];
595
- }
596
- export function resolveAunCoreSdkPkg() {
597
- const pkgName = AUN_CORE_SDK_PKG;
598
- // Strategy 1: walk up node_modules from this file (no require.resolve — avoids ESM exports issues)
599
- try {
600
- let dir = path.dirname(fileURLToPath(import.meta.url));
601
- while (true) {
602
- const candidate = path.join(dir, 'node_modules', pkgName, 'package.json');
603
- if (fs.existsSync(candidate)) {
604
- const data = JSON.parse(fs.readFileSync(candidate, 'utf-8'));
605
- if (data.name === pkgName)
606
- return { version: data.version, path: candidate };
607
- }
608
- const parent = path.dirname(dir);
609
- if (parent === dir)
610
- break;
611
- dir = parent;
612
- }
613
- }
614
- catch { /* fall through */ }
615
- // Strategy 2: npm root -g fallback (globally installed SDK)
616
- try {
617
- const npmCmd = isWindows ? 'npm.cmd' : 'npm';
618
- const globalRoot = execFileSync(npmCmd, ['root', '-g'], {
619
- encoding: 'utf-8', timeout: 10000, shell: isWindows,
620
- }).trim();
621
- const pkgPath = path.join(globalRoot, pkgName, 'package.json');
622
- if (fs.existsSync(pkgPath)) {
623
- const data = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
624
- return { version: data.version, path: pkgPath };
625
- }
626
- }
627
- catch { /* not found */ }
628
- return null;
629
- }
630
- /**
631
- * Download AUN CA root certificate to ~/.aun/CA/root/root.crt.
632
- * Idempotent: skips if file already exists. Returns true if a cert is on disk
633
- * after the call (either pre-existing or freshly downloaded).
634
- *
635
- * Must be called BEFORE constructing any AUNClient that needs to verify
636
- * gateway-issued certificates (e.g. for uploadAgentMd) — the SDK loads trusted
637
- * roots from disk at client construction time and won't pick up later writes.
638
- */
639
- export async function downloadCaRoot(aunPath, gatewayUrl, indent = '') {
640
- const caDir = path.join(aunPath, 'CA', 'root');
641
- const caCertPath = path.join(caDir, 'root.crt');
642
- if (fs.existsSync(caCertPath))
643
- return true;
644
- if (!gatewayUrl)
645
- return false;
646
- try {
647
- fs.mkdirSync(caDir, { recursive: true });
648
- const gwHttp = gatewayUrl.replace(/^wss?:/, 'https:').replace(/\/aun$/, '');
649
- const resp = await fetch(`${gwHttp}/pki/chain`, { redirect: 'follow' });
650
- if (!resp.ok) {
651
- console.warn(`${indent}⚠ CA 根证书下载失败: HTTP ${resp.status}`);
652
- return false;
653
- }
654
- const body = await resp.text();
655
- if (!body.includes('BEGIN CERTIFICATE')) {
656
- console.warn(`${indent}⚠ CA 根证书响应内容无效,跳过写入`);
657
- return false;
658
- }
659
- fs.writeFileSync(caCertPath, body);
660
- console.log(`${indent}✓ CA 根证书已下载`);
661
- return true;
662
- }
663
- catch (e) {
664
- console.warn(`${indent}⚠ CA 根证书下载失败: ${e},可稍后手动下载`);
665
- return false;
666
- }
667
- }
583
+ //
584
+ // AUN 原子操作(aidCreate, agentmdPut, downloadCaRoot, isValidAid, ...)
585
+ // 已迁移至 src/channels/aun-ops.ts。本节仅保留交互式 UI 编排。
668
586
  export async function checkAunEnvironment(rl) {
669
587
  console.log('\n🔍 AUN 环境检查...\n');
670
588
  const minVer = MIN_AUN_CORE_SDK.join('.');
@@ -688,7 +606,7 @@ export async function checkAunEnvironment(rl) {
688
606
  console.log('');
689
607
  return true;
690
608
  }
691
- if (compareVersion(installed.version, MIN_AUN_CORE_SDK)) {
609
+ if (isAunSdkVersionOk(installed.version)) {
692
610
  console.log(` ✓ ${AUN_CORE_SDK_PKG} v${installed.version}`);
693
611
  console.log('');
694
612
  return true;
@@ -711,95 +629,8 @@ export async function checkAunEnvironment(rl) {
711
629
  console.log('');
712
630
  return true;
713
631
  }
714
- export function isValidAid(name) {
715
- const labels = name.split('.');
716
- return labels.length >= 3 && labels.every(l => /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(l));
717
- }
718
- /**
719
- * Non-interactive AID creation + agent.md publish.
720
- * Reuses the same logic as `evolclaw init --non-interactive --channel aun`.
721
- *
722
- * Returns the created AID string, or throws on failure.
723
- */
724
- export async function createAidSilent(opts) {
725
- const aunPath = path.join(os.homedir(), '.aun');
726
- const aidDir = path.join(aunPath, 'AIDs', opts.aid);
727
- // Skip creation if AID already exists locally
728
- if (fs.existsSync(aidDir) && fs.existsSync(path.join(aidDir, 'private'))) {
729
- return { aid: opts.aid, alreadyExisted: true };
730
- }
731
- const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
732
- let client = new AUNClient({ aun_path: aunPath });
733
- const result = await client.auth.createAid({ aid: opts.aid });
734
- // Download CA root cert (if not already present)
735
- const caDownloaded = await downloadCaRoot(aunPath, result.gateway || '');
736
- // Rebuild client with CA cert + AID identity for uploadAgentMd
737
- const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
738
- if (caDownloaded && fs.existsSync(caCertPath)) {
739
- try {
740
- await client.close();
741
- }
742
- catch { /* ignore */ }
743
- client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid: opts.aid });
744
- }
745
- // Set gateway URL for uploadAgentMd
746
- let gatewayUrl = result.gateway || '';
747
- if (!gatewayUrl) {
748
- try {
749
- const discovery = new GatewayDiscovery({});
750
- gatewayUrl = await discovery.discover(`https://${opts.aid}/.well-known/aun-gateway`);
751
- }
752
- catch { /* fall through */ }
753
- }
754
- if (gatewayUrl) {
755
- client._gatewayUrl = gatewayUrl;
756
- }
757
- // Write initial agent.md (initialized: false, name = aid first label)
758
- const agentName = opts.aid.split('.')[0];
759
- const agentMdContent = `---\naid: "${opts.aid}"\nname: "${agentName}"\ntype: "ai"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
760
- const agentMdPath = path.join(aidDir, 'agent.md');
761
- try {
762
- await client.auth.uploadAgentMd(agentMdContent);
763
- }
764
- catch (e) {
765
- // Non-fatal: first connection will auto-retry
766
- }
767
- fs.writeFileSync(agentMdPath, agentMdContent, 'utf-8');
768
- try {
769
- await client.close();
770
- }
771
- catch { /* ignore */ }
772
- if (!fs.existsSync(agentMdPath)) {
773
- throw new Error(`agent.md write verification failed: ${agentMdPath}`);
774
- }
775
- return { aid: opts.aid, alreadyExisted: false };
776
- }
777
- /**
778
- * Append a new AUN instance to the config's channels.aun array and save.
779
- * Handles upgrade from single-object to array format.
780
- */
781
- export function appendAunInstance(config, inst) {
782
- if (!config.channels)
783
- config.channels = {};
784
- const newInst = {
785
- name: inst.name,
786
- enabled: inst.enabled ?? true,
787
- aid: inst.aid,
788
- ...(inst.owner && { owner: inst.owner }),
789
- };
790
- if (Array.isArray(config.channels.aun)) {
791
- config.channels.aun.push(newInst);
792
- }
793
- else if (config.channels.aun) {
794
- const oldInst = { ...config.channels.aun, name: config.channels.aun.name || 'aun' };
795
- config.channels.aun = [oldInst, newInst];
796
- }
797
- else {
798
- config.channels.aun = [newInst];
799
- }
800
- const p = resolvePaths();
801
- fs.writeFileSync(p.config, JSON.stringify(config, null, 2) + '\n');
802
- }
632
+ // isValidAid, createAidSilent → 已迁移至 src/channels/aun-ops.ts
633
+ // appendAunInstance 已迁移至 src/channels/aun-ops.ts
803
634
  export async function setupAunAid(rl, _config) {
804
635
  let aid = '';
805
636
  // Outer loop: allows retrying with a different AID
@@ -829,64 +660,35 @@ export async function setupAunAid(rl, _config) {
829
660
  console.log(' 已跳过 AID 创建(启动时可能连接失败)');
830
661
  break;
831
662
  }
832
- // Create AID using TS SDK directly
663
+ // Create AID + agent.md via atomic ops
833
664
  console.log(' 正在创建 AID...');
834
665
  let failed = false;
835
666
  try {
836
- const { AUNClient, GatewayDiscovery } = await import('@agentunion/fastaun');
837
- let client = new AUNClient({ aun_path: aunPath });
838
- const result = await client.auth.createAid({ aid });
667
+ const result = await aidCreate(aid);
839
668
  console.log(` ✓ AID ${result.aid} 创建成功`);
840
- // 下载 CA 根证书(如果本地不存在),从 SDK 返回的实际网关 URL 派生
841
- const caDownloaded = await downloadCaRoot(aunPath, result.gateway || '', ' ');
842
- // 重建 client:传 root_ca_path 以验证 server cert,传 aid 以加载身份
843
- const caCertPath = path.join(aunPath, 'CA', 'root', 'root.crt');
844
- if (caDownloaded && fs.existsSync(caCertPath)) {
845
- try {
846
- await client.close();
847
- }
848
- catch { /* ignore */ }
849
- client = new AUNClient({ aun_path: aunPath, root_ca_path: caCertPath, aid });
850
- }
851
- // 设置 gateway URL(从 createAid 返回值或 well-known 自动发现)
852
- let gatewayUrl = result.gateway || '';
853
- if (!gatewayUrl) {
854
- try {
855
- const discovery = new GatewayDiscovery({});
856
- gatewayUrl = await discovery.discover(`https://${aid}/.well-known/aun-gateway`);
857
- }
858
- catch { /* fall through */ }
859
- }
860
- if (gatewayUrl) {
861
- client._gatewayUrl = gatewayUrl;
862
- }
863
- // Collect agent.md info and publish
669
+ // Collect agent.md type and upload
864
670
  const typeInput = (await ask(rl, ' Agent 类型 human/ai [ai]: ')).trim().toLowerCase();
865
671
  const agentType = typeInput === 'human' ? 'human' : 'ai';
866
- const agentName = aid.split('.')[0];
867
- const agentMdContent = `---\naid: "${aid}"\nname: "${agentName}"\ntype: "${agentType}"\nversion: "1.0.0"\ndescription: ""\ntags:\n - evolclaw\ninitialized: false\n---\n`;
868
- const agentMdPath = path.join(aidDir, 'agent.md');
672
+ const content = buildInitialAgentMd({ aid, type: agentType });
869
673
  try {
870
- await client.auth.uploadAgentMd(agentMdContent);
871
- console.log(' ✓ agent.md 已发布');
674
+ await agentmdPut(content, { aid, client: result.client });
675
+ console.log(' ✓ agent.md 已发布并写入本地');
872
676
  }
873
677
  catch (e) {
874
678
  console.log(` ⚠ agent.md 发布失败(首次连接将自动重试): ${String(e.message || e).slice(0, 100)}`);
875
- }
876
- fs.writeFileSync(agentMdPath, agentMdContent, 'utf-8');
877
- if (!fs.existsSync(agentMdPath)) {
679
+ // Still write local copy as fallback
878
680
  try {
879
- await client.close();
681
+ fs.mkdirSync(aidDir, { recursive: true });
682
+ fs.writeFileSync(path.join(aidDir, 'agent.md'), content, 'utf-8');
683
+ console.log(' ✓ agent.md 已写入本地');
684
+ }
685
+ catch (we) {
686
+ console.log(` ✗ agent.md 本地写入失败: ${String(we.message || we).slice(0, 100)}`);
687
+ failed = true;
880
688
  }
881
- catch { /* ignore */ }
882
- console.log(` ✗ agent.md 本地写入校验失败: ${agentMdPath}`);
883
- failed = true;
884
- }
885
- else {
886
- console.log(' ✓ agent.md 已写入本地');
887
689
  }
888
690
  try {
889
- await client.close();
691
+ await result.client.close();
890
692
  }
891
693
  catch { /* ignore */ }
892
694
  }
@@ -1610,3 +1412,71 @@ export async function cmdInitWecom() {
1610
1412
  console.log(` 配置已写入: ${p.config}`);
1611
1413
  console.log(`\n现在可以启动服务: evolclaw restart`);
1612
1414
  }
1415
+ export function getChannelCredentialCollector(type) {
1416
+ switch (type) {
1417
+ case 'feishu':
1418
+ return async () => {
1419
+ const result = await runFeishuQrFlow();
1420
+ if (!result)
1421
+ return null;
1422
+ return { appId: result.appId, appSecret: result.appSecret, enabled: true };
1423
+ };
1424
+ case 'wechat':
1425
+ return async () => {
1426
+ const result = await runWechatQrFlow();
1427
+ if (!result)
1428
+ return null;
1429
+ return { baseUrl: result.baseUrl, token: result.token, enabled: true };
1430
+ };
1431
+ case 'aun':
1432
+ return async () => {
1433
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1434
+ try {
1435
+ if (!await checkAunEnvironment(rl))
1436
+ return null;
1437
+ const result = await setupAunAid(rl, {});
1438
+ if (!result)
1439
+ return null;
1440
+ return { aid: result.aid, owner: result.owner, enabled: true };
1441
+ }
1442
+ finally {
1443
+ rl.close();
1444
+ }
1445
+ };
1446
+ case 'dingtalk':
1447
+ return async () => {
1448
+ const result = await runDingtalkQrFlowSimple();
1449
+ if (!result)
1450
+ return null;
1451
+ return { clientId: result.clientId, clientSecret: result.clientSecret, enabled: true };
1452
+ };
1453
+ case 'qqbot':
1454
+ return async () => {
1455
+ const result = await runQQBotBindFlowSimple();
1456
+ if (!result)
1457
+ return null;
1458
+ return { appId: result.appId, clientSecret: result.clientSecret, enabled: true };
1459
+ };
1460
+ case 'wecom':
1461
+ return async () => {
1462
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1463
+ const ask = (q) => new Promise(r => rl.question(q, r));
1464
+ try {
1465
+ console.log('企业微信 AI Bot 配置\n');
1466
+ console.log('请在企业微信管理后台 → AI Bot 页面获取 Bot ID 和 Secret\n');
1467
+ const botId = (await ask(' Bot ID: ')).trim();
1468
+ if (!botId)
1469
+ return null;
1470
+ const secret = (await ask(' Secret: ')).trim();
1471
+ if (!secret)
1472
+ return null;
1473
+ return { botId, secret, enabled: true };
1474
+ }
1475
+ finally {
1476
+ rl.close();
1477
+ }
1478
+ };
1479
+ default:
1480
+ return null;
1481
+ }
1482
+ }