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,617 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* OAuth handshake probe (topic 5 of docs/rules/): state lifecycle, token
|
|
4
|
+
* privilege boundary, refresh invalidation conditions.
|
|
5
|
+
*
|
|
6
|
+
* Spell under test (AGENTS.md:25): "POST /v2/plugin/auth/state?platform=CLI
|
|
7
|
+
* (三个 X-No-* 头)→ {state, authUrl} → 轮询 GET /v2/plugin/auth/token?state=
|
|
8
|
+
* (11217=未完成)→ GET /v2/plugin/login/account;刷新 POST
|
|
9
|
+
* /v2/plugin/auth/token/refresh(X-Refresh-Token)"。
|
|
10
|
+
*
|
|
11
|
+
* Probeable WITHOUT an interactive login (this script, read-only):
|
|
12
|
+
* state creation/polling semantics, bogus-state error shape, multi-flight,
|
|
13
|
+
* X-No-* necessity, platform parameter, short-horizon TTL, and the
|
|
14
|
+
* FAILURE SHAPE of the refresh endpoint with a bogus token.
|
|
15
|
+
* Not probeable without a completed browser login: real-token privilege
|
|
16
|
+
* boundary and refresh rotation semantics — recorded as 未解 with the exact
|
|
17
|
+
* unlock condition (one interactive device login).
|
|
18
|
+
*
|
|
19
|
+
* Pre-registered predictions (2026-08-19):
|
|
20
|
+
* P-O1 bogus state → code ≠ 0 AND ≠ 11217 (invalid is distinguishable
|
|
21
|
+
* from pending)
|
|
22
|
+
* P-O2 two states coexist: creating state2 does NOT invalidate state1
|
|
23
|
+
* (state1 still answers 11217)
|
|
24
|
+
* P-O3 the three X-No-* headers are superstition: state creation without
|
|
25
|
+
* them still returns code 0 + a state
|
|
26
|
+
* P-O4 platform is a free-form client tag: platform=WORKBUDDY and the
|
|
27
|
+
* omitted-parameter form both still return code 0 + a state
|
|
28
|
+
* P-O5 state TTL ≥ 240s: a state still answers 11217 at t+60s and t+240s
|
|
29
|
+
* (the plugin's 10-min client timeout is unrelated to server TTL)
|
|
30
|
+
* P-O6 GET /v2/plugin/login/account with a PENDING state → code ≠ 0
|
|
31
|
+
* P-O7 refresh with a bogus X-Refresh-Token → non-2xx or code ≠ 0
|
|
32
|
+
* (failures are loud, not silent)
|
|
33
|
+
*
|
|
34
|
+
* Low rate: 2s spacing, single account, all endpoints are auth-flow reads;
|
|
35
|
+
* no login is completed, no real token is minted.
|
|
36
|
+
*
|
|
37
|
+
* ── v0.8 G2 扩展(2026-08-19,真实 token 已解锁:用户已完成交互登录)────────
|
|
38
|
+
* 新集合(证据默认落 docs/probes/oauth-token-<date>.jsonl):
|
|
39
|
+
* matrix token×端点矩阵:OAuth Bearer vs ck_ key 在
|
|
40
|
+
* accounts / config / dosage-notify / agenttool-search / chat 五端点
|
|
41
|
+
* 的可达性对照(oauth-handshake.md §5 解锁清单第 1 条)
|
|
42
|
+
* quota 额度端点搜寻:copilot.tencent.com + www.workbuddy.cn 两域名的
|
|
43
|
+
* plan/billing/credit/quota 候选路径 + www.codebuddy.cn 控制台
|
|
44
|
+
* plan API 候选,全部 Bearer token 实测(解锁清单第 2 条,
|
|
45
|
+
* 用户明确授权的探测面扩展)
|
|
46
|
+
* refresh 真实 refresh 轮换语义:真 refresh 一次(成功后按 refreshOAuth
|
|
47
|
+
* 同逻辑回写 auth 文件)→ 旧 refresh token 复跑观测作废码 →
|
|
48
|
+
* 旧 access token 复用观测(解锁清单第 3 条)
|
|
49
|
+
*
|
|
50
|
+
* 预注册预测(2026-08-19):
|
|
51
|
+
* P-T1 OAuth token 五端点全可达(官方 CLI OAuth 模式即如此)
|
|
52
|
+
* P-T2 token 与 ck_ key 的端点可达矩阵无差异
|
|
53
|
+
* P-T6 OAuth 下 chat 响应头同样零额度字段(R-Q1 对认证方式不变)
|
|
54
|
+
* P-QP1 copilot.tencent.com 的 billing/plan/credit/quota/subscription
|
|
55
|
+
* 猜测路径全部路径层 404(与"CLI 端点全集无 quota 查询"一致)
|
|
56
|
+
* P-QP2 www.workbuddy.cn 用同一 token 镜像可达 accounts/dosage/config
|
|
57
|
+
* P-QP3 workbuddy.cn 的 plan/credit 猜测路径同样 404
|
|
58
|
+
* P-QP4 www.codebuddy.cn 控制台 plan API 候选拒绝 Bearer(cookie 体系)
|
|
59
|
+
* P-QP5 控制台 SPA 逆向出的真实只读路径(/cgi/v2/user/getInfo、
|
|
60
|
+
* /console/accounts、/billing/meter/check-gift-claimed 等)在
|
|
61
|
+
* www.codebuddy.cn 上拒绝 Bearer(cookie 体系:uin/skey)
|
|
62
|
+
* P-QP6 同路径在 copilot.tencent.com → 404 路径层(路径族不同)
|
|
63
|
+
* P-QP7 控制台 /billing/meter/get-* 数值 API(enterprise-user-usage /
|
|
64
|
+
* daily-usage / resource / request-usage)Bearer 可达且有数值
|
|
65
|
+
* P-RR1 真实 refresh → 200 code 0 + 新 accessToken
|
|
66
|
+
* P-RR2 若 refreshToken 轮换:旧 refresh 复跑 → 401+12153(同 bogus 形态)
|
|
67
|
+
* P-RR3 旧 access token 在 refresh 后仍可用(access 不随 refresh 作废)
|
|
68
|
+
*
|
|
69
|
+
* Usage: node scripts/probe-oauth.mjs [--set core|ttl|matrix|quota|refresh]
|
|
70
|
+
* [--state <s>] [--ttl-check-only]
|
|
71
|
+
* core: creation/poll/bogus/headers/platform/multi-flight/account/refresh-shape
|
|
72
|
+
* ttl: create one state, poll at +60s and +240s
|
|
73
|
+
* matrix/quota/refresh: G2 真实 token 集合(需 ~/.dsh/codebuddy-plugin-auth.json)
|
|
74
|
+
* --state <s> --ttl-check-only: poll a previously created state once
|
|
75
|
+
* (cross-session TTL check; state string is stored in the evidence file)
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
import { appendFileSync, mkdirSync, readFileSync, existsSync, writeFileSync } from 'node:fs'
|
|
79
|
+
import { homedir } from 'node:os'
|
|
80
|
+
import { createHash } from 'node:crypto'
|
|
81
|
+
import { join, dirname } from 'node:path'
|
|
82
|
+
|
|
83
|
+
const GATEWAY = 'https://copilot.tencent.com'
|
|
84
|
+
const WORKBUDDY = 'https://www.workbuddy.cn'
|
|
85
|
+
const CONSOLE = 'https://www.codebuddy.cn'
|
|
86
|
+
const CLI_UA = 'CLI/unknown CodeBuddy/2.136.0'
|
|
87
|
+
const DSH_HOME = process.env.DSH_HOME ?? join(homedir(), '.dsh')
|
|
88
|
+
const AUTH_FILE = join(DSH_HOME, 'codebuddy-plugin-auth.json')
|
|
89
|
+
const SPACING_MS = 2000
|
|
90
|
+
const argValue = (flag) => {
|
|
91
|
+
const i = process.argv.indexOf(flag)
|
|
92
|
+
return i > 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith('--') ? process.argv[i + 1] : null
|
|
93
|
+
}
|
|
94
|
+
const SET = argValue('--set') ?? 'core'
|
|
95
|
+
const TOKEN_SETS = new Set(['matrix', 'quota', 'quota2', 'values', 'quota3', 'quota4', 'quota5', 'quota6', 'quota7', 'refresh'])
|
|
96
|
+
const OUT = argValue('--out')
|
|
97
|
+
?? join('docs', 'probes', TOKEN_SETS.has(SET)
|
|
98
|
+
? `oauth-token-${new Date().toISOString().slice(0, 10)}.jsonl`
|
|
99
|
+
: `oauth-${new Date().toISOString().slice(0, 10)}.jsonl`)
|
|
100
|
+
|
|
101
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
102
|
+
const redact = (v) => typeof v === 'string'
|
|
103
|
+
? 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>')
|
|
104
|
+
: v
|
|
105
|
+
|
|
106
|
+
const SPELL_HEADERS = { 'X-No-Authorization': 'true', 'X-No-User-Id': 'true', 'X-No-Enterprise-Id': 'true' }
|
|
107
|
+
|
|
108
|
+
async function call(name, { method = 'GET', path, headers = {}, note = '', pred = '', body = null, baseURL = GATEWAY, captureHeaders = false }) {
|
|
109
|
+
const t0 = Date.now()
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch(`${baseURL}${path}`, {
|
|
112
|
+
method,
|
|
113
|
+
headers: { Accept: 'application/json', ...headers },
|
|
114
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
115
|
+
})
|
|
116
|
+
const text = await res.text()
|
|
117
|
+
let json = null
|
|
118
|
+
try { json = JSON.parse(text) } catch { /* not JSON */ }
|
|
119
|
+
const rec = {
|
|
120
|
+
type: 'probe', name, base: baseURL, path, pred, note,
|
|
121
|
+
status: res.status, code: json?.code ?? null,
|
|
122
|
+
msg: redact(json?.msg ?? json?.error_msg ?? text.slice(0, 140)),
|
|
123
|
+
dataKeys: json?.data && typeof json.data === 'object' ? Object.keys(json.data) : null,
|
|
124
|
+
ms: Date.now() - t0,
|
|
125
|
+
}
|
|
126
|
+
if (captureHeaders) {
|
|
127
|
+
const h = {}
|
|
128
|
+
res.headers.forEach((v, k) => { h[k] = v })
|
|
129
|
+
rec.headers = h
|
|
130
|
+
rec.quotaHeaders = Object.keys(h).filter((k) => /quota|credit|limit|remain|balance|dosage/i.test(k))
|
|
131
|
+
const credit = text.match(/"credit"\s*:\s*([0-9.]+)/)
|
|
132
|
+
if (credit) rec.usageCredit = Number(credit[1])
|
|
133
|
+
}
|
|
134
|
+
appendFileSync(OUT, JSON.stringify(rec) + '\n')
|
|
135
|
+
console.log(`${name.padEnd(34)} status=${rec.status} code=${rec.code ?? '-'} keys=${JSON.stringify(rec.dataKeys)} msg=${(rec.msg ?? '').slice(0, 90)}`)
|
|
136
|
+
return { rec, json, text }
|
|
137
|
+
} catch (err) {
|
|
138
|
+
const rec = { type: 'probe', name, base: baseURL, path, pred, note, error: String(err?.message ?? err), ms: Date.now() - t0 }
|
|
139
|
+
appendFileSync(OUT, JSON.stringify(rec) + '\n')
|
|
140
|
+
console.log(`${name.padEnd(34)} ERROR ${rec.error}`)
|
|
141
|
+
return { rec, json: null, text: '' }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 令牌只以 sha256 指纹进入证据,永不落明文。 */
|
|
146
|
+
const fp = (t) => t ? createHash('sha256').update(String(t)).digest('hex').slice(0, 12) : null
|
|
147
|
+
|
|
148
|
+
/** 与插件同逻辑的双凭据解析:OAuth(auth 文件)+ 活跃 ck_ key(插件配置/env/凭据文件)。 */
|
|
149
|
+
function loadCredentials() {
|
|
150
|
+
const out = { oauth: null, key: null }
|
|
151
|
+
try {
|
|
152
|
+
const store = JSON.parse(readFileSync(AUTH_FILE, 'utf8'))
|
|
153
|
+
if (store?.auth?.accessToken) {
|
|
154
|
+
out.oauth = {
|
|
155
|
+
token: store.auth.accessToken,
|
|
156
|
+
headers: {
|
|
157
|
+
Authorization: `Bearer ${store.auth.accessToken}`,
|
|
158
|
+
'X-Domain': store.auth.domain ?? '',
|
|
159
|
+
...(store.account?.uid ? { 'X-User-Id': store.account.uid } : {}),
|
|
160
|
+
...(store.account?.enterpriseId ? { 'X-Enterprise-Id': store.account.enterpriseId } : {}),
|
|
161
|
+
},
|
|
162
|
+
store,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
} catch { /* no auth file */ }
|
|
166
|
+
let key = null
|
|
167
|
+
try {
|
|
168
|
+
const cfg = JSON.parse(readFileSync(join(DSH_HOME, 'codebuddy-plugin.json'), 'utf8'))
|
|
169
|
+
key = (cfg.apiKeys ?? []).find((k) => k.name === cfg.activeApiKey)?.key ?? null
|
|
170
|
+
} catch { /* fall through */ }
|
|
171
|
+
key ??= process.env.CODEBUDDY_API_KEY ?? null
|
|
172
|
+
if (!key) {
|
|
173
|
+
const credFile = join(DSH_HOME, '.credentials.yaml')
|
|
174
|
+
if (existsSync(credFile)) {
|
|
175
|
+
const m = readFileSync(credFile, 'utf8').match(/^\s*CODEBUDDY_API_KEY:\s*["']?([^"'\s]+)["']?\s*$/m)
|
|
176
|
+
if (m) key = m[1]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (key) out.key = { key, headers: { Authorization: `Bearer ${key}`, 'x-api-key': key } }
|
|
180
|
+
return out
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
mkdirSync(dirname(OUT), { recursive: true })
|
|
184
|
+
appendFileSync(OUT, JSON.stringify({ type: 'run', at: new Date().toISOString(), set: SET, args: process.argv.slice(2) }) + '\n')
|
|
185
|
+
console.log(`evidence → ${OUT}`)
|
|
186
|
+
|
|
187
|
+
if (process.argv.includes('--ttl-check-only')) {
|
|
188
|
+
const state = argValue('--state')
|
|
189
|
+
if (!state) throw new Error('--state required')
|
|
190
|
+
await call(`ttl-check-${Date.now()}`, {
|
|
191
|
+
path: `/v2/plugin/auth/token?state=${encodeURIComponent(state)}`,
|
|
192
|
+
headers: { 'X-No-Authorization': 'true' },
|
|
193
|
+
pred: 'P-O5-ext', note: 'cross-session state TTL check',
|
|
194
|
+
})
|
|
195
|
+
process.exit(0)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (SET === 'core') {
|
|
199
|
+
// 1. spell-faithful creation
|
|
200
|
+
const s1 = await call('create-spell-headers', {
|
|
201
|
+
method: 'POST', path: '/v2/plugin/auth/state?platform=CLI', headers: SPELL_HEADERS,
|
|
202
|
+
note: 'baseline creation with all three X-No-* headers',
|
|
203
|
+
})
|
|
204
|
+
await sleep(SPACING_MS)
|
|
205
|
+
const state1 = s1.json?.data?.state
|
|
206
|
+
if (state1) {
|
|
207
|
+
appendFileSync(OUT, JSON.stringify({ type: 'state', name: 'state1', state: state1, createdAt: Date.now() }) + '\n')
|
|
208
|
+
console.log(` state1 issued (${String(state1).length} chars)`)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 2. poll twice — pending idempotence
|
|
212
|
+
if (state1) {
|
|
213
|
+
await call('poll-pending-1', { path: `/v2/plugin/auth/token?state=${encodeURIComponent(state1)}`, headers: { 'X-No-Authorization': 'true' }, note: 'expect 11217' })
|
|
214
|
+
await sleep(SPACING_MS)
|
|
215
|
+
await call('poll-pending-2', { path: `/v2/plugin/auth/token?state=${encodeURIComponent(state1)}`, headers: { 'X-No-Authorization': 'true' }, note: 'idempotent 11217' })
|
|
216
|
+
await sleep(SPACING_MS)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 3. P-O1 bogus state
|
|
220
|
+
await call('P-O1-bogus-state', { path: '/v2/plugin/auth/token?state=bogus-k3r9x7', headers: { 'X-No-Authorization': 'true' }, pred: 'P-O1', note: 'invalid state distinguishable from pending (not 0, not 11217)' })
|
|
221
|
+
await sleep(SPACING_MS)
|
|
222
|
+
|
|
223
|
+
// 4. P-O6 account with pending state
|
|
224
|
+
if (state1) {
|
|
225
|
+
await call('P-O6-account-pending', { path: `/v2/plugin/login/account?state=${encodeURIComponent(state1)}`, headers: SPELL_HEADERS, pred: 'P-O6', note: 'account facts with a pending state → code ≠ 0' })
|
|
226
|
+
await sleep(SPACING_MS)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 5. P-O2 multi-flight: create state2, re-poll state1
|
|
230
|
+
const s2 = await call('create-second-state', { method: 'POST', path: '/v2/plugin/auth/state?platform=CLI', headers: SPELL_HEADERS, pred: 'P-O2', note: 'second state creation' })
|
|
231
|
+
await sleep(SPACING_MS)
|
|
232
|
+
if (s2.json?.data?.state) {
|
|
233
|
+
appendFileSync(OUT, JSON.stringify({ type: 'state', name: 'state2', state: s2.json.data.state, createdAt: Date.now() }) + '\n')
|
|
234
|
+
}
|
|
235
|
+
if (state1) {
|
|
236
|
+
await call('P-O2-state1-after-state2', { path: `/v2/plugin/auth/token?state=${encodeURIComponent(state1)}`, headers: { 'X-No-Authorization': 'true' }, pred: 'P-O2', note: 'state1 still 11217 after state2 exists' })
|
|
237
|
+
await sleep(SPACING_MS)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// 6. P-O3 creation without the X-No-* headers
|
|
241
|
+
await call('P-O3-create-no-headers', { method: 'POST', path: '/v2/plugin/auth/state?platform=CLI', pred: 'P-O3', note: 'X-No-* headers are superstition' })
|
|
242
|
+
await sleep(SPACING_MS)
|
|
243
|
+
|
|
244
|
+
// 7. P-O4 platform variants
|
|
245
|
+
await call('P-O4-platform-workbuddy', { method: 'POST', path: '/v2/plugin/auth/state?platform=WORKBUDDY', headers: SPELL_HEADERS, pred: 'P-O4', note: 'free-form platform tag' })
|
|
246
|
+
await sleep(SPACING_MS)
|
|
247
|
+
await call('P-O4-platform-omitted', { method: 'POST', path: '/v2/plugin/auth/state', headers: SPELL_HEADERS, pred: 'P-O4', note: 'platform omitted' })
|
|
248
|
+
await sleep(SPACING_MS)
|
|
249
|
+
|
|
250
|
+
// 8. P-O7 refresh failure shape with bogus token
|
|
251
|
+
await call('P-O7-refresh-bogus', {
|
|
252
|
+
method: 'POST', path: '/v2/plugin/auth/token/refresh',
|
|
253
|
+
headers: { 'X-Refresh-Token': 'bogus-refresh-token', 'X-Domain': '' },
|
|
254
|
+
pred: 'P-O7', note: 'refresh failure is loud (non-2xx or code ≠ 0)',
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (SET === 'ttl') {
|
|
259
|
+
const s = await call('ttl-create', { method: 'POST', path: '/v2/plugin/auth/state?platform=CLI', headers: SPELL_HEADERS, note: 'TTL ladder seed' })
|
|
260
|
+
const state = s.json?.data?.state
|
|
261
|
+
if (!state) throw new Error('no state issued')
|
|
262
|
+
appendFileSync(OUT, JSON.stringify({ type: 'state', name: 'ttl-state', state, createdAt: Date.now() }) + '\n')
|
|
263
|
+
console.log(' ttl state issued; polling at +60s and +240s')
|
|
264
|
+
await sleep(60000)
|
|
265
|
+
await call('P-O5-ttl-60s', { path: `/v2/plugin/auth/token?state=${encodeURIComponent(state)}`, headers: { 'X-No-Authorization': 'true' }, pred: 'P-O5', note: 'still 11217 at +60s' })
|
|
266
|
+
await sleep(180000)
|
|
267
|
+
await call('P-O5-ttl-240s', { path: `/v2/plugin/auth/token?state=${encodeURIComponent(state)}`, headers: { 'X-No-Authorization': 'true' }, pred: 'P-O5', note: 'still 11217 at +240s' })
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let cred = null
|
|
271
|
+
if (TOKEN_SETS.has(SET)) {
|
|
272
|
+
cred = loadCredentials()
|
|
273
|
+
if (!cred.oauth) throw new Error(`no OAuth token at ${AUTH_FILE} — G1 交互登录是前置条件`)
|
|
274
|
+
appendFileSync(OUT, JSON.stringify({
|
|
275
|
+
type: 'credentials', at: new Date().toISOString(),
|
|
276
|
+
accessTokenFp: fp(cred.oauth.token), hasKey: Boolean(cred.key), keyFp: fp(cred.key?.key),
|
|
277
|
+
accountUid: cred.oauth.store.account?.uid ?? null, domain: cred.oauth.store.auth?.domain ?? null,
|
|
278
|
+
}) + '\n')
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (SET === 'matrix') {
|
|
282
|
+
// token×端点矩阵:五端点 × 双凭据。chat 两臂为 max_tokens=1 的极小微调用
|
|
283
|
+
// (deepseek-v3 ≈0.03 credit/次),与存量探测口径一致。
|
|
284
|
+
const arms = [['oauth', cred.oauth.headers], ['ck-key', cred.key?.headers]].filter(([, h]) => h)
|
|
285
|
+
const endpoints = [
|
|
286
|
+
['accounts', { path: '/v2/accounts' }],
|
|
287
|
+
['config', { path: '/v3/config', headers: { 'User-Agent': CLI_UA } }],
|
|
288
|
+
['dosage-notify', { method: 'POST', path: '/v2/billing/meter/get-dosage-notify', headers: { 'Content-Type': 'application/json' }, body: {} }],
|
|
289
|
+
['agenttool-search', { method: 'POST', path: '/agenttool/v1/search', headers: { 'Content-Type': 'application/json', 'User-Agent': CLI_UA }, body: { query: 'quota probe', max_results: 1 } }],
|
|
290
|
+
]
|
|
291
|
+
for (const [authName, authHeaders] of arms) {
|
|
292
|
+
for (const [epName, ep] of endpoints) {
|
|
293
|
+
await call(`${epName}@${authName}`, {
|
|
294
|
+
...ep, headers: { ...ep.headers, ...authHeaders },
|
|
295
|
+
pred: 'P-T1/P-T2', note: 'token vs ck_ key 端点可达矩阵',
|
|
296
|
+
})
|
|
297
|
+
await sleep(SPACING_MS)
|
|
298
|
+
}
|
|
299
|
+
await call(`chat@${authName}`, {
|
|
300
|
+
method: 'POST', path: '/v2/chat/completions', captureHeaders: true,
|
|
301
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': CLI_UA, ...authHeaders },
|
|
302
|
+
body: { model: 'deepseek-v3', stream: true, stream_options: { include_usage: true }, max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] },
|
|
303
|
+
pred: 'P-T1/P-T6', note: 'chat 可达 + 响应头零额度字段(R-Q1 认证不变性)',
|
|
304
|
+
})
|
|
305
|
+
await sleep(SPACING_MS)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (SET === 'quota') {
|
|
310
|
+
const H = cred.oauth.headers
|
|
311
|
+
const HUA = { ...H, 'User-Agent': CLI_UA }
|
|
312
|
+
const POSTJSON = { 'Content-Type': 'application/json' }
|
|
313
|
+
// copilot.tencent.com:已知端点控制臂 + billing/plan/credit/quota 猜测路径
|
|
314
|
+
await call('control-accounts', { path: '/v2/accounts', headers: H, note: '已知端点控制臂(token)' })
|
|
315
|
+
await sleep(SPACING_MS)
|
|
316
|
+
await call('control-dosage-notify', { method: 'POST', path: '/v2/billing/meter/get-dosage-notify', headers: { ...H, ...POSTJSON }, body: {}, note: '已知端点控制臂(token)' })
|
|
317
|
+
await sleep(SPACING_MS)
|
|
318
|
+
const guesses = [
|
|
319
|
+
'/v2/billing/plan', '/v2/billing/quota', '/v2/billing/credit', '/v2/billing/credits',
|
|
320
|
+
'/v2/billing/usage', '/v2/billing/meter/get-dosage', '/v2/billing/meter/get-quota',
|
|
321
|
+
'/v2/plan', '/v2/quota', '/v2/credit', '/v2/credits',
|
|
322
|
+
'/v2/subscription', '/v2/subscriptions', '/v2/user/plan', '/v2/user/quota', '/v2/accounts/plan',
|
|
323
|
+
]
|
|
324
|
+
for (const p of guesses) {
|
|
325
|
+
await call(`guess${p}`, { path: p, headers: H, pred: 'P-QP1', note: '额度端点猜测:预期路径层 404' })
|
|
326
|
+
await sleep(SPACING_MS)
|
|
327
|
+
}
|
|
328
|
+
// www.workbuddy.cn:同账户体系镜像
|
|
329
|
+
await call('wb-accounts', { baseURL: WORKBUDDY, path: '/v2/accounts', headers: H, pred: 'P-QP2', note: 'workbuddy 镜像 accounts' })
|
|
330
|
+
await sleep(SPACING_MS)
|
|
331
|
+
await call('wb-dosage-notify', { method: 'POST', baseURL: WORKBUDDY, path: '/v2/billing/meter/get-dosage-notify', headers: { ...H, ...POSTJSON }, body: {}, pred: 'P-QP2', note: 'workbuddy 镜像 dosage-notify' })
|
|
332
|
+
await sleep(SPACING_MS)
|
|
333
|
+
await call('wb-config', { baseURL: WORKBUDDY, path: '/v3/config', headers: HUA, pred: 'P-QP2', note: 'workbuddy 镜像 config(CLI UA)' })
|
|
334
|
+
await sleep(SPACING_MS)
|
|
335
|
+
for (const p of ['/v2/billing/plan', '/v2/plan', '/v2/quota', '/v2/credit']) {
|
|
336
|
+
await call(`wb-guess${p}`, { baseURL: WORKBUDDY, path: p, headers: H, pred: 'P-QP3', note: 'workbuddy 额度猜测路径' })
|
|
337
|
+
await sleep(SPACING_MS)
|
|
338
|
+
}
|
|
339
|
+
// www.codebuddy.cn 控制台 plan API 候选(cookie 体系;Bearer 预期被拒)
|
|
340
|
+
for (const p of ['/profile/plan', '/api/profile/plan', '/api/v1/plan', '/api/user/plan', '/api/credit', '/api/v1/credit']) {
|
|
341
|
+
await call(`console${p}`, { baseURL: CONSOLE, path: p, headers: H, pred: 'P-QP4', note: '控制台 plan API 候选:Bearer 预期进不去 cookie 体系' })
|
|
342
|
+
await sleep(SPACING_MS)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (SET === 'quota2') {
|
|
347
|
+
// 第二波:从控制台 SPA(www.codebuddy.cn/profile/plan → download.codebuddy.cn
|
|
348
|
+
// 公开 bundle)逆向出的真实路径字面量。claim-gift / claim-compensation /
|
|
349
|
+
// ide/trial / get-refund-price 为写操作,按红线不测;仅测只读路径。
|
|
350
|
+
const H = cred.oauth.headers
|
|
351
|
+
const POSTJSON = { 'Content-Type': 'application/json' }
|
|
352
|
+
const consoleReads = [
|
|
353
|
+
['GET', '/cgi/v2/user/getInfo', null],
|
|
354
|
+
['GET', '/console/accounts', null],
|
|
355
|
+
['POST', '/billing/meter/check-gift-claimed', {}],
|
|
356
|
+
['POST', '/billing/meter/compensation-status', {}],
|
|
357
|
+
['POST', '/console/user/from', {}],
|
|
358
|
+
]
|
|
359
|
+
for (const [m, p, b] of consoleReads) {
|
|
360
|
+
await call(`console-real${p}`, { method: m, baseURL: CONSOLE, path: p, headers: b ? { ...H, ...POSTJSON } : H, body: b, pred: 'P-QP5', note: '控制台真实只读路径:Bearer 预期被拒(cookie 体系)' })
|
|
361
|
+
await sleep(SPACING_MS)
|
|
362
|
+
}
|
|
363
|
+
for (const [m, p, b] of consoleReads) {
|
|
364
|
+
await call(`gw-console-path${p}`, { method: m, path: p, headers: b ? { ...H, ...POSTJSON } : H, body: b, pred: 'P-QP6', note: '同路径在网关域:预期 404 路径层(路径族不同)' })
|
|
365
|
+
await sleep(SPACING_MS)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (SET === 'values') {
|
|
370
|
+
// 值捕获臂:quota2 发现的真实只读端点的 data 全值(uid/手机号等身份字段脱敏)。
|
|
371
|
+
const H = cred.oauth.headers
|
|
372
|
+
const POSTJSON = { 'Content-Type': 'application/json' }
|
|
373
|
+
const scrub = (v) => {
|
|
374
|
+
if (v && typeof v === 'object') {
|
|
375
|
+
const o = {}
|
|
376
|
+
for (const [k, x] of Object.entries(v)) o[k] = /uid|uin|phone|openid|open_id|union/i.test(k) ? '<redacted>' : scrub(x)
|
|
377
|
+
return o
|
|
378
|
+
}
|
|
379
|
+
return v
|
|
380
|
+
}
|
|
381
|
+
const arms = [
|
|
382
|
+
['GET', '/v2/accounts', null],
|
|
383
|
+
['GET', '/console/accounts', null],
|
|
384
|
+
['POST', '/billing/meter/check-gift-claimed', {}],
|
|
385
|
+
['POST', '/billing/meter/compensation-status', {}],
|
|
386
|
+
['POST', '/v2/billing/meter/get-dosage-notify', {}],
|
|
387
|
+
]
|
|
388
|
+
for (const [m, p, b] of arms) {
|
|
389
|
+
const r = await call(`values${p}`, { method: m, path: p, headers: b ? { ...H, ...POSTJSON } : H, body: b, note: 'data 全值捕获(身份字段脱敏)' })
|
|
390
|
+
appendFileSync(OUT, JSON.stringify({ type: 'values', name: `values${p}`, data: scrub(r.json?.data ?? null) }) + '\n')
|
|
391
|
+
console.log(` values${p} → ${JSON.stringify(scrub(r.json?.data ?? null)).slice(0, 300)}`)
|
|
392
|
+
await sleep(SPACING_MS)
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (SET === 'quota3') {
|
|
397
|
+
// 第三波:控制台 config chunk 逆向出的 /billing/meter/get-* 数值 API。
|
|
398
|
+
// 全部 POST 只读(控制台 bundle 字面量),Bearer token 实测。
|
|
399
|
+
const H = { ...cred.oauth.headers, 'Content-Type': 'application/json' }
|
|
400
|
+
const scrub = (v) => {
|
|
401
|
+
if (v && typeof v === 'object') {
|
|
402
|
+
if (Array.isArray(v)) return v.slice(0, 3).map(scrub)
|
|
403
|
+
const o = {}
|
|
404
|
+
for (const [k, x] of Object.entries(v)) o[k] = /uid|uin|phone|openid|open_id|union/i.test(k) ? '<redacted>' : scrub(x)
|
|
405
|
+
return o
|
|
406
|
+
}
|
|
407
|
+
return v
|
|
408
|
+
}
|
|
409
|
+
const arms = [
|
|
410
|
+
['/billing/meter/get-enterprise-user-usage', {}],
|
|
411
|
+
['/billing/meter/get-user-daily-usage', {}],
|
|
412
|
+
['/billing/meter/get-user-resource', {}],
|
|
413
|
+
['/billing/meter/get-user-request-usage', { page: 1, page_size: 3 }],
|
|
414
|
+
]
|
|
415
|
+
for (const baseURL of [GATEWAY, CONSOLE]) {
|
|
416
|
+
for (const [p, b] of arms) {
|
|
417
|
+
const r = await call(`meter${p}@${new URL(baseURL).hostname}`, { method: 'POST', baseURL, path: p, headers: H, body: b, pred: 'P-QP7', note: '数值额度/用量 API:Bearer 预期可达(check-gift-claimed 同族已证)' })
|
|
418
|
+
if (r.json?.data != null) {
|
|
419
|
+
appendFileSync(OUT, JSON.stringify({ type: 'values', name: `meter${p}@${new URL(baseURL).hostname}`, data: scrub(r.json.data) }) + '\n')
|
|
420
|
+
console.log(` data → ${JSON.stringify(scrub(r.json.data)).slice(0, 400)}`)
|
|
421
|
+
}
|
|
422
|
+
await sleep(SPACING_MS)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (SET === 'quota4') {
|
|
428
|
+
// 第四波:数值额度 API 的参数形态已逆向(控制台 bundle):
|
|
429
|
+
// daily-usage {startTime:"YYYY-MM-DD 00:00:00",endTime,pageNum,pageSize}
|
|
430
|
+
// request-usage {startTime,endTime,timezone,pageSize,version:2,pageToken}
|
|
431
|
+
// enterprise {} + X-Enterprise-Id 头
|
|
432
|
+
// resource {}(get-user-resource,截断修复:全量 Accounts)
|
|
433
|
+
const H = { ...cred.oauth.headers, 'Content-Type': 'application/json' }
|
|
434
|
+
const scrub = (v, depth = 0) => {
|
|
435
|
+
if (Array.isArray(v)) return v.map((x) => scrub(x, depth + 1))
|
|
436
|
+
if (v && typeof v === 'object') {
|
|
437
|
+
const o = {}
|
|
438
|
+
for (const [k, x] of Object.entries(v)) o[k] = /^(uid|uin|phone|openid|open_id|unionid|payeruin|enterpriseusername|displayname)$/i.test(k) ? '<redacted>' : scrub(x, depth + 1)
|
|
439
|
+
return o
|
|
440
|
+
}
|
|
441
|
+
return v
|
|
442
|
+
}
|
|
443
|
+
const today = new Date().toISOString().slice(0, 10)
|
|
444
|
+
const monthStart = `${today.slice(0, 8)}01`
|
|
445
|
+
const arms = [
|
|
446
|
+
['resource-full', '/billing/meter/get-user-resource', {}, null, 'P-QP9'],
|
|
447
|
+
['daily-usage', '/billing/meter/get-user-daily-usage', { startTime: `${monthStart} 00:00:00`, endTime: `${today} 23:59:59`, pageNum: 1, pageSize: 50 }, null, 'P-QP10'],
|
|
448
|
+
['request-usage', '/billing/meter/get-user-request-usage', { startTime: `${monthStart} 00:00:00`, endTime: `${today} 23:59:59`, timezone: 'Asia/Shanghai', pageSize: 5, version: 2, pageToken: '' }, null, 'P-QP11'],
|
|
449
|
+
['enterprise-usage', '/billing/meter/get-enterprise-user-usage', {}, process.env.CODEBUDDY_ENTERPRISE_ID || null, 'P-QP13'],
|
|
450
|
+
]
|
|
451
|
+
for (const [name, p, b, entId, pred] of arms) {
|
|
452
|
+
const headers = entId ? { ...H, 'X-Enterprise-Id': entId } : H
|
|
453
|
+
const r = await call(`quota4-${name}`, { method: 'POST', path: p, headers, body: b, pred, note: '数值额度 API 带正确参数形态' })
|
|
454
|
+
if (r.json?.data != null) {
|
|
455
|
+
appendFileSync(OUT, JSON.stringify({ type: 'values', name: `quota4-${name}`, data: scrub(r.json.data) }) + '\n')
|
|
456
|
+
console.log(` data → ${JSON.stringify(scrub(r.json.data)).slice(0, 500)}`)
|
|
457
|
+
}
|
|
458
|
+
await sleep(SPACING_MS)
|
|
459
|
+
}
|
|
460
|
+
// P-QP12:ck_ key 特权边界——get-user-resource 是否 api-key 也可达
|
|
461
|
+
if (cred.key) {
|
|
462
|
+
const r = await call('quota4-resource-ck-key', {
|
|
463
|
+
method: 'POST', path: '/billing/meter/get-user-resource',
|
|
464
|
+
headers: { ...cred.key.headers, 'Content-Type': 'application/json' }, body: {},
|
|
465
|
+
pred: 'P-QP12', note: 'ck_ key 对数值额度 API 的可达性',
|
|
466
|
+
})
|
|
467
|
+
if (r.json?.data != null) {
|
|
468
|
+
appendFileSync(OUT, JSON.stringify({ type: 'values', name: 'quota4-resource-ck-key', data: scrub(r.json.data) }) + '\n')
|
|
469
|
+
console.log(` data → ${JSON.stringify(scrub(r.json.data)).slice(0, 200)}`)
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (SET === 'quota5') {
|
|
475
|
+
// 第五波:daily/request-usage "invalid params" 变体判别。
|
|
476
|
+
const H = { ...cred.oauth.headers, 'Content-Type': 'application/json' }
|
|
477
|
+
const now = new Date()
|
|
478
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
479
|
+
const todayStart = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} 00:00:00`
|
|
480
|
+
const nowStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
|
|
481
|
+
const variants = [
|
|
482
|
+
['daily-endtime-now', '/billing/meter/get-user-daily-usage', { startTime: todayStart, endTime: nowStr, pageNum: 1, pageSize: 10 }, null],
|
|
483
|
+
['daily-pascal', '/billing/meter/get-user-daily-usage', { StartTime: todayStart, EndTime: nowStr, PageNum: 1, PageSize: 10 }, null],
|
|
484
|
+
['daily-ent', '/billing/meter/get-user-daily-usage', { startTime: todayStart, endTime: nowStr, pageNum: 1, pageSize: 10 }, process.env.CODEBUDDY_ENTERPRISE_ID || null],
|
|
485
|
+
['request-endtime-now', '/billing/meter/get-user-request-usage', { startTime: todayStart, endTime: nowStr, timezone: 'Asia/Shanghai', pageSize: 5, version: 2, pageToken: '' }, null],
|
|
486
|
+
['request-no-token', '/billing/meter/get-user-request-usage', { startTime: todayStart, endTime: nowStr, timezone: 'Asia/Shanghai', pageSize: 5, version: 2 }, null],
|
|
487
|
+
['request-ent', '/billing/meter/get-user-request-usage', { startTime: todayStart, endTime: nowStr, timezone: 'Asia/Shanghai', pageSize: 5, version: 2, pageToken: '' }, process.env.CODEBUDDY_ENTERPRISE_ID || null],
|
|
488
|
+
]
|
|
489
|
+
for (const [name, p, b, entId] of variants) {
|
|
490
|
+
const headers = entId ? { ...H, 'X-Enterprise-Id': entId } : H
|
|
491
|
+
const r = await call(`quota5-${name}`, { method: 'POST', path: p, headers, body: b, note: 'invalid params 变体判别' })
|
|
492
|
+
if (r.json?.data != null) {
|
|
493
|
+
appendFileSync(OUT, JSON.stringify({ type: 'values', name: `quota5-${name}`, data: r.json.data }) + '\n')
|
|
494
|
+
console.log(` data → ${JSON.stringify(r.json.data).slice(0, 300)}`)
|
|
495
|
+
}
|
|
496
|
+
await sleep(SPACING_MS)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (SET === 'quota6') {
|
|
501
|
+
// 第六波:personal 上下文标记判别(sessionStorage profile-enterpriseId 缺省值)。
|
|
502
|
+
const H = { ...cred.oauth.headers, 'Content-Type': 'application/json' }
|
|
503
|
+
const uid = cred.oauth.store.account?.uid ?? ''
|
|
504
|
+
const now = new Date()
|
|
505
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
506
|
+
const todayStart = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} 00:00:00`
|
|
507
|
+
const nowStr = todayStart.slice(0, 10) + ` ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
|
|
508
|
+
const body = { startTime: todayStart, endTime: nowStr, pageNum: 1, pageSize: 10 }
|
|
509
|
+
const variants = [
|
|
510
|
+
['ent-literal-personal', 'personal'],
|
|
511
|
+
['ent-empty-string', ''],
|
|
512
|
+
['ent-uid', uid],
|
|
513
|
+
]
|
|
514
|
+
for (const [name, ent] of variants) {
|
|
515
|
+
await call(`quota6-${name}`, {
|
|
516
|
+
method: 'POST', path: '/billing/meter/get-user-daily-usage',
|
|
517
|
+
headers: { ...H, 'X-Enterprise-Id': ent }, body,
|
|
518
|
+
note: 'personal 上下文标记变体',
|
|
519
|
+
})
|
|
520
|
+
await sleep(SPACING_MS)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (SET === 'quota7') {
|
|
525
|
+
// 第七波:personal 上下文的用量数据值捕获(daily + request,本月至今)。
|
|
526
|
+
const H = { ...cred.oauth.headers, 'Content-Type': 'application/json', 'X-Enterprise-Id': 'personal' }
|
|
527
|
+
const now = new Date()
|
|
528
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
529
|
+
const monthStart = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-01 00:00:00`
|
|
530
|
+
const nowStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`
|
|
531
|
+
const scrub = (v) => {
|
|
532
|
+
if (Array.isArray(v)) return v.map(scrub)
|
|
533
|
+
if (v && typeof v === 'object') {
|
|
534
|
+
const o = {}
|
|
535
|
+
for (const [k, x] of Object.entries(v)) o[k] = /^(uid|uin|phone|openid|open_id|unionid|payeruin|enterpriseusername|displayname)$/i.test(k) ? '<redacted>' : scrub(x)
|
|
536
|
+
return o
|
|
537
|
+
}
|
|
538
|
+
return v
|
|
539
|
+
}
|
|
540
|
+
const arms = [
|
|
541
|
+
['daily-personal-month', '/billing/meter/get-user-daily-usage', { startTime: monthStart, endTime: nowStr, pageNum: 1, pageSize: 31 }],
|
|
542
|
+
['request-personal-today', '/billing/meter/get-user-request-usage', { startTime: monthStart, endTime: nowStr, timezone: 'Asia/Shanghai', pageSize: 5, version: 2, pageToken: '' }],
|
|
543
|
+
]
|
|
544
|
+
for (const [name, p, b] of arms) {
|
|
545
|
+
const r = await call(`quota7-${name}`, { method: 'POST', path: p, headers: H, body: b, note: 'personal 用量值捕获' })
|
|
546
|
+
if (r.json?.data != null) {
|
|
547
|
+
appendFileSync(OUT, JSON.stringify({ type: 'values', name: `quota7-${name}`, data: scrub(r.json.data) }) + '\n')
|
|
548
|
+
console.log(` data → ${JSON.stringify(scrub(r.json.data)).slice(0, 600)}`)
|
|
549
|
+
}
|
|
550
|
+
await sleep(SPACING_MS)
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (SET === 'refresh') {
|
|
555
|
+
// 真实 refresh 轮换语义。成功后必须立即回写 auth 文件(运行中的 dsh 每次
|
|
556
|
+
// 请求都重读该文件),否则旧 refresh 作废后插件将失凭。
|
|
557
|
+
const old = cred.oauth.store.auth
|
|
558
|
+
const oldAccessFp = fp(old.accessToken)
|
|
559
|
+
const oldRefreshFp = fp(old.refreshToken)
|
|
560
|
+
console.log(` old access fp=${oldAccessFp} refresh fp=${oldRefreshFp}`)
|
|
561
|
+
const r1 = await call('P-RR1-refresh-real', {
|
|
562
|
+
method: 'POST', path: '/v2/plugin/auth/token/refresh',
|
|
563
|
+
headers: {
|
|
564
|
+
Authorization: `Bearer ${old.accessToken}`,
|
|
565
|
+
'X-Domain': old.domain ?? '',
|
|
566
|
+
'X-Refresh-Token': old.refreshToken ?? '',
|
|
567
|
+
},
|
|
568
|
+
pred: 'P-RR1', note: '真实 refresh:预期 200 code 0 + 新 accessToken',
|
|
569
|
+
})
|
|
570
|
+
const d = r1.json?.data
|
|
571
|
+
if (r1.json?.code === 0 && d?.accessToken) {
|
|
572
|
+
const rotated = Boolean(d.refreshToken) && fp(d.refreshToken) !== oldRefreshFp
|
|
573
|
+
appendFileSync(OUT, JSON.stringify({
|
|
574
|
+
type: 'refresh-result', at: new Date().toISOString(),
|
|
575
|
+
newAccessFp: fp(d.accessToken), newRefreshFp: fp(d.refreshToken),
|
|
576
|
+
accessRotated: fp(d.accessToken) !== oldAccessFp, refreshRotated: rotated,
|
|
577
|
+
expiresIn: d.expiresIn ?? null, refreshExpiresIn: d.refreshExpiresAt ?? null, domain: d.domain ?? null,
|
|
578
|
+
}) + '\n')
|
|
579
|
+
console.log(` refresh ok: access rotated=${fp(d.accessToken) !== oldAccessFp} refresh rotated=${rotated} expiresIn=${d.expiresIn ?? '-'}`)
|
|
580
|
+
// 与 providers/codebuddy/oauth.js refreshOAuth 同逻辑回写
|
|
581
|
+
const store = JSON.parse(readFileSync(AUTH_FILE, 'utf8'))
|
|
582
|
+
store.auth = {
|
|
583
|
+
accessToken: d.accessToken,
|
|
584
|
+
expiresAt: Date.now() + (d.expiresIn ?? 3600) * 1000,
|
|
585
|
+
refreshToken: d.refreshToken ?? old.refreshToken,
|
|
586
|
+
refreshExpiresAt: d.refreshExpiresAt != null ? Date.now() + d.refreshExpiresAt * 1000 : store.auth.refreshExpiresAt,
|
|
587
|
+
domain: d.domain ?? old.domain,
|
|
588
|
+
}
|
|
589
|
+
writeFileSync(AUTH_FILE, JSON.stringify(store, null, 2))
|
|
590
|
+
console.log(' auth file updated with fresh tokens')
|
|
591
|
+
await sleep(SPACING_MS)
|
|
592
|
+
await call('P-RR2-refresh-old-rerun', {
|
|
593
|
+
method: 'POST', path: '/v2/plugin/auth/token/refresh',
|
|
594
|
+
headers: {
|
|
595
|
+
Authorization: `Bearer ${old.accessToken}`,
|
|
596
|
+
'X-Domain': old.domain ?? '',
|
|
597
|
+
'X-Refresh-Token': old.refreshToken ?? '',
|
|
598
|
+
},
|
|
599
|
+
pred: 'P-RR2', note: `旧 refresh 复跑:轮换情形预期 401+12153;本次 refreshRotated=${rotated}`,
|
|
600
|
+
})
|
|
601
|
+
await sleep(SPACING_MS)
|
|
602
|
+
await call('P-RR3-old-access-accounts', {
|
|
603
|
+
path: '/v2/accounts',
|
|
604
|
+
headers: { Authorization: `Bearer ${old.accessToken}`, 'X-Domain': old.domain ?? '' },
|
|
605
|
+
pred: 'P-RR3', note: '旧 access token 复用:预期仍可用(不随 refresh 作废)',
|
|
606
|
+
})
|
|
607
|
+
await sleep(SPACING_MS)
|
|
608
|
+
await call('new-access-accounts', {
|
|
609
|
+
path: '/v2/accounts',
|
|
610
|
+
headers: { Authorization: `Bearer ${d.accessToken}`, 'X-Domain': (d.domain ?? old.domain) ?? '' },
|
|
611
|
+
note: '新 access token 可用性确认',
|
|
612
|
+
})
|
|
613
|
+
} else {
|
|
614
|
+
console.log(' refresh refused — auth file untouched (old access token still valid until stored expiresAt)')
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
console.log('done.')
|