baodan/serve.py
wsb1224 5f78598b3a input_revision 正确记录
task_service.py — create_task() 新增 input_revision 参数,存入 GenerationTask.input_revision
generation_task.py — 模型的静态 create_task 方法同步新增参数
ppt/routes.py — 三处调用(parse/generate/regenerate)均传入 session.draft_revision
poster/service.py — 海报生成传入 record.draft_revision
现在每个任务快照都记录了创建时的草稿版本号,不再一直是默认值 1
2. 海报任务进度实时同步 
celery_tasks.py — 在海报生成的每个阶段(preparing_data→20%、building_prompt→40%、requesting_image→60%、saving→85%)调用 sync_poster_progress() 同步到 PosterRecord.task_status/task_progress
任务领取时立即将 PosterRecord.task_status 设为 running
解决了"页面轮询海报记录时一直显示排队中"的问题
3. 后端测试覆盖 
test_task_state_sync.py — 新增 13 个测试用例:
sync_workspace_status 同步 cancelled/failed 到 PPT session(4 个)
sync_workspace_status 同步 cancelled/failed 到 PosterRecord(2 个)
旧任务跳过同步的安全检查(1 个)
cancel_task 状态更新和非 queued 拒绝(2 个)
mark_task_viewed 正确标记和权限校验(2 个)
input_revision 正确记录和默认值(2 个)
list_active_tasks 排除终态任务(1 个)
所有 13 个新测试 + 原有 10 个测试全部通过
4. useAutoSave TypeScript 类型修复 
useAutoSave.ts — getEndpoint 变量显式标注 () => string 类型,消除 TS2345 编译错误
2026-07-30 13:54:44 +08:00

176 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""前端静态文件服务器 + API 反向代理(支持 SSE 流式响应)。"""
import os
import http.server
import socketserver
import http.client
from urllib.parse import urlparse
PORT = int(os.getenv('PORT', 8080))
DIRECTORY = '/app/dist'
# 后端 API 地址
BACKEND_URL = os.getenv('BACKEND_URL', 'http://baodanagent-api:5001')
# 需要代理到后端的路径前缀
PROXY_PREFIXES = ['/insurance', '/api', '/v1']
# 解析后端地址
_parsed = urlparse(BACKEND_URL)
BACKEND_HOST = _parsed.hostname
BACKEND_PORT = _parsed.port or (443 if _parsed.scheme == 'https' else 80)
BACKEND_TLS = _parsed.scheme == 'https'
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIRECTORY, **kwargs)
def do_GET(self):
if self._proxy_request():
return
self._serve_static()
def do_POST(self):
if self._proxy_request():
return
self.send_error(404)
def do_PUT(self):
if self._proxy_request():
return
self.send_error(404)
def do_DELETE(self):
if self._proxy_request():
return
self.send_error(404)
def do_PATCH(self):
if self._proxy_request():
return
self.send_error(404)
def do_OPTIONS(self):
if self._proxy_request():
return
self.send_error(404)
def _proxy_request(self):
"""将 API 请求代理到后端。返回 True 表示已处理。"""
for prefix in PROXY_PREFIXES:
if self.path.startswith(prefix):
self._forward_to_backend()
return True
return False
def _forward_to_backend(self):
"""转发请求到后端服务(支持 SSE 流式响应)。"""
# 读取请求体
content_length = int(self.headers.get('Content-Length', 0))
body = self.rfile.read(content_length) if content_length > 0 else None
# 构建请求头(不转发 Content-Length由 http.client 自动设置)
headers = {}
for key in ['Content-Type', 'Authorization', 'Accept', 'Cookie']:
val = self.headers.get(key)
if val:
headers[key] = val
headers['Host'] = BACKEND_HOST
headers['Connection'] = 'keep-alive'
conn = None
try:
# 建立到后端的连接
if BACKEND_TLS:
conn = http.client.HTTPSConnection(BACKEND_HOST, BACKEND_PORT, timeout=120)
else:
conn = http.client.HTTPConnection(BACKEND_HOST, BACKEND_PORT, timeout=120)
conn.request(self.command, self.path, body=body, headers=headers)
resp = conn.getresponse()
# 检测是否为 SSE 流式响应
content_type = resp.getheader('Content-Type', '')
is_sse = 'text/event-stream' in content_type
# 转发响应头
self.send_response(resp.status)
for key, val in resp.getheaders():
key_lower = key.lower()
if key_lower not in ('transfer-encoding', 'connection'):
self.send_header(key, val)
self._send_cors_headers()
self.end_headers()
# 流式转发响应体
if is_sse:
# SSE: 逐行读取,实时转发
while True:
line = resp.readline()
if not line:
break
self.wfile.write(line)
self.wfile.flush()
else:
# 普通响应:分块读取
while True:
chunk = resp.read(8192)
if not chunk:
break
self.wfile.write(chunk)
self.wfile.flush()
except Exception as e:
try:
self.send_response(502)
self.send_header('Content-Type', 'application/json')
self._send_cors_headers()
self.end_headers()
import json
self.wfile.write(json.dumps({
"code": 502,
"message": f"后端服务不可用: {str(e)}",
"data": None
}).encode())
except Exception:
pass
finally:
if conn:
try:
conn.close()
except Exception:
pass
def _serve_static(self):
"""服务静态文件。"""
if self.path == '/':
self.path = '/index.html'
return super().do_GET()
file_path = os.path.join(DIRECTORY, self.path.lstrip('/'))
if os.path.exists(file_path) and not os.path.isdir(file_path):
return super().do_GET()
# SPA 路由回退
self.path = '/index.html'
return super().do_GET()
def _send_cors_headers(self):
"""发送 CORS 头。"""
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
def log_message(self, format, *args):
print(f"[前端服务] {format % args}")
if __name__ == '__main__':
print(f"前端服务启动,端口: {PORT}")
print(f"服务目录: {DIRECTORY}")
print(f"后端地址: {BACKEND_URL}")
print(f"代理路径: {PROXY_PREFIXES}")
socketserver.TCPServer.allow_reuse_address = True
with socketserver.ThreadingTCPServer(("", PORT), Handler) as httpd:
httpd.serve_forever()