This repository has been archived on 2026-07-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
codespace/docs/superpowers/specs/2026-07-02-gin-server-composition-design.md
T
2026-07-02 15:07:54 +08:00

288 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Gin Router and Explicit HTTP Server Composition Design
日期:2026-07-02
## 目标
将 codespace 后端的原生 Go 1.22 `http.ServeMux` 路由替换为 Gin,同时继续显式构造 `http.Server`。Gin 只负责路由和 handler 上下文,网络层仍由 `http.Server` 控制。
本次改动的核心要求:
- 使用 Gin 注册 API 路由。
- 不使用 `router.Run()``engine.Run()`
-`*gin.Engine` 显式设置为 `http.Server.Handler`
- 将服务器网络参数配置化,包括 timeout 和最大请求头大小。
- 保持现有 API 路径、HTTP method、响应 JSON 结构不变。
## 当前状态
当前代码:
- `cmd/server/main.go` 已显式构造 `http.Server`
- `internal/api/router.go` 使用 `http.NewServeMux()`
- handler 使用 `http.ResponseWriter``*http.Request``r.PathValue("id")``json.NewDecoder(r.Body)`
- `pkg/config` 只配置 `server.addr`timeout 仍硬编码在 `main.go`
## 非目标
本次不做:
- 不引入认证、CORS、rate limit、request logger middleware。
- 不改变 workspace/file/process service 接口。
- 不改变 API 路径、HTTP method、响应结构。
- 不实现 WebSocket、watcher 推送、Git、LSP、AI Agent。
- 不把网络层控制权交给 Gin 的 `Run()`
## 目标架构
```text
cmd/server/main.go
config.Load()
api.NewRouter(...)
*gin.Engine
http.Server{
Addr,
Handler: ginEngine,
ReadTimeout,
WriteTimeout,
IdleTimeout,
MaxHeaderBytes,
}
ListenAndServe()
```
关键规则:
1. `internal/api.NewRouter` 返回 `*gin.Engine`
2. `cmd/server/main.go` 显式创建 `http.Server`
3. `http.Server.Handler` 设置为 Gin engine。
4. 代码中不得出现 `.Run(` 用于启动 Gin server。
5. `ReadTimeout``WriteTimeout``IdleTimeout``MaxHeaderBytes` 来自配置。
## 配置设计
`configs/config.yaml` 扩展为:
```yaml
server:
addr: ":8080"
readTimeout: "15s"
writeTimeout: "15s"
idleTimeout: "60s"
maxHeaderBytes: 1048576
workspace:
root: "./workspaces"
process:
opencodeCommand: "opencode"
```
`pkg/config.ServerConfig` 对外使用强类型:
```go
type ServerConfig struct {
Addr string
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
}
```
YAML 加载不要依赖 `time.Duration` 的隐式解析。使用 raw config 字符串字段承接 YAML,然后通过 `time.ParseDuration` 显式解析。这样 `"15s"``"1m"``"500ms"` 都清晰可控。
环境变量增加:
- `CODESPACE_READ_TIMEOUT`
- `CODESPACE_WRITE_TIMEOUT`
- `CODESPACE_IDLE_TIMEOUT`
- `CODESPACE_MAX_HEADER_BYTES`
已有环境变量保持不变:
- `CODESPACE_ADDR`
- `CODESPACE_WORKSPACE_ROOT`
- `CODESPACE_OPENCODE_COMMAND`
默认值:
- `addr`: `:8080`
- `readTimeout`: `15s`
- `writeTimeout`: `15s`
- `idleTimeout`: `60s`
- `maxHeaderBytes`: `1048576`
## 路由设计
原路由保持不变:
```text
GET /healthz
POST /api/workspaces
GET /api/workspaces/:id
DELETE /api/workspaces/:id
GET /api/workspaces/:id/files
GET /api/workspaces/:id/files/read
PUT /api/workspaces/:id/files/write
POST /api/workspaces/:id/files/mkdir
DELETE /api/workspaces/:id/files
POST /api/workspaces/:id/files/rename
POST /api/workspaces/:id/process/start
POST /api/workspaces/:id/process/stop
POST /api/workspaces/:id/process/restart
GET /api/workspaces/:id/process/status
```
Gin 注册方式:
```go
r := gin.New()
r.GET("/healthz", healthHandler)
api := r.Group("/api")
api.POST("/workspaces", workspaceHandler.create)
api.GET("/workspaces/:id", workspaceHandler.get)
```
第一版使用 `gin.New()` 而不是 `gin.Default()`,避免隐式加入 logger middleware。只添加 `gin.Recovery()`,防止 panic 打爆进程。请求日志以后有真实需求再加。
## Handler 迁移
签名从:
```go
func (h *workspaceHandler) get(w http.ResponseWriter, r *http.Request)
```
改为:
```go
func (h *workspaceHandler) get(c *gin.Context)
```
路径参数:
```go
id := c.Param("id")
```
查询参数:
```go
path := c.Query("path")
```
JSON body
```go
if err := c.ShouldBindJSON(&req); err != nil {
writeError(c, util.New(util.CodeBadRequest, "invalid json"))
return
}
```
响应:
```go
c.JSON(http.StatusCreated, model.WorkspaceResponse{ID: ws.ID})
c.Status(http.StatusNoContent)
```
## 错误处理
保持现有错误 JSON
```json
{
"error": {
"code": "bad_request",
"message": "invalid json"
}
}
```
`internal/api/errors_helper.go` 改为接收 `*gin.Context`
```go
func writeError(c *gin.Context, err error)
```
继续使用 `util.CodeOf(err)` 映射:
- `bad_request` -> 400
- `not_found` -> 404
- `conflict` -> 409
- default -> 500
可以复用 `pkg/response.ErrorBody``pkg/response.ErrorDetail`,避免重复定义响应结构。
## Server 组合
`cmd/server/main.go` 最终保持显式组合:
```go
router := api.NewRouter(workspaceSvc, fileSvc, processSvc)
server := &http.Server{
Addr: cfg.Server.Addr,
Handler: router,
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
IdleTimeout: cfg.Server.IdleTimeout,
MaxHeaderBytes: cfg.Server.MaxHeaderBytes,
}
```
启动仍然使用:
```go
server.ListenAndServe()
```
## 测试策略
现有 `httptest` 测试继续有效,因为 `*gin.Engine` 实现 `http.Handler`
需要验证:
- `GET /healthz` 行为不变。
- workspace 创建、文件写读、process status 行为不变。
- Gin path param `:id` 正常传入 service。
- 配置默认值正确。
- YAML timeout 字符串能解析。
- 环境变量能覆盖 timeout 和 `maxHeaderBytes`
- `go test ./...` 通过。
- `grep -R "\.Run(" cmd internal || true` 无输出。
- `go run ./cmd/server``/healthz` 返回 `{"status":"ok"}`
## 依赖
新增唯一依赖:
```text
github.com/gin-gonic/gin
```
不新增其他框架或中间件依赖。
## 风险与处理
- 风险:Gin 默认模式可能输出 debug 信息。处理:启动时可设置 `gin.SetMode(gin.ReleaseMode)`,或保留 Gin 默认但不影响功能。第一版建议在 `NewRouter` 中设置 `gin.ReleaseMode`,减少噪音。
- 风险:`time.Duration` YAML 解析行为不符合预期。处理:使用 raw string + `time.ParseDuration`
- 风险:错误响应格式变化。处理:复用现有响应结构并保留 HTTP 测试。
- 风险:误用 `router.Run()`。处理:增加 grep 验证。
## 完成标准
完成后必须满足:
- `internal/api` 使用 Gin 注册路由。
- `cmd/server/main.go` 使用 `http.Server{Handler: router}`
- 不出现 Gin `Run()` 启动服务器。
- server 网络参数来自配置。
- API 行为和响应结构保持兼容。
- `go test ./...` 通过。
- server smoke test 通过。