evolcore 0.0.3 → 0.0.5
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 +83 -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/daemon-commands.js +46 -15
- package/dist/cli/handoff-command.js +2 -1
- package/dist/cli/init-channel.js +185 -67
- package/dist/cli/init.js +49 -27
- package/dist/cli/trigger-command.js +92 -11
- package/dist/config/access-policy-domain.js +18 -0
- package/dist/config/access-policy.js +110 -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 +229 -26
- 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/role-menu.js +16 -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 +134 -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-fs-store.js +19 -4
- package/dist/core/session/session-manager.js +232 -4
- 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/cross-platform.js +8 -2
- 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/process-introspect.js +19 -3
- 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 +8 -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 +364 -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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { loadDaemonConfig } from '../../config-store.js';
|
|
2
|
+
import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
|
|
3
|
+
import { compareStableSemver, parseStableSemver } from '../../utils/stable-semver.js';
|
|
4
|
+
export function evaluateEvolMenuVersionGate(input) {
|
|
5
|
+
const minimum = loadDaemonConfig().aun?.minEvolVersion;
|
|
6
|
+
if (!minimum)
|
|
7
|
+
return null;
|
|
8
|
+
const minimumVersion = parseStableSemver(minimum);
|
|
9
|
+
if (!minimumVersion) {
|
|
10
|
+
throw new Error('daemon.json.aun.minEvolVersion must use stable X.Y.Z format');
|
|
11
|
+
}
|
|
12
|
+
const received = input.protectedHeaders?.evol_version;
|
|
13
|
+
const receivedVersion = input.encrypted ? parseStableSemver(received) : null;
|
|
14
|
+
if (!receivedVersion || compareStableSemver(receivedVersion, minimumVersion) < 0) {
|
|
15
|
+
return {
|
|
16
|
+
code: 'UPGRADE_REQUIRED',
|
|
17
|
+
message: `Evol ${minimum} or later is required`,
|
|
18
|
+
data: {
|
|
19
|
+
minimum,
|
|
20
|
+
received: receivedVersion ? received : null,
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
export function isEvolMenuVersionGateEnabled() {
|
|
27
|
+
return loadDaemonConfig().aun?.minEvolVersion !== undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Token may be issued while disabled, but only this flag makes it an access requirement. */
|
|
30
|
+
export function isAunMenuTokenRequired() {
|
|
31
|
+
return loadDaemonConfig().aun?.menuTokenRequired === true;
|
|
32
|
+
}
|
|
33
|
+
export function evolMenuResponseTransportMetadata(_requestEncrypted) {
|
|
34
|
+
return {
|
|
35
|
+
encrypted: true,
|
|
36
|
+
protectedHeaders: { ec_version: readInstalledEvolcoreVersion() },
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { ConfigTarget, mutateConfig, read as readConfig, resolveEffectiveFieldWithSource, } from '../../config/config-manager.js';
|
|
3
|
+
import { dispatchToMentionMode } from '../../config/mention-mode.js';
|
|
4
|
+
import { readRolesConfig } from '../../config/roles.js';
|
|
5
|
+
import { groupInfo, groupList } from '../../aun/msg/group.js';
|
|
6
|
+
import { formatPeerKey, parsePeerKey } from '../relation/peer-identity.js';
|
|
7
|
+
const DEFAULT_DIRECTORY = {
|
|
8
|
+
getGroup: groupInfo,
|
|
9
|
+
listGroups: groupList,
|
|
10
|
+
};
|
|
11
|
+
export class GroupMenuError extends Error {
|
|
12
|
+
code;
|
|
13
|
+
data;
|
|
14
|
+
constructor(code, message, data) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.data = data;
|
|
18
|
+
this.name = 'GroupMenuError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const SETTINGS = {
|
|
22
|
+
mentionMode: 'mentionMode',
|
|
23
|
+
mentionmode: 'mentionMode',
|
|
24
|
+
rulesPolicy: 'rulesPolicy',
|
|
25
|
+
rulespolicy: 'rulesPolicy',
|
|
26
|
+
};
|
|
27
|
+
export function groupMenuIntent(kind, args) {
|
|
28
|
+
if (kind === 'query' && String(args?.view ?? '') === 'list') {
|
|
29
|
+
return { operation: 'group.list', scope: 'agent' };
|
|
30
|
+
}
|
|
31
|
+
const key = settingSelector(kind, args);
|
|
32
|
+
const scope = settingScope(args);
|
|
33
|
+
if (key === 'mentionMode') {
|
|
34
|
+
return { operation: `group.mentionmode.${kind === 'update' ? 'update' : 'current'}`, scope };
|
|
35
|
+
}
|
|
36
|
+
return { operation: `group.rulespolicy.${kind === 'update' ? 'update' : 'current'}`, scope };
|
|
37
|
+
}
|
|
38
|
+
/** Adds an explicit relation selector before authorization when a group target was supplied. */
|
|
39
|
+
export function groupMenuAuthorizationArgs(args, receivingSelf, currentPeerKey) {
|
|
40
|
+
const out = { ...(args ?? {}) };
|
|
41
|
+
if (receivingSelf && out.self === undefined)
|
|
42
|
+
out.self = receivingSelf;
|
|
43
|
+
const rawGroupId = stringArg(out.groupId);
|
|
44
|
+
if (rawGroupId && out.peer === undefined && out.peerKey === undefined) {
|
|
45
|
+
out.peer = formatPeerKey('aun', rawGroupId);
|
|
46
|
+
}
|
|
47
|
+
else if (!rawGroupId && currentPeerKey && out.peer === undefined && out.peerKey === undefined) {
|
|
48
|
+
out.peer = currentPeerKey;
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
export async function handleGroupMenu(request, context) {
|
|
53
|
+
assertSelf(request.args, context.self);
|
|
54
|
+
if (request.kind === 'query' && String(request.args?.view ?? '') === 'list') {
|
|
55
|
+
return queryGroupList(context, request.args);
|
|
56
|
+
}
|
|
57
|
+
const key = settingSelector(request.kind, request.args);
|
|
58
|
+
if (request.kind === 'update' && !stringArg(request.args?.scope)) {
|
|
59
|
+
throw new GroupMenuError('MISSING_SCOPE', 'Group Menu updates require args.scope');
|
|
60
|
+
}
|
|
61
|
+
const target = await resolveTarget(request.args, context);
|
|
62
|
+
if (request.kind === 'options')
|
|
63
|
+
return settingOptions(key, target);
|
|
64
|
+
if (request.kind === 'query')
|
|
65
|
+
return querySetting(key, target);
|
|
66
|
+
return updateSetting(key, target, request.value, request.args, context);
|
|
67
|
+
}
|
|
68
|
+
function assertSelf(args, self) {
|
|
69
|
+
const requested = stringArg(args?.self);
|
|
70
|
+
if (requested && requested !== self) {
|
|
71
|
+
throw new GroupMenuError('PERMISSION_DENIED', 'args.self does not match the receiving Agent');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function queryGroupList(context, args) {
|
|
75
|
+
assertAunAvailable(context);
|
|
76
|
+
const limit = parseLimit(args?.limit);
|
|
77
|
+
const cursor = optionalString(args?.cursor, 'args.cursor');
|
|
78
|
+
const result = await groupDirectory(context).listGroups({
|
|
79
|
+
from: context.self,
|
|
80
|
+
size: limit,
|
|
81
|
+
...(cursor ? { cursor } : {}),
|
|
82
|
+
});
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
throw new GroupMenuError('TEMPORARILY_UNAVAILABLE', result.error || 'Unable to list joined groups');
|
|
85
|
+
}
|
|
86
|
+
const defaultGroupRole = readRolesConfig(context.self).defaultRoles?.group ?? null;
|
|
87
|
+
const items = result.items.map(group => {
|
|
88
|
+
const groupId = String(group.group_aid || group.group_id);
|
|
89
|
+
const peerKey = formatPeerKey('aun', groupId);
|
|
90
|
+
const relation = readConfig(ConfigTarget.Relation, { self: context.self, peerKey }, { cache: false });
|
|
91
|
+
const explicitRole = typeof relation?.role === 'string' ? relation.role : null;
|
|
92
|
+
return {
|
|
93
|
+
groupId,
|
|
94
|
+
peerKey,
|
|
95
|
+
displayName: group.name || groupId,
|
|
96
|
+
status: group.status,
|
|
97
|
+
memberCount: group.member_count,
|
|
98
|
+
roleId: explicitRole,
|
|
99
|
+
effectiveRoleId: explicitRole ?? defaultGroupRole,
|
|
100
|
+
effectiveRoleSource: explicitRole ? 'relation' : defaultGroupRole ? 'agent-default' : 'none',
|
|
101
|
+
relationRevision: relation ? documentRevision(relation) : null,
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
self: context.self,
|
|
106
|
+
view: 'list',
|
|
107
|
+
items,
|
|
108
|
+
nextCursor: result.nextCursor ?? null,
|
|
109
|
+
total: result.total,
|
|
110
|
+
dataSource: {
|
|
111
|
+
kind: 'aun-live',
|
|
112
|
+
authoritative: true,
|
|
113
|
+
complete: !result.nextCursor && items.length >= result.total,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function settingSelector(kind, args) {
|
|
118
|
+
const parameter = kind === 'query' ? 'view' : kind === 'options' ? 'option' : 'resource';
|
|
119
|
+
const raw = stringArg(args?.[parameter]);
|
|
120
|
+
const key = raw ? SETTINGS[raw] : undefined;
|
|
121
|
+
if (!key) {
|
|
122
|
+
throw new GroupMenuError('NOT_SUPPORTED', `args.${parameter} must be mentionMode or rulesPolicy`);
|
|
123
|
+
}
|
|
124
|
+
return key;
|
|
125
|
+
}
|
|
126
|
+
function settingScope(args) {
|
|
127
|
+
const raw = stringArg(args?.scope) ?? 'relation';
|
|
128
|
+
if (raw === 'agent' || raw === 'relation')
|
|
129
|
+
return raw;
|
|
130
|
+
throw new GroupMenuError('INVALID_SCOPE', 'args.scope must be agent or relation');
|
|
131
|
+
}
|
|
132
|
+
async function resolveTarget(args, context) {
|
|
133
|
+
const scope = settingScope(args);
|
|
134
|
+
if (scope === 'agent')
|
|
135
|
+
return { scope, selector: { self: context.self } };
|
|
136
|
+
assertAunAvailable(context);
|
|
137
|
+
const groupId = groupIdFromArgs(args, context.currentGroupId);
|
|
138
|
+
const result = await groupDirectory(context).getGroup({
|
|
139
|
+
from: context.self,
|
|
140
|
+
groupId,
|
|
141
|
+
required: ['member', 'state'],
|
|
142
|
+
});
|
|
143
|
+
if (!result.ok) {
|
|
144
|
+
if (result.code === -33001 || result.code === -33006
|
|
145
|
+
|| result.code === '-33001' || result.code === '-33006') {
|
|
146
|
+
throw new GroupMenuError('NOT_FOUND', 'The target group is not joined by this Agent');
|
|
147
|
+
}
|
|
148
|
+
throw new GroupMenuError('TEMPORARILY_UNAVAILABLE', result.error || 'Unable to verify the target group');
|
|
149
|
+
}
|
|
150
|
+
const group = result.group;
|
|
151
|
+
if (!result.found || !group || !group.my_role || group.status === 'dissolved') {
|
|
152
|
+
throw new GroupMenuError('NOT_FOUND', 'The target group is not joined by this Agent');
|
|
153
|
+
}
|
|
154
|
+
const canonicalGroupId = String(group.group_aid || group.group_id || groupId);
|
|
155
|
+
return {
|
|
156
|
+
scope,
|
|
157
|
+
selector: { self: context.self, peerKey: formatPeerKey('aun', canonicalGroupId) },
|
|
158
|
+
groupId: canonicalGroupId,
|
|
159
|
+
aunMentionMode: dispatchToMentionMode(group.dispatch_mode
|
|
160
|
+
?? group.settings?.dispatch_mode
|
|
161
|
+
?? group.metadata?.dispatch_mode),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function groupIdFromArgs(args, currentGroupId) {
|
|
165
|
+
const explicitGroupId = stringArg(args?.groupId);
|
|
166
|
+
const explicitPeer = stringArg(args?.peerKey) ?? stringArg(args?.peer);
|
|
167
|
+
let peerGroupId;
|
|
168
|
+
if (explicitPeer) {
|
|
169
|
+
try {
|
|
170
|
+
const parsed = parsePeerKey(explicitPeer);
|
|
171
|
+
if (parsed.channelType !== 'aun') {
|
|
172
|
+
throw new GroupMenuError('INVALID_ARGUMENT', 'group settings require an AUN group peerKey');
|
|
173
|
+
}
|
|
174
|
+
peerGroupId = parsed.channelId;
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
if (error instanceof GroupMenuError)
|
|
178
|
+
throw error;
|
|
179
|
+
throw new GroupMenuError('INVALID_ARGUMENT', 'args.peerKey must be a valid AUN group peerKey');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (explicitGroupId && peerGroupId && explicitGroupId !== peerGroupId) {
|
|
183
|
+
throw new GroupMenuError('INVALID_ARGUMENT', 'args.groupId and args.peerKey refer to different groups');
|
|
184
|
+
}
|
|
185
|
+
const groupId = explicitGroupId ?? peerGroupId ?? currentGroupId;
|
|
186
|
+
if (!groupId) {
|
|
187
|
+
throw new GroupMenuError('INVALID_ARGUMENT', 'relation settings require args.groupId or args.peerKey');
|
|
188
|
+
}
|
|
189
|
+
return groupId;
|
|
190
|
+
}
|
|
191
|
+
function settingOptions(key, target) {
|
|
192
|
+
const state = readSettingState(key, target);
|
|
193
|
+
const values = key === 'mentionMode'
|
|
194
|
+
? [
|
|
195
|
+
{ value: 'mention-only', label: '@提及时响应' },
|
|
196
|
+
{ value: 'disabled', label: '所有消息响应' },
|
|
197
|
+
]
|
|
198
|
+
: [
|
|
199
|
+
{ value: 'load', label: '加载群规则' },
|
|
200
|
+
{ value: 'ignore', label: '忽略群规则' },
|
|
201
|
+
];
|
|
202
|
+
return [
|
|
203
|
+
{
|
|
204
|
+
value: 'clear',
|
|
205
|
+
label: key === 'mentionMode' && target.scope === 'agent' ? '使用各群设置' : '使用上级设置',
|
|
206
|
+
selected: state.override === null,
|
|
207
|
+
revision: state.revision,
|
|
208
|
+
},
|
|
209
|
+
...values.map(option => ({
|
|
210
|
+
...option,
|
|
211
|
+
selected: state.override === option.value,
|
|
212
|
+
...(state.mode ? { effective: state.mode === option.value } : {}),
|
|
213
|
+
revision: state.revision,
|
|
214
|
+
})),
|
|
215
|
+
];
|
|
216
|
+
}
|
|
217
|
+
function querySetting(key, target) {
|
|
218
|
+
return settingResponse('view', key, target, readSettingState(key, target));
|
|
219
|
+
}
|
|
220
|
+
function updateSetting(key, target, value, args, context) {
|
|
221
|
+
const raw = typeof value === 'string' ? value.trim() : '';
|
|
222
|
+
const allowed = key === 'mentionMode'
|
|
223
|
+
? new Set(['mention-only', 'disabled', 'clear'])
|
|
224
|
+
: new Set(['load', 'ignore', 'clear']);
|
|
225
|
+
if (!allowed.has(raw)) {
|
|
226
|
+
throw new GroupMenuError('INVALID_ARGUMENT', `Invalid ${key} value: ${raw || '<empty>'}`);
|
|
227
|
+
}
|
|
228
|
+
if (key === 'rulesPolicy' && context.role !== 'owner' && !context.isDaemonOwner) {
|
|
229
|
+
throw new GroupMenuError('PERMISSION_DENIED', 'rulesPolicy can only be changed by an Agent owner');
|
|
230
|
+
}
|
|
231
|
+
const expectedRevision = stringArg(args?.expectedRevision);
|
|
232
|
+
if (!expectedRevision) {
|
|
233
|
+
throw new GroupMenuError('INVALID_ARGUMENT', 'args.expectedRevision is required for Group Menu updates', {
|
|
234
|
+
kind: 'revision_required',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
const targetKind = target.scope === 'agent' ? ConfigTarget.Agent : ConfigTarget.Relation;
|
|
238
|
+
const mutation = mutateConfig(targetKind, target.selector, config => {
|
|
239
|
+
const override = configuredOverride(key, config);
|
|
240
|
+
const currentRevision = settingRevision(key, target, override);
|
|
241
|
+
if (currentRevision !== expectedRevision) {
|
|
242
|
+
throw new GroupMenuError('CONFLICT', 'The Group setting has changed', {
|
|
243
|
+
kind: 'revision_conflict',
|
|
244
|
+
expectedRevision,
|
|
245
|
+
currentRevision,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
writeOverride(key, config, raw === 'clear' ? null : raw);
|
|
249
|
+
});
|
|
250
|
+
const state = readSettingState(key, target);
|
|
251
|
+
const compatibility = key === 'rulesPolicy'
|
|
252
|
+
? groupRulesSessionCompatibility(context, target)
|
|
253
|
+
: undefined;
|
|
254
|
+
return {
|
|
255
|
+
...settingResponse('resource', key, target, state),
|
|
256
|
+
changed: mutation.changed,
|
|
257
|
+
effectiveFrom: key === 'mentionMode'
|
|
258
|
+
? 'next-message'
|
|
259
|
+
: compatibility?.sessionCompatibility.status === 'requires-new-session'
|
|
260
|
+
? 'new-session'
|
|
261
|
+
: 'next-turn',
|
|
262
|
+
...(compatibility ?? {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function settingResponse(selector, key, target, state) {
|
|
266
|
+
return {
|
|
267
|
+
[selector]: key,
|
|
268
|
+
scope: target.scope,
|
|
269
|
+
self: target.selector.self,
|
|
270
|
+
...(target.selector.peerKey ? { peerKey: target.selector.peerKey } : {}),
|
|
271
|
+
...(target.groupId ? { groupId: target.groupId } : {}),
|
|
272
|
+
override: state.override,
|
|
273
|
+
...(state.mode ? { mode: state.mode, source: state.source } : {}),
|
|
274
|
+
...(state.resolution ? { resolution: state.resolution } : {}),
|
|
275
|
+
revision: state.revision,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function readSettingState(key, target) {
|
|
279
|
+
const targetKind = target.scope === 'agent' ? ConfigTarget.Agent : ConfigTarget.Relation;
|
|
280
|
+
const local = readConfig(targetKind, target.selector, { cache: false });
|
|
281
|
+
const override = configuredOverride(key, local ?? {});
|
|
282
|
+
const revision = settingRevision(key, target, override);
|
|
283
|
+
const resolved = resolveEffectiveFieldWithSource(fieldPath(key), target.selector, { cache: false });
|
|
284
|
+
if (resolved.value !== undefined) {
|
|
285
|
+
const mode = validMode(key, resolved.value);
|
|
286
|
+
if (!mode) {
|
|
287
|
+
throw new GroupMenuError('INTERNAL_ERROR', `Configured ${key} value is invalid`);
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
override,
|
|
291
|
+
mode,
|
|
292
|
+
source: sourceName(resolved.source) ?? 'builtin',
|
|
293
|
+
revision,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (key === 'rulesPolicy') {
|
|
297
|
+
return { override, mode: 'load', source: 'builtin', revision };
|
|
298
|
+
}
|
|
299
|
+
if (target.scope === 'relation') {
|
|
300
|
+
return {
|
|
301
|
+
override,
|
|
302
|
+
mode: target.aunMentionMode ?? 'disabled',
|
|
303
|
+
source: target.aunMentionMode ? 'aun' : 'builtin',
|
|
304
|
+
revision,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return { override, resolution: 'per-group-aun', revision };
|
|
308
|
+
}
|
|
309
|
+
function configuredOverride(key, config) {
|
|
310
|
+
const value = key === 'mentionMode' ? config.mentionMode : config.groupRules?.mode;
|
|
311
|
+
if (value === undefined)
|
|
312
|
+
return null;
|
|
313
|
+
const mode = validMode(key, value);
|
|
314
|
+
if (!mode)
|
|
315
|
+
throw new GroupMenuError('INTERNAL_ERROR', `Stored ${key} override is invalid`);
|
|
316
|
+
return mode;
|
|
317
|
+
}
|
|
318
|
+
function validMode(key, value) {
|
|
319
|
+
if (key === 'mentionMode') {
|
|
320
|
+
return value === 'mention-only' || value === 'disabled' ? value : undefined;
|
|
321
|
+
}
|
|
322
|
+
return value === 'load' || value === 'ignore' ? value : undefined;
|
|
323
|
+
}
|
|
324
|
+
function writeOverride(key, config, value) {
|
|
325
|
+
if (key === 'mentionMode') {
|
|
326
|
+
if (value === null)
|
|
327
|
+
delete config.mentionMode;
|
|
328
|
+
else
|
|
329
|
+
config.mentionMode = value;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const rules = config.groupRules && typeof config.groupRules === 'object' && !Array.isArray(config.groupRules)
|
|
333
|
+
? { ...config.groupRules }
|
|
334
|
+
: {};
|
|
335
|
+
if (value === null)
|
|
336
|
+
delete rules.mode;
|
|
337
|
+
else
|
|
338
|
+
rules.mode = value;
|
|
339
|
+
if (Object.keys(rules).length === 0)
|
|
340
|
+
delete config.groupRules;
|
|
341
|
+
else
|
|
342
|
+
config.groupRules = rules;
|
|
343
|
+
}
|
|
344
|
+
function settingRevision(key, target, override) {
|
|
345
|
+
return `sha256:${crypto.createHash('sha256').update(JSON.stringify({
|
|
346
|
+
self: target.selector.self,
|
|
347
|
+
peerKey: target.selector.peerKey ?? null,
|
|
348
|
+
scope: target.scope,
|
|
349
|
+
resource: key,
|
|
350
|
+
override,
|
|
351
|
+
})).digest('hex')}`;
|
|
352
|
+
}
|
|
353
|
+
function documentRevision(value) {
|
|
354
|
+
return `sha256:${crypto.createHash('sha256').update(stableStringify(value)).digest('hex')}`;
|
|
355
|
+
}
|
|
356
|
+
function stableStringify(value) {
|
|
357
|
+
if (Array.isArray(value))
|
|
358
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
359
|
+
if (!value || typeof value !== 'object')
|
|
360
|
+
return JSON.stringify(value);
|
|
361
|
+
return `{${Object.entries(value)
|
|
362
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
363
|
+
.map(([key, nested]) => `${JSON.stringify(key)}:${stableStringify(nested)}`)
|
|
364
|
+
.join(',')}}`;
|
|
365
|
+
}
|
|
366
|
+
function fieldPath(key) {
|
|
367
|
+
return key === 'mentionMode' ? 'mentionMode' : 'groupRules.mode';
|
|
368
|
+
}
|
|
369
|
+
function sourceName(source) {
|
|
370
|
+
if (source?.target === ConfigTarget.Relation)
|
|
371
|
+
return 'relation';
|
|
372
|
+
if (source?.target === ConfigTarget.Agent)
|
|
373
|
+
return 'agent';
|
|
374
|
+
if (source?.target === ConfigTarget.Defaults)
|
|
375
|
+
return 'defaults';
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
function groupRulesSessionCompatibility(context, target) {
|
|
379
|
+
const currentSessionAffected = !!context.currentGroupId
|
|
380
|
+
&& (target.scope === 'agent' || target.groupId === context.currentGroupId);
|
|
381
|
+
if (!currentSessionAffected || !context.baseagent) {
|
|
382
|
+
return { sessionCompatibility: { status: 'not-evaluated' } };
|
|
383
|
+
}
|
|
384
|
+
const runner = context.baseagent.trim().toLowerCase();
|
|
385
|
+
const runnerKey = runner.replace(/[\s_-]+/g, '');
|
|
386
|
+
if (runnerKey === 'claude' || runnerKey === 'claudecode' || runnerKey === 'codex') {
|
|
387
|
+
return { sessionCompatibility: { status: 'supported', runner: context.baseagent } };
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
sessionCompatibility: { status: 'requires-new-session', runner: context.baseagent },
|
|
391
|
+
note: `群规则加载策略已保存;当前会话使用的 ${context.baseagent} 不支持在恢复会话中更新群规则上下文。请新建或清空该群会话后生效。`,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function groupDirectory(context) {
|
|
395
|
+
return context.roleContext.directory ?? DEFAULT_DIRECTORY;
|
|
396
|
+
}
|
|
397
|
+
function assertAunAvailable(context) {
|
|
398
|
+
if (context.roleContext.aunAvailable === false) {
|
|
399
|
+
throw new GroupMenuError('NOT_SUPPORTED', 'Group Menu requires an enabled AUN channel');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
function parseLimit(value) {
|
|
403
|
+
if (value === undefined)
|
|
404
|
+
return 50;
|
|
405
|
+
const limit = Number(value);
|
|
406
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
|
|
407
|
+
throw new GroupMenuError('INVALID_ARGUMENT', 'args.limit must be an integer between 1 and 200');
|
|
408
|
+
}
|
|
409
|
+
return limit;
|
|
410
|
+
}
|
|
411
|
+
function optionalString(value, field) {
|
|
412
|
+
if (value === undefined || value === null)
|
|
413
|
+
return undefined;
|
|
414
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
415
|
+
throw new GroupMenuError('INVALID_ARGUMENT', `${field} must be a non-empty string`);
|
|
416
|
+
}
|
|
417
|
+
return value.trim();
|
|
418
|
+
}
|
|
419
|
+
function stringArg(value) {
|
|
420
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
421
|
+
}
|