evolcore 0.0.23 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/README.md +3 -1
- package/bin/codex-managed-acl.ps1 +7 -2
- package/bin/codex-managed-hook.mjs +127 -10
- package/bin/ec-safe-output.js +85 -3
- package/bin/install-codex-managed-hooks.mjs +17 -6
- package/dist/agents/baseagent-availability.js +4 -4
- package/dist/agents/baseagent.js +29 -1
- package/dist/agents/claude-runner.js +148 -13
- package/dist/agents/codex/app-server-client.js +18 -8
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +215 -0
- package/dist/agents/codex-runner.js +438 -59
- package/dist/agents/ecagent-runner.js +17 -3
- package/dist/agents/gemini-runner.js +60 -2
- package/dist/agents/runner-types.js +1 -1
- package/dist/aun/aid/managed-operation.js +28 -6
- package/dist/aun/aid/store.js +41 -0
- package/dist/aun/msg/managed-operation.js +6 -1
- package/dist/aun/outbox.js +56 -18
- package/dist/aun/rpc/client.js +4 -5
- package/dist/channels/aun.js +147 -76
- package/dist/channels/daemon.js +7 -1
- package/dist/channels/dingtalk.js +1 -0
- package/dist/channels/feishu.js +10 -4
- package/dist/channels/wecom.js +2 -1
- package/dist/cli/agent-command.js +29 -3
- package/dist/cli/agent-create.js +538 -0
- package/dist/cli/agent.js +26 -675
- package/dist/cli/aun-commands.js +60 -20
- package/dist/cli/ctl-command.js +2 -0
- package/dist/cli/daemon-commands.js +148 -17
- package/dist/cli/index.js +2 -2
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/restart-monitor.js +432 -8
- package/dist/cli/stats.js +110 -0
- package/dist/config/builtin-roles.js +2 -0
- package/dist/core/auth/auth-gateway.js +3 -2
- package/dist/core/auth/authorization-audit.js +72 -19
- package/dist/core/auth/operation-authorizer.js +63 -3
- package/dist/core/auth/operation-catalog.js +10 -2
- package/dist/core/auth/remote-access-store.js +3 -2
- package/dist/core/baseagent-loader.js +4 -4
- package/dist/core/bootstrap-messages.js +39 -18
- package/dist/core/bootstrap-service.js +32 -26
- package/dist/core/capability/capability-manager.js +8 -4
- package/dist/core/capability/providers/codex-capability-provider.js +1 -1
- package/dist/core/command/agent-control.js +7 -2
- package/dist/core/command/command-handler.js +87 -25
- package/dist/core/command/menu-catalog.js +3 -1
- package/dist/core/command/menu-handler.js +274 -70
- package/dist/core/command/menu-protocol.js +6 -2
- package/dist/core/command/slash-gate.js +1 -3
- package/dist/core/command/slash-handler.js +157 -48
- package/dist/core/data-migration.js +5 -15
- package/dist/core/event-catalog.js +3 -0
- package/dist/core/handoff/runtime.js +3 -1
- package/dist/core/handoff/store.js +7 -1
- package/dist/core/message/im-renderer.js +13 -6
- package/dist/core/message/message-bridge.js +5 -36
- package/dist/core/message/message-utils.js +6 -3
- package/dist/core/message/response-engine.js +73 -21
- package/dist/core/model/catalog-service.js +3 -1
- package/dist/core/model/model-catalog.js +10 -0
- package/dist/core/model/model-permission.js +4 -1
- package/dist/core/permission/approval-gateway.js +169 -33
- package/dist/core/permission/tool-policy.js +13 -6
- package/dist/core/session/adapters/codex-session-file-adapter.js +71 -3
- package/dist/core/session/session-manager.js +49 -13
- package/dist/core/session/session-renew.js +1 -0
- package/dist/index.js +174 -28
- package/dist/ipc.js +68 -21
- package/dist/paths.js +23 -1
- package/dist/stats/billing.js +18 -4
- package/dist/utils/atomic-write.js +26 -2
- package/dist/utils/cross-platform.js +436 -1
- package/dist/utils/error-utils.js +11 -4
- package/dist/utils/model-prices.jsonl +23 -20
- package/dist/utils/npm-ops.js +24 -1
- package/dist/utils/windows-autostart.js +1 -1
- package/kits/templates/roles/admin.json +2 -0
- package/package.json +7 -3
- package/AGENTS.md +0 -62
- package/dist/utils/codex-cli.js +0 -43
package/dist/cli/aun-commands.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
|
5
5
|
import { getArgValue, isHelpFlag, wantsHelp } from './help.js';
|
|
6
6
|
import { ipcQuery } from '../ipc.js';
|
|
7
7
|
import { AGENT_DELEGATION_TOKEN_ENV } from '../core/auth/agent-delegation.js';
|
|
8
|
+
import { isManagementRole } from '../config/builtin-roles.js';
|
|
8
9
|
import { readBestTaskRuntimeContext } from './task-context.js';
|
|
9
10
|
// ==================== AID ====================
|
|
10
11
|
function resolveAunPath(args) {
|
|
@@ -13,24 +14,35 @@ function resolveAunPath(args) {
|
|
|
13
14
|
return args[idx + 1];
|
|
14
15
|
return process.env.AUN_HOME || undefined;
|
|
15
16
|
}
|
|
17
|
+
function isRedactedAidShowResult(info) {
|
|
18
|
+
return 'redacted' in info && info.redacted === true;
|
|
19
|
+
}
|
|
16
20
|
function renderAidShow(info, formatJson) {
|
|
17
21
|
if (formatJson) {
|
|
18
22
|
console.log(JSON.stringify(info, null, 2));
|
|
19
23
|
return;
|
|
20
24
|
}
|
|
21
25
|
console.log(`AID: ${info.aid}`);
|
|
22
|
-
|
|
26
|
+
const redacted = isRedactedAidShowResult(info);
|
|
27
|
+
if (!redacted) {
|
|
28
|
+
console.log(` 私钥: ${info.hasPrivateKey ? '有' : '无'}`);
|
|
29
|
+
}
|
|
23
30
|
console.log(` agent.md: ${info.hasAgentMd ? '有' : '无'}`);
|
|
24
31
|
if (info.hasAgentMd) {
|
|
32
|
+
const signatureReason = redacted ? undefined : info.agentMdSignatureReason;
|
|
25
33
|
const sigLabel = info.agentMdSignature === 'verified' ? '✓ 已验签'
|
|
26
34
|
: info.agentMdSignature === 'unsigned' ? '⚠ 未签名'
|
|
27
|
-
: info.agentMdSignature === 'invalid' ? `✗ 签名无效${
|
|
35
|
+
: info.agentMdSignature === 'invalid' ? `✗ 签名无效${signatureReason ? ': ' + signatureReason : ''}`
|
|
28
36
|
: '? 未知';
|
|
29
37
|
console.log(` 签名状态: ${sigLabel}`);
|
|
30
38
|
}
|
|
31
39
|
console.log(` 证书到期: ${info.certExpiresAt ?? '无证书'}${info.certExpired ? ' (已过期!)' : ''}`);
|
|
32
40
|
if (info.certSubject)
|
|
33
41
|
console.log(` 证书主体: ${info.certSubject}`);
|
|
42
|
+
if (redacted) {
|
|
43
|
+
console.log(' 本地密钥状态: 已脱敏');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
34
46
|
if (info.keyMatchesCert === false)
|
|
35
47
|
console.log(' 密钥/证书: ✗ 公钥不匹配(cert.pem 与 key.json 公钥不一致)');
|
|
36
48
|
else if (info.keyMatchesCert === true)
|
|
@@ -104,7 +116,8 @@ async function runManagedAid(args, formatJson) {
|
|
|
104
116
|
if (!parsed.ok)
|
|
105
117
|
failManagedAid(formatJson, parsed.code, parsed.reason);
|
|
106
118
|
const runtime = await readBestTaskRuntimeContext();
|
|
107
|
-
|
|
119
|
+
const requiresSelfAid = parsed.command.kind === 'show' || parsed.command.kind === 'agentmd.put';
|
|
120
|
+
if (requiresSelfAid && runtime?.selfAid && parsed.command.aid !== runtime.selfAid.replace(/^@/, '')) {
|
|
108
121
|
failManagedAid(formatJson, 'SCOPE_MISMATCH', `托管任务只能操作当前 self AID:${runtime.selfAid}`);
|
|
109
122
|
}
|
|
110
123
|
const response = await ipcQuery(resolvePaths().socket, {
|
|
@@ -212,7 +225,12 @@ async function runManagedMsg(args, formatJson) {
|
|
|
212
225
|
if (runtime && (runtime.channel !== 'aun' || runtime.chatType !== 'private')) {
|
|
213
226
|
failManagedGroup(formatJson, 'SCOPE_MISMATCH', 'ec msg history/online 只能读取当前 AUN 私聊关系');
|
|
214
227
|
}
|
|
215
|
-
|
|
228
|
+
// owner/admin 可以跨对端读历史(每次查询仍只针对一个对端)。这里只是快速
|
|
229
|
+
// 预检:角色未知时放行交给 daemon 判定,daemon 侧 handleMsgOperation 才是
|
|
230
|
+
// 权威关卡,member 由 currentRelationOnly 约束、visitor 直接拒绝。
|
|
231
|
+
const mayReadCrossPeer = parsed.command.kind === 'history'
|
|
232
|
+
&& (runtime?.peerRole === undefined || isManagementRole(runtime.peerRole));
|
|
233
|
+
if (!mayReadCrossPeer && runtime?.channelId && parsed.command.target !== runtime.channelId.replace(/^@/, '')) {
|
|
216
234
|
failManagedGroup(formatJson, 'SCOPE_MISMATCH', `只能读取当前 AUN 私聊对端:${runtime.channelId}`);
|
|
217
235
|
}
|
|
218
236
|
}
|
|
@@ -1053,7 +1071,7 @@ Commands:
|
|
|
1053
1071
|
send <from> <to> --file <path> [--as <type>] 发送文件(image|video|voice|file)
|
|
1054
1072
|
send <from> <to> --link <url> [--title T] 发送链接卡片
|
|
1055
1073
|
send <from> <to> --payload <json> 发送自定义 payload
|
|
1056
|
-
pull <from> [--after-seq N] [--limit N]
|
|
1074
|
+
pull <from> [--after-seq N] [--limit N] --app <name> 拉取收件箱(必须使用独立 slot)
|
|
1057
1075
|
ack <from> <seq> [--app <name>] 确认已读
|
|
1058
1076
|
recall <from> <message-id> [<message-id>...] 撤回消息
|
|
1059
1077
|
online <from> <target-aid> [<target-aid>...] 查询在线状态
|
|
@@ -1142,6 +1160,20 @@ Options:
|
|
|
1142
1160
|
}
|
|
1143
1161
|
const { msgSend, msgPull, msgAck, msgRecall, msgOnline, msgHistory } = await import('../aun/msg/index.js');
|
|
1144
1162
|
const commonOpts = { aunPath, slotId: appSlot };
|
|
1163
|
+
const requireConsumerSlot = (operation) => {
|
|
1164
|
+
if (appSlot)
|
|
1165
|
+
return appSlot;
|
|
1166
|
+
const error = {
|
|
1167
|
+
ok: false,
|
|
1168
|
+
code: 'APP_SLOT_REQUIRED',
|
|
1169
|
+
error: `${operation} requires an explicit --app <name> isolated consumer slot; refusing to access the daemon inbox`,
|
|
1170
|
+
};
|
|
1171
|
+
if (formatJson)
|
|
1172
|
+
console.log(JSON.stringify(error));
|
|
1173
|
+
else
|
|
1174
|
+
console.error(`❌ ${error.error}`);
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
};
|
|
1145
1177
|
if (sub === 'history') {
|
|
1146
1178
|
const target = args[2] && !args[2].startsWith('--') ? args[2] : undefined;
|
|
1147
1179
|
const historyValueFlags = new Set([
|
|
@@ -1389,9 +1421,7 @@ Options:
|
|
|
1389
1421
|
return;
|
|
1390
1422
|
}
|
|
1391
1423
|
if (sub === 'pull') {
|
|
1392
|
-
|
|
1393
|
-
console.warn('⚠ 警告: 未传 --app,当前与 daemon 共享 evolcore 消费通道。pull 会看到/影响 daemon 的消息消费;如需独立消费请用 --app <name>');
|
|
1394
|
-
}
|
|
1424
|
+
const consumerSlot = requireConsumerSlot('ec msg pull');
|
|
1395
1425
|
const afterSeqStr = getArgValue(args, '--after-seq');
|
|
1396
1426
|
const limitStr = getArgValue(args, '--limit');
|
|
1397
1427
|
const afterSeq = afterSeqStr !== undefined ? Number(afterSeqStr) : undefined;
|
|
@@ -1404,7 +1434,7 @@ Options:
|
|
|
1404
1434
|
console.error(`❌ --limit 必须是数字: ${limitStr}`);
|
|
1405
1435
|
process.exit(1);
|
|
1406
1436
|
}
|
|
1407
|
-
const result = await msgPull({ from, afterSeq, limit,
|
|
1437
|
+
const result = await msgPull({ from, afterSeq, limit, aunPath, slotId: consumerSlot });
|
|
1408
1438
|
if (!result.ok) {
|
|
1409
1439
|
if (formatJson) {
|
|
1410
1440
|
console.log(JSON.stringify(result));
|
|
@@ -1440,10 +1470,8 @@ Options:
|
|
|
1440
1470
|
console.error(`❌ seq 必须是数字: ${seqStr}`);
|
|
1441
1471
|
process.exit(1);
|
|
1442
1472
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
}
|
|
1446
|
-
const result = await msgAck({ from, seq, ...commonOpts });
|
|
1473
|
+
const consumerSlot = requireConsumerSlot('ec msg ack');
|
|
1474
|
+
const result = await msgAck({ from, seq, aunPath, slotId: consumerSlot });
|
|
1447
1475
|
if (!result.ok) {
|
|
1448
1476
|
if (formatJson) {
|
|
1449
1477
|
console.log(JSON.stringify(result));
|
|
@@ -1546,7 +1574,7 @@ export async function cmdGroup(args) {
|
|
|
1546
1574
|
send <from> <group-id> <text> --return <none|required> 发送群文本(跨会话时 --return 必填)
|
|
1547
1575
|
send <from> <group-id> --file <path> [--as <type>] 发送群文件
|
|
1548
1576
|
send <from> <group-id> --payload <json> 发送自定义 payload
|
|
1549
|
-
pull <from> <group-id> [--after-seq N] [--limit N]
|
|
1577
|
+
pull <from> <group-id> [--after-seq N] [--limit N] --app <name> 拉取群消息(必须使用独立 slot)
|
|
1550
1578
|
ack <from> <group-id> <seq> [--app <name>] 确认已读
|
|
1551
1579
|
|
|
1552
1580
|
群管理:
|
|
@@ -1877,12 +1905,18 @@ Options:
|
|
|
1877
1905
|
}
|
|
1878
1906
|
if (sub === 'pull') {
|
|
1879
1907
|
const groupId = requireGroupId();
|
|
1880
|
-
|
|
1881
|
-
|
|
1908
|
+
const consumerSlot = appSlot;
|
|
1909
|
+
if (!consumerSlot) {
|
|
1910
|
+
const error = 'ec group pull requires an explicit --app <name> isolated consumer slot; refusing to access the daemon inbox';
|
|
1911
|
+
if (formatJson)
|
|
1912
|
+
console.log(JSON.stringify({ ok: false, code: 'APP_SLOT_REQUIRED', error }));
|
|
1913
|
+
else
|
|
1914
|
+
console.error(`❌ ${error}`);
|
|
1915
|
+
process.exit(1);
|
|
1882
1916
|
}
|
|
1883
1917
|
const afterSeq = parseNumberFlag('--after-seq', '--after-seq');
|
|
1884
1918
|
const limit = parseNumberFlag('--limit', '--limit');
|
|
1885
|
-
const result = await groupPull({ from, groupId, afterSeq, limit,
|
|
1919
|
+
const result = await groupPull({ from, groupId, afterSeq, limit, aunPath, slotId: consumerSlot });
|
|
1886
1920
|
outputResult(result, () => {
|
|
1887
1921
|
const r = result;
|
|
1888
1922
|
console.log(`✓ ${r.messages.length} 条消息,latest_seq=${r.latest_message_seq}${r.has_more ? '(还有更多)' : ''}`);
|
|
@@ -1905,10 +1939,16 @@ Options:
|
|
|
1905
1939
|
console.error(`❌ seq 必须是数字: ${seqStr}`);
|
|
1906
1940
|
process.exit(1);
|
|
1907
1941
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1942
|
+
const consumerSlot = appSlot;
|
|
1943
|
+
if (!consumerSlot) {
|
|
1944
|
+
const error = 'ec group ack requires an explicit --app <name> isolated consumer slot; refusing to access the daemon inbox';
|
|
1945
|
+
if (formatJson)
|
|
1946
|
+
console.log(JSON.stringify({ ok: false, code: 'APP_SLOT_REQUIRED', error }));
|
|
1947
|
+
else
|
|
1948
|
+
console.error(`❌ ${error}`);
|
|
1949
|
+
process.exit(1);
|
|
1910
1950
|
}
|
|
1911
|
-
const result = await groupAck({ from, groupId, seq,
|
|
1951
|
+
const result = await groupAck({ from, groupId, seq, aunPath, slotId: consumerSlot });
|
|
1912
1952
|
outputResult(result, () => {
|
|
1913
1953
|
const r = result;
|
|
1914
1954
|
console.log(`✓ ack_seq=${r.ack_seq}`);
|
package/dist/cli/ctl-command.js
CHANGED
|
@@ -7,6 +7,7 @@ const CTL_HELP = `用法: ec ctl <command> [args...]
|
|
|
7
7
|
|
|
8
8
|
查询:
|
|
9
9
|
status 查看会话状态
|
|
10
|
+
status --format json 查看当前任务的结构化运行上下文
|
|
10
11
|
pwd 显示当前项目路径
|
|
11
12
|
help 显示帮助
|
|
12
13
|
|
|
@@ -20,6 +21,7 @@ const CTL_HELP = `用法: ec ctl <command> [args...]
|
|
|
20
21
|
|
|
21
22
|
示例:
|
|
22
23
|
ec ctl status
|
|
24
|
+
ec ctl status --format json
|
|
23
25
|
ec ctl name "新会话名称"
|
|
24
26
|
ec ctl send "任务已完成"
|
|
25
27
|
ec ctl file report.md
|
|
@@ -30,8 +30,50 @@ import { inspectDataMigrationRequirement } from '../core/data-migration.js';
|
|
|
30
30
|
import { isInitCancelledError, printInitCancelHint } from './init-cancel.js';
|
|
31
31
|
import { listCodexAppServerProcesses, unregisterCodexAppServerProcess } from '../agents/codex/app-server-registry.js';
|
|
32
32
|
import { DEFAULT_ECWEB_PORT, ecwebService, setEcwebEnabled } from '../config/daemon-services.js';
|
|
33
|
-
import { checkCodexProjectPath, inspectCodexManagedHook } from '../agents/codex/diagnostics.js';
|
|
33
|
+
import { checkCodexProjectPath, inspectCodexManagedHook, ensureCodexManagedHook, runCodexHookLaunchDiagnostic, } from '../agents/codex/diagnostics.js';
|
|
34
34
|
const execFileAsync = promisify(execFile);
|
|
35
|
+
/** Run before launch, and before restart stops any existing daemon. */
|
|
36
|
+
export function ensureCodexHookBeforeLifecycle(onRepaired) {
|
|
37
|
+
if (!platform.isWindows)
|
|
38
|
+
return true;
|
|
39
|
+
try {
|
|
40
|
+
const usesCodex = loadDefaults()?.active_baseagent === 'codex'
|
|
41
|
+
|| loadAllAgents().agents.some(agent => {
|
|
42
|
+
try {
|
|
43
|
+
return resolveEffective({ self: agent.aid }).active_baseagent === 'codex';
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.warn(`⚠ Agent ${agent.aid} 配置解析失败:${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
if (!usesCodex)
|
|
51
|
+
return true;
|
|
52
|
+
const result = ensureCodexManagedHook({
|
|
53
|
+
packageRoot: getPackageRoot(),
|
|
54
|
+
onInstallRequired: () => console.log('🔧 Codex 运行组件缺失,正在自动安装;Windows 提示管理员授权时请选择“是”。'),
|
|
55
|
+
});
|
|
56
|
+
if (result.status === 'installed') {
|
|
57
|
+
if (result.repaired) {
|
|
58
|
+
console.log('✅ Codex Windows managed hook 已自动安装并验证');
|
|
59
|
+
onRepaired?.();
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
console.error(`❌ Codex Windows managed hook 未就绪:${result.error || result.reason || result.status}`);
|
|
64
|
+
if (result.status === 'missing') {
|
|
65
|
+
console.error(' 本次操作已停止。下次启动或重启会再次自动尝试安装,需要允许 Windows 管理员授权。');
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.error(' 本次操作已停止。现有 Codex 策略或文件访问异常,需要管理员核对;自动安装不会覆盖异常策略。');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
console.error(`❌ Codex 启动检查失败:${error instanceof Error ? error.message : String(error)}`);
|
|
73
|
+
}
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
35
77
|
async function runAutomaticInit(action) {
|
|
36
78
|
try {
|
|
37
79
|
return await action();
|
|
@@ -66,8 +108,8 @@ function printNoSelfAgentHints(options) {
|
|
|
66
108
|
// taskkill, even though the daemon is healthy and able to shut down gracefully.
|
|
67
109
|
const DAEMON_PROBE_TIMEOUT_MS = platform.isWindows ? 5_000 : 1_000;
|
|
68
110
|
export const DAEMON_SHUTDOWN_TIMEOUT_MS = platform.isWindows ? 10_000 : 3_000;
|
|
69
|
-
async function probeDaemon(socketPath, timeoutMs = DAEMON_PROBE_TIMEOUT_MS) {
|
|
70
|
-
const response = await ipcQuery(socketPath, { type: 'ping' }, timeoutMs);
|
|
111
|
+
async function probeDaemon(socketPath, timeoutMs = DAEMON_PROBE_TIMEOUT_MS, throwOnError = false) {
|
|
112
|
+
const response = await ipcQuery(socketPath, { type: 'ping' }, timeoutMs, { throwOnError });
|
|
71
113
|
if (response?.pong !== true || !Number.isInteger(response.pid) || response.pid <= 0)
|
|
72
114
|
return null;
|
|
73
115
|
return response;
|
|
@@ -205,19 +247,39 @@ function runSourceBuild() {
|
|
|
205
247
|
});
|
|
206
248
|
});
|
|
207
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* Spawn the restart monitor and confirm it actually reached its own code.
|
|
252
|
+
*
|
|
253
|
+
* The daemon is already stopped at this point, so a monitor that dies on
|
|
254
|
+
* startup leaves nothing running and nobody watching — the exact silent
|
|
255
|
+
* failure this mechanism exists to prevent.
|
|
256
|
+
*/
|
|
257
|
+
async function spawnRestartMonitorAndConfirm(monitorEntry, reportLine) {
|
|
258
|
+
const result = await platform.launchMonitorAndConfirm([...monitorEntry.args, 'restart-monitor'], { EVOLCORE_HOME: resolvePaths().root, ...(monitorEntry.env ?? {}) }, pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid === pid), pid => scanInstances().restartMonitors.some(m => m.alive && m.record.pid !== pid));
|
|
259
|
+
if (result.ok)
|
|
260
|
+
return { ok: true };
|
|
261
|
+
for (const line of (result.stderrTail ?? '').split(/\r?\n/).filter(Boolean).slice(-8)) {
|
|
262
|
+
reportLine(` | ${line}`);
|
|
263
|
+
}
|
|
264
|
+
return { ok: false, reason: result.reason ?? 'unknown' };
|
|
265
|
+
}
|
|
208
266
|
async function requireFreshSourceBuild() {
|
|
209
267
|
if (!sourceBuildNeedsRefresh())
|
|
210
268
|
return true;
|
|
211
269
|
return await runSourceBuild();
|
|
212
270
|
}
|
|
213
|
-
function requireCompletedDataMigration(root) {
|
|
271
|
+
function requireCompletedDataMigration(root, action = 'start') {
|
|
214
272
|
const requirement = inspectDataMigrationRequirement(root);
|
|
215
273
|
if (!requirement.required)
|
|
216
274
|
return true;
|
|
217
|
-
console.error(
|
|
275
|
+
console.error(action === 'restart'
|
|
276
|
+
? `❌ 检测到 ${requirement.operationCount} 项待确认的用户数据迁移,本次不停止当前服务。`
|
|
277
|
+
: `❌ 检测到 ${requirement.operationCount} 项待确认的用户数据迁移,服务未启动。`);
|
|
218
278
|
console.error(' 先执行: ec data migrate --dry-run');
|
|
219
279
|
console.error(' 确认后: ec data migrate --apply');
|
|
220
280
|
console.error(' 迁移成功并校验后,再执行 ec start 或 ec restart。');
|
|
281
|
+
if (action === 'restart')
|
|
282
|
+
console.error(' 本次 ec restart 仅完成只读检查,未停止当前服务。');
|
|
221
283
|
process.exitCode = 2;
|
|
222
284
|
return false;
|
|
223
285
|
}
|
|
@@ -386,6 +448,14 @@ export async function cmdStart(opts = {}) {
|
|
|
386
448
|
const p = resolvePaths();
|
|
387
449
|
const existingDaemon = await probeDaemon(p.socket);
|
|
388
450
|
if (existingDaemon) {
|
|
451
|
+
let hookRepaired = false;
|
|
452
|
+
if (!ensureCodexHookBeforeLifecycle(() => { hookRepaired = true; }))
|
|
453
|
+
return;
|
|
454
|
+
if (hookRepaired) {
|
|
455
|
+
console.log('🔄 Codex 运行组件已补齐,正在自动重启服务以加载策略...');
|
|
456
|
+
await cmdRestart({ diagnose: opts.diagnose });
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
389
459
|
console.log(` EvolCore is already running (PID: ${existingDaemon.pid}, IPC reachable)`);
|
|
390
460
|
console.log(` 已在运行;已运行时长: ${resolveRunningDuration(existingDaemon.pid, existingDaemon.uptime)};部署/脚本可直接复用当前实例`);
|
|
391
461
|
return;
|
|
@@ -424,6 +494,11 @@ export async function cmdStart(opts = {}) {
|
|
|
424
494
|
console.log('\n初始化完成,开始启动 EvolCore ....');
|
|
425
495
|
}
|
|
426
496
|
}
|
|
497
|
+
// The initialization path can create the first Codex agent, so perform the
|
|
498
|
+
// machine-hook check after config migration/auto-init and before any daemon
|
|
499
|
+
// process is launched.
|
|
500
|
+
if (!ensureCodexHookBeforeLifecycle())
|
|
501
|
+
return;
|
|
427
502
|
// 控制 AID 门禁:缺 aid 且交互式 → 只补全控制 AID + owners(不重走 baseagent 向导)。
|
|
428
503
|
// 非 TTY(restart-monitor/systemd/管道)不补全(无法交互),只提示后继续启动,daemon 侧 warn 兜底。
|
|
429
504
|
const daemonCfgStart = loadDaemonConfig();
|
|
@@ -758,6 +833,8 @@ export async function cmdStop() {
|
|
|
758
833
|
export async function cmdRestart(opts = {}) {
|
|
759
834
|
const cmdStartedAt = Date.now();
|
|
760
835
|
console.log('🔄 Restarting EvolCore...');
|
|
836
|
+
if (!ensureCodexHookBeforeLifecycle())
|
|
837
|
+
return;
|
|
761
838
|
const initialStatus = scanInstances();
|
|
762
839
|
const initialOrphans = findOrphanProcesses();
|
|
763
840
|
const initialRuntimeOrphans = initialOrphans.filter(orphan => isRuntimeOrphan(orphan));
|
|
@@ -769,12 +846,12 @@ export async function cmdRestart(opts = {}) {
|
|
|
769
846
|
if (initialRuntimeOrphans.length === 0 && rejectPidIsolatedLifecycle('restart', initialStatus, ping))
|
|
770
847
|
return;
|
|
771
848
|
}
|
|
772
|
-
|
|
773
|
-
|
|
849
|
+
// 构建交给 restart-monitor:它在 daemon 停止之后才编译,dist/ 不会被活进程
|
|
850
|
+
// 占用,失败还能回滚到上一份产物。这里再编译一次会落在事务保护之外。
|
|
774
851
|
// Keep restart behavior identical to startup: both paths use one upgrade
|
|
775
852
|
// check implementation and continue when an upgrade is unavailable.
|
|
776
853
|
await runUpgradeChecks();
|
|
777
|
-
if (!requireCompletedDataMigration(resolveRoot()))
|
|
854
|
+
if (!requireCompletedDataMigration(resolveRoot(), 'restart'))
|
|
778
855
|
return;
|
|
779
856
|
// 停止所有活 main 进程(可能不止一个)
|
|
780
857
|
const status = scanInstances();
|
|
@@ -854,11 +931,30 @@ export async function cmdRestart(opts = {}) {
|
|
|
854
931
|
}
|
|
855
932
|
}
|
|
856
933
|
console.log(`⏱ restart prep done in ${((Date.now() - cmdStartedAt) / 1000).toFixed(1)}s, starting...`);
|
|
857
|
-
// 通过 restart-monitor 启动,避免在 agent
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
934
|
+
// 通过 restart-monitor 启动,避免在 agent 内调用时自杀。
|
|
935
|
+
// 开发仓下 monitor 从源码跑;安装包下先复制到 stage 目录,两种情况都让看门狗
|
|
936
|
+
// 不住在会被替换的产物里。
|
|
937
|
+
const monitorEntry = platform.resolveMonitorLaunch(getPackageRoot(), path.join(resolvePaths().root, 'data', 'monitor-stage'));
|
|
938
|
+
if (monitorEntry.note)
|
|
939
|
+
console.log(`ℹ ${monitorEntry.note}`);
|
|
940
|
+
if (!fs.existsSync(monitorEntry.entry)) {
|
|
941
|
+
console.error(`❌ 重启中止:找不到 restart-monitor 入口 ${monitorEntry.entry}`);
|
|
942
|
+
console.error(' 请先执行 npm run build 重建产物');
|
|
943
|
+
process.exitCode = 1;
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
const monitorStarted = await spawnRestartMonitorAndConfirm(monitorEntry, log => console.error(log));
|
|
947
|
+
if (!monitorStarted.ok) {
|
|
948
|
+
if (monitorEntry.stageDir)
|
|
949
|
+
platform.cleanupMonitorStage(monitorEntry.stageDir);
|
|
950
|
+
console.error(`❌ 重启中止:restart-monitor 未能启动(${monitorStarted.reason})`);
|
|
951
|
+
if (fs.existsSync(path.join(getPackageRoot(), 'src', 'cli', 'index.ts'))) {
|
|
952
|
+
console.error(' 源码可能存在语法错误导致 tsx 无法加载。请执行 npx tsc --noEmit 检查。');
|
|
953
|
+
}
|
|
954
|
+
console.error(' 服务已停止,请修复后执行 ec start。');
|
|
955
|
+
process.exitCode = 1;
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
862
958
|
console.log('🔄 Restart monitor started, waiting for service to come back online...');
|
|
863
959
|
await sleep(2000);
|
|
864
960
|
// 代码统计(开发环境)
|
|
@@ -1032,7 +1128,11 @@ export async function cmdStatus() {
|
|
|
1032
1128
|
// Start all read-only daemon queries together. On a busy Windows host each
|
|
1033
1129
|
// query can otherwise wait behind the same IPC/event-loop delay and make the
|
|
1034
1130
|
// total `ec status` time the sum of several individual timeouts.
|
|
1035
|
-
|
|
1131
|
+
let ipcFailure;
|
|
1132
|
+
const pingPromise = probeDaemon(p.socket, DAEMON_PROBE_TIMEOUT_MS, true).catch(error => {
|
|
1133
|
+
ipcFailure = error;
|
|
1134
|
+
return null;
|
|
1135
|
+
});
|
|
1036
1136
|
const statusPromise = ipcQuery(p.socket, { type: 'status' }).catch(() => null);
|
|
1037
1137
|
const aidsPromise = ipcQuery(p.socket, { type: 'aun-aids' }).catch(() => null);
|
|
1038
1138
|
const agentsPromise = ipcQuery(p.socket, { type: 'evolagent.list' }).catch(() => null);
|
|
@@ -1049,6 +1149,11 @@ export async function cmdStatus() {
|
|
|
1049
1149
|
}
|
|
1050
1150
|
if (pid) {
|
|
1051
1151
|
printStartupInfo({ pid, running: true });
|
|
1152
|
+
if (!ping) {
|
|
1153
|
+
console.log('⚠ 仅确认 daemon PID 存活,IPC 不可用,不能据此判断服务健康。');
|
|
1154
|
+
if (ipcFailure)
|
|
1155
|
+
console.log(` ${ipcFailure instanceof Error ? ipcFailure.message : String(ipcFailure)}`);
|
|
1156
|
+
}
|
|
1052
1157
|
console.log('');
|
|
1053
1158
|
console.log('📊 Process Info:');
|
|
1054
1159
|
if (processVisible) {
|
|
@@ -1159,7 +1264,9 @@ export async function cmdStatus() {
|
|
|
1159
1264
|
}
|
|
1160
1265
|
}
|
|
1161
1266
|
else {
|
|
1162
|
-
console.log('⚠ EvolCore is not running');
|
|
1267
|
+
console.log(ipcFailure ? '⚠ 无法确认 EvolCore 运行状态:IPC 不可用,且没有可见的存活 PID。' : '⚠ EvolCore is not running');
|
|
1268
|
+
if (ipcFailure)
|
|
1269
|
+
console.log(` ${ipcFailure instanceof Error ? ipcFailure.message : String(ipcFailure)}`);
|
|
1163
1270
|
}
|
|
1164
1271
|
// Session & Project statistics (从文件系统读)
|
|
1165
1272
|
if (fs.existsSync(p.sessionsDir)) {
|
|
@@ -2905,7 +3012,12 @@ async function cmdWatchWeb() {
|
|
|
2905
3012
|
const cfg = loadDaemonConfig();
|
|
2906
3013
|
const currentService = ecwebService(cfg.services);
|
|
2907
3014
|
const port = currentService?.port ?? DEFAULT_ECWEB_PORT;
|
|
2908
|
-
|
|
3015
|
+
// Avoid rewriting daemon.json on every invocation once ECWeb is already
|
|
3016
|
+
// enabled on the requested port. The persisted config is the daemon's
|
|
3017
|
+
// lifecycle source of truth, but an unchanged desired state needs no write.
|
|
3018
|
+
if (currentService?.enabled !== true || currentService.port !== port) {
|
|
3019
|
+
saveDaemonConfig({ ...cfg, services: setEcwebEnabled(cfg.services, true, port) });
|
|
3020
|
+
}
|
|
2909
3021
|
const ok = await startEcwebIfEnabled(p, { forceRestart: true });
|
|
2910
3022
|
if (!ok)
|
|
2911
3023
|
process.exit(1); // 失败原因已由 startEcwebIfEnabled 打印
|
|
@@ -2944,9 +3056,10 @@ export async function cmdMv(oldDir, newDir) {
|
|
|
2944
3056
|
}
|
|
2945
3057
|
}
|
|
2946
3058
|
// ==================== Diagnose ====================
|
|
2947
|
-
export async function cmdDiagnose() {
|
|
3059
|
+
export async function cmdDiagnose(args = []) {
|
|
2948
3060
|
const p = resolvePaths();
|
|
2949
3061
|
let hasError = false;
|
|
3062
|
+
const runHookLaunchProbe = args.includes('--codex-hook');
|
|
2950
3063
|
// 1. 检查数据目录
|
|
2951
3064
|
console.log(`[diagnose] EVOLCORE_HOME = ${p.root}`);
|
|
2952
3065
|
if (!fs.existsSync(p.root)) {
|
|
@@ -3010,6 +3123,24 @@ export async function cmdDiagnose() {
|
|
|
3010
3123
|
const hook = inspectCodexManagedHook({ packageRoot: getPackageRoot() });
|
|
3011
3124
|
if (hook.status === 'installed') {
|
|
3012
3125
|
console.log(`[diagnose] ✓ Codex managed hook 已安装: ${hook.hookPath}`);
|
|
3126
|
+
if (runHookLaunchProbe) {
|
|
3127
|
+
try {
|
|
3128
|
+
const launch = runCodexHookLaunchDiagnostic({ packageRoot: getPackageRoot() });
|
|
3129
|
+
if (launch.status === 'ok') {
|
|
3130
|
+
console.log('[diagnose] ✓ Codex managed hook 启动探针报告: ' + launch.output);
|
|
3131
|
+
for (const result of launch.results) {
|
|
3132
|
+
console.log('[diagnose] ' + result.label + ': exit=' + result.exitCode + ' pid=' + (result.pid ?? 'unknown'));
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
else {
|
|
3136
|
+
console.log('[diagnose] - Codex managed hook 启动探针: 跳过(' + (launch.reason || 'not applicable') + ')');
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
3139
|
+
catch (error) {
|
|
3140
|
+
console.error('[diagnose] ❌ Codex managed hook 启动探针失败: ' + (error instanceof Error ? error.message : String(error)));
|
|
3141
|
+
hasError = true;
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3013
3144
|
}
|
|
3014
3145
|
else if (hook.status === 'skipped') {
|
|
3015
3146
|
console.log(`[diagnose] - Codex managed hook: 跳过(${hook.reason})`);
|
package/dist/cli/index.js
CHANGED
|
@@ -80,7 +80,7 @@ function printLifecycleHelp(command) {
|
|
|
80
80
|
logs: `用法: ec logs [选项]\n\n --level <error|warn|info|debug> 最低日志级别\n --module <name> 只显示指定模块\n --raw 原始输出\n --no-color 禁用颜色`,
|
|
81
81
|
watch: `用法: ec watch [logs|log|aid|msg|web|session]\n\n ec watch 打开监控选择菜单\n ec watch logs 选择日志类型后监控\n ec watch log <类型...> 直接监控指定日志类型\n ec watch aid 监控 AID 连接\n ec watch msg 打开消息 TUI\n ec watch web 安装/升级并启动 EC Web\n ec watch session ec watch web 的兼容别名`,
|
|
82
82
|
'restart-monitor': '用法: ec restart-monitor\n\n运行内 daemon 重启与升级监控器。',
|
|
83
|
-
diagnose: '用法: ec diagnose\n\n
|
|
83
|
+
diagnose: '用法: ec diagnose [--codex-hook]\n\n检查配置、数据库、进程和编译产物。\n\n --codex-hook 运行 Windows Codex managed hook 启动探针。',
|
|
84
84
|
mv: '用法: ec mv <old-directory> <new-directory>\n\n迁移项目目录以及 Claude、Codex 和 EvolCore 会话路径。',
|
|
85
85
|
};
|
|
86
86
|
console.log(help[command]);
|
|
@@ -309,7 +309,7 @@ export async function main(args) {
|
|
|
309
309
|
printLifecycleHelp('diagnose');
|
|
310
310
|
break;
|
|
311
311
|
}
|
|
312
|
-
await cmdDiagnose();
|
|
312
|
+
await cmdDiagnose(args.slice(1));
|
|
313
313
|
break;
|
|
314
314
|
case 'data': {
|
|
315
315
|
const { cmdData } = await import('./data-command.js');
|
|
@@ -12,6 +12,9 @@ const MANAGED_TASK_COMMANDS = new Set([
|
|
|
12
12
|
'msg',
|
|
13
13
|
'queue',
|
|
14
14
|
'response',
|
|
15
|
+
// `stats` reads the usage database directly. It authorizes each managed call
|
|
16
|
+
// against the task's relation role in `resolveManagedStatsScope`.
|
|
17
|
+
'stats',
|
|
15
18
|
'status',
|
|
16
19
|
'storage',
|
|
17
20
|
'trigger',
|