|
|
|
# LinkMed Claude Code 自动研发使用指南
|
|
|
|
|
|
|
|
借助 Claude Code 实现从需求描述到代码实现、自动验证的完整研发流程,减少人工手动验证环节。
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
## 目录
|
|
|
|
|
|
|
|
1. [快速开始](#1-快速开始)
|
|
|
|
2. [整体工作流程](#2-整体工作流程)
|
|
|
|
3. [多端协作架构](#3-多端协作架构)
|
|
|
|
4. [使用方式](#4-使用方式)
|
|
|
|
5. [从 TAPD 获取任务详情](#5-从-tapd-获取任务详情)
|
|
|
|
6. [TAPD 脚本安装配置](#6-tapd-脚本安装配置)
|
|
|
|
7. [测试体系](#7-测试体系)
|
|
|
|
8. [单元测试规范(Vitest)](#8-单元测试规范vitest)
|
|
|
|
9. [E2E 测试规范(Playwright)](#9-e2e-测试规范playwright)
|
|
|
|
10. [提交规范](#10-提交规范)
|
|
|
|
11. [dev-sessions 文档规范](#11-dev-sessions-文档规范)
|
|
|
|
12. [后续规划](#12-后续规划)
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
## 1. 快速开始
|
|
|
|
|
|
|
|
### 启动方式
|
|
|
|
|
|
|
|
```bash
|
|
|
|
# 普通模式(每步操作需手动确认)
|
|
|
|
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 获取任务详情
|
|
|
|
|
|
|
|
### 查看需求列表
|
|
|
|
|
|
|
|
```bash
|
|
|
|
# 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 列表中括号内的编号)获取完整信息:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
node ~/.claude/tapd.mjs story 1008229
|
|
|
|
```
|
|
|
|
|
|
|
|
输出包含需求标题、描述、验收标准、状态、负责人等字段。
|
|
|
|
|
|
|
|
### 查看缺陷(Bug)详情
|
|
|
|
|
|
|
|
```bash
|
|
|
|
node ~/.claude/tapd.mjs bug 1008906
|
|
|
|
```
|
|
|
|
|
|
|
|
输出包含缺陷标题、描述、重现步骤、严重程度、优先级、状态,以及**所有评论和回复**。
|
|
|
|
|
|
|
|
### 评论的重要性
|
|
|
|
|
|
|
|
评论中常包含以下关键信息,**必须阅读**:
|
|
|
|
- 产品/后端对实现方案的补充说明(如 TOS 路径、接口格式)
|
|
|
|
- 对原始描述的修正或追加要求
|
|
|
|
- 评论中的截图(格式同描述图片,需下载阅读)
|
|
|
|
- 关键约束(如「仅预览,不要支持修改!!!!」)
|
|
|
|
|
|
|
|
脚本已自动输出评论内容,格式:
|
|
|
|
```
|
|
|
|
--- 评论(N 条)---
|
|
|
|
[时间] 作者:内容
|
|
|
|
↳ [时间] 作者:回复内容(缩进表示回复)
|
|
|
|
[图片: URL](评论中的截图)
|
|
|
|
```
|
|
|
|
|
|
|
|
### 在工作流中的用法
|
|
|
|
|
|
|
|
告知 Claude Code 任务 ID 后,Claude Code 会自动判断类型并调用对应命令:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
# Claude Code 内部自动调用,无需手动执行
|
|
|
|
node ~/.claude/tapd.mjs story <需求ID> # 需求
|
|
|
|
node ~/.claude/tapd.mjs bug <缺陷ID> # 缺陷
|
|
|
|
```
|
|
|
|
|
|
|
|
### 下载需求中的图片
|
|
|
|
|
|
|
|
TAPD 需求描述中常含有设计图,格式为 `[图片: https://file.tapd.cn/...]`,**必须下载并阅读**,不能仅凭文字描述实现。
|
|
|
|
|
|
|
|
```bash
|
|
|
|
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 过期处理
|
|
|
|
|
|
|
|
登录态通常可持续数周。若脚本报错或返回空数据,重新登录即可:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
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 包):
|
|
|
|
|
|
|
|
```bash
|
|
|
|
node -v # 需要 v18.0.0 以上
|
|
|
|
```
|
|
|
|
|
|
|
|
如果版本过低,可通过 [fnm](https://github.com/Schniz/fnm) 或 [nvm](https://github.com/nvm-sh/nvm) 升级:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
# fnm
|
|
|
|
fnm install 20 && fnm use 20
|
|
|
|
|
|
|
|
# nvm
|
|
|
|
nvm install 20 && nvm use 20
|
|
|
|
```
|
|
|
|
|
|
|
|
### 安装脚本
|
|
|
|
|
|
|
|
```bash
|
|
|
|
mkdir -p ~/.claude
|
|
|
|
nano ~/.claude/tapd.mjs # 或用 VS Code: code ~/.claude/tapd.mjs
|
|
|
|
```
|
|
|
|
|
|
|
|
将以下内容保存为 `~/.claude/tapd.mjs`:
|
|
|
|
|
|
|
|
```js
|
|
|
|
#!/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]');
|
|
|
|
}
|
|
|
|
```
|
|
|
|
|
|
|
|
### 首次登录
|
|
|
|
|
|
|
|
```bash
|
|
|
|
node ~/.claude/tapd.mjs login your@linkingmed.com yourpassword
|
|
|
|
# 输出 "Login successful" 表示成功
|
|
|
|
|
|
|
|
# 验证是否正常工作
|
|
|
|
node ~/.claude/tapd.mjs projects
|
|
|
|
```
|
|
|
|
|
|
|
|
### 可选:设置别名
|
|
|
|
|
|
|
|
```bash
|
|
|
|
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
|
|
|
|
```
|
|
|
|
|
|
|
|
### 文件头部模板
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
/**
|
|
|
|
* 需求 ID:1008878
|
|
|
|
* 需求描述:切换深度检索历史不应触发 cancel 请求
|
|
|
|
*
|
|
|
|
* 验收标准:
|
|
|
|
* 1. 在 Welcome 页面点击历史记录,POST /cancel 不被调用
|
|
|
|
* 2. 主动点击终止按钮时,POST /cancel 正常调用
|
|
|
|
*/
|
|
|
|
```
|
|
|
|
|
|
|
|
### 断言强度要求
|
|
|
|
|
|
|
|
E2E 测试断言应尽量验证**功能正确性**,而不只是**结构存在性**:
|
|
|
|
|
|
|
|
| 场景 | 弱断言(不推荐) | 强断言(推荐) |
|
|
|
|
|------|----------------|--------------|
|
|
|
|
| 插入块 | `afterCount > beforeCount` | 验证新块的 `data-block-flavour` / `data-level` 等属性 |
|
|
|
|
| 按钮点击 | `expect(btn).toBeVisible()` | 验证点击后产生的实际效果 |
|
|
|
|
| 表单提交 | 无错误即通过 | 验证提交后页面状态/数据变化 |
|
|
|
|
|
|
|
|
### 运行前检查清单
|
|
|
|
|
|
|
|
```bash
|
|
|
|
# 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 控制显示:
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
// ❌ 错误:会匹配所有 tab 中的元素
|
|
|
|
page.locator(".icon-toolbar .icon-btn")
|
|
|
|
|
|
|
|
// ✅ 正确:限定到当前激活的 tab
|
|
|
|
page.locator(".tab-content-layer.active .icon-toolbar .icon-btn")
|
|
|
|
```
|
|
|
|
|
|
|
|
打开编辑器时应点击**已有文件**,不要用"新建文件"按钮(新建会触发 AI 文档生成面板,AffineEditor 不渲染):
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
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):
|
|
|
|
|
|
|
|
```typescript
|
|
|
|
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. 提交规范
|
|
|
|
|
|
|
|
每次完成任务后,**不等用户提醒**,自动执行:
|
|
|
|
|
|
|
|
```bash
|
|
|
|
# 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 推测)
|
|
|
|
- 记录每张图的像素尺寸
|
|
|
|
- 区分「图片明确展示」和「需推断」的内容
|
|
|
|
|
|
|
|
**图片下载方式**:
|
|
|
|
```bash
|
|
|
|
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. 后续规划
|
|
|
|
|
|
|
|
- [x] 接入 TAPD,自动读取需求描述
|
|
|
|
- [ ] 接入 GitLab CI,push 时自动触发测试
|
|
|
|
- [ ] 测试通过后自动提交并创建 MR |
...
|
...
|
|