evolcore 0.0.1 → 0.0.3
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 +64 -0
- package/LICENSE +21 -0
- package/MIGRATION-0.5.0.md +378 -0
- package/README.md +318 -14
- package/ROLE_ACCESS_CONTROL.md +174 -0
- package/assets/.env.template +4 -0
- package/bin/ec-safe-output.js +161 -0
- package/bin/ec.js +29 -0
- package/dist/agents/baseagent.js +163 -0
- package/dist/agents/claude-runner.js +2565 -0
- package/dist/agents/codex-app-server-client.js +448 -0
- package/dist/agents/codex-runner.js +2682 -0
- package/dist/agents/gemini-runner.js +666 -0
- package/dist/agents/runner-types.js +75 -0
- package/dist/aun/aid/agentmd.js +216 -0
- package/dist/aun/aid/client.js +132 -0
- package/dist/aun/aid/control-aid.js +91 -0
- package/dist/aun/aid/identity.js +518 -0
- package/dist/aun/aid/index.js +4 -0
- package/dist/aun/aid/store.js +74 -0
- package/dist/aun/aid/types.js +1 -0
- package/dist/aun/aid/validation.js +21 -0
- package/dist/aun/group-identity.js +10 -0
- package/dist/aun/msg/group-index.js +6 -0
- package/dist/aun/msg/group.js +1231 -0
- package/dist/aun/msg/history.js +123 -0
- package/dist/aun/msg/index.js +5 -0
- package/dist/aun/msg/p2p.js +393 -0
- package/dist/aun/msg/payload-type.js +27 -0
- package/dist/aun/msg/upload.js +137 -0
- package/dist/aun/outbox.js +168 -0
- package/dist/aun/rpc/caller.js +42 -0
- package/dist/aun/rpc/connection.js +25 -0
- package/dist/aun/rpc/index.js +2 -0
- package/dist/aun/service-proxy.js +225 -0
- package/dist/aun/storage/download.js +29 -0
- package/dist/aun/storage/index.js +3 -0
- package/dist/aun/storage/manage.js +10 -0
- package/dist/aun/storage/upload.js +68 -0
- package/dist/channels/aun.js +4164 -0
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/daemon.js +422 -0
- package/dist/channels/dingtalk.js +1479 -0
- package/dist/channels/feishu.js +1865 -0
- package/dist/channels/qqbot.js +409 -0
- package/dist/channels/wechat.js +817 -0
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +1157 -0
- package/dist/cli/agent-command.js +642 -0
- package/dist/cli/agent.js +1059 -0
- package/dist/cli/aun-commands.js +2003 -0
- package/dist/cli/bench.js +1228 -0
- package/dist/cli/cli-argv.js +66 -0
- package/dist/cli/code-stats.js +329 -0
- package/dist/cli/command-log.js +82 -0
- package/dist/cli/config-selector.js +69 -0
- package/dist/cli/config.js +261 -0
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +62 -0
- package/dist/cli/daemon-commands.js +2750 -0
- package/dist/cli/fs-command.js +1447 -0
- package/dist/cli/handoff-command.js +302 -0
- package/dist/cli/help.js +35 -0
- package/dist/cli/index.js +374 -0
- package/dist/cli/init-channel.js +1372 -0
- package/dist/cli/init.js +590 -0
- package/dist/cli/link-rules.js +240 -0
- package/dist/cli/model.js +591 -0
- package/dist/cli/net-check.js +723 -0
- package/dist/cli/queue-command.js +150 -0
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +344 -0
- package/dist/cli/restart-monitor.js +480 -0
- package/dist/cli/stats.js +609 -0
- package/dist/cli/task-context.js +80 -0
- package/dist/cli/trigger-command.js +545 -0
- package/dist/cli/version.js +93 -0
- package/dist/cli/watch-logs.js +33 -0
- package/dist/cli/watch-msg.js +673 -0
- package/dist/config/boot-log.js +266 -0
- package/dist/config/builtin-role-templates.js +42 -0
- package/dist/config/builtin-roles.js +91 -0
- package/dist/config/config-batch-get.js +11 -0
- package/dist/config/config-field-policy.js +261 -0
- package/dist/config/config-manager.js +1120 -0
- package/dist/config/config-operation-service.js +384 -0
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +224 -0
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/gateway-config.js +858 -0
- package/dist/config/lifecycle.js +17 -0
- package/dist/config/mention-mode.js +27 -0
- package/dist/config/merge.js +161 -0
- package/dist/config/owner-policy.js +4 -0
- package/dist/config/peer-role-resolver.js +218 -0
- package/dist/config/resolved-config-op.js +483 -0
- package/dist/config/role-config-v4-startup.js +32 -0
- package/dist/config/role-config-v5-startup.js +27 -0
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-schema.js +105 -0
- package/dist/config/role-service.js +156 -0
- package/dist/config/role-store.js +215 -0
- package/dist/config/roles.js +64 -0
- package/dist/config/schema-registry.js +154 -0
- package/dist/config/snapshot.js +598 -0
- package/dist/config-store.js +501 -0
- package/dist/core/auth/agent-delegation.js +111 -0
- package/dist/core/auth/auth-gateway.js +166 -0
- package/dist/core/auth/authenticated-actor.js +6 -0
- package/dist/core/auth/authorization-audit.js +119 -0
- package/dist/core/auth/operation-authorizer.js +720 -0
- package/dist/core/auth/operation-catalog.js +731 -0
- package/dist/core/baseagent-loader.js +54 -0
- package/dist/core/bootstrap-service.js +175 -0
- package/dist/core/capability/capability-manager.js +316 -0
- package/dist/core/capability/providers/claude-capability-provider.js +176 -0
- package/dist/core/capability/providers/codex-capability-provider.js +148 -0
- package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
- package/dist/core/capability/types.js +27 -0
- package/dist/core/causation/audit.js +103 -0
- package/dist/core/causation/aun-association.js +111 -0
- package/dist/core/causation/context.js +93 -0
- package/dist/core/causation/index.js +4 -0
- package/dist/core/causation/types.js +2 -0
- package/dist/core/channel-loader.js +277 -0
- package/dist/core/command/agent-control.js +616 -0
- package/dist/core/command/cli-intent-parser.js +225 -0
- package/dist/core/command/command-handler.js +1733 -0
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +3452 -0
- package/dist/core/command/menu-protocol.js +247 -0
- package/dist/core/command/role-menu.js +1623 -0
- package/dist/core/command/slash-gate.js +148 -0
- package/dist/core/command/slash-handler.js +3066 -0
- package/dist/core/daemon-file-cache.js +222 -0
- package/dist/core/event-bus.js +32 -0
- package/dist/core/event-catalog.js +810 -0
- package/dist/core/evolagent-registry.js +545 -0
- package/dist/core/evolagent.js +342 -0
- package/dist/core/handoff/dispatcher.js +229 -0
- package/dist/core/handoff/mutex.js +46 -0
- package/dist/core/handoff/runtime.js +324 -0
- package/dist/core/handoff/store.js +537 -0
- package/dist/core/handoff/types.js +12 -0
- package/dist/core/inference/text-inference.js +173 -0
- package/dist/core/interaction-registration.js +10 -0
- package/dist/core/interaction-router.js +278 -0
- package/dist/core/message/create-status.js +67 -0
- package/dist/core/message/im-renderer.js +659 -0
- package/dist/core/message/items-formatter.js +76 -0
- package/dist/core/message/logical-queue-bridge.js +123 -0
- package/dist/core/message/message-bridge.js +874 -0
- package/dist/core/message/message-cache.js +56 -0
- package/dist/core/message/message-log.js +339 -0
- package/dist/core/message/message-processor.js +4 -0
- package/dist/core/message/message-queue.js +1446 -0
- package/dist/core/message/message-utils.js +76 -0
- package/dist/core/message/peer-mode.js +105 -0
- package/dist/core/message/pending-hints.js +232 -0
- package/dist/core/message/response-depth.js +33 -0
- package/dist/core/message/response-engine.js +4020 -0
- package/dist/core/message/response-snapshot.js +83 -0
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +139 -0
- package/dist/core/message/stream-idle-monitor.js +124 -0
- package/dist/core/model/config-scope.js +162 -0
- package/dist/core/model/field-scope.js +78 -0
- package/dist/core/model/model-catalog.js +227 -0
- package/dist/core/model/model-diagnostics.js +182 -0
- package/dist/core/model/model-permission.js +90 -0
- package/dist/core/permission/approval-gateway.js +1017 -0
- package/dist/core/permission/ec-command-parser.js +347 -0
- package/dist/core/permission/execution-sandbox.js +16 -0
- package/dist/core/permission/index.js +6 -0
- package/dist/core/permission/mode.js +24 -0
- package/dist/core/permission/sandbox-runtime.js +265 -0
- package/dist/core/permission/tool-policy.js +1019 -0
- package/dist/core/permission/unix-socket-policy.js +99 -0
- package/dist/core/protected-paths.js +332 -0
- package/dist/core/relation/peer-identity.js +222 -0
- package/dist/core/relation/peer-key.js +1 -0
- package/dist/core/role/runtime-policy.js +141 -0
- package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
- package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
- package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
- package/dist/core/session/session-file-adapter.js +7 -0
- package/dist/core/session/session-file-health.js +45 -0
- package/dist/core/session/session-fs-store.js +273 -0
- package/dist/core/session/session-key.js +24 -0
- package/dist/core/session/session-manager.js +1643 -0
- package/dist/core/session/session-mapper.js +100 -0
- package/dist/core/session/session-renew.js +314 -0
- package/dist/core/session/session-title.js +128 -0
- package/dist/core/session/session-turn-coordinator.js +205 -0
- package/dist/core/session/session-turns.js +67 -0
- package/dist/core/system-channels.js +29 -0
- package/dist/eck/baseagent-caps.js +18 -0
- package/dist/eck/detect.js +47 -0
- package/dist/eck/group-rules-sync.js +345 -0
- package/dist/eck/init.js +77 -0
- package/dist/eck/kit-renderer.js +359 -0
- package/dist/eck/manifest-engine.js +446 -0
- package/dist/eck/message-renderer.js +199 -0
- package/dist/eck/rules-loader.js +28 -0
- package/dist/index.js +2926 -4
- package/dist/ipc.js +777 -0
- package/dist/paths.js +262 -0
- package/dist/product.js +18 -0
- package/dist/response-system/context-builder.js +71 -0
- package/dist/response-system/coordinator.js +117 -0
- package/dist/response-system/decision-executor.js +86 -0
- package/dist/response-system/engines/v1/index.js +21 -0
- package/dist/response-system/engines/v1/interactive-flow.js +27 -0
- package/dist/response-system/engines/v1/proactive-flow.js +137 -0
- package/dist/response-system/engines/v1/types.js +1 -0
- package/dist/response-system/extensions.js +41 -0
- package/dist/response-system/index.js +6 -0
- package/dist/response-system/modes/index.js +7 -0
- package/dist/response-system/modes/single-session/index.js +72 -0
- package/dist/response-system/queues/fifo-queue.js +44 -0
- package/dist/response-system/queues/index.js +6 -0
- package/dist/response-system/queues/lifo-queue.js +42 -0
- package/dist/response-system/queues/priority-queue.js +63 -0
- package/dist/response-system/registry.js +97 -0
- package/dist/response-system/resolver.js +37 -0
- package/dist/response-system/selector.js +23 -0
- package/dist/response-system/types.js +7 -0
- package/dist/stats/billing.js +163 -0
- package/dist/stats/budget.js +93 -0
- package/dist/stats/db.js +403 -0
- package/dist/stats/eck-vars.js +89 -0
- package/dist/stats/index.js +11 -0
- package/dist/stats/normalizer.js +80 -0
- package/dist/stats/price-resolver.js +138 -0
- package/dist/stats/query.js +763 -0
- package/dist/stats/role-budget.js +168 -0
- package/dist/stats/writer.js +151 -0
- package/dist/trigger/anomaly-store.js +258 -0
- package/dist/trigger/audit.js +152 -0
- package/dist/trigger/event-source.js +119 -0
- package/dist/trigger/feedback.js +685 -0
- package/dist/trigger/history.js +290 -0
- package/dist/trigger/manager.js +294 -0
- package/dist/trigger/parser.js +595 -0
- package/dist/trigger/patch.js +155 -0
- package/dist/trigger/scheduler.js +1602 -0
- package/dist/trigger/script-executor.js +155 -0
- package/dist/trigger/state.js +145 -0
- package/dist/trigger/types.js +1 -0
- package/dist/trigger/validation.js +634 -0
- package/dist/types.js +12 -0
- package/dist/utils/aid-bind.js +313 -0
- package/dist/utils/atomic-write.js +95 -0
- package/dist/utils/avatar-upload.js +123 -0
- package/dist/utils/cross-platform.js +297 -0
- package/dist/utils/ecweb-utils.js +73 -0
- package/dist/utils/error-dict.json +153 -0
- package/dist/utils/error-utils.js +349 -0
- package/dist/utils/instance-registry.js +444 -0
- package/dist/utils/locale.js +21 -0
- package/dist/utils/log-writer.js +270 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/markdown-to-plain-text.js +20 -0
- package/dist/utils/media-cache.js +274 -0
- package/dist/utils/model-prices.jsonl +20 -0
- package/dist/utils/npm-ops.js +210 -0
- package/dist/utils/process-introspect.js +133 -0
- package/dist/utils/process-tree-stats.js +271 -0
- package/dist/utils/project-path.js +74 -0
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stats.js +410 -0
- package/dist/utils/system-memory.js +62 -0
- package/dist/utils/tool-summary.js +284 -0
- package/dist/utils/welcome.js +268 -0
- package/kits/docs/GUIDE.md +20 -0
- package/kits/docs/INDEX.md +66 -0
- package/kits/docs/aun/CHEATSHEET.md +19 -0
- package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
- package/kits/docs/channels/aun.md +65 -0
- package/kits/docs/channels/feishu.md +56 -0
- package/kits/docs/context-assembly.md +366 -0
- package/kits/docs/eck_templates/GUIDE.template.md +22 -0
- package/kits/docs/eck_templates/INDEX.template.md +28 -0
- package/kits/docs/eck_templates/path-registry.template.md +33 -0
- package/kits/docs/eck_templates/runtime.template.md +19 -0
- package/kits/docs/evolcore/INDEX.md +68 -0
- package/kits/docs/evolcore/agent.md +77 -0
- package/kits/docs/evolcore/aid.md +52 -0
- package/kits/docs/evolcore/config.md +149 -0
- package/kits/docs/evolcore/contact.md +57 -0
- package/kits/docs/evolcore/ctl.md +46 -0
- package/kits/docs/evolcore/event.md +216 -0
- package/kits/docs/evolcore/fs-architecture.md +1215 -0
- package/kits/docs/evolcore/fs.md +110 -0
- package/kits/docs/evolcore/group-fs.md +17 -0
- package/kits/docs/evolcore/group-rules.md +226 -0
- package/kits/docs/evolcore/group.md +150 -0
- package/kits/docs/evolcore/model.md +50 -0
- package/kits/docs/evolcore/msg.md +136 -0
- package/kits/docs/evolcore/response.md +75 -0
- package/kits/docs/evolcore/rpc.md +37 -0
- package/kits/docs/evolcore/self-summary.md +29 -0
- package/kits/docs/evolcore/stats.md +83 -0
- package/kits/docs/evolcore/storage.md +50 -0
- package/kits/docs/evolcore/trigger.md +539 -0
- package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
- package/kits/docs/identity/PATH_OPS.md +16 -0
- package/kits/docs/identity/ROLE_DETAIL.md +23 -0
- package/kits/docs/identity/identity-tools.md +26 -0
- package/kits/docs/path-registry.md +43 -0
- package/kits/docs/prompt-loading-architecture.md +266 -0
- package/kits/docs/venues/aun-group.md +45 -0
- package/kits/docs/venues/aun-private.md +10 -0
- package/kits/docs/venues/client-desktop.md +10 -0
- package/kits/docs/venues/client-mobile.md +10 -0
- package/kits/docs/venues/feishu-group.md +13 -0
- package/kits/docs/venues/feishu-private.md +9 -0
- package/kits/docs/venues/group.md +25 -0
- package/kits/docs/venues/private.md +10 -0
- package/kits/eck_manifest.auxiliary.json +43 -0
- package/kits/eck_manifest.json +203 -0
- package/kits/eck_message_manifest.json +63 -0
- package/kits/migrations/README-role-config-v4.md +32 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/migrations/migrate-role-config-v4.mjs +623 -0
- package/kits/migrations/migrate-role-config-v5.mjs +346 -0
- package/kits/migrations/rename-config-file.mjs +99 -0
- package/kits/rules/01-overview.md +142 -0
- package/kits/rules/02-navigation.md +76 -0
- package/kits/rules/03-identity.md +34 -0
- package/kits/rules/04-relation.md +59 -0
- package/kits/rules/05-venue.md +44 -0
- package/kits/rules/06-channel.md +59 -0
- package/kits/schemas/_meta.json +32 -0
- package/kits/schemas/agent-config.schema.1.json +177 -0
- package/kits/schemas/agent-config.schema.2.json +239 -0
- package/kits/schemas/agent-config.schema.3.json +119 -0
- package/kits/schemas/agent-config.schema.4.json +208 -0
- package/kits/schemas/agent-config.schema.5.json +326 -0
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.1.json +36 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/daemon.schema.1.json +90 -0
- package/kits/schemas/defaults.schema.1.json +81 -0
- package/kits/schemas/menu-exec-schema-commands.md +208 -0
- package/kits/schemas/migrations/README.md +28 -0
- package/kits/schemas/relation-config.schema.1.json +158 -0
- package/kits/schemas/relation-config.schema.2.json +73 -0
- package/kits/schemas/relation-config.schema.3.json +50 -0
- package/kits/schemas/relation-config.schema.4.json +47 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +201 -0
- package/kits/schemas/role-registry.schema.1.json +35 -0
- package/kits/schemas/single-session.schema.1.json +31 -0
- package/kits/templates/bootstrap-welcome.md +15 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
- package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
- package/kits/templates/message-fragments/inject-default.md +2 -0
- package/kits/templates/message-fragments/item.md +2 -0
- package/kits/templates/roles/admin.json +9 -0
- package/kits/templates/roles/member.json +41 -0
- package/kits/templates/roles/owner.json +9 -0
- package/kits/templates/roles/visitor.json +40 -0
- package/kits/templates/system-fragments/baseagent.md +14 -0
- package/kits/templates/system-fragments/bootstrap.md +16 -0
- package/kits/templates/system-fragments/channel.md +48 -0
- package/kits/templates/system-fragments/commands.md +28 -0
- package/kits/templates/system-fragments/identity.md +11 -0
- package/kits/templates/system-fragments/relation.md +19 -0
- package/kits/templates/system-fragments/session.md +53 -0
- package/kits/templates/system-fragments/venue.md +31 -0
- package/package.json +50 -15
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigManager —— 所有配置文件读写/合并的统一归口(全项目唯一合并实现点)。
|
|
3
|
+
*
|
|
4
|
+
* 不允许散落的 fs.readFileSync 直接操作配置文件。
|
|
5
|
+
*
|
|
6
|
+
* v3 设计(2026-06-19):
|
|
7
|
+
* - 覆盖链:defaults.json → agent/config.json → relation/config.json
|
|
8
|
+
* - 所有参数统一在 config.json,不再有 behavior.json
|
|
9
|
+
* - 进程级 daemon.json:独立,不参与覆盖链
|
|
10
|
+
*
|
|
11
|
+
* 详见 docs/config/01-overview.md
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { resolvePaths, agentConfig as agentConfigPath, agentContactConfig, agentRelationConfig, agentDir, agentRelationsDir, agentRolesIndex, agentRoleConfig, } from '../paths.js';
|
|
16
|
+
import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
|
|
17
|
+
import { fileCache } from '../core/daemon-file-cache.js';
|
|
18
|
+
import { isValidAid } from '../aun/aid/validation.js';
|
|
19
|
+
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
20
|
+
import { parseContactAlias } from './contact-alias.js';
|
|
21
|
+
import { loadSchema, currentVersion, isSchemaName, } from './schema-registry.js';
|
|
22
|
+
import { mergeLayers, expandVars, buildEnvResolver } from './merge.js';
|
|
23
|
+
import { normalizeAgentLifecycle } from './lifecycle.js';
|
|
24
|
+
import { isBehaviorConfigFieldPath } from './config-field-policy.js';
|
|
25
|
+
import { clearRoleStoreCache, isRoleName } from './role-store.js';
|
|
26
|
+
import { writeRoleDefinition, writeRoleRegistry } from './role-service.js';
|
|
27
|
+
import { DAEMON_SCHEMA_NAME } from '../product.js';
|
|
28
|
+
export var ConfigTarget;
|
|
29
|
+
(function (ConfigTarget) {
|
|
30
|
+
ConfigTarget["Process"] = "process";
|
|
31
|
+
ConfigTarget["Defaults"] = "defaults";
|
|
32
|
+
ConfigTarget["Agent"] = "agent";
|
|
33
|
+
ConfigTarget["Relation"] = "relation";
|
|
34
|
+
ConfigTarget["Contact"] = "contact";
|
|
35
|
+
ConfigTarget["RoleRegistry"] = "role-registry";
|
|
36
|
+
ConfigTarget["Role"] = "role";
|
|
37
|
+
})(ConfigTarget || (ConfigTarget = {}));
|
|
38
|
+
const configWriteListeners = new Set();
|
|
39
|
+
/** Observe successful in-process config writes after cache invalidation. */
|
|
40
|
+
export function onConfigWrite(listener) {
|
|
41
|
+
configWriteListeners.add(listener);
|
|
42
|
+
return () => configWriteListeners.delete(listener);
|
|
43
|
+
}
|
|
44
|
+
const TARGET_SCHEMA = {
|
|
45
|
+
[ConfigTarget.Process]: DAEMON_SCHEMA_NAME,
|
|
46
|
+
[ConfigTarget.Defaults]: 'defaults',
|
|
47
|
+
[ConfigTarget.Agent]: 'agent-config',
|
|
48
|
+
[ConfigTarget.Relation]: 'relation-config',
|
|
49
|
+
[ConfigTarget.Contact]: 'contact-book',
|
|
50
|
+
[ConfigTarget.RoleRegistry]: 'role-registry',
|
|
51
|
+
[ConfigTarget.Role]: 'role-config',
|
|
52
|
+
};
|
|
53
|
+
export class ConfigError extends Error {
|
|
54
|
+
code;
|
|
55
|
+
data;
|
|
56
|
+
constructor(code, message, data) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.data = data;
|
|
60
|
+
this.name = 'ConfigError';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
let _initialized = false;
|
|
64
|
+
/** ConfigManager 初始化(预留扩展点)。幂等。 */
|
|
65
|
+
export function initConfigManager() {
|
|
66
|
+
if (_initialized)
|
|
67
|
+
return;
|
|
68
|
+
// 初始化逻辑(如需要)
|
|
69
|
+
_initialized = true;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Daemon startup gate for the one-off role config v4 migration.
|
|
73
|
+
* This intentionally does not mutate files or provide runtime compatibility.
|
|
74
|
+
*/
|
|
75
|
+
export function assertRoleConfigV4Ready() {
|
|
76
|
+
const root = resolvePaths().root;
|
|
77
|
+
const agentsRoot = resolvePaths().agentsDir;
|
|
78
|
+
if (!fs.existsSync(agentsRoot))
|
|
79
|
+
return;
|
|
80
|
+
const issues = [];
|
|
81
|
+
const inspect = (file, logicalName, minimumVersion) => {
|
|
82
|
+
let value;
|
|
83
|
+
try {
|
|
84
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const actualVersion = value && typeof value === 'object'
|
|
91
|
+
? value.$schema_version
|
|
92
|
+
: undefined;
|
|
93
|
+
if (typeof actualVersion !== 'number' || actualVersion < minimumVersion) {
|
|
94
|
+
issues.push({
|
|
95
|
+
file,
|
|
96
|
+
detail: `$schema_version must be at least ${minimumVersion}, got ${String(actualVersion)}`,
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
let schema;
|
|
101
|
+
try {
|
|
102
|
+
schema = loadSchema(logicalName, actualVersion);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
issues.push({ file, detail: `unsupported ${logicalName} schema version ${actualVersion}` });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!schema.validate(value)) {
|
|
109
|
+
const detail = (schema.validate.errors ?? []).map(error => {
|
|
110
|
+
const extra = typeof error.params?.additionalProperty === 'string'
|
|
111
|
+
? ` (additionalProperty=${error.params.additionalProperty})`
|
|
112
|
+
: '';
|
|
113
|
+
return `${error.instancePath || '/'} ${error.message}${extra}`;
|
|
114
|
+
}).join('; ');
|
|
115
|
+
issues.push({ file, detail });
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const walkRelationConfigs = (dir) => {
|
|
119
|
+
if (!fs.existsSync(dir))
|
|
120
|
+
return [];
|
|
121
|
+
const files = [];
|
|
122
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
123
|
+
const full = path.join(dir, entry.name);
|
|
124
|
+
if (entry.isDirectory())
|
|
125
|
+
files.push(...walkRelationConfigs(full));
|
|
126
|
+
else if (entry.isFile() && entry.name === 'config.json')
|
|
127
|
+
files.push(full);
|
|
128
|
+
}
|
|
129
|
+
return files;
|
|
130
|
+
};
|
|
131
|
+
for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
|
|
132
|
+
if (!entry.isDirectory() || !isValidAid(entry.name))
|
|
133
|
+
continue;
|
|
134
|
+
const agentDirPath = path.join(agentsRoot, entry.name);
|
|
135
|
+
const agentFile = path.join(agentDirPath, 'config.json');
|
|
136
|
+
if (fs.existsSync(agentFile)) {
|
|
137
|
+
inspect(agentFile, 'agent-config', 4);
|
|
138
|
+
}
|
|
139
|
+
for (const relationFile of walkRelationConfigs(path.join(agentDirPath, 'relations'))) {
|
|
140
|
+
inspect(relationFile, 'relation-config', 4);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (!issues.length)
|
|
144
|
+
return;
|
|
145
|
+
const shown = issues.slice(0, 20)
|
|
146
|
+
.map(issue => `- ${path.relative(root, issue.file)}: ${issue.detail}`)
|
|
147
|
+
.join('\n');
|
|
148
|
+
const omitted = issues.length > 20 ? `\n- ... ${issues.length - 20} more issue(s)` : '';
|
|
149
|
+
const quotedRoot = JSON.stringify(root);
|
|
150
|
+
throw new ConfigError('ROLE_CONFIG_MIGRATION_REQUIRED', `Role config v4 migration is required before daemon startup:\n${shown}${omitted}\n` +
|
|
151
|
+
`Stop the daemon, then run dry-run and review it:\n` +
|
|
152
|
+
`node kits/migrations/migrate-role-config-v4.mjs --home ${quotedRoot}\n` +
|
|
153
|
+
`Apply only after review:\n` +
|
|
154
|
+
`node kits/migrations/migrate-role-config-v4.mjs --apply --home ${quotedRoot}`, { issues });
|
|
155
|
+
}
|
|
156
|
+
/** Validate the standalone role domain after the v4 -> v5 migration. */
|
|
157
|
+
export function assertRoleConfigV5Ready() {
|
|
158
|
+
const root = resolvePaths().root;
|
|
159
|
+
const agentsRoot = resolvePaths().agentsDir;
|
|
160
|
+
if (!fs.existsSync(agentsRoot))
|
|
161
|
+
return;
|
|
162
|
+
const issues = [];
|
|
163
|
+
const registrySchema = loadSchema('role-registry');
|
|
164
|
+
const readJson = (file) => {
|
|
165
|
+
try {
|
|
166
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const schemaErrors = (schema) => (schema.validate.errors ?? [])
|
|
174
|
+
.map(error => `${error.instancePath || '/'} ${error.message}`).join('; ');
|
|
175
|
+
for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
|
|
176
|
+
if (!entry.isDirectory() || !isValidAid(entry.name))
|
|
177
|
+
continue;
|
|
178
|
+
const aid = entry.name;
|
|
179
|
+
const agentFile = path.join(agentsRoot, aid, 'config.json');
|
|
180
|
+
if (!fs.existsSync(agentFile))
|
|
181
|
+
continue;
|
|
182
|
+
const agent = readJson(agentFile);
|
|
183
|
+
const agentVersion = agent?.$schema_version;
|
|
184
|
+
let agentSchema = null;
|
|
185
|
+
if (typeof agentVersion === 'number' && agentVersion >= 5) {
|
|
186
|
+
try {
|
|
187
|
+
agentSchema = loadSchema('agent-config', agentVersion);
|
|
188
|
+
}
|
|
189
|
+
catch { }
|
|
190
|
+
}
|
|
191
|
+
if (!agent || !agentSchema || !agentSchema.validate(agent)) {
|
|
192
|
+
if (agent)
|
|
193
|
+
issues.push({
|
|
194
|
+
file: agentFile,
|
|
195
|
+
detail: agentSchema
|
|
196
|
+
? schemaErrors(agentSchema)
|
|
197
|
+
: `agent schema version must be supported and >= 5, got ${String(agentVersion)}`,
|
|
198
|
+
});
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const registryFile = agentRolesIndex(aid);
|
|
202
|
+
if (!fs.existsSync(registryFile)) {
|
|
203
|
+
issues.push({ file: registryFile, detail: 'roles/index.json is required for Agent config v5' });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const registry = readJson(registryFile);
|
|
207
|
+
if (!registry || !registrySchema.validate(registry)) {
|
|
208
|
+
if (registry)
|
|
209
|
+
issues.push({ file: registryFile, detail: schemaErrors(registrySchema) });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (!issues.length)
|
|
214
|
+
return;
|
|
215
|
+
const shown = issues.slice(0, 20).map(issue => `- ${path.relative(root, issue.file)}: ${issue.detail}`).join('\n');
|
|
216
|
+
throw new ConfigError('ROLE_CONFIG_V5_MIGRATION_REQUIRED', `Standalone role config migration is required before daemon startup:\n${shown}\n` +
|
|
217
|
+
`node kits/migrations/migrate-role-config-v5.mjs --home ${JSON.stringify(root)}\n` +
|
|
218
|
+
`node kits/migrations/migrate-role-config-v5.mjs --apply --home ${JSON.stringify(root)}`, { issues });
|
|
219
|
+
}
|
|
220
|
+
/** Final startup gate for the contact-book authorization migration. */
|
|
221
|
+
export function assertContactBookV2Ready() {
|
|
222
|
+
const root = resolvePaths().root;
|
|
223
|
+
const agentsRoot = resolvePaths().agentsDir;
|
|
224
|
+
if (!fs.existsSync(agentsRoot))
|
|
225
|
+
return;
|
|
226
|
+
const issues = [];
|
|
227
|
+
const agentSchema = loadSchema('agent-config', 6);
|
|
228
|
+
const contactSchema = loadSchema('contact-book', 2);
|
|
229
|
+
const relationSchema = loadSchema('relation-config', 5);
|
|
230
|
+
const inspect = (file, schema) => {
|
|
231
|
+
let value;
|
|
232
|
+
try {
|
|
233
|
+
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!schema.validate(value)) {
|
|
240
|
+
const detail = (schema.validate.errors ?? [])
|
|
241
|
+
.map(error => `${error.instancePath || '/'} ${error.message}`)
|
|
242
|
+
.join('; ');
|
|
243
|
+
issues.push({ file, detail });
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const walkRelationConfigs = (dir) => {
|
|
247
|
+
if (!fs.existsSync(dir))
|
|
248
|
+
return [];
|
|
249
|
+
const files = [];
|
|
250
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
251
|
+
const full = path.join(dir, entry.name);
|
|
252
|
+
if (entry.isDirectory())
|
|
253
|
+
files.push(...walkRelationConfigs(full));
|
|
254
|
+
else if (entry.isFile() && entry.name === 'config.json')
|
|
255
|
+
files.push(full);
|
|
256
|
+
}
|
|
257
|
+
return files;
|
|
258
|
+
};
|
|
259
|
+
for (const entry of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
|
|
260
|
+
if (!entry.isDirectory() || !isValidAid(entry.name))
|
|
261
|
+
continue;
|
|
262
|
+
const dir = path.join(agentsRoot, entry.name);
|
|
263
|
+
const agentFile = path.join(dir, 'config.json');
|
|
264
|
+
if (!fs.existsSync(agentFile))
|
|
265
|
+
continue;
|
|
266
|
+
inspect(agentFile, agentSchema);
|
|
267
|
+
const contactFile = path.join(dir, 'contact.json');
|
|
268
|
+
if (fs.existsSync(contactFile)) {
|
|
269
|
+
inspect(contactFile, contactSchema);
|
|
270
|
+
inspectContactBookDomain(contactFile, agentFile, issues);
|
|
271
|
+
}
|
|
272
|
+
for (const relationFile of walkRelationConfigs(path.join(dir, 'relations'))) {
|
|
273
|
+
inspect(relationFile, relationSchema);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const correctionReceipt = path.join(root, 'data', 'contact-book-v2-relation-role-authority.json');
|
|
277
|
+
if (!hasContactRoleAuthorityReceipt(correctionReceipt)) {
|
|
278
|
+
const recoverable = findRelationRoleRemovedByExperimentalMigration(root);
|
|
279
|
+
if (recoverable) {
|
|
280
|
+
issues.push({
|
|
281
|
+
file: recoverable,
|
|
282
|
+
detail: 'experimental contact-book migration backup must be reviewed and relation role authority correction recorded',
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!issues.length)
|
|
287
|
+
return;
|
|
288
|
+
const shown = issues.slice(0, 20)
|
|
289
|
+
.map(issue => `- ${path.relative(root, issue.file)}: ${issue.detail}`)
|
|
290
|
+
.join('\n');
|
|
291
|
+
const omitted = issues.length > 20 ? `\n- ... ${issues.length - 20} more issue(s)` : '';
|
|
292
|
+
throw new ConfigError('CONTACT_BOOK_V2_MIGRATION_REQUIRED', `Contact book authorization migration is required before daemon startup:\n${shown}${omitted}\n` +
|
|
293
|
+
`node kits/migrations/migrate-contact-book-v2.mjs --home ${JSON.stringify(root)}\n` +
|
|
294
|
+
`node kits/migrations/migrate-contact-book-v2.mjs --apply --home ${JSON.stringify(root)}`, { issues });
|
|
295
|
+
}
|
|
296
|
+
function inspectContactBookDomain(contactFile, agentFile, issues) {
|
|
297
|
+
let contact;
|
|
298
|
+
let agent;
|
|
299
|
+
try {
|
|
300
|
+
contact = JSON.parse(fs.readFileSync(contactFile, 'utf8'));
|
|
301
|
+
agent = JSON.parse(fs.readFileSync(agentFile, 'utf8'));
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const owners = new Set(Array.isArray(agent?.owners) ? agent.owners.map(String) : []);
|
|
307
|
+
const configuredChannelKeys = new Set((Array.isArray(agent?.channels) ? agent.channels : [])
|
|
308
|
+
.filter((channel) => channel && typeof channel === 'object' && channel.type !== 'aun')
|
|
309
|
+
.map((channel) => {
|
|
310
|
+
const type = String(channel.type || '').trim().toLowerCase();
|
|
311
|
+
const name = String(channel.name || '').trim();
|
|
312
|
+
return type && name ? `${type}#${String(agent?.aid || '').trim()}#${name}` : '';
|
|
313
|
+
})
|
|
314
|
+
.filter(Boolean));
|
|
315
|
+
const aliases = new Map();
|
|
316
|
+
for (const [primaryId, entry] of Object.entries(contact?.contacts ?? {})) {
|
|
317
|
+
if (isExplicitGroupId(primaryId)) {
|
|
318
|
+
issues.push({ file: contactFile, detail: `group contact ${primaryId} is not supported` });
|
|
319
|
+
}
|
|
320
|
+
if (entry?.status === 'blocked' && owners.has(primaryId)) {
|
|
321
|
+
issues.push({ file: contactFile, detail: `owner contact ${primaryId} cannot be blocked` });
|
|
322
|
+
}
|
|
323
|
+
for (const alias of Array.isArray(entry?.aliases) ? entry.aliases : []) {
|
|
324
|
+
const parsed = parseContactAlias(alias, agent?.aid);
|
|
325
|
+
if (!parsed) {
|
|
326
|
+
issues.push({ file: contactFile, detail: `invalid Contact alias ${String(alias)}; expected <channelKey>:<urlEncode(peerId)>` });
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (!configuredChannelKeys.has(parsed.channelKey)) {
|
|
330
|
+
issues.push({ file: contactFile, detail: `alias ${alias} references an unconfigured channel instance` });
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const previous = aliases.get(parsed.alias);
|
|
334
|
+
if (previous && previous !== primaryId) {
|
|
335
|
+
issues.push({ file: contactFile, detail: `alias ${parsed.alias} is assigned to both ${previous} and ${primaryId}` });
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
aliases.set(parsed.alias, primaryId);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function hasContactRoleAuthorityReceipt(file) {
|
|
344
|
+
try {
|
|
345
|
+
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
346
|
+
return value?.migration === 'contact-book-v2-relation-role-authority' && value?.designVersion === 2;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
function findRelationRoleRemovedByExperimentalMigration(root) {
|
|
353
|
+
const backupsRoot = path.join(root, 'backups', 'contact-book-v2-migration');
|
|
354
|
+
if (!fs.existsSync(backupsRoot))
|
|
355
|
+
return null;
|
|
356
|
+
for (const stamp of fs.readdirSync(backupsRoot, { withFileTypes: true })) {
|
|
357
|
+
if (!stamp.isDirectory())
|
|
358
|
+
continue;
|
|
359
|
+
try {
|
|
360
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(backupsRoot, stamp.name, 'manifest.json'), 'utf8'));
|
|
361
|
+
if (manifest?.designVersion >= 2)
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
catch { }
|
|
365
|
+
const agentsRoot = path.join(backupsRoot, stamp.name, 'files', 'agents');
|
|
366
|
+
if (!fs.existsSync(agentsRoot))
|
|
367
|
+
continue;
|
|
368
|
+
for (const agent of fs.readdirSync(agentsRoot, { withFileTypes: true })) {
|
|
369
|
+
if (!agent.isDirectory() || !isValidAid(agent.name))
|
|
370
|
+
continue;
|
|
371
|
+
const relationsRoot = path.join(agentsRoot, agent.name, 'relations');
|
|
372
|
+
if (!fs.existsSync(relationsRoot))
|
|
373
|
+
continue;
|
|
374
|
+
for (const relation of fs.readdirSync(relationsRoot, { withFileTypes: true })) {
|
|
375
|
+
if (!relation.isDirectory() || relation.name.startsWith('_'))
|
|
376
|
+
continue;
|
|
377
|
+
const backupFile = path.join(relationsRoot, relation.name, 'config.json');
|
|
378
|
+
if (!fs.existsSync(backupFile))
|
|
379
|
+
continue;
|
|
380
|
+
let backupValue;
|
|
381
|
+
try {
|
|
382
|
+
backupValue = JSON.parse(fs.readFileSync(backupFile, 'utf8'));
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (typeof backupValue?.role !== 'string' || backupValue.role === 'owner')
|
|
388
|
+
continue;
|
|
389
|
+
const currentFile = path.join(root, 'agents', agent.name, 'relations', relation.name, 'config.json');
|
|
390
|
+
let currentValue;
|
|
391
|
+
try {
|
|
392
|
+
currentValue = JSON.parse(fs.readFileSync(currentFile, 'utf8'));
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (currentValue?.$schema_version === 5) {
|
|
398
|
+
return currentFile;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* 启动时检查 schema 版本(daemon 启动时调用)。
|
|
407
|
+
* 扫描所有配置文件,如有版本不匹配则警告一次。
|
|
408
|
+
*/
|
|
409
|
+
export function checkSchemaVersionsOnStartup(selfAid) {
|
|
410
|
+
schemaVersionWarningsEnabled = true;
|
|
411
|
+
const p = resolvePaths();
|
|
412
|
+
// 检查 process 层
|
|
413
|
+
try {
|
|
414
|
+
read(ConfigTarget.Process);
|
|
415
|
+
}
|
|
416
|
+
catch { }
|
|
417
|
+
// 检查 defaults 层
|
|
418
|
+
try {
|
|
419
|
+
read(ConfigTarget.Defaults);
|
|
420
|
+
}
|
|
421
|
+
catch { }
|
|
422
|
+
// 检查 agent 层(如果提供了 AID)
|
|
423
|
+
if (selfAid) {
|
|
424
|
+
try {
|
|
425
|
+
read(ConfigTarget.Agent, { self: selfAid });
|
|
426
|
+
}
|
|
427
|
+
catch { }
|
|
428
|
+
}
|
|
429
|
+
schemaVersionWarningsEnabled = false;
|
|
430
|
+
}
|
|
431
|
+
// Schema 版本警告控制:只在启动时检查一次
|
|
432
|
+
let schemaVersionWarningsEnabled = false;
|
|
433
|
+
function configWarn(...args) {
|
|
434
|
+
console.warn(...args);
|
|
435
|
+
}
|
|
436
|
+
// ── 路径解析 ──────────────────────────────────────────────────────────────
|
|
437
|
+
function targetPath(target, sel) {
|
|
438
|
+
const p = resolvePaths();
|
|
439
|
+
switch (target) {
|
|
440
|
+
case ConfigTarget.Process: return p.daemonConfig;
|
|
441
|
+
case ConfigTarget.Defaults: return p.defaultsConfig;
|
|
442
|
+
case ConfigTarget.Agent:
|
|
443
|
+
requireSelf(sel, target);
|
|
444
|
+
return agentConfigPath(sel.self);
|
|
445
|
+
case ConfigTarget.Relation:
|
|
446
|
+
requirePeer(sel, target);
|
|
447
|
+
return agentRelationConfig(sel.self, sel.peerKey);
|
|
448
|
+
case ConfigTarget.Contact:
|
|
449
|
+
requireSelf(sel, target);
|
|
450
|
+
return agentContactConfig(sel.self);
|
|
451
|
+
case ConfigTarget.RoleRegistry:
|
|
452
|
+
requireSelf(sel, target);
|
|
453
|
+
return agentRolesIndex(sel.self);
|
|
454
|
+
case ConfigTarget.Role:
|
|
455
|
+
requireSelf(sel, target);
|
|
456
|
+
if (!isRoleName(sel?.roleId))
|
|
457
|
+
throw new ConfigError('ROLE_REQUIRED', 'role 需要合法的 selector.roleId');
|
|
458
|
+
return agentRoleConfig(sel.self, sel.roleId);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function requireSelf(sel, target) {
|
|
462
|
+
if (!sel?.self)
|
|
463
|
+
throw new ConfigError('SELF_REQUIRED', `${target} 需要 selector.self`);
|
|
464
|
+
}
|
|
465
|
+
function requirePeer(sel, target) {
|
|
466
|
+
if (!sel?.self || !sel?.peerKey)
|
|
467
|
+
throw new ConfigError('PEER_REQUIRED', `${target} 需要 selector.self + peerKey`);
|
|
468
|
+
}
|
|
469
|
+
// ── env 作用域 ────────────────────────────────────────────────────────────
|
|
470
|
+
function envScopeFor(sel) {
|
|
471
|
+
const root = resolvePaths().root;
|
|
472
|
+
const scope = { rootDir: root };
|
|
473
|
+
if (sel?.self)
|
|
474
|
+
scope.agentDir = agentDir(sel.self);
|
|
475
|
+
if (sel?.self && sel?.peerKey)
|
|
476
|
+
scope.relationDir = path.join(agentRelationsDir(sel.self), sel.peerKey);
|
|
477
|
+
return scope;
|
|
478
|
+
}
|
|
479
|
+
/** 读单个配置文件,返回强类型对象;不存在返回 null(不自动创建)。未知字段保留但不校验。 */
|
|
480
|
+
export function read(target, sel, opts = {}) {
|
|
481
|
+
const file = targetPath(target, sel);
|
|
482
|
+
let raw;
|
|
483
|
+
if (opts.cache) {
|
|
484
|
+
raw = fileCache.get(file, () => atomicReadJson(file), { policy: 'mtime', group: groupFor(target, sel) });
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
raw = atomicReadJson(file);
|
|
488
|
+
}
|
|
489
|
+
if (raw === null)
|
|
490
|
+
return null;
|
|
491
|
+
// schema 版本迁移(read 时若 $schema_version < current)
|
|
492
|
+
const migrated = migrateIfNeeded(target, raw, file);
|
|
493
|
+
const normalized = target === ConfigTarget.Agent
|
|
494
|
+
? normalizeAgentConfigForRead(migrated, sel?.self)
|
|
495
|
+
: target === ConfigTarget.Relation
|
|
496
|
+
? foldLegacyProactiveBlock(migrated)
|
|
497
|
+
: migrated;
|
|
498
|
+
if (!opts.expand)
|
|
499
|
+
return normalized;
|
|
500
|
+
const resolver = buildEnvResolver(envScopeFor(sel));
|
|
501
|
+
return expandVars(normalized, resolver);
|
|
502
|
+
}
|
|
503
|
+
function normalizeAgentConfigForRead(value, aid) {
|
|
504
|
+
const config = foldLegacyProactiveBlock(normalizeAgentLifecycle(value));
|
|
505
|
+
const mutable = config;
|
|
506
|
+
normalizeShowActivitiesCompat(mutable);
|
|
507
|
+
// AID 校验(如果提供了 aid)
|
|
508
|
+
if (aid && mutable.aid && mutable.aid !== aid) {
|
|
509
|
+
const filePath = targetPath(ConfigTarget.Agent, { self: aid });
|
|
510
|
+
throw new ConfigError('VALIDATION_ERROR', `${filePath}: aid field "${mutable.aid}" != directory name "${aid}"`);
|
|
511
|
+
}
|
|
512
|
+
// Legacy configs used top-level "agents" for baseagent settings.
|
|
513
|
+
// Keep read compatibility, but never write the obsolete field back.
|
|
514
|
+
if (mutable.agents && typeof mutable.agents === 'object' && !Array.isArray(mutable.agents)) {
|
|
515
|
+
mutable.baseagents = mutable.baseagents && typeof mutable.baseagents === 'object' && !Array.isArray(mutable.baseagents)
|
|
516
|
+
? { ...mutable.agents, ...mutable.baseagents }
|
|
517
|
+
: mutable.agents;
|
|
518
|
+
delete mutable.agents;
|
|
519
|
+
}
|
|
520
|
+
// 清理 projects.defaultPath 尾部斜杠
|
|
521
|
+
if (mutable.projects?.defaultPath && typeof mutable.projects.defaultPath === 'string') {
|
|
522
|
+
mutable.projects.defaultPath = mutable.projects.defaultPath.replace(/[/\\]+$/, '');
|
|
523
|
+
}
|
|
524
|
+
return config;
|
|
525
|
+
}
|
|
526
|
+
function groupFor(target, sel) {
|
|
527
|
+
if (sel?.self && target === ConfigTarget.Agent) {
|
|
528
|
+
return `config:${sel.self}`;
|
|
529
|
+
}
|
|
530
|
+
if (target === ConfigTarget.Relation) {
|
|
531
|
+
return 'relation-prefs';
|
|
532
|
+
}
|
|
533
|
+
if (sel?.self && target === ConfigTarget.Contact) {
|
|
534
|
+
return `contact:${sel.self}`;
|
|
535
|
+
}
|
|
536
|
+
if (sel?.self && (target === ConfigTarget.RoleRegistry || target === ConfigTarget.Role)) {
|
|
537
|
+
return `role:${sel.self}`;
|
|
538
|
+
}
|
|
539
|
+
return 'config';
|
|
540
|
+
}
|
|
541
|
+
function normalizeAgentConfigForWrite(value) {
|
|
542
|
+
const mutable = { ...value };
|
|
543
|
+
normalizeShowActivitiesCompat(mutable);
|
|
544
|
+
// AID 格式校验
|
|
545
|
+
if (mutable.aid && !isValidAid(mutable.aid)) {
|
|
546
|
+
throw new ConfigError('VALIDATION_ERROR', `Invalid aid "${mutable.aid}" (must be a valid multi-level domain like mybot.agentid.pub)`);
|
|
547
|
+
}
|
|
548
|
+
// 规范化 projects 字段:只保留 rootPath 和 defaultPath
|
|
549
|
+
if (mutable.projects && typeof mutable.projects === 'object') {
|
|
550
|
+
const projects = mutable.projects;
|
|
551
|
+
const normalized = {};
|
|
552
|
+
if (typeof projects.rootPath === 'string')
|
|
553
|
+
normalized.rootPath = projects.rootPath;
|
|
554
|
+
if (typeof projects.defaultPath === 'string')
|
|
555
|
+
normalized.defaultPath = projects.defaultPath;
|
|
556
|
+
if (Object.keys(normalized).length > 0) {
|
|
557
|
+
mutable.projects = normalized;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
delete mutable.projects;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return mutable;
|
|
564
|
+
}
|
|
565
|
+
function normalizeRelationConfigForWrite(value) {
|
|
566
|
+
const mutable = { ...value };
|
|
567
|
+
normalizeShowActivitiesCompat(mutable);
|
|
568
|
+
return mutable;
|
|
569
|
+
}
|
|
570
|
+
function normalizeShowActivitiesCompat(value) {
|
|
571
|
+
if (value.show_activities === true)
|
|
572
|
+
value.show_activities = 'all';
|
|
573
|
+
else if (value.show_activities === false)
|
|
574
|
+
value.show_activities = 'none';
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* 迁移:旧的顶层 `proactive` 块 → `responseModeParams['single-session']`。
|
|
578
|
+
*
|
|
579
|
+
* single-session 合并后,proactive 首工具表态/工具汇报开关是该模式的特有参数,
|
|
580
|
+
* 运行时只从 responseModeParams['single-session'] 读(见 coordinator.resolveMode)。
|
|
581
|
+
* 存量配置里这两个键落在废弃的顶层 proactive 块,若不迁移会静默失效。
|
|
582
|
+
*
|
|
583
|
+
* 读时就地折叠:把 proactive 的两个已知键搬进模式桶(桶内已显式设置的同名键不覆盖),
|
|
584
|
+
* 删除 proactive 块。随后任何 write 都会持久化迁移后的形态,磁盘上的旧块随之消失。
|
|
585
|
+
* 逐文件(层)迁移,跨层合并交给 mergeLayers(responseModeParams 按模式 id dict 合并)。
|
|
586
|
+
*/
|
|
587
|
+
function foldLegacyProactiveBlock(value) {
|
|
588
|
+
const block = value.proactive;
|
|
589
|
+
if (!block || typeof block !== 'object' || Array.isArray(block))
|
|
590
|
+
return value;
|
|
591
|
+
const migrated = {};
|
|
592
|
+
if (typeof block.pre_tool_1stmsgchk === 'boolean')
|
|
593
|
+
migrated.pre_tool_1stmsgchk = block.pre_tool_1stmsgchk;
|
|
594
|
+
if (typeof block.tool_use_reminder === 'boolean')
|
|
595
|
+
migrated.tool_use_reminder = block.tool_use_reminder;
|
|
596
|
+
const mutable = { ...value };
|
|
597
|
+
delete mutable.proactive;
|
|
598
|
+
if (Object.keys(migrated).length === 0)
|
|
599
|
+
return mutable;
|
|
600
|
+
const params = (mutable.responseModeParams && typeof mutable.responseModeParams === 'object' && !Array.isArray(mutable.responseModeParams))
|
|
601
|
+
? { ...mutable.responseModeParams }
|
|
602
|
+
: {};
|
|
603
|
+
const bucket = (params['single-session'] && typeof params['single-session'] === 'object' && !Array.isArray(params['single-session']))
|
|
604
|
+
? { ...params['single-session'] }
|
|
605
|
+
: {};
|
|
606
|
+
// 桶内已显式设置的同名键优先,迁移值只补空缺
|
|
607
|
+
params['single-session'] = { ...migrated, ...bucket };
|
|
608
|
+
mutable.responseModeParams = params;
|
|
609
|
+
return mutable;
|
|
610
|
+
}
|
|
611
|
+
/** Write config with schema validation and atomic persistence. */
|
|
612
|
+
export function write(target, value, sel, opts = {}) {
|
|
613
|
+
const file = targetPath(target, sel);
|
|
614
|
+
if (target === ConfigTarget.RoleRegistry) {
|
|
615
|
+
writeRoleRegistry(sel.self, value);
|
|
616
|
+
finishConfigWrite(target, sel, file);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (target === ConfigTarget.Role) {
|
|
620
|
+
writeRoleDefinition(sel.self, sel.roleId, value);
|
|
621
|
+
finishConfigWrite(target, sel, file);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const schema = loadSchema(TARGET_SCHEMA[target]);
|
|
625
|
+
const withVer = ensureSchemaVersion(value, schema.version);
|
|
626
|
+
const migrated = withVer;
|
|
627
|
+
// Agent config 写入规范化(aid 校验、projects 字段清理)
|
|
628
|
+
const normalized = target === ConfigTarget.Agent
|
|
629
|
+
? normalizeAgentConfigForWrite(migrated)
|
|
630
|
+
: target === ConfigTarget.Relation
|
|
631
|
+
? normalizeRelationConfigForWrite(migrated)
|
|
632
|
+
: migrated;
|
|
633
|
+
// 1. Schema 校验
|
|
634
|
+
if (!opts.skipValidate) {
|
|
635
|
+
validateOrThrow(schema, normalized, target);
|
|
636
|
+
// 1b. responseModeParams 桶内容校验:主 schema 把 responseModeParams 当黑箱,
|
|
637
|
+
// 此处使用“模式桶 schema”逐桶校验内容(桶键 = 已登记的响应模式 id)。
|
|
638
|
+
if (target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
|
|
639
|
+
validateResponseModeParams(normalized?.responseModeParams, target);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// 2. 写入文件
|
|
643
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
644
|
+
atomicWriteJson(file, normalized);
|
|
645
|
+
finishConfigWrite(target, sel, file);
|
|
646
|
+
}
|
|
647
|
+
function finishConfigWrite(target, sel, file) {
|
|
648
|
+
if (fileCacheAvailable())
|
|
649
|
+
fileCache.invalidate(file);
|
|
650
|
+
if (sel?.self && (target === ConfigTarget.Agent || target === ConfigTarget.RoleRegistry || target === ConfigTarget.Role)) {
|
|
651
|
+
clearRoleStoreCache(sel.self);
|
|
652
|
+
}
|
|
653
|
+
const event = { target, selector: { ...(sel ?? {}) } };
|
|
654
|
+
for (const listener of configWriteListeners) {
|
|
655
|
+
try {
|
|
656
|
+
listener(event);
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
configWarn(`[config] write listener failed for ${target}: ${error instanceof Error ? error.message : String(error)}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
/** Notify cache and security listeners after a caller-owned atomic transaction commits. */
|
|
664
|
+
export function notifyConfigWrite(target, sel) {
|
|
665
|
+
finishConfigWrite(target, sel, targetPath(target, sel));
|
|
666
|
+
}
|
|
667
|
+
function ensureSchemaVersion(value, version) {
|
|
668
|
+
return value && typeof value === 'object'
|
|
669
|
+
? { ...value, $schema_version: version }
|
|
670
|
+
: value;
|
|
671
|
+
}
|
|
672
|
+
function validateOrThrow(schema, value, target) {
|
|
673
|
+
const ok = schema.validate(value);
|
|
674
|
+
const scopeErrors = validateProcessOnlyFields(target, value);
|
|
675
|
+
if (!ok || scopeErrors.length > 0) {
|
|
676
|
+
const errs = [
|
|
677
|
+
...scopeErrors,
|
|
678
|
+
...(schema.validate.errors || []).map(e => {
|
|
679
|
+
const extra = typeof e.params?.additionalProperty === 'string'
|
|
680
|
+
? ` (additionalProperty=${e.params.additionalProperty})`
|
|
681
|
+
: '';
|
|
682
|
+
return `${e.instancePath || '/'} ${e.message}${extra}`;
|
|
683
|
+
}),
|
|
684
|
+
].join('; ');
|
|
685
|
+
throw new ConfigError('SCHEMA_INVALID', `${target} 配置不合 schema(${schema.logicalName}.v${schema.version}): ${errs}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function validateProcessOnlyFields(target, value) {
|
|
689
|
+
if (target === ConfigTarget.Process || !value || typeof value !== 'object')
|
|
690
|
+
return [];
|
|
691
|
+
const debug = value.debug;
|
|
692
|
+
if (!debug || typeof debug !== 'object' || !Object.prototype.hasOwnProperty.call(debug, 'eckSnapshots'))
|
|
693
|
+
return [];
|
|
694
|
+
return ['/debug/eckSnapshots is only allowed in process config'];
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* responseModeParams 桶专项校验。
|
|
698
|
+
*
|
|
699
|
+
* 主 config schema 把 responseModeParams 当黑箱({type:object})放行,桶内部由本函数
|
|
700
|
+
* 用「模式桶 schema」逐桶校验:桶键 = 响应模式 id,须是已在 _meta.json 登记的 schema。
|
|
701
|
+
* - 未登记的桶键:收集后统一报错,点名列出,提示用 `ec response list` 查可用模式;
|
|
702
|
+
* - 已登记:用 loadSchema(modeId) 校验该桶内容(enum / additionalProperties 等)。
|
|
703
|
+
*
|
|
704
|
+
* 纯 schema 驱动,不 import response-system(避免 config→response-system 反向依赖);
|
|
705
|
+
* 「桶键≡已注册模式」经由「模式必带同名 schema 且登记进 _meta」这一约定保证。
|
|
706
|
+
*/
|
|
707
|
+
function validateResponseModeParams(params, target) {
|
|
708
|
+
if (!params || typeof params !== 'object' || Array.isArray(params))
|
|
709
|
+
return;
|
|
710
|
+
const unregistered = [];
|
|
711
|
+
for (const [modeId, bucket] of Object.entries(params)) {
|
|
712
|
+
if (!isSchemaName(modeId)) {
|
|
713
|
+
unregistered.push(modeId);
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const modeSchema = loadSchema(modeId);
|
|
717
|
+
if (!modeSchema.validate(bucket)) {
|
|
718
|
+
const errs = (modeSchema.validate.errors || [])
|
|
719
|
+
.map(e => {
|
|
720
|
+
const extra = typeof e.params?.additionalProperty === 'string'
|
|
721
|
+
? ` (additionalProperty=${e.params.additionalProperty})`
|
|
722
|
+
: '';
|
|
723
|
+
return `${e.instancePath || '/'} ${e.message}${extra}`;
|
|
724
|
+
})
|
|
725
|
+
.join('; ');
|
|
726
|
+
throw new ConfigError('SCHEMA_INVALID', `${target} responseModeParams["${modeId}"] 不合模式 schema(${modeSchema.logicalName}.v${modeSchema.version}): ${errs}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (unregistered.length > 0) {
|
|
730
|
+
throw new ConfigError('SCHEMA_INVALID', `${target} responseModeParams 含未注册的响应模式桶键: ${unregistered.map(k => `"${k}"`).join(', ')}。` +
|
|
731
|
+
`桶键必须对应已注册的响应模式(用 \`ec response list\` 查看可用模式)。`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* 纯 schema 校验(不写盘、无副作用)。返回错误信息数组,空数组表示通过。
|
|
736
|
+
* 供 config-store.validateAgentConfig 等只校验不落盘的场景使用。
|
|
737
|
+
*/
|
|
738
|
+
export function validateConfig(target, value) {
|
|
739
|
+
const schema = loadSchema(TARGET_SCHEMA[target]);
|
|
740
|
+
const withVer = ensureSchemaVersion(value, schema.version);
|
|
741
|
+
const ok = schema.validate(withVer);
|
|
742
|
+
const errors = validateProcessOnlyFields(target, withVer);
|
|
743
|
+
if (ok && errors.length === 0)
|
|
744
|
+
return [];
|
|
745
|
+
return [
|
|
746
|
+
...errors,
|
|
747
|
+
...(schema.validate.errors || []).map(e => {
|
|
748
|
+
const extra = typeof e.params?.additionalProperty === 'string'
|
|
749
|
+
? ` (additionalProperty=${e.params.additionalProperty})`
|
|
750
|
+
: '';
|
|
751
|
+
return `${e.instancePath || '/'} ${e.message}${extra}`;
|
|
752
|
+
}),
|
|
753
|
+
];
|
|
754
|
+
}
|
|
755
|
+
export function validateConfigFile(target, sel) {
|
|
756
|
+
const value = atomicReadJson(targetPath(target, sel));
|
|
757
|
+
if (value === null)
|
|
758
|
+
return { exists: false, errors: [] };
|
|
759
|
+
return { exists: true, errors: validateConfig(target, value) };
|
|
760
|
+
}
|
|
761
|
+
function fileCacheAvailable() {
|
|
762
|
+
try {
|
|
763
|
+
return !!fileCache;
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// ── ensureFile ────────────────────────────────────────────────────────────
|
|
770
|
+
/** 按 schema 的 required/default 生成骨架并写入(幂等:已存在不覆盖)。 */
|
|
771
|
+
export function ensureFile(target, sel) {
|
|
772
|
+
const file = targetPath(target, sel);
|
|
773
|
+
if (fs.existsSync(file))
|
|
774
|
+
return;
|
|
775
|
+
const schema = loadSchema(TARGET_SCHEMA[target]);
|
|
776
|
+
const skeleton = { $schema_version: schema.version };
|
|
777
|
+
const props = schema.raw.properties || {};
|
|
778
|
+
const required = schema.raw.required || [];
|
|
779
|
+
for (const key of required) {
|
|
780
|
+
if (key === '$schema_version')
|
|
781
|
+
continue;
|
|
782
|
+
if (key === 'aid' && sel?.self) {
|
|
783
|
+
skeleton.aid = sel.self;
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
const spec = props[key];
|
|
787
|
+
if (spec && 'default' in spec)
|
|
788
|
+
skeleton[key] = spec.default;
|
|
789
|
+
else if (spec?.type === 'array')
|
|
790
|
+
skeleton[key] = [];
|
|
791
|
+
else if (spec?.type === 'object')
|
|
792
|
+
skeleton[key] = {};
|
|
793
|
+
}
|
|
794
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
795
|
+
atomicWriteJson(file, skeleton);
|
|
796
|
+
}
|
|
797
|
+
// ── schema 版本迁移 ───────────────────────────────────────────────────────
|
|
798
|
+
//
|
|
799
|
+
// 同步路径只做版本号判断和告警。跨版本迁移由部署期一次性脚本完成,
|
|
800
|
+
// 避免基础设施热路径长期承载会静默改变权限策略的业务迁移逻辑。
|
|
801
|
+
function migrateIfNeeded(target, raw, file) {
|
|
802
|
+
const logical = TARGET_SCHEMA[target];
|
|
803
|
+
const cur = currentVersion(logical);
|
|
804
|
+
const have = raw?.$schema_version;
|
|
805
|
+
if (typeof have === 'number' && have < cur && schemaVersionWarningsEnabled) {
|
|
806
|
+
configWarn(`[config] ${file}: $schema_version ${have} < current ${cur} for "${logical}" - run kits/migrations/migrate-role-config-v4.mjs`);
|
|
807
|
+
}
|
|
808
|
+
return raw;
|
|
809
|
+
}
|
|
810
|
+
export function resolveAgentConfig(sel, opts = {}) {
|
|
811
|
+
// Use agent-config as the H-chain field table, but static owners only come from agent config.
|
|
812
|
+
const fields = loadSchema('agent-config').fields;
|
|
813
|
+
const agentConfig = sel.self ? read(ConfigTarget.Agent, sel, opts) : null;
|
|
814
|
+
const relationConfig = (sel.self && sel.peerKey) ? read(ConfigTarget.Relation, sel, opts) : null;
|
|
815
|
+
const layers = [
|
|
816
|
+
stripStaticOwnerField(read(ConfigTarget.Defaults, undefined, opts)),
|
|
817
|
+
agentConfig,
|
|
818
|
+
stripRelationRoleData(stripStaticOwnerField(relationConfig)),
|
|
819
|
+
];
|
|
820
|
+
const merged = mergeLayers(layers, fields);
|
|
821
|
+
return merged;
|
|
822
|
+
}
|
|
823
|
+
function stripStaticOwnerField(config) {
|
|
824
|
+
if (!config)
|
|
825
|
+
return config;
|
|
826
|
+
if (!Object.prototype.hasOwnProperty.call(config, 'owners') && !Object.prototype.hasOwnProperty.call(config, 'admins'))
|
|
827
|
+
return config;
|
|
828
|
+
const { owners: _owners, admins: _admins, ...rest } = config;
|
|
829
|
+
return rest;
|
|
830
|
+
}
|
|
831
|
+
function stripRelationRoleData(config) {
|
|
832
|
+
if (!config || (!Object.prototype.hasOwnProperty.call(config, 'role')
|
|
833
|
+
&& !Object.prototype.hasOwnProperty.call(config, 'roles')))
|
|
834
|
+
return config;
|
|
835
|
+
const { role: _role, roles: _legacyRoles, ...rest } = config;
|
|
836
|
+
return rest;
|
|
837
|
+
}
|
|
838
|
+
// ── effective(合并视图)─────────────────────────────────────────────────
|
|
839
|
+
/**
|
|
840
|
+
* 合并视图:只合并物理覆盖链 defaults → agent/config → relation/config。
|
|
841
|
+
*/
|
|
842
|
+
export function resolveEffective(sel, opts = {}) {
|
|
843
|
+
const config = resolveAgentConfig(sel, opts);
|
|
844
|
+
const effective = {
|
|
845
|
+
$schema_version: config.$schema_version ?? currentVersion('agent-config'),
|
|
846
|
+
aid: config.aid ?? sel.self ?? '',
|
|
847
|
+
enabled: config.enabled,
|
|
848
|
+
lifecycle: config.lifecycle,
|
|
849
|
+
initialized: config.initialized,
|
|
850
|
+
owners: config.owners,
|
|
851
|
+
aun: config.aun,
|
|
852
|
+
channels: config.channels ?? [],
|
|
853
|
+
models: config.models,
|
|
854
|
+
projects: config.projects,
|
|
855
|
+
capabilities: config.capabilities,
|
|
856
|
+
debug: config.debug,
|
|
857
|
+
observable: config.observable,
|
|
858
|
+
extra_backup: config.extra_backup,
|
|
859
|
+
// Runtime configuration parameters
|
|
860
|
+
active_baseagent: config.active_baseagent,
|
|
861
|
+
baseagents: config.baseagents,
|
|
862
|
+
chatmode: config.chatmode,
|
|
863
|
+
mentionMode: config.mentionMode,
|
|
864
|
+
model: config.model,
|
|
865
|
+
responseMode: config.responseMode,
|
|
866
|
+
responseModeParams: config.responseModeParams,
|
|
867
|
+
flush_delay: config.flush_delay,
|
|
868
|
+
debounce: config.debounce,
|
|
869
|
+
show_activities: config.show_activities,
|
|
870
|
+
session_renew: config.session_renew,
|
|
871
|
+
render: config.render,
|
|
872
|
+
sessionManifests: config.sessionManifests,
|
|
873
|
+
enable_rich_content: config.enable_rich_content,
|
|
874
|
+
};
|
|
875
|
+
return normalizeEffectiveCompatibility(effective);
|
|
876
|
+
}
|
|
877
|
+
function normalizeEffectiveCompatibility(effective) {
|
|
878
|
+
normalizeShowActivitiesCompat(effective);
|
|
879
|
+
const behavior = {};
|
|
880
|
+
for (const field of EFFECTIVE_BEHAVIOR_FIELDS) {
|
|
881
|
+
if (effective[field] !== undefined)
|
|
882
|
+
behavior[field] = effective[field];
|
|
883
|
+
}
|
|
884
|
+
if (Object.keys(behavior).length > 0) {
|
|
885
|
+
effective.behavior = behavior;
|
|
886
|
+
}
|
|
887
|
+
return effective;
|
|
888
|
+
}
|
|
889
|
+
const EFFECTIVE_BEHAVIOR_FIELDS = [
|
|
890
|
+
'active_baseagent',
|
|
891
|
+
'baseagents',
|
|
892
|
+
'chatmode',
|
|
893
|
+
'mentionMode',
|
|
894
|
+
'flush_delay',
|
|
895
|
+
'debounce',
|
|
896
|
+
'show_activities',
|
|
897
|
+
'session_renew',
|
|
898
|
+
'render',
|
|
899
|
+
'sessionManifests',
|
|
900
|
+
'enable_rich_content',
|
|
901
|
+
];
|
|
902
|
+
/**
|
|
903
|
+
* 给定 selector 作用域 + 顶层字段名,判定写入落点。
|
|
904
|
+
* 兼容旧调用:仅按顶层字段路由。新写入请优先使用 routeFieldPath()。
|
|
905
|
+
*/
|
|
906
|
+
export function routeField(topField, scope) {
|
|
907
|
+
return routeFieldPath(topField, scope);
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* 给定 selector 作用域 + 完整字段路径,判定 canonical 写入落点。
|
|
911
|
+
* v3 设计:所有参数统一在 config.json,按作用域路由到对应层级。
|
|
912
|
+
*/
|
|
913
|
+
export function routeFieldPath(fieldPath, scope) {
|
|
914
|
+
const topField = fieldPath.split('.')[0];
|
|
915
|
+
if (scope === 'process')
|
|
916
|
+
return routeIn(DAEMON_SCHEMA_NAME, ConfigTarget.Process, topField);
|
|
917
|
+
if (scope === 'defaults') {
|
|
918
|
+
if (isBehaviorConfigFieldPath(fieldPath) && topField !== 'session_renew') {
|
|
919
|
+
throw new ConfigError('DEFAULT_BEHAVIOR_REJECT', `--default 不支持行为字段: ${fieldPath}`);
|
|
920
|
+
}
|
|
921
|
+
return routeIn('defaults', ConfigTarget.Defaults, topField);
|
|
922
|
+
}
|
|
923
|
+
if (isBehaviorConfigFieldPath(fieldPath)) {
|
|
924
|
+
// v3 设计:行为字段按作用域路由到对应 schema
|
|
925
|
+
if (scope === 'agent') {
|
|
926
|
+
return routeIn('agent-config', ConfigTarget.Agent, topField);
|
|
927
|
+
}
|
|
928
|
+
// relation 作用域:行为字段位于 relation-config
|
|
929
|
+
return routeIn('relation-config', ConfigTarget.Relation, topField);
|
|
930
|
+
}
|
|
931
|
+
if (scope === 'agent')
|
|
932
|
+
return routeIn('agent-config', ConfigTarget.Agent, topField);
|
|
933
|
+
return routeIn('relation-config', ConfigTarget.Relation, topField);
|
|
934
|
+
}
|
|
935
|
+
function routeIn(name, target, topField) {
|
|
936
|
+
// $schema_version 是元数据字段,特殊处理
|
|
937
|
+
if (topField === '$schema_version') {
|
|
938
|
+
return {
|
|
939
|
+
field: topField,
|
|
940
|
+
target,
|
|
941
|
+
schema: name,
|
|
942
|
+
merge: 'replace',
|
|
943
|
+
enum: undefined
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const s = loadSchema(name);
|
|
947
|
+
if (!s.fields.has(topField)) {
|
|
948
|
+
const known = [...s.fields.keys()].sort();
|
|
949
|
+
const suggestion = suggestField(topField, known);
|
|
950
|
+
throw new ConfigError('UNKNOWN_FIELD', `Unknown config field "${topField}" for ${name}.`
|
|
951
|
+
+ (suggestion ? ` Did you mean "${suggestion}"?` : '')
|
|
952
|
+
+ ` Run "ec config fields" to list valid fields.`);
|
|
953
|
+
}
|
|
954
|
+
return mkRoute(s, target, topField);
|
|
955
|
+
}
|
|
956
|
+
/** 为未知字段找一个最接近的合法字段名(编辑距离 ≤2 才提示),用于 UNKNOWN_FIELD 纠错。 */
|
|
957
|
+
function suggestField(input, candidates) {
|
|
958
|
+
let best;
|
|
959
|
+
let bestDist = Infinity;
|
|
960
|
+
for (const c of candidates) {
|
|
961
|
+
const d = editDistance(input.toLowerCase(), c.toLowerCase());
|
|
962
|
+
if (d < bestDist) {
|
|
963
|
+
bestDist = d;
|
|
964
|
+
best = c;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return bestDist <= 2 ? best : undefined;
|
|
968
|
+
}
|
|
969
|
+
function editDistance(a, b) {
|
|
970
|
+
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
971
|
+
for (let j = 0; j <= b.length; j++)
|
|
972
|
+
dp[0][j] = j;
|
|
973
|
+
for (let i = 1; i <= a.length; i++) {
|
|
974
|
+
for (let j = 1; j <= b.length; j++) {
|
|
975
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
976
|
+
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return dp[a.length][b.length];
|
|
980
|
+
}
|
|
981
|
+
function mkRoute(s, target, topField) {
|
|
982
|
+
const spec = s.fields.get(topField);
|
|
983
|
+
return { field: topField, target, schema: s.logicalName, merge: spec.merge, enum: spec.enum };
|
|
984
|
+
}
|
|
985
|
+
/** 列出某作用域下所有可配置字段(供 ec config fields 使用)。 */
|
|
986
|
+
export function listFields(scope) {
|
|
987
|
+
const out = [];
|
|
988
|
+
const add = (name, target) => {
|
|
989
|
+
const s = loadSchema(name);
|
|
990
|
+
for (const f of s.fields.keys())
|
|
991
|
+
out.push(mkRoute(s, target, f));
|
|
992
|
+
};
|
|
993
|
+
if (scope === 'process') {
|
|
994
|
+
add(DAEMON_SCHEMA_NAME, ConfigTarget.Process);
|
|
995
|
+
return out;
|
|
996
|
+
}
|
|
997
|
+
if (scope === 'defaults') {
|
|
998
|
+
add('defaults', ConfigTarget.Defaults);
|
|
999
|
+
return out;
|
|
1000
|
+
}
|
|
1001
|
+
if (scope === 'agent') {
|
|
1002
|
+
add('agent-config', ConfigTarget.Agent);
|
|
1003
|
+
return out;
|
|
1004
|
+
}
|
|
1005
|
+
// relation
|
|
1006
|
+
add('relation-config', ConfigTarget.Relation);
|
|
1007
|
+
return out;
|
|
1008
|
+
}
|
|
1009
|
+
export { ConfigTarget as Target };
|
|
1010
|
+
/**
|
|
1011
|
+
* 读取单个字段并返回来源信息。
|
|
1012
|
+
* 沿覆盖链查找,返回第一个定义该字段的层级。
|
|
1013
|
+
*/
|
|
1014
|
+
export function readConfiguredFieldWithSource(field, sel, opts = {}) {
|
|
1015
|
+
const parts = field.split('.');
|
|
1016
|
+
const topField = parts[0];
|
|
1017
|
+
// 按覆盖链顺序查找:relation → agent → defaults
|
|
1018
|
+
const targets = [];
|
|
1019
|
+
if (sel.self && sel.peerKey)
|
|
1020
|
+
targets.push(ConfigTarget.Relation);
|
|
1021
|
+
if (sel.self)
|
|
1022
|
+
targets.push(ConfigTarget.Agent);
|
|
1023
|
+
targets.push(ConfigTarget.Defaults);
|
|
1024
|
+
for (const target of targets) {
|
|
1025
|
+
const config = read(target, sel, opts);
|
|
1026
|
+
if (!config)
|
|
1027
|
+
continue;
|
|
1028
|
+
// 检查顶层字段是否存在
|
|
1029
|
+
if (!(topField in config))
|
|
1030
|
+
continue;
|
|
1031
|
+
// 提取字段值(支持嵌套)
|
|
1032
|
+
let value = config[topField];
|
|
1033
|
+
for (let i = 1; i < parts.length; i++) {
|
|
1034
|
+
if (value && typeof value === 'object' && parts[i] in value) {
|
|
1035
|
+
value = value[parts[i]];
|
|
1036
|
+
}
|
|
1037
|
+
else {
|
|
1038
|
+
value = undefined;
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// 如果值存在,返回并携带来源
|
|
1043
|
+
if (value !== undefined) {
|
|
1044
|
+
const file = path.relative(resolvePaths().root, targetPath(target, sel));
|
|
1045
|
+
return {
|
|
1046
|
+
value,
|
|
1047
|
+
source: { target, file }
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
/** Raw configured-layer lookup retained as the public ConfigManager API. */
|
|
1054
|
+
export function readFieldWithSource(field, sel, opts = {}) {
|
|
1055
|
+
return readConfiguredFieldWithSource(field, sel, opts);
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* Resolve effective values and their deciding source through one implementation.
|
|
1059
|
+
* Without fields, all leaf paths are returned as a flat dot-path map.
|
|
1060
|
+
*/
|
|
1061
|
+
export function resolveEffectiveWithSources(sel, opts = {}) {
|
|
1062
|
+
const { fields, ...readOpts } = opts;
|
|
1063
|
+
const effective = resolveEffective(sel, readOpts);
|
|
1064
|
+
const paths = fields ?? listEffectiveLeafPaths(effective);
|
|
1065
|
+
const result = {};
|
|
1066
|
+
for (const field of paths) {
|
|
1067
|
+
const configured = readConfiguredFieldWithSource(field, sel, readOpts);
|
|
1068
|
+
const value = readNestedField(effective, field);
|
|
1069
|
+
result[field] = {
|
|
1070
|
+
value,
|
|
1071
|
+
source: configured?.source ?? null,
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
return result;
|
|
1075
|
+
}
|
|
1076
|
+
/** Thin single-field view over resolveEffectiveWithSources(). */
|
|
1077
|
+
export function resolveEffectiveFieldWithSource(field, sel, opts = {}) {
|
|
1078
|
+
return (resolveEffectiveWithSources(sel, { ...opts, fields: [field] })[field]
|
|
1079
|
+
?? { value: undefined, source: null });
|
|
1080
|
+
}
|
|
1081
|
+
function readNestedField(value, field) {
|
|
1082
|
+
let current = value;
|
|
1083
|
+
for (const part of field.split('.')) {
|
|
1084
|
+
if (current == null || typeof current !== 'object')
|
|
1085
|
+
return undefined;
|
|
1086
|
+
current = current[part];
|
|
1087
|
+
}
|
|
1088
|
+
return current;
|
|
1089
|
+
}
|
|
1090
|
+
function listEffectiveLeafPaths(value) {
|
|
1091
|
+
const paths = [];
|
|
1092
|
+
const visit = (current, prefix) => {
|
|
1093
|
+
if (current === undefined)
|
|
1094
|
+
return;
|
|
1095
|
+
if (!current || typeof current !== 'object' || Array.isArray(current)) {
|
|
1096
|
+
if (prefix)
|
|
1097
|
+
paths.push(prefix);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
const entries = Object.entries(current).filter(([, child]) => child !== undefined);
|
|
1101
|
+
// Dotted dictionary keys (for example role field permissions) cannot be
|
|
1102
|
+
// represented unambiguously as another dot-path segment.
|
|
1103
|
+
if (prefix && entries.some(([key]) => key.includes('.'))) {
|
|
1104
|
+
paths.push(prefix);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (entries.length === 0) {
|
|
1108
|
+
if (prefix)
|
|
1109
|
+
paths.push(prefix);
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
for (const [key, child] of entries) {
|
|
1113
|
+
if (!prefix && key === 'behavior')
|
|
1114
|
+
continue; // compatibility mirror of top-level behavior fields
|
|
1115
|
+
visit(child, prefix ? `${prefix}.${key}` : key);
|
|
1116
|
+
}
|
|
1117
|
+
};
|
|
1118
|
+
visit(value, '');
|
|
1119
|
+
return paths;
|
|
1120
|
+
}
|