evolcore 0.0.28 → 0.0.29
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 +12 -0
- package/bin/ec-safe-output.js +270 -54
- package/dist/agents/baseagent-availability.js +37 -1
- package/dist/agents/baseagent.js +19 -9
- package/dist/agents/claude-runner.js +171 -13
- package/dist/agents/codex/app-server-client.js +6 -2
- package/dist/agents/codex/diagnostics.js +4 -3
- package/dist/agents/codex-runner.js +396 -95
- package/dist/agents/gemini-runner.js +35 -17
- package/dist/aun/group/history.js +130 -90
- package/dist/aun/group/operations.js +1 -3
- package/dist/aun/menu/transport.js +5 -3
- package/dist/aun/msg/managed-operation.js +70 -0
- package/dist/aun/msg/p2p.js +1 -3
- package/dist/aun/msg/upload.js +2 -2
- package/dist/aun/outbox.js +1 -37
- package/dist/channels/aun.js +249 -89
- package/dist/channels/daemon.js +18 -1
- package/dist/channels/feishu.js +1 -2
- package/dist/cli/agent-command.js +23 -3
- package/dist/cli/agent-create.js +68 -2
- package/dist/cli/agent.js +12 -2
- package/dist/cli/aun-commands.js +87 -13
- package/dist/cli/cli-argv.js +26 -0
- package/dist/cli/daemon-commands.js +40 -10
- package/dist/cli/data-command.js +4 -4
- package/dist/cli/handoff-command.js +2 -0
- package/dist/cli/index.js +18 -1
- package/dist/cli/managed-command-guard.js +10 -0
- package/dist/cli/memory-command.js +441 -0
- package/dist/cli/restart-monitor.js +62 -6
- package/dist/cli/skill-command.js +41 -0
- package/dist/cli/task-context.js +2 -23
- package/dist/cli/trigger-command.js +9 -1
- package/dist/cli/watch-msg-diag.js +209 -0
- package/dist/config/config-field-policy.js +52 -3
- package/dist/config/config-manager.js +17 -4
- package/dist/config/contact-book-store.js +2 -2
- package/dist/config/mention-mode.js +7 -2
- package/dist/config/merge.js +28 -9
- package/dist/config/resolved-config-op.js +1 -1
- package/dist/config/runtime-env.js +137 -0
- package/dist/config/schema-registry.js +11 -1
- package/dist/config-store.js +10 -10
- package/dist/core/audit/event-key.js +3 -1
- package/dist/core/audit/log-integrity.js +4 -0
- package/dist/core/auth/agent-delegation.js +2 -1
- package/dist/core/auth/auth-gateway.js +13 -6
- package/dist/core/auth/authorization-audit.js +54 -19
- package/dist/core/auth/operation-authorizer.js +17 -0
- package/dist/core/auth/operation-catalog.js +9 -0
- package/dist/core/causation/context.js +5 -1
- package/dist/core/command/agent-control.js +1 -2
- package/dist/core/command/command-handler.js +51 -23
- package/dist/core/command/memory-slash.js +270 -0
- package/dist/core/command/menu-catalog.js +1 -0
- package/dist/core/command/menu-handler.js +315 -60
- package/dist/core/command/menu-protocol.js +42 -4
- package/dist/core/command/slash-gate.js +10 -10
- package/dist/core/command/slash-handler.js +206 -64
- package/dist/core/data-migration.js +282 -505
- package/dist/core/event-catalog.js +26 -0
- package/dist/core/evolagent-registry.js +6 -0
- package/dist/core/evolagent.js +0 -10
- package/dist/core/handoff/store.js +1 -20
- package/dist/core/inference/text-inference.js +57 -1
- package/dist/core/memory/consolidation-context.js +505 -0
- package/dist/core/memory/consolidation-ops-records.js +124 -0
- package/dist/core/memory/consolidation-trigger.js +394 -0
- package/dist/core/memory/index-manager.js +308 -0
- package/dist/core/memory/launch-consolidation.js +395 -0
- package/dist/core/memory/memory-admin.js +234 -0
- package/dist/core/memory/memory-document.js +100 -0
- package/dist/core/memory/memory-session-policy.js +41 -0
- package/dist/core/memory/memory-signature.js +257 -0
- package/dist/core/memory/memory-writer.js +432 -0
- package/dist/core/memory/staleness.js +117 -0
- package/dist/core/message/auto-mention-gate.js +454 -0
- package/dist/core/message/im-renderer.js +69 -16
- package/dist/core/message/message-bridge.js +287 -21
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +114 -27
- package/dist/core/message/pending-hints.js +329 -161
- package/dist/core/message/response-engine.js +567 -80
- package/dist/core/permission/ec-command-parser.js +1029 -80
- package/dist/core/permission/protected-paths.js +376 -126
- package/dist/core/permission/sandbox-runtime.js +40 -12
- package/dist/core/permission/tool-error-code.js +96 -13
- package/dist/core/permission/tool-policy.js +276 -23
- package/dist/core/role/runtime-policy.js +3 -7
- package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
- package/dist/core/session/session-fs-store.js +0 -76
- package/dist/core/session/session-idle-monitor.js +355 -0
- package/dist/core/session/session-manager.js +478 -17
- package/dist/core/session/session-mapper.js +33 -16
- package/dist/core/session/session-renew.js +217 -155
- package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
- package/dist/core/skill/global-watcher.js +282 -0
- package/dist/core/skill/index-generator.js +84 -0
- package/dist/core/skill/startup.js +73 -0
- package/dist/core/skill/switch-resolver.js +115 -0
- package/dist/core/task/task-activity-store.js +219 -0
- package/dist/eck/kit-renderer.js +30 -3
- package/dist/eck/manifest-engine.js +26 -4
- package/dist/index.js +708 -18
- package/dist/ipc.js +564 -2
- package/dist/paths.js +30 -9
- package/dist/response-system/engines/v1/proactive-flow.js +116 -15
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/feedback.js +1 -0
- package/dist/trigger/manager.js +0 -10
- package/dist/trigger/scheduler.js +11 -0
- package/dist/trigger/validation.js +22 -0
- package/dist/utils/avatar-upload.js +59 -18
- package/dist/utils/error-dict.json +6 -3
- package/dist/utils/error-utils.js +27 -0
- package/dist/utils/logger.js +32 -10
- package/dist/utils/media-cache.js +84 -0
- package/dist/utils/msg-diag.js +97 -0
- package/dist/utils/welcome.js +37 -8
- package/kits/docs/AUTHORING.md +314 -0
- package/kits/docs/GUIDE.md +2 -0
- package/kits/docs/INDEX.md +2 -0
- package/kits/docs/aun/CHEATSHEET.md +4 -0
- package/kits/docs/aun/SDK.md +60 -0
- package/kits/docs/context-assembly.md +10 -6
- package/kits/docs/evolcore/INDEX.md +17 -3
- package/kits/docs/evolcore/agent-create.md +24 -0
- package/kits/docs/evolcore/agent.md +5 -2
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/ec-capabilities.md +364 -0
- package/kits/docs/evolcore/group.md +34 -18
- package/kits/docs/evolcore/memory-architecture.md +78 -0
- package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
- package/kits/docs/evolcore/memory-consolidation.md +263 -0
- package/kits/docs/evolcore/memory-tags.md +63 -0
- package/kits/docs/evolcore/memory.md +110 -0
- package/kits/docs/evolcore/menu-client.md +27 -11
- package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
- package/kits/docs/evolcore/menu-protocol.md +213 -0
- package/kits/docs/evolcore/msg.md +2 -0
- package/kits/docs/evolcore/trigger.md +7 -5
- package/kits/docs/path-registry.md +6 -8
- package/kits/docs/prompt-loading-architecture.md +5 -3
- package/kits/docs/venues/group.md +5 -4
- package/kits/eck_manifest.json +294 -88
- package/kits/eck_manifest.memory.json +257 -0
- package/kits/rules/03-identity.md +8 -8
- package/kits/rules/05-venue.md +4 -2
- package/kits/rules/06-channel.md +1 -1
- package/kits/schemas/_meta.json +276 -47
- package/kits/schemas/agent-config.schema.12.json +38 -2
- package/kits/schemas/agent-config.schema.13.json +591 -0
- package/kits/schemas/daemon.schema.6.json +9 -9
- package/kits/schemas/defaults.schema.5.json +29 -0
- package/kits/schemas/relation-config.schema.8.json +14 -1
- package/kits/schemas/relation-config.schema.9.json +129 -0
- package/kits/schemas/role-config.schema.1.json +3 -3
- package/kits/skills/.switches.json +3 -0
- package/kits/skills/ec-skill/SKILL.md +217 -0
- package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
- package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
- package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
- package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
- package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
- package/kits/templates/decision/auto-mention.md +25 -0
- package/kits/templates/decision/session-renew.md +22 -0
- package/kits/templates/memory-consolidation/forget.md +104 -0
- package/kits/templates/memory-consolidation/generalize.md +76 -0
- package/kits/templates/memory-consolidation/notes.md +133 -0
- package/kits/templates/memory-consolidation/profile.md +58 -0
- package/kits/templates/memory-consolidation/promote.md +72 -0
- package/kits/templates/memory-consolidation/workflow.md +75 -0
- package/kits/templates/system-fragments/bootstrap.md +14 -5
- package/kits/templates/system-fragments/commands.md +1 -0
- package/kits/templates/system-fragments/group-history.md +6 -0
- package/kits/templates/system-fragments/identity.md +0 -3
- package/kits/templates/system-fragments/memory.md +79 -0
- package/kits/templates/system-fragments/reply.md +1 -0
- package/kits/templates/system-fragments/venue.md +1 -1
- package/package.json +4 -6
- package/dist/trigger/legacy-session-history.js +0 -19
- package/kits/templates/system-fragments/group-unread-history.md +0 -7
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ec skill` 命令。
|
|
3
|
+
*
|
|
4
|
+
* 这是一层薄转发:真正的实现是内置技能 ec-skill 自己的脚本
|
|
5
|
+
* (`kits/skills/ec-skill/scripts/cli.mjs`)。CLI 不复制逻辑,
|
|
6
|
+
* 模型侧直接跑同一份脚本,两边行为天然一致。
|
|
7
|
+
*
|
|
8
|
+
* 用子进程而非 import,理由:脚本是 ESM、位于包的 kits/ 下,
|
|
9
|
+
* 而 kits/ 随包发布但不参与 tsc 编译;用 spawn 最直接,
|
|
10
|
+
* 也保留了脚本「自定位、不依赖 cwd」的约束。
|
|
11
|
+
*
|
|
12
|
+
* 权限门禁同样在脚本层的 skill.mjs `assertPermission()`,本文件不复制一份。
|
|
13
|
+
* 两个入口(这里的 spawn 与模型侧直接 node脚本)环境变量一致,
|
|
14
|
+
* 脚本自己读 EVOLCORE_TASK_RUNTIME_CONTEXT 即可得到相同判据;
|
|
15
|
+
* 若在 CLI 层再判一遍,只是多一处会与脚本漂移的实现。
|
|
16
|
+
*/
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import { spawn } from 'child_process';
|
|
19
|
+
import { getPackageRoot, agentDir } from '../paths.js';
|
|
20
|
+
import { readTaskRuntimeContextFromEnv } from './task-context.js';
|
|
21
|
+
export async function cmdSkill(args) {
|
|
22
|
+
const script = path.join(getPackageRoot(), 'kits', 'skills', 'ec-skill', 'scripts', 'cli.mjs');
|
|
23
|
+
// 脚本在包外运行,拿不到主程序的路径工具,这里把 Agent 目录显式传进去,
|
|
24
|
+
// 让它能定位 Agent 级 skills/ 与 .switches.json。
|
|
25
|
+
// selfAid 优先取任务运行时上下文(托管会话的权威来源),其次环境变量。
|
|
26
|
+
const env = { ...process.env };
|
|
27
|
+
const selfAid = readTaskRuntimeContextFromEnv()?.selfAid || process.env.EVOLCORE_SELF_AID;
|
|
28
|
+
if (selfAid && !env.EVOLCORE_AGENT_DIR) {
|
|
29
|
+
env.EVOLCORE_AGENT_DIR = agentDir(selfAid);
|
|
30
|
+
}
|
|
31
|
+
const code = await new Promise((resolve, reject) => {
|
|
32
|
+
const child = spawn(process.execPath, [script, ...args], {
|
|
33
|
+
stdio: 'inherit',
|
|
34
|
+
env,
|
|
35
|
+
});
|
|
36
|
+
child.on('error', reject);
|
|
37
|
+
child.on('exit', (c) => resolve(c ?? 0));
|
|
38
|
+
});
|
|
39
|
+
if (code !== 0)
|
|
40
|
+
process.exitCode = code;
|
|
41
|
+
}
|
package/dist/cli/task-context.js
CHANGED
|
@@ -34,6 +34,7 @@ function normalizeTaskRuntimeContext(value) {
|
|
|
34
34
|
chatType: optionalString(value.chatType),
|
|
35
35
|
selfAid: optionalString(value.selfAid),
|
|
36
36
|
peerId: optionalString(value.peerId),
|
|
37
|
+
peerKey: optionalString(value.peerKey),
|
|
37
38
|
peerName: optionalString(value.peerName),
|
|
38
39
|
peerType: optionalString(value.peerType),
|
|
39
40
|
peerRole: optionalString(value.peerRole),
|
|
@@ -51,6 +52,7 @@ function normalizeTaskRuntimeContext(value) {
|
|
|
51
52
|
runtimeLockDir: optionalAbsolutePath(value.runtimeLockDir),
|
|
52
53
|
handoffIds: handoffIds?.length ? handoffIds : undefined,
|
|
53
54
|
causation: normalizeCausation(value.causation),
|
|
55
|
+
consolidationBatchDir: optionalString(value.consolidationBatchDir ?? value.consolidationLogBase),
|
|
54
56
|
};
|
|
55
57
|
}
|
|
56
58
|
function optionalProcessRole(value) {
|
|
@@ -60,29 +62,6 @@ function optionalProcessRole(value) {
|
|
|
60
62
|
function optionalDataScope(value) {
|
|
61
63
|
return value === 'relation' || value === 'agent' || value === 'daemon' ? value : undefined;
|
|
62
64
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Codex's shell carrier can leave one JSON-style escape layer in a message
|
|
65
|
-
* argument (for example, the two characters `\\` and `n`). Decode only that
|
|
66
|
-
* layer, and only for a task that is explicitly running the Codex backend.
|
|
67
|
-
* Actual line breaks and all non-Codex CLI input remain unchanged.
|
|
68
|
-
*/
|
|
69
|
-
export function decodeCodexMessageText(text, runtime) {
|
|
70
|
-
if (runtime?.baseagent?.trim().toLowerCase() !== 'codex')
|
|
71
|
-
return text;
|
|
72
|
-
return text.replace(/\\([nrt])/g, (_match, escape) => {
|
|
73
|
-
if (escape === 'n')
|
|
74
|
-
return '\n';
|
|
75
|
-
if (escape === 'r')
|
|
76
|
-
return '\r';
|
|
77
|
-
return '\t';
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
/** Apply the Codex-only normalization to a text message payload. */
|
|
81
|
-
export function normalizeCodexTextPayload(payload, runtime) {
|
|
82
|
-
if (payload.type !== 'text' || typeof payload.text !== 'string')
|
|
83
|
-
return;
|
|
84
|
-
payload.text = decodeCodexMessageText(payload.text, runtime);
|
|
85
|
-
}
|
|
86
65
|
function optionalAbsolutePath(value) {
|
|
87
66
|
if (typeof value !== 'string' || !path.isAbsolute(value))
|
|
88
67
|
return undefined;
|
|
@@ -5,9 +5,17 @@ import { ipcQuery } from '../ipc.js';
|
|
|
5
5
|
import { isHelpFlag, wantsHelp } from './help.js';
|
|
6
6
|
import { readDelegationCredential } from '../core/auth/agent-delegation.js';
|
|
7
7
|
import { readTaskRuntimeContextFromEnv } from './task-context.js';
|
|
8
|
+
import { assertEcSingleLine } from './cli-argv.js';
|
|
9
|
+
import { canonicalizeEvolcoreCommandArgv } from '../core/permission/ec-command-parser.js';
|
|
8
10
|
import { normalizeTriggerDefinition, resolveScriptPath, } from '../trigger/validation.js';
|
|
9
11
|
export async function cmdTrigger(args) {
|
|
12
|
+
args = canonicalizeEvolcoreCommandArgv(['ec', 'trigger', ...args]).slice(2);
|
|
10
13
|
const sub = args[0];
|
|
14
|
+
if (sub === 'create' || sub === 'set' || sub === 'update') {
|
|
15
|
+
const nameIndex = args.indexOf('--name');
|
|
16
|
+
if (nameIndex >= 0)
|
|
17
|
+
assertEcSingleLine(args[nameIndex + 1], '--name');
|
|
18
|
+
}
|
|
11
19
|
if (!sub || isHelpFlag(sub)) {
|
|
12
20
|
printHelp();
|
|
13
21
|
return;
|
|
@@ -112,7 +120,7 @@ Create 参数模式支持:
|
|
|
112
120
|
--execution-timeout <时长>(模型类 Trigger 单次执行总时限)
|
|
113
121
|
--model <模型> --effort <low|medium|high|xhigh|max>
|
|
114
122
|
--max-runs <次数> --max-duration <时长: 30s|15m|2h|1d>
|
|
115
|
-
--permission <readonly|auto|request|bypass|fullaccess>(省略则继承当前身份配置;fullaccess 仅 daemon-owner
|
|
123
|
+
--permission <readonly|auto|request|bypass|fullaccess>(省略则继承当前身份配置;fullaccess 仅 daemon-owner 可用)
|
|
116
124
|
--tz <时区> (仅 cron)
|
|
117
125
|
--target-channel <channelKey> --target-channel-id <ID> (AUN 可简写为 aun)
|
|
118
126
|
--target-chat-type <private|group> (AUN 目标必填;channelId 同时作为群聊 ID)
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ec watch diag — 消息生命周期诊断
|
|
3
|
+
* 固定N行,每行一个环节,原地刷新,显示该环节的计数 + 最后处理的消息。
|
|
4
|
+
* 数据源:各处理环节直接打点写入的 msg-diag.jsonl,不分析其他日志。
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import { resolvePaths } from '../paths.js';
|
|
10
|
+
// ==================== ANSI ====================
|
|
11
|
+
const isTTY = !!process.stdout.isTTY;
|
|
12
|
+
const RST = isTTY ? '\x1b[0m' : '';
|
|
13
|
+
const DIM = isTTY ? '\x1b[2m' : '';
|
|
14
|
+
const BOLD = isTTY ? '\x1b[1m' : '';
|
|
15
|
+
const CYAN = isTTY ? '\x1b[36m' : '';
|
|
16
|
+
const GREEN = isTTY ? '\x1b[32m' : '';
|
|
17
|
+
const YELLOW = isTTY ? '\x1b[33m' : '';
|
|
18
|
+
const BLUE = isTTY ? '\x1b[34m' : '';
|
|
19
|
+
const GRAY = isTTY ? '\x1b[38;5;244m' : '';
|
|
20
|
+
const UP = (n) => isTTY ? `\x1b[${n}A` : '';
|
|
21
|
+
const CLR = isTTY ? '\x1b[2K\r' : '';
|
|
22
|
+
// ==================== Stage definitions ====================
|
|
23
|
+
const STAGES = [
|
|
24
|
+
{ stage: 'sdk_received', label: '0. SDK收到 ', color: GRAY },
|
|
25
|
+
{ stage: 'aun_in', label: '1. AUN入站 ', color: GREEN },
|
|
26
|
+
{ stage: 'queue_enqueue', label: '2. 队列入队 ', color: CYAN },
|
|
27
|
+
{ stage: 'task_created', label: '3. 任务创建 ', color: YELLOW },
|
|
28
|
+
{ stage: 'task_start', label: '4. 任务开始 ', color: YELLOW },
|
|
29
|
+
{ stage: 'task_done', label: '5. 任务完成 ', color: BLUE },
|
|
30
|
+
{ stage: 'aun_out', label: '6. AUN出站 ', color: GREEN },
|
|
31
|
+
];
|
|
32
|
+
const state = new Map();
|
|
33
|
+
for (const s of STAGES)
|
|
34
|
+
state.set(s.stage, { count: 0 });
|
|
35
|
+
// ==================== Helpers ====================
|
|
36
|
+
function hhmmssms(iso) {
|
|
37
|
+
const d = new Date(iso);
|
|
38
|
+
return (String(d.getHours()).padStart(2, '0') + ':' +
|
|
39
|
+
String(d.getMinutes()).padStart(2, '0') + ':' +
|
|
40
|
+
String(d.getSeconds()).padStart(2, '0') + '.' +
|
|
41
|
+
String(d.getMilliseconds()).padStart(3, '0'));
|
|
42
|
+
}
|
|
43
|
+
function cut(s, n) {
|
|
44
|
+
if (!s)
|
|
45
|
+
return '';
|
|
46
|
+
return s.length <= n ? s : s.slice(0, n - 1) + '…';
|
|
47
|
+
}
|
|
48
|
+
function pad(s, n) {
|
|
49
|
+
return s.length >= n ? s : s + ' '.repeat(n - s.length);
|
|
50
|
+
}
|
|
51
|
+
// ==================== Render ====================
|
|
52
|
+
let firstRender = true;
|
|
53
|
+
const HEADER_LINES = 3;
|
|
54
|
+
function renderHeader() {
|
|
55
|
+
const cols = process.stdout.columns ?? 120;
|
|
56
|
+
let buf = '';
|
|
57
|
+
buf += `${BOLD}${CYAN}━━━ ec watch diag — 消息生命周期诊断 ━━━${RST}\n`;
|
|
58
|
+
buf += `${DIM}各环节实时显示处理计数和最后一条消息,Ctrl+C 退出${RST}\n`;
|
|
59
|
+
buf += `${GRAY}${'─'.repeat(Math.min(cols, 120))}${RST}\n`;
|
|
60
|
+
return buf;
|
|
61
|
+
}
|
|
62
|
+
function renderRow(stageDef, st) {
|
|
63
|
+
const countStr = `[${String(st.count).padStart(4)}]`;
|
|
64
|
+
const label = `${stageDef.color}${stageDef.label}${RST}`;
|
|
65
|
+
if (!st.last) {
|
|
66
|
+
return `${CLR}${label} ${DIM}${countStr}${RST} ${GRAY}(等待中...)${RST}`;
|
|
67
|
+
}
|
|
68
|
+
const e = st.last;
|
|
69
|
+
const time = hhmmssms(e.ts);
|
|
70
|
+
const from = cut(e.from || e.aid, 20);
|
|
71
|
+
const to = cut(e.to, 20);
|
|
72
|
+
const arrow = from && to ? `${from}${GRAY}→${RST}${to}` : (from || to || `${GRAY}---${RST}`);
|
|
73
|
+
const preview = e.preview ? `${DIM}"${cut(e.preview, 40)}"${RST}` : '';
|
|
74
|
+
const taskStr = e.taskId ? `${GRAY}[${cut(e.taskId, 22)}]${RST} ` : '';
|
|
75
|
+
const sessStr = e.sessionId ? `${GRAY}sess=${cut(e.sessionId, 18)}${RST} ` : '';
|
|
76
|
+
return `${CLR}${label} ${DIM}${countStr}${RST} ${GRAY}${time}${RST} ${pad(arrow, 42)} ${taskStr}${sessStr}${preview}`;
|
|
77
|
+
}
|
|
78
|
+
function render(_redrawAll) {
|
|
79
|
+
if (!isTTY)
|
|
80
|
+
return;
|
|
81
|
+
// 每次都清屏重绘,彻底避免因内容长度不一导致的行偏移问题
|
|
82
|
+
let buf = '\x1b[2J\x1b[H';
|
|
83
|
+
buf += `${BOLD}${CYAN}━━━ ec watch diag — 消息生命周期诊断 ━━━${RST}\n`;
|
|
84
|
+
const cols = process.stdout.columns ?? 120;
|
|
85
|
+
buf += `${DIM}各环节实时显示处理计数和最后一条消息,Ctrl+C 退出${RST}\n`;
|
|
86
|
+
buf += `${GRAY}${'─'.repeat(Math.min(cols, 120))}${RST}\n`;
|
|
87
|
+
for (const s of STAGES) {
|
|
88
|
+
buf += renderRow(s, state.get(s.stage)) + '\n';
|
|
89
|
+
}
|
|
90
|
+
process.stdout.write(buf);
|
|
91
|
+
firstRender = false;
|
|
92
|
+
}
|
|
93
|
+
// 非 TTY 模式打印新事件
|
|
94
|
+
function printNonTty(entry) {
|
|
95
|
+
const stageDef = STAGES.find(s => s.stage === entry.stage);
|
|
96
|
+
if (!stageDef)
|
|
97
|
+
return;
|
|
98
|
+
const st = state.get(entry.stage);
|
|
99
|
+
const time = hhmmssms(entry.ts);
|
|
100
|
+
const from = cut(entry.from || entry.aid, 14);
|
|
101
|
+
const to = cut(entry.to, 14);
|
|
102
|
+
console.log(`${stageDef.label.trim()} [${st.count}] ${time} ${from}→${to} "${entry.preview}"`);
|
|
103
|
+
}
|
|
104
|
+
// ==================== Log tailing ====================
|
|
105
|
+
function getDiagLogPath() {
|
|
106
|
+
const p = resolvePaths();
|
|
107
|
+
// resolvePaths().root 是 ~/.evolcore
|
|
108
|
+
const home = process.env.EVOLCORE_HOME ?? path.join(os.homedir(), '.evolcore');
|
|
109
|
+
return path.join(home, 'logs', 'msg-diag.jsonl');
|
|
110
|
+
}
|
|
111
|
+
function processLine(line) {
|
|
112
|
+
if (!line.trim())
|
|
113
|
+
return false;
|
|
114
|
+
try {
|
|
115
|
+
const entry = JSON.parse(line);
|
|
116
|
+
if (!entry.stage || !STAGES.find(s => s.stage === entry.stage))
|
|
117
|
+
return false;
|
|
118
|
+
const st = state.get(entry.stage);
|
|
119
|
+
if (!st)
|
|
120
|
+
return false;
|
|
121
|
+
st.count++;
|
|
122
|
+
st.last = entry;
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// ==================== Main ====================
|
|
130
|
+
export async function cmdWatchMsgDiag(_args) {
|
|
131
|
+
const logPath = getDiagLogPath();
|
|
132
|
+
if (!isTTY) {
|
|
133
|
+
// 非 TTY:只打印新事件
|
|
134
|
+
console.log('# ec watch diag — 消息生命周期诊断(非 TTY 模式)');
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
process.stdout.write('\x1b[?25l'); // 隐藏光标
|
|
138
|
+
}
|
|
139
|
+
// 初始加载:读取已有历史(最后 50KB)
|
|
140
|
+
let filePos = 0;
|
|
141
|
+
if (fs.existsSync(logPath)) {
|
|
142
|
+
const stat = fs.statSync(logPath);
|
|
143
|
+
filePos = Math.max(0, stat.size - 50000);
|
|
144
|
+
const fd = fs.openSync(logPath, 'r');
|
|
145
|
+
const buf = Buffer.alloc(stat.size - filePos);
|
|
146
|
+
fs.readSync(fd, buf, 0, buf.length, filePos);
|
|
147
|
+
fs.closeSync(fd);
|
|
148
|
+
const lines = buf.toString('utf-8').split('\n');
|
|
149
|
+
for (const line of lines)
|
|
150
|
+
processLine(line);
|
|
151
|
+
filePos = stat.size;
|
|
152
|
+
}
|
|
153
|
+
// 初始渲染
|
|
154
|
+
render(true);
|
|
155
|
+
let curLogPath = logPath;
|
|
156
|
+
function tick() {
|
|
157
|
+
// 检查文件是否存在(daemon 重启后才会创建)
|
|
158
|
+
if (!fs.existsSync(curLogPath)) {
|
|
159
|
+
if (!firstRender) {
|
|
160
|
+
// 文件消失了,说明 daemon 重启,重置状态
|
|
161
|
+
for (const s of STAGES)
|
|
162
|
+
state.set(s.stage, { count: 0 });
|
|
163
|
+
filePos = 0;
|
|
164
|
+
render(true);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const stat = fs.statSync(curLogPath);
|
|
170
|
+
if (stat.size <= filePos)
|
|
171
|
+
return; // 无新内容
|
|
172
|
+
const fd = fs.openSync(curLogPath, 'r');
|
|
173
|
+
const buf = Buffer.alloc(stat.size - filePos);
|
|
174
|
+
fs.readSync(fd, buf, 0, buf.length, filePos);
|
|
175
|
+
fs.closeSync(fd);
|
|
176
|
+
filePos = stat.size;
|
|
177
|
+
const lines = buf.toString('utf-8').split('\n');
|
|
178
|
+
let changed = false;
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
if (processLine(line)) {
|
|
181
|
+
changed = true;
|
|
182
|
+
if (!isTTY) {
|
|
183
|
+
try {
|
|
184
|
+
const entry = JSON.parse(line);
|
|
185
|
+
printNonTty(entry);
|
|
186
|
+
}
|
|
187
|
+
catch { }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// 批量处理完所有新行后,只渲染一次
|
|
192
|
+
if (changed && isTTY)
|
|
193
|
+
render(false);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
// 文件轮转中,忽略
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const interval = setInterval(tick, 300);
|
|
200
|
+
process.on('SIGINT', () => {
|
|
201
|
+
clearInterval(interval);
|
|
202
|
+
if (isTTY) {
|
|
203
|
+
process.stdout.write('\x1b[?25h\n');
|
|
204
|
+
}
|
|
205
|
+
console.log('\n诊断监控已停止');
|
|
206
|
+
process.exit(0);
|
|
207
|
+
});
|
|
208
|
+
await new Promise(() => { });
|
|
209
|
+
}
|
|
@@ -2,6 +2,8 @@ const BEHAVIOR_TOP_FIELDS = new Set([
|
|
|
2
2
|
'active_baseagent',
|
|
3
3
|
'chatmode',
|
|
4
4
|
'mentionMode',
|
|
5
|
+
'autoMentionQuietWindowMs',
|
|
6
|
+
'sessionIdleSec',
|
|
5
7
|
'groupSlashRequireMentions',
|
|
6
8
|
'responseMode',
|
|
7
9
|
'responseModeParams',
|
|
@@ -14,6 +16,7 @@ const BEHAVIOR_TOP_FIELDS = new Set([
|
|
|
14
16
|
'render',
|
|
15
17
|
'sessionManifests',
|
|
16
18
|
'enable_rich_content',
|
|
19
|
+
'memory',
|
|
17
20
|
]);
|
|
18
21
|
const BASEAGENT_BEHAVIOR_FIELDS = new Set([
|
|
19
22
|
'model',
|
|
@@ -59,6 +62,7 @@ const SENSITIVE_TOP_FIELDS = new Set([
|
|
|
59
62
|
'roles',
|
|
60
63
|
'role',
|
|
61
64
|
'extra_backup',
|
|
65
|
+
'decision_model',
|
|
62
66
|
]);
|
|
63
67
|
const CRITICAL_AGENT_FIELDS = new Set(['owners', 'admins', 'roles', 'access']);
|
|
64
68
|
const EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
@@ -75,7 +79,7 @@ export function isBehaviorConfigFieldPath(fieldPath) {
|
|
|
75
79
|
}
|
|
76
80
|
return BEHAVIOR_TOP_FIELDS.has(parts[0]);
|
|
77
81
|
}
|
|
78
|
-
export function resolveConfigFieldRule(fieldPath) {
|
|
82
|
+
export function resolveConfigFieldRule(fieldPath, scope) {
|
|
79
83
|
const field = fieldPath.trim();
|
|
80
84
|
if (!field)
|
|
81
85
|
return { class: 'unknown' };
|
|
@@ -122,6 +126,10 @@ export function resolveConfigFieldRule(fieldPath) {
|
|
|
122
126
|
return scalar(field, 'nonnegative-number');
|
|
123
127
|
if (field === 'mentionMode')
|
|
124
128
|
return scalar(field, 'mention-mode');
|
|
129
|
+
if (field === 'autoMentionQuietWindowMs')
|
|
130
|
+
return scalar(field, 'nonnegative-number');
|
|
131
|
+
if (field === 'sessionIdleSec')
|
|
132
|
+
return scalar(field, 'finite-number');
|
|
125
133
|
if (field === 'groupSlashRequireMentions')
|
|
126
134
|
return scalar(field, 'boolean');
|
|
127
135
|
if (field === 'show_activities')
|
|
@@ -132,6 +140,8 @@ export function resolveConfigFieldRule(fieldPath) {
|
|
|
132
140
|
return scalar(field, 'string');
|
|
133
141
|
if (field === 'configSnapshots')
|
|
134
142
|
return scalar('configSnapshots', 'boolean');
|
|
143
|
+
if (field === 'restartRecovery.enabled')
|
|
144
|
+
return scalar('restartRecovery.enabled', 'boolean');
|
|
135
145
|
if (top === 'chatmode') {
|
|
136
146
|
if (parts.length === 1)
|
|
137
147
|
return readonlyObject('chatmode');
|
|
@@ -158,6 +168,8 @@ export function resolveConfigFieldRule(fieldPath) {
|
|
|
158
168
|
return scalar('session_renew', 'boolean');
|
|
159
169
|
if (parts[1] === 'after_hours')
|
|
160
170
|
return scalar('session_renew', 'positive-number');
|
|
171
|
+
if (parts[1] === 'new_probability_threshold')
|
|
172
|
+
return scalar('session_renew', 'probability');
|
|
161
173
|
if (parts[1] === 'fallback_action')
|
|
162
174
|
return scalar('session_renew', 'session-renew-action');
|
|
163
175
|
return { class: 'unknown' };
|
|
@@ -188,6 +200,31 @@ export function resolveConfigFieldRule(fieldPath) {
|
|
|
188
200
|
return parts.length === 1
|
|
189
201
|
? readonlyObject('group_venue_sync')
|
|
190
202
|
: { class: 'unknown' };
|
|
203
|
+
if (top === 'memory') {
|
|
204
|
+
if (parts.length === 1)
|
|
205
|
+
return readonlyObject('memory');
|
|
206
|
+
if (field === 'memory.enableAgent')
|
|
207
|
+
return scalar('memory', 'boolean', 'owner-only');
|
|
208
|
+
if (field === 'memory.enableOwner')
|
|
209
|
+
return scalar('memory', 'boolean', 'owner-only');
|
|
210
|
+
if (field === 'memory.enableRelation') {
|
|
211
|
+
// relation scope:对端自主控制自己这条关系的记忆开关,role-overridable
|
|
212
|
+
// agent scope:owner 级管控,owner-only
|
|
213
|
+
return scope === 'relation'
|
|
214
|
+
? scalar('memory', 'boolean')
|
|
215
|
+
: scalar('memory', 'boolean', 'owner-only');
|
|
216
|
+
}
|
|
217
|
+
// 数值参数:整理节奏与容量上限,均 owner-only
|
|
218
|
+
if (field === 'memory.idleHours'
|
|
219
|
+
|| field === 'memory.checkIntervalHours'
|
|
220
|
+
|| field === 'memory.backlogHours'
|
|
221
|
+
|| field === 'memory.indexMaxBytes'
|
|
222
|
+
|| field === 'memory.forgetAfterDays'
|
|
223
|
+
|| field === 'memory.profileRegenDays') {
|
|
224
|
+
return scalar('memory', 'positive-number', 'owner-only');
|
|
225
|
+
}
|
|
226
|
+
return { class: 'unknown' };
|
|
227
|
+
}
|
|
191
228
|
return { class: 'unknown' };
|
|
192
229
|
}
|
|
193
230
|
export function isSafeBehaviorConfigField(fieldPath) {
|
|
@@ -262,6 +299,18 @@ export function parseConfigFieldValue(fieldPath, rawValue) {
|
|
|
262
299
|
? { ok: true, value }
|
|
263
300
|
: { ok: false, reason: `Config field ${fieldPath} must be a positive number` };
|
|
264
301
|
}
|
|
302
|
+
case 'finite-number': {
|
|
303
|
+
const value = Number(raw);
|
|
304
|
+
return Number.isFinite(value)
|
|
305
|
+
? { ok: true, value }
|
|
306
|
+
: { ok: false, reason: `Config field ${fieldPath} must be a finite number` };
|
|
307
|
+
}
|
|
308
|
+
case 'probability': {
|
|
309
|
+
const value = Number(raw);
|
|
310
|
+
return raw && Number.isFinite(value) && value >= 0 && value <= 1
|
|
311
|
+
? { ok: true, value }
|
|
312
|
+
: { ok: false, reason: `Config field ${fieldPath} must be a number between 0 and 1` };
|
|
313
|
+
}
|
|
265
314
|
case 'effort':
|
|
266
315
|
return EFFORTS.has(raw)
|
|
267
316
|
? { ok: true, value: raw }
|
|
@@ -271,9 +320,9 @@ export function parseConfigFieldValue(fieldPath, rawValue) {
|
|
|
271
320
|
? { ok: true, value: raw }
|
|
272
321
|
: { ok: false, reason: `Config field ${fieldPath} must be interactive or proactive` };
|
|
273
322
|
case 'mention-mode':
|
|
274
|
-
return raw === 'disabled' || raw === 'mention-only'
|
|
323
|
+
return raw === 'disabled' || raw === 'mention-only' || raw === 'auto'
|
|
275
324
|
? { ok: true, value: raw }
|
|
276
|
-
: { ok: false, reason: `Config field ${fieldPath} must be disabled
|
|
325
|
+
: { ok: false, reason: `Config field ${fieldPath} must be disabled, mention-only, or auto` };
|
|
277
326
|
case 'show-activities':
|
|
278
327
|
return raw === 'all' || raw === 'text' || raw === 'none'
|
|
279
328
|
? { ok: true, value: raw }
|
|
@@ -747,6 +747,10 @@ export function normalizeProcessConfigCompat(value) {
|
|
|
747
747
|
const debug = value.debug;
|
|
748
748
|
let changed = false;
|
|
749
749
|
let normalized = value;
|
|
750
|
+
if (Object.prototype.hasOwnProperty.call(normalized, 'fullaccess')) {
|
|
751
|
+
normalized = { ...normalized };
|
|
752
|
+
delete normalized.fullaccess;
|
|
753
|
+
}
|
|
750
754
|
if (idleMonitor && typeof idleMonitor === 'object' && !Array.isArray(idleMonitor)) {
|
|
751
755
|
const nextIdleMonitor = { ...idleMonitor };
|
|
752
756
|
if (nextIdleMonitor.apiRetryTimeout === undefined && nextIdleMonitor.retryAttemptTimeout !== undefined) {
|
|
@@ -1337,7 +1341,7 @@ export function normalizeDefaultsBehaviorCompat(defaults) {
|
|
|
1337
1341
|
if (!normalized.chatmode && (chatMode === 'interactive' || chatMode === 'proactive')) {
|
|
1338
1342
|
normalized.chatmode = { private: chatMode, group: chatMode, nothuman: chatMode };
|
|
1339
1343
|
}
|
|
1340
|
-
if (normalized.mentionMode === undefined && (config.mentionMode === 'disabled' || config.mentionMode === 'mention-only')) {
|
|
1344
|
+
if (normalized.mentionMode === undefined && (config.mentionMode === 'disabled' || config.mentionMode === 'mention-only' || config.mentionMode === 'auto')) {
|
|
1341
1345
|
normalized.mentionMode = config.mentionMode;
|
|
1342
1346
|
}
|
|
1343
1347
|
if (normalized.show_activities === undefined && ['all', 'text', 'none'].includes(config.show_activities)) {
|
|
@@ -1362,9 +1366,10 @@ function stripStaticOwnerField(config) {
|
|
|
1362
1366
|
}
|
|
1363
1367
|
function stripRelationRoleData(config) {
|
|
1364
1368
|
if (!config || (!Object.prototype.hasOwnProperty.call(config, 'role')
|
|
1365
|
-
&& !Object.prototype.hasOwnProperty.call(config, 'roles')
|
|
1369
|
+
&& !Object.prototype.hasOwnProperty.call(config, 'roles')
|
|
1370
|
+
&& !Object.prototype.hasOwnProperty.call(config, 'decision_model')))
|
|
1366
1371
|
return config;
|
|
1367
|
-
const { role: _role, roles: _legacyRoles, ...rest } = config;
|
|
1372
|
+
const { role: _role, roles: _legacyRoles, decision_model: _decisionModel, ...rest } = config;
|
|
1368
1373
|
return rest;
|
|
1369
1374
|
}
|
|
1370
1375
|
// ── effective(合并视图)─────────────────────────────────────────────────
|
|
@@ -1374,6 +1379,7 @@ function stripRelationRoleData(config) {
|
|
|
1374
1379
|
export function resolveEffective(sel, opts = {}) {
|
|
1375
1380
|
const config = resolveAgentConfig(sel, opts);
|
|
1376
1381
|
const observableDefault = loadSchema('agent-config').fields.get('observable')?.default;
|
|
1382
|
+
const autoMentionQuietWindowMsDefault = loadSchema('agent-config').fields.get('autoMentionQuietWindowMs')?.default;
|
|
1377
1383
|
const effective = {
|
|
1378
1384
|
$schema_version: config.$schema_version ?? currentVersion('agent-config'),
|
|
1379
1385
|
aid: config.aid ?? sel.self ?? '',
|
|
@@ -1384,6 +1390,7 @@ export function resolveEffective(sel, opts = {}) {
|
|
|
1384
1390
|
owners: config.owners,
|
|
1385
1391
|
fullaccess: config.fullaccess,
|
|
1386
1392
|
aun: config.aun,
|
|
1393
|
+
decision_model: config.decision_model,
|
|
1387
1394
|
channels: config.channels ?? [],
|
|
1388
1395
|
models: config.models,
|
|
1389
1396
|
projects: config.projects,
|
|
@@ -1398,6 +1405,8 @@ export function resolveEffective(sel, opts = {}) {
|
|
|
1398
1405
|
baseagents: config.baseagents,
|
|
1399
1406
|
chatmode: config.chatmode,
|
|
1400
1407
|
mentionMode: config.mentionMode,
|
|
1408
|
+
autoMentionQuietWindowMs: config.autoMentionQuietWindowMs
|
|
1409
|
+
?? (typeof autoMentionQuietWindowMsDefault === 'number' ? autoMentionQuietWindowMsDefault : 3000),
|
|
1401
1410
|
groupSlashRequireMentions: config.groupSlashRequireMentions ?? true,
|
|
1402
1411
|
model: config.model,
|
|
1403
1412
|
responseMode: config.responseMode,
|
|
@@ -1410,6 +1419,7 @@ export function resolveEffective(sel, opts = {}) {
|
|
|
1410
1419
|
render: config.render,
|
|
1411
1420
|
sessionManifests: config.sessionManifests,
|
|
1412
1421
|
enable_rich_content: config.enable_rich_content,
|
|
1422
|
+
memory: config.memory,
|
|
1413
1423
|
};
|
|
1414
1424
|
return normalizeEffectiveCompatibility(effective);
|
|
1415
1425
|
}
|
|
@@ -1430,6 +1440,7 @@ const EFFECTIVE_BEHAVIOR_FIELDS = [
|
|
|
1430
1440
|
'baseagents',
|
|
1431
1441
|
'chatmode',
|
|
1432
1442
|
'mentionMode',
|
|
1443
|
+
'autoMentionQuietWindowMs',
|
|
1433
1444
|
'groupSlashRequireMentions',
|
|
1434
1445
|
'flush_delay',
|
|
1435
1446
|
'debounce',
|
|
@@ -1456,7 +1467,9 @@ export function routeFieldPath(fieldPath, scope) {
|
|
|
1456
1467
|
if (scope === 'process')
|
|
1457
1468
|
return routeIn(DAEMON_SCHEMA_NAME, ConfigTarget.Process, topField);
|
|
1458
1469
|
if (scope === 'defaults') {
|
|
1459
|
-
if (isBehaviorConfigFieldPath(fieldPath)
|
|
1470
|
+
if (isBehaviorConfigFieldPath(fieldPath)
|
|
1471
|
+
&& topField !== 'session_renew'
|
|
1472
|
+
&& topField !== 'autoMentionQuietWindowMs') {
|
|
1460
1473
|
throw new ConfigError('DEFAULT_BEHAVIOR_REJECT', `--default 不支持行为字段: ${fieldPath}`);
|
|
1461
1474
|
}
|
|
1462
1475
|
return routeIn('defaults', ConfigTarget.Defaults, topField);
|
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { isValidAid } from '../aun/aid/validation.js';
|
|
5
5
|
import { isExplicitGroupId } from '../aun/group/index.js';
|
|
6
|
-
import { agentConfig, agentContactConfig, agentDir, resolvePaths } from '../paths.js';
|
|
6
|
+
import { agentConfig, agentContactAuditPath, agentContactConfig, agentDir, resolvePaths } from '../paths.js';
|
|
7
7
|
import { atomicReadJson, atomicWrite, atomicWriteJson } from '../utils/atomic-write.js';
|
|
8
8
|
import { fileCache } from '../core/daemon-file-cache.js';
|
|
9
9
|
import { ensureManagedLockParent, resolveManagedLockPath } from '../core/runtime-lock.js';
|
|
@@ -601,7 +601,7 @@ function journalFile(selfAid) {
|
|
|
601
601
|
return path.join(agentDir(selfAid), JOURNAL_NAME);
|
|
602
602
|
}
|
|
603
603
|
function appendAudit(selfAid, request, previousContactRevision, contactRevisionValue) {
|
|
604
|
-
const file =
|
|
604
|
+
const file = agentContactAuditPath(selfAid);
|
|
605
605
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
606
606
|
fs.appendFileSync(file, `${JSON.stringify({
|
|
607
607
|
timestamp: new Date().toISOString(),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export function normalizeMentionMode(mode) {
|
|
1
|
+
export function normalizeAunMentionMode(mode) {
|
|
3
2
|
if (typeof mode !== 'string')
|
|
4
3
|
return undefined;
|
|
5
4
|
const normalized = mode.trim().toLowerCase();
|
|
@@ -9,3 +8,9 @@ export function normalizeMentionMode(mode) {
|
|
|
9
8
|
return 'mention-only';
|
|
10
9
|
return undefined;
|
|
11
10
|
}
|
|
11
|
+
export function normalizeMentionMode(mode) {
|
|
12
|
+
const normalized = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
|
|
13
|
+
if (normalized === 'auto')
|
|
14
|
+
return 'auto';
|
|
15
|
+
return normalizeAunMentionMode(mode);
|
|
16
|
+
}
|
package/dist/config/merge.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* merge —— 类型驱动的覆盖链合并(design §三,全项目唯一实现点)+ ${VAR} 展开。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 合并规则(按字段的 x-merge 语义):
|
|
5
5
|
* - scalar:高优先级整体覆盖
|
|
6
6
|
* - list: 并集追加去重(只能加,不能减)
|
|
7
|
-
* - dict: 键并集;同键 →
|
|
7
|
+
* - dict: 键并集;同键 → 高优先级值直接覆盖(合并粒度=第一层键)
|
|
8
|
+
* 若字段在 schema 中声明了 x-merge-logic: AND 的子字段,
|
|
9
|
+
* 则该子字段做 AND 合并而非覆盖(任意层为 false 结果为 false)
|
|
8
10
|
*
|
|
9
|
-
*
|
|
11
|
+
* 合并代码永远只有这几条规则;所有"覆盖 vs 追加 vs AND"通过 schema 的 x-merge / x-merge-logic 选择表达,不为字段写特判。
|
|
10
12
|
*/
|
|
11
13
|
import fs from 'fs';
|
|
12
14
|
import path from 'path';
|
|
@@ -24,11 +26,20 @@ function unionList(base, overlay) {
|
|
|
24
26
|
}
|
|
25
27
|
return out;
|
|
26
28
|
}
|
|
27
|
-
/** dict
|
|
28
|
-
|
|
29
|
+
/** dict 第一层键合并:键并集,同键按子字段 FieldSpec 决定合并语义。
|
|
30
|
+
* 若子字段声明了 mergeLogic: AND 且两值均为 boolean,做 AND;否则高优先级覆盖。 */
|
|
31
|
+
function mergeDictWithSchema(base, overlay, children) {
|
|
29
32
|
const out = { ...base };
|
|
30
|
-
for (const [k, v] of Object.entries(overlay))
|
|
31
|
-
|
|
33
|
+
for (const [k, v] of Object.entries(overlay)) {
|
|
34
|
+
const childSpec = children?.get(k);
|
|
35
|
+
const prev = out[k];
|
|
36
|
+
if (prev !== undefined && childSpec?.mergeLogic === 'AND' && typeof prev === 'boolean' && typeof v === 'boolean') {
|
|
37
|
+
out[k] = prev && v;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
out[k] = v;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
32
43
|
return out;
|
|
33
44
|
}
|
|
34
45
|
function isPlainObject(v) {
|
|
@@ -38,6 +49,9 @@ function isPlainObject(v) {
|
|
|
38
49
|
* 按 schema 字段表,对覆盖链上"存在的每一级"做类型驱动合并。
|
|
39
50
|
* layers 从低优先级到高优先级;低优先级独有字段保留,冲突点高优先级胜。
|
|
40
51
|
* 字段未在 schema 中(理论上 ajv 已拦)按 scalar 处理。
|
|
52
|
+
*
|
|
53
|
+
* x-merge-logic: AND 语义:所有层的布尔值做 AND 合并,任意层为 false 则结果为 false。
|
|
54
|
+
* 仅对 boolean 类型的 scalar 字段有意义;值缺失的层不参与 AND(按 true 处理)。
|
|
41
55
|
*/
|
|
42
56
|
export function mergeLayers(layers, fields) {
|
|
43
57
|
const out = {};
|
|
@@ -54,11 +68,16 @@ export function mergeLayers(layers, fields) {
|
|
|
54
68
|
out[k] = v;
|
|
55
69
|
continue;
|
|
56
70
|
}
|
|
57
|
-
|
|
71
|
+
// x-merge-logic: AND — 任意层为 false 则结果为 false
|
|
72
|
+
if (spec?.mergeLogic === 'AND' && typeof v === 'boolean' && typeof prev === 'boolean') {
|
|
73
|
+
out[k] = prev && v;
|
|
74
|
+
}
|
|
75
|
+
else if (kind === 'list' && Array.isArray(prev) && Array.isArray(v)) {
|
|
58
76
|
out[k] = unionList(prev, v);
|
|
59
77
|
}
|
|
60
78
|
else if (kind === 'dict' && isPlainObject(prev) && isPlainObject(v)) {
|
|
61
|
-
|
|
79
|
+
// 传入 children,让 dict 子字段能感知 x-merge-logic: AND
|
|
80
|
+
out[k] = mergeDictWithSchema(prev, v, spec?.children);
|
|
62
81
|
}
|
|
63
82
|
else {
|
|
64
83
|
out[k] = v; // scalar 覆盖(或类型不一致时高优先级胜)
|
|
@@ -125,7 +125,7 @@ function resolveFieldCommand(argv, subcommand, options = {}) {
|
|
|
125
125
|
reason: 'fallbackModels is configured only by editing agents/defaults.json',
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
-
const fieldRule = resolveConfigFieldRule(field);
|
|
128
|
+
const fieldRule = resolveConfigFieldRule(field, selector.scope);
|
|
129
129
|
if (subcommand !== 'get' && fieldRule.class === 'safe-readonly-object') {
|
|
130
130
|
return {
|
|
131
131
|
ok: false,
|