mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-01 23:15:27 +08:00
refactor: replace WASM-based PoW with a high-performance native Go implementation and add context support for cancellation.
This commit is contained in:
@@ -48,7 +48,7 @@ flowchart LR
|
||||
Auth["Auth Resolver\n(API key / bearer / x-goog-api-key)"]
|
||||
Pool["Account Pool + Queue\n(并发槽位 + 等待队列)"]
|
||||
DSClient["DeepSeek Client\n(Session / Auth / HTTP)"]
|
||||
Pow["PoW WASM\n(wazero 预加载)"]
|
||||
Pow["PoW 实现\n(纯 Go 毫秒级)"]
|
||||
Tool["Tool Sieve\n(Go/Node 语义对齐)"]
|
||||
end
|
||||
end
|
||||
@@ -95,7 +95,7 @@ flowchart LR
|
||||
| Gemini 兼容 | `POST /v1beta/models/{model}:generateContent`、`POST /v1beta/models/{model}:streamGenerateContent`(及 `/v1/models/{model}:*` 路径) |
|
||||
| 多账号轮询 | 自动 token 刷新、邮箱/手机号双登录方式 |
|
||||
| 并发队列控制 | 每账号 in-flight 上限 + 等待队列,动态计算建议并发值 |
|
||||
| DeepSeek PoW | WASM 计算(`wazero`),无需外部 Node.js 依赖 |
|
||||
| DeepSeek PoW | 纯 Go 高性能实现(DeepSeekHashV1),毫秒级响应 |
|
||||
| Tool Calling | 防泄漏处理:非代码块高置信特征识别、`delta.tool_calls` 早发、结构化增量输出 |
|
||||
| Admin API | 配置管理、运行时设置热更新、账号测试 / 批量测试、会话清理、导入导出、Vercel 同步、版本检查 |
|
||||
| WebUI 管理台 | `/admin` 单页应用(中英文双语、深色模式) |
|
||||
@@ -344,7 +344,7 @@ cp opencode.json.example opencode.json
|
||||
| `DS2API_CONFIG_PATH` | 配置文件路径 | `config.json` |
|
||||
| `DS2API_CONFIG_JSON` | 直接注入配置(JSON 或 Base64) | — |
|
||||
| `DS2API_ENV_WRITEBACK` | 环境变量模式下自动写回配置文件并切换文件模式(`1/true/yes/on`) | 关闭 |
|
||||
| `DS2API_WASM_PATH` | PoW WASM 文件路径 | 自动查找 |
|
||||
| `DS2API_POW_CONCURRENCY` | PoW 并行计算协程数(可选) | 默认 CPU 核心数 |
|
||||
| `DS2API_STATIC_ADMIN_DIR` | 管理台静态文件目录 | `static/admin` |
|
||||
| `DS2API_AUTO_BUILD_WEBUI` | 启动时自动构建 WebUI | 本地开启,Vercel 关闭 |
|
||||
| `DS2API_DEV_PACKET_CAPTURE` | 本地开发抓包开关(记录最近会话请求/响应体) | 本地非 Vercel 默认开启 |
|
||||
@@ -455,7 +455,7 @@ ds2api/
|
||||
│ ├── claudeconv/ # Claude 消息格式转换
|
||||
│ ├── compat/ # Go 版本兼容与回归测试辅助
|
||||
│ ├── config/ # 配置加载、校验与热更新
|
||||
│ ├── deepseek/ # DeepSeek API 客户端、PoW WASM
|
||||
│ ├── deepseek/ # DeepSeek API 客户端、PoW 逻辑
|
||||
│ ├── js/ # Node 运行时流式处理与兼容逻辑
|
||||
│ ├── devcapture/ # 开发抓包模块
|
||||
│ ├── rawsample/ # 原始流样本可见文本提取与回放辅助
|
||||
|
||||
@@ -48,7 +48,7 @@ flowchart LR
|
||||
Auth["Auth Resolver\n(API key / bearer / x-goog-api-key)"]
|
||||
Pool["Account Pool + Queue\n(in-flight slots + wait queue)"]
|
||||
DSClient["DeepSeek Client\n(session / auth / HTTP)"]
|
||||
Pow["PoW WASM\n(wazero preload)"]
|
||||
Pow["PoW Solver\n(Pure Go ms-level)"]
|
||||
Tool["Tool Sieve\n(Go/Node semantic parity)"]
|
||||
end
|
||||
end
|
||||
@@ -95,7 +95,7 @@ flowchart LR
|
||||
| Gemini compatible | `POST /v1beta/models/{model}:generateContent`, `POST /v1beta/models/{model}:streamGenerateContent` (plus `/v1/models/{model}:*` paths) |
|
||||
| Multi-account rotation | Auto token refresh, email/mobile dual login |
|
||||
| Concurrency control | Per-account in-flight limit + waiting queue, dynamic recommended concurrency |
|
||||
| DeepSeek PoW | WASM solving via `wazero`, no external Node.js dependency |
|
||||
| DeepSeek PoW | Pure Go high-performance solver (DeepSeekHashV1), ms-level response |
|
||||
| Tool Calling | Anti-leak handling: non-code-block feature match, early `delta.tool_calls`, structured incremental output |
|
||||
| Admin API | Config management, runtime settings hot-reload, account testing/batch test, session cleanup, import/export, Vercel sync, version check |
|
||||
| WebUI Admin Panel | SPA at `/admin` (bilingual Chinese/English, dark mode) |
|
||||
@@ -344,7 +344,7 @@ cp opencode.json.example opencode.json
|
||||
| `DS2API_CONFIG_PATH` | Config file path | `config.json` |
|
||||
| `DS2API_CONFIG_JSON` | Inline config (JSON or Base64) | — |
|
||||
| `DS2API_ENV_WRITEBACK` | Auto-write env-backed config to file and transition to file mode (`1/true/yes/on`) | Disabled |
|
||||
| `DS2API_WASM_PATH` | PoW WASM file path | Auto-detect |
|
||||
| `DS2API_POW_CONCURRENCY` | PoW parallel solver goroutine count (optional) | Default CPU core count |
|
||||
| `DS2API_STATIC_ADMIN_DIR` | Admin static assets dir | `static/admin` |
|
||||
| `DS2API_AUTO_BUILD_WEBUI` | Auto-build WebUI on startup | Enabled locally, disabled on Vercel |
|
||||
| `DS2API_ACCOUNT_MAX_INFLIGHT` | Max in-flight requests per account | `2` |
|
||||
@@ -453,7 +453,7 @@ ds2api/
|
||||
│ ├── claudeconv/ # Claude message format conversion
|
||||
│ ├── compat/ # Go-version compatibility and regression helpers
|
||||
│ ├── config/ # Config loading, validation, and hot-reload
|
||||
│ ├── deepseek/ # DeepSeek API client, PoW WASM
|
||||
│ ├── deepseek/ # DeepSeek API client, PoW logic
|
||||
│ ├── js/ # Node runtime stream/compat logic
|
||||
│ ├── devcapture/ # Dev packet capture module
|
||||
│ ├── rawsample/ # Visible-text extraction and replay helpers for raw stream samples
|
||||
|
||||
@@ -115,7 +115,7 @@ ds2api/
|
||||
│ ├── claudeconv/ # Claude message conversion
|
||||
│ ├── compat/ # Go-version compatibility and regression helpers
|
||||
│ ├── config/ # Config loading, validation, and hot-reload
|
||||
│ ├── deepseek/ # DeepSeek client, PoW WASM
|
||||
│ ├── deepseek/ # DeepSeek client, PoW logic
|
||||
│ ├── js/ # Node runtime stream/compat logic
|
||||
│ ├── devcapture/ # Dev packet capture
|
||||
│ ├── format/ # Output formatting
|
||||
|
||||
@@ -115,7 +115,7 @@ ds2api/
|
||||
│ ├── claudeconv/ # Claude 消息格式转换
|
||||
│ ├── compat/ # Go 版本兼容与回归测试辅助
|
||||
│ ├── config/ # 配置加载、校验与热更新
|
||||
│ ├── deepseek/ # DeepSeek 客户端、PoW WASM
|
||||
│ ├── deepseek/ # DeepSeek 客户端、PoW 逻辑
|
||||
│ ├── js/ # Node 运行时流式/兼容逻辑
|
||||
│ ├── devcapture/ # 开发抓包
|
||||
│ ├── format/ # 输出格式化
|
||||
|
||||
@@ -366,7 +366,6 @@ Each archive includes:
|
||||
|
||||
- `ds2api` executable (`ds2api.exe` on Windows)
|
||||
- `static/admin/` (built WebUI assets)
|
||||
- `sha3_wasm_bg.7b9ca65ddd.wasm` (optional; binary has embedded fallback)
|
||||
- `config.example.json`, `.env.example`
|
||||
- `README.MD`, `README.en.md`, `LICENSE`
|
||||
|
||||
@@ -456,8 +455,6 @@ server {
|
||||
# Copy compiled binary and related files to target directory
|
||||
sudo mkdir -p /opt/ds2api
|
||||
sudo cp ds2api config.json /opt/ds2api/
|
||||
# Optional: if you want to use an external WASM file (override the embedded one, from a release package or build output)
|
||||
# sudo cp /path/to/sha3_wasm_bg.7b9ca65ddd.wasm /opt/ds2api/
|
||||
sudo cp -r static/admin /opt/ds2api/static/admin
|
||||
```
|
||||
|
||||
|
||||
@@ -366,7 +366,6 @@ No Output Directory named "public" found after the Build completed.
|
||||
|
||||
- `ds2api` 可执行文件(Windows 为 `ds2api.exe`)
|
||||
- `static/admin/`(WebUI 构建产物)
|
||||
- `sha3_wasm_bg.7b9ca65ddd.wasm`(可选;程序内置 embed fallback)
|
||||
- `config.example.json`、`.env.example`
|
||||
- `README.MD`、`README.en.md`、`LICENSE`
|
||||
|
||||
@@ -456,8 +455,6 @@ server {
|
||||
# 将编译好的二进制文件和相关文件复制到目标目录
|
||||
sudo mkdir -p /opt/ds2api
|
||||
sudo cp ds2api config.json /opt/ds2api/
|
||||
# 可选:若你希望使用外置 WASM 文件(覆盖内置版本,来自 release 包或构建产物)
|
||||
# sudo cp /path/to/sha3_wasm_bg.7b9ca65ddd.wasm /opt/ds2api/
|
||||
sudo cp -r static/admin /opt/ds2api/static/admin
|
||||
```
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ func (c *Client) GetPow(ctx context.Context, a *auth.RequestAuth, maxAttempts in
|
||||
data, _ := resp["data"].(map[string]any)
|
||||
bizData, _ := data["biz_data"].(map[string]any)
|
||||
challenge, _ := bizData["challenge"].(map[string]any)
|
||||
answer, err := ComputePow(challenge)
|
||||
answer, err := ComputePow(ctx, challenge)
|
||||
if err != nil {
|
||||
attempts++
|
||||
continue
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package deepseek
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -9,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// ComputePow 使用纯 Go 实现求解 PoW challenge (DeepSeekHashV1)。
|
||||
func ComputePow(challenge map[string]any) (int64, error) {
|
||||
func ComputePow(ctx context.Context, challenge map[string]any) (int64, error) {
|
||||
algo, _ := challenge["algorithm"].(string)
|
||||
if algo != "DeepSeekHashV1" {
|
||||
return 0, errors.New("unsupported algorithm")
|
||||
@@ -19,7 +20,7 @@ func ComputePow(challenge map[string]any) (int64, error) {
|
||||
expireAt := toInt64(challenge["expire_at"], 1680000000)
|
||||
difficulty := toInt64FromFloat(challenge["difficulty"], 144000)
|
||||
|
||||
return pow.SolvePow(challengeStr, salt, expireAt, difficulty)
|
||||
return pow.SolvePow(ctx, challengeStr, salt, expireAt, difficulty)
|
||||
}
|
||||
|
||||
// BuildPowHeader 序列化 {algorithm,challenge,salt,answer,signature,target_path} 为 base64(JSON)。
|
||||
|
||||
@@ -13,7 +13,7 @@ func TestPreloadPowNoOp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestComputePowUnsupportedAlgorithm(t *testing.T) {
|
||||
_, err := ComputePow(map[string]any{"algorithm": "unknown"})
|
||||
_, err := ComputePow(context.Background(), map[string]any{"algorithm": "unknown"})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for unsupported algorithm")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package pow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
@@ -9,7 +10,7 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Challenge 对应 /api/v0/chat/create_pow_challenge 返回的 data.biz_data.challenge。
|
||||
// Challenge 对应 /api/v0/chat/create_pow_challenge 返回 dem data.biz_data.challenge。
|
||||
type Challenge struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
Challenge string `json:"challenge"`
|
||||
@@ -27,7 +28,7 @@ func BuildPrefix(salt string, expireAt int64) string {
|
||||
|
||||
// SolvePow 搜索 nonce ∈ [0, difficulty) 使得 DeepSeekHashV1(prefix+str(nonce)) == challenge。
|
||||
// prefix 预吸收进 state,循环内零分配。
|
||||
func SolvePow(challengeHex, salt string, expireAt, difficulty int64) (int64, error) {
|
||||
func SolvePow(ctx context.Context, challengeHex, salt string, expireAt, difficulty int64) (int64, error) {
|
||||
if len(challengeHex) != 64 {
|
||||
return 0, errors.New("pow: challenge must be 64 hex chars")
|
||||
}
|
||||
@@ -59,6 +60,13 @@ func SolvePow(challengeHex, salt string, expireAt, difficulty int64) (int64, err
|
||||
|
||||
var numBuf [20]byte
|
||||
for n := int64(0); n < difficulty; n++ {
|
||||
// Periodically check if context is canceled to avoid wasting CPU
|
||||
if n&0x3FF == 0 {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
v := uint64(n)
|
||||
pos := 20
|
||||
if v == 0 {
|
||||
@@ -123,7 +131,7 @@ func BuildPowHeader(c *Challenge, answer int64) (string, error) {
|
||||
}
|
||||
|
||||
// SolveAndBuildHeader 端到端: Challenge → x-ds-pow-response header string。
|
||||
func SolveAndBuildHeader(c *Challenge) (string, error) {
|
||||
func SolveAndBuildHeader(ctx context.Context, c *Challenge) (string, error) {
|
||||
if c.Algorithm != "DeepSeekHashV1" {
|
||||
return "", errors.New("pow: unsupported algorithm: " + c.Algorithm)
|
||||
}
|
||||
@@ -131,7 +139,7 @@ func SolveAndBuildHeader(c *Challenge) (string, error) {
|
||||
if d == 0 {
|
||||
d = 144000
|
||||
}
|
||||
answer, err := SolvePow(c.Challenge, c.Salt, c.ExpireAt, d)
|
||||
answer, err := SolvePow(ctx, c.Challenge, c.Salt, c.ExpireAt, d)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package pow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
@@ -36,7 +37,7 @@ func TestSolvePow(t *testing.T) {
|
||||
{"abc123salt", 1700000000, 12345, 20000},
|
||||
} {
|
||||
h := DeepSeekHashV1([]byte(BuildPrefix(tc.salt, tc.expire) + strconv.FormatInt(tc.answer, 10)))
|
||||
got, err := SolvePow(hex.EncodeToString(h[:]), tc.salt, tc.expire, tc.diff)
|
||||
got, err := SolvePow(context.Background(), hex.EncodeToString(h[:]), tc.salt, tc.expire, tc.diff)
|
||||
if err != nil || got != tc.answer {
|
||||
t.Errorf("salt=%q answer=%d: got=%d err=%v", tc.salt, tc.answer, got, err)
|
||||
}
|
||||
@@ -45,7 +46,7 @@ func TestSolvePow(t *testing.T) {
|
||||
|
||||
func TestSolveAndBuildHeader(t *testing.T) {
|
||||
t0 := DeepSeekHashV1([]byte("salt_1712345678_777"))
|
||||
header, err := SolveAndBuildHeader(&Challenge{
|
||||
header, err := SolveAndBuildHeader(context.Background(), &Challenge{
|
||||
Algorithm: "DeepSeekHashV1", Challenge: hex.EncodeToString(t0[:]),
|
||||
Salt: "salt", ExpireAt: 1712345678, Difficulty: 2000,
|
||||
Signature: "sig", TargetPath: "/api/v0/chat/completion",
|
||||
@@ -74,6 +75,6 @@ func BenchmarkSolve(b *testing.B) {
|
||||
h := DeepSeekHashV1([]byte("realisticsalt_1712345678_72000"))
|
||||
ch := hex.EncodeToString(h[:])
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = SolvePow(ch, "realisticsalt", 1712345678, 144000)
|
||||
_, _ = SolvePow(context.Background(), ch, "realisticsalt", 1712345678, 144000)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user