1069 lines
34 KiB
Markdown
1069 lines
34 KiB
Markdown
# 重構計劃:cursor-api-proxy → go-zero + DDD Architecture
|
||
|
||
## 一、概述
|
||
|
||
### 目標
|
||
將 cursor-api-proxy 從標準 Go HTTP 架構重構為 go-zero + DDD(Clean Architecture)混合架構。
|
||
|
||
### 核心原則
|
||
- **internal/**:go-zero 框架特定代碼(強依賴框架)
|
||
- **pkg/**:框架無關的核心業務邏輯(DIP 反轉依賴)
|
||
- **api/**:契約優先的 API 定義(goctl 生成基礎代碼)
|
||
|
||
### 架構分層
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ api/chat.api │
|
||
│ (API 契約定義) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ internal/handler/ │
|
||
│ (HTTP Handler 層) │
|
||
│ goctl 生成 + 自訂 SSE Handler │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ internal/logic/ │
|
||
│ (業務邏輯調度層) │
|
||
│ Adapter Pattern: Request → Usecase │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ pkg/usecase/ │
|
||
│ (Application Layer) │
|
||
│ 業務流程協調、Repository 調用 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ pkg/domain/repository/ │
|
||
│ (Repository Interface) │
|
||
│ DIP 反轉依賴 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ pkg/repository/ │
|
||
│ (Infrastructure Layer) │
|
||
│ Repository Interface 實作 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 二、目錄結構(最終狀態)
|
||
|
||
```
|
||
.
|
||
├── api/ # [Contract] go-zero API 定義
|
||
│ └── chat.api # OpenAI 相容 API 規格
|
||
│
|
||
├── etc/ # [Config] 環境配置
|
||
│ ├── chat.yaml # 本地開發
|
||
│ └── chat-prod.yaml # 生產環境
|
||
│
|
||
├── cmd/ # [Entry] 入口點
|
||
│ ├── chat/ # HTTP proxy 服務
|
||
│ │ └── chat.go # go-zero 入口
|
||
│ └── cli/ # CLI 工具
|
||
│ ├── login.go
|
||
│ ├── accounts.go
|
||
│ ├── resethwid.go
|
||
│ └── usage.go
|
||
│
|
||
├── internal/ # [Framework Layer] go-zero 特定
|
||
│ ├── config/
|
||
│ │ └── config.go # 配置結構體(使用 rest.RestConf)
|
||
│ ├── handler/ # HTTP handlers (goctl 生成)
|
||
│ │ ├── routes.go # 路由註冊
|
||
│ │ ├── chat_handler.go # Chat API handler
|
||
│ │ ├── health_handler.go # Health check handler
|
||
│ │ └── models_handler.go # Models list handler
|
||
│ ├── logic/ # 業務邏輯調度層
|
||
│ │ ├── chatcompletionlogic.go # Chat completion 邏輯
|
||
│ │ ├── geminichatlogic.go # Gemini chat 邏輯
|
||
│ │ ├── anthropiclogic.go # Anthropic messages 邏輯
|
||
│ │ ├── healthlogic.go # Health 邏輯
|
||
│ │ └── modelslogic.go # Models 邏輯
|
||
│ ├── middleware/ # 中間件
|
||
│ │ ├── auth.go # API Key 驗證
|
||
│ │ └── recovery.go # Panic 恢復
|
||
│ ├── svc/ # ServiceContext (DI 容器)
|
||
│ │ └── servicecontext.go
|
||
│ └── types/ # API types (goctl 生成)
|
||
│ └── types.go
|
||
│
|
||
├── pkg/ # [Domain] 框架無關的核心邏輯
|
||
│ ├── domain/ # <Domain Layer>
|
||
│ │ ├── entity/ # 純粹業務物件(無框架依賴)
|
||
│ │ │ ├── message.go # Message, Tool, ToolCall
|
||
│ │ │ ├── chunk.go # StreamChunk
|
||
│ │ │ ├── account.go # Account, AccountStat
|
||
│ │ │ └── config.go # ProviderConfig
|
||
│ │ ├── repository/ # Repository Interface (DIP)
|
||
│ │ │ ├── account.go # AccountPool interface
|
||
│ │ │ └── provider.go # Provider interface
|
||
│ │ ├── usecase/ # Usecase Interface
|
||
│ │ │ ├── chat.go # ChatUsecase interface
|
||
│ │ │ └── agent.go # AgentRunner interface
|
||
│ │ └── const/ # 常數
|
||
│ │ ├── models.go # Model ID 常數
|
||
│ │ └── errors.go # 錯誤定義
|
||
│ │
|
||
│ ├── repository/ # <Infrastructure Layer>
|
||
│ │ ├── account.go # AccountPool 實作
|
||
│ │ └── provider.go # Provider 工廠
|
||
│ │
|
||
│ ├── usecase/ # <Application Layer>
|
||
│ │ ├── chat.go # ChatUsecase 實作
|
||
│ │ ├── agent.go # Agent 執行邏輯
|
||
│ │ ├── sanitizer.go # 消息清理
|
||
│ │ └── toolcall.go # Tool call 處理
|
||
│ │
|
||
│ ├── provider/ # Provider 實作
|
||
│ │ ├── cursor/ # Cursor CLI provider
|
||
│ │ │ └── provider.go
|
||
│ │ └── geminiweb/ # Gemini Web provider
|
||
│ │ ├── provider.go
|
||
│ │ ├── browser.go
|
||
│ │ └── pool.go
|
||
│ │
|
||
│ ├── infrastructure/ # 基礎設施
|
||
│ │ ├── process/ # 進程管理
|
||
│ │ │ ├── runner.go
|
||
│ │ │ ├── kill_unix.go
|
||
│ │ │ └── kill_windows.go
|
||
│ │ ├── parser/ # SSE parsing
|
||
│ │ │ └── stream.go
|
||
│ │ ├── httputil/ # HTTP 工具
|
||
│ │ │ └── httputil.go
|
||
│ │ ├── logger/ # 日誌
|
||
│ │ │ └── logger.go
|
||
│ │ ├── env/ # 環境變數
|
||
│ │ │ └── env.go
|
||
│ │ ├── workspace/ # 工作區管理
|
||
│ │ │ └── workspace.go
|
||
│ │ └── winlimit/ # Windows 命令行限制
|
||
│ │ └── winlimit.go
|
||
│ │
|
||
│ └── adapter/ # API 格式適配器
|
||
│ ├── openai/ # OpenAI 格式
|
||
│ │ └── openai.go
|
||
│ └── anthropic/ # Anthropic 格式
|
||
│ └── anthropic.go
|
||
│
|
||
├── build/ # [Infrastructure] 建置相關
|
||
│ ├── Dockerfile
|
||
│ └── docker-compose.yml
|
||
│
|
||
├── scripts/ # 腳本工具
|
||
├── docs/ # 文檔
|
||
│ └── MIGRATION_PLAN.md # 本計劃文件
|
||
│
|
||
├── Makefile
|
||
├── go.mod
|
||
└── go.sum
|
||
```
|
||
|
||
---
|
||
|
||
## 三、執行步驟
|
||
|
||
### Phase 1: API 定義與骨架生成
|
||
|
||
**目標**:建立 go-zero 架構骨架
|
||
|
||
#### Step 1.1: 初始化 go-zero 專案
|
||
|
||
```bash
|
||
# 安裝 goctl
|
||
go install github.com/zeromicro/go-zero/tools/goctl@latest
|
||
|
||
# 建立 API 定義目錄
|
||
mkdir -p api etc
|
||
```
|
||
|
||
#### Step 1.2: 建立 `api/chat.api`
|
||
|
||
```api
|
||
syntax = "v1"
|
||
|
||
info (
|
||
title: "Cursor API Proxy"
|
||
desc: "OpenAI-compatible API proxy for Cursor/Gemini"
|
||
author: "cursor-api-proxy"
|
||
version: "1.0"
|
||
)
|
||
|
||
// ============ Types ============
|
||
|
||
type (
|
||
// Health
|
||
HealthRequest {}
|
||
HealthResponse {
|
||
Status string `json:"status"`
|
||
Version string `json:"version"`
|
||
}
|
||
|
||
// Models
|
||
ModelsRequest {}
|
||
ModelsResponse {
|
||
Object string `json:"object"`
|
||
Data []ModelData `json:"data"`
|
||
}
|
||
|
||
ModelData {
|
||
Id string `json:"id"`
|
||
Object string `json:"object"`
|
||
OwnedBy string `json:"owned_by"`
|
||
}
|
||
|
||
// Chat Completions
|
||
ChatCompletionRequest {
|
||
Model string `json:"model"`
|
||
Messages []Message `json:"messages"`
|
||
Stream bool `json:"stream,optional"`
|
||
Tools []Tool `json:"tools,optional"`
|
||
Functions []Function `json:"functions,optional"`
|
||
MaxTokens int `json:"max_tokens,optional"`
|
||
Temperature float64 `json:"temperature,optional"`
|
||
}
|
||
|
||
Message {
|
||
Role string `json:"role"`
|
||
Content interface{} `json:"content"`
|
||
}
|
||
|
||
Tool {
|
||
Type string `json:"type"`
|
||
Function ToolFunction `json:"function"`
|
||
}
|
||
|
||
ToolFunction {
|
||
Name string `json:"name"`
|
||
Description string `json:"description"`
|
||
Parameters interface{} `json:"parameters"`
|
||
}
|
||
|
||
Function {
|
||
Name string `json:"name"`
|
||
Description string `json:"description,optional"`
|
||
Parameters interface{} `json:"parameters,optional"`
|
||
}
|
||
|
||
ChatCompletionResponse {
|
||
Id string `json:"id"`
|
||
Object string `json:"object"`
|
||
Created int64 `json:"created"`
|
||
Model string `json:"model"`
|
||
Choices []Choice `json:"choices"`
|
||
Usage Usage `json:"usage"`
|
||
}
|
||
|
||
Choice {
|
||
Index int `json:"index"`
|
||
Message *RespMessage `json:"message,optional"`
|
||
Delta *Delta `json:"delta,optional"`
|
||
FinishReason string `json:"finish_reason"`
|
||
}
|
||
|
||
RespMessage {
|
||
Role string `json:"role"`
|
||
Content string `json:"content,optional"`
|
||
ToolCalls []ToolCall `json:"tool_calls,optional"`
|
||
}
|
||
|
||
Delta struct {
|
||
Role string `json:"role,optional"`
|
||
Content string `json:"content,optional"`
|
||
ReasoningContent string `json:"reasoning_content,optional"`
|
||
ToolCalls []ToolCall `json:"tool_calls,optional"`
|
||
}
|
||
|
||
ToolCall struct {
|
||
Index int `json:"index"`
|
||
Id string `json:"id"`
|
||
Type string `json:"type"`
|
||
Function FunctionCall `json:"function"`
|
||
}
|
||
|
||
FunctionCall struct {
|
||
Name string `json:"name"`
|
||
Arguments string `json:"arguments"`
|
||
}
|
||
|
||
Usage struct {
|
||
PromptTokens int `json:"prompt_tokens"`
|
||
CompletionTokens int `json:"completion_tokens"`
|
||
TotalTokens int `json:"total_tokens"`
|
||
}
|
||
|
||
// Anthropic Messages
|
||
AnthropicRequest {
|
||
Model string `json:"model"`
|
||
Messages []Message `json:"messages"`
|
||
MaxTokens int `json:"max_tokens"`
|
||
Stream bool `json:"stream,optional"`
|
||
System string `json:"system,optional"`
|
||
}
|
||
|
||
AnthropicResponse {
|
||
Id string `json:"id"`
|
||
Type string `json:"type"`
|
||
Role string `json:"role"`
|
||
Content []ContentBlock `json:"content"`
|
||
Model string `json:"model"`
|
||
Usage AnthropicUsage `json:"usage"`
|
||
}
|
||
|
||
ContentBlock {
|
||
Type string `json:"type"`
|
||
Text string `json:"text,optional"`
|
||
}
|
||
|
||
AnthropicUsage struct {
|
||
InputTokens int `json:"input_tokens"`
|
||
OutputTokens int `json:"output_tokens"`
|
||
}
|
||
)
|
||
|
||
// ============ Routes ============
|
||
|
||
@server(
|
||
prefix: /v1
|
||
group: chat
|
||
)
|
||
service chat-api {
|
||
@doc("Health check")
|
||
@handler Health
|
||
get /health returns (HealthResponse)
|
||
|
||
@doc("List available models")
|
||
@handler Models
|
||
get /v1/models returns (ModelsResponse)
|
||
|
||
@doc("Chat completions (OpenAI compatible)")
|
||
@handler ChatCompletions
|
||
post /v1/chat/completions (ChatCompletionRequest)
|
||
|
||
@doc("Anthropic messages")
|
||
@handler AnthropicMessages
|
||
post /v1/messages (AnthropicRequest)
|
||
}
|
||
```
|
||
|
||
#### Step 1.3: 生成代碼骨架
|
||
|
||
```bash
|
||
goctl api go -api api/chat.api -dir . --style go_zero
|
||
```
|
||
|
||
#### Step 1.4: 建立 `etc/chat.yaml`
|
||
|
||
```yaml
|
||
Name: chat-api
|
||
Host: ${CURSOR_BRIDGE_HOST:0.0.0.0}
|
||
Port: ${CURSOR_BRIDGE_PORT:8080}
|
||
|
||
# API Key 驗證(可選)
|
||
Auth:
|
||
AccessSecret: ${CURSOR_API_KEY:}
|
||
AccessExpire: 86400
|
||
|
||
# Cursor 配置
|
||
AgentBin: ${CURSOR_AGENT_BIN:cursor-agent}
|
||
DefaultModel: ${CURSOR_DEFAULT_MODEL:claude-3.5-sonnet}
|
||
Provider: ${CURSOR_PROVIDER:cursor}
|
||
|
||
# 超時設定
|
||
TimeoutMs: ${CURSOR_TIMEOUT_MS:300000}
|
||
|
||
# 多帳號池
|
||
ConfigDirs:
|
||
- ${HOME}/.cursor-api-proxy/accounts/default
|
||
MultiPort: false
|
||
|
||
# TLS
|
||
TLSCertPath: ${CURSOR_TLS_CERT_PATH:}
|
||
TLSKeyPath: ${CURSOR_TLS_KEY_PATH:}
|
||
|
||
# 日誌
|
||
SessionsLogPath: ${CURSOR_SESSIONS_LOG_PATH:}
|
||
Verbose: ${CURSOR_VERBOSE:false}
|
||
|
||
# Gemini 設定
|
||
GeminiAccountDir: ${GEMINI_ACCOUNT_DIR:}
|
||
GeminiBrowserVisible: ${GEMINI_BROWSER_VISIBLE:false}
|
||
GeminiMaxSessions: ${GEMINI_MAX_SESSIONS:10}
|
||
|
||
# 工作區設定
|
||
Workspace: ${CURSOR_WORKSPACE:}
|
||
ChatOnlyWorkspace: ${CURSOR_CHAT_ONLY_WORKSPACE:true}
|
||
WinCmdlineMax: ${CURSOR_WIN_CMDLINE_MAX:32768}
|
||
|
||
# Agent 設定
|
||
Force: ${CURSOR_FORCE:false}
|
||
ApproveMcps: ${CURSOR_APPROVE_MCPS:false}
|
||
MaxMode: ${CURSOR_MAX_MODE:false}
|
||
StrictModel: ${CURSOR_STRICT_MODEL:true}
|
||
```
|
||
|
||
#### Step 1.5: 移動生成的檔案
|
||
|
||
```bash
|
||
# goctl 生成 chat.go 到根目錄,需移動到 cmd/
|
||
mkdir -p cmd/chat
|
||
mv chat.go cmd/chat/chat.go
|
||
```
|
||
|
||
---
|
||
|
||
### Phase 2: Domain 層建立
|
||
|
||
**目標**:建立 `pkg/domain/` 目錄結構
|
||
|
||
#### Step 2.1: 建立實體
|
||
|
||
```bash
|
||
mkdir -p pkg/domain/entity
|
||
```
|
||
|
||
**檔案對照表:**
|
||
|
||
| 新檔案 | 來源 |
|
||
|--------|------|
|
||
| `pkg/domain/entity/message.go` | `internal/apitypes/types.go` |
|
||
| `pkg/domain/entity/chunk.go` | `internal/apitypes/types.go` (StreamChunk) |
|
||
| `pkg/domain/entity/account.go` | `internal/pool/pool.go` (AccountStat) |
|
||
| `pkg/domain/entity/config.go` | `internal/config/config.go` (部分) |
|
||
|
||
#### Step 2.2: 建立倉儲介面
|
||
|
||
```bash
|
||
mkdir -p pkg/domain/repository
|
||
```
|
||
|
||
**檔案:**
|
||
|
||
| 新檔案 | 說明 |
|
||
|--------|------|
|
||
| `pkg/domain/repository/account.go` | AccountPool interface (從 pool.go 抽取) |
|
||
| `pkg/domain/repository/provider.go` | Provider interface (從 providers/factory.go 抽取) |
|
||
|
||
#### Step 2.3: 建立用例介面
|
||
|
||
```bash
|
||
mkdir -p pkg/domain/usecase
|
||
```
|
||
|
||
**檔案:**
|
||
|
||
| 新檔案 | 說明 |
|
||
|--------|------|
|
||
| `pkg/domain/usecase/chat.go` | ChatUsecase interface |
|
||
| `pkg/domain/usecase/agent.go` | AgentRunner interface |
|
||
|
||
#### Step 2.4: 建立常數
|
||
|
||
```bash
|
||
mkdir -p pkg/domain/const
|
||
```
|
||
|
||
**檔案:**
|
||
|
||
| 新檔案 | 來源 |
|
||
|--------|------|
|
||
| `pkg/domain/const/models.go` | `internal/models/cursormap.go` |
|
||
| `pkg/domain/const/errors.go` | 新增錯誤定義 |
|
||
|
||
---
|
||
|
||
### Phase 3: Infrastructure 層建立
|
||
|
||
**目標**:建立 `pkg/infrastructure/` 基礎設施層
|
||
|
||
#### Step 3.1: 建立目錄結構
|
||
|
||
```bash
|
||
mkdir -p pkg/infrastructure/{process,parser,httputil,logger,env,workspace,winlimit}
|
||
```
|
||
|
||
#### Step 3.2: 遷移基礎設施模組
|
||
|
||
| 原始 | 目標 |
|
||
|------|------|
|
||
| `internal/process/process.go` | `pkg/infrastructure/process/runner.go` |
|
||
| `internal/process/kill_unix.go` | `pkg/infrastructure/process/kill_unix.go` |
|
||
| `internal/process/kill_windows.go` | `pkg/infrastructure/process/kill_windows.go` |
|
||
| `internal/parser/stream.go` | `pkg/infrastructure/parser/stream.go` |
|
||
| `internal/httputil/httputil.go` | `pkg/infrastructure/httputil/httputil.go` |
|
||
| `internal/logger/logger.go` | `pkg/infrastructure/logger/logger.go` |
|
||
| `internal/env/env.go` | `pkg/infrastructure/env/env.go` |
|
||
| `internal/workspace/workspace.go` | `pkg/infrastructure/workspace/workspace.go` |
|
||
| `internal/winlimit/winlimit.go` | `pkg/infrastructure/winlimit/winlimit.go` |
|
||
|
||
---
|
||
|
||
### Phase 4: Repository 層實作
|
||
|
||
**目標**:實作 `pkg/repository/`
|
||
|
||
#### Step 4.1: 建立目錄
|
||
|
||
```bash
|
||
mkdir -p pkg/repository
|
||
```
|
||
|
||
#### Step 4.2: 遷移檔案
|
||
|
||
| 原始 | 目標 |
|
||
|------|------|
|
||
| `internal/pool/pool.go` | `pkg/repository/account.go` (AccountPool 實作) |
|
||
| `internal/providers/factory.go` | `pkg/repository/provider.go` (Provider 工廠) |
|
||
|
||
---
|
||
|
||
### Phase 5: Provider 層建立
|
||
|
||
**目標**:遷移 Provider 實作
|
||
|
||
#### Step 5.1: 建立目錄
|
||
|
||
```bash
|
||
mkdir -p pkg/provider/cursor pkg/provider/geminiweb
|
||
```
|
||
|
||
#### Step 5.2: 遷移檔案
|
||
|
||
| 原始 | 目標 |
|
||
|------|------|
|
||
| `internal/providers/cursor/provider.go` | `pkg/provider/cursor/provider.go` |
|
||
| `internal/providers/geminiweb/provider.go` | `pkg/provider/geminiweb/provider.go` |
|
||
| `internal/providers/geminiweb/browser.go` | `pkg/provider/geminiweb/browser.go` |
|
||
| `internal/providers/geminiweb/pool.go` | `pkg/provider/geminiweb/pool.go` |
|
||
| `internal/providers/geminiweb/browser_manager.go` | `pkg/provider/geminiweb/browser_manager.go` |
|
||
| `internal/providers/geminiweb/page.go` | `pkg/provider/geminiweb/page.go` |
|
||
| `internal/providers/geminiweb/playwright_provider.go` | `pkg/provider/geminiweb/playwright_provider.go` |
|
||
|
||
---
|
||
|
||
### Phase 6: Usecase 層建立
|
||
|
||
**目標**:實作業務邏輯層
|
||
|
||
#### Step 6.1: 建立目錄
|
||
|
||
```bash
|
||
mkdir -p pkg/usecase
|
||
```
|
||
|
||
#### Step 6.2: 建立檔案
|
||
|
||
| 新檔案 | 說明 | 來源 |
|
||
|--------|------|------|
|
||
| `pkg/usecase/chat.go` | 核心聊天邏輯 | 從 handlers/chat.go 抽取 |
|
||
| `pkg/usecase/agent.go` | Agent 執行邏輯 | `internal/agent/runner.go` |
|
||
| `pkg/usecase/sanitizer.go` | 消息清理 | `internal/sanitize/sanitize.go` |
|
||
| `pkg/usecase/toolcall.go` | Tool call 處理 | `internal/toolcall/toolcall.go` |
|
||
|
||
#### Step 6.3: Agent 相關檔案遷移
|
||
|
||
| 原始 | 目標 |
|
||
|------|------|
|
||
| `internal/agent/runner.go` | `pkg/usecase/runner.go` |
|
||
| `internal/agent/token.go` | `pkg/usecase/token.go` |
|
||
| `internal/agent/cmdargs.go` | `pkg/usecase/cmdargs.go` |
|
||
| `internal/agent/maxmode.go` | `pkg/usecase/maxmode.go` |
|
||
|
||
---
|
||
|
||
### Phase 7: Adapter 層建立
|
||
|
||
**目標**:API 格式轉換器
|
||
|
||
#### Step 7.1: 建立目錄
|
||
|
||
```bash
|
||
mkdir -p pkg/adapter/openai pkg/adapter/anthropic
|
||
```
|
||
|
||
#### Step 7.2: 遷移檔案
|
||
|
||
| 原始 | 目標 |
|
||
|------|------|
|
||
| `internal/openai/*.go` | `pkg/adapter/openai/*.go` |
|
||
| `internal/anthropic/*.go` | `pkg/adapter/anthropic/*.go` |
|
||
|
||
---
|
||
|
||
### Phase 8: Internal 層重組
|
||
|
||
**目標**:建立 go-zero 框架層
|
||
|
||
#### Step 8.1: 更新 Config
|
||
|
||
修改 `internal/config/config.go` 使用 go-zero 的 `rest.RestConf`:
|
||
|
||
```go
|
||
package config
|
||
|
||
import "github.com/zeromicro/go-zero/rest"
|
||
|
||
type Config struct {
|
||
rest.RestConf
|
||
|
||
// Cursor 配置
|
||
AgentBin string
|
||
DefaultModel string
|
||
Provider string
|
||
TimeoutMs int
|
||
|
||
// 多帳號池
|
||
ConfigDirs []string
|
||
MultiPort bool
|
||
|
||
// TLS
|
||
TLSCertPath string
|
||
TLSKeyPath string
|
||
|
||
// 日誌
|
||
SessionsLogPath string
|
||
Verbose bool
|
||
|
||
// Gemini
|
||
GeminiAccountDir string
|
||
GeminiBrowserVisible bool
|
||
GeminiMaxSessions int
|
||
|
||
// 工作區
|
||
Workspace string
|
||
ChatOnlyWorkspace bool
|
||
WinCmdlineMax int
|
||
|
||
// Agent
|
||
Force bool
|
||
ApproveMcps bool
|
||
MaxMode bool
|
||
StrictModel bool
|
||
}
|
||
```
|
||
|
||
#### Step 8.2: 建立 ServiceContext
|
||
|
||
```bash
|
||
mkdir -p internal/svc
|
||
```
|
||
|
||
建立 `internal/svc/servicecontext.go`:
|
||
|
||
```go
|
||
package svc
|
||
|
||
import (
|
||
"cursor-api-proxy/internal/config"
|
||
"cursor-api-proxy/pkg/domain/repository"
|
||
"cursor-api-proxy/pkg/provider"
|
||
"cursor-api-proxy/pkg/repository"
|
||
"cursor-api-proxy/pkg/usecase"
|
||
)
|
||
|
||
type ServiceContext struct {
|
||
Config config.Config
|
||
|
||
// Domain
|
||
AccountPool repository.AccountPool
|
||
ChatUsecase usecase.ChatUsecase
|
||
|
||
// Provider
|
||
Provider repository.Provider
|
||
}
|
||
|
||
func NewServiceContext(c config.Config) *ServiceContext {
|
||
accountPool := repository.NewAccountPool(c.ConfigDirs)
|
||
prov := provider.NewProvider(c)
|
||
chatUsecase := usecase.NewChatUsecase(accountPool, prov, c)
|
||
|
||
return &ServiceContext{
|
||
Config: c,
|
||
AccountPool: accountPool,
|
||
ChatUsecase: chatUsecase,
|
||
Provider: prov,
|
||
}
|
||
}
|
||
```
|
||
|
||
#### Step 8.3: 建立 Logic 層
|
||
|
||
```bash
|
||
mkdir -p internal/logic
|
||
```
|
||
|
||
**SSE 自訂 Handler 說明:**
|
||
|
||
由於 go-zero 標準 handler 回傳 JSON,但專案需要 SSE streaming,所以 Logic 層需要自訂處理:
|
||
|
||
```go
|
||
// internal/logic/chatcompletionlogic.go
|
||
func (l *ChatCompletionLogic) ChatCompletion(req *types.ChatCompletionRequest) error {
|
||
// 設定 SSE headers
|
||
l.w.Header().Set("Content-Type", "text/event-stream")
|
||
l.w.Header().Set("Cache-Control", "no-cache")
|
||
l.w.Header().Set("Connection", "keep-alive")
|
||
|
||
flusher, ok := l.w.(http.Flusher)
|
||
if !ok {
|
||
return errors.New("streaming not supported")
|
||
}
|
||
|
||
// 委託給 usecase 處理串流
|
||
err := l.svcCtx.ChatUsecase.Stream(l.ctx, usecase.ChatInput{
|
||
Model: req.Model,
|
||
Messages: l.convertMessages(req.Messages),
|
||
Tools: l.convertTools(req.Tools),
|
||
Stream: req.Stream,
|
||
}, func(chunk entity.StreamChunk) {
|
||
l.writeChunk(chunk)
|
||
flusher.Flush()
|
||
})
|
||
|
||
return err
|
||
}
|
||
```
|
||
|
||
#### Step 8.4: 建立 Handler 層
|
||
|
||
```bash
|
||
mkdir -p internal/handler
|
||
```
|
||
|
||
**自訂 Handler 處理 SSE:**
|
||
|
||
由於需要 SSE streaming,Handler 需要:
|
||
1. 不使用 goctl 的標準模板
|
||
2. 自訂 `http.HandlerFunc` 處理 SSE
|
||
3. 在 `routes.go` 中註冊自訂 handler
|
||
|
||
#### Step 8.5: 建立 Middleware
|
||
|
||
```bash
|
||
mkdir -p internal/middleware
|
||
```
|
||
|
||
| 新檔案 | 說明 |
|
||
|--------|------|
|
||
| `internal/middleware/auth.go` | API Key 驗證 |
|
||
| `internal/middleware/recovery.go` | Panic 恢復(從 router.go 抽取) |
|
||
|
||
---
|
||
|
||
### Phase 9: CLI 工具遷移
|
||
|
||
**目標**:保留 CLI 工具在 `cmd/cli/`
|
||
|
||
#### Step 9.1: 建立目錄
|
||
|
||
```bash
|
||
mkdir -p cmd/cli
|
||
```
|
||
|
||
#### Step 9.2: 遷移檔案
|
||
|
||
| 原始 | 目標 |
|
||
|------|------|
|
||
| `cmd/args.go` | `cmd/cli/args.go` |
|
||
| `cmd/login.go` | `cmd/cli/login.go` |
|
||
| `cmd/accounts.go` | `cmd/cli/accounts.go` |
|
||
| `cmd/resethwid.go` | `cmd/cli/resethwid.go` |
|
||
| `cmd/usage.go` | `cmd/cli/usage.go` |
|
||
| `cmd/sqlite.go` | `cmd/cli/sqlite.go` |
|
||
| `cmd/gemini-login/main.go` | `cmd/cli/gemini_login.go` |
|
||
|
||
---
|
||
|
||
### Phase 10: 清理與測試
|
||
|
||
#### Step 10.1: 移除舊目錄
|
||
|
||
確認所有遷移完成後:
|
||
|
||
```bash
|
||
# 移除舊的 internal 目錄(保留已遷移的)
|
||
rm -rf internal/router
|
||
rm -rf internal/handlers
|
||
rm -rf internal/providers
|
||
rm -rf internal/apitypes
|
||
rm -rf internal/pool
|
||
rm -rf internal/agent
|
||
rm -rf internal/parser
|
||
rm -rf internal/toolcall
|
||
rm -rf internal/sanitize
|
||
rm -rf internal/openai
|
||
rm -rf internal/anthropic
|
||
rm -rf internal/process
|
||
rm -rf internal/models
|
||
rm -rf internal/workspace
|
||
rm -rf internal/winlimit
|
||
rm -rf internal/httputil
|
||
rm -rf internal/logger
|
||
rm -rf internal/env
|
||
rm -rf internal/server
|
||
|
||
# 移除舊的 main.go
|
||
rm main.go
|
||
|
||
# 移除舊的 cmd 目錄(已遷移到 cmd/cli)
|
||
rm -rf cmd/gemini-login
|
||
```
|
||
|
||
#### Step 10.2: 更新 import 路徑
|
||
|
||
```bash
|
||
# 批量更新 import 路徑
|
||
# internal/ → pkg/ (domain 相關)
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/apitypes|cursor-api-proxy/pkg/domain/entity|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/pool|cursor-api-proxy/pkg/repository|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/agent|cursor-api-proxy/pkg/usecase|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/providers|cursor-api-proxy/pkg/provider|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/parser|cursor-api-proxy/pkg/infrastructure/parser|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/toolcall|cursor-api-proxy/pkg/usecase|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/sanitize|cursor-api-proxy/pkg/usecase|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/openai|cursor-api-proxy/pkg/adapter/openai|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/anthropic|cursor-api-proxy/pkg/adapter/anthropic|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/process|cursor-api-proxy/pkg/infrastructure/process|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/models|cursor-api-proxy/pkg/domain/const|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/workspace|cursor-api-proxy/pkg/infrastructure/workspace|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/winlimit|cursor-api-proxy/pkg/infrastructure/winlimit|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/httputil|cursor-api-proxy/pkg/infrastructure/httputil|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/logger|cursor-api-proxy/pkg/infrastructure/logger|g' {} \;
|
||
find . -name "*.go" -exec sed -i '' 's|cursor-api-proxy/internal/env|cursor-api-proxy/pkg/infrastructure/env|g' {} \;
|
||
```
|
||
|
||
#### Step 10.3: 測試
|
||
|
||
```bash
|
||
# 單元測試
|
||
go test ./pkg/domain/... -v
|
||
go test ./pkg/usecase/... -v
|
||
go test ./pkg/repository/... -v
|
||
|
||
# 整合測試
|
||
go test ./internal/... -v
|
||
|
||
# 建置測試
|
||
go build ./cmd/chat
|
||
go build ./cmd/cli
|
||
|
||
# 功能測試
|
||
make env
|
||
./cursor-api-proxy -f etc/chat.yaml &
|
||
curl http://localhost:8080/health
|
||
curl http://localhost:8080/v1/models
|
||
```
|
||
|
||
---
|
||
|
||
## 四、漸進式遷移順序
|
||
|
||
### 每日執行計劃
|
||
|
||
| 階段 | 任務 | 預估時間 | 依賴 |
|
||
|------|------|---------|------|
|
||
| Day 1 | Phase 1: API 定義 | 2-3 小時 | 無 |
|
||
| Day 2 | Phase 2: Domain 層 | 2-3 小時 | Phase 1 |
|
||
| Day 3 | Phase 3: Infrastructure 層 | 2-3 小時 | Phase 2 |
|
||
| Day 4 | Phase 4: Repository 層 | 2-3 小時 | Phase 3 |
|
||
| Day 5 | Phase 5: Provider 層 | 2-3 小時 | Phase 4 |
|
||
| Day 6 | Phase 6: Usecase 層 | 3-4 小時 | Phase 5 |
|
||
| Day 7 | Phase 7: Adapter 層 | 2 小時 | Phase 6 |
|
||
| Day 8 | Phase 8: Internal 層 | 4-5 小時 | Phase 7 |
|
||
| Day 9 | Phase 9: CLI 工具 | 1-2 小時 | Phase 8 |
|
||
| Day 10 | Phase 10: 清理與測試 | 3-4 小時 | Phase 9 |
|
||
|
||
### 漸進式測試策略
|
||
|
||
每個 Phase 完成後執行:
|
||
|
||
1. **單元測試**:確保新模組測試通過
|
||
2. **編譯測試**:確保代碼可編譯
|
||
3. **功能測試**:確保核心功能正常
|
||
|
||
```bash
|
||
# 每日測試腳本
|
||
#!/bin/bash
|
||
set -e
|
||
|
||
echo "=== Running unit tests ==="
|
||
go test ./pkg/... -v
|
||
|
||
echo "=== Building ==="
|
||
go build ./cmd/chat
|
||
go build ./cmd/cli
|
||
|
||
echo "=== Starting server ==="
|
||
./cursor-api-proxy -f etc/chat.yaml &
|
||
PID=$!
|
||
sleep 2
|
||
|
||
echo "=== Running functional tests ==="
|
||
curl -s http://localhost:8080/health | jq .
|
||
curl -s http://localhost:8080/v1/models | jq .
|
||
|
||
echo "=== Cleaning up ==="
|
||
kill $PID
|
||
|
||
echo "=== All tests passed ==="
|
||
```
|
||
|
||
---
|
||
|
||
## 五、回歸測試清單
|
||
|
||
### 功能測試
|
||
|
||
- [ ] Health check: `GET /health`
|
||
- [ ] Models list: `GET /v1/models`
|
||
- [ ] Chat completions (非串流): `POST /v1/chat/completions` with `stream: false`
|
||
- [ ] Chat completions (串流): `POST /v1/chat/completions` with `stream: true`
|
||
- [ ] Anthropic messages: `POST /v1/messages`
|
||
- [ ] Gemini chat completions: `POST /v1/chat/completions` (Provider=gemini-web)
|
||
- [ ] Tool calls 處理
|
||
- [ ] Rate limit 處理
|
||
- [ ] Multi-account 輪替
|
||
- [ ] API Key 驗證
|
||
- [ ] TLS/HTTPS 支援
|
||
|
||
### CLI 工具測試
|
||
|
||
- [ ] `login` 命令
|
||
- [ ] `accounts` 命令
|
||
- [ ] `resethwid` 命令
|
||
- [ ] `usage` 命令
|
||
- [ ] `gemini-login` 命令
|
||
|
||
### 效能測試
|
||
|
||
- [ ] SSE streaming latency
|
||
- [ ] Concurrent requests (10, 50, 100)
|
||
- [ ] Memory usage baseline
|
||
- [ ] Long-running stability
|
||
|
||
---
|
||
|
||
## 六、風險與注意事項
|
||
|
||
### 高風險項目
|
||
|
||
| 風險 | 描述 | 緩解措施 |
|
||
|------|------|---------|
|
||
| SSE Streaming | go-zero 標準 handler 不支援 SSE | 自訂 handler,在 Logic 層處理 SSE |
|
||
| Context 傳遞 | 需確保 context 正確傳遞到 usecase 層 | 使用 context.Context 作為參數傳遞 |
|
||
| 全局變數 | pool.go 的 global pool 需改為 DI | 使用 ServiceContext 注入 |
|
||
| 環境變數 | 需從 `CURSOR_*` 遷移到 go-zero 配置 | 保持環境變數支援,使用 `${VAR:default}` 語法 |
|
||
|
||
### 注意事項
|
||
|
||
1. **保持向後相容**:API 端點和行為必須與現有版本一致
|
||
2. **環境變數相容**:保持現有環境變數名稱
|
||
3. **Makefile 更新**:更新建置命令
|
||
4. **日誌格式**:保持現有日誌格式以便分析工具相容
|
||
|
||
---
|
||
|
||
## 七、Makefile 更新
|
||
|
||
需要新增 go-zero 相關命令:
|
||
|
||
```makefile
|
||
# go-zero 代碼生成
|
||
.PHONY: api
|
||
api:
|
||
goctl api go -api api/chat.api -dir . --style go_zero
|
||
|
||
.PHONY: api-doc
|
||
api-doc:
|
||
goctl api doc -api api/chat.api -dir docs/
|
||
|
||
# 格式化
|
||
.PHONY: fmt
|
||
fmt:
|
||
go fmt ./...
|
||
|
||
# 測試
|
||
.PHONY: test
|
||
test:
|
||
go test ./... -v -race
|
||
|
||
# 建置
|
||
.PHONY: build
|
||
build:
|
||
go build -o bin/chat-api cmd/chat/chat.go
|
||
|
||
# 運行
|
||
.PHONY: run
|
||
run:
|
||
./bin/chat-api -f etc/chat.yaml
|
||
```
|
||
|
||
---
|
||
|
||
## 八、參考資源
|
||
|
||
- [go-zero 官方文檔](https://go-zero.dev/)
|
||
- [go-zero API 定義](https://go-zero.dev/docs/tutorial/api/)
|
||
- [go-zero 最佳實踐](https://go-zero.dev/docs/tutorials/)
|
||
- [zero-skills](https://github.com/zeromicro/zero-skills)
|
||
- [ai-context](https://github.com/zeromicro/ai-context)
|
||
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/33/the-clean-architecture.html)
|
||
|
||
---
|
||
|
||
## 九、附錄:檔案遷移對照表
|
||
|
||
### 完整檔案對照表
|
||
|
||
| 原始 | 目標 | 說明 |
|
||
|------|------|------|
|
||
| `main.go` | `cmd/chat/chat.go` | HTTP 服務入口 |
|
||
| `cmd/args.go` | `cmd/cli/args.go` | CLI 參數 |
|
||
| `cmd/login.go` | `cmd/cli/login.go` | CLI login |
|
||
| `cmd/accounts.go` | `cmd/cli/accounts.go` | CLI accounts |
|
||
| `cmd/resethwid.go` | `cmd/cli/resethwid.go` | CLI resethwid |
|
||
| `cmd/usage.go` | `cmd/cli/usage.go` | CLI usage |
|
||
| `cmd/sqlite.go` | `cmd/cli/sqlite.go` | SQLite 操作 |
|
||
| `cmd/gemini-login/main.go` | `cmd/cli/gemini_login.go` | Gemini CLI |
|
||
| `internal/apitypes/types.go` | `pkg/domain/entity/message.go` | Message 實體 |
|
||
| `internal/pool/pool.go` | `pkg/repository/account.go` | AccountPool 實作 |
|
||
| `internal/agent/runner.go` | `pkg/usecase/runner.go` | Agent 執行 |
|
||
| `internal/agent/token.go` | `pkg/usecase/token.go` | Token 處理 |
|
||
| `internal/agent/cmdargs.go` | `pkg/usecase/cmdargs.go` | 命令參數 |
|
||
| `internal/agent/maxmode.go` | `pkg/usecase/maxmode.go` | Max mode |
|
||
| `internal/providers/factory.go` | `pkg/repository/provider.go` | Provider 工廠 |
|
||
| `internal/providers/cursor/provider.go` | `pkg/provider/cursor/provider.go` | Cursor provider |
|
||
| `internal/providers/geminiweb/*.go` | `pkg/provider/geminiweb/*.go` | Gemini provider |
|
||
| `internal/parser/stream.go` | `pkg/infrastructure/parser/stream.go` | SSE parsing |
|
||
| `internal/toolcall/toolcall.go` | `pkg/usecase/toolcall.go` | Tool call |
|
||
| `internal/sanitize/sanitize.go` | `pkg/usecase/sanitize.go` | 消息清理 |
|
||
| `internal/openai/*.go` | `pkg/adapter/openai/*.go` | OpenAI 格式 |
|
||
| `internal/anthropic/*.go` | `pkg/adapter/anthropic/*.go` | Anthropic 格式 |
|
||
| `internal/process/process.go` | `pkg/infrastructure/process/runner.go` | 進程管理 |
|
||
| `internal/process/kill_*.go` | `pkg/infrastructure/process/kill_*.go` | Kill 實作 |
|
||
| `internal/models/cursormap.go` | `pkg/domain/const/models.go` | Model 映射 |
|
||
| `internal/workspace/workspace.go` | `pkg/infrastructure/workspace/workspace.go` | 工作區 |
|
||
| `internal/winlimit/winlimit.go` | `pkg/infrastructure/winlimit/winlimit.go` | Win limit |
|
||
| `internal/httputil/httputil.go` | `pkg/infrastructure/httputil/httputil.go` | HTTP 工具 |
|
||
| `internal/logger/logger.go` | `pkg/infrastructure/logger/logger.go` | 日誌 |
|
||
| `internal/env/env.go` | `pkg/infrastructure/env/env.go` | 環境變數 |
|
||
| `internal/config/config.go` | `internal/config/config.go` | 配置(使用 go-zero Config) |
|
||
| `internal/router/router.go` | `internal/handler/routes.go` | 路由(重寫) |
|
||
| `internal/handlers/*.go` | `internal/logic/*.go` | 業務邏輯 |
|
||
|
||
---
|
||
|
||
**文件版本**:v1.0
|
||
**建立日期**:2026-04-03
|
||
**最後更新**:2026-04-03 |