Compare commits

..

13 Commits

Author SHA1 Message Date
CJACK.
b3eae22cef Merge pull request #111 from CJackHwang/dev
Merge pull request #110 from CJackHwang/codex/align-js-runtime-with-go-runtime-logic

Align Vercel JS stream tool-call delta handling with Go runtime
2026-03-20 10:05:25 +08:00
CJACK.
7af0098d1b Merge pull request #110 from CJackHwang/codex/align-js-runtime-with-go-runtime-logic
Align Vercel JS stream tool-call delta handling with Go runtime
2026-03-20 09:49:08 +08:00
CJACK.
17405be300 shrink vercel stream module under line gate limit 2026-03-20 09:47:22 +08:00
CJACK.
5bc03e5de6 align vercel js stream toolcall delta behavior with go runtime 2026-03-20 09:36:45 +08:00
CJACK.
5a5f93148d Merge pull request #109 from CJackHwang/dev
Merge pull request #108 from CJackHwang/codex/clean-up-unused-files-and-update-documentation-uiip50

docs: refresh deployment/testing guides and remove stale investigation report
2026-03-20 03:12:25 +08:00
CJACK.
32dc5b6099 Merge pull request #108 from CJackHwang/codex/clean-up-unused-files-and-update-documentation-uiip50
docs: refresh deployment/testing guides and remove stale investigation report
2026-03-20 03:08:09 +08:00
CJACK.
7936d4675f 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
2026-03-20 03:07:21 +08:00
CJACK.
808eafa7c6 docs: refresh deployment/testing guides and prune stale report 2026-03-20 03:05:36 +08:00
CJACK.
bcb8ed6df2 docs: prune stale docs and refresh project documentation 2026-03-20 03:05:22 +08:00
CJACK.
8ec5dcc0cc Merge pull request #106 from CJackHwang/dev
Merge pull request #105 from CJackHwang/codex/fix-issues-found-in-review

Merge pull request #104 from CJackHwang/codex/revert-to-commit-efb484b

Restore tool-call parsing and repair logic; remove accidental split files
2026-03-20 02:53:30 +08:00
CJACK.
88a79f212d Fix path control-char repair on JSON fallback parses 2026-03-20 02:52:27 +08:00
CJACK.
41c0f7ce28 Merge pull request #102 from CJackHwang/dev
Merge pull request #99 from CJackHwang/codex/refactor-toolcalls_parse.go-for-line-limits

Codex-generated pull request
2026-03-20 01:18:05 +08:00
CJACK.
43cbc4aac0 Merge pull request #97 from CJackHwang/dev
Merge pull request #96 from CJackHwang/codex/update-ci-line-count-limits-cihke3

ci: ignore test files in line gate and raise frontend limit to 500
2026-03-18 00:15:03 +08:00
13 changed files with 72 additions and 300 deletions

View File

@@ -1,93 +1,15 @@
# DS2API environment template (Go runtime) # DS2API runtime
# Copy this file to .env and adjust values.
# Updated: 2026-02
# ---------------------------------------------------------------
# Runtime
# ---------------------------------------------------------------
# HTTP listen port (default: 5001)
PORT=5001 PORT=5001
# Log level: DEBUG | INFO | WARN | ERROR
LOG_LEVEL=INFO LOG_LEVEL=INFO
# Max concurrent inflight requests per account in managed-key mode. # Admin authentication
# Default: 2 DS2API_ADMIN_KEY=change-me
# 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
# Optional waiting queue size override for managed-key mode. # Config loading (choose one)
# Default: recommended_concurrency (same as account_count * inflight_limit) # 1) file-based config
# Alias: DS2API_ACCOUNT_QUEUE_SIZE DS2API_CONFIG_PATH=/app/config.json
# DS2API_ACCOUNT_MAX_QUEUE=10 # 2) inline JSON or Base64 JSON
# DS2API_CONFIG_JSON=
# --------------------------------------------------------------- # Optional: static admin assets path
# Admin auth # DS2API_STATIC_ADMIN_DIR=/app/static/admin
# ---------------------------------------------------------------
# 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

View File

@@ -99,7 +99,7 @@ ds2api/
├── api/ ├── api/
│ ├── index.go # Vercel Serverless Go entry │ ├── index.go # Vercel Serverless Go entry
│ ├── chat-stream.js # Vercel Node.js stream relay │ ├── chat-stream.js # Vercel Node.js stream relay
│ └── helpers/ # Node.js helper modules │ └── (rewrite targets in vercel.json)
├── internal/ ├── internal/
│ ├── account/ # Account pool and concurrency queue │ ├── account/ # Account pool and concurrency queue
│ ├── adapter/ │ ├── adapter/
@@ -112,6 +112,7 @@ ds2api/
│ ├── compat/ # Compatibility helpers │ ├── compat/ # Compatibility helpers
│ ├── config/ # Config loading and hot-reload │ ├── config/ # Config loading and hot-reload
│ ├── deepseek/ # DeepSeek client, PoW WASM │ ├── deepseek/ # DeepSeek client, PoW WASM
│ ├── js/ # Node runtime stream/compat logic
│ ├── devcapture/ # Dev packet capture │ ├── devcapture/ # Dev packet capture
│ ├── format/ # Output formatting │ ├── format/ # Output formatting
│ ├── prompt/ # Prompt building │ ├── prompt/ # Prompt building
@@ -123,7 +124,9 @@ ds2api/
│ └── webui/ # WebUI static hosting │ └── webui/ # WebUI static hosting
├── webui/ # React WebUI source ├── webui/ # React WebUI source
│ └── src/ │ └── src/
│ ├── components/ # Components │ ├── app/ # Routing, auth, config state
│ ├── features/ # Feature modules
│ ├── components/ # Shared components
│ └── locales/ # Language packs │ └── locales/ # Language packs
├── scripts/ # Build and test scripts ├── scripts/ # Build and test scripts
├── static/admin/ # WebUI build output (not committed) ├── static/admin/ # WebUI build output (not committed)

View File

@@ -99,7 +99,7 @@ ds2api/
├── api/ ├── api/
│ ├── index.go # Vercel Serverless Go 入口 │ ├── index.go # Vercel Serverless Go 入口
│ ├── chat-stream.js # Vercel Node.js 流式转发 │ ├── chat-stream.js # Vercel Node.js 流式转发
│ └── helpers/ # Node.js 辅助模块 │ └── (rewrite targets in vercel.json)
├── internal/ ├── internal/
│ ├── account/ # 账号池与并发队列 │ ├── account/ # 账号池与并发队列
│ ├── adapter/ │ ├── adapter/
@@ -112,6 +112,7 @@ ds2api/
│ ├── compat/ # 兼容性辅助 │ ├── compat/ # 兼容性辅助
│ ├── config/ # 配置加载与热更新 │ ├── config/ # 配置加载与热更新
│ ├── deepseek/ # DeepSeek 客户端、PoW WASM │ ├── deepseek/ # DeepSeek 客户端、PoW WASM
│ ├── js/ # Node 运行时流式/兼容逻辑
│ ├── devcapture/ # 开发抓包 │ ├── devcapture/ # 开发抓包
│ ├── format/ # 输出格式化 │ ├── format/ # 输出格式化
│ ├── prompt/ # Prompt 构建 │ ├── prompt/ # Prompt 构建
@@ -123,7 +124,9 @@ ds2api/
│ └── webui/ # WebUI 静态托管 │ └── webui/ # WebUI 静态托管
├── webui/ # React WebUI 源码 ├── webui/ # React WebUI 源码
│ └── src/ │ └── src/
│ ├── components/ # 组件 │ ├── app/ # 路由、鉴权、配置状态
│ ├── features/ # 业务功能模块
│ ├── components/ # 通用组件
│ └── locales/ # 语言包 │ └── locales/ # 语言包
├── scripts/ # 构建与测试脚本 ├── scripts/ # 构建与测试脚本
├── static/admin/ # WebUI 构建产物(不提交) ├── static/admin/ # WebUI 构建产物(不提交)

View File

@@ -113,12 +113,8 @@ go build -o ds2api ./cmd/ds2api
# Copy env template # Copy env template
cp .env.example .env cp .env.example .env
# Generate single-line Base64 from config.json # Edit .env and set at least:
DS2API_CONFIG_JSON="$(base64 < config.json | tr -d '\n')"
# Edit .env and set:
# DS2API_ADMIN_KEY=your-admin-key # DS2API_ADMIN_KEY=your-admin-key
# DS2API_CONFIG_JSON=${DS2API_CONFIG_JSON}
# Start # Start
docker-compose up -d docker-compose up -d
@@ -366,7 +362,7 @@ Each archive includes:
- `ds2api` executable (`ds2api.exe` on Windows) - `ds2api` executable (`ds2api.exe` on Windows)
- `static/admin/` (built WebUI assets) - `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` - `config.example.json`, `.env.example`
- `README.MD`, `README.en.md`, `LICENSE` - `README.MD`, `README.en.md`, `LICENSE`
@@ -455,7 +451,9 @@ server {
```bash ```bash
# Copy compiled binary and related files to target directory # Copy compiled binary and related files to target directory
sudo mkdir -p /opt/ds2api 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 sudo cp -r static/admin /opt/ds2api/static/admin
``` ```

View File

@@ -113,12 +113,8 @@ go build -o ds2api ./cmd/ds2api
# 复制环境变量模板 # 复制环境变量模板
cp .env.example .env cp .env.example .env
# 从 config.json 生成单行 Base64 # 编辑 .env请改成你的强密码至少设置
DS2API_CONFIG_JSON="$(base64 < config.json | tr -d '\n')"
# 编辑 .env请改成你的强密码设置
# DS2API_ADMIN_KEY=your-admin-key # DS2API_ADMIN_KEY=your-admin-key
# DS2API_CONFIG_JSON=${DS2API_CONFIG_JSON}
# 启动 # 启动
docker-compose up -d docker-compose up -d
@@ -366,7 +362,7 @@ No Output Directory named "public" found after the Build completed.
- `ds2api` 可执行文件Windows 为 `ds2api.exe` - `ds2api` 可执行文件Windows 为 `ds2api.exe`
- `static/admin/`WebUI 构建产物) - `static/admin/`WebUI 构建产物)
- `sha3_wasm_bg.7b9ca65ddd.wasm` - `sha3_wasm_bg.7b9ca65ddd.wasm`(可选;程序内置 embed fallback
- `config.example.json``.env.example` - `config.example.json``.env.example`
- `README.MD``README.en.md``LICENSE` - `README.MD``README.en.md``LICENSE`
@@ -455,7 +451,9 @@ server {
```bash ```bash
# 将编译好的二进制文件和相关文件复制到目标目录 # 将编译好的二进制文件和相关文件复制到目标目录
sudo mkdir -p /opt/ds2api 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 sudo cp -r static/admin /opt/ds2api/static/admin
``` ```

View File

@@ -160,17 +160,13 @@ go run ./cmd/ds2api
# 1. 准备环境变量文件 # 1. 准备环境变量文件
cp .env.example .env cp .env.example .env
# 2. 从 config.json 生成 DS2API_CONFIG_JSON单行 Base64 # 2. 编辑 .env至少设置 DS2API_ADMIN_KEY
DS2API_CONFIG_JSON="$(base64 < config.json | tr -d '\n')"
# 3. 编辑 .env设置
# DS2API_ADMIN_KEY=请替换为强密码 # DS2API_ADMIN_KEY=请替换为强密码
# DS2API_CONFIG_JSON=${DS2API_CONFIG_JSON}
# 4. 启动 # 3. 启动
docker-compose up -d docker-compose up -d
# 5. 查看日志 # 4. 查看日志
docker-compose logs -f docker-compose logs -f
``` ```
@@ -397,7 +393,7 @@ ds2api/
├── api/ ├── api/
│ ├── index.go # Vercel Serverless Go 入口 │ ├── index.go # Vercel Serverless Go 入口
│ ├── chat-stream.js # Vercel Node.js 流式转发 │ ├── chat-stream.js # Vercel Node.js 流式转发
│ └── helpers/ # Node.js 辅助模块 │ └── (rewrite targets in vercel.json)
├── internal/ ├── internal/
│ ├── account/ # 账号池与并发队列 │ ├── account/ # 账号池与并发队列
│ ├── adapter/ │ ├── adapter/
@@ -410,6 +406,7 @@ ds2api/
│ ├── compat/ # 兼容性辅助 │ ├── compat/ # 兼容性辅助
│ ├── config/ # 配置加载与热更新 │ ├── config/ # 配置加载与热更新
│ ├── deepseek/ # DeepSeek API 客户端、PoW WASM │ ├── deepseek/ # DeepSeek API 客户端、PoW WASM
│ ├── js/ # Node 运行时流式处理与兼容逻辑
│ ├── devcapture/ # 开发抓包模块 │ ├── devcapture/ # 开发抓包模块
│ ├── format/ # 输出格式化 │ ├── format/ # 输出格式化
│ ├── prompt/ # Prompt 构建 │ ├── prompt/ # Prompt 构建
@@ -420,7 +417,9 @@ ds2api/
│ └── webui/ # WebUI 静态文件托管与自动构建 │ └── webui/ # WebUI 静态文件托管与自动构建
├── webui/ # React WebUI 源码Vite + Tailwind ├── webui/ # React WebUI 源码Vite + Tailwind
│ └── src/ │ └── src/
│ ├── components/ # AccountManager / ApiTester / BatchImport / VercelSync / Login / LandingPage │ ├── app/ # 路由、鉴权、配置状态管理
│ ├── features/ # 业务功能模块account/settings/vercel/apiTester
│ ├── components/ # 登录/落地页等通用组件
│ └── locales/ # 中英文语言包zh.json / en.json │ └── locales/ # 中英文语言包zh.json / en.json
├── scripts/ ├── scripts/
│ └── build-webui.sh # WebUI 手动构建脚本 │ └── build-webui.sh # WebUI 手动构建脚本
@@ -500,7 +499,7 @@ go test -v -run 'TestParseToolCalls|TestRepair' ./internal/util/
- **触发条件**:仅在 GitHub Release `published` 时触发(普通 push 不会触发) - **触发条件**:仅在 GitHub Release `published` 时触发(普通 push 不会触发)
- **构建产物**:多平台二进制包(`linux/amd64`、`linux/arm64`、`darwin/amd64`、`darwin/arm64`、`windows/amd64`+ `sha256sums.txt` - **构建产物**:多平台二进制包(`linux/amd64`、`linux/arm64`、`darwin/amd64`、`darwin/arm64`、`windows/amd64`+ `sha256sums.txt`
- **容器镜像发布**:仅推送到 GHCR`ghcr.io/cjackhwang/ds2api` - **容器镜像发布**:仅推送到 GHCR`ghcr.io/cjackhwang/ds2api`
- **每个压缩包包含**`ds2api` 可执行文件、`static/admin`、WASM 文件、配置示例、README、LICENSE - **每个压缩包包含**`ds2api` 可执行文件、`static/admin`、WASM 文件(同时支持内置 fallback、配置示例、README、LICENSE
## 免责声明 ## 免责声明

View File

@@ -160,17 +160,13 @@ Default URL: `http://localhost:5001`
# 1. Prepare env file # 1. Prepare env file
cp .env.example .env cp .env.example .env
# 2. Generate DS2API_CONFIG_JSON from config.json (single-line Base64) # 2. Edit .env (at least set DS2API_ADMIN_KEY)
DS2API_CONFIG_JSON="$(base64 < config.json | tr -d '\n')"
# 3. Edit .env and set:
# DS2API_ADMIN_KEY=replace-with-a-strong-secret # DS2API_ADMIN_KEY=replace-with-a-strong-secret
# DS2API_CONFIG_JSON=${DS2API_CONFIG_JSON}
# 4. Start # 3. Start
docker-compose up -d docker-compose up -d
# 5. View logs # 4. View logs
docker-compose logs -f docker-compose logs -f
``` ```
@@ -398,7 +394,7 @@ ds2api/
├── api/ ├── api/
│ ├── index.go # Vercel Serverless Go entry │ ├── index.go # Vercel Serverless Go entry
│ ├── chat-stream.js # Vercel Node.js stream relay │ ├── chat-stream.js # Vercel Node.js stream relay
│ └── helpers/ # Node.js helper modules │ └── (rewrite targets in vercel.json)
├── internal/ ├── internal/
│ ├── account/ # Account pool and concurrency queue │ ├── account/ # Account pool and concurrency queue
│ ├── adapter/ │ ├── adapter/
@@ -411,6 +407,7 @@ ds2api/
│ ├── compat/ # Compatibility helpers │ ├── compat/ # Compatibility helpers
│ ├── config/ # Config loading and hot-reload │ ├── config/ # Config loading and hot-reload
│ ├── deepseek/ # DeepSeek API client, PoW WASM │ ├── deepseek/ # DeepSeek API client, PoW WASM
│ ├── js/ # Node runtime stream/compat logic
│ ├── devcapture/ # Dev packet capture module │ ├── devcapture/ # Dev packet capture module
│ ├── format/ # Output formatting │ ├── format/ # Output formatting
│ ├── prompt/ # Prompt construction │ ├── prompt/ # Prompt construction
@@ -421,7 +418,9 @@ ds2api/
│ └── webui/ # WebUI static file serving and auto-build │ └── webui/ # WebUI static file serving and auto-build
├── webui/ # React WebUI source (Vite + Tailwind) ├── webui/ # React WebUI source (Vite + Tailwind)
│ └── src/ │ └── 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) │ └── locales/ # Language packs (zh.json / en.json)
├── scripts/ ├── scripts/
│ └── build-webui.sh # Manual WebUI build script │ └── build-webui.sh # Manual WebUI build script
@@ -484,7 +483,7 @@ Workflow: `.github/workflows/release-artifacts.yml`
- **Trigger**: only on GitHub Release `published` (normal pushes do not trigger builds) - **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` - **Outputs**: multi-platform archives (`linux/amd64`, `linux/arm64`, `darwin/amd64`, `darwin/arm64`, `windows/amd64`) + `sha256sums.txt`
- **Container publishing**: GHCR only (`ghcr.io/cjackhwang/ds2api`) - **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 ## Disclaimer

View File

@@ -51,7 +51,7 @@ DS2API 提供两个层级的测试:
1. **Preflight 检查** 1. **Preflight 检查**
- `go test ./... -count=1`(单元测试) - `go test ./... -count=1`(单元测试)
- `./tests/scripts/check-node-split-syntax.sh`Node 拆分模块语法门禁) - `./tests/scripts/check-node-split-syntax.sh`Node 拆分模块语法门禁)
- `node --test`(如仓库存在 Node 单测文件时执行;当前默认以 Go 测试 + Node 语法门禁为主) - `node --test tests/node/stream-tool-sieve.test.js tests/node/chat-stream.test.js tests/node/js_compat_test.js`
- `npm run build --prefix webui`WebUI 构建检查) - `npm run build --prefix webui`WebUI 构建检查)
2. **隔离启动**:复制 `config.json` 到临时目录,启动独立服务进程 2. **隔离启动**:复制 `config.json` 到临时目录,启动独立服务进程

View File

@@ -1,33 +1,22 @@
'use strict'; 'use strict';
const { const {
extractToolNames,
createToolSieveState, createToolSieveState,
processToolSieveChunk, processToolSieveChunk,
flushToolSieve, flushToolSieve,
parseStandaloneToolCalls, parseStandaloneToolCalls,
formatOpenAIStreamToolCalls, formatOpenAIStreamToolCalls,
} = require('../helpers/stream-tool-sieve'); } = require('../helpers/stream-tool-sieve');
const { const { BASE_HEADERS } = require('../shared/deepseek-constants');
BASE_HEADERS, const { writeOpenAIError } = require('./error_shape');
} = require('../shared/deepseek-constants'); const { parseChunkForContent, isCitation } = require('./sse_parse');
const { buildUsage } = require('./token_usage');
const {
writeOpenAIError,
} = require('./error_shape');
const {
parseChunkForContent,
isCitation,
} = require('./sse_parse');
const {
buildUsage,
} = require('./token_usage');
const { const {
resolveToolcallPolicy, resolveToolcallPolicy,
formatIncrementalToolCallDeltas,
filterIncrementalToolCallDeltasByAllowed,
} = require('./toolcall_policy'); } = require('./toolcall_policy');
const { const { createChatCompletionEmitter } = require('./stream_emitter');
createChatCompletionEmitter,
} = require('./stream_emitter');
const { const {
asString, asString,
isAbortError, isAbortError,
@@ -57,6 +46,7 @@ async function handleVercelStream(req, res, rawBody, payload) {
const searchEnabled = toBool(prep.body.search_enabled); const searchEnabled = toBool(prep.body.search_enabled);
const toolPolicy = resolveToolcallPolicy(prep.body, payload.tools); const toolPolicy = resolveToolcallPolicy(prep.body, payload.tools);
const toolNames = toolPolicy.toolNames; const toolNames = toolPolicy.toolNames;
const emitEarlyToolDeltas = toolPolicy.emitEarlyToolDeltas;
if (!model || !leaseID || !deepseekToken || !powHeader || !completionPayload) { if (!model || !leaseID || !deepseekToken || !powHeader || !completionPayload) {
writeOpenAIError(res, 500, 'invalid vercel prepare response'); writeOpenAIError(res, 500, 'invalid vercel prepare response');
@@ -132,6 +122,7 @@ async function handleVercelStream(req, res, rawBody, payload) {
const toolSieveState = createToolSieveState(); const toolSieveState = createToolSieveState();
let toolCallsEmitted = false; let toolCallsEmitted = false;
const streamToolCallIDs = new Map(); const streamToolCallIDs = new Map();
const streamToolNames = new Map();
const decoder = new TextDecoder(); const decoder = new TextDecoder();
reader = completionRes.body.getReader(); reader = completionRes.body.getReader();
let buffered = ''; let buffered = '';
@@ -255,6 +246,18 @@ async function handleVercelStream(req, res, rawBody, payload) {
} }
const events = processToolSieveChunk(toolSieveState, p.text, toolNames); const events = processToolSieveChunk(toolSieveState, p.text, toolNames);
for (const evt of events) { for (const evt of events) {
if (evt.type === 'tool_call_deltas') {
if (!emitEarlyToolDeltas) {
continue;
}
const filtered = filterIncrementalToolCallDeltasByAllowed(evt.deltas, toolNames, streamToolNames);
const formatted = formatIncrementalToolCallDeltas(filtered, streamToolCallIDs);
if (formatted.length > 0) {
toolCallsEmitted = true;
sendDeltaFrame({ tool_calls: formatted });
}
continue;
}
if (evt.type === 'tool_calls') { if (evt.type === 'tool_calls') {
toolCallsEmitted = true; toolCallsEmitted = true;
sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(evt.calls, streamToolCallIDs) }); sendDeltaFrame({ tool_calls: formatOpenAIStreamToolCalls(evt.calls, streamToolCallIDs) });

View File

@@ -26,6 +26,7 @@ func parseToolCallInput(v any) map[string]any {
repaired := repairInvalidJSONBackslashes(raw) repaired := repairInvalidJSONBackslashes(raw)
if repaired != raw { if repaired != raw {
if err := json.Unmarshal([]byte(repaired), &parsed); err == nil && parsed != nil { if err := json.Unmarshal([]byte(repaired), &parsed); err == nil && parsed != nil {
repairPathLikeControlChars(parsed)
return parsed return parsed
} }
} }
@@ -33,6 +34,7 @@ func parseToolCallInput(v any) map[string]any {
repairedLoose := RepairLooseJSON(raw) repairedLoose := RepairLooseJSON(raw)
if repairedLoose != raw { if repairedLoose != raw {
if err := json.Unmarshal([]byte(repairedLoose), &parsed); err == nil && parsed != nil { if err := json.Unmarshal([]byte(repairedLoose), &parsed); err == nil && parsed != nil {
repairPathLikeControlChars(parsed)
return parsed return parsed
} }
} }

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