# Agent 规范

本文件是 FTB 系统中所有 AI Agent 的**唯一权威定义**。任何对 agent 的能力、职责、权限、协作方式的修改，必须先在本文件落地，再同步到 architecture / decisions / workflow / roadmap / glossary。

## 何时更新本文件

1. **新增 agent**：新建一个独立 agent
2. **agent 职责变化**：输入/输出/调用条件变了
3. **agent 权限变化**：可读哪些数据、可写哪些数据变了
4. **多 agent 协作方式变化**：编排顺序、依赖关系、出错回退策略变了

不影响以上四类的 prompt 微调、模型版本切换、性能优化不需要更新本文件。

## 全局原则

1. **AI 不直接动数据，先生草案，用户确认**
   - 所有写入实体（DevTask / TestCase）必须带 `aiDraft: true` 标记
   - 用户编辑保存后，标记自动清除（在 `useDevTaskStore.updateTask` / `useTestCaseStore.updateTestCase` 里实现）

2. **每条产物强制带引用**
   - DevTask / TestCase 必须有 `references: Reference[]`，至少 1 条
   - 引用类型：`requirement` / `prototype_note` / `external`
   - 不带引用的草案不允许写入

3. **AI 输出必须有"对账报告"**
   - 拆解任务前，先输出结构化对账：
     - ✅ 完美对应（需求 ↔ 原型注释 ↔ 任务）
     - ⚠️ 需求未见原型
     - 无需求ID分组（原型有明确功能，但没有匹配到关联需求）
     - ❓ 注释含糊无法转化
   - 对账报告先呈现给用户，用户审核后才执行写入

4. **AI 预估不等于执行人预估**
   - AI 只能输出 `aiEstimateHours`
   - `estimateHours` 留给负责人/执行人确认后填写
   - AI 不输出预计开始/截止时间，排期由负责人后续维护

5. **AI 可推荐负责人，但不自动分配**
   - AI 只能从当前版本成员 `members[].name` 中输出可选的 `recommendedAssigneeName`
   - 推荐只作为采纳弹窗里的辅助信息，默认不写入 `assigneeId`
   - 用户勾选“采纳推荐负责人”后，前端再次校验推荐姓名属于当前版本成员，才写入 DevTask/TestCase
   - 没有明确匹配成员时不输出推荐字段

6. **历史版本不强制存储**
   - 系统不要求历史原型 URL 作为基准
   - 拆偏问题靠"对账报告"暴露给用户，由人工补救
   - 这条决定见 decisions.md #17

## Agent 列表

### Agent 1：Prototype Decompose Agent（原型拆解）

**目的**：把"产品方案原型 + 关联需求"拆解成开发任务草案 + 测试用例草案。关联需求是正式范围锚点；原型中明确可拆但没有匹配到关联需求的功能，也可以拆成无需求ID分组草案。

**输入**：
- 当前版本「产品方案」类型 + status=completed 的 VersionPlan，取其 `resultUrl` 作为**原型链接**（约定：产品方案的成果即原型）
- 当前版本关联需求列表（已被加进 Version 的 Requirement[]）
- 版本成员清单（带角色：前端/后端/UI/测试）

**调用入口**：版本详情页 → 产品方案 Tab → 计划状态 = completed 后，按钮「AI 拆解开发任务」或「AI 拆解测试用例」

**触发条件**：
- 当前版本至少有 1 条 type=product 的 VersionPlan 处于 `completed` 且 resultUrl 非空（提交了原型）
- 当前版本至少关联 1 条需求

**输出**：
- 对账报告（结构化文本，含"完美对应/需求未见原型/无需求ID分组/含糊"）
- DevTask 草案数组（每条带结构化 `description` + `taskTypeName` + `aiDraft: true` + `references[]` + `aiEstimateHours`，可选 `categoryCode` / `recommendedAssigneeName` / `recommendedAssigneeReason`）
- TestCase 草案数组（每条带 `taskTypeName` + `aiDraft: true` + `references[]` + `aiEstimateHours`，可选 `categoryCode` / `recommendedAssigneeName` / `recommendedAssigneeReason`）
- `target = dev_tasks` 时 `testCaseDrafts` 必须为空数组；`target = test_cases` 时 `devTaskDrafts` 必须为空数组

**原型与需求匹配规则**：
- 优先按需求编号匹配：原型批注或文本出现 `requirement.id` / `requirement.code` 时，必须优先判定为该需求命中。
- 编号未出现时，再按需求标题和需求概述（`title` / `description`）做语义匹配。
- 有 QY 编号时，`prototype_note` 引用只能使用真实存在的 QY 编号。
- 没有 QY 编号但原型文本已命中需求编号或需求概述时，不应丢弃；可只引用 `requirement`，并在 `matched.noteIds` 返回空数组。
- 既匹配不到需求编号，也匹配不到需求概述语义，但能形成明确功能名称和任务范围的原型批注，进入 `prototypeOnly`，对应草案带 `requirementName` 且不带 `requirement` 引用。
- 只有无法形成稳定任务/用例的含糊批注才进入 `ambiguous`。
- 原型批注只要包含标题、详细说明、字段、异常或验收标准之一，且能判断用户动作或系统行为，就不能进入 `ambiguous`；这类内容必须进入 `matched` 或 `prototypeOnly` 并继续拆解。

**开发任务与测试用例拆解粒度**：
- Agent 先在内部把每条 QY 或命中的需求拆成交付切片，再生成 DevTask/TestCase；交付切片维度按 UI、交互、接口、数据、异常、边界、状态流转、兼容和回归扫描。
- 每条业务规则、字段规则、交互规则、异常规则和验收标准都必须至少映射到 1 条开发任务或 1 条测试用例；本次 target 不包含的一侧可以不输出，但另一侧必须覆盖。
- DevTask 必须是一个可交付工程动作。前端、后端、接口、数据库、数据处理和集成支持要按职责拆开。
- DevTask `description` 必填，至少包含实现范围和验收点；如存在非目标范围或依赖，也必须写明。

**测试用例拆解粒度**：
- TestCase 要按功能点、UI 交互、表单校验、接口、数据一致性、权限、异常、边界、状态流转、兼容性和回归点拆细。
- 不允许用一条“验证 XX 完整流程”覆盖多个交互、多个接口或多个规则。
- 一条 QY 若同时涉及 UI、接口、数据、异常和状态变化，通常应拆出 3-8 条 TestCase。
- TestCase `description` 必填，至少包含前置条件、操作步骤和预期结果；涉及边界、异常或数据一致性时，必须写明测试数据或状态。
- DevTask / TestCase 必须输出 `taskTypeName`，但它必须是可复用的任务类型字典项，不是任务标题或任务概述。`categoryCode` 只作为可选的兼容映射字段；没有合适稳定码时可以省略，不能因此停止拆解。

**写入**：
- 用户确认后，调用 `useDevTaskStore.createTask` 和 `useTestCaseStore.createTestCase`
- 打开采纳弹窗前，前端先过滤当前版本已采纳过的重复 DevTask/TestCase 草案
- 写入前，前端按 `taskTypeName` 检查 `TaskCategory` 字典；可复用开发类型不存在时自动追加非系统任务类型。测试用例未知类型不自动入库，优先按 `categoryCode` 或默认测试类型回退。
- 写入字段中 `aiDraft: true`、`aiDraftAt: ISO时间戳`
- 写入 `aiEstimateHours`，不写入执行人预估 `estimateHours`
- DevTask / TestCase 必须写入 `versionId` 作为执行归属；`requirementId` 可选
- 无正式需求 ID 的草案写入 `requirementName` 作为分组展示名，列表显示为 `无需求ID · {requirementName}`
- 无需求ID分组不创建 Requirement，不写入需求池，不加入版本关联需求列表
- DevTask 草案不写预计开始/截止时间，负责人后续排期时再填写
- 只有用户在采纳弹窗勾选“采纳推荐负责人”时，才把已校验的 `recommendedAssigneeName` 写入 `assigneeId`

**权限**：
- 读：Version, Requirement, VersionPlan, Member
- 写：DevTask（仅 aiDraft 草案）、TestCase（仅 aiDraft 草案）
- 不得修改：Requirement、Version、VersionPlan、Member、Bug

**失败回退**：
- 原型 URL 不可达 → 报告"原型无法访问"，不写入任何数据
- 没有已完成的产品方案计划 → 报告"请先完成产品方案并提交原型成果"，不写入
- 关联需求为空 → 报告"无关联需求，无法对照"，不写入
- 对账报告全是 ❓ → 报告"原型注释含糊度过高，建议人工拆解"，不写入

**MVP 阶段限制**（V3.1）：
- 只生成 DevTask 和 TestCase 草案
- 不自动分配 assignee；AI 只提供可选推荐，用户确认采纳后才写入
- 不做"上一版基准 diff"

### Agent 2：Risk Watch Agent（小宝预警解读）

**目的**：解释小宝预警规则引擎输出的版本发版风险结果，生成项目经理可读的风险原因、延期预测、建议发版窗口和处理动作。

**输入**：
- 版本上下文：产品、项目、版本、期望发版日期。
- 规则风险结果：`riskScore`、`riskLevel`、`forecastReleaseDate`、`delayDays`、`confidence`、风险信号。
- 趋势和快照：风险分变化、连续上升/下降、关键 Bug、失败用例、阻塞和静默风险变化。
- 日报与工作活动证据：今日交付、今日进展、今日风险、进展备注、需要补充进展的事项。

**触发**：
- `on_track` 不触发。
- `at_risk`、`likely_delayed`、`blocked` 自动触发。
- `attention` 在风险分明显上升、趋势连续上升、关键 Bug 增加、失败用例增加、阻塞增加、静默风险增加、置信度下降或预测发版日延后时触发。

**输出**：`summary`、`why[]`、`forecast`、`recommendedReleaseWindow`、`suggestedActions[]`、`ownerHints[]`。

**权限**：读规则结果和压缩证据；写 `xiaobao-risk-insights` 缓存。不修改 Version、Requirement、DevTask、TestCase、Bug、Member。

**失败回退**：AI 不可用时保留规则预警，前端显示“规则预警已生成，AI 解读会在触发条件满足时自动补充”。AI 失败不影响快照保存和规则风险展示。

### Agent 3：Schedule Suggest Agent（排期建议）— 待规划

仅占位，正式规划见 roadmap.md V3.2。

## 多 Agent 协作（V3.2 规划）

当前 V3.1 只有 Prototype Decompose Agent 单独运行。
未来 Risk Watch / Schedule Suggest 引入后，编排策略另行设计。

## 实现位置

V3.1 起，所有 Agent **走 NestJS 后端**（不走 Next.js API Route），原因：
- 与现有 `product` / `requirement` 模块架构一致
- 共享 Prisma 实例便于后续 AiLog 持久化
- 多 Agent 引入时统一在 `AiGateway` 管理 prompt / 降级 / 重试

代码位置：
```
apps/server/src/modules/
├── ai/
│   ├── ai.module.ts
│   ├── ai.controller.ts                # POST /api/v1/ai/decompose
│   ├── ai.service.ts                   # 抓原型 + 调 LLM + 解析
│   ├── ai-gateway.service.ts           # Anthropic 客户端单例（懒加载，key 改了重建）
│   ├── prompts/decompose.ts            # System Prompt + Tool Schema
│   └── dto/decompose.dto.ts            # 入参校验
└── config/
    ├── config.module.ts
    ├── ai-config.controller.ts         # GET/PATCH /api/v1/config/ai, POST /test
    ├── ai-config.service.ts            # 读写 data/ai-config.json
    └── dto/update-ai-config.dto.ts
```

共享类型：`packages/shared/src/agent.ts`（前后端通用）

## 配置管理

**多提供商支持（V3.1.5 起）**：系统支持配置任意数量的 AI 提供商，每个独立的 baseURL / apiKey / model，运行时切换激活。

**支持的 API 格式**：
- `anthropic` — Anthropic 官方 + 兼容 Anthropic Messages API 格式的中转站（如 ikuncode 的 anthropic 端点）
- `openai` — OpenAI 官方 + 兼容 OpenAI Chat Completions 格式的中转站

**API Key 优先级**：当前激活提供商的 apiKey → 环境变量 `ANTHROPIC_API_KEY`（兜底，仅 anthropic 格式可用）

**前端配置入口**：`/admin/ai-config`，仅超管（`role.permissions` 含 `'*'`）可见

**字段（每个 Provider）**：
- `id`：用户自定义唯一 ID（如 `ikuncode`）
- `name`：显示名
- `format`：`anthropic` / `openai`
- `baseURL`：完整 API endpoint
- `apiKey`：完整 Key 仅在服务端持有；前端只能拿到 mask 视图
- `model`：该提供商上使用的模型 ID
- `remark`：可选备注

**全局字段**：
- `activeProviderId`：当前激活哪个 provider
- `updatedAt` / `updatedBy`：审计

**测试连接**：`POST /api/v1/config/ai/test` 带 `{ id }`，用 16 token 极简调用验证目标 provider 可用性

**激活**：`POST /api/v1/config/ai/activate` 带 `{ id }`，切换激活 provider；AiGateway 缓存自动失效，下次调用重新构建客户端

**安全约束**：
- 配置文件路径在 .gitignore 里（`apps/server/data/`），不进入版本控制
- 前端 GET 接口永远不返回完整 Key
- 修改 Key 只能从前端 PATCH 上传新值，不支持读出旧值
- 编辑提供商时若不传 apiKey，则保留原值

**数据迁移**：旧版（V3.1）的 `{ anthropicApiKey, model }` 单 key 结构，自动迁移为多 providers 结构（id 为 `anthropic-official`，自动激活）。

## 数据契约

### Reference 类型

```ts
interface Reference {
  type: 'requirement' | 'prototype_note' | 'external';
  id: string;       // REQ-008 / QY0007 / 自由文本
  label: string;    // 展示用
  url?: string;     // 可选跳转链接
}
```

### AI 草案标记

```ts
// DevTask 和 TestCase 共有
aiDraft?: boolean;
aiDraftAt?: string;  // ISO 时间戳
references?: Reference[];
aiEstimateHours?: number; // AI 预估耗时，单位小时
```

### Agent 输入预期

```ts
interface DecomposeInput {
  version: VersionWithContext;       // 上下文（不含 prototypeUrl，原型从产品方案成果取）
  prototypeUrl: string;              // 从产品方案 completed 计划的 resultUrl 派生
  requirements: Requirement[];       // 当前版本所属项目下已采纳、可用于本次拆解的需求
  members: VersionMember[];          // 该版本参与人员（带角色）
  target?: 'all' | 'dev_tasks' | 'test_cases';
}
```

要求：`requirements` 不能从产品级全量需求池直接取，必须经过项目维度和已采纳状态过滤。

### Agent 输出预期

```ts
interface DecomposeOutput {
  report: {
    matched: Array<{ reqId: string; noteIds: string[]; taskCount: number }>;
    reqOnly: string[];     // 需求 ID 列表
    prototypeOnly: Array<{ requirementName: string; noteIds: string[]; taskCount: number }>;
    ambiguous: Array<{ noteId: string; reason: string }>;
  };
  devTaskDrafts: Array<{ title: string; description: string; taskTypeName: string; categoryCode?: string; priority: Priority; aiEstimateHours: number; requirementName?: string; recommendedAssigneeName?: string; recommendedAssigneeReason?: string; references: Reference[] }>;
  testCaseDrafts: Array<{ title: string; description: string; taskTypeName: string; categoryCode?: string; priority: Priority; aiEstimateHours: number; requirementName?: string; recommendedAssigneeName?: string; recommendedAssigneeReason?: string; references: Reference[] }>;
}
```

要求：AI 不输出数据库 `categoryId`。`taskTypeName` 是必填的可复用类型名，不能写成“排行榜测试”这类当前文档专属概述；`categoryCode` 只是可选映射提示，不限制 AI 拆解。前端确认写入时按 `taskTypeName` 解析 `TaskCategory`，可复用开发类型缺失时才追加到任务类型字典；测试用例未知类型回退到已有测试分类。AI 不输出 `estimateHours`、预计开始或预计截止。草案没有 `requirement` 引用时，必须有 `requirementName` 和至少一个 `prototype_note` 引用。

### 任务/用例分组契约

```ts
interface VersionScopedRequirementGroup {
  versionId: string;       // 执行归属，必填
  requirementId?: string;  // 正式需求 ID，可选
  requirementName?: string; // 无正式需求 ID 时的展示分组名
}
```

要求：版本级聚合使用 `versionId`；需求级进度只统计带 `requirementId` 的 DevTask。无需求ID分组只影响版本任务/用例视图，不影响需求池和关联需求列表。

## 视觉规范

AI 草案在 DevTask / TestCase 列表中的视觉区分：
- 整行加 `border-l-2 border-l-purple-400 bg-purple-50/30`
- 标题旁紫色徽章「AI 草案」（`bg-purple-100 text-purple-600`）
- 用户在详情抽屉里编辑保存任意字段后，徽章和左边线自动消失
- 无需求ID分组不额外显示“原型发现”等标记，只在分组标题中展示 `无需求ID · {requirementName}`
