auto-save 2026-05-13 23:29 (~5)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
47
api/main.py
47
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():
|
||||
|
||||
@@ -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/<id>/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>
|
||||
|
||||
@@ -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
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
Reference in New Issue
Block a user