evolcore 0.0.10 → 0.0.12

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 (131) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +3 -3
  3. package/bin/codex-managed-hook.mjs +80 -0
  4. package/dist/agents/baseagent.js +4 -0
  5. package/dist/agents/claude-runner.js +158 -44
  6. package/dist/agents/codex-app-server-client.js +153 -23
  7. package/dist/agents/codex-runner.js +331 -43
  8. package/dist/agents/ecagent-runner.js +53 -6
  9. package/dist/agents/gemini-runner.js +1 -0
  10. package/dist/agents/request-identity.js +55 -0
  11. package/dist/aun/aid/client.js +11 -21
  12. package/dist/aun/aid/managed-operation.js +107 -0
  13. package/dist/aun/msg/group.js +81 -12
  14. package/dist/aun/msg/managed-operation.js +804 -0
  15. package/dist/aun/msg/mention-schema.js +20 -0
  16. package/dist/aun/msg/p2p.js +7 -0
  17. package/dist/aun/outbox.js +28 -31
  18. package/dist/channels/aun.js +418 -235
  19. package/dist/channels/daemon.js +13 -0
  20. package/dist/cli/agent-command.js +72 -17
  21. package/dist/cli/agent.js +249 -22
  22. package/dist/cli/aun-commands.js +270 -69
  23. package/dist/cli/bench.js +12 -3
  24. package/dist/cli/daemon-commands.js +52 -33
  25. package/dist/cli/fs-command.js +60 -0
  26. package/dist/cli/handoff-command.js +3 -0
  27. package/dist/cli/index.js +106 -63
  28. package/dist/cli/init-cancel.js +208 -0
  29. package/dist/cli/init-channel.js +343 -195
  30. package/dist/cli/init.js +21 -9
  31. package/dist/cli/managed-command-guard.js +41 -0
  32. package/dist/cli/model.js +11 -4
  33. package/dist/cli/queue-command.js +11 -1
  34. package/dist/cli/response.js +71 -0
  35. package/dist/cli/restart-monitor.js +4 -18
  36. package/dist/cli/trigger-command.js +4 -0
  37. package/dist/config/builtin-roles.js +5 -0
  38. package/dist/config/contact-book-store.js +11 -3
  39. package/dist/config/contact-request-service.js +161 -15
  40. package/dist/config/gateway-config.js +26 -10
  41. package/dist/config/mention-mode.js +8 -24
  42. package/dist/core/agent-reload-coordinator.js +53 -0
  43. package/dist/core/auth/agent-delegation.js +0 -1
  44. package/dist/core/auth/operation-authorizer.js +62 -154
  45. package/dist/core/auth/operation-catalog.js +264 -22
  46. package/dist/core/bootstrap-messages.js +68 -0
  47. package/dist/core/bootstrap-service.js +144 -10
  48. package/dist/core/capability/providers/claude-capability-provider.js +9 -31
  49. package/dist/core/capability/providers/codex-capability-provider.js +4 -15
  50. package/dist/core/capability/skill-discovery.js +55 -0
  51. package/dist/core/channel-loader.js +24 -7
  52. package/dist/core/command/agent-control.js +14 -11
  53. package/dist/core/command/cli-intent-parser.js +9 -0
  54. package/dist/core/command/command-handler.js +467 -6
  55. package/dist/core/command/connect-menu.js +15 -0
  56. package/dist/core/command/group-menu.js +4 -4
  57. package/dist/core/command/menu-catalog.js +366 -0
  58. package/dist/core/command/menu-handler.js +213 -110
  59. package/dist/core/command/menu-protocol.js +71 -0
  60. package/dist/core/command/slash-gate.js +5 -2
  61. package/dist/core/command/slash-handler.js +45 -25
  62. package/dist/core/daemon-file-cache.js +2 -1
  63. package/dist/core/data-migration.js +9 -2
  64. package/dist/core/evolagent-registry.js +125 -35
  65. package/dist/core/evolagent.js +12 -5
  66. package/dist/core/handoff/dispatcher.js +5 -2
  67. package/dist/core/handoff/runtime.js +145 -21
  68. package/dist/core/handoff/store.js +24 -0
  69. package/dist/core/handoff/types.js +1 -0
  70. package/dist/core/inference/text-inference.js +38 -4
  71. package/dist/core/message/logical-queue-bridge.js +1 -0
  72. package/dist/core/message/mention-schema.js +126 -0
  73. package/dist/core/message/message-bridge.js +237 -79
  74. package/dist/core/message/message-log.js +23 -0
  75. package/dist/core/message/message-queue.js +20 -4
  76. package/dist/core/message/response-engine.js +54 -36
  77. package/dist/core/model/model-catalog.js +143 -24
  78. package/dist/core/model/model-diagnostics.js +28 -10
  79. package/dist/core/permission/ec-command-parser.js +156 -10
  80. package/dist/core/permission/index.js +1 -0
  81. package/dist/core/permission/readonly-shell-query.js +532 -0
  82. package/dist/core/permission/sandbox-runtime.js +22 -1
  83. package/dist/core/permission/shell-environment.js +46 -0
  84. package/dist/core/permission/tool-policy.js +292 -111
  85. package/dist/core/protected-paths.js +17 -9
  86. package/dist/core/runner-reload-transaction.js +57 -0
  87. package/dist/core/session/session-manager.js +21 -2
  88. package/dist/eck/manifest-engine.js +39 -13
  89. package/dist/index.js +644 -180
  90. package/dist/ipc.js +246 -19
  91. package/dist/response-system/coordinator.js +6 -4
  92. package/dist/response-system/engines/v1/proactive-flow.js +26 -8
  93. package/dist/response-system/modes/single-session/index.js +14 -1
  94. package/dist/response-system/selector.js +6 -5
  95. package/dist/trigger/anomaly-store.js +4 -0
  96. package/dist/trigger/script-executor.js +1 -0
  97. package/dist/utils/aid-bind.js +3 -8
  98. package/dist/utils/codex-app-server-registry.js +90 -0
  99. package/dist/utils/codex-cli.js +5 -1
  100. package/dist/utils/cross-platform.js +15 -0
  101. package/dist/utils/log-writer.js +6 -10
  102. package/dist/utils/logger.js +5 -5
  103. package/dist/utils/npm-ops.js +5 -12
  104. package/dist/utils/tool-summary.js +11 -0
  105. package/kits/docs/channels/aun.md +1 -1
  106. package/kits/docs/context-assembly.md +2 -2
  107. package/kits/docs/evolcore/agent.md +11 -6
  108. package/kits/docs/evolcore/aid.md +14 -0
  109. package/kits/docs/evolcore/msg.md +13 -0
  110. package/kits/docs/prompt-loading-architecture.md +1 -2
  111. package/kits/docs/venues/group.md +1 -1
  112. package/kits/eck_manifest.json +0 -12
  113. package/kits/rules/01-overview.md +9 -0
  114. package/kits/rules/04-relation.md +1 -1
  115. package/kits/rules/05-venue.md +2 -2
  116. package/kits/schemas/agent-config.schema.3.json +1 -1
  117. package/kits/schemas/agent-config.schema.4.json +1 -1
  118. package/kits/schemas/relation-config.schema.2.json +1 -1
  119. package/kits/schemas/role-config.schema.1.json +1 -1
  120. package/kits/schemas/single-session.schema.1.json +3 -3
  121. package/kits/schemas/single-session.schema.2.json +3 -3
  122. package/kits/templates/roles/admin.json +27 -0
  123. package/kits/templates/roles/member.json +44 -0
  124. package/kits/templates/roles/visitor.json +40 -2
  125. package/kits/templates/system-fragments/bootstrap.md +12 -6
  126. package/kits/templates/system-fragments/channel.md +6 -0
  127. package/kits/templates/system-fragments/session.md +2 -0
  128. package/package.json +3 -2
  129. package/skills/eclink/SKILL.md +15 -3
  130. package/skills/eclink/agents/openai.yaml +3 -3
  131. package/dist/core/command/evol-menu-version-gate.js +0 -39
@@ -11,7 +11,7 @@ import { StreamIdleMonitor } from './stream-idle-monitor.js';
11
11
  import { logger } from '../../utils/logger.js';
12
12
  import { isHostChinese } from '../../utils/locale.js';
13
13
  import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
14
- import { isEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
14
+ import { isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
15
15
  import { summarizeToolInput } from '../../utils/tool-summary.js';
16
16
  import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
17
17
  import { renderKitSections } from '../../eck/kit-renderer.js';
@@ -33,7 +33,6 @@ import { eligibleFallbackModels } from '../model/model-fallback.js';
33
33
  import { RetryScheduler } from './retry-scheduler.js';
34
34
  import { constrainRuntimePermissionMode, resolveRuntimeStringField } from '../role/runtime-policy.js';
35
35
  import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
36
- import { dispatchToMentionMode } from '../../config/mention-mode.js';
37
36
  import { authorizationConfigRevision, checkRoleAccess, getFirstStaticAgentOwner, listStaticAgentAdmins, listStaticAgentOwners, resolvePeerRoleDetail, roleToSessionIdentity, } from '../../config/peer-role-resolver.js';
38
37
  import { insertUsageEvent, insertContextBreakdown, insertModelCalls } from '../../stats/writer.js';
39
38
  import { normalizeUsage } from '../../stats/normalizer.js';
@@ -727,7 +726,19 @@ export class ResponseEngine {
727
726
  const selfAid = runtime?.selfAid || session?.selfAID;
728
727
  if (!selfAid)
729
728
  return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'self agent not found' };
730
- return this.handoffRuntime.returnHandoff({
729
+ return this.returnAuthorizedHandoffResult({
730
+ sessionId: params.sessionId,
731
+ handoffId: params.handoffId,
732
+ content: params.content,
733
+ }, selfAid);
734
+ }
735
+ /** Execute a handoff return after the daemon IPC boundary has authorized the task delegation. */
736
+ async returnAuthorizedHandoffResult(params, selfAid) {
737
+ if (!this.handoffRuntime) {
738
+ return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff runtime not configured' };
739
+ }
740
+ const runtime = this.activeTaskRuntimeContexts.get(params.sessionId);
741
+ return await this.handoffRuntime.returnHandoff({
731
742
  selfAid,
732
743
  currentSessionId: params.sessionId,
733
744
  handoffId: params.handoffId,
@@ -1152,6 +1163,14 @@ export class ResponseEngine {
1152
1163
  async processMessage(message) {
1153
1164
  const idleMs = (this.globalSettings.idleMonitor?.timeout ?? 120) * 1000;
1154
1165
  const totalExecutionMs = this.totalExecutionLimitMs();
1166
+ if (message.handoffDelivery
1167
+ && this.handoffRuntime
1168
+ && this.handoffRuntime.isDiscardedDelivery(message)) {
1169
+ logger.info(`[ResponseEngine] Skip stale handoff delivery before session resolution:`
1170
+ + ` direction=${message.handoffDelivery.direction}`
1171
+ + ` handoff=${message.handoffDelivery.handoffId}`);
1172
+ return;
1173
+ }
1155
1174
  // 先解析会话,再优先用 session.metadata.channelKey 精确定位实例级 adapter
1156
1175
  // message.channel 现在存实例名(channelName),可直接用于精确路由
1157
1176
  const { session, absoluteProjectPath } = await this.resolveSession(message);
@@ -1720,11 +1739,11 @@ export class ResponseEngine {
1720
1739
  messageId: message.messageId, peerId: message.peerId, content: message.content,
1721
1740
  peerType: message.peerType || session.metadata?.peerType,
1722
1741
  chatType: chatType, isMentioned: message.isMentioned,
1723
- mentionAids: message.mentionAids, source: message.source,
1742
+ mentions: message.mentions, mentionAids: message.mentionAids, source: message.source,
1724
1743
  },
1725
1744
  modeConfig: resolvedMode.context.modeConfig,
1726
1745
  state: modeState,
1727
- isSendCommand: (toolName, toolInput) => isEvolcoreSendCommandForSession(toolName, toolInput, session.channelId),
1746
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, session.selfAID || message.selfAID),
1728
1747
  logger,
1729
1748
  } : null;
1730
1749
  if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
@@ -2118,11 +2137,21 @@ export class ResponseEngine {
2118
2137
  },
2119
2138
  };
2120
2139
  })();
2140
+ let effectivePermissionMode = 'readonly';
2121
2141
  const recordExecutionAnomaly = triggerRunId
2122
2142
  ? (anomaly) => {
2123
- recordTriggerExecutionAnomaly(triggerRunId, anomaly);
2143
+ recordTriggerExecutionAnomaly(triggerRunId, {
2144
+ ...anomaly,
2145
+ agentAid: anomaly.agentAid ?? session.selfAID ?? message.selfAID,
2146
+ sessionId: anomaly.sessionId ?? session.id,
2147
+ permissionMode: anomaly.permissionMode ?? effectivePermissionMode,
2148
+ });
2124
2149
  }
2125
2150
  : undefined;
2151
+ const runModeConfig = resolvedMode?.mode.configureRun && modeProcessCtx
2152
+ ? resolvedMode.mode.configureRun(modeProcessCtx)
2153
+ : undefined;
2154
+ const pureSessionPolicyHook = runModeConfig?.policyHook;
2126
2155
  // 设置权限审批的交互上下文(支持交互卡片)
2127
2156
  agent.setPermissionContext?.(session.id, {
2128
2157
  sendPrompt: permissionPromptForOrigin,
@@ -2144,6 +2173,7 @@ export class ResponseEngine {
2144
2173
  approvalRouting,
2145
2174
  approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
2146
2175
  recordExecutionAnomaly,
2176
+ preToolUsePolicyHook: pureSessionPolicyHook,
2147
2177
  armApprovedDelegationCommand: this.agentDelegationRegistry
2148
2178
  ? (carrierToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
2149
2179
  carrierToken,
@@ -2233,10 +2263,7 @@ export class ResponseEngine {
2233
2263
  // [迁移点2] policyHook 由 ProactiveMode.configureRun 提供(插件决策);
2234
2264
  // 引擎补充副作用:违规时注入提醒到模型上下文 + 行为探针。
2235
2265
  policyHook: (() => {
2236
- const runConfig = resolvedMode?.mode.configureRun && modeProcessCtx
2237
- ? resolvedMode.mode.configureRun(modeProcessCtx)
2238
- : undefined;
2239
- const pluginHook = runConfig?.policyHook;
2266
+ const pluginHook = pureSessionPolicyHook;
2240
2267
  if (!pluginHook)
2241
2268
  return undefined;
2242
2269
  return (toolName, toolInput) => {
@@ -2250,9 +2277,9 @@ export class ResponseEngine {
2250
2277
  agent.injectUserMessage?.(session.id, errorMsg);
2251
2278
  return result;
2252
2279
  }
2253
- // 首工具检查通过(firstToolDone false 翻成 true 且未拦截)
2254
- const stAfter = modeState.get('proactive');
2255
- if (wasFirstPending && stAfter?.firstToolDone) {
2280
+ // 这里只记录审批结果。firstToolDone 必须等真实 tool_use 事件再推进,
2281
+ // 避免伪造 managed hook IPC 提前解除门禁。
2282
+ if (wasFirstPending) {
2256
2283
  snapshot.set(session.id, taskId, { policyHook: { triggered: true, blocked: false, toolName } });
2257
2284
  }
2258
2285
  return undefined;
@@ -2290,8 +2317,6 @@ export class ResponseEngine {
2290
2317
  let handoffPromptRendered = false;
2291
2318
  let runtimeEnv;
2292
2319
  try {
2293
- // 动态构建运行时上下文提示
2294
- const contextParts = [];
2295
2320
  // 通道能力
2296
2321
  const supportsFileMarker = currentChannelType !== 'aun' && !isProactive && !!channelInfo.adapter.capabilities?.file;
2297
2322
  const capParts = [];
@@ -2301,14 +2326,11 @@ export class ResponseEngine {
2301
2326
  capParts.push('图片输出');
2302
2327
  if (!isProactive && channelInfo.adapter.capabilities?.file)
2303
2328
  capParts.push('文件发送');
2304
- // Personal layer
2329
+ // Personal state is exposed to the manifest as vars. The manifest owns
2330
+ // loading persona.md and working.md into the system prompt.
2305
2331
  const owningAgent = owningAgentForTask;
2306
2332
  const persona = owningAgent?.getPersona?.() || undefined;
2307
2333
  const working = owningAgent?.getWorkingMemory?.() || undefined;
2308
- if (persona)
2309
- contextParts.push(persona);
2310
- if (working)
2311
- contextParts.push(`[当前关注]\n${working}`);
2312
2334
  // 计算 peerKey:群聊固定按 groupId/channelId,私聊按发送者 peerId。
2313
2335
  // 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
2314
2336
  const normalizedBaseagent = normalizeBaseagent(agent.name);
@@ -2316,7 +2338,6 @@ export class ResponseEngine {
2316
2338
  // Trigger 只能降低本次调用权限,不能突破当前角色的权限上限。
2317
2339
  // 作为 per-call 入参随 modelOverride 传入 runQuery —— 与 model/effort 同构,
2318
2340
  // 不写 AgentRunner 实例字段,多对端/多会话并发共享同一 runner 实例时互不污染。
2319
- let effectivePermissionMode;
2320
2341
  const triggerPermissionModeOverride = message.triggerMeta?.permissionModeOverride;
2321
2342
  try {
2322
2343
  effectivePermissionMode = constrainRuntimePermissionMode({
@@ -2532,9 +2553,10 @@ export class ResponseEngine {
2532
2553
  channel: currentChannelType || null,
2533
2554
  venueUid: undefined,
2534
2555
  // 群 @ 处理模式 / 客户端类型 / 权限模式
2535
- // 优先 agent/relation mentionMode 配置,fallback 到服务器 dispatch_mode 缓存(协议词汇需翻译)。
2556
+ // 优先 agent/relation mentionMode 配置,fallback 到服务器 mention_mode 缓存。
2536
2557
  mentionMode: effectiveAgentConfig?.mentionMode
2537
- ?? dispatchToMentionMode(session.metadata?.dispatchMode ?? message.dispatchMode)
2558
+ ?? session.metadata?.mentionMode
2559
+ ?? message.mentionMode
2538
2560
  ?? undefined,
2539
2561
  clientType: message.clientType || undefined,
2540
2562
  permissionMode: effectivePermissionMode,
@@ -2583,17 +2605,13 @@ export class ResponseEngine {
2583
2605
  const sessionManifests = this.agentRegistry?.resolveByChannel(channelKey)?.config?.sessionManifests;
2584
2606
  const manifestFile = sessionManifests?.[sessionType] ?? 'eck_manifest.json';
2585
2607
  const kitContext = renderKitSections(kitCtx, manifestFile);
2586
- if (kitContext)
2587
- contextParts.push(kitContext);
2588
- effectiveSystemPrompt = [options?.systemPromptAppend, ...contextParts].filter(Boolean).join('\n') || undefined;
2608
+ effectiveSystemPrompt = [options?.systemPromptAppend, kitContext].filter(Boolean).join('\n') || undefined;
2589
2609
  // ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
2590
2610
  try {
2591
2611
  const estTokens = (s) => s ? Math.ceil(s.length / 4) : 0;
2592
2612
  const cbModel = effectiveModel || agentModel || 'unknown';
2593
2613
  const cbMaxTokens = 200000; // 保守默认,后续可从 model-catalog 取
2594
2614
  const systemPromptTokens = estTokens(options?.systemPromptAppend);
2595
- const personaTokens = estTokens(persona);
2596
- const workingTokens = estTokens(working);
2597
2615
  const kitTokens = estTokens(kitContext);
2598
2616
  const totalEst = estTokens(effectiveSystemPrompt);
2599
2617
  insertContextBreakdown(resolveRoot(), {
@@ -2603,7 +2621,7 @@ export class ResponseEngine {
2603
2621
  turn_count: 0, // 按 ts 排序得轮次
2604
2622
  model: cbModel,
2605
2623
  max_tokens: cbMaxTokens,
2606
- system_prompt: systemPromptTokens + personaTokens + workingTokens,
2624
+ system_prompt: systemPromptTokens,
2607
2625
  system_tools: 0, // 工具 schema 不在此层,留 0(后续 runner 层补)
2608
2626
  mcp_tools: 0,
2609
2627
  custom_agents: 0,
@@ -2629,6 +2647,7 @@ export class ResponseEngine {
2629
2647
  encrypted: message.encrypted,
2630
2648
  content: message.content, timestamp: message.timestamp,
2631
2649
  images: message.images,
2650
+ mentions: message.mentions,
2632
2651
  mentionAids: message.mentionAids,
2633
2652
  }];
2634
2653
  const peerItems = (() => {
@@ -3916,12 +3935,10 @@ export class ResponseEngine {
3916
3935
  await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
3917
3936
  }
3918
3937
  }
3919
- // 群聊分发模式同步:aun.ts 从服务器信封解析的 dispatchMode 注入到 message,
3920
- // 此处写入 session.metadata,确保 ECK 上下文的 venue fragment 正确渲染 dispatch 变量。
3921
- // 仅当 message.dispatchMode 有值且与 session 记录不一致时更新。
3922
- if (message.chatType === 'group' && message.dispatchMode && session.metadata?.dispatchMode !== message.dispatchMode) {
3923
- logger.info(`[ResponseEngine] dispatchMode sync: sessionId=${session.id} ${session.metadata?.dispatchMode ?? 'none'} -> ${message.dispatchMode}`);
3924
- session.metadata = { ...(session.metadata || {}), dispatchMode: message.dispatchMode };
3938
+ // 同步服务端 mention_mode,供同一群会话的上下文和菜单展示使用。
3939
+ if (message.chatType === 'group' && message.mentionMode && session.metadata?.mentionMode !== message.mentionMode) {
3940
+ logger.info(`[ResponseEngine] mentionMode sync: sessionId=${session.id} ${session.metadata?.mentionMode ?? 'none'} -> ${message.mentionMode}`);
3941
+ session.metadata = { ...(session.metadata || {}), mentionMode: message.mentionMode };
3925
3942
  await this.sessionManager.updateSession(session.id, { metadata: session.metadata });
3926
3943
  }
3927
3944
  // chatMode 策略由 agent/relation behavior 配置在处理阶段解析;此处不再写 session 级参数。
@@ -4293,6 +4310,7 @@ export class ResponseEngine {
4293
4310
  }
4294
4311
  // [迁移点4] 工具汇报提醒:由 ProactiveMode.onToolUse 实现
4295
4312
  if (modeHooks?.mode?.onToolUse) {
4313
+ const proactiveState = modeHooks.state.get('proactive');
4296
4314
  modeHooks.mode.onToolUse({
4297
4315
  session,
4298
4316
  state: modeHooks.state,
@@ -4300,7 +4318,7 @@ export class ResponseEngine {
4300
4318
  toolInput: event.input || {},
4301
4319
  injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
4302
4320
  getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
4303
- isSendCommand: (toolName, toolInput) => isEvolcoreSendCommandForSession(toolName, toolInput, session.channelId),
4321
+ isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', session.selfAID),
4304
4322
  logger,
4305
4323
  });
4306
4324
  }
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * model-catalog: 模型目录与单模型元数据。
3
3
  *
4
- * 目录来源(按序回退):
5
- * {baseUrl}/v1/models{baseUrl}/models → 内置 mock catalog
4
+ * 目录来源按 baseagent 隔离:Codex 预创建优先使用 CLI 动态目录;
5
+ * 其他场景探测本 baseagent 的 {baseUrl}/v1/models{baseUrl}/models
6
+ * 最后只回退到同 baseagent 的内置目录。
6
7
  *
7
8
  * baseUrl/apiKey 复用 baseagent 凭据解析链(与发消息同源),命令不接收 url/key 参数。
8
9
  * 单模型详情(info)现阶段全部为 mock,{baseUrl}/models 接口补全字段后自动接真实数据。
@@ -10,28 +11,52 @@
10
11
  * 详见 docs/model-command-design.md。
11
12
  */
12
13
  import { resolveAnthropicConfig, resolveEcagentConfig, resolveOpenaiConfig } from '../../agents/baseagent.js';
14
+ import { buildModelRequestHeaders } from '../../agents/request-identity.js';
13
15
  import { activeBaseagent } from './config-scope.js';
14
16
  import { resolveEffective } from '../../config/config-manager.js';
17
+ import { loadDefaults } from '../../config-store.js';
18
+ import { execCodexCliSync } from '../../utils/codex-cli.js';
15
19
  /** 取指定 baseagent 的 baseUrl + apiKey(复用现有解析链)。 */
16
20
  function resolveCreds(self, ba) {
17
21
  const baseagent = ba || activeBaseagent(self);
18
- // baseagents 现在在 config.json 中
19
- const config = self ? resolveEffective({ self }, { cache: true }) : { baseagents: {} };
22
+ // A new Agent has no self config yet, so resolve its gateway from defaults.
23
+ const config = self
24
+ ? resolveEffective({ self }, { cache: true, expand: true })
25
+ : { baseagents: loadDefaults()?.baseagents ?? {} };
20
26
  const block = (config.baseagents || {});
21
27
  try {
22
28
  if (baseagent === 'codex') {
23
29
  const c = block.codex;
24
30
  const r = resolveOpenaiConfig({ agents: { codex: c } }, c);
25
- return { baseUrl: r.baseUrl, apiKey: r.apiKey };
31
+ return {
32
+ baseUrl: r.baseUrl,
33
+ apiKey: r.apiKey,
34
+ agentAid: self,
35
+ headers: buildModelRequestHeaders({ baseagent: 'codex', baseUrl: r.baseUrl, agentAid: self, configuredHeaders: r.headers }),
36
+ };
26
37
  }
27
38
  if (baseagent === 'ecagent') {
28
39
  const c = block.ecagent;
29
40
  const r = resolveEcagentConfig({ agents: { ecagent: c } }, c);
30
- return { baseUrl: r.baseUrl, apiKey: r.apiKey };
41
+ return {
42
+ baseUrl: r.baseUrl,
43
+ apiKey: r.apiKey,
44
+ agentAid: self,
45
+ headers: buildModelRequestHeaders({ baseagent: 'ecagent', baseUrl: r.baseUrl, agentAid: self, configuredHeaders: r.headers }),
46
+ };
31
47
  }
48
+ if (baseagent === 'gemini')
49
+ return {};
50
+ if (baseagent !== 'claude')
51
+ return {};
32
52
  const c = block.claude;
33
53
  const r = resolveAnthropicConfig({ agents: { claude: c } }, c);
34
- return { baseUrl: r.baseUrl, apiKey: r.apiKey };
54
+ return {
55
+ baseUrl: r.baseUrl,
56
+ apiKey: r.apiKey,
57
+ agentAid: self,
58
+ headers: buildModelRequestHeaders({ baseagent: 'claude', baseUrl: r.baseUrl, agentAid: self, configuredHeaders: r.headers }),
59
+ };
35
60
  }
36
61
  catch {
37
62
  return {};
@@ -97,15 +122,21 @@ export function parseModelList(json, url) {
97
122
  }
98
123
  return genericParse(json);
99
124
  }
100
- async function fetchModelList(url, apiKey) {
125
+ const MODEL_LIST_TIMEOUT_MS = 2000;
126
+ const MODEL_CATALOG_TTL_MS = 30_000;
127
+ const CODEX_CLI_CATALOG_TIMEOUT_MS = 2000;
128
+ async function fetchModelList(url, apiKey, requestHeaders) {
129
+ let timer;
101
130
  try {
102
131
  const controller = new AbortController();
103
- const timer = setTimeout(() => controller.abort(), 5000);
132
+ timer = setTimeout(() => controller.abort(), MODEL_LIST_TIMEOUT_MS);
104
133
  const resp = await fetch(url, {
105
134
  signal: controller.signal,
106
- headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
135
+ headers: {
136
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
137
+ ...requestHeaders,
138
+ },
107
139
  });
108
- clearTimeout(timer);
109
140
  if (!resp.ok)
110
141
  return null;
111
142
  const json = await resp.json();
@@ -115,6 +146,10 @@ async function fetchModelList(url, apiKey) {
115
146
  catch {
116
147
  return null;
117
148
  }
149
+ finally {
150
+ if (timer)
151
+ clearTimeout(timer);
152
+ }
118
153
  }
119
154
  /** 内置 mock catalog(接口未就绪时的兜底;实测可用模型,见 docs 附录 A)。 */
120
155
  const MOCK_CATALOG = [
@@ -131,6 +166,53 @@ const MOCK_CATALOG = [
131
166
  { id: 'glm-4.7', owned_by: 'zhipu' },
132
167
  { id: 'MiniMax-M2.7', owned_by: 'minimax' },
133
168
  ];
169
+ const CODEX_BUILTIN_CATALOG = [
170
+ { id: 'gpt-5.5', owned_by: 'openai' },
171
+ { id: 'gpt-5.4', owned_by: 'openai' },
172
+ { id: 'gpt-5.4-mini', owned_by: 'openai' },
173
+ { id: 'gpt-5.3-codex', owned_by: 'openai' },
174
+ { id: 'gpt-5.2', owned_by: 'openai' },
175
+ ];
176
+ const GEMINI_BUILTIN_CATALOG = [
177
+ { id: 'gemini-2.5-pro', owned_by: 'google' },
178
+ { id: 'gemini-2.5-flash', owned_by: 'google' },
179
+ { id: 'gemini-2.5-flash-lite', owned_by: 'google' },
180
+ ];
181
+ const catalogCache = new Map();
182
+ const catalogInFlight = new Map();
183
+ export function clearModelCatalogCache() {
184
+ catalogCache.clear();
185
+ catalogInFlight.clear();
186
+ }
187
+ /** A failed gateway may only fall back within the selected baseagent. */
188
+ export function builtinCatalog(baseagent) {
189
+ if (baseagent === 'codex')
190
+ return CODEX_BUILTIN_CATALOG.map(model => ({ ...model }));
191
+ if (baseagent === 'gemini')
192
+ return GEMINI_BUILTIN_CATALOG.map(model => ({ ...model }));
193
+ if (baseagent === 'ecagent')
194
+ return [{ id: 'claude-sonnet-4-6', owned_by: 'anthropic' }];
195
+ return MOCK_CATALOG.map(model => ({ ...model }));
196
+ }
197
+ function codexCliCatalog() {
198
+ try {
199
+ const output = execCodexCliSync(['debug', 'models'], {
200
+ encoding: 'utf-8',
201
+ timeout: CODEX_CLI_CATALOG_TIMEOUT_MS,
202
+ stdio: ['pipe', 'pipe', 'pipe'],
203
+ });
204
+ const payload = JSON.parse(String(output));
205
+ const models = Array.isArray(payload?.models)
206
+ ? payload.models
207
+ .filter((model) => model?.visibility === 'list' && typeof model?.slug === 'string')
208
+ .map((model) => ({ id: model.slug, owned_by: 'openai' }))
209
+ : [];
210
+ return models.length > 0 ? models : null;
211
+ }
212
+ catch {
213
+ return null;
214
+ }
215
+ }
134
216
  export function modelMatches(agent, candidate, current) {
135
217
  if (!current)
136
218
  return false;
@@ -166,24 +248,61 @@ function withAliases(models, ba) {
166
248
  .map(a => ({ id: a, owned_by: 'alias' }));
167
249
  return [...aliasEntries, ...models];
168
250
  }
169
- /** 拉取模型目录:v1/models models 远端接口 → mock,并并入别名。 */
251
+ /** 拉取模型目录,并按 self + baseagent 缓存短时间结果。 */
170
252
  export async function getCatalog(self, ba) {
171
253
  const baseagent = ba || activeBaseagent(self);
172
- const { baseUrl, apiKey } = resolveCreds(self, baseagent);
254
+ const cacheKey = `${self ?? '<defaults>'}:${baseagent}`;
255
+ const cached = catalogCache.get(cacheKey);
256
+ if (cached && cached.expiresAt > Date.now())
257
+ return cached.value;
258
+ const pending = catalogInFlight.get(cacheKey);
259
+ if (pending)
260
+ return pending;
261
+ const promise = loadCatalog(self, baseagent)
262
+ .then(value => {
263
+ catalogCache.set(cacheKey, { value, expiresAt: Date.now() + MODEL_CATALOG_TTL_MS });
264
+ return value;
265
+ })
266
+ .finally(() => catalogInFlight.delete(cacheKey));
267
+ catalogInFlight.set(cacheKey, promise);
268
+ return promise;
269
+ }
270
+ async function loadCatalog(self, baseagent) {
271
+ // Pre-create has no runner yet. Mirror CodexRunner's dynamic CLI fallback
272
+ // without changing existing self-scoped catalog resolution.
273
+ if (!self && baseagent === 'codex') {
274
+ const cliModels = codexCliCatalog();
275
+ if (cliModels)
276
+ return { models: cliModels, source: 'codex-cli' };
277
+ }
278
+ const { baseUrl, apiKey, headers } = resolveCreds(self, baseagent);
173
279
  if (baseUrl) {
174
280
  const base = baseUrl.replace(/\/+$/, '');
175
- const v1 = await fetchModelList(base.endsWith('/v1') ? `${base}/models` : `${base}/v1/models`, apiKey);
176
- if (v1)
177
- return { models: withAliases(v1, baseagent), source: 'v1/models' };
178
- const plain = await fetchModelList(`${base}/models`, apiKey);
179
- if (plain)
180
- return { models: withAliases(plain, baseagent), source: 'models' };
281
+ const urls = [...new Set([
282
+ base.endsWith('/v1') ? `${base}/models` : `${base}/v1/models`,
283
+ `${base}/models`,
284
+ ])];
285
+ // Probe compatible routes concurrently so a dead route does not add
286
+ // another full timeout before the other route is attempted.
287
+ const results = await Promise.all(urls.map(url => fetchModelList(url, apiKey, headers)));
288
+ const hitIndex = results.findIndex((models) => !!models);
289
+ if (hitIndex >= 0) {
290
+ return {
291
+ models: withAliases(results[hitIndex], baseagent),
292
+ source: hitIndex === 0 ? 'v1/models' : 'models',
293
+ };
294
+ }
295
+ }
296
+ // This remote directory is Claude-only. Never use it for Codex or Gemini.
297
+ if (baseagent === 'claude') {
298
+ const remote = await fetchModelList(REMOTE_CATALOG_URL, undefined, buildModelRequestHeaders({
299
+ baseagent: 'claude',
300
+ baseUrl: REMOTE_CATALOG_URL,
301
+ }));
302
+ if (remote)
303
+ return { models: withAliases(remote, baseagent), source: 'remote' };
181
304
  }
182
- // 本地接口未实现 降级到远端目录接口(无需鉴权)
183
- const remote = await fetchModelList(REMOTE_CATALOG_URL, apiKey);
184
- if (remote)
185
- return { models: withAliases(remote, baseagent), source: 'remote' };
186
- return { models: withAliases(MOCK_CATALOG, baseagent), source: 'mock' };
305
+ return { models: withAliases(builtinCatalog(baseagent), baseagent), source: 'builtin' };
187
306
  }
188
307
  /** claude 系判定:完整 ID 或别名(opus/sonnet/haiku)。 */
189
308
  function isClaudeFamily(id) {
@@ -1,4 +1,5 @@
1
- import { resolveAnthropicConfig, resolveOpenaiConfig } from '../../agents/baseagent.js';
1
+ import { resolveAnthropicConfig, resolveEcagentConfig, resolveOpenaiConfig } from '../../agents/baseagent.js';
2
+ import { buildModelRequestHeaders } from '../../agents/request-identity.js';
2
3
  import { resolveEffective, resolveEffectiveFieldWithSource } from '../../config/config-manager.js';
3
4
  import { constrainResolvedModelForRole } from './model-permission.js';
4
5
  import { getCatalog } from './model-catalog.js';
@@ -17,17 +18,34 @@ async function fetchWithTimeout(url, opts, timeoutMs) {
17
18
  }
18
19
  function resolveGatewayCreds(self, baseagent) {
19
20
  try {
20
- const config = self ? resolveEffective({ self }, { cache: true }) : { baseagents: {} };
21
+ const config = self ? resolveEffective({ self }, { cache: true, expand: true }) : { baseagents: {} };
21
22
  const block = config.baseagents || {};
22
23
  if (baseagent === 'codex') {
23
24
  const codexCfg = block.codex || {};
24
25
  const resolved = resolveOpenaiConfig({ agents: { codex: codexCfg } }, codexCfg);
25
- return { baseUrl: resolved.baseUrl, apiKey: resolved.apiKey };
26
+ return {
27
+ baseUrl: resolved.baseUrl,
28
+ apiKey: resolved.apiKey,
29
+ headers: buildModelRequestHeaders({ baseagent: 'codex', baseUrl: resolved.baseUrl, agentAid: self, configuredHeaders: resolved.headers }),
30
+ };
26
31
  }
27
32
  if (baseagent === 'claude') {
28
33
  const claudeCfg = block.claude || {};
29
34
  const resolved = resolveAnthropicConfig({ agents: { claude: claudeCfg } }, claudeCfg);
30
- return { baseUrl: resolved.baseUrl, apiKey: resolved.apiKey };
35
+ return {
36
+ baseUrl: resolved.baseUrl,
37
+ apiKey: resolved.apiKey,
38
+ headers: buildModelRequestHeaders({ baseagent: 'claude', baseUrl: resolved.baseUrl, agentAid: self, configuredHeaders: resolved.headers }),
39
+ };
40
+ }
41
+ if (baseagent === 'ecagent') {
42
+ const ecagentCfg = block.ecagent || {};
43
+ const resolved = resolveEcagentConfig({ agents: { ecagent: ecagentCfg } }, ecagentCfg);
44
+ return {
45
+ baseUrl: resolved.baseUrl,
46
+ apiKey: resolved.apiKey,
47
+ headers: buildModelRequestHeaders({ baseagent: 'ecagent', baseUrl: resolved.baseUrl, agentAid: self, configuredHeaders: resolved.headers }),
48
+ };
31
49
  }
32
50
  }
33
51
  catch {
@@ -46,7 +64,7 @@ async function responseError(resp) {
46
64
  }
47
65
  return String(message).slice(0, 80);
48
66
  }
49
- export async function probeModel(baseUrl, apiKey, modelId, timeoutMs) {
67
+ export async function probeModel(baseUrl, apiKey, modelId, timeoutMs, requestHeaders) {
50
68
  const base = baseUrl.replace(/\/+$/, '');
51
69
  const authHeaders = apiKey
52
70
  ? { 'x-api-key': apiKey, Authorization: `Bearer ${apiKey}` }
@@ -56,7 +74,7 @@ export async function probeModel(baseUrl, apiKey, modelId, timeoutMs) {
56
74
  const anthropicStartedAt = Date.now();
57
75
  const anthropic = await fetchWithTimeout(`${base}/v1/messages`, {
58
76
  method: 'POST',
59
- headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01', ...authHeaders },
77
+ headers: { 'Content-Type': 'application/json', 'anthropic-version': '2023-06-01', ...authHeaders, ...requestHeaders },
60
78
  body: JSON.stringify({ model: modelId, max_tokens: 5, messages: [{ role: 'user', content: 'ok' }] }),
61
79
  }, Math.min(5000, remainingMs()));
62
80
  const anthropicMs = Date.now() - anthropicStartedAt;
@@ -74,7 +92,7 @@ export async function probeModel(baseUrl, apiKey, modelId, timeoutMs) {
74
92
  const openaiStartedAt = Date.now();
75
93
  const openai = await fetchWithTimeout(`${base}/v1/chat/completions`, {
76
94
  method: 'POST',
77
- headers: { 'Content-Type': 'application/json', ...authHeaders },
95
+ headers: { 'Content-Type': 'application/json', ...authHeaders, ...requestHeaders },
78
96
  body: JSON.stringify({ model: modelId, max_tokens: 5, messages: [{ role: 'user', content: 'ok' }] }),
79
97
  }, Math.min(5000, remainingMs()));
80
98
  const openaiMs = Date.now() - openaiStartedAt;
@@ -99,7 +117,7 @@ export async function runModelCheck(input) {
99
117
  steps.push(step);
100
118
  input.onStep?.(step);
101
119
  };
102
- const { baseUrl, apiKey } = resolveGatewayCreds(input.self, input.baseagent);
120
+ const { baseUrl, apiKey, headers } = resolveGatewayCreds(input.self, input.baseagent);
103
121
  if (!baseUrl) {
104
122
  log({ label: '网关地址', ok: false, detail: '未配置受支持的自定义网关,跳过 API 探测' });
105
123
  }
@@ -107,7 +125,7 @@ export async function runModelCheck(input) {
107
125
  const startedAt = Date.now();
108
126
  const ping = await fetchWithTimeout(`${baseUrl.replace(/\/+$/, '')}/v1/models`, {
109
127
  method: 'GET',
110
- headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
128
+ headers: { ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}), ...headers },
111
129
  }, 4000);
112
130
  const elapsed = Date.now() - startedAt;
113
131
  log(ping
@@ -151,7 +169,7 @@ export async function runModelCheck(input) {
151
169
  ];
152
170
  for (let index = 0; index < targets.length; index += 2) {
153
171
  const batch = await Promise.all(targets.slice(index, index + 2).map(async (target) => {
154
- const result = await probeModel(baseUrl, apiKey, target.probe, 10_000);
172
+ const result = await probeModel(baseUrl, apiKey, target.probe, 10_000, headers);
155
173
  return { id: target.display, ...result };
156
174
  }));
157
175
  for (const result of batch) {