evolcore 0.0.6 → 0.0.8
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.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -3
- package/dist/agents/baseagent.js +6 -5
- package/dist/agents/claude-runner.js +1 -0
- package/dist/agents/codex-app-server-client.js +6 -2
- package/dist/agents/codex-runner.js +8 -3
- package/dist/aun/aid/control-aid.js +40 -27
- package/dist/aun/aid/domain.js +23 -0
- package/dist/channels/aun.js +26 -21
- package/dist/cli/bench.js +4 -3
- package/dist/cli/daemon-commands.js +196 -66
- package/dist/cli/data-command.js +62 -35
- package/dist/cli/init-channel.js +1 -1
- package/dist/cli/init.js +9 -13
- package/dist/cli/restart-monitor.js +116 -22
- package/dist/config/config-manager.js +34 -3
- package/dist/config/gateway-config.js +80 -35
- package/dist/config-store.js +15 -3
- package/dist/core/baseagent-loader.js +5 -3
- package/dist/core/capability/providers/codex-capability-provider.js +2 -2
- package/dist/core/channel-loader.js +10 -1
- package/dist/core/command/menu-handler.js +17 -6
- package/dist/core/data-migration.js +517 -24
- package/dist/core/protected-paths.js +12 -1
- package/dist/index.js +755 -701
- package/dist/ipc.js +2 -0
- package/dist/utils/codex-cli.js +39 -0
- package/dist/utils/cross-platform.js +147 -18
- package/dist/utils/instance-registry.js +45 -8
- package/dist/utils/process-introspect.js +7 -3
- package/kits/rules/01-overview.md +3 -2
- package/kits/schemas/_meta.json +2 -1
- package/kits/schemas/daemon.schema.4.json +132 -0
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ import { WEB_PACKAGE_NAME } from '../product.js';
|
|
|
15
15
|
import { shouldSuppressRealRestart } from '../utils/restart-safety.js';
|
|
16
16
|
import { rotateStdoutLog } from '../utils/log-writer.js';
|
|
17
17
|
import { inspectDataMigrationRequirement } from '../core/data-migration.js';
|
|
18
|
+
import { serviceProxyNeedsEcweb, startEcwebIfEnabled } from './daemon-commands.js';
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
19
20
|
// 清理 Claude Code 环境变量,防止 SDK 认为是嵌套会话
|
|
20
21
|
function cleanEnv() {
|
|
@@ -92,6 +93,37 @@ export async function cmdRestartMonitor() {
|
|
|
92
93
|
await sleep(500);
|
|
93
94
|
}
|
|
94
95
|
}
|
|
96
|
+
// A stale instance record can disappear while the real Windows process is
|
|
97
|
+
// still alive. Refuse to create a second daemon when an unregistered process
|
|
98
|
+
// from this package is found; a duplicate would connect AUN and kick the
|
|
99
|
+
// existing session before failing on IPC/port ownership.
|
|
100
|
+
const runtimeOrphans = findOrphanProcesses().filter(o => {
|
|
101
|
+
if (o.confirmedTestDaemon)
|
|
102
|
+
return false;
|
|
103
|
+
const homeMatches = o.evolcoreHome && normalizePath(o.evolcoreHome) === normalizePath(p.root);
|
|
104
|
+
const packageMatches = !o.evolcoreHome && normalizePath(o.cmdline).includes(normalizePath(getPackageRoot()));
|
|
105
|
+
return Boolean(homeMatches || packageMatches);
|
|
106
|
+
});
|
|
107
|
+
if (runtimeOrphans.length > 0) {
|
|
108
|
+
const pids = runtimeOrphans.map(o => o.pid).join(', ');
|
|
109
|
+
log(`Stopping unregistered EvolCore process(es): ${pids}`);
|
|
110
|
+
for (const orphan of runtimeOrphans) {
|
|
111
|
+
platform.killProcess(orphan.pid, false);
|
|
112
|
+
}
|
|
113
|
+
const exited = await Promise.all(runtimeOrphans.map(async (orphan) => {
|
|
114
|
+
if (await platform.waitForProcessExit(orphan.pid, 10_000))
|
|
115
|
+
return true;
|
|
116
|
+
platform.killProcess(orphan.pid, true);
|
|
117
|
+
return platform.waitForProcessExit(orphan.pid, 5_000);
|
|
118
|
+
}));
|
|
119
|
+
if (exited.some(result => !result)) {
|
|
120
|
+
const message = `未登记的 EvolCore 进程仍未退出(PID: ${pids}),已停止重启以避免重复连接 AUN。`;
|
|
121
|
+
log(message);
|
|
122
|
+
await notifyDaemonOwners(p, `❌ ${message}`, log);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
cleanupInstances();
|
|
126
|
+
}
|
|
95
127
|
// restart-pending.json 只留给新主进程发送发起会话内的“重启成功”回执。
|
|
96
128
|
const pendingFile = path.join(daemonControlDir(), 'restart-pending.json');
|
|
97
129
|
// 等待所有活 main 进程退出(可能不止一个)
|
|
@@ -107,24 +139,24 @@ export async function cmdRestartMonitor() {
|
|
|
107
139
|
}
|
|
108
140
|
catch { }
|
|
109
141
|
}
|
|
110
|
-
await Promise.all(oldPids.map(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
142
|
+
const exited = await Promise.all(oldPids.map(async (oldPid) => {
|
|
143
|
+
if (await platform.waitForProcessExit(oldPid, 30_000)) {
|
|
144
|
+
log(`Process ${oldPid} has exited`);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
log(`ERROR: Process ${oldPid} still running after 30s, force killing`);
|
|
148
|
+
platform.killProcess(oldPid, true);
|
|
149
|
+
const forced = await platform.waitForProcessExit(oldPid, 5_000);
|
|
150
|
+
if (!forced)
|
|
151
|
+
log(`ERROR: Process ${oldPid} survived forced termination`);
|
|
152
|
+
return forced;
|
|
153
|
+
}));
|
|
154
|
+
if (exited.some(result => !result)) {
|
|
155
|
+
log('❌ Existing daemon process could not be stopped; refusing to spawn a replacement');
|
|
156
|
+
await notifyDaemonOwners(p, '❌ 旧 EvolCore 进程未能退出,已停止重启,避免重复连接 AUN。', log);
|
|
157
|
+
cleanupPendingFile(pendingFile, log);
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
128
160
|
await sleep(3000);
|
|
129
161
|
cleanupInstances();
|
|
130
162
|
}
|
|
@@ -177,9 +209,22 @@ export async function cmdRestartMonitor() {
|
|
|
177
209
|
await notifyDaemonOwners(p, `⚠️ ${WEB_PACKAGE_NAME} 升级失败,使用当前版本继续\n${ecwebUpgrade.error ?? ''}`.trim(), log);
|
|
178
210
|
break;
|
|
179
211
|
}
|
|
212
|
+
// source=ecweb 的 Service Proxy 只在 daemon 启动后的短窗口内发现实例。
|
|
213
|
+
// restart-monitor 直接 spawn daemon,必须在此先启动独立的 ec-web,不能绕过
|
|
214
|
+
// cmdStart 的 ECWeb 生命周期逻辑。
|
|
215
|
+
const ecwebStartedBeforeDaemon = serviceProxyNeedsEcweb(loadDaemonConfig())
|
|
216
|
+
? await startEcwebIfEnabled(p)
|
|
217
|
+
: false;
|
|
218
|
+
if (ecwebStartedBeforeDaemon)
|
|
219
|
+
log('ECWeb started before daemon');
|
|
180
220
|
// 启动并检测 ready signal
|
|
181
221
|
let started = await spawnAndWaitReady(p, log, READY_TIMEOUT);
|
|
182
222
|
if (started) {
|
|
223
|
+
// 没有 Service Proxy 时可以等 daemon ready 后再启动;有代理时已在上方启动,
|
|
224
|
+
// 避免重新拉起造成实例记录和反向隧道短暂失配。
|
|
225
|
+
if (!ecwebStartedBeforeDaemon && await startEcwebIfEnabled(p)) {
|
|
226
|
+
log('ECWeb started after daemon');
|
|
227
|
+
}
|
|
183
228
|
log('✓ Service restarted successfully');
|
|
184
229
|
archiveSelfHealLog(p, log);
|
|
185
230
|
// 发起会话内的“重启成功”通知由新进程自行发送,此处只负责失败/自愈告警。
|
|
@@ -192,10 +237,25 @@ export async function cmdRestartMonitor() {
|
|
|
192
237
|
cleanupPendingFile(pendingFile, log);
|
|
193
238
|
process.exit(1);
|
|
194
239
|
}
|
|
240
|
+
if (hasStartupResourceConflict(p)) {
|
|
241
|
+
const message = '服务启动失败:IPC 管道或端口仍被占用,已跳过自动修复,避免重复连接 AUN。';
|
|
242
|
+
log(message);
|
|
243
|
+
await notifyDaemonOwners(p, `❌ ${message}`, log);
|
|
244
|
+
cleanupPendingFile(pendingFile, log);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
195
247
|
// 启动失败,进入 self-heal 循环
|
|
196
248
|
log('❌ Service failed to start, entering self-heal loop');
|
|
197
249
|
eventBus.publish({ type: 'self-heal:started', reason: 'Service failed to start after restart' });
|
|
198
250
|
await notifyDaemonOwners(p, '⚠️ 服务启动失败,正在尝试自动修复...', log);
|
|
251
|
+
const claudePath = platform.resolveCommandPath('claude');
|
|
252
|
+
if (!claudePath) {
|
|
253
|
+
const message = '找不到 claude CLI,已停止自动修复;请检查后台进程 PATH。';
|
|
254
|
+
log(message);
|
|
255
|
+
await notifyDaemonOwners(p, `❌ ${message}`, log);
|
|
256
|
+
cleanupPendingFile(pendingFile, log);
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
199
259
|
for (let attempt = 1; attempt <= MAX_HEAL_ATTEMPTS; attempt++) {
|
|
200
260
|
// 前置检查:服务可能已被上一轮 claude 修复并启动
|
|
201
261
|
if (isServiceAlive()) {
|
|
@@ -209,7 +269,7 @@ export async function cmdRestartMonitor() {
|
|
|
209
269
|
log(`Self-heal attempt ${attempt}/${MAX_HEAL_ATTEMPTS}`);
|
|
210
270
|
eventBus.publish({ type: 'self-heal:attempt', attemptNumber: attempt, maxAttempts: MAX_HEAL_ATTEMPTS });
|
|
211
271
|
await notifyDaemonOwners(p, `🔧 自动修复中(第 ${attempt}/${MAX_HEAL_ATTEMPTS} 次)...`, log);
|
|
212
|
-
const healed = await invokeClaude(p, attempt, MAX_HEAL_ATTEMPTS, HEAL_TIMEOUT, log);
|
|
272
|
+
const healed = await invokeClaude(claudePath, p, attempt, MAX_HEAL_ATTEMPTS, HEAL_TIMEOUT, log);
|
|
213
273
|
// 后置检查:不管 invokeClaude 返回什么,都检查服务实际状态
|
|
214
274
|
if (isServiceAlive()) {
|
|
215
275
|
log(`✓ Service is running after attempt ${attempt}`);
|
|
@@ -276,6 +336,30 @@ async function sendHealSummary(p, attempts, log) {
|
|
|
276
336
|
function sleep(ms) {
|
|
277
337
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
278
338
|
}
|
|
339
|
+
function normalizePath(value) {
|
|
340
|
+
return value.replace(/[\\/]+/g, '/').toLowerCase();
|
|
341
|
+
}
|
|
342
|
+
function hasStartupResourceConflict(p) {
|
|
343
|
+
let daemonLog = null;
|
|
344
|
+
try {
|
|
345
|
+
daemonLog = fs.readdirSync(p.logs, { withFileTypes: true })
|
|
346
|
+
.filter(entry => entry.isFile() && /^daemon-.*\.log$/.test(entry.name))
|
|
347
|
+
.sort((a, b) => b.name.localeCompare(a.name))
|
|
348
|
+
.slice(0, 1)
|
|
349
|
+
.map(entry => path.join(p.logs, entry.name))[0] ?? null;
|
|
350
|
+
}
|
|
351
|
+
catch { }
|
|
352
|
+
const files = [path.join(p.logs, 'stdout.log'), ...(daemonLog ? [daemonLog] : [])];
|
|
353
|
+
return files.some(file => {
|
|
354
|
+
try {
|
|
355
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
356
|
+
return /EADDRINUSE|address already in use|pipe-.*already in use/i.test(content.slice(-64 * 1024));
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
279
363
|
function cleanupPendingFile(filePath, log) {
|
|
280
364
|
try {
|
|
281
365
|
if (fs.existsSync(filePath)) {
|
|
@@ -339,9 +423,17 @@ async function spawnAndWaitReady(p, log, timeout) {
|
|
|
339
423
|
}
|
|
340
424
|
}
|
|
341
425
|
log(`Ready signal not received within ${timeout / 1000}s`);
|
|
342
|
-
//
|
|
426
|
+
// A timeout is still a live-process race: do not let self-heal spawn a
|
|
427
|
+
// replacement while the timed-out daemon owns the IPC endpoint or a port.
|
|
343
428
|
if (platform.isProcessRunning(childPid)) {
|
|
344
429
|
platform.killProcess(childPid);
|
|
430
|
+
if (!await platform.waitForProcessExit(childPid, 5_000)) {
|
|
431
|
+
log(`Process ${childPid} survived graceful termination, forcing kill`);
|
|
432
|
+
platform.killProcess(childPid, true);
|
|
433
|
+
if (!await platform.waitForProcessExit(childPid, 5_000)) {
|
|
434
|
+
log(`ERROR: Process ${childPid} survived forced termination`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
345
437
|
}
|
|
346
438
|
cleanupInstances();
|
|
347
439
|
return false;
|
|
@@ -349,7 +441,7 @@ async function spawnAndWaitReady(p, log, timeout) {
|
|
|
349
441
|
/**
|
|
350
442
|
* 调用 claude CLI 进行自动修复
|
|
351
443
|
*/
|
|
352
|
-
async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
|
|
444
|
+
async function invokeClaude(claudePath, p, attempt, maxAttempts, timeout, log) {
|
|
353
445
|
const projectDir = getPackageRoot();
|
|
354
446
|
const selfHealLog = p.selfHealLog;
|
|
355
447
|
const stdoutLog = path.join(p.logs, 'stdout.log');
|
|
@@ -387,7 +479,7 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
|
|
|
387
479
|
注意:只修复导致启动失败的问题,不要做额外的重构或优化。`;
|
|
388
480
|
try {
|
|
389
481
|
log(`Invoking claude CLI (attempt ${attempt}, timeout ${timeout / 60000}min)...`);
|
|
390
|
-
const { stdout, stderr } = await execFileAsync(
|
|
482
|
+
const { stdout, stderr } = await execFileAsync(claudePath, [
|
|
391
483
|
'-p', prompt,
|
|
392
484
|
'--allowedTools', 'Read,Write,Edit,Bash,Glob,Grep',
|
|
393
485
|
'--output-format', 'text',
|
|
@@ -395,6 +487,8 @@ async function invokeClaude(p, attempt, maxAttempts, timeout, log) {
|
|
|
395
487
|
], {
|
|
396
488
|
cwd: projectDir,
|
|
397
489
|
timeout,
|
|
490
|
+
// Windows npm CLIs are .cmd shims; execFile needs a shell to run them.
|
|
491
|
+
shell: platform.isWindows,
|
|
398
492
|
env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: 'cli' },
|
|
399
493
|
maxBuffer: 10 * 1024 * 1024,
|
|
400
494
|
});
|
|
@@ -17,6 +17,7 @@ import { resolvePaths, agentConfig as agentConfigPath, agentContactConfig, agent
|
|
|
17
17
|
import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
|
|
18
18
|
import { fileCache } from '../core/daemon-file-cache.js';
|
|
19
19
|
import { isValidAid } from '../aun/aid/validation.js';
|
|
20
|
+
import { normalizeAidDomain } from '../aun/aid/domain.js';
|
|
20
21
|
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
21
22
|
import { parseContactAlias } from './contact-alias.js';
|
|
22
23
|
import { loadSchema, listSchemaVersions, currentVersion, isSchemaName, } from './schema-registry.js';
|
|
@@ -571,6 +572,20 @@ function normalizeAgentConfigForWrite(value) {
|
|
|
571
572
|
}
|
|
572
573
|
return mutable;
|
|
573
574
|
}
|
|
575
|
+
function normalizeProcessConfigForWrite(value) {
|
|
576
|
+
const configuredAidDomain = value.aun?.defaultAidDomain;
|
|
577
|
+
if (configuredAidDomain === undefined)
|
|
578
|
+
return value;
|
|
579
|
+
try {
|
|
580
|
+
return {
|
|
581
|
+
...value,
|
|
582
|
+
aun: { ...value.aun, defaultAidDomain: normalizeAidDomain(configuredAidDomain, 'aun.defaultAidDomain') },
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
throw new ConfigError('VALIDATION_ERROR', error instanceof Error ? error.message : String(error));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
574
589
|
function normalizeRelationConfigForWrite(value) {
|
|
575
590
|
const mutable = { ...value };
|
|
576
591
|
normalizeShowActivitiesCompat(mutable);
|
|
@@ -643,7 +658,9 @@ export function write(target, value, sel, opts = {}) {
|
|
|
643
658
|
? normalizeAgentConfigForWrite(migrated)
|
|
644
659
|
: target === ConfigTarget.Relation
|
|
645
660
|
? normalizeRelationConfigForWrite(migrated)
|
|
646
|
-
:
|
|
661
|
+
: target === ConfigTarget.Process
|
|
662
|
+
? normalizeProcessConfigForWrite(migrated)
|
|
663
|
+
: migrated;
|
|
647
664
|
// 1. Schema 校验
|
|
648
665
|
if (!opts.skipValidate) {
|
|
649
666
|
validateOrThrow(schema, normalized, target);
|
|
@@ -836,7 +853,7 @@ function ensureSchemaVersion(value, version) {
|
|
|
836
853
|
}
|
|
837
854
|
function validateOrThrow(schema, value, target) {
|
|
838
855
|
const ok = schema.validate(value);
|
|
839
|
-
const scopeErrors = validateProcessOnlyFields(target, value);
|
|
856
|
+
const scopeErrors = [...validateProcessOnlyFields(target, value), ...validateProcessAidDomain(target, value)];
|
|
840
857
|
if (!ok || scopeErrors.length > 0) {
|
|
841
858
|
const errs = [
|
|
842
859
|
...scopeErrors,
|
|
@@ -857,6 +874,20 @@ function validateProcessOnlyFields(target, value) {
|
|
|
857
874
|
return [];
|
|
858
875
|
return ['/debug is only allowed in process config'];
|
|
859
876
|
}
|
|
877
|
+
function validateProcessAidDomain(target, value) {
|
|
878
|
+
if (target !== ConfigTarget.Process || !value || typeof value !== 'object')
|
|
879
|
+
return [];
|
|
880
|
+
const configuredAidDomain = value?.aun?.defaultAidDomain;
|
|
881
|
+
if (configuredAidDomain === undefined)
|
|
882
|
+
return [];
|
|
883
|
+
try {
|
|
884
|
+
normalizeAidDomain(configuredAidDomain, 'aun.defaultAidDomain');
|
|
885
|
+
return [];
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
return [`/aun/defaultAidDomain ${error instanceof Error ? error.message : String(error)}`];
|
|
889
|
+
}
|
|
890
|
+
}
|
|
860
891
|
/**
|
|
861
892
|
* responseModeParams 桶专项校验。
|
|
862
893
|
*
|
|
@@ -908,7 +939,7 @@ export function validateConfig(target, value) {
|
|
|
908
939
|
: loadSchema(TARGET_SCHEMA[target]);
|
|
909
940
|
const withVer = ensureSchemaVersion(value, schema.version);
|
|
910
941
|
const ok = schema.validate(withVer);
|
|
911
|
-
const errors = validateProcessOnlyFields(target, withVer);
|
|
942
|
+
const errors = [...validateProcessOnlyFields(target, withVer), ...validateProcessAidDomain(target, withVer)];
|
|
912
943
|
if (ok && errors.length === 0)
|
|
913
944
|
return [];
|
|
914
945
|
return [
|
|
@@ -29,6 +29,47 @@ const DISPLAY_NAMES = {
|
|
|
29
29
|
};
|
|
30
30
|
// ── 掩码 ──
|
|
31
31
|
const ENV_PREFIX = '$ENV:';
|
|
32
|
+
// Claude Code supports both credential variable names. Keep AUTH_TOKEN first
|
|
33
|
+
// to match the runtime resolver's precedence.
|
|
34
|
+
const standardEnvKeys = {
|
|
35
|
+
apiKey: ['ANTHROPIC_AUTH_TOKEN'],
|
|
36
|
+
baseUrl: ['ANTHROPIC_BASE_URL'],
|
|
37
|
+
};
|
|
38
|
+
function envKeysForGateway(type, field) {
|
|
39
|
+
if (type === 'claude' && field === 'apiKey')
|
|
40
|
+
return ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY'];
|
|
41
|
+
return standardEnvKeys[field];
|
|
42
|
+
}
|
|
43
|
+
function firstEnvValue(env, names) {
|
|
44
|
+
for (const name of names) {
|
|
45
|
+
const value = env[name];
|
|
46
|
+
if (value)
|
|
47
|
+
return { name, value };
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const DEBUG_SECRET_KEY = /(?:key|token|secret|password|authorization|credential|private)/i;
|
|
52
|
+
function redactDebugValue(value, key) {
|
|
53
|
+
if (typeof value === 'string') {
|
|
54
|
+
if (value === '(未设置)')
|
|
55
|
+
return value;
|
|
56
|
+
if (key && DEBUG_SECRET_KEY.test(key) && !value.startsWith(ENV_PREFIX))
|
|
57
|
+
return '***';
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(value))
|
|
61
|
+
return value.map(item => redactDebugValue(item));
|
|
62
|
+
if (value && typeof value === 'object') {
|
|
63
|
+
return Object.fromEntries(Object.entries(value)
|
|
64
|
+
.map(([childKey, childValue]) => [childKey, redactDebugValue(childValue, childKey)]));
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function redactMismatchValue(value, field) {
|
|
69
|
+
if (!value)
|
|
70
|
+
return value;
|
|
71
|
+
return field === 'apiKey' && !value.startsWith(ENV_PREFIX) ? '***' : value;
|
|
72
|
+
}
|
|
32
73
|
function maskApiKey(raw) {
|
|
33
74
|
if (typeof raw !== 'string' || !raw)
|
|
34
75
|
return { mask: undefined, isEnvRef: false };
|
|
@@ -203,21 +244,17 @@ function detectEnvMismatch(defaults, aids) {
|
|
|
203
244
|
// 读取全局 .env 文件
|
|
204
245
|
const rootEnvPath = path.join(resolvePaths().root, '.env');
|
|
205
246
|
const rootEnvVars = parseEnvFileSync(rootEnvPath);
|
|
206
|
-
// 标准环境变量映射
|
|
207
|
-
const standardEnvKeys = {
|
|
208
|
-
apiKey: 'ANTHROPIC_AUTH_TOKEN',
|
|
209
|
-
baseUrl: 'ANTHROPIC_BASE_URL',
|
|
210
|
-
};
|
|
211
247
|
// 记录进程环境变量
|
|
212
|
-
const processEnvDebug = {
|
|
248
|
+
const processEnvDebug = redactDebugValue({
|
|
213
249
|
ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || '(未设置)',
|
|
214
250
|
ANTHROPIC_AUTH_TOKEN: process.env.ANTHROPIC_AUTH_TOKEN || '(未设置)',
|
|
215
|
-
|
|
251
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || '(未设置)',
|
|
252
|
+
});
|
|
216
253
|
// 收集调试信息返回给前端
|
|
217
254
|
result.debug = {
|
|
218
255
|
processEnv: processEnvDebug,
|
|
219
|
-
rootEnvFile: rootEnvVars,
|
|
220
|
-
defaultsConfig: defaultsBa,
|
|
256
|
+
rootEnvFile: redactDebugValue(rootEnvVars),
|
|
257
|
+
defaultsConfig: redactDebugValue(defaultsBa),
|
|
221
258
|
};
|
|
222
259
|
// 检测全局默认配置中的环境变量
|
|
223
260
|
for (const type of GATEWAY_TYPES) {
|
|
@@ -243,23 +280,25 @@ function detectEnvMismatch(defaults, aids) {
|
|
|
243
280
|
}
|
|
244
281
|
}
|
|
245
282
|
// 情况2:配置值是实际值(非 $ENV 引用),检查是否应该使用环境变量
|
|
246
|
-
else if (typeof val === 'string' && val &&
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
const
|
|
283
|
+
else if (typeof val === 'string' && val && envKeysForGateway(type, field)) {
|
|
284
|
+
const envKeys = envKeysForGateway(type, field);
|
|
285
|
+
const envValue = firstEnvValue(process.env, envKeys);
|
|
286
|
+
const fileValue = firstEnvValue(rootEnvVars, envKeys);
|
|
287
|
+
const envVarName = envValue?.name;
|
|
288
|
+
const processValue = envValue?.value;
|
|
250
289
|
// 如果进程环境中有值,但配置中的值与进程环境不一致
|
|
251
|
-
if (processValue && val !== processValue) {
|
|
290
|
+
if (processValue && val !== processValue && envVarName) {
|
|
252
291
|
// 同时检查 .env 文件:如果 .env 也没有这个值,说明需要同步
|
|
253
|
-
if (!fileValue || fileValue !== processValue) {
|
|
292
|
+
if (!fileValue || fileValue.value !== processValue) {
|
|
254
293
|
result.hasMismatch = true;
|
|
255
294
|
result.mismatches.push({
|
|
256
295
|
aid: 'defaults',
|
|
257
296
|
type,
|
|
258
297
|
field,
|
|
259
|
-
envValue: fileValue || '(.env 中未设置)',
|
|
260
|
-
configValue: val,
|
|
298
|
+
envValue: redactMismatchValue(fileValue?.value, field) || '(.env 中未设置)',
|
|
299
|
+
configValue: redactMismatchValue(val, field) || val,
|
|
261
300
|
envVarName,
|
|
262
|
-
processValue,
|
|
301
|
+
processValue: redactMismatchValue(processValue, field),
|
|
263
302
|
});
|
|
264
303
|
}
|
|
265
304
|
}
|
|
@@ -295,18 +334,20 @@ function detectEnvMismatch(defaults, aids) {
|
|
|
295
334
|
}
|
|
296
335
|
}
|
|
297
336
|
// 情况2:配置值是实际值
|
|
298
|
-
else if (typeof val === 'string' && val &&
|
|
299
|
-
const
|
|
300
|
-
const
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
337
|
+
else if (typeof val === 'string' && val && envKeysForGateway(type, field)) {
|
|
338
|
+
const envKeys = envKeysForGateway(type, field);
|
|
339
|
+
const envValue = firstEnvValue(process.env, envKeys);
|
|
340
|
+
const fileValue = firstEnvValue(agentEnvVars, envKeys);
|
|
341
|
+
const envVarName = envValue?.name;
|
|
342
|
+
const processValue = envValue?.value;
|
|
343
|
+
if (processValue && val !== processValue && envVarName) {
|
|
344
|
+
if (!fileValue || fileValue.value !== processValue) {
|
|
304
345
|
result.hasMismatch = true;
|
|
305
346
|
result.mismatches.push({
|
|
306
347
|
aid,
|
|
307
348
|
type,
|
|
308
349
|
field,
|
|
309
|
-
envValue: fileValue || '(.env 中未设置)',
|
|
350
|
+
envValue: redactMismatchValue(fileValue?.value, field) || '(.env 中未设置)',
|
|
310
351
|
configValue: `配置值与环境变量 ${envVarName} 不一致`,
|
|
311
352
|
});
|
|
312
353
|
}
|
|
@@ -707,11 +748,13 @@ export async function gatewaySyncEnv(args) {
|
|
|
707
748
|
}
|
|
708
749
|
// 情况2:实际值型,但与进程环境变量不一致
|
|
709
750
|
else {
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
751
|
+
const envKeys = envKeysForGateway(type, field);
|
|
752
|
+
const envValue = envKeys
|
|
753
|
+
? firstEnvValue(process.env, envKeys)
|
|
754
|
+
: undefined;
|
|
755
|
+
if (envValue) {
|
|
756
|
+
if (val !== envValue.value) {
|
|
757
|
+
block[field] = envValue.value;
|
|
715
758
|
hasChanges = true;
|
|
716
759
|
}
|
|
717
760
|
}
|
|
@@ -751,11 +794,13 @@ export async function gatewaySyncEnv(args) {
|
|
|
751
794
|
}
|
|
752
795
|
// 情况2:实际值型
|
|
753
796
|
else {
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
797
|
+
const envKeys = envKeysForGateway(type, field);
|
|
798
|
+
const envValue = envKeys
|
|
799
|
+
? firstEnvValue(process.env, envKeys)
|
|
800
|
+
: undefined;
|
|
801
|
+
if (envValue) {
|
|
802
|
+
if (val !== envValue.value) {
|
|
803
|
+
block[field] = envValue.value;
|
|
759
804
|
hasChanges = true;
|
|
760
805
|
}
|
|
761
806
|
}
|
package/dist/config-store.js
CHANGED
|
@@ -20,6 +20,7 @@ import path from 'path';
|
|
|
20
20
|
import { resolvePaths, agentDir, } from './paths.js';
|
|
21
21
|
import { atomicReadJson, atomicWriteJson } from './utils/atomic-write.js';
|
|
22
22
|
import { checkAgentDir, isValidAid } from './aun/aid/validation.js';
|
|
23
|
+
import { normalizeAidDomain } from './aun/aid/domain.js';
|
|
23
24
|
import { isValidChannelName } from './core/channel-loader.js';
|
|
24
25
|
import { CONFIG_SCHEMA_VERSION } from './types.js';
|
|
25
26
|
import { ConfigTarget, read as cfgRead, write as cfgWrite } from './config/config-manager.js';
|
|
@@ -29,7 +30,7 @@ import { parseStableSemver } from './utils/stable-semver.js';
|
|
|
29
30
|
/** 读 {root}/daemon.json。文件不存在返回 {},不报错。 */
|
|
30
31
|
export function loadDaemonConfig() {
|
|
31
32
|
const raw = atomicReadJson(resolvePaths().daemonConfig);
|
|
32
|
-
return validateDaemonConfig(raw ?? {});
|
|
33
|
+
return validateDaemonConfig(raw ?? {}, false);
|
|
33
34
|
}
|
|
34
35
|
let eckSnapshotsConfigCache;
|
|
35
36
|
/** Parse the process-level snapshot gate once for the current daemon lifecycle. */
|
|
@@ -50,9 +51,16 @@ export function isEckSnapshotsEnabled() {
|
|
|
50
51
|
}
|
|
51
52
|
/** 原子写入 {root}/daemon.json。调用方负责传完整对象(含要保留的字段)。 */
|
|
52
53
|
export function saveDaemonConfig(value) {
|
|
53
|
-
|
|
54
|
+
const configPath = resolvePaths().daemonConfig;
|
|
55
|
+
const current = atomicReadJson(configPath);
|
|
56
|
+
// A legacy default is irrelevant while an existing control AID is retained.
|
|
57
|
+
// Validate the field when it is created or changed, but do not make unrelated
|
|
58
|
+
// daemon.json updates fail because of a persisted, unused legacy value.
|
|
59
|
+
const validateAidDomain = current === null
|
|
60
|
+
|| current.aun?.defaultAidDomain !== value.aun?.defaultAidDomain;
|
|
61
|
+
atomicWriteJson(configPath, validateDaemonConfig(value, validateAidDomain));
|
|
54
62
|
}
|
|
55
|
-
function validateDaemonConfig(value) {
|
|
63
|
+
function validateDaemonConfig(value, validateAidDomain) {
|
|
56
64
|
const minEvolVersion = value.aun?.minEvolVersion;
|
|
57
65
|
if (minEvolVersion !== undefined && !parseStableSemver(minEvolVersion)) {
|
|
58
66
|
throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
|
|
@@ -61,6 +69,10 @@ function validateDaemonConfig(value) {
|
|
|
61
69
|
if (menuTokenRequired !== undefined && typeof menuTokenRequired !== 'boolean') {
|
|
62
70
|
throw new Error('daemon.json.aun.menuTokenRequired must be a boolean');
|
|
63
71
|
}
|
|
72
|
+
if (validateAidDomain && value.aun?.defaultAidDomain !== undefined) {
|
|
73
|
+
const defaultAidDomain = normalizeAidDomain(value.aun.defaultAidDomain, 'daemon.json.aun.defaultAidDomain');
|
|
74
|
+
return { ...value, aun: { ...value.aun, defaultAidDomain } };
|
|
75
|
+
}
|
|
64
76
|
return value;
|
|
65
77
|
}
|
|
66
78
|
const SUPPORTED_CHANNEL_TYPES = new Set([
|
|
@@ -20,16 +20,16 @@ export class AgentLoader {
|
|
|
20
20
|
* Iterate over all EvolAgents in the registry × all registered plugins.
|
|
21
21
|
* Each successful (agent, plugin) pair yields one runner instance.
|
|
22
22
|
*/
|
|
23
|
-
createAll(registry, callbacks) {
|
|
23
|
+
createAll(registry, callbacks, creationErrors) {
|
|
24
24
|
const instances = [];
|
|
25
25
|
const allAgents = registry.runnableAgents();
|
|
26
26
|
for (const agent of allAgents) {
|
|
27
|
-
instances.push(...this.createForAgent(agent, callbacks));
|
|
27
|
+
instances.push(...this.createForAgent(agent, callbacks, creationErrors));
|
|
28
28
|
}
|
|
29
29
|
return instances;
|
|
30
30
|
}
|
|
31
31
|
/** Create runners for one EvolAgent, used by runtime hot-load. */
|
|
32
|
-
createForAgent(agent, callbacks) {
|
|
32
|
+
createForAgent(agent, callbacks, creationErrors) {
|
|
33
33
|
const instances = [];
|
|
34
34
|
for (const [pluginName, plugin] of this.plugins) {
|
|
35
35
|
if (!plugin.isEnabled(agent)) {
|
|
@@ -46,6 +46,8 @@ export class AgentLoader {
|
|
|
46
46
|
logger.info(`✓ Runner created: agent=${instance.evolagentName} baseagent=${instance.baseagent}`);
|
|
47
47
|
}
|
|
48
48
|
catch (error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
creationErrors?.push({ evolagentName: agent.name, baseagent: pluginName, message });
|
|
49
51
|
logger.error(`✗ Failed to create runner for agent='${agent.name}' baseagent='${pluginName}':`, error);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { execCodexCliSync } from '../../../utils/codex-cli.js';
|
|
5
5
|
function listDirectories(dir) {
|
|
6
6
|
try {
|
|
7
7
|
if (!fs.existsSync(dir))
|
|
@@ -34,7 +34,7 @@ function discoverSkills(ctx) {
|
|
|
34
34
|
}
|
|
35
35
|
function runCodexJson(args) {
|
|
36
36
|
try {
|
|
37
|
-
const output =
|
|
37
|
+
const output = execCodexCliSync(args, {
|
|
38
38
|
encoding: 'utf-8',
|
|
39
39
|
timeout: 3000,
|
|
40
40
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -61,6 +61,8 @@ export class ChannelLoader {
|
|
|
61
61
|
aid: agent.aid,
|
|
62
62
|
owner: agent.getOwner(aunEffName),
|
|
63
63
|
enabled: true,
|
|
64
|
+
keystorePath: agent.config.aun?.keystorePath,
|
|
65
|
+
gatewayUrl: agent.config.aun?.gatewayUrl,
|
|
64
66
|
};
|
|
65
67
|
const configInsts = [aunInst];
|
|
66
68
|
for (const inst of agent.config.channels) {
|
|
@@ -240,7 +242,14 @@ export function buildReloadHooks(deps) {
|
|
|
240
242
|
const isImplicitAun = channelName === aunEffName;
|
|
241
243
|
// Find config instance: implicit AUN gets a synthetic entry; others scan channels[].
|
|
242
244
|
const cfgInst = isImplicitAun
|
|
243
|
-
? {
|
|
245
|
+
? {
|
|
246
|
+
type: 'aun',
|
|
247
|
+
name: aunEffName,
|
|
248
|
+
aid,
|
|
249
|
+
enabled: true,
|
|
250
|
+
keystorePath: agent.config?.aun?.keystorePath,
|
|
251
|
+
gatewayUrl: agent.config?.aun?.gatewayUrl,
|
|
252
|
+
}
|
|
244
253
|
: (() => {
|
|
245
254
|
const agentChannels = agent.config?.channels ?? [];
|
|
246
255
|
return agentChannels.find((i) => {
|
|
@@ -4,6 +4,7 @@ import { modelMatches } from '../model/model-catalog.js';
|
|
|
4
4
|
import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
|
|
5
5
|
import { hasModelSwitcher } from '../../agents/runner-types.js';
|
|
6
6
|
import { getCodexEfforts } from '../../agents/codex-runner.js';
|
|
7
|
+
import { execCodexCliSync, resolveCodexCliPath } from '../../utils/codex-cli.js';
|
|
7
8
|
import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
|
|
8
9
|
import { buildEnvelope } from '../message/message-utils.js';
|
|
9
10
|
import path from 'path';
|
|
@@ -45,15 +46,25 @@ import { groupMenuAuthorizationArgs, groupMenuIntent, handleGroupMenu, } from '.
|
|
|
45
46
|
*/
|
|
46
47
|
function getBaseagentVersion(cmd) {
|
|
47
48
|
try {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
const executable = cmd === 'codex' ? resolveCodexCliPath() : cmd;
|
|
50
|
+
if (!executable)
|
|
51
|
+
return null;
|
|
52
|
+
const output = cmd === 'codex'
|
|
53
|
+
? execCodexCliSync(['--version'], {
|
|
54
|
+
encoding: 'utf-8',
|
|
55
|
+
timeout: 3000,
|
|
56
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
57
|
+
})
|
|
58
|
+
: execFileSync(executable, ['--version'], {
|
|
59
|
+
encoding: 'utf-8',
|
|
60
|
+
timeout: 3000,
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
62
|
+
});
|
|
63
|
+
const versionOutput = String(output).trim();
|
|
53
64
|
// claude: "2.1.187 (Claude Code)" → 提取 "2.1.187"
|
|
54
65
|
// gemini: "0.38.0" → 直接返回
|
|
55
66
|
// codex: "codex-cli 0.142.0" → 提取 "0.142.0"
|
|
56
|
-
const match =
|
|
67
|
+
const match = versionOutput.match(/(\d+\.\d+\.\d+)/);
|
|
57
68
|
return match ? match[1] : null;
|
|
58
69
|
}
|
|
59
70
|
catch {
|