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,3056 @@
1
+ import { hasModelSwitcher, hasPermissionController } from '../../agents/runner-types.js';
2
+ import { getCodexEfforts } from '../../agents/codex-runner.js';
3
+ import { buildEnvelope } from '../message/message-utils.js';
4
+ import { resolvePaths, getPackageRoot } from '../../paths.js';
5
+ import { logger } from '../../utils/logger.js';
6
+ import crypto from 'crypto';
7
+ import path from 'path';
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+ import { checkLatestVersion, getLocalVersion, isLinkedInstall, compareVersions } from '../../utils/npm-ops.js';
11
+ import { loadDaemonConfig } from '../../config-store.js';
12
+ import { read as cfgRead, resolveEffective, routeFieldPath, write as cfgWrite, } from '../../config/config-manager.js';
13
+ import { execAgentAction } from './agent-control.js';
14
+ import { authorizeOperation, buildAuthSubject } from '../auth/auth-gateway.js';
15
+ import { resolvePermissionMode, writeScope } from '../model/config-scope.js';
16
+ import { formatPeerKey } from '../relation/peer-identity.js';
17
+ import { modelMatches } from '../model/model-catalog.js';
18
+ import { formatModelCheck, runModelCheck } from '../model/model-diagnostics.js';
19
+ import { filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
20
+ import { displaySessionTitle, isSyntheticCliPrompt } from '../session/session-title.js';
21
+ import { chatmodeFieldForPeer, resolveChatModeForField } from '../message/peer-mode.js';
22
+ import { normalizePermissionMode as normalizePermissionModeContract, PUBLIC_PERMISSION_MODES } from '../permission/mode.js';
23
+ import { dispatchToMentionMode } from '../../config/mention-mode.js';
24
+ import { isManagementRole } from '../../config/builtin-roles.js';
25
+ import { isSystemControlChannel } from '../system-channels.js';
26
+ import { guardIdleCommand, guardKnownCommand, guardRoleCommand, guardThreadCommand, isRecognizedSlashCommand, normalizeSlashContent, } from './slash-gate.js';
27
+ const allEfforts = ['low', 'medium', 'high', 'xhigh', 'max'];
28
+ const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
29
+ function defaultSlashChatmode(field) {
30
+ return field === 'private' ? 'interactive' : 'proactive';
31
+ }
32
+ function slashChatmodeField(session, chatType) {
33
+ return chatmodeFieldForPeer(session?.chatType ?? chatType, session?.metadata?.peerType);
34
+ }
35
+ function resolveSlashChatmodeTarget(params) {
36
+ const self = params.selfAID
37
+ ?? params.session?.selfAID
38
+ ?? this.getOwningAgent?.(params.channel)?.aid
39
+ ?? this.resolveSelfAID?.(params.channel);
40
+ if (!self)
41
+ return { error: '找不到当前 agent,无法读写 chatmode', code: 'MISSING_AID' };
42
+ const actualChatType = params.session?.chatType || params.chatType;
43
+ const peerId = actualChatType === 'group'
44
+ ? (params.session?.metadata?.groupId || params.channelId)
45
+ : (params.userId || params.session?.metadata?.peerId || params.channelId);
46
+ if (!peerId)
47
+ return { error: '找不到当前对端,无法读写 relation chatmode', code: 'MISSING_PEER' };
48
+ const channelType = params.session?.channelType
49
+ || this.resolveChannelType?.(params.channel)
50
+ || params.channel.split('#')[0];
51
+ const field = slashChatmodeField(params.session, params.chatType);
52
+ return {
53
+ sel: { self, peerKey: formatPeerKey(channelType, peerId) },
54
+ field,
55
+ fieldPath: `chatmode.${field}`,
56
+ };
57
+ }
58
+ function readSlashChatmode(target) {
59
+ try {
60
+ const mode = resolveChatModeForField({ ...target.sel, field: target.field });
61
+ return mode === 'interactive' || mode === 'proactive'
62
+ ? mode
63
+ : defaultSlashChatmode(target.field);
64
+ }
65
+ catch {
66
+ return defaultSlashChatmode(target.field);
67
+ }
68
+ }
69
+ function writeSlashChatmode(target, value) {
70
+ const route = routeFieldPath(target.fieldPath, 'relation');
71
+ const cur = cfgRead(route.target, target.sel) || {};
72
+ const block = cur.chatmode && typeof cur.chatmode === 'object' && !Array.isArray(cur.chatmode)
73
+ ? { ...cur.chatmode }
74
+ : {};
75
+ block[target.field] = value;
76
+ cur.chatmode = block;
77
+ cfgWrite(route.target, cur, target.sel);
78
+ }
79
+ function resolveSlashMentionModeTarget(params) {
80
+ const self = params.selfAID
81
+ ?? params.session?.selfAID
82
+ ?? this.getOwningAgent?.(params.channel)?.aid
83
+ ?? this.resolveSelfAID?.(params.channel);
84
+ if (!self)
85
+ return { error: '找不到当前 agent,无法读写 mentionMode', code: 'MISSING_AID' };
86
+ return { sel: { self }, fieldPath: 'mentionMode' };
87
+ }
88
+ function readSlashMentionMode(target, fallback = null) {
89
+ try {
90
+ const mode = resolveEffective(target.sel, { cache: true }).mentionMode;
91
+ return mode === 'disabled' || mode === 'mention-only' ? mode : fallback;
92
+ }
93
+ catch {
94
+ return fallback;
95
+ }
96
+ }
97
+ function writeSlashMentionMode(target, value) {
98
+ const route = routeFieldPath(target.fieldPath, 'agent');
99
+ const cur = cfgRead(route.target, target.sel) || {};
100
+ if (value === null)
101
+ delete cur.mentionMode;
102
+ else
103
+ cur.mentionMode = value;
104
+ cfgWrite(route.target, cur, target.sel);
105
+ }
106
+ function resolveSlashRelationTarget(params) {
107
+ const self = params.selfAID
108
+ ?? params.session?.selfAID
109
+ ?? this.getOwningAgent?.(params.channel)?.aid
110
+ ?? this.resolveSelfAID?.(params.channel);
111
+ if (!self)
112
+ return { error: 'missing current agent aid', code: 'MISSING_AID' };
113
+ const actualChatType = params.session?.chatType || params.chatType;
114
+ const peerId = actualChatType === 'group'
115
+ ? (params.session?.metadata?.groupId || params.channelId)
116
+ : (params.userId || params.session?.metadata?.peerId);
117
+ if (!peerId)
118
+ return { error: 'missing current peer id', code: 'MISSING_PEER' };
119
+ const channelType = params.session?.channelType || this.resolveChannelType?.(params.channel) || params.channel.split('#')[0];
120
+ return {
121
+ self,
122
+ peerKey: formatPeerKey(channelType, peerId),
123
+ role: params.role,
124
+ };
125
+ }
126
+ function getAvailableEfforts(agent, model) {
127
+ if (agent.name === 'claude')
128
+ return allEfforts;
129
+ if (agent.name === 'codex')
130
+ return getCodexEfforts(model);
131
+ return [];
132
+ }
133
+ function modelDisplayLabel(agent, model) {
134
+ const full = agent.resolveModelId?.(model);
135
+ return full && full !== model ? `${model} (${full})` : model;
136
+ }
137
+ function formatIdleTime(ms) {
138
+ const seconds = Math.floor(ms / 1000);
139
+ const minutes = Math.floor(seconds / 60);
140
+ const hours = Math.floor(minutes / 60);
141
+ const days = Math.floor(hours / 24);
142
+ if (days > 0)
143
+ return `${days}天前`;
144
+ if (hours > 0)
145
+ return `${hours}小时前`;
146
+ if (minutes > 0)
147
+ return `${minutes}分钟前`;
148
+ return '刚刚';
149
+ }
150
+ async function authorizeSlashIntent(params) {
151
+ const { intent, identity, session, channel, channelId, userId, selfAid, isDaemonOwner } = params;
152
+ const chatType = session?.chatType ?? params.explicitChatType;
153
+ const peerKeyId = chatType === 'group'
154
+ ? (session?.metadata?.groupId || channelId)
155
+ : userId;
156
+ const channelType = channel.split('#')[0];
157
+ const peerKey = channelType && peerKeyId ? formatPeerKey(channelType, peerKeyId) : undefined;
158
+ const subject = params.subject
159
+ ? { ...params.subject, role: identity.role, identity }
160
+ : buildAuthSubject({
161
+ selfAid,
162
+ actorId: userId,
163
+ channel,
164
+ channelType,
165
+ channelId,
166
+ chatType,
167
+ conversationId: peerKeyId,
168
+ identity,
169
+ processOwners: isDaemonOwner && userId ? [userId] : [],
170
+ fromControlChannel: false,
171
+ });
172
+ if (intent.scope === 'relation') {
173
+ intent.args = buildSlashRelationIntentArgs({ args: intent.args, selfAid: subject.selfAid, peerKey: subject.peerKey ?? peerKey });
174
+ }
175
+ const decision = await authorizeOperation({ source: 'slash', intent, subject });
176
+ if (!decision.allow) {
177
+ return { kind: 'command.error', text: decision.reason };
178
+ }
179
+ return null;
180
+ }
181
+ function triggerOperationForSlash(content) {
182
+ const subcommand = content.slice('/trigger'.length).trim().split(/\s+/, 1)[0].toLowerCase();
183
+ if (!subcommand || subcommand === 'list')
184
+ return 'trigger.list';
185
+ if (subcommand === 'show')
186
+ return 'trigger.show';
187
+ if (subcommand === 'history')
188
+ return 'trigger.history';
189
+ if (subcommand === 'set')
190
+ return 'trigger.create';
191
+ if (subcommand === 'update')
192
+ return 'trigger.update';
193
+ if (subcommand === 'enable' || subcommand === 'disable')
194
+ return 'trigger.setEnabled';
195
+ if (subcommand === 'cancel')
196
+ return 'trigger.cancel';
197
+ if (subcommand === 'delete' || subcommand === 'remove' || subcommand === 'rm')
198
+ return 'trigger.delete';
199
+ if (subcommand === 'run' || subcommand === 'test')
200
+ return 'trigger.run';
201
+ return 'trigger.list';
202
+ }
203
+ function buildSlashRelationIntentArgs(params) {
204
+ const out = { ...(params.args ?? {}) };
205
+ if (params.selfAid && out.self === undefined)
206
+ out.self = params.selfAid;
207
+ if (params.peerKey && out.peer === undefined && out.peerKey === undefined)
208
+ out.peer = params.peerKey;
209
+ return out;
210
+ }
211
+ async function canReadSlashModelList(params) {
212
+ const { identity, session, channel, channelId, userId, selfAid, isDaemonOwner } = params;
213
+ const chatType = session?.chatType ?? params.explicitChatType;
214
+ const peerKeyId = chatType === 'group'
215
+ ? (session?.metadata?.groupId || channelId)
216
+ : userId;
217
+ const channelType = channel.split('#')[0];
218
+ const peerKey = channelType && peerKeyId ? formatPeerKey(channelType, peerKeyId) : undefined;
219
+ const intent = {
220
+ operation: 'model.list',
221
+ scope: 'relation',
222
+ source: 'slash',
223
+ args: buildSlashRelationIntentArgs({ selfAid, peerKey }),
224
+ };
225
+ const subject = params.subject ?? buildAuthSubject({
226
+ selfAid,
227
+ actorId: userId,
228
+ channel,
229
+ channelType,
230
+ channelId,
231
+ chatType,
232
+ conversationId: peerKeyId,
233
+ identity,
234
+ processOwners: isDaemonOwner && userId ? [userId] : [],
235
+ fromControlChannel: false,
236
+ });
237
+ const decision = await authorizeOperation({ source: 'slash', intent, subject, audit: false });
238
+ return decision.allow;
239
+ }
240
+ function getAgentBusyInfo(handler, aid, excludeSessionKey) {
241
+ if (!aid || !handler.agentRegistry)
242
+ return null;
243
+ const handle = handler.agentRegistry.get(aid) ?? null;
244
+ const agentName = handle?.name;
245
+ if (!agentName)
246
+ return null;
247
+ const processing = handler.messageQueue?.getProcessingDetailsByAgent?.(agentName, excludeSessionKey) ?? [];
248
+ const queueCount = handler.messageQueue?.getQueueLengthByAgent?.(agentName, excludeSessionKey) ?? 0;
249
+ return { count: processing.length + queueCount, processing, agentName };
250
+ }
251
+ function getAgentBusyCount(handler, aid, excludeSessionKey) {
252
+ return getAgentBusyInfo(handler, aid, excludeSessionKey)?.count ?? null;
253
+ }
254
+ async function getGitWorkingDirInfo(projectPath) {
255
+ try {
256
+ const { execFileSync } = await import('child_process');
257
+ const isInsideWorkTree = execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
258
+ cwd: projectPath,
259
+ encoding: 'utf8',
260
+ timeout: 1000,
261
+ stdio: ['ignore', 'pipe', 'ignore']
262
+ }).trim();
263
+ if (isInsideWorkTree !== 'true')
264
+ return null;
265
+ // 获取分支名
266
+ let branch = execFileSync('git', ['branch', '--show-current'], {
267
+ cwd: projectPath,
268
+ encoding: 'utf8',
269
+ timeout: 1000,
270
+ stdio: ['ignore', 'pipe', 'ignore']
271
+ }).trim();
272
+ if (!branch) {
273
+ branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
274
+ cwd: projectPath,
275
+ encoding: 'utf8',
276
+ timeout: 1000,
277
+ stdio: ['ignore', 'pipe', 'ignore']
278
+ }).trim();
279
+ }
280
+ if (!branch)
281
+ return null;
282
+ // 检查文件状态
283
+ const statusOutput = execFileSync('git', ['--no-optional-locks', 'status', '--porcelain'], {
284
+ cwd: projectPath,
285
+ encoding: 'utf8',
286
+ timeout: 1000,
287
+ stdio: ['ignore', 'pipe', 'ignore']
288
+ });
289
+ // 解析文件状态统计
290
+ const stats = { modified: 0, added: 0, deleted: 0, untracked: 0 };
291
+ const lines = statusOutput.trim().split('\n').filter(Boolean);
292
+ for (const line of lines) {
293
+ if (line.length < 2)
294
+ continue;
295
+ const index = line[0]; // staged status
296
+ const worktree = line[1]; // unstaged status
297
+ if (line.startsWith('??')) {
298
+ stats.untracked++;
299
+ }
300
+ else if (index === 'A') {
301
+ stats.added++;
302
+ }
303
+ else if (index === 'D' || worktree === 'D') {
304
+ stats.deleted++;
305
+ }
306
+ else if (index === 'M' || worktree === 'M' || index === 'R' || index === 'C') {
307
+ stats.modified++;
308
+ }
309
+ }
310
+ // 组装显示信息
311
+ const parts = [branch];
312
+ const isDirty = lines.length > 0;
313
+ if (isDirty) {
314
+ parts[0] = branch + '*'; // 分支名后加 * 表示有修改
315
+ const fileParts = [];
316
+ if (stats.modified > 0)
317
+ fileParts.push(`!${stats.modified}`);
318
+ if (stats.added > 0)
319
+ fileParts.push(`+${stats.added}`);
320
+ if (stats.deleted > 0)
321
+ fileParts.push(`-${stats.deleted}`);
322
+ if (stats.untracked > 0)
323
+ fileParts.push(`?${stats.untracked}`);
324
+ if (fileParts.length > 0) {
325
+ parts.push(`[${fileParts.join(' ')}]`);
326
+ }
327
+ }
328
+ // 获取 ahead/behind 信息
329
+ try {
330
+ const revOutput = execFileSync('git', ['rev-list', '--left-right', '--count', '@{upstream}...HEAD'], { cwd: projectPath, timeout: 1000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
331
+ const revParts = revOutput.trim().split(/\s+/);
332
+ if (revParts.length === 2) {
333
+ const behind = parseInt(revParts[0], 10) || 0;
334
+ const ahead = parseInt(revParts[1], 10) || 0;
335
+ if (ahead > 0 || behind > 0) {
336
+ const aheadBehind = [];
337
+ if (ahead > 0)
338
+ aheadBehind.push(`↑${ahead}`);
339
+ if (behind > 0)
340
+ aheadBehind.push(`↓${behind}`);
341
+ parts.push(aheadBehind.join(' '));
342
+ }
343
+ }
344
+ }
345
+ catch {
346
+ // No upstream or error, skip ahead/behind
347
+ }
348
+ return parts.join(' ');
349
+ }
350
+ catch (err) {
351
+ return null; // git 命令失败时静默返回 null
352
+ }
353
+ }
354
+ export async function handleSlashCommand(content, channel, channelId, sendMessage, userId, threadId, chatType, source, messageId, selfAID, overrideIdentity, overrideSubject) {
355
+ // 卡片回调的 chatType 不可靠(飞书 bot 单聊 chatId 也是 oc_ 前缀),
356
+ // 不应覆盖 session 中已有的正确值
357
+ if (source === 'card-trigger')
358
+ chatType = undefined;
359
+ // 解析身份(按实例名)
360
+ const narrowedChatType = chatType === 'group' ? 'group' : chatType === 'private' ? 'private' : undefined;
361
+ const identityConversationId = narrowedChatType === 'group' ? channelId : userId;
362
+ const resolvedIdentity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, narrowedChatType, identityConversationId);
363
+ const authSubject = overrideSubject ?? buildAuthSubject({
364
+ selfAid: selfAID
365
+ ?? this.agentRegistry?.resolveByChannel(channel)?.aid
366
+ ?? this.resolveSelfAID(channel),
367
+ actorId: userId,
368
+ channel,
369
+ channelType: this.resolveChannelType(channel),
370
+ channelId,
371
+ chatType: narrowedChatType,
372
+ conversationId: identityConversationId,
373
+ identity: resolvedIdentity,
374
+ processOwners: loadDaemonConfig().owners ?? [],
375
+ fromControlChannel: false,
376
+ });
377
+ const identity = authSubject.identity;
378
+ const policy = this.getPolicy(channel);
379
+ // 按当前会话选择 agent 后端。懒加载,避免错配 session 卡住 /baseagent 等恢复命令。
380
+ const activeSession = await this.sessionManager.getActiveSession(channel, channelId);
381
+ let activeAgent;
382
+ const getActiveAgent = () => {
383
+ if (!activeAgent)
384
+ activeAgent = this.getAgent(channel, activeSession?.baseagent);
385
+ return activeAgent;
386
+ };
387
+ const getActiveAgentIfAvailable = () => {
388
+ try {
389
+ return activeSession ? getActiveAgent() : undefined;
390
+ }
391
+ catch {
392
+ return undefined;
393
+ }
394
+ };
395
+ // 规范化命令(将别名转换为完整命令)
396
+ const normalizedContent = normalizeSlashContent(content);
397
+ if (normalizedContent !== content) {
398
+ logger.debug(`[CommandHandler] normalized: "${content}" -> "${normalizedContent}"`);
399
+ }
400
+ logger.info(`[CommandHandler] handle: channel=${channel} channelId=${channelId} cmd="${normalizedContent.split(' ')[0]}" user=${userId ?? 'n/a'} role=${identity?.role ?? 'n/a'}`);
401
+ // Agent-owned 通道:禁止项目切换和 agent 切换
402
+ // 权限检查:区分用户级命令和管理级命令
403
+ const isOwner = identity.role === 'owner';
404
+ const isAdmin = isManagementRole(identity.role);
405
+ const activeChatType = activeSession?.chatType || (chatType === 'group' ? 'group' : 'private');
406
+ const getExistingSessionForCommand = async () => {
407
+ if (threadId)
408
+ return await this.sessionManager.getThreadSession(channel, channelId, threadId);
409
+ return activeSession;
410
+ };
411
+ const getEffectiveChatmode = (session, fallbackChatType = activeChatType) => {
412
+ const target = resolveSlashChatmodeTarget.call(this, {
413
+ session,
414
+ channel,
415
+ channelId,
416
+ userId,
417
+ selfAID,
418
+ role: session?.identity?.role || identity.role,
419
+ chatType: session?.chatType || fallbackChatType,
420
+ });
421
+ return 'error' in target
422
+ ? defaultSlashChatmode(slashChatmodeField(session, fallbackChatType))
423
+ : readSlashChatmode(target);
424
+ };
425
+ const threadGuard = guardThreadCommand(normalizedContent, threadId);
426
+ if (threadGuard)
427
+ return threadGuard;
428
+ // daemon owner 判定(缓存一次,后续 /restart /reload 复用)
429
+ const isDaemonOwner = authSubject.isDaemonOwner;
430
+ const authorizeIntent = (params) => authorizeSlashIntent({
431
+ ...params,
432
+ subject: authSubject,
433
+ });
434
+ // roleGuard 仅对进程级命令(/restart /reload)放行 daemon owner 绕过,
435
+ // 其余命令严格按 agent-channel 的 isAdmin 判定,不越权。
436
+ const isProcessLevelSlash = normalizedContent === '/restart' || normalizedContent === '/reload' || normalizedContent.startsWith('/reload ');
437
+ const roleGuard = guardRoleCommand(normalizedContent, activeChatType, isAdmin || (isDaemonOwner && isProcessLevelSlash));
438
+ if (roleGuard)
439
+ return roleGuard;
440
+ const idleGuard = await guardIdleCommand({
441
+ content: normalizedContent,
442
+ threadId,
443
+ channel,
444
+ channelId,
445
+ activeSession,
446
+ activeAgent: getActiveAgentIfAvailable(),
447
+ sessionManager: this.sessionManager,
448
+ messageQueue: this.messageQueue,
449
+ getAgentForSession: session => this.getAgent(channel, session.baseagent),
450
+ });
451
+ if (idleGuard)
452
+ return idleGuard;
453
+ const knownGuard = guardKnownCommand(normalizedContent);
454
+ if (knownGuard)
455
+ return knownGuard;
456
+ const isCmd = isRecognizedSlashCommand(normalizedContent);
457
+ if (!isCmd)
458
+ return undefined;
459
+ // /help 命令不需要会话
460
+ if (normalizedContent === '/help') {
461
+ const appendProcessCommands = (lines) => {
462
+ if (!isDaemonOwner)
463
+ return;
464
+ lines.push('', '🛠️ 进程级运维:', ' /restart - 重启服务', ' /reload [aid] - 热重载 Agent 配置');
465
+ };
466
+ if (!isAdmin && activeChatType === 'group') {
467
+ const lines = [
468
+ '可用命令:',
469
+ '',
470
+ '其他:',
471
+ ' /status - 显示会话状态',
472
+ ' /check - 检查 EvolAgent 实例健康',
473
+ ' /help - 显示此帮助信息',
474
+ ];
475
+ appendProcessCommands(lines);
476
+ return { kind: 'command.result', text: lines.join('\n') };
477
+ }
478
+ if (!isAdmin) {
479
+ const lines = [
480
+ '可用命令:',
481
+ '',
482
+ '🔄 会话管理:',
483
+ ' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
484
+ ' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
485
+ ' /name <新名称> - 重命名当前会话',
486
+ ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
487
+ ' /status - 显示会话状态',
488
+ ' /check - 检查 EvolAgent 实例健康',
489
+ '',
490
+ '❓ 帮助:',
491
+ ' /help - 显示此帮助信息',
492
+ ];
493
+ appendProcessCommands(lines);
494
+ return { kind: 'command.result', text: lines.join('\n') };
495
+ }
496
+ // admin+ 基础命令
497
+ const lines = [
498
+ '可用命令:',
499
+ '',
500
+ '📁 项目:',
501
+ ' /pwd - 显示当前项目路径',
502
+ '',
503
+ '🔄 会话管理:',
504
+ ' /new [名称] - 创建新会话(清空历史请用此命令,可选命名)',
505
+ ' /s [cli|名称|序号|uuid] - 列出或切换会话(cli 查看未导入的 CLI 会话)',
506
+ ' /name <新名称> - 重命名当前会话',
507
+ ' /del <名称> - 删除指定会话(仅解绑,不删除文件)',
508
+ ' /fork [名称] - 分支当前会话(从当前对话点创建分支)',
509
+ ' /rewind [N] [chat|file|all] - 查看历史/撤销指定轮次(别名: /rw)',
510
+ ' /compact - 压缩会话上下文(减少 token 用量)',
511
+ '',
512
+ '🤖 Agent 与模型:',
513
+ ' /baseagent [name] - 查看或切换 Agent 后端(别名: /base)',
514
+ ' /model [model] - 查看或切换模型',
515
+ ' /effort [level] - 查看或切换推理强度',
516
+ '',
517
+ '💬 聊天设置:',
518
+ ' /activity [all|text|none] - 查看/控制中间输出显示模式',
519
+ ' /chatmode [interactive|proactive] - 查看/切换会话模式(被动响应或主动推进)',
520
+ ' /mentionmode [mention-only|disabled] - 查看/切换群聊 @ 处理模式(仅@响应或全部响应,仅群聊)',
521
+ '',
522
+ '🔐 权限管理:',
523
+ ' /perm - 查看当前权限模式',
524
+ ...(isAdmin ? [' 权限模式请通过角色策略编辑器修改'] : []),
525
+ ' /perm allow|always|deny - 审批权限请求',
526
+ '',
527
+ '🛠️ 运维:',
528
+ ' /status - 显示会话状态',
529
+ ' /stop - 中断当前任务',
530
+ ' /check - 检查 EvolAgent 实例健康',
531
+ ...(isOwner ? [
532
+ ' /observable [true|false] - 查看或切换观察者模式',
533
+ ] : []),
534
+ ...(isDaemonOwner ? [
535
+ ' /restart - 重启服务',
536
+ ' /reload [aid] - 热重载 Agent 配置',
537
+ ] : []),
538
+ ...(!isDaemonOwner && isAdmin ? [
539
+ ' /reload - 热重载当前 Agent 配置',
540
+ ] : []),
541
+ ...(isAdmin ? [
542
+ '',
543
+ '🧰 工具:',
544
+ ` /file ${isOwner ? '[channel] ' : ''}<path> - 发送项目内文件`,
545
+ ] : []),
546
+ '',
547
+ '❓ 帮助:',
548
+ ' /help - 显示此帮助信息',
549
+ ];
550
+ return { kind: 'command.result', text: lines.join('\n') };
551
+ }
552
+ // /evolhelp 命令:返回 JSON 格式的命令列表(供程序解析)
553
+ if (normalizedContent === '/evolhelp') {
554
+ const cmds = [];
555
+ // 项目
556
+ cmds.push({ command: '/pwd', description: '显示当前项目路径', category: '项目', roles: ['admin', 'owner'] });
557
+ // 会话管理
558
+ cmds.push({ command: '/new', args: '[名称]', description: '创建新会话(清空历史请用此命令,可选命名)', category: '会话管理', roles: ['visitor', 'member', 'admin', 'owner'] });
559
+ cmds.push({ command: '/s', aliases: ['/session', '/slist'], args: '[cli|名称|序号|uuid]', description: '列出或切换会话', category: '会话管理', roles: ['visitor', 'member', 'admin', 'owner'] });
560
+ cmds.push({ command: '/name', aliases: ['/rename'], args: '<新名称>', description: '重命名当前会话', category: '会话管理', roles: ['visitor', 'member', 'admin', 'owner'] });
561
+ cmds.push({ command: '/del', args: '<名称>', description: '删除指定会话(仅解绑,不删除文件)', category: '会话管理', roles: ['visitor', 'member', 'admin', 'owner'] });
562
+ if (isAdmin) {
563
+ cmds.push({ command: '/fork', args: '[名称]', description: '分支当前会话(从当前对话点创建分支)', category: '会话管理', roles: ['admin', 'owner'] });
564
+ cmds.push({ command: '/rewind', aliases: ['/rw'], args: '[N] [chat|file|all]', description: '查看历史/撤销指定轮次', category: '会话管理', roles: ['admin', 'owner'] });
565
+ cmds.push({ command: '/compact', description: '压缩会话上下文(减少 token 用量)', category: '会话管理', roles: ['admin', 'owner'] });
566
+ }
567
+ // Agent 与模型
568
+ if (isAdmin) {
569
+ cmds.push({ command: '/baseagent', aliases: ['/base'], args: '[name]', description: '查看或切换 Agent 后端', category: 'Agent 与模型', roles: ['admin', 'owner'] });
570
+ cmds.push({ command: '/model', args: '[model]', description: '查看或切换模型', category: 'Agent 与模型', roles: ['admin', 'owner'] });
571
+ cmds.push({ command: '/effort', args: '[level]', description: '查看或切换推理强度', category: 'Agent 与模型', roles: ['admin', 'owner'] });
572
+ }
573
+ // 权限管理
574
+ if (isAdmin) {
575
+ cmds.push({ command: '/perm', description: '查看当前角色决定的权限模式', category: '权限管理', roles: ['admin', 'owner'] });
576
+ cmds.push({ command: '/perm', args: 'allow|always|deny', description: '审批权限请求', category: '权限管理', roles: ['admin', 'owner'] });
577
+ }
578
+ // 运维
579
+ cmds.push({ command: '/status', description: '显示会话状态', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
580
+ cmds.push({ command: '/stop', description: '中断当前任务', category: '运维', roles: ['admin', 'owner'] });
581
+ cmds.push({ command: '/check', description: '检查 EvolAgent 实例健康', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
582
+ if (isAdmin) {
583
+ cmds.push({ command: '/activity', args: '[all|text|none]', description: '查看/控制中间输出显示模式', category: '聊天设置', roles: ['admin', 'owner'] });
584
+ }
585
+ if (isOwner) {
586
+ cmds.push({ command: '/observable', args: '[true|false]', description: '查看或切换观察者模式', category: '运维', roles: ['owner'] });
587
+ }
588
+ if (isDaemonOwner) {
589
+ cmds.push({ command: '/restart', description: '重启服务', category: '运维', roles: ['daemon-owner'] });
590
+ cmds.push({ command: '/reload', args: '[aid]', description: '热重载 Agent 配置', category: '运维', roles: ['daemon-owner'] });
591
+ }
592
+ if (!isDaemonOwner && isAdmin) {
593
+ cmds.push({ command: '/reload', description: '热重载当前 Agent 配置', category: '运维', roles: ['admin', 'owner'] });
594
+ }
595
+ if (isAdmin) {
596
+ cmds.push({ command: '/file', args: isOwner ? '[channel] <path>' : '<path>', description: '发送项目内文件', category: '工具', roles: ['admin', 'owner'] });
597
+ }
598
+ // 聊天设置
599
+ if (isAdmin) {
600
+ cmds.push({ command: '/chatmode', args: '[interactive|proactive]', description: '查看/切换会话模式(被动响应或主动推进)', category: '聊天设置', roles: ['admin', 'owner'] });
601
+ cmds.push({ command: '/mentionmode', args: '[mention-only|disabled]', description: '查看/切换群聊 @ 处理模式(仅@响应或全部响应)', category: '聊天设置', roles: ['admin', 'owner'] });
602
+ }
603
+ // 交互
604
+ cmds.push({ command: '/ask', args: '<选项>', description: '回答 Agent 的交互式问题', category: '运维', roles: ['visitor', 'member', 'admin', 'owner'] });
605
+ // 帮助
606
+ cmds.push({ command: '/help', description: '显示帮助信息', category: '帮助', roles: ['visitor', 'member', 'admin', 'owner'] });
607
+ const categories = [...new Set(cmds.map(c => c.category))];
608
+ return { kind: 'command.result', text: JSON.stringify({ commands: cmds, categories }) };
609
+ }
610
+ // /perm 命令:权限模式查询 + 权限审批(快速路径,不进入消息队列)
611
+ if (normalizedContent.startsWith('/perm')) {
612
+ const args = normalizedContent.slice(5).trim();
613
+ const permissionDecisionLabels = {
614
+ allow: '✓ 已授权(本次),继续执行……',
615
+ always: '✓ 已授权(同会话同操作 30 分钟),继续执行……',
616
+ deny: '✓ 已拒绝',
617
+ };
618
+ // Explicit request IDs are globally unique and already bound to an exact
619
+ // authenticated approver in PermissionGateway. Handle this before session
620
+ // lookup so an owner can answer a handoff in an otherwise idle AUN chat.
621
+ const explicitParts = args.split(/\s+/);
622
+ if (explicitParts.length === 2
623
+ && (explicitParts[1] === 'allow' || explicitParts[1] === 'always' || explicitParts[1] === 'deny')) {
624
+ if (!this.permissionGateway) {
625
+ return { kind: 'command.error', text: '❌ 权限审批未启用' };
626
+ }
627
+ const decision = explicitParts[1];
628
+ const resolved = this.permissionGateway.resolvePermissionByRequestId(explicitParts[0], decision, userId, channel);
629
+ if (!resolved) {
630
+ return { kind: 'command.error', text: '❌ 请求不存在、无法验证审批人身份或审批资格已失效' };
631
+ }
632
+ return { kind: 'command.result', text: permissionDecisionLabels[decision] };
633
+ }
634
+ // 权限查询和审批依附于现有会话;不应隐式创建空会话。
635
+ const permSession = await getExistingSessionForCommand();
636
+ if (!permSession)
637
+ return { kind: 'command.result', text: '当前没有活跃会话' };
638
+ const permAgent = this.getAgent(channel, permSession.baseagent);
639
+ // 角色来自会话身份;permissionMode 只从该角色定义解析。
640
+ const permSelfAid = selfAID ?? permSession.selfAID ?? this.resolveSelfAID(channel);
641
+ const permChannelType = permSession.channelType ?? this.resolveChannelType(channel);
642
+ const permPeerKeyId = permSession.chatType === 'group'
643
+ ? (permSession.metadata?.groupId || channelId)
644
+ : (userId || permSession.metadata?.peerId);
645
+ const permPeerKey = (permChannelType && permPeerKeyId)
646
+ ? formatPeerKey(permChannelType, permPeerKeyId)
647
+ : undefined;
648
+ const permRole = permSession.identity?.role || identity.role || 'none';
649
+ const permScope = { self: permSelfAid || undefined, peerKey: permPeerKey, role: permRole };
650
+ const permIntentArgs = { self: permScope.self };
651
+ const authorizePermissionAnswer = async (decision) => authorizeIntent({
652
+ intent: {
653
+ operation: 'permission.answer',
654
+ scope: 'relation',
655
+ source: 'slash',
656
+ args: { ...permIntentArgs, value: decision },
657
+ },
658
+ identity,
659
+ session: permSession,
660
+ explicitChatType: permSession.chatType === 'group' ? 'group' : 'private',
661
+ channel,
662
+ channelId,
663
+ userId,
664
+ selfAid: permScope.self,
665
+ isDaemonOwner,
666
+ });
667
+ const resolvePermissionAnswer = (requestId, decision) => {
668
+ if (!this.permissionGateway) {
669
+ return { kind: 'command.error', text: '❌ 权限审批未启用' };
670
+ }
671
+ const pendingIds = this.permissionGateway.getPendingRequests(permSession.id);
672
+ if (!pendingIds.includes(requestId)) {
673
+ return { kind: 'command.error', text: `❌ 当前会话没有待审批请求: ${requestId}` };
674
+ }
675
+ const resolved = this.permissionGateway.resolvePermissionByRequestId(requestId, decision, userId, channel);
676
+ if (!resolved) {
677
+ return { kind: 'command.error', text: '❌ 无法验证审批人身份,或审批资格已失效' };
678
+ }
679
+ return { kind: 'command.result', text: permissionDecisionLabels[decision] };
680
+ };
681
+ // /perm(无参数):显示当前角色决定的模式。
682
+ if (!args) {
683
+ const authDenied = await authorizeIntent({
684
+ intent: {
685
+ operation: 'permission.current',
686
+ scope: 'relation',
687
+ source: 'slash',
688
+ args: permIntentArgs,
689
+ },
690
+ identity,
691
+ session: permSession,
692
+ explicitChatType: permSession.chatType === 'group' ? 'group' : 'private',
693
+ channel,
694
+ channelId,
695
+ userId,
696
+ selfAid: permScope.self,
697
+ isDaemonOwner,
698
+ });
699
+ if (authDenied)
700
+ return authDenied;
701
+ const currentMode = normalizePermissionModeContract(resolvePermissionMode(permScope)).mode;
702
+ return { kind: 'command.result', text: `当前权限模式: ${currentMode}\n由角色 ${permRole} 的策略定义决定。` };
703
+ }
704
+ const parts = args.split(/\s+/);
705
+ // /perm <mode> 不再修改配置;allow/always/deny 仍用于快捷审批。
706
+ if (parts.length === 1) {
707
+ const arg = parts[0];
708
+ // /perm allow|always|deny:快捷审批
709
+ if (arg === 'allow' || arg === 'always' || arg === 'deny') {
710
+ const authDenied = await authorizePermissionAnswer(arg);
711
+ if (authDenied)
712
+ return authDenied;
713
+ if (!this.permissionGateway) {
714
+ return { kind: 'command.error', text: '❌ 权限审批未启用' };
715
+ }
716
+ const pendingIds = this.permissionGateway.getPendingRequests(permSession.id);
717
+ if (pendingIds.length === 0) {
718
+ return { kind: 'command.error', text: '❌ 当前没有待审批的权限请求' };
719
+ }
720
+ if (pendingIds.length > 1) {
721
+ return { kind: 'command.error', text: `❌ 当前有 ${pendingIds.length} 个待审批请求,请指定 requestId:\n${pendingIds.map((id) => ` /perm ${id} ${arg}`).join('\n')}` };
722
+ }
723
+ const requestId = pendingIds[0];
724
+ return resolvePermissionAnswer(requestId, arg);
725
+ }
726
+ // /perm <mode>:提示改用角色策略编辑器。
727
+ if (hasPermissionController(permAgent)) {
728
+ const modes = permAgent.listModes();
729
+ const matched = modes.find(m => m.key === arg);
730
+ if (matched) {
731
+ return { kind: 'command.error', text: '❌ permissionMode 只能通过角色策略编辑器修改' };
732
+ }
733
+ }
734
+ // 不是已知模式名也不是 allow/deny
735
+ if (PERMISSION_MODE_KEYS.includes(arg)) {
736
+ return { kind: 'command.error', text: '❌ permissionMode 只能通过角色策略编辑器修改' };
737
+ }
738
+ return { kind: 'command.error', text: `❌ 未知参数: ${arg}\n用法: /perm 或 /perm allow|always|deny` };
739
+ }
740
+ return { kind: 'command.error', text: `❌ 未知参数: ${args}\n用法: /perm、/perm allow|always|deny 或 /perm <requestId> allow|always|deny` };
741
+ }
742
+ // /ask 命令:回答 AskUserQuestion / ExitPlanMode 的交互式问题
743
+ if (normalizedContent.startsWith('/ask')) {
744
+ const args = normalizedContent.slice(4).trim();
745
+ const askSession = await getExistingSessionForCommand();
746
+ if (!args) {
747
+ if (!askSession)
748
+ return { kind: 'command.result', text: '当前没有待回答的问题' };
749
+ const pendingIds = this.interactionRouter?.getPending(askSession.id) || [];
750
+ if (pendingIds.length === 0)
751
+ return { kind: 'command.result', text: '当前没有待回答的问题' };
752
+ return { kind: 'command.result', text: `当前有 ${pendingIds.length} 个待回答问题,请回复 /ask <选项>` };
753
+ }
754
+ if (!askSession)
755
+ return { kind: 'command.error', text: '❌ 当前没有待回答的问题' };
756
+ const fb = await this.handleInteractionFallback('ask', args, askSession.id, userId);
757
+ if (fb.matched)
758
+ return { kind: 'command.result', text: fb.result ?? '✓ 已回答' };
759
+ return { kind: 'command.error', text: '❌ 当前没有待回答的问题' };
760
+ }
761
+ // /resume 命令:返回当前项目的 Claude 会话记录(JSON)
762
+ if (normalizedContent === '/resume' || normalizedContent.startsWith('/resume ')) {
763
+ const resumeSession = await getExistingSessionForCommand();
764
+ if (!resumeSession)
765
+ return { kind: 'command.result', text: '当前没有活跃会话' };
766
+ try {
767
+ const { encodePath } = await import('../../utils/cross-platform.js');
768
+ const homeDir = os.homedir();
769
+ const encodedPath = encodePath(resumeSession.projectPath);
770
+ const projectDir = path.join(homeDir, '.claude', 'projects', encodedPath);
771
+ if (!fs.existsSync(projectDir)) {
772
+ return { kind: 'command.error', text: '❌ 未找到 Claude 会话记录目录' };
773
+ }
774
+ const jsonlFiles = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
775
+ if (jsonlFiles.length === 0) {
776
+ return { kind: 'command.error', text: '❌ 当前项目没有 Claude 会话记录' };
777
+ }
778
+ const sessions = [];
779
+ for (const file of jsonlFiles) {
780
+ const filePath = path.join(projectDir, file);
781
+ const sessionId = file.replace('.jsonl', '');
782
+ let lastTimestamp = '';
783
+ let firstUserMessage = '';
784
+ let model = '';
785
+ let branch = '';
786
+ let turns = 0;
787
+ try {
788
+ const content = fs.readFileSync(filePath, 'utf-8');
789
+ const lines = content.split('\n').filter(l => l.trim());
790
+ for (const line of lines) {
791
+ const event = JSON.parse(line);
792
+ if (event.timestamp && event.timestamp > lastTimestamp) {
793
+ lastTimestamp = event.timestamp;
794
+ }
795
+ if (event.gitBranch && !branch) {
796
+ branch = event.gitBranch;
797
+ }
798
+ if (event.type === 'user' && event.message?.role === 'user') {
799
+ const msgContent = event.message.content;
800
+ const isToolResult = Array.isArray(msgContent) && msgContent.every((c) => c.type === 'tool_result');
801
+ if (!isToolResult) {
802
+ turns++;
803
+ if (!firstUserMessage) {
804
+ let candidate = '';
805
+ if (typeof msgContent === 'string') {
806
+ candidate = msgContent;
807
+ }
808
+ else if (Array.isArray(msgContent)) {
809
+ const textBlock = msgContent.find((c) => c.type === 'text');
810
+ if (textBlock?.text) {
811
+ candidate = textBlock.text;
812
+ }
813
+ }
814
+ // 跳过 Claude Code 注入的脚手架 prompt,取第一条真人消息
815
+ if (candidate && !isSyntheticCliPrompt(candidate, 'claude')) {
816
+ firstUserMessage = candidate.slice(0, 100);
817
+ }
818
+ }
819
+ }
820
+ }
821
+ if (event.type === 'assistant' && event.message?.model && !model) {
822
+ model = event.message.model;
823
+ }
824
+ }
825
+ }
826
+ catch {
827
+ continue;
828
+ }
829
+ if (!lastTimestamp)
830
+ continue;
831
+ sessions.push({
832
+ sessionId,
833
+ lastMessageTime: lastTimestamp,
834
+ firstUserMessage: firstUserMessage || '(无消息)',
835
+ model: model || 'unknown',
836
+ turns,
837
+ branch: branch || 'unknown',
838
+ });
839
+ }
840
+ sessions.sort((a, b) => b.lastMessageTime.localeCompare(a.lastMessageTime));
841
+ return { kind: 'command.result', text: JSON.stringify(sessions, null, 2) };
842
+ }
843
+ catch (error) {
844
+ logger.error('[CommandHandler] /resume failed:', error);
845
+ return { kind: 'command.error', text: `❌ 读取会话记录失败: ${error instanceof Error ? error.message : '未知错误'}` };
846
+ }
847
+ }
848
+ // /baseagent 命令:查看或切换 Agent 后端
849
+ if (normalizedContent === '/baseagent' || normalizedContent.startsWith('/baseagent ')) {
850
+ const args = normalizedContent.slice(10).trim();
851
+ const owningAgent = this.agentRegistry?.resolveByChannel(channel);
852
+ // 切换(带参)会修改 agent active_baseagent,仅 owner 可操作;无参查询对所有人放开
853
+ if (args && !isOwner) {
854
+ return { kind: 'command.error', text: '❌ 无权限:切换 baseagent 仅限 owner 使用' };
855
+ }
856
+ const available = this.getAvailableBaseagents(channel);
857
+ if (!args) {
858
+ const activeBaseagent = owningAgent?.baseagent || this.parseDefaultBaseagent();
859
+ // 尝试发送 CommandCard 卡片
860
+ if (this.interactionRouter && available.length > 1) {
861
+ const interaction = {
862
+ type: 'interaction',
863
+ id: `agent-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
864
+ channelId,
865
+ sessionId: activeSession?.id || `agent-${Date.now()}`,
866
+ initiatorId: userId,
867
+ kind: {
868
+ kind: 'command-card',
869
+ title: '🔌 切换 Agent',
870
+ buttons: available.map((a) => ({
871
+ label: a === activeBaseagent ? `✓ ${a}` : a,
872
+ command: `/baseagent ${a}`,
873
+ style: (a === activeBaseagent ? 'primary' : 'default'),
874
+ disabled: a === activeBaseagent,
875
+ })),
876
+ },
877
+ };
878
+ const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
879
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
880
+ if (cardResult === null)
881
+ return null;
882
+ return { kind: 'command.result', text: cardResult };
883
+ }
884
+ // 降级:文本
885
+ const list = available.map((a) => `${a === activeBaseagent ? ' ✓' : ' '} ${a}`).join('\n');
886
+ const canSwitchAgent = isOwner;
887
+ if (canSwitchAgent) {
888
+ return { kind: 'command.result', text: `当前 Baseagent: ${activeBaseagent}\n\n可用:\n${list}\n用法: /baseagent <name>` };
889
+ }
890
+ return { kind: 'command.result', text: `当前 Baseagent: ${activeBaseagent}` };
891
+ }
892
+ if (!available.includes(args)) {
893
+ return { kind: 'command.error', text: `❌ 未知 Agent: ${args}\n可用: ${available.join(', ')}` };
894
+ }
895
+ if (!owningAgent) {
896
+ return { kind: 'command.error', text: '❌ 当前 channel 无绑定 agent,无法设置 active_baseagent' };
897
+ }
898
+ const previousDefaultBaseagent = owningAgent.baseagent || this.parseDefaultBaseagent();
899
+ owningAgent.setActiveBaseagent(args);
900
+ this.eventBus.publish({
901
+ type: 'agent:baseagent-changed',
902
+ aid: owningAgent.aid,
903
+ baseagent: args,
904
+ previousBaseagent: previousDefaultBaseagent,
905
+ scope: 'agent',
906
+ timestamp: Date.now(),
907
+ });
908
+ const projectName = this.getProjectName(owningAgent.projectPath);
909
+ const agentSwitchResponse = [
910
+ `✓ 已设置 Baseagent: ${args}`,
911
+ ` Agent: ${owningAgent.name}`,
912
+ ` 项目: ${projectName}`,
913
+ ].join('\n');
914
+ return { kind: 'command.result', text: agentSwitchResponse };
915
+ }
916
+ // /model 命令:查看或切换模型/推理强度
917
+ if (normalizedContent.startsWith('/model')) {
918
+ const args = normalizedContent.slice(6).trim();
919
+ if (args === 'reset' || args === 'check') {
920
+ const modelSession = await getExistingSessionForCommand();
921
+ const fallbackBaseagent = modelSession?.baseagent || this.agentRegistry?.resolveByChannel(channel)?.baseagent || this.parseDefaultBaseagent();
922
+ const modelAgent = this.getAgent(channel, fallbackBaseagent);
923
+ const modelChatType = modelSession?.chatType
924
+ ?? (chatType === 'private' || chatType === 'group' ? chatType : undefined);
925
+ const peerKeyId = modelChatType === 'group'
926
+ ? (modelSession?.metadata?.groupId || channelId)
927
+ : userId;
928
+ const channelType = this.resolveChannelType(channel);
929
+ const peerKey = channelType && peerKeyId ? formatPeerKey(channelType, peerKeyId) : undefined;
930
+ const self = selfAID ?? modelSession?.selfAID ?? this.resolveSelfAID(channel);
931
+ if (args === 'check') {
932
+ const checkAuthDenied = await authorizeIntent({
933
+ intent: {
934
+ operation: 'model.check',
935
+ scope: 'agent',
936
+ source: 'slash',
937
+ args: self ? { self } : {},
938
+ },
939
+ identity,
940
+ session: modelSession,
941
+ explicitChatType: modelChatType,
942
+ channel,
943
+ channelId,
944
+ userId,
945
+ selfAid: self,
946
+ isDaemonOwner,
947
+ });
948
+ if (checkAuthDenied)
949
+ return checkAuthDenied;
950
+ try {
951
+ const result = await runModelCheck({
952
+ self,
953
+ peerKey,
954
+ role: modelSession?.identity?.role || identity.role,
955
+ baseagent: modelAgent.name,
956
+ });
957
+ return {
958
+ kind: 'command.result',
959
+ text: formatModelCheck(result),
960
+ structured: {
961
+ ok: true,
962
+ steps: result.steps,
963
+ configuredModel: result.configuredModel ?? null,
964
+ configModelAvailable: result.configModelAvailable ?? null,
965
+ availableModels: result.availableModels,
966
+ catalogSource: result.catalogSource,
967
+ },
968
+ };
969
+ }
970
+ catch (error) {
971
+ return { kind: 'command.error', text: `模型网关检查失败: ${error instanceof Error ? error.message : String(error)}` };
972
+ }
973
+ }
974
+ const resetAuthDenied = await authorizeIntent({
975
+ intent: {
976
+ operation: 'model.reset',
977
+ scope: 'relation',
978
+ source: 'slash',
979
+ args: buildSlashRelationIntentArgs({ selfAid: self, peerKey }),
980
+ },
981
+ identity,
982
+ session: modelSession,
983
+ explicitChatType: modelChatType,
984
+ channel,
985
+ channelId,
986
+ userId,
987
+ selfAid: self,
988
+ isDaemonOwner,
989
+ });
990
+ if (resetAuthDenied)
991
+ return resetAuthDenied;
992
+ const target = resolveSlashRelationTarget.call(this, {
993
+ session: modelSession,
994
+ channel,
995
+ channelId,
996
+ userId,
997
+ selfAID: self,
998
+ role: modelSession?.identity?.role || identity.role,
999
+ chatType: modelChatType,
1000
+ });
1001
+ if ('error' in target)
1002
+ return { kind: 'command.error', text: `Cannot locate relation scope: ${target.error}` };
1003
+ try {
1004
+ writeScope('relation', target, modelAgent.name, { model: null, effort: null });
1005
+ }
1006
+ catch (error) {
1007
+ return { kind: 'command.error', text: `Failed to reset relation model settings: ${error?.message || error}` };
1008
+ }
1009
+ this.eventBus.publish({
1010
+ type: 'runner:model-changed',
1011
+ sessionId: modelSession?.id,
1012
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name,
1013
+ baseagent: modelSession?.baseagent || modelAgent.name,
1014
+ timestamp: Date.now(),
1015
+ });
1016
+ return {
1017
+ kind: 'command.result',
1018
+ text: '✓ 已清除当前关系的模型和推理强度设置',
1019
+ structured: { ok: true, scope: 'relation', self: target.self, peerKey: target.peerKey },
1020
+ };
1021
+ }
1022
+ if (!args || args === 'list' || args === 'current') {
1023
+ const modelSession = await getExistingSessionForCommand();
1024
+ const fallbackBaseagent = modelSession?.baseagent || this.agentRegistry?.resolveByChannel(channel)?.baseagent || this.parseDefaultBaseagent();
1025
+ const modelAgent = this.getAgent(channel, fallbackBaseagent);
1026
+ const modelTarget = resolveSlashRelationTarget.call(this, {
1027
+ session: modelSession,
1028
+ channel,
1029
+ channelId,
1030
+ userId,
1031
+ selfAID,
1032
+ role: modelSession?.identity?.role || identity.role,
1033
+ chatType,
1034
+ });
1035
+ const modelSelector = 'error' in modelTarget
1036
+ ? { self: selfAID ?? modelSession?.selfAID ?? this.resolveSelfAID(channel) }
1037
+ : modelTarget;
1038
+ if (args === 'list' || args === 'current') {
1039
+ const readAuthDenied = await authorizeIntent({
1040
+ intent: {
1041
+ operation: args === 'list' ? 'model.list' : 'model.current',
1042
+ scope: 'relation',
1043
+ source: 'slash',
1044
+ args: 'error' in modelTarget
1045
+ ? { self: modelSelector.self }
1046
+ : buildSlashRelationIntentArgs({ selfAid: modelTarget.self, peerKey: modelTarget.peerKey }),
1047
+ },
1048
+ identity,
1049
+ session: modelSession,
1050
+ explicitChatType: modelSession?.chatType,
1051
+ channel,
1052
+ channelId,
1053
+ userId,
1054
+ selfAid: modelSelector.self,
1055
+ isDaemonOwner,
1056
+ });
1057
+ if (readAuthDenied)
1058
+ return readAuthDenied;
1059
+ }
1060
+ let modelConfig = {};
1061
+ try {
1062
+ modelConfig = (resolveEffective(modelSelector, { cache: true }).baseagents || {})[fallbackBaseagent] || {};
1063
+ }
1064
+ catch { }
1065
+ const configuredModel = modelConfig.model;
1066
+ const configuredEffort = modelConfig[fallbackBaseagent === 'codex' ? 'reasoning' : 'effort'];
1067
+ const rawModels = hasModelSwitcher(modelAgent) ? await modelAgent.listModels() : [];
1068
+ const models = hasModelSwitcher(modelAgent)
1069
+ ? filterModelsForRole(identity.role, fallbackBaseagent, rawModels, modelAgent.resolveModelId?.bind(modelAgent))
1070
+ : rawModels;
1071
+ const currentModel = hasModelSwitcher(modelAgent) ? (configuredModel || modelAgent.getModel()) : modelAgent.name;
1072
+ const efforts = getAvailableEfforts(modelAgent, currentModel);
1073
+ const currentEffort = configuredEffort ?? modelAgent.getEffort?.() ?? 'auto';
1074
+ const canReadModelList = await canReadSlashModelList({
1075
+ subject: authSubject,
1076
+ identity,
1077
+ session: modelSession,
1078
+ explicitChatType: chatType === 'private' || chatType === 'group' ? chatType : undefined,
1079
+ channel,
1080
+ channelId,
1081
+ userId,
1082
+ selfAid: selfAID ?? this.resolveSelfAID(channel),
1083
+ isDaemonOwner,
1084
+ });
1085
+ // 尝试发送 CommandCard 卡片
1086
+ if (!args && canReadModelList && this.interactionRouter && models.length > 0) {
1087
+ const interaction = {
1088
+ type: 'interaction',
1089
+ id: `model-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1090
+ channelId,
1091
+ sessionId: modelSession?.id || `model-${Date.now()}`,
1092
+ initiatorId: userId,
1093
+ kind: {
1094
+ kind: 'command-card',
1095
+ title: '🤖 切换模型',
1096
+ buttons: models.map((m) => {
1097
+ const display = modelDisplayLabel(modelAgent, m);
1098
+ return {
1099
+ label: modelMatches(modelAgent, m, currentModel) ? `✓ ${display}` : display,
1100
+ command: `/model ${m}`,
1101
+ style: (modelMatches(modelAgent, m, currentModel) ? 'primary' : 'default'),
1102
+ disabled: modelMatches(modelAgent, m, currentModel),
1103
+ };
1104
+ }),
1105
+ },
1106
+ };
1107
+ const replyCtx = modelSession ? this.getReplyContext(modelSession) : undefined;
1108
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1109
+ if (cardResult === null)
1110
+ return null;
1111
+ return { kind: 'command.result', text: cardResult };
1112
+ }
1113
+ // 降级:文本
1114
+ const modelList = models.map((m) => ` ${modelMatches(modelAgent, m, currentModel) ? '✓' : ' '} ${modelDisplayLabel(modelAgent, m)}`).join('\n');
1115
+ const effortHint = efforts.length > 0
1116
+ ? `\n推理强度: ${currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort} (使用 /effort 调整)`
1117
+ : '';
1118
+ if (args === 'current') {
1119
+ return {
1120
+ kind: 'command.result',
1121
+ text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}`,
1122
+ structured: { ok: true, model: currentModel, effort: currentEffort, baseagent: fallbackBaseagent },
1123
+ };
1124
+ }
1125
+ if (canReadModelList) {
1126
+ const usage = !args && isAdmin ? '\n\n用法: /model <模型>' : '';
1127
+ return {
1128
+ kind: 'command.result',
1129
+ text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}\n\n可用模型:\n${modelList}${usage}`,
1130
+ ...(args === 'list' ? { structured: { ok: true, model: currentModel, effort: currentEffort, baseagent: fallbackBaseagent, models } } : {}),
1131
+ };
1132
+ }
1133
+ return { kind: 'command.result', text: `当前模型: ${modelDisplayLabel(modelAgent, currentModel)}${effortHint}` };
1134
+ }
1135
+ const modelSession = await getExistingSessionForCommand();
1136
+ const modelChatType = modelSession?.chatType
1137
+ ?? (chatType === 'private' || chatType === 'group' ? chatType : undefined);
1138
+ const peerKeyId = modelChatType === 'group'
1139
+ ? (modelSession?.metadata?.groupId || channelId)
1140
+ : userId;
1141
+ const channelType = this.resolveChannelType(channel);
1142
+ const peerKey = channelType && peerKeyId ? formatPeerKey(channelType, peerKeyId) : undefined;
1143
+ const earlyUseAuthDenied = await authorizeIntent({
1144
+ intent: {
1145
+ operation: 'model.use',
1146
+ scope: 'relation',
1147
+ source: 'slash',
1148
+ args: buildSlashRelationIntentArgs({
1149
+ args: { model: args.split(/\s+/)[0] },
1150
+ selfAid: selfAID ?? this.resolveSelfAID(channel),
1151
+ peerKey,
1152
+ }),
1153
+ },
1154
+ identity,
1155
+ session: modelSession,
1156
+ explicitChatType: modelChatType,
1157
+ channel,
1158
+ channelId,
1159
+ userId,
1160
+ selfAid: selfAID ?? this.resolveSelfAID(channel),
1161
+ isDaemonOwner,
1162
+ });
1163
+ if (earlyUseAuthDenied) {
1164
+ return {
1165
+ kind: 'command.error',
1166
+ text: earlyUseAuthDenied.text.includes('无权限') ? earlyUseAuthDenied.text : `❌ 无权限:${earlyUseAuthDenied.text}`,
1167
+ };
1168
+ }
1169
+ // 切换模型写入 agent/baseagent 配置;没有活跃会话时也不应创建空会话。
1170
+ const fallbackBaseagent = modelSession?.baseagent || this.agentRegistry?.resolveByChannel(channel)?.baseagent || this.parseDefaultBaseagent();
1171
+ const modelAgent = this.getAgent(channel, fallbackBaseagent);
1172
+ const relationTarget = resolveSlashRelationTarget.call(this, {
1173
+ session: modelSession,
1174
+ channel,
1175
+ channelId,
1176
+ userId,
1177
+ selfAID,
1178
+ role: modelSession?.identity?.role || identity.role,
1179
+ chatType: modelChatType,
1180
+ });
1181
+ const modelSelector = 'error' in relationTarget
1182
+ ? { self: selfAID ?? modelSession?.selfAID ?? this.resolveSelfAID(channel) }
1183
+ : relationTarget;
1184
+ let configuredModel;
1185
+ try {
1186
+ configuredModel = (resolveEffective(modelSelector, { cache: true }).baseagents || {})[fallbackBaseagent]?.model;
1187
+ }
1188
+ catch { }
1189
+ const models = hasModelSwitcher(modelAgent) ? await modelAgent.listModels() : [];
1190
+ const allowedModels = hasModelSwitcher(modelAgent)
1191
+ ? filterModelsForRole(identity.role, fallbackBaseagent, models, modelAgent.resolveModelId?.bind(modelAgent))
1192
+ : models;
1193
+ const parts = args.split(/\s+/);
1194
+ let newModel;
1195
+ let newEffort;
1196
+ if (parts.length === 1) {
1197
+ const arg = parts[0];
1198
+ const currentModel = hasModelSwitcher(modelAgent) ? (configuredModel || modelAgent.getModel()) : modelAgent.name;
1199
+ const efforts = getAvailableEfforts(modelAgent, currentModel);
1200
+ // effort 相关参数统一转发到 /effort
1201
+ if (efforts.includes(arg) || arg === 'auto') {
1202
+ const delegated = await this.handle(`/effort ${arg}`, channel, channelId, undefined, userId, threadId);
1203
+ return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
1204
+ }
1205
+ else if (allEfforts.includes(arg)) {
1206
+ return { kind: 'command.error', text: `⚠️ 请使用 /effort ${arg} 调整推理强度` };
1207
+ }
1208
+ else {
1209
+ const resolvedArg = hasModelSwitcher(modelAgent) ? (modelAgent.resolveModelId?.(arg) ?? arg) : arg;
1210
+ if (models.includes(resolvedArg)) {
1211
+ newModel = resolvedArg;
1212
+ }
1213
+ else if (models.includes(arg)) {
1214
+ newModel = arg;
1215
+ }
1216
+ else {
1217
+ const modelList = allowedModels.map((m) => ` ${modelMatches(modelAgent, m, currentModel) ? '✓' : ' '} ${modelDisplayLabel(modelAgent, m)}`).join('\n');
1218
+ const effortHint = efforts.length > 0 ? `\n\n推理强度请使用 /effort 命令` : '';
1219
+ return { kind: 'command.error', text: `❌ 无效参数: ${arg}\n\n可用模型:\n${modelList}${effortHint}` };
1220
+ }
1221
+ }
1222
+ }
1223
+ else {
1224
+ // 双参数:model effort
1225
+ const [modelArgRaw, effortArg] = parts;
1226
+ const modelArg = hasModelSwitcher(modelAgent)
1227
+ ? (models.includes(modelArgRaw) ? modelArgRaw : (modelAgent.resolveModelId?.(modelArgRaw) ?? modelArgRaw))
1228
+ : modelArgRaw;
1229
+ if (!models.includes(modelArg)) {
1230
+ return { kind: 'command.error', text: `❌ 无效的模型ID: ${modelArgRaw}` };
1231
+ }
1232
+ const targetEfforts = getAvailableEfforts(modelAgent, modelArg);
1233
+ if (targetEfforts.length === 0) {
1234
+ return { kind: 'command.error', text: `⚠️ ${modelArg} 不支持推理强度设置` };
1235
+ }
1236
+ if (!targetEfforts.includes(effortArg)) {
1237
+ const errorLabel = allEfforts.includes(effortArg) ? '⚠️' : '❌';
1238
+ return { kind: 'command.result', text: `${errorLabel} ${modelArg} 不支持 ${effortArg} 推理强度\n可选: ${targetEfforts.join(' / ')}` };
1239
+ }
1240
+ newModel = modelArg;
1241
+ newEffort = effortArg;
1242
+ }
1243
+ // 优先写关系级覆盖;只有无法定位关系且具备管理权限时才回退到 runner/agent 级。
1244
+ if (newModel) {
1245
+ const modelChatType = modelSession?.chatType
1246
+ ?? (chatType === 'private' || chatType === 'group' ? chatType : undefined);
1247
+ const peerKeyId = modelChatType === 'group'
1248
+ ? (modelSession?.metadata?.groupId || channelId)
1249
+ : userId;
1250
+ const channelType = this.resolveChannelType(channel);
1251
+ const peerKey = channelType && peerKeyId ? formatPeerKey(channelType, peerKeyId) : undefined;
1252
+ const useAuthDenied = await authorizeIntent({
1253
+ intent: {
1254
+ operation: 'model.use',
1255
+ scope: 'relation',
1256
+ source: 'slash',
1257
+ args: buildSlashRelationIntentArgs({
1258
+ args: { model: newModel },
1259
+ selfAid: selfAID ?? this.resolveSelfAID(channel),
1260
+ peerKey,
1261
+ }),
1262
+ },
1263
+ identity,
1264
+ session: modelSession,
1265
+ explicitChatType: modelChatType,
1266
+ channel,
1267
+ channelId,
1268
+ userId,
1269
+ selfAid: selfAID ?? this.resolveSelfAID(channel),
1270
+ isDaemonOwner,
1271
+ });
1272
+ if (useAuthDenied)
1273
+ return useAuthDenied;
1274
+ const decision = validateModelSelectionForRole({
1275
+ role: modelSession?.identity?.role || identity.role,
1276
+ baseagent: fallbackBaseagent,
1277
+ requestedModel: newModel,
1278
+ models,
1279
+ resolveModelId: modelAgent.resolveModelId?.bind(modelAgent),
1280
+ });
1281
+ if (!decision.ok) {
1282
+ return { kind: 'command.error', text: decision.message || `invalid model: ${newModel}` };
1283
+ }
1284
+ newModel = decision.model || newModel;
1285
+ }
1286
+ const changes = [];
1287
+ if (!('error' in relationTarget)) {
1288
+ try {
1289
+ writeScope('relation', {
1290
+ self: relationTarget.self,
1291
+ peerKey: relationTarget.peerKey,
1292
+ role: relationTarget.role,
1293
+ }, modelAgent.name, {
1294
+ model: newModel,
1295
+ effort: newEffort,
1296
+ });
1297
+ }
1298
+ catch (e) {
1299
+ return { kind: 'command.error', text: `Failed to update relation model settings: ${e?.message || e}` };
1300
+ }
1301
+ if (newModel)
1302
+ changes.push(`模型: ${newModel}`);
1303
+ if (newEffort)
1304
+ changes.push(`推理强度: ${newEffort}`);
1305
+ this.eventBus.publish({
1306
+ type: 'runner:model-changed',
1307
+ sessionId: modelSession?.id,
1308
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name,
1309
+ baseagent: modelSession?.baseagent || modelAgent.name,
1310
+ model: newModel,
1311
+ effort: newEffort,
1312
+ timestamp: Date.now()
1313
+ });
1314
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1315
+ return null;
1316
+ return { kind: 'command.result', text: `✓ 已切换\n ${changes.join('\n ')}` };
1317
+ }
1318
+ if (!isAdmin) {
1319
+ return { kind: 'command.error', text: `Cannot locate relation scope: ${relationTarget.error}` };
1320
+ }
1321
+ if (newModel) {
1322
+ modelAgent.setModel?.(newModel);
1323
+ this.eventBus.publish({
1324
+ type: 'runner:model-changed',
1325
+ sessionId: modelSession?.id,
1326
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name,
1327
+ baseagent: modelSession?.baseagent || modelAgent.name,
1328
+ model: newModel,
1329
+ effort: newEffort,
1330
+ timestamp: Date.now()
1331
+ });
1332
+ changes.push(`模型: ${newModel}`);
1333
+ }
1334
+ if (newEffort) {
1335
+ modelAgent.setEffort?.(newEffort);
1336
+ if (!newModel) {
1337
+ this.eventBus.publish({
1338
+ type: 'runner:model-changed',
1339
+ sessionId: modelSession?.id,
1340
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name,
1341
+ baseagent: modelSession?.baseagent || modelAgent.name,
1342
+ effort: newEffort,
1343
+ timestamp: Date.now(),
1344
+ });
1345
+ }
1346
+ changes.push(`推理强度: ${newEffort}`);
1347
+ }
1348
+ // 持久化:agent-owned channel 写到 agent.json;default 走原"就近原则"
1349
+ if (newModel) {
1350
+ const err = this.persistBaseagentModel(channel, modelAgent.name, newModel);
1351
+ if (err)
1352
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1353
+ }
1354
+ if (newEffort) {
1355
+ const err = this.persistBaseagentEffort(channel, modelAgent.name, newEffort);
1356
+ if (err)
1357
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1358
+ }
1359
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1360
+ return null;
1361
+ return { kind: 'command.result', text: `✓ 已切换\n ${changes.join('\n ')}` };
1362
+ }
1363
+ // /effort 命令:查看或切换推理强度
1364
+ if (normalizedContent.startsWith('/effort')) {
1365
+ const args = normalizedContent.slice(7).trim();
1366
+ if (!args) {
1367
+ const effortSession = await getExistingSessionForCommand();
1368
+ const fallbackBaseagent = effortSession?.baseagent || this.agentRegistry?.resolveByChannel(channel)?.baseagent || this.parseDefaultBaseagent();
1369
+ const effortAgent = this.getAgent(channel, fallbackBaseagent);
1370
+ const effortTarget = resolveSlashRelationTarget.call(this, {
1371
+ session: effortSession,
1372
+ channel,
1373
+ channelId,
1374
+ userId,
1375
+ selfAID,
1376
+ role: effortSession?.identity?.role || identity.role,
1377
+ chatType,
1378
+ });
1379
+ const effortSelector = 'error' in effortTarget
1380
+ ? { self: selfAID ?? effortSession?.selfAID ?? this.resolveSelfAID(channel) }
1381
+ : effortTarget;
1382
+ let modelConfig = {};
1383
+ try {
1384
+ modelConfig = (resolveEffective(effortSelector, { cache: true }).baseagents || {})[fallbackBaseagent] || {};
1385
+ }
1386
+ catch { }
1387
+ const configuredModel = modelConfig.model;
1388
+ const configuredEffort = modelConfig[fallbackBaseagent === 'codex' ? 'reasoning' : 'effort'];
1389
+ const currentModel = hasModelSwitcher(effortAgent) ? (configuredModel || effortAgent.getModel()) : effortAgent.name;
1390
+ const efforts = getAvailableEfforts(effortAgent, currentModel);
1391
+ const currentEffort = configuredEffort ?? effortAgent.getEffort?.() ?? 'auto';
1392
+ if (efforts.length === 0) {
1393
+ return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
1394
+ }
1395
+ // /effort(无参数):显示当前推理强度 + 发送 CommandCard 卡片
1396
+ if (this.interactionRouter) {
1397
+ const allItems = [...efforts, 'auto'];
1398
+ const interaction = {
1399
+ type: 'interaction',
1400
+ id: `effort-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1401
+ channelId,
1402
+ sessionId: effortSession?.id || `effort-${Date.now()}`,
1403
+ initiatorId: userId,
1404
+ kind: {
1405
+ kind: 'command-card',
1406
+ title: '⚡ 推理强度',
1407
+ buttons: allItems.map(e => ({
1408
+ label: e === currentEffort ? `✓ ${e}` : e,
1409
+ command: `/effort ${e}`,
1410
+ style: (e === currentEffort ? 'primary' : 'default'),
1411
+ disabled: e === currentEffort,
1412
+ })),
1413
+ },
1414
+ };
1415
+ const replyCtx = effortSession ? this.getReplyContext(effortSession) : undefined;
1416
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1417
+ if (cardResult === null)
1418
+ return null;
1419
+ return { kind: 'command.result', text: cardResult };
1420
+ }
1421
+ // 降级:文本
1422
+ const effortDisplay = currentEffort === 'auto' ? 'auto (SDK默认)' : currentEffort;
1423
+ const effortOptions = [...efforts, 'auto'].join(' / ');
1424
+ if (isAdmin) {
1425
+ return { kind: 'command.result', text: `推理强度: ${effortDisplay} 可选: ${effortOptions} 用法: /effort <level>` };
1426
+ }
1427
+ return { kind: 'command.result', text: `推理强度: ${effortDisplay}` };
1428
+ }
1429
+ const effortSession = await getExistingSessionForCommand();
1430
+ const fallbackBaseagent = effortSession?.baseagent || this.agentRegistry?.resolveByChannel(channel)?.baseagent || this.parseDefaultBaseagent();
1431
+ const effortAgent = this.getAgent(channel, fallbackBaseagent);
1432
+ const relationTarget = resolveSlashRelationTarget.call(this, {
1433
+ session: effortSession,
1434
+ channel,
1435
+ channelId,
1436
+ userId,
1437
+ selfAID,
1438
+ role: effortSession?.identity?.role || identity.role,
1439
+ chatType,
1440
+ });
1441
+ const effortSelector = 'error' in relationTarget
1442
+ ? { self: selfAID ?? effortSession?.selfAID ?? this.resolveSelfAID(channel) }
1443
+ : relationTarget;
1444
+ let modelConfig = {};
1445
+ try {
1446
+ modelConfig = (resolveEffective(effortSelector, { cache: true }).baseagents || {})[fallbackBaseagent] || {};
1447
+ }
1448
+ catch { }
1449
+ const configuredModel = modelConfig.model;
1450
+ const configuredEffort = modelConfig[fallbackBaseagent === 'codex' ? 'reasoning' : 'effort'];
1451
+ const currentModel = hasModelSwitcher(effortAgent) ? (configuredModel || effortAgent.getModel()) : effortAgent.name;
1452
+ const efforts = getAvailableEfforts(effortAgent, currentModel);
1453
+ const currentEffort = configuredEffort ?? effortAgent.getEffort?.() ?? 'auto';
1454
+ if (efforts.length === 0) {
1455
+ return { kind: 'command.error', text: '⚠️ 当前模型不支持推理强度设置' };
1456
+ }
1457
+ // Non-admin users write effort overrides to their relation scope below.
1458
+ // /effort auto:恢复 SDK 默认
1459
+ if (args === 'auto') {
1460
+ if (!('error' in relationTarget)) {
1461
+ try {
1462
+ writeScope('relation', {
1463
+ self: relationTarget.self,
1464
+ peerKey: relationTarget.peerKey,
1465
+ role: relationTarget.role,
1466
+ }, effortAgent.name, { effort: null });
1467
+ }
1468
+ catch (e) {
1469
+ return { kind: 'command.error', text: `Failed to update relation effort: ${e?.message || e}` };
1470
+ }
1471
+ this.eventBus.publish({
1472
+ type: 'runner:model-changed',
1473
+ sessionId: effortSession?.id,
1474
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name,
1475
+ baseagent: effortSession?.baseagent || effortAgent.name,
1476
+ effort: 'auto',
1477
+ timestamp: Date.now(),
1478
+ });
1479
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1480
+ return null;
1481
+ return { kind: 'command.result', text: '✓ 推理强度已恢复为 auto (SDK默认)' };
1482
+ }
1483
+ if (!isAdmin)
1484
+ return { kind: 'command.error', text: `Cannot locate relation scope: ${relationTarget.error}` };
1485
+ effortAgent.setEffort?.(undefined);
1486
+ const err = this.persistBaseagentEffort(channel, effortAgent.name, undefined);
1487
+ if (err)
1488
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1489
+ this.eventBus.publish({
1490
+ type: 'runner:model-changed',
1491
+ sessionId: effortSession?.id,
1492
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name,
1493
+ baseagent: effortSession?.baseagent || effortAgent.name,
1494
+ effort: 'auto',
1495
+ timestamp: Date.now(),
1496
+ });
1497
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1498
+ return null;
1499
+ return { kind: 'command.result', text: '✓ 推理强度已恢复为 auto (SDK默认)' };
1500
+ }
1501
+ // /effort <level>:切换推理强度
1502
+ if (!efforts.includes(args)) {
1503
+ if (allEfforts.includes(args)) {
1504
+ return { kind: 'command.error', text: `⚠️ ${currentModel} 不支持 ${args} 推理强度\n可选: ${efforts.join(' / ')}` };
1505
+ }
1506
+ return { kind: 'command.error', text: `❌ 无效参数: ${args}\n可选: ${efforts.join(' / ')} / auto` };
1507
+ }
1508
+ const newEffort = args;
1509
+ if (!('error' in relationTarget)) {
1510
+ try {
1511
+ writeScope('relation', {
1512
+ self: relationTarget.self,
1513
+ peerKey: relationTarget.peerKey,
1514
+ role: relationTarget.role,
1515
+ }, effortAgent.name, { effort: newEffort });
1516
+ }
1517
+ catch (e) {
1518
+ return { kind: 'command.error', text: `Failed to update relation effort: ${e?.message || e}` };
1519
+ }
1520
+ this.eventBus.publish({
1521
+ type: 'runner:model-changed',
1522
+ sessionId: effortSession?.id,
1523
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name,
1524
+ baseagent: effortSession?.baseagent || effortAgent.name,
1525
+ effort: newEffort,
1526
+ timestamp: Date.now(),
1527
+ });
1528
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1529
+ return null;
1530
+ return { kind: 'command.result', text: `✓ 推理强度: ${newEffort}` };
1531
+ }
1532
+ if (!isAdmin)
1533
+ return { kind: 'command.error', text: `Cannot locate relation scope: ${relationTarget.error}` };
1534
+ effortAgent.setEffort?.(newEffort);
1535
+ const err = this.persistBaseagentEffort(channel, effortAgent.name, newEffort);
1536
+ if (err)
1537
+ return { kind: 'command.result', text: `${err}\n已更新运行时配置,但未持久化` };
1538
+ this.eventBus.publish({
1539
+ type: 'runner:model-changed',
1540
+ sessionId: effortSession?.id,
1541
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name,
1542
+ baseagent: effortSession?.baseagent || effortAgent.name,
1543
+ effort: newEffort,
1544
+ timestamp: Date.now(),
1545
+ });
1546
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1547
+ return null;
1548
+ return { kind: 'command.result', text: `✓ 推理强度: ${newEffort}` };
1549
+ }
1550
+ // /reload [aid] — 热重载 agent 配置
1551
+ // daemon owner:可 reload 任意 aid(无参则 reload 自身所在 agent)
1552
+ // agent channel owner/admin:仅可 reload 自身 agent
1553
+ if (normalizedContent === '/reload' || normalizedContent.startsWith('/reload ')) {
1554
+ const aidArg = normalizedContent.slice('/reload'.length).trim() || undefined;
1555
+ const selfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
1556
+ // agent channel 的 owner/admin 不能跨 agent reload;先返回领域内错误,避免落到
1557
+ // 底层 daemon-owner 约束的英文 reason。
1558
+ if (!isDaemonOwner && aidArg && aidArg !== selfAid) {
1559
+ return { kind: 'command.error', text: '❌ 无权限:跨 agent reload 仅限 daemon owner 使用' };
1560
+ }
1561
+ const reloadScope = 'agent';
1562
+ const authDenied = await authorizeIntent({
1563
+ intent: {
1564
+ operation: 'agent.reload',
1565
+ scope: reloadScope,
1566
+ source: 'slash',
1567
+ args: { ...(aidArg ? { aid: aidArg } : {}), ...(selfAid ? { self: selfAid } : {}) },
1568
+ dangerous: true,
1569
+ },
1570
+ identity: isDaemonOwner ? { ...identity, role: 'owner' } : identity,
1571
+ session: activeSession,
1572
+ channel,
1573
+ channelId,
1574
+ userId,
1575
+ selfAid,
1576
+ isDaemonOwner,
1577
+ });
1578
+ if (authDenied)
1579
+ return authDenied;
1580
+ // 权限判断:daemon owner 或 agent channel 的 owner/admin
1581
+ if (!isDaemonOwner && !isAdmin) {
1582
+ return { kind: 'command.error', text: '❌ 无权限:/reload 仅限 daemon owner 或 agent owner/admin 使用' };
1583
+ }
1584
+ const targetAid = aidArg ?? selfAid;
1585
+ if (!targetAid) {
1586
+ return { kind: 'command.error', text: '❌ 无法确定目标 agent,请指定 aid:/reload <aid>' };
1587
+ }
1588
+ // 繁忙检查(同 menu /agent reload)
1589
+ const busyInfo = getAgentBusyInfo(this, targetAid);
1590
+ if (busyInfo && busyInfo.count > 0) {
1591
+ const processingLines = busyInfo.processing.map(p => {
1592
+ const sessionId = p.queueKey.split('::')[0] || '?';
1593
+ return ` · ${sessionId.slice(0, 32)}...`;
1594
+ }).join('\n');
1595
+ return { kind: 'command.error', text: `❌ 该 Agent 有 ${busyInfo.count} 个任务执行中,无法 reload。\n\n处理中:\n${processingLines}\n\n等待任务完成后重试,通常 30-60 秒。` };
1596
+ }
1597
+ const res = await execAgentAction('reload', { aid: targetAid }, userId ?? '', this.eventBus);
1598
+ if ('error' in res)
1599
+ return { kind: 'command.error', text: `❌ reload 失败:${res.error}` };
1600
+ return { kind: 'command.result', text: `✅ Agent ${targetAid} 配置已重载` };
1601
+ }
1602
+ // /agent, /aid, /rpc, /storage — 仅限顶级 CLI 调用,slash 输入拒绝
1603
+ if (normalizedContent === '/agent' || normalizedContent.startsWith('/agent ') ||
1604
+ normalizedContent === '/aid' || normalizedContent.startsWith('/aid ') ||
1605
+ normalizedContent === '/rpc' || normalizedContent.startsWith('/rpc ') ||
1606
+ normalizedContent === '/storage' || normalizedContent.startsWith('/storage ')) {
1607
+ return { kind: 'command.error', text: '❌ 此命令仅限 CLI 调用,不支持 slash 输入' };
1608
+ }
1609
+ if (normalizedContent === '/observable' || normalizedContent.startsWith('/observable ')) {
1610
+ if (!isOwner)
1611
+ return { kind: 'command.error', text: '❌ 观察者模式仅限 owner 查看和开关' };
1612
+ const value = normalizedContent.slice('/observable'.length).trim();
1613
+ if (!value) {
1614
+ const result = await this.execMenuQuery('/observable', channel, channelId, userId, undefined, narrowedChatType, false, identity);
1615
+ if ('error' in result)
1616
+ return { kind: 'command.error', text: `❌ ${result.error}` };
1617
+ return { kind: 'command.result', text: `观察者模式: ${result.data.observable ? 'true' : 'false'} 用法: /observable <true|false>` };
1618
+ }
1619
+ if (value !== 'true' && value !== 'false') {
1620
+ return { kind: 'command.error', text: `❌ 无效参数: ${value}\n用法: /observable <true|false>` };
1621
+ }
1622
+ const result = await this.execMenuUpdate('/observable', value, channel, channelId, userId, identity, false);
1623
+ if ('error' in result)
1624
+ return { kind: 'command.error', text: `❌ ${result.error}` };
1625
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1626
+ return null;
1627
+ return { kind: 'command.result', text: `✅ 观察者模式: ${result.data.observable ? 'true' : 'false'}` };
1628
+ }
1629
+ if (normalizedContent === '/activity' || normalizedContent.startsWith('/activity ')) {
1630
+ const activityArg = normalizedContent.slice(9).trim();
1631
+ // 带参(写操作)需 admin+;无参查询对所有人开放(owner 门在具体切换点还有一道)
1632
+ if (activityArg && !isAdmin)
1633
+ return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
1634
+ // proactive 模式下流式输出全部静默,activity 配置无意义
1635
+ if (getEffectiveChatmode(activeSession) === 'proactive') {
1636
+ return { kind: 'command.error', text: '❌ 当前会话为 proactive 模式,不支持 activity 配置(流式输出已全部静默)' };
1637
+ }
1638
+ const modeMap = {
1639
+ all: 'all',
1640
+ text: 'text',
1641
+ none: 'none',
1642
+ };
1643
+ const currentMode = this.agentRegistry?.getShowActivities?.(channel) ?? 'all';
1644
+ // 模式描述列表(用于 body 和文本降级)。
1645
+ // all = 文本 + activity;text = 仅文字进展;none = 全部静默。
1646
+ const modeDescriptions = [
1647
+ { key: 'all', configVal: 'all', label: '私聊显示' },
1648
+ { key: 'text', configVal: 'text', label: '仅文字进展' },
1649
+ { key: 'none', configVal: 'none', label: '全部静默' },
1650
+ ];
1651
+ if (!activityArg) {
1652
+ // 尝试发送 CommandCard 卡片
1653
+ {
1654
+ const interaction = {
1655
+ type: 'interaction',
1656
+ id: `activity-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1657
+ channelId,
1658
+ sessionId: activeSession?.id || '',
1659
+ initiatorId: userId,
1660
+ kind: {
1661
+ kind: 'command-card',
1662
+ title: '📋 中间输出模式',
1663
+ body: modeDescriptions.map(m => `${m.configVal === currentMode ? '✓' : '•'} **${m.key}** (${m.label})`).join('\n'),
1664
+ buttons: modeDescriptions.map(m => ({
1665
+ label: m.configVal === currentMode ? `✓ ${m.key}` : m.key,
1666
+ command: `/activity ${m.key}`,
1667
+ style: (m.configVal === currentMode ? 'primary' : 'default'),
1668
+ disabled: m.configVal === currentMode,
1669
+ })),
1670
+ },
1671
+ };
1672
+ const replyCtx = activeSession ? this.getReplyContext(activeSession) : undefined;
1673
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isOwner });
1674
+ if (cardResult === null)
1675
+ return null;
1676
+ // 卡片降级:fall through 到下方文本输出
1677
+ }
1678
+ // 降级:文本
1679
+ const modeList = modeDescriptions.map(m => {
1680
+ const prefix = m.configVal === currentMode ? '✓' : '•';
1681
+ return ` ${prefix} ${m.key} — ${m.label}`;
1682
+ }).join('\n');
1683
+ if (isOwner) {
1684
+ return { kind: 'command.result', text: `中间输出: ${currentMode} 用法: /activity <all|text|none>` };
1685
+ }
1686
+ return { kind: 'command.result', text: `中间输出: ${currentMode}` };
1687
+ }
1688
+ const newMode = modeMap[activityArg];
1689
+ if (!newMode) {
1690
+ return { kind: 'command.error', text: `❌ 无效参数: ${activityArg}\n可选: all / text / none` };
1691
+ }
1692
+ const label = modeDescriptions.find(m => m.configVal === newMode)?.label || newMode;
1693
+ if (newMode === currentMode) {
1694
+ return { kind: 'command.result', text: `📋 中间输出模式已是 ${activityArg}(${label})` };
1695
+ }
1696
+ // 切换操作仅 owner
1697
+ if (!isOwner)
1698
+ return { kind: 'command.error', text: '❌ 中间输出模式切换仅限 owner' };
1699
+ if (this.agentRegistry?.setShowActivities) {
1700
+ this.agentRegistry.setShowActivities(channel, newMode);
1701
+ }
1702
+ else {
1703
+ return { kind: 'command.error', text: `⚠️ 找不到通道 "${channel}" 所属的 self-agent,无法持久化` };
1704
+ }
1705
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1706
+ return null;
1707
+ return { kind: 'command.result', text: `✅ 中间输出模式: ${activityArg}(${label})` };
1708
+ }
1709
+ // /chatmode 命令:查看/切换当前 relation 的有效会话模式(interactive | proactive)
1710
+ // - 查看:所有人可用
1711
+ // - 设置:单聊任何角色可设置;群聊仅管理员可设置
1712
+ if (normalizedContent === '/chatmode' || normalizedContent.startsWith('/chatmode ')) {
1713
+ const arg = normalizedContent.slice(9).trim();
1714
+ if (!arg) {
1715
+ const existingChatmodeSession = await getExistingSessionForCommand();
1716
+ const currentMode = getEffectiveChatmode(existingChatmodeSession);
1717
+ // 尝试发送 CommandCard 卡片
1718
+ const modes = [
1719
+ { key: 'interactive', name: '交互模式', desc: '被动响应:收到消息时才回复,回复直接显示' },
1720
+ { key: 'proactive', name: '主动模式', desc: '主动推进:流式输出静默,由 Agent 自调 ctl send 发声' },
1721
+ ];
1722
+ const interaction = {
1723
+ type: 'interaction',
1724
+ id: `chatmode-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1725
+ channelId,
1726
+ sessionId: existingChatmodeSession?.id || `chatmode-${Date.now()}`,
1727
+ initiatorId: userId,
1728
+ kind: {
1729
+ kind: 'command-card',
1730
+ title: '🔄 会话模式',
1731
+ body: modes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.name}) - ${m.desc}`).join('\n'),
1732
+ buttons: modes.map(m => ({
1733
+ label: m.key === currentMode ? `✓ ${m.key}` : m.key,
1734
+ command: `/chatmode ${m.key}`,
1735
+ style: (m.key === currentMode ? 'primary' : 'default'),
1736
+ disabled: m.key === currentMode,
1737
+ })),
1738
+ },
1739
+ };
1740
+ const replyCtx = existingChatmodeSession ? this.getReplyContext(existingChatmodeSession) : undefined;
1741
+ const cardResult = await this.sendCommandCard({
1742
+ channel,
1743
+ channelId,
1744
+ interaction,
1745
+ replyCtx,
1746
+ canWrite: activeChatType !== 'group' || isAdmin,
1747
+ });
1748
+ if (cardResult === null)
1749
+ return null;
1750
+ // 卡片降级:fall through 到下方文本输出
1751
+ // 降级:文本
1752
+ return { kind: 'command.result', text: `会话模式: ${currentMode} 用法: /chatmode <interactive|proactive>` };
1753
+ }
1754
+ if (arg !== 'interactive' && arg !== 'proactive') {
1755
+ return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: interactive / proactive` };
1756
+ }
1757
+ if (activeChatType === 'group' && !isAdmin) {
1758
+ return { kind: 'command.error', text: '❌ 群聊中切换会话模式仅限管理员使用' };
1759
+ }
1760
+ const chatmodeSession = await getExistingSessionForCommand();
1761
+ const target = resolveSlashChatmodeTarget.call(this, {
1762
+ session: chatmodeSession,
1763
+ channel,
1764
+ channelId,
1765
+ userId,
1766
+ selfAID,
1767
+ role: chatmodeSession?.identity?.role || identity.role,
1768
+ chatType: chatmodeSession?.chatType || activeChatType,
1769
+ });
1770
+ if ('error' in target) {
1771
+ return { kind: 'command.error', text: `❌ ${target.error}` };
1772
+ }
1773
+ const currentMode = readSlashChatmode(target);
1774
+ if (arg === currentMode) {
1775
+ return { kind: 'command.result', text: `📋 当前会话模式已是 ${arg}` };
1776
+ }
1777
+ // 仅在真正需要切换时才要求会话空闲
1778
+ if (chatmodeSession && threadId) {
1779
+ const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
1780
+ if (threadSession) {
1781
+ const threadAgent = this.getAgent(channel, threadSession.baseagent);
1782
+ if (threadAgent.hasActiveStream(threadSession.id) || this.messageQueue?.isProcessing(threadSession.id)) {
1783
+ return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
1784
+ }
1785
+ }
1786
+ }
1787
+ else if (chatmodeSession && (getActiveAgent().hasActiveStream(chatmodeSession.id) || this.messageQueue?.isProcessing(chatmodeSession.id))) {
1788
+ return { kind: 'command.error', text: '⚠️ 当前正在处理消息,请稍后再试\n使用 /stop 中断当前任务后重试' };
1789
+ }
1790
+ const chatmodeAuthDenied = await authorizeIntent({
1791
+ intent: {
1792
+ operation: 'chatmode.update',
1793
+ scope: 'relation',
1794
+ source: 'slash',
1795
+ args: {
1796
+ value: arg,
1797
+ self: target.sel.self,
1798
+ peer: target.sel.peerKey,
1799
+ peerKey: target.sel.peerKey,
1800
+ },
1801
+ },
1802
+ identity,
1803
+ session: chatmodeSession,
1804
+ explicitChatType: activeChatType === 'group' ? 'group' : 'private',
1805
+ channel,
1806
+ channelId,
1807
+ userId,
1808
+ selfAid: target.sel.self,
1809
+ isDaemonOwner,
1810
+ });
1811
+ if (chatmodeAuthDenied)
1812
+ return chatmodeAuthDenied;
1813
+ try {
1814
+ writeSlashChatmode(target, arg);
1815
+ }
1816
+ catch (e) {
1817
+ return { kind: 'command.error', text: `Failed to update relation chatmode: ${e?.message || e}` };
1818
+ }
1819
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1820
+ return null;
1821
+ return { kind: 'command.result', text: `✅ 会话模式已切换: ${arg}` };
1822
+ }
1823
+ // /mentionmode 命令:查看/切换群聊 @ 处理模式(mention-only | disabled)
1824
+ // 仅群聊可用;群聊中设置需管理员权限
1825
+ if (normalizedContent === '/mentionmode' || normalizedContent.startsWith('/mentionmode ')) {
1826
+ const mentionSession = await getExistingSessionForCommand();
1827
+ if (!mentionSession)
1828
+ return { kind: 'command.error', text: '❌ 当前没有活跃会话' };
1829
+ const mentionChatType = mentionSession.chatType || activeChatType;
1830
+ if (mentionChatType !== 'group') {
1831
+ return { kind: 'command.error', text: '❌ /mentionmode 仅在群聊中可用' };
1832
+ }
1833
+ const arg = normalizedContent.slice('/mentionmode'.length).trim();
1834
+ const mentionTarget = resolveSlashMentionModeTarget.call(this, {
1835
+ session: mentionSession,
1836
+ channel,
1837
+ selfAID,
1838
+ role: mentionSession.identity?.role || identity.role,
1839
+ });
1840
+ if ('error' in mentionTarget) {
1841
+ return { kind: 'command.error', text: `❌ ${mentionTarget.error}` };
1842
+ }
1843
+ // session.metadata.dispatchMode 是 AUN 协议词汇(mention/broadcast),翻译成 mentionMode 词汇兜底
1844
+ const mentionFallback = dispatchToMentionMode(mentionSession.metadata?.dispatchMode) ?? null;
1845
+ const currentMode = readSlashMentionMode(mentionTarget, mentionFallback);
1846
+ if (!arg) {
1847
+ const displayMode = currentMode ?? '未设置(跟随群设置)';
1848
+ // 尝试发送 CommandCard 卡片
1849
+ if (isAdmin) {
1850
+ const modes = [
1851
+ { key: 'mention-only', name: '提及模式', desc: '仅当被 @ 提及(含 @all)时响应群消息' },
1852
+ { key: 'disabled', name: '全响应模式', desc: '群内所有消息都触发响应' },
1853
+ ];
1854
+ const interaction = {
1855
+ type: 'interaction',
1856
+ id: `mentionmode-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
1857
+ channelId,
1858
+ sessionId: mentionSession.id,
1859
+ initiatorId: userId,
1860
+ kind: {
1861
+ kind: 'command-card',
1862
+ title: '📡 @ 处理模式',
1863
+ body: modes.map(m => `${m.key === currentMode ? '✓' : '•'} **${m.key}** (${m.name}) - ${m.desc}`).join('\n'),
1864
+ buttons: modes.map(m => ({
1865
+ label: m.key === currentMode ? `✓ ${m.key}` : m.key,
1866
+ command: `/mentionmode ${m.key}`,
1867
+ style: (m.key === currentMode ? 'primary' : 'default'),
1868
+ disabled: m.key === currentMode,
1869
+ })),
1870
+ },
1871
+ };
1872
+ const replyCtx = this.getReplyContext(mentionSession);
1873
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx, canWrite: isAdmin });
1874
+ if (cardResult === null)
1875
+ return null;
1876
+ // 卡片降级:fall through 到下方文本输出
1877
+ }
1878
+ // 降级:文本
1879
+ if (isAdmin) {
1880
+ return { kind: 'command.result', text: `@ 处理模式: ${displayMode} 用法: /mentionmode <mention-only|disabled|clear>` };
1881
+ }
1882
+ return { kind: 'command.result', text: `@ 处理模式: ${displayMode}` };
1883
+ }
1884
+ if (arg !== 'mention-only' && arg !== 'disabled' && arg !== 'clear') {
1885
+ return { kind: 'command.error', text: `❌ 无效模式: ${arg}\n可选: mention-only / disabled / clear\n用法: /mentionmode <模式>` };
1886
+ }
1887
+ const mentionAuthDenied = await authorizeIntent({
1888
+ intent: {
1889
+ operation: 'mentionmode.update',
1890
+ scope: 'agent',
1891
+ source: 'slash',
1892
+ args: { value: arg, self: mentionTarget.sel.self },
1893
+ },
1894
+ identity,
1895
+ session: mentionSession,
1896
+ explicitChatType: 'group',
1897
+ channel,
1898
+ channelId,
1899
+ userId,
1900
+ selfAid: mentionTarget.sel.self,
1901
+ isDaemonOwner,
1902
+ });
1903
+ if (mentionAuthDenied)
1904
+ return mentionAuthDenied;
1905
+ if (arg === 'clear') {
1906
+ writeSlashMentionMode(mentionTarget, null);
1907
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1908
+ return null;
1909
+ return { kind: 'command.result', text: '✅ 已清除本地覆盖,将跟随群设置' };
1910
+ }
1911
+ if (arg === currentMode) {
1912
+ return { kind: 'command.result', text: `当前已是 ${arg}` };
1913
+ }
1914
+ writeSlashMentionMode(mentionTarget, arg);
1915
+ if (this.shouldSuppressCardTriggerResult(source, channel))
1916
+ return null;
1917
+ return { kind: 'command.result', text: `✅ @ 处理模式已切换: ${currentMode ?? '未设置'} → ${arg}` };
1918
+ }
1919
+ // /stop 命令:中断当前任务
1920
+ if (normalizedContent === '/stop') {
1921
+ const stopSession = await getExistingSessionForCommand();
1922
+ if (!stopSession)
1923
+ return { kind: 'command.result', text: '当前没有正在处理的任务' };
1924
+ const stopAgent = this.getAgent(channel, stopSession.baseagent);
1925
+ const sessionKey = stopSession.id;
1926
+ const queueLength = this.messageQueue.getQueueLength(sessionKey);
1927
+ const hasActive = stopAgent.hasActiveStream(sessionKey);
1928
+ const isProcessing = this.messageQueue.isProcessing(sessionKey);
1929
+ if (queueLength === 0 && !hasActive && !isProcessing) {
1930
+ return { kind: 'command.result', text: '当前没有正在处理的任务' };
1931
+ }
1932
+ // 发布中断事件,让 MessageProcessor 标记为 interrupted(而非 done)
1933
+ this.eventBus.publish({
1934
+ type: 'task:interrupted',
1935
+ sessionId: sessionKey,
1936
+ reason: 'stop',
1937
+ agentName: this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>',
1938
+ });
1939
+ await this.processor.interruptSession(sessionKey, 'stop');
1940
+ return { kind: 'command.result', text: '✓ 已发送中断信号,任务将尽快停止' };
1941
+ }
1942
+ // /clear 已移除:Claude/Codex/Gemini 对“清空当前 backend 历史”的语义不一致。
1943
+ // 统一使用 /new 创建新会话来开始全新上下文。
1944
+ if (normalizedContent === '/clear') {
1945
+ return { kind: 'command.error', text: '⚠️ /clear 已移除\n\n请使用 /new [名称] 创建新会话来开始全新上下文。旧会话会保留,可通过 /s 查看或切换。' };
1946
+ }
1947
+ // /compact 命令:手动压缩会话上下文
1948
+ if (normalizedContent === '/compact') {
1949
+ const session = await getExistingSessionForCommand();
1950
+ if (!session)
1951
+ return { kind: 'command.error', text: '❌ 当前没有活跃会话,无需压缩' };
1952
+ const sessionAgent = this.getAgent(channel, session.baseagent);
1953
+ if (!sessionAgent.capabilities?.compact) {
1954
+ return { kind: 'command.error', text: `❌ 当前 Agent (${sessionAgent.name}) 不支持 /compact` };
1955
+ }
1956
+ if (!session.agentSessionId) {
1957
+ return { kind: 'command.error', text: '❌ 当前会话没有历史记录,无需压缩' };
1958
+ }
1959
+ const projectPath = path.isAbsolute(session.projectPath)
1960
+ ? session.projectPath
1961
+ : path.resolve(process.cwd(), session.projectPath);
1962
+ const releaseLock = this.messageQueue.acquireLock(session.id);
1963
+ try {
1964
+ if (sendMessage) {
1965
+ await sendMessage(channelId, '⏳ 正在压缩会话上下文...', this.getReplyContext(session));
1966
+ }
1967
+ const compacted = await sessionAgent.compactSession(session.id, session.agentSessionId, projectPath);
1968
+ if (compacted) {
1969
+ return {
1970
+ kind: 'command.result',
1971
+ text: '✅ 会话压缩完成',
1972
+ };
1973
+ }
1974
+ else {
1975
+ return { kind: 'command.error', text: '❌ 会话压缩失败,请稍后重试' };
1976
+ }
1977
+ }
1978
+ finally {
1979
+ releaseLock();
1980
+ }
1981
+ }
1982
+ // 后续命令可读取现有会话,但不应在这里隐式创建新会话。
1983
+ // 真正需要新会话的命令应显式调用 createNewSession()。
1984
+ let session;
1985
+ if (threadId) {
1986
+ session = await this.sessionManager.getThreadSession(channel, channelId, threadId);
1987
+ }
1988
+ else {
1989
+ session = await this.sessionManager.getActiveSession(channel, channelId);
1990
+ }
1991
+ // /status 命令:显示会话状态
1992
+ if (normalizedContent === '/status') {
1993
+ if (!session) {
1994
+ return { kind: 'command.result', text: `📊 会话状态:当前没有活跃会话\n发送消息或使用 /new [名称] 创建会话` };
1995
+ }
1996
+ const sessionKey = this.getQueueKey(session, channel, channelId);
1997
+ const sessionAgent = this.getAgent(channel, session.baseagent);
1998
+ const isCurrentlyProcessing = this.messageQueue.isProcessing(sessionKey) || sessionAgent.hasActiveStream(sessionKey);
1999
+ const queueLength = this.messageQueue.getQueueLength(sessionKey);
2000
+ const isThread = !!session.threadId;
2001
+ let sessionStatus = isCurrentlyProcessing ? '处理中' : '空闲';
2002
+ // 处理中时显示时长
2003
+ if (isCurrentlyProcessing) {
2004
+ const elapsed = Date.now() - parseInt(session.processingState, 10);
2005
+ if (!isNaN(elapsed) && elapsed > 0) {
2006
+ const sec = Math.floor(elapsed / 1000);
2007
+ sessionStatus = sec < 60 ? `处理中 (${sec}秒)` :
2008
+ sec < 3600 ? `处理中 (${Math.floor(sec / 60)}分钟)` :
2009
+ `处理中 (${Math.floor(sec / 3600)}小时)`;
2010
+ }
2011
+ }
2012
+ const projectName = this.getProjectName(session.projectPath);
2013
+ const owningAgent = this.getOwningAgent(channel);
2014
+ const agentName = owningAgent?.name ?? 'DefaultAgent';
2015
+ const health = await this.sessionManager.getHealthStatus(session.id);
2016
+ const timeSinceSuccess = Date.now() - health.lastSuccessTime;
2017
+ const timeStr = timeSinceSuccess < 60000 ? '刚刚' :
2018
+ timeSinceSuccess < 3600000 ? `${Math.floor(timeSinceSuccess / 60000)}分钟前` :
2019
+ `${Math.floor(timeSinceSuccess / 3600000)}小时前`;
2020
+ // 获取会话文件信息并同步 name
2021
+ let sessionTurns = 0;
2022
+ if (session.agentSessionId) {
2023
+ const fileInfo = this.sessionManager.getSessionFileInfo(session.projectPath, session.agentSessionId, session.baseagent);
2024
+ sessionTurns = fileInfo.turns;
2025
+ if (fileInfo.title && fileInfo.title !== session.name) {
2026
+ await this.sessionManager.renameSession(session.id, fileInfo.title);
2027
+ session.name = fileInfo.title;
2028
+ }
2029
+ }
2030
+ const lines = [];
2031
+ const chatMode = getEffectiveChatmode(session);
2032
+ const sessionRole = session.identity?.role || identity.role || 'none';
2033
+ const sessionRoleLine = `角色身份: ${sessionRole}`;
2034
+ const mentionModeTarget = resolveSlashMentionModeTarget.call(this, {
2035
+ session,
2036
+ channel,
2037
+ selfAID,
2038
+ role: sessionRole,
2039
+ });
2040
+ // session.metadata.dispatchMode 是 AUN 协议词汇,翻译成 mentionMode 词汇兜底
2041
+ const mentionModeFallback = dispatchToMentionMode(session.metadata?.dispatchMode) ?? null;
2042
+ const mentionMode = 'error' in mentionModeTarget
2043
+ ? (mentionModeFallback ?? '未设置(跟随群设置)')
2044
+ : (readSlashMentionMode(mentionModeTarget, mentionModeFallback) ?? '未设置(跟随群设置)');
2045
+ const chatModeLine = `会话模式: ${chatMode}`;
2046
+ const dispatchModeLine = session.chatType === 'group' ? `@ 处理模式: ${mentionMode}` : null;
2047
+ if (isAdmin) {
2048
+ const gitInfo = await getGitWorkingDirInfo(session.projectPath);
2049
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${this.resolveChannelType(channel)} / 项目: ${projectName} / 会话: ${displaySessionTitle(session.name, '(未命名)')}`, `会话ID: ${session.id}`, `项目路径: ${session.projectPath}`);
2050
+ if (gitInfo) {
2051
+ lines.push(`Git: ${gitInfo}`);
2052
+ }
2053
+ lines.push(`会话状态: ${sessionStatus} / 轮数: ${sessionTurns}`, sessionRoleLine, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []));
2054
+ if (health.consecutiveErrors > 0) {
2055
+ lines.push(`异常计数: ${health.consecutiveErrors}`);
2056
+ }
2057
+ lines.push(`最后成功: ${timeStr}`, `${session.baseagent}会话: ${session.agentSessionId || '(未初始化)'}`, `创建时间: ${new Date(session.createdAt).toLocaleString('zh-CN')}`, `更新时间: ${new Date(session.updatedAt).toLocaleString('zh-CN')}`);
2058
+ }
2059
+ else {
2060
+ lines.push(`📊 ${isThread ? '话题' : '会话'}状态 (Agent: ${agentName}):`, `渠道: ${channel} / 项目: ${projectName} / ${session.baseagent}会话`);
2061
+ lines.push(`状态: ${sessionStatus} / 轮数: ${sessionTurns}`, sessionRoleLine, chatModeLine, ...(dispatchModeLine ? [dispatchModeLine] : []), `最后活跃: ${timeStr}`);
2062
+ }
2063
+ if (health.lastError) {
2064
+ lines.push('');
2065
+ lines.push(`最后错误: ${health.lastErrorType || 'unknown'}`);
2066
+ lines.push(`错误信息: ${health.lastError.substring(0, 100)}`);
2067
+ }
2068
+ return { kind: 'command.result', text: lines.join('\n') };
2069
+ }
2070
+ // /new 命令:创建新会话(支持命名)
2071
+ if (normalizedContent.startsWith('/new')) {
2072
+ const sessionName = normalizedContent.slice(4).trim() || undefined;
2073
+ if (sessionName) {
2074
+ const existing = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
2075
+ if (existing) {
2076
+ return { kind: 'command.error', text: `❌ 会话名称 "${sessionName}" 已存在,请使用其他名称` };
2077
+ }
2078
+ }
2079
+ const projectPath = this.getEffectiveDefaultPath(channel);
2080
+ if (sendMessage && session) {
2081
+ await sendMessage(channelId, `⏳ 正在创建新会话${sessionName ? `: ${sessionName}` : ''}...`, this.getReplyContext(session));
2082
+ }
2083
+ const newSessionBaseagent = this.agentRegistry?.resolveByChannel(channel)?.baseagent || this.parseDefaultBaseagent();
2084
+ const newSession = await this.sessionManager.createNewSession(channel, channelId, projectPath, sessionName, newSessionBaseagent);
2085
+ const previousAgent = getActiveAgentIfAvailable();
2086
+ if (session && previousAgent) {
2087
+ // Reset agent backend state so the new
2088
+ // session starts with a fresh conversation history
2089
+ await previousAgent.clearSession(session.id, session.agentSessionId || '', session.projectPath);
2090
+ await previousAgent.closeSession(session.id);
2091
+ }
2092
+ const newAgent = this.agentRegistry?.resolveByChannel(channel);
2093
+ const newBaseagent = newSession.baseagent || newAgent?.baseagent || this.parseDefaultBaseagent();
2094
+ // 与 /model 卡片保持一致:按 关系>角色>agent 解析实际生效的 model/effort,
2095
+ // 不直接读 EvolAgent.model(那只是 agent 级静态配置,会无视关系/角色级覆盖)。
2096
+ const newRunner = this.getAgent(channel, newSession.baseagent);
2097
+ const newModelTarget = resolveSlashRelationTarget.call(this, {
2098
+ session: newSession,
2099
+ channel,
2100
+ channelId,
2101
+ userId,
2102
+ selfAID,
2103
+ role: newSession.identity?.role || identity.role,
2104
+ chatType,
2105
+ });
2106
+ const newModelSelector = 'error' in newModelTarget
2107
+ ? { self: selfAID ?? newSession.selfAID ?? this.resolveSelfAID(channel) }
2108
+ : newModelTarget;
2109
+ let modelConfig = {};
2110
+ try {
2111
+ modelConfig = (resolveEffective(newModelSelector, { cache: true }).baseagents || {})[newBaseagent] || {};
2112
+ }
2113
+ catch { }
2114
+ const backendModel = modelConfig.model || newRunner.getModel?.() || newAgent?.model;
2115
+ const backendEffort = modelConfig[newBaseagent === 'codex' ? 'reasoning' : 'effort']
2116
+ ?? newRunner.getEffort?.()
2117
+ ?? newAgent?.effort;
2118
+ const backendBits = [newBaseagent, backendModel, backendEffort].filter(Boolean).join(' · ');
2119
+ return { kind: 'command.result', text: `✓ 已创建新会话${sessionName ? `: ${sessionName}` : ''}\n 项目: ${this.getProjectName(projectPath)}\n 后端: ${backendBits}\n 之前的对话历史已保留,可通过 /s 查看` };
2120
+ }
2121
+ // /check 命令:检查 EvolAgent 实例健康(visitor/member 可用,详情仅 admin)
2122
+ if (normalizedContent === '/check' || normalizedContent.startsWith('/check ')) {
2123
+ // 限定可见渠道:agent-owned 通道仅显示该 agent 名下的渠道;
2124
+ // 系统控制面是无持久化的全局入口,展示全部渠道;兼容旧虚拟 channel 名。
2125
+ const isGlobalCheckChannel = isSystemControlChannel(channel);
2126
+ const checkOwningAgent = isGlobalCheckChannel ? null : this.getOwningAgent(channel);
2127
+ let allowedChannels;
2128
+ if (checkOwningAgent) {
2129
+ allowedChannels = new Set(checkOwningAgent.channelInstanceNames());
2130
+ }
2131
+ else if (isGlobalCheckChannel) {
2132
+ // ECWeb / 控制 AID 全局视图:展示所有渠道
2133
+ allowedChannels = new Set(this.adapters.keys());
2134
+ }
2135
+ else {
2136
+ // default 范围:不再有 default channel 概念,等价于"所有 channel"
2137
+ const defaultNames = [];
2138
+ for (const [name] of this.adapters) {
2139
+ const owner = this.agentRegistry?.resolveByChannel(name);
2140
+ if (!owner)
2141
+ defaultNames.push(name);
2142
+ }
2143
+ allowedChannels = new Set(defaultNames);
2144
+ }
2145
+ // Default: show system health check (non-admin 仅看摘要)
2146
+ const checkAgentName = checkOwningAgent?.name ?? (isGlobalCheckChannel ? 'EvolCore' : 'DefaultAgent');
2147
+ const checkDefaultBaseagent = checkOwningAgent?.baseagent ?? this.parseDefaultBaseagent();
2148
+ const checkBaseagent = activeSession?.baseagent ?? checkDefaultBaseagent;
2149
+ const lines = [`📡 EvolAgent 实例健康 (Agent: ${checkAgentName}):`];
2150
+ lines.push(` Baseagent: ${checkBaseagent}`);
2151
+ if (checkDefaultBaseagent !== checkBaseagent) {
2152
+ lines.push(` 默认 Baseagent: ${checkDefaultBaseagent}`);
2153
+ }
2154
+ // Group by channelType
2155
+ const groups = new Map();
2156
+ for (const [name] of this.adapters) {
2157
+ if (!allowedChannels.has(name))
2158
+ continue;
2159
+ const type = this.resolveChannelType(name);
2160
+ const ch = this.channelObjects.get(name);
2161
+ let status;
2162
+ if (ch?.getStatus) {
2163
+ const s = ch.getStatus();
2164
+ status = s.connected ? '✓ 已连接' : '⏳ 重连中';
2165
+ }
2166
+ else {
2167
+ status = '✓ 已注册';
2168
+ }
2169
+ if (!groups.has(type))
2170
+ groups.set(type, []);
2171
+ groups.get(type).push({ name, status });
2172
+ }
2173
+ if (!isAdmin) {
2174
+ // visitor/member: 仅显示实例通道摘要
2175
+ const total = [...groups.values()].flat().length;
2176
+ const healthy = [...groups.values()].flat().filter(i => i.status.includes('✓')).length;
2177
+ lines.push(` ${healthy}/${total} 渠道正常`);
2178
+ return { kind: 'command.result', text: lines.join('\n') };
2179
+ }
2180
+ for (const [type, instances] of groups) {
2181
+ if (instances.length === 1) {
2182
+ lines.push(` ${type}: ${instances[0].status}`);
2183
+ }
2184
+ else {
2185
+ const parts = instances.map(i => {
2186
+ const seg = i.name.split('#');
2187
+ const instName = seg.length >= 3 ? seg.slice(2).join('#') : i.name;
2188
+ return `${i.status.includes('✓') ? '✓' : '⏳'} ${instName}`;
2189
+ });
2190
+ lines.push(` ${type}: ${parts.join(', ')}`);
2191
+ }
2192
+ }
2193
+ // 当前 agent 标识(用于 agent 维度 stats / queue 查询)。运行统计按 AID 入桶。
2194
+ const currentAgentName = checkOwningAgent?.aid ?? checkOwningAgent?.name ?? '<unknown>';
2195
+ // 系统级入口使用全局统计(不按 agent 过滤)
2196
+ const statsAgentName = isGlobalCheckChannel ? undefined : currentAgentName;
2197
+ const queuePending = isGlobalCheckChannel
2198
+ ? this.messageQueue.getGlobalQueueLength()
2199
+ : this.messageQueue.getQueueLengthByAgent(currentAgentName);
2200
+ const queueProcessing = isGlobalCheckChannel
2201
+ ? this.messageQueue.getGlobalProcessingCount()
2202
+ : this.messageQueue.getProcessingCountByAgent(currentAgentName);
2203
+ // 队列状态(系统级入口为全局;agent 入口为当前 agent 维度)
2204
+ lines.push('', '📬 队列状态:');
2205
+ lines.push(` 待处理消息: ${queuePending}`);
2206
+ lines.push(` 处理中队列: ${queueProcessing}`);
2207
+ // 运行概况(全局,进程级)
2208
+ lines.push('', '🖥️ 运行概况:');
2209
+ const uptimeMs = this.statsCollector
2210
+ ? this.statsCollector.getSnapshot().uptimeMs
2211
+ : process.uptime() * 1000;
2212
+ lines.push(` 运行时间: ${this.formatUptime(uptimeMs)}`);
2213
+ // 近 1 小时统计(ECWeb 使用全局统计,agent-owned channel 使用 agent 统计)
2214
+ if (this.statsCollector) {
2215
+ const snap = this.statsCollector.getSnapshot(statsAgentName);
2216
+ const h = snap.lastHour;
2217
+ lines.push('', '📊 近 1 小时统计:');
2218
+ lines.push(` 收到消息: ${h.received}`);
2219
+ lines.push(` 完成处理: ${h.completed}`);
2220
+ if (h.errors > 0) {
2221
+ const breakdown = Object.entries(h.errorsByType).map(([t, c]) => `${t}: ${c}`).join(', ');
2222
+ lines.push(` 处理出错: ${h.errors} (${breakdown})`);
2223
+ }
2224
+ else {
2225
+ lines.push(` 处理出错: 0`);
2226
+ }
2227
+ if (h.toolErrors > 0) {
2228
+ const toolBreakdown = Object.entries(h.toolErrorsByName).map(([t, c]) => `${t}: ${c}`).join(', ');
2229
+ lines.push(` 工具失败: ${h.toolErrors} (${toolBreakdown})`);
2230
+ }
2231
+ lines.push(` 被中断: ${h.interrupts}`);
2232
+ if (h.completed > 0) {
2233
+ lines.push(` 平均响应耗时: ${(h.avgResponseMs / 1000).toFixed(1)}s`);
2234
+ }
2235
+ }
2236
+ const checkSnap = this.statsCollector?.getSnapshot(statsAgentName);
2237
+ // AUN 渠道的 per-AID 连接富状态(reconnect / flap / lastError / kick)
2238
+ const aidStateByName = new Map();
2239
+ for (const [cname, cobj] of this.channelObjects) {
2240
+ if (typeof cobj?.getAidState === 'function') {
2241
+ try {
2242
+ aidStateByName.set(cname, cobj.getAidState());
2243
+ }
2244
+ catch { /* ignore */ }
2245
+ }
2246
+ }
2247
+ // 单个渠道实例的健康快照:基础连接态 + AUN 富状态
2248
+ const channelHealth = (cname) => {
2249
+ const type = this.resolveChannelType(cname);
2250
+ const cobj = this.channelObjects.get(cname);
2251
+ const seg = cname.split('#');
2252
+ const instName = seg.length >= 3 ? seg.slice(2).join('#') : cname;
2253
+ const aidState = aidStateByName.get(cname);
2254
+ // cobj 缺失 = 渠道未注册(如 disabled agent),视为未连接;
2255
+ // cobj 存在但无 getStatus = 已注册的活实例,视为已连接。
2256
+ let connected = cobj ? (cobj.getStatus ? !!cobj.getStatus().connected : true) : false;
2257
+ const h = { name: cname, instName, type, connected };
2258
+ if (aidState) {
2259
+ connected = aidState.status === 'connected';
2260
+ h.connected = connected;
2261
+ h.aidStatus = aidState.status;
2262
+ h.reconnectCount = aidState.reconnectCount ?? 0;
2263
+ h.flapCount = aidState.flapCount ?? 0;
2264
+ if (aidState.lastConnectedAt)
2265
+ h.lastConnectedAt = aidState.lastConnectedAt;
2266
+ if (aidState.lastError)
2267
+ h.lastError = String(aidState.lastError).slice(0, 80);
2268
+ if (aidState.kickDetail?.reason)
2269
+ h.kickReason = String(aidState.kickDetail.reason).slice(0, 80);
2270
+ }
2271
+ return h;
2272
+ };
2273
+ // 以 EvolAgent 为中心聚合:后端 + 通道明细 + 负载,并记录已归属通道
2274
+ const ownedNames = new Set();
2275
+ const evolagents = (this.agentRegistry?.list() ?? []).map((ag) => {
2276
+ const chans = (ag.channels ?? []).map((n) => { ownedNames.add(n); return channelHealth(n); });
2277
+ // 队列按 agentName 聚合,入队时 agentName === EvolAgent.aid(见 message-bridge enqueue)。
2278
+ // 这里必须用 ag.aid,不能用 ag.name —— toInfo 的 name 可能是 agent.md 的 displayName 短名,
2279
+ // 与队列里的 AID 不匹配会导致 processing/pending 恒为 0。
2280
+ const processing = this.messageQueue.getProcessingCountByAgent(ag.aid);
2281
+ const pending = this.messageQueue.getQueueLengthByAgent(ag.aid);
2282
+ return {
2283
+ name: ag.name, aid: ag.aid ?? '', status: ag.status,
2284
+ baseagent: ag.baseagent ?? null,
2285
+ model: ag.model ?? null,
2286
+ effort: ag.effort ?? null,
2287
+ projectPath: ag.projectPath ?? null,
2288
+ processing, pending,
2289
+ activeTasks: processing + pending,
2290
+ lastActivity: ag.lastActivity ?? 0,
2291
+ error: ag.error,
2292
+ channels: chans,
2293
+ };
2294
+ });
2295
+ // 未归属到任何 EvolAgent 实例的系统通道(系统级 / DefaultAgent)
2296
+ const unownedChannels = [];
2297
+ for (const [cname] of this.adapters) {
2298
+ if (!allowedChannels.has(cname) || ownedNames.has(cname))
2299
+ continue;
2300
+ unownedChannels.push(channelHealth(cname));
2301
+ }
2302
+ const structured = {
2303
+ daemon: {
2304
+ ...(typeof this.daemonStatusProvider === 'function' ? this.daemonStatusProvider() : {}),
2305
+ status: 'running',
2306
+ },
2307
+ channels: [...groups.entries()].map(([type, instances]) => ({ type, instances })),
2308
+ queue: {
2309
+ pending: queuePending,
2310
+ processing: queueProcessing,
2311
+ },
2312
+ baseagent: checkBaseagent,
2313
+ defaultBaseagent: checkDefaultBaseagent,
2314
+ uptimeMs,
2315
+ lastHour: checkSnap?.lastHour ?? null,
2316
+ evolagents,
2317
+ unownedChannels,
2318
+ };
2319
+ return { kind: 'command.result', text: lines.join('\n'), structured };
2320
+ }
2321
+ // /restart 命令:重启服务(进程级,仅 daemon owner)
2322
+ if (normalizedContent === '/restart') {
2323
+ // 进程级操作:必须是 daemon owner(daemon.json.owners),与 menu 协议 /system restart 一致。
2324
+ // agent-channel 的 owner/admin 角色不足以重启整个 daemon。
2325
+ if (!isDaemonOwner) {
2326
+ return { kind: 'command.error', text: '❌ 无权限:服务重启仅限 daemon owner 使用' };
2327
+ }
2328
+ const restartSelfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
2329
+ const authDenied = await authorizeIntent({
2330
+ intent: {
2331
+ operation: 'system.restart',
2332
+ scope: 'process',
2333
+ source: 'slash',
2334
+ args: {},
2335
+ dangerous: true,
2336
+ },
2337
+ identity: { ...identity, role: 'owner' },
2338
+ session: activeSession,
2339
+ channel,
2340
+ channelId,
2341
+ userId,
2342
+ selfAid: restartSelfAid,
2343
+ isDaemonOwner,
2344
+ });
2345
+ if (authDenied)
2346
+ return authDenied;
2347
+ const selfAid = this.agentRegistry?.resolveByChannel(channel)?.aid;
2348
+ // 排除当前会话(如果存在)。如果 activeSession 不存在,说明还没建立会话,
2349
+ // 此时检查所有任务;如果有其他会话在处理,仍然阻塞重启。
2350
+ const excludeKey = activeSession?.id;
2351
+ const busyInfo = getAgentBusyInfo(this, selfAid, excludeKey);
2352
+ if (busyInfo && busyInfo.count > 0) {
2353
+ // busyInfo.count > 0 说明【除当前会话外】还有其他会话的任务在执行
2354
+ const processingLines = busyInfo.processing.map(p => {
2355
+ const keyParts = p.queueKey.split('::');
2356
+ const sessionId = keyParts[0] || '?';
2357
+ return ` · session ${sessionId.slice(0, 32)}... (agent: ${p.agentName})`;
2358
+ }).join('\n');
2359
+ return {
2360
+ kind: 'command.error',
2361
+ text: `❌ ${excludeKey ? '其他会话' : '该 Agent'} 有 ${busyInfo.count} 个任务执行中,无法重启。\n\n处理中的会话:\n${processingLines}\n\n建议:等待这些任务完成后重试。可通过 ec ctl status 查看队列状态;典型等待时间 30-60 秒,群聊大型任务可能更长。`,
2362
+ };
2363
+ }
2364
+ const allSessions = await this.sessionManager.listSessions(channel, channelId);
2365
+ const sessionsWithMessages = allSessions
2366
+ .filter((s) => this.messageCache.hasMessages(s.id))
2367
+ .map((s) => {
2368
+ const count = this.messageCache.getCount(s.id);
2369
+ return `${s.projectPath} 有 ${count} 条新消息`;
2370
+ });
2371
+ // 执行重启逻辑(共用于卡片回调和文本确认)
2372
+ const executeRestart = async () => {
2373
+ let replyContext;
2374
+ if (threadId) {
2375
+ const threadSession = await this.sessionManager.getThreadSession(channel, channelId, threadId);
2376
+ if (threadSession)
2377
+ replyContext = this.getReplyContext(threadSession);
2378
+ }
2379
+ const restartInfo = {
2380
+ channel,
2381
+ channelId,
2382
+ timestamp: Date.now(),
2383
+ ...(replyContext?.replyToMessageId ? { rootId: replyContext.replyToMessageId } : {}),
2384
+ };
2385
+ const dataDir = resolvePaths().dataDir;
2386
+ fs.mkdirSync(dataDir, { recursive: true });
2387
+ fs.writeFileSync(path.join(dataDir, 'restart-pending.json'), JSON.stringify(restartInfo));
2388
+ const { spawn } = await import('child_process');
2389
+ spawn('node', [path.join(getPackageRoot(), 'dist', 'cli', 'index.js'), 'restart-monitor'], {
2390
+ detached: true,
2391
+ stdio: 'ignore',
2392
+ env: { ...process.env, EVOLCORE_HOME: resolvePaths().root }
2393
+ }).unref();
2394
+ this.eventBus.publish({ type: 'system:restart', channel, channelId });
2395
+ // 先发送重启反馈消息,等待发送完成后再 kill 进程
2396
+ // 避免消息还没发出去进程就退出了
2397
+ const adapter = this.adapters.get(channel);
2398
+ if (adapter) {
2399
+ try {
2400
+ const envelope = buildEnvelope({
2401
+ taskId: `restart-${Date.now()}`,
2402
+ channel,
2403
+ channelId,
2404
+ agentName: 'system',
2405
+ chatmode: 'interactive',
2406
+ replyContext,
2407
+ });
2408
+ await adapter.send(envelope, { kind: 'command.result', text: '🔄 服务正在重启,请稍候...(约 5 秒后恢复)' });
2409
+ // 等待消息发送完成后再延迟 kill
2410
+ await new Promise(resolve => setTimeout(resolve, 500));
2411
+ }
2412
+ catch (err) {
2413
+ logger.error('[System] Failed to send restart notification:', err);
2414
+ }
2415
+ }
2416
+ // 发 SIGTERM 而非直接 process.exit(0),让 index.ts 的 shutdown() 先
2417
+ // 正常关闭所有 channel(包括 Feishu WebSocket close frame),
2418
+ // 避免 Feishu 服务端因连接异常断开而重推未 ack 的消息给新进程。
2419
+ setTimeout(() => {
2420
+ logger.info('[System] Restarting by user command...');
2421
+ process.kill(process.pid, 'SIGTERM');
2422
+ }, 1000);
2423
+ return true;
2424
+ };
2425
+ // 文本确认流程
2426
+ if (sessionsWithMessages.length > 0) {
2427
+ const restartKey = `${channel}-${channelId}`;
2428
+ const dataDir = resolvePaths().dataDir;
2429
+ fs.mkdirSync(dataDir, { recursive: true });
2430
+ const restartConfirmFile = path.join(dataDir, `restart-confirm-${restartKey}.json`);
2431
+ if (fs.existsSync(restartConfirmFile)) {
2432
+ const confirmInfo = JSON.parse(fs.readFileSync(restartConfirmFile, 'utf-8'));
2433
+ const now = Date.now();
2434
+ if (now - confirmInfo.timestamp < 10000) {
2435
+ fs.unlinkSync(restartConfirmFile);
2436
+ }
2437
+ else {
2438
+ fs.writeFileSync(restartConfirmFile, JSON.stringify({ timestamp: now }));
2439
+ return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
2440
+ }
2441
+ }
2442
+ else {
2443
+ fs.writeFileSync(restartConfirmFile, JSON.stringify({ timestamp: Date.now() }));
2444
+ return { kind: 'command.result', text: sessionsWithMessages.join('\n') + '\n再次输入 /restart 将强制重启。' };
2445
+ }
2446
+ }
2447
+ await executeRestart();
2448
+ // executeRestart 内部已经发送了反馈消息,这里返回 null 避免重复发送
2449
+ return null;
2450
+ }
2451
+ // /upgrade 命令:检查版本更新,提示用户手动重启
2452
+ if (normalizedContent === '/upgrade') {
2453
+ if (!isAdmin)
2454
+ return { kind: 'command.error', text: '❌ 无权限:升级检查仅限管理员使用' };
2455
+ if (isLinkedInstall()) {
2456
+ return { kind: 'command.result', text: '⏭ 开发模式,跳过升级检查' };
2457
+ }
2458
+ const localVer = getLocalVersion();
2459
+ const remoteVer = await checkLatestVersion();
2460
+ if (!remoteVer) {
2461
+ return { kind: 'command.result', text: `⚠️ 无法连接 npm registry(当前版本 ${localVer})` };
2462
+ }
2463
+ if (compareVersions(localVer, remoteVer) >= 0) {
2464
+ return { kind: 'command.result', text: `✓ 已是最新版本 (${localVer})` };
2465
+ }
2466
+ return { kind: 'command.result', text: `📦 发现新版本 ${localVer} → ${remoteVer}\n执行 /restart 升级` };
2467
+ }
2468
+ // /pwd 命令:显示当前项目路径
2469
+ if (normalizedContent === '/pwd') {
2470
+ if (!session) {
2471
+ const defaultProjectPath = this.agentRegistry?.resolveByChannel(channel)?.projectPath || this.getEffectiveDefaultPath(channel);
2472
+ const defaultConfigName = this.getConfiguredProjectName(defaultProjectPath);
2473
+ if (defaultConfigName) {
2474
+ return { kind: 'command.result', text: `当前项目: ${defaultConfigName}\n路径: ${defaultProjectPath}` };
2475
+ }
2476
+ return { kind: 'command.result', text: `当前项目: ${defaultProjectPath}` };
2477
+ }
2478
+ const configName = this.getConfiguredProjectName(session.projectPath);
2479
+ if (configName) {
2480
+ return { kind: 'command.result', text: `当前项目: ${configName}\n路径: ${session.projectPath}` };
2481
+ }
2482
+ return { kind: 'command.result', text: `当前项目: ${session.projectPath}` };
2483
+ }
2484
+ // /file 命令:发送项目内文件,支持 /file path 和 /file channel path
2485
+ if (normalizedContent.startsWith('/file')) {
2486
+ if (!isAdmin)
2487
+ return { kind: 'command.error', text: '❌ 无权限:此命令仅限管理员使用' };
2488
+ // 飞书会将 .md 等后缀自动转为 Markdown 链接: foo.md → [foo.md](http://foo.md/)
2489
+ // 还原: 将 [text](url) 替换为 text
2490
+ const rawArg = normalizedContent.slice(5).trim().replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
2491
+ if (!rawArg) {
2492
+ const usage = isOwner
2493
+ ? '用法: /file <相对路径> 或 /file <渠道> <相对路径>\n示例: /file src/index.ts\n示例: /file feishu report.md'
2494
+ : '用法: /file <相对路径>\n示例: /file src/index.ts';
2495
+ return { kind: 'command.result', text: usage };
2496
+ }
2497
+ // 解析目标通道:第一个 token 按实例名匹配,再按 channelType 匹配
2498
+ const tokens = rawArg.split(/\s+/);
2499
+ let targetChannel = channel;
2500
+ let targetLabel = channel;
2501
+ let filePath = rawArg;
2502
+ if (tokens.length >= 2) {
2503
+ const spec = tokens[0];
2504
+ if (this.adapters.has(spec)) {
2505
+ // 精确实例名
2506
+ targetChannel = spec;
2507
+ targetLabel = spec;
2508
+ filePath = tokens.slice(1).join(' ');
2509
+ }
2510
+ else {
2511
+ // 按 channelType 查找第一个匹配的实例
2512
+ for (const [name] of this.adapters) {
2513
+ if ((this.channelTypeMap.get(name) || name) === spec) {
2514
+ targetChannel = name;
2515
+ targetLabel = spec;
2516
+ filePath = tokens.slice(1).join(' ');
2517
+ break;
2518
+ }
2519
+ }
2520
+ }
2521
+ }
2522
+ const isCrossChannel = targetChannel !== channel;
2523
+ // 跨通道不属于当前关系级操作,仍仅限 owner。
2524
+ if (isCrossChannel && identity.role !== 'owner') {
2525
+ return { kind: 'command.error', text: '❌ 跨通道发送仅限 owner' };
2526
+ }
2527
+ // 找目标 adapter
2528
+ const targetAdapter = this.adapters.get(targetChannel);
2529
+ if (!targetAdapter) {
2530
+ return { kind: 'command.error', text: `❌ 通道 ${targetLabel} 未启用或不存在` };
2531
+ }
2532
+ if (!targetAdapter.capabilities?.file) {
2533
+ return { kind: 'command.error', text: `❌ 通道 ${targetLabel} 不支持文件发送` };
2534
+ }
2535
+ const sendSession = await getExistingSessionForCommand();
2536
+ const projectPath = sendSession?.projectPath || this.agentRegistry?.resolveByChannel(channel)?.projectPath || this.getEffectiveDefaultPath(channel);
2537
+ // 路径安全校验
2538
+ if (path.isAbsolute(filePath)) {
2539
+ return { kind: 'command.error', text: '❌ 不支持绝对路径\n请使用项目内的相对路径' };
2540
+ }
2541
+ if (filePath.split(path.sep).includes('..') || filePath.split('/').includes('..')) {
2542
+ return { kind: 'command.error', text: '❌ 不支持 .. 路径穿越' };
2543
+ }
2544
+ const resolvedPath = path.resolve(projectPath, filePath);
2545
+ // 存在性检查
2546
+ if (!fs.existsSync(resolvedPath)) {
2547
+ return { kind: 'command.error', text: `❌ 文件不存在: ${filePath}` };
2548
+ }
2549
+ // 符号链接安全:realpath 后验证仍在项目目录内
2550
+ const realPath = fs.realpathSync(resolvedPath);
2551
+ const realProjectPath = fs.realpathSync(projectPath);
2552
+ if (!realPath.startsWith(realProjectPath + path.sep) && realPath !== realProjectPath) {
2553
+ return { kind: 'command.error', text: '❌ 路径不允许: 文件不在项目目录内' };
2554
+ }
2555
+ const stat = fs.statSync(resolvedPath);
2556
+ if (stat.isDirectory()) {
2557
+ return { kind: 'command.error', text: '❌ 暂不支持发送目录\n目录打包发送将在后续版本支持' };
2558
+ }
2559
+ const MAX_SIZE = 10 * 1024 * 1024;
2560
+ if (stat.size > MAX_SIZE) {
2561
+ return { kind: 'command.error', text: `❌ 文件过大: ${(stat.size / 1024 / 1024).toFixed(1)} MB (限制 10 MB)` };
2562
+ }
2563
+ // 找目标 channelId
2564
+ let targetChannelId = channelId;
2565
+ if (isCrossChannel) {
2566
+ const ownerPeerId = this.agentRegistry?.getOwner?.(targetChannel);
2567
+ targetChannelId = ownerPeerId ? (this.sessionManager.getOwnerChatId(targetChannel, ownerPeerId) ?? '') : '';
2568
+ if (!targetChannelId) {
2569
+ return { kind: 'command.error', text: `❌ 未找到 ${targetLabel} 的私聊会话,请先在该通道发送一条消息` };
2570
+ }
2571
+ }
2572
+ // 发送文件
2573
+ try {
2574
+ const replyCtx = !isCrossChannel && sendSession ? this.getReplyContext(sendSession) : undefined;
2575
+ await targetAdapter.send(buildEnvelope({ channel: targetAdapter.channelName, channelId: targetChannelId, replyContext: replyCtx }), { kind: 'result.file', filePath: realPath });
2576
+ const sizeStr = stat.size < 1024 ? `${stat.size} B`
2577
+ : stat.size < 1024 * 1024 ? `${(stat.size / 1024).toFixed(1)} KB`
2578
+ : `${(stat.size / 1024 / 1024).toFixed(1)} MB`;
2579
+ return { kind: 'command.result', text: isCrossChannel
2580
+ ? `📎 文件已通过 ${targetLabel} 发送: ${filePath} (${sizeStr})`
2581
+ : `✅ 已发送: ${filePath} (${sizeStr})` };
2582
+ }
2583
+ catch (error) {
2584
+ logger.error('[CommandHandler] /file failed:', error);
2585
+ return { kind: 'command.error', text: `❌ 文件发送失败: ${error.message || error}` };
2586
+ }
2587
+ }
2588
+ // /slist 命令:列出当前项目的会话
2589
+ // /slist — 仅 EvolCore 会话
2590
+ // /slist cli — 仅 CLI 会话(未导入的)
2591
+ if (normalizedContent === '/slist' || normalizedContent === '/slist cli') {
2592
+ if (!session) {
2593
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话
2594
+
2595
+ 请先执行以下操作之一:
2596
+ 1. 发送任意消息 - 自动创建新会话
2597
+ 2. /new [名称] - 创建命名会话` };
2598
+ }
2599
+ const showCliOnly = normalizedContent === '/slist cli';
2600
+ // /slist cli — 仅显示 CLI 会话
2601
+ if (showCliOnly) {
2602
+ const canImportCli = policy.canImportCliSession(session.chatType || 'private', identity.role);
2603
+ if (!canImportCli) {
2604
+ return { kind: 'command.error', text: '❌ 当前无权查看 CLI 会话' };
2605
+ }
2606
+ const orphanCliSessions = await this.sessionManager.listImportableCliSessions(session.projectPath, session.baseagent);
2607
+ if (orphanCliSessions.length === 0) {
2608
+ return { kind: 'command.result', text: `当前项目 ${path.basename(session.projectPath)} 没有未导入的 CLI 会话` };
2609
+ }
2610
+ // 构建显示数据(复用于卡片和文本)
2611
+ const cliDisplayItems = orphanCliSessions.map((c) => {
2612
+ const time = new Date(c.mtime).toLocaleString('zh-CN', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
2613
+ const uuid = c.uuid.substring(0, 8);
2614
+ return { uuid, fullUuid: c.uuid, time, title: c.title || '(无标题)' };
2615
+ });
2616
+ // 尝试发送 CommandCard 卡片
2617
+ if (this.interactionRouter && cliDisplayItems.length > 0) {
2618
+ const bodyLines = cliDisplayItems.map((item) => `• ${item.time} (${item.uuid}) "${item.title}"`);
2619
+ const interaction = {
2620
+ type: 'interaction',
2621
+ id: `slist-cli-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
2622
+ channelId,
2623
+ sessionId: session.id,
2624
+ initiatorId: userId,
2625
+ kind: {
2626
+ kind: 'command-card',
2627
+ title: `📋 ${path.basename(session.projectPath)} CLI 会话 (${cliDisplayItems.length})`,
2628
+ body: bodyLines.join('\n'),
2629
+ buttons: cliDisplayItems.map((item) => ({
2630
+ label: item.uuid,
2631
+ command: `/session ${item.uuid}`,
2632
+ style: 'default',
2633
+ })),
2634
+ },
2635
+ };
2636
+ const replyCtx = this.getReplyContext(session);
2637
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx });
2638
+ if (cardResult === null)
2639
+ return null;
2640
+ return { kind: 'command.result', text: cardResult };
2641
+ }
2642
+ // 降级:文本列表
2643
+ const lines = [`当前项目 ${path.basename(session.projectPath)} 的 CLI 会话 (共 ${orphanCliSessions.length} 个):`, ''];
2644
+ for (const item of cliDisplayItems) {
2645
+ lines.push(` ${item.time} (${item.uuid}) "${item.title}"`);
2646
+ }
2647
+ lines.push('');
2648
+ lines.push('使用 /s <8位uuid> 导入并切换到 CLI 会话');
2649
+ return { kind: 'command.result', text: lines.join('\n') };
2650
+ }
2651
+ // /slist — 仅显示 EvolCore 会话
2652
+ const sessions = await this.sessionManager.listSessions(channel, channelId);
2653
+ const currentProjectSessions = sessions.filter((s) => s.projectPath === session.projectPath && s.baseagent === session.baseagent && !s.threadId);
2654
+ // 从 SDK 同步会话名称(发现 CLI 改名)
2655
+ try {
2656
+ const sdkSessions = await this.sessionManager.listSdkSessions(session.projectPath, session.baseagent);
2657
+ for (const sdkSession of sdkSessions) {
2658
+ if (!sdkSession.title)
2659
+ continue;
2660
+ const dbSession = currentProjectSessions.find((s) => s.agentSessionId === sdkSession.sessionId);
2661
+ if (dbSession && sdkSession.title !== dbSession.name) {
2662
+ await this.sessionManager.renameSession(dbSession.id, sdkSession.title);
2663
+ dbSession.name = sdkSession.title;
2664
+ }
2665
+ }
2666
+ }
2667
+ catch (error) {
2668
+ logger.debug('[CommandHandler] SDK listSessions sync failed (non-critical):', error);
2669
+ }
2670
+ // 构建可显示会话列表(复用于卡片和文本)
2671
+ const maxDisplay = 10;
2672
+ const displaySessions = [];
2673
+ let displayIndex = 0;
2674
+ for (let i = 0; i < currentProjectSessions.length; i++) {
2675
+ const s = currentProjectSessions[i];
2676
+ if (displayIndex >= maxDisplay)
2677
+ break;
2678
+ const isActive = s.metadata?.isActive === true;
2679
+ displayIndex++;
2680
+ const name = displaySessionTitle(s.name, '(未命名)');
2681
+ const idleTime = formatIdleTime(Date.now() - s.updatedAt);
2682
+ const fileMissing = !!(s.agentSessionId && !this.sessionManager.checkSessionFileExists(s.projectPath, s.agentSessionId, s.baseagent));
2683
+ let status = '[空闲]';
2684
+ if (fileMissing) {
2685
+ status = '[会话文件缺失]';
2686
+ }
2687
+ else if (!!s.processingState) {
2688
+ status = '[处理中]';
2689
+ }
2690
+ else if (isActive) {
2691
+ status = '[活跃]';
2692
+ }
2693
+ displaySessions.push({ session: s, index: displayIndex, isActive, name, status, idleTime, fileMissing });
2694
+ }
2695
+ // 尝试发送 CommandCard 卡片(每个会话一个按钮,一键切换)
2696
+ if (this.interactionRouter && displaySessions.length >= 1) {
2697
+ const bodyLines = displaySessions.map(ds => {
2698
+ const prefix = ds.isActive ? '✓' : '•';
2699
+ const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
2700
+ const fileMark = ds.fileMissing ? '❌ ' : '';
2701
+ return `${prefix} ${ds.index}. ${fileMark}**${ds.name}** ${uuid} ${ds.idleTime} ${ds.status}`;
2702
+ });
2703
+ const interaction = {
2704
+ type: 'interaction',
2705
+ id: `slist-${Date.now()}-${crypto.randomBytes(4).toString('hex')}`,
2706
+ channelId,
2707
+ sessionId: session.id,
2708
+ initiatorId: userId,
2709
+ kind: {
2710
+ kind: 'command-card',
2711
+ title: `📋 ${path.basename(session.projectPath)} 会话列表`,
2712
+ body: bodyLines.join('\n'),
2713
+ buttons: displaySessions.map(ds => {
2714
+ const shortId = ds.session.agentSessionId ? ds.session.agentSessionId.substring(0, 8) : ds.name;
2715
+ return {
2716
+ label: ds.isActive ? `✓ ${ds.index}. ${shortId}` : `${ds.index}. ${shortId}`,
2717
+ command: `/session ${ds.index}`,
2718
+ style: (ds.isActive ? 'primary' : 'default'),
2719
+ disabled: ds.isActive,
2720
+ };
2721
+ }),
2722
+ },
2723
+ };
2724
+ const replyCtx = this.getReplyContext(session);
2725
+ const cardResult = await this.sendCommandCard({ channel, channelId, interaction, replyCtx });
2726
+ if (cardResult === null)
2727
+ return null;
2728
+ return { kind: 'command.result', text: cardResult };
2729
+ }
2730
+ // 降级:文本列表
2731
+ const lines = [`当前项目 ${path.basename(session.projectPath)} 的 [${session.baseagent}] 会话列表:`, ''];
2732
+ if (currentProjectSessions.length > 0) {
2733
+ for (const ds of displaySessions) {
2734
+ const prefix = ds.isActive ? ' ✓' : ' ';
2735
+ const num = `${ds.index}.`;
2736
+ const uuid = ds.session.agentSessionId ? `(${ds.session.agentSessionId.substring(0, 8)})` : '';
2737
+ if (ds.fileMissing) {
2738
+ lines.push(`${prefix} ${num} ❌ ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
2739
+ }
2740
+ else {
2741
+ lines.push(`${prefix} ${num} ${ds.name} ${uuid} - ${ds.idleTime} ${ds.status}`);
2742
+ }
2743
+ }
2744
+ const hiddenCount = currentProjectSessions.length - displayIndex;
2745
+ if (hiddenCount > 0) {
2746
+ const parts = [];
2747
+ if (hiddenCount > 0)
2748
+ parts.push(`${hiddenCount} 个更早的会话`);
2749
+ lines.push(`\n (已隐藏 ${parts.join('、')})`);
2750
+ }
2751
+ lines.push('');
2752
+ }
2753
+ lines.push('使用 /s <序号、name或8位uuid> 切换会话');
2754
+ lines.push('使用 /s cli 查看 CLI 会话');
2755
+ return { kind: 'command.result', text: lines.join('\n') };
2756
+ }
2757
+ // /session(无参数):直接复用 /slist 逻辑(含卡片交互)
2758
+ if (normalizedContent === '/session') {
2759
+ const delegated = await this.handle('/slist', channel, channelId, undefined, userId, threadId);
2760
+ return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
2761
+ }
2762
+ // /session cli(= /s cli):列出未导入的 CLI 会话
2763
+ if (normalizedContent === '/session cli') {
2764
+ const delegated = await this.handle('/slist cli', channel, channelId, undefined, userId, threadId);
2765
+ return typeof delegated === 'string' ? { kind: 'command.result', text: delegated } : delegated;
2766
+ }
2767
+ // /session 或 /s 命令:切换会话
2768
+ if (normalizedContent.startsWith('/session ')) {
2769
+ const sessionName = normalizedContent.slice(9).trim();
2770
+ if (!sessionName)
2771
+ return { kind: 'command.result', text: '用法: /s <序号、会话名称或前8位UUID>' };
2772
+ let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
2773
+ // 序号切换:纯数字时按 /slist 显示的序号匹配(超过10个时隐藏非活跃话题会话)
2774
+ if (!targetSession && /^\d+$/.test(sessionName) && session) {
2775
+ const idx = parseInt(sessionName, 10);
2776
+ const allSessions = await this.sessionManager.listSessions(channel, channelId);
2777
+ const visibleSessions = allSessions.filter((s) => s.projectPath === session.projectPath && s.baseagent === session.baseagent && !s.threadId);
2778
+ if (idx >= 1 && idx <= visibleSessions.length) {
2779
+ targetSession = visibleSessions[idx - 1];
2780
+ }
2781
+ else {
2782
+ return { kind: 'command.error', text: `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话` };
2783
+ }
2784
+ }
2785
+ if (!targetSession && sessionName.length >= 8) {
2786
+ targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
2787
+ }
2788
+ if (targetSession?.threadId) {
2789
+ return { kind: 'command.error', text: `❌ 话题会话不支持通过 /s 切换\n请在对应话题内继续对话` };
2790
+ }
2791
+ const canImport = policy.canImportCliSession(session?.chatType || 'private', identity.role);
2792
+ if (!targetSession && sessionName.length >= 8 && canImport) {
2793
+ const projectPaths = Object.values(this.projects);
2794
+ if (session) {
2795
+ projectPaths.unshift(session.projectPath);
2796
+ }
2797
+ for (const projectPath of projectPaths) {
2798
+ const currentBaseagent = session?.baseagent || this.primaryRunnerKey;
2799
+ const cliSessions = await this.sessionManager.listImportableCliSessions(projectPath, currentBaseagent);
2800
+ const cliSession = cliSessions.find((c) => c.uuid.startsWith(sessionName));
2801
+ if (cliSession) {
2802
+ const imported = await this.sessionManager.importCliSession(channel, channelId, projectPath, cliSession.uuid, currentBaseagent);
2803
+ this.eventBus.publish({ type: 'session:imported', sessionId: imported.id, agentSessionId: cliSession.uuid, projectPath });
2804
+ const projectName = this.getProjectName(projectPath);
2805
+ return { kind: 'command.result', text: `✓ 已导入 CLI 会话: ${displaySessionTitle(imported.name, '(未命名)')}\n 项目: ${projectName}\n 将继续之前的对话历史` };
2806
+ }
2807
+ }
2808
+ }
2809
+ if (!targetSession) {
2810
+ return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
2811
+ }
2812
+ const lastInput = targetSession.agentSessionId
2813
+ ? this.sessionManager.readSessionLastUserMessage(targetSession.projectPath, targetSession.agentSessionId, targetSession.baseagent)
2814
+ : null;
2815
+ const lastInputLine = lastInput ? `\n 最后输入: "${lastInput}"` : '';
2816
+ if (!session) {
2817
+ const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
2818
+ if (!switched) {
2819
+ return { kind: 'command.error', text: `❌ 切换会话失败` };
2820
+ }
2821
+ if (this.shouldSuppressCardTriggerResult(source, channel))
2822
+ return null;
2823
+ return { kind: 'command.result', text: `✓ 已切换到会话: ${displaySessionTitle(targetSession.name, sessionName)}\n 项目: ${path.basename(targetSession.projectPath)}${lastInputLine}` };
2824
+ }
2825
+ if (targetSession.id === session.id) {
2826
+ return { kind: 'command.result', text: `当前已在会话: ${displaySessionTitle(targetSession.name, sessionName)}` };
2827
+ }
2828
+ // 阻止从主会话切换到话题会话
2829
+ if (!session.threadId && targetSession.threadId) {
2830
+ return { kind: 'command.error', text: `❌ 无法从主会话切换到话题会话\n话题会话仅在对应话题内可用` };
2831
+ }
2832
+ const switched = await this.sessionManager.switchToSession(channel, channelId, targetSession.id);
2833
+ if (!switched) {
2834
+ return { kind: 'command.error', text: `❌ 切换会话失败` };
2835
+ }
2836
+ this.eventBus.publish({ type: 'session:switched', sessionId: targetSession.id, fromSessionId: session.id, toSessionId: targetSession.id });
2837
+ const continueHint = lastInput ? '\n 将继续之前的对话历史' : '\n 当前会话未有发言';
2838
+ if (this.shouldSuppressCardTriggerResult(source, channel))
2839
+ return null;
2840
+ return { kind: 'command.result', text: `✓ 已切换到会话: ${displaySessionTitle(targetSession.name, sessionName)}${continueHint}${lastInputLine}` };
2841
+ }
2842
+ // /rename 或 /name 命令:重命名当前会话
2843
+ if (normalizedContent === '/rename' || normalizedContent === '/name') {
2844
+ return { kind: 'command.result', text: '用法: /name <新名称> 或 /rename <新名称>' };
2845
+ }
2846
+ if (normalizedContent.startsWith('/rename ')) {
2847
+ const newName = normalizedContent.slice(8).trim();
2848
+ if (!newName)
2849
+ return { kind: 'command.result', text: '用法: /name <新名称> 或 /rename <新名称>' };
2850
+ if (!session) {
2851
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话
2852
+
2853
+ 请先执行以下操作之一:
2854
+ 1. 发送任意消息 - 自动创建新会话
2855
+ 2. /new [名称] - 创建命名会话
2856
+ 3. /session <名称> - 切换到已有会话` };
2857
+ }
2858
+ const existing = await this.sessionManager.getSessionByName(channel, channelId, newName);
2859
+ if (existing && existing.id !== session.id) {
2860
+ return { kind: 'command.error', text: `❌ 会话名称 "${newName}" 已存在,请使用其他名称` };
2861
+ }
2862
+ const oldName = displaySessionTitle(session.name, '(未命名)');
2863
+ const success = await this.sessionManager.renameSession(session.id, newName);
2864
+ if (success && session.agentSessionId) {
2865
+ const renameAgent = this.getAgent(channel, session.baseagent);
2866
+ await renameAgent.setSessionName?.(session.agentSessionId, newName).catch((error) => {
2867
+ logger.debug('[CommandHandler] Backend session rename sync failed:', error);
2868
+ });
2869
+ }
2870
+ if (!success) {
2871
+ return { kind: 'command.error', text: `❌ 重命名失败` };
2872
+ }
2873
+ this.eventBus.publish({ type: 'session:renamed', sessionId: session.id, oldName, newName });
2874
+ return { kind: 'command.result', text: `✓ 已将当前会话重命名为: ${newName}` };
2875
+ }
2876
+ // /del 命令:删除指定会话(仅解绑,不删除文件)
2877
+ if (normalizedContent.startsWith('/del ')) {
2878
+ const sessionName = normalizedContent.slice(5).trim();
2879
+ if (!sessionName)
2880
+ return { kind: 'command.result', text: '用法: /del <序号、会话名称或前8位UUID>' };
2881
+ if (!session) {
2882
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话` };
2883
+ }
2884
+ // 权限检查:policy 控制谁可以删除会话
2885
+ if (!policy.canDeleteSession(session.chatType || 'private', identity.role)) {
2886
+ return { kind: 'command.error', text: `❌ 无权限:群聊中仅管理员可删除会话` };
2887
+ }
2888
+ let targetSession = await this.sessionManager.getSessionByName(channel, channelId, sessionName);
2889
+ // 序号删除(与 /slist 显示序号一致)
2890
+ if (!targetSession && /^\d+$/.test(sessionName)) {
2891
+ const idx = parseInt(sessionName, 10);
2892
+ const allSessions = await this.sessionManager.listSessions(channel, channelId);
2893
+ const visibleSessions = allSessions.filter((s) => s.projectPath === session.projectPath && s.baseagent === session.baseagent && !s.threadId);
2894
+ if (idx >= 1 && idx <= visibleSessions.length) {
2895
+ targetSession = visibleSessions[idx - 1];
2896
+ }
2897
+ else {
2898
+ return { kind: 'command.error', text: `❌ 序号超出范围 (1-${visibleSessions.length})\n使用 /s 查看可用会话` };
2899
+ }
2900
+ }
2901
+ if (!targetSession && sessionName.length >= 8) {
2902
+ targetSession = await this.sessionManager.getSessionByUuidPrefix(channel, channelId, sessionName);
2903
+ }
2904
+ if (targetSession?.threadId) {
2905
+ return { kind: 'command.error', text: `❌ 请使用话题管理删除话题会话` };
2906
+ }
2907
+ if (!targetSession) {
2908
+ return { kind: 'command.error', text: `❌ 会话不存在: ${sessionName}\n使用 /s 查看可用会话` };
2909
+ }
2910
+ if (targetSession.id === session.id) {
2911
+ return { kind: 'command.error', text: `❌ 无法删除当前活跃会话\n请先切换到其他会话` };
2912
+ }
2913
+ const success = await this.sessionManager.unbindSession(targetSession.id);
2914
+ if (!success) {
2915
+ return { kind: 'command.error', text: `❌ 删除失败` };
2916
+ }
2917
+ this.eventBus.publish({ type: 'session:deleted', sessionId: targetSession.id });
2918
+ const targetAgent = this.getAgent(channel, targetSession.baseagent);
2919
+ await targetAgent.closeSession(targetSession.id);
2920
+ return { kind: 'command.result', text: `✓ 已删除会话: ${displaySessionTitle(targetSession.name, sessionName)}\n会话文件已保留,可通过 CLI 访问` };
2921
+ }
2922
+ // /fork 命令:分支当前会话
2923
+ if (normalizedContent === '/fork' || normalizedContent.startsWith('/fork ')) {
2924
+ const forkName = normalizedContent.slice(5).trim() || undefined;
2925
+ if (!session) {
2926
+ return { kind: 'command.error', text: `❌ 当前没有活跃会话,无法分支` };
2927
+ }
2928
+ if (!session.agentSessionId) {
2929
+ return { kind: 'command.error', text: `❌ 当前会话尚未初始化对话,无法分支\n\n请先发送一条消息,然后再使用 /fork` };
2930
+ }
2931
+ const forkAgent = this.getAgent(channel, session.baseagent);
2932
+ if (!forkAgent.capabilities?.fork) {
2933
+ return { kind: 'command.error', text: `❌ 当前 Agent (${forkAgent.name}) 不支持 /fork\n\n可使用 /new 创建新会话替代` };
2934
+ }
2935
+ try {
2936
+ const forkedSessionId = await forkAgent.forkSession(session.agentSessionId, session.projectPath, forkName);
2937
+ const newSession = await this.sessionManager.createForkedSession(session, forkedSessionId, forkName);
2938
+ await forkAgent.updateSessionMetadata?.(forkedSessionId, {
2939
+ gitInfo: {
2940
+ branch: null,
2941
+ commitHash: null,
2942
+ repositoryUrl: null,
2943
+ },
2944
+ evolcoreSessionId: newSession.id,
2945
+ sourceSessionId: session.id,
2946
+ }).catch((error) => {
2947
+ logger.debug('[CommandHandler] Backend fork metadata sync failed:', error);
2948
+ });
2949
+ this.eventBus.publish({ type: 'session:forked', sessionId: newSession.id, sourceSessionId: session.id, name: forkName });
2950
+ return { kind: 'command.result', text: `✅ 会话已分支: ${displaySessionTitle(newSession.name, '(未命名)')}\n新会话已激活,可以继续对话\n\n使用 /s 查看所有会话,/s <名称> 切换回原会话` };
2951
+ }
2952
+ catch (error) {
2953
+ logger.error('[CommandHandler] Fork session failed:', error);
2954
+ return { kind: 'command.error', text: `❌ 会话分支失败: ${error instanceof Error ? error.message : '未知错误'}` };
2955
+ }
2956
+ }
2957
+ // /rewind 命令:查看历史 / 回退会话
2958
+ if (normalizedContent === '/rewind' || normalizedContent.startsWith('/rewind ')) {
2959
+ const session = await getExistingSessionForCommand();
2960
+ if (!session)
2961
+ return { kind: 'command.error', text: '❌ 当前没有活跃会话' };
2962
+ const rewindBaseagent = session.baseagent ?? session.agentId;
2963
+ const rewindAgent = this.getAgent(channel, rewindBaseagent);
2964
+ if (!session.agentSessionId) {
2965
+ return { kind: 'command.error', text: '❌ 当前会话无历史记录\n\n请先发送一条消息,然后再使用 /rewind' };
2966
+ }
2967
+ if (!rewindAgent.getSessionMessages) {
2968
+ return { kind: 'command.error', text: `❌ 当前 Agent (${rewindAgent.name}) 不支持 /rewind` };
2969
+ }
2970
+ const args = normalizedContent.slice('/rewind'.length).trim();
2971
+ if (!args) {
2972
+ return { kind: 'command.result', text: await this.handleRewindList(session, rewindAgent) };
2973
+ }
2974
+ // 带参(执行回退,会删除文件/改对话)需 admin+
2975
+ if (!isAdmin)
2976
+ return { kind: 'command.error', text: '❌ 无权限:回退操作仅限管理员使用' };
2977
+ const parts = args.split(/\s+/);
2978
+ const turnNum = parseInt(parts[0], 10);
2979
+ if (isNaN(turnNum) || turnNum < 1) {
2980
+ return { kind: 'command.error', text: '❌ 无效轮次,用法:/rewind <N> chat|file|all(撤销第N轮)' };
2981
+ }
2982
+ const mode = parts[1]?.toLowerCase();
2983
+ if (!mode) {
2984
+ return { kind: 'command.error', text: `❌ 请指定回退模式:/rewind ${turnNum} chat | file | all(撤销第${turnNum}轮)` };
2985
+ }
2986
+ if (!['chat', 'file', 'all'].includes(mode)) {
2987
+ return { kind: 'command.error', text: `❌ 无效模式 "${mode}",可选:chat | file | all` };
2988
+ }
2989
+ return { kind: 'command.result', text: await this.handleRewind(session, rewindAgent, turnNum, mode) };
2990
+ }
2991
+ // /repair 命令:检查并修复会话文件
2992
+ if (normalizedContent === '/repair') {
2993
+ const repairSession = await getExistingSessionForCommand();
2994
+ if (!repairSession)
2995
+ return { kind: 'command.result', text: '当前没有活跃会话' };
2996
+ const repairAgent = this.getAgent(channel, repairSession.baseagent);
2997
+ const { checkSessionFile, backupSessionFile } = await import('../session/session-file-health.js');
2998
+ try {
2999
+ if (!repairSession.agentSessionId) {
3000
+ await this.sessionManager.resetHealthStatus(repairSession.id);
3001
+ return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 未发现问题(新会话)\n- 已重置异常计数器` };
3002
+ }
3003
+ // 通过 agent 定位 session 文件
3004
+ const sessionFile = repairAgent.resolveSessionFile?.(repairSession.agentSessionId, repairSession.projectPath) ?? null;
3005
+ if (!sessionFile) {
3006
+ // 文件不存在(已被删除或从未创建),直接重置
3007
+ await this.sessionManager.resetHealthStatus(repairSession.id);
3008
+ return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 会话文件不存在(可能已被清理)\n- 已重置异常计数器` };
3009
+ }
3010
+ const healthCheck = await checkSessionFile(sessionFile);
3011
+ if (healthCheck.corrupt) {
3012
+ const backupPath = await backupSessionFile(sessionFile);
3013
+ const fsPromises = await import('fs/promises');
3014
+ await fsPromises.unlink(sessionFile);
3015
+ await this.sessionManager.updateAgentSessionIdBySessionId(repairSession.id, '');
3016
+ repairAgent.updateSessionId(repairSession.id, '');
3017
+ await this.sessionManager.resetHealthStatus(repairSession.id);
3018
+ return { kind: 'command.result', text: `✓ 修复完成\n\n检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n修复操作:\n- 已备份损坏文件\n- 已删除损坏文件\n- 已重置异常计数器\n\n备份位置:${backupPath}` };
3019
+ }
3020
+ if (healthCheck.issues.length > 0) {
3021
+ await this.sessionManager.resetHealthStatus(repairSession.id);
3022
+ return { kind: 'command.error', text: `⚠️ 检测到问题:\n${healthCheck.issues.map((i) => `- ${i}`).join('\n')}\n\n建议使用 /new 创建新会话\n\n已重置异常计数器,可继续使用当前会话。` };
3023
+ }
3024
+ await this.sessionManager.resetHealthStatus(repairSession.id);
3025
+ return { kind: 'command.result', text: `✓ 修复完成\n\n修复内容:\n- 未发现问题\n- 已重置异常计数器` };
3026
+ }
3027
+ catch (error) {
3028
+ logger.error('[Repair] Failed:', error);
3029
+ return { kind: 'command.error', text: `❌ 修复失败: ${error.message}` };
3030
+ }
3031
+ }
3032
+ // /trigger 命令
3033
+ if (normalizedContent === '/trigger' || normalizedContent.startsWith('/trigger ')) {
3034
+ const triggerAuthDenied = await authorizeIntent({
3035
+ intent: {
3036
+ operation: triggerOperationForSlash(normalizedContent),
3037
+ scope: 'relation',
3038
+ source: 'slash',
3039
+ args: {},
3040
+ },
3041
+ identity,
3042
+ session: activeSession,
3043
+ explicitChatType: activeChatType === 'private' || activeChatType === 'group' ? activeChatType : undefined,
3044
+ channel,
3045
+ channelId,
3046
+ userId,
3047
+ selfAid: selfAID ?? this.resolveSelfAID(channel),
3048
+ isDaemonOwner,
3049
+ });
3050
+ if (triggerAuthDenied)
3051
+ return triggerAuthDenied;
3052
+ const text = await this.handleTrigger(normalizedContent, channel, channelId, userId ?? '', isAdmin, messageId, chatType, threadId);
3053
+ return { kind: 'command.result', text };
3054
+ }
3055
+ return null;
3056
+ }