# 项目工作台 MVP 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:** 构建一个以项目为中心的私有部署工作台 MVP,包含项目、Inbox、任务、Markdown 笔记资料、AI 会话、全文搜索、任务分发和 Tauri 桌面壳。 **Architecture:** 使用前后端分离 monorepo。`backend` 提供 Go Gin + Gorm + PostgreSQL API,`apps/web` 提供响应式 Svelte Web 客户端,`apps/desktop` 用 Tauri 包装 Web 客户端并提供桌面能力。先实现清晰模块边界,再逐步接入 AI、搜索和桌面特性。 **Tech Stack:** Go 1.22+, Gin, Gorm, PostgreSQL 16+, Svelte, TypeScript, Vite, Tauri 2, Vitest, Testing Library for Svelte, Playwright, Docker Compose. ## Global Constraints - 私有部署优先,用户通过管理员创建账号或邀请码注册。 - 数据以服务端为准,本地客户端只是 Tauri 壳,不做离线编辑或本地优先同步。 - 文件第一版存储在服务端本地磁盘,路径逻辑集中在文件服务中。 - 标签是项目内标签,不做全局标签体系。 - AI 默认允许回退到系统 key,必须有调用日志、基础限流、错误记录和用户 key 安全存储。 - AI 不得自动创建正式对象,Inbox 分析结果必须由用户确认。 - 任务分发只面向系统内用户,接收者只能看到任务和显式共享的关联对象。 - 全局搜索使用 PostgreSQL 关键词和全文搜索,语义搜索不进入 MVP。 - 前端只使用 Svelte,不使用 React。 - 登录页必须允许用户填写服务器 IP 或域名,并将其保存为 API base URL。 - PostgreSQL 集成验证使用 `postgres://postgres:Weidong2023~!@8.137.107.29:19432/agent_dev?sslmode=disable`。 - 前端桌面优先,同时保持移动浏览器可用。 - 每个任务结束时运行该任务列出的验证命令并提交。 --- ## File Structure ```text D:\work\senlinai\agent ├── AGENTS.md ├── apps │ ├── web │ │ ├── package.json │ │ ├── index.html │ │ ├── vite.config.ts │ │ ├── tsconfig.json │ │ └── src │ │ ├── app │ │ ├── components │ │ ├── features │ │ ├── lib │ │ └── test │ └── desktop │ ├── package.json │ └── src-tauri ├── backend │ ├── cmd │ │ └── api │ ├── internal │ │ ├── ai │ │ ├── auth │ │ ├── config │ │ ├── db │ │ ├── domain │ │ ├── files │ │ ├── httpx │ │ ├── inbox │ │ ├── notes │ │ ├── projects │ │ ├── search │ │ └── tasks │ ├── migrations │ └── tests ├── infra └── docs ``` 后端 Go module 使用: ```text senlinai-agent/backend ``` --- ## Task 1: Monorepo 与本地开发基础 **Files:** - Create: `D:\work\senlinai\agent\README.md` - Create: `D:\work\senlinai\agent\.gitignore` - Create: `D:\work\senlinai\agent\infra\docker-compose.yml` - Create: `D:\work\senlinai\agent\backend\go.mod` - Create: `D:\work\senlinai\agent\backend\cmd\api\main.go` - Create: `D:\work\senlinai\agent\backend\internal\config\config.go` - Create: `D:\work\senlinai\agent\backend\internal\httpx\router.go` - Create: `D:\work\senlinai\agent\backend\internal\httpx\router_test.go` **Interfaces:** - Produces: `config.Load() config.Config` - Produces: `httpx.NewRouter(cfg config.Config) *gin.Engine` - Produces: `GET /healthz -> {"status":"ok"}` - [ ] **Step 1: 创建目录和 Go module** ```powershell New-Item -ItemType Directory -Force backend\cmd\api, backend\internal\config, backend\internal\httpx, infra Set-Location backend go mod init senlinai-agent/backend go get github.com/gin-gonic/gin@latest github.com/stretchr/testify@latest ``` Expected: `backend/go.mod` exists. - [ ] **Step 2: 写 healthz 失败测试** Create `backend/internal/httpx/router_test.go`: ```go package httpx import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/require" "senlinai-agent/backend/internal/config" ) func TestHealthz(t *testing.T) { router := NewRouter(config.Config{Env: "test"}) req := httptest.NewRequest(http.MethodGet, "/healthz", nil) rec := httptest.NewRecorder() router.ServeHTTP(rec, req) require.Equal(t, http.StatusOK, rec.Code) require.JSONEq(t, `{"status":"ok"}`, rec.Body.String()) } ``` - [ ] **Step 3: 确认测试失败** ```powershell Set-Location D:\work\senlinai\agent\backend go test ./internal/httpx -run TestHealthz -v ``` Expected: FAIL because `NewRouter` or `config.Config` is undefined. - [ ] **Step 4: 实现 config、router、main** Create `backend/internal/config/config.go`: ```go package config import "os" type Config struct { Env string DatabaseURL string StorageDir string SystemAIKey string } func Load() Config { return Config{ Env: getenv("APP_ENV", "development"), DatabaseURL: getenv("DATABASE_URL", "postgres://agent:agent@localhost:5432/agent?sslmode=disable"), StorageDir: getenv("STORAGE_DIR", "./data/files"), SystemAIKey: os.Getenv("SYSTEM_AI_KEY"), } } func getenv(key string, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } ``` Create `backend/internal/httpx/router.go`: ```go package httpx import ( "net/http" "github.com/gin-gonic/gin" "senlinai-agent/backend/internal/config" ) func NewRouter(cfg config.Config) *gin.Engine { if cfg.Env == "test" { gin.SetMode(gin.TestMode) } router := gin.New() router.Use(gin.Recovery()) router.GET("/healthz", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) return router } ``` Create `backend/cmd/api/main.go`: ```go package main import ( "log" "senlinai-agent/backend/internal/config" "senlinai-agent/backend/internal/httpx" ) func main() { cfg := config.Load() router := httpx.NewRouter(cfg) if err := router.Run(":8080"); err != nil { log.Fatal(err) } } ``` - [ ] **Step 5: 增加基础本地开发文件** Create `.gitignore`: ```gitignore node_modules/ dist/ target/ .env data/ coverage/ *.log ``` Create `infra/docker-compose.yml`: ```yaml services: postgres: image: postgres:16 environment: POSTGRES_USER: agent POSTGRES_PASSWORD: agent POSTGRES_DB: agent ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data volumes: postgres_data: ``` Create `README.md`: ```markdown # SenlinAI Agent Workbench Private project-centered workbench MVP. ## Development ```powershell docker compose -f infra/docker-compose.yml up -d Set-Location backend go run ./cmd/api ``` ``` - [ ] **Step 6: 验证并提交** ```powershell Set-Location D:\work\senlinai\agent\backend go test ./... -v Set-Location D:\work\senlinai\agent git add README.md .gitignore infra backend git commit -m "chore: scaffold project workbench" ``` --- ## Task 2: 数据库模型与迁移边界 **Files:** - Create: `D:\work\senlinai\agent\backend\internal\db\db.go` - Create: `D:\work\senlinai\agent\backend\internal\domain\models.go` - Create: `D:\work\senlinai\agent\backend\internal\domain\models_test.go` **Interfaces:** - Produces: `db.Open(databaseURL string) (*gorm.DB, error)` - Produces: `domain.AutoMigrate(database *gorm.DB) error` - Produces models: `User`, `Project`, `InboxItem`, `Task`, `Note`, `Source`, `AISession`, `Tag`, `ProjectEvent`, `AIKey`, `AICallLog`, `TaskShare` - [ ] **Step 1: 添加 Gorm 依赖** ```powershell Set-Location D:\work\senlinai\agent\backend go get gorm.io/gorm@latest gorm.io/driver/postgres@latest gorm.io/driver/sqlite@latest ``` - [ ] **Step 2: 写 AutoMigrate 失败测试** Create `backend/internal/domain/models_test.go`: ```go package domain import ( "testing" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" ) func TestAutoMigrateCreatesCoreTables(t *testing.T) { database, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, AutoMigrate(database)) for _, table := range []string{ "users", "projects", "inbox_items", "tasks", "notes", "sources", "ai_sessions", "tags", "project_events", "ai_keys", "ai_call_logs", "task_shares", } { require.True(t, database.Migrator().HasTable(table), "missing table %s", table) } } ``` - [ ] **Step 3: 实现 domain models** Create `backend/internal/domain/models.go` with the structs listed in the interface. Required fields: ```go package domain import ( "time" "gorm.io/gorm" ) type User struct { ID uint `gorm:"primaryKey"`; Email string `gorm:"uniqueIndex;not null"`; DisplayName string `gorm:"not null"`; PasswordHash string `gorm:"not null"`; Role string `gorm:"not null;default:user"`; CreatedAt time.Time; UpdatedAt time.Time } type Project struct { ID uint `gorm:"primaryKey"`; OwnerID uint `gorm:"index;not null"`; Name string `gorm:"not null"`; Description string; CreatedAt time.Time; UpdatedAt time.Time } type InboxItem struct { ID uint `gorm:"primaryKey"`; ProjectID uint `gorm:"index;not null"`; CreatedBy uint `gorm:"index;not null"`; SourceType string `gorm:"not null"`; Title string; Body string; Status string `gorm:"not null;default:open"`; CreatedAt time.Time; UpdatedAt time.Time } type Task struct { ID uint `gorm:"primaryKey"`; ProjectID uint `gorm:"index;not null"`; CreatedBy uint `gorm:"index;not null"`; AssigneeID *uint `gorm:"index"`; Title string `gorm:"not null"`; Description string; Status string `gorm:"not null;default:open"`; SortOrder int `gorm:"not null;default:0"`; DueAt *time.Time; CreatedAt time.Time; UpdatedAt time.Time } type Note struct { ID uint `gorm:"primaryKey"`; ProjectID uint `gorm:"index;not null"`; CreatedBy uint `gorm:"index;not null"`; Title string `gorm:"not null"`; Markdown string `gorm:"type:text"`; CreatedAt time.Time; UpdatedAt time.Time } type Source struct { ID uint `gorm:"primaryKey"`; ProjectID uint `gorm:"index;not null"`; CreatedBy uint `gorm:"index;not null"`; Kind string `gorm:"not null"`; Title string `gorm:"not null"`; URL string; FilePath string; ContentText string `gorm:"type:text"`; CreatedAt time.Time; UpdatedAt time.Time } type AISession struct { ID uint `gorm:"primaryKey"`; ProjectID uint `gorm:"index;not null"`; CreatedBy uint `gorm:"index;not null"`; Title string `gorm:"not null"`; Context string `gorm:"type:text"`; CreatedAt time.Time; UpdatedAt time.Time } type Tag struct { ID uint `gorm:"primaryKey"`; ProjectID uint `gorm:"index;not null"`; Name string `gorm:"not null"`; CreatedAt time.Time } type ProjectEvent struct { ID uint `gorm:"primaryKey"`; ProjectID uint `gorm:"index;not null"`; ActorID uint `gorm:"index;not null"`; EventType string `gorm:"not null"`; EntityType string `gorm:"not null"`; EntityID uint `gorm:"not null"`; Summary string `gorm:"not null"`; CreatedAt time.Time } type AIKey struct { ID uint `gorm:"primaryKey"`; UserID uint `gorm:"uniqueIndex;not null"`; Provider string `gorm:"not null"`; EncryptedAPIKey string `gorm:"not null"`; CreatedAt time.Time; UpdatedAt time.Time } type AICallLog struct { ID uint `gorm:"primaryKey"`; UserID uint `gorm:"index;not null"`; Provider string `gorm:"not null"`; UsedKeyType string `gorm:"not null"`; Action string `gorm:"not null"`; Status string `gorm:"not null"`; Error string; CreatedAt time.Time } type TaskShare struct { ID uint `gorm:"primaryKey"`; TaskID uint `gorm:"index;not null"`; ObjectType string `gorm:"not null"`; ObjectID uint `gorm:"not null"`; CreatedAt time.Time } func AutoMigrate(database *gorm.DB) error { return database.AutoMigrate(&User{}, &Project{}, &InboxItem{}, &Task{}, &Note{}, &Source{}, &AISession{}, &Tag{}, &ProjectEvent{}, &AIKey{}, &AICallLog{}, &TaskShare{}) } ``` - [ ] **Step 4: 实现 PostgreSQL 连接** Create `backend/internal/db/db.go`: ```go package db import ( "gorm.io/driver/postgres" "gorm.io/gorm" ) func Open(databaseURL string) (*gorm.DB, error) { return gorm.Open(postgres.Open(databaseURL), &gorm.Config{}) } ``` - [ ] **Step 5: 验证并提交** ```powershell Set-Location D:\work\senlinai\agent\backend go test ./... -v Set-Location D:\work\senlinai\agent git add backend git commit -m "feat: add core data model" ``` --- ## Task 3: 私有部署认证与邀请注册 **Files:** - Create: `D:\work\senlinai\agent\backend\internal\auth\service.go` - Create: `D:\work\senlinai\agent\backend\internal\auth\service_test.go` - Create: `D:\work\senlinai\agent\backend\internal\auth\middleware.go` **Interfaces:** - Produces: `auth.Service.CreateInvite(adminID uint, email string) (string, error)` - Produces: `auth.Service.RegisterWithInvite(token, email, displayName, password string) (*domain.User, error)` - Produces: `auth.Service.Login(email, password string) (string, error)` - Produces: `auth.RequireUser(tokenVerifier func(string) (uint, error)) gin.HandlerFunc` - [ ] Write tests for invite registration, wrong-email rejection, and login failure. - [ ] Implement invite token signing with HMAC SHA-256. - [ ] Store password hashes with bcrypt. - [ ] Add bearer-token middleware that sets `currentUserID`. - [ ] Run `go test ./... -v` from `backend`. - [ ] Commit with `git commit -m "feat: add private auth foundation"`. --- ## Task 4: 项目、项目内标签与 Dashboard API **Files:** - Create: `D:\work\senlinai\agent\backend\internal\projects\service.go` - Create: `D:\work\senlinai\agent\backend\internal\projects\service_test.go` - Create: `D:\work\senlinai\agent\backend\internal\projects\handlers.go` - Modify: `D:\work\senlinai\agent\backend\internal\httpx\router.go` **Interfaces:** - Produces: `projects.Service.CreateProject(ownerID uint, name string, description string) (*domain.Project, error)` - Produces: `projects.Service.ListProjects(ownerID uint) ([]domain.Project, error)` - Produces: `projects.Service.CreateTag(projectID uint, name string) (*domain.Tag, error)` - Produces: `projects.Service.ListTags(projectID uint) ([]domain.Tag, error)` - Produces: `projects.Service.Dashboard(projectID uint) (Dashboard, error)` - [ ] Test that identical tag names can exist in different projects. - [ ] Test that dashboard counters only count objects under the requested project. - [ ] Implement project CRUD service. - [ ] Implement project-local tag service. - [ ] Implement `GET /projects`, `POST /projects`, `GET /projects/:id/dashboard`. - [ ] Run `go test ./... -v` from `backend`. - [ ] Commit with `git commit -m "feat: add project workspace service"`. --- ## Task 5: Inbox 收集与 AI 建议确认 **Files:** - Create: `D:\work\senlinai\agent\backend\internal\inbox\service.go` - Create: `D:\work\senlinai\agent\backend\internal\inbox\service_test.go` - Create: `D:\work\senlinai\agent\backend\internal\inbox\handlers.go` **Interfaces:** - Produces: `inbox.Service.Capture(input CaptureInput) (*domain.InboxItem, error)` - Produces: `inbox.Service.Analyze(itemID uint, userID uint) ([]Suggestion, error)` - Produces: `inbox.Service.Confirm(itemID uint, selected []Suggestion) error` - Produces: `Suggestion{Kind string, Title string, Body string}` - [ ] Test capture of text, link, and file metadata into project inbox. - [ ] Test that Analyze returns suggestions but creates no official objects. - [ ] Test that Confirm creates selected task/note/source and marks the inbox item `processed`. - [ ] Implement analyzer interface with a static fake for tests. - [ ] Implement handlers for capture, analyze, and confirm. - [ ] Run `go test ./... -v` from `backend`. - [ ] Commit with `git commit -m "feat: add inbox capture workflow"`. --- ## Task 6: 任务、指派与显式共享 **Files:** - Create: `D:\work\senlinai\agent\backend\internal\tasks\service.go` - Create: `D:\work\senlinai\agent\backend\internal\tasks\service_test.go` - Modify: `D:\work\senlinai\agent\backend\internal\domain\models.go` **Interfaces:** - Produces: `tasks.Service.Assign(taskID uint, assigneeID uint) error` - Produces: `tasks.Service.ShareObject(taskID uint, objectType string, objectID uint) error` - Produces: `tasks.Service.VisibleLinkedObjects(taskID uint, viewerID uint) ([]LinkedObject, error)` - [ ] Test that assignee can see the task. - [ ] Test that assignee cannot see linked note/source until explicitly shared. - [ ] Test unsupported shared object type returns an error. - [ ] Implement task assignment and sharing service. - [ ] Record assignment and sharing as project events. - [ ] Run `go test ./... -v` from `backend`. - [ ] Commit with `git commit -m "feat: add task assignment sharing"`. --- ## Task 7: 笔记、资料、本地文件存储与全文搜索 **Files:** - Create: `D:\work\senlinai\agent\backend\internal\files\service.go` - Create: `D:\work\senlinai\agent\backend\internal\files\service_test.go` - Create: `D:\work\senlinai\agent\backend\internal\notes\service.go` - Create: `D:\work\senlinai\agent\backend\internal\search\service.go` - Create: `D:\work\senlinai\agent\backend\internal\search\service_test.go` - Create: `D:\work\senlinai\agent\backend\migrations\0001_search_indexes.sql` **Interfaces:** - Produces: `files.Service.Save(projectID uint, originalName string, content io.Reader) (StoredFile, error)` - Produces: `notes.Service.CreateNote(projectID uint, userID uint, title string, markdown string) (*domain.Note, error)` - Produces: `search.Service.Search(userID uint, query string) ([]Result, error)` - [ ] Test that file storage writes under `projects//`. - [ ] Test that file path traversal in original filename is neutralized with `filepath.Base`. - [ ] Test note body search through `Search(userID, query)`. - [ ] Implement local file service. - [ ] Implement note/source service. - [ ] Implement portable test search with SQLite `LIKE`. - [ ] Add PostgreSQL `tsvector` indexes for notes, sources, inbox items, tasks, and AI sessions. - [ ] Add PostgreSQL search query using `plainto_tsquery('simple', query)`. - [ ] Run `go test ./... -v` from `backend`. - [ ] Commit with `git commit -m "feat: add files notes and search"`. --- ## Task 8: AI Provider 网关、Key 选择、日志与 Session **Files:** - Create: `D:\work\senlinai\agent\backend\internal\ai\gateway.go` - Create: `D:\work\senlinai\agent\backend\internal\ai\gateway_test.go` - Create: `D:\work\senlinai\agent\backend\internal\ai\sessions.go` **Interfaces:** - Produces: `ai.Gateway.SelectKey(userID uint) (SelectedKey, error)` - Produces: `ai.Gateway.RecordCall(userID uint, provider string, usedKeyType string, action string, status string, errText string) error` - Produces: `ai.SessionService.Create(projectID uint, userID uint, title string) (*domain.AISession, error)` - [ ] Test that user key takes precedence over system key. - [ ] Test fallback to system key when user key is absent. - [ ] Test no available key returns an explicit error. - [ ] Test AI call logging records action, provider, key type, status, and error. - [ ] Implement AI session creation under a project. - [ ] Run `go test ./... -v` from `backend`. - [ ] Commit with `git commit -m "feat: add ai gateway foundation"`. --- ## Task 9: Web App Shell、项目 Dashboard 与标签页 **Files:** - Create: `D:\work\senlinai\agent\apps\web\package.json` - Create: `D:\work\senlinai\agent\apps\web\src\app\App.tsx` - Create: `D:\work\senlinai\agent\apps\web\src\app\tabs.ts` - Create: `D:\work\senlinai\agent\apps\web\src\lib\api.ts` - Create: `D:\work\senlinai\agent\apps\web\src\features\projects\ProjectDashboard.tsx` - Create: `D:\work\senlinai\agent\apps\web\src\features\projects\ProjectDashboard.test.tsx` **Interfaces:** - Produces: `api.getProjects(): Promise` - Produces: `api.getProjectDashboard(projectID: number): Promise` - Produces: `openTab(existing: WorkspaceTab[], next: WorkspaceTab): WorkspaceTab[]` - [ ] Scaffold Vite Svelte TypeScript app. - [ ] Install Svelte, Vitest, Testing Library for Svelte, jest-dom, jsdom. - [ ] Implement server address input on the login shell and persist it as API base URL. - [ ] Test dashboard renders pending inbox, current tasks, notes, and AI sessions. - [ ] Implement API client. - [ ] Implement browser-style tab state helper. - [ ] Implement dashboard component. - [ ] Run `npm test -- --run` and `npm run build` from `apps/web`. - [ ] Commit with `git commit -m "feat: add web workspace shell"`. --- ## Task 10: Inbox、任务、笔记资料与 AI Session UI **Files:** - Create: `D:\work\senlinai\agent\apps\web\src\features\inbox\InboxPanel.tsx` - Create: `D:\work\senlinai\agent\apps\web\src\features\inbox\SuggestionList.tsx` - Create: `D:\work\senlinai\agent\apps\web\src\features\tasks\TaskList.tsx` - Create: `D:\work\senlinai\agent\apps\web\src\features\notes\MarkdownEditor.tsx` - Create: `D:\work\senlinai\agent\apps\web\src\features\ai\AISessionCards.tsx` - Create: `D:\work\senlinai\agent\apps\web\src\features\inbox\SuggestionList.test.tsx` **Interfaces:** - Produces: `SuggestionList` with explicit checkbox confirmation. - Produces: `MarkdownEditor` controlled component using `value` and `onChange`. - Produces: `AISessionCards` card list for project sessions. - [ ] Test that SuggestionList does not confirm unselected suggestions. - [ ] Implement inbox capture panel. - [ ] Implement suggestion list with checkbox selection and editable basic fields. - [ ] Implement task list with status and assignee display. - [ ] Implement controlled Markdown editor. - [ ] Implement AI session card list. - [ ] Run `npm test -- --run` and `npm run build` from `apps/web`. - [ ] Commit with `git commit -m "feat: add workspace feature panels"`. --- ## Task 11: Tauri 桌面壳与快速收集 **Files:** - Create: `D:\work\senlinai\agent\apps\desktop\package.json` - Create: `D:\work\senlinai\agent\apps\desktop\src-tauri\tauri.conf.json` - Create: `D:\work\senlinai\agent\apps\desktop\src-tauri\src\main.rs` - Create: `D:\work\senlinai\agent\apps\web\src\features\capture\QuickCapture.svelte` **Interfaces:** - Produces: Tauri app pointing at `apps/web` build/dev server. - Produces: global shortcut command opening quick capture window. - Produces: `QuickCapture` component with project destination and content input. - [ ] Scaffold Tauri app under `apps/desktop`. - [ ] Configure Tauri product name `SenlinAI Workbench`. - [ ] Configure dev URL `http://localhost:5173`. - [ ] Configure frontend dist `../../web/dist`. - [ ] Implement QuickCapture with project selector and content textarea. - [ ] Verify `npm run build` from `apps/web`. - [ ] Verify `npm run tauri build` from `apps/desktop`, documenting any missing native dependency. - [ ] Commit with `git commit -m "feat: add tauri desktop shell"`. --- ## Task 12: 端到端验证与 MVP 文档 **Files:** - Create: `D:\work\senlinai\agent\apps\web\e2e\project-workbench.spec.ts` - Modify: `D:\work\senlinai\agent\README.md` - Create: `D:\work\senlinai\agent\docs\mvp-verification.md` **Interfaces:** - Produces: one documented manual verification script. - Produces: Playwright smoke test for project dashboard and inbox suggestion confirmation. - [ ] Install Playwright in `apps/web`. - [ ] Add smoke test that verifies the project workbench screen and dashboard render. - [ ] Document backend verification: `go test ./... -v` from `backend`. - [ ] Document web verification: `npm test -- --run`, `npm run build`, `npx playwright test`. - [ ] Document desktop verification: build web first, then run Tauri build. - [ ] Document manual MVP flow: create project, capture inbox item, analyze, confirm task/note, assign task, share linked object, search, open AI session, quick capture. - [ ] Run all available verification commands. - [ ] Commit with `git commit -m "test: add mvp verification plan"`. --- ## Self-Review Spec coverage: - 项目工作区、Dashboard、Inbox、任务、笔记资料、AI 会话、项目内标签、任务分发、文件存储、AI key 策略、搜索、Tauri 壳和验证流程均有对应任务。 - 语义搜索、真实即时通讯、项目级成员体系、公开分享链接和移动原生 App 明确不进入 MVP。 Placeholder scan: - 本计划没有使用待填充占位内容。 - 所有任务都有明确文件、接口、验证命令和提交命令。 Type consistency: - 后端统一使用 `backend` 目录。 - Go module path 统一为 `senlinai-agent/backend`。 - 后端服务均以明确构造函数创建。 - 核心模型来自 `domain` 包。 - 前端 API 类型和 Dashboard 组件字段保持一致。