dsh-tap 0.20.0
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.
- package/AGENTS.md +99 -0
- package/CHANGELOG.md +751 -0
- package/LICENSE +21 -0
- package/README.en.md +59 -0
- package/README.md +256 -0
- package/cordis.patch.yml +385 -0
- package/core/bridge.js +698 -0
- package/core/json-store.js +91 -0
- package/core/rotation.js +108 -0
- package/core/usage-meter.js +176 -0
- package/docs/diagnosis-cache-quota.md +181 -0
- package/docs/diagnosis-qoder-flash.md +67 -0
- package/docs/diagnosis-trae-3003.md +302 -0
- package/docs/goals/bridge-port-host-split.md +91 -0
- package/docs/goals/desktop-adaptation.md +106 -0
- package/docs/goals/qoder-cn-provider-design.md +308 -0
- package/docs/goals/settings-card-ux-redesign-plan.md +1680 -0
- package/docs/goals/settings-card-ux-redesign.md +162 -0
- package/docs/goals/trae-agent-v3.md +41 -0
- package/docs/goals/trae-work-cn-repair.md +44 -0
- package/docs/goals/v0.8-/351/242/235/345/272/246/345/217/257/350/247/201-/346/250/241/345/236/213/345/212/250/346/200/201/345/214/226-/345/244/232/346/234/215/345/212/241/345/225/206.md +85 -0
- package/docs/pitfalls.md +105 -0
- package/docs/reverse/trae-cloud-api.md +218 -0
- package/docs/reverse/trae-model-catalog.md +129 -0
- package/docs/reverse/traework-cn.md +520 -0
- package/docs/rules/STATE.md +198 -0
- package/docs/rules/content-moderation.md +54 -0
- package/docs/rules/dev-role-boundary.md +94 -0
- package/docs/rules/extra-providers.md +42 -0
- package/docs/rules/gateway-facts.md +91 -0
- package/docs/rules/oauth-handshake.md +76 -0
- package/docs/rules/prompt-cache.md +93 -0
- package/docs/rules/quota-signals.md +125 -0
- package/docs/rules/routing.md +84 -0
- package/docs/rules/templates/oauth-reverse-checklist.md +42 -0
- package/docs/rules/trae-surface.md +242 -0
- package/docs/rules/ua-validation.md +81 -0
- package/host-config.js +282 -0
- package/index.js +2306 -0
- package/lib/client.js +2893 -0
- package/local-scan.js +104 -0
- package/package.json +82 -0
- package/providers/ark/index.js +11 -0
- package/providers/bailian/index.js +10 -0
- package/providers/bigmodel/index.js +11 -0
- package/providers/codebuddy/agenttool.js +122 -0
- package/providers/codebuddy/catalog.js +227 -0
- package/providers/codebuddy/errors.js +44 -0
- package/providers/codebuddy/headers.js +36 -0
- package/providers/codebuddy/images.js +125 -0
- package/providers/codebuddy/index.js +123 -0
- package/providers/codebuddy/oauth.js +279 -0
- package/providers/deepseek/index.js +11 -0
- package/providers/moonshot/index.js +11 -0
- package/providers/openai-compat.js +177 -0
- package/providers/openrouter/index.js +27 -0
- package/providers/qoder/catalog.js +145 -0
- package/providers/qoder/cosy.js +419 -0
- package/providers/qoder/gateway.js +563 -0
- package/providers/qoder/index.js +116 -0
- package/providers/qoder/oauth.js +364 -0
- package/providers/qoder/qoder_auth.wasm +0 -0
- package/providers/qoder/quota.js +56 -0
- package/providers/qwen/index.js +15 -0
- package/providers/tool-pairing.js +129 -0
- package/providers/trae/catalog.js +103 -0
- package/providers/trae/errors.js +85 -0
- package/providers/trae/gateway.js +853 -0
- package/providers/trae/index.js +126 -0
- package/providers/trae/oauth.js +443 -0
- package/providers/trae/quota.js +75 -0
- package/providers/trae/remote.js +365 -0
- package/scripts/capture-cache.mjs +65 -0
- package/scripts/capture-traffic.mjs +83 -0
- package/scripts/hermes-probe-dev-role.mjs +263 -0
- package/scripts/measure-latency.mjs +253 -0
- package/scripts/probe-ark-thinking.mjs +298 -0
- package/scripts/probe-cache-decline.mjs +292 -0
- package/scripts/probe-cache-ttl.mjs +221 -0
- package/scripts/probe-cache.mjs +156 -0
- package/scripts/probe-codebuddy-efforts.mjs +355 -0
- package/scripts/probe-codebuddy-tier-wiring.mjs +244 -0
- package/scripts/probe-effort-gaps.mjs +133 -0
- package/scripts/probe-media.mjs +115 -0
- package/scripts/probe-moderation.mjs +159 -0
- package/scripts/probe-oauth.mjs +617 -0
- package/scripts/probe-qoder-attribution-arm8.mjs +92 -0
- package/scripts/probe-qoder-attribution-arm9.mjs +102 -0
- package/scripts/probe-qoder-attribution.mjs +266 -0
- package/scripts/probe-qoder-flash-confirm.mjs +94 -0
- package/scripts/probe-qoder-live.mjs +344 -0
- package/scripts/probe-qoder-matrix.mjs +274 -0
- package/scripts/probe-qoder-null-content.mjs +153 -0
- package/scripts/probe-qoder-pairing.mjs +308 -0
- package/scripts/probe-qoder-quota.mjs +162 -0
- package/scripts/probe-qoder-thinking-config.mjs +52 -0
- package/scripts/probe-qoder-thinking-efforts.mjs +269 -0
- package/scripts/probe-quota.mjs +136 -0
- package/scripts/probe-quota2.mjs +144 -0
- package/scripts/probe-routing.mjs +226 -0
- package/scripts/probe-trae-3003-diagnosis.mjs +139 -0
- package/scripts/probe-trae-agent-v3.mjs +399 -0
- package/scripts/probe-trae-efforts.mjs +149 -0
- package/scripts/probe-trae-live.mjs +147 -0
- package/scripts/probe-trae-max-effort.mjs +179 -0
- package/scripts/probe-trae-model-routing.mjs +381 -0
- package/scripts/probe-trae-thinking-scene.mjs +238 -0
- package/scripts/probe-trae-transport-outage.mjs +176 -0
- package/scripts/probe-ua.mjs +508 -0
- package/scripts/trae-model-catalog.mjs +632 -0
- package/scripts/verify-agents-md.mjs +45 -0
- package/scripts/verify-bridge.mjs +1041 -0
- package/scripts/verify-core-generic.mjs +302 -0
- package/scripts/verify-desktop-acceptance.mjs +184 -0
- package/scripts/verify-host-config.mjs +265 -0
- package/scripts/verify-models.mjs +390 -0
- package/scripts/verify-providers.mjs +255 -0
- package/scripts/verify-qoder-provider.mjs +1020 -0
- package/scripts/verify-rotation.mjs +308 -0
- package/scripts/verify-trae-model-catalog.mjs +284 -0
- package/scripts/verify-trae-provider.mjs +1451 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* probe-codebuddy-efforts.mjs — CodeBuddy 通道「模型思考强度」现状探测。
|
|
4
|
+
*
|
|
5
|
+
* 目的:cordis.patch.yml 的 `reasoningEfforts` 档位表是静态清单的一部分
|
|
6
|
+
* (网关 /v3/config 只给**默认档位**不给档位清单),所以要更新档位表只能实测。
|
|
7
|
+
* 本脚本一次跑完两件事:
|
|
8
|
+
*
|
|
9
|
+
* 1) catalog 面:GET /v3/config,逐模型打印网关自述的 reasoning 声明
|
|
10
|
+
* (`reasoning.effort` 默认档 + 是否有该键),作为"档位表该不该有"的
|
|
11
|
+
* 第一手依据。
|
|
12
|
+
* 2) chat 面:对每个目标模型逐档发一次流式请求(omit/off/low/medium/high/max),
|
|
13
|
+
* 记录 HTTP 结果、reasoning_content 字符数、content 字符数、finish_reason、
|
|
14
|
+
* 响应回显 model 与 usage。判据:
|
|
15
|
+
* - 某档 HTTP 报错 / 带内错误帧 → 该档**网关不接受**(档位表不应含它);
|
|
16
|
+
* - omit(不传参)就有 reasoning_content → 模型"无参也会想",
|
|
17
|
+
* 档位表不该给 `off`;
|
|
18
|
+
* - omit 无 reasoning_content 而 off 也无 → `off` = 省略参数语义成立;
|
|
19
|
+
* - 各档长度**非单调**属正常(推理长度自适应,见 patch 头注释)。
|
|
20
|
+
*
|
|
21
|
+
* 证据落盘:docs/probes/codebuddy-efforts-<YYYY-MM-DD>.json(预注册 expect 一并写入)。
|
|
22
|
+
*
|
|
23
|
+
* 用法:
|
|
24
|
+
* node scripts/probe-codebuddy-efforts.mjs --catalog # 只打目录面(快)
|
|
25
|
+
* node scripts/probe-codebuddy-efforts.mjs # 目录 + 全档矩阵
|
|
26
|
+
* node scripts/probe-codebuddy-efforts.mjs --models glm-5.3,kimi-k2.8-preview
|
|
27
|
+
* node scripts/probe-codebuddy-efforts.mjs --levels omit,off,high
|
|
28
|
+
* node scripts/probe-codebuddy-efforts.mjs --interval 800 # 每发间隔 ms(默认 600)
|
|
29
|
+
*
|
|
30
|
+
* 凭据:CODEBUDDY_API_KEY(env)或 ~/.dsh/.credentials.yaml(与插件同解析顺序)。
|
|
31
|
+
* 红线:单账号、只读优先、发问间隔 ≥600ms(STATE.md「探测纪律」)。
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
35
|
+
import { homedir } from 'node:os'
|
|
36
|
+
import { join, dirname } from 'node:path'
|
|
37
|
+
import { fileURLToPath } from 'node:url'
|
|
38
|
+
|
|
39
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
40
|
+
const BASE = 'https://copilot.tencent.com'
|
|
41
|
+
const PATCH_FILE = join(ROOT, 'cordis.patch.yml')
|
|
42
|
+
|
|
43
|
+
// 目录面方言(catalog.js / verify-models 同款):CLI 形态 UA + x-api-key。
|
|
44
|
+
const CATALOG_HEADERS = (key) => ({
|
|
45
|
+
accept: 'application/json',
|
|
46
|
+
'x-api-key': key,
|
|
47
|
+
'user-agent': 'CLI/unknown CodeBuddy/2.136.0',
|
|
48
|
+
'x-product': 'SaaS',
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// 聊天面方言:与 cordis.patch.yml 的 codebuddy provider 头一致(不含 Authorization,
|
|
52
|
+
// 由 probe 逐请求拼 ck_ key)。
|
|
53
|
+
const CHAT_HEADERS = {
|
|
54
|
+
'User-Agent': 'CodeBuddyCode/1.0',
|
|
55
|
+
'X-IDE-Type': 'CLI',
|
|
56
|
+
'X-IDE-Name': 'CLI',
|
|
57
|
+
'X-IDE-Version': '2.133.1',
|
|
58
|
+
'X-Product-Version': '2.133.1',
|
|
59
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
60
|
+
'X-Private-Data': 'false',
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 档位 → 线值;omit = 不传 reasoning_effort(基线段);off = 插件现行语义
|
|
66
|
+
* (off 的线值是 null ⇒ 省略参数)。offwire/alias 臂用来判定"目录声明
|
|
67
|
+
* canDisableThinking 的模型到底该发什么线值才能真的关思考"。
|
|
68
|
+
*/
|
|
69
|
+
const LEVEL_WIRE = {
|
|
70
|
+
omit: undefined,
|
|
71
|
+
off: null,
|
|
72
|
+
low: 'low',
|
|
73
|
+
medium: 'medium',
|
|
74
|
+
high: 'high',
|
|
75
|
+
max: 'max',
|
|
76
|
+
// 别名/关闭思考候选拼写(判定用,不是候选档位表的默认成员)
|
|
77
|
+
offwire: 'off',
|
|
78
|
+
none: 'none',
|
|
79
|
+
minimal: 'minimal',
|
|
80
|
+
disabled: 'disabled',
|
|
81
|
+
auto: 'auto',
|
|
82
|
+
xhigh: 'xhigh',
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const argv = process.argv.slice(2)
|
|
86
|
+
const argValue = (name) => {
|
|
87
|
+
const i = argv.indexOf(name)
|
|
88
|
+
return i === -1 ? null : argv[i + 1]
|
|
89
|
+
}
|
|
90
|
+
const CATALOG_ONLY = argv.includes('--catalog')
|
|
91
|
+
const INTERVAL = Number(argValue('--interval') ?? 600)
|
|
92
|
+
const REPEAT = Math.max(1, Number(argValue('--repeat') ?? 1))
|
|
93
|
+
const SUMMARY = argValue('--summarize')
|
|
94
|
+
const OUT = argValue('--out') ?? join(ROOT, 'docs', 'probes', `codebuddy-efforts-${new Date().toISOString().slice(0, 10)}.json`)
|
|
95
|
+
|
|
96
|
+
/** 预注册预期(跑前写死,跑后逐条判定命中/推翻)。 */
|
|
97
|
+
const EXPECT = {
|
|
98
|
+
P1: '无参(omit)就产 reasoning_content 的模型:kimi-k2.7/k3/k3-1/minimax-m2.7 —— 这四家档位表不含 off',
|
|
99
|
+
P2: 'off/low/medium/high/max 五种拼写对思考型模型全部 HTTP 200 可接受(不出现 400/11102 类拒绝)',
|
|
100
|
+
P3: '目录新 CLI 模型(glm-5.3 / glm-5.3-flash / kimi-k2.8-preview / hy3-x / hy4-preview / deepseek-v4.1-flash)同样接受五档拼写',
|
|
101
|
+
P4: '各档 reasoning 长度自适应、非严格单调(off < max 不必然成立)',
|
|
102
|
+
P5: '目录 reasoning 只给默认档,不含档位清单(即档位表无法从目录派生)——2026-09-22 复查已被推翻:新模型带 supportedEfforts/canDisableThinking/defaultEffort',
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function loadApiKey() {
|
|
106
|
+
if (process.env.CODEBUDDY_API_KEY) return process.env.CODEBUDDY_API_KEY
|
|
107
|
+
const credFile = join(homedir(), '.dsh', '.credentials.yaml')
|
|
108
|
+
if (existsSync(credFile)) {
|
|
109
|
+
const m = readFileSync(credFile, 'utf8').match(/^\s*CODEBUDDY_API_KEY:\s*["']?([^"'\s]+)["']?\s*$/m)
|
|
110
|
+
if (m) return m[1]
|
|
111
|
+
}
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** patch 静态清单的 id 顺序(YAML 行扫描,布局变了就要改这里)。 */
|
|
116
|
+
function parsePatchIds() {
|
|
117
|
+
const ids = []
|
|
118
|
+
let inModels = false
|
|
119
|
+
for (const line of readFileSync(PATCH_FILE, 'utf8').split('\n')) {
|
|
120
|
+
if (/^\s*models:\s*$/.test(line)) { inModels = true; continue }
|
|
121
|
+
if (!inModels) continue
|
|
122
|
+
const m = line.match(/^\s*-\s*id:\s*(\S+)\s*$/)
|
|
123
|
+
if (m) { ids.push(m[1]); continue }
|
|
124
|
+
if (/^\s{0,10}\S/.test(line) && !/^\s{10,}/.test(line) && line.trim()) break
|
|
125
|
+
}
|
|
126
|
+
return ids
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function fetchCatalog(key) {
|
|
130
|
+
const res = await fetch(`${BASE}/v3/config`, { headers: CATALOG_HEADERS(key) })
|
|
131
|
+
const body = await res.json().catch(() => null)
|
|
132
|
+
if (!res.ok || body?.code !== 0) {
|
|
133
|
+
throw new Error(`catalog HTTP ${res.status} code=${body?.code} ${body?.msg ?? ''}`)
|
|
134
|
+
}
|
|
135
|
+
const data = body.data ?? {}
|
|
136
|
+
const cliEnabled = new Set((data.agents ?? []).find((a) => a.name === 'cli')?.models ?? [])
|
|
137
|
+
return {
|
|
138
|
+
fetchedAt: new Date().toISOString(),
|
|
139
|
+
models: (data.models ?? [])
|
|
140
|
+
.filter((m) => typeof m?.id === 'string')
|
|
141
|
+
.map((m) => ({
|
|
142
|
+
id: m.id,
|
|
143
|
+
name: m.name ?? m.id,
|
|
144
|
+
maxInputTokens: m.maxInputTokens ?? null,
|
|
145
|
+
maxOutputTokens: m.maxOutputTokens ?? null,
|
|
146
|
+
images: m.supportsImages === true,
|
|
147
|
+
cli: cliEnabled.has(m.id),
|
|
148
|
+
reasoning: m.reasoning ?? null,
|
|
149
|
+
})),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
154
|
+
|
|
155
|
+
/** 单档单发:流式聚合 reasoning/content 长度 + 响应回显 model + usage。 */
|
|
156
|
+
async function probeArm(id, level, key) {
|
|
157
|
+
const started = Date.now()
|
|
158
|
+
const body = {
|
|
159
|
+
model: id,
|
|
160
|
+
messages: [{ role: 'user', content: '9.11和9.9哪个大?先想清楚再给一句话结论。' }],
|
|
161
|
+
max_tokens: 400,
|
|
162
|
+
stream: true,
|
|
163
|
+
}
|
|
164
|
+
const wire = LEVEL_WIRE[level]
|
|
165
|
+
if (wire) body.reasoning_effort = wire
|
|
166
|
+
let res
|
|
167
|
+
try {
|
|
168
|
+
res = await fetch(`${BASE}/v2/chat/completions`, {
|
|
169
|
+
method: 'POST',
|
|
170
|
+
headers: { ...CHAT_HEADERS, Authorization: `Bearer ${key}` },
|
|
171
|
+
body: JSON.stringify(body),
|
|
172
|
+
})
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return { level, error: `network: ${err.message}`, ms: Date.now() - started }
|
|
175
|
+
}
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const text = await res.text().catch(() => '')
|
|
178
|
+
return { level, http: res.status, error: text.slice(0, 200), ms: Date.now() - started }
|
|
179
|
+
}
|
|
180
|
+
let buf = ''
|
|
181
|
+
let reasonChars = 0
|
|
182
|
+
let contentChars = 0
|
|
183
|
+
let finish = null
|
|
184
|
+
let echoModel = null
|
|
185
|
+
let usage = null
|
|
186
|
+
let streamErr = null
|
|
187
|
+
let chunks = 0
|
|
188
|
+
const dec = new TextDecoder()
|
|
189
|
+
try {
|
|
190
|
+
const reader = res.body.getReader()
|
|
191
|
+
for (;;) {
|
|
192
|
+
const { done, value } = await reader.read()
|
|
193
|
+
if (done) break
|
|
194
|
+
buf += dec.decode(value, { stream: true })
|
|
195
|
+
let nl
|
|
196
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
197
|
+
const line = buf.slice(0, nl).trim()
|
|
198
|
+
buf = buf.slice(nl + 1)
|
|
199
|
+
if (!line.startsWith('data:')) continue
|
|
200
|
+
const payload = line.slice(5).trim()
|
|
201
|
+
if (payload === '[DONE]') continue
|
|
202
|
+
let j
|
|
203
|
+
try { j = JSON.parse(payload) } catch { continue }
|
|
204
|
+
chunks++
|
|
205
|
+
if (j.error || (j.code != null && j.code !== 0)) streamErr = payload.slice(0, 200)
|
|
206
|
+
if (typeof j.model === 'string') echoModel = j.model
|
|
207
|
+
if (j.usage) usage = j.usage
|
|
208
|
+
const ch = j.choices?.[0]
|
|
209
|
+
if (ch?.delta?.reasoning_content) reasonChars += ch.delta.reasoning_content.length
|
|
210
|
+
if (ch?.delta?.content) contentChars += ch.delta.content.length
|
|
211
|
+
if (ch?.finish_reason) finish = ch.finish_reason
|
|
212
|
+
}
|
|
213
|
+
if (reasonChars + contentChars > 4000) break
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// 部分流也是证据
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
level,
|
|
220
|
+
http: res.status,
|
|
221
|
+
reasonChars,
|
|
222
|
+
contentChars,
|
|
223
|
+
finish,
|
|
224
|
+
echoModel,
|
|
225
|
+
streamErr,
|
|
226
|
+
chunks,
|
|
227
|
+
reasoningTokens: usage?.completion_tokens_details?.reasoning_tokens ?? null,
|
|
228
|
+
totalTokens: usage?.total_tokens ?? null,
|
|
229
|
+
ms: Date.now() - started,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 证据 → 档位表决策(--summarize <evidence.json>)。
|
|
235
|
+
*
|
|
236
|
+
* 判据(与 patch 头注释同一套):
|
|
237
|
+
* 1. 全臂 11102 → 目录里有、/v2 不可路由 → **不进静态清单**(routing.md R-R3);
|
|
238
|
+
* 2. 某档 HTTP/带内报错 → 该档不进表(11150 = invalid_reasoning_effort);
|
|
239
|
+
* 3. off 当且仅当**省略参数**(omit/off 两臂,等价请求)实测不产
|
|
240
|
+
* reasoning_content —— 否则目录/宿主选择器上的 "Off" 就是个假开关;
|
|
241
|
+
* 4. 其余实测可接受档位全部进表(顺序按 EFFORT_TIER_ORDER)。
|
|
242
|
+
*/
|
|
243
|
+
if (SUMMARY) {
|
|
244
|
+
const ev = JSON.parse(readFileSync(SUMMARY, 'utf8'))
|
|
245
|
+
const median = (xs) => {
|
|
246
|
+
const s = [...xs].sort((a, b) => a - b)
|
|
247
|
+
return s.length ? s[Math.floor(s.length / 2)] : null
|
|
248
|
+
}
|
|
249
|
+
const byModel = new Map()
|
|
250
|
+
for (const arm of ev.arms) {
|
|
251
|
+
if (!byModel.has(arm.id)) byModel.set(arm.id, [])
|
|
252
|
+
byModel.get(arm.id).push(arm)
|
|
253
|
+
}
|
|
254
|
+
const TARGET_LEVELS = ['low', 'medium', 'high', 'max']
|
|
255
|
+
console.log(`# ${SUMMARY}`)
|
|
256
|
+
console.log(`# catalog fetchedAt=${ev.catalog?.fetchedAt ?? '?'}`)
|
|
257
|
+
console.log('')
|
|
258
|
+
for (const [id, arms] of byModel) {
|
|
259
|
+
const errors = arms.filter((a) => a.error)
|
|
260
|
+
const codes = [...new Set(errors.map((a) => (/"(?:code)":\s*(\d+)/.exec(a.error ?? '') ?? [])[1]).filter(Boolean))]
|
|
261
|
+
const notRouted = errors.length === arms.length && codes.includes('11102')
|
|
262
|
+
const omitArms = arms.filter((a) => (a.level === 'omit' || a.level === 'off') && !a.error && !a.streamErr)
|
|
263
|
+
const baseline = omitArms.length ? median(omitArms.map((a) => a.reasonChars)) : null
|
|
264
|
+
const detail = arms
|
|
265
|
+
.filter((a) => a.level !== 'omit' && a.level !== 'off')
|
|
266
|
+
.map((a) => `${a.level}=${a.error ? `ERR${(/"(?:code)":\s*(\d+)/.exec(a.error) ?? [])[1] ?? '?'}` : a.reasonChars}`)
|
|
267
|
+
.join(' ')
|
|
268
|
+
if (notRouted) {
|
|
269
|
+
console.log(`${id.padEnd(20)} omit=${baseline ?? '-'} ${detail}`)
|
|
270
|
+
console.log(`${' '.repeat(20)} → 不可路由(11102):不进静态清单`)
|
|
271
|
+
continue
|
|
272
|
+
}
|
|
273
|
+
const accepted = TARGET_LEVELS.filter((level) => arms.some((a) => a.level === level && !a.error && !a.streamErr))
|
|
274
|
+
const rejected = TARGET_LEVELS.filter((level) => !accepted.includes(level))
|
|
275
|
+
const tiers = [...(baseline === 0 ? ['off'] : []), ...accepted]
|
|
276
|
+
console.log(`${id.padEnd(20)} omit=${baseline ?? '-'} ${detail}`)
|
|
277
|
+
console.log(`${' '.repeat(20)} → 档位表 ${JSON.stringify(tiers)}${baseline === 0 ? '' : `(省略参数会思考 → 不给 off)`}${rejected.length ? `|拒绝档位 ${rejected.join('/')}` : ''}`)
|
|
278
|
+
}
|
|
279
|
+
process.exit(0)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const key = loadApiKey()
|
|
283
|
+
if (!key) {
|
|
284
|
+
console.error('CODEBUDDY_API_KEY not found (env or ~/.dsh/.credentials.yaml)')
|
|
285
|
+
process.exit(1)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log('== catalog 面:GET /v3/config ==')
|
|
289
|
+
const catalog = await fetchCatalog(key)
|
|
290
|
+
console.log(`fetchedAt=${catalog.fetchedAt} models=${catalog.models.length}`)
|
|
291
|
+
console.log('id'.padEnd(26), 'cli', 'ctx/max'.padEnd(18), 'reasoning')
|
|
292
|
+
for (const m of catalog.models) {
|
|
293
|
+
console.log(
|
|
294
|
+
m.id.padEnd(26),
|
|
295
|
+
(m.cli ? ' ✓ ' : ' '),
|
|
296
|
+
`${m.maxInputTokens ?? '-'}/${m.maxOutputTokens ?? '-'}`.padEnd(18),
|
|
297
|
+
m.reasoning ? JSON.stringify(m.reasoning) : '(none)',
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
const withReasoning = catalog.models.filter((m) => m.reasoning)
|
|
301
|
+
console.log(`\nreasoning 键存在:${withReasoning.length}/${catalog.models.length}`)
|
|
302
|
+
const effortValues = [...new Set(withReasoning.map((m) => JSON.stringify(m.reasoning?.effort ?? m.reasoning?.defaultEffort ?? null)))]
|
|
303
|
+
console.log(`reasoning 默认档取值集合:${effortValues.join(', ')}`)
|
|
304
|
+
const tierListLike = withReasoning.filter((m) => Array.isArray(m.reasoning?.supportedEfforts) || Array.isArray(m.reasoning?.efforts) || Array.isArray(m.reasoning?.levels))
|
|
305
|
+
console.log(`目录带档位清单的模型:${tierListLike.length}${tierListLike.length ? ' → ' + tierListLike.map((m) => `${m.id}=${JSON.stringify(m.reasoning.supportedEfforts ?? m.reasoning.efforts ?? m.reasoning.levels)}`).join(' ') : ''}(P5 判定依据)`)
|
|
306
|
+
const disableLike = withReasoning.filter((m) => 'canDisableThinking' in (m.reasoning ?? {}))
|
|
307
|
+
console.log(`目录带 canDisableThinking 的模型:${disableLike.length}${disableLike.length ? ' → ' + disableLike.map((m) => `${m.id}=${m.reasoning.canDisableThinking}`).join(' ') : ''}`)
|
|
308
|
+
|
|
309
|
+
if (CATALOG_ONLY && !SUMMARY) {
|
|
310
|
+
mkdirSync(dirname(OUT), { recursive: true })
|
|
311
|
+
writeFileSync(OUT, JSON.stringify({ expect: EXPECT, catalog, arms: [] }, null, 2))
|
|
312
|
+
console.log(`\n证据落盘:${OUT}`)
|
|
313
|
+
process.exit(0)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const patchIds = parsePatchIds()
|
|
317
|
+
const explicit = argValue('--models')
|
|
318
|
+
const targets = explicit
|
|
319
|
+
? explicit.split(',').map((s) => s.trim()).filter(Boolean)
|
|
320
|
+
: [...new Set([...patchIds, ...catalog.models.filter((m) => m.cli).map((m) => m.id)])]
|
|
321
|
+
const levels = (argValue('--levels') ?? 'omit,off,low,medium,high,max').split(',').map((s) => s.trim()).filter(Boolean)
|
|
322
|
+
for (const l of levels) {
|
|
323
|
+
if (!(l in LEVEL_WIRE)) {
|
|
324
|
+
console.error(`未知档位 ${l}(可选 ${Object.keys(LEVEL_WIRE).join('/')})`)
|
|
325
|
+
process.exit(1)
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log(`\n== chat 面:${targets.length} 模型 × ${levels.length} 档(间隔 ${INTERVAL}ms)==`)
|
|
330
|
+
const arms = []
|
|
331
|
+
for (const id of targets) {
|
|
332
|
+
console.log(`\n${id}`)
|
|
333
|
+
const catalogEntry = catalog.models.find((m) => m.id === id) ?? null
|
|
334
|
+
for (const level of levels) {
|
|
335
|
+
for (let rep = 0; rep < REPEAT; rep++) {
|
|
336
|
+
const label = REPEAT > 1 ? `${level}#${rep + 1}` : level
|
|
337
|
+
process.stdout.write(` ${label.padEnd(10)}`)
|
|
338
|
+
const r = await probeArm(id, level, key)
|
|
339
|
+
if (r.error) console.log(` ✗ ${r.error}`)
|
|
340
|
+
else {
|
|
341
|
+
const sentinel = r.echoModel && r.echoModel !== id ? ` ⚠echo=${r.echoModel}` : ''
|
|
342
|
+
console.log(
|
|
343
|
+
` reason=${String(r.reasonChars).padStart(5)} content=${String(r.contentChars).padStart(4)}` +
|
|
344
|
+
` finish=${(r.finish ?? '-').padEnd(14)} ${String(r.ms).padStart(6)}ms${r.streamErr ? ` ⚠${r.streamErr}` : ''}${sentinel}`,
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
arms.push({ id, catalogReasoning: catalogEntry?.reasoning ?? null, cli: catalogEntry?.cli ?? null, rep: rep + 1, ...r })
|
|
348
|
+
await sleep(INTERVAL)
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
mkdirSync(dirname(OUT), { recursive: true })
|
|
354
|
+
writeFileSync(OUT, JSON.stringify({ expect: EXPECT, catalog, arms }, null, 2))
|
|
355
|
+
console.log(`\n证据落盘:${OUT}(${arms.length} 臂)`)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* probe-codebuddy-tier-wiring.mjs — 「思考强度」全链路**真实上游**联调。
|
|
4
|
+
*
|
|
5
|
+
* 分工:verify-bridge [17] 是离线 mock 回归(可随时跑);本脚本是真实联调,
|
|
6
|
+
* 证明线上真这么走,跑一次即可、不进 CI。
|
|
7
|
+
*
|
|
8
|
+
* 拓扑:本脚本 → 插件桥(临时 DSH_HOME,不碰用户 settings.yaml / 文件层 / 令牌)
|
|
9
|
+
* → 本地捕获代理(原样转发并记录出站体)→ https://copilot.tencent.com
|
|
10
|
+
*
|
|
11
|
+
* 断言(每条都有真实字节或真实响应作依据):
|
|
12
|
+
* [1] 真实 /v3/config → model-sync 成功;model-list 的 `efforts` 档位表与
|
|
13
|
+
* 目录声明一致(glm-5.3-flash/kimi-k2.8-preview = low/high/max、
|
|
14
|
+
* hy4-preview = high;legacy 模型走 patch 静态表;无表模型不出现)
|
|
15
|
+
* [2] settings.yaml 镜像(宿主 Model/Effort 选择器的数据源)带 reasoningEfforts
|
|
16
|
+
* [3] 真实 chat:捕获代理看到的出站体含被注入的 reasoning_effort,且上游 200 出正文
|
|
17
|
+
* [4] 负例:未声明档位 / 无表模型 → 出站体不带 reasoning_effort
|
|
18
|
+
*
|
|
19
|
+
* 用法:
|
|
20
|
+
* node scripts/probe-codebuddy-tier-wiring.mjs # 默认全套
|
|
21
|
+
* node scripts/probe-codebuddy-tier-wiring.mjs --skip-chat # 只验目录/镜像接线
|
|
22
|
+
*
|
|
23
|
+
* 凭据:CODEBUDDY_API_KEY(env)或 ~/.dsh/.credentials.yaml(与插件同解析顺序)。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createServer } from 'node:http'
|
|
27
|
+
import { Readable } from 'node:stream'
|
|
28
|
+
import { mkdtempSync, writeFileSync, readFileSync, existsSync, mkdirSync } from 'node:fs'
|
|
29
|
+
import { tmpdir, homedir } from 'node:os'
|
|
30
|
+
import { join, dirname } from 'node:path'
|
|
31
|
+
import { fileURLToPath } from 'node:url'
|
|
32
|
+
|
|
33
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
34
|
+
const UPSTREAM = 'https://copilot.tencent.com'
|
|
35
|
+
const argv = process.argv.slice(2)
|
|
36
|
+
const SKIP_CHAT = argv.includes('--skip-chat')
|
|
37
|
+
const OUT = join(ROOT, 'docs', 'probes', `codebuddy-tier-wiring-${new Date().toISOString().slice(0, 10)}.json`)
|
|
38
|
+
|
|
39
|
+
function loadApiKey() {
|
|
40
|
+
if (process.env.CODEBUDDY_API_KEY) return process.env.CODEBUDDY_API_KEY
|
|
41
|
+
const credFile = join(homedir(), '.dsh', '.credentials.yaml')
|
|
42
|
+
if (existsSync(credFile)) {
|
|
43
|
+
const m = readFileSync(credFile, 'utf8').match(/^\s*CODEBUDDY_API_KEY:\s*["']?([^"'\s]+)["']?\s*$/m)
|
|
44
|
+
if (m) return m[1]
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
50
|
+
let failures = 0
|
|
51
|
+
const check = (label, cond, detail = '') => {
|
|
52
|
+
if (cond) console.log(` ok ${label}`)
|
|
53
|
+
else { failures++; console.log(` FAIL ${label}${detail ? ` — ${detail}` : ''}`) }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function freePort() {
|
|
57
|
+
const probe = createServer()
|
|
58
|
+
await new Promise((r) => probe.listen(0, '127.0.0.1', r))
|
|
59
|
+
const { port } = probe.address()
|
|
60
|
+
await new Promise((r) => probe.close(r))
|
|
61
|
+
return port
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const key = loadApiKey()
|
|
65
|
+
if (!key) {
|
|
66
|
+
console.error('CODEBUDDY_API_KEY not found (env or ~/.dsh/.credentials.yaml)')
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ------------------------------------------------------------ 隔离的运行环境
|
|
71
|
+
process.env.DSH_HOME = mkdtempSync(join(tmpdir(), 'dsh-tap-tier-'))
|
|
72
|
+
mkdirSync(process.env.DSH_HOME, { recursive: true })
|
|
73
|
+
process.env.CODEBUDDY_API_KEY = key
|
|
74
|
+
process.env.CODEBUDDY_BRIDGE_LOG = join(process.env.DSH_HOME, 'bridge-log.jsonl')
|
|
75
|
+
|
|
76
|
+
// ------------------------------------------------- 本地捕获代理(原样转发 + 记录)
|
|
77
|
+
const captured = []
|
|
78
|
+
const proxy = createServer((req, res) => {
|
|
79
|
+
const chunks = []
|
|
80
|
+
req.on('data', (c) => chunks.push(c))
|
|
81
|
+
req.on('end', async () => {
|
|
82
|
+
const body = Buffer.concat(chunks).toString('utf8')
|
|
83
|
+
const url = `${UPSTREAM}${req.url}`
|
|
84
|
+
const headers = { ...req.headers }
|
|
85
|
+
delete headers.host
|
|
86
|
+
delete headers['content-length']
|
|
87
|
+
let upstream
|
|
88
|
+
try {
|
|
89
|
+
upstream = await fetch(url, {
|
|
90
|
+
method: req.method,
|
|
91
|
+
headers,
|
|
92
|
+
body: req.method === 'GET' || req.method === 'HEAD' ? undefined : body,
|
|
93
|
+
})
|
|
94
|
+
} catch (err) {
|
|
95
|
+
captured.push({ url: req.url, body, proxyError: String(err.message ?? err) })
|
|
96
|
+
res.writeHead(502, { 'Content-Type': 'application/json' })
|
|
97
|
+
res.end('{"code":1,"msg":"proxy upstream failed"}')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
captured.push({
|
|
101
|
+
url: req.url,
|
|
102
|
+
status: upstream.status,
|
|
103
|
+
body,
|
|
104
|
+
reasoningEffort: (() => { try { return JSON.parse(body).reasoning_effort ?? null } catch { return null } })(),
|
|
105
|
+
model: (() => { try { return JSON.parse(body).model ?? null } catch { return null } })(),
|
|
106
|
+
})
|
|
107
|
+
const out = {}
|
|
108
|
+
for (const [k, v] of upstream.headers) if (k !== 'content-encoding' && k !== 'content-length') out[k] = v
|
|
109
|
+
res.writeHead(upstream.status, out)
|
|
110
|
+
if (upstream.body) Readable.fromWeb(upstream.body).pipe(res)
|
|
111
|
+
else res.end()
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const proxyPort = await freePort()
|
|
117
|
+
await new Promise((r) => proxy.listen(proxyPort, '127.0.0.1', r))
|
|
118
|
+
const bridgePort = await freePort()
|
|
119
|
+
|
|
120
|
+
const { apply } = await import(new URL('../index.js', import.meta.url).href)
|
|
121
|
+
const routes = {}
|
|
122
|
+
const ctx = {
|
|
123
|
+
inject: (_deps, cb) => cb({ webServer: { register: (r) => { routes[r.path] = r.handler } } }),
|
|
124
|
+
web: { registerSearchProvider: () => () => {}, registerFetchProvider: () => () => {} },
|
|
125
|
+
on: () => {},
|
|
126
|
+
}
|
|
127
|
+
apply(ctx, {
|
|
128
|
+
baseURL: `http://127.0.0.1:${proxyPort}`,
|
|
129
|
+
bridgePort,
|
|
130
|
+
authMode: 'api-key',
|
|
131
|
+
})
|
|
132
|
+
await sleep(300)
|
|
133
|
+
|
|
134
|
+
// 设置路由用事件发射器驱动(同 verify-bridge 的 callRoute)。
|
|
135
|
+
const { EventEmitter } = await import('node:events')
|
|
136
|
+
const call = (body) => new Promise((resolve, reject) => {
|
|
137
|
+
const handler = routes['/dsh-tap/settings']
|
|
138
|
+
if (!handler) return reject(new Error('settings route not registered'))
|
|
139
|
+
const req = new EventEmitter()
|
|
140
|
+
req.method = body ? 'POST' : 'GET'
|
|
141
|
+
req.headers = { host: '127.0.0.1:3080', origin: 'http://127.0.0.1:3080' }
|
|
142
|
+
const res = {
|
|
143
|
+
status: 0,
|
|
144
|
+
writeHead(s) { this.status = s },
|
|
145
|
+
end(b) { let json = null; try { json = JSON.parse(b) } catch { /* non-JSON */ } resolve({ status: this.status, json }) },
|
|
146
|
+
}
|
|
147
|
+
handler(req, res)
|
|
148
|
+
if (body) {
|
|
149
|
+
const bytes = Buffer.from(JSON.stringify(body))
|
|
150
|
+
req.emit('data', bytes.subarray(0, 3))
|
|
151
|
+
req.emit('data', bytes.subarray(3))
|
|
152
|
+
req.emit('end')
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
console.log(`[1] 真实目录 → 档位表(proxy :${proxyPort},bridge :${bridgePort})`)
|
|
157
|
+
let res = await call({ action: 'model-sync' })
|
|
158
|
+
check('model-sync 成功且拿到网关目录', res.json?.sync?.ok === true && res.json?.sync?.count > 20,
|
|
159
|
+
JSON.stringify(res.json?.sync))
|
|
160
|
+
const catalogCount = res.json?.sync?.count ?? 0
|
|
161
|
+
|
|
162
|
+
res = await call({ action: 'model-list' })
|
|
163
|
+
const listed = res.json
|
|
164
|
+
const efforts = listed?.efforts ?? {}
|
|
165
|
+
const expect = {
|
|
166
|
+
'glm-5.3-flash': ['low', 'high', 'max'],
|
|
167
|
+
'kimi-k2.8-preview': ['low', 'high', 'max'],
|
|
168
|
+
'hy4-preview': ['high'],
|
|
169
|
+
}
|
|
170
|
+
for (const [id, tiers] of Object.entries(expect)) {
|
|
171
|
+
check(`目录声明 → 档位表 ${id} = ${JSON.stringify(tiers)}`,
|
|
172
|
+
JSON.stringify(efforts[id]) === JSON.stringify(tiers), JSON.stringify(efforts[id]))
|
|
173
|
+
}
|
|
174
|
+
check('静态 legacy 模型仍由 patch 表驱动(deepseek-v4-pro)',
|
|
175
|
+
JSON.stringify(efforts['deepseek-v4-pro']) === '["low","medium","high","max"]', JSON.stringify(efforts['deepseek-v4-pro']))
|
|
176
|
+
check('hy3 的 off 已撤(省略参数会思考)',
|
|
177
|
+
JSON.stringify(efforts['hy3']) === '["low","medium","high","max"]', JSON.stringify(efforts['hy3']))
|
|
178
|
+
check('无表模型不出现(auto / deepseek-v3)',
|
|
179
|
+
efforts['auto'] === undefined && efforts['deepseek-v3'] === undefined)
|
|
180
|
+
const flash = (listed?.catalog?.models ?? []).find((m) => m.id === 'glm-5.3-flash')
|
|
181
|
+
check('目录条目带完整声明(supportedEfforts/canDisableThinking/defaultEffort)',
|
|
182
|
+
JSON.stringify(flash?.supportedEfforts) === '["low","high","max"]' && flash?.canDisableThinking === true
|
|
183
|
+
&& flash?.defaultEffort === 'high', JSON.stringify(flash))
|
|
184
|
+
|
|
185
|
+
console.log('\n[2] settings.yaml 镜像(宿主 Effort 选择器的数据源)')
|
|
186
|
+
const mirror = readFileSync(join(process.env.DSH_HOME, 'settings.yaml'), 'utf8')
|
|
187
|
+
check('镜像含目录模型的 reasoningEfforts',
|
|
188
|
+
/glm-5\.3-flash[\s\S]{0,400}?reasoningEfforts/.test(mirror))
|
|
189
|
+
|
|
190
|
+
const chats = []
|
|
191
|
+
if (!SKIP_CHAT) {
|
|
192
|
+
console.log('\n[3] 真实 chat:出站注入 + 上游接受')
|
|
193
|
+
const layerPath = join(process.env.DSH_HOME, 'codebuddy-plugin.json')
|
|
194
|
+
const chat = async (model) => {
|
|
195
|
+
const r = await fetch(`http://127.0.0.1:${bridgePort}/v2/chat/completions`, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers: { 'Content-Type': 'application/json' },
|
|
198
|
+
body: JSON.stringify({ model, stream: true, max_tokens: 24, messages: [{ role: 'user', content: '只回一个字:好' }] }),
|
|
199
|
+
})
|
|
200
|
+
const text = await r.text()
|
|
201
|
+
const last = [...captured].reverse().find((c) => c.url?.endsWith('/chat/completions'))
|
|
202
|
+
chats.push({ model, status: r.status, effort: last?.reasoningEffort ?? null, bodyPreview: text.slice(0, 120) })
|
|
203
|
+
return r.status
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
writeFileSync(layerPath, JSON.stringify({ effortByModel: { 'glm-5.3-flash': 'max' } }) + '\n')
|
|
207
|
+
let status = await chat('glm-5.3-flash')
|
|
208
|
+
check('已声明档位 max → 出站体带 reasoning_effort:"max",上游 200',
|
|
209
|
+
status === 200 && chats.at(-1).effort === 'max', JSON.stringify(chats.at(-1)))
|
|
210
|
+
|
|
211
|
+
writeFileSync(layerPath, JSON.stringify({ effortByModel: { 'glm-5.3-flash': 'medium' } }) + '\n')
|
|
212
|
+
status = await chat('glm-5.3-flash')
|
|
213
|
+
check('未声明档位 medium → 不注入(仍 200)',
|
|
214
|
+
status === 200 && chats.at(-1).effort === null, JSON.stringify(chats.at(-1)))
|
|
215
|
+
|
|
216
|
+
writeFileSync(layerPath, JSON.stringify({ effortByModel: { auto: 'high' } }) + '\n')
|
|
217
|
+
status = await chat('auto')
|
|
218
|
+
check('无表模型 auto → 不注入(仍 200)',
|
|
219
|
+
status === 200 && chats.at(-1).effort === null, JSON.stringify(chats.at(-1)))
|
|
220
|
+
|
|
221
|
+
writeFileSync(layerPath, JSON.stringify({}) + '\n')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const evidence = {
|
|
225
|
+
fetchedAt: new Date().toISOString(),
|
|
226
|
+
catalogCount,
|
|
227
|
+
efforts,
|
|
228
|
+
mirrorHasReasoningEfforts: /glm-5\.3-flash[\s\S]{0,400}?reasoningEfforts/.test(mirror),
|
|
229
|
+
chats,
|
|
230
|
+
captured: captured.map((c) => ({ url: c.url, status: c.status, model: c.model, reasoningEffort: c.reasoningEffort })),
|
|
231
|
+
}
|
|
232
|
+
mkdirSync(dirname(OUT), { recursive: true })
|
|
233
|
+
writeFileSync(OUT, JSON.stringify(evidence, null, 2))
|
|
234
|
+
|
|
235
|
+
console.log(failures === 0 ? '\nall wiring checks passed' : `\n${failures} check(s) FAILED`)
|
|
236
|
+
console.log(`证据落盘:${OUT}`)
|
|
237
|
+
proxy.close()
|
|
238
|
+
process.exit(failures === 0 ? 0 : 1)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
main().catch((err) => {
|
|
242
|
+
console.error(`probe crashed: ${err.stack ?? err.message}`)
|
|
243
|
+
process.exit(1)
|
|
244
|
+
})
|