@jsonstudio/rcc 0.89.942 → 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 (91) 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 +106 -10
  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/docs/daemon-admin-ui.html +958 -0
  10. package/dist/index.js +5 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/manager/modules/quota/index.d.ts +34 -0
  13. package/dist/manager/modules/quota/index.js +291 -0
  14. package/dist/manager/modules/quota/index.js.map +1 -1
  15. package/dist/manager/modules/token/index.js +13 -2
  16. package/dist/manager/modules/token/index.js.map +1 -1
  17. package/dist/manager/quota/provider-quota-center.d.ts +48 -0
  18. package/dist/manager/quota/provider-quota-center.js +239 -0
  19. package/dist/manager/quota/provider-quota-center.js.map +1 -0
  20. package/dist/manager/quota/provider-quota-store.d.ts +17 -0
  21. package/dist/manager/quota/provider-quota-store.js +88 -0
  22. package/dist/manager/quota/provider-quota-store.js.map +1 -0
  23. package/dist/providers/auth/token-scanner/index.js +11 -3
  24. package/dist/providers/auth/token-scanner/index.js.map +1 -1
  25. package/dist/providers/core/runtime/http-request-executor.js +24 -7
  26. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  27. package/dist/providers/core/runtime/http-transport-provider.js +11 -3
  28. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  29. package/dist/providers/core/runtime/responses-provider.js +9 -3
  30. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  31. package/dist/providers/core/utils/http-client.d.ts +1 -0
  32. package/dist/providers/core/utils/http-client.js +139 -4
  33. package/dist/providers/core/utils/http-client.js.map +1 -1
  34. package/dist/providers/core/utils/snapshot-writer.d.ts +12 -0
  35. package/dist/providers/core/utils/snapshot-writer.js +99 -18
  36. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  37. package/dist/providers/mock/mock-provider-runtime.d.ts +3 -0
  38. package/dist/providers/mock/mock-provider-runtime.js +176 -4
  39. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  40. package/dist/server/handlers/chat-handler.js +13 -1
  41. package/dist/server/handlers/chat-handler.js.map +1 -1
  42. package/dist/server/handlers/handler-utils.js +5 -0
  43. package/dist/server/handlers/handler-utils.js.map +1 -1
  44. package/dist/server/handlers/messages-handler.js +13 -1
  45. package/dist/server/handlers/messages-handler.js.map +1 -1
  46. package/dist/server/handlers/responses-handler.js +73 -1
  47. package/dist/server/handlers/responses-handler.js.map +1 -1
  48. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +174 -2
  49. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  50. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +519 -0
  51. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  52. package/dist/server/runtime/http-server/executor-response.js +6 -0
  53. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  54. package/dist/server/runtime/http-server/index.d.ts +5 -0
  55. package/dist/server/runtime/http-server/index.js +205 -4
  56. package/dist/server/runtime/http-server/index.js.map +1 -1
  57. package/dist/server/runtime/http-server/middleware.d.ts +2 -0
  58. package/dist/server/runtime/http-server/middleware.js +63 -0
  59. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  60. package/dist/server/runtime/http-server/request-executor.d.ts +2 -0
  61. package/dist/server/runtime/http-server/request-executor.js +57 -10
  62. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  63. package/dist/server/runtime/http-server/routes.js +38 -1
  64. package/dist/server/runtime/http-server/routes.js.map +1 -1
  65. package/dist/server/runtime/http-server/stats-manager.d.ts +55 -0
  66. package/dist/server/runtime/http-server/stats-manager.js +462 -4
  67. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  68. package/dist/server/runtime/http-server/types.d.ts +1 -0
  69. package/dist/token-daemon/token-daemon.js +70 -25
  70. package/dist/token-daemon/token-daemon.js.map +1 -1
  71. package/dist/token-daemon/token-utils.d.ts +1 -0
  72. package/dist/token-daemon/token-utils.js +9 -1
  73. package/dist/token-daemon/token-utils.js.map +1 -1
  74. package/dist/tools/semantic-replay.js +29 -0
  75. package/dist/tools/semantic-replay.js.map +1 -1
  76. package/dist/utils/snapshot-writer.d.ts +2 -0
  77. package/dist/utils/snapshot-writer.js +47 -4
  78. package/dist/utils/snapshot-writer.js.map +1 -1
  79. package/package.json +2 -3
  80. package/scripts/analyze-codex-error-failures.mjs +24 -14
  81. package/scripts/classify-codex-samples.mjs +0 -35
  82. package/scripts/copy-modules-config.mjs +17 -1
  83. package/scripts/generate-snapshot-data.mjs +41 -11
  84. package/scripts/mock-provider/extract.mjs +239 -21
  85. package/scripts/mock-provider/run-regressions.mjs +79 -16
  86. package/scripts/quota-dryrun.mjs +124 -0
  87. package/scripts/tests/apply-patch-loop.mjs +5 -1
  88. package/scripts/tests/exec-command-loop.mjs +16 -19
  89. package/scripts/verify-apply-patch.mjs +335 -5
  90. package/scripts/verify-e2e-toolcall.mjs +49 -10
  91. package/scripts/toon-suite.mjs +0 -141
@@ -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
  }
@@ -1,141 +0,0 @@
1
- #!/usr/bin/env node
2
- // TOON tool-call probe: sends Chat requests that require arguments.toon, decodes via @toon-format/cli, and summarizes results.
3
-
4
- import fetch from 'node-fetch';
5
- import { spawnSync } from 'node:child_process';
6
-
7
- function arg(k, d){ const i=process.argv.indexOf(k); return (i>0 && process.argv[i+1]) ? process.argv[i+1] : d; }
8
- function flag(k){ return process.argv.includes(k); }
9
-
10
- const server = arg('--server', 'http://127.0.0.1:5520');
11
- const endpoint = arg('--endpoint', '/v1/chat/completions');
12
- const url = `${server.replace(/\/$/,'')}${endpoint}`;
13
- const model = arg('--model', 'glm.glm-4.6');
14
- const doExec = flag('--exec'); // optional local execution (unsafe; whitelist only)
15
-
16
- const systemMsg = (
17
- '当你使用工具 (tool_calls) 时,将 function.arguments 设为 JSON 字符串,且只包含字段 toon(字符串,多行 TOON)。' +
18
- '不要输出其它字段、不要解释。' +
19
- '示例:{"toon": "command: bash -lc \'echo ok\'\\nworkdir: .\\n"}。' +
20
- '务必返回 tool_calls,并仅填充 arguments.toon。'
21
- );
22
-
23
- const tools = [
24
- {
25
- type: 'function',
26
- function: {
27
- name: 'shell_toon',
28
- description: 'TOON 参数承载:将所有参数用 TOON 字符串放入 arguments.toon',
29
- parameters: {
30
- type: 'object',
31
- properties: { toon: { type: 'string', description: 'TOON-encoded arguments' } },
32
- required: ['toon'],
33
- additionalProperties: false
34
- }
35
- }
36
- }
37
- ];
38
-
39
- const cases = [
40
- {
41
- title: 'find_parens_exec',
42
- user: "请使用 shell_toon:bash -lc 执行:find . -type f \\( -name '*.md' -o -name '*.txt' \\) -exec basename {} \\; | sort -u | head -n 5。只返回 tool_calls。"
43
- },
44
- {
45
- title: 'awk_regex',
46
- user: "请用 shell_toon:bash -lc 执行:printf 'alpha,error\\nbeta,ok\\n' | awk -F, '{ if (\\$2 ~ /error/) print \\$1 }'。只返回 tool_calls。"
47
- },
48
- {
49
- title: 'sed_replace',
50
- user: "请用 shell_toon:bash -lc 执行:printf 'a=1\\nb=2\\n' | sed -E 's/([ab])=(\\\\d+)/\\\\1: \\\\2/g'。只返回 tool_calls。"
51
- },
52
- {
53
- title: 'python_heredoc',
54
- user: `请用 shell_toon:构造“单行” bash -lc 命令来执行多行 Python。禁止在命令中出现真实换行符;改用 ANSI-C 风格 $'...' 在“一行内”嵌入换行(\\n)。示例(仅示意):bash -lc $'python3 - <<\'PY\'\\nprint(1)\\nPY'。实际请输出:用 $'...' 方式在一行内执行 python3 - <<'PY' 的多行脚本:\nimport json\nprint(json.dumps({"ok": true, "sum": 1+2+3}))\n并保持整条命令为一行。只返回 tool_calls。`
55
- },
56
- {
57
- title: 'xargs_space',
58
- user: "请用 shell_toon:bash -lc 执行:printf 'A B.md\\nC D.md\\n' | xargs -I{} bash -lc 'echo \"{}\" | tr \" \" _'。只返回 tool_calls。"
59
- },
60
- {
61
- title: 'grep_sort_uniq',
62
- user: "请用 shell_toon:bash -lc 执行:printf 'a\\nb\\na\\nc\\n' | grep -E '[a-c]' | sort | uniq -c | sort -nr。只返回 tool_calls。"
63
- }
64
- ];
65
-
66
- function decodeToon(toonStr){
67
- const res = spawnSync('npx', ['-y','@toon-format/cli','--decode'], { input: toonStr, encoding: 'utf-8' });
68
- if ((res.status ?? 0) !== 0) return { ok: false, error: res.stderr || 'decode failed' };
69
- try { const obj = JSON.parse(res.stdout || '{}'); return { ok: true, value: obj }; } catch (e){ return { ok: false, error: 'json parse failed', raw: res.stdout }; }
70
- }
71
-
72
- function safeExec(cmd){
73
- const allow = /^(bash\s+-lc\s+).*/s.test(cmd) && /(find|printf|awk|sed|python3|ls|sort|head|uniq|tr)/.test(cmd);
74
- if (!allow) return { ok: false, skipped: true };
75
- const res = spawnSync('bash', ['-lc', cmd], { encoding: 'utf-8' });
76
- return {
77
- ok: true,
78
- code: res.status ?? 0,
79
- stdout: (res.stdout || '').split('\n').slice(0, 20).join('\n'),
80
- stderr: (res.stderr || '').split('\n').slice(0, 20).join('\n')
81
- };
82
- }
83
-
84
- async function runCase(c){
85
- const payload = {
86
- model,
87
- stream: false,
88
- tool_choice: { type: 'function', function: { name: 'shell_toon' } },
89
- messages: [ { role: 'system', content: systemMsg }, { role: 'user', content: c.user } ],
90
- tools
91
- };
92
- const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
93
- let body; try { body = await res.json(); } catch { return { title: c.title, status: 'http_error', note: res.statusText }; }
94
- const msg = body?.choices?.[0]?.message;
95
- const argStr = msg?.tool_calls?.[0]?.function?.arguments;
96
- if (typeof argStr !== 'string') return { title: c.title, status: 'no_arguments' };
97
- let argumentsObj; try { argumentsObj = JSON.parse(argStr); } catch { return { title: c.title, status: 'arguments_not_json' }; }
98
- const toon = argumentsObj?.toon;
99
- if (typeof toon !== 'string' || !toon.trim()) return { title: c.title, status: 'no_toon' };
100
- const decoded = decodeToon(toon);
101
- if (!decoded.ok) return { title: c.title, status: 'toon_decode_fail', note: decoded.error };
102
- const cmd = decoded.value?.command;
103
- let execRes = undefined;
104
- if (doExec && typeof cmd === 'string' && cmd.trim()) execRes = safeExec(cmd);
105
- return { title: c.title, status: 'ok', toon, decoded: decoded.value, exec: execRes };
106
- }
107
-
108
- async function main(){
109
- console.log(`TOON probe → ${url} model=${model} exec=${doExec?'on':'off'}`);
110
- const results = [];
111
- for (const c of cases) {
112
- try {
113
- const r = await runCase(c);
114
- results.push(r);
115
- console.log(`\n=== ${c.title} ===`);
116
- console.log(`status: ${r.status}`);
117
- if (r.status === 'ok') {
118
- const preview = String(r.toon).split('\n').slice(0,4).join('\n');
119
- console.log('toon:\n' + preview);
120
- console.log('decoded:', JSON.stringify(r.decoded));
121
- if (r.exec && r.exec.ok) {
122
- console.log('exec code:', r.exec.code);
123
- console.log('stdout:\n' + r.exec.stdout);
124
- if (r.exec.stderr) console.log('stderr:\n' + r.exec.stderr);
125
- }
126
- } else if (r.note) {
127
- console.log('note:', r.note);
128
- }
129
- } catch (e) {
130
- results.push({ title: c.title, status: 'exception', note: e?.message || String(e) });
131
- console.log(`\n=== ${c.title} ===`);
132
- console.log('status: exception');
133
- console.log('note:', e?.message || String(e));
134
- }
135
- }
136
- const ok = results.filter(x=>x.status==='ok').length;
137
- console.log('\nSUMMARY:', `${ok}/${results.length} ok`);
138
- for (const r of results) console.log(`- ${r.title}: ${r.status}`);
139
- }
140
-
141
- main().catch(e=>{ console.error('fatal:', e?.message || e); process.exit(1); });