344 lines
11 KiB
Markdown
344 lines
11 KiB
Markdown
# Architecture: Cursor Adapter
|
||
|
||
## Overview
|
||
|
||
Cursor Adapter 是一個本機 HTTP proxy server,將 OpenAI-compatible API 請求轉換為 Cursor CLI headless 模式指令,並將 Cursor CLI 的 streaming JSON 輸出轉換為 OpenAI SSE 格式回傳給 CLI 工具。
|
||
|
||
核心架構:**單一 binary、無狀態、spawn 子程序模式**。
|
||
|
||
### Requirement Traceability
|
||
|
||
| PRD Requirement | Architectural Component |
|
||
|----------------|------------------------|
|
||
| FR1: OpenAI-compatible API | HTTP Server (net/http + chi router) |
|
||
| FR2: Cursor CLI Integration | CLI Bridge (os/exec subprocess) |
|
||
| FR3: Streaming Response Conversion | Stream Converter (goroutine pipeline) |
|
||
| FR4: Model Listing | Model Registry |
|
||
| FR5: Configuration | Config Module (YAML) |
|
||
| FR6: Error Handling | Error Handler (middleware) |
|
||
| NFR1: Performance < 500ms overhead | goroutine pipeline, zero-copy streaming |
|
||
| NFR2: Concurrent requests ≤ 5 | semaphore (buffered channel) |
|
||
|
||
## System Architecture
|
||
|
||
### Technology Stack
|
||
|
||
| Layer | Technology | Justification |
|
||
|-------|-----------|---------------|
|
||
| Language | Go 1.22+ | 單一 binary、subprocess 管理好、goroutine 天然適合 streaming |
|
||
| HTTP Router | go-chi/chi v5 | 輕量、相容 net/http、middleware 支援好 |
|
||
| Config | gopkg.in/yaml.v3 | 標準 YAML 解析 |
|
||
| CLI | spf13/cobra | Go 標準 CLI 框架 |
|
||
| Testing | testify + stdlib | table-driven test + assertion helper |
|
||
|
||
### Component Architecture
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────┐
|
||
│ cursor-adapter (single binary) │
|
||
│ │
|
||
│ cmd/ │
|
||
│ └── cursor-adapter/main.go (cobra entrypoint) │
|
||
│ │
|
||
│ internal/ │
|
||
│ ├── server/ HTTP Server (chi router) │
|
||
│ │ ├── handler.go route handlers │
|
||
│ │ ├── middleware.go error + logging │
|
||
│ │ └── sse.go SSE writer helpers │
|
||
│ ├── bridge/ CLI Bridge │
|
||
│ │ ├── bridge.go spawn subprocess │
|
||
│ │ └── scanner.go stdout line reader │
|
||
│ ├── converter/ Stream Converter │
|
||
│ │ └── convert.go cursor-json → OpenAI SSE │
|
||
│ └── config/ Config Module │
|
||
│ └── config.go YAML loading + defaults │
|
||
│ │
|
||
└─────────────────────────────────────────────────┘
|
||
│
|
||
│ os/exec.CommandContext
|
||
▼
|
||
┌──────────────────┐
|
||
│ Cursor CLI │
|
||
│ agent -p ... │
|
||
│ --model ... │
|
||
│ --output-format│
|
||
│ stream-json │
|
||
└──────────────────┘
|
||
```
|
||
|
||
## Service Boundaries
|
||
|
||
單一 binary,4 個 internal package:
|
||
|
||
| Package | Responsibility | Exported |
|
||
|---------|---------------|----------|
|
||
| cmd/cursor-adapter | CLI 入口、wiring | main() |
|
||
| internal/server | HTTP routes + middleware | NewServer(), Server.Run() |
|
||
| internal/bridge | spawn/manage Cursor CLI subprocess | Bridge interface + CLIBridge |
|
||
| internal/converter | stream-json → OpenAI SSE 轉換 | Convert() functions |
|
||
| internal/config | YAML config 載入/驗證 | Config struct, Load() |
|
||
|
||
### Communication Matrix
|
||
|
||
| From | To | Pattern | Purpose |
|
||
|------|----|---------|---------|
|
||
| server/handler | bridge | interface call | 啟動子程序 |
|
||
| bridge | converter | channel (chan string) | 逐行傳遞 stdout |
|
||
| converter | server/handler | channel (chan SSEChunk) | 回傳轉換後的 chunk |
|
||
| server/handler | client | HTTP SSE | 回傳給 CLI 工具 |
|
||
|
||
## Data Flow
|
||
|
||
### Chat Completion (Streaming)
|
||
|
||
```
|
||
1. Client → POST /v1/chat/completions (stream: true)
|
||
2. handler → 驗證 request body
|
||
3. handler → 從 messages[] 組合 prompt
|
||
4. bridge → ctx, cancel := context.WithTimeout(...)
|
||
5. bridge → cmd := exec.CommandContext(ctx, "agent", "-p", prompt, "--model", model, "--output-format", "stream-json")
|
||
6. bridge → cmd.Stdout pipe → goroutine scanner 逐行讀
|
||
7. scanner → 每行送入 outputChan (chan string)
|
||
8. converter → 讀 outputChan,轉換為 SSEChunk,送入 sseChan
|
||
9. handler → flush SSE chunk 到 client
|
||
10. bridge → process 結束 → close channels → handler 發送 [DONE]
|
||
```
|
||
|
||
## Database Schema
|
||
|
||
N/A。無狀態設計,不需要資料庫。
|
||
|
||
## API Contract
|
||
|
||
### POST /v1/chat/completions
|
||
|
||
Request:
|
||
```json
|
||
{
|
||
"model": "claude-sonnet-4-20250514",
|
||
"messages": [
|
||
{"role": "user", "content": "hello"}
|
||
],
|
||
"stream": true
|
||
}
|
||
```
|
||
|
||
Response (SSE when stream: true):
|
||
```
|
||
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
|
||
|
||
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
|
||
|
||
data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
|
||
|
||
data: [DONE]
|
||
```
|
||
|
||
Response (JSON when stream: false):
|
||
```json
|
||
{
|
||
"id": "chatcmpl-xxx",
|
||
"object": "chat.completion",
|
||
"choices": [{"index": 0, "message": {"role": "assistant", "content": "Hello!"}, "finish_reason": "stop"}],
|
||
"usage": {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
||
}
|
||
```
|
||
|
||
### GET /v1/models
|
||
|
||
Response:
|
||
```json
|
||
{
|
||
"object": "list",
|
||
"data": [
|
||
{"id": "claude-sonnet-4-20250514", "object": "model", "created": 0, "owned_by": "cursor"}
|
||
]
|
||
}
|
||
```
|
||
|
||
### GET /health
|
||
|
||
Response:
|
||
```json
|
||
{"status": "ok", "cursor_cli": "available", "version": "0.1.0"}
|
||
```
|
||
|
||
### Error Codes
|
||
|
||
| Status | Code | When |
|
||
|--------|------|------|
|
||
| 400 | invalid_request | messages 為空或格式錯誤 |
|
||
| 404 | model_not_found | 指定的 model 不存在 |
|
||
| 500 | internal_error | Cursor CLI 子程序崩潰 |
|
||
| 504 | timeout | Cursor CLI 超時未回應 |
|
||
|
||
## Async / Queue Design
|
||
|
||
N/A。不需要 queue,goroutine + channel 直接串接。
|
||
|
||
## Consistency Model
|
||
|
||
N/A。無狀態 proxy,每次請求獨立。
|
||
|
||
## Error Model
|
||
|
||
| Category | Examples | Handling |
|
||
|----------|---------|----------|
|
||
| Client Error | invalid request, unknown model | 4xx,不 spawn 子程序 |
|
||
| CLI Spawn Error | agent not found, not logged in | 500 + stderr message |
|
||
| Timeout | model thinking too long | kill subprocess → 504 |
|
||
| Crash | unexpected exit | 500 + exit code |
|
||
|
||
## Security Boundaries
|
||
|
||
N/A。本機 personal tool,bind 127.0.0.1,無認證。
|
||
|
||
## Integration Boundaries
|
||
|
||
### Cursor CLI
|
||
|
||
| Property | Value |
|
||
|----------|-------|
|
||
| Integration Pattern | subprocess (os/exec.CommandContext) |
|
||
| Protocol | CLI binary (stdout pipe) |
|
||
| Authentication | 本機 `agent login` 狀態 |
|
||
| Failure Mode | binary not found / not logged in |
|
||
| Data Contract | `--output-format stream-json` |
|
||
| Timeout | 可配置,預設 300s |
|
||
|
||
## Observability
|
||
|
||
- structlog (slog) logging:INFO 請求/完成、ERROR 錯誤/timeout
|
||
- `/health` endpoint
|
||
- DEBUG level 時印出 Cursor CLI 原始 stdout
|
||
|
||
## Scaling Strategy
|
||
|
||
N/A。個人本機工具,單實例。semaphore 限制並發子程序數(預設 5)。
|
||
|
||
## Non-Functional Requirements
|
||
|
||
| NFR | Requirement | Decision | Verification |
|
||
|-----|-------------|----------|-------------|
|
||
| Performance | overhead < 500ms | goroutine pipeline, streaming pipe | 實際測量 |
|
||
| Reliability | 並發 ≤ 5 | buffered channel semaphore | 併發測試 |
|
||
| Usability | 一行啟動 | cobra CLI, sensible defaults | 手動測試 |
|
||
| Distribution | 單一 binary | Go cross-compile | `go build` |
|
||
|
||
## Mermaid Diagrams
|
||
|
||
### System Architecture
|
||
|
||
```mermaid
|
||
graph LR
|
||
CLI[Hermes/OpenCode/Claude] -->|POST /v1/chat/completions| Adapter[Cursor Adapter]
|
||
Adapter -->|exec: agent -p ... --output-format stream-json| Cursor[Cursor CLI]
|
||
Cursor -->|streaming JSON stdout| Adapter
|
||
Adapter -->|SSE streaming| CLI
|
||
```
|
||
|
||
### Sequence Diagram
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant C as CLI Tool
|
||
participant H as HTTP Handler
|
||
participant B as CLI Bridge
|
||
participant A as Cursor CLI
|
||
|
||
C->>H: POST /v1/chat/completions
|
||
H->>H: validate, extract prompt
|
||
H->>B: Execute(ctx, prompt, model)
|
||
B->>A: exec.CommandContext("agent", "-p", ...)
|
||
loop streaming
|
||
A-->>B: stdout line (JSON)
|
||
B-->>H: outputChan <- line
|
||
H->>H: convert to SSE chunk
|
||
H-->>C: data: {...}\n\n
|
||
end
|
||
A-->>B: process exit
|
||
B-->>H: close channels
|
||
H-->>C: data: [DONE]
|
||
```
|
||
|
||
### Data Flow Diagram
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A[Client Request] --> B{Validate}
|
||
B -->|invalid| C[400]
|
||
B -->|valid| D[Extract Prompt]
|
||
D --> E[exec.CommandContext]
|
||
E --> F{spawn OK?}
|
||
F -->|no| G[500]
|
||
F -->|yes| H[goroutine: scan stdout]
|
||
H --> I[outputChan]
|
||
I --> J[converter: JSON→SSE]
|
||
J --> K[flush to client]
|
||
K --> L{more?}
|
||
L -->|yes| H
|
||
L -->|no| M[send DONE]
|
||
```
|
||
|
||
## ADR
|
||
|
||
### ADR-001: Go 而非 Python
|
||
|
||
**Context**: 選擇實作語言。候選為 Go 和 Python (FastAPI)。
|
||
|
||
**Decision**: Go 1.22+。
|
||
|
||
**Consequences**:
|
||
- + 單一 binary,不需要使用者裝 Python/pip
|
||
- + `os/exec.CommandContext` 子程序管理比 Python `asyncio` 更直覺
|
||
- + goroutine + channel 天然適合 streaming pipeline
|
||
- + cross-compile,macOS/Linux/Windows 一個 `go build`
|
||
- - SSE 手動處理(但不複雜)
|
||
|
||
**Alternatives**:
|
||
- Python + FastAPI:生態好,但需要 runtime,部署麻煩
|
||
- Rust:效能最好,但開發速度慢
|
||
|
||
### ADR-002: chi router 而非 stdlib mux
|
||
|
||
**Context**: Go 1.22 的 `net/http` 已支援 method-based routing。
|
||
|
||
**Decision**: 使用 chi v5。
|
||
|
||
**Consequences**:
|
||
- + middleware 生態好(logger、recoverer、timeout)
|
||
- + route grouping 更乾淨
|
||
- + 相容 net/http Handler
|
||
- - 多一個 dependency
|
||
|
||
**Alternatives**:
|
||
- stdlib net/http:夠用,但 middleware 要自己寫
|
||
- gin:太重,對這個規模 overkill
|
||
|
||
### ADR-003: spawn 子程序而非 ACP
|
||
|
||
**Context**: Cursor CLI 支援 headless print mode 和 ACP (JSON-RPC)。
|
||
|
||
**Decision**: headless print mode (`agent -p --output-format stream-json`)。
|
||
|
||
**Consequences**:
|
||
- + 實作簡單:spawn + 讀 stdout
|
||
- + 不需要 JSON-RPC
|
||
- - 無法做 tool use(PRD 不需要)
|
||
|
||
**Alternatives**:
|
||
- ACP (JSON-RPC over stdio):功能完整,但複雜度高很多
|
||
|
||
## Risks
|
||
|
||
| Risk | Impact | Likelihood | Mitigation |
|
||
|------|--------|-----------|------------|
|
||
| Cursor CLI stream-json 格式變更 | High | Medium | 抽象 converter,格式在 const 定義 |
|
||
| Cursor CLI 不支援並發實例 | Medium | Low | semaphore + queue |
|
||
| 子程序 zombie | Medium | Low | CommandContext + Wait() |
|
||
|
||
## Open Questions
|
||
|
||
1. Cursor CLI stream-json 的確切 schema?(需實際測試)
|
||
2. Cursor CLI 能否同時跑多個 headless 實例?
|