evolcore 0.0.32 → 0.0.34

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 (214) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/bin/ec.js +3 -2
  3. package/bin/install-codex-managed-hooks.mjs +135 -37
  4. package/dist/agents/baseagent-availability.js +20 -36
  5. package/dist/agents/claude-runner.js +111 -15
  6. package/dist/agents/codex/app-server-client.js +3 -0
  7. package/dist/agents/codex/diagnostics.js +112 -54
  8. package/dist/agents/codex-runner.js +241 -63
  9. package/dist/agents/ecagent-runner.js +78 -33
  10. package/dist/agents/runner-types.js +5 -6
  11. package/dist/aun/aid/identity.js +1 -1
  12. package/dist/aun/group/operations.js +0 -1
  13. package/dist/aun/msg/history.js +1 -0
  14. package/dist/aun/msg/p2p.js +0 -1
  15. package/dist/aun/outbox.js +50 -1
  16. package/dist/channels/aun.js +44 -8
  17. package/dist/channels/daemon.js +261 -3
  18. package/dist/cli/agent-command.js +58 -24
  19. package/dist/cli/agent.js +1 -1
  20. package/dist/cli/aun-commands.js +368 -31
  21. package/dist/cli/contact.js +8 -2
  22. package/dist/cli/ctl-command.js +3 -1
  23. package/dist/cli/daemon-commands.js +197 -27
  24. package/dist/cli/debug-mode.js +154 -0
  25. package/dist/cli/fs-command.js +2 -1
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +98 -67
  28. package/dist/cli/init.js +2 -2
  29. package/dist/cli/integrity-command.js +196 -0
  30. package/dist/cli/managed-command-guard.js +5 -0
  31. package/dist/cli/memory-command.js +166 -146
  32. package/dist/cli/queue-command.js +9 -4
  33. package/dist/cli/session-command.js +194 -0
  34. package/dist/cli/skill-command.js +78 -2
  35. package/dist/cli/task-context.js +1 -0
  36. package/dist/cli/thread-command.js +622 -0
  37. package/dist/cli/trigger-command.js +10 -3
  38. package/dist/config/builtin-roles.js +1 -0
  39. package/dist/config/config-field-policy.js +6 -3
  40. package/dist/config/config-manager.js +26 -2
  41. package/dist/config/contact-book-store.js +5 -0
  42. package/dist/config/resolved-config-op.js +3 -0
  43. package/dist/config/role-service.js +13 -0
  44. package/dist/config/runtime-env.js +16 -0
  45. package/dist/config-store.js +45 -0
  46. package/dist/core/audit/log-integrity.js +3 -1
  47. package/dist/core/auth/agent-delegation.js +133 -36
  48. package/dist/core/auth/debug-context.js +139 -0
  49. package/dist/core/auth/delegation-diagnostics.js +414 -0
  50. package/dist/core/auth/operation-authorizer.js +25 -2
  51. package/dist/core/auth/operation-catalog.js +49 -0
  52. package/dist/core/background-session/session-cleanup.js +212 -0
  53. package/dist/core/background-session/types.js +10 -0
  54. package/dist/core/bootstrap-messages.js +1 -1
  55. package/dist/core/bootstrap-service.js +6 -3
  56. package/dist/core/command/agent-control.js +18 -26
  57. package/dist/core/command/command-handler.js +197 -27
  58. package/dist/core/command/daemon-diagnostics.js +1 -0
  59. package/dist/core/command/memory-slash.js +6 -2
  60. package/dist/core/command/menu-catalog.js +51 -1
  61. package/dist/core/command/menu-handler.js +114 -23
  62. package/dist/core/command/menu-protocol.js +25 -1
  63. package/dist/core/command/skill-slash.js +322 -0
  64. package/dist/core/command/slash-gate.js +6 -4
  65. package/dist/core/command/slash-handler.js +195 -13
  66. package/dist/core/command/topic-slash.js +528 -0
  67. package/dist/core/daemon-bootstrap.js +4 -7
  68. package/dist/core/evolagent-registry.js +20 -5
  69. package/dist/core/file-integrity/read.js +61 -0
  70. package/dist/core/file-integrity/store.js +262 -0
  71. package/dist/core/handoff/runtime.js +5 -11
  72. package/dist/core/handoff/store.js +5 -0
  73. package/dist/core/ipc-registration.js +175 -3
  74. package/dist/core/memory/batch-meta.js +132 -0
  75. package/dist/core/memory/consolidation-context.js +83 -29
  76. package/dist/core/memory/consolidation-ops-records.js +49 -16
  77. package/dist/core/memory/consolidation-queue-format.js +21 -0
  78. package/dist/core/memory/consolidation.js +264 -205
  79. package/dist/core/memory/index-manager.js +19 -28
  80. package/dist/core/memory/memory-admin.js +19 -14
  81. package/dist/core/memory/memory-document.js +53 -9
  82. package/dist/core/memory/memory-writer.js +30 -15
  83. package/dist/core/message/aun-ipc-sender.js +161 -6
  84. package/dist/core/message/im-renderer.js +169 -10
  85. package/dist/core/message/message-bridge.js +15 -4
  86. package/dist/core/message/message-log.js +1 -0
  87. package/dist/core/message/peer-mode.js +6 -1
  88. package/dist/core/message/response-engine.js +929 -110
  89. package/dist/core/message/subtask-tool-projection.js +140 -0
  90. package/dist/core/model/catalog-service.js +17 -12
  91. package/dist/core/permission/ec-command-parser.js +217 -9
  92. package/dist/core/permission/protected-paths.js +5 -1
  93. package/dist/core/permission/readonly-shell-query.js +43 -5
  94. package/dist/core/permission/session-path-grants.js +334 -0
  95. package/dist/core/permission/tool-canonical.js +226 -0
  96. package/dist/core/permission/tool-policy.js +30 -62
  97. package/dist/core/role/runtime-policy.js +21 -10
  98. package/dist/core/session/session-fs-store.js +123 -8
  99. package/dist/core/session/session-idle-monitor.js +249 -22
  100. package/dist/core/session/session-manager.js +129 -14
  101. package/dist/core/skill/command-parser.js +13 -0
  102. package/dist/core/skill/index-generator.js +21 -4
  103. package/dist/core/skill/startup.js +41 -0
  104. package/dist/core/skill/switch-resolver.js +55 -29
  105. package/dist/core/system-session/grants.js +40 -0
  106. package/dist/core/system-session/memory.js +222 -0
  107. package/dist/core/system-session/registry.js +74 -0
  108. package/dist/core/system-session/types.js +14 -0
  109. package/dist/core/thread/command-parser.js +125 -0
  110. package/dist/core/thread/index.js +1 -0
  111. package/dist/core/thread/ipc-operation.js +95 -0
  112. package/dist/core/thread/thread-context-storage.js +131 -0
  113. package/dist/core/thread/thread-operation.js +258 -0
  114. package/dist/core/thread/thread-route.js +33 -0
  115. package/dist/core/thread/thread-service.js +770 -0
  116. package/dist/core/thread/types.js +1 -0
  117. package/dist/eck/kit-renderer.js +7 -2
  118. package/dist/eck/manifest-engine.js +24 -4
  119. package/dist/eck/message-renderer.js +35 -0
  120. package/dist/index.js +19 -7
  121. package/dist/ipc.js +140 -50
  122. package/dist/paths.js +93 -0
  123. package/dist/response-system/engines/v1/proactive-flow.js +169 -80
  124. package/dist/stats/billing.js +20 -8
  125. package/dist/trigger/authorization.js +57 -28
  126. package/dist/trigger/feedback.js +13 -2
  127. package/dist/trigger/parser.js +70 -1
  128. package/dist/trigger/system-definitions.js +3 -3
  129. package/dist/trigger/validation.js +14 -0
  130. package/dist/utils/cross-platform.js +11 -2
  131. package/dist/utils/error-dict.json +18 -4
  132. package/dist/utils/error-utils.js +143 -1
  133. package/dist/utils/instance-registry.js +13 -8
  134. package/dist/utils/log-writer.js +12 -0
  135. package/dist/utils/process-introspect.js +11 -0
  136. package/kits/docs/context-assembly.md +1 -1
  137. package/kits/docs/evolcore/INDEX.md +1 -0
  138. package/kits/docs/evolcore/agent-create.md +40 -28
  139. package/kits/docs/evolcore/agent.md +21 -6
  140. package/kits/docs/evolcore/config.md +2 -2
  141. package/kits/docs/evolcore/ctl.md +2 -2
  142. package/kits/docs/evolcore/ec-capabilities.md +5 -1
  143. package/kits/docs/evolcore/fs-architecture.md +4 -5
  144. package/kits/docs/evolcore/fs.md +1 -1
  145. package/kits/docs/evolcore/group-collaboration.md +4 -6
  146. package/kits/docs/evolcore/group.md +11 -12
  147. package/kits/docs/evolcore/memory-architecture.md +4 -5
  148. package/kits/docs/evolcore/memory-consolidation.md +19 -15
  149. package/kits/docs/evolcore/memory-tags.md +3 -1
  150. package/kits/docs/evolcore/memory.md +24 -12
  151. package/kits/docs/evolcore/menu-exec-schema-commands.md +2 -2
  152. package/kits/docs/evolcore/menu-protocol.md +7 -0
  153. package/kits/docs/evolcore/model.md +16 -0
  154. package/kits/docs/evolcore/msg.md +8 -11
  155. package/kits/docs/evolcore/thread-chain.md +267 -0
  156. package/kits/docs/prompt-loading-architecture.md +1 -1
  157. package/kits/eck_manifest.json +55 -40
  158. package/kits/eck_manifest.memory.json +0 -12
  159. package/kits/eck_message_manifest.json +169 -10
  160. package/kits/rules/01-overview.md +11 -0
  161. package/kits/rules/03-identity.md +6 -3
  162. package/kits/rules/05-venue.md +1 -1
  163. package/kits/rules/06-channel.md +1 -1
  164. package/kits/rules/07-memory-and-thread.md +199 -0
  165. package/kits/schemas/_meta.json +7 -1
  166. package/kits/schemas/agent-config.schema.13.json +1 -1
  167. package/kits/schemas/daemon.schema.7.json +140 -0
  168. package/kits/schemas/relation-config.schema.9.json +18 -3
  169. package/kits/schemas/role-config.schema.1.json +6 -6
  170. package/kits/schemas/single-session.schema.1.json +6 -6
  171. package/kits/schemas/single-session.schema.2.json +6 -6
  172. package/kits/schemas/single-session.schema.3.json +6 -6
  173. package/kits/skills/.switches.json +12 -1
  174. package/kits/skills/ec-group/SKILL.md +234 -0
  175. package/kits/skills/ec-skill/SKILL.md +10 -1
  176. package/kits/skills/ec-skill/scripts/cli.mjs +16 -2
  177. package/kits/skills/ec-skill/scripts/skill.mjs +1 -1
  178. package/kits/skills/scrapling-article-fetch/SKILL.md +128 -0
  179. package/kits/skills/scrapling-article-fetch/scripts/check_python_env.py +54 -0
  180. package/kits/skills/scrapling-article-fetch/scripts/render_markdown_reply.py +35 -0
  181. package/kits/skills/scrapling-article-fetch/scripts/run_pipeline.sh +16 -0
  182. package/kits/skills/scrapling-article-fetch/scripts/scrapling_fetch.py +344 -0
  183. package/kits/skills/thread-ops/SKILL.md +492 -0
  184. package/kits/skills/wechat-article-media-download/SKILL.md +65 -0
  185. package/kits/skills/wechat-article-media-download/scripts/download.py +163 -0
  186. package/kits/skills/wechat-article-reader/SKILL.md +97 -0
  187. package/kits/skills/wechat-article-reader/scripts/fetch.mjs +256 -0
  188. package/kits/templates/bootstrap-welcome.md +7 -7
  189. package/kits/templates/memory-consolidation/forget.md +7 -2
  190. package/kits/templates/memory-consolidation/generalize.md +9 -4
  191. package/kits/templates/memory-consolidation/notes.md +11 -11
  192. package/kits/templates/memory-consolidation/profile.md +8 -3
  193. package/kits/templates/memory-consolidation/promote.md +8 -3
  194. package/kits/templates/memory-consolidation/workflow.md +6 -6
  195. package/kits/templates/message-fragments/handoff-request-to-target.md +1 -1
  196. package/kits/templates/message-fragments/item.md +2 -1
  197. package/kits/templates/message-fragments/thread-context.md +6 -0
  198. package/kits/templates/message-fragments/trigger-session.md +2 -0
  199. package/kits/templates/message-fragments/trigger-thread-check.md +2 -0
  200. package/kits/templates/roles/member.json +2 -0
  201. package/kits/templates/roles/visitor.json +2 -0
  202. package/kits/templates/system-fragments/bootstrap.md +67 -72
  203. package/kits/templates/system-fragments/channel.md +3 -6
  204. package/kits/templates/system-fragments/commands.md +1 -0
  205. package/kits/templates/system-fragments/managed-runtime.md +15 -0
  206. package/kits/templates/system-fragments/memory.md +16 -9
  207. package/kits/templates/system-fragments/reply.md +7 -6
  208. package/kits/templates/system-fragments/session.md +5 -14
  209. package/package.json +6 -3
  210. package/dist/core/memory/memory-session-policy.js +0 -41
  211. package/dist/core/memory/memory-signature.js +0 -257
  212. package/kits/docs/evolcore/group-fs.md +0 -17
  213. package/kits/docs/venues/aun-group.md +0 -45
  214. package/kits/docs/venues/group.md +0 -43
@@ -2,7 +2,7 @@ import { memoryHelpEntries } from './memory-slash.js';
2
2
  import { getBaseagentSnapshot, loadedBaseagentIds } from '../../agents/baseagent-availability.js';
3
3
  import { normalizePeer, writeScope } from '../model/config-scope.js';
4
4
  import { formatPeerKey } from '../relation/peer-identity.js';
5
- import { modelMatches, getCatalog, getBaseagentCatalog, resolveCatalogModel, assertCatalogSelectable, TRANSPORT_EFFORTS } from '../model/model-catalog.js';
5
+ import { modelMatches, getCatalog, getBaseagentCatalog, resolveCatalogModel, assertCatalogSelectable, CONFIGURABLE_EFFORTS, configurableEfforts } from '../model/model-catalog.js';
6
6
  import { constrainResolvedModelForRole, filterModelsForRole, validateModelSelectionForRole } from '../model/model-permission.js';
7
7
  import { hasModelSwitcher } from '../../agents/runner-types.js';
8
8
  import { resolvePaths, getPackageRoot, daemonControlDir } from '../../paths.js';
@@ -45,6 +45,7 @@ import { AGENT_DELEGATION_TOKEN_ENV } from '../auth/agent-delegation.js';
45
45
  import { evolMenuResponseTransportMetadata, menuFailure, menuCommandForName, menuSuccess, normalizeMenuError, validateTaskActivityQueryArgs, validateConfigWriteScope, validateMenuRequest, } from './menu-protocol.js';
46
46
  import { parseMenuClientProtectedHeaders } from './menu-protocol.js';
47
47
  import { TaskActivityStore } from '../task/task-activity-store.js';
48
+ import { DEFAULT_SESSION_LOOP_SECONDS, MAX_SESSION_LOOP_SECONDS, MIN_SESSION_LOOP_SECONDS, refreshSessionLoopTarget, resolveCurrentSessionLoopTarget, sessionLoopPayload, validateIdleSeconds, writeSessionLoopConfig, } from '../session/session-idle-monitor.js';
48
49
  import { remoteAccessService, validateAndRenewRemoteAccessToken } from '../auth/remote-access-store.js';
49
50
  import { listDiagnosticLogOptions, listRawConfigResources, queryDiagnosticLogs, readRawConfigResource, } from './daemon-diagnostics.js';
50
51
  import { roleMenuAction, roleMenuOperation, roleMenuOptions, roleMenuQuery, roleMenuUpdate, } from './role-menu.js';
@@ -63,7 +64,7 @@ import { normalizeTopicLabel, normalizeTopicStatus, resolveTopicArchiveFilter, }
63
64
  function getBaseagentVersion(cmd) {
64
65
  return cachedCliVersion(cmd);
65
66
  }
66
- const allEfforts = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
67
+ const allEfforts = CONFIGURABLE_EFFORTS;
67
68
  const PERMISSION_MODE_KEYS = PUBLIC_PERMISSION_MODES;
68
69
  /** menu file: fetch 文件大小上限(与 /file 和 AUN sendFile 一致) */
69
70
  const FILE_FETCH_MAX_SIZE = MAX_AUN_ATTACHMENT_SIZE;
@@ -402,14 +403,14 @@ function tokenizeArgv(line) {
402
403
  return out;
403
404
  }
404
405
  async function getAvailableEfforts(agent, model) {
406
+ const baseagent = agent?.baseagent || agent?.name;
405
407
  const dynamic = await agent.getAvailableEfforts?.(model);
406
408
  if (Array.isArray(dynamic))
407
- return dynamic;
409
+ return configurableEfforts(baseagent, dynamic);
408
410
  // Keep Menu usable with legacy/embedded runner implementations that do not
409
411
  // expose the optional catalog method. Full runners still take the dynamic
410
412
  // path above, so this fallback cannot broaden their advertised capabilities.
411
- const baseagent = agent?.baseagent || agent?.name;
412
- return (TRANSPORT_EFFORTS[baseagent] ?? []);
413
+ return configurableEfforts(baseagent);
413
414
  }
414
415
  function isDaemonControlSubject(subject) {
415
416
  return subject.isDaemonOwner === true
@@ -1143,6 +1144,18 @@ function buildMenuAuthSubject(owner, params) {
1143
1144
  fromControlChannel: params.fromControlChannel,
1144
1145
  });
1145
1146
  }
1147
+ function agentFullAccessMenuSubject(subject, source) {
1148
+ return {
1149
+ ...subject,
1150
+ agentFullAccessManagement: source === 'control'
1151
+ ? subject.agentFullAccessManagement === true
1152
+ : source === 'menu'
1153
+ && subject.channelType === 'aun'
1154
+ && !subject.fromControlChannel
1155
+ && subject.canApprovePersistentFullAccess === true
1156
+ && subject.processRole === 'daemon-owner',
1157
+ };
1158
+ }
1146
1159
  async function authorizeMenuIntent(params) {
1147
1160
  const { intent, identity, session, channel, channelId, userId, fromControlChannel } = params;
1148
1161
  if (!intent)
@@ -1456,12 +1469,7 @@ export function getMenuItems(role, chatType = 'private', scope = 'agent', authSu
1456
1469
  desc: canUseModel ? '切换当前 Agent 使用的模型版本' : '查看当前 Agent 使用的模型版本',
1457
1470
  next: canUseModel ? { type: 'select', dynamic: true } : undefined,
1458
1471
  }] : []),
1459
- ...(canSetEffort ? [{ cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', items: [
1460
- { value: 'low', label: 'Low' },
1461
- { value: 'medium', label: 'Medium' },
1462
- { value: 'high', label: 'High' },
1463
- { value: 'max', label: 'Max' },
1464
- ] } }] : []),
1472
+ ...(canSetEffort ? [{ cmd: '/effort', label: '切换推理强度', desc: '调整模型推理深度,影响响应速度与质量', next: { type: 'select', dynamic: true } }] : []),
1465
1473
  ...(canSetChatmode ? [{ cmd: '/chatmode', label: '切换会话模式', desc: '控制 Agent 主动性(被动响应或主动推进)', next: { type: 'select', items: [
1466
1474
  { value: 'interactive', label: '交互模式', desc: '仅在收到消息时响应' },
1467
1475
  { value: 'proactive', label: '主动模式', desc: 'Agent 可主动推进任务' },
@@ -1723,8 +1731,8 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1723
1731
  if (!selfAid)
1724
1732
  throw { code: 'FORBIDDEN', message: '当前 channel 无绑定 agent' };
1725
1733
  const res = this.agentApplicationService
1726
- ? await execAgentQuery({ aid: selfAid }, this.agentApplicationService, subject)
1727
- : await execAgentQuery({ aid: selfAid }, undefined, subject);
1734
+ ? await execAgentQuery({ aid: selfAid }, this.agentApplicationService, agentFullAccessMenuSubject(subject, source))
1735
+ : await execAgentQuery({ aid: selfAid }, undefined, agentFullAccessMenuSubject(subject, source));
1728
1736
  if ('error' in res)
1729
1737
  throw { code: res.code, message: res.error };
1730
1738
  const ag = res.data;
@@ -1966,7 +1974,7 @@ export async function getSubMenuItems(cmd, channel, channelId, userId, args, ove
1966
1974
  }
1967
1975
  catch (error) {
1968
1976
  if (isDaemonControlSubject(subject)) {
1969
- return ['low', 'medium', 'high', 'auto'].map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e }));
1977
+ return [...configurableEfforts(target.baseagent), 'auto'].map(e => ({ value: e, label: e === 'auto' ? 'auto (SDK默认)' : e }));
1970
1978
  }
1971
1979
  throw error;
1972
1980
  }
@@ -2066,6 +2074,26 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2066
2074
  fromControlChannel,
2067
2075
  subject: authSubject,
2068
2076
  });
2077
+ if (cmdBase === '/session' && args?.view === 'loop') {
2078
+ if (Object.keys(args).some(key => key !== 'view' && key !== 'context')) {
2079
+ return { error: 'session.loop query only accepts args.view', code: 'INVALID_ARGUMENT' };
2080
+ }
2081
+ try {
2082
+ const route = { channel, channelId, selfAid: subject.selfAid ?? '', threadId: transportReplyContext?.threadId };
2083
+ const target = await resolveCurrentSessionLoopTarget(this.sessionManager, route);
2084
+ const denied = await authorizeMenuIntent.call(this, {
2085
+ intent: { operation: 'session.loop.current', scope: 'relation', source, args: { sessionId: target.session.id, peer: target.peerKey } },
2086
+ identity, subject: { ...subject, sessionId: target.session.id }, session: target.session, channel, channelId, userId, fromControlChannel, source,
2087
+ });
2088
+ if (denied)
2089
+ return denied;
2090
+ const current = await refreshSessionLoopTarget(this.sessionManager, route, target);
2091
+ return { data: sessionLoopPayload(current) };
2092
+ }
2093
+ catch (error) {
2094
+ return menuResultFailure(error);
2095
+ }
2096
+ }
2069
2097
  const storageRequest = { type: 'menu.query', cmd, args };
2070
2098
  if (isAgentStorageMenuRequest(storageRequest)) {
2071
2099
  try {
@@ -2270,8 +2298,8 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2270
2298
  if (fromControlChannel) {
2271
2299
  if (args?.aid) {
2272
2300
  return this.agentApplicationService
2273
- ? await execAgentQuery(args, this.agentApplicationService, subject)
2274
- : await execAgentQuery(args, undefined, subject);
2301
+ ? await execAgentQuery(args, this.agentApplicationService, agentFullAccessMenuSubject(subject, source))
2302
+ : await execAgentQuery(args, undefined, agentFullAccessMenuSubject(subject, source));
2275
2303
  }
2276
2304
  return this.agentApplicationService
2277
2305
  ? await execAgentOptions(args, this.agentApplicationService)
@@ -2281,8 +2309,8 @@ export async function execMenuQuery(cmd, channel, channelId, userId, args, expli
2281
2309
  if (!selfAid)
2282
2310
  return { error: '当前 channel 无绑定 agent', code: 'FORBIDDEN' };
2283
2311
  return this.agentApplicationService
2284
- ? await execAgentQuery({ ...(args ?? {}), aid: selfAid }, this.agentApplicationService, subject)
2285
- : await execAgentQuery({ ...(args ?? {}), aid: selfAid }, undefined, subject);
2312
+ ? await execAgentQuery({ ...(args ?? {}), aid: selfAid }, this.agentApplicationService, agentFullAccessMenuSubject(subject, source))
2313
+ : await execAgentQuery({ ...(args ?? {}), aid: selfAid }, undefined, agentFullAccessMenuSubject(subject, source));
2286
2314
  }
2287
2315
  // ── /gateway 查询(只读,列出全部作用域的网关配置;apiKey 已掩码) ──
2288
2316
  if (cmdBase === '/gateway') {
@@ -2863,6 +2891,21 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2863
2891
  const cmdBase = cmd.trim().split(' ')[0];
2864
2892
  if (!cmdBase)
2865
2893
  return { error: '缺少命令', code: 'MISSING_CMD' };
2894
+ const isLoopUpdate = cmdBase === '/session' && args?.resource === 'loop';
2895
+ if (isLoopUpdate) {
2896
+ if (typeof value !== 'boolean' || Object.keys(args ?? {}).some(key => key !== 'resource' && key !== 'idleSeconds' && key !== 'context')) {
2897
+ return { error: 'session.loop requires a boolean value and only accepts resource / idleSeconds', code: 'INVALID_ARGUMENT' };
2898
+ }
2899
+ if (!value && Object.hasOwn(args ?? {}, 'idleSeconds')) {
2900
+ return { error: '关闭时不能提供 idleSeconds', code: 'INVALID_ARGUMENT' };
2901
+ }
2902
+ if (Object.hasOwn(args ?? {}, 'idleSeconds') && validateIdleSeconds(args?.idleSeconds) === undefined) {
2903
+ return { error: `idleSeconds 必须是 ${MIN_SESSION_LOOP_SECONDS}-${MAX_SESSION_LOOP_SECONDS} 的整数`, code: 'INVALID_ARGUMENT' };
2904
+ }
2905
+ }
2906
+ else if (typeof value !== 'string') {
2907
+ return { error: 'value 必须是字符串', code: 'INVALID_ARGUMENT' };
2908
+ }
2866
2909
  if (cmdBase === '/perm') {
2867
2910
  return { error: 'permissionMode 只能通过角色策略编辑器修改', code: 'ROLE_POLICY_MANAGED' };
2868
2911
  }
@@ -2888,9 +2931,6 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2888
2931
  }
2889
2932
  if (cmdBase === '/baseagent-models')
2890
2933
  return { error: 'Baseagent catalog only supports menu.query', code: 'NOT_SUPPORTED' };
2891
- const arg = value.trim();
2892
- if (!arg)
2893
- return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
2894
2934
  const identity = overrideIdentity ?? this.sessionManager.resolveIdentity(channel, userId, ...menuIdentityArgs(session));
2895
2935
  const isAdmin = isManagementRole(identity.role);
2896
2936
  const subject = buildMenuAuthSubject(this, {
@@ -2902,6 +2942,31 @@ export async function execMenuUpdate(cmd, value, channel, channelId, userId, ove
2902
2942
  fromControlChannel,
2903
2943
  subject: authSubject,
2904
2944
  });
2945
+ if (isLoopUpdate) {
2946
+ try {
2947
+ const route = { channel, channelId, selfAid: subject.selfAid ?? '', threadId: transportReplyContext?.threadId };
2948
+ const target = await resolveCurrentSessionLoopTarget(this.sessionManager, route);
2949
+ const denied = await authorizeMenuIntent.call(this, {
2950
+ intent: { operation: 'session.loop.update', scope: 'relation', source, args: { sessionId: target.session.id, peer: target.peerKey, value, ...(value ? { idleSeconds: args?.idleSeconds ?? DEFAULT_SESSION_LOOP_SECONDS } : {}) } },
2951
+ identity, subject: { ...subject, sessionId: target.session.id }, session: target.session, channel, channelId, userId, fromControlChannel, source,
2952
+ });
2953
+ if (denied)
2954
+ return denied;
2955
+ const current = await refreshSessionLoopTarget(this.sessionManager, route, target);
2956
+ const config = value === true
2957
+ ? { enabled: true, idleSeconds: args?.idleSeconds ?? DEFAULT_SESSION_LOOP_SECONDS }
2958
+ : { enabled: false };
2959
+ if (config.enabled)
2960
+ this.assertSessionLoopTrigger(current.session);
2961
+ return { data: sessionLoopPayload(writeSessionLoopConfig(this.sessionManager, current, config)) };
2962
+ }
2963
+ catch (error) {
2964
+ return menuResultFailure(error);
2965
+ }
2966
+ }
2967
+ const arg = value.trim();
2968
+ if (!arg)
2969
+ return { error: '缺少 value 参数', code: 'MISSING_VALUE' };
2905
2970
  if (cmdBase === '/role') {
2906
2971
  const authorized = await authorizeRoleMenu(this, {
2907
2972
  kind: 'update', args, value: arg, identity, subject: authSubject, session, channel, channelId,
@@ -3654,7 +3719,7 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
3654
3719
  }
3655
3720
  }
3656
3721
  }
3657
- return await execAgentAction(action, a, userId ?? '', this.eventBus, this.agentApplicationService, subject);
3722
+ return await execAgentAction(action, a, userId ?? '', this.eventBus, this.agentApplicationService, agentFullAccessMenuSubject(subject, source));
3658
3723
  }
3659
3724
  // ── 关系级 /trigger(不走 owners;复用 isAdmin + scoped 逻辑,D4 直调底层) ──
3660
3725
  if (cmdBase === '/trigger') {
@@ -4123,6 +4188,21 @@ export async function execMenuAction(cmd, action, args, channel, channelId, user
4123
4188
  }
4124
4189
  catch { }
4125
4190
  }
4191
+ // P1a.5.3: 若为话题会话,同步更新话题索引
4192
+ if (topic.threadId) {
4193
+ try {
4194
+ const { ThreadService } = await import('../../core/thread/index.js');
4195
+ const selfAid = topic.selfAID;
4196
+ const peer = topic.channelId;
4197
+ if (selfAid && peer) {
4198
+ const svc = new ThreadService(this.sessionManager);
4199
+ await svc.renameThread({ selfAid, peer, threadId: topic.threadId, name: newName });
4200
+ }
4201
+ }
4202
+ catch (err) {
4203
+ logger.debug('[MenuHandler] Thread index rename sync failed:', err);
4204
+ }
4205
+ }
4126
4206
  this.eventBus.publish({ type: 'session:renamed', sessionId: topic.id, oldName, newName });
4127
4207
  return {
4128
4208
  data: {
@@ -4754,6 +4834,7 @@ async function execMenuForSystemControl(payload, context) {
4754
4834
  : undefined,
4755
4835
  });
4756
4836
  const trustedIdentity = trustedSubject.identity;
4837
+ trustedSubject.agentFullAccessManagement = context.source === 'control' && context.authMethod === 'daemon-owner';
4757
4838
  const cmd = name ? (menuCommandForName(name) ?? payload.cmd) : payload.cmd;
4758
4839
  if (payload?.type === 'menu.action'
4759
4840
  && menuCommandToken(cmd) === '/system'
@@ -4781,6 +4862,14 @@ async function execMenuForSystemControl(payload, context) {
4781
4862
  request: payload.args.context,
4782
4863
  paircodeAllowed: context.authMethod === 'daemon-owner'
4783
4864
  && (context.source !== 'control' || context.encrypted),
4865
+ resolveLoopTarget: async (catalogContext) => {
4866
+ const target = await resolveCurrentSessionLoopTarget(this.sessionManager, {
4867
+ channel: targetChannel, channelId: catalogContext.channelId, selfAid: catalogContext.self ?? '',
4868
+ });
4869
+ if (target.peerKey !== catalogContext.peerKey)
4870
+ throw { code: 'INVALID_CONTEXT', message: '当前会话关系不匹配' };
4871
+ return { level: target.level, sessionId: target.session.id };
4872
+ },
4784
4873
  });
4785
4874
  return menuSuccess({ id, ...(name ? { name } : {}) }, data);
4786
4875
  }
@@ -4839,8 +4928,10 @@ async function execMenuForSystemControl(payload, context) {
4839
4928
  case 'menu.update': {
4840
4929
  if (!cmd)
4841
4930
  return ecwebErr(id, name, 'MISSING_CMD', '缺少 name/cmd');
4842
- if (!payload.value)
4931
+ if (payload.value === undefined || payload.value === null
4932
+ || (typeof payload.value === 'string' && payload.value.length === 0)) {
4843
4933
  return ecwebErr(id, name, 'MISSING_VALUE', '缺少 value');
4934
+ }
4844
4935
  const execution = await resolveExplicitMenuExecutionContext({
4845
4936
  subject: trustedSubject,
4846
4937
  channelType: 'aun',
@@ -4,6 +4,7 @@ import { loadDaemonConfig } from '../../config-store.js';
4
4
  import { readInstalledEvolcoreVersion } from '../../utils/evolcore-version.js';
5
5
  import { compareStableSemver, parseStableSemver } from '../../utils/stable-semver.js';
6
6
  import { logger } from '../../utils/logger.js';
7
+ import { validateIdleSeconds } from '../session/session-idle-monitor.js';
7
8
  export const MENU_REQUEST_TYPES = new Set([
8
9
  'menu.token.request',
9
10
  'menu.list',
@@ -334,9 +335,31 @@ export function validateMenuRequest(request) {
334
335
  if (scopeError)
335
336
  return scopeError;
336
337
  }
337
- if (type === 'menu.update' && (typeof request.value !== 'string' || request.value.length === 0)) {
338
+ const isSessionLoopUpdate = type === 'menu.update'
339
+ && (request.name === 'session' || request.cmd === '/session')
340
+ && request.args?.resource === 'loop';
341
+ if (isSessionLoopUpdate) {
342
+ if (typeof request.value !== 'boolean')
343
+ return { code: 'INVALID_ARGUMENT', message: 'session.loop value must be a boolean' };
344
+ const args = request.args;
345
+ const unknown = Object.keys(args ?? {}).find(key => !['resource', 'idleSeconds', 'context'].includes(key));
346
+ if (unknown)
347
+ return { code: 'INVALID_ARGUMENT', message: `session.loop update does not accept args.${unknown}` };
348
+ if (Object.hasOwn(args ?? {}, 'idleSeconds') && validateIdleSeconds(args?.idleSeconds) === undefined) {
349
+ return { code: 'INVALID_ARGUMENT', message: 'idleSeconds must be an integer between 30 and 86400' };
350
+ }
351
+ if (request.value === false && Object.hasOwn(args ?? {}, 'idleSeconds')) {
352
+ return { code: 'INVALID_ARGUMENT', message: 'idleSeconds is not allowed when session.loop is disabled' };
353
+ }
354
+ }
355
+ else if (type === 'menu.update' && (typeof request.value !== 'string' || request.value.length === 0)) {
338
356
  return { code: 'INVALID_ARGUMENT', message: 'Menu request value is required' };
339
357
  }
358
+ if (type === 'menu.query' && (request.name === 'session' || request.cmd === '/session')
359
+ && request.args?.view === 'loop'
360
+ && Object.keys(request.args).some(key => key !== 'view' && key !== 'context')) {
361
+ return { code: 'INVALID_ARGUMENT', message: 'session.loop query only accepts args.view' };
362
+ }
340
363
  if (type === 'menu.action' && (typeof request.action !== 'string' || request.action.trim().length === 0)) {
341
364
  return { code: 'INVALID_ARGUMENT', message: 'Menu request action is required' };
342
365
  }
@@ -447,6 +470,7 @@ const STABLE_CODES = new Set([
447
470
  'RESPONSE_TOO_LARGE',
448
471
  'ACTIVITY_NOT_FOUND', 'ACTIVITY_EXPIRED',
449
472
  'TOPIC_SESSION_NOT_FOUND', 'TOPIC_SESSION_LOOKUP_FAILED',
473
+ 'LOOP_TRIGGER_NOT_FOUND', 'LOOP_TRIGGER_DISABLED', 'LOOP_TRIGGER_INVALID',
450
474
  'ROOT_NOT_FOUND', 'ROOT_FORBIDDEN', 'ROOT_EXPIRED', 'ROOT_SCOPE_MISMATCH', 'ROOT_PEER_MISMATCH',
451
475
  'INVALID_ROOT_ID', 'MISSING_ROOT_ID', 'INVALID_ROOT_PATH', 'NOT_A_DIRECTORY', 'FILE_TOO_LARGE',
452
476
  ]);
@@ -0,0 +1,322 @@
1
+ /**
2
+ * skill-slash.ts — `/skill` 斜杠命令:技能查看与索引刷新
3
+ *
4
+ * 权限:
5
+ * - 只读(list / show / where):私聊所有角色可看;群聊仅 admin+ 可看
6
+ * - refresh:私聊仅 admin+;群聊一律拒绝
7
+ *
8
+ * 本文件刻意**只做只读 + refresh**:写操作(install / remove / enable / disable)
9
+ * 的权限门禁在 `kits/skills/ec-skill/scripts/skill.mjs` 的 `assertPermission()` 里,
10
+ * 那套判据(群聊拒绝、agent 级需本 Agent owner、本机级需 daemonowner)依赖运行时上下文,
11
+ * 在 slash 层重实现一份必然漂移。写操作请走 `ec skill` 或 ec-skill 技能。
12
+ */
13
+ import fs from 'fs';
14
+ import path from 'path';
15
+ import { agentDir, getPackageRoot, resolveRoot } from '../../paths.js';
16
+ import { resolveAllSkillStates } from '../skill/switch-resolver.js';
17
+ import { readSkillDescription } from '../skill/index-generator.js';
18
+ import { evaluateWhen, loadManifest, resolvePathWithDiag, } from '../../eck/manifest-engine.js';
19
+ import { DEFAULT_MANIFEST_FILE } from '../../eck/kit-renderer.js';
20
+ import { resolveAgentLifecycle } from '../../config/lifecycle.js';
21
+ const USAGE = [
22
+ '用法:',
23
+ ' /skill 技能概览(启用数 / 总数)',
24
+ ' /skill list [all] 技能列表(默认仅启用;all 显示未启用的)',
25
+ ' /skill index 查看技能索引加载情况(哪些索引文件被加载、时间、技能数)',
26
+ ' /skill index builtin 查看内置技能索引正文',
27
+ ' /skill index local 查看本机技能索引正文',
28
+ ' /skill show <name> 单个技能详情:来源、状态、生效依据、描述',
29
+ ' /skill where 打印三级技能根目录',
30
+ ' /skill refresh 重建当前 Agent 的技能索引',
31
+ '',
32
+ '写操作(install / remove / enable / disable)请用 ec skill 或 ec-skill 技能。',
33
+ ].join('\n');
34
+ const result = (text) => ({ kind: 'command.result', text });
35
+ const error = (text) => ({ kind: 'command.error', text: `❌ ${text}` });
36
+ /** 只读子命令:私聊所有角色;群聊仅 admin+。 */
37
+ export function canReadSkillSlash(ctx) {
38
+ return ctx.chatType === 'group' ? ctx.isAdmin : true;
39
+ }
40
+ /** refresh:私聊仅 admin+;群聊一律拒绝。 */
41
+ export function canRefreshSkillSlash(ctx) {
42
+ return ctx.chatType === 'group' ? false : ctx.isAdmin;
43
+ }
44
+ export function canUseSkillSlash(ctx) {
45
+ return canReadSkillSlash(ctx);
46
+ }
47
+ /** evolhelp「技能管理」条目。可见范围与 canReadSkillSlash / canRefreshSkillSlash 一致。 */
48
+ export function skillHelpEntries(ctx) {
49
+ if (!canUseSkillSlash(ctx))
50
+ return [];
51
+ const category = '技能管理';
52
+ const roles = [ctx.role];
53
+ const out = [
54
+ { command: '/skill list', description: '已启用的技能列表', category, roles },
55
+ { command: '/skill list all', description: '全部技能列表(含未启用)', category, roles },
56
+ { command: '/skill index', description: '技能索引加载情况(文件、时间、技能数)', category, roles },
57
+ { command: '/skill index builtin', description: '查看内置技能索引正文', category, roles },
58
+ { command: '/skill index local', description: '查看本机技能索引正文', category, roles },
59
+ { command: '/skill where', description: '三级技能根目录', category, roles },
60
+ ];
61
+ if (canRefreshSkillSlash(ctx)) {
62
+ out.push({ command: '/skill refresh', description: '重建当前 Agent 的技能索引', category, roles });
63
+ }
64
+ out.push({ command: '/skill show', description: '单个技能详情', args: '<name>', category, roles });
65
+ return out;
66
+ }
67
+ /** 技能三级根目录;与 ec-skill 脚本的 skillRoots() 保持一致。 */
68
+ function skillRoots(selfAid) {
69
+ return {
70
+ builtin: path.join(getPackageRoot(), 'kits', 'skills'),
71
+ local: path.join(resolveRoot(), 'skills'),
72
+ agent: selfAid ? path.join(agentDir(selfAid), 'skills') : '(未确定 agent)',
73
+ };
74
+ }
75
+ /** 目录本身是 agent 级 skills/,resolveAllSkillStates 要的是 agent 根目录。 */
76
+ function agentRootFor(selfAid) {
77
+ return selfAid ? agentDir(selfAid) : undefined;
78
+ }
79
+ function sourceLabel(source) {
80
+ return source === 'builtin' ? '内置' : '本机';
81
+ }
82
+ function stateLabel(s) {
83
+ return s.enabled ? '开' : '关';
84
+ }
85
+ function overviewLine(states) {
86
+ const enabled = states.filter(s => s.enabled).length;
87
+ return `${states.length} 个技能,${enabled} 个启用`;
88
+ }
89
+ function formatList(states, showAll) {
90
+ const shown = showAll ? states : states.filter(s => s.enabled);
91
+ if (shown.length === 0) {
92
+ return showAll ? '(没有任何技能)' : '(没有启用的技能;用 /skill list all 查看全部)';
93
+ }
94
+ const nameW = Math.max(20, ...shown.map(s => s.name.length + 2));
95
+ const lines = [`${overviewLine(states)}${showAll ? '' : '(仅列启用;/skill list all 查看全部)'}`, ''];
96
+ for (const s of shown) {
97
+ const lock = s.locked ? ' [locked]' : '';
98
+ lines.push(` ${s.name.padEnd(nameW)}${sourceLabel(s.source).padEnd(6)}${stateLabel(s)}${lock}`);
99
+ }
100
+ return lines.join('\n');
101
+ }
102
+ function formatInfo(s) {
103
+ const desc = readSkillDescription(s.skillDir);
104
+ const lines = [
105
+ `技能: ${s.name}`,
106
+ `来源: ${sourceLabel(s.source)}`,
107
+ `路径: ${s.skillDir}`,
108
+ `状态: ${stateLabel(s)}(由 ${s.decidedBy} 决定)`,
109
+ ];
110
+ if (s.locked)
111
+ lines.push('锁定: 内置级 locked,任何级别都关不掉');
112
+ lines.push('', `描述: ${desc || '(无)'}`);
113
+ return lines.join('\n');
114
+ }
115
+ function formatWhere(selfAid) {
116
+ const roots = skillRoots(selfAid);
117
+ return [
118
+ '三级技能根目录:',
119
+ ` 内置级: ${roots.builtin}`,
120
+ ` 本机级: ${roots.local}`,
121
+ ` Agent 级: ${roots.agent}`,
122
+ '',
123
+ 'Agent 级只放 .switches.json(开关),不放 SKILL.md。',
124
+ ].join('\n');
125
+ }
126
+ /**
127
+ * 读**生效的 manifest**(含 ECK override),挑出指向技能索引文件的段,
128
+ * 再逐段判定实际是否会被注入。
129
+ *
130
+ * 为什么不直接扫磁盘:`optional: true` 的文件缺失会被静默跳过,`when` 不满足或
131
+ * 总闸超限也会让段不加载。只有走 manifest 判定,`/skill index` 才能反映"实际加载"。
132
+ */
133
+ function inspectSkillIndexes(agentRoot, selfAid, lifecycle) {
134
+ const manifestFile = DEFAULT_MANIFEST_FILE;
135
+ const sections = loadManifest(manifestFile);
136
+ // 只关心指向 <agentRoot>/skills/.index-*.md 的段;override 可能加段或改路径,
137
+ // 所以按解析后的路径判断,而不是按段 id 匹配。
138
+ const skillsDir = path.join(agentRoot, 'skills');
139
+ // lifecycle 与渲染器同源解析:显式非法值按 null(fail-closed),缺省按 active 兼容。
140
+ const resolvedLifecycle = resolveAgentLifecycle({ lifecycle });
141
+ const vars = {
142
+ selfAid,
143
+ EVOLCORE_HOME: resolveRoot(),
144
+ AGENT_DIR: agentRoot,
145
+ ...(resolvedLifecycle === null ? {} : { lifecycle: resolvedLifecycle }),
146
+ };
147
+ const out = [];
148
+ for (const section of sections) {
149
+ if (section.type !== 'file' || !section.file)
150
+ continue;
151
+ // 与 kit-renderer 共用同一个解析器:变量替换 + path.normalize,不做任何空白裁剪。
152
+ // 变量缺失时 status !== 'ok',resolved 可能为 null —— 段会被跳过,而不是拼出一个
153
+ // 看起来合理的错误路径(原先的 stripBlankLines 版本会静默拼错,还吃掉路径里的空格)。
154
+ const r = resolvePathWithDiag(section.file, vars);
155
+ if (r.status !== 'ok' || !r.resolved)
156
+ continue;
157
+ const resolved = r.resolved;
158
+ if (!isSkillIndexPath(resolved, skillsDir))
159
+ continue;
160
+ const enabled = section.enabled !== false;
161
+ const whenPassed = evaluateWhen(section.when ?? 'always', vars);
162
+ const fileExists = fs.existsSync(resolved);
163
+ let reason;
164
+ if (!enabled)
165
+ reason = 'manifest 中该段 enabled: false';
166
+ else if (!whenPassed)
167
+ reason = `manifest 的 when 条件不满足(当前 lifecycle=${resolvedLifecycle ?? '值非法'})`;
168
+ else if (!fileExists)
169
+ reason = section.optional ? '文件不存在(optional,静默跳过)' : '文件不存在';
170
+ const diag = {
171
+ id: section.id,
172
+ rawPath: section.file,
173
+ resolvedPath: resolved,
174
+ loaded: enabled && whenPassed && fileExists,
175
+ ...(reason ? { reason } : {}),
176
+ fileExists,
177
+ };
178
+ if (fileExists) {
179
+ try {
180
+ const st = fs.statSync(resolved);
181
+ diag.sizeBytes = st.size;
182
+ diag.mtimeMs = st.mtimeMs;
183
+ diag.skillCount = countSkillsInIndex(fs.readFileSync(resolved, 'utf-8'));
184
+ }
185
+ catch { /* 读失败保持字段缺省 */ }
186
+ }
187
+ out.push(diag);
188
+ }
189
+ return out;
190
+ }
191
+ /** 判断解析后的路径是否落在本 agent 的 skills 索引目录里,且文件名形如 .index-*.md */
192
+ function isSkillIndexPath(resolved, skillsDir) {
193
+ const dir = path.dirname(resolved);
194
+ if (path.normalize(dir).toLowerCase() !== path.normalize(skillsDir).toLowerCase())
195
+ return false;
196
+ return /^\.index-.*\.md$/i.test(path.basename(resolved));
197
+ }
198
+ /**
199
+ * 从索引正文里数技能条目的数量。
200
+ *
201
+ * 索引由 index-generator 生成,段头形如 `本 Agent 已启用 N 个内置技能。`;
202
+ * 拿不到就退化为数 `- ` 开头的条目行。
203
+ */
204
+ function countSkillsInIndex(content) {
205
+ const headerMatch = content.match(/已启用\s*(\d+)\s*个/);
206
+ if (headerMatch)
207
+ return Number(headerMatch[1]);
208
+ const items = content.split('\n').filter(l => /^-\s+\S/.test(l.trim()));
209
+ return items.length > 0 ? items.length : undefined;
210
+ }
211
+ function formatIndexOverview(sections, selfAid) {
212
+ if (sections.length === 0) {
213
+ return '当前生效的 manifest 里没有引用任何技能索引文件。\n'
214
+ + '(技能索引由 kits/eck_manifest.json 的 skills-builtin / skills-local 段注入)';
215
+ }
216
+ const lines = [`技能索引加载情况(agent: ${selfAid})`, ''];
217
+ for (const s of sections) {
218
+ const state = s.loaded ? '已加载' : '未加载';
219
+ lines.push(`${state} ${path.basename(s.resolvedPath)}`);
220
+ lines.push(` manifest 段: ${s.id}`);
221
+ if (s.loaded) {
222
+ lines.push(` 技能数: ${s.skillCount ?? '(解析不到)'}`);
223
+ lines.push(` 文件时间: ${s.mtimeMs ? new Date(s.mtimeMs).toLocaleString('zh-CN') : '(未知)'}`);
224
+ lines.push(` 大小: ${s.sizeBytes ?? '?'} 字节`);
225
+ }
226
+ else {
227
+ lines.push(` 原因: ${s.reason ?? '未知'}`);
228
+ }
229
+ lines.push(` 路径: ${s.resolvedPath}`);
230
+ lines.push('');
231
+ }
232
+ const loaded = sections.filter(s => s.loaded);
233
+ const total = loaded.reduce((n, s) => n + (s.skillCount ?? 0), 0);
234
+ lines.push(`合计:${loaded.length}/${sections.length} 个索引文件被加载,共 ${total} 个技能`);
235
+ lines.push('查看正文: /skill index builtin 或 /skill index local');
236
+ return lines.join('\n');
237
+ }
238
+ /** 读某个索引文件正文;kind 由段 id 或路径推断。 */
239
+ function readIndexFile(selfAid, kind, lifecycle) {
240
+ const root = agentRootFor(selfAid);
241
+ if (!root)
242
+ return '❌ 无法确定当前 agent 目录';
243
+ const file = path.join(root, 'skills', kind === 'builtin' ? '.index-builtin.md' : '.index-local.md');
244
+ const sections = inspectSkillIndexes(root, selfAid, lifecycle);
245
+ const section = sections.find(s => path.basename(s.resolvedPath).toLowerCase() === path.basename(file).toLowerCase());
246
+ if (section && !section.loaded) {
247
+ return `⚠️ ${path.basename(file)} 当前未加载(${section.reason ?? '未知原因'})\n`
248
+ + `路径: ${file}`;
249
+ }
250
+ if (!fs.existsSync(file)) {
251
+ return `(${path.basename(file)} 不存在——该来源当前没有启用任何技能,索引生成时会被删除)`;
252
+ }
253
+ const content = fs.readFileSync(file, 'utf-8');
254
+ const count = countSkillsInIndex(content);
255
+ const header = `${path.basename(file)}(${count ?? '?'} 个技能,${new Date(fs.statSync(file).mtimeMs).toLocaleString('zh-CN')})`;
256
+ return `${header}\n\n${content}`;
257
+ }
258
+ export async function handleSkillSlash(ctx) {
259
+ const args = ctx.content.trim().split(/\s+/).slice(1);
260
+ const sub = args[0];
261
+ if (!sub || sub === 'help') {
262
+ if (sub === 'help')
263
+ return result(USAGE);
264
+ if (!canReadSkillSlash(ctx))
265
+ return error('群聊中技能列表仅限管理员查看');
266
+ const root = agentRootFor(ctx.selfAid);
267
+ if (!root)
268
+ return error('无法确定当前 agent 目录');
269
+ return result(`${overviewLine(resolveAllSkillStates(root))}\n\n输入 /skill help 查看全部子命令`);
270
+ }
271
+ if (!canReadSkillSlash(ctx))
272
+ return error('群聊中技能查看仅限管理员');
273
+ const root = agentRootFor(ctx.selfAid);
274
+ if (!root)
275
+ return error('无法确定当前 agent 目录');
276
+ try {
277
+ if (sub === 'list') {
278
+ const showAll = args[1] === 'all';
279
+ return result(formatList(resolveAllSkillStates(root), showAll));
280
+ }
281
+ if (sub === 'show' || sub === 'info') {
282
+ const name = args[1];
283
+ if (!name)
284
+ return error('用法: /skill show <name>');
285
+ const found = resolveAllSkillStates(root).find(s => s.name === name);
286
+ if (!found)
287
+ return error(`技能不存在: ${name}\n用 /skill list all 查看全部技能`);
288
+ return result(formatInfo(found));
289
+ }
290
+ if (sub === 'index') {
291
+ const kind = args[1];
292
+ if (kind === 'builtin' || kind === 'local') {
293
+ return result(readIndexFile(ctx.selfAid, kind, ctx.lifecycle));
294
+ }
295
+ if (kind !== undefined) {
296
+ return error(`未知索引: ${kind}\n用法: /skill index [builtin|local]`);
297
+ }
298
+ return result(formatIndexOverview(inspectSkillIndexes(root, ctx.selfAid, ctx.lifecycle), ctx.selfAid));
299
+ }
300
+ if (sub === 'where')
301
+ return result(formatWhere(ctx.selfAid));
302
+ if (sub === 'refresh') {
303
+ if (!canRefreshSkillSlash(ctx)) {
304
+ return error(ctx.chatType === 'group'
305
+ ? '群聊中不支持重建技能索引,请私聊或由管理员在终端执行'
306
+ : '无权限:重建技能索引仅限管理员');
307
+ }
308
+ if (!ctx.selfAid)
309
+ return error('无法确定当前 agent AID');
310
+ if (!ctx.refresher)
311
+ return error('daemon 未注入索引刷新入口');
312
+ const r = await ctx.refresher(ctx.selfAid);
313
+ if (!r.ok)
314
+ return error(`重建技能索引失败:${r.error ?? '未知错误'}`);
315
+ return result('✅ 已重建当前 Agent 的技能索引');
316
+ }
317
+ return error(`未知子命令: ${sub}\n\n${USAGE}`);
318
+ }
319
+ catch (e) {
320
+ return error(e instanceof Error ? e.message : String(e));
321
+ }
322
+ }