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,92 @@
|
|
|
1
|
+
// 臂 8:model_config 携带完整定价元数据(price_factor/promotion/is_free/feature_switches…)
|
|
2
|
+
// 假设:服务端计费归并要求请求侧 model_config 带价格因子(官方客户端从目录投影全带)。
|
|
3
|
+
import { readJson } from '../core/json-store.js'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { dirname, join } from 'node:path'
|
|
6
|
+
import { randomUUID } from 'node:crypto'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
9
|
+
|
|
10
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
11
|
+
const store = readJson(join(homedir(), '.dsh', 'qoder-plugin-auth.json'))
|
|
12
|
+
const { createCosyRuntime } = await import('../providers/qoder/cosy.js')
|
|
13
|
+
const { fetchQoderCatalog } = await import('../providers/qoder/catalog.js')
|
|
14
|
+
const cosy = createCosyRuntime({ wasmPath: join(ROOT, 'providers', 'qoder', 'qoder_auth.wasm') })
|
|
15
|
+
const cred = { accessToken: store.auth.accessToken, machineId: store.machine.machineId, uid: store.account.uid }
|
|
16
|
+
const INFER = 'https://gateway.qoder.com.cn'
|
|
17
|
+
const OPENAPI = 'https://openapi.qoder.com.cn'
|
|
18
|
+
const MODEL = 'qmodel_38max'
|
|
19
|
+
const PROMPT = '只回复两个字:收到'
|
|
20
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
21
|
+
|
|
22
|
+
const cat = await fetchQoderCatalog(cosy, cred, INFER)
|
|
23
|
+
const entry = cat.entries[MODEL]
|
|
24
|
+
// 官方归一化形态(bundle 实证字段):目录条目定价/能力元数据全带
|
|
25
|
+
const modelConfig = {
|
|
26
|
+
key: entry.key, display_name: entry.display_name, model: '', format: entry.format,
|
|
27
|
+
is_vl: entry.is_vl === true, is_reasoning: entry.is_reasoning === true,
|
|
28
|
+
api_key: '', url: '', source: entry.source ?? 'system',
|
|
29
|
+
max_input_tokens: entry.max_input_tokens,
|
|
30
|
+
is_default: entry.is_default === true, is_new: entry.is_new === true,
|
|
31
|
+
is_free: entry.is_free === true,
|
|
32
|
+
price_factor: entry.price_factor, original_price_factor: entry.original_price_factor,
|
|
33
|
+
promotion: entry.promotion, feature_switches: entry.feature_switches,
|
|
34
|
+
context_config: entry.context_config, thinking_config: entry.thinking_config,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const addOnUsed = async () => {
|
|
38
|
+
const r = await fetch(`${OPENAPI}/api/v2/quota/usage`, { headers: { Authorization: `Bearer ${cred.accessToken}` } })
|
|
39
|
+
return (JSON.parse(await r.text()))?.addOnQuota?.used
|
|
40
|
+
}
|
|
41
|
+
const evidence = { at: new Date().toISOString(), model: MODEL, modelConfig }
|
|
42
|
+
const before = await addOnUsed()
|
|
43
|
+
console.log('addOnUsed before:', before)
|
|
44
|
+
|
|
45
|
+
const requestId = randomUUID(), requestSetId = randomUUID(), sessionId = randomUUID()
|
|
46
|
+
const body = JSON.stringify({
|
|
47
|
+
model: MODEL, stream: true, stream_options: { include_usage: true },
|
|
48
|
+
messages: [{ role: 'user', content: PROMPT }],
|
|
49
|
+
request_id: requestId, request_set_id: requestSetId, chat_record_id: requestId,
|
|
50
|
+
session_id: sessionId, chat_task: 'FREE_INPUT',
|
|
51
|
+
chat_context: { text: PROMPT, features: [], extra: { context: [], modelConfig: { key: MODEL, is_reasoning: true }, originalContent: PROMPT }, chatPrompt: '', imageUrls: null },
|
|
52
|
+
is_reply: true, is_retry: false, source: 1, version: '3',
|
|
53
|
+
agent_id: 'agent_common', task_id: 'common', session_type: 'qoderclicn', aliyun_user_type: '',
|
|
54
|
+
model_config: modelConfig,
|
|
55
|
+
business: { product: 'cli', version: '1.1.57', type: 'agent', id: requestSetId, name: PROMPT.slice(0, 10), begin_at: Date.now(), stage: 'processing' },
|
|
56
|
+
})
|
|
57
|
+
const signed = await cosy.prepareChat(cred, { endpoint: INFER, body, modelKey: MODEL, modelSource: 'system' })
|
|
58
|
+
const res = await fetch(signed.url, { method: 'POST', headers: { ...signed.headers, 'Content-Type': 'application/json', Accept: 'text/event-stream' }, body: signed.body })
|
|
59
|
+
const raw = await res.text()
|
|
60
|
+
let usage = null, text = ''
|
|
61
|
+
for (const line of raw.split('\n')) {
|
|
62
|
+
if (!line.startsWith('data:')) continue
|
|
63
|
+
let frame; try { frame = JSON.parse(line.slice(5)) } catch { continue }
|
|
64
|
+
if (typeof frame?.body !== 'string' || frame.body === '[DONE]') continue
|
|
65
|
+
let chunk; try { chunk = JSON.parse(frame.body) } catch { continue }
|
|
66
|
+
if (chunk.usage) usage = chunk.usage
|
|
67
|
+
if (chunk.choices?.[0]?.delta?.content) text += chunk.choices[0].delta.content
|
|
68
|
+
}
|
|
69
|
+
console.log('chat HTTP', res.status, 'text=', JSON.stringify(text.slice(0, 20)))
|
|
70
|
+
console.log('usage 帧全文:', JSON.stringify(usage))
|
|
71
|
+
evidence.chat = { status: res.status, usage, text: text.slice(0, 100), raw: raw.slice(0, 12_000) }
|
|
72
|
+
|
|
73
|
+
// business/finish 收尾(与前臂同)
|
|
74
|
+
const inner = { event_time: Date.now(), event_type: 'BUSINESS_FINISH', mid: cred.machineId, aid: '', rid: randomUUID(), oid: '', yid: '', uid: cred.uid, event_data: { session_id: sessionId, business: { product: 'cli', version: '1.1.57', type: 'agent', id: requestSetId, end_at: Date.now(), stage: 'complete', name: PROMPT.slice(0, 10) } } }
|
|
75
|
+
const fin = await cosy.prepareSigned(cred, { endpoint: INFER, path: '/api/v2/service/business/finish?Encode=1', method: 'POST', mode: 'auth', body: JSON.stringify({ payload: JSON.stringify(inner), encodeVersion: '1' }) })
|
|
76
|
+
const finRes = await fetch(fin.url, { method: 'POST', headers: { ...fin.headers, 'Content-Type': 'application/json' }, body: fin.body })
|
|
77
|
+
console.log('business/finish HTTP', finRes.status, (await finRes.text()).slice(0, 80))
|
|
78
|
+
|
|
79
|
+
// 官方实测计数器 ~1 分钟内动(06:28 官方聊天 06:29 即 +5):盯 90s,每 15s 一拍
|
|
80
|
+
for (const wait of [15, 15, 15, 15, 30]) {
|
|
81
|
+
await sleep(wait * 1000)
|
|
82
|
+
const now = await addOnUsed()
|
|
83
|
+
console.log(`+${wait}s addOnUsed:`, now, now !== before ? '★ 动了!' : '')
|
|
84
|
+
if (now !== before) { evidence.counterMoved = true; break }
|
|
85
|
+
}
|
|
86
|
+
evidence.before = before
|
|
87
|
+
evidence.after = await addOnUsed()
|
|
88
|
+
mkdirSync(join(ROOT, 'docs', 'probes'), { recursive: true })
|
|
89
|
+
const out = join(ROOT, 'docs', 'probes', `qoder-attribution-arm8-${Date.now()}.json`)
|
|
90
|
+
writeFileSync(out, JSON.stringify(evidence, null, 2))
|
|
91
|
+
console.log('证据 →', out)
|
|
92
|
+
process.exit(0)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// 臂 9:大额用量 + 整数计数器(addOnQuota.used 只显整数,0.002 级探测不可见)。
|
|
2
|
+
// A = 裸 OpenAI body ×2 大输出;B = 官方归因信封 + business/finish ×2 大输出。
|
|
3
|
+
// 判定:哪一组让整数计数器跳变,即哪条路径被记账。
|
|
4
|
+
import { readJson } from '../core/json-store.js'
|
|
5
|
+
import { homedir } from 'node:os'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
import { randomUUID } from 'node:crypto'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { writeFileSync, mkdirSync } from 'node:fs'
|
|
10
|
+
|
|
11
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
12
|
+
const store = readJson(join(homedir(), '.dsh', 'qoder-plugin-auth.json'))
|
|
13
|
+
const { createCosyRuntime } = await import('../providers/qoder/cosy.js')
|
|
14
|
+
const { fetchQoderCatalog } = await import('../providers/qoder/catalog.js')
|
|
15
|
+
const cosy = createCosyRuntime({ wasmPath: join(ROOT, 'providers', 'qoder', 'qoder_auth.wasm') })
|
|
16
|
+
const cred = { accessToken: store.auth.accessToken, machineId: store.machine.machineId, uid: store.account.uid }
|
|
17
|
+
const INFER = 'https://gateway.qoder.com.cn'
|
|
18
|
+
const OPENAPI = 'https://openapi.qoder.com.cn'
|
|
19
|
+
const MODEL = process.env.QODER_QUOTA_MODEL ?? 'qmodel_38max'
|
|
20
|
+
const BIG_PROMPT = '用中文写一篇约 2500 字的科普文章,主题是"潮汐是如何形成的",要求分段详细论述。'
|
|
21
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
22
|
+
const evidence = { at: new Date().toISOString(), model: MODEL, arms: [] }
|
|
23
|
+
|
|
24
|
+
const addOnUsed = async () => {
|
|
25
|
+
const r = await fetch(`${OPENAPI}/api/v2/quota/usage`, { headers: { Authorization: `Bearer ${cred.accessToken}` } })
|
|
26
|
+
return (JSON.parse(await r.text()))?.addOnQuota?.used
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cat = await fetchQoderCatalog(cosy, cred, INFER)
|
|
30
|
+
const entry = cat.entries[MODEL]
|
|
31
|
+
|
|
32
|
+
async function bigChat(withEnvelope) {
|
|
33
|
+
const base = {
|
|
34
|
+
model: MODEL, stream: true, stream_options: { include_usage: true },
|
|
35
|
+
messages: [{ role: 'user', content: BIG_PROMPT }],
|
|
36
|
+
}
|
|
37
|
+
if (withEnvelope) {
|
|
38
|
+
const requestId = randomUUID(), requestSetId = randomUUID()
|
|
39
|
+
Object.assign(base, {
|
|
40
|
+
request_id: requestId, request_set_id: requestSetId, chat_record_id: requestId,
|
|
41
|
+
session_id: randomUUID(), chat_task: 'FREE_INPUT',
|
|
42
|
+
chat_context: { text: BIG_PROMPT, features: [], extra: { context: [], modelConfig: { key: MODEL, is_reasoning: true }, originalContent: BIG_PROMPT }, chatPrompt: '', imageUrls: null },
|
|
43
|
+
is_reply: true, is_retry: false, source: 1, version: '3',
|
|
44
|
+
agent_id: 'agent_common', task_id: 'common', session_type: 'qoderclicn', aliyun_user_type: '',
|
|
45
|
+
model_config: {
|
|
46
|
+
key: entry.key, display_name: entry.display_name, model: '', format: entry.format,
|
|
47
|
+
is_vl: entry.is_vl === true, is_reasoning: entry.is_reasoning === true,
|
|
48
|
+
api_key: '', url: '', source: entry.source ?? 'system', max_input_tokens: entry.max_input_tokens,
|
|
49
|
+
is_free: entry.is_free === true, price_factor: entry.price_factor,
|
|
50
|
+
promotion: entry.promotion, feature_switches: entry.feature_switches,
|
|
51
|
+
context_config: entry.context_config, thinking_config: entry.thinking_config,
|
|
52
|
+
},
|
|
53
|
+
business: { product: 'cli', version: '1.1.57', type: 'agent', id: requestSetId, name: BIG_PROMPT.slice(0, 10), begin_at: Date.now(), stage: 'processing' },
|
|
54
|
+
})
|
|
55
|
+
base.__requestSetId = requestSetId
|
|
56
|
+
base.__sessionId = base.session_id
|
|
57
|
+
}
|
|
58
|
+
const { __requestSetId, __sessionId, ...payload } = base
|
|
59
|
+
const signed = await cosy.prepareChat(cred, { endpoint: INFER, body: JSON.stringify(payload), modelKey: MODEL, modelSource: 'system' })
|
|
60
|
+
const res = await fetch(signed.url, { method: 'POST', headers: { ...signed.headers, 'Content-Type': 'application/json', Accept: 'text/event-stream' }, body: signed.body })
|
|
61
|
+
const raw = await res.text()
|
|
62
|
+
let usage = null, text = ''
|
|
63
|
+
for (const line of raw.split('\n')) {
|
|
64
|
+
if (!line.startsWith('data:')) continue
|
|
65
|
+
let frame; try { frame = JSON.parse(line.slice(5)) } catch { continue }
|
|
66
|
+
if (typeof frame?.body !== 'string' || frame.body === '[DONE]') continue
|
|
67
|
+
let chunk; try { chunk = JSON.parse(frame.body) } catch { continue }
|
|
68
|
+
if (chunk.usage) usage = chunk.usage
|
|
69
|
+
if (chunk.choices?.[0]?.delta?.content) text += chunk.choices[0].delta.content
|
|
70
|
+
}
|
|
71
|
+
// 信封臂补 business/finish(与插件网关同)
|
|
72
|
+
if (withEnvelope && __requestSetId) {
|
|
73
|
+
const inner = { event_time: Date.now(), event_type: 'BUSINESS_FINISH', mid: cred.machineId, aid: '', rid: randomUUID(), oid: '', yid: '', uid: cred.uid, event_data: { session_id: __sessionId, business: { product: 'cli', version: '1.1.57', type: 'agent', id: __requestSetId, end_at: Date.now(), stage: 'complete', name: BIG_PROMPT.slice(0, 10) } } }
|
|
74
|
+
const fin = await cosy.prepareSigned(cred, { endpoint: INFER, path: '/api/v2/service/business/finish?Encode=1', method: 'POST', mode: 'auth', body: JSON.stringify({ payload: JSON.stringify(inner), encodeVersion: '1' }) })
|
|
75
|
+
await fetch(fin.url, { method: 'POST', headers: { ...fin.headers, 'Content-Type': 'application/json' }, body: fin.body }).catch(() => {})
|
|
76
|
+
}
|
|
77
|
+
return { status: res.status, credits: usage?.credits ?? 0, tokens: usage?.total_tokens ?? 0, textLen: text.length, usage }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const [arm, withEnvelope] of [['A-bare', false], ['B-envelope', true]]) {
|
|
81
|
+
const before = await addOnUsed()
|
|
82
|
+
console.log(`\n══ 臂 ${arm} ══ 开始前 addOnUsed=${before}`)
|
|
83
|
+
const runs = []
|
|
84
|
+
for (let i = 0; i < 2; i++) {
|
|
85
|
+
const r = await bigChat(withEnvelope)
|
|
86
|
+
console.log(` chat#${i + 1} HTTP ${r.status} tokens=${r.tokens} credits=${r.credits} 正文长度=${r.textLen}`)
|
|
87
|
+
runs.push({ status: r.status, credits: r.credits, tokens: r.tokens })
|
|
88
|
+
await sleep(2000)
|
|
89
|
+
}
|
|
90
|
+
await sleep(45_000)
|
|
91
|
+
const after = await addOnUsed()
|
|
92
|
+
console.log(` 45s 后 addOnUsed=${after} ${after !== before ? `★ 动了 ${before}→${after}` : '(不动)'}`)
|
|
93
|
+
evidence.arms.push({ arm, withEnvelope, before, after, runs })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
evidence.verdict = evidence.arms.map((a) => `${a.arm}:${a.after !== a.before ? '计数' : '不动'}`).join(' ')
|
|
97
|
+
console.log('\n【结论】', evidence.verdict)
|
|
98
|
+
mkdirSync(join(ROOT, 'docs', 'probes'), { recursive: true })
|
|
99
|
+
const out = join(ROOT, 'docs', 'probes', `qoder-attribution-arm9-${Date.now()}.json`)
|
|
100
|
+
writeFileSync(out, JSON.stringify(evidence, null, 2))
|
|
101
|
+
console.log('证据 →', out)
|
|
102
|
+
process.exit(0)
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Qoder CN — 用量统计归因梯度实验(真实网络、本人账号)。
|
|
4
|
+
*
|
|
5
|
+
* 历史结论(2026-09-22 当日晚些时候被臂 9 推翻并修正,见踩坑 #40):
|
|
6
|
+
* 本梯度臂 1-7 用 0.002 级小额聊天判"裸 body 不记账"是**假阴性**——
|
|
7
|
+
* `addOnQuota.used` 只显整数,小额被取整吞掉;臂 9(大额双臂,
|
|
8
|
+
* scripts/probe-qoder-attribution-arm9.mjs)证明配额扣减与形态无关、
|
|
9
|
+
* 实时入账。本脚本保留为归因形态回归/上报连通性探针(finish/tracking
|
|
10
|
+
* 均回 200 success),不要再用小额计数器差分下"是否记账"的结论。
|
|
11
|
+
*
|
|
12
|
+
* 官方客户端的统计视图归因链(bundle 逆向实证,见 qoder-worker-runtime.obf.mjs):
|
|
13
|
+
* a) 一轮 query 结束后 POST /algo/api/v2/service/business/finish?Encode=1
|
|
14
|
+
* (BUSINESS_FINISH 事件,prepareRequest mode "auth");
|
|
15
|
+
* b) 聊天 body 明文里的归因字段(request_id/request_set_id/chat_record_id/
|
|
16
|
+
* session_id/chat_task/source:1/version:"3"/agent_id/task_id/session_type…);
|
|
17
|
+
* c) 聊天 body 的 business 块(id 与 request_set_id 同源);
|
|
18
|
+
* d) POST /api/v1/tracking(mode "sign",聚合 total_credits,best-effort 遥测)。
|
|
19
|
+
*
|
|
20
|
+
* 梯度(每臂 = 计数器快照 → 动作 → 静置 → 复拍):
|
|
21
|
+
* 1 finish-only 裸聊天 + business/finish
|
|
22
|
+
* 2 envelope 聊天带归因字段(无 business 块),不上报
|
|
23
|
+
* 3 env-business 聊天带归因字段 + business 块,不上报
|
|
24
|
+
* 4 env-finish 归因字段 + business 块 + business/finish
|
|
25
|
+
* 5 tracking 裸聊天 + /api/v1/tracking 回流上报
|
|
26
|
+
* 6 全保真信封 归因字段 + business 块 + system/parameters/tools
|
|
27
|
+
* 7 IDE 面 session_type=qoder_work + business.product=ide + finish
|
|
28
|
+
*
|
|
29
|
+
* 用法:node scripts/probe-qoder-attribution.mjs [--arm N] [--settle-ms 15000]
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
33
|
+
import { homedir } from 'node:os'
|
|
34
|
+
import { dirname, join } from 'node:path'
|
|
35
|
+
import { randomUUID } from 'node:crypto'
|
|
36
|
+
import { fileURLToPath } from 'node:url'
|
|
37
|
+
|
|
38
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
39
|
+
const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
40
|
+
const PROBES_DIR = join(ROOT, 'docs', 'probes')
|
|
41
|
+
const OPENAPI = process.env.QODER_OPENAPI ?? 'https://openapi.qoder.com.cn'
|
|
42
|
+
const INFER = process.env.QODER_INFER ?? 'https://gateway.qoder.com.cn'
|
|
43
|
+
const MODEL = process.env.QODER_QUOTA_MODEL ?? 'q37fmodel'
|
|
44
|
+
const PROMPT = '只回复两个字:收到'
|
|
45
|
+
|
|
46
|
+
const args = process.argv.slice(2)
|
|
47
|
+
const optVal = (f, d) => { const i = args.indexOf(f); return i >= 0 ? Number(args[i + 1]) : d }
|
|
48
|
+
const onlyArm = optVal('--arm', 0)
|
|
49
|
+
const SETTLE_MS = optVal('--settle-ms', 15_000)
|
|
50
|
+
|
|
51
|
+
const { readJson } = await import('../core/json-store.js')
|
|
52
|
+
const { createCosyRuntime } = await import('../providers/qoder/cosy.js')
|
|
53
|
+
|
|
54
|
+
const store = readJson(join(DSH_HOME, 'qoder-plugin-auth.json'))
|
|
55
|
+
if (!store?.auth?.accessToken) { console.error('未登录:先跑 probe-qoder-live --login'); process.exit(1) }
|
|
56
|
+
const token = store.auth.accessToken
|
|
57
|
+
const machineId = store.machine?.machineId
|
|
58
|
+
const uid = store.account?.uid ?? ''
|
|
59
|
+
const oid = store.account?.organizationId ?? ''
|
|
60
|
+
const cred = { accessToken: token, machineId, uid }
|
|
61
|
+
const cosy = createCosyRuntime({ wasmPath: join(ROOT, 'providers', 'qoder', 'qoder_auth.wasm') })
|
|
62
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
63
|
+
|
|
64
|
+
// aid/yx_uid 不在令牌仓里,userinfo 现取(取不到就空串,与 service account 形态一致)
|
|
65
|
+
let aid = '', yid = ''
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(`${OPENAPI}/api/v1/userinfo`, { headers: { Authorization: `Bearer ${token}` } })
|
|
68
|
+
const j = JSON.parse(await res.text())
|
|
69
|
+
aid = j?.aid ?? j?.data?.aid ?? ''
|
|
70
|
+
yid = j?.yx_uid ?? j?.data?.yx_uid ?? ''
|
|
71
|
+
console.log(`userinfo HTTP ${res.status}:aid=${aid ? '有' : '无'} yx_uid=${yid ? '有' : '无'}`)
|
|
72
|
+
} catch (e) { console.log('userinfo 拉取失败(继续,aid/yid 空串):', String(e?.message ?? e)) }
|
|
73
|
+
|
|
74
|
+
// ── 计数器(高精度哨兵 = creditsSummary.totalCredits)─────────────────────
|
|
75
|
+
async function counters(tag) {
|
|
76
|
+
const grab = async (name, url, pick) => {
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } })
|
|
79
|
+
const j = JSON.parse(await res.text())
|
|
80
|
+
return { name, status: res.status, value: pick(j) }
|
|
81
|
+
} catch (e) { return { name, netErr: String(e?.message ?? e) } }
|
|
82
|
+
}
|
|
83
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
84
|
+
const snap = {
|
|
85
|
+
tag, at: new Date().toISOString(),
|
|
86
|
+
addOnUsed: (await grab('quotaUsage', `${OPENAPI}/api/v2/quota/usage`, (j) => j?.addOnQuota?.used)).value,
|
|
87
|
+
totalCredits: (await grab('creditsSummary', `${OPENAPI}/sash/api/v1/ai-conversations/credits-summary`, (j) => j?.totalCredits)).value,
|
|
88
|
+
heatToday: (await grab('creditsHeatmap', `${OPENAPI}/sash/api/v1/ai-conversations/credits-heatmap?days=1`, (j) => j?.items?.find?.((i) => i.date === today)?.value ?? j?.items?.[0]?.value)).value,
|
|
89
|
+
}
|
|
90
|
+
console.log(` [${tag}] addOnUsed=${snap.addOnUsed} totalCredits=${snap.totalCredits} heatToday=${snap.heatToday}`)
|
|
91
|
+
return snap
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── 动作原语 ─────────────────────────────────────────────────────────────
|
|
95
|
+
async function chatOnce(extraFields) {
|
|
96
|
+
const body = JSON.stringify({
|
|
97
|
+
model: MODEL, stream: true, stream_options: { include_usage: true },
|
|
98
|
+
messages: [{ role: 'user', content: PROMPT }],
|
|
99
|
+
...extraFields,
|
|
100
|
+
})
|
|
101
|
+
const signed = await cosy.prepareChat(cred, { endpoint: INFER, body, modelKey: MODEL, modelSource: 'system' })
|
|
102
|
+
const res = await fetch(signed.url, {
|
|
103
|
+
method: 'POST',
|
|
104
|
+
headers: { ...signed.headers, 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
|
105
|
+
body: signed.body,
|
|
106
|
+
})
|
|
107
|
+
const raw = await res.text()
|
|
108
|
+
let usage = null, text = '', err = null
|
|
109
|
+
for (const line of raw.split('\n')) {
|
|
110
|
+
if (!line.startsWith('data:')) continue
|
|
111
|
+
let frame; try { frame = JSON.parse(line.slice(5)) } catch { continue }
|
|
112
|
+
if (typeof frame?.body !== 'string' || frame.body === '[DONE]') continue
|
|
113
|
+
let chunk; try { chunk = JSON.parse(frame.body) } catch { continue }
|
|
114
|
+
if (chunk.message && !chunk.choices) { err = `${chunk.code}: ${chunk.message}`; continue }
|
|
115
|
+
if (chunk.choices?.[0]?.delta?.content) text += chunk.choices[0].delta.content
|
|
116
|
+
if (chunk.usage) usage = chunk.usage
|
|
117
|
+
}
|
|
118
|
+
console.log(` chat HTTP ${res.status} text=${JSON.stringify(text.slice(0, 20))} credits=${usage?.credits} billable=${usage?.billable} reqId=${usage?.request_id ?? '(无)'} ${err ? 'ERR=' + err : ''}`)
|
|
119
|
+
return { status: res.status, usage, text: text.slice(0, 100), err, raw: raw.slice(0, 12_000) }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function businessFinish({ sessionId, requestSetId }) {
|
|
123
|
+
const inner = {
|
|
124
|
+
event_time: Date.now(), event_type: 'BUSINESS_FINISH',
|
|
125
|
+
mid: machineId, aid, rid: randomUUID(), oid, yid, uid,
|
|
126
|
+
event_data: {
|
|
127
|
+
session_id: sessionId,
|
|
128
|
+
business: { product: 'cli', version: '1.1.57', type: 'agent', id: requestSetId, end_at: Date.now(), stage: 'complete', name: PROMPT.slice(0, 10) },
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
const bodyJson = JSON.stringify({ payload: JSON.stringify(inner), encodeVersion: '1' })
|
|
132
|
+
const signed = await cosy.prepareSigned(cred, { endpoint: INFER, path: '/api/v2/service/business/finish?Encode=1', method: 'POST', mode: 'auth', body: bodyJson })
|
|
133
|
+
const res = await fetch(signed.url, { method: 'POST', headers: { ...signed.headers, 'Content-Type': 'application/json' }, body: signed.body })
|
|
134
|
+
const text = await res.text()
|
|
135
|
+
console.log(` business/finish HTTP ${res.status} url=${signed.url} resp=${text.slice(0, 160)}`)
|
|
136
|
+
return { url: signed.url, status: res.status, resp: text.slice(0, 2000), plainBody: inner }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function tracking({ sessionId, requestSetId, usage }) {
|
|
140
|
+
const envelope = [{
|
|
141
|
+
uuid: randomUUID(),
|
|
142
|
+
event_type: 'qodercli-back-flow-agent-query-finish',
|
|
143
|
+
event_time: Date.now(),
|
|
144
|
+
uid, oid, mid: machineId, aid,
|
|
145
|
+
os_arch: process.arch, os_version: '',
|
|
146
|
+
ide_type: 'CLI', ide_version: '1.1.57', cluster_env: '',
|
|
147
|
+
business_id: requestSetId, git_remote: '',
|
|
148
|
+
event_data: {
|
|
149
|
+
items: [{
|
|
150
|
+
schema_version: 2, session_id: sessionId, task_id: requestSetId,
|
|
151
|
+
request_set_id: requestSetId, prompt_id: randomUUID(),
|
|
152
|
+
entry: 'cli', product: 'cli', client_type: '5', cli_version: '1.1.57', os_type: process.platform,
|
|
153
|
+
query_callback: 'end', terminal_reason: 'complete', business_state_final: 'complete',
|
|
154
|
+
duration_ms: 3000, loop_iteration_count: 1,
|
|
155
|
+
model_request_count: 1, model_request_success_count: 1, actual_model: MODEL,
|
|
156
|
+
total_input_tokens: usage?.prompt_tokens ?? 0, total_output_tokens: usage?.completion_tokens ?? 0,
|
|
157
|
+
total_cache_read_tokens: usage?.prompt_tokens_details?.cached_tokens ?? 0, total_cache_write_tokens: 0,
|
|
158
|
+
total_thinking_tokens: usage?.completion_tokens_details?.reasoning_tokens ?? 0,
|
|
159
|
+
total_credits: usage?.credits ?? 0, total_original_credits: usage?.original_credits ?? usage?.credits ?? 0,
|
|
160
|
+
average_ttft_ms: 1000, delivery_mode: 'best_effort_background',
|
|
161
|
+
}],
|
|
162
|
+
},
|
|
163
|
+
}]
|
|
164
|
+
const signed = await cosy.prepareSigned(cred, { endpoint: INFER, path: '/api/v1/tracking', method: 'POST', mode: 'sign', body: JSON.stringify(envelope) })
|
|
165
|
+
const res = await fetch(signed.url, { method: 'POST', headers: { ...signed.headers, 'Content-Type': 'application/json' }, body: signed.body })
|
|
166
|
+
const text = await res.text()
|
|
167
|
+
console.log(` tracking HTTP ${res.status} url=${signed.url} resp=${text.slice(0, 160)}`)
|
|
168
|
+
return { url: signed.url, status: res.status, resp: text.slice(0, 2000) }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** 归因字段信封(官方 A6e 形态的归因子集,无语义字段)。 */
|
|
172
|
+
function attributionFields({ sessionId, requestId, requestSetId, withBusiness }) {
|
|
173
|
+
const f = {
|
|
174
|
+
request_id: requestId, request_set_id: requestSetId, chat_record_id: requestId,
|
|
175
|
+
session_id: sessionId,
|
|
176
|
+
chat_task: 'FREE_INPUT',
|
|
177
|
+
chat_context: { text: PROMPT, features: [], extra: { context: [], modelConfig: { key: MODEL, is_reasoning: false }, originalContent: PROMPT }, chatPrompt: '', imageUrls: null },
|
|
178
|
+
is_reply: true, is_retry: false,
|
|
179
|
+
source: 1, version: '3',
|
|
180
|
+
agent_id: 'agent_common', task_id: 'common',
|
|
181
|
+
session_type: 'qoderclicn', aliyun_user_type: '',
|
|
182
|
+
model_config: { key: MODEL, display_name: MODEL, model: '', format: 'openai', is_vl: false, is_reasoning: true, api_key: '', url: '', source: 'system', max_input_tokens: 131072 },
|
|
183
|
+
}
|
|
184
|
+
if (withBusiness) {
|
|
185
|
+
f.business = { product: 'cli', version: '1.1.57', type: 'agent', id: requestSetId, name: PROMPT.slice(0, 10), begin_at: Date.now(), stage: 'processing' }
|
|
186
|
+
}
|
|
187
|
+
return f
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── 梯度 ─────────────────────────────────────────────────────────────────
|
|
191
|
+
const evidence = { at: new Date().toISOString(), model: MODEL, settleMs: SETTLE_MS, arms: [] }
|
|
192
|
+
const ARMS = {
|
|
193
|
+
1: async () => { // finish-only
|
|
194
|
+
const ids = { sessionId: randomUUID(), requestSetId: randomUUID() }
|
|
195
|
+
const chat = await chatOnce({})
|
|
196
|
+
const finish = await businessFinish(ids)
|
|
197
|
+
return { ids, chat, finish }
|
|
198
|
+
},
|
|
199
|
+
2: async () => { // envelope
|
|
200
|
+
const ids = { sessionId: randomUUID(), requestId: randomUUID(), requestSetId: randomUUID() }
|
|
201
|
+
const chat = await chatOnce(attributionFields({ ...ids, withBusiness: false }))
|
|
202
|
+
return { ids, chat }
|
|
203
|
+
},
|
|
204
|
+
3: async () => { // env-business
|
|
205
|
+
const ids = { sessionId: randomUUID(), requestId: randomUUID(), requestSetId: randomUUID() }
|
|
206
|
+
const chat = await chatOnce(attributionFields({ ...ids, withBusiness: true }))
|
|
207
|
+
return { ids, chat }
|
|
208
|
+
},
|
|
209
|
+
4: async () => { // env-finish
|
|
210
|
+
const ids = { sessionId: randomUUID(), requestId: randomUUID(), requestSetId: randomUUID() }
|
|
211
|
+
const chat = await chatOnce(attributionFields({ ...ids, withBusiness: true }))
|
|
212
|
+
const finish = await businessFinish(ids)
|
|
213
|
+
return { ids, chat, finish }
|
|
214
|
+
},
|
|
215
|
+
5: async () => { // tracking
|
|
216
|
+
const ids = { sessionId: randomUUID(), requestSetId: randomUUID() }
|
|
217
|
+
const chat = await chatOnce({})
|
|
218
|
+
const track = await tracking({ ...ids, usage: chat.usage })
|
|
219
|
+
return { ids, chat, track }
|
|
220
|
+
},
|
|
221
|
+
6: async () => { // 全保真信封:归因字段 + business 块 + system/parameters/tools(官方 A6e 全字段)
|
|
222
|
+
const ids = { sessionId: randomUUID(), requestId: randomUUID(), requestSetId: randomUUID() }
|
|
223
|
+
const chat = await chatOnce({
|
|
224
|
+
...attributionFields({ ...ids, withBusiness: true }),
|
|
225
|
+
system: 'You are Qoder, an AI coding assistant.',
|
|
226
|
+
tools: [],
|
|
227
|
+
parameters: { max_tokens: 32768 },
|
|
228
|
+
})
|
|
229
|
+
return { ids, chat }
|
|
230
|
+
},
|
|
231
|
+
7: async () => { // IDE 面:session_type=qoder_work + business.product=ide(桌面客户端形态)
|
|
232
|
+
const ids = { sessionId: randomUUID(), requestId: randomUUID(), requestSetId: randomUUID() }
|
|
233
|
+
const fields = attributionFields({ ...ids, withBusiness: true })
|
|
234
|
+
fields.session_type = 'qoder_work'
|
|
235
|
+
fields.business.product = 'ide'
|
|
236
|
+
const chat = await chatOnce(fields)
|
|
237
|
+
const finishInner = await businessFinish(ids)
|
|
238
|
+
return { ids, chat, finish: finishInner }
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const n of [1, 2, 3, 4, 5, 6, 7]) {
|
|
243
|
+
if (onlyArm && n !== onlyArm) continue
|
|
244
|
+
console.log(`\n══ 臂 ${n} ══`)
|
|
245
|
+
const before = await counters(`arm${n}-before`)
|
|
246
|
+
const result = await ARMS[n]()
|
|
247
|
+
console.log(` 静置 ${SETTLE_MS / 1000}s…`)
|
|
248
|
+
await sleep(SETTLE_MS)
|
|
249
|
+
const after = await counters(`arm${n}-after`)
|
|
250
|
+
const moved = ['addOnUsed', 'totalCredits', 'heatToday'].filter((k) => typeof before[k] === 'number' && typeof after[k] === 'number' && after[k] !== before[k])
|
|
251
|
+
console.log(` → ${moved.length ? '★ 动了:' + moved.map((k) => `${k} ${before[k]}→${after[k]}`).join(', ') : '计数器不动'}`)
|
|
252
|
+
evidence.arms.push({ arm: n, before, after, moved, result })
|
|
253
|
+
await sleep(2000)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const movedArms = evidence.arms.filter((a) => a.moved.length).map((a) => a.arm)
|
|
257
|
+
evidence.verdict = movedArms.length
|
|
258
|
+
? `★ 臂 ${movedArms.join(',')} 让计数器增长——记账归因要素在其中`
|
|
259
|
+
: '全部臂计数器不动——归因要素未覆盖(候选:system/parameters 语义字段、sub_task、或服务端按日批处理)'
|
|
260
|
+
console.log('\n【总结】', evidence.verdict)
|
|
261
|
+
|
|
262
|
+
mkdirSync(PROBES_DIR, { recursive: true })
|
|
263
|
+
const out = join(PROBES_DIR, `qoder-attribution-${Date.now()}.json`)
|
|
264
|
+
writeFileSync(out, JSON.stringify(evidence, null, 2))
|
|
265
|
+
console.log('证据 →', out)
|
|
266
|
+
process.exit(0)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Qoder CN — Qwen3.8-Flash(qfmodel)上游节点状态确认探针(幂等、只读)。
|
|
4
|
+
*
|
|
5
|
+
* 背景(2026-09-22 定案,证据 docs/diagnosis-qoder-flash.md):qfmodel 的上游后端节点
|
|
6
|
+
* `oa_qwen-plus-main` 于 04:21:38–04:21:51 之间进入持久失败——任何客户端形态(含 Qoder
|
|
7
|
+
* 官方客户端自己的签名器/元数据/body/版本号)都会拿到 HTTP 200 信封装带内业务错误
|
|
8
|
+
* `{"code":"400","message":"[FAIL]node:oa_qwen-plus-main msg:Execution failed: null"}`。
|
|
9
|
+
* 本探针只做一件事:3 次 qfmodel + 2 次对照(qmodel_38max/q37fmodel),节点恢复的瞬间
|
|
10
|
+
* 就会翻绿。
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* node scripts/probe-qoder-flash-confirm.mjs # 打印 + 落证据 docs/probes/
|
|
14
|
+
* node scripts/probe-qoder-flash-confirm.mjs --quiet # 只打印一行结论
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { mkdirSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { homedir } from 'node:os'
|
|
19
|
+
import { dirname, join } from 'node:path'
|
|
20
|
+
import { fileURLToPath } from 'node:url'
|
|
21
|
+
|
|
22
|
+
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
23
|
+
const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
24
|
+
const PROBES_DIR = join(ROOT, 'docs', 'probes')
|
|
25
|
+
const ENDPOINT = process.env.QODER_INFER ?? 'https://gateway.qoder.com.cn'
|
|
26
|
+
const GAP_MS = Number(process.env.QODER_GAP_MS ?? 2000)
|
|
27
|
+
const quiet = process.argv.includes('--quiet')
|
|
28
|
+
|
|
29
|
+
const { readJson } = await import('../core/json-store.js')
|
|
30
|
+
const { createCosyRuntime } = await import('../providers/qoder/cosy.js')
|
|
31
|
+
|
|
32
|
+
const store = readJson(join(DSH_HOME, 'qoder-plugin-auth.json'))
|
|
33
|
+
if (!store?.auth?.accessToken) {
|
|
34
|
+
console.error('未登录:先跑 scripts/probe-qoder-live.mjs --login')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
const cred = { accessToken: store.auth.accessToken, machineId: store.machine?.machineId, uid: store.account?.uid }
|
|
38
|
+
const cosy = createCosyRuntime({ wasmPath: join(ROOT, 'providers', 'qoder', 'qoder_auth.wasm') })
|
|
39
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
40
|
+
|
|
41
|
+
/** SSE 信封 → {text, err, frames}。 */
|
|
42
|
+
function dissect(raw) {
|
|
43
|
+
let text = '', err = null, frames = 0
|
|
44
|
+
for (const line of raw.split('\n')) {
|
|
45
|
+
if (!line.startsWith('data:')) continue
|
|
46
|
+
let frame; try { frame = JSON.parse(line.slice(5)) } catch { continue }
|
|
47
|
+
if (typeof frame?.body !== 'string' || frame.body === '[DONE]') continue
|
|
48
|
+
frames++
|
|
49
|
+
let chunk; try { chunk = JSON.parse(frame.body) } catch { err = frame.body.slice(0, 200); continue }
|
|
50
|
+
if (chunk.message && !chunk.choices) { err = `${chunk.code}: ${chunk.message}`; continue }
|
|
51
|
+
if (chunk.choices?.[0]?.delta?.content) text += chunk.choices[0].delta.content
|
|
52
|
+
}
|
|
53
|
+
return { text, err, frames }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function probe(label, modelKey) {
|
|
57
|
+
const body = JSON.stringify({
|
|
58
|
+
model: modelKey, stream: true, stream_options: { include_usage: true },
|
|
59
|
+
messages: [{ role: 'user', content: '只回复两个字:收到' }],
|
|
60
|
+
})
|
|
61
|
+
const rec = { label, modelKey, at: new Date().toISOString() }
|
|
62
|
+
try {
|
|
63
|
+
const signed = await cosy.prepareChat(cred, { endpoint: ENDPOINT, body, modelKey, modelSource: 'system' })
|
|
64
|
+
rec.url = signed.url
|
|
65
|
+
rec.clientType = signed.headers['Cosy-ClientType']
|
|
66
|
+
const res = await fetch(signed.url, {
|
|
67
|
+
method: 'POST',
|
|
68
|
+
headers: { ...signed.headers, 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
|
69
|
+
body: signed.body,
|
|
70
|
+
})
|
|
71
|
+
rec.status = res.status
|
|
72
|
+
const raw = await res.text()
|
|
73
|
+
rec.raw = raw.slice(0, 20_000)
|
|
74
|
+
Object.assign(rec, dissect(raw))
|
|
75
|
+
} catch (e) {
|
|
76
|
+
rec.netErr = String(e?.message ?? e)
|
|
77
|
+
}
|
|
78
|
+
const verdict = rec.netErr ? `netErr=${rec.netErr}` : rec.err ? `❌ ${rec.err}` : rec.text?.trim() ? `✅ "${rec.text.trim().slice(0, 30)}"` : '⚠️ 无正文'
|
|
79
|
+
if (!quiet) console.log(`${(rec.err || rec.netErr) ? '❌' : '✅'} ${label.padEnd(28)} HTTP ${rec.status ?? '-'} frames=${rec.frames ?? 0} ${verdict}`)
|
|
80
|
+
return rec
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const records = []
|
|
84
|
+
for (let i = 1; i <= 3; i++) { records.push(await probe(`qfmodel #${i}`, 'qfmodel')); await sleep(GAP_MS) }
|
|
85
|
+
for (const m of ['qmodel_38max', 'q37fmodel']) { records.push(await probe(`control ${m}`, m)); await sleep(GAP_MS) }
|
|
86
|
+
|
|
87
|
+
const flashOk = records.filter((r) => r.modelKey === 'qfmodel' && !r.err && !r.netErr && r.text?.trim()).length
|
|
88
|
+
const controlOk = records.filter((r) => r.modelKey !== 'qfmodel' && !r.err && !r.netErr && r.text?.trim()).length
|
|
89
|
+
mkdirSync(PROBES_DIR, { recursive: true })
|
|
90
|
+
const out = join(PROBES_DIR, `qoder-flash-confirm-${Date.now()}.json`)
|
|
91
|
+
writeFileSync(out, JSON.stringify({ at: new Date().toISOString(), endpoint: ENDPOINT, records }, null, 2))
|
|
92
|
+
console.log(`\n【结论】qfmodel 成功 ${flashOk}/3,对照成功 ${controlOk}/2 → ${flashOk > 0 ? '★ 上游节点已恢复,可复测 --suite flash' : (controlOk === 2 ? '上游节点仍故障(对照正常,链路健康)' : '⚠️ 连对照都失败:先查账号/网络')}`)
|
|
93
|
+
console.log('证据 →', out)
|
|
94
|
+
process.exit(0)
|