bilisum 1.13.2 → 1.13.3
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/README.md +4 -4
- package/bin/bilisum.js +157 -76
- package/package.json +7 -2
- package/runtime/VERSION +1 -0
- package/runtime/apps/service/pyproject.toml +29 -0
- package/runtime/apps/service/src/video_sum_service/__init__.py +1 -0
- package/runtime/apps/service/src/video_sum_service/__main__.py +5 -0
- package/runtime/apps/service/src/video_sum_service/app.py +480 -0
- package/runtime/apps/service/src/video_sum_service/context.py +24 -0
- package/runtime/apps/service/src/video_sum_service/integrations.py +264 -0
- package/runtime/apps/service/src/video_sum_service/knowledge/__init__.py +5 -0
- package/runtime/apps/service/src/video_sum_service/knowledge/index_service.py +408 -0
- package/runtime/apps/service/src/video_sum_service/knowledge/local_llm.py +319 -0
- package/runtime/apps/service/src/video_sum_service/knowledge/rag_service.py +492 -0
- package/runtime/apps/service/src/video_sum_service/knowledge/tag_service.py +242 -0
- package/runtime/apps/service/src/video_sum_service/main.py +31 -0
- package/runtime/apps/service/src/video_sum_service/repository.py +942 -0
- package/runtime/apps/service/src/video_sum_service/routers/__init__.py +1 -0
- package/runtime/apps/service/src/video_sum_service/routers/knowledge.py +272 -0
- package/runtime/apps/service/src/video_sum_service/routers/system.py +280 -0
- package/runtime/apps/service/src/video_sum_service/routers/tasks.py +287 -0
- package/runtime/apps/service/src/video_sum_service/routers/videos.py +766 -0
- package/runtime/apps/service/src/video_sum_service/runtime_support.py +1007 -0
- package/runtime/apps/service/src/video_sum_service/schemas.py +418 -0
- package/runtime/apps/service/src/video_sum_service/settings_manager.py +130 -0
- package/runtime/apps/service/src/video_sum_service/task_artifacts.py +102 -0
- package/runtime/apps/service/src/video_sum_service/task_exports.py +108 -0
- package/runtime/apps/service/src/video_sum_service/transcribe_worker.py +9 -0
- package/runtime/apps/service/src/video_sum_service/video_assets.py +623 -0
- package/runtime/apps/service/src/video_sum_service/worker.py +381 -0
- package/runtime/apps/web/static/apple-touch-icon.png +0 -0
- package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/runtime/apps/web/static/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/runtime/apps/web/static/assets/icons/icon-180.png +0 -0
- package/runtime/apps/web/static/assets/icons/icon-512.png +0 -0
- package/runtime/apps/web/static/assets/icons/icon.svg +17 -0
- package/runtime/apps/web/static/assets/index-Cj5PVjQg.js +388 -0
- package/runtime/apps/web/static/assets/index-DU2t4_7s.css +1 -0
- package/runtime/apps/web/static/favicon-32x32.png +0 -0
- package/runtime/apps/web/static/favicon.ico +0 -0
- package/runtime/apps/web/static/favicon.svg +17 -0
- package/runtime/apps/web/static/index.html +19 -0
- package/runtime/apps/web/static/js/api.js +123 -0
- package/runtime/apps/web/static/js/main.js +932 -0
- package/runtime/apps/web/static/js/state.js +28 -0
- package/runtime/apps/web/static/js/utils.js +81 -0
- package/runtime/apps/web/static/js/views/home.js +604 -0
- package/runtime/apps/web/static/js/views/settings.js +477 -0
- package/runtime/apps/web/static/styles.css +2206 -0
- package/runtime/packages/core/pyproject.toml +18 -0
- package/runtime/packages/core/src/video_sum_core/__init__.py +1 -0
- package/runtime/packages/core/src/video_sum_core/errors.py +22 -0
- package/runtime/packages/core/src/video_sum_core/markdown_exports.py +154 -0
- package/runtime/packages/core/src/video_sum_core/models/__init__.py +1 -0
- package/runtime/packages/core/src/video_sum_core/models/tasks.py +70 -0
- package/runtime/packages/core/src/video_sum_core/pipeline/__init__.py +1 -0
- package/runtime/packages/core/src/video_sum_core/pipeline/base.py +46 -0
- package/runtime/packages/core/src/video_sum_core/pipeline/real.py +3467 -0
- package/runtime/packages/core/src/video_sum_core/transcribe_subprocess.py +173 -0
- package/runtime/packages/core/src/video_sum_core/utils.py +127 -0
- package/runtime/packages/infra/pyproject.toml +16 -0
- package/runtime/packages/infra/src/video_sum_infra/__init__.py +1 -0
- package/runtime/packages/infra/src/video_sum_infra/app.py +39 -0
- package/runtime/packages/infra/src/video_sum_infra/config.py +393 -0
- package/runtime/packages/infra/src/video_sum_infra/db.py +34 -0
- package/runtime/packages/infra/src/video_sum_infra/logging.py +54 -0
- package/runtime/packages/infra/src/video_sum_infra/paths.py +6 -0
- package/runtime/packages/infra/src/video_sum_infra/runtime.py +485 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, HTTPException, Request, status
|
|
5
|
+
from fastapi.responses import StreamingResponse
|
|
6
|
+
|
|
7
|
+
from video_sum_core.models.tasks import InputType, TaskStatus
|
|
8
|
+
from video_sum_core.utils import normalize_video_url
|
|
9
|
+
|
|
10
|
+
from video_sum_service.repository import SqliteTaskRepository
|
|
11
|
+
from video_sum_service.schemas import (
|
|
12
|
+
TaskCreateRequest,
|
|
13
|
+
TaskDetailResponse,
|
|
14
|
+
TaskEventResponse,
|
|
15
|
+
TaskMarkdownExportRequest,
|
|
16
|
+
TaskMarkdownExportResponse,
|
|
17
|
+
TaskMindMapResponse,
|
|
18
|
+
TaskProgressResponse,
|
|
19
|
+
TaskSummaryResponse,
|
|
20
|
+
)
|
|
21
|
+
from video_sum_service.context import settings_manager
|
|
22
|
+
from video_sum_service.task_artifacts import cleanup_task_files, load_task_mindmap
|
|
23
|
+
from video_sum_service.task_exports import export_task_markdown as export_task_markdown_artifact
|
|
24
|
+
from video_sum_service.video_assets import probe_video_asset
|
|
25
|
+
from video_sum_service.worker import TaskWorker
|
|
26
|
+
|
|
27
|
+
router = APIRouter(prefix="/api/v1/tasks")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@router.post("", response_model=TaskDetailResponse, status_code=status.HTTP_201_CREATED)
|
|
31
|
+
def create_task(body: TaskCreateRequest, request: Request) -> TaskDetailResponse:
|
|
32
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
33
|
+
task_worker: TaskWorker = request.app.state.task_worker
|
|
34
|
+
|
|
35
|
+
video_id = body.video_id
|
|
36
|
+
if video_id is None and body.input_type is InputType.URL:
|
|
37
|
+
probed, _, _ = probe_video_asset(body.source)
|
|
38
|
+
asset = task_store.upsert_video_asset(probed)
|
|
39
|
+
video_id = asset.video_id
|
|
40
|
+
|
|
41
|
+
normalized = normalize_video_url(body.source) if body.input_type is InputType.URL else None
|
|
42
|
+
page_number = normalized.page_number if normalized and normalized.platform == "bilibili" else None
|
|
43
|
+
|
|
44
|
+
record = task_store.create_task(
|
|
45
|
+
body,
|
|
46
|
+
video_id=video_id,
|
|
47
|
+
page_number=page_number,
|
|
48
|
+
page_title=body.title,
|
|
49
|
+
)
|
|
50
|
+
task_worker.submit(record)
|
|
51
|
+
refreshed = task_store.get_task(record.task_id)
|
|
52
|
+
assert refreshed is not None
|
|
53
|
+
return refreshed.to_detail()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.get("", response_model=list[TaskSummaryResponse])
|
|
57
|
+
def list_tasks(request: Request) -> list[TaskSummaryResponse]:
|
|
58
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
59
|
+
return [record.to_summary() for record in task_store.list_tasks()]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@router.get("/{task_id}", response_model=TaskDetailResponse)
|
|
63
|
+
def get_task(task_id: str, request: Request) -> TaskDetailResponse:
|
|
64
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
65
|
+
record = task_store.get_task(task_id)
|
|
66
|
+
if record is None:
|
|
67
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
68
|
+
return record.to_detail()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@router.delete("/{task_id}")
|
|
72
|
+
def delete_task(task_id: str, request: Request) -> dict[str, object]:
|
|
73
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
74
|
+
record = task_store.get_task(task_id)
|
|
75
|
+
if record is None:
|
|
76
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
77
|
+
# 先清理文件,再删除数据库记录:如果文件清理失败,数据库记录仍保留以便重试
|
|
78
|
+
cleanup_task_files(record, settings_manager.current)
|
|
79
|
+
deleted = task_store.delete_task(task_id)
|
|
80
|
+
if not deleted:
|
|
81
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
82
|
+
return {"deleted": True, "task_id": task_id}
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.get("/{task_id}/result", response_model=TaskDetailResponse)
|
|
86
|
+
def get_task_result(task_id: str, request: Request) -> TaskDetailResponse:
|
|
87
|
+
return get_task(task_id, request)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@router.get("/{task_id}/mindmap", response_model=TaskMindMapResponse)
|
|
91
|
+
def get_task_mindmap(task_id: str, request: Request) -> TaskMindMapResponse:
|
|
92
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
93
|
+
record = task_store.get_task(task_id)
|
|
94
|
+
if record is None:
|
|
95
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
96
|
+
|
|
97
|
+
result = record.result
|
|
98
|
+
if result is None:
|
|
99
|
+
return TaskMindMapResponse(task_id=task_id, status="idle")
|
|
100
|
+
|
|
101
|
+
mindmap_path = result.mindmap_artifact_path or result.artifacts.get("mindmap_path")
|
|
102
|
+
mindmap = None
|
|
103
|
+
status_value = result.mindmap_status or ("ready" if mindmap_path else "idle")
|
|
104
|
+
error_message = result.mindmap_error_message
|
|
105
|
+
if status_value == "generating":
|
|
106
|
+
return TaskMindMapResponse(
|
|
107
|
+
task_id=task_id,
|
|
108
|
+
status="generating",
|
|
109
|
+
error_message=error_message,
|
|
110
|
+
updated_at=result.mindmap_updated_at,
|
|
111
|
+
mindmap=None,
|
|
112
|
+
)
|
|
113
|
+
if status_value == "failed":
|
|
114
|
+
return TaskMindMapResponse(
|
|
115
|
+
task_id=task_id,
|
|
116
|
+
status="failed",
|
|
117
|
+
error_message=error_message,
|
|
118
|
+
updated_at=result.mindmap_updated_at,
|
|
119
|
+
mindmap=None,
|
|
120
|
+
)
|
|
121
|
+
if mindmap_path:
|
|
122
|
+
try:
|
|
123
|
+
mindmap = load_task_mindmap(mindmap_path)
|
|
124
|
+
except HTTPException:
|
|
125
|
+
status_value = "failed"
|
|
126
|
+
error_message = "思维导图文件缺失或已损坏,请重新生成。"
|
|
127
|
+
if mindmap is not None:
|
|
128
|
+
status_value = "ready"
|
|
129
|
+
|
|
130
|
+
return TaskMindMapResponse(
|
|
131
|
+
task_id=task_id,
|
|
132
|
+
status=status_value,
|
|
133
|
+
error_message=error_message,
|
|
134
|
+
updated_at=result.mindmap_updated_at,
|
|
135
|
+
mindmap=mindmap,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@router.post("/{task_id}/mindmap", response_model=TaskMindMapResponse)
|
|
140
|
+
def generate_task_mindmap(request: Request, task_id: str, force: bool = False) -> TaskMindMapResponse:
|
|
141
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
142
|
+
task_worker: TaskWorker = request.app.state.task_worker
|
|
143
|
+
record = task_store.get_task(task_id)
|
|
144
|
+
if record is None:
|
|
145
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
146
|
+
if record.status != TaskStatus.COMPLETED or record.result is None:
|
|
147
|
+
raise HTTPException(status_code=400, detail="仅已完成且有结果的任务可以生成思维导图。")
|
|
148
|
+
if not record.result.knowledge_note_markdown.strip():
|
|
149
|
+
raise HTTPException(status_code=400, detail="当前任务缺少知识笔记,暂时无法生成思维导图。")
|
|
150
|
+
if not record.result.artifacts.get("summary_path"):
|
|
151
|
+
raise HTTPException(status_code=400, detail="当前任务缺少摘要文件,暂时无法生成思维导图。")
|
|
152
|
+
|
|
153
|
+
existing_path = record.result.mindmap_artifact_path or record.result.artifacts.get("mindmap_path")
|
|
154
|
+
if record.result.mindmap_status == "generating" and not force:
|
|
155
|
+
return TaskMindMapResponse(
|
|
156
|
+
task_id=task_id,
|
|
157
|
+
status="generating",
|
|
158
|
+
error_message=None,
|
|
159
|
+
updated_at=record.result.mindmap_updated_at,
|
|
160
|
+
mindmap=None,
|
|
161
|
+
)
|
|
162
|
+
if existing_path and record.result.mindmap_status == "ready" and not force:
|
|
163
|
+
try:
|
|
164
|
+
return TaskMindMapResponse(
|
|
165
|
+
task_id=task_id,
|
|
166
|
+
status="ready",
|
|
167
|
+
error_message=None,
|
|
168
|
+
updated_at=record.result.mindmap_updated_at,
|
|
169
|
+
mindmap=load_task_mindmap(existing_path),
|
|
170
|
+
)
|
|
171
|
+
except HTTPException:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
generating_result = record.result.model_copy(
|
|
175
|
+
update={
|
|
176
|
+
"mindmap_status": "generating",
|
|
177
|
+
"mindmap_error_message": None,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
task_store.save_result(task_id, generating_result)
|
|
181
|
+
task_worker.submit_mindmap(task_id, force=force)
|
|
182
|
+
refreshed = task_store.get_task(task_id)
|
|
183
|
+
refreshed_result = refreshed.result if refreshed is not None else generating_result
|
|
184
|
+
return TaskMindMapResponse(
|
|
185
|
+
task_id=task_id,
|
|
186
|
+
status=refreshed_result.mindmap_status,
|
|
187
|
+
error_message=refreshed_result.mindmap_error_message,
|
|
188
|
+
updated_at=refreshed_result.mindmap_updated_at,
|
|
189
|
+
mindmap=None,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@router.post("/{task_id}/exports/markdown", response_model=TaskMarkdownExportResponse)
|
|
194
|
+
def export_task_markdown(request: Request, task_id: str, body: TaskMarkdownExportRequest) -> TaskMarkdownExportResponse:
|
|
195
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
196
|
+
return export_task_markdown_artifact(
|
|
197
|
+
task_store,
|
|
198
|
+
settings_manager.current,
|
|
199
|
+
task_id,
|
|
200
|
+
target=body.target,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@router.get("/{task_id}/events", response_model=list[TaskEventResponse])
|
|
205
|
+
def get_task_events(task_id: str, request: Request) -> list[TaskEventResponse]:
|
|
206
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
207
|
+
record = task_store.get_task(task_id)
|
|
208
|
+
if record is None:
|
|
209
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
210
|
+
return [event.to_response() for event in task_store.list_events(task_id)]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@router.get("/{task_id}/events/stream")
|
|
214
|
+
async def stream_task_events(request: Request, task_id: str, after: str | None = None) -> StreamingResponse:
|
|
215
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
216
|
+
record = task_store.get_task(task_id)
|
|
217
|
+
if record is None:
|
|
218
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
219
|
+
|
|
220
|
+
async def event_generator():
|
|
221
|
+
last_seen = after
|
|
222
|
+
idle_ticks = 0
|
|
223
|
+
terminal_statuses = {TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED}
|
|
224
|
+
|
|
225
|
+
while True:
|
|
226
|
+
if await request.is_disconnected():
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
current_record = task_store.get_task(task_id)
|
|
230
|
+
if current_record is None:
|
|
231
|
+
yield "event: error\ndata: {\"message\":\"Task not found.\"}\n\n"
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
events = task_store.list_events_after(task_id, last_seen)
|
|
235
|
+
if events:
|
|
236
|
+
idle_ticks = 0
|
|
237
|
+
for event in events:
|
|
238
|
+
if await request.is_disconnected():
|
|
239
|
+
return
|
|
240
|
+
last_seen = event.created_at.isoformat()
|
|
241
|
+
payload = {
|
|
242
|
+
"event": event.to_response().model_dump(mode="json"),
|
|
243
|
+
"status": current_record.status.value,
|
|
244
|
+
"updated_at": current_record.updated_at.isoformat(),
|
|
245
|
+
"result": current_record.result.model_dump(mode="json") if current_record.result is not None else None,
|
|
246
|
+
}
|
|
247
|
+
yield f"event: progress\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
|
|
248
|
+
else:
|
|
249
|
+
idle_ticks += 1
|
|
250
|
+
|
|
251
|
+
current_record = task_store.get_task(task_id)
|
|
252
|
+
if current_record is None or current_record.status in terminal_statuses:
|
|
253
|
+
if idle_ticks >= 2:
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
if idle_ticks >= 20:
|
|
257
|
+
if await request.is_disconnected():
|
|
258
|
+
return
|
|
259
|
+
yield "event: heartbeat\ndata: {}\n\n"
|
|
260
|
+
idle_ticks = 0
|
|
261
|
+
|
|
262
|
+
if await request.is_disconnected():
|
|
263
|
+
return
|
|
264
|
+
await asyncio.sleep(0.4)
|
|
265
|
+
|
|
266
|
+
return StreamingResponse(
|
|
267
|
+
event_generator(),
|
|
268
|
+
media_type="text/event-stream",
|
|
269
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@router.get("/{task_id}/progress", response_model=TaskProgressResponse)
|
|
274
|
+
def get_task_progress(task_id: str, request: Request) -> TaskProgressResponse:
|
|
275
|
+
task_store: SqliteTaskRepository = request.app.state.task_repository
|
|
276
|
+
record = task_store.get_task(task_id)
|
|
277
|
+
if record is None:
|
|
278
|
+
raise HTTPException(status_code=404, detail="Task not found.")
|
|
279
|
+
latest_event = task_store.get_latest_event(task_id)
|
|
280
|
+
return TaskProgressResponse(
|
|
281
|
+
task_id=record.task_id,
|
|
282
|
+
status=record.status,
|
|
283
|
+
progress=int(latest_event.progress) if latest_event is not None else 0,
|
|
284
|
+
latest_stage=latest_event.stage if latest_event is not None else None,
|
|
285
|
+
latest_message=latest_event.message if latest_event is not None else None,
|
|
286
|
+
updated_at=record.updated_at,
|
|
287
|
+
)
|