FlowTask
一个用 Tauri 2 + React + Rust 构建的本地优先桌面任务管理器。下面每一份产物都是用 OpenLogos 方法论、借助 Claude Code 生成的。
.claude-plugin,配合 skills/ commands/ hooks/,通过对话式 AI 编程实现方法论驱动的开发。 需求 —— 谁需要它,为什么?
来源:logos/resources/prd/1-product-requirements/01-requirements.md
用户画像与痛点
小李,28 岁,自由职业者 —— 同时兼顾多个项目,任务散落在便利贴、手机备忘录和笔记本里。常常错过截止日期,因为没有一个统一的地方来追踪一切。
场景概览
| ID | 场景 | 触发 | 痛点 | 优先级 |
|---|---|---|---|---|
| S01 | 新用户注册并开始使用 | 首次启动应用 | P01, P03 | P0 |
| S02 | 登录并管理今天的任务 | 已注册用户打开应用 | P01, P02 | P0 |
| S03 | 按分类组织任务 | 用户想给任务归类 | P02 | P1 |
| S04 | 修改账户密码 | 用户想修改密码 | P03 | P1 |
验收标准 —— S01:注册
产品设计 —— 用户会看到什么、做什么?
来源:logos/resources/prd/2-product-design/1-feature-specs/01-feature-specs.md
信息架构
FlowTask
├── Auth Module
│ ├── Register Page (first launch)
│ └── Login Page (returning user)
└── Main App (after login)
├── Task View (default)
│ ├── Category Sidebar
│ ├── Task List
│ └── Create/Edit Task Panel
└── Account Settings
└── Change Password Form UI 原型 —— 注册与主视图
来源:logos/resources/prd/2-product-design/2-page-design/
在产品设计阶段生成的交互式 HTML 原型 —— 每个页面都在写任何代码之前就设计好了。
S01 交互规格 —— 注册流程
- 应用启动,检测到不存在本地账户 → 显示注册页
- 注册页包含:用户名输入框、密码输入框、确认密码输入框、注册按钮、「已有账户?登录」链接
- 实时校验:用户名 2-20 字符、密码 ≥8 字符、两次密码一致
- 提交成功 → 创建本地账户(密码加密)→ 自动登录 → 跳转到任务视图
- 任务视图首次进入显示空状态:「还没有任务,点击 + 创建你的第一个任务」
| 字段 | 类型 | 必填 | 校验 |
|---|---|---|---|
| 用户名 | 文本输入框 | 是 | 2-20 个字符 |
| 密码 | 密码输入框 | 是 | ≥ 8 个字符 |
| 确认密码 | 密码输入框 | 是 | 必须与密码一致 |
实现 —— 从架构到验证通过的代码
架构概览
来源:logos/resources/prd/3-technical-plan/1-architecture/01-architecture-overview.md
graph TB
subgraph FE["Frontend (React 18 + TypeScript)"]
Pages["React Components
UI Pages"]
Store["Zustand Store
Global State"]
IPC["Tauri IPC
Frontend-Backend Bridge"]
end
subgraph BE["Backend (Rust Commands)"]
Commands["auth · task · category
Auth / Task CRUD / Category CRUD / bcrypt"]
end
subgraph DB["Data Layer"]
SQLite[("SQLite
Local File Storage")]
end
Pages --> Store
Store --> IPC
IPC --> Commands
Commands --> SQLite 业务逻辑(表单、状态、路由)放在 TypeScript;Rust 只负责数据库读写和 bcrypt 加密 —— 在保持前端开发者友好的同时,尽量减少 Rust 代码。
场景 → 时序图
来源:logos/resources/prd/3-technical-plan/2-scenario-implementation/S01-register.md
S01:用户注册 —— 时序图
sequenceDiagram
participant U as User
participant FE as React Frontend
participant BE as Tauri Commands (Rust)
participant DB as SQLite
U->>FE: Step 1: Open app
FE->>BE: Step 2: invoke('check_has_user')
BE->>DB: Step 3: SELECT COUNT(*) FROM users
DB-->>BE: Step 4: return count = 0
BE-->>FE: Step 5: return { has_user: false }
FE-->>U: Step 6: Render Register page
U->>FE: Step 7: Fill username, password, confirm & click Register
FE->>FE: Step 8: Validate — name 2-20 chars, pwd ≥8, match
Note over FE: Fail → EX-8.1 / EX-8.2 / EX-8.3
FE->>BE: Step 9: invoke('register', { username, password })
BE->>DB: Step 10: SELECT id FROM users WHERE username = ?
DB-->>BE: Step 11: return empty (username available)
Note over BE: Taken → EX-10.1
BE->>BE: Step 12: bcrypt::hash(password, cost=12)
BE->>DB: Step 13: INSERT INTO users (username, password)
DB-->>BE: Step 14: return new user { id, username, created_at }
Note over BE: Write fail → EX-13.1
BE-->>FE: Step 15: return { user_id, username }
FE->>FE: Step 16: Write Zustand Store { currentUser }
FE-->>U: Step 17: Redirect to Task View (empty state) 步骤
- 用户打开 FlowTask 桌面应用。
- React 前端调用
invoke('check_has_user'),询问 Rust 本地是否存在任何账户。 - Tauri Rust 查询 SQLite:
SELECT COUNT(*) FROM users。 - SQLite 返回
count = 0—— 没有本地账户。 - Rust 向前端返回
has_user: false。 - React 前端渲染注册页(若账户已存在,则改为显示登录页)。
- 用户填写用户名、密码、确认密码,点击「注册」。
- React 前端校验:用户名 2-20 字符、密码 ≥8、两次密码一致。校验失败 → 显示错误,跳过后端调用。→ EX-8.1 / EX-8.2 / EX-8.3
- React 前端调用
invoke('register'),将明文密码传给 Rust(IPC,无网络)。 - Rust 检查用户名是否唯一:
SELECT id FROM users WHERE username = ?。 - SQLite 返回空 —— 用户名可用。→ EX-10.1(若已被占用)
- Rust 用 bcrypt(cost=12)对密码做哈希。明文密码从不存储。
- Rust 写入用户:
INSERT INTO users。→ EX-13.1(若写入失败) - SQLite 返回新用户记录(id、username、created_at)。
- Rust 向前端返回会话。
- React 前端写入 Zustand Store,建立登录会话(桌面应用无需 token)。
- React 前端跳转到任务视图,并显示空状态引导。
异常情况
USERNAME_EXISTSDB_WRITE_ERRORAPI 规格 + 数据库 Schema
API 规格 —— Tauri IPC 命令
来源:logos/resources/api/tasks.yaml
数据库 Schema —— SQLite 表
来源:logos/resources/database/schema.yaml
测试用例设计(先于代码!)
来源:logos/resources/test/S01-test-cases.md
S01 测试用例 —— 单元测试(节选)
| ID | 描述 | 输入 | 预期 |
|---|---|---|---|
UT-S01-01 | 用户名恰好 2 字符(下边界) | "ab" | 通过 |
UT-S01-03 | 用户名 1 字符(低于边界) | "a" | 错误:需 2-20 字符 |
UT-S01-06 | 密码恰好 8 字符(下边界) | "Pass1234" | 通过 |
UT-S01-07 | 密码 7 字符(低于边界) | "Pass123" | 错误:需 ≥ 8 字符 |
UT-S01-11 | 重复用户名 → 数据库 UNIQUE 约束 | "xiaoli"(已存在) | USERNAME_EXISTS |
UT-S01-14 | 密码以 bcrypt 哈希存储 | "Pass1234" | 以 $2b$ 开头 |
S01 测试用例 —— 场景测试(节选)
| ID | 描述 | 覆盖 |
|---|---|---|
ST-S01-01 | 完整注册流程:启动 → 注册 → 任务视图 | Steps 1-17 |
ST-S01-02 | 用户名为空 → 前端拦截 | EX-8.1 |
ST-S01-05 | 重复用户名 → 后端报错 | EX-10.1 |
代码生成 —— 经由 Claude Code 的设计驱动实现
来源:src-tauri/src/lib.rs · src/pages/ · src-tauri/tests/orchestration.rs
每个 Tauri 命令都标注了它实现的场景步骤。Claude Code 读取时序图和 API 规格,然后以闭环批次生成后端 + 前端 + 测试。
后端 —— Rust Tauri 命令 → 对应 S01 Steps 9-15
// ── S01 Step 9-15: register ───────────────────────
#[tauri::command]
async fn register(
params: RegisterParams,
pool: tauri::State<'_, SqlitePool>,
) -> CmdResult<UserSession> {
let row = sqlx::query("SELECT COUNT(*) as count FROM users WHERE username = ?")
.bind(¶ms.username).fetch_one(pool.inner()).await
.map_err(|_| CommandError::new("DB_ERROR", "Query failed"))?;
if row.get::<i64, _>("count") > 0 {
return Err(CommandError::new("USERNAME_EXISTS", "Username taken"));
}
let pw_hash = hash(¶ms.password, DEFAULT_COST)
.map_err(|_| CommandError::new("HASH_ERROR", "Hash failed"))?;
let result = sqlx::query("INSERT INTO users (username, password) VALUES (?, ?)")
.bind(¶ms.username).bind(&pw_hash)
.execute(pool.inner()).await
.map_err(|_| CommandError::new("DB_WRITE_ERROR", "Write failed"))?;
Ok(UserSession {
user_id: result.last_insert_rowid(),
username: params.username,
tasks: vec![], categories: vec![],
})
}编排测试 → 端到端验证 S01
#[tokio::test]
async fn ot_s01_01_register_success() {
let pool = setup_db().await;
let session = cmd_register("xiaoli".into(), "Pass1234".into(), &pool)
.await.expect("register should succeed");
assert_eq!(session.username, "xiaoli");
assert!(session.user_id > 0);
assert!(session.tasks.is_empty());
}
#[tokio::test]
async fn ot_s01_02_register_username_exists() {
let pool = setup_db().await;
cmd_register("xiaoli".into(), "Pass1234".into(), &pool).await.unwrap();
let err = cmd_register("xiaoli".into(), "Other123".into(), &pool)
.await.expect_err("duplicate should fail");
assert_eq!(err.code, "USERNAME_EXISTS");
}openlogos verify —— 自动化验收
来源:logos/resources/verify/acceptance-report.md
验收标准可追溯性(节选)
S01-AC-001 注册返回带 user_id 的 UserSession → UT-S01-01 PASSS01-AC-002 重复用户名返回 USERNAME_EXISTS → UT-S01-02 PASSS02-AC-005 创建任务返回完整的 TaskItem → UT-S02-05 PASSS03-AC-003 删除分类将 task.category_id 置为 null → UT-S03-03 PASSS04-AC-001 修改密码返回 success=true → UT-S04-01 PASS部署与 smoke 门禁
示例项目补齐部署完成标记,并通过部署后的最小健康检查。
部署执行已完成,状态由 OpenLogos 受控记录。
部署后 smoke 用例通过,可追溯到 smoke 报告。
变更追踪 —— 每次迭代都洞察影响范围
传统的 AI 编程(「vibe coding」)只改代码,却忘了设计文档、API 规格和测试用例。久而久之,文档与现实脱节,变得毫无用处。OpenLogos 用一套结构化的 delta 工作流解决这一问题 —— 任何变更,无论多小,都必须评估它在整条产物链上的连锁影响。
openlogos mergeopenlogos deploy-doneopenlogos smoke变更传播规则
变更类型决定了最小更新范围。一个看似微小的功能请求,可能会沿着整条链条层层传导:
真实示例 —— 代码级修复
来源:logos/changes/archive/fix-register-redirect/proposal.md
fix-register-redirect
问题:注册后,应用没有跳转到任务视图。用户卡在注册页。
根因:useEffect 的条件要求 view === "loading",但注册后 view 是 "register" —— 跳转从未触发。
影响分析
App.tsx —— 扩展跳转条件以覆盖 register 与 login 视图想亲手试试?
克隆仓库在本地运行 FlowTask,或用 OpenLogos 开启你自己的项目。