diff --git a/.opencode/plans/refactor-tasks.md b/.opencode/plans/refactor-tasks.md new file mode 100644 index 0000000..5c0b5c9 --- /dev/null +++ b/.opencode/plans/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/internal/svc/service_context.go b/internal/svc/service_context.go index 0a16473..9089ad0 100644 --- a/internal/svc/service_context.go +++ b/internal/svc/service_context.go @@ -1,18 +1,23 @@ -// Code scaffolded by goctl. Safe to edit. -// goctl 1.10.1 - package svc import ( "cursor-api-proxy/internal/config" + domainrepo "cursor-api-proxy/pkg/domain/repository" + "cursor-api-proxy/pkg/repository" ) type ServiceContext struct { Config config.Config + + // Domain services + AccountPool domainrepo.AccountPool } func NewServiceContext(c config.Config) *ServiceContext { + accountPool := repository.NewAccountPool(c.ConfigDirs) + return &ServiceContext{ - Config: c, + Config: c, + AccountPool: accountPool, } } diff --git a/pkg/provider/geminiweb/playwright_provider.go b/pkg/provider/geminiweb/playwright_provider.go index dd63c59..dbfa5b2 100644 --- a/pkg/provider/geminiweb/playwright_provider.go +++ b/pkg/provider/geminiweb/playwright_provider.go @@ -2,8 +2,8 @@ package geminiweb import ( "context" - "cursor-api-proxy/pkg/domain/entity" "cursor-api-proxy/internal/config" + "cursor-api-proxy/pkg/domain/entity" "fmt" "os" "path/filepath" @@ -624,18 +624,39 @@ func (p *PlaywrightProvider) selectModel(model string) error { return nil } -// buildPromptFromMessages 從訊息列表建構提示詞 +// buildPromptFromMessagesPlaywright 從訊息列表建構提示詞 func buildPromptFromMessagesPlaywright(messages []entity.Message) string { var prompt string for _, m := range messages { + content := messageContentToStringPlaywright(m.Content) switch m.Role { case "system": - prompt += "System: " + m.Content + "\n\n" + prompt += "System: " + content + "\n\n" case "user": - prompt += m.Content + "\n\n" + prompt += content + "\n\n" case "assistant": - prompt += "Assistant: " + m.Content + "\n\n" + prompt += "Assistant: " + content + "\n\n" } } return prompt } + +// messageContentToStringPlaywright converts Message.Content to string +func messageContentToStringPlaywright(content interface{}) string { + switch v := content.(type) { + case string: + return v + case []interface{}: + var result string + for _, item := range v { + if m, ok := item.(map[string]interface{}); ok { + if text, ok := m["text"].(string); ok { + result += text + } + } + } + return result + default: + return "" + } +} diff --git a/pkg/provider/geminiweb/provider.go b/pkg/provider/geminiweb/provider.go index ca3ba70..ca0a0cb 100644 --- a/pkg/provider/geminiweb/provider.go +++ b/pkg/provider/geminiweb/provider.go @@ -2,8 +2,8 @@ package geminiweb import ( "context" - "cursor-api-proxy/pkg/domain/entity" "cursor-api-proxy/internal/config" + "cursor-api-proxy/pkg/domain/entity" "fmt" "os" "path/filepath" @@ -142,18 +142,40 @@ func (p *Provider) Generate(ctx context.Context, model string, messages []entity func buildPromptFromMessages(messages []entity.Message) string { var prompt string for _, m := range messages { + content := messageContentToString(m.Content) switch m.Role { case "system": - prompt += "System: " + m.Content + "\n\n" + prompt += "System: " + content + "\n\n" case "user": - prompt += m.Content + "\n\n" + prompt += content + "\n\n" case "assistant": - prompt += "Assistant: " + m.Content + "\n\n" + prompt += "Assistant: " + content + "\n\n" } } return prompt } +// messageContentToString converts Message.Content to string +func messageContentToString(content interface{}) string { + switch v := content.(type) { + case string: + return v + case []interface{}: + // Handle array content (multimodal) + var result string + for _, item := range v { + if m, ok := item.(map[string]interface{}); ok { + if text, ok := m["text"].(string); ok { + result += text + } + } + } + return result + default: + return "" + } +} + // RunLogin 執行登入流程(供 gemini-login 命令使用) func RunLogin(cfg config.BridgeConfig, sessionName string) error { if sessionName == "" { diff --git a/pkg/repository/account.go b/pkg/repository/account.go index 8f12aef..06b5868 100644 --- a/pkg/repository/account.go +++ b/pkg/repository/account.go @@ -3,30 +3,20 @@ package repository import ( "sync" "time" + + "cursor-api-proxy/pkg/domain/entity" ) type accountStatus struct { - configDir string - activeRequests int - lastUsed int64 - rateLimitUntil int64 - totalRequests int - totalSuccess int - totalErrors int - totalRateLimits int - totalLatencyMs int64 -} - -type AccountStat struct { - ConfigDir string - ActiveRequests int - TotalRequests int - TotalSuccess int - TotalErrors int - TotalRateLimits int - TotalLatencyMs int64 - IsRateLimited bool - RateLimitUntil int64 + configDir string + activeRequests int + lastUsed int64 + rateLimitUntil int64 + totalRequests int + totalSuccess int + totalErrors int + totalRateLimits int + totalLatencyMs int64 } type AccountPool struct { @@ -155,13 +145,13 @@ func (p *AccountPool) ReportRateLimit(configDir string, penaltyMs int64) { } } -func (p *AccountPool) GetStats() []AccountStat { +func (p *AccountPool) GetStats() []entity.AccountStat { p.mu.Lock() defer p.mu.Unlock() now := time.Now().UnixMilli() - stats := make([]AccountStat, len(p.accounts)) + stats := make([]entity.AccountStat, len(p.accounts)) for i, a := range p.accounts { - stats[i] = AccountStat{ + stats[i] = entity.AccountStat{ ConfigDir: a.configDir, ActiveRequests: a.activeRequests, TotalRequests: a.totalRequests, @@ -180,7 +170,6 @@ func (p *AccountPool) Count() int { return len(p.accounts) } - // ─── PoolHandle interface ────────────────────────────────────────────────── // PoolHandle 讓 handler 可以注入獨立的 pool 實例,避免多 port 模式共用全域 pool。 @@ -191,19 +180,19 @@ type PoolHandle interface { ReportRequestSuccess(configDir string, latencyMs int64) ReportRequestError(configDir string, latencyMs int64) ReportRateLimit(configDir string, penaltyMs int64) - GetStats() []AccountStat + GetStats() []entity.AccountStat } // GlobalPoolHandle 包裝全域函式以實作 PoolHandle 介面(單 port 模式使用) type GlobalPoolHandle struct{} -func (GlobalPoolHandle) GetNextConfigDir() string { return GetNextAccountConfigDir() } -func (GlobalPoolHandle) ReportRequestStart(d string) { ReportRequestStart(d) } -func (GlobalPoolHandle) ReportRequestEnd(d string) { ReportRequestEnd(d) } +func (GlobalPoolHandle) GetNextConfigDir() string { return GetNextAccountConfigDir() } +func (GlobalPoolHandle) ReportRequestStart(d string) { ReportRequestStart(d) } +func (GlobalPoolHandle) ReportRequestEnd(d string) { ReportRequestEnd(d) } func (GlobalPoolHandle) ReportRequestSuccess(d string, l int64) { ReportRequestSuccess(d, l) } func (GlobalPoolHandle) ReportRequestError(d string, l int64) { ReportRequestError(d, l) } func (GlobalPoolHandle) ReportRateLimit(d string, p int64) { ReportRateLimit(d, p) } -func (GlobalPoolHandle) GetStats() []AccountStat { return GetAccountStats() } +func (GlobalPoolHandle) GetStats() []entity.AccountStat { return GetAccountStats() } // ─── Global pool ─────────────────────────────────────────────────────────── @@ -273,7 +262,7 @@ func ReportRateLimit(configDir string, penaltyMs int64) { } } -func GetAccountStats() []AccountStat { +func GetAccountStats() []entity.AccountStat { globalMu.Lock() p := globalPool globalMu.Unlock()