auto-save 2026-05-13 23:29 (~5)

This commit is contained in:
2026-05-13 23:29:35 +08:00
parent 38091d318b
commit 03770b1ed8
5 changed files with 108 additions and 7 deletions

View File

@@ -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
}
]
}

View File

@@ -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():

View File

@@ -706,6 +706,7 @@ api/main.py
<tr><th>功能</th><th>接口</th><th>前端调用</th><th>说明</th></tr>
</thead>
<tbody>
<tr><td>历史列表</td><td><code>GET /jobs</code></td><td><code>listJobs</code></td><td>所有 job 精简列表id/url/status/thumbnail/mtime…按 state.json mtime 倒序。前端 URL 无 <code>?job=</code> 时拉它回填全部历史;带 <code>limit</code> 可截断。</td></tr>
<tr><td>创建任务</td><td><code>POST /jobs</code></td><td><code>createJob</code></td><td>提交 TK 链接,后台开始下载,停在 downloaded 等用户点解析。</td></tr>
<tr><td>上传视频</td><td><code>POST /jobs/upload</code></td><td><code>uploadJob</code></td><td>保存 source.mp4然后同样进入下载完成状态。</td></tr>
<tr><td>解析视频</td><td><code>POST /jobs/{id}/analyze</code></td><td><code>analyzeJob</code></td><td>拆轨 + 抽关键帧。当前不自动跑 ASR避免 audio 阻塞视觉管线。</td></tr>
@@ -830,6 +831,18 @@ api/main.py
<h2>变更记录</h2>
<p>这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。</p>
<div class="changelog">
<article class="change">
<header>
<h3>2026-05-13 · 打开应用自动恢复历史 job</h3>
<span class="tag blue">API</span>
<span class="tag violet">Page</span>
</header>
<div class="body">
<p><strong>问题:</strong>前端只从 URL <code>?job=</code> 读 job 列表,没有任何本地或后端列表回填,打开 <code>/</code> 不带参数就是空白,之前跑过的 job 看不见。</p>
<p><strong>改动:</strong>后端新增 <code>GET /jobs</code> 列表接口(返回 <code>JobSummary</code>id/url/status/progress/duration/width/height/video_url/frame_count/video_count/thumbnail/error/mtime按 state.json mtime 倒序,可带 <code>limit</code>)。前端 <code>page.tsx</code> 启动逻辑URL 有 <code>?job=</code> 时尊重 URL没有时自动调 <code>listJobs()</code> 拿全部历史,反转后让最新 job 落在末尾active。持久化基于 <code>api/jobs/&lt;id&gt;/state.json</code> 磁盘文件,不依赖浏览器存储,换浏览器/清缓存都不会傻。</p>
<p><strong>影响:</strong><code>api/main.py</code><code>JobSummary</code> 类型 + <code>list_jobs</code> endpoint<code>web/lib/api.ts</code><code>JobSummary</code> + <code>listJobs</code>)、<code>web/app/page.tsx</code>(启动 useEffect</p>
</div>
</article>
<article class="change">
<header>
<h3>2026-05-13 · 允许骨骼人使用按摩仪后状态变好</h3>

View File

@@ -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
}, [])

View File

@@ -220,6 +220,30 @@ export async function getJob(id: string): Promise<Job> {
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<JobSummary[]> {
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<Job> {
const res = await fetch(`${API_BASE}/jobs/${id}/transcribe`, { method: "POST" })
if (!res.ok) throw new Error(`transcribe ${res.status}`)