evolcore 0.0.23 → 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 (84) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +3 -1
  3. package/bin/codex-managed-acl.ps1 +7 -2
  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 +17 -6
  7. package/dist/agents/baseagent-availability.js +4 -4
  8. package/dist/agents/baseagent.js +29 -1
  9. package/dist/agents/claude-runner.js +148 -13
  10. package/dist/agents/codex/app-server-client.js +18 -8
  11. package/dist/agents/codex/catalog.js +1 -1
  12. package/dist/agents/codex/diagnostics.js +215 -0
  13. package/dist/agents/codex-runner.js +438 -59
  14. package/dist/agents/ecagent-runner.js +17 -3
  15. package/dist/agents/gemini-runner.js +60 -2
  16. package/dist/agents/runner-types.js +1 -1
  17. package/dist/aun/aid/managed-operation.js +28 -6
  18. package/dist/aun/aid/store.js +41 -0
  19. package/dist/aun/msg/managed-operation.js +6 -1
  20. package/dist/aun/outbox.js +56 -18
  21. package/dist/aun/rpc/client.js +4 -5
  22. package/dist/channels/aun.js +147 -76
  23. package/dist/channels/daemon.js +7 -1
  24. package/dist/channels/dingtalk.js +1 -0
  25. package/dist/channels/feishu.js +10 -4
  26. package/dist/channels/wecom.js +2 -1
  27. package/dist/cli/agent-command.js +29 -3
  28. package/dist/cli/agent-create.js +538 -0
  29. package/dist/cli/agent.js +26 -675
  30. package/dist/cli/aun-commands.js +60 -20
  31. package/dist/cli/ctl-command.js +2 -0
  32. package/dist/cli/daemon-commands.js +148 -17
  33. package/dist/cli/index.js +2 -2
  34. package/dist/cli/managed-command-guard.js +3 -0
  35. package/dist/cli/restart-monitor.js +432 -8
  36. package/dist/cli/stats.js +110 -0
  37. package/dist/config/builtin-roles.js +2 -0
  38. package/dist/core/auth/auth-gateway.js +3 -2
  39. package/dist/core/auth/authorization-audit.js +72 -19
  40. package/dist/core/auth/operation-authorizer.js +63 -3
  41. package/dist/core/auth/operation-catalog.js +10 -2
  42. package/dist/core/auth/remote-access-store.js +3 -2
  43. package/dist/core/baseagent-loader.js +4 -4
  44. package/dist/core/bootstrap-messages.js +39 -18
  45. package/dist/core/bootstrap-service.js +32 -26
  46. package/dist/core/capability/capability-manager.js +8 -4
  47. package/dist/core/capability/providers/codex-capability-provider.js +1 -1
  48. package/dist/core/command/agent-control.js +7 -2
  49. package/dist/core/command/command-handler.js +87 -25
  50. package/dist/core/command/menu-catalog.js +3 -1
  51. package/dist/core/command/menu-handler.js +274 -70
  52. package/dist/core/command/menu-protocol.js +6 -2
  53. package/dist/core/command/slash-gate.js +1 -3
  54. package/dist/core/command/slash-handler.js +157 -48
  55. package/dist/core/data-migration.js +5 -15
  56. package/dist/core/event-catalog.js +3 -0
  57. package/dist/core/handoff/runtime.js +3 -1
  58. package/dist/core/handoff/store.js +7 -1
  59. package/dist/core/message/im-renderer.js +13 -6
  60. package/dist/core/message/message-bridge.js +5 -36
  61. package/dist/core/message/message-utils.js +6 -3
  62. package/dist/core/message/response-engine.js +73 -21
  63. package/dist/core/model/catalog-service.js +3 -1
  64. package/dist/core/model/model-catalog.js +10 -0
  65. package/dist/core/model/model-permission.js +4 -1
  66. package/dist/core/permission/approval-gateway.js +169 -33
  67. package/dist/core/permission/tool-policy.js +13 -6
  68. package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
  69. package/dist/core/session/session-manager.js +49 -13
  70. package/dist/core/session/session-renew.js +1 -0
  71. package/dist/index.js +174 -28
  72. package/dist/ipc.js +68 -21
  73. package/dist/paths.js +23 -1
  74. package/dist/stats/billing.js +18 -4
  75. package/dist/utils/atomic-write.js +26 -2
  76. package/dist/utils/cross-platform.js +436 -1
  77. package/dist/utils/error-utils.js +11 -4
  78. package/dist/utils/model-prices.jsonl +23 -20
  79. package/dist/utils/npm-ops.js +24 -1
  80. package/dist/utils/windows-autostart.js +1 -1
  81. package/kits/templates/roles/admin.json +2 -0
  82. package/package.json +7 -3
  83. package/AGENTS.md +0 -62
  84. package/dist/utils/codex-cli.js +0 -43
@@ -5,6 +5,7 @@ import { isExplicitGroupId } from '../aun/group-identity.js';
5
5
  import { getArgValue, isHelpFlag, wantsHelp } from './help.js';
6
6
  import { ipcQuery } from '../ipc.js';
7
7
  import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
8
+ import { isManagementRole } from '../config/builtin-roles.js';
8
9
  import { readBestTaskRuntimeContext } from './task-context.js';
9
10
  // ==================== AID ====================
10
11
  function resolveAunPath(args) {
@@ -13,24 +14,35 @@ function resolveAunPath(args) {
13
14
  return args[idx + 1];
14
15
  return process.env.AUN_HOME || undefined;
15
16
  }
17
+ function isRedactedAidShowResult(info) {
18
+ return 'redacted' in info && info.redacted === true;
19
+ }
16
20
  function renderAidShow(info, formatJson) {
17
21
  if (formatJson) {
18
22
  console.log(JSON.stringify(info, null, 2));
19
23
  return;
20
24
  }
21
25
  console.log(`AID: ${info.aid}`);
22
- console.log(` 私钥: ${info.hasPrivateKey ? '有' : '无'}`);
26
+ const redacted = isRedactedAidShowResult(info);
27
+ if (!redacted) {
28
+ console.log(` 私钥: ${info.hasPrivateKey ? '有' : '无'}`);
29
+ }
23
30
  console.log(` agent.md: ${info.hasAgentMd ? '有' : '无'}`);
24
31
  if (info.hasAgentMd) {
32
+ const signatureReason = redacted ? undefined : info.agentMdSignatureReason;
25
33
  const sigLabel = info.agentMdSignature === 'verified' ? '✓ 已验签'
26
34
  : info.agentMdSignature === 'unsigned' ? '⚠ 未签名'
27
- : info.agentMdSignature === 'invalid' ? `✗ 签名无效${info.agentMdSignatureReason ? ': ' + info.agentMdSignatureReason : ''}`
35
+ : info.agentMdSignature === 'invalid' ? `✗ 签名无效${signatureReason ? ': ' + signatureReason : ''}`
28
36
  : '? 未知';
29
37
  console.log(` 签名状态: ${sigLabel}`);
30
38
  }
31
39
  console.log(` 证书到期: ${info.certExpiresAt ?? '无证书'}${info.certExpired ? ' (已过期!)' : ''}`);
32
40
  if (info.certSubject)
33
41
  console.log(` 证书主体: ${info.certSubject}`);
42
+ if (redacted) {
43
+ console.log(' 本地密钥状态: 已脱敏');
44
+ return;
45
+ }
34
46
  if (info.keyMatchesCert === false)
35
47
  console.log(' 密钥/证书: ✗ 公钥不匹配(cert.pem 与 key.json 公钥不一致)');
36
48
  else if (info.keyMatchesCert === true)
@@ -104,7 +116,8 @@ async function runManagedAid(args, formatJson) {
104
116
  if (!parsed.ok)
105
117
  failManagedAid(formatJson, parsed.code, parsed.reason);
106
118
  const runtime = await readBestTaskRuntimeContext();
107
- if (runtime?.selfAid && parsed.command.aid !== runtime.selfAid.replace(/^@/, '')) {
119
+ const requiresSelfAid = parsed.command.kind === 'show' || parsed.command.kind === 'agentmd.put';
120
+ if (requiresSelfAid && runtime?.selfAid && parsed.command.aid !== runtime.selfAid.replace(/^@/, '')) {
108
121
  failManagedAid(formatJson, 'SCOPE_MISMATCH', `托管任务只能操作当前 self AID:${runtime.selfAid}`);
109
122
  }
110
123
  const response = await ipcQuery(resolvePaths().socket, {
@@ -212,7 +225,12 @@ async function runManagedMsg(args, formatJson) {
212
225
  if (runtime && (runtime.channel !== 'aun' || runtime.chatType !== 'private')) {
213
226
  failManagedGroup(formatJson, 'SCOPE_MISMATCH', 'ec msg history/online 只能读取当前 AUN 私聊关系');
214
227
  }
215
- if (runtime?.channelId && parsed.command.target !== runtime.channelId.replace(/^@/, '')) {
228
+ // owner/admin 可以跨对端读历史(每次查询仍只针对一个对端)。这里只是快速
229
+ // 预检:角色未知时放行交给 daemon 判定,daemon 侧 handleMsgOperation 才是
230
+ // 权威关卡,member 由 currentRelationOnly 约束、visitor 直接拒绝。
231
+ const mayReadCrossPeer = parsed.command.kind === 'history'
232
+ && (runtime?.peerRole === undefined || isManagementRole(runtime.peerRole));
233
+ if (!mayReadCrossPeer && runtime?.channelId && parsed.command.target !== runtime.channelId.replace(/^@/, '')) {
216
234
  failManagedGroup(formatJson, 'SCOPE_MISMATCH', `只能读取当前 AUN 私聊对端:${runtime.channelId}`);
217
235
  }
218
236
  }
@@ -1053,7 +1071,7 @@ Commands:
1053
1071
  send <from> <to> --file <path> [--as <type>] 发送文件(image|video|voice|file)
1054
1072
  send <from> <to> --link <url> [--title T] 发送链接卡片
1055
1073
  send <from> <to> --payload <json> 发送自定义 payload
1056
- pull <from> [--after-seq N] [--limit N] 拉取收件箱
1074
+ pull <from> [--after-seq N] [--limit N] --app <name> 拉取收件箱(必须使用独立 slot)
1057
1075
  ack <from> <seq> [--app <name>] 确认已读
1058
1076
  recall <from> <message-id> [<message-id>...] 撤回消息
1059
1077
  online <from> <target-aid> [<target-aid>...] 查询在线状态
@@ -1142,6 +1160,20 @@ Options:
1142
1160
  }
1143
1161
  const { msgSend, msgPull, msgAck, msgRecall, msgOnline, msgHistory } = await import('../aun/msg/index.js');
1144
1162
  const commonOpts = { aunPath, slotId: appSlot };
1163
+ const requireConsumerSlot = (operation) => {
1164
+ if (appSlot)
1165
+ return appSlot;
1166
+ const error = {
1167
+ ok: false,
1168
+ code: 'APP_SLOT_REQUIRED',
1169
+ error: `${operation} requires an explicit --app <name> isolated consumer slot; refusing to access the daemon inbox`,
1170
+ };
1171
+ if (formatJson)
1172
+ console.log(JSON.stringify(error));
1173
+ else
1174
+ console.error(`❌ ${error.error}`);
1175
+ process.exit(1);
1176
+ };
1145
1177
  if (sub === 'history') {
1146
1178
  const target = args[2] && !args[2].startsWith('--') ? args[2] : undefined;
1147
1179
  const historyValueFlags = new Set([
@@ -1389,9 +1421,7 @@ Options:
1389
1421
  return;
1390
1422
  }
1391
1423
  if (sub === 'pull') {
1392
- if (!appSlot) {
1393
- console.warn('⚠ 警告: 未传 --app,当前与 daemon 共享 evolcore 消费通道。pull 会看到/影响 daemon 的消息消费;如需独立消费请用 --app <name>');
1394
- }
1424
+ const consumerSlot = requireConsumerSlot('ec msg pull');
1395
1425
  const afterSeqStr = getArgValue(args, '--after-seq');
1396
1426
  const limitStr = getArgValue(args, '--limit');
1397
1427
  const afterSeq = afterSeqStr !== undefined ? Number(afterSeqStr) : undefined;
@@ -1404,7 +1434,7 @@ Options:
1404
1434
  console.error(`❌ --limit 必须是数字: ${limitStr}`);
1405
1435
  process.exit(1);
1406
1436
  }
1407
- const result = await msgPull({ from, afterSeq, limit, ...commonOpts });
1437
+ const result = await msgPull({ from, afterSeq, limit, aunPath, slotId: consumerSlot });
1408
1438
  if (!result.ok) {
1409
1439
  if (formatJson) {
1410
1440
  console.log(JSON.stringify(result));
@@ -1440,10 +1470,8 @@ Options:
1440
1470
  console.error(`❌ seq 必须是数字: ${seqStr}`);
1441
1471
  process.exit(1);
1442
1472
  }
1443
- if (!appSlot) {
1444
- console.warn('⚠ 警告: 未传 --app,ack 将推进与 daemon 共享的 evolcore 消费游标,可能影响 daemon 收消息;如需独立请用 --app <name>');
1445
- }
1446
- const result = await msgAck({ from, seq, ...commonOpts });
1473
+ const consumerSlot = requireConsumerSlot('ec msg ack');
1474
+ const result = await msgAck({ from, seq, aunPath, slotId: consumerSlot });
1447
1475
  if (!result.ok) {
1448
1476
  if (formatJson) {
1449
1477
  console.log(JSON.stringify(result));
@@ -1546,7 +1574,7 @@ export async function cmdGroup(args) {
1546
1574
  send <from> <group-id> <text> --return <none|required> 发送群文本(跨会话时 --return 必填)
1547
1575
  send <from> <group-id> --file <path> [--as <type>] 发送群文件
1548
1576
  send <from> <group-id> --payload <json> 发送自定义 payload
1549
- pull <from> <group-id> [--after-seq N] [--limit N] 拉取群消息
1577
+ pull <from> <group-id> [--after-seq N] [--limit N] --app <name> 拉取群消息(必须使用独立 slot)
1550
1578
  ack <from> <group-id> <seq> [--app <name>] 确认已读
1551
1579
 
1552
1580
  群管理:
@@ -1877,12 +1905,18 @@ Options:
1877
1905
  }
1878
1906
  if (sub === 'pull') {
1879
1907
  const groupId = requireGroupId();
1880
- if (!appSlot) {
1881
- console.warn('⚠ 警告: 未传 --app,当前与 daemon 共享 evolcore 消费通道。pull 会看到/影响 daemon 的消息消费;如需独立消费请用 --app <name>');
1908
+ const consumerSlot = appSlot;
1909
+ if (!consumerSlot) {
1910
+ const error = 'ec group pull requires an explicit --app <name> isolated consumer slot; refusing to access the daemon inbox';
1911
+ if (formatJson)
1912
+ console.log(JSON.stringify({ ok: false, code: 'APP_SLOT_REQUIRED', error }));
1913
+ else
1914
+ console.error(`❌ ${error}`);
1915
+ process.exit(1);
1882
1916
  }
1883
1917
  const afterSeq = parseNumberFlag('--after-seq', '--after-seq');
1884
1918
  const limit = parseNumberFlag('--limit', '--limit');
1885
- const result = await groupPull({ from, groupId, afterSeq, limit, ...commonOpts });
1919
+ const result = await groupPull({ from, groupId, afterSeq, limit, aunPath, slotId: consumerSlot });
1886
1920
  outputResult(result, () => {
1887
1921
  const r = result;
1888
1922
  console.log(`✓ ${r.messages.length} 条消息,latest_seq=${r.latest_message_seq}${r.has_more ? '(还有更多)' : ''}`);
@@ -1905,10 +1939,16 @@ Options:
1905
1939
  console.error(`❌ seq 必须是数字: ${seqStr}`);
1906
1940
  process.exit(1);
1907
1941
  }
1908
- if (!appSlot) {
1909
- console.warn('⚠ 警告: 未传 --app,ack 将推进与 daemon 共享的 evolcore 消费游标,可能影响 daemon 收消息;如需独立请用 --app <name>');
1942
+ const consumerSlot = appSlot;
1943
+ if (!consumerSlot) {
1944
+ const error = 'ec group ack requires an explicit --app <name> isolated consumer slot; refusing to access the daemon inbox';
1945
+ if (formatJson)
1946
+ console.log(JSON.stringify({ ok: false, code: 'APP_SLOT_REQUIRED', error }));
1947
+ else
1948
+ console.error(`❌ ${error}`);
1949
+ process.exit(1);
1910
1950
  }
1911
- const result = await groupAck({ from, groupId, seq, ...commonOpts });
1951
+ const result = await groupAck({ from, groupId, seq, aunPath, slotId: consumerSlot });
1912
1952
  outputResult(result, () => {
1913
1953
  const r = result;
1914
1954
  console.log(`✓ ack_seq=${r.ack_seq}`);
@@ -7,6 +7,7 @@ const CTL_HELP = `用法: ec ctl <command> [args...]
7
7
 
8
8
  查询:
9
9
  status 查看会话状态
10
+ status --format json 查看当前任务的结构化运行上下文
10
11
  pwd 显示当前项目路径
11
12
  help 显示帮助
12
13
 
@@ -20,6 +21,7 @@ const CTL_HELP = `用法: ec ctl <command> [args...]
20
21
 
21
22
  示例:
22
23
  ec ctl status
24
+ ec ctl status --format json
23
25
  ec ctl name "新会话名称"
24
26
  ec ctl send "任务已完成"
25
27
  ec ctl file report.md
@@ -30,8 +30,50 @@ import { inspectDataMigrationRequirement } from '../core/data-migration.js';
30
30
  import { isInitCancelledError, printInitCancelHint } from './init-cancel.js';
31
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 } from '../agents/codex/diagnostics.js';
33
+ import { checkCodexProjectPath, inspectCodexManagedHook, ensureCodexManagedHook, runCodexHookLaunchDiagnostic, } from '../agents/codex/diagnostics.js';
34
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
+ }
35
77
  async function runAutomaticInit(action) {
36
78
  try {
37
79
  return await action();
@@ -66,8 +108,8 @@ function printNoSelfAgentHints(options) {
66
108
  // taskkill, even though the daemon is healthy and able to shut down gracefully.
67
109
  const DAEMON_PROBE_TIMEOUT_MS = platform.isWindows ? 5_000 : 1_000;
68
110
  export const DAEMON_SHUTDOWN_TIMEOUT_MS = platform.isWindows ? 10_000 : 3_000;
69
- async function probeDaemon(socketPath, timeoutMs = DAEMON_PROBE_TIMEOUT_MS) {
70
- 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 });
71
113
  if (response?.pong !== true || !Number.isInteger(response.pid) || response.pid <= 0)
72
114
  return null;
73
115
  return response;
@@ -205,19 +247,39 @@ function runSourceBuild() {
205
247
  });
206
248
  });
207
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
+ }
208
266
  async function requireFreshSourceBuild() {
209
267
  if (!sourceBuildNeedsRefresh())
210
268
  return true;
211
269
  return await runSourceBuild();
212
270
  }
213
- function requireCompletedDataMigration(root) {
271
+ function requireCompletedDataMigration(root, action = 'start') {
214
272
  const requirement = inspectDataMigrationRequirement(root);
215
273
  if (!requirement.required)
216
274
  return true;
217
- console.error(`❌ 检测到 ${requirement.operationCount} 项待确认的用户数据迁移,服务未启动。`);
275
+ console.error(action === 'restart'
276
+ ? `❌ 检测到 ${requirement.operationCount} 项待确认的用户数据迁移,本次不停止当前服务。`
277
+ : `❌ 检测到 ${requirement.operationCount} 项待确认的用户数据迁移,服务未启动。`);
218
278
  console.error(' 先执行: ec data migrate --dry-run');
219
279
  console.error(' 确认后: ec data migrate --apply');
220
280
  console.error(' 迁移成功并校验后,再执行 ec start 或 ec restart。');
281
+ if (action === 'restart')
282
+ console.error(' 本次 ec restart 仅完成只读检查,未停止当前服务。');
221
283
  process.exitCode = 2;
222
284
  return false;
223
285
  }
@@ -386,6 +448,14 @@ export async function cmdStart(opts = {}) {
386
448
  const p = resolvePaths();
387
449
  const existingDaemon = await probeDaemon(p.socket);
388
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
+ }
389
459
  console.log(` EvolCore is already running (PID: ${existingDaemon.pid}, IPC reachable)`);
390
460
  console.log(` 已在运行;已运行时长: ${resolveRunningDuration(existingDaemon.pid, existingDaemon.uptime)};部署/脚本可直接复用当前实例`);
391
461
  return;
@@ -424,6 +494,11 @@ export async function cmdStart(opts = {}) {
424
494
  console.log('\n初始化完成,开始启动 EvolCore ....');
425
495
  }
426
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;
427
502
  // 控制 AID 门禁:缺 aid 且交互式 → 只补全控制 AID + owners(不重走 baseagent 向导)。
428
503
  // 非 TTY(restart-monitor/systemd/管道)不补全(无法交互),只提示后继续启动,daemon 侧 warn 兜底。
429
504
  const daemonCfgStart = loadDaemonConfig();
@@ -758,6 +833,8 @@ export async function cmdStop() {
758
833
  export async function cmdRestart(opts = {}) {
759
834
  const cmdStartedAt = Date.now();
760
835
  console.log('🔄 Restarting EvolCore...');
836
+ if (!ensureCodexHookBeforeLifecycle())
837
+ return;
761
838
  const initialStatus = scanInstances();
762
839
  const initialOrphans = findOrphanProcesses();
763
840
  const initialRuntimeOrphans = initialOrphans.filter(orphan => isRuntimeOrphan(orphan));
@@ -769,12 +846,12 @@ export async function cmdRestart(opts = {}) {
769
846
  if (initialRuntimeOrphans.length === 0 && rejectPidIsolatedLifecycle('restart', initialStatus, ping))
770
847
  return;
771
848
  }
772
- if (!await requireFreshSourceBuild())
773
- return;
849
+ // 构建交给 restart-monitor:它在 daemon 停止之后才编译,dist/ 不会被活进程
850
+ // 占用,失败还能回滚到上一份产物。这里再编译一次会落在事务保护之外。
774
851
  // Keep restart behavior identical to startup: both paths use one upgrade
775
852
  // check implementation and continue when an upgrade is unavailable.
776
853
  await runUpgradeChecks();
777
- if (!requireCompletedDataMigration(resolveRoot()))
854
+ if (!requireCompletedDataMigration(resolveRoot(), 'restart'))
778
855
  return;
779
856
  // 停止所有活 main 进程(可能不止一个)
780
857
  const status = scanInstances();
@@ -854,11 +931,30 @@ export async function cmdRestart(opts = {}) {
854
931
  }
855
932
  }
856
933
  console.log(`⏱ restart prep done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s, starting...`);
857
- // 通过 restart-monitor 启动,避免在 agent 内调用时自杀
858
- const appCli = path.join(getPackageRoot(), 'dist', 'cli', 'index.js');
859
- platform.spawnDetachedNode([appCli, 'restart-monitor'], {
860
- EVOLCORE_HOME: resolvePaths().root,
861
- });
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
+ }
862
958
  console.log('🔄 Restart monitor started, waiting for service to come back online...');
863
959
  await sleep(2000);
864
960
  // 代码统计(开发环境)
@@ -1032,7 +1128,11 @@ export async function cmdStatus() {
1032
1128
  // Start all read-only daemon queries together. On a busy Windows host each
1033
1129
  // query can otherwise wait behind the same IPC/event-loop delay and make the
1034
1130
  // total `ec status` time the sum of several individual timeouts.
1035
- 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
+ });
1036
1136
  const statusPromise = ipcQuery(p.socket, { type: 'status' }).catch(() => null);
1037
1137
  const aidsPromise = ipcQuery(p.socket, { type: 'aun-aids' }).catch(() => null);
1038
1138
  const agentsPromise = ipcQuery(p.socket, { type: 'evolagent.list' }).catch(() => null);
@@ -1049,6 +1149,11 @@ export async function cmdStatus() {
1049
1149
  }
1050
1150
  if (pid) {
1051
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
+ }
1052
1157
  console.log('');
1053
1158
  console.log('📊 Process Info:');
1054
1159
  if (processVisible) {
@@ -1159,7 +1264,9 @@ export async function cmdStatus() {
1159
1264
  }
1160
1265
  }
1161
1266
  else {
1162
- 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)}`);
1163
1270
  }
1164
1271
  // Session & Project statistics (从文件系统读)
1165
1272
  if (fs.existsSync(p.sessionsDir)) {
@@ -2905,7 +3012,12 @@ async function cmdWatchWeb() {
2905
3012
  const cfg = loadDaemonConfig();
2906
3013
  const currentService = ecwebService(cfg.services);
2907
3014
  const port = currentService?.port ?? DEFAULT_ECWEB_PORT;
2908
- 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
+ }
2909
3021
  const ok = await startEcwebIfEnabled(p, { forceRestart: true });
2910
3022
  if (!ok)
2911
3023
  process.exit(1); // 失败原因已由 startEcwebIfEnabled 打印
@@ -2944,9 +3056,10 @@ export async function cmdMv(oldDir, newDir) {
2944
3056
  }
2945
3057
  }
2946
3058
  // ==================== Diagnose ====================
2947
- export async function cmdDiagnose() {
3059
+ export async function cmdDiagnose(args = []) {
2948
3060
  const p = resolvePaths();
2949
3061
  let hasError = false;
3062
+ const runHookLaunchProbe = args.includes('--codex-hook');
2950
3063
  // 1. 检查数据目录
2951
3064
  console.log(`[diagnose] EVOLCORE_HOME = ${p.root}`);
2952
3065
  if (!fs.existsSync(p.root)) {
@@ -3010,6 +3123,24 @@ export async function cmdDiagnose() {
3010
3123
  const hook = inspectCodexManagedHook({ packageRoot: getPackageRoot() });
3011
3124
  if (hook.status === 'installed') {
3012
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
+ }
3013
3144
  }
3014
3145
  else if (hook.status === 'skipped') {
3015
3146
  console.log(`[diagnose] - Codex managed hook: 跳过(${hook.reason})`);
package/dist/cli/index.js CHANGED
@@ -80,7 +80,7 @@ function printLifecycleHelp(command) {
80
80
  logs: `用法: ec logs [选项]\n\n --level <error|warn|info|debug> 最低日志级别\n --module <name> 只显示指定模块\n --raw 原始输出\n --no-color 禁用颜色`,
81
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 的兼容别名`,
82
82
  'restart-monitor': '用法: ec restart-monitor\n\n运行内 daemon 重启与升级监控器。',
83
- diagnose: '用法: ec diagnose\n\n检查配置、数据库、进程和编译产物。',
83
+ diagnose: '用法: ec diagnose [--codex-hook]\n\n检查配置、数据库、进程和编译产物。\n\n --codex-hook 运行 Windows Codex managed hook 启动探针。',
84
84
  mv: '用法: ec mv <old-directory> <new-directory>\n\n迁移项目目录以及 Claude、Codex 和 EvolCore 会话路径。',
85
85
  };
86
86
  console.log(help[command]);
@@ -309,7 +309,7 @@ export async function main(args) {
309
309
  printLifecycleHelp('diagnose');
310
310
  break;
311
311
  }
312
- await cmdDiagnose();
312
+ await cmdDiagnose(args.slice(1));
313
313
  break;
314
314
  case 'data': {
315
315
  const { cmdData } = await import('./data-command.js');
@@ -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',