claude-coding-flow 1.3.2 → 1.4.1

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.
@@ -5,7 +5,7 @@ tools: Read, Edit, Write, Bash, Glob, Grep, Agent, Skill
5
5
  model: inherit
6
6
  ---
7
7
 
8
- 你是一位资深需求分析师和 UI 架构师,将产品需求文档和 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
- .worktree/doc-gen/{模块名}-{module_id}/
24
- ├── module.json
25
- └── {任务标题}-{task_id}/
26
- ├── task.json
27
- ├── log.md
28
- ├── requirement/{原文档名}
29
- ├── images/
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": "completed", "current_phase": 1, "prev_task_id": null,
55
- "phases": [{"phase": 1, "name": "信息收集", "status": "completed", "started_at": "{timestamp}", "completed_at": "{timestamp}"}],
56
- "requirement_doc": "{文件名}", "sketch_folder": "{文件夹名}", "output_doc": "{需求名}-develop.md",
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
- 格式 `文件名/位置`,根目录 `docs/`。`/` 后:纯数字-纯数字→行号范围,非纯数字→章节标题。无 `/` 读全文。文件先查 `docs/{文件名}`,不存在则 `find docs/ -name "{文件名}" -type f` 递归搜索。
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
- cat .worktree/doc-gen/*/module.json 2>/dev/null | python3 -c "import sys,json; [print(f'{m[\"id\"]} {m[\"name\"]}') for line in sys.stdin for m in [json.loads(line)] if m.get('type')=='doc-gen']" 2>/dev/null
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
- 有模块 → AskUserQuestion(header=模块,options=模块列表 + "新建模块",最多展示3个已有);无模块文字提问"请输入模块名称"
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=需求文档:`选择其他输入文件名` / `读取 docs/ 全部文件`
97
- 2. header=草图:`无草图` / `选择其他输入文件夹名`(需含 index.yaml)
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}/requirement .worktree/doc-gen/{模块名}-{module_id}/{任务标题}-{task_id}/images`
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. **读取 index.yaml**:解析映射,验证图片文件存在性
120
- 3. **章节匹配**:行号范围→定位章节,章节标题→匹配章节;本地图片用 Read 视觉识别,Figma URL 标记留待 code-gen 处理
121
- 4. **内容提取**:UI 控件、显示字段、交互逻辑、跨控件关系
122
- 5. **生成 develop.md**
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 草图 → 8 个章节
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
- > 对应草图: {文件名/Figma URL}
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
- #### UI 草图 4 个章节(1.页面功能概述 2.页面状态机 3.数据流分析 4.页面事件流)
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/{模块}/{任务}/requirement/
182
- cp {草图文件夹}/index.yaml .worktree/doc-gen/{模块}/{任务}/images/
183
- cp {草图文件夹}/{引用的本地图片} .worktree/doc-gen/{模块}/{任务}/images/ # 仅本地图片,Figma URL 不需拷贝
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
- - 草图资源: index.yaml, 本地图片 {n} 张, Figma 链接 {n} 个
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
- WORKTREE_DIR = os.path.join(os.getcwd(), ".worktree")
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, "requirement")
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) if not f.startswith(".")
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
- from dashboard import db, logger
15
- from dashboard.models import ModuleCreate, TaskCreate, PhaseUpdate, TaskStatusUpdate
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
- path = os.path.join(task_dir, artifact_type, filepath)
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")