evolcore 0.0.24 → 0.0.26
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.
- package/CHANGELOG.md +59 -0
- package/bin/ec.js +12 -1
- package/bin/install-codex-managed-hooks.mjs +28 -1
- package/dist/agents/baseagent.js +96 -46
- package/dist/agents/claude-runner.js +54 -13
- package/dist/agents/codex/app-server-client.js +13 -5
- package/dist/agents/codex/catalog.js +1 -1
- package/dist/agents/codex/diagnostics.js +34 -1
- package/dist/agents/codex-runner.js +128 -42
- package/dist/agents/ecagent-runner.js +14 -18
- package/dist/agents/gemini-runner.js +24 -9
- package/dist/agents/runner-types.js +25 -17
- package/dist/aun/aid/agentmd.js +135 -10
- package/dist/aun/aid/client.js +1 -1
- package/dist/aun/aid/identity.js +1 -1
- package/dist/aun/aid/index.js +2 -1
- package/dist/aun/aid/managed-operation.js +53 -1
- package/dist/aun/aid/store.js +17 -13
- package/dist/aun/menu-client.js +74 -0
- package/dist/aun/msg/group.js +1 -1
- package/dist/aun/msg/managed-operation.js +1 -1
- package/dist/aun/msg/p2p.js +6 -1
- package/dist/aun/msg/upload.js +30 -22
- package/dist/aun/storage/client.js +4 -1
- package/dist/aun/storage/index.js +1 -1
- package/dist/channels/aun.js +329 -94
- package/dist/cli/agent-command.js +125 -5
- package/dist/cli/agent-create.js +151 -11
- package/dist/cli/agent.js +119 -6
- package/dist/cli/aun-commands.js +74 -7
- package/dist/cli/config.js +27 -8
- package/dist/cli/contact.js +63 -6
- package/dist/cli/daemon-commands.js +90 -25
- package/dist/cli/diag.js +31 -0
- package/dist/cli/handoff-command.js +35 -3
- package/dist/cli/index.js +7 -1
- package/dist/cli/managed-command-guard.js +3 -0
- package/dist/cli/menu-client.js +38 -23
- package/dist/cli/restart-monitor.js +57 -10
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/config-manager.js +15 -0
- package/dist/config/config-operation-service.js +165 -3
- package/dist/config/contact-operation-service.js +96 -7
- package/dist/config/resolved-config-op.js +66 -8
- package/dist/core/agent-application-service.js +2 -1
- package/dist/core/auth/authorization-audit.js +20 -12
- package/dist/core/auth/operation-authorizer.js +87 -8
- package/dist/core/auth/operation-catalog.js +51 -4
- package/dist/core/auth/remote-access-store.js +40 -1
- package/dist/core/bootstrap-service.js +1 -1
- package/dist/core/command/agent-control.js +54 -25
- package/dist/core/command/cli-intent-parser.js +18 -0
- package/dist/core/command/command-handler.js +152 -20
- package/dist/core/command/diag-service.js +118 -0
- package/dist/core/command/menu-catalog.js +13 -3
- package/dist/core/command/menu-handler.js +88 -31
- package/dist/core/command/menu-protocol.js +6 -0
- package/dist/core/command/menu-token-store.js +41 -8
- package/dist/core/command/slash-gate.js +34 -4
- package/dist/core/command/slash-handler.js +77 -12
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +3 -3
- package/dist/core/handoff/runtime.js +205 -27
- package/dist/core/handoff/store.js +193 -4
- package/dist/core/message/im-renderer.js +72 -21
- package/dist/core/message/message-bridge.js +69 -9
- package/dist/core/message/message-queue.js +26 -0
- package/dist/core/message/message-utils.js +1 -0
- package/dist/core/message/response-engine.js +48 -23
- package/dist/core/model/catalog-service.js +79 -49
- package/dist/core/model/context-window-suffix.js +33 -0
- package/dist/core/model/model-catalog.js +1 -1
- package/dist/core/model/model-permission.js +12 -3
- package/dist/core/permission/ec-command-parser.js +51 -12
- package/dist/core/permission/sandbox-runtime.js +36 -14
- package/dist/core/permission/tool-policy.js +47 -7
- package/dist/index.js +195 -31
- package/dist/ipc.js +54 -2
- package/dist/response-system/engines/v1/proactive-flow.js +59 -2
- package/dist/stats/billing.js +41 -15
- package/dist/stats/price-resolver.js +28 -10
- package/dist/utils/avatar-upload.js +282 -23
- package/dist/utils/cross-platform.js +30 -1
- package/dist/utils/error-utils.js +21 -0
- package/dist/utils/evolcore-version.js +129 -0
- package/dist/utils/logger.js +3 -1
- package/dist/utils/model-prices.jsonl +3 -3
- package/dist/utils/npm-ops.js +176 -35
- package/kits/docs/context-assembly.md +1 -0
- package/kits/docs/evolcore/INDEX.md +3 -1
- package/kits/docs/evolcore/agent-create.md +30 -11
- package/kits/docs/evolcore/agent.md +18 -8
- package/kits/docs/evolcore/aid.md +6 -4
- package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
- package/kits/docs/evolcore/config.md +7 -0
- package/kits/docs/evolcore/contact.md +12 -5
- package/kits/docs/evolcore/menu-client.md +11 -3
- package/kits/docs/evolcore/msg.md +1 -0
- package/kits/docs/prompt-loading-architecture.md +1 -0
- package/kits/docs/venues/group.md +12 -0
- package/kits/eck_manifest.json +12 -0
- package/kits/rules/03-identity.md +3 -1
- package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
- package/kits/templates/roles/admin.json +14 -2
- package/kits/templates/roles/member.json +3 -2
- package/kits/templates/roles/visitor.json +4 -3
- package/kits/templates/system-fragments/bootstrap.md +16 -5
- package/kits/templates/system-fragments/channel.md +1 -1
- package/kits/templates/system-fragments/session.md +1 -1
- package/package.json +6 -4
- package/skills/eclink/SKILL.md +0 -84
- package/skills/eclink/agents/openai.yaml +0 -4
|
@@ -1275,6 +1275,8 @@ export class MessageQueue {
|
|
|
1275
1275
|
return false;
|
|
1276
1276
|
if (first.projectPath !== next.projectPath || first.agentName !== next.agentName)
|
|
1277
1277
|
return false;
|
|
1278
|
+
// Results from the same parent handoff are still mergeable. Their
|
|
1279
|
+
// resultRefs are retained and consumed individually after one turn.
|
|
1278
1280
|
const a = first.message.handoffDelivery?.originAuthorization;
|
|
1279
1281
|
const b = next.message.handoffDelivery?.originAuthorization;
|
|
1280
1282
|
if (!a || !b)
|
|
@@ -1323,6 +1325,10 @@ export class MessageQueue {
|
|
|
1323
1325
|
let hasMessagesAfterRestartResume = false;
|
|
1324
1326
|
const originHandoffIds = [];
|
|
1325
1327
|
const seenOriginHandoffIds = new Set();
|
|
1328
|
+
const originResultIds = [];
|
|
1329
|
+
const seenOriginResultIds = new Set();
|
|
1330
|
+
const originResultRefs = [];
|
|
1331
|
+
const seenOriginResultRefs = new Set();
|
|
1326
1332
|
const allOriginHandoffs = items.every(item => this.isOriginHandoff(item.message));
|
|
1327
1333
|
for (const item of items) {
|
|
1328
1334
|
const m = item.message;
|
|
@@ -1347,6 +1353,24 @@ export class MessageQueue {
|
|
|
1347
1353
|
originHandoffIds.push(id);
|
|
1348
1354
|
}
|
|
1349
1355
|
}
|
|
1356
|
+
const refs = m.handoffDelivery.resultRefs?.length
|
|
1357
|
+
? m.handoffDelivery.resultRefs
|
|
1358
|
+
: (m.handoffDelivery.resultIds?.length
|
|
1359
|
+
? m.handoffDelivery.resultIds.map(resultId => ({ handoffId: m.handoffDelivery.handoffId, resultId }))
|
|
1360
|
+
: (m.handoffDelivery.resultId
|
|
1361
|
+
? [{ handoffId: m.handoffDelivery.handoffId, resultId: m.handoffDelivery.resultId }]
|
|
1362
|
+
: []));
|
|
1363
|
+
for (const ref of refs) {
|
|
1364
|
+
const refKey = `${ref.handoffId}\u0000${ref.resultId}`;
|
|
1365
|
+
if (!seenOriginResultRefs.has(refKey)) {
|
|
1366
|
+
seenOriginResultRefs.add(refKey);
|
|
1367
|
+
originResultRefs.push(ref);
|
|
1368
|
+
}
|
|
1369
|
+
if (!seenOriginResultIds.has(ref.resultId)) {
|
|
1370
|
+
seenOriginResultIds.add(ref.resultId);
|
|
1371
|
+
originResultIds.push(ref.resultId);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1350
1374
|
}
|
|
1351
1375
|
for (const id of [...(m.sourceMessageIds ?? []), ...(m.messageId ? [m.messageId] : [])]) {
|
|
1352
1376
|
if (!seenSourceMessageIds.has(id)) {
|
|
@@ -1418,6 +1442,8 @@ export class MessageQueue {
|
|
|
1418
1442
|
...last.message.handoffDelivery,
|
|
1419
1443
|
handoffId: originHandoffIds[0],
|
|
1420
1444
|
handoffIds: originHandoffIds,
|
|
1445
|
+
...(originResultIds.length > 0 ? { resultIds: originResultIds } : {}),
|
|
1446
|
+
...(originResultRefs.length > 0 ? { resultRefs: originResultRefs } : {}),
|
|
1421
1447
|
}
|
|
1422
1448
|
: last.message.handoffDelivery,
|
|
1423
1449
|
};
|
|
@@ -101,6 +101,7 @@ export function deliveryTargetFromSession(session) {
|
|
|
101
101
|
export function replyContextFromSession(session, context) {
|
|
102
102
|
return {
|
|
103
103
|
...(session.metadata?.replyContext ?? {}),
|
|
104
|
+
...(session.threadId ? { threadId: session.threadId } : {}),
|
|
104
105
|
...(context ?? {}),
|
|
105
106
|
// Session chatType/groupId is the persisted routing authority. The nested
|
|
106
107
|
// reply context can outlive a route correction, so only an explicit
|
|
@@ -2,7 +2,7 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
|
-
import { BaseagentRunnerUnavailableError, hasCompact,
|
|
5
|
+
import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken } from '../../agents/runner-types.js';
|
|
6
6
|
import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
|
|
7
7
|
import { IMRenderer } from './im-renderer.js';
|
|
8
8
|
import { createSendFileMarkerPattern } from './file-markers.js';
|
|
@@ -11,7 +11,8 @@ 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 {
|
|
14
|
+
import { readEvolcoreRuntimeFlavor, tagErrorTextWithEvolcoreVersion } from '../../utils/evolcore-version.js';
|
|
15
|
+
import { describeProactiveSendCommandFailure, isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
|
|
15
16
|
import { summarizeToolInput } from '../../utils/tool-summary.js';
|
|
16
17
|
import { agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
17
18
|
import { renderKitSections } from '../../eck/kit-renderer.js';
|
|
@@ -485,9 +486,6 @@ export async function resolveGroupRulesForTurn(opts, sync = syncGroupRulesContex
|
|
|
485
486
|
}
|
|
486
487
|
return sync({ selfAid: opts.selfAid, groupId: opts.groupId, channel: opts.channel });
|
|
487
488
|
}
|
|
488
|
-
function autoCompactTokensFromMaxTokens(maxTokens) {
|
|
489
|
-
return autoCompactWindowForContextWindow(maxTokens);
|
|
490
|
-
}
|
|
491
489
|
/**
|
|
492
490
|
* 构造 OutboundEnvelope —— 出站三件套的信封部分。
|
|
493
491
|
*
|
|
@@ -915,6 +913,20 @@ export class ResponseEngine {
|
|
|
915
913
|
causation: runtime?.causation,
|
|
916
914
|
});
|
|
917
915
|
}
|
|
916
|
+
async recoverAuthorizedHandoffResult(params, selfAid) {
|
|
917
|
+
if (!this.handoffRuntime) {
|
|
918
|
+
return { ok: false, code: 'HANDOFF_NOT_FOUND', error: 'handoff runtime not configured' };
|
|
919
|
+
}
|
|
920
|
+
const runtime = this.activeTaskRuntimeContexts.get(params.sessionId);
|
|
921
|
+
return this.handoffRuntime.recoverReturnHandoff({
|
|
922
|
+
selfAid,
|
|
923
|
+
recoverySessionId: params.sessionId,
|
|
924
|
+
handoffId: params.handoffId,
|
|
925
|
+
content: params.content,
|
|
926
|
+
actorId: params.actorId,
|
|
927
|
+
causation: runtime?.causation,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
918
930
|
agentRegistry;
|
|
919
931
|
setAgentRegistry(registry) {
|
|
920
932
|
this.agentRegistry = registry;
|
|
@@ -2132,6 +2144,7 @@ export class ResponseEngine {
|
|
|
2132
2144
|
modeConfig: resolvedMode.context.modeConfig,
|
|
2133
2145
|
state: modeState,
|
|
2134
2146
|
isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, chatType, selfAid),
|
|
2147
|
+
describeSendCommandFailure: describeProactiveSendCommandFailure,
|
|
2135
2148
|
logger,
|
|
2136
2149
|
} : null;
|
|
2137
2150
|
if (resolvedMode?.mode.beforeProcess && modeProcessCtx) {
|
|
@@ -2764,6 +2777,7 @@ export class ResponseEngine {
|
|
|
2764
2777
|
let previousFallbackState;
|
|
2765
2778
|
let agentModel;
|
|
2766
2779
|
let v2HandoffIds = [];
|
|
2780
|
+
let v2HandoffResultRefs = [];
|
|
2767
2781
|
let v2HandoffDirection;
|
|
2768
2782
|
let handoffPromptRendered = false;
|
|
2769
2783
|
let runtimeEnv;
|
|
@@ -2998,6 +3012,12 @@ export class ResponseEngine {
|
|
|
2998
3012
|
bootstrapAgentMdPath: agentMdPath(selfAid),
|
|
2999
3013
|
};
|
|
3000
3014
|
}
|
|
3015
|
+
if (selfAid && peerRole === 'owner' && effectivePermissionMode !== 'readonly') {
|
|
3016
|
+
modelOverride = {
|
|
3017
|
+
...(modelOverride || {}),
|
|
3018
|
+
personalMdPath: path.join(resolveRoot(), 'agents', selfAid, 'personal', 'personal.md'),
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3001
3021
|
if (normalizedBaseagent.canonical === 'claude') {
|
|
3002
3022
|
modelOverride = {
|
|
3003
3023
|
...(modelOverride || {}),
|
|
@@ -3092,7 +3112,7 @@ export class ResponseEngine {
|
|
|
3092
3112
|
KITS_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'system-fragments'),
|
|
3093
3113
|
KITS_MESSAGE_FRAGMENTS: path.join(pkgRoot, 'kits', 'templates', 'message-fragments'),
|
|
3094
3114
|
// evolcore 运行模式:dev=源码仓库 | install=全局安装包
|
|
3095
|
-
evolcoreMode:
|
|
3115
|
+
evolcoreMode: readEvolcoreRuntimeFlavor(),
|
|
3096
3116
|
// 路径变量(用于 manifest 路径展开,resolvePath 用 ctx.vars 取真值)
|
|
3097
3117
|
PERSONAL_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'personal') : undefined,
|
|
3098
3118
|
RELATIONS_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'relations') : undefined,
|
|
@@ -3235,6 +3255,7 @@ export class ResponseEngine {
|
|
|
3235
3255
|
v2HandoffIds = Array.from(new Set(items.flatMap(item => (item.handoff?.handoffIds?.length
|
|
3236
3256
|
? item.handoff.handoffIds
|
|
3237
3257
|
: (item.handoff?.handoffId ? [item.handoff.handoffId] : [])))));
|
|
3258
|
+
v2HandoffResultRefs = items.flatMap(item => item.handoff?.resultRefs ?? []);
|
|
3238
3259
|
v2HandoffDirection = message.handoffDelivery.direction;
|
|
3239
3260
|
return items;
|
|
3240
3261
|
}
|
|
@@ -3308,11 +3329,18 @@ export class ResponseEngine {
|
|
|
3308
3329
|
return;
|
|
3309
3330
|
}
|
|
3310
3331
|
if (v2HandoffIds.length > 0 && handoffPromptRendered && this.handoffRuntime && selfAid) {
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
this.handoffRuntime.completeOriginContext(selfAid, handoffId);
|
|
3314
|
-
|
|
3315
|
-
|
|
3332
|
+
if (v2HandoffDirection === 'origin' && v2HandoffResultRefs.length > 0) {
|
|
3333
|
+
for (const ref of v2HandoffResultRefs) {
|
|
3334
|
+
this.handoffRuntime.completeOriginContext(selfAid, ref.handoffId, ref.resultId);
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
else {
|
|
3338
|
+
for (const handoffId of v2HandoffIds) {
|
|
3339
|
+
if (v2HandoffDirection === 'origin')
|
|
3340
|
+
this.handoffRuntime.completeOriginContext(selfAid, handoffId);
|
|
3341
|
+
else if (v2HandoffDirection === 'target')
|
|
3342
|
+
this.handoffRuntime.completeTargetContext(selfAid, handoffId);
|
|
3343
|
+
}
|
|
3316
3344
|
}
|
|
3317
3345
|
}
|
|
3318
3346
|
// The runner may already own a stronger per-session directory (Codex
|
|
@@ -4446,7 +4474,9 @@ export class ResponseEngine {
|
|
|
4446
4474
|
kind: 'activity.batch',
|
|
4447
4475
|
items: [{
|
|
4448
4476
|
kind: 'notice',
|
|
4449
|
-
|
|
4477
|
+
// activity.batch 载荷混合正常内容与错误,顶层出站包裹不整体打标;
|
|
4478
|
+
// 这里的 notice 是 proactive 下用户能看到的错误副本,单独打标。
|
|
4479
|
+
text: tagErrorTextWithEvolcoreVersion(userMessage),
|
|
4450
4480
|
severity: 'warn',
|
|
4451
4481
|
subtype: 'task-error',
|
|
4452
4482
|
}],
|
|
@@ -4562,7 +4592,7 @@ export class ResponseEngine {
|
|
|
4562
4592
|
const shouldClose = !writerDb;
|
|
4563
4593
|
try {
|
|
4564
4594
|
const row = db.prepare(`SELECT model, input_tokens, cache_creation_tokens, cache_read_tokens,
|
|
4565
|
-
context_tokens
|
|
4595
|
+
context_tokens
|
|
4566
4596
|
FROM model_calls
|
|
4567
4597
|
WHERE session_id = ?
|
|
4568
4598
|
AND agent_session_id = ?
|
|
@@ -4584,16 +4614,11 @@ export class ResponseEngine {
|
|
|
4584
4614
|
}
|
|
4585
4615
|
if (totalTokens <= 0)
|
|
4586
4616
|
return undefined;
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
// Historic rows may contain the old fixed 350k threshold. Never let a
|
|
4593
|
-
// persisted value exceed the context window that accompanied it.
|
|
4594
|
-
const autoCompactTokens = inferredAutoCompactTokens
|
|
4595
|
-
? Math.min(candidateAutoCompactTokens, inferredAutoCompactTokens)
|
|
4596
|
-
: candidateAutoCompactTokens;
|
|
4617
|
+
// EvolCore 的自动压缩阈值固定为 650k,与模型上下文窗口无关:
|
|
4618
|
+
// 大窗口模型(1M)到 650k 由 EvolCore 压缩;小窗口模型(200k/128k)
|
|
4619
|
+
// 到不了该阈值,交给 base agent 自身的压缩机制处理。
|
|
4620
|
+
// 历史行里可能存着旧的 350k 阈值,这里一律忽略,只认当前常量。
|
|
4621
|
+
const autoCompactTokens = autoCompactWindowForModel(model);
|
|
4597
4622
|
return { totalTokens, autoCompactTokens };
|
|
4598
4623
|
}
|
|
4599
4624
|
finally {
|
|
@@ -3,11 +3,21 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { appendRequestPath, appendV1RequestPath } from '../../agents/request-identity.js';
|
|
5
5
|
import { atomicReadJson, atomicWriteJson } from '../../utils/atomic-write.js';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { parseModelId } from './context-window-suffix.js';
|
|
7
|
+
/** 单页请求的超时(含响应体读取)。多页目录里每页各自享有这个预算。 */
|
|
8
|
+
export const CATALOG_PAGE_TIMEOUT_MS = 4_000;
|
|
9
|
+
/** 整条路由(含全部分页)的总期限,兜住慢网关无限翻页。 */
|
|
10
|
+
export const CATALOG_ROUTE_TIMEOUT_MS = 15_000;
|
|
11
|
+
/**
|
|
12
|
+
* 持久化快照的格式版本。**目录规范化或能力过滤规则变化时必须递增**,
|
|
13
|
+
* 否则旧规则写下的持久化快照与新快照无法区分,会继续被服务。
|
|
14
|
+
* v2:保留上下文窗口后缀(`claude-opus-5[1m]` 不再被折叠成基础 ID)。
|
|
15
|
+
* v3:保留 Codex 上游声明的 max effort,重新获取旧快照中过滤掉的能力。
|
|
16
|
+
*/
|
|
17
|
+
export const CATALOG_SNAPSHOT_VERSION = 3;
|
|
8
18
|
export const TRANSPORT_EFFORTS = {
|
|
9
19
|
claude: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
10
|
-
codex: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'],
|
|
20
|
+
codex: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'],
|
|
11
21
|
ecagent: ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'],
|
|
12
22
|
gemini: [],
|
|
13
23
|
};
|
|
@@ -22,7 +32,7 @@ export const catalogHash = (value) => createHash('sha256').update(stable(value))
|
|
|
22
32
|
export function catalogContextKey(context) {
|
|
23
33
|
const headers = Object.fromEntries(Object.entries(context.headers ?? {}).map(([k, v]) => [k.toLowerCase(), v]));
|
|
24
34
|
return catalogHash({ scope: context.scope, baseagent: context.baseagent, provider: context.provider,
|
|
25
|
-
baseUrl: context.baseUrl?.replace(/\/+$/, ''), apiKey: context.apiKey,
|
|
35
|
+
baseUrl: context.baseUrl?.replace(/\/+$/, ''), apiKey: context.apiKey, authType: context.authType,
|
|
26
36
|
headers, queryParams: context.queryParams ?? {}, configUnavailable: context.configUnavailable });
|
|
27
37
|
}
|
|
28
38
|
const strings = (value) => Array.isArray(value)
|
|
@@ -40,37 +50,45 @@ export function parseModelList(json, _url) {
|
|
|
40
50
|
const out = new Map();
|
|
41
51
|
const items = rows(json);
|
|
42
52
|
for (const item of items) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
// 单个畸形条目只跳过自己:一条读不懂的记录不该让整份目录退化到 builtin。
|
|
54
|
+
try {
|
|
55
|
+
const rawId = typeof item === 'string' ? item : item?.id ?? item?.slug ?? item?.name ?? item?.model;
|
|
56
|
+
if (typeof rawId !== 'string' || !rawId.trim())
|
|
57
|
+
continue;
|
|
58
|
+
// 上下文窗口后缀 [1m]/[200k] 是模型身份的一部分:网关把不同窗口作为独立条目
|
|
59
|
+
// 下发,两个变体必须各自成为一条可选条目。仅规范化后缀大小写以便去重。
|
|
60
|
+
const parsed = parseModelId(rawId.trim());
|
|
61
|
+
const id = `${parsed.base}${parsed.suffix ?? ''}`;
|
|
62
|
+
if (out.has(id))
|
|
63
|
+
continue;
|
|
64
|
+
const rawLevels = item?.reasoningLevels ?? item?.supportedReasoningEfforts ?? item?.supported_reasoning_levels;
|
|
65
|
+
const levels = Array.isArray(rawLevels) ? strings(rawLevels.map(v => typeof v === 'string' ? v : v?.reasoningEffort ?? v?.effort)) : undefined;
|
|
66
|
+
const owner = item?.owned_by ?? item?.owner ?? item?.provider;
|
|
67
|
+
out.set(id, {
|
|
68
|
+
id, verification: 'listed',
|
|
69
|
+
...(typeof owner === 'string' ? { owned_by: owner } : {}),
|
|
70
|
+
...(typeof item?.created === 'number' ? { created: item.created } : {}),
|
|
71
|
+
...(typeof (item?.displayName ?? item?.display_name) === 'string' ? { displayName: item.displayName ?? item.display_name } : {}),
|
|
72
|
+
...(levels ? { reasoningLevels: levels } : {}),
|
|
73
|
+
...(typeof (item?.defaultReasoningEffort ?? item?.default_reasoning_level) === 'string' ? { defaultReasoningLevel: item.defaultReasoningEffort ?? item.default_reasoning_level } : {}),
|
|
74
|
+
...(strings(item?.protocols ?? item?.supported_protocols) ? { protocols: strings(item.protocols ?? item.supported_protocols) } : {}),
|
|
75
|
+
...(typeof item?.capabilities?.tool_calling === 'boolean' ? { supportsTools: item.capabilities.tool_calling } : {}),
|
|
76
|
+
// 网关显式给出的 context_window 优先;缺失时用后缀声明的窗口兜底。
|
|
77
|
+
...(typeof (item?.context_window ?? item?.contextWindow) === 'number' ? { contextWindow: item.context_window ?? item.contextWindow }
|
|
78
|
+
: parsed.contextWindow !== undefined ? { contextWindow: parsed.contextWindow } : {}),
|
|
79
|
+
...(typeof item?.max_output_tokens === 'number' ? { maxOutputTokens: item.max_output_tokens } : {}),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch { /* Skip the offending entry; the rest of the page stays usable. */ }
|
|
66
83
|
}
|
|
84
|
+
// 全部条目都无法解析仍视为不可信响应:避免把一个格式不符的 200 当成空目录。
|
|
67
85
|
if (items.length && !out.size)
|
|
68
86
|
throw new Error('Model list contains no valid IDs');
|
|
69
87
|
return [...out.values()];
|
|
70
88
|
}
|
|
71
89
|
const BUILTIN_IDS = {
|
|
72
90
|
codex: ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini', 'gpt-5.3-codex', 'gpt-5.2'],
|
|
73
|
-
claude: ['claude-opus-4-7', 'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001', 'deepseek-v4-pro', 'deepseek-v4-flash', 'kimi-k2.6', 'kimi-k2.5', 'glm-5.1', 'glm-5', 'glm-4.7', 'MiniMax-M2.7'],
|
|
91
|
+
claude: ['claude-opus-5', 'claude-sonnet-5', 'claude-opus-4-8', 'claude-opus-4-7', 'claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001', 'deepseek-v4-pro', 'deepseek-v4-flash', 'kimi-k2.6', 'kimi-k2.5', 'glm-5.1', 'glm-5', 'glm-4.7', 'MiniMax-M2.7'],
|
|
74
92
|
ecagent: ['claude-sonnet-4-6'],
|
|
75
93
|
gemini: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'],
|
|
76
94
|
};
|
|
@@ -89,9 +107,14 @@ export function deriveAliases(models, baseagent) {
|
|
|
89
107
|
if (!match)
|
|
90
108
|
continue;
|
|
91
109
|
const [, family, major, minor = '0'] = match;
|
|
110
|
+
// 同版本的多个窗口变体:取窗口最大的一条,别名才不会随网关下发顺序漂移。
|
|
111
|
+
const window = parseModelId(id).contextWindow ?? 0;
|
|
92
112
|
const current = best[family];
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
const better = !current || +major > current.major
|
|
114
|
+
|| (+major === current.major && +minor > current.minor)
|
|
115
|
+
|| (+major === current.major && +minor === current.minor && window > current.window);
|
|
116
|
+
if (better)
|
|
117
|
+
best[family] = { id, major: +major, minor: +minor, window };
|
|
95
118
|
}
|
|
96
119
|
return Object.fromEntries(Object.entries(best).map(([family, info]) => [family, info.id]));
|
|
97
120
|
}
|
|
@@ -133,12 +156,17 @@ async function gatewayModels(context, fetcher) {
|
|
|
133
156
|
const models = [];
|
|
134
157
|
const seen = new Set();
|
|
135
158
|
let next = url;
|
|
136
|
-
//
|
|
137
|
-
|
|
159
|
+
// 每页各自计时,避免多页目录被一个覆盖全程的超时提前掐断;
|
|
160
|
+
// 同时用总期限兜住整条路由,防止慢网关无限翻页。
|
|
161
|
+
const deadline = Date.now() + CATALOG_ROUTE_TIMEOUT_MS;
|
|
138
162
|
for (let page = 0; page < 100; page++) {
|
|
139
163
|
if (seen.has(next))
|
|
140
164
|
throw new GatewayError();
|
|
141
165
|
seen.add(next);
|
|
166
|
+
const remaining = deadline - Date.now();
|
|
167
|
+
if (remaining <= 0)
|
|
168
|
+
throw new GatewayError();
|
|
169
|
+
const signal = AbortSignal.timeout(Math.min(CATALOG_PAGE_TIMEOUT_MS, remaining));
|
|
142
170
|
const headers = new Headers(context.headers);
|
|
143
171
|
if (context.apiKey && !headers.has('authorization')) {
|
|
144
172
|
headers.set('authorization', `Bearer ${context.apiKey}`);
|
|
@@ -176,7 +204,7 @@ async function gatewayModels(context, fetcher) {
|
|
|
176
204
|
/** One instance per daemon. Offline clients use the same persistence format. */
|
|
177
205
|
export class ModelCatalogService {
|
|
178
206
|
options;
|
|
179
|
-
|
|
207
|
+
latest = new Map();
|
|
180
208
|
successes = new Map();
|
|
181
209
|
inflight = new Map();
|
|
182
210
|
constructor(options = {}) {
|
|
@@ -192,41 +220,43 @@ export class ModelCatalogService {
|
|
|
192
220
|
try {
|
|
193
221
|
const file = this.file(context);
|
|
194
222
|
const saved = file ? atomicReadJson(file) : undefined;
|
|
195
|
-
|
|
223
|
+
// 版本不匹配的快照按冷启动处理:其 ID 可能出自已废弃的规范化规则。
|
|
224
|
+
if (saved?.version === CATALOG_SNAPSHOT_VERSION && saved.snapshot && Array.isArray(saved.snapshot.models) && ['gateway', 'native'].includes(saved.snapshot.source))
|
|
196
225
|
snapshot = saved.snapshot;
|
|
197
226
|
}
|
|
198
227
|
catch { /* A missing/corrupt cache is a cold load. */ }
|
|
199
228
|
if (snapshot)
|
|
200
229
|
this.successes.set(key, snapshot);
|
|
201
230
|
}
|
|
202
|
-
return snapshot
|
|
231
|
+
return snapshot;
|
|
203
232
|
}
|
|
204
233
|
peek(context) {
|
|
205
|
-
return structuredClone(this.
|
|
234
|
+
return structuredClone(this.latest.get(this.key(context)) ?? this.previous(context) ?? this.make(context, 'builtin', 'fallback', this.legacyBuiltins(context), [{ code: 'BUILTIN', message: '内置候选,未经当前连接验证' }]));
|
|
206
235
|
}
|
|
207
236
|
async get(context, refresh = false) {
|
|
208
237
|
const key = this.key(context);
|
|
209
|
-
const
|
|
210
|
-
if (!refresh && cached && cached.retryAt > this.now())
|
|
211
|
-
return structuredClone(cached.snapshot);
|
|
212
|
-
const previous = this.previous(context);
|
|
213
|
-
if (!refresh && previous && this.now() - Date.parse(previous.fetchedAt) < CATALOG_TTL_MS && !cached) {
|
|
214
|
-
this.cache.set(key, { snapshot: previous, retryAt: Date.parse(previous.fetchedAt) + CATALOG_TTL_MS });
|
|
215
|
-
return structuredClone(previous);
|
|
216
|
-
}
|
|
238
|
+
const previous = this.latest.get(key) ?? this.previous(context);
|
|
217
239
|
let pending = this.inflight.get(key);
|
|
218
240
|
if (!pending) {
|
|
219
241
|
pending = this.load(context, previous).then(snapshot => {
|
|
220
|
-
|
|
242
|
+
// Keep any usable first result (including builtin/native fallback), but
|
|
243
|
+
// never let a failed refresh replace the last usable snapshot.
|
|
244
|
+
if (snapshot.freshness !== 'stale')
|
|
245
|
+
this.latest.set(key, snapshot);
|
|
221
246
|
return snapshot;
|
|
222
247
|
}).finally(() => this.inflight.delete(key));
|
|
223
248
|
this.inflight.set(key, pending);
|
|
224
249
|
}
|
|
225
|
-
if (!refresh && previous)
|
|
226
|
-
|
|
250
|
+
if (!refresh && previous) {
|
|
251
|
+
// Serve the last successful catalog immediately on every ordinary read,
|
|
252
|
+
// while each read also asks the shared background job to refresh it.
|
|
253
|
+
// Explicit refresh callers still await that same in-flight request.
|
|
254
|
+
void pending.catch(() => undefined);
|
|
255
|
+
return structuredClone(previous);
|
|
256
|
+
}
|
|
227
257
|
return structuredClone(await pending);
|
|
228
258
|
}
|
|
229
|
-
clear() { this.
|
|
259
|
+
clear() { this.latest.clear(); this.successes.clear(); }
|
|
230
260
|
make(context, source, freshness, models, diagnostics) {
|
|
231
261
|
const entries = adapt(models, context.baseagent);
|
|
232
262
|
const fetchedAt = new Date(this.now()).toISOString();
|
|
@@ -241,7 +271,7 @@ export class ModelCatalogService {
|
|
|
241
271
|
const file = this.file(context);
|
|
242
272
|
if (file) {
|
|
243
273
|
fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
244
|
-
atomicWriteJson(file, { version:
|
|
274
|
+
atomicWriteJson(file, { version: CATALOG_SNAPSHOT_VERSION, snapshot });
|
|
245
275
|
}
|
|
246
276
|
}
|
|
247
277
|
catch { /* Persistence failure does not discard a live result. */ }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code SDK 的上下文窗口后缀(如 `claude-opus-5[1m]`)。
|
|
3
|
+
*
|
|
4
|
+
* 网关会把同一模型的不同窗口作为独立条目下发,所以后缀是**模型身份的一部分**,
|
|
5
|
+
* 目录与选择界面必须原样保留——两个变体各成一条可选条目。
|
|
6
|
+
* 只有按基础 ID 归档的场景(查价、计费、能力参数表)才剥离后缀。
|
|
7
|
+
*/
|
|
8
|
+
/** 后缀 token → 该后缀声明的上下文窗口。 */
|
|
9
|
+
export const CONTEXT_WINDOW_SUFFIX_TOKENS = {
|
|
10
|
+
'1m': 1_000_000,
|
|
11
|
+
'200k': 200_000,
|
|
12
|
+
};
|
|
13
|
+
const SUFFIX_RE = /\[(1m|200k)\]$/i;
|
|
14
|
+
/** 拆分模型 ID 的基础部分与上下文窗口后缀。 */
|
|
15
|
+
export function parseModelId(modelId) {
|
|
16
|
+
const match = SUFFIX_RE.exec(modelId);
|
|
17
|
+
if (!match)
|
|
18
|
+
return { base: modelId };
|
|
19
|
+
const token = match[1].toLowerCase();
|
|
20
|
+
return {
|
|
21
|
+
base: modelId.slice(0, match.index),
|
|
22
|
+
suffix: `[${token}]`,
|
|
23
|
+
contextWindow: CONTEXT_WINDOW_SUFFIX_TOKENS[token],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** 去掉上下文窗口后缀。仅用于查价/计费/能力表这类按基础 ID 归档的场景。 */
|
|
27
|
+
export function stripContextWindowSuffix(modelId) {
|
|
28
|
+
return modelId.replace(SUFFIX_RE, '');
|
|
29
|
+
}
|
|
30
|
+
/** 该 ID 是否已带网关显式给出的上下文窗口后缀。 */
|
|
31
|
+
export function hasContextWindowSuffix(modelId) {
|
|
32
|
+
return SUFFIX_RE.test(modelId);
|
|
33
|
+
}
|
|
@@ -88,7 +88,7 @@ export function resolveCatalogContext(self, ba) {
|
|
|
88
88
|
: baseagent === 'claude' ? resolveAnthropicConfig(cfg, block.claude)
|
|
89
89
|
: baseagent === 'ecagent' ? resolveEcagentConfig(cfg, block.ecagent) : undefined;
|
|
90
90
|
if (resolved) {
|
|
91
|
-
Object.assign(context, { baseUrl: resolved.baseUrl, apiKey: resolved.apiKey, queryParams: resolved.queryParams,
|
|
91
|
+
Object.assign(context, { baseUrl: resolved.baseUrl, apiKey: resolved.apiKey, authType: 'authType' in resolved ? resolved.authType : undefined, queryParams: resolved.queryParams,
|
|
92
92
|
provider: 'provider' in resolved ? resolved.provider : undefined,
|
|
93
93
|
headers: buildModelRequestHeaders({ baseagent: baseagent, baseUrl: resolved.baseUrl, agentAid: self, configuredHeaders: resolved.headers }) });
|
|
94
94
|
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { getFieldPermission } from '../../config/roles.js';
|
|
2
|
+
import { stripContextWindowSuffix } from './context-window-suffix.js';
|
|
2
3
|
export function isModelAllowedByPatterns(model, allowedModels) {
|
|
3
4
|
if (allowedModels.includes('*'))
|
|
4
5
|
return true;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
// 窗口变体是同一模型的不同上下文窗口:白名单写基础 ID(`claude-opus-5`)即同时
|
|
7
|
+
// 授权其窗口变体(`claude-opus-5[1m]`)。反向不成立——白名单显式写了某个变体时,
|
|
8
|
+
// 只授权那一个变体,其它变体和裸 ID 仍被拒绝。
|
|
9
|
+
const candidates = new Set([model, stripContextWindowSuffix(model)]);
|
|
10
|
+
return allowedModels.some(pattern => {
|
|
11
|
+
if (pattern.endsWith('*')) {
|
|
12
|
+
const prefix = pattern.slice(0, -1);
|
|
13
|
+
return [...candidates].some(candidate => candidate.startsWith(prefix));
|
|
14
|
+
}
|
|
15
|
+
return candidates.has(pattern);
|
|
16
|
+
});
|
|
8
17
|
}
|
|
9
18
|
function normalizeBaseagent(baseagent) {
|
|
10
19
|
const normalized = typeof baseagent === 'string' ? baseagent.trim() : '';
|