evolcore 0.0.21 → 0.0.23

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 (112) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +53 -0
  3. package/README.md +2 -0
  4. package/bin/codex-managed-acl.ps1 +155 -0
  5. package/bin/codex-managed-hook.mjs +3 -0
  6. package/bin/install-codex-managed-hooks.mjs +376 -96
  7. package/dist/agents/baseagent-availability.js +48 -0
  8. package/dist/agents/baseagent.js +1 -1
  9. package/dist/agents/claude-runner.js +44 -226
  10. package/dist/agents/{codex-app-server-client.js → codex/app-server-client.js} +103 -17
  11. package/dist/{utils/codex-app-server-registry.js → agents/codex/app-server-registry.js} +2 -2
  12. package/dist/agents/codex/catalog.js +70 -0
  13. package/dist/agents/codex/diagnostics.js +154 -0
  14. package/dist/agents/codex-runner.js +1292 -219
  15. package/dist/agents/ecagent-runner.js +10 -31
  16. package/dist/agents/gemini-runner.js +2 -6
  17. package/dist/aun/aid/store.js +2 -0
  18. package/dist/aun/menu-client.js +272 -0
  19. package/dist/aun/msg/group.js +65 -4
  20. package/dist/aun/msg/index.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +51 -4
  22. package/dist/aun/outbox.js +7 -0
  23. package/dist/aun/rpc/client.js +8 -1
  24. package/dist/channels/aun.js +240 -36
  25. package/dist/channels/wechat.js +81 -10
  26. package/dist/cli/aun-commands.js +48 -4
  27. package/dist/cli/command-log.js +21 -2
  28. package/dist/cli/daemon-commands.js +169 -51
  29. package/dist/cli/index.js +20 -0
  30. package/dist/cli/init.js +2 -10
  31. package/dist/cli/menu-client.js +95 -0
  32. package/dist/cli/model.js +23 -8
  33. package/dist/cli/task-context.js +16 -2
  34. package/dist/cli/trigger-command.js +22 -5
  35. package/dist/config/config-field-policy.js +3 -0
  36. package/dist/config/config-manager.js +32 -5
  37. package/dist/config/contact-book-store.js +25 -3
  38. package/dist/config/schema-migration.js +14 -2
  39. package/dist/core/auth/agent-delegation.js +15 -0
  40. package/dist/core/auth/auth-gateway.js +8 -0
  41. package/dist/core/auth/authorization-audit.js +70 -6
  42. package/dist/core/auth/operation-catalog.js +4 -0
  43. package/dist/core/auth/remote-access-store.js +135 -0
  44. package/dist/core/bootstrap-messages.js +8 -0
  45. package/dist/core/bootstrap-service.js +93 -25
  46. package/dist/core/command/command-handler.js +62 -5
  47. package/dist/core/command/daemon-diagnostics.js +448 -0
  48. package/dist/core/command/menu-catalog.js +3 -0
  49. package/dist/core/command/menu-handler.js +150 -33
  50. package/dist/core/command/menu-protocol.js +120 -1
  51. package/dist/core/command/slash-gate.js +3 -3
  52. package/dist/core/command/slash-handler.js +83 -40
  53. package/dist/core/data-migration.js +11 -1
  54. package/dist/core/event-catalog.js +32 -0
  55. package/dist/core/handoff/runtime.js +23 -3
  56. package/dist/core/message/im-renderer.js +7 -3
  57. package/dist/core/message/message-bridge.js +60 -2
  58. package/dist/core/message/message-log.js +33 -0
  59. package/dist/core/message/message-queue.js +41 -15
  60. package/dist/core/message/response-engine.js +246 -114
  61. package/dist/core/model/catalog-service.js +308 -0
  62. package/dist/core/model/model-catalog.js +124 -334
  63. package/dist/core/model/model-diagnostics.js +4 -5
  64. package/dist/core/model/model-fallback.js +9 -16
  65. package/dist/core/model/model-permission.js +1 -1
  66. package/dist/core/permission/approval-gateway.js +16 -1
  67. package/dist/core/permission/ec-command-parser.js +272 -70
  68. package/dist/core/permission/protected-paths.js +74 -14
  69. package/dist/core/permission/sandbox-runtime.js +4 -2
  70. package/dist/core/permission/tool-error-code.js +12 -0
  71. package/dist/core/permission/tool-policy.js +290 -30
  72. package/dist/core/runtime-lock.js +13 -8
  73. package/dist/core/session/session-manager.js +30 -0
  74. package/dist/core/session/session-renew.js +18 -1
  75. package/dist/core/session/session-turn-coordinator.js +5 -1
  76. package/dist/index.js +104 -34
  77. package/dist/ipc.js +218 -96
  78. package/dist/paths.js +18 -0
  79. package/dist/response-system/engines/v1/proactive-flow.js +7 -2
  80. package/dist/stats/price-resolver.js +4 -0
  81. package/dist/trigger/feedback.js +14 -2
  82. package/dist/trigger/manager.js +3 -1
  83. package/dist/trigger/parser.js +86 -13
  84. package/dist/trigger/patch.js +104 -1
  85. package/dist/trigger/scheduler.js +20 -3
  86. package/dist/utils/ecweb-supervisor.js +11 -2
  87. package/dist/utils/ecweb-utils.js +8 -26
  88. package/dist/utils/error-utils.js +7 -0
  89. package/dist/utils/logger.js +9 -4
  90. package/dist/utils/npm-ops.js +47 -15
  91. package/dist/utils/tool-summary.js +59 -0
  92. package/dist/utils/windows-shell-trust.js +201 -0
  93. package/kits/docs/evolcore/INDEX.md +3 -2
  94. package/kits/docs/evolcore/agent-create.md +159 -0
  95. package/kits/docs/evolcore/agent.md +6 -0
  96. package/kits/docs/evolcore/group-collaboration.md +256 -0
  97. package/kits/docs/evolcore/group-rules.md +1 -19
  98. package/kits/docs/evolcore/group.md +3 -1
  99. package/kits/docs/evolcore/menu-client.md +34 -0
  100. package/kits/docs/evolcore/trigger.md +6 -3
  101. package/kits/docs/prompt-loading-architecture.md +6 -0
  102. package/kits/eck_message_manifest.json +6 -6
  103. package/kits/migrations/migrate-contact-book-v2.mjs +9 -1
  104. package/kits/migrations/migrate-role-config-v4.mjs +11 -5
  105. package/kits/migrations/migrate-role-config-v5.mjs +8 -2
  106. package/kits/schemas/_meta.json +3 -2
  107. package/kits/schemas/agent-config.schema.12.json +427 -0
  108. package/kits/templates/message-fragments/item.md +1 -1
  109. package/kits/templates/roles/visitor.json +1 -0
  110. package/kits/templates/system-fragments/bootstrap.md +10 -2
  111. package/kits/templates/system-fragments/commands.md +2 -2
  112. package/package.json +11 -3
@@ -1,359 +1,149 @@
1
1
  /**
2
- * model-catalog: 模型目录与单模型元数据。
3
- *
4
- * 目录来源按 baseagent 隔离:Codex 预创建优先使用 CLI 动态目录;
5
- * 其他场景探测本 baseagent 的 {baseUrl}/v1/models 与 {baseUrl}/models,
6
- * 最后只回退到同 baseagent 的内置目录。
7
- *
8
- * baseUrl/apiKey 复用 baseagent 凭据解析链(与发消息同源),命令不接收 url/key 参数。
9
- * 单模型详情(info)现阶段全部为 mock,{baseUrl}/models 接口补全字段后自动接真实数据。
10
- *
11
- * 详见 docs/model-command-design.md。
2
+ * Public model catalog facade. Online consumers query the daemon; its service
3
+ * owns refreshes and snapshots. Runners in the daemon use that same instance.
12
4
  */
13
- import { resolveAnthropicConfig, resolveEcagentConfig, resolveOpenaiConfig } from '../../agents/baseagent.js';
5
+ import path from 'node:path';
6
+ import { resolveAnthropicConfig, resolveEcagentConfig, resolveOpenaiConfig, loadCodexSettings } from '../../agents/baseagent.js';
14
7
  import { buildModelRequestHeaders } from '../../agents/request-identity.js';
15
- import { appendRequestPath, appendV1RequestPath } from '../../agents/request-identity.js';
16
8
  import { activeBaseagent } from './config-scope.js';
17
9
  import { resolveEffective } from '../../config/config-manager.js';
18
10
  import { loadDefaults } from '../../config-store.js';
19
- import { execCodexCliSync } from '../../utils/codex-cli.js';
20
- /** 取指定 baseagent 的 baseUrl + apiKey(复用现有解析链)。 */
21
- function resolveCreds(self, ba) {
22
- const baseagent = ba || activeBaseagent(self);
23
- // A new Agent has no self config yet, so resolve its gateway from defaults.
24
- const config = self
25
- ? resolveEffective({ self }, { cache: true, expand: true })
26
- : { baseagents: loadDefaults()?.baseagents ?? {} };
27
- const block = (config.baseagents || {});
11
+ import net from 'node:net';
12
+ import fs from 'node:fs';
13
+ import { getWindowsInstanceSocketPathCandidates, resolvePaths } from '../../paths.js';
14
+ import { ModelCatalogService, catalogEfforts, resolveCatalogModel } from './catalog-service.js';
15
+ export * from './catalog-service.js';
16
+ let daemonOwner = false;
17
+ export function setModelCatalogDaemonOwner(value) { daemonOwner = value; }
18
+ /** Only a refused/missing socket means offline. A busy/rejecting daemon must
19
+ * never cause another process to create an independent online catalog. */
20
+ export async function queryDaemonCatalog(self, baseagent, refresh = false) {
21
+ const paths = resolvePaths();
22
+ let controlToken;
28
23
  try {
29
- if (baseagent === 'codex') {
30
- const c = block.codex;
31
- const r = resolveOpenaiConfig({ agents: { codex: c } }, c);
32
- return {
33
- baseUrl: r.baseUrl,
34
- apiKey: r.apiKey,
35
- agentAid: self,
36
- headers: buildModelRequestHeaders({ baseagent: 'codex', baseUrl: r.baseUrl, agentAid: self, configuredHeaders: r.headers }),
37
- queryParams: r.queryParams,
38
- };
39
- }
40
- if (baseagent === 'ecagent') {
41
- const c = block.ecagent;
42
- const r = resolveEcagentConfig({ agents: { ecagent: c } }, c);
43
- return {
44
- baseUrl: r.baseUrl,
45
- apiKey: r.apiKey,
46
- agentAid: self,
47
- headers: buildModelRequestHeaders({ baseagent: 'ecagent', baseUrl: r.baseUrl, agentAid: self, configuredHeaders: r.headers }),
48
- queryParams: r.queryParams,
24
+ controlToken = fs.readFileSync(paths.controlToken, 'utf8').trim();
25
+ }
26
+ catch { /* Server returns the authorization error. */ }
27
+ const candidates = process.platform === 'win32' ? getWindowsInstanceSocketPathCandidates(paths.root) : [paths.instanceSocket];
28
+ for (const socket of [...new Set([paths.instanceSocket, ...candidates])]) {
29
+ const response = await new Promise((resolve, reject) => {
30
+ const connection = net.connect(socket);
31
+ let buffer = '';
32
+ let done = false;
33
+ const finish = (error, value = null) => {
34
+ if (done)
35
+ return;
36
+ done = true;
37
+ connection.destroy();
38
+ if (error)
39
+ reject(error);
40
+ else
41
+ resolve(value);
49
42
  };
43
+ connection.setTimeout(25_000, () => finish(new Error('模型目录 daemon 查询超时')));
44
+ connection.on('connect', () => connection.write(JSON.stringify({ type: 'model.catalog', self, baseagent, refresh, controlToken }) + '\n'));
45
+ connection.on('error', (error) => {
46
+ if (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')
47
+ finish();
48
+ else
49
+ finish(new Error('无法连接模型目录 daemon'));
50
+ });
51
+ connection.on('data', chunk => {
52
+ buffer += chunk.toString();
53
+ if (buffer.length > 8 * 1024 * 1024)
54
+ return finish(new Error('模型目录响应过大'));
55
+ const end = buffer.indexOf('\n');
56
+ if (end < 0)
57
+ return;
58
+ try {
59
+ const data = JSON.parse(buffer.slice(0, end));
60
+ if (!data.ok || !data.snapshot)
61
+ return finish(new Error(data.error || 'daemon 不支持统一模型目录,请更新并重启'));
62
+ finish(undefined, data.snapshot);
63
+ }
64
+ catch {
65
+ finish(new Error('模型目录 daemon 返回无效响应'));
66
+ }
67
+ });
68
+ connection.on('end', () => finish(new Error('模型目录 daemon 提前关闭连接')));
69
+ });
70
+ if (response)
71
+ return response;
72
+ }
73
+ return null;
74
+ }
75
+ const service = new ModelCatalogService({
76
+ storageDir: () => path.join(path.dirname(resolvePaths().claudeModelCache), 'model-catalog'),
77
+ legacyAliasFile: () => resolvePaths().claudeModelCache,
78
+ native: async (context) => (await import('../../agents/codex/catalog.js')).codexNativeCatalog(context),
79
+ });
80
+ export function resolveCatalogContext(self, ba) {
81
+ const baseagent = ba || activeBaseagent(self);
82
+ const context = { scope: self || '<defaults>', baseagent };
83
+ try {
84
+ const config = self ? resolveEffective({ self }, { cache: true, expand: true }) : { baseagents: loadDefaults()?.baseagents ?? {} };
85
+ const block = (config.baseagents ?? {});
86
+ const cfg = { agents: block };
87
+ const resolved = baseagent === 'codex' ? resolveOpenaiConfig(cfg, block.codex)
88
+ : baseagent === 'claude' ? resolveAnthropicConfig(cfg, block.claude)
89
+ : baseagent === 'ecagent' ? resolveEcagentConfig(cfg, block.ecagent) : undefined;
90
+ if (resolved) {
91
+ Object.assign(context, { baseUrl: resolved.baseUrl, apiKey: resolved.apiKey, queryParams: resolved.queryParams,
92
+ provider: 'provider' in resolved ? resolved.provider : undefined,
93
+ headers: buildModelRequestHeaders({ baseagent: baseagent, baseUrl: resolved.baseUrl, agentAid: self, configuredHeaders: resolved.headers }) });
50
94
  }
51
- if (baseagent === 'gemini')
52
- return {};
53
- if (baseagent !== 'claude')
54
- return {};
55
- const c = block.claude;
56
- const r = resolveAnthropicConfig({ agents: { claude: c } }, c);
57
- return {
58
- baseUrl: r.baseUrl,
59
- apiKey: r.apiKey,
60
- agentAid: self,
61
- headers: buildModelRequestHeaders({ baseagent: 'claude', baseUrl: r.baseUrl, agentAid: self, configuredHeaders: r.headers }),
62
- queryParams: r.queryParams,
63
- };
64
95
  }
65
96
  catch {
66
- return {};
67
- }
68
- }
69
- /** 从对象/字符串条目里提取 id(兼容 id/name/model 字段)。 */
70
- function pickId(item) {
71
- if (typeof item === 'string')
72
- return item;
73
- if (item && typeof item === 'object') {
74
- const v = item.id ?? item.name ?? item.model;
75
- return typeof v === 'string' ? v : undefined;
76
- }
77
- return undefined;
78
- }
79
- /** 从对象条目里提取厂商(兼容 owned_by/owner/provider)。 */
80
- function pickOwner(item) {
81
- if (item && typeof item === 'object') {
82
- const v = item.owned_by ?? item.owner ?? item.provider;
83
- return typeof v === 'string' ? v : undefined;
97
+ // Codex account login may not have an API key. Native listing still works,
98
+ // but do not replace a configured Agent gateway with the host connection.
99
+ const defaults = self ? resolveEffective({ self }, { cache: true, expand: true }) : { baseagents: loadDefaults()?.baseagents ?? {} };
100
+ const block = defaults.baseagents?.[baseagent];
101
+ const settings = baseagent === 'codex' ? loadCodexSettings() : undefined;
102
+ if (baseagent === 'codex' && !block?.baseUrl && !process.env.OPENAI_BASE_URL && !settings?.baseUrl && !block?.apiKey)
103
+ Object.assign(context, { provider: settings?.provider });
104
+ else
105
+ Object.assign(context, { configUnavailable: true, baseUrl: block?.baseUrl, headers: block?.headers, queryParams: block?.queryParams, apiKey: block?.apiKey });
84
106
  }
85
- return undefined;
107
+ return context;
86
108
  }
87
- /**
88
- * 通用容错解析:
89
- * - 容器:json.data[] | json.models[] | json.data.models[] | 裸数组
90
- * - 条目:字符串 或 {id|name|model, owned_by|owner|provider, created}
91
- */
92
- function genericParse(json) {
93
- const arr = Array.isArray(json) ? json
94
- : Array.isArray(json?.data) ? json.data
95
- : Array.isArray(json?.models) ? json.models
96
- : Array.isArray(json?.data?.models) ? json.data.models
97
- : [];
98
- const out = [];
99
- for (const item of arr) {
100
- const id = pickId(item);
101
- if (!id)
102
- continue;
103
- out.push({ id, owned_by: pickOwner(item), created: typeof item?.created === 'number' ? item.created : undefined });
104
- }
105
- return out;
109
+ /** Used by the IPC handler and the owner process. Never calls IPC recursively. */
110
+ export async function getLocalCatalog(self, ba, refresh = false) {
111
+ return service.get(resolveCatalogContext(self, ba), refresh);
106
112
  }
107
- /**
108
- * 网关专用 parser 注册表(扩展点)。
109
- * 当前为空:ModelGate 是标准 OpenAI list 风格,genericParse 已覆盖。
110
- * 示例(接入异形网关时取消注释并按实际格式实现):
111
- * {
112
- * name: 'some-gateway',
113
- * match: (url) => url.includes('some-gateway.example.com'),
114
- * parse: (json) => (json?.result?.modelList ?? []).map((m: any) => ({ id: m.code, owned_by: m.vendor })),
115
- * }
116
- */
117
- const GATEWAY_PARSERS = [];
118
- /** 选择 parser 并解析(专用优先,否则通用容错)。 */
119
- export function parseModelList(json, url) {
120
- for (const rule of GATEWAY_PARSERS) {
121
- try {
122
- if (rule.match(url, json))
123
- return rule.parse(json);
124
- }
125
- catch { /* 专用 parser 失败则继续尝试通用解析 */ }
113
+ export async function getCatalog(self, ba, refresh = false) {
114
+ if (!daemonOwner) {
115
+ const snapshot = await queryDaemonCatalog(self, ba, refresh);
116
+ if (snapshot)
117
+ return snapshot;
126
118
  }
127
- return genericParse(json);
119
+ return getLocalCatalog(self, ba, refresh);
128
120
  }
129
- const MODEL_LIST_TIMEOUT_MS = 2000;
130
- const MODEL_CATALOG_TTL_MS = 30_000;
131
- const CODEX_CLI_CATALOG_TIMEOUT_MS = 2000;
132
- async function fetchModelList(url, apiKey, requestHeaders) {
133
- let timer;
134
- try {
135
- const controller = new AbortController();
136
- timer = setTimeout(() => controller.abort(), MODEL_LIST_TIMEOUT_MS);
137
- const resp = await fetch(url, {
138
- signal: controller.signal,
139
- headers: {
140
- ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
141
- ...requestHeaders,
142
- },
143
- });
144
- if (!resp.ok)
145
- return null;
146
- const json = await resp.json();
147
- const entries = parseModelList(json, url);
148
- return entries.length > 0 ? entries : null;
149
- }
150
- catch {
151
- return null;
152
- }
153
- finally {
154
- if (timer)
155
- clearTimeout(timer);
156
- }
121
+ /** Connection-bound runner wrapper; effective Agent scope wins when available. */
122
+ export function getRunnerCatalog(baseagent, self, connection) {
123
+ if (self)
124
+ return getCatalog(self, baseagent);
125
+ return service.get({ ...connection, scope: '<defaults>', baseagent });
157
126
  }
158
- /** 内置 mock catalog(接口未就绪时的兜底;实测可用模型,见 docs 附录 A)。 */
159
- const MOCK_CATALOG = [
160
- { id: 'claude-opus-4-7', owned_by: 'anthropic' },
161
- { id: 'claude-opus-4-6', owned_by: 'anthropic' },
162
- { id: 'claude-sonnet-4-6', owned_by: 'anthropic' },
163
- { id: 'claude-haiku-4-5-20251001', owned_by: 'anthropic' },
164
- { id: 'deepseek-v4-pro', owned_by: 'deepseek' },
165
- { id: 'deepseek-v4-flash', owned_by: 'deepseek' },
166
- { id: 'kimi-k2.6', owned_by: 'moonshot' },
167
- { id: 'kimi-k2.5', owned_by: 'moonshot' },
168
- { id: 'glm-5.1', owned_by: 'zhipu' },
169
- { id: 'glm-5', owned_by: 'zhipu' },
170
- { id: 'glm-4.7', owned_by: 'zhipu' },
171
- { id: 'MiniMax-M2.7', owned_by: 'minimax' },
172
- ];
173
- const CODEX_BUILTIN_CATALOG = [
174
- { id: 'gpt-5.5', owned_by: 'openai' },
175
- { id: 'gpt-5.4', owned_by: 'openai' },
176
- { id: 'gpt-5.4-mini', owned_by: 'openai' },
177
- { id: 'gpt-5.3-codex', owned_by: 'openai' },
178
- { id: 'gpt-5.2', owned_by: 'openai' },
179
- ];
180
- const GEMINI_BUILTIN_CATALOG = [
181
- { id: 'gemini-2.5-pro', owned_by: 'google' },
182
- { id: 'gemini-2.5-flash', owned_by: 'google' },
183
- { id: 'gemini-2.5-flash-lite', owned_by: 'google' },
184
- ];
185
- const catalogCache = new Map();
186
- const catalogInFlight = new Map();
187
- export function clearModelCatalogCache() {
188
- catalogCache.clear();
189
- catalogInFlight.clear();
127
+ export function peekRunnerCatalog(baseagent, self, connection) {
128
+ return service.peek(self ? resolveCatalogContext(self, baseagent) : { ...connection, scope: '<defaults>', baseagent });
190
129
  }
191
- /** A failed gateway may only fall back within the selected baseagent. */
192
- export function builtinCatalog(baseagent) {
193
- if (baseagent === 'codex')
194
- return CODEX_BUILTIN_CATALOG.map(model => ({ ...model }));
195
- if (baseagent === 'gemini')
196
- return GEMINI_BUILTIN_CATALOG.map(model => ({ ...model }));
197
- if (baseagent === 'ecagent')
198
- return [{ id: 'claude-sonnet-4-6', owned_by: 'anthropic' }];
199
- return MOCK_CATALOG.map(model => ({ ...model }));
200
- }
201
- function codexCliCatalog() {
202
- try {
203
- const output = execCodexCliSync(['debug', 'models'], {
204
- encoding: 'utf-8',
205
- timeout: CODEX_CLI_CATALOG_TIMEOUT_MS,
206
- stdio: ['pipe', 'pipe', 'pipe'],
207
- });
208
- const payload = JSON.parse(String(output));
209
- const models = Array.isArray(payload?.models)
210
- ? payload.models
211
- .filter((model) => model?.visibility === 'list' && typeof model?.slug === 'string')
212
- .map((model) => ({ id: model.slug, owned_by: 'openai' }))
213
- : [];
214
- return models.length > 0 ? models : null;
215
- }
216
- catch {
217
- return null;
218
- }
130
+ export async function getModelEfforts(model, self, ba) {
131
+ const baseagent = ba || activeBaseagent(self);
132
+ return catalogEfforts(await getCatalog(self, baseagent), model, baseagent);
219
133
  }
134
+ export function clearModelCatalogCache() { service.clear(); }
220
135
  export function modelMatches(agent, candidate, current) {
221
136
  if (!current)
222
137
  return false;
223
- if (candidate === current)
224
- return true;
225
- const resolve = typeof agent?.resolveModelId === 'function'
226
- ? (value) => agent.resolveModelId(value) ?? value
227
- : (value) => value;
228
- const resolvedCandidate = resolve(candidate);
229
- const resolvedCurrent = resolve(current);
230
- return candidate === resolvedCurrent || resolvedCandidate === current || resolvedCandidate === resolvedCurrent;
231
- }
232
- /**
233
- * 远端模型目录接口(临时)。本地 baseUrl 的 /v1/models、/models 尚未实现,
234
- * 自动降级到此接口拉取真实可用模型列表;待本地接口就绪后此级自然不再命中。
235
- */
236
- const REMOTE_CATALOG_URL = 'https://mg-new.evolai.cn/claude-proxy/models';
237
- /**
238
- * 各 baseagent 的稳定别名(cc/SDK 自动解析到最新版本)。
239
- * 这些是一等公民——defaults.json 默认就存别名(如 "opus"),必须可选可校验。
240
- */
241
- const KNOWN_ALIASES = {
242
- claude: ['opus', 'sonnet', 'haiku'],
243
- codex: [],
244
- gemini: [],
245
- };
246
- /** 把别名作为虚拟条目并入目录头部(owned_by 标 alias,便于展示与校验)。 */
247
- function withAliases(models, ba) {
248
- const aliases = KNOWN_ALIASES[ba] || [];
249
- const existing = new Set(models.map(m => m.id));
250
- const aliasEntries = aliases
251
- .filter(a => !existing.has(a))
252
- .map(a => ({ id: a, owned_by: 'alias' }));
253
- return [...aliasEntries, ...models];
138
+ const resolve = (value) => agent?.resolveModelId?.(value) ?? value;
139
+ return resolve(candidate) === resolve(current);
254
140
  }
255
- /** 拉取模型目录,并按 self + baseagent 缓存短时间结果。 */
256
- export async function getCatalog(self, ba) {
257
- const baseagent = ba || activeBaseagent(self);
258
- const cacheKey = `${self ?? '<defaults>'}:${baseagent}`;
259
- const cached = catalogCache.get(cacheKey);
260
- if (cached && cached.expiresAt > Date.now())
261
- return cached.value;
262
- const pending = catalogInFlight.get(cacheKey);
263
- if (pending)
264
- return pending;
265
- const promise = loadCatalog(self, baseagent)
266
- .then(value => {
267
- catalogCache.set(cacheKey, { value, expiresAt: Date.now() + MODEL_CATALOG_TTL_MS });
268
- return value;
269
- })
270
- .finally(() => catalogInFlight.delete(cacheKey));
271
- catalogInFlight.set(cacheKey, promise);
272
- return promise;
273
- }
274
- async function loadCatalog(self, baseagent) {
275
- // Pre-create has no runner yet. Mirror CodexRunner's dynamic CLI fallback
276
- // without changing existing self-scoped catalog resolution.
277
- if (!self && baseagent === 'codex') {
278
- const cliModels = codexCliCatalog();
279
- if (cliModels)
280
- return { models: cliModels, source: 'codex-cli' };
281
- }
282
- const { baseUrl, apiKey, headers, queryParams } = resolveCreds(self, baseagent);
283
- if (baseUrl) {
284
- try {
285
- const urls = [...new Set([
286
- appendV1RequestPath(baseUrl, 'models', queryParams),
287
- appendRequestPath(baseUrl, 'models', queryParams),
288
- ])];
289
- // Probe compatible routes concurrently so a dead route does not add
290
- // another full timeout before the other route is attempted.
291
- const results = await Promise.all(urls.map(url => fetchModelList(url, apiKey, headers)));
292
- const hitIndex = results.findIndex((models) => !!models);
293
- if (hitIndex >= 0) {
294
- return {
295
- models: withAliases(results[hitIndex], baseagent),
296
- source: hitIndex === 0 ? 'v1/models' : 'models',
297
- };
298
- }
299
- }
300
- catch {
301
- // Invalid configured URLs behave like other unavailable gateways.
302
- }
303
- }
304
- // This remote directory is Claude-only. Never use it for Codex or Gemini.
305
- if (baseagent === 'claude') {
306
- const remote = await fetchModelList(REMOTE_CATALOG_URL, undefined, buildModelRequestHeaders({
307
- baseagent: 'claude',
308
- baseUrl: REMOTE_CATALOG_URL,
309
- }));
310
- if (remote)
311
- return { models: withAliases(remote, baseagent), source: 'remote' };
312
- }
313
- return { models: withAliases(builtinCatalog(baseagent), baseagent), source: 'builtin' };
314
- }
315
- /** claude 系判定:完整 ID 或别名(opus/sonnet/haiku)。 */
316
- function isClaudeFamily(id) {
317
- return /^claude-/.test(id) || ['opus', 'sonnet', 'haiku'].includes(id);
318
- }
319
- /** 单模型详情(现阶段 mock)。 */
320
141
  export async function getModelInfo(modelId, self, ba) {
321
- const cat = await getCatalog(self, ba);
322
- const entry = cat.models.find(m => m.id === modelId);
323
- const claudeFamily = isClaudeFamily(modelId);
324
- // 远端目录把所有模型的 owned_by 标成网关名 'ModelGate',别名标成 'alias',
325
- // 两者都不是真实厂商 → 回退到按 ID 推断。
326
- const NON_VENDOR = new Set(['alias', 'ModelGate', '']);
327
- const rawOwner = entry?.owned_by;
328
- const owner = claudeFamily
329
- ? 'anthropic'
330
- : (rawOwner && !NON_VENDOR.has(rawOwner) ? rawOwner : inferOwner(modelId));
331
- return {
332
- id: modelId,
333
- owned_by: owner,
334
- context_window: claudeFamily ? 200000 : 128000,
335
- max_output_tokens: 8192,
336
- pricing: { input_per_mtok: null, output_per_mtok: null, currency: 'USD' },
337
- modalities: ['text'],
338
- supports_effort: claudeFamily,
339
- status: entry ? 'available' : 'unknown',
340
- mocked: true,
341
- };
342
- }
343
- function inferOwner(id) {
344
- if (/^claude-/.test(id))
345
- return 'anthropic';
346
- if (/^gpt-/.test(id))
347
- return 'openai';
348
- if (/^gemini-/.test(id))
349
- return 'google';
350
- if (/^deepseek-/.test(id))
351
- return 'deepseek';
352
- if (/^kimi-/.test(id))
353
- return 'moonshot';
354
- if (/^glm-/.test(id))
355
- return 'zhipu';
356
- if (/^MiniMax-/i.test(id))
357
- return 'minimax';
358
- return 'unknown';
142
+ const baseagent = ba || activeBaseagent(self);
143
+ const catalog = await getCatalog(self, baseagent);
144
+ const entry = catalog.models.find(m => m.id === resolveCatalogModel(catalog, modelId));
145
+ return { id: modelId, owned_by: entry?.owned_by || 'unknown',
146
+ context_window: entry?.contextWindow ?? null, max_output_tokens: entry?.maxOutputTokens ?? null,
147
+ pricing: null, modalities: [], supports_effort: catalogEfforts(catalog, modelId, baseagent).length > 0,
148
+ status: entry ? entry.verification : 'not-listed', mocked: false };
359
149
  }
@@ -152,8 +152,8 @@ export async function runModelCheck(input) {
152
152
  }
153
153
  const catalog = await getCatalog(input.self, input.baseagent);
154
154
  const modelIds = catalog.models.filter(model => model.owned_by !== 'alias').map(model => model.id);
155
- log(catalog.source === 'mock'
156
- ? { label: '模型列表', ok: false, detail: `无法获取远端列表,使用内置 mock(${modelIds.length} 个)` }
155
+ log(catalog.freshness !== 'fresh'
156
+ ? { label: '模型列表', ok: false, detail: `目录已降级,来源 ${catalog.source}(${modelIds.length} 个)` }
157
157
  : { label: '模型列表', ok: true, detail: `来源: ${catalog.source},共 ${modelIds.length} 个模型` });
158
158
  const selector = { self: input.self, peerKey: input.peerKey };
159
159
  const modelResolution = resolveEffectiveFieldWithSource(`baseagents.${input.baseagent}.model`, selector, { cache: true });
@@ -176,9 +176,8 @@ export async function runModelCheck(input) {
176
176
  for (const entry of catalog.models) {
177
177
  if (entry.owned_by !== 'alias')
178
178
  continue;
179
- const matched = catalog.models.find(model => model.owned_by !== 'alias' && model.id.includes(entry.id));
180
- if (matched)
181
- aliases.set(entry.id, matched.id);
179
+ if (entry.aliasFor)
180
+ aliases.set(entry.id, entry.aliasFor);
182
181
  }
183
182
  const resolvedConfiguredModel = configuredModel ? aliases.get(configuredModel) ?? configuredModel : undefined;
184
183
  const targets = [
@@ -1,4 +1,4 @@
1
- import { getCodexEfforts } from '../../agents/codex-runner.js';
1
+ import { catalogEfforts, getCatalog, resolveCatalogModel } from './model-catalog.js';
2
2
  import { ConfigTarget, read } from '../../config/config-manager.js';
3
3
  export const MAX_FALLBACK_MODELS = 3;
4
4
  function isRecord(value) {
@@ -40,27 +40,20 @@ export function configuredFallbackModels(baseagent) {
40
40
  }
41
41
  return { candidates, rejected };
42
42
  }
43
- function supportsEffort(baseagent, model, effort) {
44
- if (!effort)
45
- return true;
46
- if (baseagent === 'codex')
47
- return getCodexEfforts(model).includes(effort);
48
- if (baseagent === 'claude')
49
- return ['low', 'medium', 'high', 'xhigh', 'max'].includes(effort);
50
- if (baseagent === 'ecagent')
51
- return ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'].includes(effort);
52
- return false;
53
- }
54
- export function eligibleFallbackModels(input) {
43
+ export async function eligibleFallbackModels(input) {
55
44
  const configured = configuredFallbackModels(input.baseagent);
56
45
  const candidates = [];
57
46
  const rejected = [...configured.rejected];
58
- const key = (model) => (input.resolveModelId?.(model) ?? model).trim().toLowerCase();
47
+ if (!configured.candidates.length)
48
+ return { candidates, rejected };
49
+ const catalog = await (input.catalog?.() ?? getCatalog(input.selfAid, input.baseagent));
50
+ const resolve = (model) => resolveCatalogModel(catalog, model);
51
+ const key = (model) => resolve(model).trim();
59
52
  const seen = new Set();
60
53
  if (input.currentModel)
61
54
  seen.add(key(input.currentModel));
62
55
  for (const candidate of configured.candidates) {
63
- const resolvedModel = input.resolveModelId?.(candidate.model) ?? candidate.model;
56
+ const resolvedModel = resolve(candidate.model);
64
57
  const candidateKey = key(candidate.model);
65
58
  if (seen.has(candidateKey)) {
66
59
  rejected.push({ model: candidate.model, reason: candidateKey === key(input.currentModel ?? '') ? 'current_model' : 'duplicate' });
@@ -70,7 +63,7 @@ export function eligibleFallbackModels(input) {
70
63
  rejected.push({ model: candidate.model, reason: 'not_allowed' });
71
64
  continue;
72
65
  }
73
- if (!supportsEffort(input.baseagent, resolvedModel, candidate.effort)) {
66
+ if (candidate.effort && !catalogEfforts(catalog, resolvedModel, input.baseagent).includes(candidate.effort)) {
74
67
  rejected.push({ model: candidate.model, reason: 'unsupported_effort' });
75
68
  continue;
76
69
  }
@@ -49,7 +49,7 @@ export function validateModelSelectionForRole(opts) {
49
49
  : opts.models?.includes(opts.requestedModel)
50
50
  ? opts.requestedModel
51
51
  : undefined;
52
- if (opts.models && opts.models.length > 0 && !catalogModel) {
52
+ if (opts.models && !catalogModel) {
53
53
  return {
54
54
  ok: false,
55
55
  code: 'INVALID_VALUE',