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.
Files changed (143) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +1 -31
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +157 -5
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/channels/aun.js +220 -112
  14. package/dist/channels/daemon.js +18 -4
  15. package/dist/channels/dingtalk.js +52 -137
  16. package/dist/channels/feishu.js +56 -4
  17. package/dist/channels/qqbot.js +23 -1
  18. package/dist/channels/wechat.js +303 -155
  19. package/dist/channels/wecom.js +383 -7
  20. package/dist/cli/aun-commands.js +11 -11
  21. package/dist/cli/handoff-command.js +2 -1
  22. package/dist/cli/init-channel.js +185 -67
  23. package/dist/cli/init.js +7 -5
  24. package/dist/cli/trigger-command.js +92 -11
  25. package/dist/config/access-policy-domain.js +38 -0
  26. package/dist/config/access-policy.js +134 -0
  27. package/dist/config/builtin-role-templates.js +27 -14
  28. package/dist/config/builtin-roles.js +25 -6
  29. package/dist/config/config-field-policy.js +17 -5
  30. package/dist/config/config-manager.js +198 -22
  31. package/dist/config/config-operation-service.js +35 -38
  32. package/dist/config/contact-bind-code.js +274 -0
  33. package/dist/config/contact-book-store.js +173 -5
  34. package/dist/config/contact-book.js +32 -0
  35. package/dist/config/contact-operation-service.js +9 -3
  36. package/dist/config/contact-request-service.js +331 -0
  37. package/dist/config/peer-role-resolver.js +3 -1
  38. package/dist/config/schema-registry.js +49 -24
  39. package/dist/config-store.js +14 -2
  40. package/dist/core/auth/agent-delegation.js +105 -11
  41. package/dist/core/auth/authorization-audit.js +6 -0
  42. package/dist/core/auth/operation-authorizer.js +8 -0
  43. package/dist/core/auth/operation-catalog.js +51 -3
  44. package/dist/core/auth/trigger-authorization.js +15 -0
  45. package/dist/core/bootstrap-service.js +2 -9
  46. package/dist/core/channel-loader.js +6 -2
  47. package/dist/core/command/command-handler.js +10 -13
  48. package/dist/core/command/connect-menu.js +249 -24
  49. package/dist/core/command/evol-menu-version-gate.js +38 -0
  50. package/dist/core/command/group-menu.js +421 -0
  51. package/dist/core/command/menu-handler.js +269 -85
  52. package/dist/core/command/menu-protocol.js +8 -2
  53. package/dist/core/command/menu-token-store.js +102 -0
  54. package/dist/core/command/slash-gate.js +1 -1
  55. package/dist/core/command/slash-handler.js +158 -31
  56. package/dist/core/daemon-file-cache.js +28 -0
  57. package/dist/core/event-catalog.js +29 -0
  58. package/dist/core/evolagent-registry.js +4 -39
  59. package/dist/core/handoff/runtime.js +162 -37
  60. package/dist/core/handoff/store.js +46 -2
  61. package/dist/core/handoff/types.js +1 -0
  62. package/dist/core/inference/text-inference.js +16 -74
  63. package/dist/core/message/file-markers.js +7 -0
  64. package/dist/core/message/im-renderer.js +18 -14
  65. package/dist/core/message/inbound-admission.js +124 -0
  66. package/dist/core/message/message-bridge.js +616 -74
  67. package/dist/core/message/message-log.js +1 -0
  68. package/dist/core/message/message-queue.js +185 -10
  69. package/dist/core/message/peer-mode.js +7 -8
  70. package/dist/core/message/response-engine.js +381 -79
  71. package/dist/core/permission/approval-gateway.js +17 -10
  72. package/dist/core/permission/ec-command-parser.js +101 -46
  73. package/dist/core/permission/tool-policy.js +69 -24
  74. package/dist/core/protected-paths.js +36 -11
  75. package/dist/core/session/session-manager.js +25 -3
  76. package/dist/core/session/session-mapper.js +2 -0
  77. package/dist/core/session/session-renew.js +125 -69
  78. package/dist/core/session/session-turn-coordinator.js +5 -1
  79. package/dist/eck/kit-renderer.js +12 -1
  80. package/dist/eck/message-renderer.js +79 -1
  81. package/dist/index.js +224 -89
  82. package/dist/ipc.js +81 -4
  83. package/dist/paths.js +3 -0
  84. package/dist/response-system/config-resolver.js +29 -0
  85. package/dist/response-system/coordinator.js +8 -32
  86. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  87. package/dist/response-system/index.js +1 -0
  88. package/dist/response-system/modes/single-session/index.js +7 -4
  89. package/dist/trigger/parser.js +55 -15
  90. package/dist/trigger/patch.js +4 -1
  91. package/dist/trigger/scheduler.js +441 -39
  92. package/dist/utils/error-dict.json +7 -0
  93. package/dist/utils/evolcore-version.js +21 -0
  94. package/dist/utils/logger.js +2 -2
  95. package/dist/utils/stable-semver.js +21 -0
  96. package/dist/utils/stats.js +33 -9
  97. package/kits/docs/channels/aun.md +4 -13
  98. package/kits/docs/evolcore/INDEX.md +1 -1
  99. package/kits/docs/evolcore/config.md +47 -2
  100. package/kits/docs/evolcore/contact.md +7 -3
  101. package/kits/docs/evolcore/group-rules.md +46 -4
  102. package/kits/docs/evolcore/group.md +4 -4
  103. package/kits/docs/evolcore/msg.md +5 -5
  104. package/kits/docs/evolcore/trigger.md +25 -8
  105. package/kits/docs/path-registry.md +36 -17
  106. package/kits/eck_message_manifest.json +12 -1
  107. package/kits/migrations/migrate-contact-book-v2.mjs +7 -0
  108. package/kits/rules/01-overview.md +17 -7
  109. package/kits/rules/02-navigation.md +38 -18
  110. package/kits/rules/03-identity.md +28 -24
  111. package/kits/rules/04-relation.md +44 -28
  112. package/kits/rules/05-venue.md +31 -15
  113. package/kits/rules/06-channel.md +11 -7
  114. package/kits/schemas/_meta.json +18 -7
  115. package/kits/schemas/agent-config.schema.7.json +303 -0
  116. package/kits/schemas/agent-config.schema.8.json +304 -0
  117. package/kits/schemas/agent-config.schema.9.json +374 -0
  118. package/kits/schemas/contact-book.schema.3.json +67 -0
  119. package/kits/schemas/daemon.schema.1.json +3 -2
  120. package/kits/schemas/daemon.schema.2.json +101 -0
  121. package/kits/schemas/daemon.schema.3.json +123 -0
  122. package/kits/schemas/defaults.schema.2.json +85 -0
  123. package/kits/schemas/defaults.schema.3.json +73 -0
  124. package/kits/schemas/relation-config.schema.6.json +46 -0
  125. package/kits/schemas/relation-config.schema.7.json +59 -0
  126. package/kits/schemas/single-session.schema.2.json +57 -0
  127. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  128. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  129. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  130. package/kits/templates/roles/admin.json +46 -0
  131. package/kits/templates/roles/member.json +4 -0
  132. package/kits/templates/roles/visitor.json +4 -0
  133. package/kits/templates/system-fragments/channel.md +12 -2
  134. package/kits/templates/system-fragments/identity.md +3 -1
  135. package/kits/templates/system-fragments/relation.md +1 -1
  136. package/kits/templates/system-fragments/session.md +6 -2
  137. package/package.json +4 -2
  138. package/MIGRATION-0.5.0.md +0 -378
  139. package/ROLE_ACCESS_CONTROL.md +0 -174
  140. package/dist/channels/contact-bind-code.js +0 -134
  141. package/dist/channels/wecom-card.js +0 -101
  142. package/dist/channels/wecom-onboarding.js +0 -82
  143. package/dist/channels/wecom-state.js +0 -191
@@ -25,6 +25,7 @@ import { isCapabilityType, listCapabilityOptions, queryCapabilityTypes, resolveC
25
25
  import { normalizeCliArgv, parseCliIntent, parseLegacyCliCommand, validateCliArgv, withDefaultRelationContext } from './cli-intent-parser.js';
26
26
  import { auditCommandAuthorization, hashArgv } from '../auth/authorization-audit.js';
27
27
  import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
28
+ import { evaluateRoleOperationCapability } from '../auth/operation-authorizer.js';
28
29
  import { splitConfigBatchGetArgv } from '../../config/config-batch-get.js';
29
30
  import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
30
31
  import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
@@ -37,6 +38,7 @@ import { dispatchToMentionMode } from '../../config/mention-mode.js';
37
38
  import { menuFailure, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest } from './menu-protocol.js';
38
39
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
39
40
  import { handleConnectMenu, connectMenuOperation } from './connect-menu.js';
41
+ import { groupMenuAuthorizationArgs, groupMenuIntent, handleGroupMenu, } from './group-menu.js';
40
42
  /**
41
43
  * 获取 baseagent CLI 的版本号(claude/gemini/codex)。
42
44
  * 失败返回 null(命令不存在或执行失败)。
@@ -67,6 +69,43 @@ const FILE_HASH_MAX_SIZE = 2 * 1024 * 1024;
67
69
  const FILE_LIST_DEFAULT_LIMIT = 500;
68
70
  const FILE_LIST_MAX_LIMIT = 1000;
69
71
  const topicForkTargetsInFlight = new Set();
72
+ const GROUP_RULES_RESUME_UNSUPPORTED_NOTE = '群规则加载策略已保存;当前会话使用的 baseagent 不支持在恢复会话中更新群规则上下文。请新建或清空该群会话后生效。';
73
+ function appendGroupRulesCompatibilityNote(execution, command, session) {
74
+ if (!('data' in execution)
75
+ || execution.data?.exitCode !== 0
76
+ || command?.kind !== 'field'
77
+ || (command.subcommand !== 'set' && command.subcommand !== 'unset')
78
+ || command.field !== 'groupRules.mode'
79
+ || !session?.baseagent
80
+ || session.baseagent === 'claude'
81
+ || session.baseagent === 'codex') {
82
+ return execution;
83
+ }
84
+ const note = `${GROUP_RULES_RESUME_UNSUPPORTED_NOTE}(当前:${session.baseagent})`;
85
+ const stdout = String(execution.data.stdout ?? '');
86
+ if (command.format === 'json') {
87
+ try {
88
+ const payload = JSON.parse(stdout);
89
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
90
+ return {
91
+ data: {
92
+ ...execution.data,
93
+ stdout: `${JSON.stringify({ ...payload, note }, null, 2)}\n`,
94
+ },
95
+ };
96
+ }
97
+ }
98
+ catch {
99
+ // Fall through to a text suffix if a future CLI returns non-JSON output.
100
+ }
101
+ }
102
+ return {
103
+ data: {
104
+ ...execution.data,
105
+ stdout: stdout.trimEnd() ? `${stdout.trimEnd()}\n${note}\n` : `${note}\n`,
106
+ },
107
+ };
108
+ }
70
109
  function validateCapabilityScope(args) {
71
110
  const scope = args?.scope ?? 'project';
72
111
  return scope === 'project' ? null : { error: 'MVP 只支持 scope=project', code: 'INVALID_SCOPE' };
@@ -180,9 +219,9 @@ async function resolveTopicForkSource(sessionManager, channel, channelId, source
180
219
  * 沿用 /file 的安全校验链:拒绝 `..` 穿越、realpathSync 后验证落点。
181
220
  * 成功返回 `{ realPath, projectPath, stat }`;失败返回 `{ error, code }`。
182
221
  */
183
- function resolveMenuFilePath(input, session, role, expectType) {
184
- if (!session?.projectPath)
185
- return { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
222
+ function resolveMenuFilePath(input, projectPath, role, expectType) {
223
+ if (!projectPath)
224
+ return { error: '无法解析当前 agent projectPath', code: 'NO_PROJECT' };
186
225
  const raw = (input ?? '').toString().trim();
187
226
  if (!raw)
188
227
  return { error: '缺少 path 参数', code: 'MISSING_VALUE' };
@@ -191,7 +230,7 @@ function resolveMenuFilePath(input, session, role, expectType) {
191
230
  return { error: '不支持 .. 路径穿越', code: 'NO_PERMISSION' };
192
231
  }
193
232
  // 相对路径基于 projectPath 解析;绝对路径原样
194
- const resolved = path.isAbsolute(raw) ? raw : path.resolve(session.projectPath, raw);
233
+ const resolved = path.isAbsolute(raw) ? raw : path.resolve(projectPath, raw);
195
234
  if (!fs.existsSync(resolved)) {
196
235
  return { error: '文件不存在', code: 'NOT_FOUND' };
197
236
  }
@@ -199,7 +238,7 @@ function resolveMenuFilePath(input, session, role, expectType) {
199
238
  let realProjectPath;
200
239
  try {
201
240
  realPath = fs.realpathSync(resolved);
202
- realProjectPath = fs.realpathSync(session.projectPath);
241
+ realProjectPath = fs.realpathSync(projectPath);
203
242
  }
204
243
  catch {
205
244
  return { error: '文件不存在', code: 'NOT_FOUND' };
@@ -825,6 +864,8 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
825
864
  });
826
865
  const modelScope = args?.scope === 'agent' ? 'agent' : 'relation';
827
866
  if (verb === 'query') {
867
+ if (cmdBase === '/file')
868
+ return intent('file.fetch', 'filesystem', { filePath: args?.path });
828
869
  if (cmdBase === '/model')
829
870
  return intent('model.current', modelScope);
830
871
  if (cmdBase === '/effort')
@@ -864,9 +905,9 @@ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel
864
905
  if (verb === 'action') {
865
906
  if (cmdBase === '/file') {
866
907
  if (action === 'list')
867
- return intent('file.list', 'filesystem');
908
+ return intent('file.list', 'filesystem', { filePath: args?.path });
868
909
  if (action === 'fetch')
869
- return intent('file.fetch', 'filesystem');
910
+ return intent('file.fetch', 'filesystem', { filePath: args?.path });
870
911
  }
871
912
  if (cmdBase === '/gateway')
872
913
  return intent('gateway.write', 'process', { action });
@@ -964,13 +1005,14 @@ async function authorizeMenuIntent(params) {
964
1005
  intent.source = source;
965
1006
  const decision = await authorizeOperation({ source, intent, subject });
966
1007
  if (!decision.allow) {
967
- if (intent.operation.startsWith('role.')) {
1008
+ if (intent.operation.startsWith('role.') || intent.operation.startsWith('connect.')) {
1009
+ const namespace = intent.operation.split('.', 1)[0];
968
1010
  return {
969
1011
  error: decision.reason,
970
1012
  code: 'PERMISSION_DENIED',
971
1013
  data: {
972
1014
  $schema_version: 1,
973
- kind: 'role_permission_denied',
1015
+ kind: `${namespace}_permission_denied`,
974
1016
  self: subject.selfAid ?? null,
975
1017
  actorRole: subject.role,
976
1018
  operation: intent.operation,
@@ -1042,6 +1084,57 @@ async function authorizeRoleMenu(owner, params) {
1042
1084
  return denied;
1043
1085
  return { context: buildRoleMenuContext(owner, params.channel, subject) };
1044
1086
  }
1087
+ async function executeGroupMenu(owner, params) {
1088
+ let menuIntent;
1089
+ try {
1090
+ menuIntent = groupMenuIntent(params.kind, params.args);
1091
+ }
1092
+ catch (error) {
1093
+ return menuResultFailure(error);
1094
+ }
1095
+ const authorizationArgs = groupMenuAuthorizationArgs(params.args, params.subject.selfAid, menuIntent.scope === 'relation' ? params.subject.peerKey : undefined);
1096
+ const authorizationSubject = params.subject.isDaemonOwner && params.subject.role !== 'owner'
1097
+ ? { ...params.subject, role: 'owner' }
1098
+ : params.subject;
1099
+ const denied = await authorizeMenuIntent.call(owner, {
1100
+ intent: {
1101
+ operation: menuIntent.operation,
1102
+ scope: menuIntent.scope,
1103
+ source: params.source,
1104
+ args: authorizationArgs,
1105
+ },
1106
+ identity: params.identity,
1107
+ subject: authorizationSubject,
1108
+ session: params.session,
1109
+ explicitChatType: params.explicitChatType,
1110
+ channel: params.channel,
1111
+ channelId: params.channelId,
1112
+ userId: params.userId,
1113
+ fromControlChannel: params.fromControlChannel,
1114
+ source: params.source,
1115
+ });
1116
+ if (denied)
1117
+ return { ...denied, code: 'PERMISSION_DENIED' };
1118
+ try {
1119
+ const currentGroupId = params.session?.chatType === 'group'
1120
+ ? String(params.session.metadata?.groupId || params.channelId)
1121
+ : undefined;
1122
+ const roleContext = buildRoleMenuContext(owner, params.channel, params.subject);
1123
+ return {
1124
+ data: await handleGroupMenu({ kind: params.kind, args: params.args, value: params.value }, {
1125
+ self: params.subject.selfAid ?? roleContext.self,
1126
+ role: params.subject.role,
1127
+ isDaemonOwner: params.subject.isDaemonOwner,
1128
+ currentGroupId,
1129
+ baseagent: params.session?.baseagent,
1130
+ roleContext,
1131
+ }),
1132
+ };
1133
+ }
1134
+ catch (error) {
1135
+ return menuResultFailure(error);
1136
+ }
1137
+ }
1045
1138
  function ecwebErr(id, name, code, message) {
1046
1139
  return menuFailure({ id, ...(name ? { name } : {}) }, normalizeMenuError({ code, message }));
1047
1140
  }
@@ -1107,14 +1200,59 @@ export function validateScheduleParams(scheduleType, scheduleValue) {
1107
1200
  }
1108
1201
  /**
1109
1202
  * 返回结构化命令菜单(供 menu.query 使用)
1110
- * owner 看到全部命令,admin 看到管理级命令(不含 owner-only),visitor/member 仅看到用户级命令
1203
+ * owner-only 及尚未迁移的命令保留角色语义;已迁移的命令按 operation capability 展示。
1111
1204
  */
1112
- export function getMenuItems(role, chatType = 'private', scope = 'agent') {
1205
+ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSubject) {
1113
1206
  const isOwner = role === 'owner';
1114
1207
  const isAdmin = isManagementRole(role);
1115
1208
  const isControlScope = scope === 'control';
1116
1209
  const canReadTopic = role !== 'none';
1117
1210
  const items = [];
1211
+ const normalizedChatType = chatType === 'group' ? 'group' : 'private';
1212
+ const canOperation = (operation, operationScope) => {
1213
+ if (authSubject) {
1214
+ const relationArgs = operationScope === 'relation'
1215
+ ? buildRelationIntentArgs({ selfAid: authSubject.selfAid, peerKey: authSubject.peerKey })
1216
+ : {};
1217
+ return authorizeOperation({
1218
+ source: 'menu',
1219
+ intent: { operation, scope: operationScope, source: 'menu', args: relationArgs },
1220
+ subject: authSubject,
1221
+ audit: false,
1222
+ }).allow;
1223
+ }
1224
+ return evaluateRoleOperationCapability({
1225
+ role,
1226
+ operation,
1227
+ scope: operationScope,
1228
+ chatType: normalizedChatType,
1229
+ fromControlChannel: isControlScope,
1230
+ }).allow;
1231
+ };
1232
+ const canUseModel = canOperation('model.use', 'relation');
1233
+ const canSetEffort = canOperation('model.effort', 'relation');
1234
+ const canSetChatmode = canOperation('chatmode.update', 'relation');
1235
+ const canSetMentionmode = normalizedChatType === 'group' && canOperation('mentionmode.update', 'relation');
1236
+ const canReadPermission = canOperation('permission.current', 'relation');
1237
+ const canUseFile = canOperation('file.fetch', 'filesystem') || canOperation('file.list', 'filesystem');
1238
+ const canManageProcess = authSubject ? authSubject.isDaemonOwner : isOwner;
1239
+ const configurableSettings = [
1240
+ ...(canUseModel ? [{ cmd: '/model', label: '切换模型', desc: '切换当前 Agent 使用的模型版本', next: { type: 'select', dynamic: true } }] : []),
1241
+ ...(canSetEffort ? [{ cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', items: [
1242
+ { value: 'low', label: 'Low' },
1243
+ { value: 'medium', label: 'Medium' },
1244
+ { value: 'high', label: 'High' },
1245
+ { value: 'max', label: 'Max' },
1246
+ ] } }] : []),
1247
+ ...(canSetChatmode ? [{ cmd: '/chatmode', label: '切换会话模式', desc: '控制 Agent 主动性(被动响应或主动推进)', next: { type: 'select', items: [
1248
+ { value: 'interactive', label: '交互模式', desc: '仅在收到消息时响应' },
1249
+ { value: 'proactive', label: '主动模式', desc: 'Agent 可主动推进任务' },
1250
+ ] } }] : []),
1251
+ ...(canSetMentionmode ? [{ cmd: '/mentionmode', label: '切换 @ 处理模式', desc: '控制群聊消息过滤(仅@提及或全部响应)', next: { type: 'select', items: [
1252
+ { value: 'mention-only', label: '@ 提及', desc: '仅在被 @ 提及时响应' },
1253
+ { value: 'disabled', label: '全部响应', desc: '响应群内所有消息' },
1254
+ ] } }] : []),
1255
+ ];
1118
1256
  if (!isAdmin && chatType === 'group') {
1119
1257
  return [
1120
1258
  ...(canReadTopic ? [{
@@ -1123,11 +1261,20 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent') {
1123
1261
  { cmd: '/topic', label: '话题管理', desc: '查看当前聊天的话题会话', next: { type: 'select', dynamic: true } },
1124
1262
  ]
1125
1263
  }] : []),
1264
+ ...(configurableSettings.length > 0 ? [{
1265
+ group: 'Agent 与模型',
1266
+ commands: configurableSettings,
1267
+ }] : []),
1268
+ ...(canReadPermission ? [{
1269
+ group: '权限管理',
1270
+ commands: [{ cmd: '/perm', label: '查看权限模式', desc: '权限模式由当前角色定义决定' }],
1271
+ }] : []),
1126
1272
  {
1127
1273
  group: '其他',
1128
1274
  commands: [
1129
1275
  { cmd: '/status', label: '显示会话状态' },
1130
1276
  { cmd: '/check', label: '检查 EvolAgent 实例健康' },
1277
+ ...(canUseFile ? [{ cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' }] : []),
1131
1278
  { cmd: '/help', label: '显示帮助信息' },
1132
1279
  ]
1133
1280
  }
@@ -1148,73 +1295,52 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent') {
1148
1295
  ] : []),
1149
1296
  ]
1150
1297
  });
1151
- if (isAdmin) {
1298
+ if (isAdmin || configurableSettings.length > 0) {
1152
1299
  items.push({
1153
1300
  group: 'Agent 与模型',
1154
1301
  commands: [
1155
- { cmd: '/baseagent', label: '切换 Agent 后端', desc: '切换 Agent 当前 Baseagent', next: { type: 'select', dynamic: true } },
1156
- { cmd: '/model', label: '切换模型', desc: '切换当前 Agent 使用的模型版本', next: { type: 'select', dynamic: true } },
1157
- { cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', items: [
1158
- { value: 'low', label: 'Low' },
1159
- { value: 'medium', label: 'Medium' },
1160
- { value: 'high', label: 'High' },
1161
- { value: 'max', label: 'Max' },
1162
- ] } },
1163
- { cmd: '/chatmode', label: '切换会话模式', desc: '控制 Agent 主动性(被动响应或主动推进)', next: { type: 'select', items: [
1164
- { value: 'interactive', label: '交互模式', desc: '仅在收到消息时响应' },
1165
- { value: 'proactive', label: '主动模式', desc: 'Agent 可主动推进任务' },
1166
- ] } },
1167
- { cmd: '/mentionmode', label: '切换 @ 处理模式', desc: '控制群聊消息过滤(仅@提及或全部响应)', next: { type: 'select', items: [
1168
- { value: 'mention-only', label: '@ 提及', desc: '仅在被 @ 提及时响应' },
1169
- { value: 'disabled', label: '全部响应', desc: '响应群内所有消息' },
1170
- ] } },
1171
- { cmd: '/capability', label: '能力开关管理', desc: '查看并管理 Skills / MCP / Plugins' },
1302
+ ...(isAdmin ? [{ cmd: '/baseagent', label: '切换 Agent 后端', desc: '切换 Agent 当前 Baseagent', next: { type: 'select', dynamic: true } }] : []),
1303
+ ...configurableSettings,
1304
+ ...(isAdmin ? [{ cmd: '/capability', label: '能力开关管理', desc: '查看并管理 Skills / MCP / Plugins' }] : []),
1172
1305
  ]
1173
1306
  });
1307
+ }
1308
+ if (canReadPermission) {
1174
1309
  items.push({
1175
1310
  group: '权限管理',
1176
1311
  commands: [
1177
1312
  { cmd: '/perm', label: '查看权限模式', desc: '权限模式由当前角色定义决定' },
1178
1313
  ]
1179
1314
  });
1180
- items.push({
1181
- group: '运维',
1182
- commands: [
1183
- { cmd: '/status', label: '显示会话状态', desc: '查看当前会话、项目、Agent 的详细状态' },
1184
- { cmd: '/stop', label: '中断当前任务', desc: '立即中断正在执行的 Agent 任务' },
1185
- { cmd: '/check', label: '检查 EvolAgent 实例健康', desc: '检查各 EvolAgent 实例、后端与消息通道的健康状态' },
1186
- ...(isOwner && !isControlScope ? [{
1187
- cmd: '/observable',
1188
- label: '观察者模式',
1189
- desc: '向 owners 转发 Agent 入站和出站消息',
1190
- next: { type: 'select', items: [
1191
- { value: 'true', label: '开启' },
1192
- { value: 'false', label: '关闭' },
1193
- ] },
1194
- }] : []),
1195
- { cmd: '/activity', label: '控制中间输出显示', desc: '设置工具调用过程的可见范围', next: { type: 'select', items: [
1315
+ }
1316
+ items.push({
1317
+ group: isAdmin ? '运维' : '其他',
1318
+ commands: [
1319
+ { cmd: '/status', label: '显示会话状态', desc: isAdmin ? '查看当前会话、项目、Agent 的详细状态' : '查看当前会话的基本状态' },
1320
+ ...(isAdmin ? [{ cmd: '/stop', label: '中断当前任务', desc: '立即中断正在执行的 Agent 任务' }] : []),
1321
+ { cmd: '/check', label: '检查 EvolAgent 实例健康', desc: isAdmin ? '检查各 EvolAgent 实例、后端与消息通道的健康状态' : '检查 EvolAgent 实例与消息通道健康状态' },
1322
+ ...(isOwner && !isControlScope ? [{
1323
+ cmd: '/observable',
1324
+ label: '观察者模式',
1325
+ desc: '向 owners 转发 Agent 入站和出站消息',
1326
+ next: { type: 'select', items: [
1327
+ { value: 'true', label: '开启' },
1328
+ { value: 'false', label: '关闭' },
1329
+ ] },
1330
+ }] : []),
1331
+ ...(isAdmin ? [{ cmd: '/activity', label: '控制中间输出显示', desc: '设置工具调用过程的可见范围', next: { type: 'select', items: [
1196
1332
  { value: 'all', label: '全部显示', desc: '所有用户均可见中间输出' },
1197
1333
  { value: 'text', label: '仅文字进展', desc: '显示文字进展,隐藏工具活动' },
1198
1334
  { value: 'none', label: '不显示', desc: '关闭所有中间输出' },
1199
- ] } },
1200
- ...(isControlScope && isOwner ? [
1201
- { cmd: '/restart', label: '重启服务', desc: '重启整个 EvolCore 服务进程' },
1202
- ] : []),
1203
- ...(isAdmin ? [
1204
- { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
1205
- ] : []),
1206
- ]
1207
- });
1208
- }
1209
- else {
1210
- items.push({
1211
- group: '其他',
1212
- commands: [
1213
- { cmd: '/status', label: '显示会话状态', desc: '查看当前会话的基本状态' },
1214
- { cmd: '/check', label: '检查 EvolAgent 实例健康', desc: '检查 EvolAgent 实例与消息通道健康状态' },
1215
- ]
1216
- });
1217
- }
1335
+ ] } }] : []),
1336
+ ...(isControlScope && canManageProcess && canOperation('system.restart', 'process') ? [
1337
+ { cmd: '/restart', label: '重启服务', desc: '重启整个 EvolCore 服务进程' },
1338
+ ] : []),
1339
+ ...(canUseFile ? [
1340
+ { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
1341
+ ] : []),
1342
+ ]
1343
+ });
1218
1344
  items.push({
1219
1345
  group: '帮助',
1220
1346
  commands: [
@@ -1263,12 +1389,34 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1263
1389
  if ('error' in result)
1264
1390
  throw result.error;
1265
1391
  const opts = result.data?.options ?? [];
1266
- return opts.map((opt) => ({ value: opt.option, label: opt.label, desc: opt.description }));
1392
+ return opts.map((opt) => ({
1393
+ value: opt.option,
1394
+ label: opt.label,
1395
+ desc: opt.description,
1396
+ ...(typeof opt.selected === 'boolean' ? { selected: opt.selected } : {}),
1397
+ }));
1267
1398
  }
1268
1399
  catch (error) {
1269
1400
  throw menuResultFailure(error);
1270
1401
  }
1271
1402
  }
1403
+ if (cmdBase0 === '/group') {
1404
+ const result = await executeGroupMenu(this, {
1405
+ kind: 'options', args,
1406
+ identity: subject.identity,
1407
+ subject,
1408
+ session,
1409
+ explicitChatType,
1410
+ channel,
1411
+ channelId,
1412
+ userId,
1413
+ fromControlChannel,
1414
+ source,
1415
+ });
1416
+ if ('error' in result)
1417
+ throw { code: result.code, message: result.error, data: result.data };
1418
+ return result.data;
1419
+ }
1272
1420
  // ── /agent list(只读) ──
1273
1421
  // 控制 channel:验 evolcore.owners,返回全量;agent channel:放行但仅返回自身单条。
1274
1422
  if (cmd === '/agent') {
@@ -1633,6 +1781,12 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
1633
1781
  return menuResultFailure(error);
1634
1782
  }
1635
1783
  }
1784
+ if (cmdBase === '/group') {
1785
+ return await executeGroupMenu(this, {
1786
+ kind: 'query', args, identity, subject, session,
1787
+ explicitChatType, channel, channelId, userId, fromControlChannel, source,
1788
+ });
1789
+ }
1636
1790
  const authDenied = await authorizeMenuIntent.call(this, {
1637
1791
  intent: buildMenuIntent('query', cmdBase, args, undefined, undefined, fromControlChannel),
1638
1792
  identity,
@@ -2056,13 +2210,11 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2056
2210
  return { data };
2057
2211
  }
2058
2212
  // ── name=file:文件元信息(§5.1) ──
2059
- // 权限(§6.3):agent owner/admin 或 aid channel owner。项目外文件仅 owner 可取(§6.2,由 resolveMenuFilePath 校验)。
2213
+ // operation 权限已在入口统一判定;项目外文件仍仅 owner 可取。
2060
2214
  if (cmdBase === '/file') {
2061
2215
  const role = identity.role;
2062
- if (!isManagementRole(role)) {
2063
- return { error: '无权限', code: 'NO_PERMISSION' };
2064
- }
2065
- const resolved = resolveMenuFilePath(args?.path, session, role, 'file');
2216
+ const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
2217
+ const resolved = resolveMenuFilePath(args?.path, projectPath, role, 'file');
2066
2218
  if ('error' in resolved)
2067
2219
  return resolved;
2068
2220
  const { realPath, stat } = resolved;
@@ -2153,6 +2305,12 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2153
2305
  return menuResultFailure(error);
2154
2306
  }
2155
2307
  }
2308
+ if (cmdBase === '/group') {
2309
+ return await executeGroupMenu(this, {
2310
+ kind: 'update', args, value: arg, identity, subject, session,
2311
+ channel, channelId, userId, fromControlChannel, source,
2312
+ });
2313
+ }
2156
2314
  const authDenied = await authorizeMenuIntent.call(this, {
2157
2315
  intent: buildMenuIntent('update', cmdBase, args, undefined, arg, fromControlChannel),
2158
2316
  identity,
@@ -2492,7 +2650,7 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2492
2650
  return { error: `不支持 update: ${cmdBase}`, code: 'NOT_SUPPORTED' };
2493
2651
  }
2494
2652
  /** menu.action — 触发动词。 */
2495
- export async function execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel = false, authSubject, source = 'menu') {
2653
+ export async function execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel = false, authSubject, source = 'menu', transportReplyContext, deferSideEffect) {
2496
2654
  const cmdBase = cmd.trim().split(' ')[0];
2497
2655
  if (!cmdBase)
2498
2656
  return { error: '缺少命令', code: 'MISSING_CMD' };
@@ -2586,7 +2744,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
2586
2744
  return await gatewaySyncEnv(args);
2587
2745
  return { error: `不支持 gateway action: ${action}`, code: 'NOT_SUPPORTED' };
2588
2746
  }
2589
- const { session } = await this.loadMenuContext(channel, channelId);
2747
+ const { session, evolagent } = await this.loadMenuContext(channel, channelId);
2590
2748
  const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
2591
2749
  const isMenuAdmin = isManagementRole(identity.role);
2592
2750
  // ── /agent action ──
@@ -3175,6 +3333,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3175
3333
  if (!argvValidation.ok)
3176
3334
  return { error: argvValidation.reason, code: 'INVALID_ARGUMENT' };
3177
3335
  let argv = argvValidation.argv;
3336
+ const requestedArgv = [...argvValidation.argv];
3178
3337
  argv = normalizeCliArgv(argv);
3179
3338
  const cliAgent = this.getOwningAgent?.(channel);
3180
3339
  const cliSelfAid = cliAgent?.aid;
@@ -3204,6 +3363,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3204
3363
  scope: parsed.intent.scope, dangerous: true, actorId: userId,
3205
3364
  channel, channelId, role: identity.role, decision: 'deny', code: 'NOT_ALLOWED',
3206
3365
  reason: 'Unrecognized CLI command', taskId: requestId, argvHash: hashArgv(intentArgv),
3366
+ name: 'cli', action: 'exec', args: { argv: [...requestedArgv] },
3207
3367
  });
3208
3368
  return { error: 'CLI command is not allowed', code: 'NOT_ALLOWED' };
3209
3369
  }
@@ -3242,14 +3402,21 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3242
3402
  resolvedConfigCommand: parsed.resolvedConfigCommand,
3243
3403
  allowExplicitRelationTarget: true,
3244
3404
  auditAllowed: false,
3245
- auditMetadata: { taskId: requestId, argvHash: hashArgv(authArgv) },
3405
+ auditMetadata: {
3406
+ taskId: requestId,
3407
+ argvHash: hashArgv(authArgv),
3408
+ name: 'cli',
3409
+ action: 'exec',
3410
+ args: { argv: [...requestedArgv] },
3411
+ },
3246
3412
  });
3247
3413
  if (!decision.allow) {
3248
3414
  return { error: decision.reason, code: decision.code };
3249
3415
  }
3250
3416
  authorizedCommands.push({ parsed, decision });
3251
3417
  }
3252
- const execution = await this.execCliPassthrough(argv, subject.role);
3418
+ const rawExecution = await this.execCliPassthrough(argv, subject.role);
3419
+ const execution = appendGroupRulesCompatibilityNote(rawExecution, parsedCommands[0]?.resolvedConfigCommand, session);
3253
3420
  const executionData = 'data' in execution ? execution.data : undefined;
3254
3421
  for (const { parsed, decision } of authorizedCommands) {
3255
3422
  await auditCommandAuthorization({
@@ -3259,6 +3426,8 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3259
3426
  channel, channelId, role: identity.role, isDaemonOwner,
3260
3427
  fromControlChannel: fromControlChannel ?? false, decision: 'allow',
3261
3428
  matchedRule: decision.command?.matchedRule, taskId: requestId, argvHash: hashArgv(argv),
3429
+ name: 'cli', action: 'exec',
3430
+ args: { argv: [...requestedArgv] },
3262
3431
  durationMs: executionData?.durationMs, exitCode: executionData?.exitCode,
3263
3432
  });
3264
3433
  }
@@ -3269,13 +3438,10 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3269
3438
  // fetch 文件作为独立 result.file 消息异步发回;把请求 id 作为 correlationId 透传,
3270
3439
  // 客户端用它把异步到达的文件消息对回这次 fetch 点击。
3271
3440
  if (cmdBase === '/file') {
3272
- // 权限(§6.3):agent owner/admin aid channel owner。项目外文件仅 owner(§6.2,由 resolveMenuFilePath 校验)。
3273
- if (!isManagementRole(identity.role)) {
3274
- return { error: '无权限', code: 'NO_PERMISSION' };
3275
- }
3441
+ const projectPath = session?.projectPath ?? evolagent?.projectPath ?? this.getOwningAgent?.(channel)?.projectPath;
3276
3442
  if (action === 'list') {
3277
3443
  const dirArg = (args?.path ?? '.').toString().trim() || '.';
3278
- const resolved = resolveMenuFilePath(dirArg, session, identity.role, 'directory');
3444
+ const resolved = resolveMenuFilePath(dirArg, projectPath, identity.role, 'directory');
3279
3445
  if ('error' in resolved)
3280
3446
  return resolved;
3281
3447
  const offset = parseFileListOffset(args?.offset);
@@ -3288,7 +3454,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3288
3454
  }
3289
3455
  if (action !== 'fetch')
3290
3456
  return { error: `不支持的 file action: ${action}`, code: 'NOT_SUPPORTED' };
3291
- const resolved = resolveMenuFilePath(args?.path, session, identity.role, 'file');
3457
+ const resolved = resolveMenuFilePath(args?.path, projectPath, identity.role, 'file');
3292
3458
  if ('error' in resolved)
3293
3459
  return resolved;
3294
3460
  const { realPath, stat } = resolved;
@@ -3300,9 +3466,26 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3300
3466
  return { error: '通道不存在', code: 'EXEC_FAILED' };
3301
3467
  if (!adapter.capabilities?.file)
3302
3468
  return { error: '通道不支持文件发送', code: 'NOT_SUPPORTED' };
3303
- try {
3304
- const replyCtx = session ? this.getReplyContext(session) : undefined;
3469
+ const sessionReplyContext = session ? this.getReplyContext(session) : undefined;
3470
+ const replyCtx = transportReplyContext
3471
+ ? {
3472
+ ...(sessionReplyContext ?? {}),
3473
+ ...transportReplyContext,
3474
+ metadata: {
3475
+ ...(sessionReplyContext?.metadata ?? {}),
3476
+ ...(transportReplyContext.metadata ?? {}),
3477
+ },
3478
+ }
3479
+ : sessionReplyContext;
3480
+ const sendFile = async () => {
3305
3481
  await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath, correlationId: requestId });
3482
+ };
3483
+ if (deferSideEffect) {
3484
+ deferSideEffect(sendFile);
3485
+ return { data: { accepted: true } };
3486
+ }
3487
+ try {
3488
+ await sendFile();
3306
3489
  return { data: { accepted: true } };
3307
3490
  }
3308
3491
  catch (e) {
@@ -3319,6 +3502,7 @@ const SYSTEM_CONTROL_NAME_MAP = {
3319
3502
  observable: '/observable',
3320
3503
  agent: '/agent', trigger: '/trigger', file: '/file', gateway: '/gateway',
3321
3504
  config: '/config', capability: '/capability', role: '/role',
3505
+ group: '/group', connect: '/connect',
3322
3506
  };
3323
3507
  function isProcessLevelMenu(name, cmd) {
3324
3508
  return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config'
@@ -3337,9 +3521,9 @@ async function execMenuForSystemControl(payload, context) {
3337
3521
  const isRoleRequest = name === 'role' || payload?.cmd === '/role';
3338
3522
  const isConnectRequest = name === 'connect' || payload?.cmd === '/connect';
3339
3523
  const requiresAgentTarget = isRoleRequest || isConnectRequest || [
3340
- 'observable', 'baseagent', 'model', 'effort', 'trigger', 'capability',
3524
+ 'observable', 'baseagent', 'model', 'effort', 'trigger', 'capability', 'group',
3341
3525
  ].includes(name)
3342
- || ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability'].includes(payload?.cmd);
3526
+ || ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability', '/group'].includes(payload?.cmd);
3343
3527
  const target = isProcessLevel
3344
3528
  ? { channel: SYSTEM_CONTROL_CHANNEL }
3345
3529
  : resolveExternalMenuAgentChannel(this, payload, SYSTEM_CONTROL_CHANNEL, requiresAgentTarget);
@@ -3364,7 +3548,7 @@ async function execMenuForSystemControl(payload, context) {
3364
3548
  try {
3365
3549
  switch (payload?.type) {
3366
3550
  case 'menu.list':
3367
- return menuSuccess({ id }, this.getMenuItems(trustedIdentity.role, 'private', 'control'));
3551
+ return menuSuccess({ id }, this.getMenuItems(trustedIdentity.role, 'private', 'control', trustedSubject));
3368
3552
  case 'menu.query': {
3369
3553
  if (!cmd)
3370
3554
  return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'crypto';
2
2
  export const MENU_REQUEST_TYPES = new Set([
3
+ 'menu.token.request',
3
4
  'menu.list',
4
5
  'menu.query',
5
6
  'menu.options',
@@ -12,6 +13,7 @@ const EXPLICIT_SCOPE_UPDATE_NAMES = new Set([
12
13
  'chatmode',
13
14
  'dispatch',
14
15
  'mentionmode',
16
+ 'group',
15
17
  'permission',
16
18
  ]);
17
19
  const EXPLICIT_SCOPE_UPDATE_COMMANDS = new Set([
@@ -20,6 +22,7 @@ const EXPLICIT_SCOPE_UPDATE_COMMANDS = new Set([
20
22
  '/chatmode',
21
23
  '/dispatch',
22
24
  '/mentionmode',
25
+ '/group',
23
26
  '/perm',
24
27
  ]);
25
28
  function firstCommandToken(value) {
@@ -78,6 +81,8 @@ export function validateMenuRequest(request) {
78
81
  if (!MENU_REQUEST_TYPES.has(type)) {
79
82
  return { code: 'METHOD_NOT_FOUND', message: `Unknown menu request type: ${type}` };
80
83
  }
84
+ if (type === 'menu.token.request')
85
+ return null;
81
86
  const hasName = typeof request.name === 'string' && request.name.trim().length > 0;
82
87
  const hasCompatCmd = typeof request.cmd === 'string' && request.cmd.trim().length > 0;
83
88
  if (type !== 'menu.list' && !hasName && !hasCompatCmd) {
@@ -144,8 +149,9 @@ const INVALID_ARGUMENT_CODES = new Set([
144
149
  const STABLE_CODES = new Set([
145
150
  'ROLE_ACCESS_DENIED', 'PERMISSION_DENIED', 'UNAUTHORIZED', 'FORBIDDEN',
146
151
  'INVALID_ARGUMENT', 'NOT_ALLOWED', 'NOT_SUPPORTED', 'UNSUPPORTED',
147
- 'METHOD_NOT_FOUND', 'NOT_FOUND', 'CONFLICT', 'EXECUTION_TIMEOUT',
148
- 'TEMPORARILY_UNAVAILABLE', 'INTERNAL_ERROR', 'MISSING_SCOPE',
152
+ 'METHOD_NOT_FOUND', 'NOT_FOUND', 'CONFLICT', 'EXPIRED', 'EXECUTION_TIMEOUT',
153
+ 'TEMPORARILY_UNAVAILABLE', 'INTERNAL_ERROR', 'MISSING_SCOPE', 'UPGRADE_REQUIRED',
154
+ 'MENU_TOKEN_REQUIRED', 'MENU_TOKEN_REJECTED', 'MENU_TOKEN_ENCRYPTION_REQUIRED',
149
155
  ]);
150
156
  export function normalizeMenuError(error) {
151
157
  const source = error && typeof error === 'object' ? error : {};