第9章 端到端实战:用 AI Agent 从零构建生产级应用

“Talk is cheap. Show me the code.”
— Linus Torvalds, Linux 创始人

前两章我们分别学习了 Harness Engineering 的理论与实践,以及 AI Coding 工具链的配置与优化。但这些知识如果不能在真实项目中落地,就只是纸上谈兵。

本章,我们将进行一次真正的端到端实战:用 AI Agent 从零构建一个生产级的全栈应用——TaskFlow 任务管理系统。

这不是一篇"Hello World"级别的教程。我们会走完完整的软件工程生命周期:需求分析 → 规格设计 → Harness 搭建 → 后端开发 → 前端开发 → 集成测试 → 部署上线。每一步都会展示真实的 Prompt、真实的 Agent 输出、真实的人类审查,以及真实的踩坑和修复过程。


9.1 项目概述:构建一个任务管理系统

项目需求:全栈任务管理应用(TaskFlow)

为什么选择任务管理系统?

任务管理系统看似简单,实际上涵盖了 Web 应用的所有核心复杂度:

  • 用户认证与授权:注册、登录、JWT Token、RBAC 权限
  • CRUD 操作:任务、项目、标签的增删改查
  • 复杂业务逻辑:状态机(任务流转)、排序、过滤、搜索
  • 数据关系:一对多(项目→任务)、多对多(任务↔标签)
  • 实时交互:看板拖拽、实时更新
  • 通知系统:到期提醒、分配通知
  • 性能挑战:大量任务的分页查询、全文搜索

它是 AI Coding 能力的完美试金石——足够复杂以展示 Agent 的能力边界,又足够标准化以评估输出质量。

技术选型

层次 技术选择 选择理由
前端 React 19 + TypeScript + Vite 生态成熟,AI 模型理解最深
样式 Tailwind CSS 4 快速开发,AI 生成质量高
状态管理 Zustand 5 轻量、类型安全、样板代码少
后端框架 Hono (Node.js) 高性能、TypeScript-first、API 简洁
数据库 PostgreSQL 16 关系型数据库标准选择
ORM Drizzle ORM 类型安全、SQL-like API、迁移工具完善
验证 Zod TypeScript + Python 双生态的运行时验证
认证 JWT + bcrypt 无状态认证,适合 API 场景
测试 vitest + supertest + Playwright 快、全、稳
CI/CD GitHub Actions 与 Git 工作流深度集成
部署 Docker + Railway 一键部署,运维成本低

开发策略:SDD + TDD + Harness Engineering

我们采用三阶段融合策略:

TDD — 测试驱动

Harness — 约束先行

SDD — 规格驱动

需求文档

API 规格

数据模型

类型定义

测试套件

CI 流水线

编写测试

Agent 实现

测试通过

时间线规划

Phase 内容 AI 辅助耗时 传统开发耗时 提效比
Phase 1 需求分析与规格设计 1.5 小时 1-2 天 ~8x
Phase 2 Harness 搭建 1 小时 0.5-1 天 ~4x
Phase 3 后端开发 4 小时 3-5 天 ~10x
Phase 4 前端开发 4 小时 3-5 天 ~10x
Phase 5 集成测试与质量保障 1.5 小时 1-2 天 ~8x
Phase 6 部署与运维 0.5 小时 0.5-1 天 ~8x
总计 ~12.5 小时 ~10-16 天 ~8x

9.2 Phase 1:需求分析与规格设计(SDD)

用自然语言定义产品需求文档(PRD)

第一步,我们用自然语言定义产品需求,然后让 AI Agent 帮助我们将其结构化为正式的 PRD。

初始需求描述(人类输入)

我要做一个类似 Trello 的任务管理系统。用户可以创建项目,在项目里创建任务卡片,
任务有状态流转(待办 → 进行中 → 审核中 → 完成),可以拖拽排序。
支持用户注册登录,支持给任务分配负责人、设置截止日期、添加标签。
有通知功能,任务快到期时提醒。

让 Agent 结构化需求

Prompt:
"请帮我将以下需求结构化为正式的 PRD(产品需求文档)。
包含:功能模块列表、用户故事、非功能需求、技术约束。

需求:[上述自然语言描述]"

完整的 PRD 文档

经过 Agent 辅助结构化后的 PRD:

# TaskFlow — 产品需求文档 (PRD)

## 1. 产品概述
TaskFlow 是一个面向小团队的任务管理工具,提供看板式任务管理、
团队协作和智能通知功能。

## 2. 功能模块

### 2.1 用户认证模块(Auth)
- US-01: 用户可以邮箱+密码注册
- US-02: 用户可以登录/登出
- US-03: 用户可以重置密码(邮箱验证)
- US-04: JWT Token 自动刷新

### 2.2 项目管理模块(Project)
- US-05: 用户可以创建项目
- US-06: 用户可以邀请团队成员加入项目
- US-07: 项目创建者拥有管理员权限
- US-08: 项目支持设置名称、描述、颜色标识

### 2.3 任务管理模块(Task)
- US-09: 用户可以在项目中创建任务
- US-10: 任务包含:标题、描述、优先级、状态、标签
- US-11: 任务状态流转:todo → in_progress → in_review → done
- US-12: 任务可以分配给项目成员
- US-13: 任务可以设置截止日期
- US-14: 任务支持拖拽排序
- US-15: 任务支持附件上传(≤ 10MB)

### 2.4 看板视图模块(Board)
- US-16: 按状态列展示任务卡片
- US-17: 支持拖拽任务到不同列
- US-18: 支持按标签/负责人/优先级过滤
- US-19: 支持全文搜索任务

### 2.5 通知模块(Notification)
- US-20: 任务分配时通知被分配者
- US-21: 任务状态变更时通知创建者
- US-22: 任务到期前 24 小时提醒
- US-23: 支持站内通知 + 邮件通知

## 3. 非功能需求
- NFR-01: API 响应时间 P99 < 200ms
- NFR-02: 支持 1000 并发用户
- NFR-03: 数据库查询优化(索引覆盖)
- NFR-04: 前端首屏加载 < 2s
- NFR-05: 测试覆盖率 ≥ 80%

## 4. 技术约束
- TC-01: RESTful API 设计
- TC-02: TypeScript 全栈
- TC-03: PostgreSQL 数据库
- TC-04: Docker 容器化部署
- TC-05: 无第三方 Auth 依赖(自建认证)

API 规格设计(OpenAPI 3.0)

# openapi.yaml — TaskFlow API 规格
openapi: '3.0.3'
info:
  title: TaskFlow API
  version: '1.0.0'
  description: 任务管理系统 RESTful API

servers:
  - url: /api/v1

# ===== 认证端点 =====
paths:
  /auth/register:
    post:
      tags: [Auth]
      summary: 用户注册
      operationId: register
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RegisterInput'
      responses:
        '201':
          description: 注册成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '409':
          description: 邮箱已注册
  
  /auth/login:
    post:
      tags: [Auth]
      summary: 用户登录
      operationId: login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginInput'
      responses:
        '200':
          description: 登录成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthResponse'
        '401':
          description: 认证失败
  
  /auth/refresh:
    post:
      tags: [Auth]
      summary: 刷新 Token
      operationId: refreshToken
      security:
        - BearerAuth: []
      responses:
        '200':
          description: 刷新成功

  # ===== 项目端点 =====
  /projects:
    get:
      tags: [Project]
      summary: 获取用户的项目列表
      operationId: listProjects
      security:
        - BearerAuth: []
      responses:
        '200':
          description: 项目列表
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ApiResponse'
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: '#/components/schemas/Project'
    
    post:
      tags: [Project]
      summary: 创建项目
      operationId: createProject
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateProjectInput'
      responses:
        '201':
          description: 创建成功

  /projects/{projectId}:
    get:
      tags: [Project]
      summary: 获取项目详情(含成员列表)
      operationId: getProject
      security:
        - BearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: 项目详情
        '404':
          description: 项目不存在

  # ===== 任务端点 =====
  /projects/{projectId}/tasks:
    get:
      tags: [Task]
      summary: 获取项目下的任务列表(看板视图)
      operationId: listTasks
      security:
        - BearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/TaskStatus'
        - name: assigneeId
          in: query
          schema:
            type: string
            format: uuid
        - name: tag
          in: query
          schema:
            type: string
        - name: search
          in: query
          schema:
            type: string
            minLength: 2
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: pageSize
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
      responses:
        '200':
          description: 任务列表(按状态分组)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      todo:
                        type: array
                        items:
                          $ref: '#/components/schemas/Task'
                      in_progress:
                        type: array
                        items:
                          $ref: '#/components/schemas/Task'
                      in_review:
                        type: array
                        items:
                          $ref: '#/components/schemas/Task'
                      done:
                        type: array
                        items:
                          $ref: '#/components/schemas/Task'
    
    post:
      tags: [Task]
      summary: 创建任务
      operationId: createTask
      security:
        - BearerAuth: []
      parameters:
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTaskInput'
      responses:
        '201':
          description: 创建成功

  /tasks/{taskId}:
    patch:
      tags: [Task]
      summary: 更新任务(含状态流转)
      operationId: updateTask
      security:
        - BearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTaskInput'
      responses:
        '200':
          description: 更新成功
        '400':
          description: 无效的状态转换
    
    delete:
      tags: [Task]
      summary: 删除任务
      operationId: deleteTask
      security:
        - BearerAuth: []
      parameters:
        - name: taskId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: 删除成功

  # ===== 通知端点 =====
  /notifications:
    get:
      tags: [Notification]
      summary: 获取当前用户的通知列表
      operationId: listNotifications
      security:
        - BearerAuth: []
      parameters:
        - name: unreadOnly
          in: query
          schema:
            type: boolean
            default: false
      responses:
        '200':
          description: 通知列表

components:
  schemas:
    # ===== 认证相关 =====
    RegisterInput:
      type: object
      required: [email, password, name]
      properties:
        email:
          type: string
          format: email
          maxLength: 255
        password:
          type: string
          minLength: 8
          maxLength: 128
        name:
          type: string
          minLength: 2
          maxLength: 100
    
    LoginInput:
      type: object
      required: [email, password]
      properties:
        email:
          type: string
          format: email
        password:
          type: string
    
    AuthResponse:
      type: object
      properties:
        success:
          type: boolean
          enum: [true]
        data:
          type: object
          properties:
            token:
              type: string
            refreshToken:
              type: string
            user:
              $ref: '#/components/schemas/User'
    
    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
        name:
          type: string
        avatarUrl:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
    
    # ===== 项目相关 =====
    CreateProjectInput:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        description:
          type: string
          maxLength: 500
        color:
          type: string
          pattern: '^#[0-9a-fA-F]{6}$'
          default: '#3B82F6'
    
    Project:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        color:
          type: string
        ownerId:
          type: string
          format: uuid
        memberCount:
          type: integer
        taskCount:
          type: integer
        createdAt:
          type: string
          format: date-time
    
    # ===== 任务相关 =====
    TaskStatus:
      type: string
      enum: [todo, in_progress, in_review, done]
    
    TaskPriority:
      type: string
      enum: [low, medium, high, urgent]
    
    CreateTaskInput:
      type: object
      required: [title]
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 5000
        priority:
          $ref: '#/components/schemas/TaskPriority'
        assigneeId:
          type: string
          format: uuid
          nullable: true
        dueDate:
          type: string
          format: date
          nullable: true
        tags:
          type: array
          items:
            type: string
          maxItems: 10
    
    UpdateTaskInput:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 200
        description:
          type: string
          maxLength: 5000
        status:
          $ref: '#/components/schemas/TaskStatus'
        priority:
          $ref: '#/components/schemas/TaskPriority'
        assigneeId:
          type: string
          format: uuid
          nullable: true
        dueDate:
          type: string
          format: date
          nullable: true
        tags:
          type: array
          items:
            type: string
          maxItems: 10
        position:
          type: integer
          description: 看板中的排序位置
    
    Task:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: string
          nullable: true
        status:
          $ref: '#/components/schemas/TaskStatus'
        priority:
          $ref: '#/components/schemas/TaskPriority'
        assignee:
          $ref: '#/components/schemas/User'
          nullable: true
        creator:
          $ref: '#/components/schemas/User'
        tags:
          type: array
          items:
            type: string
        dueDate:
          type: string
          format: date
          nullable: true
        position:
          type: integer
        projectId:
          type: string
          format: uuid
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    
    # ===== 通用 =====
    ApiResponse:
      type: object
      properties:
        success:
          type: boolean
        data: {}
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string

  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

数据库 Schema 设计

// ===== packages/backend/src/db/schema.ts =====
import {
  pgTable, uuid, varchar, text, boolean,
  timestamp, integer, date, index, unique,
} from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

// ===== 用户表 =====
export const users = pgTable('users', {
  id: uuid('id').defaultRandom().primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 100 }).notNull(),
  passwordHash: text('password_hash').notNull(),
  avatarUrl: text('avatar_url'),
  emailVerified: boolean('email_verified').default(false).notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
  emailIdx: index('users_email_idx').on(table.email),
}));

// ===== 项目表 =====
export const projects = pgTable('projects', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: varchar('name', { length: 100 }).notNull(),
  description: text('description'),
  color: varchar('color', { length: 7 }).default('#3B82F6').notNull(),
  ownerId: uuid('owner_id').notNull().references(() => users.id),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
  ownerIdx: index('projects_owner_idx').on(table.ownerId),
}));

// ===== 项目成员表(多对多) =====
export const projectMembers = pgTable('project_members', {
  projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
  userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  role: varchar('role', { length: 20 }).default('member').notNull(), // owner | admin | member
  joinedAt: timestamp('joined_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
  pk: index('project_members_pk').on(table.projectId, table.userId),
  uniqueMember: unique('project_members_unique').on(table.projectId, table.userId),
}));

// ===== 任务表 =====
export const tasks = pgTable('tasks', {
  id: uuid('id').defaultRandom().primaryKey(),
  title: varchar('title', { length: 200 }).notNull(),
  description: text('description'),
  status: varchar('status', { length: 20 }).default('todo').notNull(),
  priority: varchar('priority', { length: 20 }).default('medium').notNull(),
  position: integer('position').default(0).notNull(),
  projectId: uuid('project_id').notNull().references(() => projects.id, { onDelete: 'cascade' }),
  creatorId: uuid('creator_id').notNull().references(() => users.id),
  assigneeId: uuid('assignee_id').references(() => users.id),
  dueDate: date('due_date'),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
  updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
  projectIdx: index('tasks_project_idx').on(table.projectId),
  statusIdx: index('tasks_status_idx').on(table.projectId, table.status),
  assigneeIdx: index('tasks_assignee_idx').on(table.assigneeId),
  dueDateIdx: index('tasks_due_date_idx').on(table.dueDate),
  positionIdx: index('tasks_position_idx').on(table.projectId, table.position),
}));

// ===== 任务标签表(多对多) =====
export const taskTags = pgTable('task_tags', {
  taskId: uuid('task_id').notNull().references(() => tasks.id, { onDelete: 'cascade' }),
  tag: varchar('tag', { length: 50 }).notNull(),
}, (table) => ({
  taskIdx: index('task_tags_task_idx').on(table.taskId),
  tagIdx: index('task_tags_tag_idx').on(table.tag),
  uniqueTag: unique('task_tags_unique').on(table.taskId, table.tag),
}));

// ===== 通知表 =====
export const notifications = pgTable('notifications', {
  id: uuid('id').defaultRandom().primaryKey(),
  userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  type: varchar('type', { length: 50 }).notNull(),
  title: varchar('title', { length: 200 }).notNull(),
  message: text('message'),
  taskId: uuid('task_id').references(() => tasks.id, { onDelete: 'cascade' }),
  read: boolean('read').default(false).notNull(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
  userIdx: index('notifications_user_idx').on(table.userId),
  unreadIdx: index('notifications_unread_idx').on(table.userId, table.read),
}));

// ===== 关系定义 =====
export const usersRelations = relations(users, ({ many }) => ({
  ownedProjects: many(projects),
  projectMemberships: many(projectMembers),
  createdTasks: many(tasks, { relationName: 'creator' }),
  assignedTasks: many(tasks, { relationName: 'assignee' }),
  notifications: many(notifications),
}));

export const projectsRelations = relations(projects, ({ one, many }) => ({
  owner: one(users, {
    fields: [projects.ownerId],
    references: [users.id],
  }),
  members: many(projectMembers),
  tasks: many(tasks),
}));

export const projectMembersRelations = relations(projectMembers, ({ one }) => ({
  project: one(projects, {
    fields: [projectMembers.projectId],
    references: [projects.id],
  }),
  user: one(users, {
    fields: [projectMembers.userId],
    references: [users.id],
  }),
}));

export const tasksRelations = relations(tasks, ({ one, many }) => ({
  project: one(projects, {
    fields: [tasks.projectId],
    references: [projects.id],
  }),
  creator: one(users, {
    fields: [tasks.creatorId],
    references: [users.id],
    relationName: 'creator',
  }),
  assignee: one(users, {
    fields: [tasks.assigneeId],
    references: [users.id],
    relationName: 'assignee',
  }),
  tags: many(taskTags),
}));

export const taskTagsRelations = relations(taskTags, ({ one }) => ({
  task: one(tasks, {
    fields: [taskTags.taskId],
    references: [tasks.id],
  }),
}));

让 Agent 审查并完善规格

在 PRD 和 API 规格初步完成后,我们让 Agent 进行审查:

Prompt:
"请审查以下 PRD 和 API 规格文档,检查:
1. 功能完整性:是否有遗漏的用户故事?
2. API 一致性:端点设计是否 RESTful 且自洽?
3. 数据模型:Schema 是否能支撑所有功能需求?
4. 安全性:认证/授权方案是否有漏洞?
5. 性能:查询模式是否有潜在的性能瓶颈?

[附上 PRD + OpenAPI + Schema]"

Agent 审查后的典型反馈:

## 审查结果

### ✅ 做得好的
- 状态机设计清晰(todo → in_progress → in_review → done)
- 数据库索引覆盖了主要查询模式
- API 响应格式统一

### ⚠️ 建议改进
1. **遗漏:任务评论功能** — 看板系统通常需要评论/讨论功能
   → 建议:V1 可以不做,但在 Schema 中预留 comments 表
   
2. **API 设计:任务排序** — 拖拽排序需要专门的端点
   → 建议:添加 PATCH /tasks/:id/reorder { position: number }
   
3. **安全性:密码强度** — 当前只有 8 字符最低要求
   → 建议:增加复杂度要求(大写+小写+数字)
   
4. **性能:全文搜索** — PostgreSQL 的 LIKE 查询效率低
   → 建议:使用 PostgreSQL 的 tsvector + GIN 索引

9.3 Phase 2:Harness 先行

搭建项目脚手架

# 初始化 monorepo
mkdir taskflow && cd taskflow
pnpm init
cat > pnpm-workspace.yaml << 'EOF'
packages:
  - 'packages/*'
EOF

# 创建后端项目
mkdir -p packages/backend/src/{routes,services,repositories,schemas,middleware,utils,db}
cd packages/backend
pnpm init

# 安装后端依赖
pnpm add hono @hono/node-server drizzle-orm pg zod bcryptjs jsonwebtoken uuid
pnpm add -D typescript vitest supertest @types/node @types/bcryptjs \
  @types/jsonwebtoken @types/supertest drizzle-kit tsx

# 创建前端项目
cd ../..
pnpm create vite packages/frontend --template react-ts
cd packages/frontend
pnpm add zustand @tanstack/react-query react-router-dom axios \
  tailwindcss @tailwindcss/vite
pnpm add -D vitest @testing-library/react @testing-library/jest-dom \
  jsdom msw

# 创建共享包
cd ../..
mkdir -p packages/shared/src/{schemas,types}
cd packages/shared
pnpm init
pnpm add zod
pnpm add -D typescript

配置 TypeScript 严格模式

// packages/backend/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "useUnknownInCatchVariables": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

设置测试框架

// packages/backend/vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
    include: ['tests/**/*.test.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      include: ['src/**/*.ts'],
      exclude: ['src/db/migrations/**'],
      thresholds: {
        branches: 80,
        functions: 80,
        lines: 80,
        statements: 80,
      },
    },
    setupFiles: ['./tests/setup.ts'],
    testTimeout: 30000,
  },
});
// packages/backend/tests/setup.ts
import { beforeAll, afterAll, beforeEach } from 'vitest';
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from '../src/db/schema';

let pool: Pool;

beforeAll(async () => {
  pool = new Pool({
    connectionString: process.env.TEST_DATABASE_URL ||
      'postgresql://test:test@localhost:5432/taskflow_test',
  });
});

afterAll(async () => {
  await pool?.end();
});

beforeEach(async () => {
  // 每个测试前清空数据(保留表结构)
  const client = await pool.connect();
  try {
    await client.query('TRUNCATE notifications, task_tags, tasks, project_members, projects, users CASCADE');
  } finally {
    client.release();
  }
});

export { pool };
export const getTestDb = (): ReturnType<typeof drizzle> =>
  drizzle(pool, { schema });

编写 API 集成测试(测试先行)

这是 Harness Engineering 的核心——先写测试,再让 Agent 实现。

// packages/backend/tests/integration/auth.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import request from 'supertest';
import { createApp } from '../../src/app';
import type { Hono } from 'hono';

describe('Auth API', () => {
  let app: Hono;

  beforeAll(() => {
    app = createApp();
  });

  describe('POST /api/v1/auth/register', () => {
    it('应该成功注册新用户', async () => {
      const res = await request(app)
        .post('/api/v1/auth/register')
        .send({
          email: 'alice@example.com',
          password: 'Alice@12345',
          name: 'Alice Zhang',
        });

      expect(res.status).toBe(201);
      expect(res.body.success).toBe(true);
      expect(res.body.data).toHaveProperty('token');
      expect(res.body.data).toHaveProperty('refreshToken');
      expect(res.body.data.user).toMatchObject({
        email: 'alice@example.com',
        name: 'Alice Zhang',
      });
      // 密码不应该出现在响应中
      expect(res.body.data.user).not.toHaveProperty('password');
      expect(res.body.data.user).not.toHaveProperty('passwordHash');
    });

    it('邮箱格式无效应该返回 400', async () => {
      const res = await request(app)
        .post('/api/v1/auth/register')
        .send({
          email: 'not-an-email',
          password: 'Alice@12345',
          name: 'Alice',
        });

      expect(res.status).toBe(400);
      expect(res.body.success).toBe(false);
      expect(res.body.error.code).toBe('VALIDATION_ERROR');
    });

    it('密码不满足强度要求应该返回 400', async () => {
      const res = await request(app)
        .post('/api/v1/auth/register')
        .send({
          email: 'alice@example.com',
          password: 'weak',  // 太短,无大写,无数字
          name: 'Alice',
        });

      expect(res.status).toBe(400);
    });

    it('重复邮箱应该返回 409', async () => {
      // 先注册一个用户
      await request(app)
        .post('/api/v1/auth/register')
        .send({
          email: 'duplicate@example.com',
          password: 'Alice@12345',
          name: 'First User',
        });

      // 再次注册相同邮箱
      const res = await request(app)
        .post('/api/v1/auth/register')
        .send({
          email: 'duplicate@example.com',
          password: 'Bob@12345',
          name: 'Second User',
        });

      expect(res.status).toBe(409);
      expect(res.body.error.code).toBe('EMAIL_ALREADY_EXISTS');
    });
  });

  describe('POST /api/v1/auth/login', () => {
    beforeEach(async () => {
      await request(app)
        .post('/api/v1/auth/register')
        .send({
          email: 'login-test@example.com',
          password: 'Test@12345',
          name: 'Login Test',
        });
    });

    it('正确凭据应该返回 Token', async () => {
      const res = await request(app)
        .post('/api/v1/auth/login')
        .send({
          email: 'login-test@example.com',
          password: 'Test@12345',
        });

      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);
      expect(res.body.data.token).toBeTruthy();
      expect(res.body.data.user.email).toBe('login-test@example.com');
    });

    it('错误密码应该返回 401', async () => {
      const res = await request(app)
        .post('/api/v1/auth/login')
        .send({
          email: 'login-test@example.com',
          password: 'WrongPassword@123',
        });

      expect(res.status).toBe(401);
      expect(res.body.error.code).toBe('INVALID_CREDENTIALS');
    });

    it('不存在的邮箱应该返回 401(不区分邮箱不存在和密码错误)', async () => {
      const res = await request(app)
        .post('/api/v1/auth/login')
        .send({
          email: 'nonexistent@example.com',
          password: 'Test@12345',
        });

      expect(res.status).toBe(401);
      // 安全考虑:不应透露邮箱是否已注册
      expect(res.body.error.code).toBe('INVALID_CREDENTIALS');
    });
  });
});
// packages/backend/tests/integration/tasks.test.ts
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import request from 'supertest';
import { createApp } from '../../src/app';
import type { Hono } from 'hono';

describe('Task API', () => {
  let app: Hono;
  let authToken: string;
  let projectId: string;

  beforeAll(async () => {
    app = createApp();
    
    // 注册测试用户
    const authRes = await request(app)
      .post('/api/v1/auth/register')
      .send({
        email: 'task-test@example.com',
        password: 'Task@12345',
        name: 'Task Tester',
      });
    authToken = authRes.body.data.token;

    // 创建测试项目
    const projectRes = await request(app)
      .post('/api/v1/projects')
      .set('Authorization', `Bearer ${authToken}`)
      .send({ name: 'Test Project' });
    projectId = projectRes.body.data.id;
  });

  describe('POST /api/v1/projects/:projectId/tasks', () => {
    it('应该创建任务并设置默认状态为 todo', async () => {
      const res = await request(app)
        .post(`/api/v1/projects/${projectId}/tasks`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({
          title: '实现用户认证',
          priority: 'high',
          tags: ['backend', 'auth'],
        });

      expect(res.status).toBe(201);
      expect(res.body.data).toMatchObject({
        title: '实现用户认证',
        status: 'todo',
        priority: 'high',
        tags: ['backend', 'auth'],
      });
      expect(res.body.data.position).toBeGreaterThanOrEqual(0);
    });

    it('标题为空应该返回 400', async () => {
      const res = await request(app)
        .post(`/api/v1/projects/${projectId}/tasks`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ title: '', priority: 'low' });

      expect(res.status).toBe(400);
    });

    it('未认证应该返回 401', async () => {
      const res = await request(app)
        .post(`/api/v1/projects/${projectId}/tasks`)
        .send({ title: 'Unauthorized Task' });

      expect(res.status).toBe(401);
    });
  });

  describe('PATCH /api/v1/tasks/:taskId — 状态流转', () => {
    let taskId: string;

    beforeEach(async () => {
      const res = await request(app)
        .post(`/api/v1/projects/${projectId}/tasks`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ title: 'Status Test Task' });
      taskId = res.body.data.id;
    });

    it('应该允许 todo → in_progress', async () => {
      const res = await request(app)
        .patch(`/api/v1/tasks/${taskId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ status: 'in_progress' });

      expect(res.status).toBe(200);
      expect(res.body.data.status).toBe('in_progress');
    });

    it('应该允许 in_progress → in_review', async () => {
      // 先转到 in_progress
      await request(app)
        .patch(`/api/v1/tasks/${taskId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ status: 'in_progress' });

      // 再转到 in_review
      const res = await request(app)
        .patch(`/api/v1/tasks/${taskId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ status: 'in_review' });

      expect(res.status).toBe(200);
      expect(res.body.data.status).toBe('in_review');
    });

    it('应该拒绝 todo → done(跳过中间状态)', async () => {
      const res = await request(app)
        .patch(`/api/v1/tasks/${taskId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ status: 'done' });

      expect(res.status).toBe(400);
      expect(res.body.error.code).toBe('INVALID_STATUS_TRANSITION');
    });

    it('应该拒绝 done → todo(不允许回退已完成的任务)', async () => {
      // 逐步转到 done
      for (const status of ['in_progress', 'in_review', 'done']) {
        await request(app)
          .patch(`/api/v1/tasks/${taskId}`)
          .set('Authorization', `Bearer ${authToken}`)
          .send({ status });
      }

      // 尝试回退到 todo
      const res = await request(app)
        .patch(`/api/v1/tasks/${taskId}`)
        .set('Authorization', `Bearer ${authToken}`)
        .send({ status: 'todo' });

      expect(res.status).toBe(400);
    });
  });

  describe('GET /api/v1/projects/:projectId/tasks — 看板视图', () => {
    beforeEach(async () => {
      // 创建多个任务,分布在不同状态列
      const tasks = [
        { title: 'Todo Task 1', priority: 'high' },
        { title: 'Todo Task 2', priority: 'low' },
        { title: 'Progress Task', priority: 'medium' },
        { title: 'Review Task', priority: 'high' },
        { title: 'Done Task', priority: 'medium' },
      ];

      for (const task of tasks) {
        await request(app)
          .post(`/api/v1/projects/${projectId}/tasks`)
          .set('Authorization', `Bearer ${authToken}`)
          .send(task);
      }

      // 将部分任务移到其他状态
      const listRes = await request(app)
        .get(`/api/v1/projects/${projectId}/tasks`)
        .set('Authorization', `Bearer ${authToken}`);
      
      const allTasks = [
        ...listRes.body.data.todo,
        ...listRes.body.data.in_progress,
      ];

      // 移 Progress Task 到 in_progress
      const progressTask = allTasks.find(t => t.title === 'Progress Task');
      if (progressTask) {
        await request(app)
          .patch(`/api/v1/tasks/${progressTask.id}`)
          .set('Authorization', `Bearer ${authToken}`)
          .send({ status: 'in_progress' });
      }
    });

    it('应该按状态分组返回任务', async () => {
      const res = await request(app)
        .get(`/api/v1/projects/${projectId}/tasks`)
        .set('Authorization', `Bearer ${authToken}`);

      expect(res.status).toBe(200);
      expect(res.body.data).toHaveProperty('todo');
      expect(res.body.data).toHaveProperty('in_progress');
      expect(res.body.data).toHaveProperty('in_review');
      expect(res.body.data).toHaveProperty('done');
      expect(Array.isArray(res.body.data.todo)).toBe(true);
    });

    it('应该支持按优先级过滤', async () => {
      const res = await request(app)
        .get(`/api/v1/projects/${projectId}/tasks`)
        .query({ priority: 'high' })
        .set('Authorization', `Bearer ${authToken}`);

      expect(res.status).toBe(200);
      // 所有返回的任务优先级都应该是 high
      for (const column of ['todo', 'in_progress', 'in_review', 'done']) {
        for (const task of res.body.data[column]) {
          expect(task.priority).toBe('high');
        }
      }
    });
  });
});

9.4 Phase 3:后端开发——Agent 在 Harness 约束下编码

第一步:数据模型与数据库迁移

Prompt to Agent:
"请根据 packages/backend/src/db/schema.ts 中的 Drizzle Schema 生成数据库迁移文件。
使用 drizzle-kit 工具。命令:pnpm drizzle-kit generate
然后运行迁移:pnpm drizzle-kit migrate
确保迁移成功。"

Agent 执行结果:

$ pnpm drizzle-kit generate
✓ Generated migration: 0000_initial_schema.sql

$ pnpm drizzle-kit migrate
✓ Applied migration: 0000_initial_schema.sql

第二步:用户认证模块(JWT)

Prompt to Agent:
"请实现用户认证模块。需要以下文件:
1. packages/backend/src/schemas/auth.ts — Zod Schema
2. packages/backend/src/repositories/user-repository.ts — 数据访问
3. packages/backend/src/services/auth-service.ts — 业务逻辑
4. packages/backend/src/routes/auth.ts — API 路由
5. packages/backend/src/middleware/auth.ts — JWT 认证中间件

参考 packages/backend/tests/integration/auth.test.ts 中的测试用例确保实现正确。
密码使用 bcryptjs(cost factor 12),JWT 使用 HS256。
确保运行 pnpm test 后所有 auth 相关测试通过。"

Agent 生成的核心代码(人类审查后)

// ===== packages/backend/src/schemas/auth.ts =====
import { z } from 'zod';

export const RegisterInputSchema = z.object({
  email: z.string().email('无效的邮箱格式').max(255),
  password: z.string()
    .min(8, '密码至少 8 个字符')
    .max(128, '密码最多 128 个字符')
    .regex(/[A-Z]/, '密码必须包含至少一个大写字母')
    .regex(/[a-z]/, '密码必须包含至少一个小写字母')
    .regex(/[0-9]/, '密码必须包含至少一个数字'),
  name: z.string().min(2, '名称至少 2 个字符').max(100),
});
export type RegisterInput = z.infer<typeof RegisterInputSchema>;

export const LoginInputSchema = z.object({
  email: z.string().email(),
  password: z.string(),
});
export type LoginInput = z.infer<typeof LoginInputSchema>;
// ===== packages/backend/src/services/auth-service.ts =====
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import type { UserRepository } from '../repositories/user-repository';
import type { RegisterInput, LoginInput } from '../schemas/auth';

const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';
const JWT_EXPIRES_IN = '24h';
const REFRESH_TOKEN_EXPIRES_IN = '7d';
const BCRYPT_ROUNDS = 12;

export class AuthService {
  constructor(private readonly userRepo: UserRepository) {}

  async register(input: RegisterInput): Promise<AuthResult> {
    // 检查邮箱是否已注册
    const existing = await this.userRepo.findByEmail(input.email);
    if (existing) {
      throw new AppError('EMAIL_ALREADY_EXISTS', '该邮箱已注册', 409);
    }

    // 加密密码
    const passwordHash = await bcrypt.hash(input.password, BCRYPT_ROUNDS);

    // 创建用户
    const user = await this.userRepo.create({
      email: input.email,
      name: input.name,
      passwordHash,
    });

    // 生成 Token
    const token = this.generateToken(user.id);
    const refreshToken = this.generateRefreshToken(user.id);

    return {
      token,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        avatarUrl: user.avatarUrl,
        createdAt: user.createdAt,
      },
    };
  }

  async login(input: LoginInput): Promise<AuthResult> {
    const user = await this.userRepo.findByEmail(input.email);
    
    // 统一错误信息(不区分邮箱不存在和密码错误)
    if (!user) {
      throw new AppError('INVALID_CREDENTIALS', '邮箱或密码错误', 401);
    }

    const passwordValid = await bcrypt.compare(input.password, user.passwordHash);
    if (!passwordValid) {
      throw new AppError('INVALID_CREDENTIALS', '邮箱或密码错误', 401);
    }

    const token = this.generateToken(user.id);
    const refreshToken = this.generateRefreshToken(user.id);

    return {
      token,
      refreshToken,
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        avatarUrl: user.avatarUrl,
        createdAt: user.createdAt,
      },
    };
  }

  private generateToken(userId: string): string {
    return jwt.sign({ sub: userId, type: 'access' }, JWT_SECRET, {
      expiresIn: JWT_EXPIRES_IN,
    });
  }

  private generateRefreshToken(userId: string): string {
    return jwt.sign({ sub: userId, type: 'refresh' }, JWT_SECRET, {
      expiresIn: REFRESH_TOKEN_EXPIRES_IN,
    });
  }
}

interface AuthResult {
  token: string;
  refreshToken: string;
  user: {
    id: string;
    email: string;
    name: string;
    avatarUrl: string | null;
    createdAt: Date;
  };
}
// ===== packages/backend/src/middleware/auth.ts =====
import type { Context, Next } from 'hono';
import jwt from 'jsonwebtoken';

const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-change-in-production';

export interface AuthUser {
  userId: string;
}

export async function authMiddleware(c: Context, next: Next): Promise<void> {
  const authHeader = c.req.header('Authorization');
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    c.status(401);
    await c.json({
      success: false,
      error: { code: 'UNAUTHORIZED', message: '未提供认证令牌' },
    });
    return;
  }

  const token = authHeader.slice(7);

  try {
    const payload = jwt.verify(token, JWT_SECRET) as { sub: string; type: string };
    
    if (payload.type !== 'access') {
      throw new Error('Invalid token type');
    }

    // 将用户信息注入上下文
    c.set('authUser', { userId: payload.sub } satisfies AuthUser);
    await next();
  } catch {
    c.status(401);
    await c.json({
      success: false,
      error: { code: 'INVALID_TOKEN', message: '认证令牌无效或已过期' },
    });
  }
}

第三步:任务 CRUD API

Prompt to Agent:
"请实现任务管理模块。需要以下文件:
1. packages/backend/src/schemas/task.ts — Zod Schema
2. packages/backend/src/repositories/task-repository.ts — 数据访问
3. packages/backend/src/services/task-service.ts — 业务逻辑(含状态机)
4. packages/backend/src/routes/task.ts — API 路由

关键业务规则:
- 状态流转必须遵循状态机:todo ↔ in_progress → in_review → done
- 不允许跳过中间状态(todo → done 非法)
- 不允许已完成后回退(done → any 非法)
- 新任务默认状态为 todo
- 创建任务时自动计算 position(追加到 todo 列末尾)

参考测试文件:packages/backend/tests/integration/tasks.test.ts
确保 pnpm test 通过。"

状态机实现(核心逻辑)

// ===== packages/backend/src/services/task-service.ts(状态机部分)=====

/**
 * 任务状态机定义
 * 
 * 合法转换:
 *   todo → in_progress(开始工作)
 *   in_progress → in_review(提交审核)
 *   in_progress → todo(退回待办)
 *   in_review → done(审核通过)
 *   in_review → in_progress(审核退回)
 * 
 * 非法转换:
 *   todo → done(不能跳过中间状态)
 *   todo → in_review(不能跳过 in_progress)
 *   done → any(完成后不可回退)
 */
const VALID_TRANSITIONS: Record<string, string[]> = {
  todo: ['in_progress'],
  in_progress: ['in_review', 'todo'],
  in_review: ['done', 'in_progress'],
  done: [],  // 终态,不可转换
};

export class TaskService {
  constructor(
    private readonly taskRepo: TaskRepository,
    private readonly notificationService: NotificationService,
  ) {}

  async transitionStatus(
    taskId: string,
    newStatus: string,
    actorId: string,
  ): Promise<Task> {
    const task = await this.taskRepo.findById(taskId);
    if (!task) {
      throw new AppError('TASK_NOT_FOUND', '任务不存在', 404);
    }

    const currentStatus = task.status;
    const allowedNext = VALID_TRANSITIONS[currentStatus];

    if (!allowedNext || !allowedNext.includes(newStatus)) {
      throw new AppError(
        'INVALID_STATUS_TRANSITION',
        `不允许从 ${currentStatus} 转换到 ${newStatus}` +
        `允许的目标状态:${allowedNext?.join(', ') || '无'}`,
        400,
      );
    }

    const updated = await this.taskRepo.update(taskId, {
      status: newStatus,
      updatedAt: new Date(),
    });

    // 状态变更时发送通知
    await this.notificationService.notifyStatusChange(
      task,
      newStatus,
      actorId,
    );

    return updated;
  }

  async createTask(
    projectId: string,
    input: CreateTaskInput,
    creatorId: string,
  ): Promise<Task> {
    // 计算 position(追加到 todo 列末尾)
    const maxPosition = await this.taskRepo.getMaxPosition(projectId, 'todo');
    const position = (maxPosition ?? -1) + 1;

    const task = await this.taskRepo.create({
      title: input.title,
      description: input.description ?? null,
      status: 'todo',
      priority: input.priority ?? 'medium',
      position,
      projectId,
      creatorId,
      assigneeId: input.assigneeId ?? null,
      dueDate: input.dueDate ?? null,
      tags: input.tags ?? [],
    });

    // 如果有 assignee 且不是创建者自己,发送通知
    if (input.assigneeId && input.assigneeId !== creatorId) {
      await this.notificationService.notifyTaskAssigned(
        task,
        input.assigneeId,
      );
    }

    return task;
  }

  async getBoardView(
    projectId: string,
    filters?: TaskFilters,
  ): Promise<BoardView> {
    const tasks = await this.taskRepo.findByProject(projectId, filters);
    
    // 按状态分组
    const board: BoardView = {
      todo: [],
      in_progress: [],
      in_review: [],
      done: [],
    };

    for (const task of tasks) {
      const status = task.status as keyof BoardView;
      board[status].push(task);
    }

    // 每组内按 position 排序
    for (const status of Object.keys(board) as (keyof BoardView)[]) {
      board[status].sort((a, b) => a.position - b.position);
    }

    return board;
  }
}

interface BoardView {
  todo: Task[];
  in_progress: Task[];
  in_review: Task[];
  done: Task[];
}

第四步至第五步

按照相同的模式,Agent 依次完成:

  1. 看板视图 API:拖拽排序(position 计算)、过滤、搜索
  2. 通知系统:站内通知 CRUD、到期提醒(cron job)

每一步都遵循相同的工作流:

Prompt → Agent 生成代码 → 运行测试 → 修复失败 → 人类审查 → 提交

9.5 Phase 4:前端开发——AI 辅助构建用户界面

React 项目初始化与组件架构设计

Prompt to Agent:
"请为 TaskFlow 设计前端组件架构。
技术栈:React 19 + TypeScript + Tailwind CSS + Zustand + React Router

需要的页面:
1. LoginPage — 登录/注册页面
2. DashboardPage — 项目列表(用户的项目)
3. BoardPage — 看板视图(单个项目的任务看板)

需要的核心组件:
1. TaskCard — 任务卡片
2. TaskColumn — 状态列(包含多个 TaskCard)
3. TaskModal — 任务详情弹窗
4. CreateTaskForm — 创建任务表单
5. ProjectCard — 项目卡片
6. NotificationBell — 通知铃铛

状态管理(Zustand Store):
1. useAuthStore — 认证状态
2. useProjectStore — 项目数据
3. useTaskStore — 任务数据
4. useNotificationStore — 通知数据

请生成组件树结构、Store 定义、路由配置。"

认证页面开发

// ===== packages/frontend/src/pages/LoginPage.tsx =====
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../stores/auth-store';
import { apiClient } from '../lib/api-client';

export function LoginPage(): JSX.Element {
  const [isLogin, setIsLogin] = useState(true);
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [name, setName] = useState('');
  const [error, setError] = useState<string | null>(null);
  const [loading, setLoading] = useState(false);
  
  const navigate = useNavigate();
  const setAuth = useAuthStore((s) => s.setAuth);

  async function handleSubmit(e: React.FormEvent): Promise<void> {
    e.preventDefault();
    setError(null);
    setLoading(true);

    try {
      const endpoint = isLogin ? '/auth/login' : '/auth/register';
      const body = isLogin
        ? { email, password }
        : { email, password, name };

      const res = await apiClient.post(endpoint, body);
      
      setAuth({
        token: res.data.token,
        refreshToken: res.data.refreshToken,
        user: res.data.user,
      });
      
      navigate('/dashboard');
    } catch (err) {
      const message = err instanceof Error ? err.message : '操作失败';
      setError(message);
    } finally {
      setLoading(false);
    }
  }

  return (
    <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100">
      <div className="w-full max-w-md p-8 bg-white rounded-2xl shadow-xl">
        <div className="text-center mb-8">
          <h1 className="text-3xl font-bold text-gray-900">TaskFlow</h1>
          <p className="mt-2 text-gray-500">
            {isLogin ? '欢迎回来' : '创建你的账号'}
          </p>
        </div>

        <form onSubmit={handleSubmit} className="space-y-4">
          {!isLogin && (
            <div>
              <label htmlFor="name" className="block text-sm font-medium text-gray-700">
                名称
              </label>
              <input
                id="name"
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
                className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
                required={!isLogin}
                minLength={2}
                maxLength={100}
              />
            </div>
          )}

          <div>
            <label htmlFor="email" className="block text-sm font-medium text-gray-700">
              邮箱
            </label>
            <input
              id="email"
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
              required
            />
          </div>

          <div>
            <label htmlFor="password" className="block text-sm font-medium text-gray-700">
              密码
            </label>
            <input
              id="password"
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
              className="mt-1 block w-full rounded-lg border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
              required
              minLength={8}
            />
          </div>

          {error && (
            <div className="p-3 rounded-lg bg-red-50 text-red-700 text-sm">
              {error}
            </div>
          )}

          <button
            type="submit"
            disabled={loading}
            className="w-full rounded-lg bg-blue-600 px-4 py-2.5 text-white font-medium hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 transition-colors"
          >
            {loading ? '处理中...' : (isLogin ? '登录' : '注册')}
          </button>
        </form>

        <p className="mt-6 text-center text-sm text-gray-500">
          {isLogin ? '还没有账号?' : '已有账号?'}
          <button
            type="button"
            onClick={() => setIsLogin(!isLogin)}
            className="ml-1 text-blue-600 hover:text-blue-700 font-medium"
          >
            {isLogin ? '立即注册' : '返回登录'}
          </button>
        </p>
      </div>
    </div>
  );
}

看板组件(核心交互)

// ===== packages/frontend/src/components/Board/TaskCard.tsx =====
import { useDraggable } from '@dnd-kit/core';
import type { Task } from '@taskflow/shared';

interface TaskCardProps {
  task: Task;
  onOpen: (task: Task) => void;
}

const PRIORITY_COLORS: Record<string, string> = {
  urgent: 'bg-red-100 text-red-800 border-red-200',
  high: 'bg-orange-100 text-orange-800 border-orange-200',
  medium: 'bg-blue-100 text-blue-800 border-blue-200',
  low: 'bg-gray-100 text-gray-800 border-gray-200',
};

const PRIORITY_LABELS: Record<string, string> = {
  urgent: '紧急',
  high: '高',
  medium: '中',
  low: '低',
};

export function TaskCard({ task, onOpen }: TaskCardProps): JSX.Element {
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    isDragging,
  } = useDraggable({
    id: task.id,
    data: { task },
  });

  const style = transform
    ? { transform: `translate(${transform.x}px, ${transform.y}px)` }
    : undefined;

  const isOverdue = task.dueDate && new Date(task.dueDate) < new Date() && task.status !== 'done';

  return (
    <div
      ref={setNodeRef}
      style={style}
      {...attributes}
      {...listeners}
      onClick={() => onOpen(task)}
      className={`
        p-3 bg-white rounded-lg border border-gray-200 shadow-sm
        cursor-pointer hover:shadow-md transition-shadow
        ${isDragging ? 'opacity-50 shadow-lg' : ''}
        ${isOverdue ? 'border-red-300 bg-red-50' : ''}
      `}
    >
      {/* 优先级标签 */}
      <div className="flex items-center gap-2 mb-2">
        <span className={`inline-block px-2 py-0.5 rounded text-xs font-medium border ${PRIORITY_COLORS[task.priority]}`}>
          {PRIORITY_LABELS[task.priority]}
        </span>
      </div>

      {/* 标题 */}
      <h3 className="text-sm font-medium text-gray-900 line-clamp-2">
        {task.title}
      </h3>

      {/* 标签 */}
      {task.tags.length > 0 && (
        <div className="flex flex-wrap gap-1 mt-2">
          {task.tags.slice(0, 3).map((tag) => (
            <span
              key={tag}
              className="inline-block px-1.5 py-0.5 rounded bg-gray-100 text-gray-600 text-xs"
            >
              {tag}
            </span>
          ))}
          {task.tags.length > 3 && (
            <span className="text-xs text-gray-400">+{task.tags.length - 3}</span>
          )}
        </div>
      )}

      {/* 底部信息 */}
      <div className="flex items-center justify-between mt-3 pt-2 border-t border-gray-100">
        {/* 截止日期 */}
        {task.dueDate && (
          <span className={`text-xs ${isOverdue ? 'text-red-600 font-medium' : 'text-gray-500'}`}>
            📅 {task.dueDate}
          </span>
        )}
        
        {/* 负责人头像 */}
        {task.assignee && (
          <div
            className="w-6 h-6 rounded-full bg-blue-100 flex items-center justify-center text-xs font-medium text-blue-700"
            title={task.assignee.name}
          >
            {task.assignee.name[0]}
          </div>
        )}
      </div>
    </div>
  );
}

状态管理(Zustand Store)

// ===== packages/frontend/src/stores/task-store.ts =====
import { create } from 'zustand';
import type { Task, BoardView } from '@taskflow/shared';
import { apiClient } from '../lib/api-client';

interface TaskState {
  board: BoardView | null;
  selectedTask: Task | null;
  loading: boolean;
  error: string | null;
  
  // Actions
  fetchBoard: (projectId: string, filters?: TaskFilters) => Promise<void>;
  createTask: (projectId: string, input: CreateTaskInput) => Promise<Task>;
  updateTask: (taskId: string, input: UpdateTaskInput) => Promise<void>;
  moveTask: (taskId: string, newStatus: string, newPosition: number) => Promise<void>;
  deleteTask: (taskId: string) => Promise<void>;
  setSelectedTask: (task: Task | null) => void;
}

export const useTaskStore = create<TaskState>((set, get) => ({
  board: null,
  selectedTask: null,
  loading: false,
  error: null,

  fetchBoard: async (projectId, filters) => {
    set({ loading: true, error: null });
    try {
      const res = await apiClient.get(`/projects/${projectId}/tasks`, {
        params: filters,
      });
      set({ board: res.data.data, loading: false });
    } catch (err) {
      set({
        error: err instanceof Error ? err.message : '加载任务失败',
        loading: false,
      });
    }
  },

  createTask: async (projectId, input) => {
    const res = await apiClient.post(`/projects/${projectId}/tasks`, input);
    const newTask = res.data.data as Task;
    
    // 乐观更新:立即添加到看板
    const { board } = get();
    if (board) {
      set({
        board: {
          ...board,
          todo: [...board.todo, newTask],
        },
      });
    }
    
    return newTask;
  },

  updateTask: async (taskId, input) => {
    const res = await apiClient.patch(`/tasks/${taskId}`, input);
    const updatedTask = res.data.data as Task;
    
    // 更新看板中的任务
    const { board } = get();
    if (board) {
      const newBoard = { ...board };
      for (const status of Object.keys(newBoard) as (keyof BoardView)[]) {
        newBoard[status] = newBoard[status].map((t) =>
          t.id === taskId ? updatedTask : t
        );
      }
      set({ board: newBoard });
    }
    
    // 更新选中的任务
    if (get().selectedTask?.id === taskId) {
      set({ selectedTask: updatedTask });
    }
  },

  moveTask: async (taskId, newStatus, newPosition) => {
    // 乐观更新
    const { board } = get();
    if (!board) return;

    // 找到任务当前所在的列
    let movedTask: Task | undefined;
    const newBoard = { ...board };
    
    for (const status of Object.keys(newBoard) as (keyof BoardView)[]) {
      const idx = newBoard[status].findIndex((t) => t.id === taskId);
      if (idx !== -1) {
        movedTask = { ...newBoard[status][idx], status: newStatus as Task['status'], position: newPosition };
        newBoard[status] = newBoard[status].filter((t) => t.id !== taskId);
        break;
      }
    }

    if (movedTask) {
      const targetColumn = newBoard[newStatus as keyof BoardView];
      targetColumn.splice(newPosition, 0, movedTask);
      set({ board: newBoard });
    }

    // API 调用
    try {
      await apiClient.patch(`/tasks/${taskId}`, {
        status: newStatus,
        position: newPosition,
      });
    } catch {
      // 回滚
      set({ board });
      await get().fetchBoard(board.todo[0]?.projectId ?? '');
    }
  },

  deleteTask: async (taskId) => {
    await apiClient.delete(`/tasks/${taskId}`);
    
    const { board } = get();
    if (board) {
      const newBoard = { ...board };
      for (const status of Object.keys(newBoard) as (keyof BoardView)[]) {
        newBoard[status] = newBoard[status].filter((t) => t.id !== taskId);
      }
      set({ board: newBoard });
    }
  },

  setSelectedTask: (task) => set({ selectedTask: task }),
}));

9.6 Phase 5:集成测试与质量保障

端到端测试(Playwright)

// ===== packages/frontend/tests/e2e/task-flow.spec.ts =====
import { test, expect } from '@playwright/test';

test.describe('TaskFlow 完整用户流程', () => {
  const testUser = {
    email: `e2e-test-${Date.now()}@example.com`,
    password: 'E2ETest@12345',
    name: 'E2E Tester',
  };

  test('完整流程:注册 → 创建项目 → 创建任务 → 拖拽任务 → 完成任务', async ({ page }) => {
    // ===== 1. 注册 =====
    await page.goto('/register');
    await page.fill('[name="name"]', testUser.name);
    await page.fill('[name="email"]', testUser.email);
    await page.fill('[name="password"]', testUser.password);
    await page.click('button[type="submit"]');
    
    await expect(page).toHaveURL('/dashboard');
    await page.screenshot({ path: 'screenshots/01-dashboard.png' });

    // ===== 2. 创建项目 =====
    await page.click('[data-testid="create-project-btn"]');
    await page.fill('[name="projectName"]', 'E2E 测试项目');
    await page.click('[data-testid="confirm-create"]');
    
    await expect(page.locator('[data-testid="project-card"]')).toContainText('E2E 测试项目');
    await page.screenshot({ path: 'screenshots/02-project-created.png' });

    // ===== 3. 进入看板 =====
    await page.click('text=E2E 测试项目');
    await expect(page).toHaveURL(/\/board\//);
    await page.screenshot({ path: 'screenshots/03-board-empty.png' });

    // ===== 4. 创建任务 =====
    await page.click('[data-testid="add-task-btn"]');
    await page.fill('[name="title"]', '第一个测试任务');
    await page.selectOption('[name="priority"]', 'high');
    await page.fill('[name="tags"]', 'testing');
    await page.click('[data-testid="save-task"]');
    
    await expect(page.locator('[data-testid="task-card"]')).toContainText('第一个测试任务');
    await page.screenshot({ path: 'screenshots/04-task-created.png' });

    // ===== 5. 创建更多任务 =====
    const tasks = ['后端 API 开发', '前端页面开发', '数据库设计'];
    for (const title of tasks) {
      await page.click('[data-testid="add-task-btn"]');
      await page.fill('[name="title"]', title);
      await page.click('[data-testid="save-task"]');
    }
    await page.screenshot({ path: 'screenshots/05-multiple-tasks.png' });

    // ===== 6. 拖拽任务到"进行中"列 =====
    const taskCard = page.locator('[data-testid="task-card"]').first();
    const inProgressColumn = page.locator('[data-testid="column-in_progress"]');
    
    await taskCard.dragTo(inProgressColumn);
    
    // 验证任务移动成功
    await expect(inProgressColumn).toContainText(/第一个测试任务|后端 API 开发|前端页面开发|数据库设计/);
    await page.screenshot({ path: 'screenshots/06-task-moved.png' });

    // ===== 7. 打开任务详情 =====
    await taskCard.click();
    await expect(page.locator('[data-testid="task-modal"]')).toBeVisible();
    
    // 更新任务状态
    await page.selectOption('[data-testid="status-select"]', 'in_review');
    await page.click('[data-testid="save-task-detail"]');
    await page.screenshot({ path: 'screenshots/07-task-detail.png' });

    // ===== 8. 完成任务 =====
    // 再次打开任务,标记为完成
    await taskCard.click();
    await page.selectOption('[data-testid="status-select"]', 'done');
    await page.click('[data-testid="save-task-detail"]');
    await page.screenshot({ path: 'screenshots/08-task-completed.png' });
  });

  test('任务过滤和搜索', async ({ page }) => {
    // 登录(复用 fixture)
    await loginAs(page, testUser);
    
    // 进入项目看板
    await page.click('text=E2E 测试项目');
    
    // 按优先级过滤
    await page.selectOption('[data-testid="filter-priority"]', 'high');
    const visibleCards = page.locator('[data-testid="task-card"]');
    // 所有可见卡片应该都是 high 优先级
    const count = await visibleCards.count();
    for (let i = 0; i < count; i++) {
      await expect(visibleCards.nth(i)).toContainText('高');
    }
    await page.screenshot({ path: 'screenshots/09-filtered.png' });

    // 搜索任务
    await page.fill('[data-testid="search-input"]', 'API');
    await expect(page.locator('[data-testid="task-card"]')).toContainText('后端 API 开发');
    await page.screenshot({ path: 'screenshots/10-searched.png' });
  });
});

// 辅助函数
async function loginAs(
  page: Page,
  credentials: { email: string; password: string },
): Promise<void> {
  await page.goto('/login');
  await page.fill('[name="email"]', credentials.email);
  await page.fill('[name="password"]', credentials.password);
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('/dashboard');
}

性能测试与优化

// ===== packages/backend/tests/performance/api-bench.test.ts =====
import { describe, it, expect } from 'vitest';
import request from 'supertest';
import { createApp } from '../../src/app';

describe('API 性能基准测试', () => {
  const app = createApp();

  it('GET /api/v1/projects/:id/tasks 应该在 200ms 内返回(100 条任务)', async () => {
    // 准备测试数据:创建 100 条任务
    const { token, projectId } = await setupTestData(app, 100);

    const start = performance.now();
    const res = await request(app)
      .get(`/api/v1/projects/${projectId}/tasks`)
      .set('Authorization', `Bearer ${token}`);
    const elapsed = performance.now() - start;

    expect(res.status).toBe(200);
    expect(elapsed).toBeLessThan(200);
    
    console.log(`API 响应时间: ${elapsed.toFixed(2)}ms`);
  });

  it('POST /api/v1/auth/login 应该在 500ms 内完成(含 bcrypt 验证)', async () => {
    const { email, password } = await createTestUser(app);

    const start = performance.now();
    const res = await request(app)
      .post('/api/v1/auth/login')
      .send({ email, password });
    const elapsed = performance.now() - start;

    expect(res.status).toBe(200);
    expect(elapsed).toBeLessThan(500);
    
    console.log(`登录响应时间: ${elapsed.toFixed(2)}ms`);
  });
});

安全审计(OWASP 检查清单)

# 安全审计清单

## 认证安全 ✅
- [x] 密码使用 bcrypt(cost factor 12)
- [x] JWT Token 有过期时间(24h)
- [x] Refresh Token 机制
- [x] 登录错误不区分"用户不存在"和"密码错误"
- [x] 密码不出现在 API 响应中

## 输入验证 ✅
- [x] 所有 API 输入使用 Zod Schema 验证
- [x] SQL 查询使用参数化查询(Drizzle ORM 自动处理)
- [x] XSS 防护(前端使用 React 自动转义)
- [x] 文件上传大小限制(10MB)

## 授权安全 ✅
- [x] 用户只能访问自己参与的项目
- [x] 任务操作需要项目成员身份
- [x] 项目删除需要 Owner 权限

## 数据安全 ⚠️
- [x] HTTPS 强制(生产环境)
- [x] 敏感信息使用环境变量
- [ ] CORS 配置(需要生产环境域名)
- [ ] Rate Limiting(建议添加)

9.7 Phase 6:部署与运维

Docker 容器化

# ===== Dockerfile — 多阶段构建 =====

# Stage 1: 构建
FROM node:22-alpine AS builder

WORKDIR /app

# 安装 pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate

# 复制依赖文件
COPY pnpm-workspace.yaml pnpm-lock.yaml ./
COPY packages/backend/package.json packages/backend/
COPY packages/frontend/package.json packages/frontend/
COPY packages/shared/package.json packages/shared/

# 安装依赖
RUN pnpm install --frozen-lockfile

# 复制源码
COPY . .

# 构建
RUN pnpm build

# Stage 2: 运行
FROM node:22-alpine AS runner

WORKDIR /app

RUN corepack enable && corepack prepare pnpm@latest --activate

# 只复制生产依赖和构建产物
COPY --from=builder /app/packages/backend/dist ./dist
COPY --from=builder /app/packages/backend/package.json ./
COPY --from=builder /app/packages/frontend/dist ./public
COPY --from=builder /app/node_modules ./node_modules

# 非 root 用户
RUN addgroup -g 1001 -S appgroup && \
    adduser -S appuser -u 1001
USER appuser

EXPOSE 3000

ENV NODE_ENV=production

CMD ["node", "dist/index.js"]
# docker-compose.yml — 本地开发环境
version: '3.8'

services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://taskflow:taskflow@db:5432/taskflow
      - JWT_SECRET=${JWT_SECRET}
      - NODE_ENV=production
    depends_on:
      db:
        condition: service_healthy
  
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: taskflow
      POSTGRES_USER: taskflow
      POSTGRES_PASSWORD: taskflow
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U taskflow"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:

生产环境配置

# .env.production — 生产环境变量
NODE_ENV=production
PORT=3000

# 数据库(使用 Railway 提供的连接字符串)
DATABASE_URL=postgresql://...

# JWT
JWT_SECRET=<使用 openssl rand -base64 32 生成>

# 邮件服务(通知用)
SMTP_HOST=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USER=apikey
SMTP_PASS=<SendGrid API Key>

# 前端
VITE_API_BASE_URL=https://api.taskflow.example.com

CI/CD 自动化部署

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run CI
        uses: ./.github/workflows/ci.yml
      
      - name: Build Docker Image
        run: |
          docker build -t taskflow:${{ github.sha }} .
          docker tag taskflow:${{ github.sha }} taskflow:latest
      
      - name: Push to Registry
        run: |
          docker login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASS }}
          docker push registry.example.com/taskflow:${{ github.sha }}
          docker push registry.example.com/taskflow:latest
      
      - name: Deploy to Railway
        uses: bervProject/railway-deploy@main
        with:
          railway_token: ${{ secrets.RAILWAY_TOKEN }}
          service: taskflow-backend

9.8 项目复盘

时间分析

Phase AI 辅助耗时 传统开发估算 提效比 备注
需求分析与规格 1.5h 8-16h 5-10x Agent 帮助结构化 PRD,审查遗漏
Harness 搭建 1h 4-8h 4-8x 配置模板化,快速搭建
后端开发 4h 24-40h 6-10x Agent 生成 80% 代码,人类审查调整
前端开发 4h 24-40h 6-10x Agent 生成组件框架,人类调整交互细节
集成测试 1.5h 8-16h 5-10x Agent 生成测试代码,人类审查覆盖度
部署运维 0.5h 4-8h 8-16x Docker + CI/CD 模板化
总计 12.5h 72-128h ~7x 约 1.5 个工作日 vs 2-3 周

代码质量分析

指标 Agent 生成 人类审查后 说明
TypeScript 类型覆盖率 95% 99% Agent 偶尔遗漏复杂泛型
ESLint 通过率 85% 100% 主要是 unused imports
测试覆盖率 78% 85% Agent 遗漏部分边界条件
CI 首次通过率 70% 95% 主要是类型和 Lint 问题
安全漏洞 0 0 Harness 的安全检查有效

人类干预统计

干预类型 频率 说明
架构决策 5 次 Agent 提供了多个方案,人类选择
代码审查修改 ~30 处 品味相关(命名、抽象层次)
Bug 修复 8 处 Agent 生成的代码中的逻辑错误
性能优化 3 处 N+1 查询、不必要的重渲染
安全加固 2 处 CORS 配置、Rate Limiting

经验教训

做得好的

  1. Harness 先行策略有效:测试先行的方式让 Agent 生成的代码质量很高,CI 首次通过率达 70%
  2. SDD 规格驱动有效:先定义 API 规格再开发,前后端对接无缝
  3. 分步 Prompt 策略有效:将大任务拆成小步骤,每步都容易成功
  4. Cursor + Claude Code 组合高效:Cursor 做精细调整,Claude Code 做批量生成

需要改进的

  1. ⚠️ 前端交互细节:Agent 在复杂交互(如拖拽排序)上仍需较多人工调整
  2. ⚠️ 性能优化:Agent 倾向于生成"功能正确但不够高效"的代码
  3. ⚠️ 错误处理:Agent 的错误处理有时过于简单,需要人类补充边界情况

AI Agent 在这个项目中的真实贡献率

代码行数统计:
- Agent 生成:~4,500 行(75%)
- 人类编写:~1,200 行(20%,主要是配置和审查修改)
- 自动生成(脚手架/迁移):~300 行(5%)

价值贡献评估:
- Agent 贡献:~70%(节省了大量编码时间)
- 人类贡献:~30%(架构决策、品味审查、边界处理)

结论:Agent 是强大的"力量放大器",但不能替代人类的判断力。
最佳模式:Agent 做 80% 的"体力活",人类做 20% 的"脑力活"。

9.9 从单体到微服务的 Agent 辅助演进

当 TaskFlow 的用户量增长到一定规模,单体架构可能成为瓶颈。这时,AI Agent 可以辅助完成从单体到微服务的演进。

何时拆分

信号 说明 拆分建议
部署耦合 改一个模块要部署整个应用 按模块拆分
扩展不均 任务模块需要 10 个实例,通知模块只需 1 个 按扩展需求拆分
团队规模 团队 > 8 人,代码冲突频繁 按团队边界拆分
技术栈分歧 通知模块想用 Go,任务模块保持 TypeScript 按技术栈拆分

Agent 辅助拆分策略

Prompt to Agent:
"我们计划将 TaskFlow 的任务模块和通知模块拆分为独立的微服务。
请帮我制定拆分计划:
1. 分析当前的模块依赖关系
2. 识别需要拆分的代码边界
3. 设计服务间通信方案(gRPC vs 消息队列 vs REST)
4. 制定数据迁移策略(共享数据库 vs 独立数据库)
5. 输出分步实施计划"

Agent 辅助的微服务拆分遵循"绞杀者模式"(Strangler Fig Pattern):

阶段4: 清理

阶段3: 流量迁移

阶段2: 抽离服务

阶段1: 识别边界

分析代码依赖

确定拆分边界

创建独立服务

实现服务间通信

双写数据

逐步切流

移除旧代码

独立部署

微服务间通信的 Agent 辅助实现

// ===== 服务间通信:事件驱动(推荐方案)=====

// 任务服务发布事件
// packages/task-service/src/events/task-events.ts
import { EventEmitter } from 'events';

export const taskEvents = new EventEmitter();

// 任务状态变更时发布事件
export function emitTaskStatusChanged(task: Task, oldStatus: string): void {
  taskEvents.emit('task.status_changed', {
    taskId: task.id,
    projectId: task.projectId,
    title: task.title,
    oldStatus,
    newStatus: task.status,
    assigneeId: task.assigneeId,
    creatorId: task.creatorId,
    timestamp: new Date().toISOString(),
  });
}

// 通知服务订阅事件
// packages/notification-service/src/subscribers/task-subscriber.ts
import { taskEvents } from '@taskflow/task-service/events';

taskEvents.on('task.status_changed', async (event) => {
  // 发送通知给任务创建者
  if (event.newStatus === 'done') {
    await createNotification({
      userId: event.creatorId,
      type: 'task_completed',
      title: '任务已完成',
      message: `"${event.title}" 已标记为完成`,
      taskId: event.taskId,
    });
  }
  
  // 发送通知给负责人
  if (event.assigneeId && event.newStatus === 'in_review') {
    await createNotification({
      userId: event.creatorId,
      type: 'task_submitted_for_review',
      title: '任务已提交审核',
      message: `"${event.title}" 已提交审核,请查看`,
      taskId: event.taskId,
    });
  }
});

9.10 本章小结与思考题

本章小结

本章通过一个完整的端到端实战项目,展示了 AI Coding 方法论在真实项目中的应用:

  1. SDD(规格驱动开发):先定义 PRD、API 规格和数据模型,为后续的 Agent 编码提供精确的"地图"。

  2. Harness 先行:在 Agent 编码之前就搭建好类型系统、测试框架和 CI 流水线,让 Agent 在约束中自由发挥。

  3. 分步开发:将大任务拆分为多个小步骤,每步都有明确的 Prompt、测试验证和人类审查。

  4. 前后端协同:后端先完成 API,前端基于 API 规格并行开发,AI Agent 在两端都能高效产出。

  5. 质量保障:E2E 测试、性能基准、安全审计三位一体,确保交付质量。

  6. 项目复盘:通过时间分析、代码质量分析和人类干预统计,量化 AI Coding 的真实效果。

核心发现

  • AI Agent 贡献了约 70% 的代码价值,但人类的 30% 贡献(架构决策、品味审查、边界处理)不可替代
  • 最佳提效比约 7 倍(12.5 小时 vs 72-128 小时传统开发)
  • Agent 的强项是"体力活"(编码、测试生成、文档),弱项是"脑力活"(架构、品味、性能优化)
  • Harness Engineering 是将 Agent 产出质量从 70% 提升到 95% 的关键

思考题

  1. 项目规模:本章的项目是一个中等规模的 Web 应用。如果项目规模扩大 10 倍(比如一个企业级 ERP 系统),AI Coding 的策略需要如何调整?Harness 需要如何增强?

  2. 遗留系统:如果 TaskFlow 不是从零开始,而是一个有 5 年历史的遗留系统,你会如何使用 AI Agent 进行渐进式重构?策略与本章有什么不同?

  3. 团队协作:本章的开发过程是单人+AI 的模式。如果是一个 5 人团队同时使用 AI Agent 开发,你会如何协调?CLAUDE.md 和 .cursorrules 如何共享和同步?

  4. 技术债务:Agent 生成的代码中,哪些类型的问题最容易成为技术债务?你会如何在 Harness 中预防这些问题?

  5. 成本分析:计算本章项目的 AI API 调用成本(基于 Claude Sonnet 的定价:$3/M input, $15/M output)。与传统开发的人力成本对比,AI Coding 的经济效益如何?

  6. 质量权衡:在什么场景下,你会接受 Agent 生成的"不完美但可用"的代码,而不是花时间让它生成"完美"的代码?你的判断标准是什么?

  7. 微服务拆分:在 §9.9 中我们讨论了从单体到微服务的演进。你认为 Agent 辅助微服务拆分的最大风险是什么?如何设计 Harness 来降低这个风险?

  8. 未来展望:假设 AI Agent 的能力在未来 2 年内提升 10 倍(代码质量接近人类高级工程师),本章的开发流程和 Harness 策略需要如何调整?人类的角色会发生什么变化?


下一篇预告:在第10章中,我们将从个人开发者的视角扩展到团队级别。你将学习如何在团队中推广 AI Coding 实践,建立团队级的 AI 工程规范,以及管理 AI 生成代码的质量和安全。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐