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,302 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Falsification test for the core/providers split (completion criterion 2):
|
|
4
|
+
* a SECOND OpenAI-compatible upstream is wired up using ONLY core/
|
|
5
|
+
* primitives plus a plain adapter object defined inline below. core/ is
|
|
6
|
+
* imported but never modified; providers/codebuddy is NOT imported at all.
|
|
7
|
+
*
|
|
8
|
+
* Two proof surfaces:
|
|
9
|
+
*
|
|
10
|
+
* S. Static purity: every core/*.js file is scanned for CodeBuddy-specific
|
|
11
|
+
* tokens (upstream hostnames, gateway error codes, vendor header names,
|
|
12
|
+
* provider names) and for imports of providers/* — any hit means
|
|
13
|
+
* provider specifics leaked into the shared layer.
|
|
14
|
+
*
|
|
15
|
+
* R. Runtime generality: a mock OpenAI-compatible upstream + the inline
|
|
16
|
+
* `mock-openai` adapter are driven through core's bridge, rotation
|
|
17
|
+
* engine, and usage meter, asserting:
|
|
18
|
+
* 1. non-streaming inbound → aggregated chat.completion JSON
|
|
19
|
+
* 2. stream:true inbound → SSE passthrough
|
|
20
|
+
* 3. session-attribution header injection (generic feature)
|
|
21
|
+
* 4. multi-key rotation with 500 failover + cooldown skip
|
|
22
|
+
* 5. usage metering works with plain OpenAI usage (no `credit`
|
|
23
|
+
* field → recorded as 0)
|
|
24
|
+
* 6. developer-role messages pass through UNCHANGED — the
|
|
25
|
+
* developer→system rewrite is owned by the codebuddy adapter,
|
|
26
|
+
* not by core (verify-bridge.mjs §9 locks the other half)
|
|
27
|
+
* 7. credential-file discipline: json-store writes land 0600, and
|
|
28
|
+
* resolveEnvKey parses credentials lines without building a
|
|
29
|
+
* RegExp from the settings-controlled name (no regex injection)
|
|
30
|
+
*
|
|
31
|
+
* Usage: node scripts/verify-core-generic.mjs (no network, no credentials)
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { createServer } from 'node:http'
|
|
35
|
+
import { mkdtempSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
|
|
36
|
+
import { tmpdir } from 'node:os'
|
|
37
|
+
import { join, dirname } from 'node:path'
|
|
38
|
+
import { fileURLToPath } from 'node:url'
|
|
39
|
+
|
|
40
|
+
import { KeyRotator } from '../core/rotation.js'
|
|
41
|
+
import { createUsageMeter } from '../core/usage-meter.js'
|
|
42
|
+
import { createBridge } from '../core/bridge.js'
|
|
43
|
+
import { resolveEnvKey } from '../core/json-store.js'
|
|
44
|
+
|
|
45
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
46
|
+
|
|
47
|
+
let failures = 0
|
|
48
|
+
function check(label, cond, detail = '') {
|
|
49
|
+
if (cond) {
|
|
50
|
+
console.log(` ok ${label}`)
|
|
51
|
+
} else {
|
|
52
|
+
failures++
|
|
53
|
+
console.log(` FAIL ${label}${detail ? ` — ${detail}` : ''}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
58
|
+
|
|
59
|
+
async function freePort() {
|
|
60
|
+
const probe = createServer()
|
|
61
|
+
await new Promise((r) => probe.listen(0, '127.0.0.1', r))
|
|
62
|
+
const { port } = probe.address()
|
|
63
|
+
await new Promise((r) => probe.close(r))
|
|
64
|
+
return port
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ------------------------------------------------------- S. core static purity
|
|
68
|
+
console.log('\n[S] core/ static purity')
|
|
69
|
+
{
|
|
70
|
+
const coreDir = join(ROOT, 'core')
|
|
71
|
+
const files = readdirSync(coreDir).filter((f) => f.endsWith('.js'))
|
|
72
|
+
check('core/ has the expected four modules', files.length === 4, files.join(','))
|
|
73
|
+
// Upstream/vendor specifics that must never appear in core/.
|
|
74
|
+
const forbidden = /\bcodebuddy\b|copilot\.tencent|tencent|12403|14401|14407|11101|11102|11103|11128|11217|12153|10001|x-ide-|x-product-version|x-private-data|dsh-codebuddy-bridge/i
|
|
75
|
+
for (const f of files) {
|
|
76
|
+
const text = readFileSync(join(coreDir, f), 'utf8')
|
|
77
|
+
const hit = text.match(forbidden)
|
|
78
|
+
check(`core/${f} carries no provider-specific token`, !hit, hit?.[0])
|
|
79
|
+
check(`core/${f} never imports providers/*`,
|
|
80
|
+
!/from\s+['"][^'"]*providers\//.test(text) && !/import\s*\(\s*['"][^'"]*providers\//.test(text))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --------------------------------------- R. second-upstream runtime generality
|
|
85
|
+
|
|
86
|
+
// A minimal mock OpenAI-compatible upstream: speaks SSE, carries classic
|
|
87
|
+
// OpenAI usage (no vendor `credit` field), tok_1 fails 500 on demand.
|
|
88
|
+
const behavior = new Map() // token → 'ok' | 'status:NNN'
|
|
89
|
+
const arrivals = []
|
|
90
|
+
const upstream = createServer((req, res) => {
|
|
91
|
+
let raw = ''
|
|
92
|
+
req.on('data', (c) => (raw += c))
|
|
93
|
+
req.on('end', () => {
|
|
94
|
+
let parsed = null
|
|
95
|
+
try { parsed = JSON.parse(raw) } catch { /* non-JSON */ }
|
|
96
|
+
const token = (req.headers.authorization ?? '').replace(/^Bearer /, '')
|
|
97
|
+
arrivals.push({ path: req.url, raw, parsed, token, sessionId: req.headers['session_id'] ?? null })
|
|
98
|
+
const b = behavior.get(token) ?? 'ok'
|
|
99
|
+
if (b.startsWith('status:')) {
|
|
100
|
+
res.writeHead(Number(b.slice(7)), { 'Content-Type': 'application/json' })
|
|
101
|
+
res.end(JSON.stringify({ error: { message: `mock ${b}` } }))
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
res.writeHead(200, { 'Content-Type': 'text/event-stream' })
|
|
105
|
+
res.end([
|
|
106
|
+
'data: {"id":"mock","choices":[{"index":0,"delta":{"content":"hej "}}]}',
|
|
107
|
+
'',
|
|
108
|
+
'data: {"id":"mock","choices":[{"index":0,"delta":{"content":"da"}}],"usage":{"prompt_tokens":5,"completion_tokens":2,"total_tokens":7}}',
|
|
109
|
+
'',
|
|
110
|
+
'data: [DONE]',
|
|
111
|
+
'',
|
|
112
|
+
].join('\n'))
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The second upstream's adapter — everything core/ needs, nothing more.
|
|
118
|
+
* Written here, inline, against the hook contract documented in
|
|
119
|
+
* core/bridge.js: if core/ had hidden CodeBuddy coupling, this object could
|
|
120
|
+
* not drive it.
|
|
121
|
+
*/
|
|
122
|
+
const mockProvider = {
|
|
123
|
+
id: 'mock-openai',
|
|
124
|
+
logPrefix: '[mock-openai]',
|
|
125
|
+
bridgeHeaders: () => ({ 'User-Agent': 'mock-openai-cli/1.0' }),
|
|
126
|
+
// This upstream has no role restriction: passthrough. Proves the
|
|
127
|
+
// developer→system rewrite lives in the codebuddy adapter, not in core.
|
|
128
|
+
transformChatPayload: () => {},
|
|
129
|
+
extractUsage: (chunk) => chunk.usage ?? null,
|
|
130
|
+
extractStreamError: (chunk) => (chunk.error ? chunk : null),
|
|
131
|
+
bridgeResponseId: 'mock-openai-stream-bridge',
|
|
132
|
+
logHeaderNames: ['user-agent', 'content-type'],
|
|
133
|
+
sentinelAuth: 'Bearer mock-sentinel',
|
|
134
|
+
texts: { credentialUnavailable: 'mock-openai credential unavailable' },
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function main() {
|
|
138
|
+
const upstreamPort = await freePort()
|
|
139
|
+
await new Promise((r) => upstream.listen(upstreamPort, '127.0.0.1', r))
|
|
140
|
+
const baseURL = `http://127.0.0.1:${upstreamPort}`
|
|
141
|
+
|
|
142
|
+
const tmp = mkdtempSync(join(tmpdir(), 'core-generic-test-'))
|
|
143
|
+
const meter = createUsageMeter({ path: join(tmp, 'usage.json') })
|
|
144
|
+
// `let`: R4 swaps in a fresh rotator to pin the cursor deterministically.
|
|
145
|
+
let rotator = new KeyRotator()
|
|
146
|
+
const KEYS = [
|
|
147
|
+
{ name: 'k1', key: 'tok_1' },
|
|
148
|
+
{ name: 'k2', key: 'tok_2' },
|
|
149
|
+
]
|
|
150
|
+
const withCredentials = (attempt) => {
|
|
151
|
+
const emptyError = new Error('mock credential unavailable')
|
|
152
|
+
emptyError.credentialUnavailable = true
|
|
153
|
+
return rotator.run(rotator.ordered(KEYS), attempt, { cooldownMs: 400, emptyError })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const runtime = { running: false, port: null, lastError: null }
|
|
157
|
+
const bridge = createBridge({
|
|
158
|
+
settings: () => ({
|
|
159
|
+
baseURL,
|
|
160
|
+
sessionHeadersEnabled: true,
|
|
161
|
+
sessionHeaderFormat: 'openai',
|
|
162
|
+
maxConcurrentPerSession: 4,
|
|
163
|
+
}),
|
|
164
|
+
provider: mockProvider,
|
|
165
|
+
withCredentials,
|
|
166
|
+
meter,
|
|
167
|
+
forensics: { logPath: () => undefined, dumpDir: () => undefined },
|
|
168
|
+
runtime,
|
|
169
|
+
})
|
|
170
|
+
const bridgePort = await freePort()
|
|
171
|
+
const stop = bridge.listen(bridgePort)
|
|
172
|
+
await sleep(200)
|
|
173
|
+
|
|
174
|
+
const chat = (body, headers = {}) =>
|
|
175
|
+
fetch(`http://127.0.0.1:${bridgePort}/v1/chat/completions`, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
178
|
+
body: JSON.stringify(body),
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
console.log('\n[R1] non-streaming inbound → aggregated chat.completion JSON')
|
|
182
|
+
{
|
|
183
|
+
const res = await chat({ model: 'm1', stream: false, messages: [{ role: 'user', content: 'hi' }] })
|
|
184
|
+
const json = await res.json().catch(() => null)
|
|
185
|
+
check('status 200 + chat.completion shape', res.status === 200 && json?.object === 'chat.completion')
|
|
186
|
+
check('content aggregated', json?.choices?.[0]?.message?.content === 'hej da',
|
|
187
|
+
JSON.stringify(json?.choices?.[0]?.message))
|
|
188
|
+
check('response id comes from the adapter', json?.id === 'mock-openai-stream-bridge', json?.id)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log('\n[R2] stream:true inbound → SSE passthrough')
|
|
192
|
+
{
|
|
193
|
+
const res = await chat({ model: 'm1', stream: true, messages: [] })
|
|
194
|
+
const text = await res.text()
|
|
195
|
+
check('event-stream with [DONE]',
|
|
196
|
+
(res.headers.get('content-type') ?? '').includes('text/event-stream') && text.includes('[DONE]'))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log('\n[R3] session-attribution headers injected (generic feature)')
|
|
200
|
+
{
|
|
201
|
+
const before = arrivals.length
|
|
202
|
+
await (await chat({ model: 'm1', stream: true, messages: [] }, { 'X-Session-ID': 'sess-X' })).text()
|
|
203
|
+
check('session_id injected from X-Session-ID', arrivals[before]?.sessionId === 'sess-X',
|
|
204
|
+
arrivals[before]?.sessionId)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log('\n[R4] rotation: 500 failover + cooldown skip')
|
|
208
|
+
{
|
|
209
|
+
// Fresh rotator → cursor pinned at tok_1, so the first attempt order is
|
|
210
|
+
// deterministic regardless of how many requests ran above.
|
|
211
|
+
rotator = new KeyRotator()
|
|
212
|
+
behavior.set('tok_1', 'status:500')
|
|
213
|
+
arrivals.length = 0
|
|
214
|
+
const r = await chat({ model: 'm1', stream: true, messages: [] })
|
|
215
|
+
await r.text()
|
|
216
|
+
check('caller still gets 200 after tok_1 500', r.status === 200)
|
|
217
|
+
check('tok_1 failed, tok_2 completed (failover inside one request)',
|
|
218
|
+
JSON.stringify(arrivals.map((a) => a.token)) === JSON.stringify(['tok_1', 'tok_2']),
|
|
219
|
+
JSON.stringify(arrivals.map((a) => a.token)))
|
|
220
|
+
arrivals.length = 0
|
|
221
|
+
const r2 = await chat({ model: 'm1', stream: true, messages: [] })
|
|
222
|
+
await r2.text()
|
|
223
|
+
check('cooling tok_1 skipped on the next request',
|
|
224
|
+
arrivals.length === 1 && arrivals[0].token === 'tok_2', JSON.stringify(arrivals.map((a) => a.token)))
|
|
225
|
+
behavior.delete('tok_1')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log('\n[R5] usage metering with plain OpenAI usage (no credit → 0)')
|
|
229
|
+
{
|
|
230
|
+
const v = meter.view()
|
|
231
|
+
// R1(1) + R2(1) + R3(1) + R4(2) = 5 billed chat requests
|
|
232
|
+
check('every chat request metered', v.totalRequests === 5, `got ${v.totalRequests}`)
|
|
233
|
+
check('credit recorded as 0 when the upstream has no credit field', v.totalCredit === 0)
|
|
234
|
+
check('tokens recorded', v.recent[0]?.prompt === 5 && v.recent[0]?.completion === 2,
|
|
235
|
+
JSON.stringify(v.recent[0]))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log('\n[R6] developer role passes through UNCHANGED (rewrite is adapter-owned)')
|
|
239
|
+
{
|
|
240
|
+
const before = arrivals.length
|
|
241
|
+
await (await chat({
|
|
242
|
+
model: 'm1',
|
|
243
|
+
stream: true,
|
|
244
|
+
messages: [{ role: 'developer', content: 'be nice' }, { role: 'user', content: 'ping' }],
|
|
245
|
+
})).text()
|
|
246
|
+
const forwarded = arrivals[before]?.parsed
|
|
247
|
+
check('developer role preserved verbatim for this upstream',
|
|
248
|
+
forwarded?.messages?.[0]?.role === 'developer',
|
|
249
|
+
JSON.stringify(forwarded?.messages?.map((m) => m.role)))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
await stop()
|
|
253
|
+
meter.dispose()
|
|
254
|
+
await new Promise((r) => upstream.close(r))
|
|
255
|
+
|
|
256
|
+
console.log('\n[R7] json-store: 0600 credential files + injection-free key resolution')
|
|
257
|
+
{
|
|
258
|
+
// [11]+[13]: these files carry tokens/keys — the usage store is flushed
|
|
259
|
+
// through writeJson on dispose(), so the mode is observable here.
|
|
260
|
+
const st = statSync(join(tmp, 'usage.json'))
|
|
261
|
+
if (process.platform === 'win32') {
|
|
262
|
+
// win32 无 POSIX 权限位:writeFile 的 mode 选项不体现在 stat().mode
|
|
263
|
+
// (仅反映只读属性)。代码路径与 POSIX 一致(writeJson 传 0o600),
|
|
264
|
+
// 位断言只在 POSIX 平台有意义。
|
|
265
|
+
check('json-store file mode is 0600 (win32: POSIX 位不可观测,代码已传 0o600)', true,
|
|
266
|
+
'platform-skip')
|
|
267
|
+
} else {
|
|
268
|
+
check('json-store file mode is 0600', (st.mode & 0o777) === 0o600,
|
|
269
|
+
`mode ${(st.mode & 0o777).toString(8)}`)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// [17]: the credentials file is parsed line-wise, never with a RegExp
|
|
273
|
+
// built from envName (settings-controlled free text). Prefixed names so
|
|
274
|
+
// a like-named process env var can never shadow the file values.
|
|
275
|
+
const credPath = join(tmp, 'creds.yaml')
|
|
276
|
+
writeFileSync(credPath, [
|
|
277
|
+
'# dsh flat credentials file',
|
|
278
|
+
'VCG_PLAIN: sk-plain-value',
|
|
279
|
+
'VCG_QUOTED: "sk-quoted-value"',
|
|
280
|
+
'VCG_OTHER: not-mine',
|
|
281
|
+
'',
|
|
282
|
+
].join('\n'))
|
|
283
|
+
check('plain name resolves its file value', resolveEnvKey('VCG_PLAIN', credPath) === 'sk-plain-value',
|
|
284
|
+
String(resolveEnvKey('VCG_PLAIN', credPath)))
|
|
285
|
+
check('double-quoted value is dequoted', resolveEnvKey('VCG_QUOTED', credPath) === 'sk-quoted-value')
|
|
286
|
+
check('regex metachar name resolves null (no injection)',
|
|
287
|
+
resolveEnvKey('.*)|(', credPath) === null)
|
|
288
|
+
check('metachar name does NOT leak another line\'s value',
|
|
289
|
+
resolveEnvKey('.*)|(', credPath) !== 'not-mine')
|
|
290
|
+
check('absent name resolves null', resolveEnvKey('VCG_ABSENT', credPath) === null)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log(failures === 0
|
|
294
|
+
? '\ncore/ generality proven: second OpenAI-compatible upstream wired with zero core/ changes'
|
|
295
|
+
: `\n${failures} check(s) FAILED`)
|
|
296
|
+
process.exit(failures === 0 ? 0 : 1)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
main().catch((err) => {
|
|
300
|
+
console.error(`verify-core-generic crashed: ${err.stack ?? err.message}`)
|
|
301
|
+
process.exit(1)
|
|
302
|
+
})
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-desktop-acceptance.mjs — desktop 适配 goal 的「可执行验收」。
|
|
4
|
+
*
|
|
5
|
+
* 把 docs/goals/desktop-adaptation.md 的验收标准 §1–§5 逐条变成机器可判的断言。
|
|
6
|
+
* 这是 goal 的 executable spec:规约即验收,验收即断言。
|
|
7
|
+
*
|
|
8
|
+
* 三条分层(对应 goal 验收标准的可机器化程度):
|
|
9
|
+
* A. 离线结构性不变量 —— 任何环境可跑,CI 硬闸门。
|
|
10
|
+
* §4a index.js 无 desktop 特化分支(静态守卫)
|
|
11
|
+
* §4b 宿主差异收口在 host-config.js / lib/client.js 候选兜底(静态守卫)
|
|
12
|
+
* §6 goal 文档存在且含可执行验收锚点
|
|
13
|
+
* B. 桌面环境断言 —— 本机有 ~/.dsh/profiles/desktop 时跑;没有则 SKIP(不假装覆盖)。
|
|
14
|
+
* §1a desktop patch 的 llm-pi-ai.providers 键枚举可解析(接入后必含 codebuddy)
|
|
15
|
+
* §1b 手写块并存校验(volces 等不被接管/破坏)
|
|
16
|
+
* C. 活桌面实例断言 —— 需要运行中的 desktop 实例,本脚本标记 SKIP。
|
|
17
|
+
* §1c ?probe=host-config 全绿、§2 三通道端到端、§3 UI 壳回归
|
|
18
|
+
* → 这些由 dsh-ui-test / 端到端探针承担,此处诚实标注,不伪造。
|
|
19
|
+
*
|
|
20
|
+
* 跑法:node scripts/verify-desktop-acceptance.mjs [--structural]
|
|
21
|
+
* 默认(本机/接入验证):A + B 全跑,B 组结果断言 FAIL 即「适配未达成」信号。
|
|
22
|
+
* --structural(CI 硬闸门):只锁 A 组结构性不变量——它们必须恒真、与适配进度无关;
|
|
23
|
+
* B 组结果断言(§1c codebuddy 已接入)在适配完成前必然 FAIL,不该进 CI 恒闸门。
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
26
|
+
import { homedir } from 'node:os'
|
|
27
|
+
import { join, dirname } from 'node:path'
|
|
28
|
+
import { fileURLToPath } from 'node:url'
|
|
29
|
+
|
|
30
|
+
const structuralOnly = process.argv.includes('--structural')
|
|
31
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
32
|
+
|
|
33
|
+
let pass = 0
|
|
34
|
+
let fail = 0
|
|
35
|
+
let skip = 0
|
|
36
|
+
const ok = (desc, cond, extra = '') => {
|
|
37
|
+
if (cond) { pass++; console.log(` ok ${desc}`) }
|
|
38
|
+
else { fail++; console.log(` FAIL ${desc}${extra ? ' — ' + extra : ''}`) }
|
|
39
|
+
}
|
|
40
|
+
const skipped = (desc, why) => { skip++; console.log(` SKIP ${desc} — ${why}`) }
|
|
41
|
+
const section = (t) => console.log(`\n[${t}]`)
|
|
42
|
+
|
|
43
|
+
// ============================================================
|
|
44
|
+
// A 组:离线结构性不变量(任何环境可跑)
|
|
45
|
+
// ============================================================
|
|
46
|
+
section('A. 离线结构性不变量')
|
|
47
|
+
|
|
48
|
+
// §4a index.js 无 desktop 特化分支
|
|
49
|
+
{
|
|
50
|
+
const src = readFileSync(join(root, 'index.js'), 'utf8')
|
|
51
|
+
// desktop 特化 = 出现 desktop 字样的条件分支;注释/字符串里提及不计。
|
|
52
|
+
const branch = src.split('\n').filter((l) =>
|
|
53
|
+
/\b(if|else if|case|&&|\|\||\?)\b.*desktop/i.test(l) && !/^\s*(\/\/|\*)/.test(l))
|
|
54
|
+
ok('§4a index.js 无 desktop 特化分支', branch.length === 0,
|
|
55
|
+
branch.length ? `命中 ${branch.length} 行:${branch[0].trim().slice(0, 80)}` : '')
|
|
56
|
+
// 分流收口加固(goal bridge-port-host-split G3):端口偏移常量与信号函数在
|
|
57
|
+
// index.js 各唯一一处定义——「分流规则只在一处」机器可判,防分流逻辑散点复刻。
|
|
58
|
+
const offsetDefs = src.split('\n').filter((l) => /^\s*const\s+HOST_BRIDGE_PORT_OFFSET\s*=/.test(l))
|
|
59
|
+
const signalDefs = src.split('\n').filter((l) => /^\s*function\s+currentProfileDirName\s*\(/.test(l))
|
|
60
|
+
ok('§4a+ 分流收口:HOST_BRIDGE_PORT_OFFSET 常量唯一定义', offsetDefs.length === 1,
|
|
61
|
+
`定义处=${offsetDefs.length}`)
|
|
62
|
+
ok('§4a+ 分流收口:currentProfileDirName 信号函数唯一定义', signalDefs.length === 1,
|
|
63
|
+
`定义处=${signalDefs.length}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// §4b 宿主差异收口:desktop 相关选路只允许出现在 host-config.js / lib/client.js
|
|
67
|
+
{
|
|
68
|
+
const files = ['index.js', 'host-config.js', 'lib/client.js']
|
|
69
|
+
const offenders = []
|
|
70
|
+
for (const f of files) {
|
|
71
|
+
const src = readFileSync(join(root, f), 'utf8')
|
|
72
|
+
const hits = src.split('\n').filter((l) => /desktop/i.test(l))
|
|
73
|
+
if (f !== 'host-config.js' && f !== 'lib/client.js' && hits.length) {
|
|
74
|
+
offenders.push(`${f}(${hits.length})`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
ok('§4b 宿主差异收口 host-config/client(index.js 零 desktop 引用)', offenders.length === 0,
|
|
78
|
+
offenders.join(' '))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// §6 goal 文档存在且含可执行验收锚点
|
|
82
|
+
{
|
|
83
|
+
const goalPath = join(root, 'docs/goals/desktop-adaptation.md')
|
|
84
|
+
ok('§6a goal 文档存在', existsSync(goalPath))
|
|
85
|
+
if (existsSync(goalPath)) {
|
|
86
|
+
const txt = readFileSync(goalPath, 'utf8')
|
|
87
|
+
ok('§6b goal 文档引用本可执行验收', /verify-desktop-acceptance/.test(txt),
|
|
88
|
+
'goal 应写明本脚本为验收锚点')
|
|
89
|
+
ok('§6c goal 文档含验收标准节', /## 验收标准/.test(txt))
|
|
90
|
+
} else {
|
|
91
|
+
skipped('§6b/§6c goal 内容', 'goal 文档不存在')
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================
|
|
96
|
+
// B 组:桌面环境断言(有 desktop profile 才跑)
|
|
97
|
+
// ============================================================
|
|
98
|
+
section('B. 桌面环境断言')
|
|
99
|
+
|
|
100
|
+
const dshHome = join(homedir(), '.dsh')
|
|
101
|
+
const desktopPatch = join(dshHome, 'profiles', 'desktop', 'cordis.patch.yml')
|
|
102
|
+
|
|
103
|
+
// 从 desktop patch 文本抠某 provider 镜像块的 baseURL 端口(整块镜像纪律:
|
|
104
|
+
// baseURL 跟随镜像写回时的有效端口)。
|
|
105
|
+
function mirrorBlockPort(text, providerId) {
|
|
106
|
+
const m = text.match(new RegExp(`${providerId}:[\\s\\S]*?baseURL:\\s*http://127\\.0\\.0\\.1:(\\d+)`))
|
|
107
|
+
return m ? Number(m[1]) : null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (structuralOnly) {
|
|
111
|
+
skipped('B 组全部', '--structural 模式:CI 只锁 A 组恒真不变量;B 组结果断言随适配进度变化,留给本机默认跑')
|
|
112
|
+
} else if (!existsSync(desktopPatch)) {
|
|
113
|
+
skipped('B 组全部', `无 desktop profile(${desktopPatch} 不存在)——无桌面环境属预期`)
|
|
114
|
+
} else {
|
|
115
|
+
// 枚举 llm-pi-ai.providers 键(与 workflow 闸门同款逻辑,缩进层级已实测)
|
|
116
|
+
const lines = readFileSync(desktopPatch, 'utf8').split(/\r?\n/)
|
|
117
|
+
const start = lines.findIndex((l) => l.indexOf('- id: llm-pi-ai') === 0)
|
|
118
|
+
ok('§1a desktop patch 存在 llm-pi-ai 条目', start >= 0)
|
|
119
|
+
|
|
120
|
+
if (start >= 0) {
|
|
121
|
+
let end = lines.length
|
|
122
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
123
|
+
if (lines[i].indexOf('- id: ') === 0) { end = i; break }
|
|
124
|
+
}
|
|
125
|
+
const block = lines.slice(start, end)
|
|
126
|
+
let provIdx = -1, baseIndent = 0
|
|
127
|
+
for (let i = 0; i < block.length; i++) {
|
|
128
|
+
const t = block[i].trimEnd()
|
|
129
|
+
if (t.endsWith('providers:')) { provIdx = i; baseIndent = block[i].length - block[i].trimStart().length; break }
|
|
130
|
+
}
|
|
131
|
+
const keyIndent = baseIndent + 2
|
|
132
|
+
const keys = []
|
|
133
|
+
if (provIdx >= 0) {
|
|
134
|
+
for (let i = provIdx + 1; i < block.length; i++) {
|
|
135
|
+
const l = block[i]
|
|
136
|
+
if (l.trim() === '' || l.trim().startsWith('#')) continue
|
|
137
|
+
const indent = l.length - l.trimStart().length
|
|
138
|
+
if (indent <= baseIndent) break
|
|
139
|
+
if (indent !== keyIndent) continue
|
|
140
|
+
const m = l.trim().match(/^([A-Za-z0-9_-]+):\s*$/)
|
|
141
|
+
if (m) keys.push(m[1])
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
ok('§1b providers 键可枚举(结构未破坏)', keys.length > 0, `枚举到 ${JSON.stringify(keys)}`)
|
|
145
|
+
// 接入判据:适配落地后 codebuddy 必须出现;手写块 volces 必须仍在(并存不接管)
|
|
146
|
+
ok('§1c 接入判据:codebuddy 已进入 desktop providers', keys.includes('codebuddy'),
|
|
147
|
+
`当前=${JSON.stringify(keys)}(适配未完成时此项 FAIL 即「未达成」信号)`)
|
|
148
|
+
ok('§1d 手写块并存:volces 未被接管/删除', keys.includes('volces'),
|
|
149
|
+
`当前=${JSON.stringify(keys)}`)
|
|
150
|
+
// 分流端口断言(goal bridge-port-host-split G4):desktop patch 的 qoder/trae
|
|
151
|
+
// 镜像块 baseURL 端口应跟随分流值 3913/3912。两条件分开报:通道镜像块存在
|
|
152
|
+
// 与否(未启用/未同步时路由不存在属预期,SKIP)与端口值本身。
|
|
153
|
+
const patchText = readFileSync(desktopPatch, 'utf8')
|
|
154
|
+
const qoderPort = mirrorBlockPort(patchText, 'qoder')
|
|
155
|
+
const traePort = mirrorBlockPort(patchText, 'trae')
|
|
156
|
+
if (keys.includes('qoder')) {
|
|
157
|
+
ok('§4c desktop qoder 镜像 baseURL 端口 = 3913(分流)', qoderPort === 3913,
|
|
158
|
+
`当前=${qoderPort}`)
|
|
159
|
+
} else {
|
|
160
|
+
skipped('§4c qoder 镜像端口', 'providers.qoder 块不存在(通道未启用/目录未同步——路由存在性管理属预期)')
|
|
161
|
+
}
|
|
162
|
+
if (keys.includes('trae')) {
|
|
163
|
+
ok('§4d desktop trae 镜像 baseURL 端口 = 3912(分流)', traePort === 3912,
|
|
164
|
+
`当前=${traePort}`)
|
|
165
|
+
} else {
|
|
166
|
+
skipped('§4d trae 镜像端口', 'providers.trae 块不存在(通道未启用/目录未同步——路由存在性管理属预期)')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================
|
|
172
|
+
// C 组:活桌面实例断言(诚实标注,不伪造)
|
|
173
|
+
// ============================================================
|
|
174
|
+
section('C. 活桌面实例断言(诚实标注)')
|
|
175
|
+
skipped('§2 三通道端到端(CodeBuddy 桥聊天出正文)', '需运行中的 desktop 实例——由端到端探针承担,不在离线脚本伪造')
|
|
176
|
+
skipped('§3 UI 壳设置卡回归', '需按 desktop UI 壳形态适配 dsh-ui-test 驱动后另跑(先加载 .agents/skills/dsh-ui-regression/)')
|
|
177
|
+
skipped('§1e ?probe=host-config 活实例全绿', '需运行中的 desktop 实例——workflow G4 阶段承担')
|
|
178
|
+
skipped('§4e 分流端口活体验收(listen 3912/3913 探活 + 双实例 probe 各归各 + web 侧 documentPath 落盘)',
|
|
179
|
+
'需运行中的 web/desktop 双实例——由活体探针承担(证据落 docs/probes/),不在离线脚本伪造')
|
|
180
|
+
|
|
181
|
+
// ============================================================
|
|
182
|
+
console.log(`\n${pass} ok / ${fail} FAIL / ${skip} SKIP`)
|
|
183
|
+
console.log('(B 组 SKIP = 无桌面环境属预期;C 组恒 SKIP,活实例断言由探针/GUI 套件承担)')
|
|
184
|
+
process.exit(fail > 0 ? 1 : 0)
|