evolcore 0.0.19 → 0.0.21

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 (136) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +60 -9
  3. package/bin/install-codex-managed-hooks.mjs +61 -0
  4. package/dist/agents/baseagent.js +10 -6
  5. package/dist/agents/claude-runner.js +383 -111
  6. package/dist/agents/codex-app-server-client.js +10 -2
  7. package/dist/agents/codex-runner.js +405 -137
  8. package/dist/agents/ecagent-runner.js +174 -63
  9. package/dist/agents/gemini-runner.js +130 -30
  10. package/dist/agents/request-identity.js +25 -0
  11. package/dist/agents/runner-types.js +19 -0
  12. package/dist/aun/aid/agentmd.js +66 -2
  13. package/dist/aun/aid/identity.js +4 -1
  14. package/dist/aun/aid/index.js +1 -1
  15. package/dist/aun/msg/group.js +86 -9
  16. package/dist/aun/msg/history.js +213 -36
  17. package/dist/aun/msg/managed-operation.js +58 -9
  18. package/dist/aun/msg/p2p.js +26 -11
  19. package/dist/aun/outbox.js +295 -68
  20. package/dist/aun/service-proxy.js +43 -25
  21. package/dist/channels/aun.js +1004 -273
  22. package/dist/channels/daemon.js +6 -1
  23. package/dist/cli/agent-command.js +4 -3
  24. package/dist/cli/agent.js +66 -56
  25. package/dist/cli/aun-commands.js +177 -42
  26. package/dist/cli/command-log.js +10 -11
  27. package/dist/cli/contact.js +1 -0
  28. package/dist/cli/daemon-commands.js +81 -121
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/init.js +76 -24
  31. package/dist/cli/restart-monitor.js +3 -3
  32. package/dist/cli/task-context.js +46 -0
  33. package/dist/cli/trigger-command.js +1 -1
  34. package/dist/cli/watch-logs.js +10 -3
  35. package/dist/config/builtin-roles.js +1 -0
  36. package/dist/config/config-field-policy.js +16 -5
  37. package/dist/config/config-manager.js +226 -24
  38. package/dist/config/config-operation-service.js +1 -2
  39. package/dist/config/contact-operation-service.js +32 -1
  40. package/dist/config/contact-request-service.js +44 -0
  41. package/dist/config/daemon-services.js +186 -0
  42. package/dist/config/gateway-config.js +29 -16
  43. package/dist/config/lifecycle.js +16 -5
  44. package/dist/config/role-service.js +54 -3
  45. package/dist/config/schema-migration.js +550 -0
  46. package/dist/config-store.js +161 -12
  47. package/dist/core/agent-application-service.js +279 -0
  48. package/dist/core/audit/log-integrity.js +102 -0
  49. package/dist/core/auth/agent-delegation.js +31 -1
  50. package/dist/core/auth/auth-gateway.js +33 -4
  51. package/dist/core/auth/authorization-audit.js +155 -4
  52. package/dist/core/auth/operation-authorizer.js +41 -1
  53. package/dist/core/auth/operation-catalog.js +9 -1
  54. package/dist/core/bootstrap-messages.js +2 -2
  55. package/dist/core/bootstrap-service.js +27 -38
  56. package/dist/core/causation/aun-association.js +7 -4
  57. package/dist/core/channel-loader.js +0 -2
  58. package/dist/core/command/agent-control.js +56 -16
  59. package/dist/core/command/command-handler.js +290 -44
  60. package/dist/core/command/connect-menu.js +3 -4
  61. package/dist/core/command/group-menu.js +5 -7
  62. package/dist/core/command/menu-handler.js +279 -80
  63. package/dist/core/command/role-menu.js +21 -11
  64. package/dist/core/command/slash-gate.js +85 -18
  65. package/dist/core/command/slash-handler.js +350 -32
  66. package/dist/core/event-catalog.js +5 -0
  67. package/dist/core/evolagent.js +9 -4
  68. package/dist/core/handoff/dispatcher.js +4 -0
  69. package/dist/core/handoff/runtime.js +10 -0
  70. package/dist/core/handoff/store.js +32 -9
  71. package/dist/core/inference/text-inference.js +7 -15
  72. package/dist/core/message/im-renderer.js +83 -84
  73. package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
  74. package/dist/core/message/message-bridge.js +124 -10
  75. package/dist/core/message/message-log.js +14 -7
  76. package/dist/core/message/message-queue.js +206 -16
  77. package/dist/core/message/message-utils.js +12 -5
  78. package/dist/core/message/response-engine.js +495 -72
  79. package/dist/core/message/send-receipt.js +1 -0
  80. package/dist/core/message/stream-debouncer.js +9 -2
  81. package/dist/core/model/model-catalog.js +23 -15
  82. package/dist/core/model/model-diagnostics.js +28 -10
  83. package/dist/core/permission/approval-gateway.js +180 -6
  84. package/dist/core/permission/ec-command-parser.js +465 -56
  85. package/dist/core/permission/mode.js +18 -3
  86. package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
  87. package/dist/core/permission/readonly-shell-query.js +263 -9
  88. package/dist/core/permission/sandbox-runtime.js +205 -13
  89. package/dist/core/permission/tool-policy.js +673 -62
  90. package/dist/core/relation/peer-identity.js +18 -0
  91. package/dist/core/session/session-fs-store.js +154 -5
  92. package/dist/core/session/session-manager.js +299 -30
  93. package/dist/core/session/session-renew.js +19 -12
  94. package/dist/core/session/session-turn-coordinator.js +11 -4
  95. package/dist/eck/kit-renderer.js +18 -9
  96. package/dist/index.js +283 -65
  97. package/dist/ipc.js +374 -24
  98. package/dist/paths.js +64 -7
  99. package/dist/response-system/context-builder.js +1 -7
  100. package/dist/trigger/anomaly-store.js +1 -0
  101. package/dist/trigger/feedback.js +56 -5
  102. package/dist/trigger/history.js +79 -4
  103. package/dist/trigger/legacy-session-history.js +2 -2
  104. package/dist/trigger/parser.js +3 -2
  105. package/dist/trigger/validation.js +6 -1
  106. package/dist/utils/atomic-write.js +27 -0
  107. package/dist/utils/ecweb-utils.js +16 -2
  108. package/dist/utils/error-utils.js +4 -1
  109. package/dist/utils/logger.js +21 -2
  110. package/dist/utils/process-tree-stats.js +24 -4
  111. package/dist/utils/process-tree-worker.js +31 -0
  112. package/dist/utils/project-path.js +1 -2
  113. package/dist/utils/stats.js +52 -18
  114. package/dist/utils/welcome.js +2 -2
  115. package/kits/docs/INDEX.md +1 -1
  116. package/kits/docs/evolcore/INDEX.md +1 -1
  117. package/kits/docs/evolcore/contact.md +7 -1
  118. package/kits/docs/evolcore/msg.md +16 -0
  119. package/kits/rules/01-overview.md +1 -1
  120. package/kits/rules/03-identity.md +1 -1
  121. package/kits/rules/05-venue.md +1 -1
  122. package/kits/schemas/_meta.json +10 -5
  123. package/kits/schemas/agent-config.schema.10.json +2 -1
  124. package/kits/schemas/agent-config.schema.11.json +421 -0
  125. package/kits/schemas/daemon.schema.5.json +135 -0
  126. package/kits/schemas/daemon.schema.6.json +131 -0
  127. package/kits/schemas/defaults.schema.5.json +119 -0
  128. package/kits/schemas/migrations/README.md +3 -1
  129. package/kits/schemas/relation-config.schema.8.json +13 -0
  130. package/kits/schemas/role-config.schema.1.json +1 -2
  131. package/kits/schemas/single-session.schema.3.json +32 -0
  132. package/kits/templates/roles/admin.json +1 -0
  133. package/kits/templates/roles/member.json +1 -0
  134. package/kits/templates/roles/visitor.json +1 -0
  135. package/package.json +7 -3
  136. package/skills/eclink/SKILL.md +2 -0
@@ -18,7 +18,7 @@ import { tryUpgrade, tryUpgradeGlobalPkg, resolveGlobalPkg } from '../utils/npm-
18
18
  import { ecwebVersionRequirementError, fetchEcwebPairCode, resolveEcwebLaunchCommand } 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
- import { filterLogFiles, deriveLogTypes, computePreChecked, validateLogTypes, shortLogName as shortLogNameLocal } from './watch-logs.js';
21
+ import { filterLogFiles, deriveLogTypes, computePreChecked, validateLogTypes, shortLogName as shortLogNameLocal, isWatchLogFile } from './watch-logs.js';
22
22
  import { displaySessionTitle } from '../core/session/session-title.js';
23
23
  import { isNonAgentSessionChannel } from '../core/system-channels.js';
24
24
  import { printCodeStats } from './code-stats.js';
@@ -29,6 +29,7 @@ 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
31
  import { listCodexAppServerProcesses, unregisterCodexAppServerProcess } from '../utils/codex-app-server-registry.js';
32
+ import { DEFAULT_ECWEB_PORT, ecwebService, setEcwebEnabled } from '../config/daemon-services.js';
32
33
  const execFileAsync = promisify(execFile);
33
34
  async function runAutomaticInit(action) {
34
35
  try {
@@ -58,15 +59,21 @@ function printNoSelfAgentHints(options) {
58
59
  console.log(` - ${skippedAgent.dirName}: ${skippedAgent.reason}`);
59
60
  }
60
61
  }
61
- async function probeDaemon(socketPath, timeoutMs = 1000) {
62
+ // Windows named-pipe connection setup can take a couple of seconds on a busy
63
+ // host (especially on the first request after the daemon starts). A one-second
64
+ // probe causes `ec stop` to skip the IPC shutdown path and fall back to
65
+ // taskkill, even though the daemon is healthy and able to shut down gracefully.
66
+ const DAEMON_PROBE_TIMEOUT_MS = platform.isWindows ? 5_000 : 1_000;
67
+ export const DAEMON_SHUTDOWN_TIMEOUT_MS = platform.isWindows ? 10_000 : 3_000;
68
+ async function probeDaemon(socketPath, timeoutMs = DAEMON_PROBE_TIMEOUT_MS) {
62
69
  const response = await ipcQuery(socketPath, { type: 'ping' }, timeoutMs);
63
70
  if (response?.pong !== true || !Number.isInteger(response.pid) || response.pid <= 0)
64
71
  return null;
65
72
  return response;
66
73
  }
67
74
  /** Ask the daemon to run its own graceful shutdown lifecycle. */
68
- export async function requestDaemonShutdown(socketPath, timeoutMs = 3_000, reason = 'cli', expectedPids) {
69
- const daemon = await probeDaemon(socketPath, Math.min(timeoutMs, 1_000));
75
+ export async function requestDaemonShutdown(socketPath, timeoutMs = DAEMON_SHUTDOWN_TIMEOUT_MS, reason = 'cli', expectedPids) {
76
+ const daemon = await probeDaemon(socketPath, Math.min(timeoutMs, DAEMON_PROBE_TIMEOUT_MS));
70
77
  if (!daemon || (expectedPids && !expectedPids.has(daemon.pid)))
71
78
  return null;
72
79
  const response = await ipcQuery(socketPath, {
@@ -140,11 +147,6 @@ function buildDaemonEnv(p, opts) {
140
147
  ...(opts.bindBootstrap ? { EVOLCORE_BIND_BOOTSTRAP: '1' } : {}),
141
148
  };
142
149
  }
143
- export function serviceProxyNeedsEcweb(cfg) {
144
- if (!cfg.serviceProxy?.enabled)
145
- return false;
146
- return (cfg.serviceProxy.services ?? []).some(s => s.enabled !== false && s.source === 'ecweb');
147
- }
148
150
  function latestSourceMtime(dir) {
149
151
  let latest = 0;
150
152
  try {
@@ -431,8 +433,6 @@ export async function cmdStart(opts = {}) {
431
433
  console.log(` Rotated: stdout.log -> ${path.basename(stdoutRotation.rotatedPath)}`);
432
434
  }
433
435
  cleanEnv();
434
- // 在启动前确保 serviceProxy 配置完整(如果 ecweb 启用但 serviceProxy 未配置)
435
- ensureServiceProxyConfigBeforeStart(p);
436
436
  // 删除旧的 ready signal
437
437
  try {
438
438
  fs.unlinkSync(p.readySignal);
@@ -561,7 +561,7 @@ export async function cmdStart(opts = {}) {
561
561
  }
562
562
  console.log('');
563
563
  // 代码统计仅在开发环境显示(EVOLCORE_HOME 指向包目录)
564
- if (resolveRoot() === getPackageRoot()) {
564
+ if (path.normalize(resolveRoot()) === path.normalize(getPackageRoot())) {
565
565
  printCodeStats(getPackageRoot(), p.logs);
566
566
  }
567
567
  console.log(`⏱ done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s`);
@@ -654,7 +654,7 @@ export async function cmdStop() {
654
654
  console.log(`⚠ 检测到未登记的 EvolCore 进程,将一并停止: ${runtimeOrphans.map(orphan => orphan.pid).join(', ')}`);
655
655
  }
656
656
  const gracefulPid = ping && pids.has(ping.pid)
657
- ? await requestDaemonShutdown(p.socket, 3_000, 'ec stop', pids)
657
+ ? await requestDaemonShutdown(p.socket, DAEMON_SHUTDOWN_TIMEOUT_MS, 'ec stop', pids)
658
658
  : null;
659
659
  const stopped = await Promise.all([...pids].map(pid => stopPid(pid, pid === gracefulPid)));
660
660
  if (stopped.some(result => !result)) {
@@ -726,7 +726,7 @@ export async function cmdRestart(opts = {}) {
726
726
  const mainPids = new Set(aliveMains.map(entry => entry.record.pid));
727
727
  const currentPing = await probeDaemon(socketPath);
728
728
  const gracefulPid = currentPing && mainPids.has(currentPing.pid)
729
- ? await requestDaemonShutdown(socketPath, 3_000, 'ec restart', mainPids)
729
+ ? await requestDaemonShutdown(socketPath, DAEMON_SHUTDOWN_TIMEOUT_MS, 'ec restart', mainPids)
730
730
  : null;
731
731
  const stopped = await Promise.all(aliveMains.map(m => stopPid(m.record.pid, m.record.pid === gracefulPid)));
732
732
  if (stopped.some(result => !result)) {
@@ -759,7 +759,7 @@ export async function cmdRestart(opts = {}) {
759
759
  const orphanPids = new Set(runtimeOrphans.map(orphan => orphan.pid));
760
760
  const currentPing = await probeDaemon(socketPath);
761
761
  const gracefulPid = currentPing && orphanPids.has(currentPing.pid)
762
- ? await requestDaemonShutdown(socketPath, 3_000, 'ec restart', orphanPids)
762
+ ? await requestDaemonShutdown(socketPath, DAEMON_SHUTDOWN_TIMEOUT_MS, 'ec restart', orphanPids)
763
763
  : null;
764
764
  const stopped = await Promise.all(runtimeOrphans.map(o => stopPid(o.pid, o.pid === gracefulPid)));
765
765
  if (stopped.some(result => !result)) {
@@ -802,7 +802,7 @@ export async function cmdRestart(opts = {}) {
802
802
  console.log('🔄 Restart monitor started, waiting for service to come back online...');
803
803
  await sleep(2000);
804
804
  // 代码统计(开发环境)
805
- if (resolveRoot() === getPackageRoot()) {
805
+ if (path.normalize(resolveRoot()) === path.normalize(getPackageRoot())) {
806
806
  console.log('');
807
807
  printCodeStats(getPackageRoot(), resolvePaths().logs);
808
808
  }
@@ -969,7 +969,17 @@ export async function cmdStatus() {
969
969
  const p = resolvePaths();
970
970
  const status = scanInstances();
971
971
  const aliveMains = status.mains.filter(m => m.alive);
972
- const ping = await probeDaemon(p.socket);
972
+ // Start all read-only daemon queries together. On a busy Windows host each
973
+ // query can otherwise wait behind the same IPC/event-loop delay and make the
974
+ // total `ec status` time the sum of several individual timeouts.
975
+ const pingPromise = probeDaemon(p.socket).catch(() => null);
976
+ const statusPromise = ipcQuery(p.socket, { type: 'status' }).catch(() => null);
977
+ const aidsPromise = ipcQuery(p.socket, { type: 'aun-aids' }).catch(() => null);
978
+ const agentsPromise = ipcQuery(p.socket, { type: 'evolagent.list' }).catch(() => null);
979
+ // Only the ping is needed to decide whether to show the live-daemon
980
+ // sections. The remaining responses continue in parallel while local
981
+ // process/session information is rendered.
982
+ const ping = await pingPromise;
973
983
  const pid = ping?.pid ?? (aliveMains.length > 0 ? aliveMains[0].record.pid : null);
974
984
  const processVisible = !!pid && aliveMains.some(entry => entry.record.pid === pid);
975
985
  if (aliveMains.length > 1) {
@@ -1125,42 +1135,47 @@ export async function cmdStatus() {
1125
1135
  }
1126
1136
  // Channel status. A running daemon is authoritative and avoids reading
1127
1137
  // protected config files from the CLI's potentially sandboxed process.
1138
+ let agentsResponse = null;
1128
1139
  if (pid) {
1129
1140
  console.log('');
1130
- const status = await ipcQuery(p.socket, { type: 'status' });
1131
- if (status) {
1141
+ const [statusResponse, aidsResponse, agentResponse] = await Promise.all([
1142
+ statusPromise,
1143
+ aidsPromise,
1144
+ agentsPromise,
1145
+ ]);
1146
+ agentsResponse = agentResponse;
1147
+ if (statusResponse) {
1132
1148
  // 🔑 AUN AIDs 表格(详细 AUN 实例状态)
1133
1149
  try {
1134
- const aidsResp = await ipcQuery(p.socket, { type: 'aun-aids' });
1135
- if (aidsResp?.ok && aidsResp.aids?.length > 0) {
1150
+ if (aidsResponse?.ok && aidsResponse.aids?.length > 0) {
1136
1151
  console.log('🔑 AUN AIDs:');
1137
- renderAunAidsTable(aidsResp.aids);
1152
+ renderAunAidsTable(aidsResponse.aids);
1138
1153
  }
1139
1154
  }
1140
1155
  catch { /* ignore */ }
1141
1156
  // 控制 AID(daemon 进程身份)状态
1142
- if (status.controlAid) {
1143
- const state = status.controlAid.connected ? 'connected' : 'disconnected';
1144
- console.log(`control: ${status.controlAid.aid} [${state}]`);
1157
+ if (statusResponse.controlAid) {
1158
+ const state = statusResponse.controlAid.connected ? 'connected' : 'disconnected';
1159
+ console.log(`control: ${statusResponse.controlAid.aid} [${state}]`);
1145
1160
  }
1146
1161
  else {
1147
1162
  console.log('control: not configured');
1148
1163
  }
1149
- if (status.stats) {
1164
+ if (statusResponse.stats) {
1150
1165
  console.log('');
1151
1166
  console.log('📊 Last hour:');
1152
- console.log(` Messages: ${status.stats.received} received, ${status.stats.completed} completed`);
1153
- if (status.stats.errors > 0)
1154
- console.log(` Errors: ${status.stats.errors}`);
1155
- if (status.stats.completed > 0)
1156
- console.log(` Avg response: ${(status.stats.avgResponseMs / 1000).toFixed(1)}s`);
1167
+ console.log(` Messages: ${statusResponse.stats.received} received, ${statusResponse.stats.completed} completed`);
1168
+ if (statusResponse.stats.errors > 0)
1169
+ console.log(` Errors: ${statusResponse.stats.errors}`);
1170
+ if (statusResponse.stats.completed > 0)
1171
+ console.log(` Avg response: ${(statusResponse.stats.avgResponseMs / 1000).toFixed(1)}s`);
1157
1172
  }
1158
1173
  }
1159
1174
  else {
1160
1175
  // IPC unreachable but PID exists — fall back to local config if readable.
1161
1176
  const config = readStatusDefaultsConfig(p.defaultsConfig);
1162
1177
  if (config) {
1163
- console.log('🔌 Channels (IPC unreachable):');
1178
+ console.log('🔌 Channels (live status unavailable; configuration is stale):');
1164
1179
  showConfigChannels(config);
1165
1180
  }
1166
1181
  }
@@ -1176,7 +1191,7 @@ export async function cmdStatus() {
1176
1191
  // EvolAgent summary (via IPC, only when running)
1177
1192
  if (pid) {
1178
1193
  try {
1179
- const agentResult = await ipcQuery(p.socket, { type: 'evolagent.list' });
1194
+ const agentResult = agentsResponse;
1180
1195
  if (agentResult?.ok && agentResult.agents?.length > 0) {
1181
1196
  const agents = agentResult.agents;
1182
1197
  if (agents.length > 0) {
@@ -1190,6 +1205,22 @@ export async function cmdStatus() {
1190
1205
  }
1191
1206
  }
1192
1207
  }
1208
+ else {
1209
+ const { EvolAgentRegistry } = await import('../core/evolagent-registry.js');
1210
+ const diskRegistry = new EvolAgentRegistry(p.agentsDir);
1211
+ diskRegistry.loadAll();
1212
+ const agents = diskRegistry.list();
1213
+ if (agents.length > 0) {
1214
+ console.log('');
1215
+ console.log('🤖 EvolAgents (live status unavailable; stale configuration):');
1216
+ for (const a of agents) {
1217
+ const diskStatus = a.status === 'disabled' || a.status === 'error' ? a.status : 'unknown';
1218
+ const channels = summarizeChannelFingerprints(a.channels || []);
1219
+ const shortName = a.name.replace(/\.agentid\.pub$/, '');
1220
+ console.log(` ? ${shortName.padEnd(20)} ${`${diskStatus} (stale)`.padEnd(18)} ${channels}`);
1221
+ }
1222
+ }
1223
+ }
1193
1224
  }
1194
1225
  catch {
1195
1226
  // IPC query for agents failed — skip section
@@ -1783,10 +1814,10 @@ async function cmdWatchLogsFlow() {
1783
1814
  console.log(`❌ Log directory not found: ${p.logs}`);
1784
1815
  process.exit(1);
1785
1816
  }
1786
- const files = fs.readdirSync(p.logs).filter(f => f.endsWith('.log'));
1817
+ const files = fs.readdirSync(p.logs).filter(isWatchLogFile);
1787
1818
  const types = deriveLogTypes(files);
1788
1819
  if (types.length === 0) {
1789
- console.log(`⚠ ${p.logs} 下暂无 .log 文件`);
1820
+ console.log(`⚠ ${p.logs} 下暂无可监听日志文件`);
1790
1821
  return;
1791
1822
  }
1792
1823
  const fileCount = new Map();
@@ -1857,7 +1888,7 @@ function cmdWatch(filterTypes) {
1857
1888
  return c;
1858
1889
  };
1859
1890
  const listLogs = () => {
1860
- const all = fs.readdirSync(p.logs).filter(f => f.endsWith('.log')).map(f => path.join(p.logs, f));
1891
+ const all = fs.readdirSync(p.logs).filter(isWatchLogFile).map(f => path.join(p.logs, f));
1861
1892
  return filterLogFiles(all, filterTypes);
1862
1893
  };
1863
1894
  const shortName = shortLogNameLocal;
@@ -1879,7 +1910,7 @@ function cmdWatch(filterTypes) {
1879
1910
  const content = formatWatchContent(line);
1880
1911
  return `${timeStr} ${paddedName} ${content}`;
1881
1912
  };
1882
- console.log(`🔭 Watching ${p.logs}/*.log (ESC to stop)\n`);
1913
+ console.log(`🔭 Watching ${p.logs}/* log files (ESC to stop)\n`);
1883
1914
  // 显示当前实例信息和 AID 状态
1884
1915
  const instStatus = scanInstances();
1885
1916
  const aliveMainEntries = instStatus.mains.filter(m => m.alive);
@@ -2502,9 +2533,9 @@ async function waitForExistingEcweb(p, timeoutMs = 3_000) {
2502
2533
  async function printEcwebStatus(p) {
2503
2534
  try {
2504
2535
  const cfg = loadDaemonConfig();
2505
- if (cfg.ecweb?.enabled === false) {
2536
+ if (ecwebService(cfg.services)?.enabled === false) {
2506
2537
  console.log('');
2507
- console.log('🔭 ECWeb: 已禁用 (daemon.json → ecweb.enabled: false)');
2538
+ console.log('🔭 ECWeb: 已禁用 (daemon.json → services[name=ecweb].enabled: false)');
2508
2539
  return;
2509
2540
  }
2510
2541
  }
@@ -2602,7 +2633,7 @@ export function stopCodexAppServerOrphans() {
2602
2633
  /** 若 ecweb 在运行则杀掉并确认 pid/端口都已释放。 */
2603
2634
  async function stopEcwebIfRunning(p) {
2604
2635
  const alive = findAliveEcweb(p);
2605
- const port = loadDaemonConfig().ecweb?.port ?? 42705;
2636
+ const port = ecwebService(loadDaemonConfig().services)?.port ?? DEFAULT_ECWEB_PORT;
2606
2637
  const pids = new Set([
2607
2638
  ...(alive ? [alive.pid] : []),
2608
2639
  ...platform.findProcessByPort(port),
@@ -2628,78 +2659,6 @@ async function waitForPortRelease(port, timeoutMs) {
2628
2659
  }
2629
2660
  return platform.findProcessByPort(port).length === 0;
2630
2661
  }
2631
- /**
2632
- * 后台 detached 启动 ecweb;若已运行则先停再启(确保加载最新代码)。
2633
- * 启动后轮询端口确认 HTTP 服务真正就绪,打印明确的成功/失败结论(而非模糊的「已在后台启动」状态描述)。
2634
- * 返回 true=本次确实启动成功,false=未启用/未安装/启动失败。
2635
- */
2636
- /**
2637
- * 启动前检查并补全 serviceProxy 配置(如果 ecweb 启用但 serviceProxy 未配置)
2638
- */
2639
- function ensureServiceProxyConfigBeforeStart(p) {
2640
- const cfg = loadDaemonConfig();
2641
- // 仅在 ecweb 启用 + 控制 AID 已配置 + serviceProxy 未配置 ecweb 时自动添加
2642
- if (!cfg.ecweb?.enabled || !cfg.aid)
2643
- return;
2644
- const hasEcwebService = (cfg.serviceProxy?.services ?? []).some(s => s.name === 'ecweb' || s.source === 'ecweb');
2645
- if (!hasEcwebService) {
2646
- const updatedCfg = {
2647
- ...cfg,
2648
- serviceProxy: {
2649
- enabled: true,
2650
- ...(cfg.serviceProxy ?? {}),
2651
- services: [
2652
- ...(cfg.serviceProxy?.services ?? []),
2653
- {
2654
- name: 'ecweb',
2655
- source: 'ecweb',
2656
- serviceType: 'http',
2657
- visibility: 'public',
2658
- enabled: true,
2659
- metadata: {
2660
- label: 'EvolCore Dashboard',
2661
- },
2662
- },
2663
- ],
2664
- },
2665
- };
2666
- saveDaemonConfig(updatedCfg);
2667
- console.log(`✓ 自动配置 Service Proxy: https://${cfg.aid}/proxy/ecweb/`);
2668
- }
2669
- }
2670
- /**
2671
- * 确保 serviceProxy 配置包含 ecweb 服务(ECWeb 启动成功后自动配置)
2672
- */
2673
- function ensureServiceProxyConfig(cfg, port) {
2674
- // 仅在控制 AID 已配置时自动添加 serviceProxy
2675
- if (!cfg.aid)
2676
- return;
2677
- const hasEcwebService = (cfg.serviceProxy?.services ?? []).some(s => s.name === 'ecweb' || s.source === 'ecweb');
2678
- if (!hasEcwebService) {
2679
- const updatedCfg = {
2680
- ...cfg,
2681
- serviceProxy: {
2682
- enabled: true,
2683
- ...(cfg.serviceProxy ?? {}),
2684
- services: [
2685
- ...(cfg.serviceProxy?.services ?? []),
2686
- {
2687
- name: 'ecweb',
2688
- source: 'ecweb',
2689
- serviceType: 'http',
2690
- visibility: 'public',
2691
- enabled: true,
2692
- metadata: {
2693
- label: 'EvolCore Dashboard',
2694
- },
2695
- },
2696
- ],
2697
- },
2698
- };
2699
- saveDaemonConfig(updatedCfg);
2700
- console.log(` 自动配置 Service Proxy: https://${cfg.aid}/proxy/ecweb/`);
2701
- }
2702
- }
2703
2662
  /**
2704
2663
  * Start the separately installed `ec-web` process for a configured runtime.
2705
2664
  * Kept public for restart-monitor, which launches the daemon directly and
@@ -2707,7 +2666,8 @@ function ensureServiceProxyConfig(cfg, port) {
2707
2666
  */
2708
2667
  export async function startEcwebIfEnabled(p, options = {}) {
2709
2668
  const cfg = loadDaemonConfig();
2710
- if (!cfg.ecweb?.enabled)
2669
+ const service = ecwebService(cfg.services);
2670
+ if (!service?.enabled)
2711
2671
  return false;
2712
2672
  // The daemon is the lifecycle owner. CLI callers normally only need to
2713
2673
  // ensure ECWeb is available, so adopt a healthy process instead of killing
@@ -2744,7 +2704,7 @@ export async function startEcwebIfEnabled(p, options = {}) {
2744
2704
  console.log(`❌ EC Web 旧进程未清理干净,取消启动: ${error instanceof Error ? error.message : String(error)}`);
2745
2705
  return false;
2746
2706
  }
2747
- const port = cfg.ecweb.port ?? 42705;
2707
+ const port = service.port ?? DEFAULT_ECWEB_PORT;
2748
2708
  const args = ['--home', p.root, '--port', String(port)];
2749
2709
  const launch = resolveEcwebLaunchCommand(args, { installedPkg });
2750
2710
  if (!launch) {
@@ -2874,10 +2834,9 @@ async function cmdWatchWeb() {
2874
2834
  }
2875
2835
  // 2. 启动(后台)并同步配置。默认行为是替换旧实例,确保新配置/新版静态资源生效。
2876
2836
  const cfg = loadDaemonConfig();
2877
- const port = cfg.ecweb?.port ?? 42705;
2878
- saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: true, port } });
2879
- // 自动配置 serviceProxy(如果控制 AID 已配置)
2880
- ensureServiceProxyConfigBeforeStart(p);
2837
+ const currentService = ecwebService(cfg.services);
2838
+ const port = currentService?.port ?? DEFAULT_ECWEB_PORT;
2839
+ saveDaemonConfig({ ...cfg, services: setEcwebEnabled(cfg.services, true, port) });
2881
2840
  const ok = await startEcwebIfEnabled(p, { forceRestart: true });
2882
2841
  if (!ok)
2883
2842
  process.exit(1); // 失败原因已由 startEcwebIfEnabled 打印
@@ -3005,8 +2964,9 @@ export async function cmdDiagnose() {
3005
2964
  // 6. 检查 EC Web 端口与实例记录是否一致。
3006
2965
  try {
3007
2966
  const cfg = loadDaemonConfig();
3008
- if (cfg.ecweb?.enabled) {
3009
- const port = cfg.ecweb.port ?? 42705;
2967
+ const service = ecwebService(cfg.services);
2968
+ if (service?.enabled) {
2969
+ const port = service.port ?? DEFAULT_ECWEB_PORT;
3010
2970
  const portPids = platform.findProcessByPort(port);
3011
2971
  const recordedPids = new Set(readEcwebInstanceEntries(p).map(entry => entry.record.pid));
3012
2972
  const unregisteredPids = portPids.filter(pid => !recordedPids.has(pid));
@@ -3077,7 +3037,7 @@ export async function cmdWatchCommand(args) {
3077
3037
  if (requested.length > 0) {
3078
3038
  const p2 = resolvePaths();
3079
3039
  const avail = fs.existsSync(p2.logs)
3080
- ? deriveLogTypes(fs.readdirSync(p2.logs).filter(f => f.endsWith('.log')))
3040
+ ? deriveLogTypes(fs.readdirSync(p2.logs).filter(isWatchLogFile))
3081
3041
  : [];
3082
3042
  const invalid = validateLogTypes(requested, avail);
3083
3043
  if (invalid.length > 0) {
package/dist/cli/index.js CHANGED
@@ -118,6 +118,7 @@ export async function main(args) {
118
118
 
119
119
  仅初始化 defaults.json:
120
120
  ec init 交互式(写完 defaults.json 后嵌套 agent new)
121
+ 选择 ecagent 时会继续询问 API Key 和 Base URL
121
122
  ec init --non-interactive [选项]
122
123
  --baseagent <claude|codex|gemini|ecagent> 默认: PATH 中第一个可用项;ecagent 需显式选择
123
124
  --force 已存在 defaults.json 时覆盖
package/dist/cli/init.js CHANGED
@@ -6,9 +6,10 @@ import { resolvePaths, ensureDataDirs } from '../paths.js';
6
6
  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
+ import { ecwebService, setEcwebEnabled } from '../config/daemon-services.js';
9
10
  import { generateControlAid, resolveControlAidDomain } from '../aun/aid/control-aid.js';
10
11
  import { getCodexAppServerAvailability } from '../agents/codex-runner.js';
11
- import { resolveEcagentConfig } from '../agents/baseagent.js';
12
+ import { DEFAULT_ECAGENT_BASE_URL, resolveEcagentConfig } from '../agents/baseagent.js';
12
13
  import { defaultProjectsRoot } from '../utils/project-path.js';
13
14
  import { WEB_CLI_BIN, WEB_PACKAGE_LATEST } from '../product.js';
14
15
  import { autostartInstalled, autostartPlatformLabel, configureAutostart } from '../utils/autostart.js';
@@ -79,14 +80,14 @@ function ecagentUnavailableReason() {
79
80
  return error instanceof Error ? error.message : String(error);
80
81
  }
81
82
  }
82
- function buildDefaults(chosen, available, projectsDefaultPath) {
83
+ function buildDefaults(chosen, available, projectsDefaultPath, ecagentConfig) {
83
84
  const baseagents = {};
84
85
  for (const b of available) {
85
86
  baseagents[b] = {};
86
87
  }
87
88
  // ecagent is bundled with EvolCore, so declare it even when the caller
88
89
  // supplies a reduced availability list (for example, a test probe).
89
- baseagents.ecagent ??= {};
90
+ baseagents.ecagent = ecagentConfig ?? baseagents.ecagent ?? {};
90
91
  return {
91
92
  $schema_version: 1,
92
93
  active_baseagent: chosen,
@@ -94,8 +95,8 @@ function buildDefaults(chosen, available, projectsDefaultPath) {
94
95
  ...(projectsDefaultPath ? { projects: { defaultPath: projectsDefaultPath } } : {}),
95
96
  };
96
97
  }
97
- function writeDefaults(chosen, available, projectsDefaultPath) {
98
- saveDefaultsSafe(buildDefaults(chosen, available, projectsDefaultPath));
98
+ function writeDefaults(chosen, available, projectsDefaultPath, ecagentConfig) {
99
+ saveDefaultsSafe(buildDefaults(chosen, available, projectsDefaultPath, ecagentConfig));
99
100
  }
100
101
  function hasCompletedInitConfig() {
101
102
  const p = resolvePaths();
@@ -263,7 +264,7 @@ export async function cmdInit(options) {
263
264
  }
264
265
  }
265
266
  const available = detectAvailable(codexAvailability);
266
- if (available.length === 0 || (available.length === 1 && available[0] === 'ecagent' && ecagentUnavailableReason())) {
267
+ if (available.length === 0) {
267
268
  console.log('❌ 未检测到可用 baseagent。请安装至少一款:');
268
269
  console.log(' - claude CLI');
269
270
  console.log(' - gemini CLI');
@@ -361,13 +362,6 @@ export async function cmdInit(options) {
361
362
  console.log(` ${input} 当前环境不可用(可用: ${available.join('/')})`);
362
363
  continue;
363
364
  }
364
- if (input === 'ecagent') {
365
- const reason = ecagentUnavailableReason();
366
- if (reason) {
367
- console.log(` ecagent 当前环境不可用:${reason}`);
368
- continue;
369
- }
370
- }
371
365
  chosen = input;
372
366
  }
373
367
  return chosen;
@@ -405,13 +399,59 @@ export async function cmdInit(options) {
405
399
  return resolved;
406
400
  }
407
401
  }
402
+ async function askEcagentConfig() {
403
+ let configured;
404
+ try {
405
+ const raw = JSON.parse(fs.readFileSync(defaultsPath, 'utf8'));
406
+ const candidate = raw?.baseagents?.ecagent;
407
+ if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
408
+ configured = candidate;
409
+ }
410
+ }
411
+ catch {
412
+ // A malformed or missing defaults file should not prevent the user
413
+ // from supplying a fresh ecagent configuration during init.
414
+ configured = undefined;
415
+ }
416
+ const usable = (value) => {
417
+ if (typeof value !== 'string')
418
+ return false;
419
+ const normalized = value.trim();
420
+ if (!normalized)
421
+ return false;
422
+ const lower = normalized.toLowerCase();
423
+ return !lower.includes('your-') && !lower.includes('placeholder');
424
+ };
425
+ const configuredBaseUrl = [
426
+ configured?.baseUrl,
427
+ process.env.ECAGENT_BASE_URL,
428
+ DEFAULT_ECAGENT_BASE_URL,
429
+ ].find(usable) || DEFAULT_ECAGENT_BASE_URL;
430
+ const baseUrl = (await ask(rl, `ecagent Base URL [${configuredBaseUrl}]: `)).trim() || configuredBaseUrl;
431
+ const configuredApiKey = usable(configured?.apiKey) ? configured.apiKey.trim() : undefined;
432
+ const environmentApiKeyName = !configuredApiKey
433
+ ? (usable(process.env.ECAGENT_API_KEY)
434
+ ? 'ECAGENT_API_KEY'
435
+ : usable(process.env.OPENAI_API_KEY) ? 'OPENAI_API_KEY' : undefined)
436
+ : undefined;
437
+ let apiKey = '';
438
+ while (!apiKey) {
439
+ const suffix = configuredApiKey || environmentApiKeyName ? '(回车保留当前配置)' : '(必填)';
440
+ const input = (await ask(rl, `ecagent API Key ${suffix}: `)).trim();
441
+ apiKey = input || configuredApiKey || (environmentApiKeyName ? '${' + environmentApiKeyName + '}' : '');
442
+ if (!apiKey)
443
+ console.log(' API Key 不能为空,请重新输入');
444
+ }
445
+ return { apiKey, baseUrl };
446
+ }
408
447
  try {
409
448
  if (defaultsExisted) {
410
449
  const ans = (await ask(rl, `配置文件已存在: ${defaultsPath}\n 是否覆盖?[y/N] `)).trim().toLowerCase();
411
450
  if (ans === 'y' || ans === 'yes') {
412
451
  const chosen = await askBaseagent();
452
+ const ecagentConfig = chosen === 'ecagent' ? await askEcagentConfig() : undefined;
413
453
  const projectsDefaultPath = await askProjectsDefaultPath();
414
- writeDefaults(chosen, available, projectsDefaultPath);
454
+ writeDefaults(chosen, available, projectsDefaultPath, ecagentConfig);
415
455
  console.log(`\n✓ 已覆盖: ${defaultsPath}`);
416
456
  console.log(` active_baseagent: ${chosen}\n`);
417
457
  }
@@ -421,8 +461,9 @@ export async function cmdInit(options) {
421
461
  }
422
462
  else {
423
463
  const chosen = await askBaseagent();
464
+ const ecagentConfig = chosen === 'ecagent' ? await askEcagentConfig() : undefined;
424
465
  const projectsDefaultPath = await askProjectsDefaultPath();
425
- writeDefaults(chosen, available, projectsDefaultPath);
466
+ writeDefaults(chosen, available, projectsDefaultPath, ecagentConfig);
426
467
  console.log(`\n✓ 已创建: ${defaultsPath}`);
427
468
  console.log(` active_baseagent: ${chosen}\n`);
428
469
  }
@@ -578,25 +619,34 @@ export async function initTail(options = {}) {
578
619
  }
579
620
  async function handleEcwebPrompt(rl) {
580
621
  const daemonConfigForEcweb = loadDaemonConfig();
581
- if (daemonConfigForEcweb.ecweb?.enabled === undefined) {
622
+ if (ecwebService(daemonConfigForEcweb.services)?.enabled === undefined) {
582
623
  const ans = (await ask(rl, '\n是否在 ec start 时自动启动 ECWeb 控制台?[y/N] ')).trim().toLowerCase();
583
624
  if (ans === 'y' || ans === 'yes') {
584
625
  const installResult = await ensureEcwebInstalledForInit();
585
626
  if (installResult.ok) {
586
627
  const cfg = loadDaemonConfig();
587
- saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: true } });
628
+ saveDaemonConfig({
629
+ ...cfg,
630
+ services: setEcwebEnabled(cfg.services, true),
631
+ });
588
632
  console.log(' ✓ 已启用 ECWeb(ec start 将自动在后台启动)');
589
633
  console.log(' 提示:首次访问运行 ec watch web 查看配对码和 URL');
590
634
  }
591
635
  else {
592
636
  const cfg = loadDaemonConfig();
593
- saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: false } });
637
+ saveDaemonConfig({
638
+ ...cfg,
639
+ services: setEcwebEnabled(cfg.services, false),
640
+ });
594
641
  console.log(' 提示:安装完成后运行 ec start;如需立即打开控制台,运行 ec watch web');
595
642
  }
596
643
  }
597
644
  else {
598
645
  const cfg = loadDaemonConfig();
599
- saveDaemonConfig({ ...cfg, ecweb: { ...(cfg.ecweb ?? {}), enabled: false } });
646
+ saveDaemonConfig({
647
+ ...cfg,
648
+ services: setEcwebEnabled(cfg.services, false),
649
+ });
600
650
  console.log(' 已跳过(可日后运行 ec watch web 手动启动,或编辑 daemon.json)');
601
651
  }
602
652
  }
@@ -618,7 +668,7 @@ export async function initTail(options = {}) {
618
668
  const finalCfg = loadDaemonConfig();
619
669
  console.log(` 控制 AID: ${finalCfg.aid || '(未配置)'}`);
620
670
  console.log(` 管理者: ${finalCfg.owners?.join(', ') || '(未配置)'}`);
621
- console.log(` ECWeb 自启动: ${finalCfg.ecweb?.enabled ? '已启用' : '已禁用'}`);
671
+ console.log(` ECWeb 自启动: ${ecwebService(finalCfg.services)?.enabled ? '已启用' : '已禁用'}`);
622
672
  const { agents: finalAgents } = loadAllAgents();
623
673
  if (finalAgents.length === 0) {
624
674
  console.log('\n📌 下一步:创建 agent');
@@ -786,11 +836,11 @@ export async function cmdInitNonInteractive(opts, dependencies = {}) {
786
836
  const ensureEcweb = dependencies.ensureEcweb ?? (() => ensureEcwebInstalledForInit({ quiet: true }));
787
837
  const installResult = await ensureEcweb();
788
838
  if (!installResult.ok) {
789
- if (existingCfg.ecweb?.enabled === true) {
839
+ if (ecwebService(existingCfg.services)?.enabled === true) {
790
840
  try {
791
841
  saveDaemonConfig({
792
842
  ...existingCfg,
793
- ecweb: { ...(existingCfg.ecweb ?? {}), enabled: false },
843
+ services: setEcwebEnabled(existingCfg.services, false),
794
844
  });
795
845
  }
796
846
  catch (e) {
@@ -826,7 +876,9 @@ export async function cmdInitNonInteractive(opts, dependencies = {}) {
826
876
  $schema_version: existingCfg.$schema_version ?? 1,
827
877
  aid: controlAid,
828
878
  owners: [ownerAid],
829
- ...(opts.ecweb === true ? { ecweb: { ...(existingCfg.ecweb ?? {}), enabled: true } } : {}),
879
+ ...(opts.ecweb === true
880
+ ? { services: setEcwebEnabled(existingCfg.services, true) }
881
+ : {}),
830
882
  };
831
883
  saveDaemonConfig(next);
832
884
  }
@@ -854,7 +906,7 @@ export async function cmdInitNonInteractive(opts, dependencies = {}) {
854
906
  controlAid: controlAid,
855
907
  ownerAid,
856
908
  owners: [ownerAid],
857
- ecwebEnabled: opts.ecweb === true || existingCfg.ecweb?.enabled === true,
909
+ ecwebEnabled: opts.ecweb === true || ecwebService(existingCfg.services)?.enabled === true,
858
910
  baseagent: chosenBaseagent,
859
911
  projectsDefaultPath: projectsDefaultPath ?? null,
860
912
  defaultsPath: p.defaultsConfig,
@@ -14,7 +14,7 @@ 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 { requestDaemonShutdown } from './daemon-commands.js';
17
+ import { DAEMON_SHUTDOWN_TIMEOUT_MS, requestDaemonShutdown } from './daemon-commands.js';
18
18
  const execFileAsync = promisify(execFile);
19
19
  // 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
20
20
  function cleanEnv() {
@@ -107,7 +107,7 @@ export async function cmdRestartMonitor() {
107
107
  const pids = runtimeOrphans.map(o => o.pid).join(', ');
108
108
  log(`Stopping unregistered EvolCore process(es): ${pids}`);
109
109
  const orphanPids = new Set(runtimeOrphans.map(orphan => orphan.pid));
110
- const ping = await requestDaemonShutdown(p.socket, 3_000, 'restart-monitor orphan cleanup', orphanPids);
110
+ const ping = await requestDaemonShutdown(p.socket, DAEMON_SHUTDOWN_TIMEOUT_MS, 'restart-monitor orphan cleanup', orphanPids);
111
111
  for (const orphan of runtimeOrphans) {
112
112
  if (orphan.pid !== ping)
113
113
  platform.killProcess(orphan.pid, false);
@@ -134,7 +134,7 @@ export async function cmdRestartMonitor() {
134
134
  if (aliveMains.length > 0) {
135
135
  const oldPids = aliveMains.map(m => m.record.pid);
136
136
  log(`Monitoring ${oldPids.length} main process(es): ${oldPids.join(', ')}`);
137
- const gracefulPid = await requestDaemonShutdown(p.socket, 3_000, 'restart-monitor', new Set(oldPids));
137
+ const gracefulPid = await requestDaemonShutdown(p.socket, DAEMON_SHUTDOWN_TIMEOUT_MS, 'restart-monitor', new Set(oldPids));
138
138
  // 没有收到 IPC shutdown 确认的进程才走信号兜底
139
139
  for (const pid of oldPids) {
140
140
  if (pid !== gracefulPid) {