221 lines
10 KiB
Markdown
221 lines
10 KiB
Markdown
# Command Code OpenAI Bridge
|
||
|
||
这是一个只绑定本机地址的 OpenAI Chat Completions 兼容转发服务。它为每个 API 请求启动一次独立的 Command Code headless agent,将完整消息历史通过 stdin 发送给 CLI,在当前终端显示运行过程,只把官方结构化结果中的 `finalText` 返回给客户端。
|
||
|
||
默认地址:`http://127.0.0.1:18000/v1`
|
||
|
||
## 当前机器上的调查结论
|
||
|
||
- 系统:macOS 26.4.1 arm64。
|
||
- Node.js:v22.22.2,符合 Command Code 的 Node.js 22+ 要求。
|
||
- 安装方式:`npm install --global command-code@latest`。
|
||
- 已安装 Command Code:v1.10.0,npm `latest` 也是 v1.10.0。
|
||
- 可执行文件:`command-code`;macOS/Linux 短名为 `cmd`,Windows 短名为 `cmdc`。
|
||
- 登录:`command-code login`;检查:`command-code status --json`;本机已登录。
|
||
- npm 包没有 `exports` 字段,`main` 指向会直接启动 CLI 的 `dist/cli.mjs`,包尾直接解析命令行,没有公开、稳定的可嵌入 API。
|
||
- 官方 headless 调用:`command-code -p --output-format json`。不传查询参数时会从 stdin 读取。
|
||
- JSON 输出是 NDJSON:运行中输出 `AgentEvent`,正常情况下最后输出唯一的 `result` 行;最终回答位于 `finalText`,usage 和耗时也在该行。
|
||
- `--no-session` 让每次请求只使用内存会话,不写入或恢复跨请求 session。
|
||
- 官方 headless 模式明确不提供键盘、问题回答或权限批准等交互。原始 TTY/TUI 与可靠的独立 `finalText` 目前不能同时获得。
|
||
- 官方退出码:0 成功;1 常规错误;3 未登录;4 权限拒绝;5 限流;6 网络错误;7 服务端错误;8 达到 turn 上限;9 无回复;10 余额不足;130 被信号中断。
|
||
|
||
官方资料:
|
||
|
||
- [Quickstart](https://commandcode.ai/docs/quickstart)
|
||
- [CLI Reference](https://commandcode.ai/docs/reference/cli)
|
||
- [Headless Mode](https://commandcode.ai/docs/headless)
|
||
- [Permissions](https://commandcode.ai/docs/core-concepts/permissions)
|
||
|
||
## 为什么使用普通子进程与结构化事件
|
||
|
||
本项目使用官方 headless 子进程和 NDJSON,不使用 PTY,也不解析 TUI 文本。
|
||
|
||
原因是 v1.10.0 没有公开嵌入 API,交互式 TUI 没有独立的结构化最终结果通道。headless 的最后一行提供稳定 `finalText`,能保证工具参数、工具结果、状态、stderr、思考事件和 ANSI 内容不会混入 API 回复。
|
||
|
||
终端会显示:请求开始、模型、turn 状态、工具事件、工具参数、工具结果、stderr、最终回答和耗时。终端不会显示原始 Ink TUI、动画、键盘快捷键、人工权限批准、人工问题回答和详细内部思考文本。
|
||
|
||
本机 v1.10.0 实测表明,headless 即使设为 `auto-accept`,shell 仍会被拒绝。默认 `dangerously_skip_permissions: true`,因此实际传入 `--yolo`,让文件写入和命令工具能继续执行。它会绕过所有权限确认,只应对可信工作目录使用。若要只读,把 `dangerously_skip_permissions` 改为 `false`,同时把 `permission_mode` 改成 `plan`。
|
||
|
||
## 项目结构
|
||
|
||
```text
|
||
command-code-openai-bridge/
|
||
├── config.yaml
|
||
├── config.example.yaml
|
||
├── package.json
|
||
├── tsconfig.json
|
||
├── src/
|
||
│ ├── command-code.ts
|
||
│ ├── config.ts
|
||
│ ├── index.ts
|
||
│ ├── openai.ts
|
||
│ ├── renderer.ts
|
||
│ └── server.ts
|
||
├── scripts/
|
||
│ ├── install.sh
|
||
│ └── start.sh
|
||
└── examples/
|
||
├── node_client.mjs
|
||
└── python_client.py
|
||
```
|
||
|
||
## 安装
|
||
|
||
```bash
|
||
cd /Users/zen/Documents/Codex/2026-08-04/files-mentioned-by-the-user-command/outputs/command-code-openai-bridge
|
||
./scripts/install.sh
|
||
```
|
||
|
||
脚本会检查 Node.js 版本、按需安装 Command Code、安装项目依赖并构建。
|
||
|
||
如未登录:
|
||
|
||
```bash
|
||
command-code login
|
||
command-code status --json
|
||
```
|
||
|
||
`command-code login` 会打开浏览器,也可以粘贴从 Command Code Studio 创建的 API key。
|
||
|
||
## 配置
|
||
|
||
编辑 `config.yaml`:
|
||
|
||
```yaml
|
||
host: 127.0.0.1
|
||
port: 18000
|
||
command_code_executable: command-code
|
||
command_code_working_directory: .
|
||
timeout_seconds: 1800
|
||
max_request_bytes: 20971520
|
||
max_turns: 100
|
||
permission_mode: auto-accept
|
||
dangerously_skip_permissions: true
|
||
|
||
models:
|
||
command-default:
|
||
cli_model: deepseek/deepseek-v4-flash
|
||
effort: max
|
||
```
|
||
|
||
`command_code_working_directory` 决定 Command Code 能看到和操作的项目目录。相对路径以 `config.yaml` 所在目录为基准。`models.<name>.effort` 会传给 Command Code 的 `--effort`,可用值取决于对应模型。服务强制只监听 `127.0.0.1`。客户端只能选择配置中的模型名,不能注入额外 CLI 参数。
|
||
|
||
## 启动、停止与重启
|
||
|
||
```bash
|
||
./scripts/start.sh
|
||
```
|
||
|
||
`scripts/start.sh` 只运行已经生成的 `dist/index.js`,不会自动编译 TypeScript。修改 `src/` 中的源码后,必须先重新编译,再重启服务,新代码才会生效:
|
||
|
||
```bash
|
||
npm run build
|
||
./scripts/start.sh
|
||
```
|
||
|
||
首次执行 `scripts/install.sh` 时会自动安装依赖并完成一次编译。
|
||
|
||
空闲时按 Ctrl+C 停止。请求运行时第一次 Ctrl+C 取消当前 Command Code 子进程组并保留服务;请求结束后再按 Ctrl+C 停止服务。重启就是再次执行启动脚本。
|
||
|
||
客户端断开连接也会先给整个 Command Code 子进程组发送 SIGINT,随后按需升级为 SIGTERM 和 SIGKILL。总超时由 `timeout_seconds` 控制。
|
||
|
||
## API
|
||
|
||
- `GET /health`
|
||
- `GET /v1/models`
|
||
- `POST /v1/chat/completions`
|
||
|
||
Bearer Token 会被忽略。只接受文本消息。支持字符串 `content`,也支持由 `{ "type": "text", "text": "..." }` 组成的数组。图片、音频和其他 part 会返回 400。
|
||
|
||
`stream` 省略或设为 `false` 时返回普通 JSON。`stream: true` 时,服务仍会等待 Command Code 完整执行结束,再将最终文本一次性封装为 Chat Completions SSE 事件返回;它不提供实时生成过程。传入 `stream_options.include_usage: true` 时,结束前还会返回 usage 块。
|
||
|
||
### curl
|
||
|
||
```bash
|
||
curl -sS http://127.0.0.1:18000/v1/chat/completions \
|
||
-H 'Content-Type: application/json' \
|
||
-H 'Authorization: Bearer ignored' \
|
||
-d '{
|
||
"model": "command-default",
|
||
"messages": [
|
||
{"role": "user", "content": "用一句话解释递归。"}
|
||
],
|
||
"stream": false
|
||
}'
|
||
```
|
||
|
||
### OpenAI Python SDK
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
.venv/bin/pip install openai
|
||
.venv/bin/python examples/python_client.py
|
||
```
|
||
|
||
### OpenAI Node.js SDK
|
||
|
||
项目依赖已包含 `openai`:
|
||
|
||
```bash
|
||
node examples/node_client.mjs
|
||
```
|
||
|
||
## 实际 Command Code 命令
|
||
|
||
每次请求实际执行以下固定参数,完整提示词不进入命令行:
|
||
|
||
```bash
|
||
command-code \
|
||
-p \
|
||
--output-format json \
|
||
--no-session \
|
||
--skip-onboarding \
|
||
--no-auto-update \
|
||
--trust \
|
||
--max-turns 100 \
|
||
--model deepseek/deepseek-v4-flash \
|
||
--effort max \
|
||
--yolo
|
||
```
|
||
|
||
随后通过子进程 stdin 写入 UTF-8 的完整请求历史。消息序列被放进一个 JSON 对象,角色、顺序、空消息、Unicode、Markdown、代码块和自定义分隔符均不会被简单文本分隔符破坏。服务不总结、不删除、不截断消息,也不保存会话。
|
||
|
||
## 单并发和错误
|
||
|
||
同一时间只允许一个 Command Code 实例。第二个请求直接返回 HTTP 429 和 `code: busy`。请求体超过 `max_request_bytes` 返回 413。未知模型和非文本内容返回 4xx。CLI 的详细错误留在服务终端,客户端只收到简洁的 OpenAI 格式错误。
|
||
|
||
一次 CLI 异常不会结束 HTTP 服务,后续请求仍可继续。
|
||
|
||
## 本机实际检查结果
|
||
|
||
检查日期:2026-08-04。没有编写测试用例,以下均为构建后运行真实 CLI 和真实 HTTP 客户端得到的端到端结果。
|
||
|
||
- TypeScript 严格类型检查和生产构建通过。
|
||
- npm 生产依赖审计:0 个已知漏洞。
|
||
- `/health` 返回 Command Code v1.10.0、已登录、空闲。
|
||
- `/v1/models` 返回两个本地映射。
|
||
- curl 中文请求返回 HTTP 200,客户端只收到 `中文接口成功` 和标准 completion 字段。
|
||
- 字符串 content 与 text part 数组均通过;非文本 part 返回 HTTP 400。
|
||
- system、user、assistant、user 完整历史检查返回了历史 assistant 中的 `蓝鲸-42`。
|
||
- 约 96 KiB 的中文消息通过 stdin 完整传入并返回 `长文本回退成功`,没有经过命令行参数。
|
||
- read_file 工具调用、参数和文件结果显示在服务终端,API 只返回 package name。
|
||
- `--yolo` 下 shell_command 实际执行并返回 `shell-tool-ok`;`auto-accept` 下该工具确实被 headless 权限引擎拒绝。
|
||
- OpenAI Node.js SDK 示例通过。
|
||
- OpenAI Python SDK 示例通过。
|
||
- `stream: false` 返回普通 JSON;`stream: true` 在完整结果生成后一次性返回兼容 SSE。
|
||
- 并发检查中第二个请求返回 HTTP 429 和 `code: busy`,没有启动第二个 CLI。
|
||
- 超过 20 MiB 的请求体返回 HTTP 413,服务保持可用。
|
||
- 客户端 1 秒超时断开后,当前子进程组被清理,`busy` 恢复为 false,没有发现残留 headless CLI。
|
||
- 运行中按 Ctrl+C 后客户端收到 HTTP 499,服务保持运行;紧接着的真实请求返回 `中断后恢复成功`。
|
||
- 原始交互模式的本机 TTY 探测确认 Ink TUI、ANSI、输入框和双 Ctrl+C 退出行为存在;该模式没有独立结构化最终结果通道。
|
||
|
||
## 已知限制
|
||
|
||
- 没有原始 Command Code TUI、颜色布局、动画和键盘交互。
|
||
- headless 无法在服务终端进行批准、拒绝、选项选择或文字回答;`ask_user_question` 不能由等待中的 HTTP 客户端处理。
|
||
- 终端事件渲染由本项目完成,格式接近日志,无法等同原始 TUI。
|
||
- API 只实现 Chat Completions 文本范围;`stream: true` 是最终结果的 SSE 兼容封装,不是实时生成。不实现 Responses API、图片、音频、function calling 或服务端会话。
|
||
- `usage` 使用 Command Code 最终结果提供的真实 input/output token;CLI 未提供时返回 0。
|
||
- 长文本受 HTTP 请求体上限和 Command Code 模型上下文上限共同限制,不会由桥接服务自行截断。
|
||
- v1.10.0 的大输入会让 `run_end.nextState` 重复完整提示词。实测约 96 KiB 中文输入时,CLI 退出前可能截断该大事件并丢掉紧随其后的 compact `result` 行。桥接服务会忽略冗余 `run_end`,优先使用 `result.finalText`;若退出码为 0 且 result 缺失,只使用最后一个已完整结束、无工具调用的结构化 turn 文本和真实 turn usage,不从 TUI 文本解析。
|