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.
package/dist/cli/index.js CHANGED
@@ -4,7 +4,7 @@ import path from 'path';
4
4
  import os from 'os';
5
5
  import { spawn, execFileSync, execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
- import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot } from '../paths.js';
7
+ import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot, agentMdPath } from '../paths.js';
8
8
  import { loadDefaults, loadAllAgents, mergeForAgent } from '../config-store.js';
9
9
  import { resolveAnthropicConfig } from '../agents/resolve.js';
10
10
  import { migrateProject } from '../config-store.js';
@@ -235,7 +235,7 @@ function formatLocalTime(ms) {
235
235
  const d = new Date(ms);
236
236
  return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')}`;
237
237
  }
238
- function printStartupInfo() {
238
+ function printStartupInfo(opts = {}) {
239
239
  const pkgRoot = getPackageRoot();
240
240
  const isNpmInstall = pkgRoot.includes('node_modules');
241
241
  const cliRunsSource = !import.meta.url.includes('/dist/');
@@ -268,7 +268,15 @@ function printStartupInfo() {
268
268
  version = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8')).version;
269
269
  }
270
270
  catch { }
271
- console.log(` EvolClaw v${version}`);
271
+ let aunVer = null;
272
+ try {
273
+ aunVer = JSON.parse(fs.readFileSync(path.join(pkgRoot, 'node_modules', '@agentunion', 'fastaun', 'package.json'), 'utf-8')).version;
274
+ }
275
+ catch { }
276
+ const pidPart = opts.pid ? ` (PID: ${opts.pid})` : '';
277
+ const aunPart = aunVer ? ` fastaun v${aunVer}` : '';
278
+ const prefix = opts.running ? '✓ EvolClaw is running , v' : ' EvolClaw v';
279
+ console.log(`${prefix}${version}${pidPart}${aunPart}`);
272
280
  console.log(` 包路径: ${pkgRoot}`);
273
281
  console.log(` 安装类型: ${isNpmInstall ? 'npm全局安装' : '开发仓(link)'}`);
274
282
  console.log(` CLI执行: ${cliRunsSource ? '源码(tsx)' : '编译产物(dist)'}`);
@@ -829,7 +837,7 @@ async function cmdStatus() {
829
837
  console.log('');
830
838
  }
831
839
  if (pid) {
832
- console.log(`✓ EvolClaw is running (PID: ${pid})`);
840
+ printStartupInfo({ pid, running: true });
833
841
  console.log('');
834
842
  console.log('📊 Process Info:');
835
843
  try {
@@ -875,8 +883,8 @@ async function cmdStatus() {
875
883
  const configChannelNames = new Set();
876
884
  for (const cfg of agents) {
877
885
  for (const inst of cfg.channels) {
878
- // effective key: <aid>#<type>#<name>
879
- configChannelNames.add(`${cfg.aid}#${inst.type}#${inst.name}`);
886
+ // effective key: <type>#<urlEncode(selfPeerId)>#<name>
887
+ configChannelNames.add(`${inst.type}#${encodeURIComponent(cfg.aid)}#${inst.name}`);
880
888
  }
881
889
  }
882
890
  for (const s of allSessions) {
@@ -1005,7 +1013,7 @@ async function cmdStatus() {
1005
1013
  }
1006
1014
  }
1007
1015
  /**
1008
- * 把 channel fingerprint 列表(`<aid>#<type>#<name>`)折叠成展示用摘要。
1016
+ * 把 channel fingerprint 列表(`<type>#<selfPeerId>#<name>`)折叠成展示用摘要。
1009
1017
  *
1010
1018
  * 聚合规则:
1011
1019
  * - 按 type 分组
@@ -1027,8 +1035,8 @@ function summarizeChannelFingerprints(fingerprints) {
1027
1035
  }
1028
1036
  continue;
1029
1037
  }
1030
- const type = parts[1];
1031
- const name = parts.slice(2).join('#');
1038
+ const type = parts[0];
1039
+ const name = parts[2];
1032
1040
  if (!groups.has(type)) {
1033
1041
  groups.set(type, []);
1034
1042
  order.push(type);
@@ -1734,10 +1742,10 @@ async function cmdWatchAid() {
1734
1742
  const refreshedAids = new Set();
1735
1743
  function readLocalName(aid) {
1736
1744
  try {
1737
- const agentMdPath = path.join(os.homedir(), '.aun', 'AIDs', aid, 'agent.md');
1738
- if (!fs.existsSync(agentMdPath))
1745
+ const mdPath = agentMdPath(aid);
1746
+ if (!fs.existsSync(mdPath))
1739
1747
  return undefined;
1740
- const content = fs.readFileSync(agentMdPath, 'utf-8');
1748
+ const content = fs.readFileSync(mdPath, 'utf-8');
1741
1749
  const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
1742
1750
  if (!fmMatch)
1743
1751
  return undefined;
@@ -2463,12 +2471,14 @@ function archiveSelfHealLog(p, log) {
2463
2471
  * Searches across all channel types (feishu, wechat, aun) for a matching instance.
2464
2472
  */
2465
2473
  function resolveInstanceConfig(instanceName) {
2466
- // 新结构:channel key 是 <aid>#<type>#<name>,解析后从对应 agent 的 channels[] 找
2474
+ // 新结构:channel key 是 <type>#<selfPeerId>#<name>,解析后从对应 agent 的 channels[] 找
2467
2475
  const parts = instanceName.split('#');
2468
2476
  if (parts.length === 3) {
2469
- const [aid, type, name] = parts;
2477
+ const [type, encodedSelfPeerId, name] = parts;
2478
+ const selfPeerId = decodeURIComponent(encodedSelfPeerId);
2470
2479
  const { agents } = loadAllAgents();
2471
- const agent = agents.find(a => a.aid === aid);
2480
+ // AUN channel selfPeerId 就是 agent.aid
2481
+ const agent = agents.find(a => a.aid === selfPeerId);
2472
2482
  if (!agent)
2473
2483
  return null;
2474
2484
  const inst = agent.channels.find((c) => c.type === type && c.name === name);
@@ -2921,7 +2931,11 @@ Options:
2921
2931
  console.log(result.agentmdUploaded
2922
2932
  ? ' ✓ agent.md 已发布'
2923
2933
  : ' ⚠ agent.md 上传失败(可用 evolclaw aid agentmd put 重试)');
2924
- console.log(' Run `evolclaw restart` to activate.');
2934
+ console.log(result.hotLoaded
2935
+ ? ' ✓ 已热重载,agent 已上线'
2936
+ : result.hotLoadError
2937
+ ? ` ✗ 热重载失败:${result.hotLoadError}`
2938
+ : ' ⚠ 服务未运行,下次 evolclaw start 时生效');
2925
2939
  }
2926
2940
  }
2927
2941
  else {
@@ -2943,7 +2957,11 @@ Options:
2943
2957
  console.log(result.agentmdUploaded
2944
2958
  ? ' ✓ agent.md 已发布'
2945
2959
  : ' ⚠ agent.md 上传失败(可用 evolclaw aid agentmd put 重试)');
2946
- console.log(' Run `evolclaw restart` to activate.');
2960
+ console.log(result.hotLoaded
2961
+ ? ' ✓ 已热重载,agent 已上线'
2962
+ : result.hotLoadError
2963
+ ? ` ✗ 热重载失败:${result.hotLoadError}`
2964
+ : ' ⚠ 服务未运行,下次 evolclaw start 时生效');
2947
2965
  }
2948
2966
  }
2949
2967
  return;
@@ -3438,8 +3456,7 @@ Options:
3438
3456
  console.error(`❌ 无效 AID 格式: ${aid}`);
3439
3457
  process.exit(1);
3440
3458
  }
3441
- const aunBase = aunPath ?? path.join(os.homedir(), '.aun');
3442
- const localPath = path.join(aunBase, 'AIDs', aid, 'agent.md');
3459
+ const localPath = agentMdPath(aid);
3443
3460
  if (!fs.existsSync(localPath)) {
3444
3461
  console.error(`❌ 本地无 agent.md: ${aid}`);
3445
3462
  process.exit(1);
@@ -3747,12 +3764,15 @@ Options:
3747
3764
  --app <name> 指定应用 slot(隔离 ack 游标)
3748
3765
  --as-daemon ack 时显式以 daemon 身份(高危,会污染 daemon 游标)
3749
3766
  --format json 输出 JSON 格式
3767
+ --encrypt 启用端到端加密
3768
+ --thread <id> 指定话题 ID(用于多话题路由)
3750
3769
  --content-type <mime> 显式覆盖 MIME(仅 --file 模式)
3751
3770
  --text <说明> 附件说明文字(仅 --file 模式)
3752
3771
  --transcript <text> 语音转写(仅 --as voice)
3753
3772
 
3754
3773
  示例:
3755
3774
  evolclaw msg send alice.agentid.pub bob.agentid.pub "hello"
3775
+ evolclaw msg send alice.agentid.pub bob.agentid.pub "讨论项目A" --thread "project-A"
3756
3776
  evolclaw msg send alice.agentid.pub bob.agentid.pub --file ./pic.png
3757
3777
  evolclaw msg send alice.agentid.pub bob.agentid.pub --file ./demo.mp4 --as video
3758
3778
  evolclaw msg send alice.agentid.pub bob.agentid.pub --link https://example.com --title "AUN"
@@ -3826,7 +3846,29 @@ Options:
3826
3846
  body = { mode: 'text', text };
3827
3847
  }
3828
3848
  const encrypt = args.includes('--encrypt');
3829
- const result = await msgSend({ from, to, body, encrypt, ...commonOpts });
3849
+ const thread = getArgValue(args, '--thread');
3850
+ // 文件上传进度展示(非 JSON 输出时)。仅在大文件降级到 HTTP PUT 阶段会逐块更新。
3851
+ let lastPctShown = -1;
3852
+ const onUploadProgress = formatJson ? undefined : (info) => {
3853
+ if (info.phase === 'inline')
3854
+ return; // 内联阶段不分块,跳过
3855
+ if (info.phase === 'http-put') {
3856
+ const pct = info.total > 0 ? Math.floor((info.bytes / info.total) * 100) : 0;
3857
+ if (pct === lastPctShown && info.bytes < info.total)
3858
+ return;
3859
+ lastPctShown = pct;
3860
+ const mb = (n) => (n / 1024 / 1024).toFixed(2);
3861
+ const eol = info.bytes >= info.total ? '\n' : '\r';
3862
+ process.stderr.write(` ⏫ uploading: ${pct}% (${mb(info.bytes)}/${mb(info.total)} MB)${eol}`);
3863
+ }
3864
+ else if (info.phase === 'session-create') {
3865
+ process.stderr.write(' ⏫ requesting upload session...\n');
3866
+ }
3867
+ else if (info.phase === 'session-complete') {
3868
+ process.stderr.write(' ⏫ finalizing upload...\n');
3869
+ }
3870
+ };
3871
+ const result = await msgSend({ from, to, body, encrypt, thread, onUploadProgress, ...commonOpts });
3830
3872
  if (!result.ok) {
3831
3873
  if (formatJson) {
3832
3874
  console.log(JSON.stringify(result));
@@ -4408,15 +4450,23 @@ export async function main(args) {
4408
4450
  evolclaw init wecom 企业微信 AI Bot 配置(手动输入)`);
4409
4451
  }
4410
4452
  else if (args[1] === 'wechat') {
4453
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
4454
+ suppressSdkLogs();
4411
4455
  await cmdInitWechat();
4412
4456
  }
4413
4457
  else if (args[1] === 'feishu') {
4458
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
4459
+ suppressSdkLogs();
4414
4460
  await cmdInitFeishu();
4415
4461
  }
4416
4462
  else if (args[1] === 'dingtalk') {
4463
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
4464
+ suppressSdkLogs();
4417
4465
  await cmdInitDingtalk();
4418
4466
  }
4419
4467
  else if (args[1] === 'qqbot') {
4468
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
4469
+ suppressSdkLogs();
4420
4470
  await cmdInitQQBot();
4421
4471
  }
4422
4472
  else if (args[1] === 'wecom') {
@@ -4429,6 +4479,8 @@ export async function main(args) {
4429
4479
  process.exit(1);
4430
4480
  }
4431
4481
  else {
4482
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
4483
+ suppressSdkLogs();
4432
4484
  const nonInteractive = args.includes('--non-interactive');
4433
4485
  await cmdInit({
4434
4486
  nonInteractive,
@@ -4509,9 +4561,12 @@ export async function main(args) {
4509
4561
  case 'ctl':
4510
4562
  await cmdCtl(args.slice(1));
4511
4563
  break;
4512
- case 'agent':
4564
+ case 'agent': {
4565
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
4566
+ suppressSdkLogs();
4513
4567
  await cmdAgent(args.slice(1));
4514
4568
  break;
4569
+ }
4515
4570
  case 'aid': {
4516
4571
  const { suppressSdkLogs } = await import('../aun/aid/index.js');
4517
4572
  suppressSdkLogs();
@@ -10,6 +10,7 @@ import readline from 'readline';
10
10
  import path from 'path';
11
11
  import os from 'os';
12
12
  import crypto from 'crypto';
13
+ import { aidLocalDir } from '../paths.js';
13
14
  import { selectInstance } from './init.js';
14
15
  import { npmInstallGlobal } from '../utils/npm-ops.js';
15
16
  import { loadAllAgents, loadAgent } from '../config-store.js';
@@ -470,8 +471,9 @@ export async function setupAunAid(rl, _config) {
470
471
  console.log(` ⚠ agent.md 发布失败(首次连接将自动重试): ${String(e.message || e).slice(0, 100)}`);
471
472
  // Still write local copy as fallback
472
473
  try {
473
- fs.mkdirSync(aidDir, { recursive: true });
474
- fs.writeFileSync(path.join(aidDir, 'agent.md'), content, 'utf-8');
474
+ const localDir = aidLocalDir(aid);
475
+ fs.mkdirSync(localDir, { recursive: true });
476
+ fs.writeFileSync(path.join(localDir, 'agent.md'), content, 'utf-8');
475
477
  console.log(' ✓ agent.md 已写入本地');
476
478
  }
477
479
  catch (we) {
package/dist/cli/init.js CHANGED
@@ -3,7 +3,7 @@ import readline from 'readline';
3
3
  import { resolvePaths, ensureDataDirs } from '../paths.js';
4
4
  import { commandExists } from '../utils/cross-platform.js';
5
5
  import { scanInstances } from '../utils/instance-registry.js';
6
- import { saveDefaultsSafe } from '../config-store.js';
6
+ import { saveDefaultsSafe, loadAllAgents } from '../config-store.js';
7
7
  // ==================== Helpers ====================
8
8
  function ask(rl, question) {
9
9
  return new Promise(resolve => rl.question(question, resolve));
@@ -81,19 +81,21 @@ export async function cmdInit(options) {
81
81
  writeDefaults(defaultsPath, chosen);
82
82
  console.log(`✓ 已${exists ? '覆盖' : '创建'}: ${defaultsPath}`);
83
83
  console.log(` active_baseagent: ${chosen}`);
84
+ const { agents } = loadAllAgents();
85
+ if (agents.length === 0) {
86
+ console.log('\n提示:尚无 agent,运行以下命令创建:');
87
+ console.log(' evolclaw agent new <aid>.agentid.pub');
88
+ }
84
89
  return;
85
90
  }
86
91
  // ── 4. 交互式分支 ──
87
92
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
88
- try {
89
- if (exists) {
90
- const ans = (await ask(rl, `配置文件已存在: ${defaultsPath}\n 是否覆盖?[y/N] `)).trim().toLowerCase();
91
- if (ans !== 'y' && ans !== 'yes') {
92
- console.log(' 已取消');
93
- return;
94
- }
95
- }
93
+ async function askBaseagent() {
96
94
  const defaultBa = pickDefault(available);
95
+ if (available.length === 1) {
96
+ console.log(` baseagent: ${defaultBa}`);
97
+ return defaultBa;
98
+ }
97
99
  let chosen = null;
98
100
  while (chosen === null) {
99
101
  const input = (await ask(rl, `默认 baseagent (${available.join('/')}) [${defaultBa}]: `)).trim() || defaultBa;
@@ -107,17 +109,37 @@ export async function cmdInit(options) {
107
109
  }
108
110
  chosen = input;
109
111
  }
110
- writeDefaults(defaultsPath, chosen);
111
- console.log(`\n✓ 已${exists ? '覆盖' : '创建'}: ${defaultsPath}`);
112
- console.log(` active_baseagent: ${chosen}\n`);
113
- rl.close();
114
- // ── 5. 嵌套 agent new ──
115
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
116
- console.log('下一步:创建 agent\n');
117
- const { agentCreateInteractive } = await import('./agent.js');
118
- const result = await agentCreateInteractive();
119
- if (!result.ok) {
120
- console.error(`❌ ${result.error}`);
112
+ return chosen;
113
+ }
114
+ try {
115
+ if (exists) {
116
+ const ans = (await ask(rl, `配置文件已存在: ${defaultsPath}\n 是否覆盖?[y/N] `)).trim().toLowerCase();
117
+ if (ans === 'y' || ans === 'yes') {
118
+ const chosen = await askBaseagent();
119
+ writeDefaults(defaultsPath, chosen);
120
+ console.log(`\n✓ 已覆盖: ${defaultsPath}`);
121
+ console.log(` active_baseagent: ${chosen}\n`);
122
+ }
123
+ else {
124
+ console.log(' 已跳过(保留现有配置)\n');
125
+ }
126
+ }
127
+ else {
128
+ const chosen = await askBaseagent();
129
+ writeDefaults(defaultsPath, chosen);
130
+ console.log(`\n✓ 已创建: ${defaultsPath}`);
131
+ console.log(` active_baseagent: ${chosen}\n`);
132
+ }
133
+ // ── 5. 无 agent 时自动进入 agent new ──
134
+ const { agents } = loadAllAgents();
135
+ if (agents.length === 0) {
136
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
137
+ console.log('下一步:创建 agent\n');
138
+ const { agentCreateInteractive } = await import('./agent.js');
139
+ const result = await agentCreateInteractive({ rl });
140
+ if (!result.ok) {
141
+ console.error(`❌ ${result.error}`);
142
+ }
121
143
  }
122
144
  }
123
145
  finally {
@@ -316,7 +316,9 @@ function renderMessagesPanel(state, width, height) {
316
316
  const metaTags = (m.encrypt != null || m.chatmode) ? `${MAGENTA}[${encLabel}|${modeLabel}]${RST}` : '';
317
317
  let typeTag = '';
318
318
  if (m.dir === 'out') {
319
- const source = m.source === 'cli' ? 'cli' : 'daemon';
319
+ const rawSource = m.source;
320
+ // 4 种来源: daemon | ctl | msg | cli
321
+ const source = (rawSource === 'ctl' || rawSource === 'msg' || rawSource === 'cli') ? rawSource : 'daemon';
320
322
  const method = m.msgType === 'thought' ? 'thought' : 'send';
321
323
  typeTag = `${DIM}[${source}|${method}]${RST}`;
322
324
  }
@@ -348,9 +348,30 @@ export function loadAgent(aid) {
348
348
  if (raw.aid !== aid) {
349
349
  throw new Error(`[config] ${p}: aid field "${raw.aid}" != directory name "${aid}"`);
350
350
  }
351
- return expandEnvRefs(raw);
351
+ const cfg = expandEnvRefs(raw);
352
+ if (cfg.projects?.defaultPath) {
353
+ cfg.projects.defaultPath = cfg.projects.defaultPath.replace(/[/\\]+$/, '');
354
+ }
355
+ return cfg;
352
356
  }
353
357
  export function saveAgent(value) {
358
+ if (!isValidAid(value.aid)) {
359
+ throw new Error(`[config] saveAgent: invalid aid "${value.aid}" (must be a valid multi-level domain like mybot.agentid.pub)`);
360
+ }
361
+ if (value.owners) {
362
+ for (const o of value.owners) {
363
+ if (!isValidAid(o)) {
364
+ throw new Error(`[config] saveAgent: invalid owner AID "${o}" in ${value.aid} (must be a valid multi-level domain like alice.agentid.pub)`);
365
+ }
366
+ }
367
+ }
368
+ if (value.admins) {
369
+ for (const a of value.admins) {
370
+ if (!isValidAid(a)) {
371
+ throw new Error(`[config] saveAgent: invalid admin AID "${a}" in ${value.aid} (must be a valid multi-level domain like alice.agentid.pub)`);
372
+ }
373
+ }
374
+ }
354
375
  atomicWriteJson(agentConfigPath(value.aid), value);
355
376
  }
356
377
  /**
@@ -128,18 +128,18 @@ export class ChannelLoader {
128
128
  }
129
129
  const SEP = '#';
130
130
  export function formatChannelKey(k) {
131
- return `${k.aid}${SEP}${k.type}${SEP}${k.name}`;
131
+ return `${k.type}${SEP}${encodeURIComponent(k.selfPeerId)}${SEP}${k.name}`;
132
132
  }
133
133
  export function parseChannelKey(key) {
134
134
  const parts = key.split(SEP);
135
135
  if (parts.length !== 3) {
136
136
  throw new Error(`Invalid channel key (expected 3 segments separated by '#'): ${key}`);
137
137
  }
138
- const [aid, type, name] = parts;
139
- if (!aid || !type || !name) {
138
+ const [type, encodedSelfPeerId, name] = parts;
139
+ if (!type || !encodedSelfPeerId || !name) {
140
140
  throw new Error(`Invalid channel key (empty segment): ${key}`);
141
141
  }
142
- return { aid, type, name };
142
+ return { type, selfPeerId: decodeURIComponent(encodedSelfPeerId), name };
143
143
  }
144
144
  export function tryParseChannelKey(key) {
145
145
  try {
@@ -707,6 +707,10 @@ export class CommandHandler {
707
707
  return result;
708
708
  }
709
709
  async _handleInternal(content, channel, channelId, sendMessage, userId, threadId, chatType, source) {
710
+ // 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
711
+ // 不应覆盖 session 中已有的正确值
712
+ if (source === 'card-trigger')
713
+ chatType = undefined;
710
714
  // 解析身份(按实例名)
711
715
  const identity = this.sessionManager.resolveIdentity(channel, userId);
712
716
  const policy = this.getPolicy(channel);
@@ -3000,8 +3004,10 @@ export class CommandHandler {
3000
3004
  return null;
3001
3005
  }
3002
3006
  handleTrigger(content, channel, channelId, peerId, isAdmin) {
3003
- const scheduler = this.triggerScheduler;
3004
- const manager = this.triggerManager;
3007
+ // Resolve trigger manager/scheduler from the owning agent of this channel
3008
+ const owningAgent = this.getOwningAgent(channel);
3009
+ const scheduler = (owningAgent?.triggerScheduler ?? this.triggerScheduler);
3010
+ const manager = (owningAgent?.triggerManager ?? this.triggerManager);
3005
3011
  // Bare /trigger → list active
3006
3012
  if (content === '/trigger') {
3007
3013
  if (!manager)
@@ -3386,9 +3392,10 @@ export class CommandHandler {
3386
3392
  const taskId = replyContext?.metadata?.taskId;
3387
3393
  const chatmode = replyContext?.metadata?.chatmode ?? 'interactive';
3388
3394
  // --encrypt 覆盖 session 加密状态
3395
+ // 添加 source: 'ctl' 标记(用于区分 ec ctl send)
3389
3396
  const enrichedReplyContext = forceEncrypt
3390
- ? { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), encrypted: true } }
3391
- : replyContext;
3397
+ ? { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), encrypted: true, source: 'ctl' } }
3398
+ : { ...(replyContext ?? {}), metadata: { ...(replyContext?.metadata ?? {}), source: 'ctl' } };
3392
3399
  await adapter.send(buildEnvelope({ taskId, channel: adapter.channelName, channelId: session.channelId, chatmode, replyContext: enrichedReplyContext }), { kind: 'result.text', text, isFinal: true });
3393
3400
  // 出方向 jsonl 写入已下沉到 aun.ts:deliverTextEntry,message.send 成功后统一写入。
3394
3401
  return { ok: true, result: 'ok' };
@@ -51,7 +51,7 @@ export function detectDuplicates(agents) {
51
51
  export class EvolAgentRegistry {
52
52
  _agentsDir;
53
53
  agents = new Map();
54
- /** channel key (`<aid>#<type>#<name>`) → agent aid */
54
+ /** channel key (`<type>#<selfPeerId>#<name>`) → agent aid */
55
55
  channelIndex = new Map();
56
56
  /** 启动期被 ConfigStore 跳过的目录(命名非法 / 缺 config.json / 校验失败等) */
57
57
  skipped = [];
@@ -199,6 +199,12 @@ export class EvolAgentRegistry {
199
199
  logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${errs.join('; ')}`);
200
200
  return null;
201
201
  }
202
+ // Channel fingerprint 冲突检测(防止新 agent 复用已有 agent 的凭证)
203
+ const conflict = this.checkConflictForReload(raw, aid);
204
+ if (conflict) {
205
+ logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${conflict}`);
206
+ return null;
207
+ }
202
208
  const defaults = loadDefaults();
203
209
  const merged = mergeForAgent(raw, defaults);
204
210
  const agent = new EvolAgent(raw, merged);
@@ -224,12 +230,47 @@ export class EvolAgentRegistry {
224
230
  throw new Error(`Invalid config after edit: ${errs.join('; ')}`);
225
231
  const defaults = loadDefaults();
226
232
  const merged = mergeForAgent(raw, defaults);
233
+ // ── disabled → enabled 转换:需要完整启动流程 ──
234
+ if (oldAgent.status === 'disabled' && raw.enabled !== false) {
235
+ oldAgent.swapConfig(raw, merged);
236
+ const hotLoad = globalThis.__evolclaw_hotLoadAgent;
237
+ if (!hotLoad)
238
+ throw new Error(`Cannot enable agent "${aidOrName}": hot-load handler not initialized`);
239
+ // 从 registry 中移除旧的 disabled 实例,hotLoad 会重新创建
240
+ this.agents.delete(oldAgent.aid);
241
+ this.channelIndex.clear();
242
+ this.buildChannelIndex();
243
+ await hotLoad(oldAgent.aid);
244
+ logger.info(`[Reload] Agent "${aidOrName}" transitioned from disabled → enabled (full startup)`);
245
+ return;
246
+ }
247
+ // ── enabled → disabled 转换:断开所有 channel ──
248
+ if (oldAgent.status !== 'disabled' && raw.enabled === false) {
249
+ for (const ch of oldAgent.channelInstanceNames()) {
250
+ try {
251
+ await hooks.drainChannel(ch);
252
+ }
253
+ catch { }
254
+ try {
255
+ await hooks.disconnectChannel(ch);
256
+ }
257
+ catch { }
258
+ }
259
+ oldAgent.swapConfig(raw, merged);
260
+ oldAgent.status = 'disabled';
261
+ this.channelIndex.clear();
262
+ this.buildChannelIndex();
263
+ logger.info(`[Reload] Agent "${aidOrName}" disabled`);
264
+ return;
265
+ }
227
266
  const conflict = this.checkConflictForReload(raw, oldAgent.aid);
228
267
  if (conflict)
229
268
  throw new Error(`Channel conflict: ${conflict}`);
230
269
  const oldChannels = new Set(oldAgent.channelInstanceNames());
231
- // 计算新 channel keys(用 EvolAgent 的格式化)
232
- const newChannels = new Set(raw.channels.map(c => oldAgent.effectiveChannelName(c.type, c.name)));
270
+ // 计算新 channel keys:隐式 AUN + 显式非 AUN channels(与 channelInstanceNames 逻辑一致)
271
+ const aunKey = oldAgent.effectiveChannelName('aun', 'main');
272
+ const otherKeys = raw.channels.filter(c => c.type !== 'aun').map(c => oldAgent.effectiveChannelName(c.type, c.name));
273
+ const newChannels = new Set([aunKey, ...otherKeys]);
233
274
  const toRemove = [...oldChannels].filter(c => !newChannels.has(c));
234
275
  const toAdd = [...newChannels].filter(c => !oldChannels.has(c));
235
276
  const kept = [...oldChannels].filter(c => newChannels.has(c));
@@ -264,12 +305,7 @@ export class EvolAgentRegistry {
264
305
  addedSuccessfully.push(ch);
265
306
  }
266
307
  // truly kept 的 adapter 实例已经在 oldAgent.channels 里,无需迁移
267
- if (oldAgent.status === 'error' || oldAgent.status === 'disabled') {
268
- // 保持原态——swap 不改 status
269
- }
270
- else {
271
- oldAgent.status = 'running';
272
- }
308
+ oldAgent.status = 'running';
273
309
  // 重启触发器调度器(如果已初始化)
274
310
  if (oldAgent.triggerScheduler) {
275
311
  oldAgent.triggerScheduler.stop();
@@ -62,11 +62,11 @@ export class EvolAgent {
62
62
  }
63
63
  // ── Channels ──────────────────────────────────────────────────────────
64
64
  /**
65
- * effective channel key:`<aid>#<type>#<name>`。AUN 实例一个 agent 只有一条;
66
- * 其它类型靠 name 区分。
65
+ * effective channel key:`<type>#<urlEncode(selfPeerId)>#<name>`。
66
+ * AUN channel 的 selfPeerId 是 agent.aid,name 固定为 'main'。
67
67
  */
68
68
  effectiveChannelName(type, rawName) {
69
- return formatChannelKey({ aid: this.aid, type, name: rawName });
69
+ return formatChannelKey({ type, selfPeerId: this.aid, name: rawName });
70
70
  }
71
71
  channelInstanceNames() {
72
72
  // AUN channel 隐式存在(从 agent.aid 派生),不需要在 channels[] 里声明
@@ -97,7 +97,7 @@ export class EvolAgent {
97
97
  */
98
98
  isAunChannelKey(channelKey) {
99
99
  const parsed = tryParseChannelKey(channelKey);
100
- return parsed?.type === 'aun' && parsed.aid === this.aid;
100
+ return parsed?.type === 'aun' && parsed.selfPeerId === this.aid;
101
101
  }
102
102
  getOwner(channelKey) {
103
103
  if (this.isAunChannelKey(channelKey)) {
@@ -201,10 +201,10 @@ export class IMRenderer {
201
201
  call_id: callId || this.synthCallId(),
202
202
  name,
203
203
  ok,
204
- result,
205
- error,
206
- duration_ms: durationMs,
207
- text: descText,
204
+ ...(result !== undefined && { result }),
205
+ ...(error !== undefined && { error }),
206
+ ...(durationMs !== undefined && { duration_ms: durationMs }),
207
+ ...(descText !== undefined && { text: descText }),
208
208
  });
209
209
  this.messageTimestamps.push(Date.now());
210
210
  if (this.diagEnabled)
@@ -3,6 +3,8 @@ import { logger } from '../../utils/logger.js';
3
3
  import { StreamDebouncer } from './stream-debouncer.js';
4
4
  import { appendMessageLog, buildInboundEntry } from './message-log.js';
5
5
  import { buildEnvelope } from './message-processor.js';
6
+ import { chatDirPath } from '../session/session-fs-store.js';
7
+ import { resolvePaths } from '../../paths.js';
6
8
  /**
7
9
  * MessageBridge — Channel 与 Core 之间的消息桥梁
8
10
  *
@@ -74,8 +76,31 @@ export class MessageBridge {
74
76
  // 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
75
77
  const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
76
78
  const cmdContent = contentForCmd || content;
77
- if (this.cmdHandler.isCommand(cmdContent)) {
79
+ const isCmd = this.cmdHandler.isCommand(cmdContent);
80
+ if (isCmd) {
78
81
  logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
82
+ // 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
83
+ try {
84
+ const chatDir = chatDirPath(resolvePaths().sessionsDir, msg.channelType || effectiveChannelType, msg.channelId, msg.selfId);
85
+ const inboundEncrypt = msg.replyContext?.metadata?.encrypted != null ? !!(msg.replyContext.metadata.encrypted) : undefined;
86
+ const inboundChatmode = msg.replyContext?.metadata?.chatmode;
87
+ appendMessageLog(chatDir, buildInboundEntry({
88
+ from: msg.peerId || 'unknown',
89
+ to: msg.selfId || 'self',
90
+ chatType: msg.chatType || 'private',
91
+ groupId: msg.groupId ?? null,
92
+ msgId: msg.messageId ?? null,
93
+ content,
94
+ replyTo: msg.replyContext?.replyToMessageId ?? null,
95
+ permMode: null,
96
+ timestamp: Date.now(),
97
+ encrypt: inboundEncrypt,
98
+ chatmode: inboundChatmode,
99
+ }));
100
+ }
101
+ catch (e) {
102
+ logger.debug(`[MessageBridge] Failed to log inbound command: ${e}`);
103
+ }
79
104
  }
80
105
  if (await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
81
106
  logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId: `cmd-${msg.messageId || Date.now()}`, payload: { kind: 'command.result', text } });