LinkMed Claude Code 自动研发使用指南
借助 Claude Code 实现从需求描述到代码实现、自动验证的完整研发流程,减少人工手动验证环节。
目录
- 快速开始
- 整体工作流程
- 多端协作架构
- 使用方式
- 从 TAPD 获取任务详情
- TAPD 脚本安装配置
- 测试体系
- 单元测试规范(Vitest)
- E2E 测试规范(Playwright)
- 提交规范
- dev-sessions 文档规范
- 后续规划
1. 快速开始
启动方式
# 普通模式(每步操作需手动确认)
claude
# 全自动模式(跳过所有权限确认,适合批量任务)
claude --dangerously-skip-permissions
# 全自动模式 + 直接传入任务
claude --dangerously-skip-permissions -p "帮我打包并提交代码"
⚠️
--dangerously-skip-permissions为高风险模式,Claude 可直接执行文件读写、Git 操作、终端命令,不会弹出确认框,包括删除文件、强制推送、覆盖数据等破坏性操作。仅在本地受信任环境中使用,不要在生产服务器或共享环境中开启。建议配合CLAUDE.md中的约束说明,明确告知 Claude 哪些操作是禁止的。
2. 整体工作流程
┌─────────────────────────────────────────────────────┐
│ 1. 获取任务(需求 / 缺陷,含图片和评论) │
│ · 直接描述需求/缺陷内容 │
│ · 或提供 TAPD ID,脚本自动拉取详情+评论 │
│ 需求:node ~/.claude/tapd.mjs story <ID> │
│ 缺陷:node ~/.claude/tapd.mjs bug <ID> │
│ · 从描述/评论中提取图片 URL,下载并阅读 │
│ COOKIE=$(cat ~/.claude/tapd-cookie.txt) │
│ curl -s -L -H "Cookie: $COOKIE" \ │
│ -H "Referer: https://www.tapd.cn/" <URL> │
│ -o /tmp/req_img.png │
│ · 评论中若有关键信息(设计说明、补充要求等) │
│ 必须纳入分析,写入 dev-sessions 文档 │
└─────────────────────┬───────────────────────────────┘
│ 逐个取出
┌─────────────────────▼───────────────────────────────┐
│ 2. 分析与规划 │
│ · 理解需求目标和验收标准 │
│ · 阅读相关代码,理解现有架构和模块 │
│ · 制定实现方案,必要时与开发者确认 │
└─────────────────────┬───────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 3. 实现代码 │
│ · 每修改完一个文件,立刻运行 lint 和 type-check │
│ npm run lint && npm run type-check │
│ · 有错误自动修复,无需人工干预,修完再继续 │
│ · 遵循项目现有代码规范和架构模式 │
└─────────────────────┬───────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ 4. 单元测试(Vitest) │
│ · 针对 Store 逻辑、工具函数编写/更新测试 │
│ · npm run test:unit │
│ · 测试文件位于 src/test/**/*.test.ts │
└──────────┬──────────────────────────┬───────────────┘
↓ 全部通过 ↓ 有失败
│ ┌───────────────────────┐
│ │ 定位失败原因 │
│ │ → 修复代码或测试用例 │
│ │ → 重新运行测试 │
│ └──────────┬────────────┘
│ ↑
│ └── 循环直到全部通过
┌─────────────────────────────────────────────────────┐
│ 5. E2E 测试(Playwright) │
│ · 针对需要真实浏览器的交互场景编写测试 │
│ · 存放至 e2e/tests/{模块}/{需求ID}-描述.spec.ts │
│ · npm run test:e2e 自动运行验证 │
└──────────┬──────────────────────────┬───────────────┘
↓ 全部通过 ↓ 有失败
┌──────────────────────┐ ┌──────────────────────────┐
│ 6. 逐条核查验收标准 │ │ 定位失败原因 │
│ · 对照需求验收标准 │ │ → 修复代码或测试用例 │
│ 逐条确认代码实现 │ │ → 重新运行测试 │
│ · 发现遗漏则补充实现 │ └────────────┬─────────────┘
└──────────┬───────────┘ ↑
↓ └── 循环直到全部通过
┌──────────────────────────────────────────────────────┐
│ 7. 提交并推送代码(每次必做,无需等待用户提醒) │
│ · git commit(含功能代码、测试、文档) │
│ · git push │
│ · 更新 dev-sessions 过程记录文档 │
└──────────┬───────────────────────────────────────────┘
│ ↑
│ └── 循环直到全部通过
↓ 还有下一个需求
┌──────────────────────┐
│ 返回步骤 1,处理 │
│ 下一个需求 │
└──────────────────────┘
↓ 所有需求完成
全部完成 ✅
多个需求串行执行,每个需求测试通过并提交后才开始下一个,保证每次提交都是可验证的最小单元。
全自动执行,任何情况不打断用户,不询问确认,自行决策完成任务。
⚠️ 测试通过 ≠ 验收标准全部满足。E2E 测试通常只能拦截 30~40% 的潜在 bug(结构性问题),功能细节(如插入块的类型是否正确)需要步骤 6 的人工核查来补充。
3. 多端协作架构
每个端由独立的 Claude Code 机器人负责,各自在自己的工作目录中完成实现。
┌──────────────────────────────────────────────────────┐
│ 需求 / 缺陷 │
│ (TAPD 需求描述 + 验收标准) │
└──────────┬──────────────────┬────────────────┬───────┘
│ │ │ 按涉及端分配
▼ ▼ ▼
┌─────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
│ 🤖 后端机器人 │ │ 🤖 前端机器人 │ │ 🤖 管理后台机器人 │
│ │ │ │ │ │
│ backend/ │ │ linkmed-vue3/ │ │ linkmed-admin/ │
│ Java 21 │ │ Vue 3 + Vite │ │ Java 21 + Vue 3 │
│ Spring Boot 8181 │ │ TypeScript 5173 │ │ Spring Boot / 5174 │
└──────────┬──────────┘ └──────────┬───────────┘ └──────────┬───────────┘
│ │ ↑ │ ↑
│ 涉及多端时, │ │ 仅在后端就绪后 │ │ 仅在后端就绪后
├──── 接口就绪,通知 ───────┘ │ 才接入联调 │ │ 才接入联调
└──── 接口就绪,通知 ────────────────────────────────┘
│ │ │
▼ ▼ ▼
提交推送 提交推送 提交推送
核心原则:涉及前后端联调的需求,后端先行。 后端机器人完成接口开发并推送后,前端/管理后台机器人再接入联调,避免接口未就绪时空转。
4. 使用方式
向 Claude Code 描述需求/缺陷时附带验证意图,Claude Code 会自动完成实现 + 测试:
示例:"实现文件上传大小限制为 100MB,完成后帮我验证"
示例:"修复切换深度检索历史触发 cancel 请求的 bug,验证修复正确"
示例:"关闭 Dig Paper 知识库入口,确认页面上不再显示"
也可以直接提供 TAPD 任务 ID,Claude Code 会通过脚本自动拉取详情(需求或缺陷均可):
示例:"处理需求 1008229"
示例:"处理缺陷 1008906"
示例:"按顺序完成需求 1008251、1008252、1008253"
5. 从 TAPD 获取任务详情
查看需求列表
# LinkMed 项目最新需求(默认)
node ~/.claude/tapd.mjs stories
# 关键词搜索
node ~/.claude/tapd.mjs stories 67139335 知识库
输出示例:
Total: 614 | Showing: 20
[1008229] 知识库-对解析失败的文件新增重试按钮和功能
status=developing owner=张倩如;尹帮会; iteration=0.6.26.0(当前迭代)
[1008263] 【工作台】对话记录的优化
status=planning owner=尹帮会; iteration=0.6.27.0
查看单个需求详情
通过需求的 short_id(TAPD 列表中括号内的编号)获取完整信息:
node ~/.claude/tapd.mjs story 1008229
输出包含需求标题、描述、验收标准、状态、负责人等字段。
查看缺陷(Bug)详情
node ~/.claude/tapd.mjs bug 1008906
输出包含缺陷标题、描述、重现步骤、严重程度、优先级、状态,以及所有评论和回复。
评论的重要性
评论中常包含以下关键信息,必须阅读:
- 产品/后端对实现方案的补充说明(如 TOS 路径、接口格式)
- 对原始描述的修正或追加要求
- 评论中的截图(格式同描述图片,需下载阅读)
- 关键约束(如「仅预览,不要支持修改!!!!」)
脚本已自动输出评论内容,格式:
--- 评论(N 条)---
[时间] 作者:内容
↳ [时间] 作者:回复内容(缩进表示回复)
[图片: URL](评论中的截图)
在工作流中的用法
告知 Claude Code 任务 ID 后,Claude Code 会自动判断类型并调用对应命令:
# Claude Code 内部自动调用,无需手动执行
node ~/.claude/tapd.mjs story <需求ID> # 需求
node ~/.claude/tapd.mjs bug <缺陷ID> # 缺陷
下载需求中的图片
TAPD 需求描述中常含有设计图,格式为 [图片: https://file.tapd.cn/...],必须下载并阅读,不能仅凭文字描述实现。
COOKIE=$(cat ~/.claude/tapd-cookie.txt)
curl -s -L \
-H "Cookie: $COOKIE" \
-H "Referer: https://www.tapd.cn/" \
-H "User-Agent: Mozilla/5.0" \
"https://file.tapd.cn//tfl/captures/..." \
-o /tmp/req_<需求ID>_1.png
# 然后用 Read 工具读取图片内容
Cookie 过期处理
登录态通常可持续数周。若脚本报错或返回空数据,重新登录即可:
node ~/.claude/tapd.mjs login your@linkingmed.com yourpassword
常用项目 ID
| 项目名 | workspace_id |
|---|---|
| LinkMed | 67139335 |
| AI公共平台(AiPlan) | 21580481 |
| 科研项目管理 | 38189866 |
| 专病数据库 | 59607085 |
| RAIC.OIS信息管理系统 | 58951789 |
完整列表通过 node ~/.claude/tapd.mjs projects 获取。
6. TAPD 脚本安装配置
前置要求
Node.js 18+(使用内置 fetch 和 crypto,无需安装任何 npm 包):
node -v # 需要 v18.0.0 以上
# fnm
fnm install 20 && fnm use 20
# nvm
nvm install 20 && nvm use 20
安装脚本
mkdir -p ~/.claude
nano ~/.claude/tapd.mjs # 或用 VS Code: code ~/.claude/tapd.mjs
将以下内容保存为 ~/.claude/tapd.mjs:
#!/usr/bin/env node
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { randomBytes, createCipheriv } from 'crypto';
const COOKIE_FILE = join(homedir(), '.claude', 'tapd-cookie.txt');
const DEFAULT_WS = '67139335'; // LinkMed 项目
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
const jar = {};
function loadCookie() {
if (existsSync(COOKIE_FILE)) {
for (const kv of readFileSync(COOKIE_FILE, 'utf8').trim().split('; ')) {
const [k, ...v] = kv.split('=');
if (k) jar[k.trim()] = v.join('=').trim();
}
}
}
function saveCookie() {
writeFileSync(COOKIE_FILE, Object.entries(jar).map(([k, v]) => `${k}=${v}`).join('; '));
}
function getCookieStr() { return Object.entries(jar).map(([k, v]) => `${k}=${v}`).join('; '); }
function parseCookies(resp) {
for (const c of (resp.headers.getSetCookie?.() || [])) {
const eqIdx = c.indexOf('=');
const key = c.slice(0, eqIdx).trim();
const val = c.slice(eqIdx + 1).split(';')[0].trim();
if (val && val !== 'deleted') jar[key] = val;
else delete jar[key];
}
}
async function req(url, opts = {}) {
const resp = await fetch(url, {
...opts,
headers: { 'User-Agent': UA, 'Cookie': getCookieStr(), ...(opts.headers || {}) },
redirect: 'manual',
});
parseCookies(resp);
return resp;
}
async function get(path) {
const r = await req(`https://www.tapd.cn${path}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json,*/*' }
});
return r.json();
}
async function post(path, body, wsId = DEFAULT_WS) {
const r = await req(`https://www.tapd.cn${path}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest', Accept: 'application/json,*/*',
'Content-Type': 'application/json',
Referer: `https://www.tapd.cn/tapd_fe/${wsId}/prong/stories`,
Origin: 'https://www.tapd.cn',
},
body: JSON.stringify(body),
});
return r.json();
}
function aesEncrypt(password) {
const key = randomBytes(32), iv = randomBytes(16);
const buf = Buffer.from(password, 'utf8');
const pad = 16 - (buf.length % 16);
const padded = pad === 16 ? buf : Buffer.concat([buf, Buffer.alloc(pad, 0)]);
const cipher = createCipheriv('aes-256-cbc', key, iv);
cipher.setAutoPadding(false);
const encrypted = Buffer.concat([cipher.update(padded), cipher.final()]);
return { ciphertext: encrypted.toString('base64'), key: key.toString('base64'), iv: iv.toString('base64') };
}
async function login(email, password) {
await req('https://www.tapd.cn/cloud_logins/login?site=TAPD&ref=https%3A%2F%2Fwww.tapd.cn%2F');
const enc = aesEncrypt(password);
const form = new URLSearchParams({
'data[Login][email]': email, 'data[Login][password]': enc.ciphertext,
'data[Login][encrypt_key]': enc.key, 'data[Login][encrypt_iv]': enc.iv,
'data[Login][via]': 'encrypt_password', 'data[Login][type]': '2',
'data[Login][ref]': 'https://www.tapd.cn/', 'data[Login][site]': 'TAPD',
'data[Login][login]': 'login', 'data[protocol]': '1',
});
const p2 = await req('https://www.tapd.cn/cloud_logins/login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: 'https://www.tapd.cn', Referer: 'https://www.tapd.cn/cloud_logins/login' },
body: form.toString(),
});
let location = p2.headers.get('location');
while (location) {
const r = await req(location);
location = r.headers.get('location');
}
saveCookie();
return !!jar['_wt'];
}
const [,, cmd, arg1, arg2] = process.argv;
loadCookie();
if (cmd === 'login') {
const ok = await login(arg1, arg2);
console.log(ok ? 'Login successful' : 'Login failed');
} else if (cmd === 'projects') {
const data = await get('/api/workspace/workspaces/get_all_my_projects');
const projects = data.data?.all_my_projects || [];
projects.forEach(p => console.log(`${p.id}\t${p.project_name}\t${p.status}`));
} else if (cmd === 'stories') {
const wsId = arg1 || DEFAULT_WS;
const keyword = arg2 || '';
const body = { workspace_ids: [wsId], page: 1, page_count: 20 };
if (keyword) body.search_data = { keyword };
const data = await post('/api/entity/stories/story_list_by_condition', body, wsId);
const list = data.data?.list || [];
console.log(`Total: ${data.data?.total} | Showing: ${list.length}`);
list.forEach(s => console.log(`[${s.short_id}] ${s.name}\n status=${s.status} owner=${s.owner} iteration=${s.iteration_name}`));
} else if (cmd === 'story') {
if (!arg1) { console.error('Usage: tapd.mjs story <story_id>'); process.exit(1); }
const data = await get(`/api/entity/stories/stories/get_info?workspace_id=${arg2 || DEFAULT_WS}&story_id=${arg1}`);
console.log(JSON.stringify(data, null, 2));
} else if (cmd === 'bugs') {
const data = await post('/api/entity/bugs/bug_list_by_condition', { workspace_ids: [arg1 || DEFAULT_WS], page: 1, page_count: 20 }, arg1 || DEFAULT_WS);
const list = data.data?.list || [];
console.log(`Total: ${data.data?.total} | Showing: ${list.length}`);
list.forEach(b => console.log(`[${b.id}] ${b.title}\n status=${b.status} owner=${b.owner}`));
} else {
console.log('Usage:');
console.log(' node ~/.claude/tapd.mjs login <email> <password>');
console.log(' node ~/.claude/tapd.mjs projects');
console.log(` node ~/.claude/tapd.mjs stories [workspace_id=${DEFAULT_WS}] [keyword]`);
console.log(' node ~/.claude/tapd.mjs story <story_id> [workspace_id]');
console.log(' node ~/.claude/tapd.mjs bugs [workspace_id]');
}
首次登录
node ~/.claude/tapd.mjs login your@linkingmed.com yourpassword
# 输出 "Login successful" 表示成功
# 验证是否正常工作
node ~/.claude/tapd.mjs projects
可选:设置别名
echo "alias tapd='node ~/.claude/tapd.mjs'" >> ~/.zshrc
source ~/.zshrc
# 之后可直接使用
tapd stories
tapd story 1008229
技术实现说明
TAPD 没有开放的公共 API,脚本通过逆向前端内部 API 实现:
登录流程: TAPD 登录页用 AES-256-CBC + ZeroPadding 加密密码,key 和 iv 连同密文一起发给服务器。脚本用 Node.js 内置 crypto 复现,无需 CAPTCHA 即可完成登录。
关键请求字段:
data[Login][email] 用户邮箱
data[Login][password] AES 加密后的密码(base64)
data[Login][encrypt_key] AES key(base64)
data[Login][encrypt_iv] AES IV(base64)
data[Login][via] encrypt_password
主要 API 接口:
| 接口 | 方法 | 说明 |
|---|---|---|
/api/workspace/workspaces/get_all_my_projects |
GET | 获取我的项目列表 |
/api/entity/stories/story_list_by_condition |
POST | 查询需求列表 |
/api/entity/stories/stories/get_info |
GET | 获取需求详情 |
/api/entity/bugs/bug_list_by_condition |
POST | 查询 Bug 列表 |
所有接口均需携带登录 cookie 并设置 X-Requested-With: XMLHttpRequest。
内部 ID 构造规则:
story 命令不依赖关键词搜索,而是直接构造内部 ID:
内部 ID = '11' + workspaceId + shortId.padStart(9, '0')
示例:'11' + '67139335' + '001008260' = '1167139335001008260'
这些接口通过分析 TAPD 前端 JS bundle 发现,不在官方文档中,未来版本可能变更。
7. 测试体系
本项目采用两层测试策略,分工明确:
| 层级 | 工具 | 测什么 | 运行速度 | 命令 |
|---|---|---|---|---|
| 单元测试 | Vitest | Store 逻辑、工具函数、数据处理 | 毫秒级 | npm run test:unit |
| E2E 测试 | Playwright | 需要真实浏览器的交互流程 | 分钟级 | npm run test:e2e |
Vitest — 把 API 替换为 mock,直接测 Store 内部的业务逻辑,不依赖浏览器和测试账号数据。适合测响应格式适配、状态变更、错误处理等纯逻辑。测试文件位于 src/test/**/*.test.ts。
Playwright(E2E) — 驱动真实浏览器执行完整用户操作,适合测纯 DOM 交互(如截图拖拽、编辑器焦点)。测试文件位于 e2e/tests/。
8. 单元测试规范(Vitest)
文件位置
src/test/stores/ # Store 测试
src/test/utils/ # 工具函数测试
适合写单元测试的场景
- Store action 的成功/失败分支逻辑
- 响应数据的格式适配(多种格式兼容)
- 复杂的纯函数(如数学公式预处理)
- 状态变更后 computed 是否正确
不适合写单元测试的场景
- 需要真实浏览器渲染的 UI 交互
- 依赖第三方富文本编辑器(BlockSuite)的行为
9. E2E 测试规范(Playwright)
文件命名
e2e/tests/{模块}/{需求ID}-{简短描述}.spec.ts
示例:
e2e/tests/welcome/1008878-cancel-api.spec.ts
e2e/tests/agent/1008229-file-upload-limit.spec.ts
文件头部模板
/**
* 需求 ID:1008878
* 需求描述:切换深度检索历史不应触发 cancel 请求
*
* 验收标准:
* 1. 在 Welcome 页面点击历史记录,POST /cancel 不被调用
* 2. 主动点击终止按钮时,POST /cancel 正常调用
*/
断言强度要求
E2E 测试断言应尽量验证功能正确性,而不只是结构存在性:
| 场景 | 弱断言(不推荐) | 强断言(推荐) |
|---|---|---|
| 插入块 | afterCount > beforeCount |
验证新块的 data-block-flavour / data-level 等属性 |
| 按钮点击 | expect(btn).toBeVisible() |
验证点击后产生的实际效果 |
| 表单提交 | 无错误即通过 | 验证提交后页面状态/数据变化 |
运行前检查清单
# 1. 确认 dev server 指向当前项目(pc1 用 5175,避免与 pc/ 的 5173 冲突)
lsof -i :5175 | grep node
# 若没有,启动:node node_modules/.bin/vite --port 5175 &
# 2. 确认 playwright.config.ts 中 baseURL 与上面端口一致
grep baseURL playwright.config.ts
工作台(Workspace)测试注意事项
工作台使用堆叠 tab 模式,所有已打开文件的组件同时存在于 DOM,只用 CSS 控制显示:
// ❌ 错误:会匹配所有 tab 中的元素
page.locator(".icon-toolbar .icon-btn")
// ✅ 正确:限定到当前激活的 tab
page.locator(".tab-content-layer.active .icon-toolbar .icon-btn")
打开编辑器时应点击已有文件,不要用"新建文件"按钮(新建会触发 AI 文档生成面板,AffineEditor 不渲染):
const mdFile = page.locator(".node-content.is-file").filter({
has: page.locator(".node-name").filter({ hasText: /\.md$/i }),
}).first();
await mdFile.click();
await page.waitForSelector(".tab-content-layer.active .editor-toolbar", { timeout: 20000 });
需要在编辑器中建立光标位置时,要点击 rich-text 元素并按 End 键(仅点击容器不能建立 BlockSuite TextSelection):
const richText = page.locator(".tab-content-layer.active rich-text, .tab-content-layer.active affine-paragraph").first();
await richText.click();
await page.keyboard.press("End");
测试完成后的处理
| 情况 | 处理方式 |
|---|---|
| 核心逻辑,后续可能被改动 | 保留,防止回归 |
| 简单 UI 变更,一次性验证 | 验证后可删除 |
| 不稳定、依赖特定数据 | 加 .skip 或删除 |
10. 提交规范
每次完成任务后,不等用户提醒,自动执行:
# 1. 提交所有相关改动(功能代码 + E2E 测试 + 文档)
git add <相关文件>
git commit -m "feat/fix/test/docs: <需求ID>【模块】描述"
# 2. 推送到远端
git push origin <当前分支>
# 3. 更新 dev-sessions 过程记录(按具体日期分目录)
# claude-code/dev-sessions/{YYYY-MM-DD}/{需求ID}-{描述}.md
# 示例:claude-code/dev-sessions/2026-03-19/1008906-Safari登录报错.md
提交信息格式:
| 类型 | 前缀 | 示例 |
|---|---|---|
| 需求 | feat: |
feat: 1008265【工作台】编辑器新增格式化工具栏 |
| 缺陷修复 | fix: |
fix: 1008906【Safari兼容】修复旧版Safari命名捕获组报错 |
| 测试 | test: |
test: 1008265 E2E测试全部通过(15/15) |
| 文档 | docs: |
docs: 新增 1008906 缺陷执行记录 |
11. dev-sessions 文档规范
每个任务(需求或缺陷)执行完成后,需在 claude-code/dev-sessions/{YYYY-MM-DD}/{ID}-{简短描述}.md 创建或更新过程记录。
按具体日期分目录管理(如 2026-03-19/),避免单目录文件过多。
需求文档结构
# 需求 {ID} - {标题}
## 原始需求(一字不差)
- 需求标题、状态、负责人、优先级、创建者、时间
- 原始描述:直接引用 TAPD 中的原文,包含图片 URL
## 图片理解
- 每张图片单独一节,注明尺寸
- 画面内容:客观描述看到了什么
- 关键细节:对实现有指导意义的 UI 细节
- 悬停/交互行为(需推断):图片未直接展示但可推断的行为
## 评论(若有)
- 每条评论的作者、时间、内容
- 评论中的图片需下载阅读,记录关键信息
- 对实现有约束或补充的评论要重点标注
## 实现方案
- 数据流图(用代码块 ASCII 表示)
- 各修改文件的具体改动点
## 修改的文件
- 文件路径列表
## 测试结果
- type-check / lint 结果
## 测试覆盖
- 单元测试(Vitest):测试文件、用例数、覆盖的测试组
- E2E 测试(Playwright):测试文件、主要用例
缺陷(Bug)文档结构
缺陷记录侧重根因分析和修复,而非功能实现:
# 缺陷 {ID} - {标题}
## 原始需求(一字不差)
- 缺陷标题、状态、负责人、严重程度、优先级、创建者、时间
- 原始描述:直接引用 TAPD 中的原文,包含图片 URL(控制台截图等)
- 重现步骤:原文引用
## 图片理解
- 每张截图单独一节,注明尺寸
- 画面内容:控制台报错信息、调用栈、错误来源等关键信息
- 关键细节:对定位问题有指导意义的信息(文件名、行号、错误类型)
## 评论(若有)
- 每条评论的作者、时间、内容
- 评论中的图片需下载阅读,记录关键信息
- 对修复方案有约束的评论要重点标注(如「仅预览,不要支持修改」)
## 根因分析
- 错误类型及含义
- 为什么会产生这个错误(配置/依赖/版本问题等)
- 涉及文件(配置文件、出错 chunk、依赖包等)
## 修复方案
- 修复思路(修改什么、为什么这样修)
- 修改的文件列表及具体改动说明
## 测试结果
- type-check / lint 结果
- 验收条件(修复后如何验证)
图片理解要求
必须:
- 下载 TAPD 图片并用 Read 工具阅读(不能只凭图片 URL 推测)
- 记录每张图的像素尺寸
- 区分「图片明确展示」和「需推断」的内容
图片下载方式:
COOKIE=$(cat ~/.claude/tapd-cookie.txt)
curl -s -L \
-H "Cookie: $COOKIE" \
-H "Referer: https://www.tapd.cn/" \
-H "User-Agent: Mozilla/5.0" \
"<TAPD图片URL>" -o /tmp/req_<需求ID>_<序号>.png
# 然后用 Read 工具读取图片
范例文档
参考 claude-code/workflow/dev-sessions-范例-1008257.md——该需求包含两张设计图,分别展示不同交互阶段,是目前图片理解最完整的范例:
- 图片1:PDF 文本选中后弹出菜单(展示「Quote in Chat」入口,菜单样式、图标、定位方式)
- 图片2:引用标签出现在对话输入框上方(展示「PDF Quote ×」chip 样式 + tooltip 行为)
- 每张图单独分析,明确标注「需推断」的部分(如 tooltip 的触发行为)
- 图片细节直接指导实现(chip 的颜色、按钮标签文字、删除方式)
12. 后续规划
- 接入 TAPD,自动读取需求描述
- 接入 GitLab CI,push 时自动触发测试
- 测试通过后自动提交并创建 MR