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
package/local-scan.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * local-scan.js — G7 本机登录态自动检测(只读扫描 + 确认后导入)。
3
+ *
4
+ * 红线:扫描只读、绝不回传任何 secret 值——findings 只带路径/类型/过期等
5
+ * 元数据;importLocalCredential 在用户确认后把凭据从原文件直接搬进对应落点
6
+ * (key 型上游走 openai-compat 注册表,.credentials.yaml 0600)。
7
+ *
8
+ * 每个探测器的产出:{ source, label, path, kind, importable, reason?, detail? }
9
+ * - importable=true 的 finding 必须带 detail.import = { id, baseURL, keyFrom }
10
+ * (keyFrom 指出凭据在哪个文件哪个字段,导入时才读真值)。
11
+ */
12
+
13
+ import { readFileSync } from 'node:fs'
14
+ import { homedir } from 'node:os'
15
+ import { join } from 'node:path'
16
+
17
+ const HOME = homedir()
18
+
19
+ function readJsonSafe(path) {
20
+ try {
21
+ return JSON.parse(readFileSync(path, 'utf8'))
22
+ } catch {
23
+ return null
24
+ }
25
+ }
26
+
27
+ function exists(path) {
28
+ try {
29
+ readFileSync(path)
30
+ return true
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
36
+ /** 各工具探测器。新增工具 = 往这里加一条(保持只读)。 */
37
+ const DETECTORS = [
38
+ function qwen() {
39
+ const creds = readJsonSafe(join(HOME, '.qwen', 'oauth_creds.json'))
40
+ if (!creds) return null
41
+ const resource = typeof creds.resource_url === 'string' && creds.resource_url ? creds.resource_url : null
42
+ const expiry = typeof creds.expiry_date === 'number' ? creds.expiry_date : null
43
+ const expired = expiry != null && expiry < Date.now()
44
+ if (typeof creds.access_token !== 'string' || !creds.access_token || !resource) {
45
+ return { source: 'qwen', label: 'Qwen Code(千问)', path: '~/.qwen/oauth_creds.json', kind: 'oauth', importable: false, reason: 'oauth_creds.json 缺 access_token/resource_url' }
46
+ }
47
+ // access_token 当 Bearer 直用(OpenAI 兼容端点 https://<resource>/v1);
48
+ // 过期与否以导入时探针实测为准(expiry_date 与真实有效期未必一致;
49
+ // 另:Qwen OAuth 免费额度 2026-04-15 已停服,存量 token 大概率被拒)。
50
+ return {
51
+ source: 'qwen', label: 'Qwen Code(千问)', path: '~/.qwen/oauth_creds.json', kind: 'oauth', importable: true,
52
+ detail: {
53
+ expiresAt: expiry,
54
+ expiredHint: expired,
55
+ import: { id: 'qwen', displayName: 'Qwen(本机导入)', baseURL: `https://${resource.replace(/^https?:\/\//, '').replace(/\/+$/, '')}/v1`, keyFrom: { file: join(HOME, '.qwen', 'oauth_creds.json'), field: 'access_token' } },
56
+ },
57
+ }
58
+ },
59
+ function codex() {
60
+ const auth = readJsonSafe(join(HOME, '.codex', 'auth.json'))
61
+ if (!auth) return null
62
+ return { source: 'codex', label: 'Codex(ChatGPT OAuth)', path: '~/.codex/auth.json', kind: 'oauth', importable: false, reason: 'ChatGPT 后端 responses 方言接入复杂,v0.8 暂不支持' }
63
+ },
64
+ function codebuddyCli() {
65
+ if (!exists(join(HOME, '.codebuddy', 'user-state.json'))) return null
66
+ return { source: 'codebuddy-cli', label: 'CodeBuddy CLI', path: '~/.codebuddy/', kind: 'unknown', importable: false, reason: '文件里无独立凭据(登录态在系统 keyring/内存);本插件 OAuth 已覆盖同一网关' }
67
+ },
68
+ function kimiCode() {
69
+ if (!exists(join(HOME, '.kimi-code', 'config.toml'))) return null
70
+ return { source: 'kimi-code', label: 'Kimi Code', path: '~/.kimi-code/', kind: 'unknown', importable: false, reason: '本机服务令牌非 API key;kimi-coding 凭据如已配则无需导入' }
71
+ },
72
+ function zcode() {
73
+ if (!exists(join(HOME, '.zcode', 'cli', 'config.json'))) return null
74
+ return { source: 'zcode', label: 'ZCode', path: '~/.zcode/', kind: 'unknown', importable: false, reason: '已安装,但凭据不在可见文件里(疑似 keyring/sqlite)' }
75
+ },
76
+ ]
77
+
78
+ /** 只读扫描:返回所有命中(含"已安装但不可导入"及其原因)。 */
79
+ export function scanLocalCredentials() {
80
+ const found = []
81
+ for (const detect of DETECTORS) {
82
+ try {
83
+ const hit = detect()
84
+ if (hit) found.push(hit)
85
+ } catch {
86
+ // 单个探测器失败不影响其余(只读操作,失败多为文件竞态删除)。
87
+ }
88
+ }
89
+ return found
90
+ }
91
+
92
+ /**
93
+ * 按 finding.detail.import.keyFrom 读凭据真值(导入路径专用,扫描路径禁用)。
94
+ * 返回 { id, displayName, baseURL, apiKey }。
95
+ */
96
+ export function readImportCredential(findings, source) {
97
+ const f = findings.find((x) => x.source === source)
98
+ if (!f || !f.importable || !f.detail?.import) throw new Error(`${source} 不可导入`)
99
+ const { id, displayName, baseURL, keyFrom } = f.detail.import
100
+ const doc = readJsonSafe(keyFrom.file)
101
+ const apiKey = doc && typeof doc[keyFrom.field] === 'string' ? doc[keyFrom.field].trim() : ''
102
+ if (!apiKey) throw new Error(`${keyFrom.file} 里没有 ${keyFrom.field}`)
103
+ return { id, displayName, baseURL, apiKey }
104
+ }
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "dsh-tap",
3
+ "version": "0.20.0",
4
+ "description": "Unofficial upstream bundle for DeepSeek Harness (dsh): CodeBuddy (copilot.tencent.com) with a runtime-synced model catalogue, web search/fetch, image generation and a streaming bridge; TraeWork CN and Qoder CN subscription channels via local OpenAI translation gateways; plus a key-type OpenAI-compatible provider registry - all managed from a 4-block accordion settings card.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "index.js",
14
+ "local-scan.js",
15
+ "host-config.js",
16
+ "core/",
17
+ "providers/",
18
+ "lib/client.js",
19
+ "cordis.patch.yml",
20
+ "scripts/",
21
+ "docs/goals/",
22
+ "docs/reverse/",
23
+ "docs/rules/",
24
+ "docs/pitfalls.md",
25
+ "docs/diagnosis-cache-quota.md",
26
+ "docs/diagnosis-trae-3003.md",
27
+ "docs/diagnosis-qoder-flash.md",
28
+ "README.md",
29
+ "README.en.md",
30
+ "LICENSE",
31
+ "CHANGELOG.md",
32
+ "AGENTS.md"
33
+ ],
34
+ "scripts": {
35
+ "verify": "node scripts/verify-models.mjs",
36
+ "verify:bridge": "node scripts/verify-bridge.mjs",
37
+ "verify:core": "node scripts/verify-core-generic.mjs",
38
+ "verify:providers": "node scripts/verify-providers.mjs",
39
+ "verify:trae": "node scripts/verify-trae-model-catalog.mjs",
40
+ "verify:trae-provider": "node scripts/verify-trae-provider.mjs",
41
+ "verify:qoder": "node scripts/verify-qoder-provider.mjs",
42
+ "verify:host-config": "node scripts/verify-host-config.mjs",
43
+ "verify:desktop-acceptance": "node scripts/verify-desktop-acceptance.mjs"
44
+ },
45
+ "keywords": [
46
+ "dsh-plugin",
47
+ "deepseek-harness",
48
+ "codebuddy",
49
+ "trae",
50
+ "qoder",
51
+ "llm",
52
+ "deepseek",
53
+ "glm",
54
+ "kimi",
55
+ "minimax",
56
+ "hunyuan"
57
+ ],
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/taikaikaikai-pixel/dsh-tap.git"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/taikaikaikai-pixel/dsh-tap/issues"
64
+ },
65
+ "homepage": "https://github.com/taikaikaikai-pixel/dsh-tap#readme",
66
+ "license": "MIT",
67
+ "dependencies": {
68
+ "@deepseek-ai/schemastery": "3.18.1",
69
+ "yaml": "2.8.0"
70
+ },
71
+ "dsh": {
72
+ "bundle": {
73
+ "patch": "./cordis.patch.yml"
74
+ },
75
+ "client": {
76
+ "inject": [
77
+ "@deepseek-ai/dsh-client-ui-slots"
78
+ ],
79
+ "platform": "web"
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,11 @@
1
+ import { createOpenAICompatProvider } from '../openai-compat.js'
2
+
3
+ /**
4
+ * 火山引擎 Ark(方舟)preset。OpenAI 兼容端点 = /api/v3;
5
+ * 目录 GET /models 用 Bearer <Agent Plan key>。
6
+ */
7
+ export default createOpenAICompatProvider({
8
+ id: 'ark',
9
+ displayName: '火山引擎 Ark',
10
+ baseURL: 'https://ark.cn-beijing.volces.com/api/v3',
11
+ })
@@ -0,0 +1,10 @@
1
+ import { createOpenAICompatProvider } from '../openai-compat.js'
2
+
3
+ /**
4
+ * 阿里云百炼 preset。OpenAI 兼容模式端点 = dashscope compatible-mode。
5
+ */
6
+ export default createOpenAICompatProvider({
7
+ id: 'bailian',
8
+ displayName: '阿里云百炼',
9
+ baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
10
+ })
@@ -0,0 +1,11 @@
1
+ import { createOpenAICompatProvider } from '../openai-compat.js'
2
+
3
+ /**
4
+ * 智谱 BigModel preset。OpenAI 兼容端点 = /api/paas/v4;
5
+ * 2026-09-11 实测:GET /models 假 key 401(令牌已过期或验证不正确)。
6
+ */
7
+ export default createOpenAICompatProvider({
8
+ id: 'bigmodel',
9
+ displayName: '智谱 BigModel',
10
+ baseURL: 'https://open.bigmodel.cn/api/paas/v4',
11
+ })
@@ -0,0 +1,122 @@
1
+ /**
2
+ * providers/codebuddy/agenttool.js — CodeBuddy /agenttool 后端:
3
+ * dsh 原生 web_search / web_fetch 工具的网关实现。
4
+ *
5
+ * 端点方言:POST JSON,信封 {code, msg, data/results};错误以 JSON code/msg
6
+ * 返回(探测证据见 docs/rules/)。UA 头组历史上被 12403 拒过,2026-08-19
7
+ * 复测已无 UA 门(docs/rules/ua-validation.md §2)——头组按零行为变更保留。
8
+ *
9
+ * 计量说明(docs/rules/quota-signals.md R-Q2,STATE.md 课题 4 放行):探测
10
+ * 实测 agenttool search/webfetch 响应体**无计量字段**——重构前 search/fetch
11
+ * 里 `if (data?.usage) recordUsage(...)` 是死路径,本次删除,不重建。
12
+ */
13
+
14
+ import { USER_AGENT } from './headers.js'
15
+ import { CREDENTIAL_UNAVAILABLE_MESSAGE } from './errors.js'
16
+
17
+ /**
18
+ * @param {{ withKeyRotation: (settingsFn: () => object, attempt: Function) => Promise<object> }} deps
19
+ */
20
+ export function createAgentTool({ withKeyRotation }) {
21
+ async function callAgentTool(settings, path, payload, signal) {
22
+ const { res, err } = await withKeyRotation(settings, (cred) => fetch(`${settings().baseURL}${path}`, {
23
+ method: 'POST',
24
+ signal,
25
+ headers: {
26
+ Authorization: cred.authorization,
27
+ ...cred.headers,
28
+ 'Content-Type': 'application/json',
29
+ Accept: 'application/json',
30
+ 'User-Agent': USER_AGENT,
31
+ 'X-Requested-With': 'XMLHttpRequest',
32
+ },
33
+ body: JSON.stringify(payload),
34
+ }))
35
+ if (err) {
36
+ if (err.message === CREDENTIAL_UNAVAILABLE_MESSAGE) throw err
37
+ if (signal?.aborted) throw err
38
+ // undici hides the real reason in err.cause (ECONNRESET, terminated,
39
+ // certificate errors …) — a bare "fetch failed" is undebuggable.
40
+ const causes = []
41
+ for (let e = err; e; e = e.cause) causes.push(e.code ?? e.message ?? String(e))
42
+ throw new Error(`CodeBuddy agenttool ${path} network error: ${causes.join(' ← ')}`)
43
+ }
44
+ if (!res.ok) {
45
+ const body = await res.text().catch(() => '')
46
+ // The gateway answers errors as JSON with code/msg — surface both.
47
+ let codeMsg = ''
48
+ try { const j = JSON.parse(body); if (j?.code != null) codeMsg = ` code ${j.code}: ${j.msg ?? ''}` } catch { /* not JSON */ }
49
+ throw new Error(`CodeBuddy agenttool ${path} HTTP ${res.status}${codeMsg || ` ${body.slice(0, 160)}`}`)
50
+ }
51
+ const data = await res.json().catch(() => null)
52
+ if (data && data.code != null && data.code !== 0) {
53
+ throw new Error(`CodeBuddy agenttool ${path} error ${data.code}: ${data.msg ?? ''}`)
54
+ }
55
+ return data
56
+ }
57
+
58
+ /** Search backend: POST /agenttool/v1/search {query, type, max_results}. */
59
+ function makeSearchProvider(settings) {
60
+ return {
61
+ id: 'codebuddy',
62
+ available() {
63
+ return true // cheap check only; real failures surface per request
64
+ },
65
+ async search(request, signal) {
66
+ const s = settings()
67
+ const data = await callAgentTool(
68
+ settings,
69
+ '/agenttool/v1/search',
70
+ {
71
+ query: request.query,
72
+ type: 'text2text',
73
+ max_results: request.maxResults ?? s.searchMaxResults,
74
+ },
75
+ signal,
76
+ )
77
+ const results = Array.isArray(data?.results) ? data.results : []
78
+ return {
79
+ sources: results
80
+ .filter((r) => typeof r?.url === 'string' && r.url.length > 0)
81
+ .map((r) => ({
82
+ url: r.url,
83
+ ...(typeof r.title === 'string' && r.title.length > 0 ? { title: r.title } : {}),
84
+ ...(typeof r.snippet === 'string' && r.snippet.length > 0 ? { snippet: r.snippet } : {}),
85
+ })),
86
+ // The seam itself truncates to maxResults; we already passed it
87
+ // through as max_results, so nothing extra was cut here.
88
+ truncated: false,
89
+ }
90
+ },
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Fetch backend: POST /agenttool/v1/webfetch {url} → {url, title, content}.
96
+ * The endpoint answers with decoded content or a JSON error, never the
97
+ * target page's HTTP status, so a successful call reports statusCode 200
98
+ * and the body is classified as text.
99
+ */
100
+ function makeFetchProvider(settings) {
101
+ return {
102
+ id: 'codebuddy',
103
+ available() {
104
+ return true
105
+ },
106
+ async fetch(request, signal) {
107
+ const s = settings()
108
+ const data = await callAgentTool(settings, '/agenttool/v1/webfetch', { url: request.url }, signal)
109
+ const content = typeof data?.content === 'string' ? data.content : ''
110
+ const cap = s.fetchBodyCap
111
+ return {
112
+ url: typeof data?.url === 'string' && data.url.length > 0 ? data.url : request.url,
113
+ statusCode: 200,
114
+ body: { kind: 'text', content: content.slice(0, cap) },
115
+ truncated: content.length > cap,
116
+ }
117
+ },
118
+ }
119
+ }
120
+
121
+ return { makeSearchProvider, makeFetchProvider }
122
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * providers/codebuddy/catalog.js — CodeBuddy 网关的目录与额度端点方言。
3
+ *
4
+ * - GET /v3/config:网关自有模型目录(官方 CLI 同款)。方言:UA 必须过
5
+ * 12403 门(规则见 headers.js 注释与 docs/rules/ua-validation.md),
6
+ * API key 另需 x-api-key 头,OAuth 走 Authorization;`X-Product: SaaS`
7
+ * 实测迷信(ua-validation.md §3 no-xproduct 臂 200),为零行为变更保留。
8
+ * - GET /v2/accounts + POST /v2/billing/meter/get-dosage-notify:凭据可达的
9
+ * 额度侧信号(docs/rules/quota-signals.md)。**所有端点响应头零额度信号**
10
+ * (P-Q1/P-Q4 实测)。额度账户级与 WorkBuddy 共享(同一
11
+ * Tencent-Cloud.coding-copilot 认证域)。
12
+ * - POST /billing/meter/get-user-resource:**数值剩余额度**(R-Q7,2026-08-19
13
+ * G2 发现)——控制台计费路径族,**OAuth Bearer 专属**(`ck_` key 401),
14
+ * 返回资源包列表(CapacityRemain/Precise 总量口径、CycleCapacity* 周期口径、
15
+ * TotalDosage)。api-key 模式跳过该调用,卡片回落估算档。
16
+ */
17
+
18
+ import { USER_AGENT } from './headers.js'
19
+
20
+ /**
21
+ * 「目录在列 ≠ /v2 可路由」的已知死条目(恒 11102 service info not found,
22
+ * 踩坑 #42 同族)。判定证据:docs/probes/codebuddy-efforts-matrix-2026-09-22.json
23
+ * (六臂全 11102)+ docs/probes/routing-2026-08-19.jsonl;2026-09-26 复核
24
+ * hy4-preview-x 已离开目录(集合保留——重现即再置灰)。
25
+ * 用途 = 设置卡「可路由 M」计数 + 行置灰徽标;**不改**镜像/选择器行为
26
+ * ( transparency only,移除是另一个决策)。上游修复后从表里删 id 即可。
27
+ */
28
+ export const UNROUTABLE_MODELS = {
29
+ 'glm-4.6v': '目录在列但 /v2/chat/completions 恒 11102 service info not found',
30
+ 'kimi-k2-thinking': '目录在列但 /v2/chat/completions 恒 11102 service info not found',
31
+ 'minimax-m2.5': '目录在列但 /v2/chat/completions 恒 11102 service info not found',
32
+ 'hy4-preview-x': '目录在列但 /v2/chat/completions 恒 11102 service info not found',
33
+ }
34
+
35
+ /**
36
+ * @param {{ resolveCredential: (settingsFn: () => object) => Promise<object|null>,
37
+ * envKey: (envName: string|undefined) => string|null }} deps
38
+ */
39
+ export function createCatalog({ resolveCredential, envKey }) {
40
+ /** 目录端点的认证方言:Authorization + api-key 模式另带 x-api-key。 */
41
+ async function dialectHeaders(settingsFn) {
42
+ const s = settingsFn()
43
+ const cred = await resolveCredential(settingsFn)
44
+ if (!cred) return null
45
+ const headers = {
46
+ Accept: 'application/json',
47
+ Authorization: cred.authorization,
48
+ 'User-Agent': USER_AGENT,
49
+ 'X-Product': 'SaaS', // 迷信(见文件头注释),零行为变更保留
50
+ }
51
+ if (s.authMode !== 'oauth') {
52
+ const active = s.apiKeys.find((k) => k.name === s.activeApiKey)
53
+ const raw = active?.key ?? envKey(s.apiKeyEnv)
54
+ if (raw) headers['x-api-key'] = raw
55
+ }
56
+ return headers
57
+ }
58
+
59
+ /** Fetch the gateway's own model catalog (GET /v3/config). */
60
+ async function fetchModelCatalog(settingsFn) {
61
+ const headers = await dialectHeaders(settingsFn)
62
+ if (!headers) throw new Error('凭据不可用:请先在登录区配置 API Key 或完成 OAuth 登录')
63
+ const res = await fetch(`${settingsFn().baseURL}/v3/config`, { headers })
64
+ if (!res.ok) throw new Error(`模型目录 HTTP ${res.status}`)
65
+ const body = await res.json()
66
+ if (body?.code !== 0) throw new Error(`模型目录错误:${body?.code} ${body?.msg ?? ''}`)
67
+ const data = body.data ?? {}
68
+ const cliEnabled = new Set(
69
+ (data.agents ?? []).find((a) => a.name === 'cli')?.models ?? [],
70
+ )
71
+ const models = (data.models ?? [])
72
+ .filter((m) => typeof m?.id === 'string')
73
+ .map((m) => {
74
+ // 思考强度声明两代形态(2026-09-22 实测):
75
+ // legacy {"effort":"high","summary":"auto"} —— 只给默认档;
76
+ // current {"canDisableThinking":true,"defaultEffort":"high",
77
+ // "supportedEfforts":["low","high","max"]} —— 给能力清单。
78
+ // 后者是档位表的权威来源(无需探测即可驱动选择器与出站线值)。
79
+ const reasoning = m.reasoning && typeof m.reasoning === 'object' ? m.reasoning : null
80
+ const supported = Array.isArray(reasoning?.supportedEfforts)
81
+ ? reasoning.supportedEfforts.filter((s) => typeof s === 'string' && s)
82
+ : null
83
+ const defaultEffort = typeof reasoning?.defaultEffort === 'string' && reasoning.defaultEffort
84
+ ? reasoning.defaultEffort
85
+ : null
86
+ return {
87
+ id: m.id,
88
+ name: typeof m.name === 'string' ? m.name : m.id,
89
+ maxInputTokens: m.maxInputTokens ?? null,
90
+ maxOutputTokens: m.maxOutputTokens ?? null,
91
+ images: m.supportsImages === true,
92
+ cli: cliEnabled.has(m.id),
93
+ reasoning: !!reasoning && (reasoning.effort != null || defaultEffort != null
94
+ || supported != null || typeof reasoning.canDisableThinking === 'boolean'),
95
+ // 目录标注的**默认**档(legacy 的 effort 或新形态的 defaultEffort)——
96
+ // 卡片把它当"未设置时的既有档位"展示,不是档位清单。
97
+ reasoningEffort: typeof reasoning?.effort === 'string' && reasoning.effort
98
+ ? reasoning.effort
99
+ : defaultEffort,
100
+ defaultEffort,
101
+ supportedEfforts: supported,
102
+ canDisableThinking: typeof reasoning?.canDisableThinking === 'boolean'
103
+ ? reasoning.canDisableThinking
104
+ : null,
105
+ }
106
+ })
107
+ return { models, fetchedAt: Date.now() }
108
+ }
109
+
110
+ /**
111
+ * Quota-side signals the plugin credentials can actually reach. Cached 60s;
112
+ * never throws. OAuth 模式追加数值剩余额度(/billing/meter/get-user-resource,
113
+ * quota-signals.md R-Q7);api-key 模式该路径族 401(OAuth-only),
114
+ * numericQuota=false,卡片应手填总额度做估算档。
115
+ */
116
+ let quotaCache = { at: 0, value: null }
117
+
118
+ /** get-user-resource 的数值字段带 Precise 字符串变体;统一取数值。 */
119
+ const num = (v) => {
120
+ const n = Number(v)
121
+ return Number.isFinite(n) ? n : null
122
+ }
123
+
124
+ async function fetchResourceQuota(s, headers) {
125
+ const res = await fetch(`${s.baseURL}/billing/meter/get-user-resource`, {
126
+ method: 'POST',
127
+ headers: { ...headers, 'Content-Type': 'application/json' },
128
+ body: '{}',
129
+ })
130
+ const body = await res.json().catch(() => null)
131
+ const data = body?.code === 0 ? body.data?.Response?.Data ?? null : null
132
+ if (!data) return { resource: null, resourceError: `code ${body?.code ?? `http ${res.status}`}` }
133
+ const packs = (data.Accounts ?? []).map((a) => ({
134
+ name: a.PackageName ?? '',
135
+ size: num(a.CapacitySizePrecise ?? a.CapacitySize),
136
+ remain: num(a.CapacityRemainPrecise ?? a.CapacityRemain),
137
+ cycleSize: num(a.CycleCapacitySizePrecise ?? a.CycleCapacitySize),
138
+ cycleRemain: num(a.CycleCapacityRemainPrecise ?? a.CycleCapacityRemain),
139
+ cycleUsed: num(a.CycleCapacityUsedPrecise ?? a.CycleCapacityUsed),
140
+ cycleStart: a.CycleStartTime ?? '',
141
+ cycleEnd: a.CycleEndTime ?? '',
142
+ }))
143
+ const sum = (k) => Math.round(packs.reduce((acc, p) => acc + (p[k] ?? 0), 0) * 100) / 100
144
+ return {
145
+ resource: {
146
+ // TotalDosage = Σ 总量口径剩余(实测吻合,取整);cycle* 为当前周期口径
147
+ totalRemain: num(data.TotalDosage),
148
+ cycleRemain: sum('cycleRemain'),
149
+ cycleUsed: sum('cycleUsed'),
150
+ cycleSize: sum('cycleSize'),
151
+ packs,
152
+ },
153
+ resourceError: null,
154
+ }
155
+ }
156
+
157
+ async function fetchQuotaSnapshot(settingsFn) {
158
+ const s = settingsFn()
159
+ const cred = await resolveCredential(settingsFn)
160
+ if (!cred) return { error: '凭据不可用:请先配置 API Key 或完成 OAuth 登录' }
161
+ const headers = {
162
+ Accept: 'application/json',
163
+ Authorization: cred.authorization,
164
+ ...cred.headers,
165
+ 'User-Agent': USER_AGENT,
166
+ 'X-Product': 'SaaS',
167
+ }
168
+ // Same dialect as fetchModelCatalog: api-key mode also carries the raw key
169
+ // in x-api-key (the gateway authenticates these routes by it).
170
+ if (s.authMode !== 'oauth') {
171
+ const active = s.apiKeys.find((k) => k.name === s.activeApiKey)
172
+ const raw = active?.key ?? envKey(s.apiKeyEnv)
173
+ if (raw) headers['x-api-key'] = raw
174
+ }
175
+ const readJsonBody = (r) => r.json().catch(() => null)
176
+ const oauthMode = s.authMode === 'oauth'
177
+ const [accBody, dosageBody, resourcePart] = await Promise.all([
178
+ fetch(`${s.baseURL}/v2/accounts`, { headers }).then(readJsonBody, () => null),
179
+ fetch(`${s.baseURL}/v2/billing/meter/get-dosage-notify`, {
180
+ method: 'POST',
181
+ headers: { ...headers, 'Content-Type': 'application/json' },
182
+ body: '{}',
183
+ }).then(readJsonBody, () => null),
184
+ // 数值额度:OAuth-only(R-Q7);api-key 模式不浪费一次 401。
185
+ oauthMode
186
+ ? fetchResourceQuota(s, headers).catch(() => ({ resource: null, resourceError: 'network' }))
187
+ : Promise.resolve({ resource: null, resourceError: null }),
188
+ ])
189
+ const accounts = accBody?.code === 0 ? accBody.data?.accounts ?? [] : null
190
+ // The "current" account is the one flagged lastLogin (observed live), else first.
191
+ const current = accounts?.find((a) => a?.lastLogin === true) ?? accounts?.[0] ?? null
192
+ const dosage = dosageBody?.code === 0 ? dosageBody.data ?? null : null
193
+ return {
194
+ account: current ? {
195
+ nickname: current.nickname ?? '',
196
+ type: current.type ?? '',
197
+ enterpriseName: current.enterpriseName ?? '',
198
+ pluginEnabled: current.pluginEnabled === true,
199
+ } : null,
200
+ accountsError: accBody?.code === 0 ? null : `code ${accBody?.code ?? 'http'}`,
201
+ dosage: dosage ? {
202
+ code: dosage.dosageNotifyCode ?? 0,
203
+ text: dosage.dosageNotifyZh || dosage.dosageNotifyEn || '',
204
+ skipUrl: dosage.skipUrl ?? '',
205
+ } : null,
206
+ dosageError: dosageBody?.code === 0 ? null : `code ${dosageBody?.code ?? 'http'}`,
207
+ numericQuota: oauthMode,
208
+ resource: resourcePart.resource,
209
+ resourceError: resourcePart.resourceError,
210
+ fetchedAt: Date.now(),
211
+ }
212
+ }
213
+
214
+ function quotaSnapshot(settingsFn) {
215
+ if (quotaCache.value && Date.now() - quotaCache.at < 60_000) {
216
+ return Promise.resolve(quotaCache.value)
217
+ }
218
+ return fetchQuotaSnapshot(settingsFn)
219
+ .catch((err) => ({ error: err?.message ?? String(err) }))
220
+ .then((value) => {
221
+ quotaCache = { at: Date.now(), value }
222
+ return value
223
+ })
224
+ }
225
+
226
+ return { fetchModelCatalog, quotaSnapshot }
227
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * providers/codebuddy/errors.js — CodeBuddy 网关错误码表。
3
+ *
4
+ * 每条含义均以探测证据为准(docs/rules/ 下对应规则文档,可复跑脚本在
5
+ * scripts/)。这里是**语义参照表**,不是触发逻辑——代码路径按位置内联
6
+ * 引用,禁止把表项当作"必须长这样"的咒语使用。
7
+ */
8
+
9
+ export const ERROR_CODES = {
10
+ /** /v2/chat/completions 仅流式:非流式请求报此码。证据:AGENTS.md 网关事实节。 */
11
+ 11101: 'chat requires stream:true',
12
+ /** UA 门("check ua"):/v3/config 检查 UA 含 codebuddy/ + 含点版本段。docs/rules/ua-validation.md */
13
+ 12403: 'user-agent gate',
14
+ /** 路由注册表层:image 家族模型未注册(消息逐字回显模型名)。docs/rules/routing.md */
15
+ 14401: 'image model not routed',
16
+ /** 路由配置缺失:video/3d 家族注册表层 + video 后端派发层兜底。docs/rules/routing.md */
17
+ 14407: 'route config not found',
18
+ /** 路由注册表层:chat 家族模型未注册("service info not found")。docs/rules/routing.md */
19
+ 11102: 'service info not found',
20
+ /** chat 后端派发层兜底。docs/rules/routing.md */
21
+ 11103: 'backend dispatch failure',
22
+ /** 入口通道校验层拒绝("Illegal API invocation from an unapproved channel"):
23
+ * 字面量 role:"developer" 单列拒绝的当前拒绝面(2026-08-18 由 200/content_filter
24
+ * 变迁为 500/11128)。docs/rules/content-moderation.md */
25
+ 11128: 'unapproved channel',
26
+ /** 缺少必填参数(OAuth state 创建省略 platform 时观测)。docs/rules/oauth-handshake.md */
27
+ 10001: 'missing required parameter',
28
+ /** OAuth token 轮询未完成——pending/bogus/过期三态同码,不可区分。docs/rules/oauth-handshake.md */
29
+ 11217: 'oauth state not ready (indistinguishable)',
30
+ /** refresh token 无效(HTTP 401 伴随)。docs/rules/oauth-handshake.md */
31
+ 12153: 'refresh token invalid',
32
+ /** reasoning_effort 档位拼写不被该模型接受(extError.code =
33
+ * invalid_reasoning_effort)。2026-09-22 实测:deepseek-v4-pro 拒绝
34
+ * off/disabled/auto(11150),同一拼写在 glm-5.3-flash 上被静默接受——
35
+ * 档位表只列实测/目录声明过的拼写。证据 docs/probes/codebuddy-efforts-*.json */
36
+ 11150: 'invalid_reasoning_effort',
37
+ }
38
+
39
+ /**
40
+ * 凭据不可用错误(组合根在候选为空时抛出;各出站路径凭 message 识别并
41
+ * 原样透传,不包装成网络错误)。文案保持与 0.7.4 一致——调用方与设置卡
42
+ * 均按此串匹配。
43
+ */
44
+ export const CREDENTIAL_UNAVAILABLE_MESSAGE = 'CodeBuddy 凭据不可用(检查插件配置卡的登录设置)'
@@ -0,0 +1,36 @@
1
+ /**
2
+ * providers/codebuddy/headers.js — CodeBuddy 网关的客户端头组。
3
+ *
4
+ * 每个字段的"规则/迷信"判定均以实测为准(docs/rules/ua-validation.md §3,
5
+ * 证据 docs/probes/ua-2026-08-19.jsonl,74 条,可复跑 scripts/probe-ua.mjs):
6
+ *
7
+ * | 字段 | 判定 |
8
+ * |---|---|
9
+ * | User-Agent 中的 `CodeBuddy/2.136.0` 段 | **部分是规则**:/v3/config 的 UA 门只做子串匹配 `/codebuddy\/[^a-z\s]*\./i`(大小写不敏感、版本段只需含点、数值不查);`CLI/unknown` 前缀与具体版本号是迷信。/v2/chat 与 /agenttool 当前实测无 UA 门。 |
10
+ * | X-IDE-Type / X-IDE-Name / X-IDE-Version / X-Product-Version | **对校验是迷信**(变体矩阵里缺失不影响任何端点的 200) |
11
+ * | X-Requested-With / X-Private-Data | **迷信**(探测脚本只发 Accept/Auth/x-api-key/UA 仍全通) |
12
+ * | X-Product: SaaS(仅目录/额度端点携带) | **迷信**(no-xproduct 臂 200) |
13
+ *
14
+ * 工程立场:严格按已证规则构造 UA;其余字段标注为迷信但**原样保留**——
15
+ * 本次重构以零行为变更为纪律,是否参与遥测/路由未证伪(见 ua-validation.md §6),
16
+ * 不作为能力依赖,也不贸然下线。
17
+ */
18
+
19
+ /**
20
+ * 规则成分:`CodeBuddy/x.y` 段(/v3/config UA 门的唯一检查对象)。
21
+ * 完整串照抄官方 CLI 形态,前缀与版本号均为已证迷信。
22
+ * 历史注:AGENTS.md 曾记录 "/agenttool UA 必须是 CLI 形态(12403)"——2026-08-19
23
+ * 复测 /agenttool 已无 UA 门(ua-validation.md §2 R-B),该记录已过时。
24
+ */
25
+ export const USER_AGENT = 'CLI/unknown CodeBuddy/2.136.0'
26
+
27
+ /** 桥与 agenttool 出站的静态头组;规则成分仅 UA 一段,其余为保留的迷信。 */
28
+ export const CLIENT_HEADERS = {
29
+ 'User-Agent': USER_AGENT,
30
+ 'X-IDE-Type': 'CLI',
31
+ 'X-IDE-Name': 'CLI',
32
+ 'X-IDE-Version': '2.133.1',
33
+ 'X-Product-Version': '2.133.1',
34
+ 'X-Requested-With': 'XMLHttpRequest',
35
+ 'X-Private-Data': 'false',
36
+ }