From da778a18fb83ee138f51e3dcd6322a643be46381 Mon Sep 17 00:00:00 2001 From: CJACK Date: Tue, 7 Apr 2026 01:20:01 +0800 Subject: [PATCH] refactor: replace WASM-based PoW with a high-performance native Go implementation and add context support for cancellation. --- README.MD | 8 ++++---- README.en.md | 8 ++++---- docs/CONTRIBUTING.en.md | 2 +- docs/CONTRIBUTING.md | 2 +- docs/DEPLOY.en.md | 3 --- docs/DEPLOY.md | 3 --- internal/deepseek/client_auth.go | 2 +- internal/deepseek/pow.go | 5 +++-- internal/deepseek/pow_test.go | 2 +- pow/deepseek_pow.go | 16 ++++++++++++---- pow/deepseek_pow_test.go | 7 ++++--- 11 files changed, 31 insertions(+), 27 deletions(-) diff --git a/README.MD b/README.MD index d0c003a..6e5411d 100644 --- a/README.MD +++ b/README.MD @@ -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/ # 原始流样本可见文本提取与回放辅助 diff --git a/README.en.md b/README.en.md index 5959041..df764ab 100644 --- a/README.en.md +++ b/README.en.md @@ -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 diff --git a/docs/CONTRIBUTING.en.md b/docs/CONTRIBUTING.en.md index 0752b98..3370141 100644 --- a/docs/CONTRIBUTING.en.md +++ b/docs/CONTRIBUTING.en.md @@ -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 diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ad16e97..a4408df 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -115,7 +115,7 @@ ds2api/ │ ├── claudeconv/ # Claude 消息格式转换 │ ├── compat/ # Go 版本兼容与回归测试辅助 │ ├── config/ # 配置加载、校验与热更新 -│ ├── deepseek/ # DeepSeek 客户端、PoW WASM +│ ├── deepseek/ # DeepSeek 客户端、PoW 逻辑 │ ├── js/ # Node 运行时流式/兼容逻辑 │ ├── devcapture/ # 开发抓包 │ ├── format/ # 输出格式化 diff --git a/docs/DEPLOY.en.md b/docs/DEPLOY.en.md index 145b186..8273ff7 100644 --- a/docs/DEPLOY.en.md +++ b/docs/DEPLOY.en.md @@ -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 ``` diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 598c210..e4969d4 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -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 ``` diff --git a/internal/deepseek/client_auth.go b/internal/deepseek/client_auth.go index 3cbf323..e953327 100644 --- a/internal/deepseek/client_auth.go +++ b/internal/deepseek/client_auth.go @@ -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 diff --git a/internal/deepseek/pow.go b/internal/deepseek/pow.go index 54f678d..9d839de 100644 --- a/internal/deepseek/pow.go +++ b/internal/deepseek/pow.go @@ -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)。 diff --git a/internal/deepseek/pow_test.go b/internal/deepseek/pow_test.go index 3f1104c..0161f62 100644 --- a/internal/deepseek/pow_test.go +++ b/internal/deepseek/pow_test.go @@ -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") } diff --git a/pow/deepseek_pow.go b/pow/deepseek_pow.go index f6ea7de..bb9b2b4 100644 --- a/pow/deepseek_pow.go +++ b/pow/deepseek_pow.go @@ -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 } diff --git a/pow/deepseek_pow_test.go b/pow/deepseek_pow_test.go index 73c6f66..d2ed773 100644 --- a/pow/deepseek_pow_test.go +++ b/pow/deepseek_pow_test.go @@ -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) } }