evolclaw 3.1.5 → 3.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +53 -3
  2. package/dist/agents/claude-runner.js +69 -24
  3. package/dist/agents/kit-renderer.js +15 -4
  4. package/dist/aun/aid/agentmd.js +10 -3
  5. package/dist/aun/msg/group.js +2 -2
  6. package/dist/channels/aun.js +98 -12
  7. package/dist/channels/dingtalk.js +1 -1
  8. package/dist/channels/feishu.js +31 -9
  9. package/dist/channels/qqbot.js +1 -1
  10. package/dist/channels/wechat.js +1 -1
  11. package/dist/channels/wecom.js +1 -1
  12. package/dist/cli/agent.js +10 -11
  13. package/dist/cli/bench.js +1 -5
  14. package/dist/cli/help.js +8 -0
  15. package/dist/cli/index.js +91 -128
  16. package/dist/cli/init.js +37 -21
  17. package/dist/cli/link-rules.js +1 -7
  18. package/dist/cli/model.js +231 -6
  19. package/dist/config-store.js +1 -22
  20. package/dist/core/command-handler.js +181 -48
  21. package/dist/core/evolagent.js +0 -18
  22. package/dist/core/message/im-renderer.js +9 -20
  23. package/dist/core/message/message-bridge.js +7 -3
  24. package/dist/core/message/message-processor.js +138 -35
  25. package/dist/core/relation/peer-identity.js +23 -11
  26. package/dist/core/trigger/parser.js +4 -4
  27. package/dist/core/trigger/scheduler.js +20 -6
  28. package/dist/index.js +55 -5
  29. package/dist/ipc.js +1 -1
  30. package/dist/utils/error-utils.js +6 -0
  31. package/dist/utils/process-introspect.js +7 -5
  32. package/kits/docs/INDEX.md +4 -8
  33. package/kits/docs/context-assembly.md +1 -0
  34. package/kits/docs/evolclaw/INDEX.md +43 -0
  35. package/kits/docs/evolclaw/group.md +13 -6
  36. package/kits/docs/evolclaw/model.md +51 -0
  37. package/kits/docs/evolclaw/msg.md +5 -0
  38. package/kits/docs/venues/group.md +13 -1
  39. package/kits/eck_manifest.json +9 -0
  40. package/kits/rules/06-channel.md +5 -1
  41. package/kits/templates/system-fragments/baseagent.md +7 -1
  42. package/kits/templates/system-fragments/channel.md +7 -5
  43. package/kits/templates/system-fragments/commands.md +19 -0
  44. package/kits/templates/system-fragments/session.md +9 -0
  45. package/kits/templates/system-fragments/venue.md +15 -0
  46. package/package.json +3 -3
@@ -283,7 +283,8 @@ export class FeishuChannel {
283
283
  else if (msg.message_type === 'merge_forward') {
284
284
  const { text: mergedText, images: mergedImages } = await this.extractMergeForwardContent(msg.message_id, msg.chat_id);
285
285
  if (mergedText) {
286
- const finalContent = quotedText + mergedText;
286
+ // 直接发送合并转发时,parent_id 指向自己,引用解析会把相同内容填入 quotedText 导致重复,丢弃
287
+ const finalContent = mergedText;
287
288
  const allImages = [...quotedImages, ...mergedImages];
288
289
  await this.messageHandler({ channelId: msg.chat_id, content: finalContent, images: allImages.length > 0 ? allImages : undefined, peerId, peerName, messageId: msg.message_id, threadId, rootId, chatType });
289
290
  }
@@ -513,10 +514,15 @@ export class FeishuChannel {
513
514
  if (options.replyInThread) {
514
515
  replyData.reply_in_thread = true;
515
516
  }
516
- await this.client.im.message.reply({
517
+ const replyRes = await this.client.im.message.reply({
517
518
  path: { message_id: options.replyToMessageId },
518
519
  data: replyData
519
520
  });
521
+ if (options.replyInThread && options.onThreadCreated) {
522
+ const newThreadId = replyRes?.data?.thread_id;
523
+ if (newThreadId)
524
+ options.onThreadCreated(newThreadId);
525
+ }
520
526
  }
521
527
  else {
522
528
  await this.client.im.message.create({
@@ -710,13 +716,18 @@ export class FeishuChannel {
710
716
  // seenThreads 无时间戳,仅限容量(话题持久存在,不按时间清理)
711
717
  if (this.seenThreads.size > 1000)
712
718
  this.seenThreads.clear();
713
- // 重写文件,去掉过期条目
714
- if (this.config.seenMsgFile && this.seenMessages.size > 0) {
719
+ // 重写文件,去掉过期条目(仅在有记录被清理时才写)
720
+ if (cleaned > 0 && this.config.seenMsgFile) {
715
721
  try {
716
- const lines = [...this.seenMessages.entries()]
717
- .map(([id, ts]) => JSON.stringify({ id, ts }))
718
- .join('\n') + '\n';
719
- fs.writeFileSync(this.config.seenMsgFile, lines);
722
+ if (this.seenMessages.size === 0) {
723
+ fs.unlinkSync(this.config.seenMsgFile);
724
+ }
725
+ else {
726
+ const lines = [...this.seenMessages.entries()]
727
+ .map(([id, ts]) => JSON.stringify({ id, ts }))
728
+ .join('\n') + '\n';
729
+ fs.writeFileSync(this.config.seenMsgFile, lines);
730
+ }
720
731
  }
721
732
  catch { }
722
733
  }
@@ -1196,6 +1207,15 @@ export function buildResolvedV2(interaction, response) {
1196
1207
  });
1197
1208
  bodyElements.push({ tag: 'markdown', content: lines.join('\n') });
1198
1209
  }
1210
+ // CommandCard: 显示原有按钮列表(保留上下文)
1211
+ if (kind.kind === 'command-card' && kind.buttons?.length) {
1212
+ const lines = kind.buttons.map(btn => {
1213
+ const prefix = btn.command === action ? '✓' : '•';
1214
+ const cleanLabel = btn.label.replace(/^✓\s*/, '');
1215
+ return `${prefix} ${cleanLabel}`;
1216
+ });
1217
+ bodyElements.push({ tag: 'markdown', content: lines.join('\n') });
1218
+ }
1199
1219
  return {
1200
1220
  toast: { type: 'success', content: statusText },
1201
1221
  card: {
@@ -1403,7 +1423,7 @@ export class FeishuChannelPlugin {
1403
1423
  const adapter = {
1404
1424
  channelName: inst.name,
1405
1425
  channelKey: inst.name,
1406
- capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true },
1426
+ capabilities: { file: true, image: true, interaction: true, markdown: true, thought: false, status: true, thread: true },
1407
1427
  send: async (envelope, payload) => {
1408
1428
  const ctx = envelope.replyContext;
1409
1429
  const channelId = envelope.channelId;
@@ -1417,6 +1437,8 @@ export class FeishuChannelPlugin {
1417
1437
  const sendCtx = { ...(ctx ?? {}) };
1418
1438
  if (payload.kind === 'result.text' && payload.isFinal)
1419
1439
  sendCtx.title = '✅ 最终回复:';
1440
+ if (ctx?.metadata?.onThreadCreated)
1441
+ sendCtx.onThreadCreated = ctx.metadata.onThreadCreated;
1420
1442
  await channel.sendMessage(channelId, payload.text, sendCtx);
1421
1443
  return;
1422
1444
  }
@@ -335,7 +335,7 @@ export class QQBotChannelPlugin {
335
335
  const adapter = {
336
336
  channelName: inst.name,
337
337
  channelKey: inst.name,
338
- capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
338
+ capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false, thread: false },
339
339
  send: async (envelope, payload) => {
340
340
  const ctx = envelope.replyContext;
341
341
  const channelId = envelope.channelId;
@@ -731,7 +731,7 @@ export class WechatChannelPlugin {
731
731
  const adapter = {
732
732
  channelName: inst.name,
733
733
  channelKey: inst.name,
734
- capabilities: { file: false, image: false, interaction: false, markdown: false, thought: false, status: true },
734
+ capabilities: { file: false, image: false, interaction: false, markdown: false, thought: false, status: true, thread: false },
735
735
  send: async (envelope, payload) => {
736
736
  const channelId = envelope.channelId;
737
737
  switch (payload.kind) {
@@ -491,7 +491,7 @@ export class WecomChannelPlugin {
491
491
  const adapter = {
492
492
  channelName: inst.name,
493
493
  channelKey: inst.name,
494
- capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false },
494
+ capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false, thread: false },
495
495
  send: async (envelope, payload) => {
496
496
  const ctx = envelope.replyContext;
497
497
  const channelId = envelope.channelId;
package/dist/cli/agent.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import os from 'os';
4
3
  import readline from 'readline';
5
4
  import { resolvePaths, agentMdPath as getAgentMdPathFromPaths, aunPath as defaultAunPath } from '../paths.js';
6
5
  import { loadDefaults, loadAllAgents, loadAgent, saveAgent, ensureAgentDirSkeleton } from '../config-store.js';
@@ -11,11 +10,6 @@ import { commandExists } from '../utils/cross-platform.js';
11
10
  import { isCodexSdkAvailable } from '../agents/codex-runner.js';
12
11
  // ==================== Helpers ====================
13
12
  const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
14
- const BASEAGENT_ENV_KEY = {
15
- claude: 'ANTHROPIC_API_KEY',
16
- codex: 'OPENAI_API_KEY',
17
- gemini: 'GEMINI_API_KEY',
18
- };
19
13
  function isBaseagentAvailable(baseagent) {
20
14
  if (baseagent === 'codex')
21
15
  return isCodexSdkAvailable();
@@ -30,8 +24,7 @@ function pickDefaultBaseagent(available) {
30
24
  return available.includes('claude') ? 'claude' : available[0];
31
25
  }
32
26
  function buildBaseagentsBlock(chosen) {
33
- const env = BASEAGENT_ENV_KEY[chosen];
34
- return { [chosen]: env ? { apiKey: `$ENV:${env}` } : {} };
27
+ return { [chosen]: {} };
35
28
  }
36
29
  const DEFAULT_CHATMODE = { private: 'interactive', group: 'proactive', nothuman: 'proactive' };
37
30
  const DEFAULT_DISPATCH = 'mention';
@@ -296,11 +289,11 @@ export async function agentCreateInteractive(opts = {}) {
296
289
  const defaults = loadDefaults();
297
290
  const rootPath = defaults?.projects?.rootPath
298
291
  || (defaults?.projects?.defaultPath && path.dirname(defaults.projects.defaultPath))
299
- || path.join(os.homedir(), 'evolclaw-projects');
292
+ || resolvePaths().root + '/projects';
300
293
  suggestedProjectPath = deriveAgentProjectPath(rootPath, aid);
301
294
  }
302
295
  catch {
303
- suggestedProjectPath = deriveAgentProjectPath(path.join(os.homedir(), 'evolclaw-projects'), aid);
296
+ suggestedProjectPath = deriveAgentProjectPath(resolvePaths().root + '/projects', aid);
304
297
  }
305
298
  const projectInput = (await ask(`Project path [${suggestedProjectPath}]: `)).trim();
306
299
  const projectPath = projectInput || suggestedProjectPath;
@@ -633,7 +626,7 @@ export async function agentSyncAids() {
633
626
  const defaults = loadDefaults();
634
627
  const rootPath = defaults?.projects?.rootPath
635
628
  || (defaults?.projects?.defaultPath && path.dirname(defaults.projects.defaultPath))
636
- || path.join(os.homedir(), 'evolclaw-projects');
629
+ || resolvePaths().root + '/projects';
637
630
  const created = [];
638
631
  for (const aid of localAids) {
639
632
  if (existingAids.has(aid))
@@ -761,6 +754,12 @@ export async function agentSet(aid, key, rawValue) {
761
754
  return { ok: false, error: `Failed to read config: ${e?.message || e}` };
762
755
  }
763
756
  const value = parseJsonValue(rawValue);
757
+ // active_baseagent 白名单校验:只允许已知 baseagent,挡住把模型名(如 deepseek)误设为后端
758
+ if (key === 'active_baseagent') {
759
+ if (typeof value !== 'string' || !BASEAGENT_CANDIDATES.includes(value)) {
760
+ return { ok: false, error: `无效 active_baseagent: ${JSON.stringify(value)}(可选: ${BASEAGENT_CANDIDATES.join(' / ')})` };
761
+ }
762
+ }
764
763
  setNestedValue(config, key, value);
765
764
  try {
766
765
  saveAgent(config);
package/dist/cli/bench.js CHANGED
@@ -8,7 +8,7 @@ import { aidList, aidCreate } from '../aun/aid/identity.js';
8
8
  import { msgSend, msgPull } from '../aun/msg/index.js';
9
9
  import { getPackageRoot, aunPath as defaultAunPath } from '../paths.js';
10
10
  import { getAidStore, loadClient, SLOT } from '../aun/aid/store.js';
11
- import { isHelpFlag } from './help.js';
11
+ import { isHelpFlag, getArgValue } from './help.js';
12
12
  const execFileAsync = promisify(execFile);
13
13
  // ==================== ANSI ====================
14
14
  const GREEN = '\x1b[32m';
@@ -132,10 +132,6 @@ function percentile(sorted, p) {
132
132
  const idx = Math.ceil((p / 100) * sorted.length) - 1;
133
133
  return sorted[Math.max(0, idx)];
134
134
  }
135
- function getArgValue(args, flag) {
136
- const idx = args.indexOf(flag);
137
- return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : undefined;
138
- }
139
135
  // ==================== Promise Pool ====================
140
136
  function withTimeout(promise, ms, label) {
141
137
  return new Promise((resolve, reject) => {
package/dist/cli/help.js CHANGED
@@ -21,3 +21,11 @@ export function wantsHelp(args) {
21
21
  return true;
22
22
  return false;
23
23
  }
24
+ /**
25
+ * 取出 `--flag <value>` 形式的参数值。
26
+ * flag 不存在或其后无值时返回 undefined。
27
+ */
28
+ export function getArgValue(args, flag) {
29
+ const idx = args.indexOf(flag);
30
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
31
+ }
package/dist/cli/index.js CHANGED
@@ -11,7 +11,7 @@ import { migrateProject } from '../config-store.js';
11
11
  import { cmdInit } from './init.js';
12
12
  import { ipcQuery } from '../ipc.js';
13
13
  import { cmdInitWechat, cmdInitFeishu, cmdInitDingtalk, cmdInitQQBot, cmdInitWecom } from './init-channel.js';
14
- import { isHelpFlag, wantsHelp } from './help.js';
14
+ import { isHelpFlag, wantsHelp, getArgValue } from './help.js';
15
15
  import * as platform from '../utils/cross-platform.js';
16
16
  import { EventBus } from '../core/event-bus.js';
17
17
  import { tryUpgrade, tryUpgradeAunSdk } from '../utils/npm-ops.js';
@@ -292,6 +292,13 @@ async function cmdStart() {
292
292
  // 旧配置自动迁移(evolclaw.json → 新结构)
293
293
  const { autoMigrateIfNeeded } = await import('../config-store.js');
294
294
  autoMigrateIfNeeded();
295
+ // 未初始化时自动引导
296
+ const defaults = loadDefaults();
297
+ if (!defaults || !defaults.baseagents || Object.keys(defaults.baseagents).length === 0) {
298
+ console.log('⚡ 未检测到初始化配置,自动启动初始化向导...\n');
299
+ await cmdInit();
300
+ return;
301
+ }
295
302
  // 检查至少有一个 self-agent
296
303
  const { agents, skipped } = loadAllAgents();
297
304
  if (agents.length === 0) {
@@ -313,7 +320,7 @@ async function cmdStart() {
313
320
  const aliveMains = status.mains.filter(m => m.alive);
314
321
  if (aliveMains.length > 0) {
315
322
  const first = aliveMains[0];
316
- console.log(`❌ EvolClaw is already running (PID: ${aliveMains.map(m => m.record.pid).join(', ')})`);
323
+ console.log(` EvolClaw is already running (PID: ${aliveMains.map(m => m.record.pid).join(', ')})`);
317
324
  console.log(` 启动于: ${new Date(first.record.startedAtIso).toLocaleString()}`);
318
325
  console.log(` 启动方式: ${first.record.launchedBy}`);
319
326
  // 报告 AID 状态
@@ -371,14 +378,7 @@ async function cmdStart() {
371
378
  const checkReady = () => {
372
379
  // ready signal 出现(优先检查,避免 Windows 上误判进程状态)
373
380
  if (fs.existsSync(p.readySignal)) {
374
- const pkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'package.json'), 'utf-8'));
375
- let aunVer = 'unknown';
376
- try {
377
- const aunPkg = JSON.parse(fs.readFileSync(path.join(getPackageRoot(), 'node_modules', '@agentunion', 'fastaun', 'package.json'), 'utf-8'));
378
- aunVer = aunPkg.version;
379
- }
380
- catch { /* ignore */ }
381
- console.log(`✓ EvolClaw v${pkg.version} started successfully (PID: ${childPid}) fastaun v${aunVer}`);
381
+ console.log(`✓ EvolClaw started successfully (PID: ${childPid})`);
382
382
  console.log(` EVOLCLAW_HOME: ${resolveRoot()}`);
383
383
  console.log(` Logs: ${p.logs}/`);
384
384
  // 从主日志提取渠道连接摘要
@@ -495,7 +495,6 @@ async function cmdStop() {
495
495
  }
496
496
  async function cmdRestart(opts = {}) {
497
497
  const cmdStartedAt = Date.now();
498
- printStartupInfo();
499
498
  console.log('🔄 Restarting EvolClaw...');
500
499
  // 版本检查与自动升级
501
500
  console.log('📦 Checking for updates...');
@@ -2103,95 +2102,22 @@ async function cmdWatchAid() {
2103
2102
  platform.onShutdown(cleanup);
2104
2103
  }
2105
2104
  async function cmdWatchWeb() {
2106
- const p = resolvePaths();
2107
- fs.mkdirSync(p.instanceDir, { recursive: true });
2108
- const useColor = !!process.stdout.isTTY;
2109
- const RST = useColor ? '\x1b[0m' : '';
2110
- const DIM = useColor ? '\x1b[2m' : '';
2111
- const BOLD = useColor ? '\x1b[1m' : '';
2112
- const CYAN = useColor ? '\x1b[36m' : '';
2113
- const GREEN = useColor ? '\x1b[32m' : '';
2114
- const YELLOW = useColor ? '\x1b[33m' : '';
2115
- const logLine = (line) => {
2116
- const t = new Date();
2117
- const ts = `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}:${String(t.getSeconds()).padStart(2, '0')}`;
2118
- process.stdout.write(`${DIM}${ts}${RST} ${line}\n`);
2119
- };
2120
- // 调试日志文件:每次运行 watch web 时清空,便于建立调试闭环
2121
- // 查看 sessions 调试日志 → 读这个文件
2122
- const logFile = path.join(p.logs, 'watch-web.log');
2123
- try {
2124
- fs.mkdirSync(p.logs, { recursive: true });
2125
- fs.writeFileSync(logFile, `# watch-web debug log\n# started ${new Date().toISOString()} pid=${process.pid}\n`);
2126
- }
2127
- catch { /* best effort */ }
2128
- const fileLog = (line) => {
2129
- const t = new Date();
2130
- const ts = `${String(t.getHours()).padStart(2, '0')}:${String(t.getMinutes()).padStart(2, '0')}:${String(t.getSeconds()).padStart(2, '0')}.${String(t.getMilliseconds()).padStart(3, '0')}`;
2105
+ // ecweb 是独立插件包(可执行命令),按需安装。
2106
+ // 复用 npm-ops.npmInstallGlobal(含 EACCES→sudo 回退、Windows npm.cmd、超时)。
2107
+ const { execFileSync } = await import('child_process');
2108
+ const home = resolvePaths().root;
2109
+ if (!platform.commandExists('ecweb')) {
2110
+ process.stdout.write('📦 ecweb 未安装,正在从 npm 安装...\n');
2111
+ const { npmInstallGlobal } = await import('../utils/npm-ops.js');
2131
2112
  try {
2132
- fs.appendFileSync(logFile, `${ts} ${line.replace(/\x1b\[[0-9;]*m/g, '')}\n`);
2113
+ await npmInstallGlobal('ecweb');
2133
2114
  }
2134
- catch { /* ignore */ }
2135
- };
2136
- // 同时输出到终端和日志文件
2137
- const log = (line) => { logLine(line); fileLog(line); };
2138
- const { startWatchWebServer } = await import('./watch-web/server.js');
2139
- let handle;
2140
- try {
2141
- handle = await startWatchWebServer({ log });
2142
- }
2143
- catch (e) {
2144
- console.error(`❌ 启动 Web 服务失败: ${e?.message || e}`);
2145
- process.exit(1);
2146
- }
2147
- // 注册 instance 文件
2148
- const instanceFile = path.join(p.instanceDir, `watch-web-${process.pid}.json`);
2149
- fs.writeFileSync(instanceFile, JSON.stringify({
2150
- pid: process.pid, startedAt: Date.now(), startedAtIso: new Date().toISOString(),
2151
- type: 'watch-web', port: handle.port,
2152
- }, null, 2));
2153
- // 列出本机访问地址
2154
- const os = await import('os');
2155
- const ifaces = os.networkInterfaces();
2156
- const lanIps = [];
2157
- for (const list of Object.values(ifaces)) {
2158
- for (const ni of list || []) {
2159
- if (ni.family === 'IPv4' && !ni.internal)
2160
- lanIps.push(ni.address);
2161
- }
2162
- }
2163
- process.stdout.write(`\n${BOLD}${CYAN}🔭 EvolClaw Watch Web${RST}\n\n`);
2164
- process.stdout.write(` ${BOLD}配对码:${RST} ${GREEN}${BOLD}${handle.pairingCode}${RST} ${DIM}(5 分钟内有效,配对后 token 缓存 24h 自动续期)${RST}\n\n`);
2165
- process.stdout.write(` ${BOLD}本机:${RST} http://localhost:${handle.port}\n`);
2166
- for (const ip of lanIps) {
2167
- process.stdout.write(` ${BOLD}局域网:${RST} http://${ip}:${handle.port}\n`);
2168
- }
2169
- process.stdout.write(`\n ${DIM}绑定 0.0.0.0,远程可访问。按任意键退出。${RST}\n`);
2170
- process.stdout.write(` ${DIM}调试日志: ${logFile}${RST}\n\n`);
2171
- const cleanup = () => {
2172
- try {
2173
- fs.unlinkSync(instanceFile);
2115
+ catch (e) {
2116
+ process.stderr.write(`❌ 安装 ecweb 失败: ${e?.stderr || e?.message || e}\n 可手动安装: npm install -g ecweb\n`);
2117
+ process.exit(1);
2174
2118
  }
2175
- catch { }
2176
- handle.close().finally(() => process.exit(0));
2177
- };
2178
- process.on('exit', () => { try {
2179
- fs.unlinkSync(instanceFile);
2180
- }
2181
- catch { } });
2182
- process.on('SIGINT', cleanup);
2183
- process.on('SIGTERM', cleanup);
2184
- platform.onShutdown(cleanup);
2185
- // 按任意键退出
2186
- if (process.stdin.isTTY) {
2187
- process.stdin.setRawMode(true);
2188
- process.stdin.resume();
2189
- process.stdin.on('data', (key) => {
2190
- logLine(`${YELLOW}收到退出指令,关闭服务…${RST}`);
2191
- cleanup();
2192
- });
2193
2119
  }
2194
- await new Promise(() => { });
2120
+ execFileSync('ecweb', ['--home', home], { stdio: 'inherit' });
2195
2121
  }
2196
2122
  async function cmdRestartMonitor() {
2197
2123
  const p = resolvePaths();
@@ -2719,8 +2645,6 @@ async function cmdMv(oldDir, newDir) {
2719
2645
  console.log('✓ 项目目录已移动');
2720
2646
  if (r.evolclawDbUpdated > 0)
2721
2647
  console.log(`✓ EvolClaw 会话存储已更新 (${r.evolclawDbUpdated} 条记录)`);
2722
- if (r.evolclawConfigUpdated)
2723
- console.log('✓ agent config projects.list 已更新');
2724
2648
  console.log('\n迁移完成!');
2725
2649
  }
2726
2650
  catch (e) {
@@ -2894,7 +2818,7 @@ Agent:
2894
2818
  // ==================== Agent ====================
2895
2819
  async function cmdAgent(args) {
2896
2820
  const sub = args[0];
2897
- const formatJson = args.includes('--format') && args[args.indexOf('--format') + 1] === 'json';
2821
+ const formatJson = getArgValue(args, '--format') === 'json';
2898
2822
  if (!sub || isHelpFlag(sub)) {
2899
2823
  console.log(`用法: evolclaw agent <command>
2900
2824
 
@@ -3469,7 +3393,7 @@ function resolveAunPath(args) {
3469
3393
  }
3470
3394
  async function cmdAid(args) {
3471
3395
  const sub = args[0];
3472
- const formatJson = args.includes('--format') && args[args.indexOf('--format') + 1] === 'json';
3396
+ const formatJson = getArgValue(args, '--format') === 'json';
3473
3397
  const aunPath = resolveAunPath(args);
3474
3398
  if (!sub || isHelpFlag(sub)) {
3475
3399
  console.log(`用法: evolclaw aid <command>
@@ -3970,6 +3894,10 @@ async function cmdRpc(args) {
3970
3894
 
3971
3895
  每行 JSON 格式: {"method":"<namespace.method>","params":{...}}
3972
3896
 
3897
+ Options:
3898
+ --app <name> 指定应用 slot(独立消费通道)。仅对 message.pull / group.pull
3899
+ 等消费类方法有意义——隔离 seq 游标与消息过滤;默认与 daemon 共享通道。
3900
+
3973
3901
  示例:
3974
3902
  evolclaw rpc --as alice.agentid.pub --params '{"method":"message.send","params":{"to":"bob.agentid.pub","payload":{"type":"text","text":"hello"}}}'
3975
3903
  evolclaw rpc --as alice.agentid.pub --params calls.jsonl`);
@@ -3978,6 +3906,7 @@ async function cmdRpc(args) {
3978
3906
  const asIdx = args.indexOf('--as');
3979
3907
  const paramsIdx = args.indexOf('--params');
3980
3908
  const aunPath = resolveAunPath(args);
3909
+ const appSlot = getArgValue(args, '--app');
3981
3910
  if (asIdx === -1 || asIdx + 1 >= args.length) {
3982
3911
  console.error('❌ 缺少 --as <aid>');
3983
3912
  process.exit(1);
@@ -4022,11 +3951,11 @@ async function cmdRpc(args) {
4022
3951
  }
4023
3952
  const { rpcCall, rpcBatch } = await import('../aun/rpc/index.js');
4024
3953
  if (calls.length === 1) {
4025
- const result = await rpcCall(aid, calls[0].method, calls[0].params, { aunPath });
3954
+ const result = await rpcCall(aid, calls[0].method, calls[0].params, { aunPath, slotId: appSlot });
4026
3955
  console.log(JSON.stringify(result));
4027
3956
  }
4028
3957
  else {
4029
- const results = await rpcBatch(aid, calls, { aunPath });
3958
+ const results = await rpcBatch(aid, calls, { aunPath, slotId: appSlot });
4030
3959
  for (const r of results) {
4031
3960
  console.log(JSON.stringify(r));
4032
3961
  }
@@ -4036,7 +3965,7 @@ async function cmdRpc(args) {
4036
3965
  async function cmdStorage(args) {
4037
3966
  const sub = args[0];
4038
3967
  const aunPath = resolveAunPath(args);
4039
- const formatJson = args.includes('--format') && args[args.indexOf('--format') + 1] === 'json';
3968
+ const formatJson = getArgValue(args, '--format') === 'json';
4040
3969
  if (!sub || isHelpFlag(sub)) {
4041
3970
  console.log(`用法: evolclaw storage <command> <aid> [options]
4042
3971
 
@@ -4183,7 +4112,7 @@ Commands:
4183
4112
  async function cmdMsg(args) {
4184
4113
  const sub = args[0];
4185
4114
  const aunPath = resolveAunPath(args);
4186
- const formatJson = args.includes('--format') && args[args.indexOf('--format') + 1] === 'json';
4115
+ const formatJson = getArgValue(args, '--format') === 'json';
4187
4116
  const appIdx = args.indexOf('--app');
4188
4117
  const appSlot = appIdx >= 0 ? args[appIdx + 1] : undefined;
4189
4118
  if (!sub || isHelpFlag(sub)) {
@@ -4207,6 +4136,8 @@ Options:
4207
4136
  --content-type <mime> 显式覆盖 MIME(仅 --file 模式)
4208
4137
  --text <说明> 附件说明文字(仅 --file 模式)
4209
4138
  --transcript <text> 语音转写(仅 --as voice)
4139
+ -- end-of-options:其后所有参数按正文处理
4140
+ (用于发送恰好等于某 flag 的文本,如 send a b -- --encrypt)
4210
4141
 
4211
4142
  示例:
4212
4143
  evolclaw msg send alice.agentid.pub bob.agentid.pub "hello"
@@ -4465,7 +4396,7 @@ Options:
4465
4396
  async function cmdGroup(args) {
4466
4397
  const sub = args[0];
4467
4398
  const aunPath = resolveAunPath(args);
4468
- const formatJson = args.includes('--format') && args[args.indexOf('--format') + 1] === 'json';
4399
+ const formatJson = getArgValue(args, '--format') === 'json';
4469
4400
  const appIdx = args.indexOf('--app');
4470
4401
  const appSlot = appIdx >= 0 ? args[appIdx + 1] : undefined;
4471
4402
  if (!sub || isHelpFlag(sub)) {
@@ -4496,8 +4427,11 @@ async function cmdGroup(args) {
4496
4427
  Options:
4497
4428
  --app <name> 指定应用 slot(独立消费通道,不影响 daemon)
4498
4429
  --format json 输出 JSON 格式
4499
- --mention <aid> 发送时 @ 某个成员(可多次)
4430
+ --encrypt 启用端到端加密(仅 send)
4431
+ --mention <aid> 发送时 @ 某个成员(可多次,或用逗号分隔多个 aid)
4500
4432
  --mention-all 发送时 @ 所有人
4433
+ -- end-of-options:其后所有参数按正文处理
4434
+ (用于发送恰好等于某 flag 的文本,如 send a g -- --encrypt)
4501
4435
 
4502
4436
  示例:
4503
4437
  evolclaw group create alice.agentid.pub "Dev Team" --visibility private
@@ -4529,12 +4463,23 @@ Options:
4529
4463
  }
4530
4464
  return gid;
4531
4465
  };
4532
- // 收集 --mention(可多次)
4466
+ // 收集 --mention(可多次;每次的值支持逗号分隔多个 aid)
4533
4467
  const collectMentions = () => {
4534
4468
  const mentions = [];
4535
- for (let i = 0; i < args.length - 1; i++) {
4536
- if (args[i] === '--mention') {
4537
- mentions.push({ aid: args[i + 1] });
4469
+ for (let i = 0; i < args.length; i++) {
4470
+ if (args[i] !== '--mention')
4471
+ continue;
4472
+ const val = args[i + 1];
4473
+ if (val === undefined || val.startsWith('--')) {
4474
+ console.error(`❌ --mention 后面缺少 <aid>`);
4475
+ process.exit(1);
4476
+ }
4477
+ for (const aid of val.split(',').map(s => s.trim()).filter(Boolean)) {
4478
+ if (!isValidAid(aid)) {
4479
+ console.error(`❌ --mention 的 aid 无效: ${aid}`);
4480
+ process.exit(1);
4481
+ }
4482
+ mentions.push({ aid });
4538
4483
  }
4539
4484
  }
4540
4485
  if (args.includes('--mention-all')) {
@@ -4828,32 +4773,50 @@ Options:
4828
4773
  process.exit(1);
4829
4774
  }
4830
4775
  // ==================== Main ====================
4831
- function getArgValue(args, flag) {
4832
- const idx = args.indexOf(flag);
4833
- return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : undefined;
4834
- }
4835
4776
  /**
4836
- * 收集位置参数(从 startIdx 开始),跳过 flag 及其值。
4837
- * 已知"取值"的 flag 会消耗下一个 arg;已知"开关"的 flag 只占自身。
4777
+ * 收集位置参数(从 startIdx 开始)。
4778
+ *
4779
+ * flag 判定采用**精确匹配已知 flag 集合**,而非 `startsWith('--')`——
4780
+ * 这样"正文恰好以 -- 开头"(如消息文本 `--file 坏了`)不会被误当 flag 吞掉。
4781
+ * 仅当 token 精确等于某个已知 flag 时才按 flag 处理:
4782
+ * - VALUE_FLAGS:消耗自身 + 下一个 arg(flag 的值)
4783
+ * - BOOLEAN_FLAGS:仅消耗自身
4784
+ * 其余以 -- 开头但不在集合中的 token,一律视为正文。
4785
+ *
4786
+ * 另支持 POSIX `--` end-of-options 分隔符:遇到单独的 `--` 后,
4787
+ * 其后所有 token 无条件按正文处理(用于发送精确等于某 flag 的文本,如 `-- --encrypt`)。
4838
4788
  */
4789
+ const VALUE_FLAGS = new Set([
4790
+ '--format', '--app', '--after-seq', '--limit', '--file', '--link',
4791
+ '--payload', '--title', '--description', '--text', '--transcript',
4792
+ '--as', '--content-type', '--mention', '--visibility', '--join-mode',
4793
+ '--group-id', '--name', '--message', '--answer', '--page', '--size',
4794
+ '--aun-path', '--thread',
4795
+ ]);
4796
+ const BOOLEAN_FLAGS = new Set([
4797
+ '--encrypt', '--mention-all',
4798
+ ]);
4839
4799
  function collectPositional(args, startIdx) {
4840
- const VALUE_FLAGS = new Set([
4841
- '--format', '--app', '--after-seq', '--limit', '--file', '--link',
4842
- '--payload', '--title', '--description', '--text', '--transcript',
4843
- '--as', '--content-type', '--mention', '--visibility', '--join-mode',
4844
- '--group-id', '--name', '--message', '--answer', '--page', '--size',
4845
- '--aun-path',
4846
- ]);
4847
4800
  const out = [];
4801
+ let endOfFlags = false;
4848
4802
  for (let i = startIdx; i < args.length; i++) {
4849
4803
  const a = args[i];
4850
- if (a.startsWith('--')) {
4851
- if (VALUE_FLAGS.has(a))
4852
- i++; // 跳过 flag 的值
4853
- // else: 开关 flag,自身已被跳过
4804
+ if (endOfFlags) {
4805
+ out.push(a);
4806
+ continue;
4807
+ }
4808
+ if (a === '--') {
4809
+ endOfFlags = true;
4854
4810
  continue;
4855
4811
  }
4856
- out.push(a);
4812
+ if (VALUE_FLAGS.has(a)) {
4813
+ i++;
4814
+ continue;
4815
+ } // 精确匹配取值 flag:跳过其值
4816
+ if (BOOLEAN_FLAGS.has(a)) {
4817
+ continue;
4818
+ } // 精确匹配开关 flag:仅跳过自身
4819
+ out.push(a); // 其余(含以 -- 开头的未知 token)= 正文
4857
4820
  }
4858
4821
  return out;
4859
4822
  }