@jsonstudio/llms 0.6.1739 → 0.6.1890

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 (107) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-request.d.ts +3 -0
  2. package/dist/conversion/compat/actions/deepseek-web-request.js +350 -0
  3. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +3 -0
  4. package/dist/conversion/compat/actions/deepseek-web-response.js +886 -0
  5. package/dist/conversion/compat/actions/gemini-cli-request.js +3 -1
  6. package/dist/conversion/compat/profiles/chat-deepseek-web.json +18 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +166 -2
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +169 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +6 -0
  10. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +12 -0
  11. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -0
  12. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +4 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.js +365 -144
  14. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +9 -0
  15. package/dist/conversion/hub/policy/policy-engine.d.ts +2 -0
  16. package/dist/conversion/hub/policy/policy-engine.js +8 -0
  17. package/dist/conversion/hub/process/chat-process.js +466 -16
  18. package/dist/conversion/hub/response/provider-response.js +0 -35
  19. package/dist/conversion/responses/responses-openai-bridge.d.ts +2 -0
  20. package/dist/conversion/responses/responses-openai-bridge.js +166 -8
  21. package/dist/conversion/shared/anthropic-message-utils.js +10 -1
  22. package/dist/conversion/shared/protocol-field-allowlists.d.ts +2 -2
  23. package/dist/conversion/shared/protocol-field-allowlists.js +4 -0
  24. package/dist/conversion/shared/tool-governor.js +102 -0
  25. package/dist/guidance/index.js +17 -0
  26. package/dist/router/virtual-router/bootstrap.js +46 -1
  27. package/dist/router/virtual-router/classifier.js +59 -4
  28. package/dist/router/virtual-router/engine/health/index.js +6 -6
  29. package/dist/router/virtual-router/engine/routing-state/store.js +16 -3
  30. package/dist/router/virtual-router/engine-logging.js +62 -24
  31. package/dist/router/virtual-router/engine-selection/route-utils.js +20 -20
  32. package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -2
  33. package/dist/router/virtual-router/engine.d.ts +3 -1
  34. package/dist/router/virtual-router/engine.js +359 -39
  35. package/dist/router/virtual-router/features.js +2 -1
  36. package/dist/router/virtual-router/pre-command-file-resolver.d.ts +2 -0
  37. package/dist/router/virtual-router/pre-command-file-resolver.js +90 -0
  38. package/dist/router/virtual-router/provider-registry.js +3 -1
  39. package/dist/router/virtual-router/routing-instructions.d.ts +15 -1
  40. package/dist/router/virtual-router/routing-instructions.js +110 -151
  41. package/dist/router/virtual-router/routing-pre-command-actions.d.ts +3 -0
  42. package/dist/router/virtual-router/routing-pre-command-actions.js +26 -0
  43. package/dist/router/virtual-router/routing-pre-command-parser.d.ts +2 -0
  44. package/dist/router/virtual-router/routing-pre-command-parser.js +85 -0
  45. package/dist/router/virtual-router/routing-pre-command-state-codec.d.ts +3 -0
  46. package/dist/router/virtual-router/routing-pre-command-state-codec.js +24 -0
  47. package/dist/router/virtual-router/routing-stop-message-actions.d.ts +2 -0
  48. package/dist/router/virtual-router/routing-stop-message-actions.js +96 -0
  49. package/dist/router/virtual-router/routing-stop-message-parser.d.ts +3 -0
  50. package/dist/router/virtual-router/routing-stop-message-parser.js +142 -0
  51. package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +4 -0
  52. package/dist/router/virtual-router/routing-stop-message-state-codec.js +85 -0
  53. package/dist/router/virtual-router/sticky-session-store.js +206 -57
  54. package/dist/router/virtual-router/stop-message-stage-template-files.d.ts +12 -0
  55. package/dist/router/virtual-router/stop-message-stage-template-files.js +67 -0
  56. package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
  57. package/dist/router/virtual-router/stop-message-state-sync.js +5 -0
  58. package/dist/router/virtual-router/token-file-scanner.d.ts +9 -0
  59. package/dist/router/virtual-router/token-file-scanner.js +64 -3
  60. package/dist/router/virtual-router/tool-signals.d.ts +5 -0
  61. package/dist/router/virtual-router/tool-signals.js +42 -3
  62. package/dist/router/virtual-router/types.d.ts +19 -1
  63. package/dist/router/virtual-router/types.js +1 -0
  64. package/dist/servertool/clock/config.d.ts +1 -1
  65. package/dist/servertool/clock/config.js +27 -4
  66. package/dist/servertool/clock/state.js +41 -2
  67. package/dist/servertool/clock/task-store.d.ts +2 -2
  68. package/dist/servertool/clock/task-store.js +1 -1
  69. package/dist/servertool/clock/tasks.d.ts +3 -1
  70. package/dist/servertool/clock/tasks.js +209 -18
  71. package/dist/servertool/clock/types.d.ts +17 -0
  72. package/dist/servertool/continue-execution/log.d.ts +3 -0
  73. package/dist/servertool/continue-execution/log.js +13 -0
  74. package/dist/servertool/engine.js +414 -68
  75. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +6 -6
  76. package/dist/servertool/handlers/clock-auto.js +54 -71
  77. package/dist/servertool/handlers/clock.js +121 -6
  78. package/dist/servertool/handlers/continue-execution.d.ts +1 -0
  79. package/dist/servertool/handlers/continue-execution.js +91 -0
  80. package/dist/servertool/handlers/followup-request-builder.js +13 -0
  81. package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -1
  82. package/dist/servertool/handlers/iflow-model-error-retry.js +1 -1
  83. package/dist/servertool/handlers/recursive-detection-guard.js +1 -1
  84. package/dist/servertool/handlers/stop-message-auto.js +386 -257
  85. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +43 -0
  86. package/dist/servertool/handlers/stop-message-stage-policy.js +684 -0
  87. package/dist/servertool/handlers/vision.js +1 -1
  88. package/dist/servertool/log/progress-file.d.ts +14 -0
  89. package/dist/servertool/log/progress-file.js +88 -0
  90. package/dist/servertool/pre-command-hooks.d.ts +17 -0
  91. package/dist/servertool/pre-command-hooks.js +491 -0
  92. package/dist/servertool/registry.d.ts +23 -6
  93. package/dist/servertool/registry.js +66 -1
  94. package/dist/servertool/server-side-tools.d.ts +1 -0
  95. package/dist/servertool/server-side-tools.js +216 -14
  96. package/dist/servertool/stop-gateway-context.d.ts +14 -0
  97. package/dist/servertool/stop-gateway-context.js +167 -0
  98. package/dist/servertool/stop-message-compare-context.d.ts +24 -0
  99. package/dist/servertool/stop-message-compare-context.js +133 -0
  100. package/dist/servertool/types.d.ts +12 -0
  101. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -0
  102. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +36 -1
  103. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +3 -0
  104. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +3 -0
  105. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +118 -1
  106. package/dist/tools/apply-patch/args-normalizer/default-actions.js +1 -1
  107. package/package.json +1 -1
@@ -0,0 +1,90 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import os from 'node:os';
4
+ function resolveRoutecodexUserDir() {
5
+ const override = process.env.ROUTECODEX_USER_DIR;
6
+ if (override && override.trim()) {
7
+ return override.trim();
8
+ }
9
+ const home = os.homedir();
10
+ if (!home) {
11
+ throw new Error('precommand: cannot resolve homedir');
12
+ }
13
+ return path.join(home, '.routecodex');
14
+ }
15
+ function resolvePreCommandBaseDir() {
16
+ return path.resolve(resolveRoutecodexUserDir(), 'precommand');
17
+ }
18
+ function normalizeRelativePath(raw) {
19
+ const normalized = path.posix.normalize(raw.replace(/\\/g, '/'));
20
+ if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../')) {
21
+ throw new Error('precommand: invalid relative path');
22
+ }
23
+ return normalized;
24
+ }
25
+ function normalizePreCommandRelativePath(raw, fromFileScheme) {
26
+ const normalized = normalizeRelativePath(raw);
27
+ if (normalized === 'precommand') {
28
+ throw new Error('precommand: expected script file under ~/.routecodex/precommand');
29
+ }
30
+ if (normalized.startsWith('precommand/')) {
31
+ return normalized.slice('precommand/'.length);
32
+ }
33
+ if (fromFileScheme) {
34
+ throw new Error('precommand file://: path must be under file://precommand/...');
35
+ }
36
+ return normalized;
37
+ }
38
+ export function resolvePreCommandScriptPath(raw) {
39
+ let text = typeof raw === 'string' ? raw.trim() : '';
40
+ if (!text) {
41
+ throw new Error('precommand: missing script path');
42
+ }
43
+ if (text.startsWith('<') && text.endsWith('>') && text.length >= 3) {
44
+ text = text.slice(1, -1).trim();
45
+ }
46
+ const fromFileScheme = /^file:\/\//i.test(text);
47
+ const relRaw = fromFileScheme ? text.slice('file://'.length).trim() : text;
48
+ if (!relRaw) {
49
+ throw new Error('precommand file://: missing relative path');
50
+ }
51
+ if (relRaw.startsWith('/') || relRaw.startsWith('\\') || /^[a-zA-Z]:[\\/]/.test(relRaw)) {
52
+ throw new Error('precommand: only supports paths relative to ~/.routecodex/precommand');
53
+ }
54
+ const relToPreCommand = normalizePreCommandRelativePath(relRaw, fromFileScheme);
55
+ const base = resolvePreCommandBaseDir();
56
+ const abs = path.resolve(base, relToPreCommand);
57
+ if (abs !== base && !abs.startsWith(`${base}${path.sep}`)) {
58
+ throw new Error('precommand: path escapes ~/.routecodex/precommand');
59
+ }
60
+ let stat;
61
+ try {
62
+ stat = fs.statSync(abs);
63
+ }
64
+ catch (err) {
65
+ const message = err && typeof err.message === 'string' ? err.message : String(err || 'unknown error');
66
+ throw new Error(`precommand: cannot stat ${abs}: ${message}`);
67
+ }
68
+ if (!stat.isFile()) {
69
+ throw new Error(`precommand: not a file: ${abs}`);
70
+ }
71
+ return abs;
72
+ }
73
+ export function isPreCommandScriptPathAllowed(rawPath) {
74
+ const scriptPath = typeof rawPath === 'string' ? rawPath.trim() : '';
75
+ if (!scriptPath) {
76
+ return false;
77
+ }
78
+ const base = resolvePreCommandBaseDir();
79
+ const abs = path.resolve(scriptPath);
80
+ if (abs !== base && !abs.startsWith(`${base}${path.sep}`)) {
81
+ return false;
82
+ }
83
+ try {
84
+ const stat = fs.statSync(abs);
85
+ return stat.isFile();
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
@@ -84,7 +84,8 @@ export class ProviderRegistry {
84
84
  processMode: profile.processMode || 'chat',
85
85
  responsesConfig: profile.responsesConfig,
86
86
  streaming: profile.streaming,
87
- maxContextTokens: profile.maxContextTokens
87
+ maxContextTokens: profile.maxContextTokens,
88
+ ...(profile.deepseek ? { deepseek: profile.deepseek } : {})
88
89
  };
89
90
  }
90
91
  static normalizeProfile(key, profile) {
@@ -103,6 +104,7 @@ export class ProviderRegistry {
103
104
  responsesConfig: profile.responsesConfig,
104
105
  streaming: profile.streaming,
105
106
  maxContextTokens: profile.maxContextTokens,
107
+ ...(profile.deepseek ? { deepseek: profile.deepseek } : {}),
106
108
  ...(profile.serverToolsDisabled ? { serverToolsDisabled: true } : {})
107
109
  };
108
110
  }
@@ -1,13 +1,16 @@
1
1
  import type { StandardizedMessage } from '../../conversion/hub/types/standardized.js';
2
2
  export interface RoutingInstruction {
3
- type: 'force' | 'sticky' | 'prefer' | 'disable' | 'enable' | 'clear' | 'allow' | 'stopMessageSet' | 'stopMessageClear';
3
+ type: 'force' | 'sticky' | 'prefer' | 'disable' | 'enable' | 'clear' | 'allow' | 'stopMessageSet' | 'stopMessageMode' | 'stopMessageClear' | 'preCommandSet' | 'preCommandClear';
4
4
  provider?: string;
5
5
  keyAlias?: string;
6
6
  keyIndex?: number;
7
7
  model?: string;
8
8
  pathLength?: number;
9
+ processMode?: 'chat' | 'passthrough';
9
10
  stopMessageText?: string;
10
11
  stopMessageMaxRepeats?: number;
12
+ stopMessageStageMode?: 'on' | 'off' | 'auto';
13
+ preCommandScriptPath?: string;
11
14
  }
12
15
  export interface RoutingInstructionState {
13
16
  forcedTarget?: {
@@ -16,6 +19,7 @@ export interface RoutingInstructionState {
16
19
  keyIndex?: number;
17
20
  model?: string;
18
21
  pathLength?: number;
22
+ processMode?: 'chat' | 'passthrough';
19
23
  };
20
24
  stickyTarget?: {
21
25
  provider?: string;
@@ -23,6 +27,7 @@ export interface RoutingInstructionState {
23
27
  keyIndex?: number;
24
28
  model?: string;
25
29
  pathLength?: number;
30
+ processMode?: 'chat' | 'passthrough';
26
31
  };
27
32
  preferTarget?: {
28
33
  provider?: string;
@@ -30,6 +35,7 @@ export interface RoutingInstructionState {
30
35
  keyIndex?: number;
31
36
  model?: string;
32
37
  pathLength?: number;
38
+ processMode?: 'chat' | 'passthrough';
33
39
  };
34
40
  allowedProviders: Set<string>;
35
41
  disabledProviders: Set<string>;
@@ -46,6 +52,14 @@ export interface RoutingInstructionState {
46
52
  stopMessageUsed?: number;
47
53
  stopMessageUpdatedAt?: number;
48
54
  stopMessageLastUsedAt?: number;
55
+ stopMessageStage?: string;
56
+ stopMessageStageMode?: 'on' | 'off' | 'auto';
57
+ stopMessageObservationHash?: string;
58
+ stopMessageObservationStableCount?: number;
59
+ stopMessageBdWorkState?: string;
60
+ preCommandSource?: string;
61
+ preCommandScriptPath?: string;
62
+ preCommandUpdatedAt?: number;
49
63
  }
50
64
  export declare function parseRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
51
65
  /**
@@ -1,5 +1,10 @@
1
1
  import { extractMessageText } from './message-utils.js';
2
- import { resolveStopMessageText } from './stop-message-file-resolver.js';
2
+ import { parseStopMessageInstruction } from './routing-stop-message-parser.js';
3
+ import { applyStopMessageInstructionToState } from './routing-stop-message-actions.js';
4
+ import { parsePreCommandInstruction } from './routing-pre-command-parser.js';
5
+ import { applyPreCommandInstructionToState, clearPreCommandState } from './routing-pre-command-actions.js';
6
+ import { deserializeStopMessageState, serializeStopMessageState } from './routing-stop-message-state-codec.js';
7
+ import { deserializePreCommandState, serializePreCommandState } from './routing-pre-command-state-codec.js';
3
8
  export function parseRoutingInstructions(messages) {
4
9
  const instructions = [];
5
10
  // 从最新一条携带路由指令标记(<** ... **>)的 user 消息中解析指令,
@@ -45,10 +50,8 @@ export function parseRoutingInstructions(messages) {
45
50
  for (const segment of segments) {
46
51
  const parsed = parseSingleInstruction(segment);
47
52
  if (parsed) {
48
- // stopMessage is a "command" and must only be set/cleared from the *latest* user message.
49
- // Otherwise, clients that resend full history (including a past "<**stopMessage:...**>" message)
50
- // would keep re-applying stopMessage after it has been consumed/cleared.
51
- if ((parsed.type === 'stopMessageSet' || parsed.type === 'stopMessageClear') &&
53
+ // preCommand 是强副作用命令(执行本地脚本),仅允许从 latest user message 生效。
54
+ if ((parsed.type === 'preCommandSet' || parsed.type === 'preCommandClear') &&
52
55
  lastUserIndex >= 0 &&
53
56
  sanitizedIndex >= 0 &&
54
57
  sanitizedIndex !== lastUserIndex) {
@@ -105,7 +108,7 @@ function expandInstructionSegments(instruction) {
105
108
  }
106
109
  // stopMessage 指令需要整体解析,不能按逗号拆分,否则类似
107
110
  // "<**stopMessage:\"继续\",3**>" 会被错误拆成 ["stopMessage:\"继续\"", "3"]。
108
- if (/^stopMessage\s*:/i.test(trimmed)) {
111
+ if (/^stopMessage\s*:/i.test(trimmed) || /^precommand(?:\s*:|$)/i.test(trimmed)) {
109
112
  return [trimmed];
110
113
  }
111
114
  const prefix = trimmed[0];
@@ -124,80 +127,75 @@ function splitInstructionTargets(content) {
124
127
  .map((segment) => segment.trim())
125
128
  .filter((segment) => segment.length > 0);
126
129
  }
130
+ function splitTargetAndProcessMode(rawTarget) {
131
+ const trimmed = typeof rawTarget === 'string' ? rawTarget.trim() : '';
132
+ if (!trimmed) {
133
+ return { target: '' };
134
+ }
135
+ const separatorIndex = trimmed.lastIndexOf(':');
136
+ if (separatorIndex <= 0 || separatorIndex === trimmed.length - 1) {
137
+ return { target: trimmed };
138
+ }
139
+ const target = trimmed.slice(0, separatorIndex).trim();
140
+ const modeToken = trimmed.slice(separatorIndex + 1).trim().toLowerCase();
141
+ if (!target) {
142
+ return { target: trimmed };
143
+ }
144
+ if (modeToken === 'passthrough') {
145
+ return { target, processMode: 'passthrough' };
146
+ }
147
+ if (modeToken === 'chat') {
148
+ return { target, processMode: 'chat' };
149
+ }
150
+ return { target };
151
+ }
152
+ function parseNamedTargetInstruction(instruction, prefix) {
153
+ const re = new RegExp('^' + prefix + '\\s*:', 'i');
154
+ if (!re.test(instruction)) {
155
+ return null;
156
+ }
157
+ const body = instruction.slice(instruction.indexOf(':') + 1).trim();
158
+ if (!body) {
159
+ return null;
160
+ }
161
+ const { target, processMode } = splitTargetAndProcessMode(body);
162
+ if (!target) {
163
+ return null;
164
+ }
165
+ const parsed = parseTarget(target);
166
+ if (!parsed) {
167
+ return null;
168
+ }
169
+ const normalized = normalizeStickyOrForceTarget(parsed);
170
+ return { type: prefix, ...normalized, ...(processMode ? { processMode } : {}) };
171
+ }
127
172
  function parseSingleInstruction(instruction) {
128
173
  if (instruction === 'clear') {
129
174
  return { type: 'clear' };
130
175
  }
131
- if (/^stopMessage\s*:/i.test(instruction)) {
132
- const body = instruction.slice('stopMessage'.length + 1).trim();
133
- if (!body) {
134
- return null;
135
- }
136
- if (/^clear$/i.test(body)) {
137
- return { type: 'stopMessageClear' };
138
- }
139
- let text = '';
140
- let maxRepeats = 1;
141
- let cursor = body;
142
- if (cursor[0] === '"') {
143
- let escaped = false;
144
- let endIndex = -1;
145
- for (let i = 1; i < cursor.length; i += 1) {
146
- const ch = cursor[i];
147
- if (escaped) {
148
- escaped = false;
149
- continue;
150
- }
151
- if (ch === '\\') {
152
- escaped = true;
153
- continue;
154
- }
155
- if (ch === '"') {
156
- endIndex = i;
157
- break;
158
- }
159
- }
160
- if (endIndex <= 0) {
161
- return null;
162
- }
163
- const rawText = cursor.slice(1, endIndex);
164
- text = rawText.replace(/\\"/g, '"');
165
- cursor = cursor.slice(endIndex + 1).trim();
166
- if (cursor.startsWith(',')) {
167
- const countRaw = cursor.slice(1).trim();
168
- if (countRaw) {
169
- const parsed = Number.parseInt(countRaw, 10);
170
- if (!Number.isNaN(parsed) && parsed > 0) {
171
- maxRepeats = parsed;
172
- }
173
- }
174
- }
175
- }
176
- else {
177
- // 支持无引号的简单形式:stopMessage:继续,3
178
- const parts = cursor.split(',').map((part) => part.trim()).filter(Boolean);
179
- if (!parts.length) {
180
- return null;
181
- }
182
- text = parts[0];
183
- if (parts.length > 1) {
184
- const parsed = Number.parseInt(parts[1], 10);
185
- if (!Number.isNaN(parsed) && parsed > 0) {
186
- maxRepeats = parsed;
187
- }
188
- }
189
- }
190
- if (!text) {
191
- return null;
192
- }
193
- return {
194
- type: 'stopMessageSet',
195
- stopMessageText: resolveStopMessageText(text),
196
- stopMessageMaxRepeats: maxRepeats
197
- };
176
+ const preCommandInstruction = parsePreCommandInstruction(instruction);
177
+ if (preCommandInstruction) {
178
+ return preCommandInstruction;
179
+ }
180
+ const stopMessageInstruction = parseStopMessageInstruction(instruction);
181
+ if (stopMessageInstruction) {
182
+ return stopMessageInstruction;
183
+ }
184
+ const stickyInstruction = parseNamedTargetInstruction(instruction, 'sticky');
185
+ if (stickyInstruction) {
186
+ return stickyInstruction;
187
+ }
188
+ const forceInstruction = parseNamedTargetInstruction(instruction, 'force');
189
+ if (forceInstruction) {
190
+ return forceInstruction;
191
+ }
192
+ const preferInstruction = parseNamedTargetInstruction(instruction, 'prefer');
193
+ if (preferInstruction) {
194
+ return preferInstruction;
198
195
  }
199
196
  if (instruction.startsWith('!')) {
200
- const target = instruction.substring(1).trim();
197
+ const rawTarget = instruction.substring(1).trim();
198
+ const { target, processMode } = splitTargetAndProcessMode(rawTarget);
201
199
  if (!target) {
202
200
  return null;
203
201
  }
@@ -218,7 +216,7 @@ function parseSingleInstruction(instruction) {
218
216
  return null;
219
217
  }
220
218
  const normalized = normalizeStickyOrForceTarget(parsed);
221
- return { type: 'prefer', ...normalized };
219
+ return { type: 'prefer', ...normalized, ...(processMode ? { processMode } : {}) };
222
220
  }
223
221
  else if (instruction.startsWith('#')) {
224
222
  const target = instruction.substring(1).trim();
@@ -362,11 +360,20 @@ export function applyRoutingInstructions(instructions, currentState) {
362
360
  disabledProviders: new Set(currentState.disabledProviders),
363
361
  disabledKeys: new Map(Array.from(currentState.disabledKeys.entries()).map(([k, v]) => [k, new Set(v)])),
364
362
  disabledModels: new Map(Array.from(currentState.disabledModels.entries()).map(([k, v]) => [k, new Set(v)])),
363
+ stopMessageSource: currentState.stopMessageSource,
365
364
  stopMessageText: currentState.stopMessageText,
366
365
  stopMessageMaxRepeats: currentState.stopMessageMaxRepeats,
367
366
  stopMessageUsed: currentState.stopMessageUsed,
368
367
  stopMessageUpdatedAt: currentState.stopMessageUpdatedAt,
369
- stopMessageLastUsedAt: currentState.stopMessageLastUsedAt
368
+ stopMessageLastUsedAt: currentState.stopMessageLastUsedAt,
369
+ stopMessageStage: currentState.stopMessageStage,
370
+ stopMessageStageMode: currentState.stopMessageStageMode,
371
+ stopMessageObservationHash: currentState.stopMessageObservationHash,
372
+ stopMessageObservationStableCount: currentState.stopMessageObservationStableCount,
373
+ stopMessageBdWorkState: currentState.stopMessageBdWorkState,
374
+ preCommandSource: currentState.preCommandSource,
375
+ preCommandScriptPath: currentState.preCommandScriptPath,
376
+ preCommandUpdatedAt: currentState.preCommandUpdatedAt
370
377
  };
371
378
  let allowReset = false;
372
379
  let disableReset = false;
@@ -378,7 +385,8 @@ export function applyRoutingInstructions(instructions, currentState) {
378
385
  keyAlias: instruction.keyAlias,
379
386
  keyIndex: instruction.keyIndex,
380
387
  model: instruction.model,
381
- pathLength: instruction.pathLength
388
+ pathLength: instruction.pathLength,
389
+ processMode: instruction.processMode
382
390
  };
383
391
  // 保留 stickyTarget,允许单次 force 覆盖但不清除持久 sticky
384
392
  // newState.stickyTarget = undefined;
@@ -389,7 +397,8 @@ export function applyRoutingInstructions(instructions, currentState) {
389
397
  keyAlias: instruction.keyAlias,
390
398
  keyIndex: instruction.keyIndex,
391
399
  model: instruction.model,
392
- pathLength: instruction.pathLength
400
+ pathLength: instruction.pathLength,
401
+ processMode: instruction.processMode
393
402
  };
394
403
  newState.forcedTarget = undefined;
395
404
  break;
@@ -399,7 +408,8 @@ export function applyRoutingInstructions(instructions, currentState) {
399
408
  keyAlias: instruction.keyAlias,
400
409
  keyIndex: instruction.keyIndex,
401
410
  model: instruction.model,
402
- pathLength: instruction.pathLength
411
+ pathLength: instruction.pathLength,
412
+ processMode: instruction.processMode
403
413
  };
404
414
  newState.forcedTarget = undefined;
405
415
  newState.stickyTarget = undefined;
@@ -490,43 +500,16 @@ export function applyRoutingInstructions(instructions, currentState) {
490
500
  newState.disabledProviders.clear();
491
501
  newState.disabledKeys.clear();
492
502
  newState.disabledModels.clear();
503
+ clearPreCommandState(newState);
493
504
  break;
494
- case 'stopMessageSet': {
495
- const text = typeof instruction.stopMessageText === 'string' && instruction.stopMessageText.trim()
496
- ? instruction.stopMessageText.trim()
497
- : '';
498
- const maxRepeats = typeof instruction.stopMessageMaxRepeats === 'number' && Number.isFinite(instruction.stopMessageMaxRepeats)
499
- ? Math.floor(instruction.stopMessageMaxRepeats)
500
- : 0;
501
- if (text && maxRepeats > 0) {
502
- const sameText = typeof newState.stopMessageText === 'string' &&
503
- newState.stopMessageText.trim() === text;
504
- const sameMax = typeof newState.stopMessageMaxRepeats === 'number' &&
505
- Math.floor(newState.stopMessageMaxRepeats) === maxRepeats;
506
- const isSameInstruction = sameText && sameMax;
507
- const used = typeof newState.stopMessageUsed === 'number' && Number.isFinite(newState.stopMessageUsed)
508
- ? Math.max(0, Math.floor(newState.stopMessageUsed))
509
- : 0;
510
- const hasLastUsedAt = typeof newState.stopMessageLastUsedAt === 'number' && Number.isFinite(newState.stopMessageLastUsedAt);
511
- const shouldRearm = !isSameInstruction || used > 0 || hasLastUsedAt;
512
- newState.stopMessageText = text;
513
- newState.stopMessageMaxRepeats = maxRepeats;
514
- newState.stopMessageSource = 'explicit';
515
- if (shouldRearm) {
516
- newState.stopMessageUsed = 0;
517
- newState.stopMessageUpdatedAt = Date.now();
518
- newState.stopMessageLastUsedAt = undefined;
519
- }
520
- }
521
- break;
522
- }
505
+ case 'stopMessageSet':
506
+ case 'stopMessageMode':
523
507
  case 'stopMessageClear':
524
- newState.stopMessageText = undefined;
525
- newState.stopMessageMaxRepeats = undefined;
526
- newState.stopMessageUsed = undefined;
527
- newState.stopMessageSource = undefined;
528
- newState.stopMessageUpdatedAt = undefined;
529
- newState.stopMessageLastUsedAt = undefined;
508
+ applyStopMessageInstructionToState(instruction, newState);
509
+ break;
510
+ case 'preCommandSet':
511
+ case 'preCommandClear':
512
+ applyPreCommandInstructionToState(instruction, newState);
530
513
  break;
531
514
  }
532
515
  }
@@ -569,24 +552,8 @@ export function serializeRoutingInstructionState(state) {
569
552
  provider,
570
553
  models: Array.from(models)
571
554
  })),
572
- ...(typeof state.stopMessageSource === 'string' && state.stopMessageSource.trim()
573
- ? { stopMessageSource: state.stopMessageSource }
574
- : {}),
575
- ...(typeof state.stopMessageText === 'string' && state.stopMessageText.trim()
576
- ? { stopMessageText: state.stopMessageText }
577
- : {}),
578
- ...(typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
579
- ? { stopMessageMaxRepeats: state.stopMessageMaxRepeats }
580
- : {}),
581
- ...(typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
582
- ? { stopMessageUsed: state.stopMessageUsed }
583
- : {}),
584
- ...(typeof state.stopMessageUpdatedAt === 'number' && Number.isFinite(state.stopMessageUpdatedAt)
585
- ? { stopMessageUpdatedAt: state.stopMessageUpdatedAt }
586
- : {}),
587
- ...(typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)
588
- ? { stopMessageLastUsedAt: state.stopMessageLastUsedAt }
589
- : {})
555
+ ...serializeStopMessageState(state),
556
+ ...serializePreCommandState(state)
590
557
  };
591
558
  }
592
559
  export function deserializeRoutingInstructionState(data) {
@@ -599,8 +566,16 @@ export function deserializeRoutingInstructionState(data) {
599
566
  disabledKeys: new Map(),
600
567
  disabledModels: new Map(),
601
568
  stopMessageText: undefined,
569
+ stopMessageSource: undefined,
602
570
  stopMessageMaxRepeats: undefined,
603
- stopMessageUsed: undefined
571
+ stopMessageUsed: undefined,
572
+ stopMessageStage: undefined,
573
+ stopMessageStageMode: undefined,
574
+ stopMessageObservationHash: undefined,
575
+ stopMessageBdWorkState: undefined,
576
+ preCommandSource: undefined,
577
+ preCommandScriptPath: undefined,
578
+ preCommandUpdatedAt: undefined
604
579
  };
605
580
  if (data.forcedTarget && typeof data.forcedTarget === 'object') {
606
581
  state.forcedTarget = data.forcedTarget;
@@ -631,23 +606,7 @@ export function deserializeRoutingInstructionState(data) {
631
606
  }
632
607
  }
633
608
  }
634
- if (typeof data.stopMessageSource === 'string' && data.stopMessageSource.trim()) {
635
- state.stopMessageSource = data.stopMessageSource.trim();
636
- }
637
- if (typeof data.stopMessageText === 'string' && data.stopMessageText.trim()) {
638
- state.stopMessageText = data.stopMessageText;
639
- }
640
- if (typeof data.stopMessageMaxRepeats === 'number' && Number.isFinite(data.stopMessageMaxRepeats)) {
641
- state.stopMessageMaxRepeats = Math.floor(data.stopMessageMaxRepeats);
642
- }
643
- if (typeof data.stopMessageUsed === 'number' && Number.isFinite(data.stopMessageUsed)) {
644
- state.stopMessageUsed = Math.max(0, Math.floor(data.stopMessageUsed));
645
- }
646
- if (typeof data.stopMessageUpdatedAt === 'number' && Number.isFinite(data.stopMessageUpdatedAt)) {
647
- state.stopMessageUpdatedAt = data.stopMessageUpdatedAt;
648
- }
649
- if (typeof data.stopMessageLastUsedAt === 'number' && Number.isFinite(data.stopMessageLastUsedAt)) {
650
- state.stopMessageLastUsedAt = data.stopMessageLastUsedAt;
651
- }
609
+ deserializeStopMessageState(data, state);
610
+ deserializePreCommandState(data, state);
652
611
  return state;
653
612
  }
@@ -0,0 +1,3 @@
1
+ import type { RoutingInstruction, RoutingInstructionState } from './routing-instructions.js';
2
+ export declare function applyPreCommandInstructionToState(instruction: RoutingInstruction, state: RoutingInstructionState): boolean;
3
+ export declare function clearPreCommandState(state: RoutingInstructionState): void;
@@ -0,0 +1,26 @@
1
+ export function applyPreCommandInstructionToState(instruction, state) {
2
+ switch (instruction.type) {
3
+ case 'preCommandSet': {
4
+ const scriptPath = typeof instruction.preCommandScriptPath === 'string' && instruction.preCommandScriptPath.trim()
5
+ ? instruction.preCommandScriptPath.trim()
6
+ : '';
7
+ if (!scriptPath) {
8
+ return true;
9
+ }
10
+ state.preCommandScriptPath = scriptPath;
11
+ state.preCommandSource = 'explicit';
12
+ state.preCommandUpdatedAt = Date.now();
13
+ return true;
14
+ }
15
+ case 'preCommandClear':
16
+ clearPreCommandState(state);
17
+ return true;
18
+ default:
19
+ return false;
20
+ }
21
+ }
22
+ export function clearPreCommandState(state) {
23
+ state.preCommandScriptPath = undefined;
24
+ state.preCommandSource = undefined;
25
+ state.preCommandUpdatedAt = undefined;
26
+ }
@@ -0,0 +1,2 @@
1
+ import type { RoutingInstruction } from './routing-instructions.js';
2
+ export declare function parsePreCommandInstruction(instruction: string): RoutingInstruction | null;
@@ -0,0 +1,85 @@
1
+ import { resolvePreCommandScriptPath } from './pre-command-file-resolver.js';
2
+ const DEFAULT_PRECOMMAND_SCRIPT = 'default.sh';
3
+ export function parsePreCommandInstruction(instruction) {
4
+ const trimmed = typeof instruction === 'string' ? instruction.trim() : '';
5
+ if (!trimmed) {
6
+ return null;
7
+ }
8
+ if (/^precommand$/i.test(trimmed)) {
9
+ return {
10
+ type: 'preCommandSet',
11
+ preCommandScriptPath: resolvePreCommandScriptPath(resolveDefaultScriptRef())
12
+ };
13
+ }
14
+ if (!/^precommand\s*:/i.test(trimmed)) {
15
+ return null;
16
+ }
17
+ const body = trimmed.slice('precommand'.length + 1).trim();
18
+ if (!body) {
19
+ return null;
20
+ }
21
+ const parsedValue = readPreCommandToken(body);
22
+ if (!parsedValue) {
23
+ return null;
24
+ }
25
+ const normalized = parsedValue.trim();
26
+ if (!normalized) {
27
+ return null;
28
+ }
29
+ if (/^(?:clear|off|none)$/i.test(normalized)) {
30
+ return { type: 'preCommandClear' };
31
+ }
32
+ if (/^on$/i.test(normalized)) {
33
+ return {
34
+ type: 'preCommandSet',
35
+ preCommandScriptPath: resolvePreCommandScriptPath(resolveDefaultScriptRef())
36
+ };
37
+ }
38
+ return {
39
+ type: 'preCommandSet',
40
+ preCommandScriptPath: resolvePreCommandScriptPath(normalized)
41
+ };
42
+ }
43
+ function resolveDefaultScriptRef() {
44
+ const configured = process.env.ROUTECODEX_PRECOMMAND_DEFAULT_SCRIPT;
45
+ if (typeof configured === 'string' && configured.trim()) {
46
+ return configured.trim();
47
+ }
48
+ return DEFAULT_PRECOMMAND_SCRIPT;
49
+ }
50
+ function readPreCommandToken(body) {
51
+ if (!body) {
52
+ return null;
53
+ }
54
+ const first = body[0];
55
+ if (first === '"' || first === "'") {
56
+ const end = findClosingQuote(body, first);
57
+ if (end <= 0) {
58
+ return null;
59
+ }
60
+ return body.slice(1, end).replace(/\\"/g, '"').replace(/\\'/g, "'");
61
+ }
62
+ const comma = body.indexOf(',');
63
+ if (comma >= 0) {
64
+ return body.slice(0, comma).trim();
65
+ }
66
+ return body.trim();
67
+ }
68
+ function findClosingQuote(text, quote) {
69
+ let escaped = false;
70
+ for (let idx = 1; idx < text.length; idx += 1) {
71
+ const ch = text[idx];
72
+ if (escaped) {
73
+ escaped = false;
74
+ continue;
75
+ }
76
+ if (ch === '\\') {
77
+ escaped = true;
78
+ continue;
79
+ }
80
+ if (ch === quote) {
81
+ return idx;
82
+ }
83
+ }
84
+ return -1;
85
+ }
@@ -0,0 +1,3 @@
1
+ import type { RoutingInstructionState } from './routing-instructions.js';
2
+ export declare function serializePreCommandState(state: RoutingInstructionState): Record<string, unknown>;
3
+ export declare function deserializePreCommandState(data: Record<string, unknown>, state: RoutingInstructionState): void;