evolcore 0.0.12 → 0.0.13

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 (104) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/bin/ec-safe-output.js +26 -7
  3. package/dist/agents/claude-runner.js +53 -7
  4. package/dist/agents/codex-app-server-client.js +53 -5
  5. package/dist/agents/codex-runner.js +104 -59
  6. package/dist/agents/ecagent-runner.js +4 -1
  7. package/dist/aun/aid/control-aid.js +36 -13
  8. package/dist/aun/aid/store.js +13 -7
  9. package/dist/aun/group-fs-download.js +56 -0
  10. package/dist/aun/group-identity.js +1 -2
  11. package/dist/aun/msg/group.js +11 -33
  12. package/dist/aun/msg/index.js +1 -2
  13. package/dist/aun/msg/managed-operation.js +116 -55
  14. package/dist/aun/msg/upload.js +16 -1
  15. package/dist/aun/outbox.js +31 -2
  16. package/dist/aun/rpc/client.js +66 -0
  17. package/dist/aun/rpc/index.js +1 -2
  18. package/dist/aun/storage/{upload.js → client.js} +11 -2
  19. package/dist/aun/storage/index.js +1 -2
  20. package/dist/channels/aun.js +371 -136
  21. package/dist/cli/agent.js +9 -96
  22. package/dist/cli/cli-argv.js +10 -0
  23. package/dist/cli/config.js +9 -17
  24. package/dist/cli/ctl-command.js +1 -1
  25. package/dist/cli/daemon-commands.js +39 -24
  26. package/dist/cli/fs-command.js +19 -10
  27. package/dist/cli/init.js +77 -8
  28. package/dist/cli/queue-command.js +42 -0
  29. package/dist/cli/restart-monitor.js +3 -14
  30. package/dist/cli/trigger-command.js +7 -0
  31. package/dist/config/access-policy.js +1 -2
  32. package/dist/config/builtin-roles.js +5 -0
  33. package/dist/config/config-manager.js +21 -1
  34. package/dist/config/peer-role-resolver.js +6 -2
  35. package/dist/config/role-migration-startup.js +53 -0
  36. package/dist/config/role-store.js +1 -1
  37. package/dist/core/auth/agent-delegation.js +1 -1
  38. package/dist/core/auth/auth-gateway.js +29 -1
  39. package/dist/core/auth/authorization-audit.js +15 -0
  40. package/dist/core/auth/operation-authorizer.js +1 -0
  41. package/dist/core/auth/operation-catalog.js +9 -0
  42. package/dist/core/bootstrap-messages.js +9 -2
  43. package/dist/core/bootstrap-service.js +1 -0
  44. package/dist/core/command/command-handler.js +198 -51
  45. package/dist/core/command/menu-catalog.js +4 -0
  46. package/dist/core/command/menu-handler.js +85 -18
  47. package/dist/core/command/menu-protocol.js +23 -2
  48. package/dist/core/command/slash-handler.js +58 -10
  49. package/dist/core/event-catalog.js +6 -0
  50. package/dist/core/handoff/runtime.js +12 -1
  51. package/dist/core/handoff/store.js +5 -1
  52. package/dist/core/interaction-router.js +10 -0
  53. package/dist/core/message/im-renderer.js +19 -0
  54. package/dist/core/message/message-bridge.js +63 -11
  55. package/dist/core/message/message-log.js +2 -0
  56. package/dist/core/message/message-queue.js +267 -39
  57. package/dist/core/message/message-utils.js +81 -4
  58. package/dist/core/message/response-engine.js +107 -41
  59. package/dist/core/message/stream-debouncer.js +10 -2
  60. package/dist/core/permission/approval-gateway.js +1 -0
  61. package/dist/core/permission/ec-command-parser.js +93 -14
  62. package/dist/core/permission/index.js +1 -1
  63. package/dist/core/permission/mode.js +15 -0
  64. package/dist/core/permission/readonly-shell-query.js +148 -8
  65. package/dist/core/permission/sandbox-runtime.js +21 -0
  66. package/dist/core/permission/tool-policy.js +249 -27
  67. package/dist/core/protected-paths.js +107 -2
  68. package/dist/core/session/session-manager.js +21 -2
  69. package/dist/core/session/session-mapper.js +20 -1
  70. package/dist/eck/detect.js +18 -1
  71. package/dist/eck/group-rules-sync.js +15 -44
  72. package/dist/index.js +161 -86
  73. package/dist/ipc.js +16 -3
  74. package/dist/paths.js +3 -0
  75. package/dist/trigger/feedback.js +64 -11
  76. package/dist/trigger/manager.js +21 -1
  77. package/dist/trigger/parser.js +14 -2
  78. package/dist/trigger/scheduler.js +1 -0
  79. package/dist/trigger/validation.js +67 -6
  80. package/dist/utils/ecweb-supervisor.js +332 -0
  81. package/dist/utils/error-utils.js +10 -0
  82. package/kits/docs/evolcore/trigger.md +17 -1
  83. package/kits/migrations/migrate-contact-book-v2.mjs +1 -2
  84. package/kits/schemas/role-config.schema.1.json +1 -0
  85. package/kits/templates/roles/admin.json +5 -0
  86. package/kits/templates/roles/member.json +7 -1
  87. package/kits/templates/roles/visitor.json +2 -0
  88. package/package.json +1 -1
  89. package/dist/aun/msg/payload-type.js +0 -27
  90. package/dist/aun/rpc/caller.js +0 -42
  91. package/dist/aun/rpc/connection.js +0 -25
  92. package/dist/aun/storage/manage.js +0 -10
  93. package/dist/config/access-policy-domain.js +0 -18
  94. package/dist/config/config-batch-get.js +0 -11
  95. package/dist/config/owner-policy.js +0 -4
  96. package/dist/config/role-config-v4-startup.js +0 -32
  97. package/dist/config/role-config-v5-startup.js +0 -27
  98. package/dist/core/auth/trigger-authorization.js +0 -15
  99. package/dist/core/interaction-registration.js +0 -10
  100. package/dist/core/permission/execution-sandbox.js +0 -16
  101. package/dist/core/relation/peer-key.js +0 -1
  102. package/dist/core/session/session-key.js +0 -24
  103. package/dist/eck/baseagent-caps.js +0 -18
  104. package/dist/eck/rules-loader.js +0 -28
package/dist/cli/agent.js CHANGED
@@ -8,18 +8,14 @@ import { resolveConfigCommand } from '../config/resolved-config-op.js';
8
8
  import { executeResolvedConfigCommand } from '../config/config-operation-service.js';
9
9
  import { ipcQuery } from '../ipc.js';
10
10
  import { CONFIG_SCHEMA_VERSION } from '../types.js';
11
- import { normalizeAgentLifecycle, withLifecycleForWrite } from '../config/lifecycle.js';
12
- import { bindPostBootstrapWelcomeOutboxSession, hasPendingPostBootstrapWelcomeOutbox, postBootstrapWelcomeOperationId, preparePostBootstrapWelcomeOutbox, } from '../core/bootstrap-messages.js';
13
11
  import { ensureInitialRoleRegistry } from '../config/role-service.js';
14
- import { formatChannelKey, isValidChannelName } from '../core/channel-loader.js';
12
+ import { isValidChannelName } from '../core/channel-loader.js';
15
13
  import { commandExists } from '../utils/cross-platform.js';
16
14
  import { getCodexAppServerAvailability, isCodexAppServerAvailable } from '../agents/codex-runner.js';
17
15
  import { resolveEcagentConfig } from '../agents/baseagent.js';
18
16
  import { agentProjectRootFromDefaults, deriveAgentProjectPath } from '../utils/project-path.js';
19
17
  import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
20
18
  import { readTaskRuntimeContextFromEnv } from './task-context.js';
21
- import { EventBus } from '../core/event-bus.js';
22
- import { SessionManager } from '../core/session/session-manager.js';
23
19
  import { isValidAid } from '../aun/aid/validation.js';
24
20
  function withStaticOwner(config, owner) {
25
21
  const value = owner?.trim();
@@ -1196,33 +1192,9 @@ export async function agentRename(aid, name) {
1196
1192
  return { ok: true, aid, name };
1197
1193
  }
1198
1194
  // ==================== agentReady ====================
1199
- async function ensureOfflinePostBootstrapMainSession(aid, config) {
1200
- const owner = config.owners?.[0];
1201
- if (!owner)
1202
- throw new Error(`Agent "${aid}" has no owner for the post-bootstrap main session`);
1203
- const p = resolvePaths();
1204
- const aunName = config.channels?.find(channel => channel.type === 'aun')?.name || 'main';
1205
- const channel = formatChannelKey({ type: 'aun', selfAID: aid, name: aunName });
1206
- const manager = new SessionManager(p.sessionsDir, new EventBus());
1207
- const active = await manager.getActiveSession(channel, owner);
1208
- const ensured = await manager.ensureNewMainSessionForOperation({
1209
- channel,
1210
- channelId: owner,
1211
- projectPath: active?.projectPath
1212
- || config.projects?.defaultPath
1213
- || path.join(p.root, 'projects', aid.split('.')[0]),
1214
- baseagent: config.active_baseagent || active?.baseagent || 'claude',
1215
- operationId: postBootstrapWelcomeOperationId(aid),
1216
- identityMetadata: {
1217
- ...(active?.metadata ?? {}),
1218
- channelKey: channel,
1219
- peerId: owner,
1220
- },
1221
- });
1222
- return ensured.session.id;
1223
- }
1224
1195
  export async function agentReady(aid) {
1225
- const taskSelfAid = readTaskRuntimeContextFromEnv()?.selfAid;
1196
+ const managedSessionId = process.env.EVOLCORE_SESSION_ID;
1197
+ const taskSelfAid = managedSessionId ? readTaskRuntimeContextFromEnv()?.selfAid : undefined;
1226
1198
  if (taskSelfAid && taskSelfAid.replace(/^@/, '') !== aid.replace(/^@/, '')) {
1227
1199
  return {
1228
1200
  ok: false,
@@ -1231,77 +1203,18 @@ export async function agentReady(aid) {
1231
1203
  };
1232
1204
  }
1233
1205
  const p = resolvePaths();
1234
- const managedSessionId = process.env.EVOLCORE_SESSION_ID;
1235
- if (managedSessionId) {
1236
- const delegationToken = process.env[AGENT_DELEGATION_TOKEN_ENV];
1237
- if (!delegationToken)
1206
+ try {
1207
+ const delegationToken = managedSessionId ? process.env[AGENT_DELEGATION_TOKEN_ENV] : undefined;
1208
+ if (managedSessionId && !delegationToken) {
1238
1209
  return { ok: false, code: 'DELEGATION_REQUIRED', error: `${AGENT_DELEGATION_TOKEN_ENV} is not set for the active task` };
1210
+ }
1239
1211
  const result = await ipcQuery(p.socket, {
1240
1212
  type: 'evolagent.bootstrapComplete',
1241
1213
  aid,
1242
- sessionId: managedSessionId,
1243
- delegationToken,
1214
+ ...(managedSessionId ? { sessionId: managedSessionId, delegationToken } : {}),
1244
1215
  }, 30_000);
1245
1216
  if (!result)
1246
- return { ok: false, code: 'DAEMON_UNAVAILABLE', error: 'Unable to connect to the evolcore daemon' };
1247
- return result.ok
1248
- ? { ok: true, aid, reloaded: result.reloaded === true }
1249
- : { ok: false, code: result.code, error: result.error || 'Failed to complete bootstrap' };
1250
- }
1251
- const config = loadAgent(aid);
1252
- if (!config)
1253
- return { ok: false, error: `Agent "${aid}" not found` };
1254
- try {
1255
- let controlToken;
1256
- try {
1257
- controlToken = fs.readFileSync(p.controlToken, 'utf8').trim() || undefined;
1258
- }
1259
- catch { }
1260
- const result = await ipcQuery(p.socket, { type: 'evolagent.bootstrapComplete', aid, controlToken }, 30_000);
1261
- if (!result) {
1262
- const offlineConfig = loadAgent(aid) || config;
1263
- const lifecycle = normalizeAgentLifecycle(offlineConfig).lifecycle;
1264
- if (lifecycle !== 'bootstrapping') {
1265
- if (lifecycle === 'active') {
1266
- if (hasPendingPostBootstrapWelcomeOutbox(aid)) {
1267
- try {
1268
- const sessionId = await ensureOfflinePostBootstrapMainSession(aid, offlineConfig);
1269
- if (!bindPostBootstrapWelcomeOutboxSession(aid, sessionId)) {
1270
- throw new Error(`Failed to bind the completion welcome to session ${sessionId}`);
1271
- }
1272
- }
1273
- catch (e) {
1274
- return {
1275
- ok: false,
1276
- code: 'MAIN_SESSION_CREATE_FAILED',
1277
- error: e instanceof Error ? e.message : 'Failed to recover the post-bootstrap main session',
1278
- };
1279
- }
1280
- }
1281
- return { ok: true, aid, reloaded: false };
1282
- }
1283
- return { ok: false, code: 'INVALID_LIFECYCLE', error: `Agent "${aid}" is ${lifecycle}; only a bootstrapping agent can become ready` };
1284
- }
1285
- const owner = offlineConfig.owners?.[0];
1286
- if (!owner)
1287
- return { ok: false, code: 'OWNER_REQUIRED', error: `Agent "${aid}" has no owner for the completion welcome` };
1288
- try {
1289
- preparePostBootstrapWelcomeOutbox(aid, owner);
1290
- const sessionId = await ensureOfflinePostBootstrapMainSession(aid, offlineConfig);
1291
- if (!bindPostBootstrapWelcomeOutboxSession(aid, sessionId)) {
1292
- throw new Error(`Failed to bind the completion welcome to session ${sessionId}`);
1293
- }
1294
- saveAgent(withLifecycleForWrite(offlineConfig, 'active'));
1295
- }
1296
- catch (e) {
1297
- return {
1298
- ok: false,
1299
- code: 'READY_PERSIST_FAILED',
1300
- error: e instanceof Error ? e.message : 'Failed to persist bootstrap completion',
1301
- };
1302
- }
1303
- return { ok: true, aid, reloaded: false };
1304
- }
1217
+ return { ok: false, code: 'DAEMON_UNAVAILABLE', error: 'Unable to connect to the evolcore daemon; agent ready requires daemon IPC' };
1305
1218
  if (!result.ok) {
1306
1219
  return { ok: false, code: result.code, error: result.error || 'Failed to complete bootstrap' };
1307
1220
  }
@@ -3,6 +3,16 @@ export function normalizeCliArgv(argv) {
3
3
  return argv.slice(1);
4
4
  return argv;
5
5
  }
6
+ export function splitConfigBatchGetArgv(inputArgv) {
7
+ const argv = normalizeCliArgv(inputArgv);
8
+ if (argv[0] !== 'config' || argv[1] !== 'get')
9
+ return null;
10
+ const fields = [];
11
+ let i = 2;
12
+ for (; i < argv.length && !argv[i].startsWith('-'); i++)
13
+ fields.push(argv[i]);
14
+ return { fields, rest: argv.slice(i) };
15
+ }
6
16
  export function validateCliArgv(value) {
7
17
  if (!Array.isArray(value) || value.length === 0)
8
18
  return { ok: false, reason: 'args.argv must be a non-empty string array' };
@@ -5,7 +5,7 @@ import { AUTHENTICATED_CONFIG_ROLE_ENV, executeResolvedConfigCommand, } from '..
5
5
  import { ipcQuery } from '../ipc.js';
6
6
  import { resolvePaths } from '../paths.js';
7
7
  import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
8
- import { splitConfigBatchGetArgv } from '../config/config-batch-get.js';
8
+ import { splitConfigBatchGetArgv } from './cli-argv.js';
9
9
  function emit(formatJson, payload, text) {
10
10
  if (formatJson)
11
11
  console.log(JSON.stringify(payload, null, 2));
@@ -47,6 +47,8 @@ function renderConfigExecution(result, formatJson) {
47
47
  switch (result.subcommand) {
48
48
  case 'get':
49
49
  return emit(formatJson, getResultPayload(result), () => getResultLine(result));
50
+ case 'batch-get':
51
+ return renderBatchGet(result.results, formatJson);
50
52
  case 'set':
51
53
  return emit(formatJson, {
52
54
  ok: true,
@@ -186,23 +188,13 @@ function runDirectBatchGet(fields, rest, formatJson) {
186
188
  }
187
189
  async function runManagedBatchGet(fields, rest, formatJson) {
188
190
  const { sessionId, delegationToken } = managedSession(formatJson);
189
- const results = [];
190
- for (const field of fields) {
191
- const response = await managedConfigQuery(['get', field, ...rest], sessionId, delegationToken);
192
- if (!response) {
193
- results.push({ ok: false, field, code: 'DAEMON_UNAVAILABLE', error: 'Unable to connect to the evolcore daemon' });
194
- }
195
- else if (!response.ok || !response.result || !response.result.ok) {
196
- results.push({ ok: false, field, code: response.code || 'CONFIG_AUTH_FAILED', error: response.error || 'config operation failed' });
197
- }
198
- else if (response.result.subcommand === 'get') {
199
- results.push(response.result);
200
- }
201
- else {
202
- results.push({ ok: false, field, code: 'UNEXPECTED_RESULT', error: `config get ${field} produced a non-get result` });
203
- }
191
+ const response = await managedConfigQuery(['get', ...fields, ...rest], sessionId, delegationToken);
192
+ if (!response)
193
+ fail(formatJson, 'DAEMON_UNAVAILABLE', 'Unable to connect to the evolcore daemon');
194
+ if (!response.ok || !response.result) {
195
+ fail(formatJson, response.code || 'CONFIG_AUTH_FAILED', response.error || 'config operation failed', response.data);
204
196
  }
205
- renderBatchGet(results, formatJson);
197
+ renderConfigExecution(response.result, formatJson);
206
198
  }
207
199
  const HELP = `Usage: ec config <command> [options]
208
200
 
@@ -16,7 +16,7 @@ const CTL_HELP = `用法: ec ctl <command> [args...]
16
16
  消息:
17
17
  send <消息内容> 主动发送文本消息(proactive 模式)
18
18
  file [channel] <path> 发送项目内文件
19
- queue 查看/管理会话消息队列
19
+ queue [--after <time>] [--before <time>] 查看/管理会话消息队列
20
20
 
21
21
  示例:
22
22
  ec ctl status
@@ -389,9 +389,6 @@ export async function cmdStart(opts = {}) {
389
389
  cleanEnv();
390
390
  // 在启动前确保 serviceProxy 配置完整(如果 ecweb 启用但 serviceProxy 未配置)
391
391
  ensureServiceProxyConfigBeforeStart(p);
392
- const ecwebStartedBeforeDaemon = serviceProxyNeedsEcweb(loadDaemonConfig())
393
- ? await startEcwebIfEnabled(p)
394
- : false;
395
392
  // 删除旧的 ready signal
396
393
  try {
397
394
  fs.unlinkSync(p.readySignal);
@@ -421,23 +418,11 @@ export async function cmdStart(opts = {}) {
421
418
  console.log(` EVOLCORE_HOME: ${resolveRoot()}`);
422
419
  console.log(` Logs: ${p.logs}/`);
423
420
  console.log(`⏱ ready in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
424
- if (!ecwebStartedBeforeDaemon) {
425
- startEcwebIfEnabled(p)
426
- .then((ecwebStarted) => {
427
- if (shouldPrintNoSelfAgentHints) {
428
- printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
429
- }
430
- })
431
- .catch((err) => {
432
- console.error(`⚠ ECWeb 启动检查失败: ${err instanceof Error ? err.message : String(err)}`);
433
- if (shouldPrintNoSelfAgentHints) {
434
- printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: false, skipped });
435
- }
436
- });
437
- }
438
- else if (shouldPrintNoSelfAgentHints) {
439
- printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: true, skipped });
440
- }
421
+ void waitForExistingEcweb(p).then((ecweb) => {
422
+ if (shouldPrintNoSelfAgentHints) {
423
+ printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted: ecweb !== null, skipped });
424
+ }
425
+ });
441
426
  }, 500);
442
427
  let forwardingShutdown = false;
443
428
  const forwardSignal = (signal) => {
@@ -536,8 +521,9 @@ export async function cmdStart(opts = {}) {
536
521
  printCodeStats(getPackageRoot(), p.logs);
537
522
  }
538
523
  console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
539
- // ECWeb 自动后台启动
540
- const ecwebStarted = ecwebStartedBeforeDaemon || await startEcwebIfEnabled(p);
524
+ // ECWeb lifecycle belongs to the daemon; the CLI only reports whether
525
+ // the supervised instance became healthy.
526
+ const ecwebStarted = await waitForExistingEcweb(p) !== null;
541
527
  if (shouldPrintNoSelfAgentHints) {
542
528
  printNoSelfAgentHints({ daemonConfig: loadDaemonConfig(), ecwebStarted, skipped });
543
529
  }
@@ -2422,6 +2408,24 @@ async function probeEcwebReady(port) {
2422
2408
  return false;
2423
2409
  }
2424
2410
  }
2411
+ /**
2412
+ * Wait briefly for an already-launched ECWeb to become healthy. `ec start`,
2413
+ * restart-monitor, and the daemon can overlap by a few milliseconds; treating
2414
+ * a healthy instance as an adoption target avoids killing/replacing it.
2415
+ */
2416
+ async function waitForExistingEcweb(p, timeoutMs = 3_000) {
2417
+ const deadline = Date.now() + timeoutMs;
2418
+ do {
2419
+ for (const entry of findAliveEcwebs(p)) {
2420
+ if (await probeEcwebReady(entry.port))
2421
+ return entry;
2422
+ }
2423
+ if (Date.now() >= deadline)
2424
+ break;
2425
+ await sleep(250);
2426
+ } while (true);
2427
+ return null;
2428
+ }
2425
2429
  /**
2426
2430
  * 输出 ECWeb 状态。判定口径:
2427
2431
  * 1. 进程是否存活(instance/ 下的 pid 文件 + isProcessRunning)
@@ -2633,10 +2637,21 @@ function ensureServiceProxyConfig(cfg, port) {
2633
2637
  * Kept public for restart-monitor, which launches the daemon directly and
2634
2638
  * therefore cannot rely on cmdStart's post-ready ECWeb startup path.
2635
2639
  */
2636
- export async function startEcwebIfEnabled(p) {
2640
+ export async function startEcwebIfEnabled(p, options = {}) {
2637
2641
  const cfg = loadDaemonConfig();
2638
2642
  if (!cfg.ecweb?.enabled)
2639
2643
  return false;
2644
+ // The daemon is the lifecycle owner. CLI callers normally only need to
2645
+ // ensure ECWeb is available, so adopt a healthy process instead of killing
2646
+ // it. `ec watch web` passes forceRestart explicitly for an intentional
2647
+ // manual replacement.
2648
+ if (!options.forceRestart) {
2649
+ const existing = await waitForExistingEcweb(p);
2650
+ if (existing) {
2651
+ console.log(`✓ ECWeb 已在运行 (PID: ${existing.pid}) http://localhost:${existing.port}`);
2652
+ return true;
2653
+ }
2654
+ }
2640
2655
  const installedPkg = resolveGlobalPkg(WEB_PACKAGE_NAME);
2641
2656
  if (!installedPkg) {
2642
2657
  console.log(`⚠ EC Web 未安装,跳过启动(运行 npm i -g ${WEB_PACKAGE_NAME}@latest 后可用)`);
@@ -2795,7 +2810,7 @@ async function cmdWatchWeb() {
2795
2810
  saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: true, port } });
2796
2811
  // 自动配置 serviceProxy(如果控制 AID 已配置)
2797
2812
  ensureServiceProxyConfigBeforeStart(p);
2798
- const ok = await startEcwebIfEnabled(p);
2813
+ const ok = await startEcwebIfEnabled(p, { forceRestart: true });
2799
2814
  if (!ok)
2800
2815
  process.exit(1); // 失败原因已由 startEcwebIfEnabled 打印
2801
2816
  // 启动成功的访问信息已由 startEcwebIfEnabled 打印,无需 printEcwebAccess 重复输出
@@ -6,6 +6,7 @@ import { agentmdGet, getAidStore, isValidAid, loadClient, SLOT } from '../aun/ai
6
6
  import { ipcQuery } from '../ipc.js';
7
7
  import { resolvePaths } from '../paths.js';
8
8
  import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
9
+ import { downloadGroupFsBytes } from '../aun/group-fs-download.js';
9
10
  class FsCliError extends Error {
10
11
  code;
11
12
  suggestion;
@@ -257,6 +258,21 @@ async function runManagedFs(args, formatJson) {
257
258
  for (const item of result.items ?? [])
258
259
  console.log(JSON.stringify(item));
259
260
  return;
261
+ case 'mkdir':
262
+ console.log(`✓ 已创建目录: ${result.path}`);
263
+ return;
264
+ case 'upload':
265
+ console.log(`✓ 已上传: ${result.localPath} -> ${result.path}`);
266
+ return;
267
+ case 'copy':
268
+ console.log(`✓ 已复制: ${result.src} -> ${result.path}`);
269
+ return;
270
+ case 'move':
271
+ console.log(`✓ 已移动: ${result.src} -> ${result.path}`);
272
+ return;
273
+ case 'remove':
274
+ console.log(`✓ 已删除: ${result.path}`);
275
+ return;
260
276
  }
261
277
  }
262
278
  function parseCommonOptions(args) {
@@ -385,8 +401,9 @@ async function fsCat(args, opts) {
385
401
  if (route.backend === 'group') {
386
402
  statInfo = await client.group.fs.stat(remoteRef(target));
387
403
  const size = numberFrom(statInfo, ['size', 'size_bytes', 'bytes']);
388
- const downloaded = await client.group.fs.cp(remoteRef(target), { kind: 'blob' }, { verifyHash: true });
389
- bytes = bytesFromDownload(downloaded);
404
+ const downloaded = await downloadGroupFsBytes(client, remoteRef(target));
405
+ bytes = downloaded.bytes;
406
+ statInfo = { ...statInfo, ...downloaded.ticket };
390
407
  statInfo = { ...statInfo, size: bytes.byteLength };
391
408
  if (size !== undefined && size > opts.maxCatBytes) {
392
409
  binaryHeadOnly = true;
@@ -1420,14 +1437,6 @@ function contentTypeFrom(value) {
1420
1437
  const raw = value?.contentType ?? value?.content_type ?? value?.mime ?? value?.mime_type;
1421
1438
  return raw ? String(raw).toLowerCase() : undefined;
1422
1439
  }
1423
- function bytesFromDownload(value) {
1424
- const data = value?.data;
1425
- if (data instanceof Uint8Array)
1426
- return data;
1427
- if (data instanceof ArrayBuffer)
1428
- return new Uint8Array(data);
1429
- throw new FsCliError('BACKEND_ERROR', '下载接口未返回字节内容', undefined, value);
1430
- }
1431
1440
  function looksBinary(bytes, contentType) {
1432
1441
  if (contentType) {
1433
1442
  if (contentType.startsWith('text/'))
package/dist/cli/init.js CHANGED
@@ -7,7 +7,7 @@ import { commandExists } from '../utils/cross-platform.js';
7
7
  import { scanInstances } from '../utils/instance-registry.js';
8
8
  import { saveDefaultsSafe, loadAllAgents, loadDaemonConfig, saveDaemonConfig } from '../config-store.js';
9
9
  import { generateControlAid, resolveControlAidDomain } from '../aun/aid/control-aid.js';
10
- import { getCodexAppServerAvailability, isCodexAppServerAvailable } from '../agents/codex-runner.js';
10
+ import { getCodexAppServerAvailability } from '../agents/codex-runner.js';
11
11
  import { resolveEcagentConfig } from '../agents/baseagent.js';
12
12
  import { defaultProjectsRoot } from '../utils/project-path.js';
13
13
  import { WEB_CLI_BIN, WEB_PACKAGE_LATEST } from '../product.js';
@@ -28,15 +28,44 @@ export function expandLeadingTildePath(value, homeDir = os.homedir()) {
28
28
  }
29
29
  const BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini', 'ecagent'];
30
30
  const AUTO_BASEAGENT_CANDIDATES = ['claude', 'codex', 'gemini'];
31
- function isBaseagentAvailable(baseagent) {
31
+ function isBaseagentAvailable(baseagent, codexAvailability) {
32
32
  if (baseagent === 'codex')
33
- return isCodexAppServerAvailable();
33
+ return (codexAvailability ?? getCodexAppServerAvailability()).available;
34
34
  if (baseagent === 'ecagent')
35
35
  return true;
36
36
  return commandExists(baseagent);
37
37
  }
38
- function detectAvailable() {
39
- return BASEAGENT_CANDIDATES.filter(isBaseagentAvailable);
38
+ function detectAvailable(codexAvailability = getCodexAppServerAvailability()) {
39
+ return BASEAGENT_CANDIDATES.filter(baseagent => isBaseagentAvailable(baseagent, codexAvailability));
40
+ }
41
+ /** Offer installation only when the probe positively identified an old CLI. */
42
+ export async function maybeUpgradeCodexForInit(availability, dependencies) {
43
+ if (!availability.requiresUpgrade) {
44
+ return { availability, offered: false, accepted: false };
45
+ }
46
+ const accepted = await dependencies.confirm();
47
+ if (!accepted)
48
+ return { availability, offered: true, accepted: false };
49
+ try {
50
+ const install = dependencies.install ?? (async () => {
51
+ const { npmInstallGlobal } = await import('../utils/npm-ops.js');
52
+ await npmInstallGlobal('@openai/codex@latest');
53
+ });
54
+ await install();
55
+ }
56
+ catch (error) {
57
+ return {
58
+ availability,
59
+ offered: true,
60
+ accepted: true,
61
+ error: installErrorMessage(error),
62
+ };
63
+ }
64
+ return {
65
+ availability: (dependencies.probe ?? getCodexAppServerAvailability)(),
66
+ offered: true,
67
+ accepted: true,
68
+ };
40
69
  }
41
70
  function pickDefault(available) {
42
71
  return AUTO_BASEAGENT_CANDIDATES.find(candidate => available.includes(candidate));
@@ -192,7 +221,45 @@ export async function cmdInit(options) {
192
221
  return false;
193
222
  }
194
223
  // ── 2. 探测 baseagent ──
195
- const available = detectAvailable();
224
+ let codexAvailability = getCodexAppServerAvailability();
225
+ if (!options?.nonInteractive && codexAvailability.version) {
226
+ console.log(`检测到 Codex CLI: ${codexAvailability.version}`);
227
+ }
228
+ if (!options?.nonInteractive && codexAvailability.requiresUpgrade) {
229
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
230
+ let result;
231
+ try {
232
+ result = await maybeUpgradeCodexForInit(codexAvailability, {
233
+ confirm: async () => {
234
+ console.log(`⚠ ${codexAvailability.reason}`);
235
+ const answer = (await ask(rl, '是否立即升级 Codex CLI?[Y/n] ')).trim().toLowerCase();
236
+ return answer === '' || answer === 'y' || answer === 'yes';
237
+ },
238
+ });
239
+ }
240
+ finally {
241
+ try {
242
+ rl.close();
243
+ }
244
+ catch { /* ignore */ }
245
+ }
246
+ if (!result.accepted) {
247
+ console.log(' 已跳过 Codex CLI 升级');
248
+ }
249
+ else if (result.error) {
250
+ console.log(` ⚠ Codex CLI 升级失败: ${result.error}`);
251
+ }
252
+ else {
253
+ codexAvailability = result.availability;
254
+ if (codexAvailability.available) {
255
+ console.log(` ✓ Codex CLI 已升级至 ${codexAvailability.version}`);
256
+ }
257
+ else {
258
+ console.log(` ⚠ Codex CLI 升级后仍不可用: ${codexAvailability.reason ?? '未知原因'}`);
259
+ }
260
+ }
261
+ }
262
+ const available = detectAvailable(codexAvailability);
196
263
  if (available.length === 0 || (available.length === 1 && available[0] === 'ecagent' && ecagentUnavailableReason())) {
197
264
  console.log('❌ 未检测到可用 baseagent。请安装至少一款:');
198
265
  console.log(' - claude CLI');
@@ -202,8 +269,10 @@ export async function cmdInit(options) {
202
269
  return false;
203
270
  }
204
271
  console.log('检测到可用的 baseagent:');
205
- for (const b of available)
206
- console.log(` ● ${b}`);
272
+ for (const b of available) {
273
+ const version = b === 'codex' ? codexAvailability.version : undefined;
274
+ console.log(` ● ${b}${version ? ` (CLI ${version})` : ''}`);
275
+ }
207
276
  console.log('');
208
277
  const defaultsPath = p.defaultsConfig;
209
278
  const defaultsExisted = fs.existsSync(defaultsPath);
@@ -10,10 +10,18 @@ export async function cmdQueue(args) {
10
10
 
11
11
  查询:
12
12
  --agent <aid> 必填,目标 agent(如 mybot.agentid.pub)
13
+ --after <time> 只显示 adapter 接收时间晚于该值的消息(epoch ms 或 ISO)
14
+ --before <time> 只显示 adapter 接收时间早于该值的消息(epoch ms 或 ISO)
13
15
  --showid 显示 messageId
14
16
  --full 显示完整消息内容(不截断)
15
17
  --format json 以 JSON 格式输出
16
18
 
19
+ JSON 时间字段:
20
+ receivedAt adapter 收到消息时间(epoch ms)
21
+ enqueuedAt 实际进入队列时间(epoch ms)
22
+ queueWaitMs 排队等待时长(ms)
23
+ processingStartedAt 开始处理时间(epoch ms,active)
24
+
17
25
  操作:
18
26
  --clear 清空该 agent 的所有待处理消息
19
27
  --cancel <messageId> 取消指定消息
@@ -21,6 +29,7 @@ export async function cmdQueue(args) {
21
29
 
22
30
  示例:
23
31
  ec queue --agent mybot.agentid.pub
32
+ ec queue --agent mybot.agentid.pub --after 2026-08-19T12:00:00+08:00
24
33
  ec queue --agent mybot.agentid.pub --showid
25
34
  ec queue --agent mybot.agentid.pub --clear
26
35
  ec queue --agent mybot.agentid.pub --cancel msg_d4e5f6
@@ -46,6 +55,12 @@ export async function cmdQueue(args) {
46
55
  process.exit(1);
47
56
  }
48
57
  const formatJson = formatValue === 'json';
58
+ const after = parseQueueTime(getArgValue(args, '--after'), '--after');
59
+ const before = parseQueueTime(getArgValue(args, '--before'), '--before');
60
+ if (after !== undefined && before !== undefined && after >= before) {
61
+ console.error('❌ --after 必须早于 --before');
62
+ process.exit(1);
63
+ }
49
64
  const clear = args.includes('--clear');
50
65
  const cancelIdx = args.indexOf('--cancel');
51
66
  const cancelCandidate = cancelIdx >= 0 && cancelIdx + 1 < args.length ? args[cancelIdx + 1] : undefined;
@@ -80,6 +95,8 @@ export async function cmdQueue(args) {
80
95
  action,
81
96
  messageId: cancelMsgId,
82
97
  sessionKey,
98
+ after,
99
+ before,
83
100
  sessionId: managed,
84
101
  delegationToken: process.env[AGENT_DELEGATION_TOKEN_ENV],
85
102
  } : {
@@ -88,6 +105,8 @@ export async function cmdQueue(args) {
88
105
  action,
89
106
  messageId: cancelMsgId,
90
107
  sessionKey,
108
+ after,
109
+ before,
91
110
  });
92
111
  if (!result) {
93
112
  console.error('错误: 无法连接 evolcore 服务');
@@ -118,6 +137,16 @@ export async function cmdQueue(args) {
118
137
  }
119
138
  console.log(renderQueueItemsCli(items, showId, full, agent));
120
139
  }
140
+ function parseQueueTime(value, flag) {
141
+ if (value === undefined)
142
+ return undefined;
143
+ const parsed = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
144
+ if (!Number.isFinite(parsed)) {
145
+ console.error(`❌ ${flag} 必须是 epoch 毫秒或 ISO 时间: ${value}`);
146
+ process.exit(1);
147
+ }
148
+ return parsed;
149
+ }
121
150
  function renderQueueItemsCli(items, showId, full, aid) {
122
151
  const lines = [`${aid} — 队列 (${items.length} 条)`, ''];
123
152
  if (items.length === 0) {
@@ -151,6 +180,12 @@ function renderQueueItemsCli(items, showId, full, aid) {
151
180
  elapsed = sec >= 60 ? `${Math.floor(sec / 60)}m${sec % 60}s` : `${sec}s`;
152
181
  }
153
182
  parts.push(` ${elapsed.padEnd(elapsedColWidth)} `);
183
+ const wait = item.queueWaitMs == null ? '—' : formatDuration(item.queueWaitMs);
184
+ parts.push(` wait:${wait.padEnd(6)} `);
185
+ if (item.receivedAt != null)
186
+ parts.push(` recv:${formatQueueTime(item.receivedAt)} `);
187
+ if (item.enqueuedAt != null)
188
+ parts.push(` enq:${formatQueueTime(item.enqueuedAt)} `);
154
189
  // 内容列
155
190
  const content = full ? item.preview.replace(/\.\.\.$/, '') : item.preview;
156
191
  parts.push(`"${content}"`);
@@ -158,3 +193,10 @@ function renderQueueItemsCli(items, showId, full, aid) {
158
193
  }
159
194
  return lines.join('\n');
160
195
  }
196
+ function formatDuration(ms) {
197
+ const sec = Math.max(0, Math.round(ms / 1000));
198
+ return sec >= 60 ? `${Math.floor(sec / 60)}m${sec % 60}s` : `${sec}s`;
199
+ }
200
+ function formatQueueTime(timestamp) {
201
+ return new Date(timestamp).toISOString();
202
+ }
@@ -14,7 +14,6 @@ import { WEB_PACKAGE_NAME } from '../product.js';
14
14
  import { shouldSuppressRealRestart } from '../utils/restart-safety.js';
15
15
  import { rotateStdoutLog } from '../utils/log-writer.js';
16
16
  import { inspectDataMigrationRequirement } from '../core/data-migration.js';
17
- import { serviceProxyNeedsEcweb, startEcwebIfEnabled } from './daemon-commands.js';
18
17
  const execFileAsync = promisify(execFile);
19
18
  // 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
20
19
  function cleanEnv() {
@@ -194,22 +193,12 @@ export async function cmdRestartMonitor() {
194
193
  await notifyDaemonOwners(p, `⚠️ ${WEB_PACKAGE_NAME} 升级失败,使用当前版本继续\n${ecwebUpgrade.error ?? ''}`.trim(), log);
195
194
  break;
196
195
  }
197
- // source=ecweb Service Proxy 只在 daemon 启动后的短窗口内发现实例。
198
- // restart-monitor 直接 spawn daemon,必须在此先启动独立的 ec-web,不能绕过
199
- // cmdStart ECWeb 生命周期逻辑。
200
- const ecwebStartedBeforeDaemon = serviceProxyNeedsEcweb(loadDaemonConfig())
201
- ? await startEcwebIfEnabled(p)
202
- : false;
203
- if (ecwebStartedBeforeDaemon)
204
- log('ECWeb started before daemon');
196
+ // ECWeb is owned by the daemon. Starting it here used to race with the
197
+ // daemon's own launcher and ECWeb's stale-instance cleanup, which could
198
+ // leave ServiceProxy pointed at a dead PID.
205
199
  // 启动并检测 ready signal
206
200
  let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
207
201
  if (started) {
208
- // 没有 Service Proxy 时可以等 daemon ready 后再启动;有代理时已在上方启动,
209
- // 避免重新拉起造成实例记录和反向隧道短暂失配。
210
- if (!ecwebStartedBeforeDaemon && await startEcwebIfEnabled(p)) {
211
- log('ECWeb started after daemon');
212
- }
213
202
  log('✓ Service restarted successfully');
214
203
  archiveSelfHealLog(p, log);
215
204
  // 发起会话内的“重启成功”通知由新进程自行发送,此处只负责失败/自愈告警。
@@ -113,6 +113,7 @@ Create 参数模式支持:
113
113
  --permission <readonly|auto|request|bypass>(省略则继承当前身份配置)
114
114
  --tz <时区> (仅 cron)
115
115
  --target-channel <channelKey> --target-channel-id <ID> (AUN 可简写为 aun)
116
+ --target-chat-type <private|group> (AUN 目标必填;channelId 同时作为群聊 ID)
116
117
  --target-session <main|thread> [--target-thread-id <threadId>]
117
118
  --trigger-thread <by-run|by-trigger> (兼容旧值 per-run)
118
119
  --name <名称>
@@ -263,6 +264,11 @@ async function createTrigger(args, json) {
263
264
  if (!parsed.targetChannel || !parsed.targetChannelId) {
264
265
  throw new Error('CLI flag 模式需要显式指定 --target-channel <channelKey> --target-channel-id <id>');
265
266
  }
267
+ const targetIsAun = parsed.targetChannel === 'aun' || parsed.targetChannel.startsWith('aun#');
268
+ if (targetIsAun && !parsed.targetChatType) {
269
+ throw new Error('AUN 目标必须显式指定 --target-chat-type private 或 group');
270
+ }
271
+ const targetChatType = parsed.targetChatType;
266
272
  const now = Date.now();
267
273
  if (parsed.targetSession === 'thread' && !parsed.targetThreadId) {
268
274
  throw new Error('--target-session thread 需要 --target-thread-id');
@@ -300,6 +306,7 @@ async function createTrigger(args, json) {
300
306
  channelKey: parsed.targetChannel,
301
307
  channelId: parsed.targetChannelId,
302
308
  session: parsed.targetSession,
309
+ ...(targetChatType ? { chatType: targetChatType } : {}),
303
310
  ...(parsed.targetSession === 'thread' ? { threadId: parsed.targetThreadId } : {}),
304
311
  },
305
312
  },