@wwkit/freetoken 1.0.1

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/cli/helpers/args.js +48 -0
  4. package/cli/index.js +374 -0
  5. package/cli/pid-manager.js +87 -0
  6. package/client/index.html +25 -0
  7. package/client/public/favicon.svg +1 -0
  8. package/client/public/icons.svg +24 -0
  9. package/client/src/App.vue +136 -0
  10. package/client/src/assets/vite.svg +1 -0
  11. package/client/src/assets/vue.svg +1 -0
  12. package/client/src/components/ChatBox.vue +320 -0
  13. package/client/src/components/CheckList.vue +52 -0
  14. package/client/src/components/CodeBlock.vue +99 -0
  15. package/client/src/components/DetailBlock.vue +47 -0
  16. package/client/src/components/HelloWorld.vue +95 -0
  17. package/client/src/components/LangSwitch.vue +32 -0
  18. package/client/src/components/LinkList.vue +32 -0
  19. package/client/src/components/OsSwitch.vue +42 -0
  20. package/client/src/components/OsToggle.vue +15 -0
  21. package/client/src/components/PageHeader.vue +31 -0
  22. package/client/src/components/StepList.vue +92 -0
  23. package/client/src/composables/useClipboard.js +26 -0
  24. package/client/src/composables/useOs.js +22 -0
  25. package/client/src/data/docs/agent.js +1002 -0
  26. package/client/src/data/docs/auth.js +631 -0
  27. package/client/src/data/docs/builtin-tools.js +322 -0
  28. package/client/src/data/docs/chat.js +181 -0
  29. package/client/src/data/docs/index.js +9 -0
  30. package/client/src/data/docs/skills.js +396 -0
  31. package/client/src/data/docs/spec-conversion.js +1042 -0
  32. package/client/src/data/freeModels/bluesliu.js +97 -0
  33. package/client/src/data/freeModels/index.js +11 -0
  34. package/client/src/data/freeModels/nvidia.js +126 -0
  35. package/client/src/data/freeModels/openrouter.js +116 -0
  36. package/client/src/data/harness/claude.js +89 -0
  37. package/client/src/data/harness/codex.js +66 -0
  38. package/client/src/data/harness/dsh.js +86 -0
  39. package/client/src/data/harness/hermes.js +56 -0
  40. package/client/src/data/harness/index.js +22 -0
  41. package/client/src/data/harness/opencode.js +77 -0
  42. package/client/src/data/proxy/api-relay.js +53 -0
  43. package/client/src/data/proxy/builtin-proxy.js +54 -0
  44. package/client/src/data/proxy/cf-workers.js +67 -0
  45. package/client/src/data/proxy/ecs-forward.js +75 -0
  46. package/client/src/data/proxy/ecs-reverse.js +89 -0
  47. package/client/src/data/proxy/index.js +15 -0
  48. package/client/src/docs/claudecode.md +44 -0
  49. package/client/src/docs/codex.md +90 -0
  50. package/client/src/docs/hermesagent.md +42 -0
  51. package/client/src/docs/opencode.md +103 -0
  52. package/client/src/locales/en.js +436 -0
  53. package/client/src/locales/index.js +31 -0
  54. package/client/src/locales/zh-CN.js +461 -0
  55. package/client/src/main.js +16 -0
  56. package/client/src/router/index.js +76 -0
  57. package/client/src/style.css +15 -0
  58. package/client/src/views/AdminModelsView.vue +214 -0
  59. package/client/src/views/DocsView.vue +1604 -0
  60. package/client/src/views/FreeModelsView.vue +518 -0
  61. package/client/src/views/HarnessView.vue +314 -0
  62. package/client/src/views/HomeView.vue +147 -0
  63. package/client/src/views/ProxyView.vue +112 -0
  64. package/client/src/views/TokenMarketView.vue +26 -0
  65. package/client/vite.config.js +41 -0
  66. package/package.json +85 -0
  67. package/scripts/build-zip.sh +61 -0
  68. package/scripts/postinstall.js +7 -0
  69. package/server/src/config/targets.json +1 -0
  70. package/server/src/index.js +52 -0
  71. package/server/src/lib/coding-test.js +215 -0
  72. package/server/src/lib/database.js +353 -0
  73. package/server/src/lib/run-test.js +15 -0
  74. package/server/src/lib/scheduler.js +21 -0
  75. package/server/src/lib/tester.js +310 -0
  76. package/server/src/routes/admin.js +48 -0
  77. package/server/src/routes/proxy.js +64 -0
  78. package/server/src/routes/speed.js +87 -0
  79. package/src/config.js +45 -0
  80. package/src/config.json5 +27 -0
  81. package/src/index.js +10 -0
@@ -0,0 +1,631 @@
1
+ // 登录认证模块:多种 LLM 后端的认证机制
2
+ // - DeepSeek 网页端认证(基于 ds-free-api 的 ds_core 库)
3
+ // - CodeArts IDE OAuth 认证(基于 @wwkit/llmproxy 的 providers/codearts/)
4
+ // 认证逻辑独立于协议转换——先认证拿到可用凭证,再谈格式转换
5
+
6
+ export default {
7
+ id: 'auth',
8
+ name: 'Auth',
9
+ badge: '登录认证',
10
+ tagline: '多种 LLM 后端的认证机制:网页端逆向认证、IDE OAuth 认证',
11
+ description:
12
+ '不同 LLM 后端的认证机制差异巨大——DeepSeek 网页端需要逆向 PoW/WAF/Session 体系,CodeArts IDE 使用 OAuth 2.0 PKCE + DPoP + 华为云 SDK 签名。本模块独立于协议转换,专注说明认证机制本身。',
13
+
14
+ subModules: [
15
+ {
16
+ id: 'deepseek-web',
17
+ name: 'DeepSeek 网页端认证',
18
+ badge: '逆向工程',
19
+ tagline: '从网页端登录到可用 Session 的完整认证链',
20
+ description:
21
+ 'ds-free-api 的 ds_core 库(ds_core/src/accounts/)将 DeepSeek 网页端的认证流程封装为 AccountPool + AccountGuard 模型。1 个账号 = 1 个 session = 1 个并发槽位。整个认证链分为后端认证(面向 DeepSeek)和前端管理(面向客户端)两层,完全独立于协议转换逻辑。',
22
+
23
+ // ── 认证架构 ──
24
+ architecture: {
25
+ title: '认证架构',
26
+ description:
27
+ 'ds_core 分为三个子模块:client(原始 HTTP)、pool(账号池管理)、pow(PoW 求解)。上层通过 Accounts facade 统一调用,不直接接触子模块。',
28
+ layers: [
29
+ {
30
+ name: 'Accounts Facade',
31
+ file: 'ds_core/src/accounts.rs',
32
+ role: '统一入口,封装 pool/client/solver,对外只暴露 get_account() 和 v0_chat()',
33
+ },
34
+ {
35
+ name: 'AccountPool',
36
+ file: 'ds_core/src/accounts/pool.rs',
37
+ role: '账号池管理:初始化、选择(LIFO 空闲最久优先)、AccountGuard 生命周期',
38
+ },
39
+ {
40
+ name: 'Client',
41
+ file: 'ds_core/src/accounts/client.rs',
42
+ role: '原始 HTTP 客户端:登录、创建会话、completion、文件上传、停止流。解析 Envelope 信封格式',
43
+ },
44
+ {
45
+ name: 'PowSolver',
46
+ file: 'ds_core/src/accounts/pow.rs',
47
+ role: 'PoW 挑战求解:wasmtime 加载 WASM、动态探测导出函数、执行 DeepSeekHashV1',
48
+ },
49
+ ],
50
+ codeStructure: `ds_core/src/
51
+ ├── lib.rs # 公共 API: re-exports DsCore, CoreError
52
+ ├── accounts.rs # Facade: Accounts struct
53
+ ├── accounts/
54
+ │ ├── client.rs # 原始 HTTP: API 端点, Envelope 解析
55
+ │ ├── pool.rs # 账号池: init, selection, AccountGuard
56
+ │ └── pow.rs # PoW: wasmtime WASM, DeepSeekHashV1
57
+ ├── chat.rs # Facade: Chat struct (会话编排)
58
+ └── config.rs # DsCoreConfig, AccountConfig`,
59
+ },
60
+
61
+ // ── 账号池初始化 ──
62
+ poolInit: {
63
+ title: '账号池初始化',
64
+ description:
65
+ 'AccountPool::init() 并发初始化所有账号(Semaphore 上限 13)。每个账号独立完成 4 步初始化,失败重试 3 次(2s 间隔),全部失败标记为 InitFailed。',
66
+ steps: [
67
+ { step: '1. 登录', detail: '调用 DeepSeek 登录接口,用账号凭据换取 Bearer Token。这是认证链的起点——没有 Token 后续所有操作都会被拒绝' },
68
+ { step: '2. 创建聊天会话', detail: '用 Token 调用 create_session 接口创建一个聊天会话。1 账号 = 1 session = 1 并发' },
69
+ { step: '3. 健康检查', detail: '执行一次带 PoW 的测试 completion,验证会话可写。如果这一步失败说明 session 有问题(可能被 WAF 拦截或 PoW 求解失败)' },
70
+ { step: '4. 重命名会话', detail: '将 session 标题改为 "managed-by-ai-free-api",标记为代理管理的会话' },
71
+ ],
72
+ retry: '每个账号重试 3 次,间隔 2s。全部失败标记为 InitFailed,不参与轮转。',
73
+ concurrency: '并发初始化上限 13(tokio::sync::Semaphore 控制),避免大量账号同时登录触发风控。',
74
+ },
75
+
76
+ // ── PoW 挑战求解 ──
77
+ pow: {
78
+ title: 'PoW 挑战求解(Proof of Work)',
79
+ description:
80
+ 'DeepSeek 网页端要求 Proof of Work 挑战——这是反爬机制的核心。每次聊天请求前必须求解一次 PoW,否则请求被拒绝。ds-free-api 用 wasmtime 执行 DeepSeek 的 WASM 求解器。',
81
+ mechanism: [
82
+ { step: '1. 获取挑战参数', detail: '从 DeepSeek 获取 challenge 参数(难度值、算法标识等)。参数每次请求不同' },
83
+ { step: '2. 加载 WASM 模块', detail: 'wasmtime 加载 DeepSeek 的 WASM 求解器模块。WASM 代码从 DeepSeek CDN 下载,URL 可在 config.toml 中配置' },
84
+ { step: '3. 动态探测导出函数', detail: '不硬编码 WASM 导出函数名——动态探测所有导出符号,找到求解入口。这确保 DeepSeek 更新 WASM 后仍能工作' },
85
+ { step: '4. 执行 DeepSeekHashV1', detail: '在 WASM 中执行哈希求解算法,找到一个满足难度条件的 nonce 值' },
86
+ { step: '5. 返回 proof', detail: '将求解结果(nonce + proof)附加到聊天请求中,DeepSeek 后端验证通过后才处理请求' },
87
+ ],
88
+ codeExample: `// ds_core/src/accounts/pow.rs 核心逻辑(伪代码)
89
+
90
+ pub struct PowSolver {
91
+ engine: wasmtime::Engine,
92
+ module: wasmtime::Module,
93
+ }
94
+
95
+ impl PowSolver {
96
+ // 加载 WASM 模块
97
+ pub fn load(wasm_url: &str) -> Result<Self> {
98
+ let wasm_bytes = download(wasm_url)?;
99
+ let engine = Engine::default();
100
+ let module = Module::new(&engine, &wasm_bytes)?;
101
+ Ok(Self { engine, module })
102
+ }
103
+
104
+ // 求解 PoW 挑战
105
+ pub fn solve(&self, challenge: &Challenge) -> Result<Proof> {
106
+ let store = Store::new(&self.engine, ());
107
+ let instance = self.module.instantiate(&mut store, [])?;
108
+
109
+ // 动态探测导出函数(不硬编码符号名)
110
+ let solve_func = probe_export(&instance, &store, "solve")?;
111
+ let result = solve_func.call(&mut store, &[challenge.into()])?;
112
+
113
+ Ok(Proof::from(result))
114
+ }
115
+ }`,
116
+ notes: [
117
+ 'PoW 是 DeepSeek 网页端的反爬机制,不是标准 API 认证——标准 API 只需 API Key',
118
+ '每次聊天请求前必须求解一次 PoW,求解结果不可复用',
119
+ 'WASM 模块可能被 DeepSeek 更新(重新编译),动态探测导出函数确保兼容性',
120
+ '如果 WASM URL 变更,需更新 config.toml 中的 wasm_url 配置',
121
+ 'wasmtime 是重量级依赖(编译慢、二进制大),但确保了 PoW 求解的正确性',
122
+ ],
123
+ },
124
+
125
+ // ── WAF 绕过 ──
126
+ waf: {
127
+ title: 'WAF 绕过(AWS Web Application Firewall)',
128
+ description:
129
+ 'DeepSeek 部署了 AWS WAF 防护,会拦截非浏览器请求。ds-free-api 使用 wreq(BoringSSL)模拟 Chrome 136 TLS 指纹来绕过 WAF 检测。',
130
+ mechanism: [
131
+ { step: 'TLS 指纹模拟', detail: 'wreq 使用 BoringSSL 编译,模拟 Chrome 136 的 TLS ClientHello 指纹。WAF 通过 TLS 指纹判断请求是否来自真实浏览器' },
132
+ { step: 'WAF Challenge 检测', detail: '如果收到 HTTP 202(WAF Challenge 响应),说明被 WAF 拦截。非美国 IP 更容易触发' },
133
+ { step: '代理转发', detail: '配置 config.toml [proxy] 使用美国 IP 代理,避免地域性 WAF 拦截' },
134
+ { step: '指纹更新', detail: '如果 WAF 更新指纹策略(HTTP 403 或连接重置),需更新 wreq 版本或切换模拟配置文件' },
135
+ ],
136
+ configExample: `# config.toml
137
+ [proxy]
138
+ url = "http://your-us-proxy:8080" # 美国 IP 代理,避免 WAF Challenge`,
139
+ notes: [
140
+ 'WAF 绕过是逆向网页端的特有需求——标准 API 不需要处理 WAF',
141
+ 'TLS 指纹模拟是被动绕过,不是主动攻击 WAF',
142
+ 'wreq + BoringSSL 编译依赖 cmake、g++、libclang-dev',
143
+ '如果 WAF 升级检测策略,可能需要更新 wreq 或切换到其他 HTTP 客户端',
144
+ ],
145
+ },
146
+
147
+ // ── Session 管理 ──
148
+ session: {
149
+ title: 'Session 生命周期管理',
150
+ description:
151
+ '每次聊天请求创建一个临时 session,请求结束后销毁。AccountGuard 确保账号在 session 存活期间被标记为忙碌,GuardedStream 确保 session 在流结束时清理。',
152
+ lifecycle: [
153
+ { phase: '1. 获取账号', detail: 'AccountPool::get_account() → AccountGuard。LIFO 空闲最久优先,DashMap 无锁读。AtomicBool 标记忙碌' },
154
+ { phase: '2. 创建 session', detail: '为本次请求创建专用聊天会话(不是复用初始化时的 session)' },
155
+ { phase: '3. 上传历史', detail: '多轮对话历史分块上传为文件(split_history),最后 1-2 轮内联在 prompt 中' },
156
+ { phase: '4. PoW + Completion', detail: '求解 PoW → 发送 completion 请求 → 接收 SSE 流响应' },
157
+ { phase: '5. 流式传输', detail: 'GuardedStream 包装 SSE 流,保持账号忙碌状态直到流结束' },
158
+ { phase: '6. 销毁 session', detail: 'GuardedStream::drop 时销毁 session 并释放账号。异常断开时调用 stop_stream' },
159
+ ],
160
+ accountGuard: `// AccountGuard 核心逻辑(伪代码)
161
+ pub struct AccountGuard {
162
+ account: Arc<Account>,
163
+ }
164
+
165
+ impl AccountGuard {
166
+ fn new(account: Arc<Account>) -> Self {
167
+ account.busy.store(true, Ordering::Relaxed); // 标记忙碌
168
+ Self { account }
169
+ }
170
+ }
171
+
172
+ impl Drop for AccountGuard {
173
+ fn drop(&mut self) {
174
+ self.account.busy.store(false, Ordering::Relaxed); // 释放
175
+ }
176
+ }
177
+
178
+ // GuardedStream 在流结束时触发 AccountGuard::drop
179
+ // 确保 session 在流传输期间不被其他请求复用`,
180
+ notes: [
181
+ '1 账号 = 1 session = 1 并发——水平扩展靠增加账号数量',
182
+ '每次请求创建新 session(不复用),确保多轮对话隔离',
183
+ 'GuardedStream::drop 保证异常断开时也能清理 session',
184
+ 'active_sessions: Arc<Mutex<HashMap>> 追踪所有活跃 session',
185
+ ],
186
+ },
187
+
188
+ // ── 客户端认证(管理面板) ──
189
+ clientAuth: {
190
+ title: '客户端认证(管理面板 + API Key)',
191
+ description:
192
+ 'ds-free-api 对客户端提供两层认证:管理面板使用 JWT(bcrypt 密码),API 请求使用 API Key。两者独立于 DeepSeek 后端认证。',
193
+ layers: [
194
+ {
195
+ name: '管理面板认证',
196
+ steps: [
197
+ { step: '1. 设置密码', detail: '首次访问 /admin 引导设置管理密码。bcrypt 哈希存储,不明文保存' },
198
+ { step: '2. 登录签发 JWT', detail: '密码验证通过后签发 JWT(24h 有效)。前端存储在 localStorage' },
199
+ { step: '3. 登录限流', detail: '5 次登录失败锁定 5 分钟,防止暴力破解' },
200
+ { step: '4. 密码重置吊销', detail: '密码重置时吊销所有旧 JWT,强制重新登录' },
201
+ ],
202
+ },
203
+ {
204
+ name: 'API Key 认证',
205
+ steps: [
206
+ { step: '1. 创建 API Key', detail: '通过管理面板创建/删除 API Key。存储在 config.toml 的 [[api_keys]] 中' },
207
+ { step: '2. 请求鉴权', detail: '客户端请求携带 Authorization: Bearer <api_key>。HashSet O(1) 查找' },
208
+ { step: '3. 空配置无认证', detail: 'config 为空时(无 API Key)不启用认证,任何请求都可访问' },
209
+ ],
210
+ },
211
+ ],
212
+ securityNotes: [
213
+ '管理密码使用 bcrypt 哈希,不存储明文',
214
+ 'JWT 密钥在首次设置密码时随机生成,存储在 config.toml',
215
+ 'config.toml 文件权限 0600,原子写入(tmp + rename)',
216
+ '账号 ID 在响应头中脱敏(x-ds-account)',
217
+ '请求体不记录日志,敏感信息不外泄',
218
+ 'CORS 默认仅允许 localhost:22217,可配置允许的 Origin 列表',
219
+ ],
220
+ },
221
+
222
+ // ── 与标准 API 认证的对比 ──
223
+ comparison: {
224
+ title: '与标准 API 认证的对比',
225
+ description: 'DeepSeek 网页端认证远比标准 API 复杂——标准 API 只需一个 Key,网页端需要逆向整套反爬体系。',
226
+ table: [
227
+ { aspect: '认证方式', standard: 'API Key(Bearer Token)', deepseek: '账号密码登录 + PoW + WAF 绕过 + Session 管理' },
228
+ { aspect: '认证步骤', standard: '1 步(携带 Key)', deepseek: '7+ 步(登录→Session→PoW→WAF→请求→清理)' },
229
+ { aspect: '并发模型', standard: '无限制(按量计费)', deepseek: '1 账号 = 1 并发(Session 级限流)' },
230
+ { aspect: '反爬措施', standard: '无', deepseek: 'PoW 挑战 + AWS WAF + TLS 指纹检测' },
231
+ { aspect: 'Session 管理', standard: '无状态(每次请求独立)', deepseek: '有状态(创建→使用→销毁,异常清理)' },
232
+ { aspect: '成本', standard: '按 Token 计费', deepseek: '免费(但有速率限制和并发限制)' },
233
+ { aspect: '稳定性', standard: '高(SLA 保障)', deepseek: '低(WASM 可能更新、WAF 可能升级、账号可能被封)' },
234
+ ],
235
+ },
236
+ },
237
+
238
+ // ── CodeArts IDE OAuth 认证 ──
239
+ {
240
+ id: 'codearts-oauth',
241
+ name: 'CodeArts IDE OAuth',
242
+ badge: 'OAuth 2.0',
243
+ tagline: 'PKCE + DPoP + STS Token + SDK-HMAC-SHA256 签名',
244
+ description:
245
+ '@wwkit/llmproxy 的 providers/codearts/ 模块逆向了 CodeArts IDE 插件(snap_AIIDE v5.3.0)的认证流程。使用 OAuth 2.0 PKCE + DPoP 向华为云 STS 换取临时安全凭证(AK/SK/SecurityToken),再用 SDK-HMAC-SHA256 对每个请求签名。与 DeepSeek 网页端认证完全不同——这是标准 OAuth 流程,不需要 PoW 或 WAF 绕过。',
246
+
247
+ architecture: {
248
+ title: '认证架构',
249
+ description:
250
+ 'llmproxy 的 provider 插件架构:providers/<type>/ 下放 auth.js / sign.js / client.js / config.js / error.js 五个文件,core/factory.js 按路径约定动态 import,新增 provider 只需放文件无需改代码。',
251
+ layers: [
252
+ {
253
+ name: 'CodeartsAuth',
254
+ file: 'providers/codearts/auth.js',
255
+ role: 'OAuth 2.0 PKCE + DPoP 认证:浏览器登录 → 换 STS Token → 持久化 → 自动续期',
256
+ },
257
+ {
258
+ name: 'CodeartsSign',
259
+ file: 'providers/codearts/sign.js',
260
+ role: 'SDK-HMAC-SHA256 请求签名:构造 Canonical Request → HMAC 签名 → 返回 Authorization 头',
261
+ },
262
+ {
263
+ name: 'CodeartsClient',
264
+ file: 'providers/codearts/client.js',
265
+ role: '请求发送:auth 拿凭证 + sign 签名 + undici fetch + SSE 异常检测',
266
+ },
267
+ {
268
+ name: 'CodeartsConfig',
269
+ file: 'providers/codearts/config.js',
270
+ role: '固定配置:benefit 模型列表(每日免费额度模型自动附加 maas_type 头)',
271
+ },
272
+ {
273
+ name: 'CodeartsError',
274
+ file: 'providers/codearts/error.js',
275
+ role: '错误模式识别:TM.00001041 = 并发会话超限',
276
+ },
277
+ ],
278
+ codeStructure: `providers/codearts/
279
+ ├── auth.js # OAuth PKCE + DPoP → STS Token
280
+ ├── sign.js # SDK-HMAC-SHA256 签名 + SIGN_CONFIG (host/basePath)
281
+ ├── client.js # auth + sign + fetch + SSE guard
282
+ ├── config.js # BENEFIT_MODELS (每日免费额度)
283
+ └── error.js # sessionLimit 错误模式`,
284
+ },
285
+
286
+ oauthFlow: {
287
+ title: 'OAuth 2.0 PKCE + DPoP 认证流程',
288
+ description:
289
+ '完整的 7 步 OAuth 流程,从生成密钥对到拿到可用凭证。PKCE 防止授权码拦截,DPoP 证明请求来自密钥持有者。',
290
+ steps: [
291
+ { step: '1. 生成 PKCE pair', detail: 'code_verifier = randomBytes(64).hex;code_challenge = SHA256(code_verifier) → base64url。PKCE 防止授权码被中间人截获——verifier 只在客户端,不经过浏览器' },
292
+ { step: '2. 生成 DPoP ES256 密钥对', detail: 'crypto.generateKeyPairSync("ec", { namedCurve: "P-256" }) → publicKeyJwk + privateKeyJwk。DPoP (Demonstrating Proof-of-Possession) 让每个 token 请求都附带密钥持有证明' },
293
+ { step: '3. 启动本地回调服务器', detail: 'http.createServer 监听 127.0.0.1 随机端口,等待 /oauth/callback?code=xxx 回调' },
294
+ { step: '4. 构建授权 URL + 打开浏览器', detail: 'https://codearts.huaweicloud.com/portal/authorize?client_id=codearts-agent&code_challenge=<PKCE>&... → 经过 IAM 登录页 → 用户手动登录 → 重定向到回调' },
295
+ { step: '5. 换 STS Token', detail: 'POST https://sts.cn-north-4.myhuaweicloud.com/v1/oauth2/tokens,Headers: DPoP=<ES256 签名 JWT>,Body: client_id + code + code_verifier + grant_type=authorization_code → 返回 access_key_id + secret_access_key + security_token + refresh_token' },
296
+ { step: '6. 凭证持久化', detail: '存储到 ~/.config/llmproxy/.creds.json(0600 权限),含 AK/SK/SecurityToken/refreshToken/loginContext(PKCE pair + DPoP 密钥对,用于续期)' },
297
+ { step: '7. 自动续期', detail: 'getCredentials() 检查 expiresAt,过期前 5 分钟用 refresh_token 续期(grant_type=refresh_token + 同一 DPoP 密钥)。续期失败 → 自动触发浏览器重新登录' },
298
+ ],
299
+ codeExample: `// providers/codearts/auth.js 核心逻辑
300
+
301
+ // PKCE 生成
302
+ const codeVerifier = crypto.randomBytes(64).toString("hex")
303
+ const codeChallenge = crypto.createHash("sha256")
304
+ .update(codeVerifier).digest("base64")
305
+ .replace(/\\+/g, "-").replace(/\\//g, "_").replace(/=/g, "")
306
+
307
+ // DPoP 签名
308
+ _signDPoP(dpopKeyPair, method, url) {
309
+ const payload = { htm: method, htu: url, iat: Date.now()/1000, jti: random }
310
+ const header = { alg: "ES256", typ: "dpop+jwt", jwk: dpopKeyPair.publicKeyJwk }
311
+ const data = base64url(header) + "." + base64url(payload)
312
+ const sig = crypto.createSign("SHA256").update(data)
313
+ .sign(dpopKeyPair.privateKey) // ES256 签名
314
+ return data + "." + base64url(derToRawRS(sig))
315
+ }
316
+
317
+ // STS Token 交换
318
+ const res = await fetch("https://sts.cn-north-4.myhuaweicloud.com/v1/oauth2/tokens", {
319
+ method: "POST",
320
+ headers: { "Content-Type": "application/x-www-form-urlencoded", "DPoP": dpop },
321
+ body: "client_id=codearts-agent&code=<auth_code>&code_verifier=<pkce>&grant_type=authorization_code"
322
+ })
323
+ // → { credentials: { access_key_id, secret_access_key, security_token, expiration },
324
+ // refresh_token, domain_id, user_id, user_name }`,
325
+ notes: [
326
+ 'PKCE:code_verifier 只在客户端,不经过浏览器,防止授权码拦截攻击',
327
+ 'DPoP:每次换 token 时用 ES256 私钥签名一个 JWT(htm+htu+iat+jti),证明密钥持有者身份',
328
+ 'STS Token 是华为云临时安全凭证(AK/SK/SecurityToken),有过期时间,不是永久 Token',
329
+ 'loginContext(含 DPoP 私钥 JWK)一起持久化,续期时重建 KeyPair 对象',
330
+ '凭证文件 0600 权限,原子写入(先写 tmp 再 rename)',
331
+ 'chat 请求路径上不允许自动登录(autoLogin: false),避免阻塞请求——凭证缺失直接抛错',
332
+ ],
333
+ },
334
+
335
+ sdkSign: {
336
+ title: 'SDK-HMAC-SHA256 请求签名',
337
+ description:
338
+ '拿到 STS 凭证后,每个 chat 请求需要用华为云 SDK 签名。这是华为云 API Gateway 的标准认证方式,不是 OAuth token 直接使用。',
339
+ mechanism: [
340
+ { step: '1. 构造 Canonical Request', detail: 'HTTP方法 + 规范URI(末尾加/) + 规范查询参数(排序+URL编码) + 规范Headers(小写+排序) + 签名Headers列表(分号分隔) + Body的SHA256哈希' },
341
+ { step: '2. 构造 String to Sign', detail: '"SDK-HMAC-SHA256" + SDK日期(UTC) + SHA256(Canonical Request)' },
342
+ { step: '3. HMAC-SHA256 签名', detail: 'signature = HMAC-SHA256(secretAccessKey, String to Sign)' },
343
+ { step: '4. 返回认证头', detail: 'Authorization: SDK-HMAC-SHA256 Access=<AK>, SignedHeaders=<headers>, Signature=<sig> + X-Sdk-Date: <UTC时间>' },
344
+ ],
345
+ codeExample: `// providers/codearts/sign.js 核心逻辑
346
+
347
+ sign(req) {
348
+ const bodyHash = sha256(req.bodyStr)
349
+ const canonicalRequest = [
350
+ req.method.toUpperCase(), // POST
351
+ canonicalUri, // /api/v2/chat/completions/
352
+ canonicalQueryString, // 排序+URL编码的查询参数
353
+ canonicalHeaders, // 小写+排序的 headers
354
+ signedHeadersStr, // host;x-sdk-date;x-security-token
355
+ bodyHash, // body 的 SHA256
356
+ ].join("\\n")
357
+
358
+ const stringToSign = [
359
+ "SDK-HMAC-SHA256",
360
+ sdkDate,
361
+ sha256(canonicalRequest),
362
+ ].join("\\n")
363
+
364
+ const signature = hmacSha256(secretAccessKey, stringToSign)
365
+ return {
366
+ Authorization: "SDK-HMAC-SHA256 Access=" + accessKeyId +
367
+ ", SignedHeaders=" + signedHeadersStr +
368
+ ", Signature=" + signature,
369
+ "X-Sdk-Date": sdkDate,
370
+ }
371
+ }`,
372
+ notes: [
373
+ '签名用 SK(secretAccessKey)作为 HMAC 密钥,AK(accessKeyId)放在 Authorization 头',
374
+ 'SecurityToken 通过 X-Security-Token 头传递,因为 STS 凭证是临时的',
375
+ '每个请求都重新签名——SDK 日期和 body hash 每次不同',
376
+ 'benefit 模型(glm-5.3-flash 等)自动附加 maas_type: benefit 头,消耗每日免费额度',
377
+ ],
378
+ },
379
+
380
+ requestFlow: {
381
+ title: '请求发送流程',
382
+ description:
383
+ 'CodeartsClient 封装了"auth 拿凭证 + sign 签名 + fetch 发送"的完整链路。客户端只需发标准 OpenAI 请求,代理自动添加认证。',
384
+ steps: [
385
+ { step: '1. 获取凭证', detail: 'authProvider.getCredentials({ autoLogin: false }) → AK/SK/SecurityToken。chat 路径不自动登录,凭证缺失直接抛错' },
386
+ { step: '2. 构建请求头', detail: 'Content-Type + Host + X-Security-Token(如有)+ maas_type: benefit(如 benefit 模型)' },
387
+ { step: '3. SDK 签名', detail: 'signProvider.sign({ method, url, headers, bodyStr, accessKeyId, secretAccessKey }) → Authorization + X-Sdk-Date' },
388
+ { step: '4. 发送请求', detail: 'undici fetch(connections: 1, keepAliveTimeout: 100ms,避免上游会话池占用)' },
389
+ { step: '5. SSE 异常检测', detail: 'wrapSSEAbnormalGuard 检测上游 SSE 流中的非标准错误(InferHub.ModelArts.xxx),改写为标准 OpenAI 错误格式' },
390
+ ],
391
+ },
392
+
393
+ comparison: {
394
+ title: '与 DeepSeek 网页端认证的对比',
395
+ description: '两种认证机制完全不同——CodeArts 是标准 OAuth + SDK 签名,DeepSeek 是逆向 PoW/WAF/Session。',
396
+ table: [
397
+ { aspect: '认证方式', deepseek: '账号密码 + PoW + WAF 绕过', codearts: 'OAuth 2.0 PKCE + DPoP' },
398
+ { aspect: '凭证类型', deepseek: 'Bearer Token(Session 级)', codearts: 'STS 临时凭证(AK/SK/SecurityToken)' },
399
+ { aspect: '请求签名', deepseek: '无(Token 直接用)', codearts: 'SDK-HMAC-SHA256(每个请求签名)' },
400
+ { aspect: '反爬措施', deepseek: 'PoW WASM + AWS WAF + TLS 指纹', codearts: '无(标准 OAuth 流程)' },
401
+ { aspect: 'Session 管理', deepseek: '有状态(创建→使用→销毁)', codearts: '无状态(Token 续期即可)' },
402
+ { aspect: '并发模型', deepseek: '1 账号 = 1 并发', codearts: '无限制(按 STS 额度)' },
403
+ { aspect: '凭证续期', deepseek: '重新登录(无 refresh)', codearts: 'refresh_token 自动续期(5 分钟前)' },
404
+ { aspect: '用户交互', deepseek: '无(全自动逆向)', codearts: '首次需浏览器手动登录' },
405
+ { aspect: '稳定性', deepseek: '低(WASM/WAF 可能更新)', codearts: '高(标准 OAuth 2.0 协议)' },
406
+ ],
407
+ },
408
+ },
409
+
410
+ // ── bluesllm W3 账密认证 ──
411
+ {
412
+ id: 'bluesllm-w3',
413
+ name: 'bluesllm W3 认证',
414
+ badge: '账密登录',
415
+ tagline: 'W3 工号密码登录 + Token 缓存 + 模板替换 + 三种认证模式',
416
+ description:
417
+ '@wego/bluesllm 的 auth.js 支持三种认证类型(auth.type):w3(默认,账密登录获取 token)、apikey(静态 Key)、cookie(静态凭据)。核心是 TokenManager——用 W3 工号密码调用华为内部登录接口,获取 cloudDragonTokens.authToken,缓存 24 小时,过期自动刷新。认证与协议转换完全分离:createAuthHooks() 返回 headerHook(注入认证头)和 requestHook(注入 body 参数),与 translator.js 无关。',
418
+
419
+ architecture: {
420
+ title: '认证架构',
421
+ description:
422
+ 'bluesllm 的认证模块分为 TokenManager(W3 登录+缓存)和 createAuthHooks(Hook 工厂)两部分。配置驱动——所有行为差异由 configs/*.json 的 auth/headers/body 段控制。',
423
+ layers: [
424
+ {
425
+ name: 'TokenManager',
426
+ file: 'src/auth.js',
427
+ role: 'W3 账密登录:POST 登录接口 → 提取 cloudDragonTokens.authToken + department → 缓存到 <name>-token.json(24h TTL)→ 过期自动刷新',
428
+ },
429
+ {
430
+ name: 'createAuthHooks',
431
+ file: 'src/auth.js',
432
+ role: 'Hook 工厂:根据 auth.type 创建 headerHook(注入认证头)+ requestHook(注入 body 参数 + 强制 stream)',
433
+ },
434
+ {
435
+ name: 'Config',
436
+ file: 'configs/base.json',
437
+ role: '认证配置:auth.type / login_url / app_name / token_cache_duration / env_dir / env_file',
438
+ },
439
+ {
440
+ name: 'env.json',
441
+ file: '~/.bluesllm/env.json',
442
+ role: '凭证存储:W3_USER_ID + W3_USER_PWD(w3 模式)或 {NAME}_API_KEY(apikey 模式)',
443
+ },
444
+ ],
445
+ codeStructure: `src/auth.js
446
+ ├── TokenManager # W3 登录 + token 缓存
447
+ │ ├── getData(force) # 获取有效 token(缓存优先)
448
+ │ ├── getCached() # 从磁盘读缓存
449
+ │ ├── isTokenValid(cached) # 检查 expiry
450
+ │ ├── refresh() # 调用登录接口
451
+ │ ├── getCodemateData(data) # 提取 token + department
452
+ │ └── saveData(data) # 持久化到磁盘
453
+ ├── createAuthHooks(config) # Hook 工厂
454
+ │ ├── headerHook # 注入认证头({token}/{api_key} 模板替换)
455
+ │ └── requestHook # 注入 body 参数 + force_stream
456
+ ├── getApiKey(config) # apikey 模式:读 env.json 或内置默认值
457
+ ├── getCookieEnv(config) # cookie 模式:读 env.json 前缀字段
458
+ └── getEnvConfig(envFile) # w3 模式:读 W3_USER_ID/W3_USER_PWD`,
459
+ },
460
+
461
+ threeAuthModes: {
462
+ title: '三种认证模式',
463
+ description:
464
+ 'bluesllm 通过 auth.type 配置切换三种认证模式,无需改代码。三种模式共享 headerHook/requestHook 接口,但凭证来源不同。',
465
+ modes: [
466
+ {
467
+ name: 'w3(默认)',
468
+ type: 'auth.type = "w3"',
469
+ credential: 'W3 工号密码 → 登录接口 → cloudDragonTokens.authToken + department',
470
+ template: '{token} / {department} 模板替换到 headers.inject',
471
+ useCase: '华为内部模型服务(cida/codemate 等),需要 W3 账号认证',
472
+ },
473
+ {
474
+ name: 'apikey',
475
+ type: 'auth.type = "apikey"',
476
+ credential: 'env.json 中的 {NAME}_API_KEY 或代码内置默认值',
477
+ template: '{api_key} 模板替换到 headers.inject',
478
+ useCase: '公开代理服务,用户无需配置 W3 账号,内置 API Key 即可使用',
479
+ },
480
+ {
481
+ name: 'cookie',
482
+ type: 'auth.type = "cookie"',
483
+ credential: 'env.json 中 {NAME}_ 前缀的所有字段(如 AUTH_TOKEN、COOKIE)',
484
+ template: '{auth_token} / {cookie} 等模板替换到 headers.inject',
485
+ useCase: '使用浏览器 Cookie 认证的服务,从 env.json 读取静态凭据',
486
+ },
487
+ ],
488
+ },
489
+
490
+ w3Flow: {
491
+ title: 'W3 账密登录流程',
492
+ description:
493
+ 'TokenManager 的完整认证流程:从 env.json 读取账密 → 调用登录接口 → 提取 token + department → 缓存 → 模板替换注入请求头。',
494
+ steps: [
495
+ { step: '1. 读取账密', detail: '从 ~/.bluesllm/env.json 读取 W3_USER_ID 和 W3_USER_PWD。缺失则抛错"Missing W3 credentials"' },
496
+ { step: '2. 检查缓存', detail: '从 <name>-token.json 读取缓存的 token 数据,检查 codemate.expiry 是否在有效期内(24h TTL)' },
497
+ { step: '3. 调用登录接口', detail: 'POST https://rnd-idea-api.huawei.com/ideaclientservice/login/v4/secureLogin,Body: { user, password, requireUserInfo, appName, oauthApp, requireCodeHubOpenToken }' },
498
+ { step: '4. 提取 token', detail: '从响应中提取 cloudDragonTokens.authToken(token)和 userInfo 中的 hwDepartName1-6(拼接为 department)' },
499
+ { step: '5. 持久化', detail: '将完整登录响应 + codemate(token + department + expiry)写入 <name>-token.json。多代理实例各自独立缓存' },
500
+ { step: '6. 模板替换', detail: 'headerHook 将 headers.inject 中的 {token} 替换为 authToken、{department} 替换为部门路径,注入到请求头' },
501
+ { step: '7. 自动刷新', detail: 'getData() 检查缓存有效性,过期时自动调用 refresh() 重新登录。重试时(ctx.retry > 0)强制刷新' },
502
+ ],
503
+ codeExample: `// src/auth.js — TokenManager 核心逻辑
504
+
505
+ class TokenManager {
506
+ // 获取有效 token(缓存优先)
507
+ async getData(force = false) {
508
+ if (force) return this.refresh()
509
+ const cached = this.getCached()
510
+ if (this.isTokenValid(cached)) return cached
511
+ return this.refresh()
512
+ }
513
+
514
+ // 调用 W3 登录接口
515
+ async refresh() {
516
+ const response = await fetch(this.loginUrl, {
517
+ method: "POST",
518
+ headers: {
519
+ "X-Language": "en",
520
+ "Content-Type": "application/json",
521
+ "X-User-Id": this.userId,
522
+ },
523
+ body: JSON.stringify({
524
+ user: this.userId,
525
+ password: this.userPwd,
526
+ requireUserInfo: true,
527
+ appName: this.appName,
528
+ oauthApp: this.appName,
529
+ requireCodeHubOpenToken: true,
530
+ }),
531
+ })
532
+ const data = await response.json()
533
+ data.codemate = this.getCodemateData(data)
534
+ this.saveData(data)
535
+ return data
536
+ }
537
+
538
+ // 提取 token + department
539
+ getCodemateData(data) {
540
+ const token = data?.cloudDragonTokens?.authToken
541
+ if (!token) return {}
542
+ const dept = [hwDepartName1, ..., hwDepartName6]
543
+ .filter(Boolean).join("/")
544
+ return { token, department: dept, expiry: now + 86400 }
545
+ }
546
+ }
547
+
548
+ // headerHook 模板替换
549
+ headerHook = async (ctx, incomingHeaders) => {
550
+ const data = await tokenManager.getData(ctx.retry > 0)
551
+ const token = data.codemate.token
552
+ const department = data.codemate.department
553
+ for (const [key, val] of Object.entries(inject)) {
554
+ result[key] = String(val)
555
+ .replaceAll("{token}", token)
556
+ .replaceAll("{department}", department)
557
+ }
558
+ }`,
559
+ notes: [
560
+ 'W3 登录是华为内部认证——使用工号密码,不是 OAuth 或 API Key',
561
+ 'cloudDragonTokens.authToken 是登录后获得的 Bearer Token,用于后续 API 请求',
562
+ 'department 是用户部门路径(如 "Cloud/BU1/Team2"),部分上游需要此信息',
563
+ 'token 缓存 24 小时(86400 秒),过期自动刷新,无需用户干预',
564
+ '重试时强制刷新 token(ctx.retry > 0 → force=true),避免用过期 token 重试',
565
+ 'env.json 权限应为 0600,包含明文密码——不提交到 git',
566
+ 'NODE_TLS_REJECT_UNAUTHORIZED=0:内网 SSL 证书不校验(华为内网常见)',
567
+ ],
568
+ },
569
+
570
+ hookSystem: {
571
+ title: 'Hook 系统:认证与请求注入',
572
+ description:
573
+ 'createAuthHooks() 返回 headerHook 和 requestHook 两个异步函数。headerHook 在请求发送前注入认证头,requestHook 在请求体发送前注入参数。两者与协议转换完全分离。',
574
+ headerHook: `// headerHook: 注入认证头
575
+ async function headerHook(ctx, incomingHeaders) {
576
+ // 1. 获取凭证(w3/apikey/cookie 三选一)
577
+ const { token, department } = await tokenManager.getData()
578
+ // 2. 展开客户端传入的 headers(可选)
579
+ if (spread_incoming) Object.assign(result, incomingHeaders)
580
+ // 3. 注入静态 headers(模板替换)
581
+ for (const [key, val] of Object.entries(inject)) {
582
+ result[key] = val
583
+ .replaceAll("{token}", token)
584
+ .replaceAll("{department}", department)
585
+ }
586
+ // 4. 注入动态 headers(uuid / timestamp_ms 等)
587
+ for (const [key, funcName] of Object.entries(dynamic)) {
588
+ result[key] = resolveDynamic(funcName)
589
+ }
590
+ return result
591
+ }`,
592
+ requestHook: `// requestHook: 注入 body 参数
593
+ async function requestHook(ctx, body) {
594
+ // 1. 强制 stream=true(body.force_stream)
595
+ if (force_stream) parsed.stream = true
596
+ // 2. 注入缺失参数(body.inject,仅在 undefined/null 时填充)
597
+ for (const [key, val] of Object.entries(inject)) {
598
+ if (parsed[key] === undefined) parsed[key] = val
599
+ }
600
+ // 3. 强制覆盖参数(body.override,无条件替换)
601
+ for (const [key, val] of Object.entries(override)) {
602
+ parsed[key] = val
603
+ }
604
+ return body
605
+ }`,
606
+ notes: [
607
+ 'headerHook 和 requestHook 是异步函数,接收 (ctx, data) 参数,返回修改后的 data',
608
+ 'ctx.retry 表示当前重试次数,> 0 时强制刷新 token',
609
+ 'ctx.logger 可选链式调用(this.logger?.info?.(...)),logger 不存在时静默跳过',
610
+ 'Throw HookAbortError 可中止请求——认证失败时抛出带 status_code 的错误',
611
+ 'force_stream 让上游强制返回流式响应,代理再决定是否 merge_stream_to_non_stream',
612
+ ],
613
+ },
614
+
615
+ comparison: {
616
+ title: '三种认证机制对比',
617
+ description: 'bluesllm W3 认证、CodeArts OAuth、DeepSeek 逆向认证三种机制的本质差异。',
618
+ table: [
619
+ { aspect: '认证方式', bluesllm: 'W3 工号密码登录', codearts: 'OAuth 2.0 PKCE + DPoP', deepseek: '账号密码 + PoW + WAF' },
620
+ { aspect: '凭证类型', bluesllm: 'cloudDragonTokens.authToken(Bearer)', codearts: 'STS 临时凭证(AK/SK/Token)', deepseek: 'Session Token' },
621
+ { aspect: '请求签名', bluesllm: '无(Token 模板替换到头)', codearts: 'SDK-HMAC-SHA256', deepseek: '无(Token 直接用)' },
622
+ { aspect: '凭证缓存', bluesllm: '<name>-token.json(24h)', codearts: '.creds.json(含 refresh_token)', deepseek: '内存(Session 级)' },
623
+ { aspect: '自动续期', bluesllm: '过期重新登录(无 refresh_token)', codearts: 'refresh_token 续期(5 分钟前)', deepseek: '重新登录' },
624
+ { aspect: '用户交互', bluesllm: '无(配置文件存账密)', codearts: '首次需浏览器登录', deepseek: '无(全自动逆向)' },
625
+ { aspect: '多模式', bluesllm: '✅ w3 / apikey / cookie 三选一', codearts: '❌ 仅 OAuth', deepseek: '❌ 仅逆向' },
626
+ { aspect: '认证与转换', bluesllm: '分离(Hook 系统 vs translator.js)', codearts: '分离(auth.js vs 无转换)', deepseek: '分离(ds_core vs adapter)' },
627
+ ],
628
+ },
629
+ },
630
+ ],
631
+ }