evolcore 0.0.27 → 0.0.29-test.0

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 (195) hide show
  1. package/CHANGELOG.md +38 -15
  2. package/bin/ec-safe-output.js +270 -54
  3. package/dist/agents/baseagent-availability.js +37 -1
  4. package/dist/agents/baseagent.js +29 -14
  5. package/dist/agents/claude-runner.js +171 -13
  6. package/dist/agents/codex/app-server-client.js +13 -2
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex/diagnostics.js +4 -3
  9. package/dist/agents/codex-runner.js +444 -106
  10. package/dist/agents/gemini-runner.js +35 -17
  11. package/dist/aun/group/history.js +130 -90
  12. package/dist/aun/group/operations.js +1 -3
  13. package/dist/aun/menu/client.js +1 -0
  14. package/dist/aun/menu/transport.js +5 -3
  15. package/dist/aun/msg/managed-operation.js +70 -0
  16. package/dist/aun/msg/p2p.js +1 -3
  17. package/dist/aun/msg/upload.js +2 -2
  18. package/dist/aun/outbox.js +1 -37
  19. package/dist/channels/aun.js +262 -86
  20. package/dist/channels/daemon.js +18 -1
  21. package/dist/channels/feishu.js +1 -2
  22. package/dist/cli/agent-command.js +23 -3
  23. package/dist/cli/agent-create.js +68 -2
  24. package/dist/cli/agent.js +12 -2
  25. package/dist/cli/aun-commands.js +87 -13
  26. package/dist/cli/cli-argv.js +26 -0
  27. package/dist/cli/daemon-commands.js +116 -149
  28. package/dist/cli/data-command.js +4 -4
  29. package/dist/cli/diag.js +1 -1
  30. package/dist/cli/handoff-command.js +2 -0
  31. package/dist/cli/index.js +18 -1
  32. package/dist/cli/managed-command-guard.js +10 -0
  33. package/dist/cli/memory-command.js +441 -0
  34. package/dist/cli/menu-client.js +4 -0
  35. package/dist/cli/restart-monitor.js +127 -23
  36. package/dist/cli/skill-command.js +41 -0
  37. package/dist/cli/task-context.js +2 -23
  38. package/dist/cli/trigger-command.js +9 -1
  39. package/dist/cli/watch-msg-diag.js +209 -0
  40. package/dist/config/config-field-policy.js +52 -3
  41. package/dist/config/config-manager.js +17 -4
  42. package/dist/config/contact-book-store.js +2 -2
  43. package/dist/config/mention-mode.js +7 -2
  44. package/dist/config/merge.js +28 -9
  45. package/dist/config/resolved-config-op.js +1 -1
  46. package/dist/config/runtime-env.js +137 -0
  47. package/dist/config/schema-registry.js +11 -1
  48. package/dist/config-store.js +10 -10
  49. package/dist/core/audit/event-key.js +3 -1
  50. package/dist/core/audit/log-integrity.js +4 -0
  51. package/dist/core/auth/agent-delegation.js +2 -1
  52. package/dist/core/auth/auth-gateway.js +13 -6
  53. package/dist/core/auth/authorization-audit.js +54 -19
  54. package/dist/core/auth/operation-authorizer.js +17 -0
  55. package/dist/core/auth/operation-catalog.js +9 -0
  56. package/dist/core/causation/context.js +5 -1
  57. package/dist/core/command/agent-control.js +1 -2
  58. package/dist/core/command/command-handler.js +80 -24
  59. package/dist/core/command/diag-service.js +292 -27
  60. package/dist/core/command/memory-slash.js +270 -0
  61. package/dist/core/command/menu-catalog.js +1 -0
  62. package/dist/core/command/menu-handler.js +331 -79
  63. package/dist/core/command/menu-protocol.js +45 -4
  64. package/dist/core/command/slash-gate.js +10 -10
  65. package/dist/core/command/slash-handler.js +230 -79
  66. package/dist/core/data-migration.js +282 -505
  67. package/dist/core/event-catalog.js +26 -0
  68. package/dist/core/evolagent-registry.js +6 -0
  69. package/dist/core/evolagent.js +0 -10
  70. package/dist/core/handoff/store.js +1 -20
  71. package/dist/core/inference/text-inference.js +57 -1
  72. package/dist/core/memory/consolidation-context.js +505 -0
  73. package/dist/core/memory/consolidation-ops-records.js +124 -0
  74. package/dist/core/memory/consolidation-trigger.js +394 -0
  75. package/dist/core/memory/index-manager.js +308 -0
  76. package/dist/core/memory/launch-consolidation.js +395 -0
  77. package/dist/core/memory/memory-admin.js +234 -0
  78. package/dist/core/memory/memory-document.js +100 -0
  79. package/dist/core/memory/memory-session-policy.js +41 -0
  80. package/dist/core/memory/memory-signature.js +257 -0
  81. package/dist/core/memory/memory-writer.js +432 -0
  82. package/dist/core/memory/staleness.js +117 -0
  83. package/dist/core/message/auto-mention-gate.js +454 -0
  84. package/dist/core/message/im-renderer.js +90 -20
  85. package/dist/core/message/message-bridge.js +333 -23
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/message-queue.js +114 -27
  88. package/dist/core/message/pending-hints.js +329 -161
  89. package/dist/core/message/response-engine.js +596 -90
  90. package/dist/core/permission/ec-command-parser.js +1044 -87
  91. package/dist/core/permission/protected-paths.js +376 -126
  92. package/dist/core/permission/sandbox-runtime.js +40 -12
  93. package/dist/core/permission/tool-error-code.js +96 -13
  94. package/dist/core/permission/tool-policy.js +277 -24
  95. package/dist/core/role/runtime-policy.js +3 -7
  96. package/dist/core/session/{runner-task-history.js → runner-event-summary.js} +1 -1
  97. package/dist/core/session/session-fs-store.js +0 -76
  98. package/dist/core/session/session-idle-monitor.js +355 -0
  99. package/dist/core/session/session-manager.js +482 -17
  100. package/dist/core/session/session-mapper.js +33 -16
  101. package/dist/core/session/session-renew.js +217 -155
  102. package/dist/core/session/{session-turns.js → session-transcript.js} +35 -6
  103. package/dist/core/skill/global-watcher.js +282 -0
  104. package/dist/core/skill/index-generator.js +84 -0
  105. package/dist/core/skill/startup.js +73 -0
  106. package/dist/core/skill/switch-resolver.js +115 -0
  107. package/dist/core/task/task-activity-store.js +219 -0
  108. package/dist/eck/kit-renderer.js +30 -3
  109. package/dist/eck/manifest-engine.js +26 -4
  110. package/dist/index.js +722 -171
  111. package/dist/ipc.js +564 -2
  112. package/dist/paths.js +30 -9
  113. package/dist/product.js +1 -0
  114. package/dist/response-system/engines/v1/proactive-flow.js +116 -15
  115. package/dist/response-system/modes/single-session/index.js +3 -0
  116. package/dist/trigger/feedback.js +1 -0
  117. package/dist/trigger/manager.js +0 -10
  118. package/dist/trigger/scheduler.js +11 -0
  119. package/dist/trigger/validation.js +22 -0
  120. package/dist/utils/atomic-write.js +98 -0
  121. package/dist/utils/avatar-upload.js +59 -18
  122. package/dist/utils/cross-platform.js +87 -16
  123. package/dist/utils/error-dict.json +6 -3
  124. package/dist/utils/error-utils.js +27 -0
  125. package/dist/utils/logger.js +32 -10
  126. package/dist/utils/media-cache.js +84 -0
  127. package/dist/utils/msg-diag.js +97 -0
  128. package/dist/utils/npm-ops.js +119 -4
  129. package/dist/utils/package-version-info.js +244 -0
  130. package/dist/utils/welcome.js +37 -8
  131. package/dist/utils/windows-shell-trust.js +37 -0
  132. package/kits/docs/AUTHORING.md +314 -0
  133. package/kits/docs/GUIDE.md +2 -0
  134. package/kits/docs/INDEX.md +2 -0
  135. package/kits/docs/aun/CHEATSHEET.md +4 -0
  136. package/kits/docs/aun/SDK.md +60 -0
  137. package/kits/docs/channels/aun.md +8 -0
  138. package/kits/docs/context-assembly.md +10 -6
  139. package/kits/docs/evolcore/INDEX.md +17 -3
  140. package/kits/docs/evolcore/agent-create.md +24 -0
  141. package/kits/docs/evolcore/agent.md +5 -2
  142. package/kits/docs/evolcore/config.md +1 -1
  143. package/kits/docs/evolcore/ec-capabilities.md +364 -0
  144. package/kits/docs/evolcore/group.md +34 -18
  145. package/kits/docs/evolcore/memory-architecture.md +78 -0
  146. package/kits/docs/evolcore/memory-consolidation-batches.md +133 -0
  147. package/kits/docs/evolcore/memory-consolidation.md +263 -0
  148. package/kits/docs/evolcore/memory-tags.md +63 -0
  149. package/kits/docs/evolcore/memory.md +110 -0
  150. package/kits/docs/evolcore/menu-client.md +27 -11
  151. package/kits/{schemas → docs/evolcore}/menu-exec-schema-commands.md +12 -6
  152. package/kits/docs/evolcore/menu-protocol.md +213 -0
  153. package/kits/docs/evolcore/msg.md +2 -0
  154. package/kits/docs/evolcore/trigger.md +7 -5
  155. package/kits/docs/path-registry.md +6 -8
  156. package/kits/docs/prompt-loading-architecture.md +5 -3
  157. package/kits/docs/venues/group.md +5 -4
  158. package/kits/eck_manifest.json +294 -88
  159. package/kits/eck_manifest.memory.json +257 -0
  160. package/kits/rules/03-identity.md +8 -8
  161. package/kits/rules/05-venue.md +4 -2
  162. package/kits/rules/06-channel.md +1 -1
  163. package/kits/schemas/_meta.json +276 -47
  164. package/kits/schemas/agent-config.schema.12.json +38 -2
  165. package/kits/schemas/agent-config.schema.13.json +591 -0
  166. package/kits/schemas/daemon.schema.6.json +9 -9
  167. package/kits/schemas/defaults.schema.5.json +29 -0
  168. package/kits/schemas/relation-config.schema.8.json +14 -1
  169. package/kits/schemas/relation-config.schema.9.json +129 -0
  170. package/kits/schemas/role-config.schema.1.json +3 -3
  171. package/kits/skills/.switches.json +3 -0
  172. package/kits/skills/ec-skill/SKILL.md +217 -0
  173. package/kits/skills/ec-skill/scripts/cli.mjs +128 -0
  174. package/kits/skills/ec-skill/scripts/deps.mjs +107 -0
  175. package/kits/skills/ec-skill/scripts/frontmatter.mjs +104 -0
  176. package/kits/skills/ec-skill/scripts/skill.mjs +629 -0
  177. package/kits/skills/ec-skill/scripts/template/SKILL.md.tmpl +30 -0
  178. package/kits/templates/decision/auto-mention.md +25 -0
  179. package/kits/templates/decision/session-renew.md +22 -0
  180. package/kits/templates/memory-consolidation/forget.md +104 -0
  181. package/kits/templates/memory-consolidation/generalize.md +76 -0
  182. package/kits/templates/memory-consolidation/notes.md +133 -0
  183. package/kits/templates/memory-consolidation/profile.md +58 -0
  184. package/kits/templates/memory-consolidation/promote.md +72 -0
  185. package/kits/templates/memory-consolidation/workflow.md +75 -0
  186. package/kits/templates/system-fragments/bootstrap.md +14 -5
  187. package/kits/templates/system-fragments/commands.md +1 -0
  188. package/kits/templates/system-fragments/group-history.md +6 -0
  189. package/kits/templates/system-fragments/identity.md +0 -3
  190. package/kits/templates/system-fragments/memory.md +79 -0
  191. package/kits/templates/system-fragments/reply.md +1 -0
  192. package/kits/templates/system-fragments/venue.md +1 -1
  193. package/package.json +4 -6
  194. package/dist/trigger/legacy-session-history.js +0 -19
  195. package/kits/templates/system-fragments/group-unread-history.md +0 -7
@@ -1,5 +1,9 @@
1
+ import path from 'node:path';
2
+ import { createHash } from 'node:crypto';
1
3
  import { isValidAid } from '../../aun/aid/validation.js';
4
+ import { atomicReadJson, atomicWriteJson } from '../../utils/atomic-write.js';
2
5
  import { remoteAccessService } from '../auth/remote-access-store.js';
6
+ import { KeyedFairMutex } from '../handoff/mutex.js';
3
7
  export const DIAGNOSTIC_AGENT_AID = 'diag.agentid.cn';
4
8
  /** IPC verifies local control authority; managed calls must independently prove their actor. */
5
9
  export async function executeDiagIpc(service, registry, owners, cmd) {
@@ -55,11 +59,17 @@ export class DiagService {
55
59
  context;
56
60
  send;
57
61
  tokens;
62
+ sessionControlTail = Promise.resolve();
58
63
  constructor(context, send, tokens = remoteAccessService) {
59
64
  this.context = context;
60
65
  this.send = send;
61
66
  this.tokens = tokens;
62
67
  }
68
+ withSessionControl(operation) {
69
+ const pending = this.sessionControlTail.then(operation, operation);
70
+ this.sessionControlTail = pending.then(() => undefined, () => undefined);
71
+ return pending;
72
+ }
63
73
  /** Local CLI authority is checked by IPC; slash identity is authenticated AUN peerId. */
64
74
  async execute(command, actorAid) {
65
75
  const { controlAid, owners } = this.context();
@@ -72,47 +82,302 @@ export class DiagService {
72
82
  if (!previous)
73
83
  return { ok: true, message: '当前没有诊断授权' };
74
84
  const diag = previous.diagnostic;
75
- try {
76
- await this.send(diag.agentAid, `诊断授权已取消。目标 daemon:${controlAid};case:${diag.caseId};沟通 Owner:${diag.ownerAid}。请停止这次诊断的后续访问,并告知 Owner。`);
77
- return { ok: true, message: '诊断授权已撤销,已通知诊断 Agent' };
78
- }
79
- catch {
80
- return { ok: true, code: 'DIAG_STOP_NOTICE_FAILED', message: '诊断授权已撤销;取消通知发送失败,诊断 Agent 后续访问将被拒绝' };
81
- }
85
+ return this.withSessionControl(async () => {
86
+ try {
87
+ await this.send(diag.agentAid, [
88
+ '诊断事件:cancel',
89
+ '诊断授权已取消。',
90
+ `目标 daemon:${controlAid}`,
91
+ `case:${diag.caseId}`,
92
+ `沟通 Owner:${diag.ownerAid}`,
93
+ '请停止该 case 的后续 Menu 访问,并告知 Owner 该 case 已取消。',
94
+ '不要把本通知当作新的诊断请求;请向 Owner 发送取消确认。',
95
+ ].join('\n'));
96
+ }
97
+ catch {
98
+ return { ok: true, code: 'DIAG_STOP_NOTICE_FAILED', message: '诊断授权已撤销;取消通知发送失败,诊断 Agent 后续访问将被拒绝' };
99
+ }
100
+ return { ok: true, message: '诊断授权已撤销,已通知诊断 Agent 停止该 case;远端执行状态未确认' };
101
+ });
82
102
  }
83
103
  const ownerAid = actorAid ?? command.ownerAid ?? owners[0];
84
104
  if (!ownerAid || !isValidAid(ownerAid) || !owners.includes(ownerAid))
85
105
  return { ok: false, code: 'DAEMON_OWNER_REQUIRED', message: '请配置本机 owners,或指定其中一个有效 Owner AID' };
86
106
  if (!command.problem?.trim() || command.problem.length > 16_000)
87
107
  return { ok: false, code: 'INVALID_ARGUMENT', message: '请提供 1 到 16000 字符的问题描述' };
108
+ const appendExisting = async (existing) => {
109
+ if (!existing.diagnostic)
110
+ return { ok: false, code: 'DIAG_AUTH_FAILED', message: '当前诊断授权记录无效' };
111
+ if (existing.diagnostic.ownerAid !== ownerAid) {
112
+ return { ok: false, code: 'DIAG_OWNER_MISMATCH', message: `当前诊断已绑定沟通 Owner ${existing.diagnostic.ownerAid},不能由 ${ownerAid} 追加信息` };
113
+ }
114
+ const { caseId } = existing.diagnostic;
115
+ const append = [
116
+ '诊断事件:append',
117
+ '当前诊断 case 的追加信息(不要新建诊断授权或 case):',
118
+ `目标 daemon:${controlAid}`,
119
+ `沟通 Owner:${ownerAid}`,
120
+ `case:${caseId}`,
121
+ `问题补充:\n${command.problem.trim()}`,
122
+ '请在现有诊断上下文中处理;继续使用原有授权,必要时向 Owner 汇报新增影响。',
123
+ ].join('\n');
124
+ return this.withSessionControl(async () => {
125
+ if (this.tokens.getDiagnosticToken(controlAid)?.token !== existing.token) {
126
+ return { ok: false, code: 'DIAG_CANCELLED', caseId, ownerAid, agentAid: existing.diagnostic.agentAid, message: '追加信息发送前诊断授权已取消' };
127
+ }
128
+ try {
129
+ await this.send(existing.diagnostic.agentAid, append);
130
+ }
131
+ catch (error) {
132
+ const reason = error instanceof Error ? error.message : String(error);
133
+ return { ok: false, code: 'DIAG_APPEND_FAILED', caseId, ownerAid, agentAid: existing.diagnostic.agentAid,
134
+ message: `诊断追加信息发送失败:${reason};原诊断授权仍保持有效` };
135
+ }
136
+ if (this.tokens.getDiagnosticToken(controlAid)?.token !== existing.token) {
137
+ return { ok: false, code: 'DIAG_CANCELLED', caseId, ownerAid, agentAid: existing.diagnostic.agentAid, message: '追加信息发送期间诊断授权已取消' };
138
+ }
139
+ return { ok: true, code: 'DIAG_APPENDED', caseId, ownerAid, agentAid: existing.diagnostic.agentAid,
140
+ message: `已将补充信息追加到现有诊断 case ${caseId},未创建新的诊断授权` };
141
+ });
142
+ };
143
+ const existing = this.tokens.getDiagnosticToken(controlAid);
144
+ if (existing?.diagnostic)
145
+ return appendExisting(existing);
88
146
  let record;
89
147
  try {
90
148
  record = this.tokens.issueDiagnosticToken(controlAid, DIAGNOSTIC_AGENT_AID, ownerAid);
91
149
  }
92
150
  catch (error) {
151
+ // Another request may have reserved the grant after the inspection above.
152
+ // Treat that reservation as the same case and append instead of surfacing a duplicate error.
153
+ const current = error?.code === 'DIAG_ALREADY_ACTIVE' ? this.tokens.getDiagnosticToken(controlAid) : undefined;
154
+ if (current?.diagnostic)
155
+ return appendExisting(current);
93
156
  return { ok: false, code: error.code ?? 'DIAG_AUTH_FAILED', message: error.message };
94
157
  }
95
158
  const caseId = record.diagnostic.caseId;
96
- const invitation = [
97
- '请协助诊断以下主机,并主动联系指定 Owner 展示进展、追问和报告。',
98
- `目标 daemon Control AID:${controlAid}`, `沟通 Owner AID:${ownerAid}`, `case:${caseId}`,
99
- `远程访问 Token:${record.token}`,
100
- '请通过标准 Menu 调用诊断;Token 放入 E2EE 受保护头。权限复用该主机既有 Control Menu,包含 restart。有效访问滑动续期,空闲 60 分钟失效。',
101
- '先给 Owner 发开始消息,使用通用 handoff 保留目标、Token、问题和发现;无需等待 Owner 回复即可开始。授权失效后停止访问。',
102
- `问题描述:\n${command.problem.trim()}`,
103
- ].join('\n');
104
- try {
105
- await this.send(DIAGNOSTIC_AGENT_AID, invitation);
106
- }
107
- catch (error) {
108
- this.tokens.revokeDiagnosticToken(controlAid, record.token);
109
- const reason = error instanceof Error ? error.message : String(error);
110
- return { ok: false, code: 'DIAG_INVITATION_FAILED', message: `未能确认诊断邀请发送成功:${reason}。本次授权已撤销,请重试` };
159
+ const sessionName = `diag-${caseId.slice(0, 8)}`;
160
+ return this.withSessionControl(async () => {
161
+ if (this.tokens.getDiagnosticToken(controlAid)?.token !== record.token) {
162
+ return { ok: false, code: 'DIAG_CANCELLED', message: '邀请发送前授权已撤销,本次诊断已取消' };
163
+ }
164
+ const invitation = [
165
+ '诊断事件:new',
166
+ '请协助诊断以下主机,并主动联系指定 Owner 展示进展、追问和报告。',
167
+ `目标 daemon Control AID:${controlAid}`, `沟通 Owner AID:${ownerAid}`, `case:${caseId}`,
168
+ `诊断创建时间(Unix毫秒):${record.createdAt}`,
169
+ `远程访问 Token:${record.token}`,
170
+ '请通过标准 Menu 调用诊断;Token 放入 E2EE 受保护头。权限复用该主机既有 Control Menu,包含 restart。有效访问滑动续期,空闲 60 分钟失效。',
171
+ `诊断端应在处理前按 case 建立或选择独立 EvolCore 会话「${sessionName}」,不得恢复或复用其他 case 的 Codex 执行会话。`,
172
+ '先给 Owner 发开始消息,使用通用 handoff 保留目标、Token、问题和发现;无需等待 Owner 回复即可开始。授权失效后停止访问。若会话隔离失败,先告知 Owner 并暂停 Menu 访问。',
173
+ `问题描述:\n${command.problem.trim()}`,
174
+ ].join('\n');
175
+ try {
176
+ await this.send(DIAGNOSTIC_AGENT_AID, invitation);
177
+ }
178
+ catch (error) {
179
+ this.tokens.revokeDiagnosticToken(controlAid, record.token);
180
+ const reason = error instanceof Error ? error.message : String(error);
181
+ return { ok: false, code: 'DIAG_INVITATION_FAILED', message: `未能确认诊断邀请发送成功:${reason}。本次授权已撤销,请重试` };
182
+ }
183
+ // A stop may race with delivery; never report an already revoked grant as active.
184
+ if (this.tokens.getDiagnosticToken(controlAid)?.token !== record.token)
185
+ return { ok: false, code: 'DIAG_CANCELLED', message: '邀请发送期间授权已撤销,本次诊断已取消' };
186
+ return { ok: true, caseId, ownerAid, agentAid: DIAGNOSTIC_AGENT_AID,
187
+ message: `诊断邀请已发出;诊断 Agent 收到后会按 case 建立独立执行会话,并联系 ${ownerAid}。` };
188
+ });
189
+ }
190
+ }
191
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
192
+ const INVITATION = '请协助诊断以下主机,并主动联系指定 Owner 展示进展、追问和报告。';
193
+ const APPEND = '当前诊断 case 的追加信息(不要新建诊断授权或 case):';
194
+ const diagnosticLocks = new KeyedFairMutex();
195
+ const diagnosticLedgerPath = (chatDir) => path.join(chatDir, 'diagnostic-cases.json');
196
+ export const diagnosticTokenHash = (token) => createHash('sha256').update(token).digest('hex');
197
+ /** Parse only the generated header, never quoted text or the user's problem. */
198
+ export function parseDiagnosticEvent(content) {
199
+ const text = content.trim().replace(/\r\n/g, '\n');
200
+ const first = text.split('\n', 1)[0];
201
+ const explicit = /^诊断事件[::]\s*(new|append|cancel)$/.exec(first)?.[1];
202
+ if (first.startsWith('诊断事件') && !explicit)
203
+ throw new Error('未知诊断事件,未选择执行会话');
204
+ // Existing released senders need no new slash command or protocol support.
205
+ const kind = explicit ?? (first === INVITATION ? 'new' : first === APPEND ? 'append'
206
+ : text.startsWith('诊断授权已取消。') ? 'cancel' : undefined);
207
+ if (!kind)
208
+ return undefined;
209
+ const header = text.split(/^问题(?:描述|补充)[::]/m, 1)[0];
210
+ const legacyCancel = !explicit && kind === 'cancel';
211
+ const fields = legacyCancel ? header.replace(/^诊断授权已取消。/, '').replace(/;/g, '\n') : header;
212
+ const field = (label) => {
213
+ const matches = [...fields.matchAll(new RegExp(`^${label}[::]([^\\n]*)$`, 'gm'))];
214
+ return matches.length === 1 ? matches[0][1].trim() : undefined;
215
+ };
216
+ const caseId = field('case')?.toLowerCase();
217
+ const controlAid = field('目标 daemon(?: Control AID)?');
218
+ const ownerAid = legacyCancel
219
+ ? field('沟通 Owner')?.split('。', 1)[0]
220
+ : field('沟通 Owner(?: AID)?');
221
+ if (!caseId || !UUID.test(caseId) || !controlAid || !isValidAid(controlAid) || !ownerAid || !isValidAid(ownerAid)) {
222
+ throw new Error('诊断通知字段无效,未选择执行会话');
223
+ }
224
+ if (kind !== 'new')
225
+ return { kind, caseId, controlAid, ownerAid };
226
+ const token = field('远程访问 Token');
227
+ if (!token || !/^[0-9a-f]{64}$/i.test(token))
228
+ throw new Error('诊断邀请缺少有效 Token,未选择执行会话');
229
+ const created = field('诊断创建时间(Unix毫秒)');
230
+ const createdAt = created === undefined ? undefined : Number(created);
231
+ if (createdAt !== undefined && (!Number.isSafeInteger(createdAt) || createdAt <= 0))
232
+ throw new Error('诊断创建时间无效');
233
+ return { kind, caseId, controlAid, ownerAid, tokenHash: diagnosticTokenHash(token), createdAt };
234
+ }
235
+ function readDiagnosticLedger(chatDir) {
236
+ const value = atomicReadJson(diagnosticLedgerPath(chatDir));
237
+ if (!value)
238
+ return { version: 1, cases: {} };
239
+ if (value.version !== 1 || !value.cases || typeof value.cases !== 'object' || Array.isArray(value.cases)
240
+ || Object.entries(value.cases).some(([key, record]) => !UUID.test(key) || !record
241
+ || !['active', 'closed'].includes(record.state) || !isValidAid(record.ownerAid))) {
242
+ throw new Error('诊断会话记录损坏,已阻止恢复旧会话');
243
+ }
244
+ return value;
245
+ }
246
+ /** Called again at execution and Menu boundaries, including delayed/recovered work. */
247
+ export function isDiagnosticSessionActive(session, chatDir) {
248
+ if (session.metadata?.diagnosticStatus === 'cancelled')
249
+ return false;
250
+ const binding = session.metadata?.diagnostic;
251
+ if (!binding)
252
+ return true;
253
+ const ledger = readDiagnosticLedger(chatDir);
254
+ const record = ledger.cases[binding.caseId];
255
+ return ledger.activeCaseId === binding.caseId && record?.state === 'active'
256
+ && record.sessionId === session.id && record.tokenHash === binding.tokenHash
257
+ && record.ownerAid === binding.ownerAid && session.channelId === binding.controlAid;
258
+ }
259
+ /** Invalid authorization is terminal; it must not be revived by a retry/append. */
260
+ export function closeDiagnosticSession(session, chatDir) {
261
+ const binding = session.metadata?.diagnostic;
262
+ if (!binding)
263
+ return;
264
+ const ledger = readDiagnosticLedger(chatDir);
265
+ const record = ledger.cases[binding.caseId];
266
+ if (record?.sessionId !== session.id)
267
+ return;
268
+ record.state = 'closed';
269
+ atomicWriteJson(diagnosticLedgerPath(chatDir), ledger);
270
+ }
271
+ /** The bridge already applies normal authentication/admission before calling this. */
272
+ export class DiagnosticSessionRouter {
273
+ sessions;
274
+ stopSession;
275
+ constructor(sessions, stopSession) {
276
+ this.sessions = sessions;
277
+ this.stopSession = stopSession;
278
+ }
279
+ async route(input) {
280
+ const { event, message, channel, channelType, selfAid, chatDir } = input;
281
+ if (channelType !== 'aun' || selfAid !== DIAGNOSTIC_AGENT_AID || message.chatType !== 'private'
282
+ || message.threadId || (message.source !== undefined && message.source !== 'user')
283
+ || !(message.encrypted ?? message.replyContext?.metadata?.encrypted)
284
+ || message.peerId !== event.controlAid || message.channelId !== event.controlAid || !message.messageId) {
285
+ throw new Error('诊断通知来源或会话不匹配,未执行会话切换');
111
286
  }
112
- // A stop may race with delivery; never report an already revoked grant as active.
113
- if (this.tokens.getDiagnosticToken(controlAid)?.token !== record.token)
114
- return { ok: false, code: 'DIAG_CANCELLED', message: '邀请发送期间授权已撤销,本次诊断已取消' };
115
- return { ok: true, caseId, ownerAid, agentAid: DIAGNOSTIC_AGENT_AID,
116
- message: `诊断邀请已发出,远程诊断 Agent 将联系 ${ownerAid},请在与 ${DIAGNOSTIC_AGENT_AID} 的会话中查看处理过程。` };
287
+ return diagnosticLocks.forKey(diagnosticLedgerPath(chatDir)).runExclusive(async () => {
288
+ const all = await this.sessions.listSessions(channel, message.channelId, { archiveFilter: 'all' });
289
+ const ledger = readDiagnosticLedger(chatDir);
290
+ const find = (caseId) => all.find(s => !s.threadId && s.selfAID === selfAid
291
+ && s.metadata?.diagnostic?.caseId === caseId && s.metadata.diagnostic.controlAid === event.controlAid);
292
+ let record = ledger.cases[event.caseId];
293
+ if (record && record.ownerAid !== event.ownerAid)
294
+ throw new Error('诊断通知 Owner 与原授权不一致');
295
+ if (event.kind === 'cancel') {
296
+ let session = find(event.caseId);
297
+ if (!session) {
298
+ const legacy = this.sessions.getActiveSessionSync(channel, message.channelId, channelType, selfAid);
299
+ if (legacy && !legacy.threadId && !legacy.metadata?.diagnostic) {
300
+ await this.sessions.patchSessionMetadata(legacy.id, { diagnosticStatus: 'cancelled' });
301
+ session = legacy;
302
+ }
303
+ }
304
+ // Keep a tombstone even if cancellation overtook the first invitation.
305
+ ledger.cases[event.caseId] = { ...record, ownerAid: event.ownerAid, state: 'closed', sessionId: record?.sessionId ?? session?.id };
306
+ atomicWriteJson(diagnosticLedgerPath(chatDir), ledger);
307
+ if (session)
308
+ await this.stopSession(session.id);
309
+ return { notice: '已关闭该诊断并停止其待处理任务。' };
310
+ }
311
+ if (record?.state === 'closed')
312
+ return { notice: '该诊断已结束,旧邀请和追加消息不会重新执行。' };
313
+ if (event.kind === 'append' && !record)
314
+ throw new Error('未找到该诊断的独立会话,请停止诊断后重新发起');
315
+ if (record && event.kind === 'new' && record.tokenHash !== event.tokenHash)
316
+ throw new Error('相同诊断的授权不一致,未恢复会话');
317
+ if (!record) {
318
+ if (find(event.caseId))
319
+ throw new Error('诊断生命周期记录缺失,拒绝恢复旧会话');
320
+ const sequence = Number.isSafeInteger(message.gatewaySeq) ? message.gatewaySeq : undefined;
321
+ const last = ledger.activeCaseId ? ledger.cases[ledger.activeCaseId] : undefined;
322
+ const stale = last && (sequence !== undefined && last.sequence !== undefined
323
+ ? sequence <= last.sequence
324
+ : event.createdAt !== undefined && last.createdAt !== undefined
325
+ ? event.createdAt <= last.createdAt : true);
326
+ if (stale)
327
+ return { notice: '邀请顺序已过期或无法确认,未恢复旧诊断。' };
328
+ // Persist all revocations before interruption/creation. A crash here
329
+ // cannot let a restored old task use its previous authorization.
330
+ for (const previous of Object.values(ledger.cases))
331
+ previous.state = 'closed';
332
+ record = { state: 'active', ownerAid: event.ownerAid, tokenHash: event.tokenHash, sequence, createdAt: event.createdAt };
333
+ ledger.cases[event.caseId] = record;
334
+ ledger.activeCaseId = event.caseId;
335
+ atomicWriteJson(diagnosticLedgerPath(chatDir), ledger);
336
+ }
337
+ if (ledger.activeCaseId !== event.caseId)
338
+ return { notice: '该诊断已被新诊断替代。' };
339
+ const oldActive = this.sessions.getActiveSessionSync(channel, message.channelId, channelType, selfAid);
340
+ if (!record.cleanupComplete && oldActive && !oldActive.threadId && !oldActive.metadata?.diagnostic) {
341
+ // Migration from the historical shared Control conversation. Owner
342
+ // conversations and sessions belonging to other hosts are untouched.
343
+ await this.sessions.patchSessionMetadata(oldActive.id, { diagnosticStatus: 'cancelled' });
344
+ await this.stopSession(oldActive.id);
345
+ }
346
+ // Retry interruption too after an interrupted creation attempt.
347
+ for (const previous of record.cleanupComplete ? [] : all) {
348
+ if (!previous.threadId && previous.metadata?.diagnostic?.controlAid === event.controlAid
349
+ && previous.metadata.diagnostic.caseId !== event.caseId)
350
+ await this.stopSession(previous.id);
351
+ }
352
+ let session = find(event.caseId);
353
+ if (record.sessionId && (!session || record.sessionId !== session.id))
354
+ throw new Error('诊断会话绑定缺失,拒绝回退到历史会话');
355
+ if (!session) {
356
+ if (event.kind !== 'new' || !input.baseagent)
357
+ throw new Error('无法创建诊断执行会话');
358
+ const metadata = {
359
+ channelKey: channel, peerId: event.controlAid, peerName: message.peerName,
360
+ diagnostic: { caseId: event.caseId, controlAid: event.controlAid, ownerAid: event.ownerAid, tokenHash: record.tokenHash },
361
+ };
362
+ // The case binding is committed in the first session snapshot. Replay
363
+ // can find it even if the process died before saving the ledger pointer.
364
+ session = await this.sessions.createNewSession(channel, message.channelId, input.projectPath, `diag-${event.caseId.slice(0, 8)}`, input.baseagent, metadata);
365
+ if (session.agentSessionId || session.metadata?.agentSessions || session.metadata?.resumeAt) {
366
+ throw new Error('诊断会话错误继承了旧执行上下文');
367
+ }
368
+ }
369
+ if (session.metadata?.diagnostic?.tokenHash !== record.tokenHash)
370
+ throw new Error('诊断会话授权绑定不一致');
371
+ // A concurrent Menu response can close this case while we await runner
372
+ // interruption/session creation. Never overwrite that terminal state.
373
+ const latest = readDiagnosticLedger(chatDir);
374
+ const current = latest.cases[event.caseId];
375
+ if (latest.activeCaseId !== event.caseId || current?.state !== 'active')
376
+ return { notice: '该诊断已结束。' };
377
+ current.sessionId = session.id;
378
+ current.cleanupComplete = true;
379
+ atomicWriteJson(diagnosticLedgerPath(chatDir), latest);
380
+ return { session };
381
+ });
117
382
  }
118
383
  }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * memory-slash.ts — `/memory` 斜杠命令:记忆查看、维护与整理
3
+ *
4
+ * 权限(按作用域):
5
+ * - relation:私聊里当前对端可看、admin+ 可写;群聊只有 admin+ 可看可写,且只能操作本群关系
6
+ * - agent:admin+ 可看可写(私聊)
7
+ * - owner:仅该 owner 本人(agent 配置的第一个 owner)可看可写(私聊)
8
+ * 不支持跨关系浏览:relation 永远是发起命令的这段关系。
9
+ */
10
+ import { CONSOLIDATION_MODES, MODE_SCOPE } from '../memory/consolidation-context.js';
11
+ import { MEMORY_SCOPES, isMemoryScope, memoryOverview, listMemoryFiles, formatMemoryFiles, readMemoryIndex, showMemoryFile, formatPendingNotes, showPendingNote, reindexScope, dropNote, archiveMemoryFile, restoreMemoryFile, } from '../memory/memory-admin.js';
12
+ const USAGE = [
13
+ '用法:',
14
+ ' /memory 记忆概览',
15
+ ' /memory notes 待整理 note 列表',
16
+ ' /memory note <slug> 查看 note',
17
+ ' /memory note drop <slug> 撤销 note',
18
+ ' /memory index <scope> 查看 MEMORY.md 索引',
19
+ ' /memory list <scope> [archived] 正式 / 已归档记忆列表',
20
+ ' /memory show <scope> <slug> [archived] 查看单条记忆',
21
+ ' /memory archive <scope> <slug> 归档记忆(可用 restore 恢复)',
22
+ ' /memory restore <scope> <slug> 恢复已归档记忆',
23
+ ' /memory reindex <scope> 重建索引',
24
+ ' /memory consolidate [mode] [scope] 拉起整理(mode: notes|generalize|promote|profile|forget;forget 需指定 scope)',
25
+ 'scope: relation | agent | owner',
26
+ ].join('\n');
27
+ const result = (text) => ({ kind: 'command.result', text });
28
+ const error = (text) => ({ kind: 'command.error', text: `❌ ${text}` });
29
+ export function canViewMemoryScope(ctx, scope) {
30
+ if (ctx.chatType === 'group')
31
+ return scope === 'relation' && ctx.isAdmin;
32
+ if (scope === 'relation')
33
+ return ctx.role !== 'none';
34
+ if (scope === 'agent')
35
+ return ctx.isAdmin;
36
+ return ctx.isOwnerSelf;
37
+ }
38
+ export function canWriteMemoryScope(ctx, scope) {
39
+ if (ctx.chatType === 'group')
40
+ return scope === 'relation' && ctx.isAdmin;
41
+ if (scope === 'owner')
42
+ return ctx.isOwnerSelf;
43
+ return ctx.isAdmin;
44
+ }
45
+ /** evolhelp / 菜单是否展示 /memory:至少能看一个作用域。 */
46
+ export function canUseMemorySlash(ctx) {
47
+ return MEMORY_SCOPES.some(scope => canViewMemoryScope(ctx, scope));
48
+ }
49
+ const SCOPE_LABEL = { relation: '当前关系', agent: 'Agent 级', owner: 'Owner 级' };
50
+ /**
51
+ * evolhelp「记忆管理」条目:能一键执行的都展开成无参数条目,只有需要 slug 的才带 args。
52
+ * 可见范围与 canViewMemoryScope / canWriteMemoryScope 一致。
53
+ */
54
+ export function memoryHelpEntries(ctx) {
55
+ const category = '记忆管理';
56
+ const roles = [ctx.role];
57
+ const view = MEMORY_SCOPES.filter(s => canViewMemoryScope(ctx, s));
58
+ if (view.length === 0)
59
+ return [];
60
+ const write = MEMORY_SCOPES.filter(s => canWriteMemoryScope(ctx, s));
61
+ const out = [];
62
+ const add = (command, description, args) => out.push({ command, ...(args ? { args } : {}), description, category, roles });
63
+ add('/memory', '记忆概览:条数、待整理 note、索引大小');
64
+ if (view.includes('relation'))
65
+ add('/memory notes', '待整理 note 列表');
66
+ for (const s of view)
67
+ add(`/memory list ${s}`, `${SCOPE_LABEL[s]}正式记忆列表`);
68
+ for (const s of view)
69
+ add(`/memory index ${s}`, `${SCOPE_LABEL[s]}索引 MEMORY.md`);
70
+ for (const s of view)
71
+ add(`/memory list ${s} archived`, `${SCOPE_LABEL[s]}已归档记忆`);
72
+ if (write.length > 0) {
73
+ if (write.includes('relation')) {
74
+ add('/memory consolidate notes', '整理待处理 note');
75
+ add('/memory consolidate profile', '重新生成关系画像');
76
+ }
77
+ if (write.includes('agent') && view.includes('relation'))
78
+ add('/memory consolidate generalize', '泛化为 Agent 级记忆');
79
+ if (write.includes('owner') && view.includes('relation'))
80
+ add('/memory consolidate promote', '提升为 Owner 级记忆');
81
+ for (const s of write)
82
+ add(`/memory consolidate forget ${s}`, `${SCOPE_LABEL[s]}遗忘审查`);
83
+ for (const s of write)
84
+ add(`/memory reindex ${s}`, `重建${SCOPE_LABEL[s]}索引`);
85
+ }
86
+ // 以下需要 slug,必须输入
87
+ add('/memory show', '查看单条记忆正文(slug 见 list 结果)', '<scope> <slug> [archived]');
88
+ if (view.includes('relation'))
89
+ add('/memory note', '查看单条 note', '<slug>');
90
+ if (write.length > 0) {
91
+ add('/memory archive', '归档一条记忆', '<scope> <slug>');
92
+ add('/memory restore', '恢复已归档记忆', '<scope> <slug>');
93
+ if (write.includes('relation'))
94
+ add('/memory note drop', '撤销一条 note', '<slug>');
95
+ }
96
+ add('/memory help', '全部子命令说明');
97
+ return out;
98
+ }
99
+ function denied(scope, write, chatType) {
100
+ if (chatType === 'group')
101
+ return '群聊中只有管理员可以查看和维护本群的 relation 记忆';
102
+ if (scope === 'owner')
103
+ return 'owner 级记忆仅限该 owner 本人操作';
104
+ return write ? `无权限:${scope} 级记忆的维护仅限管理员` : `无权限:${scope} 级记忆仅限管理员查看`;
105
+ }
106
+ export async function handleMemorySlash(ctx) {
107
+ if (!ctx.selfAid)
108
+ return error('无法确定当前 agent AID');
109
+ const args = ctx.content.trim().split(/\s+/).slice(1);
110
+ const sub = args[0];
111
+ const target = {
112
+ selfAid: ctx.selfAid,
113
+ peerKey: ctx.peerKey,
114
+ ownerAid: ctx.ownerAid,
115
+ memoryConfig: ctx.memoryConfig,
116
+ };
117
+ const needScope = (raw, write) => {
118
+ if (!isMemoryScope(raw))
119
+ return error(`scope 必须是 relation / agent / owner\n\n${USAGE}`);
120
+ const allowed = write ? canWriteMemoryScope(ctx, raw) : canViewMemoryScope(ctx, raw);
121
+ if (!allowed)
122
+ return error(denied(raw, write, ctx.chatType));
123
+ if (raw === 'relation' && !ctx.peerKey)
124
+ return error('当前会话没有关系上下文,无法操作 relation 记忆');
125
+ if (raw === 'owner' && !ctx.ownerAid)
126
+ return error('当前 agent 未配置 owner');
127
+ return raw;
128
+ };
129
+ const isPayload = (v) => typeof v === 'object' && v !== null;
130
+ try {
131
+ if (!sub) {
132
+ const scopes = MEMORY_SCOPES.filter(s => canViewMemoryScope(ctx, s)
133
+ && (s !== 'relation' || !!ctx.peerKey) && (s !== 'owner' || !!ctx.ownerAid));
134
+ if (scopes.length === 0)
135
+ return error('无权限查看记忆');
136
+ return result(`${memoryOverview(target, scopes)}\n\n输入 /memory help 查看全部子命令`);
137
+ }
138
+ if (sub === 'help')
139
+ return result(USAGE);
140
+ if (sub === 'notes') {
141
+ const scope = needScope('relation', false);
142
+ if (isPayload(scope))
143
+ return scope;
144
+ return result(await formatPendingNotes(target));
145
+ }
146
+ if (sub === 'note') {
147
+ if (args[1] === 'drop') {
148
+ const scope = needScope('relation', true);
149
+ if (isPayload(scope))
150
+ return scope;
151
+ if (!args[2])
152
+ return error('用法: /memory note drop <slug>');
153
+ await dropNote(target, args[2]);
154
+ return result(`✅ 已撤销 note: ${args[2]}`);
155
+ }
156
+ const scope = needScope('relation', false);
157
+ if (isPayload(scope))
158
+ return scope;
159
+ if (!args[1])
160
+ return error('用法: /memory note <slug>');
161
+ return result(showPendingNote(target, args[1]));
162
+ }
163
+ if (sub === 'index') {
164
+ const scope = needScope(args[1], false);
165
+ if (isPayload(scope))
166
+ return scope;
167
+ return result(readMemoryIndex(target, scope));
168
+ }
169
+ if (sub === 'list') {
170
+ const scope = needScope(args[1], false);
171
+ if (isPayload(scope))
172
+ return scope;
173
+ const archived = args[2] === 'archived';
174
+ return result(formatMemoryFiles(scope, listMemoryFiles(target, scope, archived), archived));
175
+ }
176
+ if (sub === 'show') {
177
+ const scope = needScope(args[1], false);
178
+ if (isPayload(scope))
179
+ return scope;
180
+ if (!args[2])
181
+ return error('用法: /memory show <scope> <slug> [archived]');
182
+ return result(await showMemoryFile(target, scope, args[2], args[3] === 'archived'));
183
+ }
184
+ if (sub === 'archive' || sub === 'restore') {
185
+ const scope = needScope(args[1], true);
186
+ if (isPayload(scope))
187
+ return scope;
188
+ if (!args[2])
189
+ return error(`用法: /memory ${sub} <scope> <slug>`);
190
+ if (sub === 'archive') {
191
+ await archiveMemoryFile(target, scope, args[2]);
192
+ return result(`✅ 已归档 ${scope}/${args[2]},索引已重建\n恢复: /memory restore ${scope} ${args[2]}`);
193
+ }
194
+ await restoreMemoryFile(target, scope, args[2]);
195
+ return result(`✅ 已恢复 ${scope}/${args[2]},索引已重建`);
196
+ }
197
+ if (sub === 'reindex') {
198
+ const scope = needScope(args[1], true);
199
+ if (isPayload(scope))
200
+ return scope;
201
+ const r = await reindexScope(target, scope);
202
+ return result(`✅ ${scope} 索引已重建(${r.indexed} 条)`);
203
+ }
204
+ if (sub === 'consolidate')
205
+ return await consolidate(ctx, target, args[1], args[2], needScope, isPayload);
206
+ return error(`未知子命令: ${sub}\n\n${USAGE}`);
207
+ }
208
+ catch (e) {
209
+ return error(e?.message || String(e));
210
+ }
211
+ }
212
+ async function consolidate(ctx, target, rawMode, rawScope, needScope, isPayload) {
213
+ const mode = (rawMode ?? 'notes');
214
+ if (!CONSOLIDATION_MODES.includes(mode))
215
+ return error(`mode 必须是 ${CONSOLIDATION_MODES.join(' / ')}`);
216
+ // 与 ipc checkMemoryEnabled 一致的开关检查
217
+ const mc = target.memoryConfig;
218
+ if (mc?.enableRelation === false)
219
+ return error('记忆系统已停用(memory.enableRelation=false)');
220
+ if (mode === 'generalize' && mc?.enableAgent === false)
221
+ return error('泛化模式已停用(memory.enableAgent=false)');
222
+ if (mode === 'promote' && mc?.enableOwner === false)
223
+ return error('提升模式已停用(memory.enableOwner=false)');
224
+ // 产出写到哪一层,就按那一层的写权限判定;整理都需要关系上下文(notes/profile 读当前关系)
225
+ let scopeOverride;
226
+ let writeScope;
227
+ if (mode === 'forget') {
228
+ if (!rawScope)
229
+ return error('forget 模式需要指定 scope: /memory consolidate forget <relation|agent|owner>');
230
+ const s = needScope(rawScope, true);
231
+ if (isPayload(s))
232
+ return s;
233
+ scopeOverride = s;
234
+ writeScope = s;
235
+ }
236
+ else {
237
+ writeScope = MODE_SCOPE[mode];
238
+ const s = needScope(writeScope, true);
239
+ if (isPayload(s))
240
+ return s;
241
+ }
242
+ if ((mode === 'notes' || mode === 'profile' || mode === 'generalize') && !ctx.peerKey) {
243
+ return error('当前会话没有关系上下文,无法整理');
244
+ }
245
+ if (!ctx.launcher)
246
+ return error('daemon 未注入整理入口,无法拉起整理会话');
247
+ const launched = await ctx.launcher({
248
+ taskCtx: {
249
+ selfAid: target.selfAid,
250
+ peerKey: ctx.peerKey,
251
+ channel: ctx.channelType,
252
+ peerId: ctx.peerId,
253
+ chatType: ctx.chatType,
254
+ threadId: ctx.threadId,
255
+ sessionId: ctx.sessionId,
256
+ baseagent: ctx.baseagent,
257
+ },
258
+ mode,
259
+ triggeredBy: 'manual',
260
+ ownerAid: ctx.ownerAid,
261
+ ownerInvolved: ctx.isOwnerSelf,
262
+ forgetAfterDays: mc?.forgetAfterDays,
263
+ ...(scopeOverride ? { scopeOverride } : {}),
264
+ });
265
+ const label = `${mode}${scopeOverride ? `(${scopeOverride})` : ''}`;
266
+ if (!launched || launched.queued) {
267
+ return result(`⏳ 整理请求已入队(${label}),前一个整理会话结束后自动开始`);
268
+ }
269
+ return result(`✅ 已拉起整理会话(${label})${launched.executionSessionId ? `\n会话: ${launched.executionSessionId}` : ''}\n完成后会在当前对话反馈结果`);
270
+ }
@@ -69,6 +69,7 @@ const ENTRIES = [
69
69
  relation('effort.relation', 'effort', '推理强度', 20, '/effort', 'model.effort.current', { options: { cmd: '/effort', operation: 'model.effort.current', scope: 'relation', mode: 'dynamic', dependsOn: ['model.relation'] }, update: { cmd: '/effort', operation: 'model.effort', scope: 'relation' }, control: 'select' }),
70
70
  relation('chatmode.relation', 'chatmode', '对话模式', 30, '/chatmode', 'chatmode.current', { options: { cmd: '/chatmode', operation: 'chatmode.current', scope: 'relation', mode: 'static', items: [{ value: 'interactive', label: '交互模式' }, { value: 'proactive', label: '主动模式' }] }, update: { cmd: '/chatmode', operation: 'chatmode.update', scope: 'relation' }, control: 'select' }),
71
71
  relation('activity.relation', 'activity', '中间输出', 40, '/activity', 'activity.current', { options: { cmd: '/activity', operation: 'activity.current', scope: 'relation', mode: 'static', items: [{ value: 'all', label: '私聊显示' }, { value: 'text', label: '仅文字进展' }, { value: 'none', label: '全部静默' }] }, update: { cmd: '/activity', operation: 'activity.update', scope: 'relation' }, control: 'select' }),
72
+ { id: 'task.activities', type: 'list', contexts: ['private_relation', 'group_relation'], name: 'task', label: '任务活动', section: 'relation_behavior', order: 45, query: { cmd: '/task', args: { view: 'activities' }, operation: 'task.activity.read', scope: 'relation', mode: 'dynamic' }, control: 'list' },
72
73
  relation('permissionMode.relation', 'permission', '权限模式', 50, '/perm', 'permission.current', { query: { cmd: '/perm', args: { scope: 'role' }, operation: 'permission.current', scope: 'relation' }, options: undefined, update: { cmd: '/perm', operation: 'permission.current', scope: 'relation', supported: false }, control: 'select' }),
73
74
  relation('mentionMode.relation', 'group', '群消息响应', 60, '/group', 'group.mentionmode.current', { contexts: ['group_relation'], query: { cmd: '/group', args: { view: 'mentionMode', scope: 'relation' }, operation: 'group.mentionmode.current', scope: 'relation' }, options: { cmd: '/group', args: { option: 'mentionMode', scope: 'relation' }, operation: 'group.mentionmode.current', scope: 'relation' }, update: { cmd: '/group', args: { resource: 'mentionMode', scope: 'relation' }, operation: 'group.mentionmode.update', scope: 'relation' }, control: 'select' }),
74
75
  relation('rulesPolicy.relation', 'group', '群规则策略', 70, '/group', 'group.rulespolicy.current', { contexts: ['group_relation'], query: { cmd: '/group', args: { view: 'rulesPolicy', scope: 'relation' }, operation: 'group.rulespolicy.current', scope: 'relation' }, options: { cmd: '/group', args: { option: 'rulesPolicy', scope: 'relation' }, operation: 'group.rulespolicy.current', scope: 'relation' }, update: { cmd: '/group', args: { resource: 'rulesPolicy', scope: 'relation' }, operation: 'group.rulespolicy.update', scope: 'relation' }, control: 'select' }),