@jsonstudio/rcc 0.89.555 → 0.89.611
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/dist/build-info.js +2 -2
- package/dist/modules/llmswitch/bridge.d.ts +43 -0
- package/dist/modules/llmswitch/bridge.js +103 -0
- package/dist/modules/llmswitch/bridge.js.map +1 -1
- package/dist/monitoring/semantic-config-loader.js +3 -1
- package/dist/monitoring/semantic-config-loader.js.map +1 -1
- package/dist/providers/core/runtime/http-transport-provider.d.ts +3 -0
- package/dist/providers/core/runtime/http-transport-provider.js +70 -4
- package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
- package/dist/providers/core/runtime/responses-provider.d.ts +2 -2
- package/dist/providers/core/runtime/responses-provider.js +33 -28
- package/dist/providers/core/runtime/responses-provider.js.map +1 -1
- package/dist/providers/core/utils/provider-error-reporter.js +7 -7
- package/dist/providers/core/utils/provider-error-reporter.js.map +1 -1
- package/dist/providers/core/utils/snapshot-writer.js +6 -2
- package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
- package/dist/server/runtime/http-server/index.js +59 -47
- package/dist/server/runtime/http-server/index.js.map +1 -1
- package/dist/server/runtime/http-server/llmswitch-loader.d.ts +0 -1
- package/dist/server/runtime/http-server/llmswitch-loader.js +17 -21
- package/dist/server/runtime/http-server/llmswitch-loader.js.map +1 -1
- package/dist/server/runtime/http-server/request-executor.d.ts +6 -0
- package/dist/server/runtime/http-server/request-executor.js +113 -37
- package/dist/server/runtime/http-server/request-executor.js.map +1 -1
- package/node_modules/@jsonstudio/llms/dist/conversion/codecs/gemini-openai-codec.js +15 -1
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/iflow-web-search.d.ts +18 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/iflow-web-search.js +87 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-gemini.json +14 -15
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-glm.json +194 -190
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-iflow.json +199 -195
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-qwen.json +20 -20
- package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/responses-c4m.json +42 -42
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline.js +5 -1
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/session-identifiers.d.ts +9 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/session-identifiers.js +76 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +31 -2
- package/node_modules/@jsonstudio/llms/dist/conversion/hub/process/chat-process.js +89 -25
- package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.js +75 -4
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/anthropic-message-utils.js +41 -6
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/errors.d.ts +20 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/errors.js +28 -0
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-conversation-store.js +30 -3
- package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-output-builder.js +68 -6
- package/node_modules/@jsonstudio/llms/dist/filters/special/request-toolcalls-stringify.d.ts +13 -0
- package/node_modules/@jsonstudio/llms/dist/filters/special/request-toolcalls-stringify.js +103 -3
- package/node_modules/@jsonstudio/llms/dist/filters/special/response-tool-text-canonicalize.d.ts +16 -0
- package/node_modules/@jsonstudio/llms/dist/filters/special/response-tool-text-canonicalize.js +27 -3
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/classifier.js +4 -2
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.d.ts +30 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.js +618 -42
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/health-manager.d.ts +23 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/health-manager.js +14 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.d.ts +15 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.js +40 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/routing-instructions.d.ts +34 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/routing-instructions.js +393 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/sticky-session-store.d.ts +3 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/sticky-session-store.js +110 -0
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/tool-signals.js +0 -22
- package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.d.ts +41 -0
- package/node_modules/@jsonstudio/llms/dist/servertool/engine.js +42 -1
- package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.js +157 -4
- package/node_modules/@jsonstudio/llms/dist/servertool/types.d.ts +6 -0
- package/node_modules/@jsonstudio/llms/package.json +1 -1
- package/package.json +8 -5
- package/scripts/mock-provider/run-regressions.mjs +38 -2
- package/scripts/verify-apply-patch.mjs +132 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ProviderHealthConfig, ProviderHealthState } from './types.js';
|
|
2
|
+
interface ProviderInternalState extends ProviderHealthState {
|
|
3
|
+
lastFailureAt?: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class ProviderHealthManager {
|
|
6
|
+
private readonly states;
|
|
7
|
+
private config;
|
|
8
|
+
configure(config?: ProviderHealthConfig): void;
|
|
9
|
+
registerProviders(providerKeys: string[]): void;
|
|
10
|
+
recordFailure(providerKey: string, reason?: string): ProviderInternalState;
|
|
11
|
+
/**
|
|
12
|
+
* 为可恢复错误(例如 429)提供短暂冷静期:在 cooldownMs 内将 providerKey
|
|
13
|
+
* 视为不可用,但不使用 fatalCooldownMs 的长熔断时间。
|
|
14
|
+
*/
|
|
15
|
+
cooldownProvider(providerKey: string, reason?: string, overrideMs?: number): ProviderInternalState;
|
|
16
|
+
recordSuccess(providerKey: string): ProviderInternalState;
|
|
17
|
+
tripProvider(providerKey: string, reason?: string, cooldownOverrideMs?: number): ProviderInternalState;
|
|
18
|
+
isAvailable(providerKey: string): boolean;
|
|
19
|
+
getSnapshot(): ProviderHealthState[];
|
|
20
|
+
getConfig(): Required<ProviderHealthConfig>;
|
|
21
|
+
private getState;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
@@ -39,6 +39,20 @@ export class ProviderHealthManager {
|
|
|
39
39
|
// 只有显式 fatal 或 tripProvider 调用时才会进入 tripped 状态。
|
|
40
40
|
return state;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* 为可恢复错误(例如 429)提供短暂冷静期:在 cooldownMs 内将 providerKey
|
|
44
|
+
* 视为不可用,但不使用 fatalCooldownMs 的长熔断时间。
|
|
45
|
+
*/
|
|
46
|
+
cooldownProvider(providerKey, reason, overrideMs) {
|
|
47
|
+
const state = this.getState(providerKey);
|
|
48
|
+
state.failureCount += 1;
|
|
49
|
+
state.state = 'tripped';
|
|
50
|
+
state.reason = reason;
|
|
51
|
+
const ttl = overrideMs ?? this.config.cooldownMs;
|
|
52
|
+
state.cooldownExpiresAt = Date.now() + ttl;
|
|
53
|
+
state.lastFailureAt = Date.now();
|
|
54
|
+
return state;
|
|
55
|
+
}
|
|
42
56
|
recordSuccess(providerKey) {
|
|
43
57
|
const state = this.getState(providerKey);
|
|
44
58
|
state.failureCount = 0;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ProviderProfile, TargetMetadata } from './types.js';
|
|
2
|
+
export declare class ProviderRegistry {
|
|
3
|
+
private readonly providers;
|
|
4
|
+
constructor(profiles?: Record<string, ProviderProfile>);
|
|
5
|
+
load(profiles: Record<string, ProviderProfile>): void;
|
|
6
|
+
get(providerKey: string): ProviderProfile;
|
|
7
|
+
has(providerKey: string): boolean;
|
|
8
|
+
listKeys(): string[];
|
|
9
|
+
resolveRuntimeKeyByAlias(providerId: string, keyAlias: string): string | null;
|
|
10
|
+
resolveRuntimeKeyByIndex(providerId: string, keyIndex: number): string | null;
|
|
11
|
+
listProviderKeys(providerId: string): string[];
|
|
12
|
+
resolveRuntimeKeyByModel(providerId: string, modelId: string): string | null;
|
|
13
|
+
buildTarget(providerKey: string): TargetMetadata;
|
|
14
|
+
private static normalizeProfile;
|
|
15
|
+
}
|
|
@@ -28,6 +28,46 @@ export class ProviderRegistry {
|
|
|
28
28
|
listKeys() {
|
|
29
29
|
return Array.from(this.providers.keys());
|
|
30
30
|
}
|
|
31
|
+
resolveRuntimeKeyByAlias(providerId, keyAlias) {
|
|
32
|
+
const pattern = new RegExp(`^${providerId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.${keyAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\.|$)`);
|
|
33
|
+
for (const key of this.providers.keys()) {
|
|
34
|
+
if (pattern.test(key)) {
|
|
35
|
+
return key;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
resolveRuntimeKeyByIndex(providerId, keyIndex) {
|
|
41
|
+
const index = keyIndex - 1;
|
|
42
|
+
if (index < 0)
|
|
43
|
+
return null;
|
|
44
|
+
const keys = this.listProviderKeys(providerId);
|
|
45
|
+
if (index >= keys.length)
|
|
46
|
+
return null;
|
|
47
|
+
return keys[index];
|
|
48
|
+
}
|
|
49
|
+
listProviderKeys(providerId) {
|
|
50
|
+
const pattern = new RegExp(`^${providerId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.`);
|
|
51
|
+
return this.listKeys().filter(key => pattern.test(key));
|
|
52
|
+
}
|
|
53
|
+
resolveRuntimeKeyByModel(providerId, modelId) {
|
|
54
|
+
if (!providerId || !modelId) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const normalizedModel = modelId.trim();
|
|
58
|
+
if (!normalizedModel) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const providerKeys = this.listProviderKeys(providerId);
|
|
62
|
+
for (const key of providerKeys) {
|
|
63
|
+
const profile = this.providers.get(key);
|
|
64
|
+
const candidate = profile?.modelId ?? deriveModelId(key);
|
|
65
|
+
if (candidate === normalizedModel) {
|
|
66
|
+
return key;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
31
71
|
buildTarget(providerKey) {
|
|
32
72
|
const profile = this.get(providerKey);
|
|
33
73
|
const modelId = profile.modelId ?? deriveModelId(profile.providerKey);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { StandardizedMessage } from '../../conversion/hub/types/standardized.js';
|
|
2
|
+
export interface RoutingInstruction {
|
|
3
|
+
type: 'force' | 'sticky' | 'disable' | 'enable' | 'clear' | 'allow';
|
|
4
|
+
provider?: string;
|
|
5
|
+
keyAlias?: string;
|
|
6
|
+
keyIndex?: number;
|
|
7
|
+
model?: string;
|
|
8
|
+
pathLength?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface RoutingInstructionState {
|
|
11
|
+
forcedTarget?: {
|
|
12
|
+
provider?: string;
|
|
13
|
+
keyAlias?: string;
|
|
14
|
+
keyIndex?: number;
|
|
15
|
+
model?: string;
|
|
16
|
+
pathLength?: number;
|
|
17
|
+
};
|
|
18
|
+
stickyTarget?: {
|
|
19
|
+
provider?: string;
|
|
20
|
+
keyAlias?: string;
|
|
21
|
+
keyIndex?: number;
|
|
22
|
+
model?: string;
|
|
23
|
+
pathLength?: number;
|
|
24
|
+
};
|
|
25
|
+
allowedProviders: Set<string>;
|
|
26
|
+
disabledProviders: Set<string>;
|
|
27
|
+
disabledKeys: Map<string, Set<string | number>>;
|
|
28
|
+
disabledModels: Map<string, Set<string>>;
|
|
29
|
+
}
|
|
30
|
+
export declare function parseRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
|
|
31
|
+
export declare function applyRoutingInstructions(instructions: RoutingInstruction[], currentState: RoutingInstructionState): RoutingInstructionState;
|
|
32
|
+
export declare function cleanMessagesFromRoutingInstructions(messages: StandardizedMessage[]): StandardizedMessage[];
|
|
33
|
+
export declare function serializeRoutingInstructionState(state: RoutingInstructionState): Record<string, unknown>;
|
|
34
|
+
export declare function deserializeRoutingInstructionState(data: Record<string, unknown>): RoutingInstructionState;
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { extractMessageText } from './message-utils.js';
|
|
2
|
+
export function parseRoutingInstructions(messages) {
|
|
3
|
+
const instructions = [];
|
|
4
|
+
// 从最新一条携带路由指令标记(<** ... **>)的 user 消息中解析指令,
|
|
5
|
+
// 而不是简单地取“最后一条 user 消息”。这样可以在服务重启后,通过完整
|
|
6
|
+
// 会话历史恢复 sticky/黑名单状态,同时保持“最后一次指令生效”的语义。
|
|
7
|
+
let sanitized = null;
|
|
8
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
9
|
+
const message = messages[idx];
|
|
10
|
+
if (!message || message.role !== 'user') {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const content = extractMessageText(message);
|
|
14
|
+
if (!content) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const candidate = stripCodeSegments(content);
|
|
18
|
+
if (!candidate) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!/<\*\*[^*]+\*\*>/.test(candidate)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
sanitized = candidate;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
if (!sanitized) {
|
|
28
|
+
return instructions;
|
|
29
|
+
}
|
|
30
|
+
const regex = /<\*\*([^*]+)\*\*>/g;
|
|
31
|
+
let match;
|
|
32
|
+
while ((match = regex.exec(sanitized)) !== null) {
|
|
33
|
+
const instruction = match[1].trim();
|
|
34
|
+
if (!instruction) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const segments = expandInstructionSegments(instruction);
|
|
38
|
+
for (const segment of segments) {
|
|
39
|
+
const parsed = parseSingleInstruction(segment);
|
|
40
|
+
if (parsed) {
|
|
41
|
+
instructions.push(parsed);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return instructions;
|
|
46
|
+
}
|
|
47
|
+
function expandInstructionSegments(instruction) {
|
|
48
|
+
const trimmed = instruction.trim();
|
|
49
|
+
if (!trimmed) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const prefix = trimmed[0];
|
|
53
|
+
if (prefix === '!' || prefix === '#' || prefix === '@') {
|
|
54
|
+
const tokens = splitInstructionTargets(trimmed.substring(1));
|
|
55
|
+
return tokens
|
|
56
|
+
.map((token) => token.replace(/^[!#@]+/, '').trim())
|
|
57
|
+
.filter((token) => token.length > 0)
|
|
58
|
+
.map((token) => `${prefix}${token}`);
|
|
59
|
+
}
|
|
60
|
+
return splitInstructionTargets(trimmed);
|
|
61
|
+
}
|
|
62
|
+
function splitInstructionTargets(content) {
|
|
63
|
+
return content
|
|
64
|
+
.split(',')
|
|
65
|
+
.map((segment) => segment.trim())
|
|
66
|
+
.filter((segment) => segment.length > 0);
|
|
67
|
+
}
|
|
68
|
+
function parseSingleInstruction(instruction) {
|
|
69
|
+
if (instruction === 'clear') {
|
|
70
|
+
return { type: 'clear' };
|
|
71
|
+
}
|
|
72
|
+
if (instruction.startsWith('!')) {
|
|
73
|
+
const target = instruction.substring(1).trim();
|
|
74
|
+
if (!target) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const parsed = parseTarget(target);
|
|
78
|
+
if (!parsed) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
// 约定:
|
|
82
|
+
// - "!providerA,providerB":允许列表(whitelist),用于快速限制可用 provider 集合;
|
|
83
|
+
// - "!provider.model" / "!provider.alias.model" / "!provider.2":sticky 语义,按 provider / alias / model 过滤当前路由池。
|
|
84
|
+
//
|
|
85
|
+
// 这样可以在不破坏既有 "!glm,openai" 语义的前提下,引入基于模型 / alias 的 sticky 行为。
|
|
86
|
+
if (!target.includes('.')) {
|
|
87
|
+
if (parsed.provider) {
|
|
88
|
+
return { type: 'allow', provider: parsed.provider, pathLength: parsed.pathLength };
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const normalized = normalizeStickyOrForceTarget(parsed);
|
|
93
|
+
return { type: 'sticky', ...normalized };
|
|
94
|
+
}
|
|
95
|
+
else if (instruction.startsWith('#')) {
|
|
96
|
+
const target = instruction.substring(1).trim();
|
|
97
|
+
const parsed = parseTarget(target);
|
|
98
|
+
if (parsed) {
|
|
99
|
+
return { type: 'disable', ...parsed };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (instruction.startsWith('@')) {
|
|
103
|
+
const target = instruction.substring(1).trim();
|
|
104
|
+
const parsed = parseTarget(target);
|
|
105
|
+
if (parsed) {
|
|
106
|
+
return { type: 'enable', ...parsed };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (isValidProviderModel(instruction)) {
|
|
110
|
+
const parsed = parseTarget(instruction);
|
|
111
|
+
if (parsed) {
|
|
112
|
+
const normalized = normalizeStickyOrForceTarget(parsed);
|
|
113
|
+
return { type: 'force', ...normalized };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else if (isValidIdentifier(instruction)) {
|
|
117
|
+
// 仅 provider 标识(无 .)时,视为 provider 级白名单,等价于 "<**!provider**>"。
|
|
118
|
+
// 这样可以用 "<**antigravity**>" 快速激活当前 routing 中所有 antigravity 相关池子,
|
|
119
|
+
// 并保证路由仅命中该 provider 的所有模型/key。
|
|
120
|
+
return {
|
|
121
|
+
type: 'allow',
|
|
122
|
+
provider: instruction,
|
|
123
|
+
pathLength: 1
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function parseTarget(target) {
|
|
129
|
+
if (!target) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
const parts = target.split('.');
|
|
133
|
+
const pathLength = parts.length;
|
|
134
|
+
if (parts.length === 0) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const provider = parts[0];
|
|
138
|
+
if (!provider || !isValidIdentifier(provider)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
if (parts.length === 1) {
|
|
142
|
+
return { provider, pathLength };
|
|
143
|
+
}
|
|
144
|
+
if (parts.length === 2) {
|
|
145
|
+
const second = parts[1];
|
|
146
|
+
const keyIndex = parseInt(second, 10);
|
|
147
|
+
if (!isNaN(keyIndex) && keyIndex > 0) {
|
|
148
|
+
return { provider, keyIndex, pathLength };
|
|
149
|
+
}
|
|
150
|
+
if (isValidIdentifier(second)) {
|
|
151
|
+
return { provider, model: second, keyAlias: second, pathLength };
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
if (parts.length === 3) {
|
|
156
|
+
const keyAlias = parts[1];
|
|
157
|
+
const model = parts[2];
|
|
158
|
+
if (isValidIdentifier(keyAlias) && isValidIdentifier(model)) {
|
|
159
|
+
return { provider, keyAlias, model, pathLength };
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function normalizeStickyOrForceTarget(target) {
|
|
166
|
+
if (target &&
|
|
167
|
+
target.pathLength === 2 &&
|
|
168
|
+
typeof target.model === 'string' &&
|
|
169
|
+
typeof target.keyAlias === 'string' &&
|
|
170
|
+
target.model === target.keyAlias) {
|
|
171
|
+
const clone = { ...target };
|
|
172
|
+
delete clone.keyAlias;
|
|
173
|
+
return clone;
|
|
174
|
+
}
|
|
175
|
+
return target;
|
|
176
|
+
}
|
|
177
|
+
function isValidIdentifier(id) {
|
|
178
|
+
return /^[a-zA-Z0-9_-]+$/.test(id);
|
|
179
|
+
}
|
|
180
|
+
function isValidProviderModel(providerModel) {
|
|
181
|
+
const pattern = /^[a-zA-Z0-9_-]+(?:\.[a-zA-Z0-9_-]+)+$/;
|
|
182
|
+
return pattern.test(providerModel);
|
|
183
|
+
}
|
|
184
|
+
function stripCodeSegments(text) {
|
|
185
|
+
if (!text) {
|
|
186
|
+
return '';
|
|
187
|
+
}
|
|
188
|
+
// Remove fenced code blocks ```...``` or ~~~...~~~
|
|
189
|
+
let sanitized = text.replace(/```[\s\S]*?```/g, ' ');
|
|
190
|
+
sanitized = sanitized.replace(/~~~[\s\S]*?~~~/g, ' ');
|
|
191
|
+
// Remove inline code `...`
|
|
192
|
+
sanitized = sanitized.replace(/`[^`]*`/g, ' ');
|
|
193
|
+
return sanitized;
|
|
194
|
+
}
|
|
195
|
+
export function applyRoutingInstructions(instructions, currentState) {
|
|
196
|
+
const newState = {
|
|
197
|
+
forcedTarget: currentState.forcedTarget ? { ...currentState.forcedTarget } : undefined,
|
|
198
|
+
stickyTarget: currentState.stickyTarget ? { ...currentState.stickyTarget } : undefined,
|
|
199
|
+
allowedProviders: new Set(currentState.allowedProviders),
|
|
200
|
+
disabledProviders: new Set(currentState.disabledProviders),
|
|
201
|
+
disabledKeys: new Map(Array.from(currentState.disabledKeys.entries()).map(([k, v]) => [k, new Set(v)])),
|
|
202
|
+
disabledModels: new Map(Array.from(currentState.disabledModels.entries()).map(([k, v]) => [k, new Set(v)]))
|
|
203
|
+
};
|
|
204
|
+
let allowReset = false;
|
|
205
|
+
let disableReset = false;
|
|
206
|
+
for (const instruction of instructions) {
|
|
207
|
+
switch (instruction.type) {
|
|
208
|
+
case 'force':
|
|
209
|
+
newState.forcedTarget = {
|
|
210
|
+
provider: instruction.provider,
|
|
211
|
+
keyAlias: instruction.keyAlias,
|
|
212
|
+
keyIndex: instruction.keyIndex,
|
|
213
|
+
model: instruction.model,
|
|
214
|
+
pathLength: instruction.pathLength
|
|
215
|
+
};
|
|
216
|
+
// 保留 stickyTarget,允许单次 force 覆盖但不清除持久 sticky
|
|
217
|
+
// newState.stickyTarget = undefined;
|
|
218
|
+
break;
|
|
219
|
+
case 'sticky':
|
|
220
|
+
newState.stickyTarget = {
|
|
221
|
+
provider: instruction.provider,
|
|
222
|
+
keyAlias: instruction.keyAlias,
|
|
223
|
+
keyIndex: instruction.keyIndex,
|
|
224
|
+
model: instruction.model,
|
|
225
|
+
pathLength: instruction.pathLength
|
|
226
|
+
};
|
|
227
|
+
newState.forcedTarget = undefined;
|
|
228
|
+
break;
|
|
229
|
+
case 'allow':
|
|
230
|
+
if (!allowReset) {
|
|
231
|
+
newState.allowedProviders.clear();
|
|
232
|
+
allowReset = true;
|
|
233
|
+
}
|
|
234
|
+
if (instruction.provider) {
|
|
235
|
+
newState.allowedProviders.add(instruction.provider);
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
case 'disable': {
|
|
239
|
+
if (!disableReset) {
|
|
240
|
+
newState.disabledProviders.clear();
|
|
241
|
+
newState.disabledKeys.clear();
|
|
242
|
+
newState.disabledModels.clear();
|
|
243
|
+
disableReset = true;
|
|
244
|
+
}
|
|
245
|
+
if (instruction.provider) {
|
|
246
|
+
const hasKeySpecifier = instruction.keyAlias || instruction.keyIndex !== undefined;
|
|
247
|
+
const hasModelSpecifier = typeof instruction.model === 'string' && instruction.model.length > 0;
|
|
248
|
+
if (hasKeySpecifier) {
|
|
249
|
+
if (!newState.disabledKeys.has(instruction.provider)) {
|
|
250
|
+
newState.disabledKeys.set(instruction.provider, new Set());
|
|
251
|
+
}
|
|
252
|
+
const keySet = newState.disabledKeys.get(instruction.provider);
|
|
253
|
+
if (instruction.keyAlias) {
|
|
254
|
+
keySet.add(instruction.keyAlias);
|
|
255
|
+
}
|
|
256
|
+
if (instruction.keyIndex !== undefined) {
|
|
257
|
+
keySet.add(instruction.keyIndex);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (hasModelSpecifier) {
|
|
261
|
+
if (!newState.disabledModels.has(instruction.provider)) {
|
|
262
|
+
newState.disabledModels.set(instruction.provider, new Set());
|
|
263
|
+
}
|
|
264
|
+
newState.disabledModels.get(instruction.provider).add(instruction.model);
|
|
265
|
+
}
|
|
266
|
+
if (!hasKeySpecifier && !hasModelSpecifier) {
|
|
267
|
+
newState.disabledProviders.add(instruction.provider);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case 'enable': {
|
|
273
|
+
if (instruction.provider) {
|
|
274
|
+
const hasKeySpecifier = instruction.keyAlias || instruction.keyIndex !== undefined;
|
|
275
|
+
const hasModelSpecifier = typeof instruction.model === 'string' && instruction.model.length > 0;
|
|
276
|
+
if (hasKeySpecifier) {
|
|
277
|
+
const keySet = newState.disabledKeys.get(instruction.provider);
|
|
278
|
+
if (keySet) {
|
|
279
|
+
if (instruction.keyAlias) {
|
|
280
|
+
keySet.delete(instruction.keyAlias);
|
|
281
|
+
}
|
|
282
|
+
if (instruction.keyIndex !== undefined) {
|
|
283
|
+
keySet.delete(instruction.keyIndex);
|
|
284
|
+
}
|
|
285
|
+
if (keySet.size === 0) {
|
|
286
|
+
newState.disabledKeys.delete(instruction.provider);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (hasModelSpecifier) {
|
|
291
|
+
const modelSet = newState.disabledModels.get(instruction.provider);
|
|
292
|
+
if (modelSet) {
|
|
293
|
+
modelSet.delete(instruction.model);
|
|
294
|
+
if (modelSet.size === 0) {
|
|
295
|
+
newState.disabledModels.delete(instruction.provider);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!hasKeySpecifier && !hasModelSpecifier) {
|
|
300
|
+
newState.disabledProviders.delete(instruction.provider);
|
|
301
|
+
newState.disabledKeys.delete(instruction.provider);
|
|
302
|
+
newState.disabledModels.delete(instruction.provider);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case 'clear':
|
|
308
|
+
newState.forcedTarget = undefined;
|
|
309
|
+
newState.stickyTarget = undefined;
|
|
310
|
+
newState.allowedProviders.clear();
|
|
311
|
+
newState.disabledProviders.clear();
|
|
312
|
+
newState.disabledKeys.clear();
|
|
313
|
+
newState.disabledModels.clear();
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return newState;
|
|
318
|
+
}
|
|
319
|
+
export function cleanMessagesFromRoutingInstructions(messages) {
|
|
320
|
+
return messages
|
|
321
|
+
.map((message) => {
|
|
322
|
+
if (message.role !== 'user' || typeof message.content !== 'string') {
|
|
323
|
+
return message;
|
|
324
|
+
}
|
|
325
|
+
const cleanedContent = message.content.replace(/<\*\*[^*]+\*\*>/g, '').trim();
|
|
326
|
+
return {
|
|
327
|
+
...message,
|
|
328
|
+
content: cleanedContent
|
|
329
|
+
};
|
|
330
|
+
})
|
|
331
|
+
.filter((message) => {
|
|
332
|
+
if (message.role !== 'user') {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
if (typeof message.content !== 'string') {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
return message.content.trim().length > 0;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
export function serializeRoutingInstructionState(state) {
|
|
342
|
+
return {
|
|
343
|
+
forcedTarget: state.forcedTarget,
|
|
344
|
+
stickyTarget: state.stickyTarget,
|
|
345
|
+
allowedProviders: Array.from(state.allowedProviders),
|
|
346
|
+
disabledProviders: Array.from(state.disabledProviders),
|
|
347
|
+
disabledKeys: Array.from(state.disabledKeys.entries()).map(([provider, keys]) => ({
|
|
348
|
+
provider,
|
|
349
|
+
keys: Array.from(keys)
|
|
350
|
+
})),
|
|
351
|
+
disabledModels: Array.from(state.disabledModels.entries()).map(([provider, models]) => ({
|
|
352
|
+
provider,
|
|
353
|
+
models: Array.from(models)
|
|
354
|
+
}))
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
export function deserializeRoutingInstructionState(data) {
|
|
358
|
+
const state = {
|
|
359
|
+
forcedTarget: undefined,
|
|
360
|
+
stickyTarget: undefined,
|
|
361
|
+
allowedProviders: new Set(),
|
|
362
|
+
disabledProviders: new Set(),
|
|
363
|
+
disabledKeys: new Map(),
|
|
364
|
+
disabledModels: new Map()
|
|
365
|
+
};
|
|
366
|
+
if (data.forcedTarget && typeof data.forcedTarget === 'object') {
|
|
367
|
+
state.forcedTarget = data.forcedTarget;
|
|
368
|
+
}
|
|
369
|
+
if (data.stickyTarget && typeof data.stickyTarget === 'object') {
|
|
370
|
+
state.stickyTarget = data.stickyTarget;
|
|
371
|
+
}
|
|
372
|
+
if (Array.isArray(data.allowedProviders)) {
|
|
373
|
+
state.allowedProviders = new Set(data.allowedProviders);
|
|
374
|
+
}
|
|
375
|
+
if (Array.isArray(data.disabledProviders)) {
|
|
376
|
+
state.disabledProviders = new Set(data.disabledProviders);
|
|
377
|
+
}
|
|
378
|
+
if (Array.isArray(data.disabledKeys)) {
|
|
379
|
+
for (const entry of data.disabledKeys) {
|
|
380
|
+
if (entry.provider && Array.isArray(entry.keys)) {
|
|
381
|
+
state.disabledKeys.set(entry.provider, new Set(entry.keys));
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (Array.isArray(data.disabledModels)) {
|
|
386
|
+
for (const entry of data.disabledModels) {
|
|
387
|
+
if (entry.provider && Array.isArray(entry.models)) {
|
|
388
|
+
state.disabledModels.set(entry.provider, new Set(entry.models));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return state;
|
|
393
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { RoutingInstructionState } from './routing-instructions.js';
|
|
2
|
+
export declare function loadRoutingInstructionStateSync(key: string | undefined): RoutingInstructionState | null;
|
|
3
|
+
export declare function saveRoutingInstructionStateAsync(key: string | undefined, state: RoutingInstructionState | null): void;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { serializeRoutingInstructionState, deserializeRoutingInstructionState } from './routing-instructions.js';
|
|
5
|
+
function isPersistentKey(key) {
|
|
6
|
+
if (!key)
|
|
7
|
+
return false;
|
|
8
|
+
return key.startsWith('session:') || key.startsWith('conversation:');
|
|
9
|
+
}
|
|
10
|
+
function resolveSessionDir() {
|
|
11
|
+
try {
|
|
12
|
+
const override = process.env.ROUTECODEX_SESSION_DIR;
|
|
13
|
+
if (override && override.trim()) {
|
|
14
|
+
return override.trim();
|
|
15
|
+
}
|
|
16
|
+
const home = os.homedir();
|
|
17
|
+
if (!home) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return path.join(home, '.routecodex', 'sessions');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function keyToFilename(key) {
|
|
27
|
+
if (!isPersistentKey(key)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const idx = key.indexOf(':');
|
|
31
|
+
if (idx <= 0 || idx === key.length - 1) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const scope = key.substring(0, idx); // "session" | "conversation"
|
|
35
|
+
const rawId = key.substring(idx + 1);
|
|
36
|
+
const safeId = rawId.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
37
|
+
return `${scope}-${safeId}.json`;
|
|
38
|
+
}
|
|
39
|
+
export function loadRoutingInstructionStateSync(key) {
|
|
40
|
+
if (!isPersistentKey(key)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const dir = resolveSessionDir();
|
|
44
|
+
const filename = keyToFilename(key);
|
|
45
|
+
if (!dir || !filename) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const filepath = path.join(dir, filename);
|
|
49
|
+
try {
|
|
50
|
+
if (!fs.existsSync(filepath)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const raw = fs.readFileSync(filepath, 'utf8');
|
|
54
|
+
if (!raw) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
const payload = parsed && typeof parsed.version === 'number'
|
|
59
|
+
? parsed.state
|
|
60
|
+
: parsed;
|
|
61
|
+
if (!payload || typeof payload !== 'object') {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return deserializeRoutingInstructionState(payload);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function saveRoutingInstructionStateAsync(key, state) {
|
|
71
|
+
if (!isPersistentKey(key)) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const dir = resolveSessionDir();
|
|
75
|
+
const filename = keyToFilename(key);
|
|
76
|
+
if (!dir || !filename) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const filepath = path.join(dir, filename);
|
|
80
|
+
// 空状态意味着清除持久化文件
|
|
81
|
+
if (!state) {
|
|
82
|
+
try {
|
|
83
|
+
fs.unlink(filepath, () => {
|
|
84
|
+
// ignore errors (e.g. ENOENT)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// ignore sync unlink failures
|
|
89
|
+
}
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const payload = {
|
|
93
|
+
version: 1,
|
|
94
|
+
state: serializeRoutingInstructionState(state)
|
|
95
|
+
};
|
|
96
|
+
try {
|
|
97
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// ignore mkdir errors; write below will fail silently
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
fs.writeFile(filepath, JSON.stringify(payload), { encoding: 'utf8' }, () => {
|
|
104
|
+
// ignore async write errors
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// ignore sync write failures
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -85,22 +85,6 @@ const SHELL_WRITE_PATTERNS = [
|
|
|
85
85
|
'go install',
|
|
86
86
|
'make install'
|
|
87
87
|
];
|
|
88
|
-
const SHELL_SEARCH_PATTERNS = [
|
|
89
|
-
'rg ',
|
|
90
|
-
'rg-',
|
|
91
|
-
'grep ',
|
|
92
|
-
'grep-',
|
|
93
|
-
'ripgrep',
|
|
94
|
-
'find ',
|
|
95
|
-
'fd ',
|
|
96
|
-
'locate ',
|
|
97
|
-
'search ',
|
|
98
|
-
'ack ',
|
|
99
|
-
'ag ',
|
|
100
|
-
'where ',
|
|
101
|
-
'which ',
|
|
102
|
-
'codesearch'
|
|
103
|
-
];
|
|
104
88
|
const SHELL_READ_PATTERNS = [
|
|
105
89
|
'ls',
|
|
106
90
|
'dir ',
|
|
@@ -362,9 +346,6 @@ function classifyShellCommand(command) {
|
|
|
362
346
|
if (segments.some((segment) => matchesAnyPattern(segment, SHELL_WRITE_PATTERNS))) {
|
|
363
347
|
return 'write';
|
|
364
348
|
}
|
|
365
|
-
if (segments.some((segment) => matchesAnyPattern(segment, SHELL_SEARCH_PATTERNS))) {
|
|
366
|
-
return 'search';
|
|
367
|
-
}
|
|
368
349
|
if (segments.some((segment) => matchesAnyPattern(segment, SHELL_READ_PATTERNS))) {
|
|
369
350
|
return 'read';
|
|
370
351
|
}
|
|
@@ -372,9 +353,6 @@ function classifyShellCommand(command) {
|
|
|
372
353
|
if (matchesAnyPattern(stripped, SHELL_WRITE_PATTERNS)) {
|
|
373
354
|
return 'write';
|
|
374
355
|
}
|
|
375
|
-
if (matchesAnyPattern(stripped, SHELL_SEARCH_PATTERNS)) {
|
|
376
|
-
return 'search';
|
|
377
|
-
}
|
|
378
356
|
if (matchesAnyPattern(stripped, SHELL_READ_PATTERNS)) {
|
|
379
357
|
return 'read';
|
|
380
358
|
}
|