evolcore 0.0.22 → 0.0.23

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 (75) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +24 -14
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/install-codex-managed-hooks.mjs +373 -95
  6. package/dist/agents/baseagent-availability.js +48 -0
  7. package/dist/agents/baseagent.js +1 -1
  8. package/dist/agents/claude-runner.js +30 -226
  9. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +73 -13
  10. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  11. package/dist/agents/codex/catalog.js +70 -0
  12. package/dist/agents/codex/diagnostics.js +154 -0
  13. package/dist/agents/codex-runner.js +407 -139
  14. package/dist/agents/ecagent-runner.js +10 -31
  15. package/dist/agents/gemini-runner.js +2 -6
  16. package/dist/aun/aid/store.js +2 -0
  17. package/dist/aun/menu-client.js +272 -0
  18. package/dist/aun/msg/group.js +65 -4
  19. package/dist/aun/msg/index.js +1 -1
  20. package/dist/aun/msg/managed-operation.js +51 -4
  21. package/dist/aun/rpc/client.js +8 -1
  22. package/dist/channels/aun.js +31 -1
  23. package/dist/channels/wechat.js +81 -10
  24. package/dist/cli/aun-commands.js +48 -4
  25. package/dist/cli/command-log.js +21 -2
  26. package/dist/cli/daemon-commands.js +142 -45
  27. package/dist/cli/index.js +20 -0
  28. package/dist/cli/init.js +2 -10
  29. package/dist/cli/menu-client.js +95 -0
  30. package/dist/cli/model.js +23 -8
  31. package/dist/cli/task-context.js +12 -2
  32. package/dist/cli/trigger-command.js +9 -1
  33. package/dist/config/schema-migration.js +14 -2
  34. package/dist/core/auth/agent-delegation.js +10 -7
  35. package/dist/core/auth/authorization-audit.js +4 -0
  36. package/dist/core/auth/operation-catalog.js +4 -0
  37. package/dist/core/auth/remote-access-store.js +135 -0
  38. package/dist/core/command/command-handler.js +41 -5
  39. package/dist/core/command/daemon-diagnostics.js +448 -0
  40. package/dist/core/command/menu-catalog.js +3 -0
  41. package/dist/core/command/menu-handler.js +141 -33
  42. package/dist/core/command/menu-protocol.js +119 -0
  43. package/dist/core/command/slash-gate.js +3 -3
  44. package/dist/core/command/slash-handler.js +42 -22
  45. package/dist/core/message/message-queue.js +20 -15
  46. package/dist/core/message/response-engine.js +74 -73
  47. package/dist/core/model/catalog-service.js +308 -0
  48. package/dist/core/model/model-catalog.js +124 -334
  49. package/dist/core/model/model-diagnostics.js +4 -5
  50. package/dist/core/model/model-fallback.js +9 -16
  51. package/dist/core/model/model-permission.js +1 -1
  52. package/dist/core/permission/approval-gateway.js +16 -1
  53. package/dist/core/permission/protected-paths.js +63 -4
  54. package/dist/core/permission/sandbox-runtime.js +4 -2
  55. package/dist/core/permission/tool-policy.js +244 -25
  56. package/dist/core/runtime-lock.js +13 -8
  57. package/dist/index.js +40 -29
  58. package/dist/ipc.js +123 -81
  59. package/dist/trigger/manager.js +3 -1
  60. package/dist/trigger/parser.js +77 -13
  61. package/dist/trigger/patch.js +104 -1
  62. package/dist/utils/ecweb-supervisor.js +11 -2
  63. package/dist/utils/ecweb-utils.js +8 -26
  64. package/dist/utils/error-utils.js +7 -0
  65. package/dist/utils/npm-ops.js +47 -15
  66. package/kits/docs/evolcore/INDEX.md +1 -0
  67. package/kits/docs/evolcore/agent-create.md +13 -0
  68. package/kits/docs/evolcore/group-collaboration.md +7 -2
  69. package/kits/docs/evolcore/menu-client.md +34 -0
  70. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  71. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  72. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  73. package/kits/templates/roles/visitor.json +1 -0
  74. package/kits/templates/system-fragments/bootstrap.md +8 -1
  75. package/package.json +11 -3
@@ -40,6 +40,8 @@ const BACKOFF_DELAY_MS = 30_000;
40
40
  const RETRY_DELAY_MS = 2_000;
41
41
  const TYPING_TICKET_TTL_MS = 5 * 60 * 1000; // 5 min cache
42
42
  const SESSION_EXPIRED_ERRCODE = -14;
43
+ const INVALID_SYNC_CURSOR_ERRCODE = -1;
44
+ const MAX_SYNC_CURSOR_BYTES = 64 * 1024;
43
45
  export class WechatApiError extends Error {
44
46
  endpoint;
45
47
  ret;
@@ -71,6 +73,26 @@ export class WechatContextTokenRefreshRequiredError extends Error {
71
73
  }
72
74
  }
73
75
  }
76
+ /**
77
+ * iLink cursors are opaque text, so validation deliberately stays conservative:
78
+ * reject values that cannot be safely round-tripped through the UTF-8 state file
79
+ * without imposing a specific encoding on future protocol versions.
80
+ */
81
+ export function validateWechatSyncCursor(cursor) {
82
+ if (typeof cursor !== 'string') {
83
+ return { valid: false, reason: 'invalid_type' };
84
+ }
85
+ if (Buffer.byteLength(cursor, 'utf8') > MAX_SYNC_CURSOR_BYTES) {
86
+ return { valid: false, reason: 'too_large' };
87
+ }
88
+ if (cursor.includes('\uFFFD')) {
89
+ return { valid: false, reason: 'invalid_utf8' };
90
+ }
91
+ if (/[\s\u0000-\u001F\u007F-\u009F]/u.test(cursor)) {
92
+ return { valid: false, reason: 'invalid_character' };
93
+ }
94
+ return { valid: true };
95
+ }
74
96
  /** Classify fetch/undici failures using the categories exposed by Tencent 2.4.6. */
75
97
  export function classifyFetchError(err) {
76
98
  if (err instanceof Error && err.name === 'AbortError') {
@@ -196,6 +218,7 @@ export class WechatChannel {
196
218
  pendingContextMessages = new Map();
197
219
  typingTicketCache = new Map();
198
220
  getUpdatesBuf = '';
221
+ lastRejectedSyncCursor;
199
222
  syncBufPath;
200
223
  contextTokensPath;
201
224
  onSessionExpired;
@@ -243,14 +266,24 @@ export class WechatChannel {
243
266
  throw new Error('WeChat reauthentication required; run: ec init wechat');
244
267
  }
245
268
  // 恢复游标
269
+ this.getUpdatesBuf = '';
270
+ this.lastRejectedSyncCursor = undefined;
246
271
  try {
247
272
  if (fs.existsSync(this.syncBufPath)) {
248
- this.getUpdatesBuf = fs.readFileSync(this.syncBufPath, 'utf-8');
249
- logger.info(`[WeChat] Restored sync cursor (${this.getUpdatesBuf.length} bytes)`);
273
+ const restoredCursor = fs.readFileSync(this.syncBufPath, 'utf-8');
274
+ const validation = validateWechatSyncCursor(restoredCursor);
275
+ if (validation.valid) {
276
+ this.getUpdatesBuf = restoredCursor;
277
+ logger.info(`[WeChat] Restored sync cursor (${Buffer.byteLength(restoredCursor, 'utf8')} bytes)`);
278
+ }
279
+ else {
280
+ this.clearSyncCursor(`persisted cursor is invalid (${validation.reason})`, restoredCursor);
281
+ }
250
282
  }
251
283
  }
252
- catch {
253
- // ignore
284
+ catch (err) {
285
+ logger.warn(`[WeChat] Failed to restore sync cursor; starting from an empty cursor: ${err instanceof Error ? err.message : String(err)}`);
286
+ this.getUpdatesBuf = '';
254
287
  }
255
288
  // 通知 ilink 后端:bot 上线
256
289
  try {
@@ -431,13 +464,22 @@ export class WechatChannel {
431
464
  }
432
465
  consecutiveFailures = 0;
433
466
  // 保存游标
434
- if (resp.get_updates_buf) {
435
- this.getUpdatesBuf = resp.get_updates_buf;
436
- try {
437
- fs.writeFileSync(this.syncBufPath, this.getUpdatesBuf, 'utf-8');
467
+ if (resp.get_updates_buf !== undefined) {
468
+ const validation = validateWechatSyncCursor(resp.get_updates_buf);
469
+ if (!validation.valid) {
470
+ this.clearSyncCursor(`server returned an invalid cursor (${validation.reason})`, typeof resp.get_updates_buf === 'string' ? resp.get_updates_buf : undefined);
438
471
  }
439
- catch {
440
- // best-effort
472
+ else if (resp.get_updates_buf) {
473
+ if (resp.get_updates_buf !== this.lastRejectedSyncCursor) {
474
+ this.lastRejectedSyncCursor = undefined;
475
+ }
476
+ this.getUpdatesBuf = resp.get_updates_buf;
477
+ try {
478
+ fs.writeFileSync(this.syncBufPath, this.getUpdatesBuf, 'utf-8');
479
+ }
480
+ catch {
481
+ // best-effort
482
+ }
441
483
  }
442
484
  }
443
485
  // 处理消息
@@ -450,6 +492,17 @@ export class WechatChannel {
450
492
  return;
451
493
  if (this.isSessionPaused())
452
494
  return;
495
+ if (err instanceof WechatApiError &&
496
+ err.endpoint === 'ilink/bot/getupdates' &&
497
+ err.hasCode(INVALID_SYNC_CURSOR_ERRCODE) &&
498
+ this.getUpdatesBuf &&
499
+ this.getUpdatesBuf !== this.lastRejectedSyncCursor) {
500
+ const rejectedCursor = this.getUpdatesBuf;
501
+ this.clearSyncCursor('getUpdates returned ret=-1', rejectedCursor);
502
+ consecutiveFailures = 0;
503
+ logger.warn('[WeChat] Retrying getUpdates immediately with an empty sync cursor');
504
+ continue;
505
+ }
453
506
  consecutiveFailures++;
454
507
  if (err instanceof WechatApiError) {
455
508
  logger.error(`[WeChat] getUpdates API error (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES}): ${err.message}`);
@@ -470,6 +523,24 @@ export class WechatChannel {
470
523
  }
471
524
  }
472
525
  }
526
+ clearSyncCursor(reason, rejectedCursor) {
527
+ if (rejectedCursor)
528
+ this.lastRejectedSyncCursor = rejectedCursor;
529
+ this.getUpdatesBuf = '';
530
+ try {
531
+ let persistedAction = 'no persisted cursor';
532
+ if (fs.existsSync(this.syncBufPath)) {
533
+ const quarantinePath = `${this.syncBufPath}.corrupt-${Date.now()}`;
534
+ fs.renameSync(this.syncBufPath, quarantinePath);
535
+ persistedAction = `quarantined as ${path.basename(quarantinePath)}`;
536
+ }
537
+ logger.warn(`[WeChat] Cleared sync cursor: ${reason} (${persistedAction})`);
538
+ }
539
+ catch (err) {
540
+ logger.warn(`[WeChat] Cleared in-memory sync cursor but failed to quarantine persisted cursor: ` +
541
+ `${err instanceof Error ? err.message : String(err)}`);
542
+ }
543
+ }
473
544
  // ── Inbound Message Handler ──────────────────────────────────────────
474
545
  async handleInboundMessage(msg) {
475
546
  if (msg.message_type !== MSG_TYPE_USER)
@@ -299,6 +299,14 @@ function renderManagedGroupResult(args, result, formatJson) {
299
299
  console.log(`✓ 成功 ${result.added?.length ?? 0},失败 ${result.failed?.length ?? 0}`);
300
300
  return;
301
301
  }
302
+ if (sub === 'invites') {
303
+ console.log(`✓ ${result.invites?.length ?? 0} 个待处理邀请`);
304
+ return;
305
+ }
306
+ if (sub === 'accept-invite' || sub === 'reject-invite' || sub === 'revoke-invite') {
307
+ console.log(`✓ group ${sub} 完成`);
308
+ return;
309
+ }
302
310
  console.log(`✓ group ${sub} 完成`);
303
311
  }
304
312
  export async function cmdAid(args) {
@@ -1558,7 +1566,11 @@ export async function cmdGroup(args) {
1558
1566
  成员:
1559
1567
  join <from> <group-id> [--message M] [--answer A] 申请加入
1560
1568
  leave <from> <group-id> 退出群
1561
- invite <from> <group-id> <member-aid> [<member-aid>...] 邀请成员
1569
+ invite <from> <group-id> <member-aid> [<member-aid>...] 创建定向邀请
1570
+ invites <from> [--page N] [--size N] 查看收到的邀请
1571
+ accept-invite <from> <invite-id> 接受邀请
1572
+ reject-invite <from> <invite-id> 拒绝邀请
1573
+ revoke-invite <from> <invite-id> 撤回邀请
1562
1574
  kick <from> <group-id> <member-aid> 踢出成员
1563
1575
  members <from> <group-id> [--page N] [--size N] 列出群成员
1564
1576
  online <from> <group-id> 查看在线成员
@@ -1582,6 +1594,7 @@ Options:
1582
1594
  --text <说明> 附件说明文字(仅 --file 模式)
1583
1595
  --transcript <text> 语音转写(仅 --as voice)
1584
1596
  --group-id <aid> 指定自定义群 AID(仅 create)
1597
+ --member-type <type> 定向邀请成员类型:human|ai(仅 invite)
1585
1598
  -- end-of-options:其后所有参数按正文处理
1586
1599
  (用于发送恰好等于某 flag 的文本,如 send a g -- --encrypt)
1587
1600
 
@@ -1617,7 +1630,7 @@ Options:
1617
1630
  renderManagedGroupResult(args, result, formatJson);
1618
1631
  return;
1619
1632
  }
1620
- const { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupSuspend, groupResume, groupJoin, groupLeave, groupInvite, groupKick, groupMembers, groupOnline, groupSetRole, groupTransferOwner, groupBan, groupUnban, groupBanlist, groupRules, groupUpdateRules, groupRulesFileGet, groupRulesFileSet, groupRulesFilePublish, } = await import('../aun/msg/index.js');
1633
+ const { groupSend, groupPull, groupAck, groupCreate, groupInfo, groupList, groupUpdate, groupDissolve, groupSuspend, groupResume, groupJoin, groupLeave, groupInvite, groupInvites, groupAcceptInvite, groupRejectInvite, groupRevokeInvite, groupKick, groupMembers, groupOnline, groupSetRole, groupTransferOwner, groupBan, groupUnban, groupBanlist, groupRules, groupUpdateRules, groupRulesFileGet, groupRulesFileSet, groupRulesFilePublish, } = await import('../aun/msg/index.js');
1621
1634
  const commonOpts = { aunPath, slotId: appSlot };
1622
1635
  // 通用 group_id 提取(第三参数)
1623
1636
  const requireGroupId = () => {
@@ -1693,6 +1706,31 @@ Options:
1693
1706
  }
1694
1707
  return parsed;
1695
1708
  };
1709
+ if (sub === 'invites') {
1710
+ const page = parseNumberFlag('--page', '--page');
1711
+ const size = parseNumberFlag('--size', '--size');
1712
+ const result = await groupInvites({ from, page, size, ...commonOpts });
1713
+ outputResult(result, () => {
1714
+ const r = result;
1715
+ console.log(`✓ ${r.invites.length} 个待处理邀请:`);
1716
+ for (const invite of r.invites) {
1717
+ console.log(` ${invite.invite_id ?? invite.id ?? '-'} ${invite.group_id ?? invite.group_aid ?? '-'} ${invite.inviter_aid ?? invite.from ?? '-'}`);
1718
+ }
1719
+ });
1720
+ return;
1721
+ }
1722
+ if (sub === 'accept-invite' || sub === 'reject-invite' || sub === 'revoke-invite') {
1723
+ const inviteId = args[2];
1724
+ if (!inviteId || inviteId.startsWith('--')) {
1725
+ console.error(`用法: ec group ${sub} <from> <invite-id>`);
1726
+ process.exit(1);
1727
+ }
1728
+ const action = sub === 'accept-invite' ? groupAcceptInvite
1729
+ : sub === 'reject-invite' ? groupRejectInvite : groupRevokeInvite;
1730
+ const result = await action({ from, inviteId, ...commonOpts });
1731
+ outputResult(result, () => console.log(`✓ 已${sub === 'accept-invite' ? '接受' : sub === 'reject-invite' ? '拒绝' : '撤回'}邀请 ${inviteId}`));
1732
+ return;
1733
+ }
1696
1734
  const requireFlagValue = (flag, label) => {
1697
1735
  const value = getArgValue(args, flag);
1698
1736
  if (value === undefined || value.startsWith('--')) {
@@ -2147,7 +2185,12 @@ Options:
2147
2185
  process.exit(1);
2148
2186
  }
2149
2187
  }
2150
- const result = await groupInvite({ from, groupId, members, ...commonOpts });
2188
+ const memberType = getArgValue(args, '--member-type');
2189
+ if (memberType !== undefined && memberType !== 'human' && memberType !== 'ai') {
2190
+ console.error('❌ --member-type 必须是 human 或 ai');
2191
+ process.exit(1);
2192
+ }
2193
+ const result = await groupInvite({ from, groupId, members, memberType: memberType, ...commonOpts });
2151
2194
  outputResult(result, () => {
2152
2195
  const r = result;
2153
2196
  console.log(`✓ 成功 ${r.added.length},失败 ${r.failed.length}`);
@@ -2300,7 +2343,7 @@ Options:
2300
2343
  });
2301
2344
  return;
2302
2345
  }
2303
- console.error(`未知子命令: ${sub}\n用法: ec group [send|pull|ack|create|list|info|update|dissolve|suspend|resume|rules|join|leave|invite|kick|members|online|role|owner|ban|unban]`);
2346
+ console.error(`未知子命令: ${sub}\n用法: ec group [send|pull|ack|create|list|info|update|dissolve|suspend|resume|rules|join|leave|invite|invites|accept-invite|reject-invite|revoke-invite|kick|members|online|role|owner|ban|unban]`);
2304
2347
  process.exit(1);
2305
2348
  }
2306
2349
  // ==================== Main ====================
@@ -2322,6 +2365,7 @@ const VALUE_FLAGS = new Set([
2322
2365
  '--payload', '--title', '--description', '--text', '--transcript',
2323
2366
  '--as', '--content-type', '--mention', '--visibility', '--join-mode',
2324
2367
  '--group-id', '--name', '--message', '--answer', '--page', '--size',
2368
+ '--member-type',
2325
2369
  '--aun-path', '--thread', '--return',
2326
2370
  ]);
2327
2371
  const BOOLEAN_FLAGS = new Set([
@@ -59,8 +59,8 @@ export function logCliCommand(cmd, args) {
59
59
  const line = JSON.stringify({
60
60
  ts: localTimestamp(),
61
61
  cmd,
62
- // 完整命令行,可直接拷贝执行(含 ec 前缀,含空格的参数自动加引号)
63
- command: `ec ${args.map(shellQuote).join(' ')}`,
62
+ // 可审计命令行(敏感参数掩码;含 ec 前缀,含空格参数自动加引号)
63
+ command: `ec ${loggableArgs(args).map(shellQuote).join(' ')}`,
64
64
  selfAid: selfAid || undefined,
65
65
  sessionId: process.env.EVOLCORE_SESSION_ID || undefined,
66
66
  }) + '\n';
@@ -82,6 +82,25 @@ function shellQuote(arg) {
82
82
  return arg;
83
83
  return `'${arg.replace(/'/g, `'\\''`)}'`;
84
84
  }
85
+ function loggableArgs(args) {
86
+ const output = [];
87
+ let redactNext = false;
88
+ for (const arg of args) {
89
+ if (redactNext) {
90
+ output.push('[REDACTED]');
91
+ redactNext = false;
92
+ continue;
93
+ }
94
+ const flag = arg.replace(/^--?/, '').replace(/=.*$/, '').toLowerCase();
95
+ if (/^(?:remote-token|ecweb-token|menu-token|token|password|secret)$/.test(flag)) {
96
+ output.push(arg.includes('=') ? `${arg.slice(0, arg.indexOf('=') + 1)}[REDACTED]` : arg);
97
+ redactNext = !arg.includes('=');
98
+ continue;
99
+ }
100
+ output.push(arg.replace(/((?:"|')?(?:ecweb[_-]?token|menu[_-]?token|access[_-]?token|refresh[_-]?token|password|secret)(?:"|')?\s*[:=]\s*)("(?:[^"\\]|\\.)*"|'[^']*'|[^,}\s]+)/gi, '$1"[REDACTED]"'));
101
+ }
102
+ return output;
103
+ }
85
104
  /** 本地日期 tag YYYYMMDD,用于日志文件按天切分(一天一个文件)。 */
86
105
  function dayTag() {
87
106
  const d = new Date();
@@ -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,8 +28,9 @@ 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 } from '../agents/codex/diagnostics.js';
33
34
  const execFileAsync = promisify(execFile);
34
35
  async function runAutomaticInit(action) {
35
36
  try {
@@ -177,10 +178,15 @@ function sourceBuildNeedsRefresh() {
177
178
  function runSourceBuild() {
178
179
  return new Promise(resolve => {
179
180
  console.log('🔧 源码比 daemon 编译产物更新,自动重新构建...');
180
- const child = spawn(platform.isWindows ? 'npm.cmd' : 'npm', ['run', 'build'], {
181
+ // Windows cannot spawn a .cmd shim directly with shell:false on recent
182
+ // Node versions. Invoke cmd.exe explicitly instead of using spawn's
183
+ // shell:true mode, which also avoids the DEP0190 warning.
184
+ const command = platform.isWindows ? (process.env.ComSpec ?? process.env.COMSPEC ?? 'cmd.exe') : 'npm';
185
+ const args = platform.isWindows ? ['/d', '/c', 'npm.cmd', 'run', 'build'] : ['run', 'build'];
186
+ const child = spawn(command, args, {
181
187
  cwd: getPackageRoot(),
182
188
  stdio: 'inherit',
183
- shell: platform.isWindows,
189
+ windowsHide: platform.isWindows,
184
190
  });
185
191
  child.on('error', err => {
186
192
  console.error(`❌ 无法启动构建进程: ${err.message}`);
@@ -215,6 +221,64 @@ function requireCompletedDataMigration(root) {
215
221
  process.exitCode = 2;
216
222
  return false;
217
223
  }
224
+ /**
225
+ * Check package updates before starting or restarting the daemon.
226
+ * A failed upgrade is reported, but the currently installed version remains
227
+ * usable and the caller continues with its normal lifecycle.
228
+ */
229
+ export async function runUpgradeChecks(upgradeFn = signal => tryUpgrade({ signal }), ecwebUpgradeFn = signal => tryUpgradeGlobalPkg(() => resolveGlobalPkg(WEB_PACKAGE_NAME), WEB_PACKAGE_NAME, { signal }), timeoutMs = 60_000) {
230
+ console.log('📦 Checking for updates...');
231
+ const runWithTimeout = async (label, fn) => {
232
+ const controller = new AbortController();
233
+ let timer;
234
+ try {
235
+ return await Promise.race([
236
+ fn(controller.signal),
237
+ new Promise(resolve => {
238
+ timer = setTimeout(() => {
239
+ resolve({ status: 'failed', error: `${label} upgrade timed out` });
240
+ controller.abort();
241
+ }, timeoutMs);
242
+ }),
243
+ ]);
244
+ }
245
+ catch (error) {
246
+ return { status: 'failed', error: error instanceof Error ? error.message : String(error) };
247
+ }
248
+ finally {
249
+ if (timer)
250
+ clearTimeout(timer);
251
+ }
252
+ };
253
+ const upgrade = await runWithTimeout('EvolCore', upgradeFn);
254
+ switch (upgrade.status) {
255
+ case 'upgraded':
256
+ console.log(`✅ Upgraded: ${upgrade.from} → ${upgrade.to}`);
257
+ break;
258
+ case 'no-update':
259
+ console.log(`✓ Already up to date (${upgrade.from})`);
260
+ break;
261
+ case 'skipped':
262
+ console.log(upgrade.error
263
+ ? '⏭ Skipped upgrade (network unavailable)'
264
+ : '⏭ Skipped upgrade check (dev mode)');
265
+ break;
266
+ case 'failed':
267
+ console.log(`⚠ Upgrade failed${upgrade.from && upgrade.to ? ` (${upgrade.from} → ${upgrade.to})` : ''}, continuing with current version${upgrade.error ? `: ${upgrade.error}` : ''}`);
268
+ break;
269
+ }
270
+ const ecwebUpgrade = await runWithTimeout(WEB_PACKAGE_NAME, ecwebUpgradeFn);
271
+ switch (ecwebUpgrade.status) {
272
+ case 'upgraded':
273
+ console.log(`✅ ${WEB_PACKAGE_NAME} upgraded: ${ecwebUpgrade.from} → ${ecwebUpgrade.to}`);
274
+ break;
275
+ case 'no-update':
276
+ break;
277
+ case 'failed':
278
+ console.log(`⚠ ${WEB_PACKAGE_NAME} upgrade failed${ecwebUpgrade.from && ecwebUpgrade.to ? ` (${ecwebUpgrade.from} → ${ecwebUpgrade.to})` : ''}${ecwebUpgrade.error ? `: ${ecwebUpgrade.error}` : ''}`);
279
+ break;
280
+ }
281
+ }
218
282
  // ==================== Commands ====================
219
283
  /**
220
284
  * 检测并展示跨 HOME 残留的 evolcore 主进程。
@@ -329,6 +393,9 @@ export async function cmdStart(opts = {}) {
329
393
  ensureDataDirs();
330
394
  if (!await requireFreshSourceBuild())
331
395
  return;
396
+ // A stopped container may miss the daily upgrade trigger. Check once on
397
+ // every real start so the service does not stay on an old installation.
398
+ await (opts.upgradeChecks ?? runUpgradeChecks)();
332
399
  // 旧配置自动迁移(daemon.json → 新结构)
333
400
  const { autoMigrateIfNeeded } = await import('../config-store.js');
334
401
  autoMigrateIfNeeded();
@@ -704,37 +771,9 @@ export async function cmdRestart(opts = {}) {
704
771
  }
705
772
  if (!await requireFreshSourceBuild())
706
773
  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
- }
774
+ // Keep restart behavior identical to startup: both paths use one upgrade
775
+ // check implementation and continue when an upgrade is unavailable.
776
+ await runUpgradeChecks();
738
777
  if (!requireCompletedDataMigration(resolveRoot()))
739
778
  return;
740
779
  // 停止所有活 main 进程(可能不止一个)
@@ -2667,7 +2706,7 @@ async function stopEcwebIfRunning(p) {
2667
2706
  throw new Error(`EC Web old process or port ${port} could not be released`);
2668
2707
  }
2669
2708
  // 清理 pid 文件(仅 ecweb-<pid>.json / watch-web-<pid>.json,
2670
- // 不碰 ecweb-tokens.json 等非 pid 文件,否则会清空配对 token 库导致每次重启都要重新配对)
2709
+ // 不碰 daemon-owned control-access-tokens.json 等非 pid 文件,否则会清空凭据库导致每次重启都要重新配对)
2671
2710
  removeEcwebInstanceFiles(p);
2672
2711
  return pids.size > 0;
2673
2712
  }
@@ -2759,23 +2798,32 @@ export async function startEcwebIfEnabled(p, options = {}) {
2759
2798
  fs.mkdirSync(p.instanceDir, { recursive: true });
2760
2799
  const startedAt = getProcessStartTime(pid) ?? Date.now();
2761
2800
  fs.writeFileSync(path.join(p.instanceDir, `ecweb-${pid}.json`), JSON.stringify({ pid, port, startedAt, startedAtIso: new Date(startedAt).toISOString() }, null, 2));
2762
- // 轮询端口确认 HTTP 服务真正就绪(spawn 成功 ≠ 端口绑定成功),顺便拿配对码
2801
+ // 轮询端口确认 HTTP 服务真正就绪(spawn 成功 ≠ 端口绑定成功)。
2802
+ // 配对码由 daemon 提供,daemon 尚未启动时不应把已经就绪的 ECWeb 杀掉。
2803
+ let ready = false;
2763
2804
  let pair = null;
2764
2805
  for (let i = 0; i < 20; i++) {
2765
- pair = await fetchEcwebPairCode(port);
2766
- if (pair)
2806
+ ready = await probeEcwebReady(port);
2807
+ if (ready) {
2808
+ pair = await fetchRemotePairingCode();
2767
2809
  break;
2810
+ }
2768
2811
  await sleep(250);
2769
2812
  }
2770
- if (pair) {
2813
+ if (ready) {
2771
2814
  const actualStartedAt = getProcessStartTime(pid);
2772
2815
  if (actualStartedAt !== null) {
2773
2816
  fs.writeFileSync(path.join(p.instanceDir, `ecweb-${pid}.json`), JSON.stringify({ pid, port, startedAt: actualStartedAt, startedAtIso: new Date(actualStartedAt).toISOString() }, null, 2));
2774
2817
  }
2775
- const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2776
2818
  const actionLabel = wasRunning ? '已重启' : '启动成功';
2777
2819
  console.log(`✓ ECWeb ${actionLabel} (PID: ${pid}) http://localhost:${port}`);
2778
- console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,已配对过的浏览器无需重新配对)`);
2820
+ if (pair) {
2821
+ const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2822
+ console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,已配对过的浏览器无需重新配对)`);
2823
+ }
2824
+ else {
2825
+ console.log(' daemon 尚未提供配对码;EvolCore 启动后可通过 /pair 或 ECWeb 页面获取');
2826
+ }
2779
2827
  return true;
2780
2828
  }
2781
2829
  console.log(`❌ ECWeb 启动失败:端口 ${port} 未就绪(进程 PID ${pid} 可能已退出,查看 logs/watch-web.log)`);
@@ -2801,13 +2849,13 @@ async function printEcwebAccess(port) {
2801
2849
  console.log(`🔭 ECWeb http://localhost:${port}`);
2802
2850
  let pair = null;
2803
2851
  for (let i = 0; i < 10 && !pair; i++) {
2804
- pair = await fetchEcwebPairCode(port);
2852
+ pair = await fetchRemotePairingCode();
2805
2853
  if (!pair)
2806
2854
  await sleep(300);
2807
2855
  }
2808
2856
  if (pair) {
2809
2857
  const mins = Math.max(0, Math.round((pair.expiresAt - Date.now()) / 60000));
2810
- console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,配对后 token 缓存 24h)`);
2858
+ console.log(` 配对码: ${pair.code} (约 ${mins} 分钟内有效,配对后远程访问 token 缓存 30 天)`);
2811
2859
  }
2812
2860
  else {
2813
2861
  console.log(' 配对码: 暂不可用(稍后重试 ec watch web,或查看 logs/watch-web.log)');
@@ -2924,7 +2972,56 @@ export async function cmdDiagnose() {
2924
2972
  else {
2925
2973
  console.log(`[diagnose] ✓ 已加载 ${agents.length} 个 self-agent`);
2926
2974
  }
2927
- // 3. 检查 Anthropic 配置(用首个 self-agent 的 effective config)
2975
+ // 3. 检查实际使用 Codex 的 Agent 项目目录。项目路径属于 Agent 的
2976
+ // effective config,逐个输出 AID,避免只检查第一个 agent 掩盖坏配置。
2977
+ const codexAgents = agents.filter(agent => {
2978
+ try {
2979
+ return resolveEffective({ self: agent.aid }).active_baseagent === 'codex';
2980
+ }
2981
+ catch (error) {
2982
+ console.error(`[diagnose] ❌ Agent ${agent.aid} effective config 解析失败: ${error instanceof Error ? error.message : String(error)}`);
2983
+ hasError = true;
2984
+ return false;
2985
+ }
2986
+ });
2987
+ for (const agent of codexAgents) {
2988
+ try {
2989
+ const effective = resolveEffective({ self: agent.aid });
2990
+ const check = checkCodexProjectPath(effective.projects?.defaultPath);
2991
+ if (check.status === 'ok') {
2992
+ console.log(`[diagnose] ✓ Codex 项目路径 ${agent.aid}: ${check.path}`);
2993
+ }
2994
+ else {
2995
+ console.error(`[diagnose] ❌ Codex 项目路径 ${agent.aid}: ${check.path || '(未配置)'} (${check.reason || check.status})`);
2996
+ hasError = true;
2997
+ }
2998
+ for (const warning of check.warnings)
2999
+ console.warn(`[diagnose] ⚠️ Codex 项目路径 ${agent.aid}: ${warning}`);
3000
+ }
3001
+ catch (error) {
3002
+ console.error(`[diagnose] ❌ Codex 项目路径检查失败 ${agent.aid}: ${error instanceof Error ? error.message : String(error)}`);
3003
+ hasError = true;
3004
+ }
3005
+ }
3006
+ if (codexAgents.length === 0) {
3007
+ console.log('[diagnose] - Codex managed hook: 跳过(没有 Agent 使用 Codex)');
3008
+ }
3009
+ else {
3010
+ const hook = inspectCodexManagedHook({ packageRoot: getPackageRoot() });
3011
+ if (hook.status === 'installed') {
3012
+ console.log(`[diagnose] ✓ Codex managed hook 已安装: ${hook.hookPath}`);
3013
+ }
3014
+ else if (hook.status === 'skipped') {
3015
+ console.log(`[diagnose] - Codex managed hook: 跳过(${hook.reason})`);
3016
+ }
3017
+ else {
3018
+ const prefix = hook.status === 'missing' ? '⚠️' : '❌';
3019
+ const writer = hook.status === 'missing' ? console.warn : console.error;
3020
+ writer(`[diagnose] ${prefix} Codex managed hook ${hook.status}: ${hook.reason || 'unknown error'}${hook.requirements ? ` (${hook.requirements})` : ''}`);
3021
+ hasError = true;
3022
+ }
3023
+ }
3024
+ // 4. 检查 Anthropic 配置(用首个 self-agent 的 effective config)
2928
3025
  if (agents.length > 0) {
2929
3026
  try {
2930
3027
  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
 
@@ -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) {