@jsonstudio/rcc 0.89.935 → 0.89.1083

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 (96) hide show
  1. package/README.md +1 -42
  2. package/dist/build-info.js +2 -2
  3. package/dist/build-info.js.map +1 -1
  4. package/dist/cli.js +120 -16
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/quota-daemon.d.ts +2 -0
  7. package/dist/commands/quota-daemon.js +89 -0
  8. package/dist/commands/quota-daemon.js.map +1 -0
  9. package/dist/commands/token-daemon.js +1 -1
  10. package/dist/commands/token-daemon.js.map +1 -1
  11. package/dist/docs/daemon-admin-ui.html +958 -0
  12. package/dist/index.js +54 -4
  13. package/dist/index.js.map +1 -1
  14. package/dist/manager/modules/quota/index.d.ts +34 -0
  15. package/dist/manager/modules/quota/index.js +291 -0
  16. package/dist/manager/modules/quota/index.js.map +1 -1
  17. package/dist/manager/modules/token/index.js +14 -3
  18. package/dist/manager/modules/token/index.js.map +1 -1
  19. package/dist/manager/quota/provider-quota-center.d.ts +48 -0
  20. package/dist/manager/quota/provider-quota-center.js +239 -0
  21. package/dist/manager/quota/provider-quota-center.js.map +1 -0
  22. package/dist/manager/quota/provider-quota-store.d.ts +17 -0
  23. package/dist/manager/quota/provider-quota-store.js +88 -0
  24. package/dist/manager/quota/provider-quota-store.js.map +1 -0
  25. package/dist/providers/auth/token-scanner/index.js +11 -3
  26. package/dist/providers/auth/token-scanner/index.js.map +1 -1
  27. package/dist/providers/core/runtime/http-request-executor.js +24 -7
  28. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  29. package/dist/providers/core/runtime/http-transport-provider.js +11 -3
  30. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  31. package/dist/providers/core/runtime/responses-provider.js +9 -3
  32. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  33. package/dist/providers/core/utils/http-client.d.ts +1 -0
  34. package/dist/providers/core/utils/http-client.js +139 -4
  35. package/dist/providers/core/utils/http-client.js.map +1 -1
  36. package/dist/providers/core/utils/snapshot-writer.d.ts +12 -0
  37. package/dist/providers/core/utils/snapshot-writer.js +99 -18
  38. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  39. package/dist/providers/mock/mock-provider-runtime.d.ts +3 -0
  40. package/dist/providers/mock/mock-provider-runtime.js +176 -4
  41. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  42. package/dist/server/handlers/chat-handler.js +13 -1
  43. package/dist/server/handlers/chat-handler.js.map +1 -1
  44. package/dist/server/handlers/handler-utils.js +5 -0
  45. package/dist/server/handlers/handler-utils.js.map +1 -1
  46. package/dist/server/handlers/messages-handler.js +13 -1
  47. package/dist/server/handlers/messages-handler.js.map +1 -1
  48. package/dist/server/handlers/responses-handler.js +73 -1
  49. package/dist/server/handlers/responses-handler.js.map +1 -1
  50. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +174 -2
  51. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  52. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +519 -0
  53. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  54. package/dist/server/runtime/http-server/executor-response.js +6 -0
  55. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  56. package/dist/server/runtime/http-server/index.d.ts +5 -0
  57. package/dist/server/runtime/http-server/index.js +205 -4
  58. package/dist/server/runtime/http-server/index.js.map +1 -1
  59. package/dist/server/runtime/http-server/middleware.d.ts +2 -0
  60. package/dist/server/runtime/http-server/middleware.js +63 -0
  61. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  62. package/dist/server/runtime/http-server/request-executor.d.ts +2 -0
  63. package/dist/server/runtime/http-server/request-executor.js +57 -10
  64. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  65. package/dist/server/runtime/http-server/routes.js +38 -1
  66. package/dist/server/runtime/http-server/routes.js.map +1 -1
  67. package/dist/server/runtime/http-server/stats-manager.d.ts +55 -0
  68. package/dist/server/runtime/http-server/stats-manager.js +462 -4
  69. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  70. package/dist/server/runtime/http-server/types.d.ts +1 -0
  71. package/dist/token-daemon/token-daemon.d.ts +3 -1
  72. package/dist/token-daemon/token-daemon.js +130 -8
  73. package/dist/token-daemon/token-daemon.js.map +1 -1
  74. package/dist/token-daemon/token-utils.d.ts +1 -0
  75. package/dist/token-daemon/token-utils.js +9 -1
  76. package/dist/token-daemon/token-utils.js.map +1 -1
  77. package/dist/tools/semantic-replay.js +29 -0
  78. package/dist/tools/semantic-replay.js.map +1 -1
  79. package/dist/utils/snapshot-writer.d.ts +2 -0
  80. package/dist/utils/snapshot-writer.js +47 -4
  81. package/dist/utils/snapshot-writer.js.map +1 -1
  82. package/package.json +2 -3
  83. package/scripts/analyze-apply-patch-exec-failures.mjs +153 -0
  84. package/scripts/analyze-apply-patch-samples.mjs +242 -0
  85. package/scripts/analyze-codex-error-failures.mjs +24 -14
  86. package/scripts/classify-codex-samples.mjs +0 -35
  87. package/scripts/copy-modules-config.mjs +17 -1
  88. package/scripts/generate-snapshot-data.mjs +41 -11
  89. package/scripts/mock-provider/extract.mjs +254 -21
  90. package/scripts/mock-provider/run-regressions.mjs +97 -16
  91. package/scripts/quota-dryrun.mjs +124 -0
  92. package/scripts/tests/apply-patch-loop.mjs +5 -1
  93. package/scripts/tests/exec-command-loop.mjs +16 -19
  94. package/scripts/verify-apply-patch.mjs +335 -5
  95. package/scripts/verify-e2e-toolcall.mjs +49 -10
  96. package/scripts/toon-suite.mjs +0 -141
@@ -8,6 +8,7 @@ import { Readable } from 'node:stream';
8
8
  import http from 'node:http';
9
9
  import { setTimeout as delay } from 'node:timers/promises';
10
10
  import { spawnSync } from 'node:child_process';
11
+ import chalk from 'chalk';
11
12
  import { createTempConfig, startServer, stopServer } from '../lib/routecodex-runner.mjs';
12
13
  import { GeminiSemanticMapper } from '../../sharedmodule/llmswitch-core/dist/conversion/hub/semantic-mappers/gemini-mapper.js';
13
14
 
@@ -30,6 +31,8 @@ const STAGE_SUFFIX = '_req_outbound_stage2_format_build.json';
30
31
  const STAGE1_SUFFIX = '_req_outbound_stage1_semantic_map.json';
31
32
  const MOCK_PROVIDER_ID = 'mock.apply_patch.toolloop';
32
33
 
34
+ const chalkError = typeof chalk?.redBright === 'function' ? chalk.redBright : (value) => value;
35
+
33
36
  function listProcessesOnPort(port) {
34
37
  try {
35
38
  const res = spawnSync('lsof', ['-ti', `tcp:${port}`], { encoding: 'utf-8' });
@@ -702,6 +705,7 @@ async function main() {
702
705
  }
703
706
 
704
707
  main().catch((error) => {
705
- console.error(`[tool-loop] FAILED: ${error.message}`);
708
+ const msg = error instanceof Error ? (error.stack || error.message) : String(error ?? '');
709
+ console.error(chalkError(`[tool-loop] FAILED: ${msg}`));
706
710
  process.exit(1);
707
711
  });
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * exec_command TOON → JSON 回环验证(模拟 Responses 客户端)。
3
+ * exec_command JSON 形态回环验证(模拟 Responses 客户端)。
4
4
  *
5
5
  * 目标:
6
- * - 构造一条带 exec_command TOON arguments 的 chat 响应;
7
- * - 通过 llmswitch-core 的 response 工具过滤管线(ResponseToolArgumentsToonDecodeFilter)做解码;
8
- * - 使用 codex 的工具注册表 validateToolCall 校验最终 JSON 形状(必须包含 cmd,且不再暴露 toon);
9
- * - 只从“客户端视角”观察:发送/接收的都是 JSON,TOON 对客户端完全透明。
6
+ * - 构造一条带 exec_command JSON arguments 的 chat 响应;
7
+ * - 通过 llmswitch-core 的 response 工具过滤管线做统一治理;
8
+ * - 校验最终 JSON 形状(必须包含 cmd,且不暴露 toon);
9
+ * - 再通过 Responses 映射验证 /v1/responses 视图同样保持 JSON 语义。
10
10
  */
11
11
 
12
12
  import path from 'node:path';
@@ -25,9 +25,9 @@ async function main() {
25
25
  'conversion/responses/responses-openai-bridge'
26
26
  );
27
27
 
28
- // 构造一条模拟的 chat 响应,其中 exec_command 使用 TOON 编码参数。
28
+ // 构造一条模拟的 chat 响应,其中 exec_command 直接使用 JSON 编码参数。
29
29
  const chatPayload = {
30
- id: 'chatcmpl_exec_toon',
30
+ id: 'chatcmpl_exec_args',
31
31
  object: 'chat.completion',
32
32
  created: Math.floor(Date.now() / 1000),
33
33
  model: 'gpt-5.2-codex',
@@ -39,18 +39,15 @@ async function main() {
39
39
  content: null,
40
40
  tool_calls: [
41
41
  {
42
- id: 'call_exec_toon',
42
+ id: 'call_exec_args',
43
43
  type: 'function',
44
44
  function: {
45
45
  name: 'exec_command',
46
46
  arguments: JSON.stringify({
47
- toon: [
48
- 'cmd: echo 1',
49
- 'yield_time_ms: 500',
50
- 'max_output_tokens: 128',
51
- 'shell: /bin/bash',
52
- 'login: false'
53
- ].join('\n')
47
+ cmd: 'echo 1',
48
+ workdir: '.',
49
+ yield_time_ms: 500,
50
+ max_output_tokens: 128
54
51
  })
55
52
  }
56
53
  }
@@ -61,10 +58,10 @@ async function main() {
61
58
  ]
62
59
  };
63
60
 
64
- // 通过 response 工具管线运行,触发 TOON → JSON 解码。
61
+ // 通过 response 工具管线运行,触发统一的工具参数治理/归一化。
65
62
  const filtered = await runChatResponseToolFilters(chatPayload, {
66
63
  entryEndpoint: '/v1/chat/completions',
67
- requestId: 'req_exec_toon',
64
+ requestId: 'req_exec_args',
68
65
  profile: 'openai-chat'
69
66
  });
70
67
 
@@ -116,7 +113,7 @@ async function main() {
116
113
  // 延伸验证:基于 chat 结果构建 Responses payload,确保 /v1/responses 视图中的
117
114
  // function_call.arguments 同样保持 exec_command JSON 语义,而不会重新出现 toon。
118
115
  const responsesPayload = buildResponsesPayloadFromChat(filtered, {
119
- requestId: 'verify_exec_command_toon'
116
+ requestId: 'verify_exec_command_args'
120
117
  });
121
118
  const outputItems = Array.isArray(responsesPayload?.output) ? responsesPayload.output : [];
122
119
  const fnCall = outputItems.find(
@@ -153,7 +150,7 @@ async function main() {
153
150
  console.log(
154
151
  `[exec-command-loop] decoded cmd="${args.cmd}" yield_time_ms=${args.yield_time_ms ?? 'n/a'} max_output_tokens=${args.max_output_tokens ?? 'n/a'}`
155
152
  );
156
- console.log('✅ exec_command TOON decode passed (chat + responses views are JSON-only)');
153
+ console.log('✅ exec_command arguments normalization passed (chat + responses views are JSON-only)');
157
154
  }
158
155
 
159
156
  main().catch((error) => {
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import path from 'node:path';
9
9
  import { fileURLToPath, pathToFileURL } from 'node:url';
10
+ import chalk from 'chalk';
10
11
 
11
12
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
13
  const repoRoot = path.resolve(__dirname, '..');
@@ -14,6 +15,8 @@ const coreLoaderPath = path.join(repoRoot, 'dist', 'modules', 'llmswitch', 'core
14
15
  const coreLoaderUrl = pathToFileURL(coreLoaderPath).href;
15
16
  const { importCoreModule } = await import(coreLoaderUrl);
16
17
 
18
+ const chalkError = typeof chalk?.redBright === 'function' ? chalk.redBright : (value) => value;
19
+
17
20
  async function loadCoreModule(subpath) {
18
21
  return importCoreModule(subpath);
19
22
  }
@@ -99,6 +102,47 @@ async function runApplyPatchTextCase(label, payloadText) {
99
102
  }
100
103
  }
101
104
 
105
+ async function runApplyPatchArgsCase(label, argsString) {
106
+ const { validateToolCall } = await loadCoreModule('tools/tool-registry');
107
+ const validation = validateToolCall('apply_patch', argsString);
108
+ if (!validation?.ok) {
109
+ throw new Error(
110
+ `[verify-apply-patch] ${label}: validateToolCall failed with reason=${validation?.reason}`
111
+ );
112
+ }
113
+ let parsed;
114
+ try {
115
+ parsed = JSON.parse(validation.normalizedArgs || '{}');
116
+ } catch (error) {
117
+ throw new Error(
118
+ `[verify-apply-patch] ${label}: normalized arguments not valid JSON: ${(error && error.message) || String(error)}`
119
+ );
120
+ }
121
+ const normalizedPatchText =
122
+ typeof parsed?.patch === 'string'
123
+ ? parsed.patch
124
+ : typeof parsed?.input === 'string'
125
+ ? parsed.input
126
+ : '';
127
+ if (!normalizedPatchText) {
128
+ throw new Error(`[verify-apply-patch] ${label}: missing normalized patch text`);
129
+ }
130
+ if (!normalizedPatchText.startsWith('*** Begin Patch')) {
131
+ throw new Error(`[verify-apply-patch] ${label}: patch does not start with *** Begin Patch`);
132
+ }
133
+ if (!normalizedPatchText.includes('\n*** End Patch')) {
134
+ throw new Error(`[verify-apply-patch] ${label}: patch missing *** End Patch`);
135
+ }
136
+ if (
137
+ normalizedPatchText.includes('*** End Patch","input":"*** Begin Patch') ||
138
+ normalizedPatchText.includes('*** End Patch","patch":"*** Begin Patch') ||
139
+ normalizedPatchText.includes('*** End Patch\\",\\"input\\":\\"*** Begin Patch') ||
140
+ normalizedPatchText.includes('*** End Patch\\",\\"patch\\":\\"*** Begin Patch')
141
+ ) {
142
+ throw new Error(`[verify-apply-patch] ${label}: patch still contains stitched JSON keys`);
143
+ }
144
+ }
145
+
102
146
  async function main() {
103
147
  if (String(process.env.ROUTECODEX_VERIFY_SKIP || '').trim() === '1') {
104
148
  console.log('[verify-apply-patch] 跳过(ROUTECODEX_VERIFY_SKIP=1)');
@@ -106,6 +150,294 @@ async function main() {
106
150
  }
107
151
 
108
152
  try {
153
+ const { validateToolCall } = await loadCoreModule('tools/tool-registry');
154
+ const escapeNewlines = (value) => String(value || '').replace(/\n/g, '\\n');
155
+
156
+ // Regression: tolerate newline-escaped patch text (e.g. "*** Begin Patch\\n...") and
157
+ // normalize into a real multi-line unified diff string.
158
+ {
159
+ const escapedPatch = '*** Begin Patch\\n*** End Patch';
160
+ const validation = validateToolCall('apply_patch', escapedPatch);
161
+ if (!validation?.ok) {
162
+ throw new Error(
163
+ `[verify-apply-patch] escaped_patch: validateToolCall failed with reason=${validation?.reason}`
164
+ );
165
+ }
166
+ let parsed;
167
+ try {
168
+ parsed = JSON.parse(validation.normalizedArgs);
169
+ } catch (error) {
170
+ throw new Error(
171
+ `[verify-apply-patch] escaped_patch: normalized arguments not valid JSON: ${(error && error.message) || String(error)}`
172
+ );
173
+ }
174
+ const patchText =
175
+ typeof parsed?.patch === 'string'
176
+ ? parsed.patch
177
+ : typeof parsed?.input === 'string'
178
+ ? parsed.input
179
+ : '';
180
+ if (!patchText || typeof patchText !== 'string') {
181
+ throw new Error('[verify-apply-patch] escaped_patch: missing patch text in normalized args');
182
+ }
183
+ if (patchText.includes('\\n') && !patchText.includes('\n')) {
184
+ throw new Error('[verify-apply-patch] escaped_patch: patch still contains literal \\\\n without real newlines');
185
+ }
186
+ if (patchText.split('\n')[0] !== '*** Begin Patch') {
187
+ throw new Error('[verify-apply-patch] escaped_patch: patch first line is not *** Begin Patch');
188
+ }
189
+ }
190
+
191
+ // Regression: validateToolCall should be idempotent for already-normalized JSON arguments.
192
+ // Previously we could mis-detect the whole JSON as patch text, producing a merged line like:
193
+ // "*** End Patch\",\"input\":\"*** Begin Patch"
194
+ {
195
+ const patchText = '*** Begin Patch\n*** End Patch';
196
+ const alreadyNormalized = JSON.stringify({ patch: patchText, input: patchText });
197
+ const validation = validateToolCall('apply_patch', alreadyNormalized);
198
+ if (!validation?.ok) {
199
+ throw new Error(
200
+ `[verify-apply-patch] already_normalized: validateToolCall failed with reason=${validation?.reason}`
201
+ );
202
+ }
203
+ const parsed = JSON.parse(validation.normalizedArgs);
204
+ const normalizedPatchText =
205
+ typeof parsed?.patch === 'string'
206
+ ? parsed.patch
207
+ : typeof parsed?.input === 'string'
208
+ ? parsed.input
209
+ : '';
210
+ if (!normalizedPatchText) {
211
+ throw new Error('[verify-apply-patch] already_normalized: missing patch text in normalized args');
212
+ }
213
+ if (normalizedPatchText.includes('","input":"*** Begin Patch') || normalizedPatchText.includes('*** End Patch","input":"')) {
214
+ throw new Error('[verify-apply-patch] already_normalized: patch text incorrectly contains serialized JSON keys');
215
+ }
216
+ if (normalizedPatchText.split('\n')[0] !== '*** Begin Patch') {
217
+ throw new Error('[verify-apply-patch] already_normalized: patch first line is not *** Begin Patch');
218
+ }
219
+ if (!normalizedPatchText.includes('*** End Patch')) {
220
+ throw new Error('[verify-apply-patch] already_normalized: patch missing *** End Patch');
221
+ }
222
+ }
223
+
224
+ // Regression: accept a typical sequence of apply_patch calls (add/update/append/escape/multi-hunk/delete).
225
+ // Note: we only validate tool governance & patch text normalization, not filesystem application.
226
+ {
227
+ const addBasic = [
228
+ '*** Begin Patch',
229
+ '*** Add File: .apply_patch_basic_add.txt',
230
+ '+apply_patch basic add ok',
231
+ '+line2',
232
+ '*** End Patch'
233
+ ].join('\n');
234
+ const updateBasic = [
235
+ '*** Begin Patch',
236
+ '*** Update File: .apply_patch_basic_add.txt',
237
+ '@@',
238
+ '-apply_patch basic add ok',
239
+ '+apply_patch basic add+update ok',
240
+ ' line2',
241
+ '*** End Patch'
242
+ ].join('\n');
243
+ const appendBasic = [
244
+ '*** Begin Patch',
245
+ '*** Update File: .apply_patch_basic_add.txt',
246
+ '@@',
247
+ ' apply_patch basic add+update ok',
248
+ ' line2',
249
+ '+line3 (append)',
250
+ '*** End Patch'
251
+ ].join('\n');
252
+ const addEscapeChars = [
253
+ '*** Begin Patch',
254
+ '*** Add File: .apply_patch_escape_chars.txt',
255
+ '+quotes: "double" and \'single\'',
256
+ '+backslash: \\',
257
+ '+json: {"a":1,"b":"x"}',
258
+ '+template: ${notInterpolated}',
259
+ '*** End Patch'
260
+ ].join('\n');
261
+ const addMulti = [
262
+ '*** Begin Patch',
263
+ '*** Add File: .apply_patch_multi_hunk.txt',
264
+ '+Header',
265
+ '+Section A',
266
+ '+Section B',
267
+ '+Footer',
268
+ '*** End Patch'
269
+ ].join('\n');
270
+ const updateMulti = [
271
+ '*** Begin Patch',
272
+ '*** Update File: .apply_patch_multi_hunk.txt',
273
+ '@@',
274
+ ' Header',
275
+ '-Section A',
276
+ '+Section A (updated)',
277
+ ' Section B',
278
+ ' Footer',
279
+ '@@',
280
+ ' Header',
281
+ ' Section A (updated)',
282
+ '-Section B',
283
+ '+Section B (updated)',
284
+ ' Footer',
285
+ '*** End Patch'
286
+ ].join('\n');
287
+ const deleteBasic = [
288
+ '*** Begin Patch',
289
+ '*** Delete File: .apply_patch_basic_add.txt',
290
+ '*** End Patch'
291
+ ].join('\n');
292
+ const deleteEscapeChars = [
293
+ '*** Begin Patch',
294
+ '*** Delete File: .apply_patch_escape_chars.txt',
295
+ '*** End Patch'
296
+ ].join('\n');
297
+ const deleteMulti = [
298
+ '*** Begin Patch',
299
+ '*** Delete File: .apply_patch_multi_hunk.txt',
300
+ '*** End Patch'
301
+ ].join('\n');
302
+
303
+ const cases = [
304
+ ['seq_basic_add_text', addBasic],
305
+ ['seq_basic_update_text', updateBasic],
306
+ ['seq_basic_append_text', appendBasic],
307
+ ['seq_escape_chars_text', addEscapeChars],
308
+ ['seq_multi_add_text', addMulti],
309
+ ['seq_multi_update_text', updateMulti],
310
+ ['seq_basic_delete_text', deleteBasic],
311
+ ['seq_escape_chars_delete_text', deleteEscapeChars],
312
+ ['seq_multi_delete_text', deleteMulti]
313
+ ];
314
+
315
+ for (const [label, patchText] of cases) {
316
+ await runApplyPatchArgsCase(label, patchText);
317
+ await runApplyPatchArgsCase(
318
+ `${label}_json`,
319
+ JSON.stringify({ patch: patchText, input: patchText })
320
+ );
321
+ await runApplyPatchArgsCase(
322
+ `${label}_json_escaped_newlines`,
323
+ JSON.stringify({ patch: escapeNewlines(patchText), input: escapeNewlines(patchText) })
324
+ );
325
+ }
326
+ }
327
+
328
+ // Regression: tolerate <arg_key>/<arg_value> artifacts stitched into patch strings.
329
+ // Some upstream providers may leak XML-like markup into JSON string fields.
330
+ {
331
+ const patchText = [
332
+ '*** Begin Patch',
333
+ '*** Delete File: .apply_patch_escape_test.txt',
334
+ '*** End Patch'
335
+ ].join('\n');
336
+ const injected = `${patchText}</arg_key><arg_value>input</arg_key><arg_value>${patchText}`;
337
+ const argsString = JSON.stringify({ patch: injected });
338
+ const validation = validateToolCall('apply_patch', argsString);
339
+ if (!validation?.ok) {
340
+ throw new Error(
341
+ `[verify-apply-patch] arg_key_artifacts: validateToolCall failed with reason=${validation?.reason}`
342
+ );
343
+ }
344
+ const parsed = JSON.parse(validation.normalizedArgs);
345
+ const normalizedPatchText =
346
+ typeof parsed?.patch === 'string'
347
+ ? parsed.patch
348
+ : typeof parsed?.input === 'string'
349
+ ? parsed.input
350
+ : '';
351
+ if (normalizedPatchText.includes('</arg_key><arg_value>')) {
352
+ throw new Error('[verify-apply-patch] arg_key_artifacts: patch still contains arg_key artifacts');
353
+ }
354
+ if (parsed?.patch !== parsed?.input) {
355
+ throw new Error('[verify-apply-patch] arg_key_artifacts: patch/input mismatch after normalization');
356
+ }
357
+ }
358
+
359
+ // Regression: invalid JSON containers should be classified as invalid_json (not missing_changes).
360
+ {
361
+ const invalidJson = '{"file":"a.ts","changes":[{"kind":"create_file","lines":["x"],"file</arg_key><arg_value>a.ts"}]}';
362
+ const validation = validateToolCall('apply_patch', invalidJson);
363
+ if (validation?.ok || validation?.reason !== 'invalid_json') {
364
+ throw new Error(
365
+ `[verify-apply-patch] invalid_json: expected invalid_json reason, got ok=${validation?.ok} reason=${validation?.reason}`
366
+ );
367
+ }
368
+ }
369
+
370
+ // Regression: patches can contain ``` blocks inside file content; do not treat them as outer fences.
371
+ {
372
+ const patchText = [
373
+ '*** Begin Patch',
374
+ '*** Add File: src/demo-codefence.md',
375
+ '+```json',
376
+ '+{\"ok\":true}',
377
+ '+```',
378
+ '*** End Patch'
379
+ ].join('\n');
380
+ const validation = validateToolCall('apply_patch', patchText);
381
+ if (!validation?.ok) {
382
+ throw new Error(
383
+ `[verify-apply-patch] inner_codefence: validateToolCall failed with reason=${validation?.reason}`
384
+ );
385
+ }
386
+ const parsed = JSON.parse(validation.normalizedArgs);
387
+ const normalizedPatchText =
388
+ typeof parsed?.patch === 'string'
389
+ ? parsed.patch
390
+ : typeof parsed?.input === 'string'
391
+ ? parsed.input
392
+ : '';
393
+ if (!normalizedPatchText.startsWith('*** Begin Patch')) {
394
+ throw new Error('[verify-apply-patch] inner_codefence: patch lost *** Begin Patch header');
395
+ }
396
+ if (!normalizedPatchText.includes('*** Add File: src/demo-codefence.md')) {
397
+ throw new Error('[verify-apply-patch] inner_codefence: missing Add File header');
398
+ }
399
+ if (!normalizedPatchText.includes('+```json') || !normalizedPatchText.includes('+```')) {
400
+ throw new Error('[verify-apply-patch] inner_codefence: missing fenced lines inside patch');
401
+ }
402
+ }
403
+
404
+ // Regression: tolerate newline-escaped snippets inside structured payload fields.
405
+ // Some models/clients double-escape multi-line anchors/targets (e.g. "\\n ").
406
+ {
407
+ const structuredArgs = JSON.stringify({
408
+ file: 'src/demo-escaped-snippet.ts',
409
+ changes: [
410
+ {
411
+ kind: 'replace',
412
+ target: 'const alpha = 1;\\n const beta = 2;',
413
+ lines: ['const alpha = 1;', ' const beta = 3;']
414
+ }
415
+ ]
416
+ });
417
+ const validation = validateToolCall('apply_patch', structuredArgs);
418
+ if (!validation?.ok) {
419
+ throw new Error(
420
+ `[verify-apply-patch] escaped_structured_snippet: validateToolCall failed with reason=${validation?.reason}`
421
+ );
422
+ }
423
+ const parsed = JSON.parse(validation.normalizedArgs);
424
+ const patchText =
425
+ typeof parsed?.patch === 'string'
426
+ ? parsed.patch
427
+ : typeof parsed?.input === 'string'
428
+ ? parsed.input
429
+ : '';
430
+ if (!patchText) {
431
+ throw new Error('[verify-apply-patch] escaped_structured_snippet: missing patch text in normalized args');
432
+ }
433
+ if (patchText.includes('const alpha = 1;\\n') || patchText.includes('\\n const beta = 2;')) {
434
+ throw new Error('[verify-apply-patch] escaped_structured_snippet: patch still contains literal \\\\n in target');
435
+ }
436
+ if (!patchText.includes('-const alpha = 1;') || !patchText.includes('- const beta = 2;')) {
437
+ throw new Error('[verify-apply-patch] escaped_structured_snippet: expected multi-line "-" target not found');
438
+ }
439
+ }
440
+
109
441
  const plainJson = JSON.stringify({
110
442
  file: 'src/demo.ts',
111
443
  changes: [
@@ -136,11 +468,9 @@ async function main() {
136
468
 
137
469
  console.log('✅ verify-apply-patch: text→tool_calls pipeline passed');
138
470
  } catch (error) {
139
- console.error(error);
140
- console.error(
141
- '❌ verify-apply-patch 失败:',
142
- error instanceof Error ? error.message : String(error ?? 'Unknown error')
143
- );
471
+ const msg = error instanceof Error ? (error.stack || error.message) : String(error ?? 'Unknown error');
472
+ console.error(chalkError(msg));
473
+ console.error(chalkError(`❌ verify-apply-patch 失败: ${error instanceof Error ? error.message : String(error ?? 'Unknown error')}`));
144
474
  process.exit(1);
145
475
  }
146
476
  }
@@ -29,6 +29,24 @@ const AGENTS_INSTRUCTIONS = (() => {
29
29
  }
30
30
  })();
31
31
 
32
+ function readServerApiKeyFromConfig(configPath) {
33
+ try {
34
+ const raw = fs.readFileSync(configPath, 'utf8');
35
+ const json = raw && raw.trim() ? JSON.parse(raw) : {};
36
+ const apikey = json?.httpserver?.apikey;
37
+ return typeof apikey === 'string' && apikey.trim() ? apikey.trim() : '';
38
+ } catch {
39
+ return '';
40
+ }
41
+ }
42
+
43
+ function buildAuthHeaders(serverApiKey) {
44
+ if (!serverApiKey) {
45
+ return {};
46
+ }
47
+ return { 'x-api-key': serverApiKey };
48
+ }
49
+
32
50
  async function main() {
33
51
  if (!VERIFY_CONFIG) {
34
52
  console.error('❌ ROUTECODEX_VERIFY_CONFIG 未设置,无法运行端到端校验');
@@ -36,6 +54,8 @@ async function main() {
36
54
  }
37
55
 
38
56
  console.log(`[verify:e2e-toolcall] 使用配置: ${VERIFY_CONFIG}`);
57
+ const serverApiKey = readServerApiKeyFromConfig(VERIFY_CONFIG);
58
+ const authHeaders = buildAuthHeaders(serverApiKey);
39
59
  const serverEnv = {
40
60
  ...process.env,
41
61
  ROUTECODEX_CONFIG_PATH: VERIFY_CONFIG,
@@ -61,11 +81,12 @@ async function main() {
61
81
  try {
62
82
  await waitForServer();
63
83
  await waitForRouterWarmup();
64
- await runToolcallVerification();
84
+ await runModelsSmokeCheck(authHeaders);
85
+ await runToolcallVerification(authHeaders);
65
86
  console.log('✅ 端到端工具调用校验通过');
66
87
 
67
- await runDaemonAdminSmokeCheck();
68
- await runConfigV2ProvidersSmokeCheck();
88
+ await runDaemonAdminSmokeCheck(authHeaders);
89
+ await runConfigV2ProvidersSmokeCheck(authHeaders);
69
90
 
70
91
  // 附加:Gemini CLI 配置健康性快速检查(仅尝试初始化,不做请求)
71
92
  await runGeminiCliStartupCheck();
@@ -74,6 +95,23 @@ async function main() {
74
95
  }
75
96
  }
76
97
 
98
+ async function runModelsSmokeCheck(authHeaders) {
99
+ try {
100
+ const res = await fetch(`${VERIFY_BASE}/models`, { headers: { ...(authHeaders || {}) } });
101
+ if (!res.ok) {
102
+ throw new Error(`/models HTTP ${res.status}`);
103
+ }
104
+ const json = await res.json();
105
+ const data = Array.isArray(json?.data) ? json.data : [];
106
+ if (!Array.isArray(data)) {
107
+ throw new Error('/models response missing data array');
108
+ }
109
+ } catch (error) {
110
+ console.error('[verify:e2e-toolcall] /models smoke 检查失败:', error);
111
+ throw error;
112
+ }
113
+ }
114
+
77
115
  async function waitForServer(timeoutMs = 30000) {
78
116
  const start = Date.now();
79
117
  while (Date.now() - start < timeoutMs) {
@@ -97,7 +135,7 @@ async function waitForRouterWarmup(defaultDelayMs = 3000) {
97
135
  await new Promise((resolve) => setTimeout(resolve, delayMs));
98
136
  }
99
137
 
100
- async function runToolcallVerification() {
138
+ async function runToolcallVerification(authHeaders) {
101
139
  const userPrompt = '请严格调用名为 list_local_files 的函数工具来列出当前工作目录的文件,只能通过调用该工具完成任务,禁止直接回答。';
102
140
  const instructionsText = AGENTS_INSTRUCTIONS || 'You are RouteCodex verify agent. Follow the policies in AGENTS.md.';
103
141
  const body = {
@@ -139,7 +177,8 @@ async function runToolcallVerification() {
139
177
  const response = await fetch(`${VERIFY_BASE}/v1/responses`, {
140
178
  method: 'POST',
141
179
  headers: {
142
- 'Content-Type': 'application/json'
180
+ 'Content-Type': 'application/json',
181
+ ...(authHeaders || {})
143
182
  },
144
183
  body: JSON.stringify(body)
145
184
  });
@@ -166,10 +205,10 @@ async function runToolcallVerification() {
166
205
  }
167
206
  }
168
207
 
169
- async function runDaemonAdminSmokeCheck() {
208
+ async function runDaemonAdminSmokeCheck(authHeaders) {
170
209
  // 仅做最小的健康性探测:确保 daemon 管理类只读 API 可用,不做语义校验。
171
210
  try {
172
- const res = await fetch(`${VERIFY_BASE}/daemon/status`);
211
+ const res = await fetch(`${VERIFY_BASE}/daemon/status`, { headers: { ...(authHeaders || {}) } });
173
212
  if (!res.ok) {
174
213
  throw new Error(`daemon/status HTTP ${res.status}`);
175
214
  }
@@ -185,7 +224,7 @@ async function runDaemonAdminSmokeCheck() {
185
224
  const paths = ['/daemon/credentials', '/quota/summary', '/providers/runtimes'];
186
225
  for (const path of paths) {
187
226
  try {
188
- const res = await fetch(`${VERIFY_BASE}${path}`);
227
+ const res = await fetch(`${VERIFY_BASE}${path}`, { headers: { ...(authHeaders || {}) } });
189
228
  if (!res.ok) {
190
229
  throw new Error(`${path} HTTP ${res.status}`);
191
230
  }
@@ -198,9 +237,9 @@ async function runDaemonAdminSmokeCheck() {
198
237
  }
199
238
  }
200
239
 
201
- async function runConfigV2ProvidersSmokeCheck() {
240
+ async function runConfigV2ProvidersSmokeCheck(authHeaders) {
202
241
  try {
203
- const res = await fetch(`${VERIFY_BASE}/config/providers/v2`);
242
+ const res = await fetch(`${VERIFY_BASE}/config/providers/v2`, { headers: { ...(authHeaders || {}) } });
204
243
  if (!res.ok) {
205
244
  throw new Error(`/config/providers/v2 HTTP ${res.status}`);
206
245
  }