add docker file and claude flow output
This commit is contained in:
parent
564aef548a
commit
663b2f4c63
|
|
@ -0,0 +1,40 @@
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
# cursor-api-proxy 設定範例
|
||||||
|
# 複製為 .env 後填入你的設定:cp .env.example .env
|
||||||
|
# ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# ── 伺服器設定 ────────────────────────────────────────────────
|
||||||
|
# Docker 模式固定使用 0.0.0.0;本機直接執行時可改 127.0.0.1
|
||||||
|
CURSOR_BRIDGE_HOST=0.0.0.0
|
||||||
|
CURSOR_BRIDGE_PORT=8766
|
||||||
|
CURSOR_BRIDGE_API_KEY=
|
||||||
|
CURSOR_BRIDGE_TIMEOUT_MS=3600000
|
||||||
|
CURSOR_BRIDGE_MULTI_PORT=false
|
||||||
|
CURSOR_BRIDGE_VERBOSE=false
|
||||||
|
|
||||||
|
# ── Agent / 模型設定 ──────────────────────────────────────────
|
||||||
|
# Docker 模式:容器內 agent 路徑(預設掛載至 /usr/local/bin/agent)
|
||||||
|
CURSOR_AGENT_BIN=/usr/local/bin/agent
|
||||||
|
CURSOR_BRIDGE_DEFAULT_MODEL=auto
|
||||||
|
CURSOR_BRIDGE_STRICT_MODEL=true
|
||||||
|
CURSOR_BRIDGE_MAX_MODE=false
|
||||||
|
CURSOR_BRIDGE_FORCE=false
|
||||||
|
CURSOR_BRIDGE_APPROVE_MCPS=false
|
||||||
|
|
||||||
|
# ── 工作區與帳號 ──────────────────────────────────────────────
|
||||||
|
CURSOR_BRIDGE_WORKSPACE=/workspace
|
||||||
|
CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE=true
|
||||||
|
# 多帳號設定目錄(逗號分隔),留空則自動探索 ~/.cursor-api-proxy/accounts/
|
||||||
|
CURSOR_CONFIG_DIRS=
|
||||||
|
|
||||||
|
# ── TLS / HTTPS(選用)────────────────────────────────────────
|
||||||
|
CURSOR_BRIDGE_TLS_CERT=
|
||||||
|
CURSOR_BRIDGE_TLS_KEY=
|
||||||
|
|
||||||
|
# ── Docker 專用:宿主機路徑對映 ──────────────────────────────
|
||||||
|
# 宿主機上 Cursor agent 二進位檔的實際路徑
|
||||||
|
CURSOR_AGENT_HOST_BIN=/usr/local/bin/agent
|
||||||
|
# 宿主機上的帳號資料目錄(會掛載至容器的 /root/.cursor-api-proxy)
|
||||||
|
CURSOR_ACCOUNTS_DIR=~/.cursor-api-proxy
|
||||||
|
# 宿主機上要掛載進容器的工作區目錄
|
||||||
|
WORKSPACE_DIR=/tmp/workspace
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# ── Stage 1: 編譯 ─────────────────────────────────────────────
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
# 臨時降版 go directive 以符合 1.24 編譯器,不修改原始 go.mod
|
||||||
|
RUN go mod edit -go=1.24 && go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o cursor-api-proxy .
|
||||||
|
|
||||||
|
# ── Stage 2: 執行環境 ──────────────────────────────────────────
|
||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /build/cursor-api-proxy .
|
||||||
|
|
||||||
|
EXPOSE 8766
|
||||||
|
|
||||||
|
ENTRYPOINT ["./cursor-api-proxy"]
|
||||||
77
Makefile
77
Makefile
|
|
@ -27,14 +27,12 @@ CHAT_ONLY_WORKSPACE ?= true
|
||||||
CONFIG_DIRS ?=
|
CONFIG_DIRS ?=
|
||||||
|
|
||||||
# ── Cursor / Claude Code(~/.claude)────────────────
|
# ── Cursor / Claude Code(~/.claude)────────────────
|
||||||
# 預設 id 須與 GET http://HOST:PORT/v1/models 回傳的 data[].id 一致;可於命令列覆寫
|
|
||||||
CLAUDE_SETTINGS ?= $(HOME)/.claude/settings.json
|
CLAUDE_SETTINGS ?= $(HOME)/.claude/settings.json
|
||||||
CLAUDE_JSON ?= $(HOME)/.claude.json
|
CLAUDE_JSON ?= $(HOME)/.claude.json
|
||||||
ANTHROPIC_AUTH_TOKEN ?=
|
ANTHROPIC_AUTH_TOKEN ?=
|
||||||
ANTHROPIC_DEFAULT_SONNET_MODEL ?= claude-4.6-sonnet-medium
|
ANTHROPIC_DEFAULT_SONNET_MODEL ?= claude-4.6-sonnet-medium
|
||||||
ANTHROPIC_DEFAULT_OPUS_MODEL ?= claude-4.6-opus-max
|
ANTHROPIC_DEFAULT_OPUS_MODEL ?= claude-4.6-opus-max
|
||||||
ANTHROPIC_DEFAULT_HAIKU_MODEL ?= gemini-3-flash
|
ANTHROPIC_DEFAULT_HAIKU_MODEL ?= gemini-3-flash
|
||||||
# 僅影響 claude-settings 寫入的 ANTHROPIC_BASE_URL(預設等同 HOST;可設為 localhost)
|
|
||||||
ANTHROPIC_BASE_HOST ?= $(HOST)
|
ANTHROPIC_BASE_HOST ?= $(HOST)
|
||||||
|
|
||||||
# ── TLS / HTTPS ───────────────────────────────
|
# ── TLS / HTTPS ───────────────────────────────
|
||||||
|
|
@ -50,8 +48,14 @@ ENV_FILE ?= .env
|
||||||
|
|
||||||
OPENCODE_CONFIG ?= $(HOME)/.config/opencode/opencode.json
|
OPENCODE_CONFIG ?= $(HOME)/.config/opencode/opencode.json
|
||||||
|
|
||||||
|
# ── Docker 設定 ───────────────────────────────
|
||||||
|
DOCKER_IMAGE ?= cursor-api-proxy
|
||||||
|
DOCKER_TAG ?= latest
|
||||||
|
DOCKER_COMPOSE ?= docker compose
|
||||||
|
|
||||||
.PHONY: env run build clean help opencode opencode-models pm2 pm2-stop pm2-logs claude-code pm2-claude-code \
|
.PHONY: env run build clean help opencode opencode-models pm2 pm2-stop pm2-logs claude-code pm2-claude-code \
|
||||||
claude-settings claude-onboarding claude-cursor-setup
|
claude-settings claude-onboarding claude-cursor-setup \
|
||||||
|
docker-build docker-up docker-down docker-logs docker-restart docker-shell docker-env docker-setup
|
||||||
|
|
||||||
## 產生 .env 檔(預設輸出至 .env,可用 ENV_FILE=xxx 覆寫)
|
## 產生 .env 檔(預設輸出至 .env,可用 ENV_FILE=xxx 覆寫)
|
||||||
env:
|
env:
|
||||||
|
|
@ -186,6 +190,63 @@ pm2-stop:
|
||||||
pm2-logs:
|
pm2-logs:
|
||||||
pm2 logs cursor-api-proxy
|
pm2 logs cursor-api-proxy
|
||||||
|
|
||||||
|
|
||||||
|
## ──────────────────────────────────────────────────
|
||||||
|
## Docker Compose 指令
|
||||||
|
## ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
## 複製 .env.example 為 .env(首次設定)
|
||||||
|
docker-env:
|
||||||
|
@if [ -f .env ]; then \
|
||||||
|
echo ".env 已存在,若要重置請手動刪除後再執行"; \
|
||||||
|
else \
|
||||||
|
cp .env.example .env; \
|
||||||
|
echo "已建立 .env,請編輯填入設定後執行 make docker-up"; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
## 建置 Docker 映像檔
|
||||||
|
docker-build:
|
||||||
|
$(DOCKER_COMPOSE) build
|
||||||
|
|
||||||
|
## 啟動 Docker Compose(背景執行)
|
||||||
|
docker-up:
|
||||||
|
@if [ ! -f .env ]; then \
|
||||||
|
echo "找不到 .env,請先執行:make docker-env"; exit 1; \
|
||||||
|
fi
|
||||||
|
$(DOCKER_COMPOSE) up -d
|
||||||
|
@echo "cursor-api-proxy 已啟動(http://0.0.0.0:$(PORT))"
|
||||||
|
|
||||||
|
## 首次設定並啟動(複製 .env + build + up,一步完成)
|
||||||
|
docker-setup:
|
||||||
|
@if [ ! -f .env ]; then \
|
||||||
|
cp .env.example .env; \
|
||||||
|
echo "已建立 .env,請先編輯填入必要設定(CURSOR_AGENT_HOST_BIN、CURSOR_ACCOUNTS_DIR),然後重新執行 make docker-setup"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
$(DOCKER_COMPOSE) build
|
||||||
|
$(DOCKER_COMPOSE) up -d
|
||||||
|
@echo "cursor-api-proxy 已啟動(http://0.0.0.0:$(PORT))"
|
||||||
|
@echo "查看日誌:make docker-logs"
|
||||||
|
|
||||||
|
## 停止並移除容器
|
||||||
|
docker-down:
|
||||||
|
$(DOCKER_COMPOSE) down
|
||||||
|
|
||||||
|
## 查看容器日誌(即時跟蹤)
|
||||||
|
docker-logs:
|
||||||
|
$(DOCKER_COMPOSE) logs -f cursor-api-proxy
|
||||||
|
|
||||||
|
## 重新建置並啟動容器
|
||||||
|
docker-restart:
|
||||||
|
$(DOCKER_COMPOSE) down
|
||||||
|
$(DOCKER_COMPOSE) build
|
||||||
|
$(DOCKER_COMPOSE) up -d
|
||||||
|
@echo "cursor-api-proxy 已重新啟動"
|
||||||
|
|
||||||
|
## 進入容器 shell(除錯用)
|
||||||
|
docker-shell:
|
||||||
|
$(DOCKER_COMPOSE) exec cursor-api-proxy sh
|
||||||
|
|
||||||
## 顯示說明
|
## 顯示說明
|
||||||
help:
|
help:
|
||||||
@echo "可用目標:"
|
@echo "可用目標:"
|
||||||
|
|
@ -204,6 +265,16 @@ help:
|
||||||
@echo " make claude-cursor-setup 同上兩步一次完成"
|
@echo " make claude-cursor-setup 同上兩步一次完成"
|
||||||
@echo " make clean 刪除二進位檔與 .env"
|
@echo " make clean 刪除二進位檔與 .env"
|
||||||
@echo ""
|
@echo ""
|
||||||
|
@echo "Docker Compose 指令:"
|
||||||
|
@echo " make docker-env 複製 .env.example 為 .env(首次設定)"
|
||||||
|
@echo " make docker-setup 首次設定並啟動(自動複製 .env + build + up)"
|
||||||
|
@echo " make docker-build 建置 Docker 映像檔"
|
||||||
|
@echo " make docker-up 啟動容器(背景執行,需已有 .env)"
|
||||||
|
@echo " make docker-down 停止並移除容器"
|
||||||
|
@echo " make docker-logs 查看容器即時日誌"
|
||||||
|
@echo " make docker-restart 重新建置並啟動容器"
|
||||||
|
@echo " make docker-shell 進入容器 shell(除錯用)"
|
||||||
|
@echo ""
|
||||||
@echo "覆寫範例:"
|
@echo "覆寫範例:"
|
||||||
@echo " make env PORT=9000 API_KEY=mysecret TIMEOUT_MS=60000"
|
@echo " make env PORT=9000 API_KEY=mysecret TIMEOUT_MS=60000"
|
||||||
@echo " make pm2-claude-code PORT=8765 API_KEY=mykey"
|
@echo " make pm2-claude-code PORT=8765 API_KEY=mykey"
|
||||||
|
|
|
||||||
278
README.md
278
README.md
|
|
@ -13,7 +13,101 @@
|
||||||
- **HWID 重置**:內建反偵測功能,可重置機器識別碼
|
- **HWID 重置**:內建反偵測功能,可重置機器識別碼
|
||||||
- **連線池**:最佳化的連線管理
|
- **連線池**:最佳化的連線管理
|
||||||
|
|
||||||
## 快速開始
|
---
|
||||||
|
|
||||||
|
## 🐳 Docker Compose 部署(推薦,跨電腦通用)
|
||||||
|
|
||||||
|
使用 Docker Compose 可以讓代理在任何有 Docker 的電腦上一鍵啟動,**無需安裝 Go 環境**。
|
||||||
|
|
||||||
|
> **前提**:宿主機需已安裝 Cursor CLI(`agent` 二進位檔),且已登入至少一個帳號。
|
||||||
|
|
||||||
|
### 快速開始(三步驟)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone 專案
|
||||||
|
git clone https://github.com/your-repo/cursor-api-proxy-go.git
|
||||||
|
cd cursor-api-proxy-go
|
||||||
|
|
||||||
|
# 2. 建立並編輯 .env(首次執行會提示你先填寫設定)
|
||||||
|
make docker-setup
|
||||||
|
```
|
||||||
|
|
||||||
|
第一次執行 `make docker-setup` 時,若尚未有 `.env`,它會自動幫你從 `.env.example` 複製,然後提示你填入必要設定後再執行一次。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 3. 編輯 .env 後再次啟動
|
||||||
|
vim .env
|
||||||
|
make docker-setup
|
||||||
|
```
|
||||||
|
|
||||||
|
啟動後確認運作:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8766/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### .env 關鍵設定
|
||||||
|
|
||||||
|
| 變數 | 說明 | 範例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `CURSOR_AGENT_HOST_BIN` | 宿主機 Cursor agent 二進位檔的**完整路徑** | `/usr/local/bin/agent` |
|
||||||
|
| `CURSOR_ACCOUNTS_DIR` | 宿主機帳號資料目錄 | `~/.cursor-api-proxy` |
|
||||||
|
| `WORKSPACE_DIR` | 要掛載進容器的工作區目錄 | `/home/user/projects` |
|
||||||
|
| `CURSOR_BRIDGE_PORT` | 監聽連接埠(預設 8766) | `8766` |
|
||||||
|
| `CURSOR_BRIDGE_API_KEY` | API 鑑別金鑰(選用,建議設定) | `my-secret-key` |
|
||||||
|
|
||||||
|
### 尋找 Cursor agent 路徑
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS / Linux
|
||||||
|
which agent 2>/dev/null || find /usr/local -name agent 2>/dev/null | head -5
|
||||||
|
|
||||||
|
# 如果 Cursor 是透過 AppImage 或自訂路徑安裝
|
||||||
|
find ~/.cursor -name agent 2>/dev/null | head -5
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Compose 常用指令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docker-setup # 首次設定並啟動(自動複製 .env + build + up)
|
||||||
|
make docker-env # 僅複製 .env.example 為 .env
|
||||||
|
make docker-build # 重新建置映像檔
|
||||||
|
make docker-up # 啟動容器(需已有 .env)
|
||||||
|
make docker-down # 停止並移除容器
|
||||||
|
make docker-logs # 查看即時日誌
|
||||||
|
make docker-restart # 重新建置並啟動
|
||||||
|
make docker-shell # 進入容器 shell(除錯用)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 接入 Claude Code(Docker 模式)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 代理啟動後,設定 Claude Code 使用代理
|
||||||
|
export ANTHROPIC_BASE_URL=http://localhost:8766
|
||||||
|
export ANTHROPIC_API_KEY=my-secret-key # 若有設定 CURSOR_BRIDGE_API_KEY
|
||||||
|
|
||||||
|
# 或一鍵寫入 ~/.claude/settings.json
|
||||||
|
make claude-settings PORT=8766 ANTHROPIC_BASE_HOST=localhost
|
||||||
|
make claude-onboarding
|
||||||
|
|
||||||
|
claude
|
||||||
|
```
|
||||||
|
|
||||||
|
### 在其他電腦(或同區網)使用
|
||||||
|
|
||||||
|
1. 在一台電腦上啟動 Docker Compose(`CURSOR_BRIDGE_HOST=0.0.0.0`)
|
||||||
|
2. 確認 `CURSOR_BRIDGE_PORT` 防火牆已開放
|
||||||
|
3. 其他電腦將 `ANTHROPIC_BASE_URL` 指向這台的 IP
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ANTHROPIC_BASE_URL=http://192.168.1.100:8766
|
||||||
|
export ANTHROPIC_API_KEY=my-secret-key
|
||||||
|
claude
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 本機直接執行(pm2 模式)
|
||||||
|
|
||||||
### 1. 建置
|
### 1. 建置
|
||||||
|
|
||||||
|
|
@ -32,18 +126,20 @@ go build -o cursor-api-proxy .
|
||||||
### 3. 啟動伺服器
|
### 3. 啟動伺服器
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 直接執行(前景)
|
||||||
./cursor-api-proxy
|
./cursor-api-proxy
|
||||||
|
|
||||||
|
# 用 pm2 背景執行
|
||||||
|
make env PORT=8766 API_KEY=mysecret
|
||||||
|
make pm2
|
||||||
```
|
```
|
||||||
|
|
||||||
預設監聽 `127.0.0.1:8765`。
|
### 4. 設定 Claude Code
|
||||||
|
|
||||||
### 4. 設定 API Key(選用)
|
|
||||||
|
|
||||||
如果需要外部存取,建議設定 API Key:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export CURSOR_BRIDGE_API_KEY=my-secret-key
|
make claude-settings PORT=8766
|
||||||
./cursor-api-proxy
|
make claude-onboarding
|
||||||
|
claude
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -52,19 +148,14 @@ export CURSOR_BRIDGE_API_KEY=my-secret-key
|
||||||
|
|
||||||
Claude Code 使用 Anthropic SDK,透過環境變數設定即可改用你的代理。
|
Claude Code 使用 Anthropic SDK,透過環境變數設定即可改用你的代理。
|
||||||
|
|
||||||
### 步驟
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 啟動代理(確認已在背景執行)
|
# 1. 啟動代理(Docker 或 pm2,確認已在背景執行)
|
||||||
./cursor-api-proxy &
|
|
||||||
|
|
||||||
# 2. 設定環境變數,讓 Claude Code 改用你的代理
|
# 2. 設定環境變數
|
||||||
export ANTHROPIC_BASE_URL=http://127.0.0.1:8765
|
export ANTHROPIC_BASE_URL=http://127.0.0.1:8766
|
||||||
|
export ANTHROPIC_API_KEY=my-secret-key # 若未設定 API_KEY,填任意值
|
||||||
|
|
||||||
# 3. 如果代理有設定 CURSOR_BRIDGE_API_KEY,這裡填相同的值;沒有的話隨便填
|
# 3. 啟動 Claude Code
|
||||||
export ANTHROPIC_API_KEY=my-secret-key
|
|
||||||
|
|
||||||
# 4. 啟動 Claude Code
|
|
||||||
claude
|
claude
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -79,13 +170,7 @@ claude
|
||||||
| `opus-4.6` | `claude-opus-4-6` |
|
| `opus-4.6` | `claude-opus-4-6` |
|
||||||
| `sonnet-4.6` | `claude-sonnet-4-6` |
|
| `sonnet-4.6` | `claude-sonnet-4-6` |
|
||||||
| `opus-4.5-thinking` | `claude-opus-4-5-thinking` |
|
| `opus-4.5-thinking` | `claude-opus-4-5-thinking` |
|
||||||
| `sonnet-4.7` (未來新增) | `claude-sonnet-4-7` (自動生成) |
|
| `sonnet-4.7`(未來) | `claude-sonnet-4-7`(自動生成) |
|
||||||
|
|
||||||
### 也可直接指定模型
|
|
||||||
|
|
||||||
```bash
|
|
||||||
claude --model claude-sonnet-4-6
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -93,15 +178,8 @@ claude --model claude-sonnet-4-6
|
||||||
|
|
||||||
OpenCode 透過 `~/.config/opencode/opencode.json` 設定 provider,使用 OpenAI 相容格式。
|
OpenCode 透過 `~/.config/opencode/opencode.json` 設定 provider,使用 OpenAI 相容格式。
|
||||||
|
|
||||||
### 步驟
|
|
||||||
|
|
||||||
1. 啟動代理伺服器
|
1. 啟動代理伺服器
|
||||||
|
2. 編輯 `~/.config/opencode/opencode.json`:
|
||||||
```bash
|
|
||||||
./cursor-api-proxy &
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 編輯 `~/.config/opencode/opencode.json`,在 `provider` 中新增一個 provider:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|
@ -110,64 +188,42 @@ OpenCode 透過 `~/.config/opencode/opencode.json` 設定 provider,使用 Open
|
||||||
"npm": "@ai-sdk/openai-compatible",
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
"name": "Cursor Agent",
|
"name": "Cursor Agent",
|
||||||
"options": {
|
"options": {
|
||||||
"baseURL": "http://127.0.0.1:8765/v1",
|
"baseURL": "http://127.0.0.1:8766/v1",
|
||||||
"apiKey": "unused"
|
"apiKey": "unused"
|
||||||
},
|
},
|
||||||
"models": {
|
"models": {
|
||||||
"auto": { "name": "Cursor Auto" },
|
"auto": { "name": "Cursor Auto" },
|
||||||
"sonnet-4.6": { "name": "Sonnet 4.6" },
|
"sonnet-4.6": { "name": "Sonnet 4.6" },
|
||||||
"opus-4.6": { "name": "Opus 4.6" },
|
"opus-4.6": { "name": "Opus 4.6" }
|
||||||
"opus-4.6-thinking": { "name": "Opus 4.6 Thinking" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
若代理有設定 `CURSOR_BRIDGE_API_KEY`,把 `apiKey` 改成相同的值。
|
或使用指令一次設定:
|
||||||
|
|
||||||
3. 在 `opencode.json` 中設定預設模型:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"model": "cursor/sonnet-4.6"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 查看可用模型
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:8765/v1/models | jq '.data[].id'
|
make opencode PORT=8766
|
||||||
|
make opencode-models PORT=8766
|
||||||
```
|
```
|
||||||
|
|
||||||
代理的 `/v1/models` 端點會回傳 Cursor CLI 目前支援的所有模型 ID,把結果加到 `opencode.json` 的 `models` 中即可。
|
|
||||||
|
|
||||||
### 在 OpenCode 中切換模型
|
|
||||||
|
|
||||||
OpenCode 的模型切換使用 `/model` 指令,從 `opencode.json` 的 `models` 清單中選擇。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 模型對映原理
|
## 模型對映原理
|
||||||
|
|
||||||
兩端使用不同的模型 ID 格式:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Claude Code OpenCode
|
Claude Code OpenCode
|
||||||
│ │
|
│ │
|
||||||
│ claude-opus-4-6 │ opus-4.6 (Cursor 原生 ID)
|
│ claude-opus-4-6 │ opus-4.6
|
||||||
│ (Anthropic alias) │ (OpenAI 相容格式)
|
|
||||||
▼ ▼
|
▼ ▼
|
||||||
┌──────────────────────┐ ┌──────────────────────┐
|
┌──────────────────────────────────────┐
|
||||||
│ ResolveToCursorModel │ │ 直接傳給代理 │
|
│ ResolveToCursorModel │
|
||||||
│ claude-opus-4-6 │ │ opus-4.6 │
|
│ claude-opus-4-6 → opus-4.6 │
|
||||||
│ ↓ │ │ ↓ │
|
└──────────────────────────────────────┘
|
||||||
│ opus-4.6 │ │ opus-4.6 │
|
│
|
||||||
└──────────────────────┘ └──────────────────────┘
|
▼
|
||||||
│ │
|
Cursor CLI (agent --model opus-4.6)
|
||||||
└──────────┬───────────┘
|
|
||||||
▼
|
|
||||||
Cursor CLI (agent --model opus-4.6)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Anthropic 模型對映表(Claude Code 使用)
|
### Anthropic 模型對映表(Claude Code 使用)
|
||||||
|
|
@ -181,66 +237,43 @@ Claude Code OpenCode
|
||||||
| `opus-4.5` | `claude-opus-4-5` | Claude 4.5 Opus |
|
| `opus-4.5` | `claude-opus-4-5` | Claude 4.5 Opus |
|
||||||
| `opus-4.5-thinking` | `claude-opus-4-5-thinking` | Claude 4.5 Opus (Thinking) |
|
| `opus-4.5-thinking` | `claude-opus-4-5-thinking` | Claude 4.5 Opus (Thinking) |
|
||||||
| `sonnet-4.5` | `claude-sonnet-4-5` | Claude 4.5 Sonnet |
|
| `sonnet-4.5` | `claude-sonnet-4-5` | Claude 4.5 Sonnet |
|
||||||
| `sonnet-4.5-thinking` | `claude-sonnet-4-5-thinking` | Claude 4.5 Sonnet (Thinking) |
|
|
||||||
|
|
||||||
### 動態對映(自動)
|
Cursor CLI 新增模型時自動生成對應 `claude-*` ID,無需手動更新。
|
||||||
|
|
||||||
Cursor CLI 新增模型時(如 `opus-4.7`、`sonnet-5.0`),代理自動生成對應的 `claude-*` ID,無需手動更新。
|
|
||||||
|
|
||||||
規則:`<family>-<major>.<minor>` → `claude-<family>-<major>-<minor>`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 帳號管理
|
## 帳號管理
|
||||||
|
|
||||||
### 登入帳號
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 登入
|
||||||
./cursor-api-proxy login myaccount
|
./cursor-api-proxy login myaccount
|
||||||
|
|
||||||
# 使用代理登入
|
# 使用代理登入
|
||||||
./cursor-api-proxy login myaccount --proxy=http://127.0.0.1:7890
|
./cursor-api-proxy login myaccount --proxy=http://127.0.0.1:7890
|
||||||
```
|
|
||||||
|
|
||||||
### 列出帳號
|
# 列出所有帳號
|
||||||
|
|
||||||
```bash
|
|
||||||
./cursor-api-proxy accounts
|
./cursor-api-proxy accounts
|
||||||
```
|
|
||||||
|
|
||||||
### 登出帳號
|
# 登出
|
||||||
|
|
||||||
```bash
|
|
||||||
./cursor-api-proxy logout myaccount
|
./cursor-api-proxy logout myaccount
|
||||||
```
|
|
||||||
|
|
||||||
### 重置 HWID(反BAN)
|
# 重置 HWID(反BAN)
|
||||||
|
|
||||||
```bash
|
|
||||||
# 基本重置
|
|
||||||
./cursor-api-proxy reset-hwid
|
./cursor-api-proxy reset-hwid
|
||||||
|
|
||||||
# 深度清理(清除 session 和 cookies)
|
# 深度清理(清除 session 和 cookies)
|
||||||
./cursor-api-proxy reset-hwid --deep-clean
|
./cursor-api-proxy reset-hwid --deep-clean
|
||||||
```
|
```
|
||||||
|
|
||||||
### 啟動選項
|
|
||||||
|
|
||||||
| 選項 | 說明 |
|
|
||||||
|------|------|
|
|
||||||
| `--tailscale` | 綁定到 `0.0.0.0` 供區域網路存取 |
|
|
||||||
| `-h, --help` | 顯示說明 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API 端點
|
## API 端點
|
||||||
|
|
||||||
| 端點 | 方法 | 說明 |
|
| 端點 | 方法 | 說明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `http://127.0.0.1:8765/v1/chat/completions` | POST | OpenAI 格式聊天完成 |
|
| `/v1/chat/completions` | POST | OpenAI 格式聊天完成 |
|
||||||
| `http://127.0.0.1:8765/v1/models` | GET | 列出可用模型 |
|
| `/v1/messages` | POST | Anthropic 格式聊天 |
|
||||||
| `http://127.0.0.1:8765/v1/chat/messages` | POST | Anthropic 格式聊天 |
|
| `/v1/models` | GET | 列出可用模型 |
|
||||||
| `http://127.0.0.1:8765/health` | GET | 健康檢查 |
|
| `/health` | GET | 健康檢查 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -250,10 +283,10 @@ Cursor CLI 新增模型時(如 `opus-4.7`、`sonnet-5.0`),代理自動生
|
||||||
|
|
||||||
| 變數 | 預設值 | 說明 |
|
| 變數 | 預設值 | 說明 |
|
||||||
|------|--------|------|
|
|------|--------|------|
|
||||||
| `CURSOR_BRIDGE_HOST` | `127.0.0.1` | 監聽位址(設為 `0.0.0.0` 可供區域網路存取) |
|
| `CURSOR_BRIDGE_HOST` | `127.0.0.1` | 監聽位址(Docker 模式固定 `0.0.0.0`) |
|
||||||
| `CURSOR_BRIDGE_PORT` | `8765` | 監聽連接埠 |
|
| `CURSOR_BRIDGE_PORT` | `8766` | 監聽連接埠 |
|
||||||
| `CURSOR_BRIDGE_API_KEY` | _(無)_ | API 鑑別金鑰,設定後所有請求需帶此金鑰 |
|
| `CURSOR_BRIDGE_API_KEY` | _(無)_ | API 鑑別金鑰 |
|
||||||
| `CURSOR_BRIDGE_TIMEOUT_MS` | `300000` | 請求逾時毫秒數(預設 5 分鐘) |
|
| `CURSOR_BRIDGE_TIMEOUT_MS` | `3600000` | 請求逾時毫秒數(預設 1 小時) |
|
||||||
| `CURSOR_BRIDGE_MULTI_PORT` | `false` | 啟用多連接埠模式 |
|
| `CURSOR_BRIDGE_MULTI_PORT` | `false` | 啟用多連接埠模式 |
|
||||||
| `CURSOR_BRIDGE_VERBOSE` | `false` | 啟用詳細日誌輸出 |
|
| `CURSOR_BRIDGE_VERBOSE` | `false` | 啟用詳細日誌輸出 |
|
||||||
|
|
||||||
|
|
@ -261,12 +294,10 @@ Cursor CLI 新增模型時(如 `opus-4.7`、`sonnet-5.0`),代理自動生
|
||||||
|
|
||||||
| 變數 | 預設值 | 說明 |
|
| 變數 | 預設值 | 說明 |
|
||||||
|------|--------|------|
|
|------|--------|------|
|
||||||
| `CURSOR_AGENT_BIN` / `CURSOR_CLI_BIN` / `CURSOR_CLI_PATH` | `agent` | Cursor CLI 二進位檔路徑 |
|
| `CURSOR_AGENT_BIN` | `agent` | Cursor CLI 二進位檔路徑 |
|
||||||
| `CURSOR_AGENT_NODE` | _(無)_ | Node.js 執行檔路徑(Windows 使用) |
|
|
||||||
| `CURSOR_AGENT_SCRIPT` | _(無)_ | Agent 腳本路徑(Windows 使用) |
|
|
||||||
| `CURSOR_BRIDGE_DEFAULT_MODEL` | `auto` | 預設使用的模型 ID |
|
| `CURSOR_BRIDGE_DEFAULT_MODEL` | `auto` | 預設使用的模型 ID |
|
||||||
| `CURSOR_BRIDGE_STRICT_MODEL` | `true` | 嚴格模式:禁止使用不在清單中的模型 |
|
| `CURSOR_BRIDGE_STRICT_MODEL` | `true` | 嚴格模式:禁止使用不在清單中的模型 |
|
||||||
| `CURSOR_BRIDGE_MAX_MODE` | `false` | 啟用 Max Mode(消耗更多額度) |
|
| `CURSOR_BRIDGE_MAX_MODE` | `false` | 啟用 Max Mode |
|
||||||
| `CURSOR_BRIDGE_FORCE` | `false` | 強制執行,不詢問確認 |
|
| `CURSOR_BRIDGE_FORCE` | `false` | 強制執行,不詢問確認 |
|
||||||
| `CURSOR_BRIDGE_APPROVE_MCPS` | `false` | 自動核准 MCP 工具呼叫 |
|
| `CURSOR_BRIDGE_APPROVE_MCPS` | `false` | 自動核准 MCP 工具呼叫 |
|
||||||
|
|
||||||
|
|
@ -276,22 +307,15 @@ Cursor CLI 新增模型時(如 `opus-4.7`、`sonnet-5.0`),代理自動生
|
||||||
|------|--------|------|
|
|------|--------|------|
|
||||||
| `CURSOR_BRIDGE_WORKSPACE` | _(目前目錄)_ | 工作目錄路徑 |
|
| `CURSOR_BRIDGE_WORKSPACE` | _(目前目錄)_ | 工作目錄路徑 |
|
||||||
| `CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE` | `true` | 限制 agent 只能存取工作目錄 |
|
| `CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE` | `true` | 限制 agent 只能存取工作目錄 |
|
||||||
| `CURSOR_CONFIG_DIRS` / `CURSOR_ACCOUNT_DIRS` | _(自動探索)_ | 帳號設定目錄,多個用逗號分隔 |
|
| `CURSOR_CONFIG_DIRS` | _(自動探索)_ | 帳號設定目錄,多個用逗號分隔 |
|
||||||
|
|
||||||
### TLS / HTTPS
|
### Docker 專用(.env 設定)
|
||||||
|
|
||||||
| 變數 | 預設值 | 說明 |
|
| 變數 | 說明 |
|
||||||
|------|--------|------|
|
|------|------|
|
||||||
| `CURSOR_BRIDGE_TLS_CERT` | _(無)_ | TLS 憑證檔路徑(啟用 HTTPS) |
|
| `CURSOR_AGENT_HOST_BIN` | 宿主機 Cursor agent 二進位檔路徑(掛載進容器) |
|
||||||
| `CURSOR_BRIDGE_TLS_KEY` | _(無)_ | TLS 私鑰檔路徑(啟用 HTTPS) |
|
| `CURSOR_ACCOUNTS_DIR` | 宿主機帳號資料目錄(掛載進容器) |
|
||||||
|
| `WORKSPACE_DIR` | 宿主機工作區目錄(掛載進容器) |
|
||||||
### 記錄與 Windows 特定
|
|
||||||
|
|
||||||
| 變數 | 預設值 | 說明 |
|
|
||||||
|------|--------|------|
|
|
||||||
| `CURSOR_BRIDGE_SESSIONS_LOG` | `~/.cursor-api-proxy/sessions.log` | Session 記錄檔路徑 |
|
|
||||||
| `CURSOR_BRIDGE_WIN_CMDLINE_MAX` | `30000` | Windows 命令列最大長度(4096–32700) |
|
|
||||||
| `COMSPEC` | `cmd.exe` | Windows 命令直譯器路徑 |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -300,17 +324,17 @@ Cursor CLI 新增模型時(如 `opus-4.7`、`sonnet-5.0`),代理自動生
|
||||||
**Q: 為什麼需要登入帳號?**
|
**Q: 為什麼需要登入帳號?**
|
||||||
A: Cursor API 需要驗證才能使用,請先登入你的 Cursor 帳號。
|
A: Cursor API 需要驗證才能使用,請先登入你的 Cursor 帳號。
|
||||||
|
|
||||||
|
**Q: Docker 容器裡找不到 agent?**
|
||||||
|
A: 確認 `.env` 的 `CURSOR_AGENT_HOST_BIN` 填的是宿主機上正確的路徑,容器會把它掛載到 `/usr/local/bin/agent`。
|
||||||
|
|
||||||
**Q: 如何處理被BAN的問題?**
|
**Q: 如何處理被BAN的問題?**
|
||||||
A: 使用 `reset-hwid` 命令重置機器識別碼,加上 `--deep-clean` 進行更徹底的清理。
|
A: 使用 `reset-hwid` 命令重置機器識別碼,加上 `--deep-clean` 進行更徹底的清理。
|
||||||
|
|
||||||
**Q: 可以在其他設備上使用嗎?**
|
**Q: 可以在其他設備上使用嗎?**
|
||||||
A: 可以,使用 `--tailscale` 選項啟動伺服器,然後透過區域網路 IP 存取。
|
A: 可以。Docker 模式下 `CURSOR_BRIDGE_HOST` 預設為 `0.0.0.0`,開放區域網路存取。其他設備將 `ANTHROPIC_BASE_URL` 指向這台電腦的 IP 即可。
|
||||||
|
|
||||||
**Q: 模型列表多久更新一次?**
|
**Q: 模型列表多久更新一次?**
|
||||||
A: 每次呼叫 `GET /v1/models` 時,代理會即時呼叫 Cursor CLI 的 `--list-models` 取得最新模型,並自動生成對應的 `claude-*` ID。
|
A: 每次呼叫 `GET /v1/models` 時即時更新,自動生成對應的 `claude-*` ID。
|
||||||
|
|
||||||
**Q: 未來 Cursor 新增模型怎麼辦?**
|
|
||||||
A: 不用改任何東西。只要新模型符合 `<family>-<major>.<minor>` 命名規則,代理會自動生成對應的 `claude-*` ID。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
services:
|
||||||
|
cursor-api-proxy:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: cursor-api-proxy:latest
|
||||||
|
container_name: cursor-api-proxy
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "${CURSOR_BRIDGE_PORT:-8766}:${CURSOR_BRIDGE_PORT:-8766}"
|
||||||
|
environment:
|
||||||
|
- CURSOR_BRIDGE_HOST=0.0.0.0
|
||||||
|
volumes:
|
||||||
|
# Cursor CLI 二進位檔(從宿主機掛載,唯讀)
|
||||||
|
- ${CURSOR_AGENT_HOST_BIN:-/usr/local/bin/agent}:/usr/local/bin/agent:ro
|
||||||
|
# 帳號設定目錄(持久化帳號資料)
|
||||||
|
- ${CURSOR_ACCOUNTS_DIR:-~/.cursor-api-proxy}:/root/.cursor-api-proxy
|
||||||
|
# 工作區(選用,掛載你想讓 agent 存取的專案目錄)
|
||||||
|
- ${WORKSPACE_DIR:-/tmp/workspace}:/workspace
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "-qO-", "http://localhost:${CURSOR_BRIDGE_PORT:-8766}/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
@ -3,7 +3,7 @@ package agent
|
||||||
import "cursor-api-proxy/internal/config"
|
import "cursor-api-proxy/internal/config"
|
||||||
|
|
||||||
func BuildAgentFixedArgs(cfg config.BridgeConfig, workspaceDir, model string, stream bool) []string {
|
func BuildAgentFixedArgs(cfg config.BridgeConfig, workspaceDir, model string, stream bool) []string {
|
||||||
args := []string{"--print", "--plan"}
|
args := []string{"--print"}
|
||||||
if cfg.ApproveMcps {
|
if cfg.ApproveMcps {
|
||||||
args = append(args, "--approve-mcps")
|
args = append(args, "--approve-mcps")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -273,7 +273,7 @@ func LoadEnvConfig(e EnvSource, cwd string) LoadedEnv {
|
||||||
TLSCertPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_CERT"}), cwd),
|
TLSCertPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_CERT"}), cwd),
|
||||||
TLSKeyPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_KEY"}), cwd),
|
TLSKeyPath: resolveAbs(getEnvVal(e, []string{"CURSOR_BRIDGE_TLS_KEY"}), cwd),
|
||||||
SessionsLogPath: sessionsLogPath,
|
SessionsLogPath: sessionsLogPath,
|
||||||
ChatOnlyWorkspace: envBool(e, []string{"CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE"}, true),
|
ChatOnlyWorkspace: envBool(e, []string{"CURSOR_BRIDGE_CHAT_ONLY_WORKSPACE"}, false),
|
||||||
Verbose: envBool(e, []string{"CURSOR_BRIDGE_VERBOSE"}, false),
|
Verbose: envBool(e, []string{"CURSOR_BRIDGE_VERBOSE"}, false),
|
||||||
MaxMode: envBool(e, []string{"CURSOR_BRIDGE_MAX_MODE"}, false),
|
MaxMode: envBool(e, []string{"CURSOR_BRIDGE_MAX_MODE"}, false),
|
||||||
ConfigDirs: configDirs,
|
ConfigDirs: configDirs,
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,10 @@ import (
|
||||||
"cursor-api-proxy/internal/winlimit"
|
"cursor-api-proxy/internal/winlimit"
|
||||||
"cursor-api-proxy/internal/workspace"
|
"cursor-api-proxy/internal/workspace"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
|
@ -166,12 +167,44 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
|
||||||
})
|
})
|
||||||
|
|
||||||
if hasTools {
|
if hasTools {
|
||||||
// tools 模式:先累積所有內容,完成後再一次性輸出(因為 tool_calls 需要完整解析)
|
// tools 模式:先即時串流文字,一旦偵測到 tool call 標記就切換為累積模式
|
||||||
|
toolCallMarkerRe := regexp.MustCompile(`<tool_call>|<function_calls>`)
|
||||||
|
var toolCallMode bool
|
||||||
|
|
||||||
|
textBlockOpen := false
|
||||||
|
textBlockIndex := 0
|
||||||
|
|
||||||
p = parser.CreateStreamParserWithThinking(
|
p = parser.CreateStreamParserWithThinking(
|
||||||
func(text string) {
|
func(text string) {
|
||||||
accumulated += text
|
accumulated += text
|
||||||
chunkNum++
|
chunkNum++
|
||||||
logger.LogStreamChunk(model, text, chunkNum)
|
logger.LogStreamChunk(model, text, chunkNum)
|
||||||
|
|
||||||
|
if toolCallMode {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if toolCallMarkerRe.MatchString(text) {
|
||||||
|
if textBlockOpen {
|
||||||
|
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": textBlockIndex})
|
||||||
|
textBlockOpen = false
|
||||||
|
}
|
||||||
|
toolCallMode = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !textBlockOpen {
|
||||||
|
textBlockIndex = 0
|
||||||
|
writeEvent(map[string]interface{}{
|
||||||
|
"type": "content_block_start",
|
||||||
|
"index": textBlockIndex,
|
||||||
|
"content_block": map[string]string{"type": "text", "text": ""},
|
||||||
|
})
|
||||||
|
textBlockOpen = true
|
||||||
|
}
|
||||||
|
writeEvent(map[string]interface{}{
|
||||||
|
"type": "content_block_delta",
|
||||||
|
"index": textBlockIndex,
|
||||||
|
"delta": map[string]string{"type": "text_delta", "text": text},
|
||||||
|
})
|
||||||
},
|
},
|
||||||
func(thinking string) {
|
func(thinking string) {
|
||||||
accumulatedThinking += thinking
|
accumulatedThinking += thinking
|
||||||
|
|
@ -195,7 +228,11 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsed.HasToolCalls() {
|
if parsed.HasToolCalls() {
|
||||||
if parsed.TextContent != "" {
|
if textBlockOpen {
|
||||||
|
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": textBlockIndex})
|
||||||
|
blockIndex = textBlockIndex + 1
|
||||||
|
}
|
||||||
|
if parsed.TextContent != "" && !textBlockOpen && !toolCallMode {
|
||||||
writeEvent(map[string]interface{}{
|
writeEvent(map[string]interface{}{
|
||||||
"type": "content_block_start", "index": blockIndex,
|
"type": "content_block_start", "index": blockIndex,
|
||||||
"content_block": map[string]string{"type": "text", "text": ""},
|
"content_block": map[string]string{"type": "text", "text": ""},
|
||||||
|
|
@ -236,17 +273,27 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
|
||||||
})
|
})
|
||||||
writeEvent(map[string]interface{}{"type": "message_stop"})
|
writeEvent(map[string]interface{}{"type": "message_stop"})
|
||||||
} else {
|
} else {
|
||||||
writeEvent(map[string]interface{}{
|
if textBlockOpen {
|
||||||
"type": "content_block_start", "index": blockIndex,
|
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": textBlockIndex})
|
||||||
"content_block": map[string]string{"type": "text", "text": ""},
|
} else if accumulated != "" {
|
||||||
})
|
writeEvent(map[string]interface{}{
|
||||||
if accumulated != "" {
|
"type": "content_block_start", "index": blockIndex,
|
||||||
|
"content_block": map[string]string{"type": "text", "text": ""},
|
||||||
|
})
|
||||||
writeEvent(map[string]interface{}{
|
writeEvent(map[string]interface{}{
|
||||||
"type": "content_block_delta", "index": blockIndex,
|
"type": "content_block_delta", "index": blockIndex,
|
||||||
"delta": map[string]string{"type": "text_delta", "text": accumulated},
|
"delta": map[string]string{"type": "text_delta", "text": accumulated},
|
||||||
})
|
})
|
||||||
|
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": blockIndex})
|
||||||
|
blockIndex++
|
||||||
|
} else {
|
||||||
|
writeEvent(map[string]interface{}{
|
||||||
|
"type": "content_block_start", "index": blockIndex,
|
||||||
|
"content_block": map[string]string{"type": "text", "text": ""},
|
||||||
|
})
|
||||||
|
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": blockIndex})
|
||||||
|
blockIndex++
|
||||||
}
|
}
|
||||||
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": blockIndex})
|
|
||||||
writeEvent(map[string]interface{}{
|
writeEvent(map[string]interface{}{
|
||||||
"type": "message_delta",
|
"type": "message_delta",
|
||||||
"delta": map[string]interface{}{"stop_reason": "end_turn", "stop_sequence": nil},
|
"delta": map[string]interface{}{"stop_reason": "end_turn", "stop_sequence": nil},
|
||||||
|
|
@ -258,9 +305,6 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// 非 tools 模式:即時串流 thinking 和 text
|
// 非 tools 模式:即時串流 thinking 和 text
|
||||||
// blockCount 追蹤已開啟的 block 數量
|
|
||||||
// thinkingOpen 代表 thinking block 是否已開啟且尚未關閉
|
|
||||||
// textOpen 代表 text block 是否已開啟且尚未關閉
|
|
||||||
blockCount := 0
|
blockCount := 0
|
||||||
thinkingOpen := false
|
thinkingOpen := false
|
||||||
textOpen := false
|
textOpen := false
|
||||||
|
|
@ -270,12 +314,10 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
|
||||||
accumulated += text
|
accumulated += text
|
||||||
chunkNum++
|
chunkNum++
|
||||||
logger.LogStreamChunk(model, text, chunkNum)
|
logger.LogStreamChunk(model, text, chunkNum)
|
||||||
// 若 thinking block 尚未關閉,先關閉它
|
|
||||||
if thinkingOpen {
|
if thinkingOpen {
|
||||||
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": blockCount - 1})
|
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": blockCount - 1})
|
||||||
thinkingOpen = false
|
thinkingOpen = false
|
||||||
}
|
}
|
||||||
// 若 text block 尚未開啟,先開啟它
|
|
||||||
if !textOpen {
|
if !textOpen {
|
||||||
writeEvent(map[string]interface{}{
|
writeEvent(map[string]interface{}{
|
||||||
"type": "content_block_start",
|
"type": "content_block_start",
|
||||||
|
|
@ -294,7 +336,6 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
|
||||||
func(thinking string) {
|
func(thinking string) {
|
||||||
accumulatedThinking += thinking
|
accumulatedThinking += thinking
|
||||||
chunkNum++
|
chunkNum++
|
||||||
// 若 thinking block 尚未開啟,先開啟它
|
|
||||||
if !thinkingOpen {
|
if !thinkingOpen {
|
||||||
writeEvent(map[string]interface{}{
|
writeEvent(map[string]interface{}{
|
||||||
"type": "content_block_start",
|
"type": "content_block_start",
|
||||||
|
|
@ -312,12 +353,10 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
|
||||||
},
|
},
|
||||||
func() {
|
func() {
|
||||||
logger.LogTrafficResponse(cfg.Verbose, model, accumulated, true)
|
logger.LogTrafficResponse(cfg.Verbose, model, accumulated, true)
|
||||||
// 關閉尚未關閉的 thinking block
|
|
||||||
if thinkingOpen {
|
if thinkingOpen {
|
||||||
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": blockCount - 1})
|
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": blockCount - 1})
|
||||||
thinkingOpen = false
|
thinkingOpen = false
|
||||||
}
|
}
|
||||||
// 若 text block 尚未開啟(全部都是 thinking,沒有 text),開啟並立即關閉空的 text block
|
|
||||||
if !textOpen {
|
if !textOpen {
|
||||||
writeEvent(map[string]interface{}{
|
writeEvent(map[string]interface{}{
|
||||||
"type": "content_block_start",
|
"type": "content_block_start",
|
||||||
|
|
@ -326,7 +365,6 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
|
||||||
})
|
})
|
||||||
blockCount++
|
blockCount++
|
||||||
}
|
}
|
||||||
// 關閉 text block
|
|
||||||
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": blockCount - 1})
|
writeEvent(map[string]interface{}{"type": "content_block_stop", "index": blockCount - 1})
|
||||||
writeEvent(map[string]interface{}{
|
writeEvent(map[string]interface{}{
|
||||||
"type": "message_delta",
|
"type": "message_delta",
|
||||||
|
|
@ -351,7 +389,6 @@ func HandleAnthropicMessages(w http.ResponseWriter, r *http.Request, cfg config.
|
||||||
}
|
}
|
||||||
result, err := agent.RunAgentStreamWithContext(cfg, ws.WorkspaceDir, cmdArgs, wrappedParser, ws.TempDir, configDir, ctx)
|
result, err := agent.RunAgentStreamWithContext(cfg, ws.WorkspaceDir, cmdArgs, wrappedParser, ws.TempDir, configDir, ctx)
|
||||||
|
|
||||||
// agent 結束後,若未收到 result/success 訊號,強制 flush 以確保 SSE stream 正確結尾
|
|
||||||
if ctx.Err() == nil {
|
if ctx.Err() == nil {
|
||||||
p.Flush()
|
p.Flush()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue