diff --git a/Makefile b/Makefile index 152f528..4ff0fbe 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,26 @@ # 編輯下方變數,然後執行 make env 產生 .env 檔 # ────────────────────────────────────────────── +# ── go-zero 代碼生成 ─────────────────────────── +.PHONY: api api-doc gen fmt lint + +api: + goctl api go -api api/chat.api -dir . --style go_zero + +api-doc: + goctl api doc -api api/chat.api -dir docs/api/ + +gen: api + go mod tidy + go fmt ./... + +fmt: + go fmt ./... + +lint: + go vet ./... + go fmt ./... + # ── 伺服器設定 ───────────────────────────────── HOST ?= 127.0.0.1 PORT ?= 8766 diff --git a/api/chat.api b/api/chat.api new file mode 100644 index 0000000..a02cf5b --- /dev/null +++ b/api/chat.api @@ -0,0 +1,141 @@ +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 { + Role string `json:"role,optional"` + Content string `json:"content,optional"` + ReasoningContent string `json:"reasoning_content,optional"` + ToolCalls []ToolCall `json:"tool_calls,optional"` + } + ToolCall { + Index int `json:"index"` + Id string `json:"id"` + Type string `json:"type"` + Function FunctionCall `json:"function"` + } + FunctionCall { + Name string `json:"name"` + Arguments string `json:"arguments"` + } + Usage { + 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 { + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + } +) + +// ============ Routes ============ +@server ( + prefix: /v1 + group: chat +) +service chat-api { + @handler Health + get /health returns (HealthResponse) + + @handler Models + get /v1/models returns (ModelsResponse) + + @handler ChatCompletions + post /v1/chat/completions (ChatCompletionRequest) + + @handler AnthropicMessages + post /v1/messages (AnthropicRequest) +} + diff --git a/cmd/chat/chat.go b/cmd/chat/chat.go new file mode 100644 index 0000000..7d59dea --- /dev/null +++ b/cmd/chat/chat.go @@ -0,0 +1,34 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package main + +import ( + "flag" + "fmt" + + "cursor-api-proxy/internal/config" + "cursor-api-proxy/internal/handler" + "cursor-api-proxy/internal/svc" + + "github.com/zeromicro/go-zero/core/conf" + "github.com/zeromicro/go-zero/rest" +) + +var configFile = flag.String("f", "etc/chat-api.yaml", "the config file") + +func main() { + flag.Parse() + + var c config.Config + conf.MustLoad(*configFile, &c) + + server := rest.MustNewServer(c.RestConf) + defer server.Stop() + + ctx := svc.NewServiceContext(c) + handler.RegisterHandlers(server, ctx) + + fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port) + server.Start() +} diff --git a/docs/MIGRATION_PLAN.md b/docs/MIGRATION_PLAN.md new file mode 100644 index 0000000..0dff93d --- /dev/null +++ b/docs/MIGRATION_PLAN.md @@ -0,0 +1,1069 @@ +# 重構計劃: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/ # +│ │ ├── 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/ # +│ │ ├── account.go # AccountPool 實作 +│ │ └── provider.go # Provider 工廠 +│ │ +│ ├── usecase/ # +│ │ ├── 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 \ No newline at end of file diff --git a/docs/REFACTOR_TASKS.md b/docs/REFACTOR_TASKS.md new file mode 100644 index 0000000..5c0b5c9 --- /dev/null +++ b/docs/REFACTOR_TASKS.md @@ -0,0 +1,462 @@ +# REFACTOR TASKS + +重構任務拆分,支援 git worktree 並行開發。 + +--- + +## Task Overview + +### 並行策略 + +``` +時間軸 ──────────────────────────────────────────────────────────────► + +Task 0: Init (必須先完成) + │ + ├── Task 1: Domain Layer ─────────────────────────┐ + │ │ + │ ┌── Task 2: Infrastructure Layer ────────────┤── 並行 + │ │ │ + │ └── Task 3: Repository Layer ────────────────┘ + │ (依賴 Task 1) + │ + ├── Task 4: Provider Layer ──────────────────────┐ + │ (依賴 Task 1) │ + │ │── 可並行 + ├── Task 5: Usecase Layer ───────────────────────┤ + │ (依賴 Task 3) │ + │ │ + ├── Task 6: Adapter Layer ───────────────────────┘ + │ (依賴 Task 1) + │ + ├── Task 7: Internal Layer ──────────────────────┐ + │ (整合所有,必須最後) │ + │ │── 序列 + ├── Task 8: CLI Tools │ + │ │ + └── Task 9: Cleanup & Tests ────────────────────┘ +``` + +### Worktree 分支規劃 + +| 分支名稱 | 基於 | 任務 | 可並行 | +|---------|------|------|--------| +| `refactor/init` | `master` | Task 0 | ❌ | +| `refactor/domain` | `refactor/init` | Task 1 | ✅ | +| `refactor/infrastructure` | `refactor/init` | Task 2 | ✅ | +| `refactor/repository` | `refactor/domain` | Task 3 | ✅ | +| `refactor/provider` | `refactor/domain` | Task 4 | ✅ | +| `refactor/usecase` | `refactor/repository` | Task 5 | ✅ | +| `refactor/adapter` | `refactor/domain` | Task 6 | ✅ | +| `refactor/internal` | 合併所有 | Task 7 | ❌ | +| `refactor/cli` | `refactor/init` | Task 8 | ✅ | +| `refactor/cleanup` | 合併所有 | Task 9 | ❌ | + +--- + +## Task 0: 初始化 + +### 分支 +`refactor/init` + +### 依賴 +無(必須先完成) + +### 小任務 + +- [ ] **0.1** 更新 go.mod (5min) + - `go get github.com/zeromicro/go-zero@latest` + - `go mod tidy` + +- [ ] **0.2** 建立目錄 (1min) + - `mkdir -p api etc` + +- [ ] **0.3** 建立 `api/chat.api` (15min) + - 定義 API types + - 定義 routes + +- [ ] **0.4** 建立 `etc/chat.yaml` (5min) + - 配置參數 + +- [ ] **0.5** 更新 Makefile (10min) + - 新增 goctl 命令 + +- [ ] **0.6** 提交 (2min) + +**預估時間**: ~30min + +--- + +## Task 1: Domain Layer + +### 分支 +`refactor/domain` + +### 依賴 +Task 0 完成 + +### 小任務 + +- [ ] **1.1** 建立目錄結構 (1min) + - `pkg/domain/entity` + - `pkg/domain/repository` + - `pkg/domain/usecase` + - `pkg/domain/const` + +- [ ] **1.2** `entity/message.go` (10min) + - Message, Tool, ToolFunction, ToolCall + +- [ ] **1.3** `entity/chunk.go` (5min) + - StreamChunk, ChunkType + +- [ ] **1.4** `entity/account.go` (5min) + - Account, AccountStat + +- [ ] **1.5** `repository/account.go` (10min) + - AccountPool interface + +- [ ] **1.6** `repository/provider.go` (5min) + - Provider interface + +- [ ] **1.7** `usecase/chat.go` (15min) + - ChatUsecase interface + +- [ ] **1.8** `usecase/agent.go` (5min) + - AgentRunner interface + +- [ ] **1.9** `const/models.go` (10min) + - Model 常數 + +- [ ] **1.10** `const/errors.go` (5min) + - 錯誤定義 + +- [ ] **1.11** 提交 (2min) + +**預估時間**: ~2h + +--- + +## Task 2: Infrastructure Layer + +### 分支 +`refactor/infrastructure` + +### 依賴 +Task 0 完成(可與 Task 1 並行) + +### 小任務 + +- [ ] **2.1** 建立目錄 (2min) + - `pkg/infrastructure/{process,parser,httputil,logger,env,workspace,winlimit}` + +- [ ] **2.2** 遷移 process (10min) + - runner.go, kill_unix.go, kill_windows.go, process_test.go + +- [ ] **2.3** 遷移 parser (5min) + - stream.go, stream_test.go + +- [ ] **2.4** 遷移 httputil (5min) + - httputil.go, httputil_test.go + +- [ ] **2.5** 遷移 logger (5min) + - logger.go + +- [ ] **2.6** 遷移 env (5min) + - env.go, env_test.go + +- [ ] **2.7** 遷移 workspace (5min) + - workspace.go + +- [ ] **2.8** 遷移 winlimit (5min) + - winlimit.go, winlimit_test.go + +- [ ] **2.9** 驗證編譯 (5min) + +- [ ] **2.10** 提交 (2min) + +**預估時間**: ~1h + +--- + +## Task 3: Repository Layer + +### 分支 +`refactor/repository` + +### 依賴 +Task 1 完成 + +### 小任務 + +- [ ] **3.1** 建立目錄 (1min) + +- [ ] **3.2** 遷移 account.go (20min) + - AccountPool 實作 + - 移除全局變數 + +- [ ] **3.3** 遷移 provider.go (10min) + - Provider 工廠 + +- [ ] **3.4** 遷移測試 (5min) + +- [ ] **3.5** 驗證編譯 (5min) + +- [ ] **3.6** 提交 (2min) + +**預估時間**: ~1h + +--- + +## Task 4: Provider Layer + +### 分支 +`refactor/provider` + +### 依賴 +Task 1 完成 + +### 小任務 + +- [ ] **4.1** 建立目錄 (1min) + - `pkg/provider/cursor` + - `pkg/provider/geminiweb` + +- [ ] **4.2** 遷移 cursor provider (5min) + +- [ ] **4.3** 遷移 geminiweb provider (10min) + +- [ ] **4.4** 更新 import (5min) + +- [ ] **4.5** 驗證編譯 (5min) + +- [ ] **4.6** 提交 (2min) + +**預估時間**: ~30min + +--- + +## Task 5: Usecase Layer + +### 分支 +`refactor/usecase` + +### 依賴 +Task 3 完成 + +### 小任務 + +- [ ] **5.1** 建立目錄 (1min) + +- [ ] **5.2** 建立 chat.go (30min) + - 核心聊天邏輯 + +- [ ] **5.3** 遷移 agent.go (20min) + - runner, token, cmdargs, maxmode + +- [ ] **5.4** 遷移 sanitizer (10min) + +- [ ] **5.5** 遷移 toolcall (10min) + +- [ ] **5.6** 驗證編譯 (5min) + +- [ ] **5.7** 提交 (2min) + +**預估時間**: ~2h + +--- + +## Task 6: Adapter Layer + +### 分支 +`refactor/adapter` + +### 依賴 +Task 1 完成 + +### 小任務 + +- [ ] **6.1** 建立目錄 (1min) + +- [ ] **6.2** 遷移 openai adapter (10min) + +- [ ] **6.3** 遷移 anthropic adapter (10min) + +- [ ] **6.4** 更新 import (5min) + +- [ ] **6.5** 驗證編譯 (5min) + +- [ ] **6.6** 提交 (2min) + +**預估時間**: ~30min + +--- + +## Task 7: Internal Layer + +### 分支 +`refactor/internal` + +### 依賴 +Task 1-6 全部完成 + +### 小任務 + +- [ ] **7.1** 合併所有分支 (5min) + +- [ ] **7.2** 更新 config/config.go (15min) + - 使用 rest.RestConf + +- [ ] **7.3** 建立 svc/servicecontext.go (30min) + - DI 容器 + +- [ ] **7.4** 建立 logic/ (1h) + - chatcompletionlogic.go + - geminichatlogic.go + - anthropiclogic.go + - healthlogic.go + - modelslogic.go + +- [ ] **7.5** 建立 handler/ (1h) + - 自訂 SSE handler + +- [ ] **7.6** 建立 middleware/ (20min) + - auth.go + - recovery.go + +- [ ] **7.7** 建立 types/ (5min) + - goctl 生成 + +- [ ] **7.8** 更新 import (30min) + - 批量更新 + +- [ ] **7.9** 驗證編譯 (10min) + +- [ ] **7.10** 提交 (2min) + +**預估時間**: ~4h + +--- + +## Task 8: CLI Tools + +### 分支 +`refactor/cli` + +### 依賴 +Task 0 完成 + +### 小任務 + +- [ ] **8.1** 建立目錄 (1min) + +- [ ] **8.2** 遷移 CLI 工具 (10min) + +- [ ] **8.3** 遷移 gemini-login (5min) + +- [ ] **8.4** 更新 import (5min) + +- [ ] **8.5** 提交 (2min) + +**預估時間**: ~30min + +--- + +## Task 9: Cleanup & Tests + +### 分支 +`refactor/cleanup` + +### 依賴 +Task 7 完成 + +### 小任務 + +- [ ] **9.1** 移除舊目錄 (5min) + +- [ ] **9.2** 更新 import (30min) + - 批量 sed + +- [ ] **9.3** 建立 cmd/chat/chat.go (10min) + +- [ ] **9.4** SSE 整合測試 (2h) + +- [ ] **9.5** 回歸測試 (1h) + +- [ ] **9.6** 更新 README (15min) + +- [ ] **9.7** 提交 (2min) + +**預估時間**: ~4h + +--- + +## 並行執行計劃 + +### Wave 1 (可完全並行) +``` +Terminal 1: Task 0 (init) → 30min +Terminal 2: (等待 Task 0) +``` + +### Wave 2 (可完全並行) +``` +Terminal 1: Task 1 (domain) → 2h +Terminal 2: Task 2 (infrastructure) → 1h +Terminal 3: Task 8 (cli) → 30min +``` + +### Wave 3 (可部分並行) +``` +Terminal 1: Task 3 (repository) → 1h (依賴 Task 1) +Terminal 2: Task 4 (provider) → 30min (依賴 Task 1) +Terminal 3: Task 6 (adapter) → 30min (依賴 Task 1) +Terminal 4: (等待 Task 3) +``` + +### Wave 4 (可部分並行) +``` +Terminal 1: Task 5 (usecase) → 2h (依賴 Task 3) +Terminal 2: (等待 Task 5) +``` + +### Wave 5 (序列) +``` +Task 7 (internal) → 4h +Task 9 (cleanup) → 4h +``` + +**總時間估計**: +- 完全序列: ~15h +- 並行執行: ~9h +- 節省: ~40% + +--- + +## Git Worktree 指令 + +```bash +# 創建 worktrees +git worktree add ../worktrees/init -b refactor/init +git worktree add ../worktrees/domain -b refactor/domain +git worktree add ../worktrees/infrastructure -b refactor/infrastructure +git worktree add ../worktrees/repository -b refactor/repository +git worktree add ../worktrees/provider -b refactor/provider +git worktree add ../worktrees/usecase -b refactor/usecase +git worktree add ../worktrees/adapter -b refactor/adapter +git worktree add ../worktrees/cli -b refactor/cli + +# 並行工作 +cd ../worktrees/domain && # Terminal 1 +cd ../worktrees/infrastructure && # Terminal 2 +cd ../worktrees/cli && # Terminal 3 + +# 清理 worktrees +git worktree remove ../worktrees/init +git worktree remove ../worktrees/domain +# ... 等等 +``` + +--- + +**文件版本**: v1.0 +**建立日期**: 2026-04-03 \ No newline at end of file diff --git a/docs/TODOS.md b/docs/TODOS.md new file mode 100644 index 0000000..5155b72 --- /dev/null +++ b/docs/TODOS.md @@ -0,0 +1,312 @@ +# TODOS + +重構 cursor-api-proxy → go-zero + DDD Architecture 的待辦事項。 + +--- + +## Phase 1: API 定義與骨架生成 + +### DONE +- [x] 建立 `api/chat.api` 定義檔 +- [x] 建立 `etc/chat.yaml` 配置檔 +- [x] 生成代碼骨架 +- [x] 移動 `chat.go` 到 `cmd/chat/` + +### TODO + +#### TODO-1: 全局變數遷移清單 +- **What**: 建立全局變數到 ServiceContext 的遷移清單 +- **Why**: 現有代碼有多個全局變數,遷移時容易遺漏 +- **Files**: + - `internal/pool/pool.go:36-38` → `globalPool`, `globalMu` → ServiceContext + - `internal/process/process.go:117` → `MaxModeFn` → ServiceContext + - `internal/handlers/chat.go:28-29` → `rateLimitRe`, `retryAfterRe` → ServiceContext 或常數 + - `internal/models/cursormap.go:8,47,51` → 正則表達式常數化 +- **Decision**: ServiceContext 注入 +- **Effort**: human ~2h / CC ~30min +- **Depends on**: Phase 2 (Domain 層建立) +- **Status**: pending + +#### TODO-2: go.mod 更新 +- **What**: 添加 go-zero 依賴到 go.mod +- **Why**: 現有 go.mod 沒有 go-zero 依賴 +- **Command**: `go get github.com/zeromicro/go-zero@latest` +- **Decision**: 使用最新穩定版 +- **Effort**: human ~5min / CC ~1min +- **Depends on**: Phase 1 開始前 +- **Status**: pending + +#### TODO-3: Makefile 更新 +- **What**: 更新 Makefile 以支援 go-zero 的建置流程 +- **Why**: 需要新增 goctl 命令和整合現有 env/run 命令 +- **Commands to add**: + ```makefile + .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: gen + gen: api + go mod tidy + ``` +- **Decision**: 需要追蹤 +- **Effort**: human ~30min / CC ~10min +- **Depends on**: Phase 1 (API 定義與骨架生成) +- **Status**: pending + +--- + +## Phase 2: Domain 層建立 + +### DONE +- [ ] 建立 `pkg/domain/entity/` +- [ ] 建立 `pkg/domain/repository/` +- [ ] 建立 `pkg/domain/usecase/` +- [ ] 建立 `pkg/domain/const/` + +### TODO + +#### TODO-4: import 循環依賴檢測 +- **What**: 在每個 Phase 完成後執行 `go build ./...` 檢測循環依賴 +- **Why**: DDD 架構分層容易產生循環依賴 +- **Potential cycles**: + - `pkg/usecase` ↔ `pkg/domain/usecase` + - `pkg/repository` ↔ `pkg/domain/repository` +- **Command**: `go build ./... && go test ./... -run=none` +- **Depends on**: 每個 Phase 完成後 +- **Status**: pending + +--- + +## Phase 8: Internal 層重組 + +### DONE +- [ ] 更新 `internal/config/config.go` +- [ ] 建立 `internal/svc/servicecontext.go` +- [ ] 建立 `internal/logic/` +- [ ] 建立 `internal/handler/` +- [ ] 建立 `internal/middleware/` + +### TODO + +#### TODO-5: SSE 整合測試 +- **What**: 增加 SSE streaming 的端對端測試 +- **Why**: SSE 是核心功能,自訂 handler 容易出錯,沒有測試覆蓋 +- **Test cases**: + 1. SSE streaming 請求正常返回 + 2. SSE client disconnect 正確處理 + 3. SSE timeout 正確處理 + 4. 非串流請求轉 SSE 格式 +- **Implementation**: + ```go + // tests/integration/sse_test.go + func TestSSEStreaming(t *testing.T) { + // 使用 httptest 模擬 SSE 客戶端 + // 驗證 data: [DONE] 正確返回 + } + ``` +- **Decision**: 使用 `rest.WithCustom` 路由 +- **Effort**: human ~2h / CC ~30min +- **Depends on**: Phase 8 完成(Internal 層重組) +- **Status**: pending + +#### TODO-6: SSE Handler 實作 +- **What**: 使用 `rest.WithCustom` 實作 SSE streaming handler +- **Why**: go-zero 標準 handler 不支援 SSE,需要自訂 +- **Implementation**: + ```go + // internal/handler/chat_handler.go + func NewChatHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // SSE 設定 + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.Header().Set("Connection", "keep-alive") + + // 委託給 usecase + svcCtx.ChatUsecase.Stream(r.Context(), input, callback) + } + } + ``` +- **Decision**: 使用 `rest.WithCustom` 路由 +- **Effort**: human ~2h / CC ~30min +- **Depends on**: Phase 6 (Usecase 層建立) +- **Status**: pending + +--- + +## Phase 10: 清理與測試 + +### DONE +- [ ] 移除舊目錄 +- [ ] 更新 import 路徑 +- [ ] 執行測試 + +### TODO + +#### TODO-7: 測試文件遷移 +- **What**: 測試文件跟隨源碼遷移到 pkg/ +- **Why**: 測試應該與源碼在同一目錄 +- **Files to migrate**: + - `internal/httputil/httputil_test.go` → `pkg/infrastructure/httputil/` + - `internal/config/config_test.go` → `internal/config/` (保留) + - `internal/sanitize/sanitize_test.go` → `pkg/usecase/` + - `internal/models/cursormap_test.go` → `pkg/domain/const/` + - `internal/models/cursorcli_test.go` → `pkg/domain/const/` + - `internal/parser/stream_test.go` → `pkg/infrastructure/parser/` + - `internal/env/env_test.go` → `pkg/infrastructure/env/` + - `internal/winlimit/winlimit_test.go` → `pkg/infrastructure/winlimit/` + - `internal/anthropic/anthropic_test.go` → `pkg/adapter/anthropic/` + - `internal/pool/pool_test.go` → `pkg/repository/` + - `internal/openai/openai_test.go` → `pkg/adapter/openai/` + - `internal/process/process_test.go` → `pkg/infrastructure/process/` +- **Decision**: 測試遷移到 pkg/ +- **Effort**: human ~1h / CC ~10min +- **Depends on**: Phase 3-7 完成 +- **Status**: pending + +#### TODO-8: ServiceContext 單例 Pool +- **What**: AccountPool 使用單例模式,透過 sync.Once 確保只初始化一次 +- **Why**: 避免每次請求創建新 Pool 的開銷 +- **Implementation**: + ```go + // pkg/repository/account.go + var ( + globalPool *AccountPool + globalPoolOnce sync.Once + ) + + func GetAccountPool(configDirs []string) *AccountPool { + globalPoolOnce.Do(func() { + globalPool = NewAccountPool(configDirs) + }) + return globalPool + } + ``` +- **Decision**: 使用單例 Pool +- **Effort**: human ~30min / CC ~10min +- **Depends on**: Phase 4 (Repository 層實作) +- **Status**: pending + +--- + +## Phase 獨立 TODO + +### TODO-9: 回歸測試自動化 +- **What**: 建立自動化回歸測試腳本 +- **Why**: 確保每次遷移後功能正常 +- **Script**: + ```bash + # scripts/regression-test.sh + #!/bin/bash + set -e + + echo "=== Health check ===" + curl -s http://localhost:8080/health | jq . + + echo "=== Models list ===" + curl -s http://localhost:8080/v1/models | jq . + + echo "=== Chat completion (non-streaming) ===" + curl -s -X POST http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model":"test","messages":[{"role":"user","content":"hi"}],"stream":false}' | jq . + + echo "=== Chat completion (streaming) ===" + curl -s -X POST http://localhost:8080/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model":"test","messages":[{"role":"user","content":"hi"}],"stream":true}' + ``` +- **Depends on**: Phase 10 完成 +- **Status**: pending + +--- + +## Summary + +| TODO | Phase | Effort | Status | +|------|-------|--------|--------| +| TODO-1: 全局變數遷移清單 | Phase 2 | 2h | pending | +| TODO-2: go.mod 更新 | Phase 1 | 5min | pending | +| TODO-3: Makefile 更新 | Phase 1 | 30min | pending | +| TODO-4: import 循環依賴檢測 | Each Phase | 5min | pending | +| TODO-5: SSE 整合測試 | Phase 8 | 2h | pending | +| TODO-6: SSE Handler 實作 | Phase 8 | 2h | pending | +| TODO-7: 測試文件遷移 | Phase 10 | 1h | pending | +| TODO-8: ServiceContext 單例 Pool | Phase 4 | 30min | pending | +| TODO-9: 回歸測試自動化 | Phase 10 | 30min | pending | + +--- + +## Dependencies Graph + +``` +Phase 1 (API 定義) + │ + ├── TODO-2: go.mod 更新 (必须在開始前完成) + ├── TODO-3: Makefile 更新 + │ + ▼ +Phase 2 (Domain 層) + │ + ├── TODO-1: 全局變數遷移清單 + ├── TODO-4: import 循環依賴檢測 + │ + ▼ +Phase 3 (Infrastructure 層) + │ + ├── TODO-4: import 循環依賴檢測 + │ + ▼ +Phase 4 (Repository 層) + │ + ├── TODO-8: ServiceContext 單例 Pool + ├── TODO-4: import 循環依賴檢測 + │ + ▼ +Phase 5 (Provider 層) + │ + ├── TODO-4: import 循環依賴檢測 + │ + ▼ +Phase 6 (Usecase 層) + │ + ├── TODO-4: import 循環依賴檢測 + │ + ▼ +Phase 7 (Adapter 層) + │ + ├── TODO-4: import 循環依賴檢測 + │ + ▼ +Phase 8 (Internal 層) + │ + ├── TODO-5: SSE 整合測試 + ├── TODO-6: SSE Handler 實作 + ├── TODO-4: import 循環依賴檢測 + │ + ▼ +Phase 9 (CLI 工具) + │ + ├── TODO-4: import 循環依賴檢測 + │ + ▼ +Phase 10 (清理與測試) + │ + ├── TODO-7: 測試文件遷移 + ├── TODO-9: 回歸測試自動化 + ├── TODO-4: import 循環依賴檢測 + │ + ▼ +完成 +``` + +--- + +**文件版本**: v1.0 +**建立日期**: 2026-04-03 +**最後更新**: 2026-04-03 \ No newline at end of file diff --git a/refactor.md b/docs/refactor.md similarity index 100% rename from refactor.md rename to docs/refactor.md diff --git a/etc/chat-api.yaml b/etc/chat-api.yaml new file mode 100644 index 0000000..bf439ed --- /dev/null +++ b/etc/chat-api.yaml @@ -0,0 +1,3 @@ +Name: chat-api +Host: 0.0.0.0 +Port: 8888 diff --git a/etc/chat.yaml b/etc/chat.yaml new file mode 100644 index 0000000..877d553 --- /dev/null +++ b/etc/chat.yaml @@ -0,0 +1,45 @@ +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} \ No newline at end of file diff --git a/go.mod b/go.mod index 341a00d..dc91f7c 100644 --- a/go.mod +++ b/go.mod @@ -3,26 +3,68 @@ module cursor-api-proxy go 1.25.0 require ( + github.com/go-rod/rod v0.116.2 github.com/google/uuid v1.6.0 + github.com/playwright-community/playwright-go v0.5700.1 + github.com/zeromicro/go-zero v1.10.1 modernc.org/sqlite v1.48.0 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/deckarep/golang-set/v2 v2.8.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/go-jose/go-jose/v3 v3.0.5 // indirect - github.com/go-rod/rod v0.116.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-stack/stack v1.8.1 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/grafana/pyroscope-go v1.2.8 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/playwright-community/playwright-go v0.5700.1 // indirect + github.com/openzipkin/zipkin-go v0.4.3 // indirect + github.com/pelletier/go-toml/v2 v2.3.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/titanous/json5 v1.0.0 // indirect github.com/ysmood/fetchup v0.2.3 // indirect github.com/ysmood/goob v0.4.0 // indirect github.com/ysmood/got v0.40.0 // indirect github.com/ysmood/gson v0.7.3 // indirect github.com/ysmood/leakless v0.9.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 // indirect + go.opentelemetry.io/otel/exporters/zipkin v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/automaxprocs v1.6.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/net v0.50.0 // indirect golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go.sum b/go.sum index 77d212a..eb6cee6 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,148 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ= github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/pyroscope-go v1.2.8 h1:UvCwIhlx9DeV7F6TW/z8q1Mi4PIm3vuUJ2ZlCEvmA4M= +github.com/grafana/pyroscope-go v1.2.8/go.mod h1:SSi59eQ1/zmKoY/BKwa5rSFsJaq+242Bcrr4wPix1g8= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og= +github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg= +github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c= +github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= +github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/playwright-community/playwright-go v0.5700.1 h1:PNFb1byWqrTT720rEO0JL88C6Ju0EmUnR5deFLvtP/U= github.com/playwright-community/playwright-go v0.5700.1/go.mod h1:MlSn1dZrx8rszbCxY6x3qK89ZesJUYVx21B2JnkoNF0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= +github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0= +github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/titanous/json5 v1.0.0 h1:hJf8Su1d9NuI/ffpxgxQfxh/UiBFZX7bMPid0rIL/7s= +github.com/titanous/json5 v1.0.0/go.mod h1:7JH1M8/LHKc6cyP5o5g3CSaRj+mBrIimTxzpvmckH8c= github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/gop v0.2.0 h1:+tFrG0TWPxT6p9ZaZs+VY+opCvHU8/3Fk6BaNv6kqKg= +github.com/ysmood/gop v0.2.0/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zeromicro/go-zero v1.10.1 h1:1nM3ilvYx97GUqyaNH2IQPtfNyK7tp5JvN63c7m6QKU= +github.com/zeromicro/go-zero v1.10.1/go.mod h1:z41DXmO6gx/Se7Ow5UIwPxcUmpVj3ebhoNCcZ1gfp5k= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 h1:DvJDOPmSWQHWywQS6lKL+pb8s3gBLOZUtw4N+mavW1I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0/go.mod h1:EtekO9DEJb4/jRyN4v4Qjc2yA7AtfCBuz2FynRUWTXs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 h1:MzfofMZN8ulNqobCmCAVbqVL5syHw+eB2qPRkCMA/fQ= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0/go.mod h1:E73G9UFtKRXrxhBsHtG00TB5WxX57lpsQzogDkqBTz8= +go.opentelemetry.io/otel/exporters/zipkin v1.40.0 h1:zu+I4j+FdO6xIxBVPeuncQVbjxUM4LiMgv6GwGe9REE= +go.opentelemetry.io/otel/exporters/zipkin v1.40.0/go.mod h1:zS6cC4nFBYXbu18e7aLfMzubBjOiN7ZcROu477qtMf8= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= +go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= @@ -51,6 +155,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -61,6 +167,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -78,6 +185,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= @@ -85,8 +194,30 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M= +google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409 h1:H86B94AW+VfJWDqFeEbBPhEtHzJwJfTbgE2lZa54ZAQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260128011058-8636f8732409/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= +gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= +gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= diff --git a/internal/config/config.go b/internal/config/config.go index bf7ea2e..6c959b9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -2,8 +2,53 @@ package config import ( "cursor-api-proxy/internal/env" + + "github.com/zeromicro/go-zero/rest" ) +// Config for go-zero (generated by goctl) +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 + + // API Key + RequiredKey string +} + +// BridgeConfig for backward compatibility with existing code type BridgeConfig struct { AgentBin string Host string @@ -31,6 +76,7 @@ type BridgeConfig struct { GeminiMaxSessions int } +// LoadBridgeConfig loads config from environment (for backward compatibility) func LoadBridgeConfig(e env.EnvSource, cwd string) BridgeConfig { loaded := env.LoadEnvConfig(e, cwd) return BridgeConfig{ diff --git a/internal/handler/chat/anthropic_messages_handler.go b/internal/handler/chat/anthropic_messages_handler.go new file mode 100644 index 0000000..45da151 --- /dev/null +++ b/internal/handler/chat/anthropic_messages_handler.go @@ -0,0 +1,31 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package chat + +import ( + "net/http" + + "cursor-api-proxy/internal/logic/chat" + "cursor-api-proxy/internal/svc" + "cursor-api-proxy/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +func AnthropicMessagesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.AnthropicRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := chat.NewAnthropicMessagesLogic(r.Context(), svcCtx) + err := l.AnthropicMessages(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/chat/chat_completions_handler.go b/internal/handler/chat/chat_completions_handler.go new file mode 100644 index 0000000..1b09da3 --- /dev/null +++ b/internal/handler/chat/chat_completions_handler.go @@ -0,0 +1,31 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package chat + +import ( + "net/http" + + "cursor-api-proxy/internal/logic/chat" + "cursor-api-proxy/internal/svc" + "cursor-api-proxy/internal/types" + "github.com/zeromicro/go-zero/rest/httpx" +) + +func ChatCompletionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req types.ChatCompletionRequest + if err := httpx.Parse(r, &req); err != nil { + httpx.ErrorCtx(r.Context(), w, err) + return + } + + l := chat.NewChatCompletionsLogic(r.Context(), svcCtx) + err := l.ChatCompletions(&req) + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.Ok(w) + } + } +} diff --git a/internal/handler/chat/health_handler.go b/internal/handler/chat/health_handler.go new file mode 100644 index 0000000..3149606 --- /dev/null +++ b/internal/handler/chat/health_handler.go @@ -0,0 +1,24 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package chat + +import ( + "net/http" + + "cursor-api-proxy/internal/logic/chat" + "cursor-api-proxy/internal/svc" + "github.com/zeromicro/go-zero/rest/httpx" +) + +func HealthHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := chat.NewHealthLogic(r.Context(), svcCtx) + resp, err := l.Health() + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/chat/models_handler.go b/internal/handler/chat/models_handler.go new file mode 100644 index 0000000..440749e --- /dev/null +++ b/internal/handler/chat/models_handler.go @@ -0,0 +1,24 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package chat + +import ( + "net/http" + + "cursor-api-proxy/internal/logic/chat" + "cursor-api-proxy/internal/svc" + "github.com/zeromicro/go-zero/rest/httpx" +) + +func ModelsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := chat.NewModelsLogic(r.Context(), svcCtx) + resp, err := l.Models() + if err != nil { + httpx.ErrorCtx(r.Context(), w, err) + } else { + httpx.OkJsonCtx(r.Context(), w, resp) + } + } +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go new file mode 100644 index 0000000..6a5c668 --- /dev/null +++ b/internal/handler/routes.go @@ -0,0 +1,41 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.1 + +package handler + +import ( + "net/http" + + chat "cursor-api-proxy/internal/handler/chat" + "cursor-api-proxy/internal/svc" + + "github.com/zeromicro/go-zero/rest" +) + +func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) { + server.AddRoutes( + []rest.Route{ + { + Method: http.MethodGet, + Path: "/health", + Handler: chat.HealthHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/v1/chat/completions", + Handler: chat.ChatCompletionsHandler(serverCtx), + }, + { + Method: http.MethodPost, + Path: "/v1/messages", + Handler: chat.AnthropicMessagesHandler(serverCtx), + }, + { + Method: http.MethodGet, + Path: "/v1/models", + Handler: chat.ModelsHandler(serverCtx), + }, + }, + rest.WithPrefix("/v1"), + ) +} diff --git a/internal/logic/chat/anthropic_messages_logic.go b/internal/logic/chat/anthropic_messages_logic.go new file mode 100644 index 0000000..b8a840c --- /dev/null +++ b/internal/logic/chat/anthropic_messages_logic.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package chat + +import ( + "context" + + "cursor-api-proxy/internal/svc" + "cursor-api-proxy/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type AnthropicMessagesLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewAnthropicMessagesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AnthropicMessagesLogic { + return &AnthropicMessagesLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *AnthropicMessagesLogic) AnthropicMessages(req *types.AnthropicRequest) error { + // todo: add your logic here and delete this line + + return nil +} diff --git a/internal/logic/chat/chat_completions_logic.go b/internal/logic/chat/chat_completions_logic.go new file mode 100644 index 0000000..36cbe67 --- /dev/null +++ b/internal/logic/chat/chat_completions_logic.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package chat + +import ( + "context" + + "cursor-api-proxy/internal/svc" + "cursor-api-proxy/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ChatCompletionsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewChatCompletionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ChatCompletionsLogic { + return &ChatCompletionsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ChatCompletionsLogic) ChatCompletions(req *types.ChatCompletionRequest) error { + // todo: add your logic here and delete this line + + return nil +} diff --git a/internal/logic/chat/health_logic.go b/internal/logic/chat/health_logic.go new file mode 100644 index 0000000..e7046ac --- /dev/null +++ b/internal/logic/chat/health_logic.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package chat + +import ( + "context" + + "cursor-api-proxy/internal/svc" + "cursor-api-proxy/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type HealthLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewHealthLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HealthLogic { + return &HealthLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *HealthLogic) Health() (resp *types.HealthResponse, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/logic/chat/models_logic.go b/internal/logic/chat/models_logic.go new file mode 100644 index 0000000..31eac5e --- /dev/null +++ b/internal/logic/chat/models_logic.go @@ -0,0 +1,33 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package chat + +import ( + "context" + + "cursor-api-proxy/internal/svc" + "cursor-api-proxy/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type ModelsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewModelsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ModelsLogic { + return &ModelsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *ModelsLogic) Models() (resp *types.ModelsResponse, err error) { + // todo: add your logic here and delete this line + + return +} diff --git a/internal/svc/service_context.go b/internal/svc/service_context.go new file mode 100644 index 0000000..0a16473 --- /dev/null +++ b/internal/svc/service_context.go @@ -0,0 +1,18 @@ +// Code scaffolded by goctl. Safe to edit. +// goctl 1.10.1 + +package svc + +import ( + "cursor-api-proxy/internal/config" +) + +type ServiceContext struct { + Config config.Config +} + +func NewServiceContext(c config.Config) *ServiceContext { + return &ServiceContext{ + Config: c, + } +} diff --git a/internal/types/types.go b/internal/types/types.go new file mode 100644 index 0000000..e28bd37 --- /dev/null +++ b/internal/types/types.go @@ -0,0 +1,132 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.1 + +package types + +type AnthropicRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + MaxTokens int `json:"max_tokens"` + Stream bool `json:"stream,optional"` + System string `json:"system,optional"` +} + +type AnthropicResponse struct { + Id string `json:"id"` + Type string `json:"type"` + Role string `json:"role"` + Content []ContentBlock `json:"content"` + Model string `json:"model"` + Usage AnthropicUsage `json:"usage"` +} + +type AnthropicUsage struct { + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` +} + +type ChatCompletionRequest struct { + 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"` +} + +type ChatCompletionResponse struct { + Id string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []Choice `json:"choices"` + Usage Usage `json:"usage"` +} + +type Choice struct { + Index int `json:"index"` + Message RespMessage `json:"message,optional"` + Delta Delta `json:"delta,optional"` + FinishReason string `json:"finish_reason"` +} + +type ContentBlock struct { + Type string `json:"type"` + Text string `json:"text,optional"` +} + +type Delta struct { + Role string `json:"role,optional"` + Content string `json:"content,optional"` + ReasoningContent string `json:"reasoning_content,optional"` + ToolCalls []ToolCall `json:"tool_calls,optional"` +} + +type Function struct { + Name string `json:"name"` + Description string `json:"description,optional"` + Parameters interface{} `json:"parameters,optional"` +} + +type FunctionCall struct { + Name string `json:"name"` + Arguments string `json:"arguments"` +} + +type HealthRequest struct { +} + +type HealthResponse struct { + Status string `json:"status"` + Version string `json:"version"` +} + +type Message struct { + Role string `json:"role"` + Content interface{} `json:"content"` +} + +type ModelData struct { + Id string `json:"id"` + Object string `json:"object"` + OwnedBy string `json:"owned_by"` +} + +type ModelsRequest struct { +} + +type ModelsResponse struct { + Object string `json:"object"` + Data []ModelData `json:"data"` +} + +type RespMessage struct { + Role string `json:"role"` + Content string `json:"content,optional"` + ToolCalls []ToolCall `json:"tool_calls,optional"` +} + +type Tool struct { + Type string `json:"type"` + Function ToolFunction `json:"function"` +} + +type ToolCall struct { + Index int `json:"index"` + Id string `json:"id"` + Type string `json:"type"` + Function FunctionCall `json:"function"` +} + +type ToolFunction struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters interface{} `json:"parameters"` +} + +type Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` +}