diff --git a/.memory/worklog.json b/.memory/worklog.json index 174b06d..42ddcf4 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -2545,6 +2545,13 @@ "type": "session-heartbeat", "message": "Claude 会话活跃 · 最近命令:claude · 1 项未提交变更 · 最近提交:auto-save 2026-05-13 23:17 (~2)", "files_changed": 1 + }, + { + "ts": "2026-05-13T23:24:03+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 23:23 (~1)", + "hash": "38091d3", + "files_changed": 1 } ] } diff --git a/api/main.py b/api/main.py index 8adc7bf..d6e0451 100644 --- a/api/main.py +++ b/api/main.py @@ -850,6 +850,53 @@ def health() -> dict: } +class JobSummary(BaseModel): + id: str + url: str + status: JobStatus + progress: int = 0 + message: str = "" + duration: float = 0.0 + width: int = 0 + height: int = 0 + video_url: str = "" + frame_count: int = 0 + video_count: int = 0 + thumbnail: str = "" + error: str = "" + mtime: float = 0.0 + + +@app.get("/jobs", response_model=list[JobSummary]) +def list_jobs(limit: int | None = None) -> list[JobSummary]: + """所有 job 的精简列表,按磁盘 state.json mtime 倒序(最新优先)。前端无 ?job= 时用它回填历史。""" + items: list[JobSummary] = [] + for job_id, job in JOBS.items(): + state_path = JOBS_DIR / job_id / "state.json" + mtime = state_path.stat().st_mtime if state_path.exists() else 0.0 + thumb = f"/jobs/{job_id}/frames/{job.frames[0].index}.jpg" if job.frames else "" + items.append(JobSummary( + id=job.id, + url=job.url, + status=job.status, + progress=job.progress, + message=job.message, + duration=job.duration, + width=job.width, + height=job.height, + video_url=job.video_url, + frame_count=len(job.frames), + video_count=len(job.generated_videos), + thumbnail=thumb, + error=job.error, + mtime=mtime, + )) + items.sort(key=lambda s: s.mtime, reverse=True) + if limit is not None and limit > 0: + items = items[:limit] + return items + + @app.post("/jobs", response_model=Job) async def create_job(req: CreateJobReq, bg: BackgroundTasks) -> Job: if not req.url.strip(): diff --git a/docs/source-analysis.html b/docs/source-analysis.html index b3d842a..2ccc55a 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -706,6 +706,7 @@ api/main.py 功能接口前端调用说明 + 历史列表GET /jobslistJobs所有 job 精简列表(id/url/status/thumbnail/mtime…),按 state.json mtime 倒序。前端 URL 无 ?job= 时拉它回填全部历史;带 limit 可截断。 创建任务POST /jobscreateJob提交 TK 链接,后台开始下载,停在 downloaded 等用户点解析。 上传视频POST /jobs/uploaduploadJob保存 source.mp4,然后同样进入下载完成状态。 解析视频POST /jobs/{id}/analyzeanalyzeJob拆轨 + 抽关键帧。当前不自动跑 ASR,避免 audio 阻塞视觉管线。 @@ -830,6 +831,18 @@ api/main.py

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-13 · 打开应用自动恢复历史 job

+ API + Page +
+
+

问题:前端只从 URL ?job= 读 job 列表,没有任何本地或后端列表回填,打开 / 不带参数就是空白,之前跑过的 job 看不见。

+

改动:后端新增 GET /jobs 列表接口(返回 JobSummary:id/url/status/progress/duration/width/height/video_url/frame_count/video_count/thumbnail/error/mtime,按 state.json mtime 倒序,可带 limit)。前端 page.tsx 启动逻辑:URL 有 ?job= 时尊重 URL;没有时自动调 listJobs() 拿全部历史,反转后让最新 job 落在末尾(active)。持久化基于 api/jobs/<id>/state.json 磁盘文件,不依赖浏览器存储,换浏览器/清缓存都不会傻。

+

影响:api/main.pyJobSummary 类型 + list_jobs endpoint)、web/lib/api.tsJobSummary + listJobs)、web/app/page.tsx(启动 useEffect)。

+
+

2026-05-13 · 允许骨骼人使用按摩仪后状态变好

diff --git a/web/app/page.tsx b/web/app/page.tsx index dd1a762..b82b368 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -16,7 +16,7 @@ import { ThemeToggle } from "@/components/theme-toggle" import { StoryboardBar } from "@/components/storyboard-bar" import { StoryboardWorkbench } from "@/components/storyboard-workbench" import { - addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, + addManualFrame, analyzeJob, createJob, getJob, listJobs, uploadJob, deleteFrame, deleteGeneratedImage, deleteGeneratedVideo, generateStoryboardVideo, type Job, type ImageRef, type StoryboardScene, } from "@/lib/api" @@ -321,19 +321,29 @@ export default function Home() { } }, [job, selectedFrames, setJob]) - // URL ?job=xxx,yyy 自动恢复多个 job + // 启动恢复:URL ?job=xxx,yyy 优先;否则从后端拉全部历史(按 mtime 倒序,最新放末尾) useEffect(() => { const params = new URLSearchParams(window.location.search) - const idsStr = params.get("job") ?? "" - const ids = idsStr.split(",").filter(Boolean) - if (ids.length === 0) return - Promise.all(ids.map((id) => getJob(id).catch(() => null))).then((results) => { + const ids = (params.get("job") ?? "").split(",").filter(Boolean) + const restore = async () => { + let targetIds = ids + if (targetIds.length === 0) { + try { + const list = await listJobs() + targetIds = list.map((s) => s.id).reverse() + } catch { + return + } + } + if (targetIds.length === 0) return + const results = await Promise.all(targetIds.map((id) => getJob(id).catch(() => null))) const valid = results.filter((j): j is Job => !!j) if (valid.length > 0) { setJobs(valid) setActiveJobId(valid[valid.length - 1].id) } - }) + } + void restore() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/web/lib/api.ts b/web/lib/api.ts index 26c1513..8c9b2b8 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -220,6 +220,30 @@ export async function getJob(id: string): Promise { return res.json() } +export interface JobSummary { + id: string + url: string + status: JobStatus + progress: number + message: string + duration: number + width: number + height: number + video_url: string + frame_count: number + video_count: number + thumbnail: string + error: string + mtime: number +} + +export async function listJobs(limit?: number): Promise { + const qs = limit && limit > 0 ? `?limit=${limit}` : "" + const res = await fetch(`${API_BASE}/jobs${qs}`) + if (!res.ok) throw new Error(`listJobs ${res.status}`) + return res.json() +} + export async function triggerTranscribe(id: string): Promise { const res = await fetch(`${API_BASE}/jobs/${id}/transcribe`, { method: "POST" }) if (!res.ok) throw new Error(`transcribe ${res.status}`)