evolclaw 3.2.0 → 3.3.0

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 (83) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +1 -2
  3. package/dist/agents/{resolve.js → baseagent.js} +34 -5
  4. package/dist/agents/claude-runner.js +120 -27
  5. package/dist/agents/codex-app-server-client.js +364 -0
  6. package/dist/agents/codex-runner.js +1069 -141
  7. package/dist/agents/gemini-runner.js +2 -2
  8. package/dist/agents/runner-types.js +28 -0
  9. package/dist/aun/aid/store.js +1 -1
  10. package/dist/aun/storage/download.js +1 -1
  11. package/dist/aun/storage/upload.js +13 -1
  12. package/dist/channels/aun.js +406 -293
  13. package/dist/channels/dingtalk.js +77 -140
  14. package/dist/channels/feishu.js +97 -150
  15. package/dist/channels/qqbot.js +75 -138
  16. package/dist/channels/wechat.js +75 -136
  17. package/dist/channels/wecom.js +75 -138
  18. package/dist/cli/agent.js +8 -5
  19. package/dist/cli/index.js +177 -44
  20. package/dist/cli/init.js +33 -6
  21. package/dist/cli/model.js +1 -1
  22. package/dist/cli/stats.js +558 -0
  23. package/dist/cli/version.js +87 -0
  24. package/dist/cli/watch-msg.js +5 -2
  25. package/dist/config-store.js +12 -6
  26. package/dist/core/channel-loader.js +84 -82
  27. package/dist/core/command-handler.js +473 -114
  28. package/dist/core/evolagent-registry.js +1 -0
  29. package/dist/core/evolagent.js +1 -1
  30. package/dist/core/interaction-router.js +8 -0
  31. package/dist/core/message/command-handler-agent-control.js +63 -1
  32. package/dist/core/message/im-renderer.js +35 -13
  33. package/dist/core/message/items-formatter.js +9 -1
  34. package/dist/core/message/message-bridge.js +49 -21
  35. package/dist/core/message/message-log.js +1 -0
  36. package/dist/core/message/message-processor.js +295 -35
  37. package/dist/core/message/message-queue.js +2 -2
  38. package/dist/core/message/pending-hints.js +232 -0
  39. package/dist/core/message/response-depth.js +56 -0
  40. package/dist/core/model/model-catalog.js +1 -1
  41. package/dist/core/model/model-scope.js +2 -2
  42. package/dist/core/permission.js +9 -12
  43. package/dist/core/relation/peer-identity.js +16 -1
  44. package/dist/core/session/adapters/codex-session-file-adapter.js +4 -2
  45. package/dist/core/session/session-manager.js +27 -13
  46. package/dist/core/session/session-title.js +26 -0
  47. package/dist/core/stats/billing.js +151 -0
  48. package/dist/core/stats/budget.js +93 -0
  49. package/dist/core/stats/db.js +314 -0
  50. package/dist/core/stats/eck-vars.js +84 -0
  51. package/dist/core/stats/index.js +10 -0
  52. package/dist/core/stats/normalizer.js +78 -0
  53. package/dist/core/stats/query.js +760 -0
  54. package/dist/core/stats/writer.js +115 -0
  55. package/dist/core/trigger/manager.js +34 -0
  56. package/dist/core/trigger/parser.js +9 -3
  57. package/dist/core/trigger/scheduler.js +20 -17
  58. package/dist/{agents → eck}/manifest-engine.js +20 -1
  59. package/dist/{agents → eck}/message-renderer.js +24 -1
  60. package/dist/index.js +130 -8
  61. package/dist/ipc.js +17 -1
  62. package/dist/utils/cross-platform.js +23 -5
  63. package/dist/utils/ecweb-pair.js +20 -0
  64. package/dist/utils/stats.js +14 -0
  65. package/kits/docs/evolclaw/INDEX.md +3 -1
  66. package/kits/docs/evolclaw/fs-architecture.md +1215 -0
  67. package/kits/docs/evolclaw/fs.md +131 -0
  68. package/kits/docs/evolclaw/group-fs.md +209 -0
  69. package/kits/docs/evolclaw/stats.md +70 -0
  70. package/kits/docs/venues/aun-group.md +29 -6
  71. package/kits/docs/venues/group.md +5 -4
  72. package/kits/eck_manifest.json +12 -0
  73. package/kits/eck_message_manifest.json +30 -3
  74. package/kits/rules/05-venue.md +1 -1
  75. package/kits/templates/message-fragments/inject-default.md +2 -0
  76. package/kits/templates/system-fragments/response-depth.md +16 -0
  77. package/package.json +4 -4
  78. package/dist/agents/baseagent-normalize.js +0 -19
  79. package/dist/core/relation/peer-key.js +0 -16
  80. package/dist/evolclaw-config.js +0 -11
  81. package/dist/utils/channel-helpers.js +0 -46
  82. /package/dist/core/{cache/file-cache.js → daemon-file-cache.js} +0 -0
  83. /package/dist/{agents → eck}/kit-renderer.js +0 -0
@@ -1,7 +1,7 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { logger } from '../utils/logger.js';
3
3
  import { requireOptional } from '../utils/npm-ops.js';
4
- import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
4
+ import { resolveShowActivities, showActivitiesPolicy } from '../core/channel-loader.js';
5
5
  import { formatItemsAsText } from '../core/message/items-formatter.js';
6
6
  // ── WecomChannel ───────────────────────────────────────────────────────────────
7
7
  export class WecomChannel {
@@ -465,143 +465,80 @@ function isValidCredential(value) {
465
465
  }
466
466
  export class WecomChannelPlugin {
467
467
  name = 'wecom';
468
- isEnabled(config) {
469
- const raw = config.channels?.wecom;
470
- if (!raw)
471
- return false;
472
- if (Array.isArray(raw)) {
473
- return raw.some(inst => inst.enabled !== false && isValidCredential(inst.botId) && isValidCredential(inst.secret));
474
- }
475
- if (raw.enabled === false)
476
- return false;
477
- return isValidCredential(raw.botId) && isValidCredential(raw.secret);
478
- }
479
- async createChannels(config) {
480
- const instances = normalizeChannelInstances(config.channels?.wecom, 'wecom');
481
- const result = [];
482
- for (const inst of instances) {
483
- if (inst.enabled === false)
484
- continue;
485
- if (!isValidCredential(inst.botId) || !isValidCredential(inst.secret))
486
- continue;
487
- const channel = new WecomChannel({
488
- botId: inst.botId,
489
- secret: inst.secret,
490
- });
491
- const adapter = {
492
- channelName: inst.name,
493
- channelKey: inst.name,
494
- capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false, thread: false },
495
- send: async (envelope, payload) => {
496
- const ctx = envelope.replyContext;
497
- const channelId = envelope.channelId;
498
- switch (payload.kind) {
499
- case 'result.text':
500
- case 'command.result':
501
- case 'command.error':
502
- case 'system.notice':
503
- case 'system.error':
504
- case 'result.error':
505
- await channel.sendMessage(channelId, payload.text);
506
- return;
507
- case 'result.file':
508
- await channel.sendFile(channelId, payload.filePath);
509
- return;
510
- case 'result.image':
511
- await channel.sendImage(channelId, payload.data);
512
- return;
513
- case 'activity.batch': {
514
- const filtered = payload.items.filter((i) => !(i.kind === 'tool_result' && i.ok));
515
- const text = formatItemsAsText(filtered);
516
- if (text)
517
- await channel.sendMessage(channelId, text);
518
- return;
519
- }
520
- case 'interaction':
521
- if (payload.fallbackText)
522
- await channel.sendMessage(channelId, payload.fallbackText);
523
- return;
524
- case 'status.started':
525
- case 'status.completed':
526
- case 'status.interrupted':
527
- case 'status.error':
528
- case 'status.timeout':
529
- case 'status.progress':
530
- case 'custom':
531
- return;
532
- default:
533
- logger.warn(`[WeCom] Unhandled payload kind: ${payload.kind}`);
468
+ async createInstance(inst, ctx) {
469
+ if (inst.enabled === false)
470
+ return null;
471
+ if (!isValidCredential(inst.botId) || !isValidCredential(inst.secret))
472
+ return null;
473
+ const channel = new WecomChannel({
474
+ botId: inst.botId,
475
+ secret: inst.secret,
476
+ });
477
+ const mode = resolveShowActivities(inst);
478
+ const adapter = {
479
+ channelName: inst.name,
480
+ channelKey: inst.name,
481
+ capabilities: { file: true, image: true, interaction: false, markdown: true, thought: false, status: false, thread: false },
482
+ send: async (envelope, payload) => {
483
+ const channelId = envelope.channelId;
484
+ switch (payload.kind) {
485
+ case 'result.text':
486
+ case 'command.result':
487
+ case 'command.error':
488
+ case 'system.notice':
489
+ case 'system.error':
490
+ case 'result.error':
491
+ await channel.sendMessage(channelId, payload.text);
492
+ return;
493
+ case 'result.file':
494
+ await channel.sendFile(channelId, payload.filePath);
495
+ return;
496
+ case 'result.image':
497
+ await channel.sendImage(channelId, payload.data);
498
+ return;
499
+ case 'activity.batch': {
500
+ const filtered = payload.items.filter((i) => !(i.kind === 'tool_result' && i.ok));
501
+ const text = formatItemsAsText(filtered);
502
+ if (text)
503
+ await channel.sendMessage(channelId, text);
504
+ return;
534
505
  }
535
- },
536
- };
537
- const policy = {
538
- canSwitchProject: (_chatType, identity) => identity === 'owner' || identity === 'admin',
539
- canListProjects: (_chatType, identity) => identity === 'owner' || identity === 'admin',
540
- canCreateSession: () => true,
541
- canDeleteSession: () => true,
542
- canImportCliSession: (_chatType, identity) => identity === 'owner' || identity === 'admin',
543
- messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
544
- showMiddleResult: (chatType, identity) => {
545
- const mode = getChannelShowActivities(config, inst.name);
546
- if (mode === 'none')
547
- return false;
548
- if (mode === 'dm-only')
549
- return chatType === 'private';
550
- if (mode === 'owner-dm-only')
551
- return chatType === 'private' && identity === 'owner';
552
- return true;
553
- },
554
- showIdleMonitor: (chatType, identity) => {
555
- const mode = getChannelShowActivities(config, inst.name);
556
- if (mode === 'none')
557
- return false;
558
- if (mode === 'dm-only')
559
- return chatType === 'private';
560
- if (mode === 'owner-dm-only')
561
- return chatType === 'private' && identity === 'owner';
562
- return true;
563
- },
564
- accumulateErrors: () => true,
565
- };
566
- const options = {
567
- fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g,
568
- supportsImages: true,
569
- flushDelay: inst.flushDelay,
570
- };
571
- result.push({
572
- channelType: 'wecom',
573
- adapter,
574
- channel,
575
- policy,
576
- options,
577
- connect: () => channel.connect(),
578
- disconnect: () => channel.disconnect(),
579
- onProjectPathRequest: () => Promise.resolve(config.projects?.defaultPath || process.cwd()),
580
- registerBridge(bridge, channelType) {
581
- bridge.register(adapter.channelName, (handler) => channel.onMessage(async (event) => {
582
- handler({
583
- channel: adapter.channelName,
584
- channelType,
585
- channelId: event.channelId,
586
- selfAID: inst.agentName,
587
- content: event.content,
588
- images: event.images,
589
- chatType: event.chatType || 'private',
590
- peerId: event.peerId || '',
591
- peerName: event.peerName,
592
- messageId: event.messageId,
593
- });
594
- }), (channelId, text) => channel.sendMessage(channelId, text), adapter, channelType);
595
- },
596
- });
597
- }
598
- return result;
599
- }
600
- async createChannel(config) {
601
- const instances = await this.createChannels(config);
602
- if (instances.length === 0) {
603
- throw new Error('WeCom config missing or invalid');
604
- }
605
- return instances[0];
506
+ case 'interaction':
507
+ if (payload.fallbackText)
508
+ await channel.sendMessage(channelId, payload.fallbackText);
509
+ return;
510
+ default: return;
511
+ }
512
+ },
513
+ };
514
+ const policy = {
515
+ canSwitchProject: (_, identity) => identity === 'owner' || identity === 'admin',
516
+ canListProjects: (_, identity) => identity === 'owner' || identity === 'admin',
517
+ canCreateSession: () => true,
518
+ canDeleteSession: () => true,
519
+ canImportCliSession: (_, identity) => identity === 'owner' || identity === 'admin',
520
+ messagePrefix: (chatType, peerName) => (chatType === 'group' && peerName) ? `[${peerName}] ` : '',
521
+ showMiddleResult: (chatType, identity) => showActivitiesPolicy(mode, chatType, identity),
522
+ showIdleMonitor: (chatType, identity) => showActivitiesPolicy(mode, chatType, identity),
523
+ accumulateErrors: () => true,
524
+ };
525
+ return {
526
+ channelType: 'wecom', adapter, channel,
527
+ policy,
528
+ options: { fileMarkerPattern: /\[SEND_FILE:(?:(\w+):)?([^\]]+)\]/g, supportsImages: true, flushDelay: inst.flushDelay },
529
+ connect: () => channel.connect(),
530
+ disconnect: () => channel.disconnect(),
531
+ onProjectPathRequest: () => Promise.resolve(ctx.defaultProjectPath),
532
+ registerBridge(bridge, channelType) {
533
+ bridge.register(adapter.channelName, (handler) => channel.onMessage(async (event) => {
534
+ handler({
535
+ channel: adapter.channelName, channelType, channelId: event.channelId,
536
+ selfAID: ctx.agentName, content: event.content, images: event.images,
537
+ chatType: event.chatType || 'private', peerId: event.peerId || '',
538
+ peerName: event.peerName, messageId: event.messageId,
539
+ });
540
+ }), (channelId, text) => channel.sendMessage(channelId, text), adapter, channelType);
541
+ },
542
+ };
606
543
  }
607
544
  }
package/dist/cli/agent.js CHANGED
@@ -7,12 +7,12 @@ import { ipcQuery } from '../ipc.js';
7
7
  import { CONFIG_SCHEMA_VERSION } from '../types.js';
8
8
  import { isValidChannelName } from '../core/channel-loader.js';
9
9
  import { commandExists } from '../utils/cross-platform.js';
10
- import { isCodexSdkAvailable } from '../agents/codex-runner.js';
10
+ import { getCodexAppServerAvailability, isCodexAppServerAvailable } from '../agents/codex-runner.js';
11
11
  // ==================== Helpers ====================
12
12
  const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
13
13
  function isBaseagentAvailable(baseagent) {
14
14
  if (baseagent === 'codex')
15
- return isCodexSdkAvailable();
15
+ return isCodexAppServerAvailable();
16
16
  return commandExists(baseagent);
17
17
  }
18
18
  function detectAvailableBaseagents() {
@@ -313,7 +313,7 @@ export async function agentCreateInteractive(opts = {}) {
313
313
  // Baseagent
314
314
  const available = detectAvailableBaseagents();
315
315
  if (available.length === 0) {
316
- return { ok: false, error: `No usable baseagent detected. Install claude/gemini CLI or optional dependency @openai/codex-sdk.` };
316
+ return { ok: false, error: `No usable baseagent detected. Install claude/gemini CLI or codex CLI with app-server.` };
317
317
  }
318
318
  const defaultBa = pickDefaultBaseagent(available);
319
319
  let baseagent;
@@ -467,7 +467,7 @@ export async function agentCreateNonInteractive(opts) {
467
467
  // Baseagent
468
468
  const available = detectAvailableBaseagents();
469
469
  if (available.length === 0) {
470
- return failValidating(`No usable baseagent detected. Install claude/gemini CLI or optional dependency @openai/codex-sdk.`);
470
+ return failValidating(`No usable baseagent detected. Install claude/gemini CLI or codex CLI with app-server.`);
471
471
  }
472
472
  let baseagent;
473
473
  if (opts.baseagent) {
@@ -475,7 +475,10 @@ export async function agentCreateNonInteractive(opts) {
475
475
  return failValidating(`Invalid baseagent: ${opts.baseagent} (options: ${BASEAGENT_CANDIDATES.join('/')})`);
476
476
  }
477
477
  if (!available.includes(opts.baseagent)) {
478
- return failValidating(`${opts.baseagent} is not available in the current environment (available: ${available.join('/')})`);
478
+ const reason = opts.baseagent === 'codex'
479
+ ? getCodexAppServerAvailability().reason
480
+ : undefined;
481
+ return failValidating(reason || `${opts.baseagent} is not available in the current environment (available: ${available.join('/')})`);
479
482
  }
480
483
  baseagent = opts.baseagent;
481
484
  }
package/dist/cli/index.js CHANGED
@@ -5,10 +5,10 @@ import os from 'os';
5
5
  import { spawn, execFileSync, execFile } from 'child_process';
6
6
  import { promisify } from 'util';
7
7
  import { resolveRoot, resolvePaths, ensureDataDirs, getPackageRoot, agentMdPath } from '../paths.js';
8
- import { loadDefaults, loadAllAgents, mergeForAgent } from '../config-store.js';
9
- import { loadEvolclawConfig } from '../evolclaw-config.js';
10
- import { resolveAnthropicConfig } from '../agents/resolve.js';
8
+ import { loadDefaults, loadAllAgents, mergeForAgent, loadEvolclawConfig, saveEvolclawConfig } from '../config-store.js';
9
+ import { resolveAnthropicConfig } from '../agents/baseagent.js';
11
10
  import { migrateProject } from '../config-store.js';
11
+ import readline from 'readline';
12
12
  import { cmdInit, needsControlAidInit, initTail } from './init.js';
13
13
  import { ipcQuery } from '../ipc.js';
14
14
  import { cmdInitWechat, cmdInitFeishu, cmdInitDingtalk, cmdInitQQBot, cmdInitWecom, cmdInitAun } from './init-channel.js';
@@ -16,8 +16,10 @@ import { isHelpFlag, wantsHelp, getArgValue } from './help.js';
16
16
  import * as platform from '../utils/cross-platform.js';
17
17
  import { EventBus } from '../core/event-bus.js';
18
18
  import { tryUpgrade, tryUpgradeAunSdk } from '../utils/npm-ops.js';
19
+ import { fetchEcwebPairCode } from '../utils/ecweb-pair.js';
19
20
  import { resolveAunCoreSdkPkg, AUN_CORE_SDK_PKG } from '../aun/aid/client.js';
20
21
  import { scanInstances, cleanupInstances, writeRestartMonitor, removeRestartMonitor, isRestartMonitorWinner, findOrphanProcesses, killOrphans } from '../utils/instance-registry.js';
22
+ import { displaySessionTitle } from '../core/session/session-title.js';
21
23
  // Suppress Node.js ExperimentalWarning (e.g. SQLite) from cluttering CLI output
22
24
  process.removeAllListeners('warning');
23
25
  process.on('warning', (w) => { if (w.name === 'ExperimentalWarning')
@@ -313,6 +315,27 @@ async function cmdStart() {
313
315
  if (!evolclawCfgStart.aid) {
314
316
  console.log('⚠ 控制 AID 未配置(非交互式启动,跳过补全)。如需进程身份/远程管理,请运行 evolclaw init');
315
317
  }
318
+ else if (process.stdin.isTTY) {
319
+ // 证书缺失时在 CLI 侧提示,daemon 是后台进程无终端不做交互
320
+ const certKey = path.join(resolvePaths().root, 'AIDs', evolclawCfgStart.aid, 'private', 'key.json');
321
+ if (!fs.existsSync(certKey)) {
322
+ console.log(`⚠ 控制 AID 证书缺失:${evolclawCfgStart.aid}`);
323
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
324
+ const ans = await new Promise(res => rl.question(' [1] 继续启动 [2] 重新生成 AID [3] 退出 [1/2/3]: ', res));
325
+ rl.close();
326
+ if (ans.trim() === '3') {
327
+ process.exit(0);
328
+ }
329
+ if (ans.trim() === '2') {
330
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
331
+ suppressSdkLogs();
332
+ const { generateControlAid } = await import('../aun/aid/control-aid.js');
333
+ const result = await generateControlAid();
334
+ saveEvolclawConfig({ ...loadEvolclawConfig(), aid: result.aid });
335
+ console.log(`✓ 新控制 AID: ${result.aid}`);
336
+ }
337
+ }
338
+ }
316
339
  // 检查至少有一个 self-agent
317
340
  const { agents, skipped } = loadAllAgents();
318
341
  if (agents.length === 0) {
@@ -436,6 +459,8 @@ async function cmdStart() {
436
459
  countLines(getPackageRoot(), p.logs);
437
460
  }
438
461
  console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
462
+ // ECWeb 自动后台启动
463
+ startEcwebIfEnabled(p);
439
464
  return;
440
465
  }
441
466
  // 超时
@@ -914,7 +939,7 @@ async function cmdStatus() {
914
939
  const projectName = path.basename(s.projectPath);
915
940
  const sessionType = s.threadId ? '话题会话' : '主会话';
916
941
  const chatType = s.chatType === 'group' ? '群聊' : '单聊';
917
- const sessionName = s.name || '默认会话';
942
+ const sessionName = displaySessionTitle(s.name);
918
943
  const timeAgo = formatTimeAgo(Date.now() - s.updatedAt);
919
944
  const dot = s.isActive ? '•' : '○';
920
945
  const agentSidLabel = s.agentSessionId ? ` [${s.agentSessionId}]` : '';
@@ -2123,54 +2148,129 @@ async function cmdWatchAid() {
2123
2148
  }
2124
2149
  platform.onShutdown(cleanup);
2125
2150
  }
2151
+ /** 扫描 instance/ 目录,返回存活的 ecweb 实例(ecweb-<pid>.json)。 */
2152
+ function findAliveEcweb(p) {
2153
+ if (!fs.existsSync(p.instanceDir))
2154
+ return null;
2155
+ for (const file of fs.readdirSync(p.instanceDir)) {
2156
+ if (!file.startsWith('ecweb-') || !file.endsWith('.json'))
2157
+ continue;
2158
+ try {
2159
+ const rec = JSON.parse(fs.readFileSync(path.join(p.instanceDir, file), 'utf-8'));
2160
+ if (rec.pid && platform.isProcessRunning(rec.pid))
2161
+ return { pid: rec.pid, port: rec.port ?? 42705 };
2162
+ fs.unlinkSync(path.join(p.instanceDir, file));
2163
+ }
2164
+ catch { }
2165
+ }
2166
+ return null;
2167
+ }
2168
+ /** 若 ecweb 在运行则杀掉并清理 pid 文件,返回是否成功 kill。 */
2169
+ function stopEcwebIfRunning(p) {
2170
+ const alive = findAliveEcweb(p);
2171
+ if (!alive)
2172
+ return false;
2173
+ try {
2174
+ platform.killProcess(alive.pid);
2175
+ }
2176
+ catch { }
2177
+ // 清理 pid 文件
2178
+ try {
2179
+ for (const file of fs.readdirSync(p.instanceDir)) {
2180
+ if (file.startsWith('ecweb-') && file.endsWith('.json')) {
2181
+ fs.unlinkSync(path.join(p.instanceDir, file));
2182
+ }
2183
+ }
2184
+ }
2185
+ catch { }
2186
+ return true;
2187
+ }
2188
+ /** 后台 detached 启动 ecweb;若已运行则先停再启(确保加载最新代码)。 */
2189
+ function startEcwebIfEnabled(p) {
2190
+ const cfg = loadEvolclawConfig();
2191
+ if (!cfg.ecweb?.enabled)
2192
+ return;
2193
+ stopEcwebIfRunning(p); // 先停旧进程(有则停),保证加载最新代码
2194
+ const exe = platform.resolveCommandPath('evolclaw-web');
2195
+ if (!exe)
2196
+ return; // 未安装,静默跳过
2197
+ const port = cfg.ecweb.port ?? 42705;
2198
+ const isBatch = /\.(cmd|bat)$/i.test(exe);
2199
+ const args = ['--home', p.root, '--port', String(port)];
2200
+ const child = isBatch
2201
+ ? spawn(`"${exe}"`, args.map(a => `"${a}"`), { detached: true, stdio: 'ignore', shell: true, windowsHide: true })
2202
+ : spawn(exe, args, { detached: true, stdio: 'ignore', windowsHide: true });
2203
+ child.unref();
2204
+ const pid = child.pid;
2205
+ if (!pid)
2206
+ return;
2207
+ fs.mkdirSync(p.instanceDir, { recursive: true });
2208
+ fs.writeFileSync(path.join(p.instanceDir, `ecweb-${pid}.json`), JSON.stringify({ pid, port, startedAt: Date.now() }, null, 2));
2209
+ console.log(`🔭 ECWeb 已在后台启动 (PID: ${pid}) http://localhost:${port}`);
2210
+ console.log(` 运行 ec watch web 查看配对码`);
2211
+ }
2212
+ /** 显示 ecweb 访问信息 + 配对码(启动后 ecweb 需要一点时间起 HTTP,故重试几次)。 */
2213
+ async function printEcwebAccess(port) {
2214
+ console.log(`🔭 ECWeb http://localhost:${port}`);
2215
+ let pair = null;
2216
+ for (let i = 0; i < 10 && !pair; i++) {
2217
+ pair = await fetchEcwebPairCode(port);
2218
+ if (!pair)
2219
+ await sleep(300);
2220
+ }
2221
+ if (pair) {
2222
+ const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2223
+ console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,配对后 token 缓存 24h)`);
2224
+ }
2225
+ else {
2226
+ console.log(' 配对码: 暂不可用(稍后重试 ec watch web,或查看 logs/watch-web.log)');
2227
+ }
2228
+ }
2126
2229
  async function cmdWatchWeb() {
2127
- // evolclaw-web 是独立插件包(可执行命令),按需安装。
2128
- // 复用 npm-ops.npmInstallGlobal(含 EACCES→sudo 回退、Windows npm.cmd、超时)。
2129
- const { execFileSync } = await import('child_process');
2130
- const home = resolvePaths().root;
2230
+ const p = resolvePaths();
2231
+ // 1. 检查安装
2131
2232
  if (!platform.commandExists('evolclaw-web')) {
2132
- process.stdout.write('📦 evolclaw-web 未安装,正在从 npm 安装...\n');
2233
+ process.stdout.write('📦 evolclaw-web 未安装。');
2234
+ if (!process.stdin.isTTY) {
2235
+ process.stdout.write(' 请手动安装: npm install -g evolclaw-web\n');
2236
+ process.exit(1);
2237
+ }
2238
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
2239
+ const ans = await new Promise(res => rl.question(' 立即安装?[Y/n] ', res));
2240
+ rl.close();
2241
+ if (ans.trim().toLowerCase() === 'n') {
2242
+ process.exit(0);
2243
+ }
2244
+ process.stdout.write('\n');
2133
2245
  const { npmInstallGlobal } = await import('../utils/npm-ops.js');
2134
2246
  try {
2135
2247
  await npmInstallGlobal('evolclaw-web');
2136
2248
  }
2137
2249
  catch (e) {
2138
- process.stderr.write(`❌ 安装 evolclaw-web 失败: ${e?.stderr || e?.message || e}\n 可手动安装: npm install -g evolclaw-web\n`);
2250
+ process.stderr.write(`❌ 安装失败: ${e?.stderr || e?.message || e}\n`);
2139
2251
  process.exit(1);
2140
2252
  }
2141
2253
  }
2142
- // 解析可执行文件的真实绝对路径:
2143
- // - Windows bin 是 evolclaw-web.cmd,execFileSync 不会自动补后缀
2144
- // - 刚安装的命令可能不在当前进程已缓存的 PATH 里,用 where/which 重新探测
2145
- const exe = platform.resolveCommandPath('evolclaw-web');
2146
- if (!exe) {
2147
- process.stderr.write('❌ 已安装 evolclaw-web 但无法定位可执行文件。\n 请重新打开终端后再次运行,或手动执行: evolclaw-web --home ' + home + '\n');
2148
- process.exit(1);
2149
- }
2150
- // Node 18.20+/20+/22 起,execFile 拒绝直接 spawn .cmd/.bat(CVE-2024-27980),必须 shell:true。
2151
- // shell 模式下含空格的路径/参数需加引号。
2152
- // evolclaw-web 是前台长驻服务:用户 Ctrl-C、被新实例的单实例保护 SIGKILL、或正常退出,
2153
- // execFileSync 都会抛错(signal 终止时 status=null)。这些都是正常生命周期,
2154
- // 不应让父进程 evolclaw 带堆栈崩溃。只有真正的非信号失败才提示。
2155
- const isBatch = /\.(cmd|bat)$/i.test(exe);
2156
- try {
2157
- if (isBatch) {
2158
- const q = (s) => `"${s}"`;
2159
- execFileSync(q(exe), ['--home', q(home)], { stdio: 'inherit', shell: true });
2160
- }
2161
- else {
2162
- execFileSync(exe, ['--home', home], { stdio: 'inherit' });
2163
- }
2254
+ // 2. 检查是否已运行
2255
+ const alive = findAliveEcweb(p);
2256
+ if (alive) {
2257
+ await printEcwebAccess(alive.port);
2258
+ return;
2164
2259
  }
2165
- catch (e) {
2166
- // 信号终止(SIGINT/SIGTERM/SIGKILL)= 用户主动退出或被新实例顶替,静默返回
2167
- if (e?.signal)
2168
- return;
2169
- // 退出码非 0 但非信号:可能是启动失败,提示但不崩溃
2170
- if (typeof e?.status === 'number' && e.status !== 0) {
2171
- process.stderr.write(`⚠ evolclaw-web 退出(code ${e.status})\n`);
2172
- }
2260
+ // 3. 启动(后台)并同步配置
2261
+ const cfg = loadEvolclawConfig();
2262
+ const port = cfg.ecweb?.port ?? 42705;
2263
+ if (cfg.ecweb?.enabled === undefined) {
2264
+ // 首次手动启动时自动写入 enabled:true
2265
+ saveEvolclawConfig({ ...cfg, ecweb: { enabled: true, port } });
2266
+ }
2267
+ startEcwebIfEnabled(p);
2268
+ const started = findAliveEcweb(p);
2269
+ if (!started) {
2270
+ process.stderr.write('❌ 启动失败,请检查 evolclaw-web 是否正确安装\n');
2271
+ process.exit(1);
2173
2272
  }
2273
+ await printEcwebAccess(started.port);
2174
2274
  }
2175
2275
  async function cmdRestartMonitor() {
2176
2276
  const p = resolvePaths();
@@ -2827,13 +2927,23 @@ async function cmdCtl(args) {
2827
2927
  Agent:
2828
2928
  agent <subcommand> EvolAgent 管理(list/show/new/enable/disable/reload/delete)
2829
2929
 
2930
+ 触发器:
2931
+ trigger 查看活跃触发器
2932
+ trigger list 查看所有触发器(含历史)
2933
+ trigger set --delay <时长> --prompt <内容> 延迟触发(如 15m、2h)
2934
+ trigger set --at <ISO时间> --prompt <内容> 定时触发(如 2026-06-10T09:00)
2935
+ trigger set --cron '<表达式>' --prompt <内容> 周期触发(如 '*/15 * * * *')
2936
+ trigger cancel <名称> 取消触发器
2937
+ trigger update <名称> ... 修改触发器参数
2938
+
2830
2939
  运维:
2831
2940
  restart [channel] 重启服务或重连指定渠道
2832
2941
 
2833
2942
  示例:
2834
2943
  evolclaw ctl model sonnet
2835
2944
  evolclaw ctl effort high
2836
- evolclaw ctl compact`);
2945
+ evolclaw ctl compact
2946
+ evolclaw ctl "trigger set --cron '*/15 * * * *' --prompt '现在时间?'"`);
2837
2947
  process.exit(1);
2838
2948
  }
2839
2949
  // help 不需要连接服务,直接复用无参数时的帮助输出
@@ -4075,11 +4185,16 @@ Commands:
4075
4185
  process.exit(1);
4076
4186
  }
4077
4187
  if (formatJson) {
4078
- console.log(JSON.stringify({ ok: true, objectKey: remotePath, isPublic, ref: `${aid}/${remotePath}` }));
4188
+ console.log(JSON.stringify({ ok: true, objectKey: remotePath, isPublic, ref: `${aid}/${remotePath}`, publicUrl: result.publicUrl ?? null }));
4079
4189
  }
4080
4190
  else {
4081
4191
  console.log(`✓ 已上传: ${remotePath}${isPublic ? ' (公开)' : ''}`);
4082
- console.log(` 引用: ${aid}/${remotePath}`);
4192
+ if (result.publicUrl) {
4193
+ console.log(` 🔗 访问: ${result.publicUrl}`);
4194
+ }
4195
+ else {
4196
+ console.log(` 引用: ${aid}/${remotePath}`);
4197
+ }
4083
4198
  console.log(` 下载: evolclaw storage download ${aid} ${aid}/${remotePath}`);
4084
4199
  }
4085
4200
  return;
@@ -4959,7 +5074,12 @@ export async function main(args) {
4959
5074
  case 'logs':
4960
5075
  cmdLogs(args.slice(1));
4961
5076
  break;
4962
- case 'watch':
5077
+ case 'watch': {
5078
+ // watch 子命令(aid/msg)会调 AUN SDK(aidLookup 刷名片、对端探测等),
5079
+ // 与 aid/msg/group 等命令一致:进 case 先关掉 SDK 的 [aun_core] 日志,
5080
+ // 否则 SDK debug 日志会直喷终端、糊住 watch 的 TUI 面板。
5081
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
5082
+ suppressSdkLogs();
4963
5083
  if (args[1] === 'aid') {
4964
5084
  await cmdWatchAid();
4965
5085
  }
@@ -4999,6 +5119,7 @@ export async function main(args) {
4999
5119
  cmdWatch();
5000
5120
  }
5001
5121
  break;
5122
+ }
5002
5123
  case 'restart-monitor':
5003
5124
  await cmdRestartMonitor();
5004
5125
  break;
@@ -5060,6 +5181,18 @@ export async function main(args) {
5060
5181
  await cmdModel(args.slice(1));
5061
5182
  break;
5062
5183
  }
5184
+ case 'stats': {
5185
+ const { handleStats } = await import('./stats.js');
5186
+ await handleStats(args.slice(1));
5187
+ break;
5188
+ }
5189
+ case 'version':
5190
+ case '-v':
5191
+ case '--version': {
5192
+ const { handleVersion } = await import('./version.js');
5193
+ handleVersion(args.slice(1));
5194
+ break;
5195
+ }
5063
5196
  case 'bench': {
5064
5197
  const { suppressSdkLogs } = await import('../aun/aid/index.js');
5065
5198
  suppressSdkLogs();