evolcore 0.0.22 → 0.0.24

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 (117) hide show
  1. package/CHANGELOG.md +74 -14
  2. package/README.md +4 -0
  3. package/bin/codex-managed-acl.ps1 +160 -0
  4. package/bin/codex-managed-hook.mjs +127 -10
  5. package/bin/ec-safe-output.js +85 -3
  6. package/bin/install-codex-managed-hooks.mjs +384 -95
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +30 -2
  9. package/dist/agents/claude-runner.js +179 -240
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +90 -20
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +369 -0
  14. package/dist/agents/codex-runner.js +839 -192
  15. package/dist/agents/ecagent-runner.js +27 -34
  16. package/dist/agents/gemini-runner.js +62 -8
  17. package/dist/agents/runner-types.js +1 -1
  18. package/dist/aun/aid/managed-operation.js +28 -6
  19. package/dist/aun/aid/store.js +43 -0
  20. package/dist/aun/menu-client.js +272 -0
  21. package/dist/aun/msg/group.js +65 -4
  22. package/dist/aun/msg/index.js +1 -1
  23. package/dist/aun/msg/managed-operation.js +57 -5
  24. package/dist/aun/outbox.js +56 -18
  25. package/dist/aun/rpc/client.js +7 -1
  26. package/dist/channels/aun.js +178 -77
  27. package/dist/channels/daemon.js +7 -1
  28. package/dist/channels/dingtalk.js +1 -0
  29. package/dist/channels/feishu.js +10 -4
  30. package/dist/channels/wechat.js +81 -10
  31. package/dist/channels/wecom.js +2 -1
  32. package/dist/cli/agent-command.js +29 -3
  33. package/dist/cli/agent-create.js +538 -0
  34. package/dist/cli/agent.js +26 -675
  35. package/dist/cli/aun-commands.js +108 -24
  36. package/dist/cli/command-log.js +21 -2
  37. package/dist/cli/ctl-command.js +2 -0
  38. package/dist/cli/daemon-commands.js +289 -61
  39. package/dist/cli/index.js +22 -2
  40. package/dist/cli/init.js +2 -10
  41. package/dist/cli/managed-command-guard.js +3 -0
  42. package/dist/cli/menu-client.js +95 -0
  43. package/dist/cli/model.js +23 -8
  44. package/dist/cli/restart-monitor.js +432 -8
  45. package/dist/cli/stats.js +110 -0
  46. package/dist/cli/task-context.js +12 -2
  47. package/dist/cli/trigger-command.js +9 -1
  48. package/dist/config/builtin-roles.js +2 -0
  49. package/dist/config/schema-migration.js +14 -2
  50. package/dist/core/auth/agent-delegation.js +10 -7
  51. package/dist/core/auth/auth-gateway.js +3 -2
  52. package/dist/core/auth/authorization-audit.js +75 -18
  53. package/dist/core/auth/operation-authorizer.js +63 -3
  54. package/dist/core/auth/operation-catalog.js +14 -2
  55. package/dist/core/auth/remote-access-store.js +136 -0
  56. package/dist/core/baseagent-loader.js +4 -4
  57. package/dist/core/bootstrap-messages.js +39 -18
  58. package/dist/core/bootstrap-service.js +32 -26
  59. package/dist/core/capability/capability-manager.js +8 -4
  60. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  61. package/dist/core/command/agent-control.js +7 -2
  62. package/dist/core/command/command-handler.js +128 -30
  63. package/dist/core/command/daemon-diagnostics.js +448 -0
  64. package/dist/core/command/menu-catalog.js +6 -1
  65. package/dist/core/command/menu-handler.js +410 -98
  66. package/dist/core/command/menu-protocol.js +123 -0
  67. package/dist/core/command/slash-gate.js +4 -6
  68. package/dist/core/command/slash-handler.js +199 -70
  69. package/dist/core/data-migration.js +5 -15
  70. package/dist/core/event-catalog.js +3 -0
  71. package/dist/core/handoff/runtime.js +3 -1
  72. package/dist/core/handoff/store.js +7 -1
  73. package/dist/core/message/im-renderer.js +13 -6
  74. package/dist/core/message/message-bridge.js +5 -36
  75. package/dist/core/message/message-queue.js +20 -15
  76. package/dist/core/message/message-utils.js +6 -3
  77. package/dist/core/message/response-engine.js +147 -94
  78. package/dist/core/model/catalog-service.js +310 -0
  79. package/dist/core/model/model-catalog.js +134 -334
  80. package/dist/core/model/model-diagnostics.js +4 -5
  81. package/dist/core/model/model-fallback.js +9 -16
  82. package/dist/core/model/model-permission.js +5 -2
  83. package/dist/core/permission/approval-gateway.js +183 -32
  84. package/dist/core/permission/protected-paths.js +63 -4
  85. package/dist/core/permission/sandbox-runtime.js +4 -2
  86. package/dist/core/permission/tool-policy.js +257 -31
  87. package/dist/core/runtime-lock.js +13 -8
  88. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  89. package/dist/core/session/session-manager.js +49 -13
  90. package/dist/core/session/session-renew.js +1 -0
  91. package/dist/index.js +214 -57
  92. package/dist/ipc.js +191 -102
  93. package/dist/paths.js +23 -1
  94. package/dist/stats/billing.js +18 -4
  95. package/dist/trigger/manager.js +3 -1
  96. package/dist/trigger/parser.js +77 -13
  97. package/dist/trigger/patch.js +104 -1
  98. package/dist/utils/atomic-write.js +26 -2
  99. package/dist/utils/cross-platform.js +436 -1
  100. package/dist/utils/ecweb-supervisor.js +11 -2
  101. package/dist/utils/ecweb-utils.js +8 -26
  102. package/dist/utils/error-utils.js +16 -2
  103. package/dist/utils/model-prices.jsonl +23 -20
  104. package/dist/utils/npm-ops.js +70 -15
  105. package/dist/utils/windows-autostart.js +1 -1
  106. package/kits/docs/evolcore/INDEX.md +1 -0
  107. package/kits/docs/evolcore/agent-create.md +13 -0
  108. package/kits/docs/evolcore/group-collaboration.md +7 -2
  109. package/kits/docs/evolcore/menu-client.md +34 -0
  110. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  111. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  112. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  113. package/kits/templates/roles/admin.json +2 -0
  114. package/kits/templates/roles/visitor.json +1 -0
  115. package/kits/templates/system-fragments/bootstrap.md +8 -1
  116. package/package.json +16 -4
  117. package/dist/utils/codex-cli.js +0 -43
@@ -15,7 +15,7 @@ import { cmdInit, needsControlAidInit, initTail } from './init.js';
15
15
  import * as platform from '../utils/cross-platform.js';
16
16
  import { EventBus } from '../core/event-bus.js';
17
17
  import { tryUpgrade, tryUpgradeGlobalPkg, resolveGlobalPkg } from '../utils/npm-ops.js';
18
- import { ecwebVersionRequirementError, fetchEcwebPairCode, resolveEcwebLaunchCommand } from '../utils/ecweb-utils.js';
18
+ import { ecwebVersionRequirementError, resolveEcwebLaunchCommand, fetchRemotePairingCode } from '../utils/ecweb-utils.js';
19
19
  import { isHostChinese } from '../utils/locale.js';
20
20
  import { scanInstances, cleanupInstances, findOrphanProcesses, killOrphans } from '../utils/instance-registry.js';
21
21
  import { filterLogFiles, deriveLogTypes, computePreChecked, validateLogTypes, shortLogName as shortLogNameLocal, isWatchLogFile } from './watch-logs.js';
@@ -28,9 +28,52 @@ import { WEB_CLI_BIN, WEB_PACKAGE_LATEST, WEB_PACKAGE_NAME } from '../product.js
28
28
  import { rotateStdoutLog } from '../utils/log-writer.js';
29
29
  import { inspectDataMigrationRequirement } from '../core/data-migration.js';
30
30
  import { isInitCancelledError, printInitCancelHint } from './init-cancel.js';
31
- import { listCodexAppServerProcesses, unregisterCodexAppServerProcess } from '../utils/codex-app-server-registry.js';
31
+ import { listCodexAppServerProcesses, unregisterCodexAppServerProcess } from '../agents/codex/app-server-registry.js';
32
32
  import { DEFAULT_ECWEB_PORT, ecwebService, setEcwebEnabled } from '../config/daemon-services.js';
33
+ import { checkCodexProjectPath, inspectCodexManagedHook, ensureCodexManagedHook, runCodexHookLaunchDiagnostic, } from '../agents/codex/diagnostics.js';
33
34
  const execFileAsync = promisify(execFile);
35
+ /** Run before launch, and before restart stops any existing daemon. */
36
+ export function ensureCodexHookBeforeLifecycle(onRepaired) {
37
+ if (!platform.isWindows)
38
+ return true;
39
+ try {
40
+ const usesCodex = loadDefaults()?.active_baseagent === 'codex'
41
+ || loadAllAgents().agents.some(agent => {
42
+ try {
43
+ return resolveEffective({ self: agent.aid }).active_baseagent === 'codex';
44
+ }
45
+ catch (error) {
46
+ console.warn(`⚠ Agent ${agent.aid} 配置解析失败:${error instanceof Error ? error.message : String(error)}`);
47
+ return false;
48
+ }
49
+ });
50
+ if (!usesCodex)
51
+ return true;
52
+ const result = ensureCodexManagedHook({
53
+ packageRoot: getPackageRoot(),
54
+ onInstallRequired: () => console.log('🔧 Codex 运行组件缺失,正在自动安装;Windows 提示管理员授权时请选择“是”。'),
55
+ });
56
+ if (result.status === 'installed') {
57
+ if (result.repaired) {
58
+ console.log('✅ Codex Windows managed hook 已自动安装并验证');
59
+ onRepaired?.();
60
+ }
61
+ return true;
62
+ }
63
+ console.error(`❌ Codex Windows managed hook 未就绪:${result.error || result.reason || result.status}`);
64
+ if (result.status === 'missing') {
65
+ console.error(' 本次操作已停止。下次启动或重启会再次自动尝试安装,需要允许 Windows 管理员授权。');
66
+ }
67
+ else {
68
+ console.error(' 本次操作已停止。现有 Codex 策略或文件访问异常,需要管理员核对;自动安装不会覆盖异常策略。');
69
+ }
70
+ }
71
+ catch (error) {
72
+ console.error(`❌ Codex 启动检查失败:${error instanceof Error ? error.message : String(error)}`);
73
+ }
74
+ process.exitCode = 1;
75
+ return false;
76
+ }
34
77
  async function runAutomaticInit(action) {
35
78
  try {
36
79
  return await action();
@@ -65,8 +108,8 @@ function printNoSelfAgentHints(options) {
65
108
  // taskkill, even though the daemon is healthy and able to shut down gracefully.
66
109
  const DAEMON_PROBE_TIMEOUT_MS = platform.isWindows ? 5_000 : 1_000;
67
110
  export const DAEMON_SHUTDOWN_TIMEOUT_MS = platform.isWindows ? 10_000 : 3_000;
68
- async function probeDaemon(socketPath, timeoutMs = DAEMON_PROBE_TIMEOUT_MS) {
69
- const response = await ipcQuery(socketPath, { type: 'ping' }, timeoutMs);
111
+ async function probeDaemon(socketPath, timeoutMs = DAEMON_PROBE_TIMEOUT_MS, throwOnError = false) {
112
+ const response = await ipcQuery(socketPath, { type: 'ping' }, timeoutMs, { throwOnError });
70
113
  if (response?.pong !== true || !Number.isInteger(response.pid) || response.pid <= 0)
71
114
  return null;
72
115
  return response;
@@ -177,10 +220,15 @@ function sourceBuildNeedsRefresh() {
177
220
  function runSourceBuild() {
178
221
  return new Promise(resolve => {
179
222
  console.log('🔧 源码比 daemon 编译产物更新,自动重新构建...');
180
- const child = spawn(platform.isWindows ? 'npm.cmd' : 'npm', ['run', 'build'], {
223
+ // Windows cannot spawn a .cmd shim directly with shell:false on recent
224
+ // Node versions. Invoke cmd.exe explicitly instead of using spawn's
225
+ // shell:true mode, which also avoids the DEP0190 warning.
226
+ const command = platform.isWindows ? (process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe') : 'npm';
227
+ const args = platform.isWindows ? ['/d', '/c', 'npm.cmd', 'run', 'build'] : ['run', 'build'];
228
+ const child = spawn(command, args, {
181
229
  cwd: getPackageRoot(),
182
230
  stdio: 'inherit',
183
- shell: platform.isWindows,
231
+ windowsHide: platform.isWindows,
184
232
  });
185
233
  child.on('error', err => {
186
234
  console.error(`❌ 无法启动构建进程: ${err.message}`);
@@ -199,22 +247,100 @@ function runSourceBuild() {
199
247
  });
200
248
  });
201
249
  }
250
+ /**
251
+ * Spawn the restart monitor and confirm it actually reached its own code.
252
+ *
253
+ * The daemon is already stopped at this point, so a monitor that dies on
254
+ * startup leaves nothing running and nobody watching — the exact silent
255
+ * failure this mechanism exists to prevent.
256
+ */
257
+ async function spawnRestartMonitorAndConfirm(monitorEntry, reportLine) {
258
+ const result = await platform.launchMonitorAndConfirm([...monitorEntry.args, 'restart-monitor'], { EVOLCORE_HOME: resolvePaths().root, ...(monitorEntry.env ?? {}) }, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid === pid), pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid));
259
+ if (result.ok)
260
+ return { ok: true };
261
+ for (const line of (result.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
262
+ reportLine(` | ${line}`);
263
+ }
264
+ return { ok: false, reason: result.reason ?? 'unknown' };
265
+ }
202
266
  async function requireFreshSourceBuild() {
203
267
  if (!sourceBuildNeedsRefresh())
204
268
  return true;
205
269
  return await runSourceBuild();
206
270
  }
207
- function requireCompletedDataMigration(root) {
271
+ function requireCompletedDataMigration(root, action = 'start') {
208
272
  const requirement = inspectDataMigrationRequirement(root);
209
273
  if (!requirement.required)
210
274
  return true;
211
- console.error(`❌ 检测到 ${requirement.operationCount} 项待确认的用户数据迁移,服务未启动。`);
275
+ console.error(action === 'restart'
276
+ ? `❌ 检测到 ${requirement.operationCount} 项待确认的用户数据迁移,本次不停止当前服务。`
277
+ : `❌ 检测到 ${requirement.operationCount} 项待确认的用户数据迁移,服务未启动。`);
212
278
  console.error(' 先执行: ec data migrate --dry-run');
213
279
  console.error(' 确认后: ec data migrate --apply');
214
280
  console.error(' 迁移成功并校验后,再执行 ec start 或 ec restart。');
281
+ if (action === 'restart')
282
+ console.error(' 本次 ec restart 仅完成只读检查,未停止当前服务。');
215
283
  process.exitCode = 2;
216
284
  return false;
217
285
  }
286
+ /**
287
+ * Check package updates before starting or restarting the daemon.
288
+ * A failed upgrade is reported, but the currently installed version remains
289
+ * usable and the caller continues with its normal lifecycle.
290
+ */
291
+ export async function runUpgradeChecks(upgradeFn = signal => tryUpgrade({ signal }), ecwebUpgradeFn = signal => tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME, { signal }), timeoutMs = 60_000) {
292
+ console.log('📦 Checking for updates...');
293
+ const runWithTimeout = async (label, fn) => {
294
+ const controller = new AbortController();
295
+ let timer;
296
+ try {
297
+ return await Promise.race([
298
+ fn(controller.signal),
299
+ new Promise(resolve => {
300
+ timer = setTimeout(() => {
301
+ resolve({ status: 'failed', error: `${label} upgrade timed out` });
302
+ controller.abort();
303
+ }, timeoutMs);
304
+ }),
305
+ ]);
306
+ }
307
+ catch (error) {
308
+ return { status: 'failed', error: error instanceof Error ? error.message : String(error) };
309
+ }
310
+ finally {
311
+ if (timer)
312
+ clearTimeout(timer);
313
+ }
314
+ };
315
+ const upgrade = await runWithTimeout('EvolCore', upgradeFn);
316
+ switch (upgrade.status) {
317
+ case 'upgraded':
318
+ console.log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
319
+ break;
320
+ case 'no-update':
321
+ console.log(`✓ Already up to date (${upgrade.from})`);
322
+ break;
323
+ case 'skipped':
324
+ console.log(upgrade.error
325
+ ? '⏭ Skipped upgrade (network unavailable)'
326
+ : '⏭ Skipped upgrade check (dev mode)');
327
+ break;
328
+ case 'failed':
329
+ console.log(`⚠ Upgrade failed${upgrade.from && upgrade.to ? ` (${upgrade.from} → ${upgrade.to})` : ''}, continuing with current version${upgrade.error ? `: ${upgrade.error}` : ''}`);
330
+ break;
331
+ }
332
+ const ecwebUpgrade = await runWithTimeout(WEB_PACKAGE_NAME, ecwebUpgradeFn);
333
+ switch (ecwebUpgrade.status) {
334
+ case 'upgraded':
335
+ console.log(`✅ ${WEB_PACKAGE_NAME} upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
336
+ break;
337
+ case 'no-update':
338
+ break;
339
+ case 'failed':
340
+ console.log(`⚠ ${WEB_PACKAGE_NAME} upgrade failed${ecwebUpgrade.from && ecwebUpgrade.to ? ` (${ecwebUpgrade.from} → ${ecwebUpgrade.to})` : ''}${ecwebUpgrade.error ? `: ${ecwebUpgrade.error}` : ''}`);
341
+ break;
342
+ }
343
+ }
218
344
  // ==================== Commands ====================
219
345
  /**
220
346
  * 检测并展示跨 HOME 残留的 evolcore 主进程。
@@ -322,6 +448,14 @@ export async function cmdStart(opts = {}) {
322
448
  const p = resolvePaths();
323
449
  const existingDaemon = await probeDaemon(p.socket);
324
450
  if (existingDaemon) {
451
+ let hookRepaired = false;
452
+ if (!ensureCodexHookBeforeLifecycle(() => { hookRepaired = true; }))
453
+ return;
454
+ if (hookRepaired) {
455
+ console.log('🔄 Codex 运行组件已补齐,正在自动重启服务以加载策略...');
456
+ await cmdRestart({ diagnose: opts.diagnose });
457
+ return;
458
+ }
325
459
  console.log(` EvolCore is already running (PID: ${existingDaemon.pid}, IPC reachable)`);
326
460
  console.log(` 已在运行;已运行时长: ${resolveRunningDuration(existingDaemon.pid, existingDaemon.uptime)};部署/脚本可直接复用当前实例`);
327
461
  return;
@@ -329,6 +463,9 @@ export async function cmdStart(opts = {}) {
329
463
  ensureDataDirs();
330
464
  if (!await requireFreshSourceBuild())
331
465
  return;
466
+ // A stopped container may miss the daily upgrade trigger. Check once on
467
+ // every real start so the service does not stay on an old installation.
468
+ await (opts.upgradeChecks ?? runUpgradeChecks)();
332
469
  // 旧配置自动迁移(daemon.json → 新结构)
333
470
  const { autoMigrateIfNeeded } = await import('../config-store.js');
334
471
  autoMigrateIfNeeded();
@@ -357,6 +494,11 @@ export async function cmdStart(opts = {}) {
357
494
  console.log('\n初始化完成,开始启动 EvolCore ....');
358
495
  }
359
496
  }
497
+ // The initialization path can create the first Codex agent, so perform the
498
+ // machine-hook check after config migration/auto-init and before any daemon
499
+ // process is launched.
500
+ if (!ensureCodexHookBeforeLifecycle())
501
+ return;
360
502
  // 控制 AID 门禁:缺 aid 且交互式 → 只补全控制 AID + owners(不重走 baseagent 向导)。
361
503
  // 非 TTY(restart-monitor/systemd/管道)不补全(无法交互),只提示后继续启动,daemon 侧 warn 兜底。
362
504
  const daemonCfgStart = loadDaemonConfig();
@@ -691,6 +833,8 @@ export async function cmdStop() {
691
833
  export async function cmdRestart(opts = {}) {
692
834
  const cmdStartedAt = Date.now();
693
835
  console.log('🔄 Restarting EvolCore...');
836
+ if (!ensureCodexHookBeforeLifecycle())
837
+ return;
694
838
  const initialStatus = scanInstances();
695
839
  const initialOrphans = findOrphanProcesses();
696
840
  const initialRuntimeOrphans = initialOrphans.filter(orphan => isRuntimeOrphan(orphan));
@@ -702,40 +846,12 @@ export async function cmdRestart(opts = {}) {
702
846
  if (initialRuntimeOrphans.length === 0 && rejectPidIsolatedLifecycle('restart', initialStatus, ping))
703
847
  return;
704
848
  }
705
- if (!await requireFreshSourceBuild())
706
- return;
707
- // 版本检查与自动升级
708
- console.log('📦 Checking for updates...');
709
- const upgrade = await tryUpgrade();
710
- switch (upgrade.status) {
711
- case 'upgraded':
712
- console.log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
713
- break;
714
- case 'no-update':
715
- console.log(`✓ Already up to date (${upgrade.from})`);
716
- break;
717
- case 'skipped':
718
- console.log(upgrade.error
719
- ? '⏭ Skipped upgrade (network unavailable)'
720
- : '⏭ Skipped upgrade check (dev mode)');
721
- break;
722
- case 'failed':
723
- console.log(`⚠ Upgrade failed (${upgrade.from} → ${upgrade.to}), continuing with current version`);
724
- break;
725
- }
726
- // EC Web 是独立全局包,已安装时单独检查升级。
727
- const ecwebUpgrade = await tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME);
728
- switch (ecwebUpgrade.status) {
729
- case 'upgraded':
730
- console.log(`✅ ${WEB_PACKAGE_NAME} upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
731
- break;
732
- case 'no-update':
733
- break; // silent
734
- case 'failed':
735
- console.log(`⚠ ${WEB_PACKAGE_NAME} upgrade failed (${ecwebUpgrade.from} → ${ecwebUpgrade.to})`);
736
- break;
737
- }
738
- if (!requireCompletedDataMigration(resolveRoot()))
849
+ // 构建交给 restart-monitor:它在 daemon 停止之后才编译,dist/ 不会被活进程
850
+ // 占用,失败还能回滚到上一份产物。这里再编译一次会落在事务保护之外。
851
+ // Keep restart behavior identical to startup: both paths use one upgrade
852
+ // check implementation and continue when an upgrade is unavailable.
853
+ await runUpgradeChecks();
854
+ if (!requireCompletedDataMigration(resolveRoot(), 'restart'))
739
855
  return;
740
856
  // 停止所有活 main 进程(可能不止一个)
741
857
  const status = scanInstances();
@@ -815,11 +931,30 @@ export async function cmdRestart(opts = {}) {
815
931
  }
816
932
  }
817
933
  console.log(`⏱ restart prep done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s, starting...`);
818
- // 通过 restart-monitor 启动,避免在 agent 内调用时自杀
819
- const appCli = path.join(getPackageRoot(), 'dist', 'cli', 'index.js');
820
- platform.spawnDetachedNode([appCli, 'restart-monitor'], {
821
- EVOLCORE_HOME: resolvePaths().root,
822
- });
934
+ // 通过 restart-monitor 启动,避免在 agent 内调用时自杀。
935
+ // 开发仓下 monitor 从源码跑;安装包下先复制到 stage 目录,两种情况都让看门狗
936
+ // 不住在会被替换的产物里。
937
+ const monitorEntry = platform.resolveMonitorLaunch(getPackageRoot(), path.join(resolvePaths().root, 'data', 'monitor-stage'));
938
+ if (monitorEntry.note)
939
+ console.log(`ℹ ${monitorEntry.note}`);
940
+ if (!fs.existsSync(monitorEntry.entry)) {
941
+ console.error(`❌ 重启中止:找不到 restart-monitor 入口 ${monitorEntry.entry}`);
942
+ console.error(' 请先执行 npm run build 重建产物');
943
+ process.exitCode = 1;
944
+ return;
945
+ }
946
+ const monitorStarted = await spawnRestartMonitorAndConfirm(monitorEntry, log => console.error(log));
947
+ if (!monitorStarted.ok) {
948
+ if (monitorEntry.stageDir)
949
+ platform.cleanupMonitorStage(monitorEntry.stageDir);
950
+ console.error(`❌ 重启中止:restart-monitor 未能启动(${monitorStarted.reason})`);
951
+ if (fs.existsSync(path.join(getPackageRoot(), 'src', 'cli', 'index.ts'))) {
952
+ console.error(' 源码可能存在语法错误导致 tsx 无法加载。请执行 npx tsc --noEmit 检查。');
953
+ }
954
+ console.error(' 服务已停止,请修复后执行 ec start。');
955
+ process.exitCode = 1;
956
+ return;
957
+ }
823
958
  console.log('🔄 Restart monitor started, waiting for service to come back online...');
824
959
  await sleep(2000);
825
960
  // 代码统计(开发环境)
@@ -993,7 +1128,11 @@ export async function cmdStatus() {
993
1128
  // Start all read-only daemon queries together. On a busy Windows host each
994
1129
  // query can otherwise wait behind the same IPC/event-loop delay and make the
995
1130
  // total `ec status` time the sum of several individual timeouts.
996
- const pingPromise = probeDaemon(p.socket).catch(() => null);
1131
+ let ipcFailure;
1132
+ const pingPromise = probeDaemon(p.socket, DAEMON_PROBE_TIMEOUT_MS, true).catch(error => {
1133
+ ipcFailure = error;
1134
+ return null;
1135
+ });
997
1136
  const statusPromise = ipcQuery(p.socket, { type: 'status' }).catch(() => null);
998
1137
  const aidsPromise = ipcQuery(p.socket, { type: 'aun-aids' }).catch(() => null);
999
1138
  const agentsPromise = ipcQuery(p.socket, { type: 'evolagent.list' }).catch(() => null);
@@ -1010,6 +1149,11 @@ export async function cmdStatus() {
1010
1149
  }
1011
1150
  if (pid) {
1012
1151
  printStartupInfo({ pid, running: true });
1152
+ if (!ping) {
1153
+ console.log('⚠ 仅确认 daemon PID 存活,IPC 不可用,不能据此判断服务健康。');
1154
+ if (ipcFailure)
1155
+ console.log(` ${ipcFailure instanceof Error ? ipcFailure.message : String(ipcFailure)}`);
1156
+ }
1013
1157
  console.log('');
1014
1158
  console.log('📊 Process Info:');
1015
1159
  if (processVisible) {
@@ -1120,7 +1264,9 @@ export async function cmdStatus() {
1120
1264
  }
1121
1265
  }
1122
1266
  else {
1123
- console.log('⚠ EvolCore is not running');
1267
+ console.log(ipcFailure ? '⚠ 无法确认 EvolCore 运行状态:IPC 不可用,且没有可见的存活 PID。' : '⚠ EvolCore is not running');
1268
+ if (ipcFailure)
1269
+ console.log(` ${ipcFailure instanceof Error ? ipcFailure.message : String(ipcFailure)}`);
1124
1270
  }
1125
1271
  // Session & Project statistics (从文件系统读)
1126
1272
  if (fs.existsSync(p.sessionsDir)) {
@@ -2667,7 +2813,7 @@ async function stopEcwebIfRunning(p) {
2667
2813
  throw new Error(`EC Web old process or port ${port} could not be released`);
2668
2814
  }
2669
2815
  // 清理 pid 文件(仅 ecweb-<pid>.json / watch-web-<pid>.json,
2670
- // 不碰 ecweb-tokens.json 等非 pid 文件,否则会清空配对 token 库导致每次重启都要重新配对)
2816
+ // 不碰 daemon-owned control-access-tokens.json 等非 pid 文件,否则会清空凭据库导致每次重启都要重新配对)
2671
2817
  removeEcwebInstanceFiles(p);
2672
2818
  return pids.size > 0;
2673
2819
  }
@@ -2759,23 +2905,32 @@ export async function startEcwebIfEnabled(p, options = {}) {
2759
2905
  fs.mkdirSync(p.instanceDir, { recursive: true });
2760
2906
  const startedAt = getProcessStartTime(pid) ?? Date.now();
2761
2907
  fs.writeFileSync(path.join(p.instanceDir, `ecweb-${pid}.json`), JSON.stringify({ pid, port, startedAt, startedAtIso: new Date(startedAt).toISOString() }, null, 2));
2762
- // 轮询端口确认 HTTP 服务真正就绪(spawn 成功 ≠ 端口绑定成功),顺便拿配对码
2908
+ // 轮询端口确认 HTTP 服务真正就绪(spawn 成功 ≠ 端口绑定成功)。
2909
+ // 配对码由 daemon 提供,daemon 尚未启动时不应把已经就绪的 ECWeb 杀掉。
2910
+ let ready = false;
2763
2911
  let pair = null;
2764
2912
  for (let i = 0; i < 20; i++) {
2765
- pair = await fetchEcwebPairCode(port);
2766
- if (pair)
2913
+ ready = await probeEcwebReady(port);
2914
+ if (ready) {
2915
+ pair = await fetchRemotePairingCode();
2767
2916
  break;
2917
+ }
2768
2918
  await sleep(250);
2769
2919
  }
2770
- if (pair) {
2920
+ if (ready) {
2771
2921
  const actualStartedAt = getProcessStartTime(pid);
2772
2922
  if (actualStartedAt !== null) {
2773
2923
  fs.writeFileSync(path.join(p.instanceDir, `ecweb-${pid}.json`), JSON.stringify({ pid, port, startedAt: actualStartedAt, startedAtIso: new Date(actualStartedAt).toISOString() }, null, 2));
2774
2924
  }
2775
- const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2776
2925
  const actionLabel = wasRunning ? '已重启' : '启动成功';
2777
2926
  console.log(`✓ ECWeb ${actionLabel} (PID: ${pid}) http://localhost:${port}`);
2778
- console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,已配对过的浏览器无需重新配对)`);
2927
+ if (pair) {
2928
+ const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2929
+ console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,已配对过的浏览器无需重新配对)`);
2930
+ }
2931
+ else {
2932
+ console.log(' daemon 尚未提供配对码;EvolCore 启动后可通过 /pair 或 ECWeb 页面获取');
2933
+ }
2779
2934
  return true;
2780
2935
  }
2781
2936
  console.log(`❌ ECWeb 启动失败:端口 ${port} 未就绪(进程 PID ${pid} 可能已退出,查看 logs/watch-web.log)`);
@@ -2801,13 +2956,13 @@ async function printEcwebAccess(port) {
2801
2956
  console.log(`🔭 ECWeb http://localhost:${port}`);
2802
2957
  let pair = null;
2803
2958
  for (let i = 0; i < 10 && !pair; i++) {
2804
- pair = await fetchEcwebPairCode(port);
2959
+ pair = await fetchRemotePairingCode();
2805
2960
  if (!pair)
2806
2961
  await sleep(300);
2807
2962
  }
2808
2963
  if (pair) {
2809
2964
  const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2810
- console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,配对后 token 缓存 24h)`);
2965
+ console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,配对后远程访问 token 缓存 30 天)`);
2811
2966
  }
2812
2967
  else {
2813
2968
  console.log(' 配对码: 暂不可用(稍后重试 ec watch web,或查看 logs/watch-web.log)');
@@ -2857,7 +3012,12 @@ async function cmdWatchWeb() {
2857
3012
  const cfg = loadDaemonConfig();
2858
3013
  const currentService = ecwebService(cfg.services);
2859
3014
  const port = currentService?.port ?? DEFAULT_ECWEB_PORT;
2860
- saveDaemonConfig({ ...cfg, services: setEcwebEnabled(cfg.services, true, port) });
3015
+ // Avoid rewriting daemon.json on every invocation once ECWeb is already
3016
+ // enabled on the requested port. The persisted config is the daemon's
3017
+ // lifecycle source of truth, but an unchanged desired state needs no write.
3018
+ if (currentService?.enabled !== true || currentService.port !== port) {
3019
+ saveDaemonConfig({ ...cfg, services: setEcwebEnabled(cfg.services, true, port) });
3020
+ }
2861
3021
  const ok = await startEcwebIfEnabled(p, { forceRestart: true });
2862
3022
  if (!ok)
2863
3023
  process.exit(1); // 失败原因已由 startEcwebIfEnabled 打印
@@ -2896,9 +3056,10 @@ export async function cmdMv(oldDir, newDir) {
2896
3056
  }
2897
3057
  }
2898
3058
  // ==================== Diagnose ====================
2899
- export async function cmdDiagnose() {
3059
+ export async function cmdDiagnose(args = []) {
2900
3060
  const p = resolvePaths();
2901
3061
  let hasError = false;
3062
+ const runHookLaunchProbe = args.includes('--codex-hook');
2902
3063
  // 1. 检查数据目录
2903
3064
  console.log(`[diagnose] EVOLCORE_HOME = ${p.root}`);
2904
3065
  if (!fs.existsSync(p.root)) {
@@ -2924,7 +3085,74 @@ export async function cmdDiagnose() {
2924
3085
  else {
2925
3086
  console.log(`[diagnose] ✓ 已加载 ${agents.length} 个 self-agent`);
2926
3087
  }
2927
- // 3. 检查 Anthropic 配置(用首个 self-agent 的 effective config)
3088
+ // 3. 检查实际使用 Codex 的 Agent 项目目录。项目路径属于 Agent 的
3089
+ // effective config,逐个输出 AID,避免只检查第一个 agent 掩盖坏配置。
3090
+ const codexAgents = agents.filter(agent => {
3091
+ try {
3092
+ return resolveEffective({ self: agent.aid }).active_baseagent === 'codex';
3093
+ }
3094
+ catch (error) {
3095
+ console.error(`[diagnose] ❌ Agent ${agent.aid} effective config 解析失败: ${error instanceof Error ? error.message : String(error)}`);
3096
+ hasError = true;
3097
+ return false;
3098
+ }
3099
+ });
3100
+ for (const agent of codexAgents) {
3101
+ try {
3102
+ const effective = resolveEffective({ self: agent.aid });
3103
+ const check = checkCodexProjectPath(effective.projects?.defaultPath);
3104
+ if (check.status === 'ok') {
3105
+ console.log(`[diagnose] ✓ Codex 项目路径 ${agent.aid}: ${check.path}`);
3106
+ }
3107
+ else {
3108
+ console.error(`[diagnose] ❌ Codex 项目路径 ${agent.aid}: ${check.path || '(未配置)'} (${check.reason || check.status})`);
3109
+ hasError = true;
3110
+ }
3111
+ for (const warning of check.warnings)
3112
+ console.warn(`[diagnose] ⚠️ Codex 项目路径 ${agent.aid}: ${warning}`);
3113
+ }
3114
+ catch (error) {
3115
+ console.error(`[diagnose] ❌ Codex 项目路径检查失败 ${agent.aid}: ${error instanceof Error ? error.message : String(error)}`);
3116
+ hasError = true;
3117
+ }
3118
+ }
3119
+ if (codexAgents.length === 0) {
3120
+ console.log('[diagnose] - Codex managed hook: 跳过(没有 Agent 使用 Codex)');
3121
+ }
3122
+ else {
3123
+ const hook = inspectCodexManagedHook({ packageRoot: getPackageRoot() });
3124
+ if (hook.status === 'installed') {
3125
+ console.log(`[diagnose] ✓ Codex managed hook 已安装: ${hook.hookPath}`);
3126
+ if (runHookLaunchProbe) {
3127
+ try {
3128
+ const launch = runCodexHookLaunchDiagnostic({ packageRoot: getPackageRoot() });
3129
+ if (launch.status === 'ok') {
3130
+ console.log('[diagnose] ✓ Codex managed hook 启动探针报告: ' + launch.output);
3131
+ for (const result of launch.results) {
3132
+ console.log('[diagnose] ' + result.label + ': exit=' + result.exitCode + ' pid=' + (result.pid ?? 'unknown'));
3133
+ }
3134
+ }
3135
+ else {
3136
+ console.log('[diagnose] - Codex managed hook 启动探针: 跳过(' + (launch.reason || 'not applicable') + ')');
3137
+ }
3138
+ }
3139
+ catch (error) {
3140
+ console.error('[diagnose] ❌ Codex managed hook 启动探针失败: ' + (error instanceof Error ? error.message : String(error)));
3141
+ hasError = true;
3142
+ }
3143
+ }
3144
+ }
3145
+ else if (hook.status === 'skipped') {
3146
+ console.log(`[diagnose] - Codex managed hook: 跳过(${hook.reason})`);
3147
+ }
3148
+ else {
3149
+ const prefix = hook.status === 'missing' ? '⚠️' : '❌';
3150
+ const writer = hook.status === 'missing' ? console.warn : console.error;
3151
+ writer(`[diagnose] ${prefix} Codex managed hook ${hook.status}: ${hook.reason || 'unknown error'}${hook.requirements ? ` (${hook.requirements})` : ''}`);
3152
+ hasError = true;
3153
+ }
3154
+ }
3155
+ // 4. 检查 Anthropic 配置(用首个 self-agent 的 effective config)
2928
3156
  if (agents.length > 0) {
2929
3157
  try {
2930
3158
  const merged = resolveEffective({ self: agents[0].aid });
package/dist/cli/index.js CHANGED
@@ -46,6 +46,7 @@ AUN:
46
46
  fs AUN 文件系统
47
47
  storage 底层对象存储命令
48
48
  rpc 调用底层 AUN RPC
49
+ menu-client 向远程 daemon 发送标准 Menu 请求
49
50
  contact 联系人黑名单管理
50
51
  net AUN 网络链路诊断
51
52
 
@@ -79,7 +80,7 @@ function printLifecycleHelp(command) {
79
80
  logs: `用法: ec logs [选项]\n\n --level <error|warn|info|debug> 最低日志级别\n --module <name> 只显示指定模块\n --raw 原始输出\n --no-color 禁用颜色`,
80
81
  watch: `用法: ec watch [logs|log|aid|msg|web|session]\n\n ec watch 打开监控选择菜单\n ec watch logs 选择日志类型后监控\n ec watch log <类型...> 直接监控指定日志类型\n ec watch aid 监控 AID 连接\n ec watch msg 打开消息 TUI\n ec watch web 安装/升级并启动 EC Web\n ec watch session ec watch web 的兼容别名`,
81
82
  'restart-monitor': '用法: ec restart-monitor\n\n运行内 daemon 重启与升级监控器。',
82
- diagnose: '用法: ec diagnose\n\n检查配置、数据库、进程和编译产物。',
83
+ diagnose: '用法: ec diagnose [--codex-hook]\n\n检查配置、数据库、进程和编译产物。\n\n --codex-hook 运行 Windows Codex managed hook 启动探针。',
83
84
  mv: '用法: ec mv <old-directory> <new-directory>\n\n迁移项目目录以及 Claude、Codex 和 EvolCore 会话路径。',
84
85
  };
85
86
  console.log(help[command]);
@@ -308,7 +309,7 @@ export async function main(args) {
308
309
  printLifecycleHelp('diagnose');
309
310
  break;
310
311
  }
311
- await cmdDiagnose();
312
+ await cmdDiagnose(args.slice(1));
312
313
  break;
313
314
  case 'data': {
314
315
  const { cmdData } = await import('./data-command.js');
@@ -350,6 +351,25 @@ export async function main(args) {
350
351
  await cmdRpc(args.slice(1));
351
352
  break;
352
353
  }
354
+ case 'menu-client': {
355
+ const { suppressSdkLogs } = await import('../aun/aid/index.js');
356
+ suppressSdkLogs();
357
+ const { cmdMenuClient } = await import('./menu-client.js');
358
+ try {
359
+ await cmdMenuClient(args.slice(1));
360
+ }
361
+ catch (error) {
362
+ const payload = {
363
+ ok: false,
364
+ code: typeof error?.code === 'string' ? error.code : 'MENU_CLIENT_FAILED',
365
+ error: error?.message ?? String(error),
366
+ ...(error?.data === undefined ? {} : { data: error.data }),
367
+ };
368
+ console.error(JSON.stringify(payload));
369
+ process.exitCode = 1;
370
+ }
371
+ break;
372
+ }
353
373
  case 'storage': {
354
374
  const { suppressSdkLogs } = await import('../aun/aid/index.js');
355
375
  suppressSdkLogs();
package/dist/cli/init.js CHANGED
@@ -14,6 +14,7 @@ import { defaultProjectsRoot } from '../utils/project-path.js';
14
14
  import { WEB_CLI_BIN, WEB_PACKAGE_LATEST } from '../product.js';
15
15
  import { autostartInstalled, autostartPlatformLabel, configureAutostart } from '../utils/autostart.js';
16
16
  import { askInit, isInitCancelledError, printInitCancelHint } from './init-cancel.js';
17
+ import { AUTO_BASEAGENT_CANDIDATES, BASEAGENT_CANDIDATES, detectAvailableBaseagents, } from '../agents/baseagent-availability.js';
17
18
  // ==================== Helpers ====================
18
19
  function ask(rl, question) {
19
20
  return askInit(rl, question);
@@ -27,17 +28,8 @@ export function expandLeadingTildePath(value, homeDir = os.homedir()) {
27
28
  }
28
29
  return value;
29
30
  }
30
- const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
31
- const AUTO_BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
32
- function isBaseagentAvailable(baseagent, codexAvailability) {
33
- if (baseagent === 'codex')
34
- return (codexAvailability ?? getCodexAppServerAvailability()).available;
35
- if (baseagent === 'ecagent')
36
- return true;
37
- return commandExists(baseagent);
38
- }
39
31
  function detectAvailable(codexAvailability = getCodexAppServerAvailability()) {
40
- return BASEAGENT_CANDIDATES.filter(baseagent => isBaseagentAvailable(baseagent, codexAvailability));
32
+ return detectAvailableBaseagents(codexAvailability);
41
33
  }
42
34
  /** Offer installation only when the probe positively identified an old CLI. */
43
35
  export async function maybeUpgradeCodexForInit(availability, dependencies) {
@@ -12,6 +12,9 @@ const MANAGED_TASK_COMMANDS = new Set([
12
12
  'msg',
13
13
  'queue',
14
14
  'response',
15
+ // `stats` reads the usage database directly. It authorizes each managed call
16
+ // against the task's relation role in `resolveManagedStatsScope`.
17
+ 'stats',
15
18
  'status',
16
19
  'storage',
17
20
  'trigger',