From b105d54c00eb2e13d8d31dda842535b06350dd70 Mon Sep 17 00:00:00 2001 From: CJACK Date: Mon, 6 Apr 2026 02:38:15 +0800 Subject: [PATCH] feat: add admin endpoints for capturing, querying, and persisting raw upstream samples and increase default capture limits --- API.en.md | 71 +++++++++++++++++++++++++++++++++++++++++ API.md | 71 +++++++++++++++++++++++++++++++++++++++++ README.MD | 16 +++++++--- README.en.md | 25 ++++++++++----- docs/CONTRIBUTING.en.md | 1 + docs/CONTRIBUTING.md | 1 + docs/DEPLOY.en.md | 2 +- docs/DEPLOY.md | 2 +- docs/TESTING.md | 15 +++++++++ 9 files changed, 190 insertions(+), 14 deletions(-) diff --git a/API.en.md b/API.en.md index efdc5b7..7276c86 100644 --- a/API.en.md +++ b/API.en.md @@ -138,6 +138,9 @@ Gemini-compatible clients can also send `x-goog-api-key`, `?key=`, or `?api_key= | POST | `/admin/accounts/sessions/delete-all` | Admin | Delete all sessions for one account | | POST | `/admin/import` | Admin | Batch import keys/accounts | | POST | `/admin/test` | Admin | Test API through service | +| POST | `/admin/dev/raw-samples/capture` | Admin | Fire one request and persist it as a raw sample | +| GET | `/admin/dev/raw-samples/query` | Admin | Search current in-memory capture chains by prompt keyword | +| POST | `/admin/dev/raw-samples/save` | Admin | Persist a selected in-memory capture chain as a raw sample | | POST | `/admin/vercel/sync` | Admin | Sync config to Vercel | | GET | `/admin/vercel/status` | Admin | Vercel sync status | | POST | `/admin/vercel/status` | Admin | Vercel sync status / draft compare | @@ -883,6 +886,74 @@ Test API availability through the service itself. } ``` +### `POST /admin/dev/raw-samples/capture` + +Internally issues one `/v1/chat/completions` request through the service, then persists the request metadata and raw upstream SSE into `tests/raw_stream_samples//`. + +Common request fields: + +| Field | Required | Default | Notes | +| --- | --- | --- | --- | +| `message` | No | `你好` | Convenience single-turn user message | +| `messages` | No | Auto-derived from `message` | OpenAI-style message array | +| `model` | No | `deepseek-chat` | Target model | +| `stream` | No | `true` | Recommended to keep streaming enabled so raw SSE is recorded | +| `api_key` | No | First configured key | Business API key to use | +| `sample_id` | No | Auto-generated | Sample directory name | + +On success, the response headers include: + +- `X-Ds2-Sample-Id` +- `X-Ds2-Sample-Dir` +- `X-Ds2-Sample-Meta` +- `X-Ds2-Sample-Upstream` + +If the request itself succeeds but the process did not record a new upstream capture, the endpoint returns: + +```json +{"detail":"no upstream capture was recorded"} +``` + +### `GET /admin/dev/raw-samples/query` + +Searches the current process's in-memory capture entries and groups `completion + continue` rounds by `chat_session_id`. + +**Query parameters**: + +| Param | Default | Notes | +| --- | --- | --- | +| `q` | empty | Fuzzy match against request/response text | +| `limit` | `20` | Max number of chains returned | + +**Response fields** include: + +- `items[].chain_key` +- `items[].capture_ids` +- `items[].round_count` +- `items[].initial_label` +- `items[].request_preview` +- `items[].response_preview` + +### `POST /admin/dev/raw-samples/save` + +Persists one selected in-memory capture chain into `tests/raw_stream_samples//`. + +Any one of these selectors is accepted: + +```json +{"chain_key":"session:xxxx","sample_id":"tmp-from-memory"} +``` + +```json +{"capture_id":"cap_xxx","sample_id":"tmp-from-memory"} +``` + +```json +{"query":"Guangzhou weather","sample_id":"tmp-from-memory"} +``` + +The success payload includes `sample_id`, `dir`, `meta_path`, and `upstream_path`. + ### `POST /admin/vercel/sync` | Field | Required | Notes | diff --git a/API.md b/API.md index dd6dcb0..8552052 100644 --- a/API.md +++ b/API.md @@ -138,6 +138,9 @@ Gemini 兼容客户端还可以使用 `x-goog-api-key`、`?key=` 或 `?api_key=` | POST | `/admin/accounts/sessions/delete-all` | Admin | 删除某账号的全部会话 | | POST | `/admin/import` | Admin | 批量导入 keys/accounts | | POST | `/admin/test` | Admin | 测试当前 API 可用性 | +| POST | `/admin/dev/raw-samples/capture` | Admin | 直接发起一次请求并保存为 raw sample | +| GET | `/admin/dev/raw-samples/query` | Admin | 按问题关键词查询当前内存抓包链 | +| POST | `/admin/dev/raw-samples/save` | Admin | 把命中的内存抓包链保存为 raw sample | | POST | `/admin/vercel/sync` | Admin | 同步配置到 Vercel | | GET | `/admin/vercel/status` | Admin | Vercel 同步状态 | | POST | `/admin/vercel/status` | Admin | Vercel 同步状态 / 草稿对比 | @@ -886,6 +889,74 @@ data: {"type":"message_stop"} } ``` +### `POST /admin/dev/raw-samples/capture` + +直接通过服务自身发起一次 `/v1/chat/completions` 请求,并把请求元信息和上游原始 SSE 保存到 `tests/raw_stream_samples//`。 + +常用请求字段: + +| 字段 | 必填 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `message` | 否 | `你好` | 便捷单轮用户消息 | +| `messages` | 否 | 自动由 `message` 生成 | OpenAI 风格消息数组 | +| `model` | 否 | `deepseek-chat` | 目标模型 | +| `stream` | 否 | `true` | 建议保留流式,以记录原始 SSE | +| `api_key` | 否 | 配置中第一个 key | 调用业务接口使用的 key | +| `sample_id` | 否 | 自动生成 | 样本目录名 | + +成功时会在响应头里附带: + +- `X-Ds2-Sample-Id` +- `X-Ds2-Sample-Dir` +- `X-Ds2-Sample-Meta` +- `X-Ds2-Sample-Upstream` + +如果请求本身成功,但当前进程没有记录到新的上游抓包,会返回: + +```json +{"detail":"no upstream capture was recorded"} +``` + +### `GET /admin/dev/raw-samples/query` + +按关键词查询当前进程内存里的抓包记录,并按 `chat_session_id` 归并 `completion + continue` 链。 + +**查询参数**: + +| 参数 | 默认值 | 说明 | +| --- | --- | --- | +| `q` | 空 | 按请求体/响应体关键词模糊匹配 | +| `limit` | `20` | 返回链条数上限 | + +**响应字段**包含: + +- `items[].chain_key` +- `items[].capture_ids` +- `items[].round_count` +- `items[].initial_label` +- `items[].request_preview` +- `items[].response_preview` + +### `POST /admin/dev/raw-samples/save` + +把当前内存中的某条抓包链落盘为 `tests/raw_stream_samples//`。 + +支持以下任一种选中方式: + +```json +{"chain_key":"session:xxxx","sample_id":"tmp-from-memory"} +``` + +```json +{"capture_id":"cap_xxx","sample_id":"tmp-from-memory"} +``` + +```json +{"query":"广州天气","sample_id":"tmp-from-memory"} +``` + +成功响应会返回 `sample_id`、`dir`、`meta_path`、`upstream_path`。 + ### `POST /admin/vercel/sync` | 字段 | 必填 | 说明 | diff --git a/README.MD b/README.MD index c689d0e..6fbde90 100644 --- a/README.MD +++ b/README.MD @@ -348,8 +348,8 @@ cp opencode.json.example opencode.json | `DS2API_STATIC_ADMIN_DIR` | 管理台静态文件目录 | `static/admin` | | `DS2API_AUTO_BUILD_WEBUI` | 启动时自动构建 WebUI | 本地开启,Vercel 关闭 | | `DS2API_DEV_PACKET_CAPTURE` | 本地开发抓包开关(记录最近会话请求/响应体) | 本地非 Vercel 默认开启 | -| `DS2API_DEV_PACKET_CAPTURE_LIMIT` | 本地抓包保留条数(超出自动淘汰) | `5` | -| `DS2API_DEV_PACKET_CAPTURE_MAX_BODY_BYTES` | 单条响应体最大记录字节数 | `2097152` | +| `DS2API_DEV_PACKET_CAPTURE_LIMIT` | 本地抓包保留条数(超出自动淘汰) | `20` | +| `DS2API_DEV_PACKET_CAPTURE_MAX_BODY_BYTES` | 单条响应体最大记录字节数 | `5242880` | | `DS2API_ACCOUNT_MAX_INFLIGHT` | 每账号最大并发 in-flight 请求数 | `2` | | `DS2API_ACCOUNT_MAX_QUEUE` | 等待队列上限 | `recommended_concurrency` | | `DS2API_GLOBAL_MAX_INFLIGHT` | 全局最大 in-flight 请求数 | `recommended_concurrency` | @@ -403,13 +403,13 @@ Gemini 路由还可以使用 `x-goog-api-key`,或在没有认证头时使用 ` ## 本地开发抓包工具 -用于定位「responses 思考流/工具调用」等问题。开启后会自动记录最近 N 条 DeepSeek 对话上游请求体与响应体(默认 5 条,超出自动淘汰)。 +用于定位「responses 思考流/工具调用」等问题。开启后会自动记录最近 N 条 DeepSeek 对话上游请求体与响应体(默认 20 条,超出自动淘汰;单条响应体默认最多记录 5 MB)。 启用示例: ```bash DS2API_DEV_PACKET_CAPTURE=true \ -DS2API_DEV_PACKET_CAPTURE_LIMIT=5 \ +DS2API_DEV_PACKET_CAPTURE_LIMIT=20 \ go run ./cmd/ds2api ``` @@ -417,6 +417,8 @@ go run ./cmd/ds2api - `GET /admin/dev/captures`:查看抓包列表(最新在前) - `DELETE /admin/dev/captures`:清空抓包 +- `GET /admin/dev/raw-samples/query?q=关键词&limit=20`:按问题关键词查询当前内存抓包,并按 `chat_session_id` 归并 `completion + continue` 链 +- `POST /admin/dev/raw-samples/save`:把命中的某条抓包链保存为 `tests/raw_stream_samples//` 回放样本 返回字段包含: @@ -424,6 +426,12 @@ go run ./cmd/ds2api - `response_body`:上游返回的原始流式内容拼接文本 - `response_truncated`:是否触发单条大小截断 +保存接口支持用 `query`、`chain_key` 或 `capture_id` 选中目标。例如: + +```json +{"query":"广州天气","sample_id":"gz-weather-from-memory"} +``` + ## 项目结构 ```text diff --git a/README.en.md b/README.en.md index 1eb4387..a944a9c 100644 --- a/README.en.md +++ b/README.en.md @@ -353,8 +353,8 @@ cp opencode.json.example opencode.json | `DS2API_VERCEL_INTERNAL_SECRET` | Vercel hybrid streaming internal auth | Falls back to `DS2API_ADMIN_KEY` | | `DS2API_VERCEL_STREAM_LEASE_TTL_SECONDS` | Stream lease TTL seconds | `900` | | `DS2API_DEV_PACKET_CAPTURE` | Local dev packet capture switch (record recent request/response bodies) | Enabled by default on non-Vercel local runtime | -| `DS2API_DEV_PACKET_CAPTURE_LIMIT` | Number of captured sessions to retain (auto-evict overflow) | `5` | -| `DS2API_DEV_PACKET_CAPTURE_MAX_BODY_BYTES` | Max recorded bytes per captured response body | `2097152` | +| `DS2API_DEV_PACKET_CAPTURE_LIMIT` | Number of captured sessions to retain (auto-evict overflow) | `20` | +| `DS2API_DEV_PACKET_CAPTURE_MAX_BODY_BYTES` | Max recorded bytes per captured response body | `5242880` | | `VERCEL_TOKEN` | Vercel sync token | — | | `VERCEL_PROJECT_ID` | Vercel project ID | — | | `VERCEL_TEAM_ID` | Vercel team ID | — | @@ -392,21 +392,22 @@ Queue limit = DS2API_ACCOUNT_MAX_QUEUE (default = recommended concurrency) When `tools` is present in the request, DS2API performs anti-leak handling: 1. Toolcall feature matching is enabled only in **non-code-block context** (fenced examples are ignored) - - In non-code-block context, tool JSON may still be recognized even when mixed with normal prose; surrounding prose can remain as text output. -2. `responses` streaming strictly uses official item lifecycle events (`response.output_item.*`, `response.content_part.*`, `response.function_call_arguments.*`) -3. Tool names not declared in the `tools` schema are strictly rejected and will not be emitted as valid tool calls +2. The parser prioritizes XML/Markup, while also accepting JSON / ANTML / invoke / text-kv, and normalizes everything into the internal tool-call structure +3. `responses` streaming strictly uses official item lifecycle events (`response.output_item.*`, `response.content_part.*`, `response.function_call_arguments.*`) 4. `responses` supports and enforces `tool_choice` (`auto`/`none`/`required`/forced function); `required` violations return `422` for non-stream and `response.failed` for stream -5. Valid tool call events are only emitted after passing policy validation, preventing invalid tool names from entering the client execution chain +5. The output protocol follows the client request (OpenAI / Claude / Gemini native shapes); model-side prompting can prefer XML, and the compatibility layer handles the protocol-specific translation + +> Note: the current parser still prioritizes “parse successfully whenever possible”; hard allow-list rejection for undeclared tool names is not enabled yet. ## Local Dev Packet Capture -This is for debugging issues such as Responses reasoning streaming and tool-call handoff. When enabled, DS2API stores the latest N DeepSeek conversation payload pairs (request body + upstream response body), defaulting to 5 entries with auto-eviction. +This is for debugging issues such as Responses reasoning streaming and tool-call handoff. When enabled, DS2API stores the latest N DeepSeek conversation payload pairs (request body + upstream response body), defaulting to 20 entries with auto-eviction; each response body is capped at 5 MB by default. Enable example: ```bash DS2API_DEV_PACKET_CAPTURE=true \ -DS2API_DEV_PACKET_CAPTURE_LIMIT=5 \ +DS2API_DEV_PACKET_CAPTURE_LIMIT=20 \ go run ./cmd/ds2api ``` @@ -414,6 +415,8 @@ Inspect/clear (Admin JWT required): - `GET /admin/dev/captures`: list captured items (newest first) - `DELETE /admin/dev/captures`: clear captured items +- `GET /admin/dev/raw-samples/query?q=keyword&limit=20`: search current in-memory captures by prompt keyword and group `completion + continue` by `chat_session_id` +- `POST /admin/dev/raw-samples/save`: persist a selected capture chain as `tests/raw_stream_samples//` Response fields include: @@ -421,6 +424,12 @@ Response fields include: - `response_body`: concatenated raw upstream stream body text - `response_truncated`: whether body-size truncation happened +The save endpoint can target a chain by `query`, `chain_key`, or `capture_id`. Example: + +```json +{"query":"Guangzhou weather","sample_id":"gz-weather-from-memory"} +``` + ## Project Structure ```text diff --git a/docs/CONTRIBUTING.en.md b/docs/CONTRIBUTING.en.md index 619ffc6..1af41b4 100644 --- a/docs/CONTRIBUTING.en.md +++ b/docs/CONTRIBUTING.en.md @@ -41,6 +41,7 @@ npm install # 3. Start dev server (hot reload) npm run dev # Default: http://localhost:5173, auto-proxies API to backend +# host: 0.0.0.0 is not configured, so LAN access is not enabled by default ``` WebUI tech stack: diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e6dd138..75c4337 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -41,6 +41,7 @@ npm install # 3. 启动开发服务器(热更新) npm run dev # 默认监听 http://localhost:5173,自动代理 API 到后端 +# 当前未配置 host: 0.0.0.0,因此默认不对局域网开放 ``` WebUI 技术栈: diff --git a/docs/DEPLOY.en.md b/docs/DEPLOY.en.md index d92780d..145b186 100644 --- a/docs/DEPLOY.en.md +++ b/docs/DEPLOY.en.md @@ -65,7 +65,7 @@ cp config.example.json config.json go run ./cmd/ds2api ``` -Default address: `http://0.0.0.0:5001` (override with `PORT`). +Default local access URL: `http://127.0.0.1:5001`; the server actually binds to `0.0.0.0:5001` (override with `PORT`). ### 1.2 WebUI Build diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 3d10821..598c210 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -65,7 +65,7 @@ cp config.example.json config.json go run ./cmd/ds2api ``` -默认监听 `http://0.0.0.0:5001`,可通过 `PORT` 环境变量覆盖。 +默认本地访问地址是 `http://127.0.0.1:5001`;服务实际绑定 `0.0.0.0:5001`,可通过 `PORT` 环境变量覆盖。 ### 1.2 WebUI 构建 diff --git a/docs/TESTING.md b/docs/TESTING.md index 6975de5..f56fb3a 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -260,6 +260,21 @@ POST /admin/dev/raw-samples/capture 这个接口会把请求元信息和上游原始流写入 `tests/raw_stream_samples//`,以后可以直接拿来做回放和字段分析。派生输出会在本地回放时再生成,不再落在样本目录里。 +### 从内存抓包查询并保存样本 + +如果问题刚刚在本地复现过,也可以先查当前进程内存里的抓包,再选择性落盘: + +```bash +GET /admin/dev/raw-samples/query?q=广州&limit=10 +POST /admin/dev/raw-samples/save +{"chain_key":"session:xxxx","sample_id":"tmp-from-memory"} +``` + +说明: +- `query` 会按 `chat_session_id` 把 `completion + continue` 归并成一条链,适合定位接续思考问题。 +- `save` 支持用 `query`、`chain_key` 或 `capture_id` 选中目标。 +- 生成的样本目录仍然是 `tests/raw_stream_samples//`,可以直接喂给回放脚本。 + ### 指定输出目录和超时 ```bash