Merge pull request #107 from CJackHwang/codex/clean-up-unused-files-and-update-documentation

docs: prune stale files and refresh docs, add .env.example, align READMEs/DEPLOY/CONTRIBUTING
This commit is contained in:
CJACK.
2026-03-20 03:07:21 +08:00
committed by GitHub
10 changed files with 40 additions and 257 deletions

View File

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

View File

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

View File

@@ -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 构建产物(不提交)

View File

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

View File

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

View File

@@ -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
## 免责声明

View File

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

View File

@@ -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 核心单元,整个中继层就能将其挽救,并把正确的工具结果呈现给模型和用户**。 这不仅修复了缺陷,更极大地增强了工具网关的通用性和鲁棒性。

View File

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

View File

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