231 lines
8.9 KiB
Markdown
231 lines
8.9 KiB
Markdown
# 森林AI API And Backend Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 建立不兼容旧接口的 `/api/v1` camelCase 契约,并把后端拆分为职责清晰、以 identity 对外的业务模块。
|
||
|
||
**Architecture:** Gorm 模型继续集中在 `internal/models`,HTTP request/response DTO 放在各业务包。`httpx` 负责通用错误、identity 参数和 CORS;service 负责所有权与业务规则;handler 不直接构造存储路径或查询数据库。
|
||
|
||
**Tech Stack:** Go、Gin、Gorm、PostgreSQL、httptest。
|
||
|
||
## Global Constraints
|
||
|
||
- 外部 API 前缀为 `/api/v1`,字段为 camelCase,对外 ID 为 UUIDv7 identity。
|
||
- 数据库模型和表名前缀规则保持 `Sa` / `sa_`。
|
||
- session token 必须签名并过期;handler 只从认证 middleware 读取用户。
|
||
- 文件路径必须由文件服务构造;标签必须限定在项目内。
|
||
- AI 请求必须限流并记录 provider、key 类型、action、status 和 error。
|
||
- 允许不兼容旧 API,但不得自动删除生产数据。
|
||
|
||
---
|
||
|
||
### Task 1: 通用 v1 路由、错误和 identity 参数
|
||
|
||
**Files:**
|
||
- Create: `backend/internal/httpx/response.go`
|
||
- Create: `backend/internal/httpx/params.go`
|
||
- Create: `backend/internal/httpx/response_test.go`
|
||
- Modify: `backend/internal/httpx/router.go`
|
||
- Modify: `backend/internal/httpx/router_test.go`
|
||
- Modify: `backend/internal/config/config.go`
|
||
- Modify: `backend/etc/agent.dev.yaml`
|
||
|
||
**Interfaces:**
|
||
- Produces: `Error(c, status, code, message)`, `IdentityParam(c, name) (string, bool)` and `/api/v1` route group.
|
||
|
||
- [ ] **Step 1: Write failing response and routing tests**
|
||
|
||
```go
|
||
func TestErrorUsesStableEnvelope(t *testing.T) {
|
||
recorder := httptest.NewRecorder()
|
||
ctx, _ := gin.CreateTestContext(recorder)
|
||
Error(ctx, http.StatusBadRequest, "invalid_request", "请求参数无效")
|
||
require.JSONEq(t, `{"error":{"code":"invalid_request","message":"请求参数无效"}}`, recorder.Body.String())
|
||
}
|
||
|
||
func TestStatusLivesUnderAPIV1(t *testing.T) {
|
||
router := NewRouter(config.Config{Env: "test"})
|
||
req := httptest.NewRequest(http.MethodGet, "/api/v1/status", nil)
|
||
recorder := httptest.NewRecorder()
|
||
router.ServeHTTP(recorder, req)
|
||
require.Equal(t, http.StatusOK, recorder.Code)
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Verify tests fail**
|
||
|
||
Run: `go test ./internal/httpx -run 'TestErrorUsesStableEnvelope|TestStatusLivesUnderAPIV1' -v`
|
||
|
||
Expected: FAIL because helpers and route do not exist.
|
||
|
||
- [ ] **Step 3: Implement response helpers and configured CORS**
|
||
|
||
Use:
|
||
|
||
```go
|
||
type ErrorBody struct { Code string `json:"code"`; Message string `json:"message"` }
|
||
type ErrorEnvelope struct { Error ErrorBody `json:"error"` }
|
||
|
||
func Error(c *gin.Context, status int, code, message string) {
|
||
c.AbortWithStatusJSON(status, ErrorEnvelope{Error: ErrorBody{Code: code, Message: message}})
|
||
}
|
||
```
|
||
|
||
Change the API group to `router.Group("/api/v1")`. Add `AllowedOrigins []string` to config and build the CORS map from config with safe development defaults.
|
||
|
||
- [ ] **Step 4: Verify the package**
|
||
|
||
Run: `go test ./internal/httpx -v`
|
||
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```powershell
|
||
git add backend/internal/httpx backend/internal/config backend/etc/agent.dev.yaml
|
||
git commit -m "refactor(api): introduce v1 response contract"
|
||
```
|
||
|
||
### Task 2: 项目 DTO、identity 查询与项目更新
|
||
|
||
**Files:**
|
||
- Create: `backend/internal/logic/projects/dto.go`
|
||
- Create: `backend/internal/logic/projects/ownership.go`
|
||
- Modify: `backend/internal/logic/projects/service.go`
|
||
- Modify: `backend/internal/logic/projects/handlers.go`
|
||
- Modify: `backend/internal/logic/projects/handlers_test.go`
|
||
- Modify: `backend/internal/logic/projects/service_test.go`
|
||
|
||
**Interfaces:**
|
||
- Produces: `ProjectDTO`, `CreateProjectRequest`, `UpdateProjectRequest`, `FindOwnedProject(userID, identity)`.
|
||
|
||
- [ ] **Step 1: Write failing contract tests**
|
||
|
||
Add tests that create a project, call `GET /api/v1/projects`, assert `id` equals its identity, assert no numeric `ID` field exists, and patch `name`, `identifier`, `icon`, `background`, `description` through `PATCH /api/v1/projects/:projectId`.
|
||
|
||
- [ ] **Step 2: Verify failures**
|
||
|
||
Run: `go test ./internal/logic/projects -run 'TestListProjectsUsesIdentityDTO|TestUpdateProject' -v`
|
||
|
||
Expected: FAIL on old response and missing PATCH route.
|
||
|
||
- [ ] **Step 3: Implement explicit DTO mapping**
|
||
|
||
```go
|
||
type ProjectDTO struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Identifier string `json:"identifier"`
|
||
Icon string `json:"icon"`
|
||
Background string `json:"background"`
|
||
Description string `json:"description"`
|
||
}
|
||
|
||
func projectDTO(project models.SaProject) ProjectDTO {
|
||
return ProjectDTO{ID: project.Identity, Name: project.Name, Identifier: project.Identifier, Icon: project.Icon, Background: project.Background, Description: project.Description}
|
||
}
|
||
```
|
||
|
||
Resolve projects with `owner_id = ? AND identity = ?`. Return 404 for missing owned projects and 409 for duplicate identifiers.
|
||
|
||
- [ ] **Step 4: Verify project contracts**
|
||
|
||
Run: `go test ./internal/logic/projects -v`
|
||
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```powershell
|
||
git add backend/internal/logic/projects
|
||
git commit -m "refactor(api): expose project identity DTOs"
|
||
```
|
||
|
||
### Task 3: 拆分工作区聚合查询
|
||
|
||
**Files:**
|
||
- Create: `backend/internal/logic/projects/workspace.go`
|
||
- Create: `backend/internal/logic/projects/workspace_test.go`
|
||
- Modify: `backend/internal/logic/projects/service.go`
|
||
- Modify: `backend/internal/logic/projects/dto.go`
|
||
|
||
**Interfaces:**
|
||
- Produces: `Workspace(userID uint, projectIdentity string) (WorkspaceDTO, error)`.
|
||
|
||
- [ ] **Step 1: Move existing workspace tests to identity-based expectations**
|
||
|
||
Assert that project, tasks, tags, notes/sources, sessions and cron plans all expose string identities and RFC3339 timestamps, and that tasks return the actual project-scoped tag.
|
||
|
||
- [ ] **Step 2: Verify tests fail against the old aggregate**
|
||
|
||
Run: `go test ./internal/logic/projects -run Workspace -v`
|
||
|
||
Expected: FAIL on numeric IDs and preformatted display times.
|
||
|
||
- [ ] **Step 3: Move workspace-only code and remove presentation formatting**
|
||
|
||
Move `workspaceCounts`, `workspaceChannels`, `workspaceInbox`, `workspaceTasks`, `workspaceAISessions`, `workspaceNotesSources` and `workspaceCronPlans` into `workspace.go`. DTO times must use `time.Time`/`*time.Time` JSON serialization; delete `displayTime`, `displayOptionalTime` and `displayDuration` from backend presentation logic.
|
||
|
||
- [ ] **Step 4: Verify and check file focus**
|
||
|
||
Run: `go test ./internal/logic/projects -v`
|
||
|
||
Expected: PASS; `service.go` contains project CRUD only and is under 300 lines.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```powershell
|
||
git add backend/internal/logic/projects
|
||
git commit -m "refactor(backend): isolate workspace aggregation"
|
||
```
|
||
|
||
### Task 4: 任务、标签、资料与计划任务职责迁移
|
||
|
||
**Files:**
|
||
- Create: `backend/internal/logic/tasks/dto.go`
|
||
- Create: `backend/internal/logic/tasks/handlers.go`
|
||
- Create: `backend/internal/logic/tasks/handlers_test.go`
|
||
- Create: `backend/internal/logic/files/handlers.go`
|
||
- Create: `backend/internal/logic/files/handlers_test.go`
|
||
- Create: `backend/internal/logic/projects/tag_handlers.go`
|
||
- Create: `backend/internal/logic/projects/cron_handlers.go`
|
||
- Modify: `backend/internal/logic/tasks/service.go`
|
||
- Modify: `backend/internal/logic/files/service.go`
|
||
- Modify: `backend/internal/logic/projects/handlers.go`
|
||
- Modify: `backend/cmd/api/main.go`
|
||
|
||
**Interfaces:**
|
||
- Produces: identity-based Task DTO and separate route registrars for tasks/files/tags/cron plans.
|
||
|
||
- [ ] **Step 1: Write failing registrar contract tests**
|
||
|
||
Cover create/update task, create/list tag, upload source and create cron plan. Assert camelCase responses, identity IDs, project ownership, tag scope and source paths that do not contain absolute storage roots.
|
||
|
||
- [ ] **Step 2: Verify tests fail**
|
||
|
||
Run: `go test ./internal/logic/tasks ./internal/logic/files ./internal/logic/projects -v`
|
||
|
||
Expected: FAIL because registrars and DTOs are missing.
|
||
|
||
- [ ] **Step 3: Move handlers without weakening rules**
|
||
|
||
`tasks.Service` must accept a DB transaction or repository dependency and retain explicit-share checks. `files.Handler` must call `files.Service.Save` before creating the source record. Tag and cron handlers may remain in `projects` but must not be in the core project handler file.
|
||
|
||
- [ ] **Step 4: Register every handler under `/api/v1`**
|
||
|
||
Construct and pass auth, projects, tasks, files, inbox, search and AI registrars in `cmd/api/main.go`. Add Chinese comments to ownership, sharing, Inbox confirmation and file-path boundaries.
|
||
|
||
- [ ] **Step 5: Verify backend**
|
||
|
||
Run: `go test ./...`
|
||
|
||
Expected: PASS.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```powershell
|
||
git add backend/internal/logic backend/cmd/api/main.go
|
||
git commit -m "refactor(backend): split project write responsibilities"
|
||
```
|
||
|