| # Web端接口实现 & 数美SDK逆向研究报告 |
|
|
| ## 一、概述 |
|
|
| 本项目新增了对 DeepSeek **Web端接口**的支持,可通过设置面板在"Android端"和"Web端"之间热切换,无需重启服务。 |
|
|
| ### 核心功能 |
|
|
| | 特性 | Android端 | Web端 | |
| |------|-----------|-------| |
| | User-Agent | `DeepSeek/2.1.2 Android/32` | Chrome 137 浏览器UA | |
| | TLS指纹 | Safari | Chrome (HTTP/2) | |
| | x-client-platform | `android` | `web` | |
| | device_id | `"android_device"` | 数美SDK加密字符串 | |
| | x-hif-dliq | 无 | 动态轮询获取 | |
| | x-hif-leim | 无 | 动态轮询获取 | |
| | 浏览器特有头 | 无 | sec-ch-ua, origin, referer等 | |
|
|
| ### 架构设计 |
|
|
| ``` |
| ┌─────────────────────────────────────────────────────────────┐ |
| │ 设置面板 (WebUI) │ |
| │ 选择: Android / Web 接口平台 │ |
| └─────────────────────────┬───────────────────────────────────┘ |
| │ PUT /admin/settings |
| ▼ |
| ┌─────────────────────────────────────────────────────────────┐ |
| │ Handler.applyPlatformChange() │ |
| │ DS.SetPlatform("web" / "android") │ |
| ├─────────────────────────────────────────────────────────────┤ |
| │ 1. dsprotocol.ApplyPlatform() → 切换 BaseHeaders / UA │ |
| │ 2. RefreshTransport() → 切换 TLS 指纹 │ |
| │ 3. hifPoller.Start()/Stop() → 启动/停止 HIF 轮询 │ |
| └─────────────────────────────────────────────────────────────┘ |
| ``` |
|
|
| 切换后立即生效,新请求自动使用对应平台的 UA、TLS 指纹、Headers 和 device_id 算法。 |
| |
| --- |
| |
| ## 二、文件变更清单 |
| |
| ### 新增文件(8个) |
| |
| | 文件 | 说明 | |
| |------|------| |
| | `internal/deepseek/protocol/constants_web.json` | Web端协议常量定义 | |
| | `internal/deepseek/hif/hif.go` | HIF头部轮询器 | |
| | `internal/deepseek/hif/hif_test.go` | HIF单元测试 | |
| | `internal/shumei/device_id.go` | 数美SDK device_id生成入口 | |
| | `internal/shumei/crypto.go` | RSA/DES/AES/MD5/Gzip加密工具 | |
| | `internal/shumei/confusion.go` | DES字段混淆映射表 | |
| | `internal/shumei/api.go` | 数美设备指纹API调用 | |
| | `webui/src/features/settings/PlatformSection.jsx` | 平台切换UI组件 | |
| |
| ### 修改文件(18个) |
| |
| #### Config层 |
| - `internal/config/config.go` — PlatformConfig结构体、PlatformMode()、IsWebPlatform() |
| - `internal/config/codec.go` — JSON序列化支持 |
| - `internal/config/store_accessors.go` — Store级访问器 |
| - `internal/config/validation.go` — 平台配置校验 |
|
|
| #### Protocol层 |
| - `internal/deepseek/protocol/constants.go` — ApplyPlatform()、WebExtraHeaders()、UserAgent() |
|
|
| #### Transport层 |
| - `internal/deepseek/transport/transport.go` — Chrome TLS指纹、NewWithPlatform() |
|
|
| #### Client层 |
| - `internal/deepseek/client/client_core.go` — SetPlatform()、RefreshTransport()、HIF poller集成 |
| - `internal/deepseek/client/client_auth.go` — Web端login(数美device_id) |
| - `internal/deepseek/client/client_completion.go` — Web端completion headers |
| - `internal/deepseek/client/client_continue.go` — Web端continue headers |
| - `internal/deepseek/client/proxy.go` — 平台感知代理客户端 |
|
|
| #### Admin API |
| - `internal/httpapi/admin/shared/deps.go` — DeepSeekCaller接口添加SetPlatform |
| - `internal/httpapi/admin/settings/handler_settings_*.go` — 设置保存/读取/热加载 |
|
|
| #### 前端 |
| - `webui/src/features/settings/SettingsContainer.jsx` — 集成PlatformSection |
| - `webui/src/features/settings/useSettingsForm.js` — 平台表单数据流 |
| - `webui/src/locales/zh.json` / `en.json` — 国际化翻译 |
|
|
| --- |
|
|
| ## 三、数美SDK逆向分析报告 |
|
|
| ### 3.1 SDK基本信息 |
|
|
| - **SDK名称**:数美科技(Shumei)DeviceFingerprint Web SDK |
| - **版本**:3.0.0 / subVersion 1.0.0 |
| - **加载方式**:通过 `https://cdn.deepseek.com/static/chat/fp-1.min.js` 动态加载 |
| - **配置来源**:`window._smConf`(由DeepSeek页面注入) |
| - **API端点**:`https://fp-it-acc.portal101.cn/deviceprofile/v4` |
|
|
| ### 3.2 device_id生成流程 |
| |
| ``` |
| 1. 生成UUID v4 (uid) |
| │ |
| 2. 计算priId = md5(uid)[:16] ────→ 作为AES密钥 |
| │ |
| 3. RSA加密uid (PKCS#1 v1.5) ────→ ep字段 |
| │ |
| 4. 收集23项浏览器指纹数据 |
| │ |
| 5. DES-ECB逐字段加密+字段名混淆 |
| │ |
| 6. 计算tn = md5(sorted key=value) ──→ 完整性校验 |
| │ |
| 7. JSON → Gzip压缩 |
| │ |
| 8. AES-CBC加密 (priId + IV="0102030405060708" + ZeroPadding) |
| │ |
| 9. POST到数美API |
| │ |
| 10. 解析响应 code=1100 → detail.deviceId |
| │ |
| 11. getDeviceId() = "B" + deviceId |
| ``` |
| |
| ### 3.3 关键配置参数 |
| |
| | 参数 | 值 | 说明 | |
| |------|-----|------| |
| | RSA公钥 | `MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDetfEgYD4aE1ZjmWJ6/jnPurhzI+yeRoJHWrnNtQMte3stQ4VjG3yu21FuN75E6cDpA9KtDXwcB2M/FiGUAe3G0rNotbWI8+SjZfUbW/OILFTzY0uaeEkmVGW5WyJ6weQbbr1xTCPa2OO3YIMeZljWUYHG5h21WAm/PATg8im8cQIDAQAB` | 用于加密uid | |
| | organization | `P9usCUBauxft8eAmUXaZ` | 设备标识归属 | |
| | appId | `default` | 应用ID | |
| | apiHost | `fp-it-acc.portal101.cn` | 数美API域名 | |
| |
| ### 3.4 指纹数据收集(23项) |
| |
| | 字段 | 来源 | 说明 | |
| |------|------|------| |
| | ua | navigator.userAgent | 浏览器UA | |
| | platform | navigator.platform | Win32 | |
| | canvas | canvas 2D绘图 | 绘制'http://www.ishumei.com'文字取哈希 | |
| | timezone | Date.getTimezoneOffset() | 时区偏移 | |
| | res | screen.width/height/colorDepth/devicePixelRatio | 屏幕信息 | |
| | plugins | navigator.plugins | 浏览器插件列表 | |
| | referer | document.referrer | 来源页面 | |
| | url | location.href | 当前URL | |
| | clientSize | screenLeft/screenTop/body尺寸 | 窗口大小 | |
| | vpw | UUID v4 | 随机值 | |
| | svm | 时间戳 | 当前时间 | |
| | trees | UUID v4 | 随机值 | |
| | pmf | 时间戳 | 当前时间 | |
| | cpucount | navigator.hardwareConcurrency | CPU核心数 | |
| | battery | navigator.getBattery() | 电池状态 | |
| | connectionRtt | navigator.connection.rtt | 网络延迟 | |
| | maxTouchPoints | navigator.maxTouchPoints | 触控点数 | |
| | status | Flash检测+webdriver检测 | 环境检测 | |
| | rtype | 固定值"1" | 类型标识 | |
| | sdkver | "3.0.0" | SDK版本 | |
| | subVersion | "1.0.0" | 子版本 | |
| | appId | "default" | 应用ID | |
| | organization | 配置值 | 组织标识 | |
| |
| ### 3.5 DES字段混淆 |
| |
| 每个指纹字段使用独立的8字节DES密钥进行ECB模式加密,然后Base64编码,最后字段名映射为混淆名称。 |
| |
| **混淆映射表(部分)**: |
| |
| | 原字段 | DES密钥 | 混淆名 | |
| |--------|---------|--------| |
| | appId | `q8zb1bs1` | vw | |
| | organization | `ht432iov` | tj | |
| | os | `i7g8jtql` | vh | |
| | ua | `z5jc9qyp` | wr | |
| | canvas | `qy1d6fmu` | mi | |
| | plugins | `j916a142` | vc | |
| | referer | `rf149ntc` | hv | |
| | res | `rbtd2cl6` | pi | |
| | timezone | `3w9lg8pr` | oi | |
| | svm | `mqcala0h` | nu | |
| | trees | `al8x9zt7` | hc | |
| | vpw | `xzvnu5jt` | st | |
| | protocol | (不加密) | protocol | |
| |
| ### 3.6 加密算法详解 |
| |
| #### RSA加密(uid → ep) |
| - 使用JSEncrypt库 |
| - PKCS#1 v1.5填充 |
| - 输出为Base64字符串 |
| |
| #### DES-ECB加密(字段混淆) |
| - 每个字段独立使用8字节密钥 |
| - ECB模式,逐块加密 |
| - 输出Base64编码 |
| |
| #### AES-CBC加密(数据体) |
| - 密钥:`priId`(md5(uid)[:16],UTF-8编码) |
| - IV:`0102030405060708` |
| - 模式:CBC |
| - 填充:ZeroPadding |
| - 输入:gzip压缩后的JSON |
| - 输出:hex字符串 |
| |
| #### 完整性校验(tn字段) |
| ``` |
| tn = md5(sorted_key_value_string(data)) |
| ``` |
| 对所有key排序后拼接`key+value`为字符串,取MD5。 |
| |
| ### 3.7 API请求格式 |
| |
| **请求**: |
| ``` |
| POST https://fp-it-acc.portal101.cn/deviceprofile/v4 |
|
|
| { |
| "appId": "default", |
| "organization": "P9usCUBauxft8eAmUXaZ", |
| "ep": "RSA加密的uid(hex)", |
| "data": "AES加密后的hex字符串", |
| "os": "web", |
| "encode": 5, |
| "compress": 2 |
| } |
| ``` |
| |
| **成功响应**: |
| ```json |
| { |
| "code": 1100, |
| "detail": { |
| "deviceId": "XkNeGKe7f4Y5FtZamaxQPqL3ypegujSyds5eQGup13ajuer9XhBAM82w1QWifIm6KUNv2hu3tUTMwoXrVzD0ZA==" |
| } |
| } |
| ``` |
|
|
| 最终返回的device_id为:`"B" + deviceId` |
| |
| ### 3.8 Go实现要点 |
| |
| 1. **不需要浏览器**:device_id由数美服务器生成,客户端只负责发送指纹数据和接收结果 |
| 2. **指纹数据可伪造**:服务端不校验指纹真实性,只需要格式正确 |
| 3. **关键依赖**:crypto/des、crypto/aes、crypto/rsa、crypto/md5、compress/gzip |
| 4. **DES-ECB**:Go标准库不提供ECB模式,需手动逐块实现 |
| 5. **AES ZeroPadding**:标准库只提供PKCS7,需手动补零 |
|
|
| --- |
|
|
| ## 四、HIF头部轮询机制 |
|
|
| ### 4.1 概述 |
|
|
| Web端每个请求需要携带两个特殊头部: |
| - `x-hif-dliq` |
| - `x-hif-leim` |
|
|
| 这两个值通过轮询DeepSeek的两个端点动态获取。 |
|
|
| ### 4.2 端点 |
|
|
| | 类型 | URL | 对应头部 | |
| |------|-----|----------| |
| | dliq | `https://hif-dliq.deepseek.com/query` | x-hif-dliq | |
| | leim | `https://hif-leim.deepseek.com/query` | x-hif-leim | |
|
|
| ### 4.3 响应格式 |
|
|
| ```json |
| { |
| "code": 0, |
| "data": { |
| "biz_code": 0, |
| "biz_data": { |
| "value": "9lspMrX9S0NlRuD7+ChfCz3ENqJPjH4qnMfnlPAwq0wfRuH2nmSepbg=.xuVOOVjUztQikXFf" |
| } |
| } |
| } |
| ``` |
|
|
| 响应头 `x-hif-ttl` 包含缓存有效期(秒),默认600秒。 |
|
|
| ### 4.4 轮询策略 |
|
|
| ``` |
| 初始化 → 启动两个goroutine轮询 |
| │ |
| ├── 成功 → 缓存value → 等待TTL秒 → 继续轮询 |
| │ |
| └── 失败 → 指数退避(1s→2s→4s→...→max 600s) → 重试 |
| ``` |
|
|
| - 初始重试间隔:1秒 |
| - 退避倍数:2x |
| - 最大间隔:600秒 |
| - 成功间隔:TTL(从响应头获取) |
|
|
| ### 4.5 Go实现 |
|
|
| `internal/deepseek/hif/hif.go` 实现了完整的轮询逻辑: |
|
|
| ```go |
| type HIFPoller struct { |
| mu sync.RWMutex |
| dliq string |
| leim string |
| stopCh chan struct{} |
| client *trans.Client // 使用Chrome TLS |
| fallback *http.Client // 标准HTTP fallback |
| } |
| |
| func (p *HIFPoller) Start() // 启动两个轮询goroutine |
| func (p *HIFPoller) Stop() // 停止轮询 |
| func (p *HIFPoller) GetHeaders() map[string]string // 获取当前HIF头部 |
| ``` |
|
|
| --- |
|
|
| ## 五、TLS指纹对比 |
|
|
| ### Android端 |
|
|
| ``` |
| ClientHello: HelloSafari_Auto |
| ALPN: 强制 http/1.1 (forceHTTP11ALPN) |
| ``` |
|
|
| ### Web端 |
|
|
| ``` |
| ClientHello: HelloChrome_Auto |
| ALPN: 自然协商 (支持 h2 和 http/1.1) |
| ForceAttemptHTTP2: true |
| ``` |
|
|
| 实现位于 `internal/deepseek/transport/transport.go`: |
|
|
| ```go |
| func TLSDialerForPlatform(platform string) func(...) { |
| if platform == "web" { |
| return chromeTLSDialer(dialContext) // Chrome + HTTP/2 |
| } |
| return safariTLSDialer(dialContext) // Safari + HTTP/1.1 |
| } |
| ``` |
|
|
| --- |
|
|
| ## 六、使用方式 |
|
|
| ### 6.1 设置面板切换 |
|
|
| 访问 WebUI 设置页面,在"接口平台"选项中选择: |
| - **Android端(默认)**:使用Safari TLS指纹,device_id为固定值 |
| - **Web端(Chrome浏览器)**:使用Chrome TLS指纹+HTTP/2,动态生成数美device_id,轮询HIF头部 |
|
|
| 切换后**立即生效**,无需重启服务。 |
|
|
| ### 6.2 环境变量 |
|
|
| ```bash |
| # 也可通过环境变量设置默认平台 |
| DS2API_PLATFORM_MODE=web |
| ``` |
|
|
| ### 6.3 API设置 |
|
|
| ```bash |
| curl -X PUT http://localhost:5001/admin/settings \ |
| -H "Authorization: Bearer admin" \ |
| -H "Content-Type: application/json" \ |
| -d '{"platform":{"mode":"web"}}' |
| ``` |
|
|
| --- |
|
|
| ## 七、兼容性说明 |
|
|
| - **默认行为不变**:未设置platform时默认为"android" |
| - **向后兼容**:所有现有配置和API保持不变 |
| - **热切换**:切换平台时自动重建HTTP客户端、更新headers、启停HIF轮询 |
| - **代理兼容**:使用代理时同样应用正确的TLS指纹和headers |
|
|
| --- |
|
|
| ## 八、测试状态 |
|
|
| ``` |
| ✅ go build ./... 编译通过 |
| ✅ go vet (核心包) 静态检查通过 |
| ✅ go test deepseek/... deepseek包测试通过 |
| ✅ go test shumei/... 数美包测试通过 |
| ✅ npm run build webui 前端构建通过 |
| ``` |
|
|
| > 注意:`TestGetSettingsIncludesCurrentInputFileDefaults` 是预先存在的测试失败,与本次改动无关。 |
|
|