evolcore 0.0.1 → 0.0.2

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 (371) hide show
  1. package/CHANGELOG.md +813 -0
  2. package/LICENSE +21 -0
  3. package/MIGRATION-0.5.0.md +378 -0
  4. package/README.md +318 -14
  5. package/ROLE_ACCESS_CONTROL.md +174 -0
  6. package/assets/.env.template +4 -0
  7. package/assets/brand/evolcore/README.md +19 -0
  8. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  9. package/assets/brand/evolcore/evolcore-app-icon.svg +13 -0
  10. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  11. package/assets/brand/evolcore/evolcore-brand-board.svg +126 -0
  12. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  13. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  14. package/assets/brand/evolcore/evolcore-logo-reverse.svg +14 -0
  15. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  16. package/assets/brand/evolcore/evolcore-logo.svg +14 -0
  17. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  18. package/assets/brand/evolcore/evolcore-mark.svg +10 -0
  19. package/bin/ec-safe-output.js +161 -0
  20. package/bin/ec.js +29 -0
  21. package/dist/agents/baseagent.js +163 -0
  22. package/dist/agents/claude-runner.js +2385 -0
  23. package/dist/agents/codex-app-server-client.js +448 -0
  24. package/dist/agents/codex-runner.js +2639 -0
  25. package/dist/agents/gemini-runner.js +666 -0
  26. package/dist/agents/runner-types.js +75 -0
  27. package/dist/aun/aid/agentmd.js +216 -0
  28. package/dist/aun/aid/client.js +132 -0
  29. package/dist/aun/aid/control-aid.js +91 -0
  30. package/dist/aun/aid/identity.js +518 -0
  31. package/dist/aun/aid/index.js +4 -0
  32. package/dist/aun/aid/store.js +74 -0
  33. package/dist/aun/aid/types.js +1 -0
  34. package/dist/aun/aid/validation.js +21 -0
  35. package/dist/aun/group-identity.js +10 -0
  36. package/dist/aun/msg/group-index.js +6 -0
  37. package/dist/aun/msg/group.js +1231 -0
  38. package/dist/aun/msg/history.js +123 -0
  39. package/dist/aun/msg/index.js +5 -0
  40. package/dist/aun/msg/p2p.js +393 -0
  41. package/dist/aun/msg/payload-type.js +27 -0
  42. package/dist/aun/msg/upload.js +137 -0
  43. package/dist/aun/outbox.js +160 -0
  44. package/dist/aun/rpc/caller.js +42 -0
  45. package/dist/aun/rpc/connection.js +25 -0
  46. package/dist/aun/rpc/index.js +2 -0
  47. package/dist/aun/service-proxy.js +225 -0
  48. package/dist/aun/storage/download.js +29 -0
  49. package/dist/aun/storage/index.js +3 -0
  50. package/dist/aun/storage/manage.js +10 -0
  51. package/dist/aun/storage/upload.js +68 -0
  52. package/dist/channels/aun.js +4147 -0
  53. package/dist/channels/daemon.js +422 -0
  54. package/dist/channels/dingtalk.js +649 -0
  55. package/dist/channels/feishu.js +1789 -0
  56. package/dist/channels/qqbot.js +409 -0
  57. package/dist/channels/wechat.js +817 -0
  58. package/dist/channels/wecom.js +565 -0
  59. package/dist/cli/agent-command.js +641 -0
  60. package/dist/cli/agent.js +1059 -0
  61. package/dist/cli/aun-commands.js +1948 -0
  62. package/dist/cli/bench.js +1228 -0
  63. package/dist/cli/cli-argv.js +66 -0
  64. package/dist/cli/code-stats.js +329 -0
  65. package/dist/cli/command-log.js +82 -0
  66. package/dist/cli/config-selector.js +69 -0
  67. package/dist/cli/config.js +261 -0
  68. package/dist/cli/ctl-command.js +62 -0
  69. package/dist/cli/daemon-commands.js +2887 -0
  70. package/dist/cli/fs-command.js +1447 -0
  71. package/dist/cli/handoff-command.js +302 -0
  72. package/dist/cli/help.js +31 -0
  73. package/dist/cli/index.js +358 -0
  74. package/dist/cli/init-channel.js +1377 -0
  75. package/dist/cli/init.js +553 -0
  76. package/dist/cli/link-rules.js +240 -0
  77. package/dist/cli/model.js +590 -0
  78. package/dist/cli/net-check.js +723 -0
  79. package/dist/cli/queue-command.js +126 -0
  80. package/dist/cli/response.js +345 -0
  81. package/dist/cli/restart-monitor.js +456 -0
  82. package/dist/cli/stats.js +607 -0
  83. package/dist/cli/task-context.js +80 -0
  84. package/dist/cli/trigger-command.js +505 -0
  85. package/dist/cli/version.js +88 -0
  86. package/dist/cli/watch-logs.js +33 -0
  87. package/dist/cli/watch-msg.js +673 -0
  88. package/dist/config/boot-log.js +266 -0
  89. package/dist/config/builtin-role-templates.js +30 -0
  90. package/dist/config/builtin-roles.js +85 -0
  91. package/dist/config/config-batch-get.js +11 -0
  92. package/dist/config/config-field-policy.js +261 -0
  93. package/dist/config/config-manager.js +916 -0
  94. package/dist/config/config-operation-service.js +384 -0
  95. package/dist/config/contact-book.js +371 -0
  96. package/dist/config/gateway-config.js +858 -0
  97. package/dist/config/lifecycle.js +17 -0
  98. package/dist/config/mention-mode.js +27 -0
  99. package/dist/config/merge.js +161 -0
  100. package/dist/config/owner-policy.js +4 -0
  101. package/dist/config/peer-role-resolver.js +139 -0
  102. package/dist/config/resolved-config-op.js +483 -0
  103. package/dist/config/role-config-v4-startup.js +32 -0
  104. package/dist/config/role-config-v5-startup.js +27 -0
  105. package/dist/config/role-schema.js +105 -0
  106. package/dist/config/role-service.js +159 -0
  107. package/dist/config/role-store.js +204 -0
  108. package/dist/config/roles.js +55 -0
  109. package/dist/config/schema-registry.js +154 -0
  110. package/dist/config/snapshot.js +598 -0
  111. package/dist/config-store.js +503 -0
  112. package/dist/core/auth/agent-delegation.js +111 -0
  113. package/dist/core/auth/auth-gateway.js +166 -0
  114. package/dist/core/auth/authenticated-actor.js +6 -0
  115. package/dist/core/auth/authorization-audit.js +110 -0
  116. package/dist/core/auth/operation-authorizer.js +718 -0
  117. package/dist/core/auth/operation-catalog.js +675 -0
  118. package/dist/core/baseagent-loader.js +54 -0
  119. package/dist/core/bootstrap-service.js +175 -0
  120. package/dist/core/capability/capability-manager.js +316 -0
  121. package/dist/core/capability/providers/claude-capability-provider.js +176 -0
  122. package/dist/core/capability/providers/codex-capability-provider.js +148 -0
  123. package/dist/core/capability/providers/gemini-capability-provider.js +10 -0
  124. package/dist/core/capability/types.js +27 -0
  125. package/dist/core/causation/audit.js +103 -0
  126. package/dist/core/causation/aun-association.js +111 -0
  127. package/dist/core/causation/context.js +93 -0
  128. package/dist/core/causation/index.js +4 -0
  129. package/dist/core/causation/types.js +2 -0
  130. package/dist/core/channel-loader.js +277 -0
  131. package/dist/core/command/agent-control.js +616 -0
  132. package/dist/core/command/cli-intent-parser.js +225 -0
  133. package/dist/core/command/command-handler.js +1638 -0
  134. package/dist/core/command/menu-handler.js +3354 -0
  135. package/dist/core/command/menu-protocol.js +247 -0
  136. package/dist/core/command/role-menu.js +1524 -0
  137. package/dist/core/command/slash-gate.js +148 -0
  138. package/dist/core/command/slash-handler.js +3056 -0
  139. package/dist/core/daemon-file-cache.js +216 -0
  140. package/dist/core/event-bus.js +32 -0
  141. package/dist/core/event-catalog.js +740 -0
  142. package/dist/core/evolagent-registry.js +546 -0
  143. package/dist/core/evolagent.js +331 -0
  144. package/dist/core/handoff/dispatcher.js +229 -0
  145. package/dist/core/handoff/mutex.js +46 -0
  146. package/dist/core/handoff/runtime.js +324 -0
  147. package/dist/core/handoff/store.js +537 -0
  148. package/dist/core/handoff/types.js +12 -0
  149. package/dist/core/inference/text-inference.js +173 -0
  150. package/dist/core/interaction-registration.js +10 -0
  151. package/dist/core/interaction-router.js +278 -0
  152. package/dist/core/message/create-status.js +67 -0
  153. package/dist/core/message/im-renderer.js +657 -0
  154. package/dist/core/message/items-formatter.js +76 -0
  155. package/dist/core/message/logical-queue-bridge.js +123 -0
  156. package/dist/core/message/message-bridge.js +803 -0
  157. package/dist/core/message/message-cache.js +56 -0
  158. package/dist/core/message/message-log.js +339 -0
  159. package/dist/core/message/message-processor.js +4 -0
  160. package/dist/core/message/message-queue.js +1436 -0
  161. package/dist/core/message/message-utils.js +70 -0
  162. package/dist/core/message/peer-mode.js +105 -0
  163. package/dist/core/message/pending-hints.js +232 -0
  164. package/dist/core/message/response-depth.js +33 -0
  165. package/dist/core/message/response-engine.js +3776 -0
  166. package/dist/core/message/response-snapshot.js +83 -0
  167. package/dist/core/message/stream-debouncer.js +130 -0
  168. package/dist/core/message/stream-idle-monitor.js +124 -0
  169. package/dist/core/model/config-scope.js +162 -0
  170. package/dist/core/model/field-scope.js +78 -0
  171. package/dist/core/model/model-catalog.js +227 -0
  172. package/dist/core/model/model-diagnostics.js +182 -0
  173. package/dist/core/model/model-permission.js +90 -0
  174. package/dist/core/permission/approval-gateway.js +1017 -0
  175. package/dist/core/permission/ec-command-parser.js +347 -0
  176. package/dist/core/permission/execution-sandbox.js +16 -0
  177. package/dist/core/permission/index.js +6 -0
  178. package/dist/core/permission/mode.js +24 -0
  179. package/dist/core/permission/sandbox-runtime.js +265 -0
  180. package/dist/core/permission/tool-policy.js +987 -0
  181. package/dist/core/permission/unix-socket-policy.js +99 -0
  182. package/dist/core/protected-paths.js +330 -0
  183. package/dist/core/relation/peer-identity.js +222 -0
  184. package/dist/core/relation/peer-key.js +1 -0
  185. package/dist/core/role/runtime-policy.js +141 -0
  186. package/dist/core/session/adapters/claude-session-file-adapter.js +218 -0
  187. package/dist/core/session/adapters/codex-session-file-adapter.js +333 -0
  188. package/dist/core/session/adapters/gemini-session-file-adapter.js +181 -0
  189. package/dist/core/session/session-file-adapter.js +7 -0
  190. package/dist/core/session/session-file-health.js +45 -0
  191. package/dist/core/session/session-fs-store.js +232 -0
  192. package/dist/core/session/session-key.js +24 -0
  193. package/dist/core/session/session-manager.js +1587 -0
  194. package/dist/core/session/session-mapper.js +100 -0
  195. package/dist/core/session/session-renew.js +314 -0
  196. package/dist/core/session/session-title.js +128 -0
  197. package/dist/core/session/session-turn-coordinator.js +205 -0
  198. package/dist/core/session/session-turns.js +67 -0
  199. package/dist/core/system-channels.js +29 -0
  200. package/dist/eck/baseagent-caps.js +18 -0
  201. package/dist/eck/detect.js +47 -0
  202. package/dist/eck/group-rules-sync.js +345 -0
  203. package/dist/eck/init.js +77 -0
  204. package/dist/eck/kit-renderer.js +359 -0
  205. package/dist/eck/manifest-engine.js +446 -0
  206. package/dist/eck/message-renderer.js +199 -0
  207. package/dist/eck/rules-loader.js +28 -0
  208. package/dist/index.js +2870 -4
  209. package/dist/ipc.js +748 -0
  210. package/dist/paths.js +262 -0
  211. package/dist/product.js +18 -0
  212. package/dist/response-system/context-builder.js +71 -0
  213. package/dist/response-system/coordinator.js +117 -0
  214. package/dist/response-system/decision-executor.js +86 -0
  215. package/dist/response-system/engines/v1/index.js +21 -0
  216. package/dist/response-system/engines/v1/interactive-flow.js +27 -0
  217. package/dist/response-system/engines/v1/proactive-flow.js +137 -0
  218. package/dist/response-system/engines/v1/types.js +1 -0
  219. package/dist/response-system/extensions.js +41 -0
  220. package/dist/response-system/index.js +6 -0
  221. package/dist/response-system/modes/index.js +7 -0
  222. package/dist/response-system/modes/single-session/index.js +72 -0
  223. package/dist/response-system/queues/fifo-queue.js +44 -0
  224. package/dist/response-system/queues/index.js +6 -0
  225. package/dist/response-system/queues/lifo-queue.js +42 -0
  226. package/dist/response-system/queues/priority-queue.js +63 -0
  227. package/dist/response-system/registry.js +97 -0
  228. package/dist/response-system/resolver.js +37 -0
  229. package/dist/response-system/selector.js +23 -0
  230. package/dist/response-system/types.js +7 -0
  231. package/dist/stats/billing.js +151 -0
  232. package/dist/stats/budget.js +93 -0
  233. package/dist/stats/db.js +403 -0
  234. package/dist/stats/eck-vars.js +89 -0
  235. package/dist/stats/index.js +11 -0
  236. package/dist/stats/normalizer.js +80 -0
  237. package/dist/stats/price-resolver.js +138 -0
  238. package/dist/stats/query.js +763 -0
  239. package/dist/stats/role-budget.js +168 -0
  240. package/dist/stats/writer.js +151 -0
  241. package/dist/trigger/anomaly-store.js +258 -0
  242. package/dist/trigger/audit.js +152 -0
  243. package/dist/trigger/event-source.js +119 -0
  244. package/dist/trigger/feedback.js +685 -0
  245. package/dist/trigger/history.js +290 -0
  246. package/dist/trigger/manager.js +291 -0
  247. package/dist/trigger/parser.js +520 -0
  248. package/dist/trigger/patch.js +148 -0
  249. package/dist/trigger/scheduler.js +1600 -0
  250. package/dist/trigger/script-executor.js +155 -0
  251. package/dist/trigger/state.js +145 -0
  252. package/dist/trigger/types.js +1 -0
  253. package/dist/trigger/validation.js +621 -0
  254. package/dist/types.js +12 -0
  255. package/dist/utils/aid-bind.js +299 -0
  256. package/dist/utils/atomic-write.js +95 -0
  257. package/dist/utils/avatar-upload.js +123 -0
  258. package/dist/utils/cross-platform.js +297 -0
  259. package/dist/utils/ecweb-utils.js +73 -0
  260. package/dist/utils/error-dict.json +153 -0
  261. package/dist/utils/error-utils.js +349 -0
  262. package/dist/utils/instance-registry.js +437 -0
  263. package/dist/utils/locale.js +21 -0
  264. package/dist/utils/log-writer.js +224 -0
  265. package/dist/utils/logger.js +89 -0
  266. package/dist/utils/markdown-to-plain-text.js +20 -0
  267. package/dist/utils/media-cache.js +271 -0
  268. package/dist/utils/model-prices.jsonl +17 -0
  269. package/dist/utils/npm-ops.js +210 -0
  270. package/dist/utils/process-introspect.js +133 -0
  271. package/dist/utils/process-tree-stats.js +271 -0
  272. package/dist/utils/project-path.js +74 -0
  273. package/dist/utils/stats.js +410 -0
  274. package/dist/utils/tool-summary.js +284 -0
  275. package/dist/utils/welcome.js +268 -0
  276. package/kits/docs/GUIDE.md +20 -0
  277. package/kits/docs/INDEX.md +65 -0
  278. package/kits/docs/aun/CHEATSHEET.md +19 -0
  279. package/kits/docs/aun/SYNC_PROTOCOL.md +15 -0
  280. package/kits/docs/channels/aun.md +65 -0
  281. package/kits/docs/channels/feishu.md +56 -0
  282. package/kits/docs/context-assembly.md +366 -0
  283. package/kits/docs/eck_templates/GUIDE.template.md +22 -0
  284. package/kits/docs/eck_templates/INDEX.template.md +28 -0
  285. package/kits/docs/eck_templates/path-registry.template.md +33 -0
  286. package/kits/docs/eck_templates/runtime.template.md +19 -0
  287. package/kits/docs/evolcore/INDEX.md +66 -0
  288. package/kits/docs/evolcore/agent.md +69 -0
  289. package/kits/docs/evolcore/aid.md +49 -0
  290. package/kits/docs/evolcore/config.md +149 -0
  291. package/kits/docs/evolcore/ctl.md +46 -0
  292. package/kits/docs/evolcore/event.md +216 -0
  293. package/kits/docs/evolcore/fs-architecture.md +1215 -0
  294. package/kits/docs/evolcore/fs.md +101 -0
  295. package/kits/docs/evolcore/group-fs.md +17 -0
  296. package/kits/docs/evolcore/group-rules.md +226 -0
  297. package/kits/docs/evolcore/group.md +141 -0
  298. package/kits/docs/evolcore/model.md +47 -0
  299. package/kits/docs/evolcore/msg.md +130 -0
  300. package/kits/docs/evolcore/response.md +80 -0
  301. package/kits/docs/evolcore/rpc.md +35 -0
  302. package/kits/docs/evolcore/self-summary.md +29 -0
  303. package/kits/docs/evolcore/stats.md +70 -0
  304. package/kits/docs/evolcore/storage.md +49 -0
  305. package/kits/docs/evolcore/trigger.md +524 -0
  306. package/kits/docs/identity/AID_PROFILE_SPEC.md +26 -0
  307. package/kits/docs/identity/PATH_OPS.md +16 -0
  308. package/kits/docs/identity/ROLE_DETAIL.md +23 -0
  309. package/kits/docs/identity/identity-tools.md +26 -0
  310. package/kits/docs/path-registry.md +43 -0
  311. package/kits/docs/prompt-loading-architecture.md +266 -0
  312. package/kits/docs/venues/aun-group.md +45 -0
  313. package/kits/docs/venues/aun-private.md +10 -0
  314. package/kits/docs/venues/client-desktop.md +10 -0
  315. package/kits/docs/venues/client-mobile.md +10 -0
  316. package/kits/docs/venues/feishu-group.md +13 -0
  317. package/kits/docs/venues/feishu-private.md +9 -0
  318. package/kits/docs/venues/group.md +25 -0
  319. package/kits/docs/venues/private.md +10 -0
  320. package/kits/eck_manifest.auxiliary.json +43 -0
  321. package/kits/eck_manifest.json +191 -0
  322. package/kits/eck_message_manifest.json +63 -0
  323. package/kits/migrations/README-role-config-v4.md +32 -0
  324. package/kits/migrations/migrate-role-config-v4.mjs +623 -0
  325. package/kits/migrations/migrate-role-config-v5.mjs +346 -0
  326. package/kits/migrations/rename-config-file.mjs +99 -0
  327. package/kits/rules/01-overview.md +142 -0
  328. package/kits/rules/02-navigation.md +76 -0
  329. package/kits/rules/03-identity.md +34 -0
  330. package/kits/rules/04-relation.md +59 -0
  331. package/kits/rules/05-venue.md +44 -0
  332. package/kits/rules/06-channel.md +59 -0
  333. package/kits/schemas/_meta.json +29 -0
  334. package/kits/schemas/agent-config.schema.1.json +177 -0
  335. package/kits/schemas/agent-config.schema.2.json +239 -0
  336. package/kits/schemas/agent-config.schema.3.json +119 -0
  337. package/kits/schemas/agent-config.schema.4.json +208 -0
  338. package/kits/schemas/agent-config.schema.5.json +326 -0
  339. package/kits/schemas/contact-book.schema.1.json +36 -0
  340. package/kits/schemas/daemon.schema.1.json +90 -0
  341. package/kits/schemas/defaults.schema.1.json +81 -0
  342. package/kits/schemas/menu-exec-schema-commands.md +208 -0
  343. package/kits/schemas/migrations/README.md +28 -0
  344. package/kits/schemas/relation-config.schema.1.json +158 -0
  345. package/kits/schemas/relation-config.schema.2.json +73 -0
  346. package/kits/schemas/relation-config.schema.3.json +50 -0
  347. package/kits/schemas/relation-config.schema.4.json +47 -0
  348. package/kits/schemas/role-config.schema.1.json +200 -0
  349. package/kits/schemas/role-registry.schema.1.json +35 -0
  350. package/kits/schemas/single-session.schema.1.json +31 -0
  351. package/kits/templates/bootstrap-welcome.md +15 -0
  352. package/kits/templates/message-fragments/handoff-request-to-target.md +13 -0
  353. package/kits/templates/message-fragments/handoff-response-to-origin.md +10 -0
  354. package/kits/templates/message-fragments/inject-default.md +2 -0
  355. package/kits/templates/message-fragments/item.md +2 -0
  356. package/kits/templates/roles/admin.json +8 -0
  357. package/kits/templates/roles/member.json +40 -0
  358. package/kits/templates/roles/owner.json +8 -0
  359. package/kits/templates/roles/visitor.json +39 -0
  360. package/kits/templates/system-fragments/baseagent.md +14 -0
  361. package/kits/templates/system-fragments/bootstrap.md +16 -0
  362. package/kits/templates/system-fragments/channel.md +48 -0
  363. package/kits/templates/system-fragments/commands.md +26 -0
  364. package/kits/templates/system-fragments/identity.md +11 -0
  365. package/kits/templates/system-fragments/relation.md +19 -0
  366. package/kits/templates/system-fragments/session.md +53 -0
  367. package/kits/templates/system-fragments/venue.md +31 -0
  368. package/package.json +50 -15
  369. package/dist/index.d.ts +0 -7
  370. package/dist/index.d.ts.map +0 -1
  371. package/dist/index.js.map +0 -1
@@ -0,0 +1,3354 @@
1
+ import { normalizePeer, writeScope } from '../model/config-scope.js';
2
+ import { formatPeerKey } from '../relation/peer-identity.js';
3
+ import { modelMatches } from '../model/model-catalog.js';
4
+ import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
5
+ import { hasModelSwitcher } from '../../agents/runner-types.js';
6
+ import { getCodexEfforts } from '../../agents/codex-runner.js';
7
+ import { resolvePaths, getPackageRoot } from '../../paths.js';
8
+ import { buildEnvelope } from '../message/message-utils.js';
9
+ import path from 'path';
10
+ import fs from 'fs';
11
+ import crypto from 'crypto';
12
+ import { execFileSync } from 'child_process';
13
+ import { CronExpressionParser } from 'cron-parser';
14
+ import { parseDuration } from '../../trigger/parser.js';
15
+ import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions, resolveGlobalPkg } from '../../utils/npm-ops.js';
16
+ import { commandExists } from '../../utils/cross-platform.js';
17
+ import { loadDefaults, loadDaemonConfig } from '../../config-store.js';
18
+ import { WEB_PACKAGE_NAME } from '../../product.js';
19
+ import { read as cfgRead, resolveEffective, resolveEffectiveFieldWithSource, routeFieldPath, write as cfgWrite, ConfigTarget, } from '../../config/config-manager.js';
20
+ import { execAgentAction, execAgentQuery, execAgentOptions, resolveProjectPath } from './agent-control.js';
21
+ import { gatewayList, gatewayUpdate, gatewayDelete, gatewayTest, gatewayModels, gatewaySetPrice, gatewaySyncEnv } from '../../config/gateway-config.js';
22
+ import { displaySessionTitle } from '../session/session-title.js';
23
+ import { buildSessionTurnList } from '../session/session-turns.js';
24
+ import { isCapabilityType, listCapabilityOptions, queryCapabilityTypes, resolveCapabilityContext, updateCapabilityPolicy, } from '../capability/capability-manager.js';
25
+ import { normalizeCliArgv, parseCliIntent, parseLegacyCliCommand, validateCliArgv, withDefaultRelationContext } from './cli-intent-parser.js';
26
+ import { auditCommandAuthorization, hashArgv } from '../auth/authorization-audit.js';
27
+ import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
28
+ import { splitConfigBatchGetArgv } from '../../config/config-batch-get.js';
29
+ import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
30
+ import { PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
31
+ import { resolveRuntimePermissionMode, resolveRuntimeStringField, validateRuntimeStringFieldOverride, } from '../role/runtime-policy.js';
32
+ import { isManagementRole } from '../../config/builtin-roles.js';
33
+ import { SYSTEM_CONTROL_CHANNEL } from '../system-channels.js';
34
+ import { logger } from '../../utils/logger.js';
35
+ import { dispatchToMentionMode } from '../../config/mention-mode.js';
36
+ import { menuFailure, menuSuccess, normalizeMenuError, validateConfigWriteScope, validateMenuRequest } from './menu-protocol.js';
37
+ import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
38
+ /**
39
+ * 获取 baseagent CLI 的版本号(claude/gemini/codex)。
40
+ * 失败返回 null(命令不存在或执行失败)。
41
+ */
42
+ function getBaseagentVersion(cmd) {
43
+ try {
44
+ const output = execFileSync(cmd, ['--version'], {
45
+ encoding: 'utf-8',
46
+ timeout: 3000,
47
+ stdio: ['ignore', 'pipe', 'pipe'],
48
+ }).trim();
49
+ // claude: "2.1.187 (Claude Code)" → 提取 "2.1.187"
50
+ // gemini: "0.38.0" → 直接返回
51
+ // codex: "codex-cli 0.142.0" → 提取 "0.142.0"
52
+ const match = output.match(/(\d+\.\d+\.\d+)/);
53
+ return match ? match[1] : null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
60
+ const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
61
+ /** menu file: fetch 文件大小上限(与 /file 一致) */
62
+ const FILE_FETCH_MAX_SIZE = 10 * 1024 * 1024;
63
+ /** menu file: query 的 sha256 仅对 ≤ 2 MB 文件计算,超过返回 null(见设计文档 §7 决策 2) */
64
+ const FILE_HASH_MAX_SIZE = 2 * 1024 * 1024;
65
+ const FILE_LIST_DEFAULT_LIMIT = 500;
66
+ const FILE_LIST_MAX_LIMIT = 1000;
67
+ const topicForkTargetsInFlight = new Set();
68
+ function validateCapabilityScope(args) {
69
+ const scope = args?.scope ?? 'project';
70
+ return scope === 'project' ? null : { error: 'MVP 只支持 scope=project', code: 'INVALID_SCOPE' };
71
+ }
72
+ function sanitizeCapabilityOptionsForRole(items, role) {
73
+ if (isManagementRole(role))
74
+ return items;
75
+ return items.map(item => {
76
+ const { desc: _desc, ...rest } = item;
77
+ return rest;
78
+ });
79
+ }
80
+ /**
81
+ * 从菜单上下文的 session 推导 resolveIdentity 所需的 chatType/conversationId,
82
+ * 保证群聊场景命中群成员角色表(否则回退私聊语义会误判角色)。
83
+ * 返回可展开到 resolveIdentity 位置参数的元组:[chatType, conversationId]。
84
+ */
85
+ function menuIdentityArgs(session) {
86
+ if (!session)
87
+ return [];
88
+ const chatType = session.chatType === 'group' ? 'group' : session.chatType === 'private' ? 'private' : undefined;
89
+ const conversationId = chatType === 'group' ? (session.metadata?.groupId || session.channelId) : session.metadata?.peerId;
90
+ return [chatType, conversationId];
91
+ }
92
+ function sanitizeCapabilityQueryForRole(data, role) {
93
+ if (isManagementRole(role))
94
+ return data;
95
+ const { projectPath: _projectPath, ...rest } = data;
96
+ return rest;
97
+ }
98
+ function resolveCapabilityTarget(params) {
99
+ const scopeError = validateCapabilityScope(params.args);
100
+ if (scopeError)
101
+ return scopeError;
102
+ const requestedAid = params.args?.aid ?? params.args?.agent;
103
+ const targetAgent = requestedAid
104
+ ? this.agentRegistry?.get?.(String(requestedAid))
105
+ : params.evolagent ?? this.getOwningAgent?.(params.channel);
106
+ if (!targetAgent) {
107
+ return { error: requestedAid ? `未找到 Agent: ${requestedAid}` : '当前 channel 无绑定 agent', code: requestedAid ? 'NOT_FOUND' : 'FORBIDDEN' };
108
+ }
109
+ if (!params.fromControlChannel) {
110
+ const selfAid = this.getOwningAgent?.(params.channel)?.aid;
111
+ if (selfAid && targetAgent.aid !== selfAid) {
112
+ return { error: '跨 agent 操作仅允许通过控制 AID channel 执行', code: 'FORBIDDEN' };
113
+ }
114
+ }
115
+ const freshConfig = (() => {
116
+ try {
117
+ return resolveEffective({ self: targetAgent.aid }, { cache: true });
118
+ }
119
+ catch {
120
+ return targetAgent.config;
121
+ }
122
+ })();
123
+ const fallbackBaseagent = (() => {
124
+ try {
125
+ return targetAgent.baseagent;
126
+ }
127
+ catch {
128
+ return undefined;
129
+ }
130
+ })();
131
+ const ctx = resolveCapabilityContext({
132
+ aid: targetAgent.aid,
133
+ baseagent: params.args?.baseagent ?? params.session?.baseagent ?? freshConfig?.active_baseagent ?? fallbackBaseagent,
134
+ projectPath: targetAgent.projectPath,
135
+ sessionProjectPath: params.session?.projectPath,
136
+ sessionBaseagent: params.session?.baseagent,
137
+ config: freshConfig,
138
+ });
139
+ if ('error' in ctx)
140
+ return ctx;
141
+ return { ctx, config: freshConfig, agent: targetAgent };
142
+ }
143
+ function getRenameName(args) {
144
+ return (args?.name ?? args?.title ?? args?.value ?? '').toString().trim();
145
+ }
146
+ function buildSessionPayload(session, name) {
147
+ const payload = { id: session.id, name };
148
+ if (session.agentSessionId)
149
+ payload.agentSessionId = session.agentSessionId;
150
+ return payload;
151
+ }
152
+ async function findMainSessionTarget(sessionManager, channel, channelId, target, activeSession) {
153
+ if (!target) {
154
+ return activeSession && !activeSession.threadId ? activeSession : undefined;
155
+ }
156
+ const sessions = (await sessionManager.listSessions(channel, channelId))
157
+ .filter((s) => !s.threadId);
158
+ return sessions.find((s) => s.name === target ||
159
+ s.id === target ||
160
+ (target.length >= 8 && s.id.startsWith(target)) ||
161
+ s.agentSessionId === target ||
162
+ (!!s.agentSessionId && target.length >= 8 && s.agentSessionId.startsWith(target)));
163
+ }
164
+ async function resolveTopicForkSource(sessionManager, channel, channelId, sourceThreadId) {
165
+ const threadId = typeof sourceThreadId === 'string' ? sourceThreadId.trim() : '';
166
+ if (threadId)
167
+ return sessionManager.getThreadSession(channel, channelId, threadId);
168
+ const active = await sessionManager.getActiveSession(channel, channelId);
169
+ return active && !active.threadId ? active : undefined;
170
+ }
171
+ /**
172
+ * 解析并校验 menu `name=file` 的目标路径(query/fetch 共用)。
173
+ *
174
+ * 与文本 `/file` 的差异(见设计文档 §6.2):
175
+ * - 接受项目内**绝对路径**(文本 /file 仍拒绝绝对路径)
176
+ * - 项目外文件仅 aid channel owner(identity.role === 'owner')可取
177
+ *
178
+ * 沿用 /file 的安全校验链:拒绝 `..` 穿越、realpathSync 后验证落点。
179
+ * 成功返回 `{ realPath, projectPath, stat }`;失败返回 `{ error, code }`。
180
+ */
181
+ function resolveMenuFilePath(input, session, role, expectType) {
182
+ if (!session?.projectPath)
183
+ return { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
184
+ const raw = (input ?? '').toString().trim();
185
+ if (!raw)
186
+ return { error: '缺少 path 参数', code: 'MISSING_VALUE' };
187
+ // 拒绝 .. 路径穿越(兼容两种分隔符)
188
+ if (raw.split(path.sep).includes('..') || raw.split('/').includes('..')) {
189
+ return { error: '不支持 .. 路径穿越', code: 'NO_PERMISSION' };
190
+ }
191
+ // 相对路径基于 projectPath 解析;绝对路径原样
192
+ const resolved = path.isAbsolute(raw) ? raw : path.resolve(session.projectPath, raw);
193
+ if (!fs.existsSync(resolved)) {
194
+ return { error: '文件不存在', code: 'NOT_FOUND' };
195
+ }
196
+ let realPath;
197
+ let realProjectPath;
198
+ try {
199
+ realPath = fs.realpathSync(resolved);
200
+ realProjectPath = fs.realpathSync(session.projectPath);
201
+ }
202
+ catch {
203
+ return { error: '文件不存在', code: 'NOT_FOUND' };
204
+ }
205
+ const inProject = realPath === realProjectPath || realPath.startsWith(realProjectPath + path.sep);
206
+ // 项目外文件:仅 aid channel owner 可取(§6.2)
207
+ if (!inProject && role !== 'owner') {
208
+ return { error: '无权限:项目外文件仅 owner 可取', code: 'NO_PERMISSION' };
209
+ }
210
+ let stat;
211
+ try {
212
+ stat = fs.statSync(realPath);
213
+ }
214
+ catch {
215
+ return { error: '文件不存在', code: 'NOT_FOUND' };
216
+ }
217
+ if (expectType === 'file' && stat.isDirectory()) {
218
+ return { error: '暂不支持目录', code: 'NOT_SUPPORTED' };
219
+ }
220
+ if (expectType === 'directory' && !stat.isDirectory()) {
221
+ return { error: '不是目录', code: 'NOT_A_DIRECTORY' };
222
+ }
223
+ return { realPath, projectPath: realProjectPath, stat };
224
+ }
225
+ function parseFileListOffset(value) {
226
+ const n = Number(value);
227
+ return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
228
+ }
229
+ function parseFileListLimit(value) {
230
+ const raw = value === undefined || value === null || value === ''
231
+ ? FILE_LIST_DEFAULT_LIMIT
232
+ : Number(value);
233
+ const n = Number.isFinite(raw) ? Math.floor(raw) : FILE_LIST_DEFAULT_LIMIT;
234
+ return Math.min(Math.max(1, n), FILE_LIST_MAX_LIMIT);
235
+ }
236
+ function isInProject(realPath, realProjectPath) {
237
+ return realPath === realProjectPath || realPath.startsWith(realProjectPath + path.sep);
238
+ }
239
+ function getDirectoryEntryInfo(realPath, realProjectPath, role, dirent) {
240
+ if (dirent.isDirectory())
241
+ return { isDirectory: true, followTarget: true };
242
+ if (!dirent.isSymbolicLink())
243
+ return { isDirectory: false, followTarget: true };
244
+ const full = path.join(realPath, dirent.name);
245
+ let targetRealPath;
246
+ try {
247
+ targetRealPath = fs.realpathSync(full);
248
+ }
249
+ catch {
250
+ return { isDirectory: false, followTarget: false };
251
+ }
252
+ if (role !== 'owner' && !isInProject(targetRealPath, realProjectPath)) {
253
+ return { isDirectory: false, followTarget: false };
254
+ }
255
+ try {
256
+ return { isDirectory: fs.statSync(full).isDirectory(), followTarget: true };
257
+ }
258
+ catch {
259
+ return { isDirectory: false, followTarget: false };
260
+ }
261
+ }
262
+ function listDirectory(realPath, options) {
263
+ const { offset, limit, includeHidden, projectPath, role } = options;
264
+ let dirents;
265
+ try {
266
+ dirents = fs.readdirSync(realPath, { withFileTypes: true });
267
+ }
268
+ catch (e) {
269
+ const code = e?.code === 'EACCES' || e?.code === 'EPERM' ? 'NO_PERMISSION' : 'EXEC_FAILED';
270
+ return { error: `目录读取失败: ${e?.message ?? e}`, code };
271
+ }
272
+ if (!includeHidden) {
273
+ dirents = dirents.filter(d => !d.name.startsWith('.'));
274
+ }
275
+ const entryInfoByName = new Map();
276
+ for (const dirent of dirents) {
277
+ entryInfoByName.set(dirent.name, getDirectoryEntryInfo(realPath, projectPath, role, dirent));
278
+ }
279
+ dirents.sort((a, b) => {
280
+ const ad = entryInfoByName.get(a.name)?.isDirectory ?? false;
281
+ const bd = entryInfoByName.get(b.name)?.isDirectory ?? false;
282
+ if (ad !== bd)
283
+ return ad ? -1 : 1;
284
+ return a.name.localeCompare(b.name);
285
+ });
286
+ const total = dirents.length;
287
+ const page = dirents.slice(offset, offset + limit);
288
+ const entries = page.map(dirent => {
289
+ const full = path.join(realPath, dirent.name);
290
+ const info = entryInfoByName.get(dirent.name) ?? { isDirectory: false, followTarget: true };
291
+ // Only stat when we need size/mtime (dirent already tells us if it's a directory)
292
+ let size = null;
293
+ let mtime = 0;
294
+ let birthtime = 0;
295
+ if (!info.isDirectory) {
296
+ try {
297
+ const stat = info.followTarget ? fs.statSync(full) : fs.lstatSync(full);
298
+ size = stat.size;
299
+ mtime = stat.mtimeMs;
300
+ birthtime = stat.birthtimeMs;
301
+ }
302
+ catch { }
303
+ }
304
+ return {
305
+ name: dirent.name,
306
+ type: info.isDirectory ? 'directory' : 'file',
307
+ size,
308
+ mtime,
309
+ birthtime,
310
+ };
311
+ });
312
+ return {
313
+ data: {
314
+ entries,
315
+ total,
316
+ offset,
317
+ limit,
318
+ hasMore: offset + entries.length < total,
319
+ },
320
+ };
321
+ }
322
+ const CLI_EXEC_WHITELIST = {
323
+ status: '*',
324
+ model: '*',
325
+ stats: '*',
326
+ agent: new Set(['list', 'show', 'get']),
327
+ aid: new Set(['list', 'show', 'lookup']),
328
+ storage: new Set(['ls', 'quota']),
329
+ };
330
+ function tokenizeArgv(line) {
331
+ const out = [];
332
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
333
+ let m;
334
+ while ((m = re.exec(line)) !== null) {
335
+ out.push(m[1] ?? m[2] ?? m[3] ?? '');
336
+ }
337
+ return out;
338
+ }
339
+ function getAvailableEfforts(agent, model) {
340
+ if (agent.name === 'claude') {
341
+ return allEfforts;
342
+ }
343
+ if (agent.name === 'codex') {
344
+ return getCodexEfforts(model);
345
+ }
346
+ return [];
347
+ }
348
+ function modelDisplayLabel(agent, model) {
349
+ const full = agent.resolveModelId?.(model);
350
+ return full && full !== model ? `${model} (${full})` : model;
351
+ }
352
+ function menuStringArg(args, key) {
353
+ const value = args?.[key];
354
+ if (typeof value !== 'string')
355
+ return undefined;
356
+ const trimmed = value.trim();
357
+ return trimmed || undefined;
358
+ }
359
+ const LEGACY_SESSION_CONFIG_ARG_KEYS = [
360
+ 'session',
361
+ 'sessionId',
362
+ 'session_id',
363
+ 'sessionKey',
364
+ 'session_key',
365
+ 'currentSession',
366
+ 'current_session',
367
+ 'threadId',
368
+ 'thread_id',
369
+ 'targetSession',
370
+ 'target_session',
371
+ 'targetSessionId',
372
+ 'target_session_id',
373
+ 'targetSessionStrategy',
374
+ 'targetThreadId',
375
+ 'target_thread_id',
376
+ ];
377
+ function withoutLegacySessionConfigArgs(args) {
378
+ if (!args || !LEGACY_SESSION_CONFIG_ARG_KEYS.some(key => key in args))
379
+ return args;
380
+ const sanitized = { ...args };
381
+ for (const key of LEGACY_SESSION_CONFIG_ARG_KEYS)
382
+ delete sanitized[key];
383
+ return sanitized;
384
+ }
385
+ function toMenuSource(source) {
386
+ if (source?.target === ConfigTarget.Relation)
387
+ return 'relation';
388
+ if (source?.target === ConfigTarget.Agent)
389
+ return 'agent';
390
+ if (source?.target === ConfigTarget.Defaults)
391
+ return 'defaults';
392
+ return null;
393
+ }
394
+ function menuConfigSource(fieldPath, sel) {
395
+ try {
396
+ const source = resolveEffectiveFieldWithSource(fieldPath, sel, { cache: true }).source;
397
+ return toMenuSource(source);
398
+ }
399
+ catch { }
400
+ return null;
401
+ }
402
+ function menuModelConfigFieldPath(target) {
403
+ if (target.field === 'effort' && target.baseagent === 'codex') {
404
+ return `baseagents.${target.baseagent}.reasoning`;
405
+ }
406
+ return target.fieldPath;
407
+ }
408
+ function menuChatmodeScope(args) {
409
+ const raw = menuStringArg(args, 'scope') ?? 'relation';
410
+ if (raw === 'agent' || raw === 'relation')
411
+ return raw;
412
+ return { error: `无效 scope: ${raw},可选: agent / relation`, code: 'INVALID_SCOPE' };
413
+ }
414
+ function menuChatmodeField(args, session, explicitChatType) {
415
+ const raw = menuStringArg(args, 'field')
416
+ ?? menuStringArg(args, 'key')
417
+ ?? menuStringArg(args, 'chatType');
418
+ if (raw) {
419
+ const key = raw.startsWith('chatmode.') ? raw.slice('chatmode.'.length) : raw;
420
+ if (key === 'private' || key === 'group' || key === 'nothuman')
421
+ return key;
422
+ return { error: `无效 chatmode 字段: ${raw},可选: private / group / nothuman`, code: 'INVALID_FIELD' };
423
+ }
424
+ return chatmodeFieldForPeer(session?.chatType ?? explicitChatType, session?.metadata?.peerType);
425
+ }
426
+ function defaultChatmodeForField(field) {
427
+ return field === 'private' ? 'interactive' : 'proactive';
428
+ }
429
+ function normalizeMenuPeer(input) {
430
+ try {
431
+ return normalizePeer(input);
432
+ }
433
+ catch (e) {
434
+ return { error: e?.message || String(e), code: e?.code || 'INVALID_PEER' };
435
+ }
436
+ }
437
+ function resolveMenuChatmodeTarget(params) {
438
+ const scope = menuChatmodeScope(params.args);
439
+ if (typeof scope !== 'string')
440
+ return scope;
441
+ const field = menuChatmodeField(params.args, params.session, params.explicitChatType);
442
+ if (typeof field !== 'string')
443
+ return field;
444
+ const explicitSelf = menuStringArg(params.args, 'self')
445
+ ?? menuStringArg(params.args, 'aid');
446
+ const currentSelf = this.getOwningAgent?.(params.channel)?.aid
447
+ ?? params.session?.selfAID
448
+ ?? undefined;
449
+ const self = explicitSelf ?? currentSelf;
450
+ if (!self)
451
+ return { error: '缺少 self/aid 参数', code: 'MISSING_AID' };
452
+ if (!params.fromControlChannel && explicitSelf && currentSelf && explicitSelf !== currentSelf) {
453
+ return { error: '只能设置当前 agent 的 chatmode', code: 'FORBIDDEN' };
454
+ }
455
+ const sel = { self };
456
+ if (scope === 'agent') {
457
+ return { scope, sel, role: params.role, field, fieldPath: `chatmode.${field}` };
458
+ }
459
+ const explicitPeer = menuStringArg(params.args, 'peer') ?? menuStringArg(params.args, 'peerKey');
460
+ let peerKey;
461
+ if (explicitPeer) {
462
+ const normalized = normalizeMenuPeer(explicitPeer);
463
+ if (typeof normalized !== 'string')
464
+ return normalized;
465
+ peerKey = normalized;
466
+ }
467
+ else {
468
+ const chatType = params.session?.chatType ?? params.explicitChatType ?? 'private';
469
+ const channelType = this.resolveChannelType?.(params.channel) ?? params.session?.channelType;
470
+ const peerKeyId = chatType === 'group'
471
+ ? (params.session?.metadata?.groupId || params.channelId)
472
+ : (params.userId || params.session?.metadata?.peerId || params.channelId);
473
+ if (channelType && peerKeyId)
474
+ peerKey = formatPeerKey(channelType, peerKeyId);
475
+ }
476
+ if (!peerKey)
477
+ return { error: 'relation scope 需要 peer/peerKey,或可推导的当前对端', code: 'MISSING_PEER' };
478
+ return { scope, sel: { ...sel, peerKey }, role: params.role, field, fieldPath: `chatmode.${field}` };
479
+ }
480
+ function readMenuChatmode(target) {
481
+ try {
482
+ const resolved = resolveEffectiveFieldWithSource(target.fieldPath, target.sel, { cache: true });
483
+ const decision = resolveRuntimeStringField({
484
+ selfAid: target.sel.self,
485
+ role: target.role,
486
+ field: target.fieldPath,
487
+ configuredValue: resolved.value,
488
+ });
489
+ const mode = decision.effectiveValue ?? resolveChatModeForField({ ...target.sel, role: target.role, field: target.field });
490
+ if (mode === 'interactive' || mode === 'proactive') {
491
+ return {
492
+ value: mode,
493
+ source: decision.decidedBy === 'role'
494
+ ? 'role'
495
+ : toMenuSource(resolved.source) ?? 'builtin',
496
+ };
497
+ }
498
+ }
499
+ catch { }
500
+ return { value: defaultChatmodeForField(target.field), source: 'builtin' };
501
+ }
502
+ function writeMenuChatmode(target, value) {
503
+ const route = routeFieldPath(target.fieldPath, target.scope);
504
+ const cur = cfgRead(route.target, target.sel) || {};
505
+ const block = cur.chatmode && typeof cur.chatmode === 'object' && !Array.isArray(cur.chatmode)
506
+ ? { ...cur.chatmode }
507
+ : {};
508
+ block[target.field] = value;
509
+ cur.chatmode = block;
510
+ cfgWrite(route.target, cur, target.sel);
511
+ }
512
+ function menuMentionModeScope(args) {
513
+ const raw = menuStringArg(args, 'scope') ?? 'relation';
514
+ if (raw === 'agent' || raw === 'relation')
515
+ return raw;
516
+ return { error: `无效 scope: ${raw},可选: agent / relation`, code: 'INVALID_SCOPE' };
517
+ }
518
+ function resolveMenuMentionModeTarget(params) {
519
+ const scope = menuMentionModeScope(params.args);
520
+ if (typeof scope !== 'string')
521
+ return scope;
522
+ const explicitSelf = menuStringArg(params.args, 'self')
523
+ ?? menuStringArg(params.args, 'aid');
524
+ const currentSelf = this.getOwningAgent?.(params.channel)?.aid
525
+ ?? params.session?.selfAID
526
+ ?? undefined;
527
+ const self = explicitSelf ?? currentSelf;
528
+ if (!self)
529
+ return { error: '缺少 self/aid 参数', code: 'MISSING_AID' };
530
+ if (!params.fromControlChannel && explicitSelf && currentSelf && explicitSelf !== currentSelf) {
531
+ return { error: '只能设置当前 agent 的 mentionMode', code: 'FORBIDDEN' };
532
+ }
533
+ const sel = { self };
534
+ if (scope === 'agent')
535
+ return { scope, sel, fieldPath: 'mentionMode' };
536
+ const explicitPeer = menuStringArg(params.args, 'peer') ?? menuStringArg(params.args, 'peerKey');
537
+ let peerKey;
538
+ if (explicitPeer) {
539
+ const normalized = normalizeMenuPeer(explicitPeer);
540
+ if (typeof normalized !== 'string')
541
+ return normalized;
542
+ peerKey = normalized;
543
+ }
544
+ else {
545
+ const chatType = params.session?.chatType ?? params.explicitChatType ?? 'private';
546
+ const channelType = this.resolveChannelType?.(params.channel) ?? params.session?.channelType;
547
+ const peerKeyId = chatType === 'group'
548
+ ? (params.session?.metadata?.groupId || params.channelId)
549
+ : (params.userId || params.session?.metadata?.peerId || params.channelId);
550
+ if (channelType && peerKeyId)
551
+ peerKey = formatPeerKey(channelType, peerKeyId);
552
+ }
553
+ if (!peerKey)
554
+ return { error: 'relation scope 需要 peer/peerKey,或可推导的当前对端', code: 'MISSING_PEER' };
555
+ return { scope, sel: { ...sel, peerKey }, fieldPath: 'mentionMode' };
556
+ }
557
+ function readMenuMentionMode(target, fallback = null, fallbackSource = null) {
558
+ try {
559
+ const resolved = resolveEffectiveFieldWithSource(target.fieldPath, target.sel, { cache: true });
560
+ if (resolved.value === 'disabled' || resolved.value === 'mention-only') {
561
+ return { value: resolved.value, source: toMenuSource(resolved.source) };
562
+ }
563
+ }
564
+ catch { }
565
+ return { value: fallback, source: fallback === null ? null : fallbackSource };
566
+ }
567
+ function writeMenuMentionMode(target, value) {
568
+ const route = routeFieldPath(target.fieldPath, target.scope);
569
+ const cur = cfgRead(route.target, target.sel) || {};
570
+ if (value === null)
571
+ delete cur.mentionMode;
572
+ else
573
+ cur.mentionMode = value;
574
+ cfgWrite(route.target, cur, target.sel);
575
+ }
576
+ function menuPermissionScope(args) {
577
+ const raw = menuStringArg(args, 'scope') ?? 'role';
578
+ if (raw === 'role')
579
+ return raw;
580
+ return { error: `无效 scope: ${raw},permissionMode 只能在角色定义中配置`, code: 'INVALID_SCOPE' };
581
+ }
582
+ function resolveMenuPermissionTarget(params) {
583
+ const scope = menuPermissionScope(params.args);
584
+ if (typeof scope !== 'string')
585
+ return scope;
586
+ const explicitSelf = menuStringArg(params.args, 'self')
587
+ ?? menuStringArg(params.args, 'aid');
588
+ const currentSelf = this.getOwningAgent?.(params.channel)?.aid
589
+ ?? params.session?.selfAID
590
+ ?? undefined;
591
+ const self = explicitSelf ?? currentSelf;
592
+ if (!self)
593
+ return { error: '缺少 self/aid 参数', code: 'MISSING_AID' };
594
+ if (!params.fromControlChannel && explicitSelf && currentSelf && explicitSelf !== currentSelf) {
595
+ return { error: '只能设置当前 agent 的 permission', code: 'FORBIDDEN' };
596
+ }
597
+ return { scope, sel: { self }, role: params.role || 'none', fieldPath: 'permissionMode' };
598
+ }
599
+ function readMenuPermission(target) {
600
+ const decision = resolveRuntimePermissionMode({ selfAid: target.sel.self, role: target.role });
601
+ return {
602
+ value: decision.effectiveValue,
603
+ source: decision.decidedBy === 'role' ? 'role' : 'builtin',
604
+ };
605
+ }
606
+ function menuModelScope(args) {
607
+ const raw = menuStringArg(args, 'scope') ?? 'relation';
608
+ if (raw === 'agent' || raw === 'relation')
609
+ return raw;
610
+ return { error: `无效 scope: ${raw},可选: agent / relation`, code: 'INVALID_SCOPE' };
611
+ }
612
+ function resolveMenuModelTarget(params) {
613
+ const scope = menuModelScope(params.args);
614
+ if (typeof scope !== 'string')
615
+ return scope;
616
+ const explicitSelf = menuStringArg(params.args, 'self')
617
+ ?? menuStringArg(params.args, 'aid');
618
+ const currentAgent = this.getOwningAgent?.(params.channel) ?? null;
619
+ const requestedAgent = explicitSelf
620
+ ? (this.agentRegistry?.get?.(explicitSelf) ?? null)
621
+ : null;
622
+ const currentSelf = currentAgent?.aid
623
+ ?? params.session?.selfAID
624
+ ?? undefined;
625
+ const self = explicitSelf ?? currentSelf;
626
+ if (!self)
627
+ return { error: '缺少 self/aid 参数', code: 'MISSING_AID' };
628
+ if (!params.fromControlChannel && explicitSelf && currentSelf && explicitSelf !== currentSelf) {
629
+ return { error: '只能设置当前 agent 的 model/effort', code: 'FORBIDDEN' };
630
+ }
631
+ const baseagent = menuStringArg(params.args, 'baseagent')
632
+ ?? params.session?.baseagent
633
+ ?? params.session?.agentId
634
+ ?? requestedAgent?.baseagent
635
+ ?? currentAgent?.baseagent
636
+ ?? this.parseDefaultBaseagent?.();
637
+ if (!baseagent)
638
+ return { error: '缺少 baseagent 参数', code: 'MISSING_BASEAGENT' };
639
+ const sel = { self };
640
+ if (scope === 'agent') {
641
+ return { scope, sel, role: params.role, baseagent, field: params.field, fieldPath: `baseagents.${baseagent}.${params.field}` };
642
+ }
643
+ const explicitPeer = menuStringArg(params.args, 'peer') ?? menuStringArg(params.args, 'peerKey');
644
+ let peerKey;
645
+ if (explicitPeer) {
646
+ const normalized = normalizeMenuPeer(explicitPeer);
647
+ if (typeof normalized !== 'string')
648
+ return normalized;
649
+ peerKey = normalized;
650
+ }
651
+ else {
652
+ const chatType = params.session?.chatType ?? params.explicitChatType ?? 'private';
653
+ const channelType = this.resolveChannelType?.(params.channel) ?? params.session?.channelType;
654
+ const peerKeyId = chatType === 'group'
655
+ ? (params.session?.metadata?.groupId || params.channelId)
656
+ : (params.userId || params.session?.metadata?.peerId || params.channelId);
657
+ if (channelType && peerKeyId)
658
+ peerKey = formatPeerKey(channelType, peerKeyId);
659
+ }
660
+ if (!peerKey)
661
+ return { error: 'relation scope 需要 peer/peerKey,或可推导的当前对端', code: 'MISSING_PEER' };
662
+ return { scope, sel: { ...sel, peerKey }, role: params.role, baseagent, field: params.field, fieldPath: `baseagents.${baseagent}.${params.field}` };
663
+ }
664
+ function readMenuModel(target, agent) {
665
+ try {
666
+ const fieldPath = menuModelConfigFieldPath(target);
667
+ const resolved = resolveEffectiveFieldWithSource(fieldPath, target.sel, { cache: true });
668
+ if (target.field === 'model') {
669
+ const decision = constrainResolvedModelForRole({
670
+ selfAid: target.sel.self,
671
+ role: target.role,
672
+ baseagent: target.baseagent,
673
+ model: resolved.value,
674
+ resolveModelId: typeof agent?.resolveModelId === 'function' ? agent.resolveModelId.bind(agent) : undefined,
675
+ });
676
+ if (decision.model) {
677
+ return { value: decision.model, source: decision.constrained ? 'role' : toMenuSource(resolved.source) };
678
+ }
679
+ }
680
+ else {
681
+ const decision = resolveRuntimeStringField({
682
+ selfAid: target.sel.self,
683
+ role: target.role,
684
+ field: fieldPath,
685
+ configuredValue: resolved.value,
686
+ });
687
+ if (decision.effectiveValue) {
688
+ return {
689
+ value: decision.effectiveValue,
690
+ source: decision.decidedBy === 'role' ? 'role' : toMenuSource(resolved.source),
691
+ };
692
+ }
693
+ }
694
+ }
695
+ catch { }
696
+ const fallback = target.field === 'model'
697
+ ? (typeof agent?.getModel === 'function' ? agent.getModel() : agent?.name)
698
+ : (typeof agent?.getEffort === 'function' ? agent.getEffort() : undefined);
699
+ return {
700
+ value: fallback ?? null,
701
+ source: fallback == null ? null : 'runner',
702
+ };
703
+ }
704
+ function menuActivityScope(args) {
705
+ const raw = menuStringArg(args, 'scope') ?? 'relation';
706
+ if (raw === 'agent' || raw === 'relation')
707
+ return raw;
708
+ return { error: `无效 scope: ${raw},可选: agent / relation`, code: 'INVALID_SCOPE' };
709
+ }
710
+ function resolveMenuActivityTarget(params) {
711
+ const scope = menuActivityScope(params.args);
712
+ if (typeof scope !== 'string')
713
+ return scope;
714
+ const explicitSelf = menuStringArg(params.args, 'self') ?? menuStringArg(params.args, 'aid');
715
+ const currentSelf = this.getOwningAgent?.(params.channel)?.aid
716
+ ?? params.session?.selfAID
717
+ ?? undefined;
718
+ const self = explicitSelf ?? currentSelf;
719
+ if (!self)
720
+ return { error: '缺少 self/aid 参数', code: 'MISSING_AID' };
721
+ if (!params.fromControlChannel && explicitSelf && currentSelf && explicitSelf !== currentSelf) {
722
+ return { error: '只能设置当前 agent 的 activity', code: 'FORBIDDEN' };
723
+ }
724
+ const sel = { self };
725
+ if (scope === 'agent')
726
+ return { scope, sel, fieldPath: 'show_activities' };
727
+ const explicitPeer = menuStringArg(params.args, 'peer') ?? menuStringArg(params.args, 'peerKey');
728
+ let peerKey;
729
+ if (explicitPeer) {
730
+ const normalized = normalizeMenuPeer(explicitPeer);
731
+ if (typeof normalized !== 'string')
732
+ return normalized;
733
+ peerKey = normalized;
734
+ }
735
+ else {
736
+ const chatType = params.session?.chatType ?? params.explicitChatType ?? 'private';
737
+ const channelType = this.resolveChannelType?.(params.channel) ?? params.session?.channelType;
738
+ const peerKeyId = chatType === 'group'
739
+ ? (params.session?.metadata?.groupId || params.channelId)
740
+ : (params.userId || params.session?.metadata?.peerId || params.channelId);
741
+ if (channelType && peerKeyId)
742
+ peerKey = formatPeerKey(channelType, peerKeyId);
743
+ }
744
+ if (!peerKey)
745
+ return { error: 'relation scope 需要 peer/peerKey,或可推导的当前对端', code: 'MISSING_PEER' };
746
+ return { scope, sel: { ...sel, peerKey }, fieldPath: 'show_activities' };
747
+ }
748
+ function readMenuActivity(target, fallback = 'all') {
749
+ try {
750
+ const resolved = resolveEffectiveFieldWithSource(target.fieldPath, target.sel, { cache: true });
751
+ if (resolved.value === 'all' || resolved.value === 'text' || resolved.value === 'none') {
752
+ return { value: resolved.value, source: toMenuSource(resolved.source) ?? 'builtin' };
753
+ }
754
+ }
755
+ catch { }
756
+ return { value: fallback, source: 'builtin' };
757
+ }
758
+ function writeMenuActivity(target, value) {
759
+ const route = routeFieldPath(target.fieldPath, target.scope);
760
+ const cur = cfgRead(route.target, target.sel) || {};
761
+ cur.show_activities = value;
762
+ cfgWrite(route.target, cur, target.sel);
763
+ }
764
+ function writeMenuModel(target, value) {
765
+ if (target.field === 'model') {
766
+ writeScope(target.scope, target.sel, target.baseagent, { model: value });
767
+ }
768
+ else {
769
+ writeScope(target.scope, target.sel, target.baseagent, { effort: value });
770
+ }
771
+ }
772
+ export function isProcessLevelOwner(peerId, owners) {
773
+ if (!peerId)
774
+ return false;
775
+ return (owners ?? []).includes(peerId);
776
+ }
777
+ // ── 控制面双轨鉴权(见 docs/.../2026-06-10-control-channel-auth-design.md)──
778
+ // 白名单而非黑名单:默认安全,新增进程级 action 需显式加入。
779
+ /** /agent 的进程级 action:仅控制 channel 可执行。 */
780
+ export const PROCESS_LEVEL_AGENT_ACTIONS = new Set(['create', 'delete', 'enable', 'disable']);
781
+ /** /agent 的「本 agent 自管理」action:agent channel 仅 owner/admin 可对自身 aid 执行;update 另行收紧为 owner。 */
782
+ export const SELF_MANAGE_AGENT_ACTIONS = new Set(['update', 'reload']);
783
+ /** 判断 (cmdBase, action) 是否为进程级操作(仅控制 channel 可执行)。
784
+ * /system 全部进程级;/agent 仅 create/delete/enable/disable 进程级;其余关系级。 */
785
+ export function isProcessLevelAction(cmdBase, action) {
786
+ if (cmdBase === '/system')
787
+ return true;
788
+ if (cmdBase === '/gateway')
789
+ return true; // 网关改 baseagent 凭证,进程级,仅控制 channel
790
+ if (cmdBase === '/config')
791
+ return true; // 配置查询/修改,进程级,仅控制 channel
792
+ if (cmdBase === '/agent')
793
+ return PROCESS_LEVEL_AGENT_ACTIONS.has(action ?? '');
794
+ return false;
795
+ }
796
+ /** 控制面作用域闸门:在每个 exec 入口算出 cmdBase/action 后调用。
797
+ * 闸1 进程级 action:仅控制 channel。
798
+ * 闸2 跨 agent 寻址(args.aid ≠ 自身):仅控制 channel。
799
+ * 命中返回 FORBIDDEN 结果,否则返回 null(放行,后续走原有 owner/role 鉴权)。 */
800
+ function gateControlScope(opts) {
801
+ const { cmdBase, action, args, channel, fromControlChannel } = opts;
802
+ if (fromControlChannel)
803
+ return null;
804
+ if (isProcessLevelAction(cmdBase, action)) {
805
+ return { error: '此操作仅允许通过控制 AID channel 执行', code: 'FORBIDDEN' };
806
+ }
807
+ const targetAid = args?.aid;
808
+ if (targetAid) {
809
+ const currentAgentAid = this.getOwningAgent?.(channel)?.aid;
810
+ if (targetAid !== currentAgentAid) {
811
+ return { error: '跨 agent 操作仅允许通过控制 AID channel 执行', code: 'FORBIDDEN' };
812
+ }
813
+ }
814
+ return null;
815
+ }
816
+ function buildMenuIntent(verb, cmdBase, args, action, value, fromControlChannel = false) {
817
+ const payload = { ...(args ?? {}), ...(value !== undefined ? { value } : {}) };
818
+ const intent = (operation, scope = 'relation', extra) => ({
819
+ operation,
820
+ scope,
821
+ source: 'menu',
822
+ args: { ...payload, ...(extra ?? {}) },
823
+ });
824
+ const modelScope = args?.scope === 'agent' ? 'agent' : 'relation';
825
+ if (verb === 'query') {
826
+ if (cmdBase === '/model')
827
+ return intent('model.current', modelScope);
828
+ if (cmdBase === '/effort')
829
+ return intent('model.current', modelScope);
830
+ if (cmdBase === '/chatmode')
831
+ return intent('chatmode.current', args?.scope === 'agent' ? 'agent' : 'relation');
832
+ if (cmdBase === '/mentionmode')
833
+ return intent('mentionmode.current', args?.scope === 'agent' ? 'agent' : 'relation');
834
+ if (cmdBase === '/gateway')
835
+ return intent('gateway.read', 'process');
836
+ if (cmdBase === '/config')
837
+ return intent('config.read', 'process');
838
+ if (cmdBase === '/system')
839
+ return intent('system.status', 'process');
840
+ if (cmdBase === '/agent') {
841
+ return intent(fromControlChannel && !args?.aid ? 'agent.list' : 'agent.show', fromControlChannel ? 'control' : 'agent');
842
+ }
843
+ if (cmdBase === '/trigger')
844
+ return intent('trigger.list', 'relation');
845
+ }
846
+ if (verb === 'update') {
847
+ if (cmdBase === '/model')
848
+ return intent('model.use', modelScope, { model: value });
849
+ if (cmdBase === '/effort')
850
+ return intent('model.effort', modelScope, { effort: value });
851
+ if (cmdBase === '/chatmode')
852
+ return intent('chatmode.update', args?.scope === 'agent' ? 'agent' : 'relation', { value });
853
+ if (cmdBase === '/mentionmode')
854
+ return intent('mentionmode.update', args?.scope === 'agent' ? 'agent' : 'relation', { value });
855
+ if (cmdBase === '/gateway')
856
+ return intent('gateway.write', 'process');
857
+ if (cmdBase === '/config')
858
+ return intent('config.write', 'process');
859
+ if (cmdBase === '/trigger')
860
+ return intent('trigger.update', 'relation', { action: 'update' });
861
+ }
862
+ if (verb === 'action') {
863
+ if (cmdBase === '/file') {
864
+ if (action === 'list')
865
+ return intent('file.list', 'filesystem');
866
+ if (action === 'fetch')
867
+ return intent('file.fetch', 'filesystem');
868
+ }
869
+ if (cmdBase === '/gateway')
870
+ return intent('gateway.write', 'process', { action });
871
+ if (cmdBase === '/system') {
872
+ if (action === 'restart')
873
+ return intent('system.restart', 'process', { action });
874
+ if (action === 'upgrade')
875
+ return intent('system.upgrade', 'process', { action });
876
+ if (action === 'check')
877
+ return intent('system.status', 'process', { action });
878
+ }
879
+ if (cmdBase === '/agent') {
880
+ if (action === 'reload')
881
+ return intent('agent.reload', fromControlChannel ? 'control' : 'agent', { action });
882
+ if (action === 'create')
883
+ return intent('agent.create', 'control', { action });
884
+ if (action === 'delete' || action === 'disable')
885
+ return intent('agent.delete', 'control', { action });
886
+ }
887
+ if (cmdBase === '/trigger') {
888
+ if (action === 'set')
889
+ return intent('trigger.create', 'relation', { action });
890
+ if (action === 'show')
891
+ return intent('trigger.show', 'relation', { action });
892
+ if (action === 'history')
893
+ return intent('trigger.history', 'relation', { action });
894
+ if (action === 'enable' || action === 'disable')
895
+ return intent('trigger.setEnabled', 'relation', { action });
896
+ if (action === 'cancel')
897
+ return intent('trigger.cancel', 'relation', { action });
898
+ if (action === 'delete')
899
+ return intent('trigger.delete', 'relation', { action });
900
+ if (action === 'run' || action === 'test')
901
+ return intent('trigger.run', 'relation', { action });
902
+ }
903
+ }
904
+ return null;
905
+ }
906
+ function buildRelationIntentArgs(params) {
907
+ const out = { ...(params.args ?? {}) };
908
+ if (params.selfAid && out.self === undefined)
909
+ out.self = params.selfAid;
910
+ if (params.peerKey && out.peer === undefined && out.peerKey === undefined)
911
+ out.peer = params.peerKey;
912
+ return out;
913
+ }
914
+ function buildMenuAuthSubject(owner, params) {
915
+ if (params.subject)
916
+ return params.subject;
917
+ const agent = owner.getOwningAgent?.(params.channel);
918
+ const selfAid = agent?.aid || params.session?.selfAID;
919
+ const channelType = owner.resolveChannelType?.(params.channel) || params.session?.channelType;
920
+ const chatType = params.session?.chatType ?? params.explicitChatType ?? 'private';
921
+ const peerKeyId = chatType === 'group'
922
+ ? (params.session?.metadata?.groupId || params.channelId)
923
+ : params.userId;
924
+ return buildAuthSubject({
925
+ selfAid,
926
+ actorId: params.userId,
927
+ channel: params.channel,
928
+ channelType: channelType || params.channel.split('#')[0],
929
+ channelId: params.channelId,
930
+ chatType,
931
+ conversationId: peerKeyId,
932
+ identity: params.identity,
933
+ processOwners: loadDaemonConfig().owners ?? [],
934
+ fromControlChannel: params.fromControlChannel,
935
+ });
936
+ }
937
+ async function authorizeMenuIntent(params) {
938
+ const { intent, identity, session, channel, channelId, userId, fromControlChannel } = params;
939
+ if (!intent)
940
+ return null;
941
+ const subject = buildMenuAuthSubject(this, {
942
+ identity,
943
+ session,
944
+ explicitChatType: params.explicitChatType,
945
+ channel,
946
+ channelId,
947
+ userId,
948
+ fromControlChannel,
949
+ subject: params.subject,
950
+ });
951
+ if (intent.operation.startsWith('trigger.') && !subject.selfAid) {
952
+ return { error: 'Trigger operation requires a channel bound to an Agent', code: 'ROLE_ACCESS_DENIED' };
953
+ }
954
+ if (intent.scope === 'relation') {
955
+ intent.args = buildRelationIntentArgs({
956
+ args: intent.args,
957
+ selfAid: subject.selfAid,
958
+ peerKey: subject.peerKey,
959
+ });
960
+ }
961
+ const source = params.source ?? 'menu';
962
+ intent.source = source;
963
+ const decision = await authorizeOperation({ source, intent, subject });
964
+ if (!decision.allow) {
965
+ if (intent.operation.startsWith('role.')) {
966
+ return {
967
+ error: decision.reason,
968
+ code: 'PERMISSION_DENIED',
969
+ data: {
970
+ $schema_version: 1,
971
+ kind: 'role_permission_denied',
972
+ self: subject.selfAid ?? null,
973
+ actorRole: subject.role,
974
+ operation: intent.operation,
975
+ },
976
+ };
977
+ }
978
+ return { error: decision.reason, code: decision.code };
979
+ }
980
+ return null;
981
+ }
982
+ function buildRoleMenuContext(owner, channel, subject) {
983
+ const owningAgent = owner.getOwningAgent?.(channel);
984
+ const self = owningAgent?.aid ?? subject.selfAid;
985
+ if (!self) {
986
+ throw {
987
+ code: 'NOT_FOUND',
988
+ message: 'Role Menu requires a receiving Agent',
989
+ data: { $schema_version: 1, kind: 'agent_not_found' },
990
+ };
991
+ }
992
+ const aunAvailable = (owningAgent?.channelInstanceNames?.() ?? [])
993
+ .some((channelName) => owner.hasRegisteredChannel?.(channelName, 'aun') === true);
994
+ return {
995
+ self,
996
+ actorRole: subject.role,
997
+ actorAid: subject.actorId,
998
+ chatType: subject.chatType,
999
+ aunAvailable,
1000
+ availableBaseagents: Array.from(new Set([
1001
+ ...Object.keys(owningAgent?.config?.baseagents ?? {}),
1002
+ ...(owningAgent?.baseagent ? [owningAgent.baseagent] : []),
1003
+ ...(owningAgent?.name ? (owner.getAvailableBaseagentsForOwner?.(owningAgent.name) ?? []) : []),
1004
+ ])),
1005
+ listModels: async (baseagent) => {
1006
+ const runner = owner.getAgent?.(channel, baseagent);
1007
+ if (!runner?.listModels)
1008
+ return [];
1009
+ return await runner.listModels() ?? [];
1010
+ },
1011
+ resolveModelId: (baseagent, model) => {
1012
+ const runner = owner.getAgent?.(channel, baseagent);
1013
+ return typeof runner?.resolveModelId === 'function' ? runner.resolveModelId(model) : undefined;
1014
+ },
1015
+ };
1016
+ }
1017
+ async function authorizeRoleMenu(owner, params) {
1018
+ const subject = buildMenuAuthSubject(owner, {
1019
+ identity: params.identity,
1020
+ session: params.session,
1021
+ channel: params.channel,
1022
+ channelId: params.channelId,
1023
+ userId: params.userId,
1024
+ fromControlChannel: params.fromControlChannel,
1025
+ subject: params.subject,
1026
+ });
1027
+ const operation = roleMenuOperation(params.kind, params.args, params.value);
1028
+ const denied = await authorizeMenuIntent.call(owner, {
1029
+ intent: { operation, scope: 'agent', source: params.source, args: { ...(params.args ?? {}) } },
1030
+ identity: params.identity,
1031
+ subject,
1032
+ session: params.session,
1033
+ channel: params.channel,
1034
+ channelId: params.channelId,
1035
+ userId: params.userId,
1036
+ fromControlChannel: params.fromControlChannel,
1037
+ source: params.source,
1038
+ });
1039
+ if (denied)
1040
+ return denied;
1041
+ return { context: buildRoleMenuContext(owner, params.channel, subject) };
1042
+ }
1043
+ function ecwebErr(id, name, code, message) {
1044
+ return menuFailure({ id, ...(name ? { name } : {}) }, normalizeMenuError({ code, message }));
1045
+ }
1046
+ function ecwebResp(id, name, result) {
1047
+ return 'error' in result
1048
+ ? menuFailure({ id, ...(name ? { name } : {}) }, normalizeMenuError(result))
1049
+ : menuSuccess({ id, ...(name ? { name } : {}) }, result.data);
1050
+ }
1051
+ function menuResultFailure(error) {
1052
+ const normalized = normalizeMenuError(error);
1053
+ return { error: normalized.message, code: normalized.code, ...(normalized.data !== undefined ? { data: normalized.data } : {}) };
1054
+ }
1055
+ function resolveExternalMenuAgentChannel(owner, payload, fallbackChannel, required) {
1056
+ const requestedAid = typeof payload?.agent === 'string' ? payload.agent.trim() : '';
1057
+ if (!requestedAid) {
1058
+ return required
1059
+ ? { error: '缺少顶层 agent 参数', code: 'MISSING_AID' }
1060
+ : { channel: fallbackChannel };
1061
+ }
1062
+ const agent = owner.agentRegistry?.get?.(requestedAid) ?? null;
1063
+ if (!agent)
1064
+ return { error: `未找到 Agent: ${requestedAid}`, code: 'NOT_FOUND' };
1065
+ const channel = agent.channelInstanceNames?.()[0];
1066
+ if (!channel)
1067
+ return { error: `Agent 无可用 channel: ${requestedAid}`, code: 'NOT_FOUND' };
1068
+ return { channel };
1069
+ }
1070
+ function parseScheduleDurationMs(value) {
1071
+ const numeric = Number(value);
1072
+ if (Number.isFinite(numeric))
1073
+ return numeric;
1074
+ const parsed = parseDuration(value);
1075
+ return parsed ?? Number.NaN;
1076
+ }
1077
+ export function validateScheduleParams(scheduleType, scheduleValue) {
1078
+ if (!['once', 'delay', 'at', 'cron', 'interval', 'event'].includes(scheduleType)) {
1079
+ return `无效 scheduleType: ${scheduleType}(可选: once / delay / at / cron / interval / event)`;
1080
+ }
1081
+ if (scheduleType === 'once')
1082
+ return null;
1083
+ if (scheduleType === 'delay' || scheduleType === 'interval') {
1084
+ const ms = parseScheduleDurationMs(scheduleValue);
1085
+ if (!Number.isFinite(ms) || ms <= 0)
1086
+ return `${scheduleType} 的 scheduleValue 需为正数时长(如 30s / 15m / 2h / 1d): ${scheduleValue}`;
1087
+ }
1088
+ else if (scheduleType === 'at') {
1089
+ const ts = new Date(scheduleValue).getTime();
1090
+ if (!Number.isFinite(ts))
1091
+ return `at 的 scheduleValue 需为合法时间: ${scheduleValue}`;
1092
+ }
1093
+ else if (scheduleType === 'cron') {
1094
+ try {
1095
+ CronExpressionParser.parse(scheduleValue);
1096
+ }
1097
+ catch {
1098
+ return `无效 cron 表达式: ${scheduleValue}`;
1099
+ }
1100
+ }
1101
+ else if (!scheduleValue) {
1102
+ return 'event 的 scheduleValue 不能为空';
1103
+ }
1104
+ return null;
1105
+ }
1106
+ /**
1107
+ * 返回结构化命令菜单(供 menu.query 使用)
1108
+ * owner 看到全部命令,admin 看到管理级命令(不含 owner-only),visitor/member 仅看到用户级命令
1109
+ */
1110
+ export function getMenuItems(role, chatType = 'private', scope = 'agent') {
1111
+ const isOwner = role === 'owner';
1112
+ const isAdmin = isManagementRole(role);
1113
+ const isControlScope = scope === 'control';
1114
+ const canReadTopic = role !== 'none';
1115
+ const items = [];
1116
+ if (!isAdmin && chatType === 'group') {
1117
+ return [
1118
+ ...(canReadTopic ? [{
1119
+ group: '话题管理',
1120
+ commands: [
1121
+ { cmd: '/topic', label: '话题管理', desc: '查看当前聊天的话题会话', next: { type: 'select', dynamic: true } },
1122
+ ]
1123
+ }] : []),
1124
+ {
1125
+ group: '其他',
1126
+ commands: [
1127
+ { cmd: '/status', label: '显示会话状态' },
1128
+ { cmd: '/check', label: '检查 EvolAgent 实例健康' },
1129
+ { cmd: '/help', label: '显示帮助信息' },
1130
+ ]
1131
+ }
1132
+ ];
1133
+ }
1134
+ items.push({
1135
+ group: '会话管理',
1136
+ commands: [
1137
+ { cmd: '/new', label: '创建新会话', desc: '清空历史,开始全新对话', next: { type: 'text' } },
1138
+ { cmd: '/s', label: '切换会话', desc: '切换到同项目下的其他会话', next: { type: 'select', dynamic: true } },
1139
+ ...(canReadTopic ? [{ cmd: '/topic', label: '话题管理', desc: '查看与管理当前聊天的话题会话', next: { type: 'select', dynamic: true } }] : []),
1140
+ { cmd: '/name', label: '重命名当前会话', desc: '为当前会话设置一个易识别的名称', next: { type: 'text' } },
1141
+ { cmd: '/del', label: '删除指定会话', desc: '永久删除一个非活跃会话', next: { type: 'select', dynamic: true } },
1142
+ ...(isAdmin ? [
1143
+ { cmd: '/fork', label: '分支当前会话', desc: '基于当前会话创建独立分支', next: { type: 'text' } },
1144
+ { cmd: '/rewind', label: '查看历史/撤销指定轮次', desc: '回退会话到指定轮次,可选择撤销文件改动' },
1145
+ { cmd: '/compact', label: '压缩会话上下文', desc: '将长对话压缩为摘要以节省 token' },
1146
+ ] : []),
1147
+ ]
1148
+ });
1149
+ if (isAdmin) {
1150
+ items.push({
1151
+ group: 'Agent 与模型',
1152
+ commands: [
1153
+ { cmd: '/baseagent', label: '切换 Agent 后端', desc: '切换 Agent 当前 Baseagent', next: { type: 'select', dynamic: true } },
1154
+ { cmd: '/model', label: '切换模型', desc: '切换当前 Agent 使用的模型版本', next: { type: 'select', dynamic: true } },
1155
+ { cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', items: [
1156
+ { value: 'low', label: 'Low' },
1157
+ { value: 'medium', label: 'Medium' },
1158
+ { value: 'high', label: 'High' },
1159
+ { value: 'max', label: 'Max' },
1160
+ ] } },
1161
+ { cmd: '/chatmode', label: '切换会话模式', desc: '控制 Agent 主动性(被动响应或主动推进)', next: { type: 'select', items: [
1162
+ { value: 'interactive', label: '交互模式', desc: '仅在收到消息时响应' },
1163
+ { value: 'proactive', label: '主动模式', desc: 'Agent 可主动推进任务' },
1164
+ ] } },
1165
+ { cmd: '/mentionmode', label: '切换 @ 处理模式', desc: '控制群聊消息过滤(仅@提及或全部响应)', next: { type: 'select', items: [
1166
+ { value: 'mention-only', label: '@ 提及', desc: '仅在被 @ 提及时响应' },
1167
+ { value: 'disabled', label: '全部响应', desc: '响应群内所有消息' },
1168
+ ] } },
1169
+ { cmd: '/capability', label: '能力开关管理', desc: '查看并管理 Skills / MCP / Plugins' },
1170
+ ]
1171
+ });
1172
+ items.push({
1173
+ group: '权限管理',
1174
+ commands: [
1175
+ { cmd: '/perm', label: '查看权限模式', desc: '权限模式由当前角色定义决定' },
1176
+ ]
1177
+ });
1178
+ items.push({
1179
+ group: '运维',
1180
+ commands: [
1181
+ { cmd: '/status', label: '显示会话状态', desc: '查看当前会话、项目、Agent 的详细状态' },
1182
+ { cmd: '/stop', label: '中断当前任务', desc: '立即中断正在执行的 Agent 任务' },
1183
+ { cmd: '/check', label: '检查 EvolAgent 实例健康', desc: '检查各 EvolAgent 实例、后端与消息通道的健康状态' },
1184
+ ...(isOwner && !isControlScope ? [{
1185
+ cmd: '/observable',
1186
+ label: '观察者模式',
1187
+ desc: '向 owners 转发 Agent 入站和出站消息',
1188
+ next: { type: 'select', items: [
1189
+ { value: 'true', label: '开启' },
1190
+ { value: 'false', label: '关闭' },
1191
+ ] },
1192
+ }] : []),
1193
+ { cmd: '/activity', label: '控制中间输出显示', desc: '设置工具调用过程的可见范围', next: { type: 'select', items: [
1194
+ { value: 'all', label: '全部显示', desc: '所有用户均可见中间输出' },
1195
+ { value: 'text', label: '仅文字进展', desc: '显示文字进展,隐藏工具活动' },
1196
+ { value: 'none', label: '不显示', desc: '关闭所有中间输出' },
1197
+ ] } },
1198
+ ...(isControlScope && isOwner ? [
1199
+ { cmd: '/restart', label: '重启服务', desc: '重启整个 EvolCore 服务进程' },
1200
+ ] : []),
1201
+ ...(isAdmin ? [
1202
+ { cmd: '/file', label: '发送项目内文件', desc: '将项目目录内的文件发送给用户' },
1203
+ ] : []),
1204
+ ]
1205
+ });
1206
+ }
1207
+ else {
1208
+ items.push({
1209
+ group: '其他',
1210
+ commands: [
1211
+ { cmd: '/status', label: '显示会话状态', desc: '查看当前会话的基本状态' },
1212
+ { cmd: '/check', label: '检查 EvolAgent 实例健康', desc: '检查 EvolAgent 实例与消息通道健康状态' },
1213
+ ]
1214
+ });
1215
+ }
1216
+ items.push({
1217
+ group: '帮助',
1218
+ commands: [
1219
+ { cmd: '/help', label: '显示帮助信息', desc: '列出所有可用命令及说明' },
1220
+ ]
1221
+ });
1222
+ return items;
1223
+ }
1224
+ /** 动态子菜单:根据 cmd 路径返回选项列表(供 menu.query + cmd 使用) */
1225
+ export async function getSubMenuItems(cmd, channel, channelId, userId, args, overrideIdentity, explicitChatType, fromControlChannel = false, authSubject, source = 'menu') {
1226
+ const session = await this.sessionManager.getActiveSession(channel, channelId);
1227
+ const subject = buildMenuAuthSubject(this, {
1228
+ identity: overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session)),
1229
+ session,
1230
+ explicitChatType,
1231
+ channel,
1232
+ channelId,
1233
+ userId,
1234
+ fromControlChannel,
1235
+ subject: authSubject,
1236
+ });
1237
+ const cmdBase0 = cmd.trim().split(' ')[0];
1238
+ const gated0 = gateControlScope.call(this, { cmdBase: cmdBase0, args, channel, fromControlChannel });
1239
+ if (gated0)
1240
+ throw { code: gated0.code, message: gated0.error };
1241
+ if (cmdBase0 === '/role') {
1242
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1243
+ const authorized = await authorizeRoleMenu(this, {
1244
+ kind: 'options', args, identity, subject: authSubject, session, channel, channelId,
1245
+ userId, fromControlChannel, source,
1246
+ });
1247
+ if ('error' in authorized)
1248
+ throw authorized;
1249
+ return await roleMenuOptions(authorized.context, args);
1250
+ }
1251
+ // ── /agent list(只读) ──
1252
+ // 控制 channel:验 evolcore.owners,返回全量;agent channel:放行但仅返回自身单条。
1253
+ if (cmd === '/agent') {
1254
+ if (fromControlChannel) {
1255
+ if (!subject.isDaemonOwner) {
1256
+ throw { code: 'FORBIDDEN', message: '操作需要 owner 权限' };
1257
+ }
1258
+ const res = await execAgentOptions(args);
1259
+ if ('error' in res)
1260
+ throw { code: res.code, message: res.error };
1261
+ return res.data.agents.map(ag => ({ value: ag.aid, label: ag.name || ag.aid, desc: ag.status }));
1262
+ }
1263
+ // agent channel:作用域绑定自身,仅返回自身单条
1264
+ const selfAid = this.getOwningAgent?.(channel)?.aid;
1265
+ if (!selfAid)
1266
+ throw { code: 'FORBIDDEN', message: '当前 channel 无绑定 agent' };
1267
+ const res = await execAgentOptions(args);
1268
+ if ('error' in res)
1269
+ throw { code: res.code, message: res.error };
1270
+ return res.data.agents
1271
+ .filter(ag => ag.aid === selfAid)
1272
+ .map(ag => ({ value: ag.aid, label: ag.name || ag.aid, desc: ag.status }));
1273
+ }
1274
+ if (cmd === '/capability') {
1275
+ const type = args?.type;
1276
+ if (!isCapabilityType(type)) {
1277
+ throw { code: type ? 'INVALID_TYPE' : 'INVALID_ARGS', message: 'args.type 必须是 skill / mcp / plugin' };
1278
+ }
1279
+ const target = resolveCapabilityTarget.call(this, { channel, args, session, fromControlChannel });
1280
+ if ('error' in target)
1281
+ throw { code: target.code, message: target.error };
1282
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1283
+ const options = await listCapabilityOptions(target.ctx, target.config, type);
1284
+ return sanitizeCapabilityOptionsForRole(options, identity.role);
1285
+ }
1286
+ // ── 关系级 /trigger list(每个 trigger 一个 MenuItem) ──
1287
+ if (cmd === '/trigger') {
1288
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1289
+ const authDenied = await authorizeMenuIntent.call(this, {
1290
+ intent: buildMenuIntent('query', cmd, args, undefined, undefined, fromControlChannel),
1291
+ identity,
1292
+ subject,
1293
+ session,
1294
+ explicitChatType,
1295
+ channel,
1296
+ channelId,
1297
+ userId,
1298
+ fromControlChannel,
1299
+ source,
1300
+ });
1301
+ if (authDenied)
1302
+ throw { code: authDenied.code, message: authDenied.error, data: authDenied.data };
1303
+ const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
1304
+ const scope = args?.options === 'all' ? 'all' : 'enabled';
1305
+ const role = identity.role;
1306
+ const isAdmin = isManagementRole(role);
1307
+ if (!triggerScheduler)
1308
+ return [];
1309
+ const list = triggerScheduler.list({ all: scope === 'all' });
1310
+ return list
1311
+ .filter((definition) => this.canAccessTriggerDefinition(definition, userId ?? '', channel, isAdmin))
1312
+ .map((definition) => {
1313
+ const view = this.definitionToTriggerView(definition, triggerScheduler);
1314
+ return {
1315
+ ...view,
1316
+ value: definition.id,
1317
+ label: definition.name,
1318
+ desc: `${view.scheduleType}${view.nextFireAt ? ` | 下次 ${new Date(view.nextFireAt).toLocaleString()}` : ''}`,
1319
+ status: definition.enabled ? 'active' : 'disabled',
1320
+ };
1321
+ });
1322
+ }
1323
+ if (cmd === '/topic') {
1324
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1325
+ if (!this.canReadTopics(identity.role)) {
1326
+ throw { code: 'FORBIDDEN', message: '无权限查看话题' };
1327
+ }
1328
+ if (args?.mode === 'fork-turns') {
1329
+ const source = await resolveTopicForkSource(this.sessionManager, channel, channelId, args?.sourceThreadId);
1330
+ if (!source)
1331
+ throw { code: 'NOT_FOUND', message: '来源会话不存在' };
1332
+ if (source.chatType !== 'private' || explicitChatType === 'group') {
1333
+ throw { code: 'NOT_SUPPORTED', message: '历史上下文分叉仅支持私聊' };
1334
+ }
1335
+ if (!source.agentSessionId)
1336
+ return [];
1337
+ const agent = this.getAgent(channel, source.baseagent);
1338
+ if (!agent.capabilities?.forkAtTurn || !agent.getSessionMessages || !agent.forkSessionAt)
1339
+ return [];
1340
+ const turns = buildSessionTurnList(await agent.getSessionMessages(source.agentSessionId, source.projectPath));
1341
+ return turns.map((turn) => ({
1342
+ value: turn.assistantUuid,
1343
+ label: `#${turn.index} ${turn.userContent}`,
1344
+ preview: turn.userContent,
1345
+ turn: turn.index,
1346
+ assistantMessageId: turn.assistantUuid,
1347
+ userMessageId: turn.userUuid,
1348
+ }));
1349
+ }
1350
+ const sessions = await this.sessionManager.listSessions(channel, channelId);
1351
+ return sessions
1352
+ .filter((s) => !!s.threadId)
1353
+ .map((s) => this.buildTopicMenuItem(s));
1354
+ }
1355
+ if (cmd === '/s' || cmd === '/session' || cmd === '/del') {
1356
+ const sessions = await this.sessionManager.listSessions(channel, channelId);
1357
+ const active = cmd === '/del' ? await this.sessionManager.getActiveSession(channel, channelId) : null;
1358
+ const currentSession = session;
1359
+ const items = sessions
1360
+ .filter((s) => !s.threadId)
1361
+ .filter((s) => !active || s.id !== active.id)
1362
+ .map((s) => {
1363
+ const displayName = displaySessionTitle(s.name, s.id.slice(0, 8));
1364
+ const item = {
1365
+ value: s.name || s.id.slice(0, 8),
1366
+ label: displayName,
1367
+ selected: currentSession ? s.id === currentSession.id : false,
1368
+ };
1369
+ if (s.agentSessionId) {
1370
+ item.agentSessionId = s.agentSessionId;
1371
+ const fileInfo = this.sessionManager.getSessionFileInfo(s.projectPath, s.agentSessionId, s.baseagent);
1372
+ if (fileInfo.turns)
1373
+ item.turns = fileInfo.turns;
1374
+ const firstMsg = this.sessionManager.readSessionFirstMessage(s.projectPath, s.agentSessionId, s.baseagent);
1375
+ if (firstMsg)
1376
+ item.preview = firstMsg.length > 80 ? firstMsg.slice(0, 80) + '…' : firstMsg;
1377
+ }
1378
+ if (s.updatedAt)
1379
+ item.lastActive = s.updatedAt;
1380
+ return item;
1381
+ });
1382
+ if (cmd === '/s' || cmd === '/session') {
1383
+ items.push({ value: 'cli', label: '查看 CLI 会话', desc: '列出未导入的 CLI 本地会话' });
1384
+ }
1385
+ return items;
1386
+ }
1387
+ if (cmd === '/baseagent') {
1388
+ const requestedAgent = typeof args?.aid === 'string'
1389
+ ? (this.agentRegistry?.get?.(args.aid) ?? null)
1390
+ : null;
1391
+ const currentAgent = requestedAgent?.baseagent
1392
+ ?? this.agentRegistry?.resolveByChannel(channel)?.baseagent
1393
+ ?? this.parseDefaultBaseagent?.();
1394
+ const available = requestedAgent?.name
1395
+ ? (this.getAvailableBaseagentsForOwner?.(requestedAgent.name) ?? [])
1396
+ : this.getAvailableBaseagents(channel);
1397
+ return available.map((name) => ({ value: name, label: name, selected: name === currentAgent }));
1398
+ }
1399
+ if (cmd === '/model') {
1400
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1401
+ const target = resolveMenuModelTarget.call(this, {
1402
+ args,
1403
+ session,
1404
+ channel,
1405
+ channelId,
1406
+ userId,
1407
+ role: identity.role,
1408
+ explicitChatType,
1409
+ fromControlChannel,
1410
+ field: 'model',
1411
+ });
1412
+ if ('error' in target)
1413
+ throw { code: target.code, message: target.error };
1414
+ const agent = this.getAgent(channel, target.baseagent);
1415
+ if (hasModelSwitcher(agent) && agent.listModels) {
1416
+ const role = identity.role;
1417
+ const authIntent = {
1418
+ operation: 'model.list',
1419
+ scope: target.scope,
1420
+ source: 'menu',
1421
+ args: {},
1422
+ };
1423
+ const subject = buildMenuAuthSubject(this, {
1424
+ identity,
1425
+ session,
1426
+ explicitChatType,
1427
+ channel,
1428
+ channelId,
1429
+ userId,
1430
+ fromControlChannel,
1431
+ subject: authSubject,
1432
+ });
1433
+ authIntent.args = target.scope === 'relation'
1434
+ ? buildRelationIntentArgs({ args, selfAid: target.sel.self, peerKey: target.sel.peerKey })
1435
+ : { ...(args ?? {}), self: target.sel.self };
1436
+ const decision = await authorizeOperation({ source: 'menu', intent: authIntent, subject, audit: false });
1437
+ if (!decision.allow)
1438
+ throw { code: decision.code, message: decision.reason };
1439
+ const rawModels = await agent.listModels() ?? [];
1440
+ const models = filterModelsForRole(role, target.baseagent, rawModels, agent.resolveModelId?.bind(agent));
1441
+ const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'current');
1442
+ const currentModel = requestedModel || readMenuModel(target, agent).value || agent.getModel();
1443
+ if (models.length > 0)
1444
+ return models.map((m) => ({ value: m, label: modelDisplayLabel(agent, m), selected: modelMatches(agent, m, currentModel) }));
1445
+ }
1446
+ return null;
1447
+ }
1448
+ // if (cmd === '/restart') {
1449
+ // const isOwner = userId ? this.sessionManager.resolveIdentity(channel, userId).role === 'owner' : false;
1450
+ // // 列出所有 channel type
1451
+ // const visibleTypes = new Set<string>();
1452
+ // for (const [name] of this.adapters) {
1453
+ // const t = this.channelTypeMap.get(name);
1454
+ // if (t) visibleTypes.add(t);
1455
+ // }
1456
+ // const channels = [...visibleTypes].map(type => ({ value: type, label: type, desc: '重连此类型所有渠道实例' }));
1457
+ // if (isOwner) channels.unshift({ value: '', label: '重启服务', desc: '重启整个 EvolCore 服务进程' });
1458
+ // return channels;
1459
+ // }
1460
+ if (cmd === '/activity') {
1461
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1462
+ const target = resolveMenuActivityTarget.call(this, {
1463
+ args,
1464
+ session,
1465
+ channel,
1466
+ channelId,
1467
+ userId,
1468
+ role: identity.role,
1469
+ explicitChatType,
1470
+ fromControlChannel,
1471
+ });
1472
+ if ('error' in target)
1473
+ throw { code: target.code, message: target.error };
1474
+ const fallback = this.agentRegistry?.getShowActivities?.(channel) ?? 'all';
1475
+ const currentMode = readMenuActivity(target, fallback).value;
1476
+ return [
1477
+ { value: 'all', label: '私聊显示', selected: currentMode === 'all' },
1478
+ { value: 'text', label: '仅文字进展', selected: currentMode === 'text' },
1479
+ { value: 'none', label: '全部静默', selected: currentMode === 'none' },
1480
+ ];
1481
+ }
1482
+ if (cmd === '/effort') {
1483
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1484
+ const target = resolveMenuModelTarget.call(this, {
1485
+ args,
1486
+ session,
1487
+ channel,
1488
+ channelId,
1489
+ userId,
1490
+ role: identity.role,
1491
+ explicitChatType,
1492
+ fromControlChannel,
1493
+ field: 'effort',
1494
+ });
1495
+ if ('error' in target)
1496
+ throw { code: target.code, message: target.error };
1497
+ const agent = this.getAgent(channel, target.baseagent);
1498
+ const requestedModel = menuStringArg(args, 'model') ?? menuStringArg(args, 'currentModel');
1499
+ const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
1500
+ const currentModel = requestedModel || (hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name);
1501
+ const efforts = getAvailableEfforts(agent, currentModel);
1502
+ const currentEffort = menuStringArg(args, 'effort') ?? menuStringArg(args, 'current') ?? readMenuModel(target, agent).value ?? 'auto';
1503
+ const allItems = [...efforts, 'auto'];
1504
+ return allItems.map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e, selected: e === currentEffort }));
1505
+ }
1506
+ if (cmd === '/chatmode') {
1507
+ const target = resolveMenuChatmodeTarget.call(this, {
1508
+ args,
1509
+ session,
1510
+ channel,
1511
+ channelId,
1512
+ userId,
1513
+ role: (overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session))).role,
1514
+ explicitChatType,
1515
+ fromControlChannel,
1516
+ });
1517
+ if ('error' in target)
1518
+ throw { code: target.code, message: target.error };
1519
+ const currentMode = readMenuChatmode(target).value;
1520
+ return [
1521
+ { value: 'interactive', label: '交互模式', selected: currentMode === 'interactive' },
1522
+ { value: 'proactive', label: '主动模式', selected: currentMode === 'proactive' },
1523
+ ];
1524
+ }
1525
+ if (cmd === '/mentionmode') {
1526
+ const target = resolveMenuMentionModeTarget.call(this, {
1527
+ args,
1528
+ session,
1529
+ channel,
1530
+ channelId,
1531
+ userId,
1532
+ role: (overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session))).role,
1533
+ explicitChatType,
1534
+ fromControlChannel,
1535
+ });
1536
+ if ('error' in target)
1537
+ throw { code: target.code, message: target.error };
1538
+ const fallback = dispatchToMentionMode(session?.metadata?.dispatchMode) ?? null;
1539
+ const currentMode = readMenuMentionMode(target, fallback, fallback === null ? null : 'session').value;
1540
+ return [
1541
+ { value: 'mention-only', label: '@提及时响应', selected: currentMode === 'mention-only' },
1542
+ { value: 'disabled', label: '所有消息响应', selected: currentMode === 'disabled' },
1543
+ ];
1544
+ }
1545
+ if (cmd === '/perm') {
1546
+ return [];
1547
+ }
1548
+ return null;
1549
+ }
1550
+ // ── Menu Protocol exec ────────────────────────────────────────────────
1551
+ //
1552
+ // 三个入口对应 menu.query / menu.update / menu.action:
1553
+ // execMenuQuery — 查询某项当前值(无会话时多数 fallback 到 evolagent config)
1554
+ // execMenuUpdate — 写入新值(持久化到 session 或 evolagent config)
1555
+ // execMenuAction — 触发动词(stop/restart/new/delete/compact/fork/switch/check/upgrade)
1556
+ //
1557
+ // 所有方法返回 { data } 或 { error, code? }。code 是结构化错误码(NO_ACTIVE_SESSION 等),
1558
+ // 客户端可据此决定降级策略。message-bridge 把 code 透传到 menu.response。
1559
+ /** menu.query — 查询当前值。 */
1560
+ export async function execMenuQuery(cmd, channel, channelId, userId, args, explicitChatType, fromControlChannel = false, overrideIdentity, authSubject, source = 'menu') {
1561
+ const cmdBase = cmd.trim().split(' ')[0];
1562
+ if (!cmdBase)
1563
+ return { error: '缺少命令', code: 'MISSING_CMD' };
1564
+ const gated = gateControlScope.call(this, { cmdBase, args, channel, fromControlChannel });
1565
+ if (gated)
1566
+ return gated;
1567
+ const { session, evolagent } = await this.loadMenuContext(channel, channelId);
1568
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1569
+ const subject = buildMenuAuthSubject(this, {
1570
+ identity,
1571
+ session,
1572
+ explicitChatType,
1573
+ channel,
1574
+ channelId,
1575
+ userId,
1576
+ fromControlChannel,
1577
+ subject: authSubject,
1578
+ });
1579
+ if (cmdBase === '/role') {
1580
+ const authorized = await authorizeRoleMenu(this, {
1581
+ kind: 'query', args, identity, subject, session, channel, channelId,
1582
+ userId, fromControlChannel, source,
1583
+ });
1584
+ if ('error' in authorized)
1585
+ return authorized;
1586
+ try {
1587
+ return { data: await roleMenuQuery(authorized.context, args) };
1588
+ }
1589
+ catch (error) {
1590
+ return menuResultFailure(error);
1591
+ }
1592
+ }
1593
+ const authDenied = await authorizeMenuIntent.call(this, {
1594
+ intent: buildMenuIntent('query', cmdBase, args, undefined, undefined, fromControlChannel),
1595
+ identity,
1596
+ subject,
1597
+ session,
1598
+ explicitChatType,
1599
+ channel,
1600
+ channelId,
1601
+ userId,
1602
+ fromControlChannel,
1603
+ source,
1604
+ });
1605
+ if (authDenied)
1606
+ return authDenied;
1607
+ // ── /agent 查询(只读) ──
1608
+ // 控制 channel:验 owners,按 args.aid 查任意 agent;agent channel:强制查自身。
1609
+ if (cmdBase === '/agent') {
1610
+ if (fromControlChannel) {
1611
+ if (!subject.isDaemonOwner) {
1612
+ return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
1613
+ }
1614
+ return await execAgentQuery(args);
1615
+ }
1616
+ const selfAid = this.getOwningAgent?.(channel)?.aid;
1617
+ if (!selfAid)
1618
+ return { error: '当前 channel 无绑定 agent', code: 'FORBIDDEN' };
1619
+ return await execAgentQuery({ ...(args ?? {}), aid: selfAid });
1620
+ }
1621
+ // ── /gateway 查询(只读,列出全部作用域的网关配置;apiKey 已掩码) ──
1622
+ // 进程级:闸门已要求 fromControlChannel;此处再验 owners 非空。
1623
+ if (cmdBase === '/gateway') {
1624
+ if (!subject.isDaemonOwner) {
1625
+ return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
1626
+ }
1627
+ return gatewayList();
1628
+ }
1629
+ // ── /config 查询(只读,查询各层配置) ──
1630
+ if (cmdBase === '/config') {
1631
+ const scope = args?.scope || 'process';
1632
+ if (scope === 'process') {
1633
+ // 进程级配置需要 owner 权限
1634
+ if (!fromControlChannel || !subject.isDaemonOwner) {
1635
+ return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
1636
+ }
1637
+ const cfg = loadDaemonConfig();
1638
+ return { data: { scope: 'process', config: cfg } };
1639
+ }
1640
+ if (scope === 'defaults') {
1641
+ // 全局默认配置需要 owner 权限
1642
+ if (!fromControlChannel || !subject.isDaemonOwner) {
1643
+ return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
1644
+ }
1645
+ const { read, ConfigTarget } = await import('../../config/config-manager.js');
1646
+ const cfg = read(ConfigTarget.Defaults, undefined, { cache: true });
1647
+ return { data: { scope: 'defaults', config: cfg } };
1648
+ }
1649
+ if (scope === 'agent') {
1650
+ const aid = args?.aid;
1651
+ if (!aid)
1652
+ return { error: '缺少 aid 参数', code: 'MISSING_AID' };
1653
+ // 控制 channel 可以查任意 agent,agent channel 只能查自身
1654
+ if (!fromControlChannel) {
1655
+ const selfAid = this.getOwningAgent?.(channel)?.aid;
1656
+ if (selfAid !== aid) {
1657
+ return { error: '只能查询自己的配置', code: 'FORBIDDEN' };
1658
+ }
1659
+ }
1660
+ const { read, ConfigTarget } = await import('../../config/config-manager.js');
1661
+ const cfg = read(ConfigTarget.Agent, { self: aid }, { cache: true });
1662
+ return { data: { scope: 'agent', aid, config: cfg } };
1663
+ }
1664
+ return { error: '未知的 scope', code: 'INVALID_SCOPE' };
1665
+ }
1666
+ if (cmdBase === '/capability') {
1667
+ const type = args?.type;
1668
+ if (type !== undefined && !isCapabilityType(type)) {
1669
+ return { error: 'type 必须是 skill / mcp / plugin', code: 'INVALID_TYPE' };
1670
+ }
1671
+ const target = resolveCapabilityTarget.call(this, { channel, args, session, evolagent, fromControlChannel });
1672
+ if ('error' in target)
1673
+ return target;
1674
+ const identity = this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
1675
+ const data = queryCapabilityTypes(target.ctx, target.config, type);
1676
+ return { data: sanitizeCapabilityQueryForRole(data, identity.role) };
1677
+ }
1678
+ if (cmdBase === '/pwd') {
1679
+ const sessPath = session?.projectPath;
1680
+ const fallbackPath = evolagent?.config?.projects?.defaultPath;
1681
+ const path = sessPath ?? fallbackPath ?? null;
1682
+ const name = path ? this.getProjectName(path) : null;
1683
+ const source = sessPath != null
1684
+ ? 'session'
1685
+ : fallbackPath != null
1686
+ ? (evolagent?.aid
1687
+ ? menuConfigSource('projects.defaultPath', { self: evolagent.aid }) ?? 'agent'
1688
+ : 'agent')
1689
+ : null;
1690
+ return { data: { name, path, source } };
1691
+ }
1692
+ if (cmdBase === '/session' || cmdBase === '/s') {
1693
+ if (!session) {
1694
+ return { data: { status: 'no-session' } };
1695
+ }
1696
+ const sessionKey = this.getQueueKey(session, channel, channelId);
1697
+ const sessionAgent = this.getAgent(channel, session.baseagent);
1698
+ const isProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
1699
+ const queueLength = this.messageQueue.getQueueLength(sessionKey);
1700
+ const health = await this.sessionManager.getHealthStatus(session.id);
1701
+ let processingDuration;
1702
+ if (isProcessing && session.processingState) {
1703
+ const elapsed = Date.now() - parseInt(session.processingState, 10);
1704
+ if (!isNaN(elapsed) && elapsed > 0)
1705
+ processingDuration = Math.floor(elapsed / 1000);
1706
+ }
1707
+ let turns = 0;
1708
+ if (session.agentSessionId) {
1709
+ const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.agentSessionId, session.baseagent);
1710
+ turns = fileInfo.turns;
1711
+ }
1712
+ const data = {
1713
+ name: session.name || null,
1714
+ agentSessionId: session.agentSessionId || null,
1715
+ status: isProcessing ? 'processing' : 'idle',
1716
+ createdAt: session.createdAt,
1717
+ updatedAt: session.updatedAt,
1718
+ };
1719
+ if (processingDuration !== undefined)
1720
+ data.processingDuration = processingDuration;
1721
+ if (queueLength > 0)
1722
+ data.queueLength = queueLength;
1723
+ if (turns > 0)
1724
+ data.turns = turns;
1725
+ if (health.lastSuccessTime)
1726
+ data.lastSuccess = health.lastSuccessTime;
1727
+ if (health.consecutiveErrors)
1728
+ data.consecutiveErrors = health.consecutiveErrors;
1729
+ if (health.lastError)
1730
+ data.lastError = { type: health.lastErrorType || 'unknown', message: health.lastError.substring(0, 100) };
1731
+ return { data };
1732
+ }
1733
+ if (cmdBase === '/topic') {
1734
+ if (!this.canReadTopics(identity.role)) {
1735
+ return { error: '无权限查看话题', code: 'FORBIDDEN' };
1736
+ }
1737
+ const target = (args?.target ?? '').toString().trim();
1738
+ if (!target)
1739
+ return { error: '缺少 args.target', code: 'MISSING_VALUE' };
1740
+ const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
1741
+ if (!topic)
1742
+ return { error: '话题不存在', code: 'NOT_FOUND' };
1743
+ const sessionKey = this.getQueueKey(topic, channel, channelId);
1744
+ const sessionAgent = this.getAgent(channel, topic.baseagent);
1745
+ const isProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
1746
+ const queueLength = this.messageQueue.getQueueLength(sessionKey);
1747
+ const health = await this.sessionManager.getHealthStatus(topic.id);
1748
+ let processingDuration;
1749
+ if (isProcessing && topic.processingState) {
1750
+ const elapsed = Date.now() - parseInt(topic.processingState, 10);
1751
+ if (!isNaN(elapsed) && elapsed > 0)
1752
+ processingDuration = Math.floor(elapsed / 1000);
1753
+ }
1754
+ let turns = 0;
1755
+ if (topic.agentSessionId) {
1756
+ turns = this.sessionManager.getSessionFileInfo(topic.projectPath, topic.agentSessionId, topic.baseagent).turns;
1757
+ }
1758
+ const data = {
1759
+ threadId: topic.threadId,
1760
+ name: topic.name || null,
1761
+ agentSessionId: topic.agentSessionId || null,
1762
+ status: isProcessing ? 'processing' : 'idle',
1763
+ createdAt: topic.createdAt,
1764
+ updatedAt: topic.updatedAt,
1765
+ };
1766
+ if (processingDuration !== undefined)
1767
+ data.processingDuration = processingDuration;
1768
+ if (queueLength > 0)
1769
+ data.queueLength = queueLength;
1770
+ if (turns > 0)
1771
+ data.turns = turns;
1772
+ if (health.lastSuccessTime)
1773
+ data.lastSuccess = health.lastSuccessTime;
1774
+ if (health.consecutiveErrors)
1775
+ data.consecutiveErrors = health.consecutiveErrors;
1776
+ if (health.lastError)
1777
+ data.lastError = { type: health.lastErrorType || 'unknown', message: health.lastError.substring(0, 100) };
1778
+ return { data };
1779
+ }
1780
+ if (cmdBase === '/baseagent') {
1781
+ const requestedAgent = typeof args?.aid === 'string'
1782
+ ? (this.agentRegistry?.get?.(args.aid) ?? null)
1783
+ : null;
1784
+ const targetAgent = requestedAgent ?? evolagent;
1785
+ const value = targetAgent?.baseagent ?? targetAgent?.config?.active_baseagent ?? null;
1786
+ const source = value == null
1787
+ ? null
1788
+ : targetAgent?.aid
1789
+ ? menuConfigSource('active_baseagent', { self: targetAgent.aid }) ?? 'agent'
1790
+ : 'agent';
1791
+ return { data: { baseagent: value, scope: 'agent', source } };
1792
+ }
1793
+ if (cmdBase === '/model') {
1794
+ const target = resolveMenuModelTarget.call(this, {
1795
+ args,
1796
+ session,
1797
+ channel,
1798
+ channelId,
1799
+ userId,
1800
+ role: identity.role,
1801
+ explicitChatType,
1802
+ fromControlChannel,
1803
+ field: 'model',
1804
+ });
1805
+ if ('error' in target)
1806
+ return target;
1807
+ const agent = this.getAgent(channel, target.baseagent);
1808
+ const current = readMenuModel(target, agent);
1809
+ return {
1810
+ data: {
1811
+ model: current.value,
1812
+ baseagent: target.baseagent,
1813
+ source: current.source,
1814
+ scope: target.scope,
1815
+ field: target.fieldPath,
1816
+ self: target.sel.self,
1817
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
1818
+ },
1819
+ };
1820
+ }
1821
+ if (cmdBase === '/effort') {
1822
+ const target = resolveMenuModelTarget.call(this, {
1823
+ args,
1824
+ session,
1825
+ channel,
1826
+ channelId,
1827
+ userId,
1828
+ role: identity.role,
1829
+ explicitChatType,
1830
+ fromControlChannel,
1831
+ field: 'effort',
1832
+ });
1833
+ if ('error' in target)
1834
+ return target;
1835
+ const agent = this.getAgent(channel, target.baseagent);
1836
+ const current = readMenuModel(target, agent);
1837
+ return {
1838
+ data: {
1839
+ effort: current.value,
1840
+ baseagent: target.baseagent,
1841
+ source: current.source,
1842
+ scope: target.scope,
1843
+ field: target.fieldPath,
1844
+ self: target.sel.self,
1845
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
1846
+ },
1847
+ };
1848
+ }
1849
+ if (cmdBase === '/chatmode') {
1850
+ const target = resolveMenuChatmodeTarget.call(this, {
1851
+ args,
1852
+ session,
1853
+ channel,
1854
+ channelId,
1855
+ userId,
1856
+ role: identity.role,
1857
+ explicitChatType,
1858
+ fromControlChannel,
1859
+ });
1860
+ if ('error' in target)
1861
+ return target;
1862
+ const current = readMenuChatmode(target);
1863
+ return {
1864
+ data: {
1865
+ mode: current.value,
1866
+ source: current.source,
1867
+ scope: target.scope,
1868
+ field: target.fieldPath,
1869
+ self: target.sel.self,
1870
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
1871
+ },
1872
+ };
1873
+ }
1874
+ if (cmdBase === '/mentionmode') {
1875
+ const chatType = session?.chatType ?? explicitChatType ?? 'private';
1876
+ if (chatType !== 'group') {
1877
+ return { error: 'mentionMode 仅在群聊会话中有效', code: 'NOT_APPLICABLE' };
1878
+ }
1879
+ const target = resolveMenuMentionModeTarget.call(this, {
1880
+ args,
1881
+ session,
1882
+ channel,
1883
+ channelId,
1884
+ userId,
1885
+ role: identity.role,
1886
+ explicitChatType,
1887
+ fromControlChannel,
1888
+ });
1889
+ if ('error' in target)
1890
+ return target;
1891
+ // session metadata 是 AUN 协议词汇,翻译成 mentionMode 词汇;evolagent.config 已是 mentionMode 字段
1892
+ const fallback = dispatchToMentionMode(session?.metadata?.dispatchMode) ?? null;
1893
+ const current = readMenuMentionMode(target, fallback, fallback === null ? null : 'session');
1894
+ return {
1895
+ data: {
1896
+ mode: current.value,
1897
+ source: current.source,
1898
+ scope: target.scope,
1899
+ field: target.fieldPath,
1900
+ self: target.sel.self,
1901
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
1902
+ },
1903
+ };
1904
+ }
1905
+ if (cmdBase === '/observable') {
1906
+ if (identity.role !== 'owner')
1907
+ return { error: '观察者模式仅限 owner 查看', code: 'NO_PERMISSION' };
1908
+ if (!evolagent)
1909
+ return { error: '找不到通道所属 agent', code: 'MISSING_AID' };
1910
+ const observable = evolagent?.getObservable() ?? false;
1911
+ const source = evolagent?.aid
1912
+ ? menuConfigSource('observable', { self: evolagent.aid }) ?? 'builtin'
1913
+ : 'builtin';
1914
+ return { data: { observable, source } };
1915
+ }
1916
+ if (cmdBase === '/perm') {
1917
+ const target = resolveMenuPermissionTarget.call(this, {
1918
+ args,
1919
+ session,
1920
+ channel,
1921
+ channelId,
1922
+ userId,
1923
+ role: identity.role,
1924
+ explicitChatType,
1925
+ fromControlChannel,
1926
+ });
1927
+ if ('error' in target)
1928
+ return target;
1929
+ const current = readMenuPermission(target);
1930
+ return {
1931
+ data: {
1932
+ mode: current.value,
1933
+ source: current.source,
1934
+ scope: target.scope,
1935
+ role: target.role,
1936
+ field: target.fieldPath,
1937
+ self: target.sel.self,
1938
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
1939
+ },
1940
+ };
1941
+ }
1942
+ if (cmdBase === '/activity') {
1943
+ const target = resolveMenuActivityTarget.call(this, {
1944
+ args,
1945
+ session,
1946
+ channel,
1947
+ channelId,
1948
+ userId,
1949
+ role: identity.role,
1950
+ explicitChatType,
1951
+ fromControlChannel,
1952
+ });
1953
+ if ('error' in target)
1954
+ return target;
1955
+ const fallback = this.agentRegistry?.getShowActivities?.(channel) ?? 'all';
1956
+ const current = readMenuActivity(target, fallback);
1957
+ return {
1958
+ data: {
1959
+ mode: current.value,
1960
+ source: current.source,
1961
+ scope: target.scope,
1962
+ field: target.fieldPath,
1963
+ self: target.sel.self,
1964
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
1965
+ },
1966
+ };
1967
+ }
1968
+ if (cmdBase === '/system') {
1969
+ if (!subject.isDaemonOwner) {
1970
+ return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
1971
+ }
1972
+ const owningAgent = this.getOwningAgent(channel);
1973
+ const data = {
1974
+ aid: loadDaemonConfig().aid ?? null,
1975
+ pid: process.pid,
1976
+ node: process.version,
1977
+ uptime: Math.floor(process.uptime()),
1978
+ };
1979
+ try {
1980
+ const pkgPath = path.join(getPackageRoot(), 'package.json');
1981
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
1982
+ if (pkg?.version)
1983
+ data.version = pkg.version;
1984
+ }
1985
+ catch { }
1986
+ try {
1987
+ const fp = path.join(getPackageRoot(), 'node_modules', '@agentunion', 'fastaun', 'package.json');
1988
+ const fp2 = JSON.parse(fs.readFileSync(fp, 'utf-8'));
1989
+ if (fp2?.version)
1990
+ data.fastaunVersion = fp2.version;
1991
+ }
1992
+ catch { }
1993
+ // ecweb 是独立全局包,读其已装版本(与 /upgrade 一致)
1994
+ const ecwebPkg = resolveGlobalPkg(WEB_PACKAGE_NAME);
1995
+ if (ecwebPkg?.version)
1996
+ data.ecwebVersion = ecwebPkg.version;
1997
+ const channels = owningAgent?.channelInstanceNames?.() ?? [];
1998
+ if (channels.length) {
1999
+ // 将 channelKey 字符串(如 "feishu#aid#name")解析为对象 { type, instName }
2000
+ data.channels = channels.map((key) => {
2001
+ const parts = key.split('#');
2002
+ return { type: parts[0], instName: parts[2] || parts[0] };
2003
+ });
2004
+ }
2005
+ // 收集主机级可用的所有 baseagent 类型(检测已安装的 CLI + 版本)
2006
+ const baseagents = [];
2007
+ for (const cmd of ['claude', 'gemini', 'codex']) {
2008
+ if (commandExists(cmd)) {
2009
+ baseagents.push({ name: cmd, version: getBaseagentVersion(cmd) });
2010
+ }
2011
+ }
2012
+ data.baseagents = baseagents;
2013
+ return { data };
2014
+ }
2015
+ // ── name=file:文件元信息(§5.1) ──
2016
+ // 权限(§6.3):agent owner/admin 或 aid channel owner。项目外文件仅 owner 可取(§6.2,由 resolveMenuFilePath 校验)。
2017
+ if (cmdBase === '/file') {
2018
+ const role = identity.role;
2019
+ if (!isManagementRole(role)) {
2020
+ return { error: '无权限', code: 'NO_PERMISSION' };
2021
+ }
2022
+ const resolved = resolveMenuFilePath(args?.path, session, role, 'file');
2023
+ if ('error' in resolved)
2024
+ return resolved;
2025
+ const { realPath, stat } = resolved;
2026
+ // sha256 仅对 ≤ 2 MB 文件计算,超过返回 null,客户端降级到 size+mtime(§4、§7 决策 2)
2027
+ let sha256 = null;
2028
+ if (stat.size <= FILE_HASH_MAX_SIZE) {
2029
+ try {
2030
+ sha256 = crypto.createHash('sha256').update(fs.readFileSync(realPath)).digest('hex');
2031
+ }
2032
+ catch {
2033
+ sha256 = null;
2034
+ }
2035
+ }
2036
+ return {
2037
+ data: {
2038
+ path: (args?.path ?? '').toString(),
2039
+ sha256,
2040
+ size: stat.size,
2041
+ mtime: stat.mtimeMs,
2042
+ },
2043
+ };
2044
+ }
2045
+ return { error: `不支持 query: ${cmdBase}`, code: 'NOT_SUPPORTED' };
2046
+ }
2047
+ /** menu.update — 写入新值。 */
2048
+ export async function execMenuUpdate(cmd, value, channel, channelId, userId, overrideIdentity, fromControlChannel = false, args, authSubject, source = 'menu') {
2049
+ const cmdBase = cmd.trim().split(' ')[0];
2050
+ if (!cmdBase)
2051
+ return { error: '缺少命令', code: 'MISSING_CMD' };
2052
+ if (cmdBase === '/perm') {
2053
+ return { error: 'permissionMode 只能通过角色策略编辑器修改', code: 'ROLE_POLICY_MANAGED' };
2054
+ }
2055
+ const scopeError = validateConfigWriteScope(cmdBase, args);
2056
+ if (scopeError)
2057
+ return { error: scopeError.message, code: scopeError.code };
2058
+ args = withoutLegacySessionConfigArgs(args);
2059
+ const gated = gateControlScope.call(this, { cmdBase, args, channel, fromControlChannel });
2060
+ if (gated)
2061
+ return gated;
2062
+ const arg = value.trim();
2063
+ if (!arg)
2064
+ return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
2065
+ const { session, evolagent } = await this.loadMenuContext(channel, channelId);
2066
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
2067
+ const isAdmin = isManagementRole(identity.role);
2068
+ const subject = buildMenuAuthSubject(this, {
2069
+ identity,
2070
+ session,
2071
+ channel,
2072
+ channelId,
2073
+ userId,
2074
+ fromControlChannel,
2075
+ subject: authSubject,
2076
+ });
2077
+ if (cmdBase === '/role') {
2078
+ const authorized = await authorizeRoleMenu(this, {
2079
+ kind: 'update', args, value: arg, identity, subject: authSubject, session, channel, channelId,
2080
+ userId, fromControlChannel, source,
2081
+ });
2082
+ if ('error' in authorized)
2083
+ return authorized;
2084
+ try {
2085
+ return { data: await roleMenuUpdate(authorized.context, args, arg) };
2086
+ }
2087
+ catch (error) {
2088
+ return menuResultFailure(error);
2089
+ }
2090
+ }
2091
+ const authDenied = await authorizeMenuIntent.call(this, {
2092
+ intent: buildMenuIntent('update', cmdBase, args, undefined, arg, fromControlChannel),
2093
+ identity,
2094
+ subject,
2095
+ session,
2096
+ channel,
2097
+ channelId,
2098
+ userId,
2099
+ fromControlChannel,
2100
+ source,
2101
+ });
2102
+ if (authDenied)
2103
+ return authDenied;
2104
+ // ── 进程级 /gateway update(网关配置,value 为 JSON:{scope,type,patch}) ──
2105
+ if (cmdBase === '/gateway') {
2106
+ let body;
2107
+ try {
2108
+ body = JSON.parse(arg);
2109
+ }
2110
+ catch {
2111
+ return { error: 'value 需为 JSON', code: 'INVALID_ARGS' };
2112
+ }
2113
+ return await gatewayUpdate(body);
2114
+ }
2115
+ // ── 关系级 /trigger update(调度参数,value 为 JSON 字符串) ──
2116
+ if (cmdBase === '/trigger') {
2117
+ let patch;
2118
+ try {
2119
+ patch = JSON.parse(arg);
2120
+ }
2121
+ catch {
2122
+ return { error: 'value 需为 JSON', code: 'INVALID_ARGS' };
2123
+ }
2124
+ if (!patch?.nameOrId)
2125
+ return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
2126
+ const isAdmin = isManagementRole(identity.role);
2127
+ if (!isAdmin && !userId)
2128
+ return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
2129
+ const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
2130
+ if (!triggerScheduler)
2131
+ return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
2132
+ const updated = await this.updateTriggerFromPatch(triggerScheduler, patch.nameOrId, patch, channel, channelId, userId ?? '', isAdmin);
2133
+ if (!updated.ok) {
2134
+ return {
2135
+ error: updated.error,
2136
+ code: updated.code ?? (/不存在|无权限/.test(updated.error) ? 'NOT_FOUND' : 'INVALID_ARGS'),
2137
+ ...(updated.currentRevision ? { data: { currentRevision: updated.currentRevision } } : {}),
2138
+ };
2139
+ }
2140
+ return { data: { id: updated.trigger.id, nextFireAt: updated.trigger.nextFireAt, revision: updated.revision } };
2141
+ }
2142
+ if (cmdBase === '/capability') {
2143
+ const type = args?.type;
2144
+ if (!isCapabilityType(type)) {
2145
+ return { error: 'args.type 必须是 skill / mcp / plugin', code: type ? 'INVALID_TYPE' : 'INVALID_ARGS' };
2146
+ }
2147
+ const name = (args?.name ?? '').toString().trim();
2148
+ if (name) {
2149
+ if (!isAdmin)
2150
+ return { error: '无权限', code: 'NO_PERMISSION' };
2151
+ }
2152
+ else if (identity.role !== 'owner') {
2153
+ return { error: '类型级能力策略仅 owner 可修改', code: 'NO_PERMISSION' };
2154
+ }
2155
+ const target = resolveCapabilityTarget.call(this, { channel, args, session, evolagent, fromControlChannel });
2156
+ if ('error' in target)
2157
+ return target;
2158
+ const support = queryCapabilityTypes(target.ctx, target.config, type).capabilities[type];
2159
+ if (!support?.canUpdate) {
2160
+ return { error: support?.reason || '当前 baseagent 不支持 capability 更新', code: 'NOT_SUPPORTED' };
2161
+ }
2162
+ if (name) {
2163
+ const options = await listCapabilityOptions(target.ctx, target.config, type);
2164
+ if (!options.some(item => item.value === name)) {
2165
+ return { error: `未发现能力: ${name}`, code: 'NOT_FOUND' };
2166
+ }
2167
+ }
2168
+ try {
2169
+ const data = updateCapabilityPolicy(target.ctx.aid, target.ctx.baseagent, type, arg, name || undefined);
2170
+ return { data };
2171
+ }
2172
+ catch (e) {
2173
+ return { error: e?.message || String(e), code: e?.code || 'EXEC_FAILED' };
2174
+ }
2175
+ }
2176
+ if (cmdBase === '/baseagent') {
2177
+ if (identity.role !== 'owner')
2178
+ return { error: '无权限', code: 'NO_PERMISSION' };
2179
+ if (!evolagent)
2180
+ return { error: '当前 channel 无绑定 agent,无法设置 active_baseagent', code: 'EXEC_FAILED' };
2181
+ const valid = this.getAvailableBaseagents(channel);
2182
+ if (valid.length && !valid.includes(arg)) {
2183
+ return { error: `无效 baseagent: ${arg},可选: ${valid.join(' / ')}`, code: 'INVALID_VALUE' };
2184
+ }
2185
+ const previousBaseagent = evolagent.baseagent;
2186
+ evolagent.setActiveBaseagent(arg);
2187
+ this.eventBus.publish({
2188
+ type: 'agent:baseagent-changed',
2189
+ aid: evolagent.aid,
2190
+ baseagent: arg,
2191
+ previousBaseagent,
2192
+ scope: 'agent',
2193
+ timestamp: Date.now(),
2194
+ });
2195
+ return { data: { baseagent: arg, scope: 'agent' } };
2196
+ }
2197
+ if (cmdBase === '/model') {
2198
+ const target = resolveMenuModelTarget.call(this, {
2199
+ args,
2200
+ session,
2201
+ channel,
2202
+ channelId,
2203
+ userId,
2204
+ role: identity.role,
2205
+ fromControlChannel,
2206
+ field: 'model',
2207
+ });
2208
+ if ('error' in target)
2209
+ return target;
2210
+ const agent = this.getAgent(channel, target.baseagent);
2211
+ let targetModel = arg;
2212
+ if (hasModelSwitcher(agent)) {
2213
+ const models = (await agent.listModels?.()) ?? [];
2214
+ const decision = validateModelSelectionForRole({
2215
+ role: identity.role,
2216
+ baseagent: target.baseagent,
2217
+ requestedModel: arg,
2218
+ models,
2219
+ resolveModelId: agent.resolveModelId?.bind(agent),
2220
+ });
2221
+ if (!decision.ok)
2222
+ return { error: decision.message || `invalid model: ${arg}`, code: decision.code || 'INVALID_VALUE' };
2223
+ targetModel = decision.model || arg;
2224
+ if (models.length && !models.includes(targetModel)) {
2225
+ return { error: `无效模型: ${arg}`, code: 'INVALID_VALUE' };
2226
+ }
2227
+ }
2228
+ try {
2229
+ writeMenuModel(target, targetModel);
2230
+ }
2231
+ catch (e) {
2232
+ return { error: e?.message || String(e), code: e?.code || 'CONFIG_WRITE_FAILED', ...(e?.data !== undefined ? { data: e.data } : {}) };
2233
+ }
2234
+ this.eventBus.publish({
2235
+ type: 'runner:model-changed',
2236
+ sessionId: session?.id,
2237
+ agentName: evolagent?.name,
2238
+ baseagent: target.baseagent,
2239
+ model: targetModel,
2240
+ timestamp: Date.now(),
2241
+ });
2242
+ return {
2243
+ data: {
2244
+ model: targetModel,
2245
+ baseagent: target.baseagent,
2246
+ scope: target.scope,
2247
+ field: target.fieldPath,
2248
+ self: target.sel.self,
2249
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
2250
+ },
2251
+ };
2252
+ }
2253
+ if (cmdBase === '/effort') {
2254
+ const target = resolveMenuModelTarget.call(this, {
2255
+ args,
2256
+ session,
2257
+ channel,
2258
+ channelId,
2259
+ userId,
2260
+ role: identity.role,
2261
+ fromControlChannel,
2262
+ field: 'effort',
2263
+ });
2264
+ if ('error' in target)
2265
+ return target;
2266
+ const agent = this.getAgent(channel, target.baseagent);
2267
+ const modelTarget = { ...target, field: 'model', fieldPath: `baseagents.${target.baseagent}.model` };
2268
+ const currentModel = hasModelSwitcher(agent) ? (readMenuModel(modelTarget, agent).value || agent.getModel()) : agent.name;
2269
+ const validEfforts = getAvailableEfforts(agent, currentModel);
2270
+ const allValid = [...validEfforts, 'auto'];
2271
+ if (!allValid.includes(arg)) {
2272
+ return { error: `无效推理强度: ${arg},可选: ${allValid.join(' / ')}`, code: 'INVALID_VALUE' };
2273
+ }
2274
+ const roleDecision = validateRuntimeStringFieldOverride({
2275
+ selfAid: target.sel.self,
2276
+ role: target.role,
2277
+ field: menuModelConfigFieldPath(target),
2278
+ value: arg,
2279
+ });
2280
+ if (!roleDecision.ok) {
2281
+ return { error: roleDecision.message || '当前角色不允许修改推理强度', code: roleDecision.code || 'ROLE_VALUE_NOT_ALLOWED' };
2282
+ }
2283
+ try {
2284
+ writeMenuModel(target, arg === 'auto' ? null : arg);
2285
+ }
2286
+ catch (e) {
2287
+ return { error: e?.message || String(e), code: e?.code || 'CONFIG_WRITE_FAILED', ...(e?.data !== undefined ? { data: e.data } : {}) };
2288
+ }
2289
+ this.eventBus.publish({
2290
+ type: 'runner:model-changed',
2291
+ sessionId: session?.id,
2292
+ agentName: evolagent?.name,
2293
+ baseagent: target.baseagent,
2294
+ effort: arg,
2295
+ timestamp: Date.now(),
2296
+ });
2297
+ return {
2298
+ data: {
2299
+ effort: arg,
2300
+ baseagent: target.baseagent,
2301
+ scope: target.scope,
2302
+ field: target.fieldPath,
2303
+ self: target.sel.self,
2304
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
2305
+ },
2306
+ };
2307
+ }
2308
+ if (cmdBase === '/chatmode') {
2309
+ if (arg !== 'interactive' && arg !== 'proactive') {
2310
+ return { error: `无效模式: ${arg}`, code: 'INVALID_VALUE' };
2311
+ }
2312
+ const target = resolveMenuChatmodeTarget.call(this, {
2313
+ args,
2314
+ session,
2315
+ channel,
2316
+ channelId,
2317
+ userId,
2318
+ role: identity.role,
2319
+ fromControlChannel,
2320
+ });
2321
+ if ('error' in target)
2322
+ return target;
2323
+ const roleDecision = validateRuntimeStringFieldOverride({
2324
+ selfAid: target.sel.self,
2325
+ role: target.role,
2326
+ field: target.fieldPath,
2327
+ value: arg,
2328
+ });
2329
+ if (!roleDecision.ok) {
2330
+ return { error: roleDecision.message || '当前角色不允许修改对话模式', code: roleDecision.code || 'ROLE_VALUE_NOT_ALLOWED' };
2331
+ }
2332
+ try {
2333
+ writeMenuChatmode(target, arg);
2334
+ }
2335
+ catch (e) {
2336
+ return { error: e?.message || String(e), code: e?.code || 'CONFIG_WRITE_FAILED', ...(e?.data !== undefined ? { data: e.data } : {}) };
2337
+ }
2338
+ return {
2339
+ data: {
2340
+ mode: arg,
2341
+ scope: target.scope,
2342
+ field: target.fieldPath,
2343
+ self: target.sel.self,
2344
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
2345
+ },
2346
+ };
2347
+ }
2348
+ if (cmdBase === '/mentionmode') {
2349
+ if (arg !== 'mention-only' && arg !== 'disabled' && arg !== 'clear') {
2350
+ return { error: `无效模式: ${arg}`, code: 'INVALID_VALUE' };
2351
+ }
2352
+ const chatType = session?.chatType;
2353
+ if (!session || chatType !== 'group') {
2354
+ return { error: 'mentionMode 仅在群聊会话中有效', code: 'NOT_APPLICABLE' };
2355
+ }
2356
+ const target = resolveMenuMentionModeTarget.call(this, {
2357
+ args,
2358
+ session,
2359
+ channel,
2360
+ channelId,
2361
+ userId,
2362
+ role: identity.role,
2363
+ fromControlChannel,
2364
+ });
2365
+ if ('error' in target)
2366
+ return target;
2367
+ try {
2368
+ writeMenuMentionMode(target, arg === 'clear' ? null : arg);
2369
+ }
2370
+ catch (e) {
2371
+ return { error: e?.message || String(e), code: e?.code || 'CONFIG_WRITE_FAILED', ...(e?.data !== undefined ? { data: e.data } : {}) };
2372
+ }
2373
+ return {
2374
+ data: {
2375
+ mode: arg === 'clear' ? null : arg,
2376
+ scope: target.scope,
2377
+ field: target.fieldPath,
2378
+ self: target.sel.self,
2379
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
2380
+ },
2381
+ };
2382
+ }
2383
+ if (cmdBase === '/activity') {
2384
+ const modeMap = { all: 'all', text: 'text', none: 'none' };
2385
+ const newMode = modeMap[arg];
2386
+ if (!newMode)
2387
+ return { error: `无效模式: ${arg},可选: all / text / none`, code: 'INVALID_VALUE' };
2388
+ if (identity.role !== 'owner')
2389
+ return { error: '中间输出模式切换仅限 owner', code: 'NO_PERMISSION' };
2390
+ const target = resolveMenuActivityTarget.call(this, {
2391
+ args,
2392
+ session,
2393
+ channel,
2394
+ channelId,
2395
+ userId,
2396
+ role: identity.role,
2397
+ fromControlChannel,
2398
+ });
2399
+ if ('error' in target)
2400
+ return target;
2401
+ try {
2402
+ writeMenuActivity(target, newMode);
2403
+ }
2404
+ catch (e) {
2405
+ return { error: e?.message || String(e), code: e?.code || 'CONFIG_WRITE_FAILED', ...(e?.data !== undefined ? { data: e.data } : {}) };
2406
+ }
2407
+ return {
2408
+ data: {
2409
+ mode: newMode,
2410
+ scope: target.scope,
2411
+ field: target.fieldPath,
2412
+ self: target.sel.self,
2413
+ ...(target.sel.peerKey ? { peerKey: target.sel.peerKey } : {}),
2414
+ },
2415
+ };
2416
+ }
2417
+ if (cmdBase === '/observable') {
2418
+ if (identity.role !== 'owner')
2419
+ return { error: '观察者模式仅限 owner 开关', code: 'NO_PERMISSION' };
2420
+ if (arg !== 'true' && arg !== 'false')
2421
+ return { error: `无效值: ${arg},可选: true / false`, code: 'INVALID_VALUE' };
2422
+ if (!evolagent)
2423
+ return { error: '找不到通道所属 agent,无法持久化', code: 'MISSING_AID' };
2424
+ evolagent.setObservable(arg === 'true');
2425
+ return { data: { observable: arg === 'true' } };
2426
+ }
2427
+ return { error: `不支持 update: ${cmdBase}`, code: 'NOT_SUPPORTED' };
2428
+ }
2429
+ /** menu.action — 触发动词。 */
2430
+ export async function execMenuAction(cmd, action, args, channel, channelId, userId, overrideIdentity, explicitChatType, requestId, fromControlChannel = false, authSubject, source = 'menu') {
2431
+ const cmdBase = cmd.trim().split(' ')[0];
2432
+ if (!cmdBase)
2433
+ return { error: '缺少命令', code: 'MISSING_CMD' };
2434
+ if (!action)
2435
+ return { error: '缺少 action', code: 'MISSING_VALUE' };
2436
+ const gated = gateControlScope.call(this, { cmdBase, action, args, channel, fromControlChannel });
2437
+ if (gated)
2438
+ return gated;
2439
+ const { session: authSession } = await this.loadMenuContext(channel, channelId);
2440
+ const authIdentity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(authSession));
2441
+ const subject = buildMenuAuthSubject(this, {
2442
+ identity: authIdentity,
2443
+ session: authSession,
2444
+ explicitChatType,
2445
+ channel,
2446
+ channelId,
2447
+ userId,
2448
+ fromControlChannel,
2449
+ subject: authSubject,
2450
+ });
2451
+ if (cmdBase === '/system' && fromControlChannel && !subject.isDaemonOwner) {
2452
+ return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
2453
+ }
2454
+ if (cmdBase === '/role') {
2455
+ const authorized = await authorizeRoleMenu(this, {
2456
+ kind: 'action', args, identity: authIdentity, subject, session: authSession,
2457
+ channel, channelId, userId, fromControlChannel, source,
2458
+ });
2459
+ if ('error' in authorized)
2460
+ return authorized;
2461
+ try {
2462
+ return { data: await roleMenuAction(authorized.context, action, args) };
2463
+ }
2464
+ catch (error) {
2465
+ return menuResultFailure(error);
2466
+ }
2467
+ }
2468
+ if (cmdBase === '/agent' && !fromControlChannel && !args?.aid) {
2469
+ const selfAid = this.getOwningAgent?.(channel)?.aid;
2470
+ if (selfAid)
2471
+ args = { ...(args ?? {}), aid: selfAid };
2472
+ }
2473
+ const authDenied = await authorizeMenuIntent.call(this, {
2474
+ intent: buildMenuIntent('action', cmdBase, args, action, undefined, fromControlChannel),
2475
+ identity: authIdentity,
2476
+ subject,
2477
+ session: authSession,
2478
+ explicitChatType,
2479
+ channel,
2480
+ channelId,
2481
+ userId,
2482
+ fromControlChannel,
2483
+ source,
2484
+ });
2485
+ if (authDenied)
2486
+ return authDenied;
2487
+ // ── /gateway action(进程级,gate 已确保 fromControlChannel) ──
2488
+ // test=连通性测试;delete=删除配置;models=模型+价格;set-price=改网关价格;sync-env=同步环境变量。
2489
+ if (cmdBase === '/gateway') {
2490
+ if (action === 'test')
2491
+ return await gatewayTest(args);
2492
+ if (action === 'delete')
2493
+ return await gatewayDelete(args);
2494
+ if (action === 'models')
2495
+ return await gatewayModels(args);
2496
+ if (action === 'set-price')
2497
+ return gatewaySetPrice(args);
2498
+ if (action === 'sync-env')
2499
+ return await gatewaySyncEnv(args);
2500
+ return { error: `不支持 gateway action: ${action}`, code: 'NOT_SUPPORTED' };
2501
+ }
2502
+ const { session } = await this.loadMenuContext(channel, channelId);
2503
+ const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
2504
+ const isMenuAdmin = isManagementRole(identity.role);
2505
+ // ── /agent action ──
2506
+ // 控制 channel:验 evolcore.owners,可执行进程级(create/delete/enable/disable)+ 自管理(update/reload)。
2507
+ // agent channel:闸门已挡掉进程级 + 跨 agent,仅 update/reload 能到此;reload 允许 owner/admin,update 仅 owner。
2508
+ if (cmdBase === '/agent') {
2509
+ if (fromControlChannel) {
2510
+ if (!subject.isDaemonOwner) {
2511
+ return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
2512
+ }
2513
+ }
2514
+ else {
2515
+ // 本 agent 自管理:reload 允许 owner/admin;update 会改鉴权相关元配置,仍仅 owner。
2516
+ const isAgentOwner = identity.role === 'owner';
2517
+ const isAgentAdmin = isManagementRole(identity.role);
2518
+ if (action === 'update' ? !isAgentOwner : !isAgentAdmin) {
2519
+ return { error: action === 'update' ? '本 agent 配置更新仅 owner 可执行' : '本 agent 自管理操作仅 owner/admin 可执行', code: 'FORBIDDEN' };
2520
+ }
2521
+ const selfAid = this.getOwningAgent?.(channel)?.aid;
2522
+ if (!selfAid)
2523
+ return { error: '当前 channel 无绑定 agent', code: 'FORBIDDEN' };
2524
+ args = { ...(args ?? {}), aid: selfAid };
2525
+ }
2526
+ const a = { ...(args ?? {}) };
2527
+ if (action === 'create') {
2528
+ a.project = resolveProjectPath(a.project, a.aid ?? '', loadDefaults());
2529
+ }
2530
+ // queue-clear:清空指定 agent 的待处理消息(不影响处理中),直接走 messageQueue。
2531
+ if (action === 'queue-clear') {
2532
+ if (!a.aid)
2533
+ return { error: '缺少 aid', code: 'INVALID_ARGS' };
2534
+ const handle = this.agentRegistry?.get(a.aid) ?? null;
2535
+ const agentName = handle?.name;
2536
+ if (!agentName)
2537
+ return { error: `未找到 Agent: ${a.aid}`, code: 'NOT_FOUND' };
2538
+ const cleared = this.messageQueue.clearByAgent(agentName);
2539
+ return { data: { cleared } };
2540
+ }
2541
+ // mute / unmute:禁言/解禁。禁言后消息照常入队但不消费,解禁后恢复。
2542
+ if (action === 'mute' || action === 'unmute') {
2543
+ if (!a.aid)
2544
+ return { error: '缺少 aid', code: 'INVALID_ARGS' };
2545
+ const handle = this.agentRegistry?.get(a.aid) ?? null;
2546
+ const agentName = handle?.name;
2547
+ if (!agentName)
2548
+ return { error: `未找到 Agent: ${a.aid}`, code: 'NOT_FOUND' };
2549
+ if (action === 'mute')
2550
+ this.messageQueue.muteAgent(agentName);
2551
+ else
2552
+ this.messageQueue.unmuteAgent(agentName);
2553
+ return { data: { aid: a.aid, muted: action === 'mute' } };
2554
+ }
2555
+ // interrupt:只打断当前处理中的任务,不断开 Agent 渠道,也不清空待处理队列。
2556
+ if (action === 'interrupt') {
2557
+ if (!a.aid)
2558
+ return { error: '缺少 aid', code: 'INVALID_ARGS' };
2559
+ const handle = this.agentRegistry?.get(a.aid) ?? null;
2560
+ const agentName = handle?.name;
2561
+ if (!agentName)
2562
+ return { error: `未找到 Agent: ${a.aid}`, code: 'NOT_FOUND' };
2563
+ const interrupted = this.messageQueue.interruptByAgent(agentName);
2564
+ if (interrupted === 0)
2565
+ return { error: '当前没有正在处理的任务', code: 'NO_ACTIVE_TASK' };
2566
+ return { data: { aid: a.aid, action, interrupted } };
2567
+ }
2568
+ // start / stop:运行时连/断渠道,不改 config.enabled。
2569
+ if (action === 'start' || action === 'stop') {
2570
+ if (!a.aid)
2571
+ return { error: '缺少 aid', code: 'INVALID_ARGS' };
2572
+ const hooks = globalThis.__evolcore_reloadHooks;
2573
+ if (!hooks)
2574
+ return { error: 'Reload hooks 未初始化', code: 'INTERNAL' };
2575
+ try {
2576
+ if (action === 'stop') {
2577
+ if (!this.agentRegistry?.stopAgent)
2578
+ return { error: 'stopAgent 不可用', code: 'INTERNAL' };
2579
+ await this.agentRegistry.stopAgent(a.aid, hooks);
2580
+ this.eventBus.publish({ type: 'agent:stopped', aid: a.aid, timestamp: Date.now() });
2581
+ // 中断该 agent 正在执行的大模型调用
2582
+ const handle = this.agentRegistry.get(a.aid);
2583
+ if (handle)
2584
+ this.messageQueue.interruptByAgent(handle.name);
2585
+ }
2586
+ else {
2587
+ if (!this.agentRegistry?.startAgent)
2588
+ return { error: 'startAgent 不可用', code: 'INTERNAL' };
2589
+ await this.agentRegistry.startAgent(a.aid, hooks);
2590
+ this.eventBus.publish({ type: 'agent:started', aid: a.aid, timestamp: Date.now() });
2591
+ }
2592
+ return { data: { aid: a.aid, action } };
2593
+ }
2594
+ catch (e) {
2595
+ this.eventBus.publish({ type: 'agent:error', aid: a.aid, action, error: e?.message || String(e), timestamp: Date.now() });
2596
+ return { error: e?.message || String(e), code: 'INTERNAL' };
2597
+ }
2598
+ }
2599
+ // reload / disable / delete 会中断 agent 正在处理的任务,执行前检查是否繁忙。
2600
+ // 队列按 agent 名计数,故先用 registry 把 aid 解析成 name;force 跳过。
2601
+ if ((action === 'reload' || action === 'disable' || action === 'delete') && a.aid && !a.force) {
2602
+ const handle = this.agentRegistry?.get(a.aid) ?? null;
2603
+ const agentName = handle?.name;
2604
+ if (agentName) {
2605
+ const busy = this.messageQueue.getProcessingCountByAgent(agentName)
2606
+ + this.messageQueue.getQueueLengthByAgent(agentName);
2607
+ if (busy > 0) {
2608
+ return { error: `该 Agent 有 ${busy} 个任务执行中`, code: 'BUSY' };
2609
+ }
2610
+ }
2611
+ }
2612
+ return await execAgentAction(action, a, userId ?? '', this.eventBus);
2613
+ }
2614
+ // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
2615
+ if (cmdBase === '/trigger') {
2616
+ const role = identity.role;
2617
+ const isAdmin = isManagementRole(role);
2618
+ const triggerScheduler = this.getTriggerSchedulerForChannel?.(channel);
2619
+ if (!triggerScheduler)
2620
+ return { error: '触发器功能未启用', code: 'NOT_SUPPORTED' };
2621
+ if (action === 'set') {
2622
+ // args 结构化 → 直接组装 ParsedTriggerSet(绕过 parseTriggerSet 文本解析,无注入风险)
2623
+ if (!args?.scheduleType || (args.scheduleType !== 'once' && !args?.scheduleValue) || !args?.prompt) {
2624
+ return { error: '缺少必填参数:scheduleType / scheduleValue / prompt', code: 'INVALID_ARGS' };
2625
+ }
2626
+ // menu 路径绕过了 parseTriggerSet 的校验,必须自行校验枚举/数值,
2627
+ // 避免非法调度参数进入 scheduler。
2628
+ const schedErr = validateScheduleParams(args.scheduleType, String(args.scheduleValue ?? ''));
2629
+ if (schedErr)
2630
+ return { error: schedErr, code: 'INVALID_ARGS' };
2631
+ const targetSession = args.targetSession ?? args.targetSessionStrategy ?? 'main';
2632
+ if (!['main', 'thread'].includes(targetSession)) {
2633
+ return { error: `无效 targetSession: ${targetSession}`, code: 'INVALID_ARGS' };
2634
+ }
2635
+ const triggerThread = args.triggerThread;
2636
+ if (triggerThread !== undefined && triggerThread !== 'per_run' && triggerThread !== 'by_trigger') {
2637
+ return { error: `无效 triggerThread: ${triggerThread}`, code: 'INVALID_ARGS' };
2638
+ }
2639
+ const parsed = {
2640
+ scheduleType: args.scheduleType,
2641
+ scheduleValue: String(args.scheduleValue ?? ''),
2642
+ executionType: args.executionType ?? 'target_session',
2643
+ feedbackStrategy: args.feedbackStrategy ?? (args.targetChannel ? 'target' : 'origin'),
2644
+ targetSession,
2645
+ prompt: String(args.prompt),
2646
+ name: args.name,
2647
+ targetChannel: args.targetChannel,
2648
+ targetChannelId: args.targetChannelId,
2649
+ targetThreadId: args.targetThreadId,
2650
+ agentId: args.agentId,
2651
+ model: args.model,
2652
+ effort: args.effort,
2653
+ permissionMode: args.permissionMode,
2654
+ triggerThread,
2655
+ };
2656
+ const r = await this.registerTriggerFromParsed(parsed, channel, channelId, userId ?? '', undefined, this.resolveMenuChatType(channel, channelId, explicitChatType), undefined, isAdmin);
2657
+ if (!r.ok)
2658
+ return { error: r.error, code: /已存在|exists|重复/.test(r.error) ? 'CONFLICT' : 'INVALID_ARGS' };
2659
+ return { data: { id: r.trigger.id, name: r.trigger.name, nextFireAt: r.trigger.nextFireAt } };
2660
+ }
2661
+ if (action === 'cancel') {
2662
+ const nameOrId = args?.nameOrId;
2663
+ if (!nameOrId)
2664
+ return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
2665
+ if (!isAdmin && !userId)
2666
+ return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
2667
+ const trigger = this.findTriggerDefinition(triggerScheduler, nameOrId, userId ?? '', channel, isAdmin);
2668
+ if (!trigger)
2669
+ return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
2670
+ const cancelled = triggerScheduler.cancel(trigger.id);
2671
+ this.eventBus.publish({ type: 'trigger:cancelled', triggerId: cancelled.id, name: cancelled.name, by: userId ?? '' });
2672
+ return { data: { id: cancelled.id, cancelled: true } };
2673
+ }
2674
+ if (action === 'enable' || action === 'disable') {
2675
+ const nameOrId = args?.nameOrId;
2676
+ if (!nameOrId)
2677
+ return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
2678
+ if (!isAdmin && !userId)
2679
+ return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
2680
+ const trigger = this.findTriggerDefinition(triggerScheduler, nameOrId, userId ?? '', channel, isAdmin);
2681
+ if (!trigger)
2682
+ return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
2683
+ const updated = triggerScheduler.setEnabled(trigger.id, action === 'enable');
2684
+ return { data: { id: updated.id, enabled: updated.enabled } };
2685
+ }
2686
+ if (action === 'delete') {
2687
+ const nameOrId = args?.nameOrId;
2688
+ if (!nameOrId)
2689
+ return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
2690
+ if (!isAdmin && !userId)
2691
+ return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
2692
+ const trigger = this.findTriggerDefinition(triggerScheduler, nameOrId, userId ?? '', channel, isAdmin);
2693
+ if (!trigger)
2694
+ return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
2695
+ if (trigger.enabled)
2696
+ return { error: '请先禁用触发器再删除', code: 'INVALID_STATE' };
2697
+ const deleted = triggerScheduler.delete(trigger.id);
2698
+ return { data: { id: deleted.id, deleted: true } };
2699
+ }
2700
+ if (action === 'run' || action === 'test') {
2701
+ const nameOrId = args?.nameOrId;
2702
+ if (!nameOrId)
2703
+ return { error: '缺少 nameOrId', code: 'INVALID_ARGS' };
2704
+ if (!isAdmin && !userId)
2705
+ return { error: '无法确认身份,请确保渠道提供发送者 ID', code: 'FORBIDDEN' };
2706
+ const trigger = this.findTriggerDefinition(triggerScheduler, nameOrId, userId ?? '', channel, isAdmin);
2707
+ if (!trigger)
2708
+ return { error: '触发器不存在或无权限', code: 'NOT_FOUND' };
2709
+ if (!trigger.enabled)
2710
+ return { error: '触发器已禁用,不能立即执行', code: 'INVALID_STATE' };
2711
+ let eventPayload;
2712
+ if (action === 'test') {
2713
+ if (trigger.source.type !== 'event') {
2714
+ return { error: '测试执行仅支持 Event Trigger', code: 'INVALID_ARGS' };
2715
+ }
2716
+ if (!args?.eventPayload || typeof args.eventPayload !== 'object' || Array.isArray(args.eventPayload)) {
2717
+ return { error: '模拟事件 Payload 必须是 JSON 对象', code: 'INVALID_ARGS' };
2718
+ }
2719
+ const payloadJson = JSON.stringify(args.eventPayload);
2720
+ if (Buffer.byteLength(payloadJson, 'utf8') > 64 * 1024) {
2721
+ return { error: '模拟事件 Payload 不能超过 64 KiB', code: 'INVALID_ARGS' };
2722
+ }
2723
+ eventPayload = args.eventPayload;
2724
+ }
2725
+ const result = await triggerScheduler.run(trigger.id, {
2726
+ dryRun: action === 'run' && args?.dryRun === true,
2727
+ ...(eventPayload ? { eventPayload } : {}),
2728
+ });
2729
+ return {
2730
+ data: {
2731
+ id: trigger.id,
2732
+ runId: result.runId,
2733
+ status: result.status,
2734
+ reason: result.reason,
2735
+ ...(result.conflictRunId ? { conflictRunId: result.conflictRunId } : {}),
2736
+ },
2737
+ };
2738
+ }
2739
+ return { error: `不支持的 trigger action: ${action}`, code: 'INVALID_ARGS' };
2740
+ }
2741
+ if (cmdBase === '/topic') {
2742
+ if (action === 'fork') {
2743
+ if (!this.canReadTopics(identity.role))
2744
+ return { error: '无权限创建话题分支', code: 'FORBIDDEN' };
2745
+ const targetThreadId = (args?.targetThreadId ?? '').toString().trim();
2746
+ const sourceAssistantMessageId = (args?.sourceAssistantMessageId ?? '').toString().trim();
2747
+ const name = getRenameName(args);
2748
+ if (!targetThreadId)
2749
+ return { error: '缺少 args.targetThreadId', code: 'MISSING_VALUE' };
2750
+ if (targetThreadId.length > 512 || /[\x00-\x1F\x7F]/.test(targetThreadId)) {
2751
+ return { error: 'args.targetThreadId 无效', code: 'INVALID_ARGS' };
2752
+ }
2753
+ if (!sourceAssistantMessageId)
2754
+ return { error: '缺少 args.sourceAssistantMessageId', code: 'MISSING_VALUE' };
2755
+ if (explicitChatType === 'group')
2756
+ return { error: '历史上下文分叉仅支持私聊', code: 'NOT_SUPPORTED' };
2757
+ const existingTopic = await this.sessionManager.getThreadSession(channel, channelId, targetThreadId);
2758
+ if (existingTopic) {
2759
+ const forkSource = existingTopic.metadata?.forkSource;
2760
+ const requestedSourceThreadId = typeof args?.sourceThreadId === 'string' && args.sourceThreadId.trim()
2761
+ ? args.sourceThreadId.trim()
2762
+ : null;
2763
+ if (requestId
2764
+ && forkSource?.requestId === requestId
2765
+ && forkSource.assistantMessageId === sourceAssistantMessageId
2766
+ && forkSource.threadId === requestedSourceThreadId) {
2767
+ return {
2768
+ data: {
2769
+ action: 'fork',
2770
+ success: true,
2771
+ topic: {
2772
+ ...buildSessionPayload(existingTopic, existingTopic.name || ''),
2773
+ threadId: existingTopic.threadId,
2774
+ sourceSessionId: forkSource.sessionId,
2775
+ sourceThreadId: forkSource.threadId,
2776
+ sourceAssistantMessageId: forkSource.assistantMessageId,
2777
+ sourceTurn: forkSource.turn,
2778
+ },
2779
+ },
2780
+ };
2781
+ }
2782
+ return { error: '目标话题已存在', code: 'CONFLICT' };
2783
+ }
2784
+ if (name) {
2785
+ const existingName = await this.sessionManager.getSessionByName?.(channel, channelId, name);
2786
+ if (existingName)
2787
+ return { error: `名称 "${name}" 已存在`, code: 'CONFLICT' };
2788
+ }
2789
+ const source = await resolveTopicForkSource(this.sessionManager, channel, channelId, args?.sourceThreadId);
2790
+ if (!source)
2791
+ return { error: '来源会话不存在', code: 'NOT_FOUND' };
2792
+ if (source.chatType !== 'private')
2793
+ return { error: '历史上下文分叉仅支持私聊', code: 'NOT_SUPPORTED' };
2794
+ if (!source.agentSessionId)
2795
+ return { error: '来源会话暂无对话历史', code: 'INVALID_STATE' };
2796
+ const agent = this.getAgent(channel, source.baseagent);
2797
+ if (!agent.capabilities?.forkAtTurn || !agent.getSessionMessages || !agent.forkSessionAt) {
2798
+ return { error: `当前 Agent (${agent.name}) 不支持按轮次分叉`, code: 'NOT_SUPPORTED' };
2799
+ }
2800
+ const sourceKey = this.getQueueKey(source, channel, channelId);
2801
+ const isBusy = !!source.processingState
2802
+ || this.messageQueue.isProcessing?.(sourceKey)
2803
+ || this.messageQueue.getQueueLength?.(sourceKey) > 0
2804
+ || agent.hasActiveStream(sourceKey);
2805
+ if (isBusy)
2806
+ return { error: '来源会话正在处理消息,请完成后重试', code: 'BUSY' };
2807
+ const targetKey = JSON.stringify([source.channelType || source.channel, source.selfAID || '', channelId, targetThreadId]);
2808
+ if (topicForkTargetsInFlight.has(targetKey))
2809
+ return { error: '目标话题正在创建', code: 'CONFLICT' };
2810
+ topicForkTargetsInFlight.add(targetKey);
2811
+ const releaseLock = this.messageQueue.acquireLock?.(sourceKey) ?? (() => { });
2812
+ try {
2813
+ if (this.messageQueue.isProcessing?.(sourceKey) || agent.hasActiveStream(sourceKey)) {
2814
+ return { error: '来源会话正在处理消息,请完成后重试', code: 'BUSY' };
2815
+ }
2816
+ const messages = await agent.getSessionMessages(source.agentSessionId, source.projectPath);
2817
+ const turns = buildSessionTurnList(messages);
2818
+ const forkTurn = turns.find((turn) => turn.assistantUuid === sourceAssistantMessageId);
2819
+ if (!forkTurn)
2820
+ return { error: '分叉点已失效,请刷新历史后重试', code: 'NOT_FOUND' };
2821
+ const forkedAgentSessionId = await agent.forkSessionAt(source.agentSessionId, source.projectPath, sourceAssistantMessageId, name || undefined);
2822
+ const topic = await this.sessionManager.createForkedThreadSession(source, forkedAgentSessionId, targetThreadId, {
2823
+ name: name || undefined,
2824
+ creatorPeerId: userId,
2825
+ sourceAssistantMessageId,
2826
+ sourceTurn: forkTurn.index,
2827
+ requestId,
2828
+ });
2829
+ await agent.updateSessionMetadata?.(forkedAgentSessionId, {
2830
+ evolcoreSessionId: topic.id,
2831
+ sourceSessionId: source.id,
2832
+ sourceAssistantMessageId,
2833
+ sourceTurn: forkTurn.index,
2834
+ }).catch((error) => {
2835
+ logger.debug(`[MenuHandler] Topic fork metadata sync failed: ${error}`);
2836
+ });
2837
+ this.eventBus.publish({
2838
+ type: 'session:forked',
2839
+ sessionId: topic.id,
2840
+ sourceSessionId: source.id,
2841
+ name: topic.name,
2842
+ threadId: topic.threadId,
2843
+ sourceAssistantMessageId,
2844
+ sourceTurn: forkTurn.index,
2845
+ });
2846
+ return {
2847
+ data: {
2848
+ action: 'fork',
2849
+ success: true,
2850
+ topic: {
2851
+ ...buildSessionPayload(topic, topic.name || ''),
2852
+ threadId: topic.threadId,
2853
+ sourceSessionId: source.id,
2854
+ sourceThreadId: source.threadId || null,
2855
+ sourceAssistantMessageId,
2856
+ sourceTurn: forkTurn.index,
2857
+ },
2858
+ },
2859
+ };
2860
+ }
2861
+ catch (error) {
2862
+ logger.error('[MenuHandler] Topic history fork failed:', error);
2863
+ return { error: error instanceof Error ? error.message : '话题分叉失败', code: 'EXEC_FAILED' };
2864
+ }
2865
+ finally {
2866
+ releaseLock();
2867
+ topicForkTargetsInFlight.delete(targetKey);
2868
+ }
2869
+ }
2870
+ if (action !== 'delete' && action !== 'rename') {
2871
+ return { error: `不支持的 topic action: ${action}`, code: 'NOT_SUPPORTED' };
2872
+ }
2873
+ const target = (args?.target ?? '').toString().trim();
2874
+ if (!target)
2875
+ return { error: '缺少 args.target', code: 'MISSING_VALUE' };
2876
+ const renameName = action === 'rename' ? getRenameName(args) : '';
2877
+ if (action === 'rename' && !renameName)
2878
+ return { error: '缺少 args.name', code: 'MISSING_VALUE' };
2879
+ const topic = await this.sessionManager.getThreadSession(channel, channelId, target);
2880
+ if (!topic)
2881
+ return { error: '话题不存在', code: 'NOT_FOUND' };
2882
+ const chatType = topic.chatType === 'group'
2883
+ ? 'group'
2884
+ : topic.chatType === 'private'
2885
+ ? 'private'
2886
+ : this.resolveMenuChatType(channel, channelId, explicitChatType);
2887
+ if (!this.canDeleteTopic(identity.role, chatType, topic, userId)) {
2888
+ return { error: action === 'rename' ? '无权限重命名话题' : '无权限删除话题', code: 'FORBIDDEN' };
2889
+ }
2890
+ if (action === 'rename') {
2891
+ const newName = renameName;
2892
+ const existing = await this.sessionManager.getSessionByName?.(channel, channelId, newName);
2893
+ if (existing && existing.id !== topic.id) {
2894
+ return { error: `名称 "${newName}" 已存在`, code: 'CONFLICT' };
2895
+ }
2896
+ const oldName = displaySessionTitle(topic.name, topic.threadId || '(未命名)');
2897
+ const success = await this.sessionManager.renameSession(topic.id, newName);
2898
+ if (!success)
2899
+ return { error: '重命名失败', code: 'EXEC_FAILED' };
2900
+ if (topic.agentSessionId) {
2901
+ try {
2902
+ const targetAgent = this.getAgent(channel, topic.baseagent);
2903
+ await targetAgent.setSessionName?.(topic.agentSessionId, newName);
2904
+ }
2905
+ catch { }
2906
+ }
2907
+ this.eventBus.publish({ type: 'session:renamed', sessionId: topic.id, oldName, newName });
2908
+ return {
2909
+ data: {
2910
+ action: 'rename',
2911
+ success: true,
2912
+ topic: {
2913
+ ...buildSessionPayload(topic, newName),
2914
+ threadId: topic.threadId,
2915
+ },
2916
+ },
2917
+ };
2918
+ }
2919
+ const success = await this.sessionManager.unbindSession(topic.id);
2920
+ if (!success)
2921
+ return { error: '删除失败', code: 'DELETE_FAILED' };
2922
+ this.eventBus.publish({ type: 'session:deleted', sessionId: topic.id });
2923
+ const targetAgent = this.getAgent(channel, topic.baseagent);
2924
+ await targetAgent.closeSession?.(topic.id);
2925
+ return { data: { deleted: true } };
2926
+ }
2927
+ // ── /session 系列 ──
2928
+ if (cmdBase === '/session' || cmdBase === '/s') {
2929
+ if (action === 'stop') {
2930
+ if (!session)
2931
+ return { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
2932
+ const sessionKey = this.getQueueKey(session, channel, channelId);
2933
+ const sessionAgent = this.getAgent(channel, session.baseagent);
2934
+ const hasActive = sessionAgent.hasActiveStream(sessionKey);
2935
+ const queueLength = this.messageQueue.getQueueLength(sessionKey);
2936
+ if (queueLength === 0 && !hasActive) {
2937
+ return { error: '当前没有正在处理的任务', code: 'NO_ACTIVE_TASK' };
2938
+ }
2939
+ this.eventBus.publish({
2940
+ type: 'task:interrupted',
2941
+ sessionId: sessionKey,
2942
+ reason: 'stop',
2943
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
2944
+ });
2945
+ await this.processor.interruptSession(sessionKey, 'stop');
2946
+ return { data: { action: 'stop', success: true } };
2947
+ }
2948
+ if (action === 'new') {
2949
+ const name = (args?.name ?? '').toString().trim();
2950
+ return await this.delegateAsAction(action, name ? `/new ${name}` : '/new', channel, channelId, userId, { enrichSession: true, authSubject: subject });
2951
+ }
2952
+ if (action === 'rename') {
2953
+ const newName = getRenameName(args);
2954
+ if (!newName)
2955
+ return { error: '缺少 args.name', code: 'MISSING_VALUE' };
2956
+ const target = (args?.target ?? '').toString().trim();
2957
+ const targetSession = await findMainSessionTarget(this.sessionManager, channel, channelId, target, session);
2958
+ if (!targetSession) {
2959
+ return target
2960
+ ? { error: `会话不存在: ${target}`, code: 'NOT_FOUND' }
2961
+ : { error: '当前无活跃会话', code: 'NO_ACTIVE_SESSION' };
2962
+ }
2963
+ const targetChatType = targetSession.chatType === 'group'
2964
+ ? 'group'
2965
+ : targetSession.chatType === 'private'
2966
+ ? 'private'
2967
+ : this.resolveMenuChatType(channel, channelId, explicitChatType);
2968
+ if (targetChatType === 'group' && !isMenuAdmin) {
2969
+ return { error: '无权限:群聊中仅管理员可重命名会话', code: 'NO_PERMISSION' };
2970
+ }
2971
+ const existing = await this.sessionManager.getSessionByName?.(channel, channelId, newName);
2972
+ if (existing && existing.id !== targetSession.id) {
2973
+ return { error: `名称 "${newName}" 已存在`, code: 'CONFLICT' };
2974
+ }
2975
+ const oldName = displaySessionTitle(targetSession.name, '(未命名)');
2976
+ const success = await this.sessionManager.renameSession(targetSession.id, newName);
2977
+ if (!success)
2978
+ return { error: '重命名失败', code: 'EXEC_FAILED' };
2979
+ if (targetSession.agentSessionId) {
2980
+ try {
2981
+ const targetAgent = this.getAgent(channel, targetSession.baseagent);
2982
+ await targetAgent.setSessionName?.(targetSession.agentSessionId, newName);
2983
+ }
2984
+ catch { }
2985
+ }
2986
+ this.eventBus.publish({ type: 'session:renamed', sessionId: targetSession.id, oldName, newName });
2987
+ return { data: { action: 'rename', success: true, session: buildSessionPayload(targetSession, newName) } };
2988
+ }
2989
+ if (action === 'delete') {
2990
+ const target = (args?.target ?? '').toString().trim();
2991
+ if (!target)
2992
+ return { error: '缺少 args.target', code: 'MISSING_VALUE' };
2993
+ return await this.delegateAsAction(action, `/del ${target}`, channel, channelId, userId, { authSubject: subject });
2994
+ }
2995
+ if (action === 'switch') {
2996
+ const target = (args?.target ?? '').toString().trim();
2997
+ if (!target)
2998
+ return { error: '缺少 args.target', code: 'MISSING_VALUE' };
2999
+ return await this.delegateAsAction(action, `/s ${target}`, channel, channelId, userId, { enrichSession: true, authSubject: subject });
3000
+ }
3001
+ if (action === 'compact') {
3002
+ const need = this.requireSession(session);
3003
+ if (need)
3004
+ return need;
3005
+ return await this.delegateAsAction(action, '/compact', channel, channelId, userId, { authSubject: subject });
3006
+ }
3007
+ if (action === 'fork') {
3008
+ const need = this.requireSession(session);
3009
+ if (need)
3010
+ return need;
3011
+ const name = (args?.name ?? '').toString().trim();
3012
+ return await this.delegateAsAction(action, name ? `/fork ${name}` : '/fork', channel, channelId, userId, { enrichSession: true, authSubject: subject });
3013
+ }
3014
+ return { error: `不支持的 session action: ${action}`, code: 'NOT_SUPPORTED' };
3015
+ }
3016
+ // ── /system 系列 ──
3017
+ if (cmdBase === '/system') {
3018
+ // D1 迁移:进程级鉴权统一查 daemon.json owners,替代各 action 内联的 identity.role 判断
3019
+ if (!subject.isDaemonOwner) {
3020
+ return { error: '操作需要 owner 权限', code: 'FORBIDDEN' };
3021
+ }
3022
+ if (action === 'restart') {
3023
+ const restartInfo = { channel, channelId, timestamp: Date.now() };
3024
+ const dataDir = resolvePaths().dataDir;
3025
+ fs.mkdirSync(dataDir, { recursive: true });
3026
+ fs.writeFileSync(path.join(dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
3027
+ const { spawn } = await import('child_process');
3028
+ spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
3029
+ detached: true,
3030
+ stdio: 'ignore',
3031
+ env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
3032
+ }).unref();
3033
+ this.eventBus.publish({ type: 'system:restart', channel, channelId });
3034
+ setTimeout(() => { process.kill(process.pid, 'SIGTERM'); }, 1000);
3035
+ return { data: { action: 'restart', success: true } };
3036
+ }
3037
+ if (action === 'check') {
3038
+ const r = await this.delegateAsAction(action, '/check', channel, channelId, userId, { overrideIdentity, authSubject: subject });
3039
+ // delegateAsAction 会把 structured 展开到 data 顶层,并保留 data.structured 兼容旧客户端。
3040
+ return r;
3041
+ }
3042
+ if (action === 'upgrade') {
3043
+ const devMode = isLinkedInstall();
3044
+ const localEvolcore = getLocalVersion();
3045
+ // fastaun 本地版本:从 node_modules 读取(与 menu.query name=system 一致)
3046
+ let localFastaun = null;
3047
+ try {
3048
+ const fp = path.join(getPackageRoot(), 'node_modules', '@agentunion', 'fastaun', 'package.json');
3049
+ localFastaun = JSON.parse(fs.readFileSync(fp, 'utf-8'))?.version ?? null;
3050
+ }
3051
+ catch { }
3052
+ const [evolcoreRemote, fastaunRemote, ecwebRemote] = await Promise.all([
3053
+ checkLatestVersion('evolcore'),
3054
+ checkLatestVersion('@agentunion/fastaun'),
3055
+ checkLatestVersion(WEB_PACKAGE_NAME),
3056
+ ]);
3057
+ const cmp = (local, remote) => !!(local && remote && compareVersions(local, remote) < 0);
3058
+ return {
3059
+ data: {
3060
+ devMode,
3061
+ evolcore: { local: localEvolcore, remote: evolcoreRemote, hasUpdate: cmp(localEvolcore, evolcoreRemote) },
3062
+ fastaun: { local: localFastaun, remote: fastaunRemote, hasUpdate: cmp(localFastaun, fastaunRemote) },
3063
+ // ecweb 本地版本由 ECWeb 进程自身注入(data.ecwebVersion),此处仅给 remote
3064
+ ecweb: { remote: ecwebRemote },
3065
+ },
3066
+ };
3067
+ }
3068
+ return { error: `不支持的 system action: ${action}`, code: 'NOT_SUPPORTED' };
3069
+ }
3070
+ // ── /cli 透传 ──
3071
+ if (cmdBase === '/cli') {
3072
+ if (action !== 'exec')
3073
+ return { error: `不支持的 cli action: ${action}`, code: 'NOT_SUPPORTED' };
3074
+ const argvValidation = Array.isArray(args?.argv)
3075
+ ? validateCliArgv(args.argv)
3076
+ : typeof args?.command === 'string'
3077
+ ? parseLegacyCliCommand(args.command)
3078
+ : { ok: false, reason: 'CLI exec requires args.argv' };
3079
+ if (!argvValidation.ok)
3080
+ return { error: argvValidation.reason, code: 'INVALID_ARGUMENT' };
3081
+ let argv = argvValidation.argv;
3082
+ argv = normalizeCliArgv(argv);
3083
+ const cliAgent = this.getOwningAgent?.(channel);
3084
+ const cliSelfAid = cliAgent?.aid;
3085
+ const cliChannelType = this.resolveChannelType?.(channel);
3086
+ const cliChatType = session?.chatType ?? explicitChatType;
3087
+ const cliPeerKeyId = cliChatType === 'group'
3088
+ ? (session?.metadata?.groupId || channelId)
3089
+ : userId;
3090
+ const cliPeerKey = cliChannelType && cliPeerKeyId ? formatPeerKey(cliChannelType, cliPeerKeyId) : undefined;
3091
+ if (Array.isArray(args?.argv)) {
3092
+ argv = withDefaultRelationContext(argv, { self: cliSelfAid, peer: cliPeerKey });
3093
+ }
3094
+ // Batch get is a CLI-only aggregate; authorize each underlying get before spawning once.
3095
+ const batchGet = splitConfigBatchGetArgv(argv);
3096
+ const isBatchGet = !!batchGet && batchGet.fields.length > 1;
3097
+ const intentArgvs = isBatchGet
3098
+ ? batchGet.fields.map(field => ['config', 'get', field, ...batchGet.rest])
3099
+ : [argv];
3100
+ const parsedCommands = [];
3101
+ for (const intentArgv of intentArgvs) {
3102
+ const parsed = parseCliIntent(intentArgv, 'menu.cli', { defaultRelation: { self: cliSelfAid, peer: cliPeerKey } });
3103
+ if (parsed.kind === 'invalid')
3104
+ return { error: parsed.reason, code: parsed.code };
3105
+ if (parsed.kind === 'raw') {
3106
+ await auditCommandAuthorization({
3107
+ ts: Date.now(), source: 'menu.cli', operation: parsed.intent.operation,
3108
+ scope: parsed.intent.scope, dangerous: true, actorId: userId,
3109
+ channel, channelId, role: identity.role, decision: 'deny', code: 'NOT_ALLOWED',
3110
+ reason: 'Unrecognized CLI command', taskId: requestId, argvHash: hashArgv(intentArgv),
3111
+ });
3112
+ return { error: 'CLI command is not allowed', code: 'NOT_ALLOWED' };
3113
+ }
3114
+ parsedCommands.push(parsed);
3115
+ }
3116
+ const subject = buildMenuAuthSubject(this, {
3117
+ identity,
3118
+ session,
3119
+ explicitChatType,
3120
+ channel,
3121
+ channelId,
3122
+ userId,
3123
+ fromControlChannel: fromControlChannel ?? false,
3124
+ subject: authSubject,
3125
+ });
3126
+ const selfAid = subject.selfAid;
3127
+ const peerKey = subject.peerKey;
3128
+ const isDaemonOwner = subject.isDaemonOwner;
3129
+ if (isBatchGet) {
3130
+ const first = parsedCommands[0].resolvedConfigCommand;
3131
+ if (!first)
3132
+ return { error: 'Unable to resolve batch config get', code: 'INVALID_CONFIG_COMMAND' };
3133
+ argv = ['config', 'get', ...batchGet.fields, ...first.canonicalArgv.slice(3)];
3134
+ }
3135
+ else if (parsedCommands[0].resolvedConfigCommand) {
3136
+ argv = parsedCommands[0].resolvedConfigCommand.canonicalArgv;
3137
+ }
3138
+ const authorizedCommands = [];
3139
+ for (let i = 0; i < parsedCommands.length; i++) {
3140
+ const parsed = parsedCommands[i];
3141
+ const authArgv = parsed.resolvedConfigCommand?.canonicalArgv ?? intentArgvs[i] ?? argv;
3142
+ const decision = await authorizeOperation({
3143
+ source: 'menu.cli',
3144
+ intent: parsed.intent,
3145
+ subject,
3146
+ resolvedConfigCommand: parsed.resolvedConfigCommand,
3147
+ allowExplicitRelationTarget: true,
3148
+ auditAllowed: false,
3149
+ auditMetadata: { taskId: requestId, argvHash: hashArgv(authArgv) },
3150
+ });
3151
+ if (!decision.allow) {
3152
+ return { error: decision.reason, code: decision.code };
3153
+ }
3154
+ authorizedCommands.push({ parsed, decision });
3155
+ }
3156
+ const execution = await this.execCliPassthrough(argv, subject.role);
3157
+ const executionData = 'data' in execution ? execution.data : undefined;
3158
+ for (const { parsed, decision } of authorizedCommands) {
3159
+ await auditCommandAuthorization({
3160
+ ts: Date.now(), source: 'menu.cli', operation: parsed.intent.operation,
3161
+ scope: parsed.intent.scope, dangerous: parsed.intent.dangerous ?? false,
3162
+ actorId: userId, selfAid, peerKey,
3163
+ channel, channelId, role: identity.role, isDaemonOwner,
3164
+ fromControlChannel: fromControlChannel ?? false, decision: 'allow',
3165
+ matchedRule: decision.command?.matchedRule, taskId: requestId, argvHash: hashArgv(argv),
3166
+ durationMs: executionData?.durationMs, exitCode: executionData?.exitCode,
3167
+ });
3168
+ }
3169
+ return execution;
3170
+ }
3171
+ // ── name=file action=list/fetch:目录浏览 / 拉取文件 ──
3172
+ // list 返回 JSON 目录项;fetch 内部等价于 /file <path>,复用 resolveMenuFilePath 校验链 + adapter.send(result.file)。
3173
+ // fetch 文件作为独立 result.file 消息异步发回;把请求 id 作为 correlationId 透传,
3174
+ // 客户端用它把异步到达的文件消息对回这次 fetch 点击。
3175
+ if (cmdBase === '/file') {
3176
+ // 权限(§6.3):agent owner/admin 或 aid channel owner。项目外文件仅 owner(§6.2,由 resolveMenuFilePath 校验)。
3177
+ if (!isManagementRole(identity.role)) {
3178
+ return { error: '无权限', code: 'NO_PERMISSION' };
3179
+ }
3180
+ if (action === 'list') {
3181
+ const dirArg = (args?.path ?? '.').toString().trim() || '.';
3182
+ const resolved = resolveMenuFilePath(dirArg, session, identity.role, 'directory');
3183
+ if ('error' in resolved)
3184
+ return resolved;
3185
+ const offset = parseFileListOffset(args?.offset);
3186
+ const limit = parseFileListLimit(args?.limit);
3187
+ const includeHidden = args?.includeHidden === true;
3188
+ const listed = listDirectory(resolved.realPath, { offset, limit, includeHidden, projectPath: resolved.projectPath, role: identity.role });
3189
+ if ('error' in listed)
3190
+ return listed;
3191
+ return { data: { path: dirArg, ...listed.data } };
3192
+ }
3193
+ if (action !== 'fetch')
3194
+ return { error: `不支持的 file action: ${action}`, code: 'NOT_SUPPORTED' };
3195
+ const resolved = resolveMenuFilePath(args?.path, session, identity.role, 'file');
3196
+ if ('error' in resolved)
3197
+ return resolved;
3198
+ const { realPath, stat } = resolved;
3199
+ if (stat.size > FILE_FETCH_MAX_SIZE) {
3200
+ return { error: `文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 ${FILE_FETCH_MAX_SIZE / 1024 / 1024} MB)`, code: 'FILE_TOO_LARGE' };
3201
+ }
3202
+ const adapter = this.adapters.get(channel);
3203
+ if (!adapter)
3204
+ return { error: '通道不存在', code: 'EXEC_FAILED' };
3205
+ if (!adapter.capabilities?.file)
3206
+ return { error: '通道不支持文件发送', code: 'NOT_SUPPORTED' };
3207
+ try {
3208
+ const replyCtx = session ? this.getReplyContext(session) : undefined;
3209
+ await adapter.send(buildEnvelope({ channel: adapter.channelName, channelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath, correlationId: requestId });
3210
+ return { data: { accepted: true } };
3211
+ }
3212
+ catch (e) {
3213
+ return { error: `文件发送失败: ${e?.message ?? e}`, code: 'EXEC_FAILED' };
3214
+ }
3215
+ }
3216
+ return { error: `不支持 action: ${cmdBase}`, code: 'NOT_SUPPORTED' };
3217
+ }
3218
+ const SYSTEM_CONTROL_NAME_MAP = {
3219
+ pwd: '/pwd', session: '/session', baseagent: '/baseagent', model: '/model',
3220
+ topic: '/topic',
3221
+ effort: '/effort', chatmode: '/chatmode', mentionmode: '/mentionmode',
3222
+ permission: '/perm', activity: '/activity', system: '/system',
3223
+ observable: '/observable',
3224
+ agent: '/agent', trigger: '/trigger', file: '/file', gateway: '/gateway',
3225
+ config: '/config', capability: '/capability', role: '/role',
3226
+ };
3227
+ function isProcessLevelMenu(name, cmd) {
3228
+ return name === 'system' || name === 'agent' || name === 'gateway' || name === 'config'
3229
+ || cmd === '/system' || cmd === '/agent' || cmd === '/gateway' || cmd === '/config';
3230
+ }
3231
+ async function execMenuForSystemControl(payload, context) {
3232
+ const id = payload?.id ?? '';
3233
+ const name = payload?.name;
3234
+ if (name === 'cli' || payload?.cmd === '/cli') {
3235
+ const message = context.source === 'ecweb'
3236
+ ? 'cli 不在 ECWeb 控制范围'
3237
+ : 'cli 不在控制 channel 范围';
3238
+ return { type: 'menu.response', id, name, error: { code: 'NOT_SUPPORTED', message } };
3239
+ }
3240
+ const isProcessLevel = isProcessLevelMenu(name, payload?.cmd);
3241
+ const isRoleRequest = name === 'role' || payload?.cmd === '/role';
3242
+ const requiresAgentTarget = isRoleRequest || [
3243
+ 'observable', 'baseagent', 'model', 'effort', 'trigger', 'capability',
3244
+ ].includes(name)
3245
+ || ['/observable', '/baseagent', '/model', '/effort', '/trigger', '/capability'].includes(payload?.cmd);
3246
+ const target = isProcessLevel
3247
+ ? { channel: SYSTEM_CONTROL_CHANNEL }
3248
+ : resolveExternalMenuAgentChannel(this, payload, SYSTEM_CONTROL_CHANNEL, requiresAgentTarget);
3249
+ if ('error' in target)
3250
+ return ecwebErr(id, name, target.code, target.error);
3251
+ const targetChannel = target.channel;
3252
+ const targetAid = this.getOwningAgent?.(targetChannel)?.aid;
3253
+ const trustedSubject = buildAuthSubject({
3254
+ selfAid: targetAid,
3255
+ actorId: context.actorAid,
3256
+ channel: targetChannel,
3257
+ channelType: 'aun',
3258
+ channelId: context.actorAid || SYSTEM_CONTROL_CHANNEL,
3259
+ chatType: 'private',
3260
+ conversationId: context.actorAid || SYSTEM_CONTROL_CHANNEL,
3261
+ identity: context.localDirect ? { role: 'owner', mode: 'interactive' } : undefined,
3262
+ processOwners: context.owners,
3263
+ fromControlChannel: true,
3264
+ });
3265
+ const trustedIdentity = trustedSubject.identity;
3266
+ const cmd = name ? (SYSTEM_CONTROL_NAME_MAP[name] ?? payload.cmd) : payload.cmd;
3267
+ try {
3268
+ switch (payload?.type) {
3269
+ case 'menu.list':
3270
+ return menuSuccess({ id }, this.getMenuItems(trustedIdentity.role, 'private', 'control'));
3271
+ case 'menu.query': {
3272
+ if (!cmd)
3273
+ return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
3274
+ const r = await this.execMenuQuery(cmd, targetChannel, targetChannel, context.actorAid, payload.args, undefined, true, trustedIdentity, trustedSubject, context.source);
3275
+ return ecwebResp(id, name, r);
3276
+ }
3277
+ case 'menu.options': {
3278
+ if (!cmd)
3279
+ return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
3280
+ const data = await this.getSubMenuItems(cmd, targetChannel, targetChannel, context.actorAid, payload.args, trustedIdentity, undefined, true, trustedSubject, context.source) ?? [];
3281
+ return menuSuccess({ id, ...(name ? { name } : {}) }, data);
3282
+ }
3283
+ case 'menu.update': {
3284
+ if (!cmd)
3285
+ return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
3286
+ if (!payload.value)
3287
+ return ecwebErr(id, name, 'MISSING_VALUE', '缺少 value');
3288
+ const r = await this.execMenuUpdate(cmd, payload.value, targetChannel, targetChannel, context.actorAid, trustedIdentity, true, payload.args, trustedSubject, context.source);
3289
+ return ecwebResp(id, name, r);
3290
+ }
3291
+ case 'menu.action': {
3292
+ if (!cmd)
3293
+ return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
3294
+ if (!payload.action)
3295
+ return ecwebErr(id, name, 'MISSING_VALUE', '缺少 action');
3296
+ const r = await this.execMenuAction(cmd, payload.action, payload.args, targetChannel, targetChannel, context.actorAid, trustedIdentity, undefined, id, true, trustedSubject, context.source);
3297
+ return ecwebResp(id, name, r);
3298
+ }
3299
+ default:
3300
+ return ecwebErr(id, name, 'METHOD_NOT_FOUND', `未知类型: ${payload?.type}`);
3301
+ }
3302
+ }
3303
+ catch (e) {
3304
+ return menuFailure({ id, ...(name ? { name } : {}) }, normalizeMenuError(e));
3305
+ }
3306
+ }
3307
+ /** ECWeb 专用入口:只信任 IPC 信封中的服务端认证上下文,不信任 Menu payload 自报身份。 */
3308
+ export async function execMenuForEcweb(payload, trusted) {
3309
+ const id = payload?.id ?? '';
3310
+ const name = payload?.name;
3311
+ const validationError = validateMenuRequest(payload ?? {});
3312
+ if (validationError)
3313
+ return menuFailure({ id, ...(name ? { name } : {}) }, validationError);
3314
+ const isProcessLevel = isProcessLevelMenu(name, payload?.cmd);
3315
+ const owners = loadDaemonConfig().owners ?? [];
3316
+ if (isProcessLevel && owners.length === 0) {
3317
+ return { type: 'menu.response', id, name, error: { code: 'FORBIDDEN', message: '请在 daemon.json 配置 owners 后使用进程级操作' } };
3318
+ }
3319
+ if (!trusted || (!trusted.localDirect && !trusted.actorAid)) {
3320
+ return menuFailure({ id, ...(name ? { name } : {}) }, {
3321
+ code: 'PERMISSION_DENIED',
3322
+ message: 'ECWeb operation requires a trusted local connection or authenticated actor',
3323
+ data: { $schema_version: 1, kind: 'role_permission_denied', self: payload?.args?.self ?? null },
3324
+ });
3325
+ }
3326
+ const userId = trusted.actorAid ?? (trusted.localDirect ? (isProcessLevel ? owners[0] : 'local-direct') : undefined);
3327
+ return execMenuForSystemControl.call(this, payload, {
3328
+ source: 'ecweb',
3329
+ actorAid: userId || 'local-direct',
3330
+ localDirect: trusted.localDirect,
3331
+ owners,
3332
+ });
3333
+ }
3334
+ /** 控制 AID channel 专用入口:peerId 必须 ∈ evolcore.owners。
3335
+ * 全量权限(进程级 + 跨 agent + 关系级),fromControlChannel=true 放行闸门。
3336
+ * 与 ECWeb 入口的区别:鉴权主体是真实 peerId(而非注入 owner),按 evolcore.owners 校验。 */
3337
+ export async function execMenuForControl(payload, peerId) {
3338
+ const id = payload?.id ?? '';
3339
+ const name = payload?.name;
3340
+ const validationError = validateMenuRequest(payload ?? {});
3341
+ if (validationError)
3342
+ return menuFailure({ id, ...(name ? { name } : {}) }, validationError);
3343
+ const owners = loadDaemonConfig().owners ?? [];
3344
+ const isRoleRequest = name === 'role' || payload?.cmd === '/role';
3345
+ if (!isRoleRequest && !isProcessLevelOwner(peerId, owners)) {
3346
+ return menuFailure({ id, ...(name ? { name } : {}) }, { code: 'ROLE_ACCESS_DENIED', message: '控制 channel 操作需要 owner 权限' });
3347
+ }
3348
+ return execMenuForSystemControl.call(this, payload, {
3349
+ source: 'control',
3350
+ actorAid: peerId,
3351
+ localDirect: false,
3352
+ owners,
3353
+ });
3354
+ }