Files
agent/docs/superpowers/plans/2026-07-18-project-workbench-mvp.md

646 lines
24 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.
# 项目工作台 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/<projectID>/`.
- [ ] 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<Project[]>`
- Produces: `api.getProjectDashboard(projectID: number): Promise<ProjectDashboardSummary>`
- 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 组件字段保持一致。