refactor: replace WASM-based PoW with a high-performance native Go implementation and add context support for cancellation.

This commit is contained in:
CJACK
2026-04-07 01:20:01 +08:00
parent e7d561694a
commit da778a18fb
11 changed files with 31 additions and 27 deletions

View File

@@ -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/ # 原始流样本可见文本提取与回放辅助

View File

@@ -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

View File

@@ -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

View File

@@ -115,7 +115,7 @@ ds2api/
│ ├── claudeconv/ # Claude 消息格式转换
│ ├── compat/ # Go 版本兼容与回归测试辅助
│ ├── config/ # 配置加载、校验与热更新
│ ├── deepseek/ # DeepSeek 客户端、PoW WASM
│ ├── deepseek/ # DeepSeek 客户端、PoW 逻辑
│ ├── js/ # Node 运行时流式/兼容逻辑
│ ├── devcapture/ # 开发抓包
│ ├── format/ # 输出格式化

View File

@@ -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
```

View File

@@ -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
```

View File

@@ -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

View File

@@ -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)。

View File

@@ -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")
}

View File

@@ -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
}

View File

@@ -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)
}
}