@jsonstudio/llms 0.6.1749 → 0.6.1892

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 +325 -38
  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 +11 -1
  40. package/dist/router/virtual-router/routing-instructions.js +101 -183
  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 +1 -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 +15 -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 +352 -257
  85. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +22 -1
  86. package/dist/servertool/handlers/stop-message-stage-policy.js +472 -60
  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>;
@@ -47,9 +53,13 @@ export interface RoutingInstructionState {
47
53
  stopMessageUpdatedAt?: number;
48
54
  stopMessageLastUsedAt?: number;
49
55
  stopMessageStage?: string;
56
+ stopMessageStageMode?: 'on' | 'off' | 'auto';
50
57
  stopMessageObservationHash?: string;
51
58
  stopMessageObservationStableCount?: number;
52
59
  stopMessageBdWorkState?: string;
60
+ preCommandSource?: string;
61
+ preCommandScriptPath?: string;
62
+ preCommandUpdatedAt?: number;
53
63
  }
54
64
  export declare function parseRoutingInstructions(messages: StandardizedMessage[]): RoutingInstruction[];
55
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();
@@ -369,9 +367,13 @@ export function applyRoutingInstructions(instructions, currentState) {
369
367
  stopMessageUpdatedAt: currentState.stopMessageUpdatedAt,
370
368
  stopMessageLastUsedAt: currentState.stopMessageLastUsedAt,
371
369
  stopMessageStage: currentState.stopMessageStage,
370
+ stopMessageStageMode: currentState.stopMessageStageMode,
372
371
  stopMessageObservationHash: currentState.stopMessageObservationHash,
373
372
  stopMessageObservationStableCount: currentState.stopMessageObservationStableCount,
374
- stopMessageBdWorkState: currentState.stopMessageBdWorkState
373
+ stopMessageBdWorkState: currentState.stopMessageBdWorkState,
374
+ preCommandSource: currentState.preCommandSource,
375
+ preCommandScriptPath: currentState.preCommandScriptPath,
376
+ preCommandUpdatedAt: currentState.preCommandUpdatedAt
375
377
  };
376
378
  let allowReset = false;
377
379
  let disableReset = false;
@@ -383,7 +385,8 @@ export function applyRoutingInstructions(instructions, currentState) {
383
385
  keyAlias: instruction.keyAlias,
384
386
  keyIndex: instruction.keyIndex,
385
387
  model: instruction.model,
386
- pathLength: instruction.pathLength
388
+ pathLength: instruction.pathLength,
389
+ processMode: instruction.processMode
387
390
  };
388
391
  // 保留 stickyTarget,允许单次 force 覆盖但不清除持久 sticky
389
392
  // newState.stickyTarget = undefined;
@@ -394,7 +397,8 @@ export function applyRoutingInstructions(instructions, currentState) {
394
397
  keyAlias: instruction.keyAlias,
395
398
  keyIndex: instruction.keyIndex,
396
399
  model: instruction.model,
397
- pathLength: instruction.pathLength
400
+ pathLength: instruction.pathLength,
401
+ processMode: instruction.processMode
398
402
  };
399
403
  newState.forcedTarget = undefined;
400
404
  break;
@@ -404,7 +408,8 @@ export function applyRoutingInstructions(instructions, currentState) {
404
408
  keyAlias: instruction.keyAlias,
405
409
  keyIndex: instruction.keyIndex,
406
410
  model: instruction.model,
407
- pathLength: instruction.pathLength
411
+ pathLength: instruction.pathLength,
412
+ processMode: instruction.processMode
408
413
  };
409
414
  newState.forcedTarget = undefined;
410
415
  newState.stickyTarget = undefined;
@@ -495,51 +500,16 @@ export function applyRoutingInstructions(instructions, currentState) {
495
500
  newState.disabledProviders.clear();
496
501
  newState.disabledKeys.clear();
497
502
  newState.disabledModels.clear();
503
+ clearPreCommandState(newState);
498
504
  break;
499
- case 'stopMessageSet': {
500
- const text = typeof instruction.stopMessageText === 'string' && instruction.stopMessageText.trim()
501
- ? instruction.stopMessageText.trim()
502
- : '';
503
- const maxRepeats = typeof instruction.stopMessageMaxRepeats === 'number' && Number.isFinite(instruction.stopMessageMaxRepeats)
504
- ? Math.floor(instruction.stopMessageMaxRepeats)
505
- : 0;
506
- if (text && maxRepeats > 0) {
507
- const sameText = typeof newState.stopMessageText === 'string' &&
508
- newState.stopMessageText.trim() === text;
509
- const sameMax = typeof newState.stopMessageMaxRepeats === 'number' &&
510
- Math.floor(newState.stopMessageMaxRepeats) === maxRepeats;
511
- const isSameInstruction = sameText && sameMax;
512
- const used = typeof newState.stopMessageUsed === 'number' && Number.isFinite(newState.stopMessageUsed)
513
- ? Math.max(0, Math.floor(newState.stopMessageUsed))
514
- : 0;
515
- const hasLastUsedAt = typeof newState.stopMessageLastUsedAt === 'number' && Number.isFinite(newState.stopMessageLastUsedAt);
516
- const shouldRearm = !isSameInstruction || used > 0 || hasLastUsedAt;
517
- newState.stopMessageText = text;
518
- newState.stopMessageMaxRepeats = maxRepeats;
519
- newState.stopMessageSource = 'explicit';
520
- if (shouldRearm) {
521
- newState.stopMessageUsed = 0;
522
- newState.stopMessageUpdatedAt = Date.now();
523
- newState.stopMessageLastUsedAt = undefined;
524
- newState.stopMessageStage = undefined;
525
- newState.stopMessageObservationHash = undefined;
526
- newState.stopMessageObservationStableCount = 0;
527
- newState.stopMessageBdWorkState = undefined;
528
- }
529
- }
530
- break;
531
- }
505
+ case 'stopMessageSet':
506
+ case 'stopMessageMode':
532
507
  case 'stopMessageClear':
533
- newState.stopMessageText = undefined;
534
- newState.stopMessageMaxRepeats = undefined;
535
- newState.stopMessageUsed = undefined;
536
- newState.stopMessageSource = undefined;
537
- newState.stopMessageUpdatedAt = undefined;
538
- newState.stopMessageLastUsedAt = undefined;
539
- newState.stopMessageStage = undefined;
540
- newState.stopMessageObservationHash = undefined;
541
- newState.stopMessageObservationStableCount = undefined;
542
- newState.stopMessageBdWorkState = undefined;
508
+ applyStopMessageInstructionToState(instruction, newState);
509
+ break;
510
+ case 'preCommandSet':
511
+ case 'preCommandClear':
512
+ applyPreCommandInstructionToState(instruction, newState);
543
513
  break;
544
514
  }
545
515
  }
@@ -582,36 +552,8 @@ export function serializeRoutingInstructionState(state) {
582
552
  provider,
583
553
  models: Array.from(models)
584
554
  })),
585
- ...(typeof state.stopMessageSource === 'string' && state.stopMessageSource.trim()
586
- ? { stopMessageSource: state.stopMessageSource }
587
- : {}),
588
- ...(typeof state.stopMessageText === 'string' && state.stopMessageText.trim()
589
- ? { stopMessageText: state.stopMessageText }
590
- : {}),
591
- ...(typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
592
- ? { stopMessageMaxRepeats: state.stopMessageMaxRepeats }
593
- : {}),
594
- ...(typeof state.stopMessageUsed === 'number' && Number.isFinite(state.stopMessageUsed)
595
- ? { stopMessageUsed: state.stopMessageUsed }
596
- : {}),
597
- ...(typeof state.stopMessageUpdatedAt === 'number' && Number.isFinite(state.stopMessageUpdatedAt)
598
- ? { stopMessageUpdatedAt: state.stopMessageUpdatedAt }
599
- : {}),
600
- ...(typeof state.stopMessageLastUsedAt === 'number' && Number.isFinite(state.stopMessageLastUsedAt)
601
- ? { stopMessageLastUsedAt: state.stopMessageLastUsedAt }
602
- : {}),
603
- ...(typeof state.stopMessageStage === 'string' && state.stopMessageStage.trim()
604
- ? { stopMessageStage: state.stopMessageStage.trim() }
605
- : {}),
606
- ...(typeof state.stopMessageObservationHash === 'string' && state.stopMessageObservationHash.trim()
607
- ? { stopMessageObservationHash: state.stopMessageObservationHash.trim() }
608
- : {}),
609
- ...(typeof state.stopMessageObservationStableCount === 'number' && Number.isFinite(state.stopMessageObservationStableCount)
610
- ? { stopMessageObservationStableCount: Math.max(0, Math.floor(state.stopMessageObservationStableCount)) }
611
- : {}),
612
- ...(typeof state.stopMessageBdWorkState === 'string' && state.stopMessageBdWorkState.trim()
613
- ? { stopMessageBdWorkState: state.stopMessageBdWorkState.trim() }
614
- : {})
555
+ ...serializeStopMessageState(state),
556
+ ...serializePreCommandState(state)
615
557
  };
616
558
  }
617
559
  export function deserializeRoutingInstructionState(data) {
@@ -628,8 +570,12 @@ export function deserializeRoutingInstructionState(data) {
628
570
  stopMessageMaxRepeats: undefined,
629
571
  stopMessageUsed: undefined,
630
572
  stopMessageStage: undefined,
573
+ stopMessageStageMode: undefined,
631
574
  stopMessageObservationHash: undefined,
632
- stopMessageBdWorkState: undefined
575
+ stopMessageBdWorkState: undefined,
576
+ preCommandSource: undefined,
577
+ preCommandScriptPath: undefined,
578
+ preCommandUpdatedAt: undefined
633
579
  };
634
580
  if (data.forcedTarget && typeof data.forcedTarget === 'object') {
635
581
  state.forcedTarget = data.forcedTarget;
@@ -660,35 +606,7 @@ export function deserializeRoutingInstructionState(data) {
660
606
  }
661
607
  }
662
608
  }
663
- if (typeof data.stopMessageSource === 'string' && data.stopMessageSource.trim()) {
664
- state.stopMessageSource = data.stopMessageSource.trim();
665
- }
666
- if (typeof data.stopMessageText === 'string' && data.stopMessageText.trim()) {
667
- state.stopMessageText = data.stopMessageText;
668
- }
669
- if (typeof data.stopMessageMaxRepeats === 'number' && Number.isFinite(data.stopMessageMaxRepeats)) {
670
- state.stopMessageMaxRepeats = Math.floor(data.stopMessageMaxRepeats);
671
- }
672
- if (typeof data.stopMessageUsed === 'number' && Number.isFinite(data.stopMessageUsed)) {
673
- state.stopMessageUsed = Math.max(0, Math.floor(data.stopMessageUsed));
674
- }
675
- if (typeof data.stopMessageUpdatedAt === 'number' && Number.isFinite(data.stopMessageUpdatedAt)) {
676
- state.stopMessageUpdatedAt = data.stopMessageUpdatedAt;
677
- }
678
- if (typeof data.stopMessageLastUsedAt === 'number' && Number.isFinite(data.stopMessageLastUsedAt)) {
679
- state.stopMessageLastUsedAt = data.stopMessageLastUsedAt;
680
- }
681
- if (typeof data.stopMessageStage === 'string' && data.stopMessageStage.trim()) {
682
- state.stopMessageStage = data.stopMessageStage.trim();
683
- }
684
- if (typeof data.stopMessageObservationHash === 'string' && data.stopMessageObservationHash.trim()) {
685
- state.stopMessageObservationHash = data.stopMessageObservationHash.trim();
686
- }
687
- if (typeof data.stopMessageObservationStableCount === 'number' && Number.isFinite(data.stopMessageObservationStableCount)) {
688
- state.stopMessageObservationStableCount = Math.max(0, Math.floor(data.stopMessageObservationStableCount));
689
- }
690
- if (typeof data.stopMessageBdWorkState === 'string' && data.stopMessageBdWorkState.trim()) {
691
- state.stopMessageBdWorkState = data.stopMessageBdWorkState.trim();
692
- }
609
+ deserializeStopMessageState(data, state);
610
+ deserializePreCommandState(data, state);
693
611
  return state;
694
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;