claude-coding-flow 1.0.0 → 1.1.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 +57 -16
- package/dashboard/db.py +296 -0
- package/dashboard/logger.py +110 -0
- package/dashboard/main.py +253 -0
- package/dashboard/models.py +26 -0
- package/dashboard/requirements.txt +2 -0
- package/dashboard/static/app.js +639 -0
- package/dashboard/static/index.html +190 -0
- package/dashboard/static/style.css +1034 -0
- package/package.json +3 -2
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, HTTPException
|
|
6
|
+
from fastapi.staticfiles import StaticFiles
|
|
7
|
+
from fastapi.responses import FileResponse
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import db
|
|
11
|
+
import logger
|
|
12
|
+
from models import ModuleCreate, TaskCreate, PhaseUpdate, TaskStatusUpdate
|
|
13
|
+
except ImportError:
|
|
14
|
+
from dashboard import db, logger
|
|
15
|
+
from dashboard.models import ModuleCreate, TaskCreate, PhaseUpdate, TaskStatusUpdate
|
|
16
|
+
|
|
17
|
+
app = FastAPI()
|
|
18
|
+
|
|
19
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
20
|
+
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.get("/")
|
|
24
|
+
def index():
|
|
25
|
+
return FileResponse(STATIC_DIR / "index.html")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ── Module API ──
|
|
29
|
+
|
|
30
|
+
@app.get("/api/modules")
|
|
31
|
+
def list_modules():
|
|
32
|
+
modules = db.get_modules()
|
|
33
|
+
for m in modules:
|
|
34
|
+
tasks = db.get_module_tasks(m["id"])
|
|
35
|
+
m["task_count"] = len(tasks)
|
|
36
|
+
m["latest_task"] = tasks[-1]["title"] if tasks else None
|
|
37
|
+
m.pop("_path", None)
|
|
38
|
+
return modules
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.get("/api/modules/{module_id}")
|
|
42
|
+
def get_module(module_id: str):
|
|
43
|
+
module = db.get_module(module_id)
|
|
44
|
+
if not module:
|
|
45
|
+
raise HTTPException(status_code=404, detail="Module not found")
|
|
46
|
+
module.pop("_path", None)
|
|
47
|
+
module["tasks"] = db.get_module_tasks(module_id)
|
|
48
|
+
return module
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.post("/api/modules")
|
|
52
|
+
def create_module(body: ModuleCreate):
|
|
53
|
+
return {"message": "模块由 code-gen 创建,Dashboard 仅读取"}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── Task API ──
|
|
57
|
+
|
|
58
|
+
@app.get("/api/tasks")
|
|
59
|
+
def list_tasks():
|
|
60
|
+
return db.get_tasks()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@app.get("/api/tasks/{task_id}")
|
|
64
|
+
def get_task(task_id: str):
|
|
65
|
+
task = db.get_task(task_id)
|
|
66
|
+
if not task:
|
|
67
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
68
|
+
return task
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.post("/api/tasks")
|
|
72
|
+
def create_task(body: TaskCreate):
|
|
73
|
+
return {"message": "任务由 code-gen 创建,Dashboard 仅读取"}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.delete("/api/tasks/{task_id}")
|
|
77
|
+
def delete_task(task_id: str):
|
|
78
|
+
import shutil
|
|
79
|
+
task_dir = db.get_task_dir(task_id)
|
|
80
|
+
if not task_dir:
|
|
81
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
82
|
+
shutil.rmtree(task_dir, ignore_errors=True)
|
|
83
|
+
return {"ok": True}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── Log & Snapshot API ──
|
|
87
|
+
|
|
88
|
+
@app.get("/api/tasks/{task_id}/logs")
|
|
89
|
+
def get_logs(task_id: str):
|
|
90
|
+
task_dir = db.get_task_dir(task_id)
|
|
91
|
+
if not task_dir:
|
|
92
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
93
|
+
return {
|
|
94
|
+
"logs": logger.get_logs(task_dir),
|
|
95
|
+
"token_summary": logger.get_token_summary(task_dir),
|
|
96
|
+
"text_log": logger.get_text_log(task_dir),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.get("/api/tasks/{task_id}/snapshots")
|
|
101
|
+
def get_snapshots(task_id: str):
|
|
102
|
+
task_dir = db.get_task_dir(task_id)
|
|
103
|
+
if not task_dir:
|
|
104
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
105
|
+
return logger.list_snapshots(task_dir)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.get("/api/tasks/{task_id}/snapshots/{snapshot_name}/{filepath:path}")
|
|
109
|
+
def get_snapshot_file(task_id: str, snapshot_name: str, filepath: str):
|
|
110
|
+
task_dir = db.get_task_dir(task_id)
|
|
111
|
+
if not task_dir:
|
|
112
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
113
|
+
content = logger.get_snapshot_file(task_dir, snapshot_name, filepath)
|
|
114
|
+
if content is None:
|
|
115
|
+
raise HTTPException(status_code=404, detail="Snapshot file not found")
|
|
116
|
+
return {"file": filepath, "content": content}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.get("/api/tasks/{task_id}/plan")
|
|
120
|
+
def get_plan(task_id: str):
|
|
121
|
+
task_dir = db.get_task_dir(task_id)
|
|
122
|
+
if not task_dir:
|
|
123
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
124
|
+
plan = logger.get_plan(task_dir)
|
|
125
|
+
if plan is None:
|
|
126
|
+
raise HTTPException(status_code=404, detail="Plan not found")
|
|
127
|
+
return {"plan": plan}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ── 按类型过滤模块 ──
|
|
131
|
+
|
|
132
|
+
@app.get("/api/modules/type/{module_type}")
|
|
133
|
+
def list_modules_by_type(module_type: str):
|
|
134
|
+
if module_type not in ("code-gen", "doc-gen", "bug-fix"):
|
|
135
|
+
raise HTTPException(status_code=400, detail="Invalid module type")
|
|
136
|
+
modules = db.get_modules_by_type(module_type)
|
|
137
|
+
for m in modules:
|
|
138
|
+
tasks = db.get_module_tasks(m["id"])
|
|
139
|
+
m["task_count"] = len(tasks)
|
|
140
|
+
m["latest_task"] = tasks[-1]["title"] if tasks else None
|
|
141
|
+
m.pop("_path", None)
|
|
142
|
+
return modules
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ── doc-gen 产物 API ──
|
|
146
|
+
|
|
147
|
+
@app.get("/api/tasks/{task_id}/doc-artifacts")
|
|
148
|
+
def get_doc_artifacts(task_id: str):
|
|
149
|
+
task_dir = db.get_task_dir(task_id)
|
|
150
|
+
if not task_dir:
|
|
151
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
152
|
+
return db.get_doc_gen_artifacts(task_dir)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.get("/api/tasks/{task_id}/doc-artifacts/{artifact_type}/{filepath:path}")
|
|
156
|
+
def get_doc_artifact_file(task_id: str, artifact_type: str, filepath: str):
|
|
157
|
+
task_dir = db.get_task_dir(task_id)
|
|
158
|
+
if not task_dir:
|
|
159
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
160
|
+
|
|
161
|
+
if artifact_type == "develop":
|
|
162
|
+
path = os.path.join(task_dir, filepath)
|
|
163
|
+
elif artifact_type in ("requirement", "images"):
|
|
164
|
+
path = os.path.join(task_dir, artifact_type, filepath)
|
|
165
|
+
else:
|
|
166
|
+
raise HTTPException(status_code=400, detail="Invalid artifact type")
|
|
167
|
+
|
|
168
|
+
if not os.path.exists(path):
|
|
169
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
170
|
+
|
|
171
|
+
if filepath.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp')):
|
|
172
|
+
return FileResponse(path)
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
176
|
+
return {"content": f.read()}
|
|
177
|
+
except UnicodeDecodeError:
|
|
178
|
+
return FileResponse(path)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ── bug-fix 信息 API ──
|
|
182
|
+
|
|
183
|
+
@app.get("/api/tasks/{task_id}/bug-info")
|
|
184
|
+
def get_bug_info(task_id: str):
|
|
185
|
+
task_dir = db.get_task_dir(task_id)
|
|
186
|
+
if not task_dir:
|
|
187
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
188
|
+
return db.get_bug_fix_info(task_dir)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ── 任务链 API ──
|
|
192
|
+
|
|
193
|
+
@app.get("/api/modules/{module_id}/task-chain")
|
|
194
|
+
def get_task_chain(module_id: str):
|
|
195
|
+
module = db.get_module(module_id)
|
|
196
|
+
if not module:
|
|
197
|
+
raise HTTPException(status_code=404, detail="Module not found")
|
|
198
|
+
return db.get_task_chain(module_id)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@app.get("/api/tasks/{task_id}/cross-relations")
|
|
202
|
+
def get_cross_relations(task_id: str):
|
|
203
|
+
task = db.get_task(task_id)
|
|
204
|
+
if not task:
|
|
205
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
206
|
+
return db.get_cross_relations(task_id)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# ── CLI ──
|
|
210
|
+
|
|
211
|
+
def cli():
|
|
212
|
+
parser = argparse.ArgumentParser(description="Code-Gen Dashboard CLI")
|
|
213
|
+
sub = parser.add_subparsers(dest="command")
|
|
214
|
+
|
|
215
|
+
# serve
|
|
216
|
+
sub.add_parser("serve", help="启动 Dashboard 服务")
|
|
217
|
+
|
|
218
|
+
# list modules
|
|
219
|
+
sub.add_parser("modules", help="列出所有模块")
|
|
220
|
+
|
|
221
|
+
# scan
|
|
222
|
+
sub.add_parser("scan", help="扫描 worktree 目录并显示信息")
|
|
223
|
+
|
|
224
|
+
args = parser.parse_args()
|
|
225
|
+
|
|
226
|
+
if args.command == "modules":
|
|
227
|
+
for m in db.get_modules():
|
|
228
|
+
tasks = db.get_module_tasks(m["id"])
|
|
229
|
+
print(f"{m['id']} {m['name']} ({len(tasks)} tasks)")
|
|
230
|
+
|
|
231
|
+
elif args.command == "scan":
|
|
232
|
+
modules = db.get_modules()
|
|
233
|
+
if not modules:
|
|
234
|
+
print("worktree 为空,暂无模块")
|
|
235
|
+
for m in modules:
|
|
236
|
+
tasks = db.get_module_tasks(m["id"])
|
|
237
|
+
print(f"\n模块: {m['name']} ({m['id']})")
|
|
238
|
+
for t in tasks:
|
|
239
|
+
phase_status = " ".join(
|
|
240
|
+
f"P{p['phase']}:{p['status']}" for p in t.get("phases", [])
|
|
241
|
+
)
|
|
242
|
+
print(f" 任务: {t['title']} ({t['id']}) [{t['status']}] {phase_status}")
|
|
243
|
+
|
|
244
|
+
elif args.command == "serve":
|
|
245
|
+
import uvicorn
|
|
246
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
247
|
+
|
|
248
|
+
else:
|
|
249
|
+
parser.print_help()
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
cli()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import Optional, List
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ModuleCreate(BaseModel):
|
|
6
|
+
name: str
|
|
7
|
+
type: str = "code-gen"
|
|
8
|
+
id: Optional[str] = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TaskCreate(BaseModel):
|
|
12
|
+
id: str
|
|
13
|
+
module_id: str
|
|
14
|
+
title: str
|
|
15
|
+
requirements_doc: str = ""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PhaseUpdate(BaseModel):
|
|
19
|
+
phase: int
|
|
20
|
+
status: str # running / completed / failed
|
|
21
|
+
detail: Optional[str] = None
|
|
22
|
+
error: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TaskStatusUpdate(BaseModel):
|
|
26
|
+
status: str # running / completed / failed
|