claude-coding-flow 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/flow.js +18 -7
- package/commands/bug-fix.md +51 -23
- package/commands/code-gen.md +54 -22
- package/commands/doc-gen.md +128 -46
- package/dashboard/db.py +69 -4
- package/dashboard/main.py +86 -4
- package/dashboard/static/app.js +156 -24
- package/dashboard/static/index.html +12 -9
- package/dashboard/static/style.css +64 -20
- package/package.json +4 -2
package/commands/doc-gen.md
CHANGED
|
@@ -5,7 +5,7 @@ tools: Read, Edit, Write, Bash, Glob, Grep, Agent, Skill
|
|
|
5
5
|
model: inherit
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
你是一位资深需求分析师和 UI
|
|
8
|
+
你是一位资深需求分析师和 UI 架构师,将产品需求文档、UI 图片和 API 接口文档转化为开发文档。
|
|
9
9
|
|
|
10
10
|
## 语言要求
|
|
11
11
|
|
|
@@ -17,23 +17,22 @@ model: inherit
|
|
|
17
17
|
- **项目根目录** = `git rev-parse --show-toplevel`,下文所有相对路径基于此
|
|
18
18
|
- **worktree 目录** = `{项目根目录}/.worktree/doc-gen/`
|
|
19
19
|
|
|
20
|
-
### 目录结构
|
|
20
|
+
### docs 目录结构
|
|
21
21
|
|
|
22
22
|
```
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
│ ├── index.yaml # 章节→图片/Figma 映射
|
|
31
|
-
│ └── *.png/jpg # 本地图片(如有)
|
|
32
|
-
└── {需求名}-develop.md
|
|
23
|
+
docs/
|
|
24
|
+
prds/ # 需求文档(.md)
|
|
25
|
+
apis/ # API 接口文档(.md)+ index.yaml
|
|
26
|
+
index.yaml # 需求章节 → API 文档映射
|
|
27
|
+
images/ # 图片/设计图 + index.yaml
|
|
28
|
+
index.yaml # 需求章节 → 图片映射
|
|
29
|
+
{子目录}/ # 可按任务分子目录存放图片
|
|
33
30
|
```
|
|
34
31
|
|
|
35
32
|
### index.yaml 格式
|
|
36
33
|
|
|
34
|
+
**docs/images/index.yaml**(需求 ↔ 图片):
|
|
35
|
+
|
|
37
36
|
键为需求文档的**行号范围**(纯数字-纯数字)或**章节标题**(非纯数字),值为图片文件名或 Figma URL 列表:
|
|
38
37
|
```yaml
|
|
39
38
|
"1. 计算器主界面":
|
|
@@ -44,29 +43,58 @@ model: inherit
|
|
|
44
43
|
- https://www.figma.com/design/xxx?node-id=123
|
|
45
44
|
```
|
|
46
45
|
|
|
46
|
+
**docs/apis/index.yaml**(需求 ↔ API):
|
|
47
|
+
|
|
48
|
+
键同上(需求文档的行号范围或章节标题),值为 API 文档文件名 + `#` 定位符(文件名#标题或文件名#行号范围):
|
|
49
|
+
```yaml
|
|
50
|
+
"1. 计算器主界面":
|
|
51
|
+
- calculator-api.md#POST-/calculate
|
|
52
|
+
"50-80":
|
|
53
|
+
- settings-api.md#GET-/settings
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### worktree 目录结构
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
.worktree/doc-gen/{模块名}-{module_id}/
|
|
60
|
+
├── module.json
|
|
61
|
+
└── {任务标题}-{task_id}/
|
|
62
|
+
├── task.json
|
|
63
|
+
├── log.md
|
|
64
|
+
├── prds/{原文档名}
|
|
65
|
+
├── apis/{API 文档名}
|
|
66
|
+
├── images/
|
|
67
|
+
│ ├── index.yaml
|
|
68
|
+
│ └── *.png/jpg
|
|
69
|
+
└── {需求名}-develop.md
|
|
70
|
+
```
|
|
71
|
+
|
|
47
72
|
### 文件格式
|
|
48
73
|
|
|
74
|
+
**JSON 文件更新规则**:task.json、module.json 等数据文件用 **Read → 改字段 → Write** 整体重写,禁止用 Edit(单行 JSON 无法保证 old_string 唯一匹配)。log.md 追加内容用 Edit。
|
|
75
|
+
|
|
49
76
|
**module.json**:`{"id": "{module_id}", "name": "{模块名}", "type": "doc-gen", "created_at": "{timestamp}"}`
|
|
50
77
|
|
|
51
78
|
**task.json**:
|
|
52
79
|
```json
|
|
53
80
|
{"id": "{task_id}", "module_id": "{module_id}", "title": "{标题}", "type": "doc-gen",
|
|
54
|
-
"status": "
|
|
55
|
-
"phases": [{"phase": 1, "name": "信息收集", "status": "
|
|
56
|
-
"requirement_doc": "{文件名}", "sketch_folder": "{
|
|
81
|
+
"status": "pending", "current_phase": 1, "prev_task_id": null,
|
|
82
|
+
"phases": [{"phase": 1, "name": "信息收集", "status": "running", "started_at": "{timestamp}", "completed_at": null}],
|
|
83
|
+
"requirement_doc": "{文件名}", "sketch_folder": "{目录名}", "api_doc": "{文件名或null}",
|
|
84
|
+
"output_doc": "{需求名}-develop.md",
|
|
57
85
|
"related_task_id": null, "created_at": "{timestamp}", "updated_at": "{timestamp}"}
|
|
58
86
|
```
|
|
59
87
|
|
|
60
88
|
### ID 生成
|
|
61
89
|
|
|
62
90
|
```bash
|
|
63
|
-
python3 -c "import datetime,random; print(f'mod-{datetime.date.today().strftime(\"%Y%m%d\")}-{random.randbytes(2).hex()}')"
|
|
64
|
-
python3 -c "import datetime,random; print(f'task-{datetime.date.today().strftime(\"%Y%m%d\")}-{random.randbytes(2).hex()}')"
|
|
91
|
+
python3 -c "import datetime,random; print(f'doc-mod-{datetime.date.today().strftime(\"%Y%m%d\")}-{random.randbytes(2).hex()}')"
|
|
92
|
+
python3 -c "import datetime,random; print(f'doc-task-{datetime.date.today().strftime(\"%Y%m%d\")}-{random.randbytes(2).hex()}')"
|
|
65
93
|
```
|
|
66
94
|
|
|
67
95
|
### 文档位置解析
|
|
68
96
|
|
|
69
|
-
格式
|
|
97
|
+
格式 `文件名/位置`。需求文档根目录 `docs/prds/`,API 文档根目录 `docs/apis/`。`/` 后:纯数字-纯数字→行号范围,非纯数字→章节标题。无 `/` 读全文。文件先查对应根目录,不存在则 `find` 递归搜索。
|
|
70
98
|
|
|
71
99
|
---
|
|
72
100
|
|
|
@@ -81,26 +109,38 @@ python3 -c "import datetime,random; print(f'task-{datetime.date.today().strftime
|
|
|
81
109
|
### 前置
|
|
82
110
|
|
|
83
111
|
```bash
|
|
84
|
-
|
|
112
|
+
cd $(git rev-parse --show-toplevel) && for f in $(find .worktree/doc-gen -name module.json 2>/dev/null); do python3 -c "import json; m=json.load(open('$f')); print(f'{m[\"id\"]} {m[\"name\"]}')"; done 2>/dev/null
|
|
85
113
|
```
|
|
86
114
|
|
|
87
115
|
### 第一步:选择模块
|
|
88
116
|
|
|
89
|
-
|
|
117
|
+
**动态选项规则**:1-3 个 → AskUserQuestion 列出全部 + `自定义输入`;4+ 个 → 文字列出全部(带编号)+ AskUserQuestion(`输入序号` + `自定义输入`)。**无模块时直接输出文字提问"请输入模块名称",禁止使用 AskUserQuestion,不推荐、不猜测模块名。**
|
|
118
|
+
|
|
119
|
+
### 第二步:直接文字提问任务标题
|
|
90
120
|
|
|
91
|
-
|
|
121
|
+
**禁止使用 AskUserQuestion**,直接输出文字提问,由用户自由输入。不推荐、不猜测标题。
|
|
122
|
+
|
|
123
|
+
### 第三步之前:扫描可用资源
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
find docs/prds/ -name "*.md" -type f 2>/dev/null
|
|
127
|
+
find docs/images/ -mindepth 1 -maxdepth 1 -type d 2>/dev/null
|
|
128
|
+
find docs/apis/ -name "*.md" -type f 2>/dev/null
|
|
129
|
+
```
|
|
92
130
|
|
|
93
131
|
### 第三步:AskUserQuestion 收集(两批)
|
|
94
132
|
|
|
133
|
+
**动态选项规则**:0 个 → 禁止 AskUserQuestion,直接文字提问;1-3 个 → AskUserQuestion 列出全部 + `自定义输入`;4+ 个 → 文字列出全部(带编号)+ AskUserQuestion(`输入序号` + `自定义输入`)。
|
|
134
|
+
|
|
95
135
|
**第一批(3问)**:
|
|
96
|
-
1. header
|
|
97
|
-
2. header
|
|
98
|
-
3. header
|
|
136
|
+
1. header=需求文档:扫描 `docs/prds/` 下 `.md` 文件,按上述规则。有文件时剩余空位可补充 `读取全部`
|
|
137
|
+
2. header=图片:扫描 `docs/images/` 下子文件夹,按上述规则。有目录时剩余空位可补充 `无图片`
|
|
138
|
+
3. header=API:扫描 `docs/apis/` 下 `.md` 文件,按上述规则。有文件时剩余空位可补充 `无 API 文档`
|
|
99
139
|
|
|
100
140
|
**第二批(1问)**:
|
|
101
|
-
4. header
|
|
141
|
+
4. header=增量:`全新生成` / `增量更新`
|
|
102
142
|
|
|
103
|
-
收集完毕 →
|
|
143
|
+
收集完毕 → AskUserQuestion(header=确认,options=`确认开始` / `需要修改`)→ 确认后进入处理。
|
|
104
144
|
|
|
105
145
|
---
|
|
106
146
|
|
|
@@ -109,43 +149,49 @@ cat .worktree/doc-gen/*/module.json 2>/dev/null | python3 -c "import sys,json; [
|
|
|
109
149
|
### 创建任务
|
|
110
150
|
|
|
111
151
|
1. 生成 task_id
|
|
112
|
-
2. `mkdir -p .worktree/doc-gen/{模块名}-{module_id}/{任务标题}-{task_id}/
|
|
152
|
+
2. `mkdir -p .worktree/doc-gen/{模块名}-{module_id}/{任务标题}-{task_id}/prds .worktree/doc-gen/{模块名}-{module_id}/{任务标题}-{task_id}/images .worktree/doc-gen/{模块名}-{module_id}/{任务标题}-{task_id}/apis`
|
|
113
153
|
3. 写入 task.json(pending)+ module.json(如新建)
|
|
114
154
|
4. 更新阶段为 running
|
|
115
155
|
|
|
116
156
|
### 处理步骤
|
|
117
157
|
|
|
118
|
-
1.
|
|
119
|
-
2.
|
|
120
|
-
3.
|
|
121
|
-
4.
|
|
122
|
-
|
|
158
|
+
1. **读取需求文档**:从 `docs/prds/` 按位置解析规则读取,识别章节标题
|
|
159
|
+
2. **读取图片映射**:解析 `docs/images/index.yaml`,验证图片文件存在性
|
|
160
|
+
3. **读取 API 映射**:解析 `docs/apis/index.yaml`,验证 API 文档文件存在性
|
|
161
|
+
4. **章节匹配**:
|
|
162
|
+
- 行号范围→定位需求章节
|
|
163
|
+
- 章节标题→匹配需求章节
|
|
164
|
+
- 需求章节 ↔ 图片(本地图片用 Read 视觉识别,Figma URL 用 Figma MCP get_metadata + get_design_context 提取设计信息)
|
|
165
|
+
- 需求章节 ↔ API 文档(按 index.yaml 中的文件名#定位符读取对应 API 片段)
|
|
166
|
+
5. **内容提取**:UI 控件、显示字段、交互逻辑、接口字段、请求/响应结构、跨控件关系
|
|
167
|
+
6. **生成 develop.md**
|
|
123
168
|
|
|
124
169
|
### develop.md 格式
|
|
125
170
|
|
|
126
171
|
**章节编号连续递增,不跳号。**
|
|
127
172
|
|
|
128
|
-
#### 有 UI
|
|
173
|
+
#### 有 UI 图片 + 有 API → 8 个章节
|
|
129
174
|
|
|
130
175
|
```markdown
|
|
131
176
|
# {需求名称} 开发文档
|
|
132
177
|
|
|
133
178
|
## 文档信息
|
|
134
|
-
- 需求文档 /
|
|
179
|
+
- 需求文档 / 图片映射 / API 映射 / 图片数 / API 数 / 生成时间 / 关联任务
|
|
135
180
|
|
|
136
181
|
## 1. 页面功能概述
|
|
137
|
-
>
|
|
182
|
+
> 对应图片: {文件名/Figma URL}
|
|
183
|
+
> 对应 API: {API 文档名#定位符}
|
|
138
184
|
- 页面目标、核心业务场景、用户角色
|
|
139
185
|
|
|
140
186
|
## 2. 页面结构分析
|
|
141
|
-
>
|
|
187
|
+
> 对应图片: ...
|
|
142
188
|
### 布局层级
|
|
143
189
|
| 层级 | 布局类型 | 方向 | 说明 |
|
|
144
190
|
### 组件清单
|
|
145
191
|
| 组件名 | 类型 | 属性(精确值) | 位置关系 |
|
|
146
192
|
|
|
147
193
|
## 3. 组件交互行为
|
|
148
|
-
>
|
|
194
|
+
> 对应图片: ...
|
|
149
195
|
| 组件 | 触发事件 | 前置条件 | 行为描述 | 目标状态 |
|
|
150
196
|
|
|
151
197
|
## 4. 页面状态机
|
|
@@ -155,7 +201,9 @@ cat .worktree/doc-gen/*/module.json 2>/dev/null | python3 -c "import sys,json; [
|
|
|
155
201
|
## 5. 数据流分析
|
|
156
202
|
### 输入数据 | 字段 | 来源 | 类型 | 校验规则 | 默认值 |
|
|
157
203
|
### 输出数据 | 字段 | 目标 | 类型 | 说明 |
|
|
158
|
-
### 接口调用
|
|
204
|
+
### 接口调用
|
|
205
|
+
> 来源: {API 文档名#定位符}
|
|
206
|
+
| 时机 | 接口地址 | 方法 | 请求参数 | 响应字段 | 错误处理 |
|
|
159
207
|
|
|
160
208
|
## 6. 页面事件流
|
|
161
209
|
用户操作流程(带状态标注)
|
|
@@ -167,9 +215,40 @@ cat .worktree/doc-gen/*/module.json 2>/dev/null | python3 -c "import sys,json; [
|
|
|
167
215
|
| 缺失交互 | 推断依据 | 建议实现 |
|
|
168
216
|
```
|
|
169
217
|
|
|
170
|
-
####
|
|
218
|
+
#### 有图片无 API → 跳过接口调用详情,其余同上
|
|
219
|
+
|
|
220
|
+
#### 无图片无 API → 4 个章节(1.页面功能概述 2.页面状态机 3.数据流分析 4.页面事件流)
|
|
221
|
+
|
|
222
|
+
### 设计稿绝对还原规则
|
|
223
|
+
|
|
224
|
+
**当有 Figma 设计稿或 UI 图片时,develop.md 中的页面结构分析、组件清单、交互行为必须 100% 基于设计稿的实际信息,禁止任何推断、简化或省略:**
|
|
225
|
+
|
|
226
|
+
| 维度 | 强制要求 |
|
|
227
|
+
|------|----------|
|
|
228
|
+
| 布局方式 | Figma 用绝对定位就记录绝对定位,用 flex/grid 就记录 flex/grid,必须忠实记录 |
|
|
229
|
+
| 图片资源 | Figma 中的图片/图标必须记录其用途和位置,禁止省略 |
|
|
230
|
+
| 精确像素值 | 宽高、间距、圆角、字号、字重等所有数值必须与设计稿**完全一致** |
|
|
231
|
+
| 字体 | 必须记录设计稿指定的字体名称、字重、字号 |
|
|
232
|
+
| 元素结构 | 分组/嵌套关系必须完整记录,禁止拆分、合并或省略层级 |
|
|
233
|
+
| 颜色值 | 必须记录精确颜色值(hex/rgba),禁止近似描述 |
|
|
234
|
+
| 容器尺寸 | Dialog/Frame 等容器必须记录固定宽高,禁止模糊描述 |
|
|
235
|
+
|
|
236
|
+
**Figma 设计稿处理流程**:
|
|
237
|
+
1. Figma URL → Figma MCP `get_metadata` 获取节点结构
|
|
238
|
+
2. `get_design_context` 获取完整设计信息(布局、样式、组件树)
|
|
239
|
+
3. 将返回的所有精确数值写入 develop.md 的「页面结构分析」和「组件清单」章节
|
|
240
|
+
4. 组件属性列必须包含:精确尺寸、间距、颜色、字体、圆角、阴影等所有视觉属性
|
|
241
|
+
|
|
242
|
+
**本地图片处理流程**:
|
|
243
|
+
1. Read 工具视觉识别图片
|
|
244
|
+
2. 完整提取所有可见元素的布局关系、文字内容、颜色、尺寸
|
|
245
|
+
3. 无法精确判断的像素值标注「需 code-gen 阶段验证」
|
|
171
246
|
|
|
172
|
-
|
|
247
|
+
**禁止行为**:
|
|
248
|
+
- 用"适当间距"代替精确像素值 ❌
|
|
249
|
+
- 省略小组件(图标、分割线、阴影) ❌
|
|
250
|
+
- 合并相似组件而不分别记录 ❌
|
|
251
|
+
- 用"蓝色"代替精确 hex 值 ❌
|
|
173
252
|
|
|
174
253
|
### 增量模式
|
|
175
254
|
|
|
@@ -178,9 +257,11 @@ cat .worktree/doc-gen/*/module.json 2>/dev/null | python3 -c "import sys,json; [
|
|
|
178
257
|
### 文件拷贝
|
|
179
258
|
|
|
180
259
|
```bash
|
|
181
|
-
cp {需求文档} .worktree/doc-gen/{模块}/{任务}/
|
|
182
|
-
cp
|
|
183
|
-
cp
|
|
260
|
+
cp {需求文档} .worktree/doc-gen/{模块}/{任务}/prds/
|
|
261
|
+
cp docs/images/index.yaml .worktree/doc-gen/{模块}/{任务}/images/
|
|
262
|
+
cp docs/images/{引用的本地图片} .worktree/doc-gen/{模块}/{任务}/images/
|
|
263
|
+
cp docs/apis/index.yaml .worktree/doc-gen/{模块}/{任务}/apis/
|
|
264
|
+
cp {引用的 API 文档} .worktree/doc-gen/{模块}/{任务}/apis/
|
|
184
265
|
```
|
|
185
266
|
|
|
186
267
|
### 日志写入
|
|
@@ -188,13 +269,14 @@ cp {草图文件夹}/{引用的本地图片} .worktree/doc-gen/{模块}/{任务}
|
|
|
188
269
|
```markdown
|
|
189
270
|
## 信息收集
|
|
190
271
|
- 需求文档: {文件名} ({完整路径}, {章节数})
|
|
191
|
-
-
|
|
272
|
+
- 图片资源: index.yaml, 本地图片 {n} 张, Figma 链接 {n} 个
|
|
273
|
+
- API 资源: index.yaml, API 文档 {n} 个
|
|
192
274
|
- 关联任务: {task_id 或 "无"}
|
|
193
275
|
|
|
194
276
|
## 处理结果
|
|
195
|
-
- 章节匹配: {统计} | 控件提取: {总数} | 开发文档: {文件名}
|
|
277
|
+
- 章节匹配: {统计} | 控件提取: {总数} | API 接口: {总数} | 开发文档: {文件名}
|
|
196
278
|
```
|
|
197
279
|
|
|
198
280
|
### 完成
|
|
199
281
|
|
|
200
|
-
更新 task.json(completed +
|
|
282
|
+
更新 task.json(completed + 真实时间戳)。时间格式统一用 `date '+%Y-%m-%d %H:%M:%S'`,禁止带 T 或微秒。输出:开发文档路径、章节数、控件数、匹配图片数、匹配 API 数。
|
package/dashboard/db.py
CHANGED
|
@@ -3,7 +3,8 @@ import os
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
from typing import Optional, List, Dict
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
7
|
+
WORKTREE_DIR = os.environ.get("FLOW_WORKTREE_DIR", os.path.join(_PROJECT_ROOT, ".worktree"))
|
|
7
8
|
|
|
8
9
|
PHASE_NAMES_CODE_GEN = {
|
|
9
10
|
1: "需求收集", 2: "信息加载", 3: "方案规划",
|
|
@@ -55,10 +56,16 @@ def get_modules() -> List[Dict]:
|
|
|
55
56
|
result.append(mod)
|
|
56
57
|
except Exception:
|
|
57
58
|
pass
|
|
59
|
+
result.sort(key=lambda m: m.get("created_at", ""), reverse=True)
|
|
58
60
|
return result
|
|
59
61
|
|
|
60
62
|
|
|
61
63
|
def get_module(module_id: str) -> Optional[Dict]:
|
|
64
|
+
if module_id == "bug-fix-flat":
|
|
65
|
+
flat = _get_flat_tasks("bug-fix")
|
|
66
|
+
if not flat:
|
|
67
|
+
return None
|
|
68
|
+
return {"id": "bug-fix-flat", "name": "Bug 修复", "type": "bug-fix"}
|
|
62
69
|
for mod in get_modules():
|
|
63
70
|
if mod["id"] == module_id:
|
|
64
71
|
return mod
|
|
@@ -93,6 +100,8 @@ def _scan_task_dirs(module_id: str) -> List[str]:
|
|
|
93
100
|
|
|
94
101
|
|
|
95
102
|
def get_module_tasks(module_id: str) -> List[Dict]:
|
|
103
|
+
if module_id == "bug-fix-flat":
|
|
104
|
+
return _get_flat_tasks("bug-fix")
|
|
96
105
|
result = []
|
|
97
106
|
for path in _scan_task_dirs(module_id):
|
|
98
107
|
try:
|
|
@@ -108,6 +117,26 @@ def get_tasks() -> List[Dict]:
|
|
|
108
117
|
result = []
|
|
109
118
|
for mod in get_modules():
|
|
110
119
|
result.extend(get_module_tasks(mod["id"]))
|
|
120
|
+
# bug-fix: no module, scan directly
|
|
121
|
+
result.extend(_get_flat_tasks("bug-fix"))
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _get_flat_tasks(type_dir: str) -> List[Dict]:
|
|
126
|
+
"""扫描无模块的类型目录(如 bug-fix)下所有任务"""
|
|
127
|
+
wt = _worktree()
|
|
128
|
+
type_path = os.path.join(wt, type_dir)
|
|
129
|
+
if not os.path.isdir(type_path):
|
|
130
|
+
return []
|
|
131
|
+
result = []
|
|
132
|
+
for name in sorted(os.listdir(type_path)):
|
|
133
|
+
path = os.path.join(type_path, name)
|
|
134
|
+
if os.path.isdir(path) and os.path.exists(os.path.join(path, "task.json")):
|
|
135
|
+
try:
|
|
136
|
+
with open(os.path.join(path, "task.json"), "r", encoding="utf-8") as f:
|
|
137
|
+
result.append(json.load(f))
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
111
140
|
return result
|
|
112
141
|
|
|
113
142
|
|
|
@@ -129,6 +158,21 @@ def get_task_dir(task_id: str) -> Optional[str]:
|
|
|
129
158
|
return path
|
|
130
159
|
except Exception:
|
|
131
160
|
pass
|
|
161
|
+
# bug-fix: scan flat directory
|
|
162
|
+
wt = _worktree()
|
|
163
|
+
bf_path = os.path.join(wt, "bug-fix")
|
|
164
|
+
if os.path.isdir(bf_path):
|
|
165
|
+
for name in sorted(os.listdir(bf_path)):
|
|
166
|
+
path = os.path.join(bf_path, name)
|
|
167
|
+
task_file = os.path.join(path, "task.json")
|
|
168
|
+
if os.path.isdir(path) and os.path.exists(task_file):
|
|
169
|
+
try:
|
|
170
|
+
with open(task_file, "r", encoding="utf-8") as f:
|
|
171
|
+
task = json.load(f)
|
|
172
|
+
if task["id"] == task_id:
|
|
173
|
+
return path
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
132
176
|
return None
|
|
133
177
|
|
|
134
178
|
|
|
@@ -136,6 +180,11 @@ def get_task_dir(task_id: str) -> Optional[str]:
|
|
|
136
180
|
|
|
137
181
|
def get_modules_by_type(module_type: str) -> List[Dict]:
|
|
138
182
|
"""按 type 字段过滤模块,未指定 type 的默认为 code-gen"""
|
|
183
|
+
if module_type == "bug-fix":
|
|
184
|
+
flat = _get_flat_tasks("bug-fix")
|
|
185
|
+
if not flat:
|
|
186
|
+
return []
|
|
187
|
+
return [{"id": "bug-fix-flat", "name": "Bug 修复", "type": "bug-fix", "task_count": len(flat)}]
|
|
139
188
|
result = []
|
|
140
189
|
for mod in get_modules():
|
|
141
190
|
mod_type = mod.get("type", "code-gen")
|
|
@@ -148,9 +197,11 @@ def get_modules_by_type(module_type: str) -> List[Dict]:
|
|
|
148
197
|
|
|
149
198
|
def get_doc_gen_artifacts(task_dir: str) -> Dict:
|
|
150
199
|
"""读取 doc-gen 任务的产物信息"""
|
|
151
|
-
result = {"requirement_doc": None, "images": [], "develop_doc": None}
|
|
200
|
+
result = {"requirement_doc": None, "images": [], "figma_links": [], "develop_doc": None}
|
|
152
201
|
|
|
153
|
-
req_dir = os.path.join(task_dir, "
|
|
202
|
+
req_dir = os.path.join(task_dir, "prds")
|
|
203
|
+
if not os.path.exists(req_dir):
|
|
204
|
+
req_dir = os.path.join(task_dir, "requirement")
|
|
154
205
|
if os.path.exists(req_dir):
|
|
155
206
|
files = [f for f in os.listdir(req_dir) if not f.startswith(".")]
|
|
156
207
|
result["requirement_doc"] = files[0] if files else None
|
|
@@ -158,8 +209,22 @@ def get_doc_gen_artifacts(task_dir: str) -> Dict:
|
|
|
158
209
|
img_dir = os.path.join(task_dir, "images")
|
|
159
210
|
if os.path.exists(img_dir):
|
|
160
211
|
result["images"] = sorted(
|
|
161
|
-
f for f in os.listdir(img_dir)
|
|
212
|
+
f for f in os.listdir(img_dir)
|
|
213
|
+
if not f.startswith(".") and not f.endswith(".yaml") and not f.endswith(".yml")
|
|
162
214
|
)
|
|
215
|
+
# 解析 index.yaml 提取 Figma 链接
|
|
216
|
+
idx_path = os.path.join(img_dir, "index.yaml")
|
|
217
|
+
if os.path.exists(idx_path):
|
|
218
|
+
try:
|
|
219
|
+
with open(idx_path, "r", encoding="utf-8") as f:
|
|
220
|
+
for line in f:
|
|
221
|
+
line = line.strip()
|
|
222
|
+
if "figma.com" in line.lower():
|
|
223
|
+
url = line.lstrip("- ").strip()
|
|
224
|
+
if url:
|
|
225
|
+
result["figma_links"].append(url)
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
163
228
|
|
|
164
229
|
for f in os.listdir(task_dir):
|
|
165
230
|
if f.endswith("-develop.md"):
|
package/dashboard/main.py
CHANGED
|
@@ -11,11 +11,24 @@ try:
|
|
|
11
11
|
import logger
|
|
12
12
|
from models import ModuleCreate, TaskCreate, PhaseUpdate, TaskStatusUpdate
|
|
13
13
|
except ImportError:
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import sys
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
16
|
+
import db
|
|
17
|
+
import logger
|
|
18
|
+
from models import ModuleCreate, TaskCreate, PhaseUpdate, TaskStatusUpdate
|
|
16
19
|
|
|
17
20
|
app = FastAPI()
|
|
18
21
|
|
|
22
|
+
|
|
23
|
+
@app.middleware("http")
|
|
24
|
+
async def no_cache_middleware(request, call_next):
|
|
25
|
+
response = await call_next(request)
|
|
26
|
+
if request.url.path.startswith("/api/"):
|
|
27
|
+
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
|
|
28
|
+
response.headers["Pragma"] = "no-cache"
|
|
29
|
+
response.headers["Expires"] = "0"
|
|
30
|
+
return response
|
|
31
|
+
|
|
19
32
|
STATIC_DIR = Path(__file__).parent / "static"
|
|
20
33
|
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
21
34
|
|
|
@@ -45,6 +58,27 @@ def get_module(module_id: str):
|
|
|
45
58
|
raise HTTPException(status_code=404, detail="Module not found")
|
|
46
59
|
module.pop("_path", None)
|
|
47
60
|
module["tasks"] = db.get_module_tasks(module_id)
|
|
61
|
+
# Enrich code-gen tasks with bug-fix ref details
|
|
62
|
+
for task in module["tasks"]:
|
|
63
|
+
bug_refs = task.get("bug_fix_refs", [])
|
|
64
|
+
if bug_refs:
|
|
65
|
+
task["bug_fix_details"] = []
|
|
66
|
+
for ref_id in bug_refs:
|
|
67
|
+
ref_task = db.get_task(ref_id)
|
|
68
|
+
if ref_task:
|
|
69
|
+
task["bug_fix_details"].append({
|
|
70
|
+
"id": ref_task["id"],
|
|
71
|
+
"title": ref_task["title"],
|
|
72
|
+
"status": ref_task.get("bug_status", ref_task.get("status", "")),
|
|
73
|
+
})
|
|
74
|
+
# Enrich doc-gen tasks with image/figma count
|
|
75
|
+
if module.get("type") == "doc-gen":
|
|
76
|
+
for task in module["tasks"]:
|
|
77
|
+
task_dir = db.get_task_dir(task["id"])
|
|
78
|
+
if task_dir:
|
|
79
|
+
art = db.get_doc_gen_artifacts(task_dir)
|
|
80
|
+
task["_image_count"] = len(art.get("images", []))
|
|
81
|
+
task["_figma_count"] = len(art.get("figma_links", []))
|
|
48
82
|
return module
|
|
49
83
|
|
|
50
84
|
|
|
@@ -83,6 +117,24 @@ def delete_task(task_id: str):
|
|
|
83
117
|
return {"ok": True}
|
|
84
118
|
|
|
85
119
|
|
|
120
|
+
@app.delete("/api/modules/{module_id}")
|
|
121
|
+
def delete_module(module_id: str):
|
|
122
|
+
import shutil
|
|
123
|
+
module = db.get_module(module_id)
|
|
124
|
+
if not module:
|
|
125
|
+
raise HTTPException(status_code=404, detail="Module not found")
|
|
126
|
+
if module.get("type") == "doc-gen":
|
|
127
|
+
raise HTTPException(status_code=403, detail="需求分析模块不允许删除")
|
|
128
|
+
tasks = db.get_module_tasks(module_id)
|
|
129
|
+
if tasks:
|
|
130
|
+
raise HTTPException(status_code=400, detail="模块下仍有任务,无法删除")
|
|
131
|
+
mod_path = module.get("_path")
|
|
132
|
+
if mod_path and os.path.isdir(mod_path):
|
|
133
|
+
shutil.rmtree(mod_path, ignore_errors=True)
|
|
134
|
+
return {"ok": True}
|
|
135
|
+
raise HTTPException(status_code=404, detail="Module directory not found")
|
|
136
|
+
|
|
137
|
+
|
|
86
138
|
# ── Log & Snapshot API ──
|
|
87
139
|
|
|
88
140
|
@app.get("/api/tasks/{task_id}/logs")
|
|
@@ -160,8 +212,13 @@ def get_doc_artifact_file(task_id: str, artifact_type: str, filepath: str):
|
|
|
160
212
|
|
|
161
213
|
if artifact_type == "develop":
|
|
162
214
|
path = os.path.join(task_dir, filepath)
|
|
163
|
-
elif artifact_type in ("requirement", "images"):
|
|
164
|
-
|
|
215
|
+
elif artifact_type in ("prds", "requirement", "images"):
|
|
216
|
+
if artifact_type == "requirement":
|
|
217
|
+
artifact_type = "prds"
|
|
218
|
+
dir_name = artifact_type
|
|
219
|
+
if not os.path.exists(os.path.join(task_dir, dir_name)):
|
|
220
|
+
dir_name = "requirement"
|
|
221
|
+
path = os.path.join(task_dir, dir_name, filepath)
|
|
165
222
|
else:
|
|
166
223
|
raise HTTPException(status_code=400, detail="Invalid artifact type")
|
|
167
224
|
|
|
@@ -178,6 +235,31 @@ def get_doc_artifact_file(task_id: str, artifact_type: str, filepath: str):
|
|
|
178
235
|
return FileResponse(path)
|
|
179
236
|
|
|
180
237
|
|
|
238
|
+
@app.put("/api/tasks/{task_id}/doc-artifacts/{artifact_type}/{filepath:path}")
|
|
239
|
+
def save_doc_artifact_file(task_id: str, artifact_type: str, filepath: str, body: dict):
|
|
240
|
+
task_dir = db.get_task_dir(task_id)
|
|
241
|
+
if not task_dir:
|
|
242
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
243
|
+
|
|
244
|
+
if artifact_type == "develop":
|
|
245
|
+
path = os.path.join(task_dir, filepath)
|
|
246
|
+
elif artifact_type in ("prds", "requirement", "images", "apis"):
|
|
247
|
+
dir_name = "prds" if artifact_type == "requirement" else artifact_type
|
|
248
|
+
if not os.path.exists(os.path.join(task_dir, dir_name)):
|
|
249
|
+
dir_name = "requirement"
|
|
250
|
+
path = os.path.join(task_dir, dir_name, filepath)
|
|
251
|
+
else:
|
|
252
|
+
raise HTTPException(status_code=400, detail="Invalid artifact type")
|
|
253
|
+
|
|
254
|
+
if not os.path.exists(path):
|
|
255
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
256
|
+
|
|
257
|
+
content = body.get("content", "")
|
|
258
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
259
|
+
f.write(content)
|
|
260
|
+
return {"ok": True}
|
|
261
|
+
|
|
262
|
+
|
|
181
263
|
# ── bug-fix 信息 API ──
|
|
182
264
|
|
|
183
265
|
@app.get("/api/tasks/{task_id}/bug-info")
|