evolcore 0.0.3 → 0.0.4
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/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +1 -31
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +157 -5
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/channels/aun.js +220 -112
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +52 -137
- package/dist/channels/feishu.js +56 -4
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +383 -7
- package/dist/cli/aun-commands.js +11 -11
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +7 -5
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +27 -14
- package/dist/config/builtin-roles.js +25 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +198 -22
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +173 -5
- package/dist/config/contact-book.js +32 -0
- package/dist/config/contact-operation-service.js +9 -3
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +3 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -2
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +6 -0
- package/dist/core/auth/operation-authorizer.js +8 -0
- package/dist/core/auth/operation-catalog.js +51 -3
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +10 -13
- package/dist/core/command/connect-menu.js +249 -24
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +269 -85
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +158 -31
- package/dist/core/daemon-file-cache.js +28 -0
- package/dist/core/event-catalog.js +29 -0
- package/dist/core/evolagent-registry.js +4 -39
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +18 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +616 -74
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +185 -10
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +381 -79
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +69 -24
- package/dist/core/protected-paths.js +36 -11
- package/dist/core/session/session-manager.js +25 -3
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +224 -89
- package/dist/ipc.js +81 -4
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/trigger/parser.js +55 -15
- package/dist/trigger/patch.js +4 -1
- package/dist/trigger/scheduler.js +441 -39
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +7 -3
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +4 -4
- package/kits/docs/evolcore/msg.md +5 -5
- package/kits/docs/evolcore/trigger.md +25 -8
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +18 -7
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +46 -0
- package/kits/templates/roles/member.json +4 -0
- package/kits/templates/roles/visitor.json +4 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +4 -2
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/dist/channels/contact-bind-code.js +0 -134
- package/dist/channels/wecom-card.js +0 -101
- package/dist/channels/wecom-onboarding.js +0 -82
- package/dist/channels/wecom-state.js +0 -191
|
@@ -12,19 +12,22 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
|
+
import crypto from 'crypto';
|
|
15
16
|
import { resolvePaths, agentConfig as agentConfigPath, agentContactConfig, agentRelationConfig, agentDir, agentRelationsDir, agentRolesIndex, agentRoleConfig, } from '../paths.js';
|
|
16
17
|
import { atomicReadJson, atomicWriteJson } from '../utils/atomic-write.js';
|
|
17
18
|
import { fileCache } from '../core/daemon-file-cache.js';
|
|
18
19
|
import { isValidAid } from '../aun/aid/validation.js';
|
|
19
20
|
import { isExplicitGroupId } from '../aun/group-identity.js';
|
|
20
21
|
import { parseContactAlias } from './contact-alias.js';
|
|
21
|
-
import { loadSchema, currentVersion, isSchemaName, } from './schema-registry.js';
|
|
22
|
+
import { loadSchema, listSchemaVersions, currentVersion, isSchemaName, } from './schema-registry.js';
|
|
22
23
|
import { mergeLayers, expandVars, buildEnvResolver } from './merge.js';
|
|
23
24
|
import { normalizeAgentLifecycle } from './lifecycle.js';
|
|
24
25
|
import { isBehaviorConfigFieldPath } from './config-field-policy.js';
|
|
25
26
|
import { clearRoleStoreCache, isRoleName } from './role-store.js';
|
|
26
27
|
import { writeRoleDefinition, writeRoleRegistry } from './role-service.js';
|
|
28
|
+
import { mutateContactBookTransactionSync } from './contact-book-store.js';
|
|
27
29
|
import { DAEMON_SCHEMA_NAME } from '../product.js';
|
|
30
|
+
import { validateAccessPolicyDomain } from './access-policy-domain.js';
|
|
28
31
|
export var ConfigTarget;
|
|
29
32
|
(function (ConfigTarget) {
|
|
30
33
|
ConfigTarget["Process"] = "process";
|
|
@@ -36,6 +39,8 @@ export var ConfigTarget;
|
|
|
36
39
|
ConfigTarget["Role"] = "role";
|
|
37
40
|
})(ConfigTarget || (ConfigTarget = {}));
|
|
38
41
|
const configWriteListeners = new Set();
|
|
42
|
+
const CONFIG_MUTATION_LOCK_WAIT_MS = 5_000;
|
|
43
|
+
const CONFIG_MUTATION_LOCK_RETRY_MS = 20;
|
|
39
44
|
/** Observe successful in-process config writes after cache invalidation. */
|
|
40
45
|
export function onConfigWrite(listener) {
|
|
41
46
|
configWriteListeners.add(listener);
|
|
@@ -224,10 +229,13 @@ export function assertContactBookV2Ready() {
|
|
|
224
229
|
if (!fs.existsSync(agentsRoot))
|
|
225
230
|
return;
|
|
226
231
|
const issues = [];
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
232
|
+
const supportedSchemas = (name, minimumVersion) => (listSchemaVersions(name)
|
|
233
|
+
.filter(version => version >= minimumVersion)
|
|
234
|
+
.map(version => loadSchema(name, version)));
|
|
235
|
+
const agentSchemas = supportedSchemas('agent-config', 6);
|
|
236
|
+
const contactSchemas = supportedSchemas('contact-book', 2);
|
|
237
|
+
const relationSchemas = supportedSchemas('relation-config', 5);
|
|
238
|
+
const inspect = (file, schemas) => {
|
|
231
239
|
let value;
|
|
232
240
|
try {
|
|
233
241
|
value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
@@ -236,8 +244,9 @@ export function assertContactBookV2Ready() {
|
|
|
236
244
|
issues.push({ file, detail: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` });
|
|
237
245
|
return;
|
|
238
246
|
}
|
|
239
|
-
|
|
240
|
-
|
|
247
|
+
const candidates = Array.isArray(schemas) ? schemas : [schemas];
|
|
248
|
+
if (!candidates.some(schema => schema.validate(value))) {
|
|
249
|
+
const detail = (candidates.at(-1)?.validate.errors ?? [])
|
|
241
250
|
.map(error => `${error.instancePath || '/'} ${error.message}`)
|
|
242
251
|
.join('; ');
|
|
243
252
|
issues.push({ file, detail });
|
|
@@ -263,14 +272,14 @@ export function assertContactBookV2Ready() {
|
|
|
263
272
|
const agentFile = path.join(dir, 'config.json');
|
|
264
273
|
if (!fs.existsSync(agentFile))
|
|
265
274
|
continue;
|
|
266
|
-
inspect(agentFile,
|
|
275
|
+
inspect(agentFile, agentSchemas);
|
|
267
276
|
const contactFile = path.join(dir, 'contact.json');
|
|
268
277
|
if (fs.existsSync(contactFile)) {
|
|
269
|
-
inspect(contactFile,
|
|
278
|
+
inspect(contactFile, contactSchemas);
|
|
270
279
|
inspectContactBookDomain(contactFile, agentFile, issues);
|
|
271
280
|
}
|
|
272
281
|
for (const relationFile of walkRelationConfigs(path.join(dir, 'relations'))) {
|
|
273
|
-
inspect(relationFile,
|
|
282
|
+
inspect(relationFile, relationSchemas);
|
|
274
283
|
}
|
|
275
284
|
}
|
|
276
285
|
const correctionReceipt = path.join(root, 'data', 'contact-book-v2-relation-role-authority.json');
|
|
@@ -394,7 +403,7 @@ function findRelationRoleRemovedByExperimentalMigration(root) {
|
|
|
394
403
|
catch {
|
|
395
404
|
continue;
|
|
396
405
|
}
|
|
397
|
-
if (currentValue?.$schema_version === 5) {
|
|
406
|
+
if (typeof currentValue?.$schema_version === 'number' && currentValue.$schema_version >= 5) {
|
|
398
407
|
return currentFile;
|
|
399
408
|
}
|
|
400
409
|
}
|
|
@@ -621,7 +630,12 @@ export function write(target, value, sel, opts = {}) {
|
|
|
621
630
|
finishConfigWrite(target, sel, file);
|
|
622
631
|
return;
|
|
623
632
|
}
|
|
624
|
-
const
|
|
633
|
+
const requestedVersion = value && typeof value === 'object'
|
|
634
|
+
? Number(value.$schema_version)
|
|
635
|
+
: NaN;
|
|
636
|
+
const schema = target === ConfigTarget.Contact && requestedVersion === 2
|
|
637
|
+
? loadSchema('contact-book', 2)
|
|
638
|
+
: loadSchema(TARGET_SCHEMA[target]);
|
|
625
639
|
const withVer = ensureSchemaVersion(value, schema.version);
|
|
626
640
|
const migrated = withVer;
|
|
627
641
|
// Agent config 写入规范化(aid 校验、projects 字段清理)
|
|
@@ -635,15 +649,166 @@ export function write(target, value, sel, opts = {}) {
|
|
|
635
649
|
validateOrThrow(schema, normalized, target);
|
|
636
650
|
// 1b. responseModeParams 桶内容校验:主 schema 把 responseModeParams 当黑箱,
|
|
637
651
|
// 此处使用“模式桶 schema”逐桶校验内容(桶键 = 已登记的响应模式 id)。
|
|
638
|
-
if (target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
|
|
652
|
+
if (target === ConfigTarget.Defaults || target === ConfigTarget.Agent || target === ConfigTarget.Relation) {
|
|
639
653
|
validateResponseModeParams(normalized?.responseModeParams, target);
|
|
640
654
|
}
|
|
655
|
+
if (target === ConfigTarget.Agent) {
|
|
656
|
+
const selfAid = sel?.self || String(normalized?.aid || '').trim();
|
|
657
|
+
const issue = validateAccessPolicyDomain(selfAid, normalized);
|
|
658
|
+
if (issue)
|
|
659
|
+
throw new ConfigError(issue.code, issue.message);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// Owner 是实际联系人。将 Agent 配置和缺失的 Owner 联系人写入同一可恢复事务,
|
|
663
|
+
// 避免渠道身份绑定承担联系人创建职责。
|
|
664
|
+
const ownerIds = target === ConfigTarget.Agent && Array.isArray(normalized.owners)
|
|
665
|
+
? [...new Set(normalized.owners
|
|
666
|
+
.map((value) => String(value || '').trim())
|
|
667
|
+
.filter((value) => isValidAid(value)))]
|
|
668
|
+
: [];
|
|
669
|
+
if (target === ConfigTarget.Agent && sel?.self && ownerIds.length > 0) {
|
|
670
|
+
const before = readTextOrNull(file);
|
|
671
|
+
const after = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
672
|
+
mutateContactBookTransactionSync({
|
|
673
|
+
selfAid: sel.self,
|
|
674
|
+
actor: 'agent-owner-sync',
|
|
675
|
+
mutation: { type: 'ensure-contacts', primaryIds: ownerIds },
|
|
676
|
+
}, [{ file, before, after }]);
|
|
677
|
+
finishConfigWrite(target, sel, file);
|
|
678
|
+
return;
|
|
641
679
|
}
|
|
642
680
|
// 2. 写入文件
|
|
643
681
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
644
682
|
atomicWriteJson(file, normalized);
|
|
645
683
|
finishConfigWrite(target, sel, file);
|
|
646
684
|
}
|
|
685
|
+
/**
|
|
686
|
+
* Mutate one configuration document while holding a file-scoped, cross-process lock.
|
|
687
|
+
*
|
|
688
|
+
* Callers must keep the callback synchronous. This is the safe primitive for
|
|
689
|
+
* read-modify-write paths; direct `read()` followed by `write()` cannot protect
|
|
690
|
+
* unrelated fields from another process writing the same config document.
|
|
691
|
+
*/
|
|
692
|
+
export function mutateConfig(target, sel, mutate) {
|
|
693
|
+
if (target === ConfigTarget.RoleRegistry || target === ConfigTarget.Role || target === ConfigTarget.Contact) {
|
|
694
|
+
throw new ConfigError('MUTATION_UNSUPPORTED', `mutateConfig does not support ${target}`);
|
|
695
|
+
}
|
|
696
|
+
const file = targetPath(target, sel);
|
|
697
|
+
const release = acquireConfigMutationLock(file);
|
|
698
|
+
try {
|
|
699
|
+
const current = read(target, sel, { cache: false });
|
|
700
|
+
const next = current ? cloneConfigValue(current) : configSkeleton(target, sel);
|
|
701
|
+
const before = stableConfigValue(current ?? next);
|
|
702
|
+
const value = mutate(next);
|
|
703
|
+
const changed = before !== stableConfigValue(next);
|
|
704
|
+
if (changed)
|
|
705
|
+
write(target, next, sel);
|
|
706
|
+
return { value, changed };
|
|
707
|
+
}
|
|
708
|
+
finally {
|
|
709
|
+
release();
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
function cloneConfigValue(value) {
|
|
713
|
+
return JSON.parse(JSON.stringify(value));
|
|
714
|
+
}
|
|
715
|
+
function stableConfigValue(value) {
|
|
716
|
+
return JSON.stringify(sortConfigValue(value));
|
|
717
|
+
}
|
|
718
|
+
function sortConfigValue(value) {
|
|
719
|
+
if (Array.isArray(value))
|
|
720
|
+
return value.map(sortConfigValue);
|
|
721
|
+
if (!value || typeof value !== 'object')
|
|
722
|
+
return value;
|
|
723
|
+
return Object.fromEntries(Object.entries(value)
|
|
724
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
725
|
+
.map(([key, nested]) => [key, sortConfigValue(nested)]));
|
|
726
|
+
}
|
|
727
|
+
function acquireConfigMutationLock(file) {
|
|
728
|
+
const lockDir = `${file}.mutation.lock`;
|
|
729
|
+
const ownerFile = path.join(lockDir, 'owner.json');
|
|
730
|
+
const token = crypto.randomBytes(16).toString('hex');
|
|
731
|
+
const candidate = `${lockDir}.candidate-${process.pid}-${token}`;
|
|
732
|
+
const candidateOwner = path.join(candidate, 'owner.json');
|
|
733
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
734
|
+
fs.mkdirSync(candidate);
|
|
735
|
+
fs.writeFileSync(candidateOwner, `${JSON.stringify({ pid: process.pid, token })}\n`, { mode: 0o600 });
|
|
736
|
+
let acquired = false;
|
|
737
|
+
const deadline = Date.now() + CONFIG_MUTATION_LOCK_WAIT_MS;
|
|
738
|
+
try {
|
|
739
|
+
while (true) {
|
|
740
|
+
try {
|
|
741
|
+
fs.renameSync(candidate, lockDir);
|
|
742
|
+
acquired = true;
|
|
743
|
+
return () => releaseConfigMutationLock(lockDir, ownerFile, token);
|
|
744
|
+
}
|
|
745
|
+
catch (error) {
|
|
746
|
+
if (error?.code !== 'EEXIST' && error?.code !== 'ENOTEMPTY')
|
|
747
|
+
throw error;
|
|
748
|
+
if (reclaimStaleConfigMutationLock(lockDir, ownerFile))
|
|
749
|
+
continue;
|
|
750
|
+
if (Date.now() >= deadline) {
|
|
751
|
+
throw new ConfigError('LOCK_TIMEOUT', `Timed out waiting for config mutation lock: ${file}`);
|
|
752
|
+
}
|
|
753
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, CONFIG_MUTATION_LOCK_RETRY_MS);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
finally {
|
|
758
|
+
if (!acquired)
|
|
759
|
+
fs.rmSync(candidate, { recursive: true, force: true });
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function reclaimStaleConfigMutationLock(lockDir, ownerFile) {
|
|
763
|
+
let raw;
|
|
764
|
+
let pid;
|
|
765
|
+
try {
|
|
766
|
+
raw = fs.readFileSync(ownerFile, 'utf8');
|
|
767
|
+
pid = Number(JSON.parse(raw).pid);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
773
|
+
return false;
|
|
774
|
+
try {
|
|
775
|
+
process.kill(pid, 0);
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
if (error?.code !== 'ESRCH')
|
|
780
|
+
return false;
|
|
781
|
+
const reclaimed = `${lockDir}.reclaimed-${crypto.createHash('sha256').update(raw).digest('hex').slice(0, 16)}`;
|
|
782
|
+
try {
|
|
783
|
+
fs.renameSync(lockDir, reclaimed);
|
|
784
|
+
fs.rmSync(reclaimed, { recursive: true, force: true });
|
|
785
|
+
return true;
|
|
786
|
+
}
|
|
787
|
+
catch (renameError) {
|
|
788
|
+
return renameError?.code === 'ENOENT';
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function releaseConfigMutationLock(lockDir, ownerFile, token) {
|
|
793
|
+
try {
|
|
794
|
+
const owner = JSON.parse(fs.readFileSync(ownerFile, 'utf8'));
|
|
795
|
+
if (owner.token === token)
|
|
796
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
797
|
+
}
|
|
798
|
+
catch {
|
|
799
|
+
// A replaced lock is not ours to release.
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function readTextOrNull(file) {
|
|
803
|
+
try {
|
|
804
|
+
return fs.readFileSync(file, 'utf8');
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
if (error?.code === 'ENOENT')
|
|
808
|
+
return null;
|
|
809
|
+
throw error;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
647
812
|
function finishConfigWrite(target, sel, file) {
|
|
648
813
|
if (fileCacheAvailable())
|
|
649
814
|
fileCache.invalidate(file);
|
|
@@ -688,10 +853,9 @@ function validateOrThrow(schema, value, target) {
|
|
|
688
853
|
function validateProcessOnlyFields(target, value) {
|
|
689
854
|
if (target === ConfigTarget.Process || !value || typeof value !== 'object')
|
|
690
855
|
return [];
|
|
691
|
-
|
|
692
|
-
if (!debug || typeof debug !== 'object' || !Object.prototype.hasOwnProperty.call(debug, 'eckSnapshots'))
|
|
856
|
+
if (!Object.prototype.hasOwnProperty.call(value, 'debug'))
|
|
693
857
|
return [];
|
|
694
|
-
return ['/debug
|
|
858
|
+
return ['/debug is only allowed in process config'];
|
|
695
859
|
}
|
|
696
860
|
/**
|
|
697
861
|
* responseModeParams 桶专项校验。
|
|
@@ -736,7 +900,12 @@ function validateResponseModeParams(params, target) {
|
|
|
736
900
|
* 供 config-store.validateAgentConfig 等只校验不落盘的场景使用。
|
|
737
901
|
*/
|
|
738
902
|
export function validateConfig(target, value) {
|
|
739
|
-
const
|
|
903
|
+
const requestedVersion = value && typeof value === 'object'
|
|
904
|
+
? Number(value.$schema_version)
|
|
905
|
+
: NaN;
|
|
906
|
+
const schema = target === ConfigTarget.Contact && requestedVersion === 2
|
|
907
|
+
? loadSchema('contact-book', 2)
|
|
908
|
+
: loadSchema(TARGET_SCHEMA[target]);
|
|
740
909
|
const withVer = ensureSchemaVersion(value, schema.version);
|
|
741
910
|
const ok = schema.validate(withVer);
|
|
742
911
|
const errors = validateProcessOnlyFields(target, withVer);
|
|
@@ -772,6 +941,11 @@ export function ensureFile(target, sel) {
|
|
|
772
941
|
const file = targetPath(target, sel);
|
|
773
942
|
if (fs.existsSync(file))
|
|
774
943
|
return;
|
|
944
|
+
const skeleton = configSkeleton(target, sel);
|
|
945
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
946
|
+
atomicWriteJson(file, skeleton);
|
|
947
|
+
}
|
|
948
|
+
function configSkeleton(target, sel) {
|
|
775
949
|
const schema = loadSchema(TARGET_SCHEMA[target]);
|
|
776
950
|
const skeleton = { $schema_version: schema.version };
|
|
777
951
|
const props = schema.raw.properties || {};
|
|
@@ -791,8 +965,7 @@ export function ensureFile(target, sel) {
|
|
|
791
965
|
else if (spec?.type === 'object')
|
|
792
966
|
skeleton[key] = {};
|
|
793
967
|
}
|
|
794
|
-
|
|
795
|
-
atomicWriteJson(file, skeleton);
|
|
968
|
+
return skeleton;
|
|
796
969
|
}
|
|
797
970
|
// ── schema 版本迁移 ───────────────────────────────────────────────────────
|
|
798
971
|
//
|
|
@@ -823,9 +996,11 @@ export function resolveAgentConfig(sel, opts = {}) {
|
|
|
823
996
|
function stripStaticOwnerField(config) {
|
|
824
997
|
if (!config)
|
|
825
998
|
return config;
|
|
826
|
-
if (!Object.prototype.hasOwnProperty.call(config, 'owners')
|
|
999
|
+
if (!Object.prototype.hasOwnProperty.call(config, 'owners')
|
|
1000
|
+
&& !Object.prototype.hasOwnProperty.call(config, 'admins')
|
|
1001
|
+
&& !Object.prototype.hasOwnProperty.call(config, 'access'))
|
|
827
1002
|
return config;
|
|
828
|
-
const { owners: _owners, admins: _admins, ...rest } = config;
|
|
1003
|
+
const { owners: _owners, admins: _admins, access: _access, ...rest } = config;
|
|
829
1004
|
return rest;
|
|
830
1005
|
}
|
|
831
1006
|
function stripRelationRoleData(config) {
|
|
@@ -853,7 +1028,6 @@ export function resolveEffective(sel, opts = {}) {
|
|
|
853
1028
|
models: config.models,
|
|
854
1029
|
projects: config.projects,
|
|
855
1030
|
capabilities: config.capabilities,
|
|
856
|
-
debug: config.debug,
|
|
857
1031
|
observable: config.observable,
|
|
858
1032
|
extra_backup: config.extra_backup,
|
|
859
1033
|
// Runtime configuration parameters
|
|
@@ -868,6 +1042,7 @@ export function resolveEffective(sel, opts = {}) {
|
|
|
868
1042
|
debounce: config.debounce,
|
|
869
1043
|
show_activities: config.show_activities,
|
|
870
1044
|
session_renew: config.session_renew,
|
|
1045
|
+
groupRules: config.groupRules,
|
|
871
1046
|
render: config.render,
|
|
872
1047
|
sessionManifests: config.sessionManifests,
|
|
873
1048
|
enable_rich_content: config.enable_rich_content,
|
|
@@ -895,6 +1070,7 @@ const EFFECTIVE_BEHAVIOR_FIELDS = [
|
|
|
895
1070
|
'debounce',
|
|
896
1071
|
'show_activities',
|
|
897
1072
|
'session_renew',
|
|
1073
|
+
'groupRules',
|
|
898
1074
|
'render',
|
|
899
1075
|
'sessionManifests',
|
|
900
1076
|
'enable_rich_content',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConfigError, ConfigTarget, ensureFile, listFields, read, resolveEffectiveFieldWithSource, resolveEffectiveWithSources, validateConfigFile,
|
|
1
|
+
import { ConfigError, ConfigTarget, ensureFile, listFields, mutateConfig, read, resolveEffectiveFieldWithSource, resolveEffectiveWithSources, validateConfigFile, } from './config-manager.js';
|
|
2
2
|
import { collectConfigFiles, diffVersions, listAllVersions, prune, readCurrent, restore, snapshot, isConfigSnapshotsEnabled, } from './snapshot.js';
|
|
3
3
|
import { readBootLog } from './boot-log.js';
|
|
4
4
|
import { currentVersion, fieldSchemaDefault, isSchemaName, listSchemaNames, listSchemaVersions, readRawSchema, schemaHistoryEntry, } from './schema-registry.js';
|
|
@@ -302,27 +302,26 @@ function executeSet(op, options) {
|
|
|
302
302
|
if (!op.route)
|
|
303
303
|
return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
|
|
304
304
|
const selector = selectorFor(op, options);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
write(op.route.target, existing, selector);
|
|
305
|
+
mutateConfig(op.route.target, selector, existing => {
|
|
306
|
+
if (op.route.merge === 'list') {
|
|
307
|
+
const previous = getNested(existing, op.field);
|
|
308
|
+
const previousItems = Array.isArray(previous) ? previous : [];
|
|
309
|
+
const nextItems = Array.isArray(op.value) ? op.value : [op.value];
|
|
310
|
+
const merged = [...new Set([...previousItems, ...nextItems].map(value => typeof value === 'object' ? JSON.stringify(value) : value))]
|
|
311
|
+
.map(value => {
|
|
312
|
+
try {
|
|
313
|
+
return typeof value === 'string' && value.startsWith('{') ? JSON.parse(value) : value;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return value;
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
setNested(existing, op.field, merged);
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
setNested(existing, op.field, op.value);
|
|
323
|
+
}
|
|
324
|
+
});
|
|
326
325
|
return {
|
|
327
326
|
ok: true,
|
|
328
327
|
subcommand: 'set',
|
|
@@ -336,22 +335,20 @@ function executeUnset(op, options) {
|
|
|
336
335
|
if (!op.route)
|
|
337
336
|
return { ok: false, code: 'UNKNOWN_FIELD', error: `Unknown config field: ${op.field}` };
|
|
338
337
|
const selector = selectorFor(op, options);
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
delete current[leaf];
|
|
354
|
-
write(op.route.target, existing, selector);
|
|
338
|
+
const { value: removed } = mutateConfig(op.route.target, selector, existing => {
|
|
339
|
+
const parts = op.field.split('.');
|
|
340
|
+
let current = existing;
|
|
341
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
342
|
+
if (current == null)
|
|
343
|
+
break;
|
|
344
|
+
current = current[parts[i]];
|
|
345
|
+
}
|
|
346
|
+
const leaf = parts[parts.length - 1];
|
|
347
|
+
const exists = !!current && typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, leaf);
|
|
348
|
+
if (exists)
|
|
349
|
+
delete current[leaf];
|
|
350
|
+
return exists;
|
|
351
|
+
});
|
|
355
352
|
return { ok: true, subcommand: 'unset', field: op.field, removed, scope: op.configScope };
|
|
356
353
|
}
|
|
357
354
|
function selectorFor(command, options = {}) {
|