@jsonstudio/rcc 0.89.873 → 0.89.932

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 (39) hide show
  1. package/README.md +44 -0
  2. package/dist/build-info.js +2 -2
  3. package/dist/providers/core/runtime/gemini-cli-http-provider.js +15 -7
  4. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  5. package/dist/providers/core/runtime/responses-provider.js +17 -19
  6. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  7. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.d.ts +3 -0
  8. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +138 -0
  9. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -0
  10. package/dist/server/runtime/http-server/daemon-admin/providers-handler.d.ts +3 -0
  11. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +166 -0
  12. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -0
  13. package/dist/server/runtime/http-server/daemon-admin/quota-handler.d.ts +3 -0
  14. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js +109 -0
  15. package/dist/server/runtime/http-server/daemon-admin/quota-handler.js.map +1 -0
  16. package/dist/server/runtime/http-server/daemon-admin/status-handler.d.ts +3 -0
  17. package/dist/server/runtime/http-server/daemon-admin/status-handler.js +43 -0
  18. package/dist/server/runtime/http-server/daemon-admin/status-handler.js.map +1 -0
  19. package/dist/server/runtime/http-server/daemon-admin-routes.d.ts +19 -0
  20. package/dist/server/runtime/http-server/daemon-admin-routes.js +27 -0
  21. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -0
  22. package/dist/server/runtime/http-server/index.d.ts +5 -0
  23. package/dist/server/runtime/http-server/index.js +34 -1
  24. package/dist/server/runtime/http-server/index.js.map +1 -1
  25. package/dist/server/runtime/http-server/request-executor.d.ts +3 -0
  26. package/dist/server/runtime/http-server/request-executor.js +68 -2
  27. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  28. package/dist/server/runtime/http-server/routes.d.ts +3 -0
  29. package/dist/server/runtime/http-server/routes.js +12 -0
  30. package/dist/server/runtime/http-server/routes.js.map +1 -1
  31. package/package.json +4 -3
  32. package/scripts/analyze-codex-error-failures.mjs +4 -2
  33. package/scripts/analyze-usage-estimate.mjs +240 -0
  34. package/scripts/tests/apply-patch-loop.mjs +266 -7
  35. package/scripts/tests/exec-command-loop.mjs +165 -0
  36. package/scripts/tool-classification-report.ts +281 -0
  37. package/scripts/verification/samples/openai-chat-list-local-files.json +1 -1
  38. package/scripts/verify-codex-error-samples.mjs +4 -1
  39. package/scripts/verify-e2e-toolcall.mjs +52 -0
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * exec_command TOON → JSON 回环验证(模拟 Responses 客户端)。
4
+ *
5
+ * 目标:
6
+ * - 构造一条带 exec_command TOON arguments 的 chat 响应;
7
+ * - 通过 llmswitch-core 的 response 工具过滤管线(ResponseToolArgumentsToonDecodeFilter)做解码;
8
+ * - 使用 codex 的工具注册表 validateToolCall 校验最终 JSON 形状(必须包含 cmd,且不再暴露 toon);
9
+ * - 只从“客户端视角”观察:发送/接收的都是 JSON,TOON 对客户端完全透明。
10
+ */
11
+
12
+ import path from 'node:path';
13
+ import { fileURLToPath, pathToFileURL } from 'node:url';
14
+
15
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
16
+ const repoRoot = path.resolve(__dirname, '..', '..');
17
+ const coreLoaderPath = path.join(repoRoot, 'dist', 'modules', 'llmswitch', 'core-loader.js');
18
+ const coreLoaderUrl = pathToFileURL(coreLoaderPath).href;
19
+
20
+ const { importCoreModule } = await import(coreLoaderUrl);
21
+
22
+ async function main() {
23
+ const { runChatResponseToolFilters } = await importCoreModule('conversion/shared/tool-filter-pipeline');
24
+ const { buildResponsesPayloadFromChat } = await importCoreModule(
25
+ 'conversion/responses/responses-openai-bridge'
26
+ );
27
+
28
+ // 构造一条模拟的 chat 响应,其中 exec_command 使用 TOON 编码参数。
29
+ const chatPayload = {
30
+ id: 'chatcmpl_exec_toon',
31
+ object: 'chat.completion',
32
+ created: Math.floor(Date.now() / 1000),
33
+ model: 'gpt-5.2-codex',
34
+ choices: [
35
+ {
36
+ index: 0,
37
+ message: {
38
+ role: 'assistant',
39
+ content: null,
40
+ tool_calls: [
41
+ {
42
+ id: 'call_exec_toon',
43
+ type: 'function',
44
+ function: {
45
+ name: 'exec_command',
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')
54
+ })
55
+ }
56
+ }
57
+ ]
58
+ },
59
+ finish_reason: 'tool_calls'
60
+ }
61
+ ]
62
+ };
63
+
64
+ // 通过 response 工具管线运行,触发 TOON → JSON 解码。
65
+ const filtered = await runChatResponseToolFilters(chatPayload, {
66
+ entryEndpoint: '/v1/chat/completions',
67
+ requestId: 'req_exec_toon',
68
+ profile: 'openai-chat'
69
+ });
70
+
71
+ const choice = filtered?.choices?.[0];
72
+ const msg = choice?.message;
73
+ const toolCalls = Array.isArray(msg?.tool_calls) ? msg.tool_calls : [];
74
+ if (!toolCalls.length) {
75
+ throw new Error('[exec-command-loop] decoded payload missing tool_calls');
76
+ }
77
+
78
+ const fn = toolCalls[0]?.function;
79
+ if (!fn || typeof fn !== 'object') {
80
+ throw new Error('[exec-command-loop] first tool_call.function missing');
81
+ }
82
+ if (fn.name !== 'exec_command') {
83
+ throw new Error(`[exec-command-loop] expected exec_command, got ${String(fn.name)}`);
84
+ }
85
+ if (typeof fn.arguments !== 'string' || !fn.arguments.trim()) {
86
+ throw new Error('[exec-command-loop] decoded exec_command.arguments must be non-empty JSON string');
87
+ }
88
+
89
+ let args;
90
+ try {
91
+ args = JSON.parse(fn.arguments);
92
+ } catch (error) {
93
+ throw new Error(
94
+ `[exec-command-loop] decoded exec_command arguments not valid JSON: ${
95
+ error instanceof Error ? error.message : String(error ?? 'unknown')
96
+ }`
97
+ );
98
+ }
99
+
100
+ if (!args || typeof args !== 'object') {
101
+ throw new Error('[exec-command-loop] decoded exec_command arguments not an object');
102
+ }
103
+
104
+ // 与 codex exec_command Responses 工具保持一致:cmd 为必填字段,其它为可选字段。
105
+ if (typeof args.cmd !== 'string' || !args.cmd.trim()) {
106
+ throw new Error('[exec-command-loop] decoded exec_command.args missing cmd');
107
+ }
108
+
109
+ const forbiddenKeys = ['toon'];
110
+ for (const key of forbiddenKeys) {
111
+ if (Object.prototype.hasOwnProperty.call(args, key)) {
112
+ throw new Error(`[exec-command-loop] decoded exec_command.args must not expose ${key} to client`);
113
+ }
114
+ }
115
+
116
+ // 延伸验证:基于 chat 结果构建 Responses payload,确保 /v1/responses 视图中的
117
+ // function_call.arguments 同样保持 exec_command JSON 语义,而不会重新出现 toon。
118
+ const responsesPayload = buildResponsesPayloadFromChat(filtered, {
119
+ requestId: 'verify_exec_command_toon'
120
+ });
121
+ const outputItems = Array.isArray(responsesPayload?.output) ? responsesPayload.output : [];
122
+ const fnCall = outputItems.find(
123
+ (item) => item && item.type === 'function_call' && item.name === 'exec_command'
124
+ );
125
+ if (!fnCall) {
126
+ throw new Error('[exec-command-loop] Responses payload missing exec_command function_call');
127
+ }
128
+ if (typeof fnCall.arguments !== 'string' || !fnCall.arguments.trim()) {
129
+ throw new Error(
130
+ '[exec-command-loop] Responses function_call.arguments must be non-empty JSON string'
131
+ );
132
+ }
133
+ let respArgs;
134
+ try {
135
+ respArgs = JSON.parse(fnCall.arguments);
136
+ } catch (error) {
137
+ throw new Error(
138
+ `[exec-command-loop] Responses function_call.arguments not valid JSON: ${
139
+ error instanceof Error ? error.message : String(error ?? 'unknown')
140
+ }`
141
+ );
142
+ }
143
+ if (!respArgs || typeof respArgs !== 'object') {
144
+ throw new Error('[exec-command-loop] Responses function_call.arguments not an object');
145
+ }
146
+ if (typeof respArgs.cmd !== 'string' || !respArgs.cmd.trim()) {
147
+ throw new Error('[exec-command-loop] Responses exec_command.args missing cmd');
148
+ }
149
+ if (Object.prototype.hasOwnProperty.call(respArgs, 'toon')) {
150
+ throw new Error('[exec-command-loop] Responses exec_command.args must not expose toon');
151
+ }
152
+
153
+ console.log(
154
+ `[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
+ );
156
+ console.log('✅ exec_command TOON decode passed (chat + responses views are JSON-only)');
157
+ }
158
+
159
+ main().catch((error) => {
160
+ console.error(
161
+ '[exec-command-loop] FAILED:',
162
+ error instanceof Error ? error.message : String(error ?? 'unknown')
163
+ );
164
+ process.exit(1);
165
+ });
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Generate a tool classification report for codex samples using the
4
+ * virtual router tool classifier (read/write/search/other).
5
+ *
6
+ * Usage:
7
+ * npx tsx scripts/tool-classification-report.ts [samplesRoot] [outputFile]
8
+ *
9
+ * Defaults:
10
+ * samplesRoot = ~/.routecodex/codex-samples
11
+ * outputFile = ./reports/tool-classification-report.md
12
+ */
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import os from 'node:os';
16
+ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
17
+
18
+ import {
19
+ canonicalizeToolName,
20
+ classifyToolCallForReport,
21
+ type ToolCategory
22
+ } from '../sharedmodule/llmswitch-core/src/router/virtual-router/tool-signals.js';
23
+
24
+ type CategoryCounts = Record<ToolCategory, number>;
25
+
26
+ type ExampleEntry = {
27
+ category: ToolCategory;
28
+ file: string;
29
+ snippet?: string;
30
+ };
31
+
32
+ type ToolSummary = {
33
+ total: number;
34
+ categories: CategoryCounts;
35
+ examples: Record<ToolCategory, ExampleEntry[]>;
36
+ };
37
+
38
+ const CATEGORY_KEYS: ToolCategory[] = ['websearch', 'read', 'write', 'search', 'other'];
39
+ const SAMPLE_SUFFIX = '_req_process_tool_filters_request_pre.json';
40
+ const EXAMPLE_LIMIT = 5;
41
+
42
+ async function main(): Promise<void> {
43
+ const [, , rootArg, outputArg] = process.argv;
44
+ const sampleRoot = rootArg || path.join(os.homedir(), '.routecodex', 'codex-samples');
45
+ const outputFile = outputArg || path.join(process.cwd(), 'reports', 'tool-classification-report.md');
46
+
47
+ if (!fs.existsSync(sampleRoot)) {
48
+ throw new Error(`Sample root not found: ${sampleRoot}`);
49
+ }
50
+
51
+ const files = await collectSampleFiles(sampleRoot);
52
+ files.sort((a, b) => a.localeCompare(b));
53
+
54
+ const processedRequests = new Set<string>();
55
+ const summary = new Map<string, ToolSummary>();
56
+ const categoryTotals: CategoryCounts = emptyCounts();
57
+ let totalCalls = 0;
58
+ let classifiedCalls = 0;
59
+ let unclassifiedCalls = 0;
60
+ let skippedDuplicates = 0;
61
+
62
+ for (const filePath of files) {
63
+ const requestKey = extractRequestKey(filePath);
64
+ if (processedRequests.has(requestKey)) {
65
+ skippedDuplicates += 1;
66
+ continue;
67
+ }
68
+ processedRequests.add(requestKey);
69
+
70
+ const data = await readJsonSafe(filePath);
71
+ if (!data) {
72
+ continue;
73
+ }
74
+ const messages = Array.isArray(data.messages) ? data.messages : [];
75
+ for (const msg of messages) {
76
+ if (!msg || typeof msg !== 'object') {
77
+ continue;
78
+ }
79
+ if (msg.role !== 'assistant') {
80
+ continue;
81
+ }
82
+ const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
83
+ for (const call of toolCalls) {
84
+ totalCalls += 1;
85
+ const classification = classifyToolCallForReport(call as any);
86
+ if (!classification) {
87
+ unclassifiedCalls += 1;
88
+ continue;
89
+ }
90
+ classifiedCalls += 1;
91
+ const toolName = canonicalizeToolName(classification.name ?? '') || '(unknown)';
92
+ const entry = ensureSummary(summary, toolName);
93
+ entry.total += 1;
94
+ entry.categories[classification.category] += 1;
95
+ categoryTotals[classification.category] += 1;
96
+ const examples = entry.examples[classification.category];
97
+ if (examples.length < EXAMPLE_LIMIT) {
98
+ examples.push({
99
+ category: classification.category,
100
+ file: path.relative(sampleRoot, filePath),
101
+ snippet: classification.commandSnippet || buildFallbackSnippet(call)
102
+ });
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ const orderedSummaries = Array.from(summary.entries()).sort((a, b) => b[1].total - a[1].total);
109
+ const report = buildReport({
110
+ sampleRoot,
111
+ outputFile,
112
+ filesScanned: files.length,
113
+ uniqueRequests: processedRequests.size,
114
+ skippedDuplicates,
115
+ totalCalls,
116
+ classifiedCalls,
117
+ unclassifiedCalls,
118
+ categoryTotals,
119
+ summaries: orderedSummaries
120
+ });
121
+
122
+ await mkdir(path.dirname(outputFile), { recursive: true });
123
+ await writeFile(outputFile, report.join('\n'), 'utf8');
124
+ console.log(`[tool-classification-report] wrote ${outputFile}`);
125
+ }
126
+
127
+ function emptyCounts(): CategoryCounts {
128
+ return { websearch: 0, read: 0, write: 0, search: 0, other: 0 };
129
+ }
130
+
131
+ function emptyExampleBuckets(): Record<ToolCategory, ExampleEntry[]> {
132
+ return {
133
+ websearch: [],
134
+ read: [],
135
+ write: [],
136
+ search: [],
137
+ other: []
138
+ };
139
+ }
140
+
141
+ function ensureSummary(summary: Map<string, ToolSummary>, name: string): ToolSummary {
142
+ if (!summary.has(name)) {
143
+ summary.set(name, {
144
+ total: 0,
145
+ categories: emptyCounts(),
146
+ examples: emptyExampleBuckets()
147
+ });
148
+ }
149
+ return summary.get(name)!;
150
+ }
151
+
152
+ async function collectSampleFiles(dir: string): Promise<string[]> {
153
+ const entries = await readdir(dir, { withFileTypes: true });
154
+ const files: string[] = [];
155
+ for (const entry of entries) {
156
+ const fullPath = path.join(dir, entry.name);
157
+ if (entry.isDirectory()) {
158
+ const nested = await collectSampleFiles(fullPath);
159
+ files.push(...nested);
160
+ } else if (entry.isFile() && entry.name.endsWith(SAMPLE_SUFFIX)) {
161
+ files.push(fullPath);
162
+ }
163
+ }
164
+ return files;
165
+ }
166
+
167
+ async function readJsonSafe(filePath: string): Promise<any | null> {
168
+ try {
169
+ const raw = await readFile(filePath, 'utf8');
170
+ return JSON.parse(raw);
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ function extractRequestKey(filePath: string): string {
177
+ const base = path.basename(filePath);
178
+ const match = base.match(/^(req_[^_]+_[^_]+)/);
179
+ return match ? match[1] : base;
180
+ }
181
+
182
+ function sanitizeSnippet(snippet?: string): string {
183
+ if (!snippet) {
184
+ return '(no snippet)';
185
+ }
186
+ return snippet.replace(/[\r\n]+/g, ' ').replace(/`/g, "'");
187
+ }
188
+
189
+ function titleCase(category: ToolCategory): string {
190
+ if (category === 'websearch') {
191
+ return 'WebSearch';
192
+ }
193
+ return category.charAt(0).toUpperCase() + category.slice(1);
194
+ }
195
+
196
+ function buildFallbackSnippet(call: any): string | undefined {
197
+ const rawArgs = call?.function?.arguments;
198
+ if (typeof rawArgs === 'string' && rawArgs.trim()) {
199
+ const trimmed = rawArgs.trim().replace(/\s+/g, ' ');
200
+ return trimmed.length > 120 ? `${trimmed.slice(0, 117)}…` : trimmed;
201
+ }
202
+ return undefined;
203
+ }
204
+
205
+ function buildReport(options: {
206
+ sampleRoot: string;
207
+ outputFile: string;
208
+ filesScanned: number;
209
+ uniqueRequests: number;
210
+ skippedDuplicates: number;
211
+ totalCalls: number;
212
+ classifiedCalls: number;
213
+ unclassifiedCalls: number;
214
+ categoryTotals: CategoryCounts;
215
+ summaries: Array<[string, ToolSummary]>;
216
+ }): string[] {
217
+ const {
218
+ sampleRoot,
219
+ filesScanned,
220
+ uniqueRequests,
221
+ skippedDuplicates,
222
+ totalCalls,
223
+ classifiedCalls,
224
+ unclassifiedCalls,
225
+ categoryTotals,
226
+ summaries
227
+ } = options;
228
+
229
+ const lines: string[] = [];
230
+ lines.push('# Tool Classification Report');
231
+ lines.push('');
232
+ lines.push(`- Sample root: ${sampleRoot}`);
233
+ lines.push(`- Files scanned: ${filesScanned}`);
234
+ lines.push(`- Unique requests: ${uniqueRequests}`);
235
+ lines.push(`- Skipped duplicates: ${skippedDuplicates}`);
236
+ lines.push(`- Tool calls processed: ${totalCalls}`);
237
+ lines.push(`- Classified calls: ${classifiedCalls}`);
238
+ lines.push(`- Unclassified calls: ${unclassifiedCalls}`);
239
+ lines.push(`- Generated at: ${new Date().toISOString()}`);
240
+ lines.push('');
241
+ lines.push('## Category Totals');
242
+ lines.push('');
243
+ for (const category of CATEGORY_KEYS) {
244
+ lines.push(`- ${category}: ${categoryTotals[category]}`);
245
+ }
246
+ lines.push('');
247
+ lines.push('## Per-Tool Summary');
248
+ lines.push('');
249
+ const categoryHeader = CATEGORY_KEYS.map((key) => titleCase(key)).join(' | ');
250
+ lines.push(`| Tool | Total | ${categoryHeader} |`);
251
+ lines.push(`| --- | ---: | ${CATEGORY_KEYS.map(() => '---:').join(' | ')} |`);
252
+ for (const [name, info] of summaries) {
253
+ const counts = CATEGORY_KEYS.map((key) => info.categories[key]);
254
+ lines.push(`| ${name} | ${info.total} | ${counts.join(' | ')} |`);
255
+ }
256
+ lines.push('');
257
+ for (const [name, info] of summaries) {
258
+ lines.push(`### ${name} (total ${info.total})`);
259
+ const countSummary = CATEGORY_KEYS.map((key) => `${key}: ${info.categories[key]}`).join(', ');
260
+ lines.push(`Counts → ${countSummary}`);
261
+ for (const category of CATEGORY_KEYS) {
262
+ const examples = info.examples[category];
263
+ if (!examples.length) {
264
+ continue;
265
+ }
266
+ lines.push(`- ${category} examples:`);
267
+ for (const example of examples) {
268
+ lines.push(
269
+ ` - ${example.file}: \`${sanitizeSnippet(example.snippet)}\``
270
+ );
271
+ }
272
+ }
273
+ lines.push('');
274
+ }
275
+ return lines;
276
+ }
277
+
278
+ main().catch((err) => {
279
+ console.error('[tool-classification-report] failed', err);
280
+ process.exit(1);
281
+ });
@@ -3,7 +3,7 @@
3
3
  "source": "synthetic",
4
4
  "endpoint": "/v1/chat/completions",
5
5
  "payload": {
6
- "model": "glm-4.6",
6
+ "model": "glm-4.7",
7
7
  "temperature": 0,
8
8
  "stream": false,
9
9
  "tool_choice": {
@@ -24,7 +24,10 @@ const ROOT =
24
24
  : path.join(os.homedir(), '.routecodex', 'errorsamples');
25
25
 
26
26
  const ERROR_PATTERNS = [
27
- 'failed to parse exec_command arguments',
27
+ // exec_command / apply_patch 参数解码错误(CLI 侧报错)
28
+ 'failed to parse function arguments: missing field `cmd`',
29
+ 'failed to parse function arguments: missing field `input`',
30
+ // 历史回滚:统一 diff 校验失败
28
31
  'apply_patch verification failed'
29
32
  ];
30
33
 
@@ -64,6 +64,9 @@ async function main() {
64
64
  await runToolcallVerification();
65
65
  console.log('✅ 端到端工具调用校验通过');
66
66
 
67
+ await runDaemonAdminSmokeCheck();
68
+ await runConfigV2ProvidersSmokeCheck();
69
+
67
70
  // 附加:Gemini CLI 配置健康性快速检查(仅尝试初始化,不做请求)
68
71
  await runGeminiCliStartupCheck();
69
72
  } finally {
@@ -163,6 +166,55 @@ async function runToolcallVerification() {
163
166
  }
164
167
  }
165
168
 
169
+ async function runDaemonAdminSmokeCheck() {
170
+ // 仅做最小的健康性探测:确保 daemon 管理类只读 API 可用,不做语义校验。
171
+ try {
172
+ const res = await fetch(`${VERIFY_BASE}/daemon/status`);
173
+ if (!res.ok) {
174
+ throw new Error(`daemon/status HTTP ${res.status}`);
175
+ }
176
+ const json = await res.json();
177
+ if (!json || typeof json !== 'object' || json.ok !== true) {
178
+ throw new Error('daemon/status 返回值不符合预期形态');
179
+ }
180
+ } catch (error) {
181
+ console.error('[verify:e2e-toolcall] /daemon/status smoke 检查失败:', error);
182
+ throw error;
183
+ }
184
+
185
+ const paths = ['/daemon/credentials', '/quota/summary', '/providers/runtimes'];
186
+ for (const path of paths) {
187
+ try {
188
+ const res = await fetch(`${VERIFY_BASE}${path}`);
189
+ if (!res.ok) {
190
+ throw new Error(`${path} HTTP ${res.status}`);
191
+ }
192
+ // 只要 JSON 可解析即可视为通过,避免把语义校验塞到这里。
193
+ await res.json().catch(() => ({}));
194
+ } catch (error) {
195
+ console.error('[verify:e2e-toolcall] Daemon admin smoke 检查失败:', path, error);
196
+ throw error;
197
+ }
198
+ }
199
+ }
200
+
201
+ async function runConfigV2ProvidersSmokeCheck() {
202
+ try {
203
+ const res = await fetch(`${VERIFY_BASE}/config/providers/v2`);
204
+ if (!res.ok) {
205
+ throw new Error(`/config/providers/v2 HTTP ${res.status}`);
206
+ }
207
+ const json = await res.json();
208
+ if (!Array.isArray(json)) {
209
+ // 出于兼容考虑,只要不是致命错误就通过;Config V2 视图是辅助信息。
210
+ console.warn('[verify:e2e-toolcall] /config/providers/v2 未返回数组形态,跳过进一步校验');
211
+ }
212
+ } catch (error) {
213
+ console.error('[verify:e2e-toolcall] Config V2 providers smoke 检查失败:', error);
214
+ // 不将 Config V2 视图问题视为构建阻断条件,以免挡住主链路;仅提示。
215
+ }
216
+ }
217
+
166
218
  async function runGeminiCliStartupCheck() {
167
219
  if (!GEMINI_CLI_CONFIG || !fs.existsSync(GEMINI_CLI_CONFIG)) {
168
220
  return;