diff --git a/.env.example b/.env.example index d63f133..8a7cc5f 100644 --- a/.env.example +++ b/.env.example @@ -1,93 +1,15 @@ -# DS2API environment template (Go runtime) -# Copy this file to .env and adjust values. -# Updated: 2026-02 - -# --------------------------------------------------------------- -# Runtime -# --------------------------------------------------------------- -# HTTP listen port (default: 5001) +# DS2API runtime PORT=5001 - -# Log level: DEBUG | INFO | WARN | ERROR LOG_LEVEL=INFO -# Max concurrent inflight requests per account in managed-key mode. -# Default: 2 -# Recommended client concurrency is calculated dynamically as: -# account_count * DS2API_ACCOUNT_MAX_INFLIGHT -# So by default it is account_count * 2. -# Requests beyond inflight slots enter a waiting queue first. -# Default queue size equals recommended concurrency, so 429 starts after: -# account_count * DS2API_ACCOUNT_MAX_INFLIGHT * 2 -# Alias: DS2API_ACCOUNT_CONCURRENCY -# DS2API_ACCOUNT_MAX_INFLIGHT=2 +# Admin authentication +DS2API_ADMIN_KEY=change-me -# Optional waiting queue size override for managed-key mode. -# Default: recommended_concurrency (same as account_count * inflight_limit) -# Alias: DS2API_ACCOUNT_QUEUE_SIZE -# DS2API_ACCOUNT_MAX_QUEUE=10 +# Config loading (choose one) +# 1) file-based config +DS2API_CONFIG_PATH=/app/config.json +# 2) inline JSON or Base64 JSON +# DS2API_CONFIG_JSON= -# --------------------------------------------------------------- -# Admin auth -# --------------------------------------------------------------- -# Admin key for /admin login and protected admin APIs. -# Default is "admin" when unset, but setting it explicitly is recommended. -DS2API_ADMIN_KEY=admin - -# Optional JWT signing secret for admin token. -# Defaults to DS2API_ADMIN_KEY when unset. -# DS2API_JWT_SECRET=change-me - -# Optional admin JWT validity in hours (default: 24) -# DS2API_JWT_EXPIRE_HOURS=24 - -# --------------------------------------------------------------- -# Config source (choose one) -# --------------------------------------------------------------- -# Option A: config file path (local/dev recommended) -# DS2API_CONFIG_PATH=config.json - -# Option B: JSON string -# DS2API_CONFIG_JSON={"keys":["your-api-key"],"accounts":[{"email":"user@example.com","password":"xxx","token":""}]} - -# Option C: Base64 encoded JSON (recommended for Vercel env var) -# DS2API_CONFIG_JSON=eyJrZXlzIjpbInlvdXItYXBpLWtleSJdLCJhY2NvdW50cyI6W3siZW1haWwiOiJ1c2VyQGV4YW1wbGUuY29tIiwicGFzc3dvcmQiOiJ4eHgiLCJ0b2tlbiI6IiJ9XX0= -# -# Generate from local config.json: -# DS2API_CONFIG_JSON="$(base64 < config.json | tr -d '\n')" - -# --------------------------------------------------------------- -# Paths (optional) -# --------------------------------------------------------------- -# WASM file used for PoW solving -# DS2API_WASM_PATH=sha3_wasm_bg.7b9ca65ddd.wasm - -# Built admin static assets directory -# DS2API_STATIC_ADMIN_DIR=static/admin - -# Auto-build WebUI on startup when static/admin is missing. -# Default: enabled on local/Docker, disabled on Vercel. -# DS2API_AUTO_BUILD_WEBUI=true - -# Internal auth secret used by the Vercel hybrid streaming path -# (Go prepare endpoint <-> Node stream function). -# Optional: falls back to DS2API_ADMIN_KEY when unset. -# DS2API_VERCEL_INTERNAL_SECRET=change-me - -# Stream lease TTL seconds for Vercel hybrid streaming. -# During this window, the managed account stays occupied until Node calls release. -# Default: 900 (15 minutes) -# DS2API_VERCEL_STREAM_LEASE_TTL_SECONDS=900 - -# --------------------------------------------------------------- -# Vercel sync integration (optional) -# --------------------------------------------------------------- -# VERCEL_TOKEN=your-vercel-token -# VERCEL_PROJECT_ID=prj_xxxxxxxxxxxx -# VERCEL_TEAM_ID=team_xxxxxxxxxxxx - -# Optional: Vercel deployment protection bypass secret. -# If deployment protection is enabled, DS2API will use this value as -# x-vercel-protection-bypass for internal Node->Go calls on Vercel. -# You can also use VERCEL_AUTOMATION_BYPASS_SECRET directly. -# DS2API_VERCEL_PROTECTION_BYPASS=your-bypass-secret +# Optional: static admin assets path +# DS2API_STATIC_ADMIN_DIR=/app/static/admin diff --git a/CONTRIBUTING.en.md b/CONTRIBUTING.en.md index 156b025..92bdcdb 100644 --- a/CONTRIBUTING.en.md +++ b/CONTRIBUTING.en.md @@ -99,7 +99,7 @@ ds2api/ ├── api/ │ ├── index.go # Vercel Serverless Go entry │ ├── chat-stream.js # Vercel Node.js stream relay -│ └── helpers/ # Node.js helper modules +│ └── (rewrite targets in vercel.json) ├── internal/ │ ├── account/ # Account pool and concurrency queue │ ├── adapter/ @@ -112,6 +112,7 @@ ds2api/ │ ├── compat/ # Compatibility helpers │ ├── config/ # Config loading and hot-reload │ ├── deepseek/ # DeepSeek client, PoW WASM +│ ├── js/ # Node runtime stream/compat logic │ ├── devcapture/ # Dev packet capture │ ├── format/ # Output formatting │ ├── prompt/ # Prompt building @@ -123,7 +124,9 @@ ds2api/ │ └── webui/ # WebUI static hosting ├── webui/ # React WebUI source │ └── src/ -│ ├── components/ # Components +│ ├── app/ # Routing, auth, config state +│ ├── features/ # Feature modules +│ ├── components/ # Shared components │ └── locales/ # Language packs ├── scripts/ # Build and test scripts ├── static/admin/ # WebUI build output (not committed) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd44b9a..1bb0567 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,7 +99,7 @@ ds2api/ ├── api/ │ ├── index.go # Vercel Serverless Go 入口 │ ├── chat-stream.js # Vercel Node.js 流式转发 -│ └── helpers/ # Node.js 辅助模块 +│ └── (rewrite targets in vercel.json) ├── internal/ │ ├── account/ # 账号池与并发队列 │ ├── adapter/ @@ -112,6 +112,7 @@ ds2api/ │ ├── compat/ # 兼容性辅助 │ ├── config/ # 配置加载与热更新 │ ├── deepseek/ # DeepSeek 客户端、PoW WASM +│ ├── js/ # Node 运行时流式/兼容逻辑 │ ├── devcapture/ # 开发抓包 │ ├── format/ # 输出格式化 │ ├── prompt/ # Prompt 构建 @@ -123,7 +124,9 @@ ds2api/ │ └── webui/ # WebUI 静态托管 ├── webui/ # React WebUI 源码 │ └── src/ -│ ├── components/ # 组件 +│ ├── app/ # 路由、鉴权、配置状态 +│ ├── features/ # 业务功能模块 +│ ├── components/ # 通用组件 │ └── locales/ # 语言包 ├── scripts/ # 构建与测试脚本 ├── static/admin/ # WebUI 构建产物(不提交) diff --git a/DEPLOY.en.md b/DEPLOY.en.md index 127696a..92f35ee 100644 --- a/DEPLOY.en.md +++ b/DEPLOY.en.md @@ -366,7 +366,7 @@ Each archive includes: - `ds2api` executable (`ds2api.exe` on Windows) - `static/admin/` (built WebUI assets) -- `sha3_wasm_bg.7b9ca65ddd.wasm` +- `sha3_wasm_bg.7b9ca65ddd.wasm` (optional; binary has embedded fallback) - `config.example.json`, `.env.example` - `README.MD`, `README.en.md`, `LICENSE` @@ -455,7 +455,9 @@ server { ```bash # Copy compiled binary and related files to target directory sudo mkdir -p /opt/ds2api -sudo cp ds2api config.json sha3_wasm_bg.7b9ca65ddd.wasm /opt/ds2api/ +sudo cp ds2api config.json /opt/ds2api/ +# Optional: if you want to use an external WASM file (override embedded one) +# sudo cp sha3_wasm_bg.7b9ca65ddd.wasm /opt/ds2api/ sudo cp -r static/admin /opt/ds2api/static/admin ``` diff --git a/DEPLOY.md b/DEPLOY.md index 2df4238..3e77a0a 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -366,7 +366,7 @@ No Output Directory named "public" found after the Build completed. - `ds2api` 可执行文件(Windows 为 `ds2api.exe`) - `static/admin/`(WebUI 构建产物) -- `sha3_wasm_bg.7b9ca65ddd.wasm` +- `sha3_wasm_bg.7b9ca65ddd.wasm`(可选;程序内置 embed fallback) - `config.example.json`、`.env.example` - `README.MD`、`README.en.md`、`LICENSE` @@ -455,7 +455,9 @@ server { ```bash # 将编译好的二进制文件和相关文件复制到目标目录 sudo mkdir -p /opt/ds2api -sudo cp ds2api config.json sha3_wasm_bg.7b9ca65ddd.wasm /opt/ds2api/ +sudo cp ds2api config.json /opt/ds2api/ +# 可选:若你希望使用外置 WASM 文件(覆盖内置版本) +# sudo cp sha3_wasm_bg.7b9ca65ddd.wasm /opt/ds2api/ sudo cp -r static/admin /opt/ds2api/static/admin ``` diff --git a/README.MD b/README.MD index 6b9383a..d8c5872 100644 --- a/README.MD +++ b/README.MD @@ -397,7 +397,7 @@ ds2api/ ├── api/ │ ├── index.go # Vercel Serverless Go 入口 │ ├── chat-stream.js # Vercel Node.js 流式转发 -│ └── helpers/ # Node.js 辅助模块 +│ └── (rewrite targets in vercel.json) ├── internal/ │ ├── account/ # 账号池与并发队列 │ ├── adapter/ @@ -410,6 +410,7 @@ ds2api/ │ ├── compat/ # 兼容性辅助 │ ├── config/ # 配置加载与热更新 │ ├── deepseek/ # DeepSeek API 客户端、PoW WASM +│ ├── js/ # Node 运行时流式处理与兼容逻辑 │ ├── devcapture/ # 开发抓包模块 │ ├── format/ # 输出格式化 │ ├── prompt/ # Prompt 构建 @@ -420,7 +421,9 @@ ds2api/ │ └── webui/ # WebUI 静态文件托管与自动构建 ├── webui/ # React WebUI 源码(Vite + Tailwind) │ └── src/ -│ ├── components/ # AccountManager / ApiTester / BatchImport / VercelSync / Login / LandingPage +│ ├── app/ # 路由、鉴权、配置状态管理 +│ ├── features/ # 业务功能模块(account/settings/vercel/apiTester) +│ ├── components/ # 登录/落地页等通用组件 │ └── locales/ # 中英文语言包(zh.json / en.json) ├── scripts/ │ └── build-webui.sh # WebUI 手动构建脚本 @@ -500,7 +503,7 @@ go test -v -run 'TestParseToolCalls|TestRepair' ./internal/util/ - **触发条件**:仅在 GitHub Release `published` 时触发(普通 push 不会触发) - **构建产物**:多平台二进制包(`linux/amd64`、`linux/arm64`、`darwin/amd64`、`darwin/arm64`、`windows/amd64`)+ `sha256sums.txt` - **容器镜像发布**:仅推送到 GHCR(`ghcr.io/cjackhwang/ds2api`) -- **每个压缩包包含**:`ds2api` 可执行文件、`static/admin`、WASM 文件、配置示例、README、LICENSE +- **每个压缩包包含**:`ds2api` 可执行文件、`static/admin`、WASM 文件(同时支持内置 fallback)、配置示例、README、LICENSE ## 免责声明 diff --git a/README.en.md b/README.en.md index 1c07c23..7f0278b 100644 --- a/README.en.md +++ b/README.en.md @@ -398,7 +398,7 @@ ds2api/ ├── api/ │ ├── index.go # Vercel Serverless Go entry │ ├── chat-stream.js # Vercel Node.js stream relay -│ └── helpers/ # Node.js helper modules +│ └── (rewrite targets in vercel.json) ├── internal/ │ ├── account/ # Account pool and concurrency queue │ ├── adapter/ @@ -411,6 +411,7 @@ ds2api/ │ ├── compat/ # Compatibility helpers │ ├── config/ # Config loading and hot-reload │ ├── deepseek/ # DeepSeek API client, PoW WASM +│ ├── js/ # Node runtime stream/compat logic │ ├── devcapture/ # Dev packet capture module │ ├── format/ # Output formatting │ ├── prompt/ # Prompt construction @@ -421,7 +422,9 @@ ds2api/ │ └── webui/ # WebUI static file serving and auto-build ├── webui/ # React WebUI source (Vite + Tailwind) │ └── src/ -│ ├── components/ # AccountManager / ApiTester / BatchImport / VercelSync / Login / LandingPage +│ ├── app/ # Routing, auth, config state +│ ├── features/ # Feature modules (account/settings/vercel/apiTester) +│ ├── components/ # Shared UI pieces (login/landing, etc.) │ └── locales/ # Language packs (zh.json / en.json) ├── scripts/ │ └── build-webui.sh # Manual WebUI build script @@ -484,7 +487,7 @@ Workflow: `.github/workflows/release-artifacts.yml` - **Trigger**: only on GitHub Release `published` (normal pushes do not trigger builds) - **Outputs**: multi-platform archives (`linux/amd64`, `linux/arm64`, `darwin/amd64`, `darwin/arm64`, `windows/amd64`) + `sha256sums.txt` - **Container publishing**: GHCR only (`ghcr.io/cjackhwang/ds2api`) -- **Each archive includes**: `ds2api` executable, `static/admin`, WASM file, config template, README, LICENSE +- **Each archive includes**: `ds2api` executable, `static/admin`, WASM file (with embedded fallback support), config template, README, LICENSE ## Disclaimer diff --git a/misc/deepseek_functioncalling_bug/report.md b/misc/deepseek_functioncalling_bug/report.md deleted file mode 100644 index f723578..0000000 --- a/misc/deepseek_functioncalling_bug/report.md +++ /dev/null @@ -1,101 +0,0 @@ -# DeepSeek Function Calling 缺陷分析与 ds2api 的增强修复策略 - -> **相关 PR**: #74 (代码核心实现) 与 #75 (Merge to dev) -> **问题背景**: 解决因包括 DeepSeek 在内的部分模型在函数调用(Function Calling/Tool Call)表现不够“规范”,从而导致工具调用失败的问题。 - -## 一、底层架构对比:为什么会产生 Function Calling 缺陷? - -在探讨缺陷前,我们需要理解两种 Function Calling 的底层结构差异: - -### 1. OpenAI 的原生结构化返回 (API 级分离) -在 OpenAI 的规范中,**聊天文字与工具调用是在底层的 JSON 结构中被硬性拆分的**: -* 聊天废话存放在 `response.choices[0].message.content` 里。 -* 工具请求存放在单独的数组 `response.choices[0].message.tool_calls` 里。 - -**优势:** 这种设计对客户端极其友好。客户端只需判断 `tool_calls` 是否为空,就能决定是执行代码还是渲染文字。它支持同时并发多个工具请求,且底层的生成殷勤被严格训练和约束,极少抛出语法错误的 JSON。 - -### 2. DeepSeek 等模型的“单文本流”机制 -相比之下,部分未经深度专门微调的模型(或者在特定的通信适配层中),它们依然倾向于把一切内容打包成一个纯文本流吐出。这就是为什么它们的输出往往不仅包含了本该属于 `tool_calls` 结构里的 JSON,还会像个“老实人”一样夹杂了属于 `content` 里的散文。 - ---- - -## 二、DeepSeek 在 Function Calling 上的特定缺陷表现 - -相比于 OpenAI 严格遵循 API 约定的原生结构,DeepSeek 等开源/国产推理模型在工具调用时,经常会暴露出以下三种典型的“不守规矩”的输出行为: - -### 1. 混合输出:散文文本与工具 JSON 混杂 (Mixed Prose Streams) -当应用要求模型直接返回工具请求时,DeepSeek 有时候会**“忍不住想和用户搭话”**。 -它常常前置一段解释性废话,中间插入工具调用的 JSON 参数,并在末尾再补上一句总结: -```text -好的,我这就帮你读取 README.md 的内容: -{"tool_calls":[{"name":"read_file","input":{"path":"README.md"}}]} -请稍等片刻,我马上把它读出来。 -``` -**旧版系统痛点:** -原有的代码存在**严格模式(Strict Mode)**校验: -```go -// 如果解析到的 JSON 块前后存在任何非空字符串,就放弃当作工具调用! -if strings.TrimSpace(state.recentTextTail) != "" || strings.TrimSpace(prefixPart) != "" ... { - return captured, nil, "", true -} -``` -这直接导致上述结构被网关认定是一段“普通聊天”,直接原封不动地返回给用户,这直接干挂了后续的工具自动执行流程。 - -### 2. 工具名格式幻觉:擅自修改或前缀化工具名称 -由于 DeepSeek 的预训练数据中有大量的代码和不同的平台结构,它在回复工具名称时,常常无法忠实于 System Prompt 中提供的纯命名(也就是 `name: "read_file"`),而是加上前缀或者拼写变形,例如: -* `{"name": "mcp.search_web"}` (自带命名空间) -* `{"name": "tools.read_file"}` -* `{"name": "search-web"}` (下划线变成了中划线) - -**旧版系统痛点:** -旧版系统对于工具名的匹配几乎只有“绝对相等”的字典级比对,只要差了一个字符或加了前缀,就会由于找不到合法工具而直接失败。 - -### 3. Role 角色的非标准返回 -在部分工具通信流的响应中,返回的内容其所属的 `role` 没有被标准化处理,可能携带意料之外的属性,或是与下游严格比对出现冲突。 - ---- - -## 二、PR #74 的代码增强修复方案 - -为了解决大模型这种自身的不规范行为,PR #74 在系统的中间层网关联入了一个**极其包容的容错引擎**。它并不强制要求模型“改过自新”,而是主动做了以下三块增强: - -### 1. 从流中分离混合内容(废除 Strict Mode) -修改了 `internal/adapter/openai/tool_sieve_core.go`。 -取消了前后包裹文本的拦截逻辑。当系统扫描到流式结构中有完整的 `{"tool_calls":...}` 时,它会将废话和 JSON 分发到不同的事件流中: -```go -if prefix != "" { - // 将前面的“好的,帮你读文件”剥离出来作为常规文本输出 - state.noteText(prefix) - events = append(events, toolStreamEvent{Content: prefix}) -} -// 捕获并拦截中间的工具请求,进行背后执行 -state.pendingToolCalls = calls -``` -**效果:** 用户的屏幕上只能看到正常的文字交流,而后端的工具也会立刻挂载。 - -### 2. 多级宽容匹配引擎 (Resolve Allowed Tool Name) -在 `internal/util/toolcalls_parse.go` 中,新增了一个由严到松降级匹配的强大漏斗策略函数 `resolveAllowedToolName`: - -1. **绝对匹配**:和以前一样,`read_file` == `read_file`。 -2. **忽略大小写**:`Read_File` 算作合法。 -3. **命名空间抹除**:通过寻找最后一个 `.` 来剥离前缀,强制将 `mcp.search_web` 还原出真实的 `search_web`。 -4. **终极正则清洗**: - 引入 `var toolNameLoosePattern = regexp.MustCompile(`[^a-z0-9]+`)`。 - 这个正则剥离了字符串里所有的符号、空格、格式符。 - 将传入的 `read-file` 洗除符号成为 `readfile`,并去和系统中所有合法工具同样清洗后的版本进行比较。只要核心字母一致,即算作匹配成功。 - -### 3. Role 归一化 (Normalize OpenAIRoleForPrompt) -在 `internal/adapter/openai/responses_input_items.go` 等处,引入了特定的 `normalizeOpenAIRoleForPrompt(role)` 清洗,保证输入和传递给上游的 Role 枚举始终受控,消除了因为意外的身份字段传参崩溃。 - ---- - -## 报告总结与 tool_sieve 的本质作用 - -PR #74 / #75 并没有从模型本身开刀,而是基于**网关应足够健壮**的设计哲学。 - -**其实整个增强实现,本质上实现了一个名为 `tool_sieve` (工具筛子) 的中间层网关。** -面对 DeepSeek 这种吐出一团混合了聊天文字与 JSON 面团的“不标准”数据流,`tool_sieve` 就像一个勤劳的高精度筛子,不仅人工揉开了面团: -1. 它把散文分拣出来,塞回标准结构的 `content` 字段去展示; -2. 剥离并清洗出有瑕疵的 JSON 块,按照 OpenAI 的标准格式小心翼翼地放进 `tool_calls` 结构里去等待执行。 - -这意味着,即便 AI 被配置了奇怪的回复设定、加粗了强调语言,甚至是犯了标点符号拼写小失误,**只要它输出了可以拼凑成工具指令的 JSON 核心单元,整个中继层就能将其挽救,并把正确的工具结果呈现给模型和用户**。 这不仅修复了缺陷,更极大地增强了工具网关的通用性和鲁棒性。 diff --git a/plans/refactor-baseline.md b/plans/refactor-baseline.md deleted file mode 100644 index 151683a..0000000 --- a/plans/refactor-baseline.md +++ /dev/null @@ -1,32 +0,0 @@ -# DS2API Refactor Baseline (Historical Snapshot) - -- Snapshot time: `2026-02-22T08:53:54Z` -- Snapshot branch: `dev` -- Snapshot HEAD: `5d3989a` -- Scope: backend + node api + webui large-file decoupling (no behavior change) - -## Gate Commands - -1. `./tests/scripts/run-unit-all.sh` - - Result: PASS - - Includes: - - `go test ./...` - - `node --test api/helpers/stream-tool-sieve.test.js api/chat-stream.test.js api/compat/js_compat_test.js` -2. `npm --prefix webui run build` - - Result: PASS -3. `./tests/scripts/check-refactor-line-gate.sh` - - Result: PASS (`checked=131 missing=0 over_limit=0`) -4. Stage gates (1-5) replay: - - `go test ./internal/config ./internal/admin ./internal/account ./internal/deepseek ./internal/format/openai` -> PASS - - `go test ./internal/adapter/openai ./internal/util ./internal/sse ./internal/compat` -> PASS - - `go test ./internal/adapter/claude ./internal/adapter/gemini ./internal/config` -> PASS - - `go test ./internal/testsuite ./cmd/ds2api-tests` -> PASS - - `node --test api/helpers/stream-tool-sieve.test.js api/chat-stream.test.js api/compat/js_compat_test.js` -> PASS -5. Final full regression: - - `go test ./... -count=1` -> PASS - -## Notes - -- This file records a historical baseline for refactor process tracking. -- It is not intended to represent the current repository HEAD. -- Frontend manual smoke for phase 6 still requires human execution and sign-off. diff --git a/plans/refactor-line-gate.md b/plans/refactor-line-gate.md deleted file mode 100644 index 103782d..0000000 --- a/plans/refactor-line-gate.md +++ /dev/null @@ -1,22 +0,0 @@ -# Refactor Line Gate - -## Rules - -1. Backend production files upper bound: `<= 300` lines. -2. Frontend (`webui/`) production files upper bound: `<= 500` lines. -3. Entry/facade files upper bound: `<= 120` lines. -4. Scope is limited to target files in `plans/refactor-line-gate-targets.txt`. -5. Test files are out of scope for this gate. - -## Command - -```bash -./tests/scripts/check-refactor-line-gate.sh -``` - -## Naming Note - -- Original split plan used `internal/admin/handler_accounts_test.go` for account probing logic. -- In Go, `*_test.go` files are test-only compilation units and cannot host production handlers. -- The production file is implemented as `internal/admin/handler_accounts_testing.go`. -