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.
Files changed (121) hide show
  1. package/AGENTS.md +99 -0
  2. package/CHANGELOG.md +751 -0
  3. package/LICENSE +21 -0
  4. package/README.en.md +59 -0
  5. package/README.md +256 -0
  6. package/cordis.patch.yml +385 -0
  7. package/core/bridge.js +698 -0
  8. package/core/json-store.js +91 -0
  9. package/core/rotation.js +108 -0
  10. package/core/usage-meter.js +176 -0
  11. package/docs/diagnosis-cache-quota.md +181 -0
  12. package/docs/diagnosis-qoder-flash.md +67 -0
  13. package/docs/diagnosis-trae-3003.md +302 -0
  14. package/docs/goals/bridge-port-host-split.md +91 -0
  15. package/docs/goals/desktop-adaptation.md +106 -0
  16. package/docs/goals/qoder-cn-provider-design.md +308 -0
  17. package/docs/goals/settings-card-ux-redesign-plan.md +1680 -0
  18. package/docs/goals/settings-card-ux-redesign.md +162 -0
  19. package/docs/goals/trae-agent-v3.md +41 -0
  20. package/docs/goals/trae-work-cn-repair.md +44 -0
  21. 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
  22. package/docs/pitfalls.md +105 -0
  23. package/docs/reverse/trae-cloud-api.md +218 -0
  24. package/docs/reverse/trae-model-catalog.md +129 -0
  25. package/docs/reverse/traework-cn.md +520 -0
  26. package/docs/rules/STATE.md +198 -0
  27. package/docs/rules/content-moderation.md +54 -0
  28. package/docs/rules/dev-role-boundary.md +94 -0
  29. package/docs/rules/extra-providers.md +42 -0
  30. package/docs/rules/gateway-facts.md +91 -0
  31. package/docs/rules/oauth-handshake.md +76 -0
  32. package/docs/rules/prompt-cache.md +93 -0
  33. package/docs/rules/quota-signals.md +125 -0
  34. package/docs/rules/routing.md +84 -0
  35. package/docs/rules/templates/oauth-reverse-checklist.md +42 -0
  36. package/docs/rules/trae-surface.md +242 -0
  37. package/docs/rules/ua-validation.md +81 -0
  38. package/host-config.js +282 -0
  39. package/index.js +2306 -0
  40. package/lib/client.js +2893 -0
  41. package/local-scan.js +104 -0
  42. package/package.json +82 -0
  43. package/providers/ark/index.js +11 -0
  44. package/providers/bailian/index.js +10 -0
  45. package/providers/bigmodel/index.js +11 -0
  46. package/providers/codebuddy/agenttool.js +122 -0
  47. package/providers/codebuddy/catalog.js +227 -0
  48. package/providers/codebuddy/errors.js +44 -0
  49. package/providers/codebuddy/headers.js +36 -0
  50. package/providers/codebuddy/images.js +125 -0
  51. package/providers/codebuddy/index.js +123 -0
  52. package/providers/codebuddy/oauth.js +279 -0
  53. package/providers/deepseek/index.js +11 -0
  54. package/providers/moonshot/index.js +11 -0
  55. package/providers/openai-compat.js +177 -0
  56. package/providers/openrouter/index.js +27 -0
  57. package/providers/qoder/catalog.js +145 -0
  58. package/providers/qoder/cosy.js +419 -0
  59. package/providers/qoder/gateway.js +563 -0
  60. package/providers/qoder/index.js +116 -0
  61. package/providers/qoder/oauth.js +364 -0
  62. package/providers/qoder/qoder_auth.wasm +0 -0
  63. package/providers/qoder/quota.js +56 -0
  64. package/providers/qwen/index.js +15 -0
  65. package/providers/tool-pairing.js +129 -0
  66. package/providers/trae/catalog.js +103 -0
  67. package/providers/trae/errors.js +85 -0
  68. package/providers/trae/gateway.js +853 -0
  69. package/providers/trae/index.js +126 -0
  70. package/providers/trae/oauth.js +443 -0
  71. package/providers/trae/quota.js +75 -0
  72. package/providers/trae/remote.js +365 -0
  73. package/scripts/capture-cache.mjs +65 -0
  74. package/scripts/capture-traffic.mjs +83 -0
  75. package/scripts/hermes-probe-dev-role.mjs +263 -0
  76. package/scripts/measure-latency.mjs +253 -0
  77. package/scripts/probe-ark-thinking.mjs +298 -0
  78. package/scripts/probe-cache-decline.mjs +292 -0
  79. package/scripts/probe-cache-ttl.mjs +221 -0
  80. package/scripts/probe-cache.mjs +156 -0
  81. package/scripts/probe-codebuddy-efforts.mjs +355 -0
  82. package/scripts/probe-codebuddy-tier-wiring.mjs +244 -0
  83. package/scripts/probe-effort-gaps.mjs +133 -0
  84. package/scripts/probe-media.mjs +115 -0
  85. package/scripts/probe-moderation.mjs +159 -0
  86. package/scripts/probe-oauth.mjs +617 -0
  87. package/scripts/probe-qoder-attribution-arm8.mjs +92 -0
  88. package/scripts/probe-qoder-attribution-arm9.mjs +102 -0
  89. package/scripts/probe-qoder-attribution.mjs +266 -0
  90. package/scripts/probe-qoder-flash-confirm.mjs +94 -0
  91. package/scripts/probe-qoder-live.mjs +344 -0
  92. package/scripts/probe-qoder-matrix.mjs +274 -0
  93. package/scripts/probe-qoder-null-content.mjs +153 -0
  94. package/scripts/probe-qoder-pairing.mjs +308 -0
  95. package/scripts/probe-qoder-quota.mjs +162 -0
  96. package/scripts/probe-qoder-thinking-config.mjs +52 -0
  97. package/scripts/probe-qoder-thinking-efforts.mjs +269 -0
  98. package/scripts/probe-quota.mjs +136 -0
  99. package/scripts/probe-quota2.mjs +144 -0
  100. package/scripts/probe-routing.mjs +226 -0
  101. package/scripts/probe-trae-3003-diagnosis.mjs +139 -0
  102. package/scripts/probe-trae-agent-v3.mjs +399 -0
  103. package/scripts/probe-trae-efforts.mjs +149 -0
  104. package/scripts/probe-trae-live.mjs +147 -0
  105. package/scripts/probe-trae-max-effort.mjs +179 -0
  106. package/scripts/probe-trae-model-routing.mjs +381 -0
  107. package/scripts/probe-trae-thinking-scene.mjs +238 -0
  108. package/scripts/probe-trae-transport-outage.mjs +176 -0
  109. package/scripts/probe-ua.mjs +508 -0
  110. package/scripts/trae-model-catalog.mjs +632 -0
  111. package/scripts/verify-agents-md.mjs +45 -0
  112. package/scripts/verify-bridge.mjs +1041 -0
  113. package/scripts/verify-core-generic.mjs +302 -0
  114. package/scripts/verify-desktop-acceptance.mjs +184 -0
  115. package/scripts/verify-host-config.mjs +265 -0
  116. package/scripts/verify-models.mjs +390 -0
  117. package/scripts/verify-providers.mjs +255 -0
  118. package/scripts/verify-qoder-provider.mjs +1020 -0
  119. package/scripts/verify-rotation.mjs +308 -0
  120. package/scripts/verify-trae-model-catalog.mjs +284 -0
  121. package/scripts/verify-trae-provider.mjs +1451 -0
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * probe-effort-gaps.mjs — 「思考档位声明有没有真的到路由条目」缺口检测(免凭据)。
4
+ *
5
+ * 为什么需要(踩坑 #64):宿主输入框的「推理等级」只认模型条目的
6
+ * `reasoningEfforts`,而声明要一路走过「上游目录 → 适配器投影 → 镜像块」三层,
7
+ * **任何一层丢掉都表现为"UI 里没入口"且零报错**——0.18.0 之前的 Qoder 14 个模型
8
+ * 全丢就是这个形态。本脚本把三层对账成一张表,让漏项可见而不是靠人肉发现。
9
+ *
10
+ * 检查(只读,零写入):
11
+ * 1. 插件侧声明:live 实例 `model-list`(codebuddy)与 GET 视图
12
+ * `qoder.models.efforts`(qoder)——这是"适配器投影后"的档位表;
13
+ * 2. 路由侧实况:宿主配置层(profile 的 cordis.patch.yml / 旧宿主 settings.yaml)
14
+ * 里 `llm-pi-ai.providers.<route>` 的条目是否带同样的 `reasoningEfforts`,
15
+ * 以及路由是否声明了 `compat.supportsReasoningEffort`(缺它 pi-ai 出站不写
16
+ * `reasoning_effort` = 出档但不生效);
17
+ * 3. 缺口 = 插件声明了档位而路由条目没有(或路由缺 compat)⇒ exit 1。
18
+ *
19
+ * 用法:
20
+ * node scripts/probe-effort-gaps.mjs [--url http://127.0.0.1:19387] [--token <web token>] [--patch <cordis.patch.yml>]
21
+ * Trae:本机无 state.vscdb 时目录不可用,如实报 SKIP(不伪造)。
22
+ */
23
+ import { readFileSync, readdirSync, existsSync } from 'node:fs'
24
+ import { homedir } from 'node:os'
25
+ import { join } from 'node:path'
26
+ import YAML from 'yaml'
27
+
28
+ const args = process.argv.slice(2)
29
+ const argOf = (n, d) => { const i = args.indexOf(n); return i >= 0 && args[i + 1] !== undefined ? args[i + 1] : d }
30
+ const BASE = argOf('--url', 'http://127.0.0.1:19387').replace(/\/+$/, '')
31
+ const TOKEN = argOf('--token', '')
32
+ const withToken = (u) => (TOKEN ? u + (u.includes('?') ? '&' : '?') + 'token=' + encodeURIComponent(TOKEN) : u)
33
+
34
+ async function getView() {
35
+ const res = await fetch(withToken(`${BASE}/dsh-tap/settings`), { signal: AbortSignal.timeout(15000) })
36
+ if (!res.ok) throw new Error(`GET /dsh-tap/settings HTTP ${res.status}`)
37
+ return res.json()
38
+ }
39
+ async function action(name) {
40
+ const res = await fetch(withToken(`${BASE}/dsh-tap/settings`), {
41
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: name }),
42
+ signal: AbortSignal.timeout(60000),
43
+ })
44
+ return res.json().catch(() => null)
45
+ }
46
+
47
+ /** 宿主配置层:显式 --patch,否则在 ~/.dsh 里找带 llm-pi-ai 条目的那份。 */
48
+ function findPatch(explicit) {
49
+ if (explicit) return explicit
50
+ const candidates = []
51
+ const profiles = join(homedir(), '.dsh', 'profiles')
52
+ if (existsSync(profiles)) {
53
+ for (const d of readdirSync(profiles)) {
54
+ const p = join(profiles, d, 'cordis.patch.yml')
55
+ if (existsSync(p)) candidates.push(p)
56
+ }
57
+ }
58
+ candidates.push(join(homedir(), '.dsh', 'settings.yaml'))
59
+ for (const p of candidates) {
60
+ try {
61
+ const doc = YAML.parse(readFileSync(p, 'utf8'))
62
+ const list = Array.isArray(doc) ? doc : [doc]
63
+ const entry = list.find((e) => e?.id === 'llm-pi-ai')
64
+ if (entry?.config?.providers) return p
65
+ } catch { /* next */ }
66
+ }
67
+ return null
68
+ }
69
+
70
+ function routeBlock(patchPath, route) {
71
+ const doc = YAML.parse(readFileSync(patchPath, 'utf8'))
72
+ const list = Array.isArray(doc) ? doc : [doc]
73
+ const entry = list.find((e) => e?.id === 'llm-pi-ai')
74
+ return entry?.config?.providers?.[route] ?? null
75
+ }
76
+
77
+ const problems = []
78
+ const notes = []
79
+ const check = (route, declared, block, enabledIds) => {
80
+ if (!declared) { console.log(`\n[${route}] 插件侧未提供档位声明(旧宿主或未同步)→ SKIP`); return }
81
+ const enabled = enabledIds ? new Set(enabledIds) : null
82
+ const ids = Object.keys(declared).filter((id) => (declared[id] ?? []).length > 0 && (!enabled || enabled.has(id)))
83
+ if (!block) { console.log(`\n[${route}] 路由块不在宿主配置层 → SKIP(未启用?)`); return }
84
+ const compat = block?.compat?.supportsReasoningEffort === true
85
+ console.log(`\n[${route}] 声明了档位且**已启用**的模型 ${ids.length} 个;路由条目 ${(block.models ?? []).length} 个;compat.supportsReasoningEffort=${compat}`)
86
+ const byId = new Map((block.models ?? []).map((m) => [m.id, m]))
87
+ let tiered = 0
88
+ for (const id of ids) {
89
+ const entry = byId.get(id)
90
+ if (!entry) {
91
+ // 声明了但**路由里没这个模型** ⇒ 选择器不会列出它,谈不上"档位缺口";
92
+ // 这是配置分歧(未铺进选择器),单独记一条提示。
93
+ notes.push(`${route}/${id}: 插件声明了档位,但路由条目里没有这个模型(未铺进选择器)`)
94
+ continue
95
+ }
96
+ tiered += 1
97
+ const mirrored = entry?.reasoningEfforts && Object.keys(entry.reasoningEfforts).length ? Object.keys(entry.reasoningEfforts) : null
98
+ const okRow = mirrored && compat
99
+ console.log(` ${okRow ? 'ok ' : 'GAP '} ${id.padEnd(22)} 插件声明=[${declared[id].join('/')}] 路由镜像=${mirrored ? `[${mirrored.join('/')}]` : '(无)'}`)
100
+ if (!mirrored) problems.push(`${route}/${id}: 插件声明了档位但路由条目没有 reasoningEfforts(宿主选择器不会出档)`)
101
+ else if (!compat) problems.push(`${route}: 有档位表但路由缺 compat.supportsReasoningEffort(出档也不生效)`)
102
+ }
103
+ if (tiered === 0) console.log(` (声明档位且已启用的模型一个都不在路由里)`)
104
+ }
105
+
106
+ const view = await getView()
107
+ const patchPath = findPatch(argOf('--patch', null))
108
+ console.log(`实例 ${BASE}|宿主配置层 ${patchPath ?? '(未找到)'}`)
109
+ if (!patchPath) { console.error('找不到宿主配置层(--patch 指定 profile 的 cordis.patch.yml 或 ~/.dsh/settings.yaml)'); process.exitCode = 2 } else {
110
+
111
+ const modelList = await action('model-list')
112
+ // 只对账**已启用**的模型(effectiveIds):用户显式停用的模型本就不该铺进路由,
113
+ // 拿"声明"全集去比会把停用项误报成缺口。
114
+ check('codebuddy', modelList?.efforts ?? null, routeBlock(patchPath, 'codebuddy'), view?.models?.effectiveIds)
115
+ check('qoder', view?.qoder?.models?.efforts ?? null, routeBlock(patchPath, 'qoder'), null)
116
+ // Trae(G3 起出档):声明 = 目录 reasoning_effort_config 原文拼写;镜像键是
117
+ // 宿主枚举映射(light→low/extra_high→xhigh),对账只看「有表 + 有 compat」,
118
+ // 键名单不同属预期形态。
119
+ check('trae', view?.trae?.models?.efforts ?? null, routeBlock(patchPath, 'trae'), null)
120
+
121
+ if (notes.length) {
122
+ console.log('\n=== 提示(不是档位缺口,是配置分歧)===')
123
+ for (const n of notes) console.log(' ·', n)
124
+ }
125
+ if (problems.length) {
126
+ console.log('\n=== 缺口 ===')
127
+ for (const p of problems) console.log(' ✗', p)
128
+ console.log(`\n共 ${problems.length} 处:声明没走到宿主选择器(重启 dsh 后再跑一次;仍是缺口就是投影/镜像代码漏了)`)
129
+ process.exitCode = 1
130
+ } else {
131
+ console.log('\n=== 无档位缺口:所有已铺进路由的声明都到了条目,且路由声明了 compat ===')
132
+ }
133
+ }
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Probe the CodeBuddy gateway's media endpoints (item 3): image generation,
4
+ * video generation, and 3D candidates. Repeatable; every attempt is appended
5
+ * to the output JSON with the request shape and a SANITIZED response (signed
6
+ * COS URLs are stripped to their path — the query carries credentials).
7
+ *
8
+ * Usage:
9
+ * node scripts/probe-media.mjs [--out docs/probes/media-<ts>.json]
10
+ * [--prompt "..."] [--model hunyuan-image-v3.0-art]
11
+ *
12
+ * Credentials come from ~/.dsh/codebuddy-plugin.json (active api key); the
13
+ * key is never printed or written to the output file.
14
+ */
15
+
16
+ import { readFileSync, mkdirSync, writeFileSync } from 'node:fs'
17
+ import { homedir } from 'node:os'
18
+ import { join, dirname } from 'node:path'
19
+ import { fileURLToPath } from 'node:url'
20
+
21
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
22
+ const args = process.argv.slice(2)
23
+ const opt = (name, dflt) => {
24
+ const i = args.indexOf(`--${name}`)
25
+ return i >= 0 ? args[i + 1] : dflt
26
+ }
27
+ const PROMPT = opt('prompt', 'a small red circle centered on a plain white background')
28
+ const IMG_MODEL = opt('model', 'hunyuan-image-v3.0-art')
29
+ const OUT = opt('out', join(ROOT, 'docs', 'probes', `media-${new Date().toISOString().slice(0, 10)}.json`))
30
+
31
+ const cfg = JSON.parse(readFileSync(join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'codebuddy-plugin.json'), 'utf8'))
32
+ const KEY = cfg.apiKeys?.find((k) => k.name === cfg.activeApiKey)?.key
33
+ if (!KEY) { console.error('no active api key in codebuddy-plugin.json'); process.exit(1) }
34
+ const BASE = cfg.baseURL ?? 'https://copilot.tencent.com'
35
+
36
+ const CLI_HEADERS = {
37
+ 'User-Agent': 'CLI/unknown CodeBuddy/2.136.0',
38
+ 'X-IDE-Type': 'CLI',
39
+ 'X-IDE-Name': 'CLI',
40
+ 'X-Requested-With': 'XMLHttpRequest',
41
+ }
42
+
43
+ /** Strip signed-url credentials and cap long values for the sample file. */
44
+ function sanitize(value, depth = 0) {
45
+ if (typeof value === 'string') {
46
+ if (/^https?:\/\//.test(value)) {
47
+ const [base] = value.split('?')
48
+ return value.includes('?') ? `${base}?<signed-query-stripped>` : value
49
+ }
50
+ return value.length > 300 ? `${value.slice(0, 300)}…(${value.length} chars)` : value
51
+ }
52
+ if (Array.isArray(value)) return value.map((v) => sanitize(v, depth + 1))
53
+ if (value && typeof value === 'object') {
54
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, sanitize(v, depth + 1)]))
55
+ }
56
+ return value
57
+ }
58
+
59
+ const records = []
60
+
61
+ async function attempt(name, path, payload, { cliHeaders = true } = {}) {
62
+ const headers = {
63
+ 'Content-Type': 'application/json',
64
+ Accept: 'application/json',
65
+ Authorization: 'Bearer <redacted-in-sample>',
66
+ ...(cliHeaders ? CLI_HEADERS : {}),
67
+ }
68
+ const t0 = Date.now()
69
+ let record
70
+ try {
71
+ const res = await fetch(`${BASE}${path}`, {
72
+ method: 'POST',
73
+ headers: { ...headers, Authorization: `Bearer ${KEY}` },
74
+ body: JSON.stringify(payload),
75
+ })
76
+ const text = await res.text()
77
+ let body = null
78
+ try { body = JSON.parse(text) } catch { body = text.slice(0, 400) }
79
+ record = { name, path, payload, cliHeaders, status: res.status, ms: Date.now() - t0, body: sanitize(body) }
80
+ } catch (err) {
81
+ record = { name, path, payload, cliHeaders, ms: Date.now() - t0, error: String(err?.message ?? err) }
82
+ }
83
+ records.push({ ts: new Date().toISOString(), ...record })
84
+ console.log(`${record.status ?? 'ERR'} ${String(record.ms).padStart(6)}ms ${name} ${JSON.stringify(record.body ?? record.error)?.slice(0, 220)}`)
85
+ return record
86
+ }
87
+
88
+ async function main() {
89
+ console.log(`probing ${BASE} → ${OUT}`)
90
+ // 1. image generation, with and without the CLI-shaped UA
91
+ await attempt('images/generations (CLI headers)', '/v2/images/generations', {
92
+ model: IMG_MODEL, prompt: PROMPT, size: '1024x1024', n: 1,
93
+ })
94
+ await attempt('images/generations (plain UA)', '/v2/images/generations', {
95
+ model: IMG_MODEL, prompt: PROMPT, size: '1024x1024', n: 1,
96
+ }, { cliHeaders: false })
97
+ // 2. video: the catalog lists no video model; historical probe returned
98
+ // 14407 "route config not found" — re-confirm.
99
+ await attempt('videos/generations', '/v2/videos/generations', {
100
+ model: 'hunyuan-video-t2v', prompt: PROMPT,
101
+ })
102
+ // 3. 3D: no CLI trace, no catalog model — probe the obvious candidates.
103
+ await attempt('3d/generations', '/v2/3d/generations', { model: 'hunyuan-3d', prompt: PROMPT })
104
+ await attempt('agenttool 3d', '/agenttool/v1/3d', { prompt: PROMPT })
105
+
106
+ // close the JSON array
107
+ mkdirSync(dirname(OUT), { recursive: true })
108
+ writeFileSync(OUT, JSON.stringify(records, null, 2) + '\n')
109
+ console.log(`samples → ${OUT}`)
110
+ }
111
+
112
+ main().catch((err) => {
113
+ console.error(`probe-media crashed: ${err.stack ?? err.message}`)
114
+ process.exit(1)
115
+ })
@@ -0,0 +1,159 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Moderation-layer probe (topic 6 of docs/rules/): at WHICH gateway layer
4
+ * does the role:"developer" rejection fire?
5
+ *
6
+ * Spell/incident under test (AGENTS.md:32, CHANGELOG 0.7.4): since
7
+ * 2026-08-18 16:24 UTC the gateway's content moderation answers payloads
8
+ * containing a role:"developer" message with finish_reason: content_filter
9
+ * ("您当前输入的信息存在敏感内容"); developer→system passes; deepseek-v3 was
10
+ * "unaffected" — but ONLY because pi-ai never emits developer for
11
+ * non-reasoning models. Nobody has yet sent a developer-role message to a
12
+ * non-reasoning model deliberately.
13
+ *
14
+ * Candidate layers:
15
+ * L1 gateway pre-routing middleware: a model-agnostic role check
16
+ * L2 reasoning-model backend path: only those backends run the check
17
+ * L3 output-side moderation (not our case: the refusal is pre-generation)
18
+ *
19
+ * Pre-registered predictions (2026-08-19):
20
+ * P-M1 deepseek-v3 + developer role → content_filter too (L1: the check
21
+ * is model-agnostic; v3's "immunity" was purely a pi-ai artifact)
22
+ * P-M2 the rejection is pre-generation but the gateway still meters the
23
+ * processed input: usage present, prompt_tokens ≈ input size,
24
+ * credit > 0
25
+ * P-M3 position independence: a developer message NOT in first position
26
+ * (after a user message) is still rejected
27
+ * P-M4 control: identical bytes with developer→system pass with normal
28
+ * finish_reason (stop/length)
29
+ *
30
+ * Understanding-only, no circumvention research: every arm reproduces the
31
+ * DOCUMENTED rejection surface; the plugin's bridge rewrite stays as-is.
32
+ * Low rate: 2s spacing, 5 arms, max_tokens=8.
33
+ *
34
+ * Usage: node scripts/probe-moderation.mjs [--out docs/probes/moderation-YYYY-MM-DD.jsonl]
35
+ */
36
+
37
+ import { readFileSync, existsSync, appendFileSync, mkdirSync } from 'node:fs'
38
+ import { homedir } from 'node:os'
39
+ import { join, dirname } from 'node:path'
40
+
41
+ const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
42
+ const GATEWAY = 'https://copilot.tencent.com'
43
+ const SPACING_MS = 2000
44
+ const OUT = process.argv.includes('--out')
45
+ ? process.argv[process.argv.indexOf('--out') + 1]
46
+ : join('docs', 'probes', `moderation-${new Date().toISOString().slice(0, 10)}.jsonl`)
47
+
48
+ function resolveKey() {
49
+ try {
50
+ const cfg = JSON.parse(readFileSync(join(DSH_HOME, 'codebuddy-plugin.json'), 'utf8'))
51
+ const active = (cfg.apiKeys ?? []).find((k) => k.name === cfg.activeApiKey)
52
+ if (active?.key) return active.key
53
+ } catch { /* fall through */ }
54
+ if (process.env.CODEBUDDY_API_KEY) return process.env.CODEBUDDY_API_KEY
55
+ const credFile = join(DSH_HOME, '.credentials.yaml')
56
+ if (existsSync(credFile)) {
57
+ const m = readFileSync(credFile, 'utf8').match(/^\s*CODEBUDDY_API_KEY:\s*["']?([^"'\s]+)["']?\s*$/m)
58
+ if (m) return m[1]
59
+ }
60
+ throw new Error('no CodeBuddy credential found')
61
+ }
62
+
63
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
64
+ const SYS = 'You are a concise test agent. Answer with exactly: OK'
65
+ const redact = (v) => typeof v === 'string'
66
+ ? v.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, '<redacted>').replace(/\b\d{6,}\b/g, '<redacted>')
67
+ : v
68
+
69
+ const ARMS = [
70
+ { name: 'M0-control-developer', model: 'glm-5.2', pred: null,
71
+ messages: [{ role: 'developer', content: SYS }, { role: 'user', content: 'hi' }],
72
+ note: 'reproduce the documented rejection on a reasoning model' },
73
+ { name: 'P-M1-v3-developer', model: 'deepseek-v3', pred: 'P-M1',
74
+ messages: [{ role: 'developer', content: SYS }, { role: 'user', content: 'hi' }],
75
+ note: 'L1 predicts content_filter even on a non-reasoning model' },
76
+ { name: 'P-M3-developer-middle', model: 'glm-5.2', pred: 'P-M3',
77
+ messages: [{ role: 'user', content: 'hi' }, { role: 'developer', content: SYS }, { role: 'user', content: 'reply OK' }],
78
+ note: 'developer not in first position still rejected' },
79
+ { name: 'P-M4-control-system', model: 'glm-5.2', pred: 'P-M4',
80
+ messages: [{ role: 'system', content: SYS }, { role: 'user', content: 'hi' }],
81
+ note: 'identical bytes with system pass with a normal finish_reason' },
82
+ ]
83
+
84
+ // Round 2 (2026-08-19): the rejection surface has MORPHED since the
85
+ // 2026-08-18 incident — then: 200 SSE + finish_reason content_filter +
86
+ // refusal text; now: HTTP 500 + code 11128 "Illegal API invocation from an
87
+ // unapproved channel", no usage, no credit, on v3 AND glm AND mid-payload
88
+ // positions (P-M1 direction confirmed: model-agnostic; P-M3 confirmed:
89
+ // position-agnostic; P-M2 moot: nothing is metered).
90
+ // The gate's wording is "unapproved channel" — is 11128 a developer-SPECIFIC
91
+ // denylist or a role WHITELIST (system/user/assistant/tool)? One clean
92
+ // discriminator (understanding only — a made-up role has no client value):
93
+ const ARMS_R2 = [
94
+ { name: 'P-M5-bogus-role', model: 'glm-5.2', pred: 'P-M5',
95
+ messages: [{ role: 'hacker', content: SYS }, { role: 'user', content: 'hi' }],
96
+ note: 'whitelist hypothesis → same 500/11128; developer-denylist hypothesis → a different error' },
97
+ ]
98
+
99
+ async function probe(key, arm) {
100
+ const t0 = Date.now()
101
+ try {
102
+ const res = await fetch(`${GATEWAY}/v2/chat/completions`, {
103
+ method: 'POST',
104
+ headers: {
105
+ 'Content-Type': 'application/json',
106
+ Authorization: `Bearer ${key}`,
107
+ 'User-Agent': 'CLI/unknown CodeBuddy/2.136.0',
108
+ },
109
+ body: JSON.stringify({
110
+ model: arm.model, stream: true, stream_options: { include_usage: true },
111
+ max_tokens: 8, messages: arm.messages,
112
+ }),
113
+ })
114
+ const text = await res.text()
115
+ let finishReason = null, content = '', usage = null
116
+ for (const line of text.split('\n')) {
117
+ const l = line.trim()
118
+ if (!l.startsWith('data:')) continue
119
+ let chunk
120
+ try { chunk = JSON.parse(l.slice(5).trim()) } catch { continue }
121
+ if (chunk.usage) usage = chunk.usage
122
+ const ch = chunk.choices?.[0]
123
+ if (ch?.finish_reason) finishReason = ch.finish_reason
124
+ if (typeof ch?.delta?.content === 'string') content += ch.delta.content
125
+ }
126
+ // Non-SSE bodies (e.g. the 500 envelope) are the rejection surface —
127
+ // record them verbatim (redacted), they carry the layer's fingerprint.
128
+ const bodyPreview = finishReason === null && content === '' ? redact(text.slice(0, 300)) : null
129
+ return {
130
+ type: 'probe', name: arm.name, model: arm.model, pred: arm.pred, note: arm.note,
131
+ roles: arm.messages.map((m) => m.role),
132
+ status: res.status, finishReason,
133
+ contentPreview: redact(content.slice(0, 80)),
134
+ bodyPreview,
135
+ usage: usage ? {
136
+ prompt_tokens: usage.prompt_tokens, completion_tokens: usage.completion_tokens,
137
+ credit: usage.credit, prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens,
138
+ } : null,
139
+ ms: Date.now() - t0,
140
+ }
141
+ } catch (err) {
142
+ return { type: 'probe', name: arm.name, model: arm.model, pred: arm.pred, note: arm.note, error: String(err?.message ?? err), ms: Date.now() - t0 }
143
+ }
144
+ }
145
+
146
+ const key = resolveKey()
147
+ mkdirSync(dirname(OUT), { recursive: true })
148
+ appendFileSync(OUT, JSON.stringify({ type: 'run', at: new Date().toISOString(), gateway: GATEWAY }) + '\n')
149
+ console.log(`evidence → ${OUT}`)
150
+
151
+ const SET = process.argv.includes('--set') ? process.argv[process.argv.indexOf('--set') + 1] : 'matrix'
152
+ const arms = SET === 'r2' ? ARMS_R2 : ARMS
153
+ for (const arm of arms) {
154
+ const rec = await probe(key, arm)
155
+ appendFileSync(OUT, JSON.stringify(rec) + '\n')
156
+ console.log(`${arm.name.padEnd(24)} status=${rec.status ?? '-'} finish=${rec.finishReason ?? '-'} credit=${rec.usage?.credit ?? '-'} prompt=${rec.usage?.prompt_tokens ?? '-'} body=${(rec.bodyPreview ?? rec.contentPreview ?? rec.error ?? '').slice(0, 90)}`)
157
+ await sleep(SPACING_MS)
158
+ }
159
+ console.log('done.')