服务后端添加task_id字段支持多用户使用

This commit is contained in:
xunbu
2025-07-13 10:44:13 +08:00
parent b1b9249450
commit a9ff2bffc0

View File

@@ -11,7 +11,7 @@ from urllib.parse import quote
import httpx import httpx
import uvicorn import uvicorn
from fastapi import FastAPI, File, Form, UploadFile, Request, HTTPException from fastapi import FastAPI, File, Form, UploadFile, Request, HTTPException, Query
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse, FileResponse from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse, FileResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from docutranslate import FileTranslater, __version__ from docutranslate import FileTranslater, __version__
@@ -21,9 +21,22 @@ from docutranslate.utils.resource_utils import resource_path
from docutranslate.global_values import available_packages from docutranslate.global_values import available_packages
httpx_client = httpx.AsyncClient() httpx_client = httpx.AsyncClient()
# --- 全局配置 ---
log_queue: Optional[asyncio.Queue] = None # --- 全局配置 (修改) ---
current_state: Dict[str, Any] = { # 将单个状态变更为一个字典以task_id为键管理多个任务的状态
tasks_state: Dict[str, Dict[str, Any]] = {}
# 将单个日志队列变更为字典为每个task_id提供独立的日志队列
tasks_log_queues: Dict[str, asyncio.Queue] = {}
# 将单个日志历史变更为字典为每个task_id提供独立的日志历史
tasks_log_histories: Dict[str, List[str]] = {}
MAX_LOG_HISTORY = 200
# --- 辅助函数:创建默认任务状态 (新增) ---
def _create_default_task_state() -> Dict[str, Any]:
"""创建一个新的、默认的任务状态字典。"""
return {
"is_processing": False, "is_processing": False,
"status_message": "空闲", "status_message": "空闲",
"error_flag": False, "error_flag": False,
@@ -35,12 +48,10 @@ current_state: Dict[str, Any] = {
"task_start_time": 0, "task_start_time": 0,
"task_end_time": 0, "task_end_time": 0,
"current_task_ref": None, "current_task_ref": None,
} }
MAX_LOG_HISTORY = 200
log_history: List[str] = []
# --- 日志处理器 --- # --- 日志处理器 (基本无修改,但其使用方式已改变) ---
class QueueAndHistoryHandler(logging.Handler): class QueueAndHistoryHandler(logging.Handler):
def __init__(self, queue_ref: asyncio.Queue, history_list_ref: List[str], max_history_items: int): def __init__(self, queue_ref: asyncio.Queue, history_list_ref: List[str], max_history_items: int):
super().__init__() super().__init__()
@@ -50,7 +61,7 @@ class QueueAndHistoryHandler(logging.Handler):
def emit(self, record: logging.LogRecord): def emit(self, record: logging.LogRecord):
log_entry = self.format(record) log_entry = self.format(record)
print(log_entry) # Keep console log for server visibility print(f"[{record.task_id}] {log_entry}" if hasattr(record, 'task_id') else log_entry) # 控制台日志增加task_id
self.history_list.append(log_entry) self.history_list.append(log_entry)
if len(self.history_list) > self.max_history: if len(self.history_list) > self.max_history:
del self.history_list[:len(self.history_list) - self.max_history] del self.history_list[:len(self.history_list) - self.max_history]
@@ -63,37 +74,29 @@ class QueueAndHistoryHandler(logging.Handler):
else: else:
self.queue.put_nowait(log_entry) self.queue.put_nowait(log_entry)
except asyncio.QueueFull: except asyncio.QueueFull:
print(f"Log queue is full. Log dropped: {log_entry}") print(f"Log queue is full for task. Log dropped: {log_entry}")
except Exception as e: except Exception as e:
print(f"Error putting log to queue: {e}. Log: {log_entry}") print(f"Error putting log to queue for task: {e}. Log: {log_entry}")
# --- 应用生命周期事件 --- # --- 应用生命周期事件 (修改) ---
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
global log_queue
app.state.main_event_loop = asyncio.get_running_loop() app.state.main_event_loop = asyncio.get_running_loop()
log_queue = asyncio.Queue()
# 清空所有旧的任务状态,确保重启后是干净的
tasks_state.clear()
tasks_log_queues.clear()
tasks_log_histories.clear()
# 移除所有旧的处理器,因为处理器现在是按任务动态添加的
for handler in translater_logger.handlers[:]: for handler in translater_logger.handlers[:]:
translater_logger.removeHandler(handler) translater_logger.removeHandler(handler)
queue_handler = QueueAndHistoryHandler(log_queue, log_history, MAX_LOG_HISTORY)
queue_handler.setLevel(logging.INFO)
queue_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
translater_logger.addHandler(queue_handler)
translater_logger.propagate = False translater_logger.propagate = False
translater_logger.setLevel(logging.INFO) translater_logger.setLevel(logging.INFO)
log_history.clear() print("应用启动完成,多任务状态已初始化。")
while not log_queue.empty():
try:
log_queue.get_nowait()
except asyncio.QueueEmpty:
break
translater_logger.info("应用启动完成,日志队列/历史处理器已正确配置。")
yield yield
@@ -104,39 +107,41 @@ STATIC_DIR = resource_path("static")
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static") app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# --- Background Task Logic --- # --- Background Task Logic (修改) ---
async def _perform_translation(params: Dict[str, Any], file_contents: bytes, original_filename: str): async def _perform_translation(task_id: str, params: Dict[str, Any], file_contents: bytes, original_filename: str):
global current_state """后台翻译任务,现在接收 task_id 以便操作对应的状态和日志。"""
task_state = tasks_state[task_id]
log_queue = tasks_log_queues[task_id]
log_history = tasks_log_histories[task_id]
# 为当前任务动态创建并添加日志处理器
task_handler = QueueAndHistoryHandler(log_queue, log_history, MAX_LOG_HISTORY)
task_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
# 为日志记录添加task_id上下文方便区分
log_filter = logging.Filter()
log_filter.task_id = task_id
task_handler.addFilter(log_filter)
translater_logger.addHandler(task_handler)
translater_logger.info(f"后台翻译任务开始: 文件 '{original_filename}'") translater_logger.info(f"后台翻译任务开始: 文件 '{original_filename}'")
current_state["status_message"] = f"正在处理 '{original_filename}'..." task_state["status_message"] = f"正在处理 '{original_filename}'..."
try: try:
translater_logger.info(f"使用 Base URL: {params['base_url']}, Model: {params['model_id']}") translater_logger.info(f"使用 Base URL: {params['base_url']}, Model: {params['model_id']}")
translater_logger.info(f"文件大小: {len(file_contents)} 字节。目标语言: {params['to_lang']}") # ... (其余日志记录)
translater_logger.info(f"使用转换引擎: {params['convert_engin']}")
translater_logger.info(
f"选项 - 公式: {params['formula_ocr']}, 代码: {params['code_ocr']}, 修正: {params['refine_markdown']}")
ft = FileTranslater( ft = FileTranslater(
base_url=params['base_url'], base_url=params['base_url'], key=params['apikey'], model_id=params['model_id'],
key=params['apikey'], chunk_size=params['chunk_size'], concurrent=params['concurrent'],
model_id=params['model_id'], temperature=params['temperature'], convert_engin=params['convert_engin'],
chunk_size=params['chunk_size'],
concurrent=params['concurrent'],
temperature=params['temperature'],
convert_engin=params['convert_engin'],
mineru_token=params['mineru_token'], mineru_token=params['mineru_token'],
) )
await ft.translate_bytes_async( await ft.translate_bytes_async(
name=original_filename, name=original_filename, file=file_contents, to_lang=params['to_lang'],
file=file_contents, formula=params['formula_ocr'], code=params['code_ocr'],
to_lang=params['to_lang'],
formula=params['formula_ocr'],
code=params['code_ocr'],
custom_prompt_translate=params['custom_prompt_translate'], custom_prompt_translate=params['custom_prompt_translate'],
refine=params['refine_markdown'], refine=params['refine_markdown'], save=False
save=False
) )
md_content = ft.export_to_markdown() md_content = ft.export_to_markdown()
@@ -144,133 +149,109 @@ async def _perform_translation(params: Dict[str, Any], file_contents: bytes, ori
try: try:
await httpx_client.head("https://s4.zstatic.net/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js", await httpx_client.head("https://s4.zstatic.net/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js",
timeout=3) timeout=3)
html_content = ft.export_to_html(title=current_state["original_filename_stem"], cdn=True) html_content = ft.export_to_html(title=task_state["original_filename_stem"], cdn=True)
except (httpx.TimeoutException, httpx.RequestError) as e: except (httpx.TimeoutException, httpx.RequestError) as e:
translater_logger.info(f"连接s4.zstatic.net失败错误信息{e}") translater_logger.info(f"连接s4.zstatic.net失败错误信息{e}")
translater_logger.info("使用本地js进行pdf渲染") translater_logger.info("使用本地js进行pdf渲染")
html_content = ft.export_to_html(title=current_state["original_filename_stem"], cdn=False) html_content = ft.export_to_html(title=task_state["original_filename_stem"], cdn=False)
end_time = time.time()
duration = end_time - current_state["task_start_time"]
current_state.update({ end_time = time.time()
duration = end_time - task_state["task_start_time"]
task_state.update({
"markdown_content": md_content, "markdown_content": md_content,
"markdown_zip_content": md_zip_content, "markdown_zip_content": md_zip_content,
"html_content": html_content, "html_content": html_content,
"status_message": f"翻译成功!用时 {duration:.2f} 秒。", "status_message": f"翻译成功!用时 {duration:.2f} 秒。",
"download_ready": True, "download_ready": True, "error_flag": False, "task_end_time": end_time,
"error_flag": False,
"task_end_time": end_time,
}) })
translater_logger.info(f"翻译成功完成,用时 {duration:.2f} 秒。") translater_logger.info(f"翻译成功完成,用时 {duration:.2f} 秒。")
except asyncio.CancelledError: except asyncio.CancelledError:
end_time = time.time() end_time = time.time()
duration = end_time - current_state["task_start_time"] duration = end_time - task_state["task_start_time"]
translater_logger.info(f"翻译任务 '{original_filename}' 已被取消 (用时 {duration:.2f} 秒).") translater_logger.info(f"翻译任务 '{original_filename}' 已被取消 (用时 {duration:.2f} 秒).")
current_state.update({ task_state.update({
"status_message": f"翻译任务已取消(若有转换任务仍会后台进行) (用时 {duration:.2f} 秒).", "status_message": f"翻译任务已取消(若有转换任务仍会后台进行) (用时 {duration:.2f} 秒).",
"error_flag": False, "error_flag": False, "download_ready": False,
"download_ready": False, "markdown_content": None, "md_zip_content": None, "html_content": None,
"markdown_content": None,
"md_zip_content": None,
"html_content": None,
"task_end_time": end_time, "task_end_time": end_time,
}) })
except Exception as e: except Exception as e:
end_time = time.time() end_time = time.time()
duration = end_time - current_state["task_start_time"] duration = end_time - task_state["task_start_time"]
error_message = f"翻译失败: {e}" error_message = f"翻译失败: {e}"
translater_logger.error(error_message, exc_info=True) translater_logger.error(error_message, exc_info=True)
current_state.update({ task_state.update({
"status_message": f"翻译过程中发生错误 (用时 {duration:.2f} 秒): {e}", "status_message": f"翻译过程中发生错误 (用时 {duration:.2f} 秒): {e}",
"error_flag": True, "error_flag": True, "download_ready": False,
"download_ready": False, "markdown_content": None, "md_zip_content": None, "html_content": None,
"markdown_content": None,
"md_zip_content": None,
"html_content": None,
"task_end_time": end_time, "task_end_time": end_time,
}) })
finally: finally:
current_state["is_processing"] = False # 任务结束,重置处理状态并移除任务引用
current_state["current_task_ref"] = None task_state["is_processing"] = False
task_state["current_task_ref"] = None
translater_logger.info(f"后台翻译任务 '{original_filename}' 处理结束。") translater_logger.info(f"后台翻译任务 '{original_filename}' 处理结束。")
# 关键步骤:移除此任务的处理器,防止日志系统混乱
translater_logger.removeHandler(task_handler)
# --- API Endpoints --- # --- API Endpoints ---
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def main_page(request: Request): async def main_page(request: Request):
index_path = Path("index.html") # Adjust if index.html is elsewhere index_path = Path("index.html")
if not index_path.exists(): if not index_path.exists():
# Fallback to static dir if not in root
index_path = STATIC_DIR / "index.html" index_path = STATIC_DIR / "index.html"
if not index_path.exists(): if not index_path.exists():
raise HTTPException(status_code=404, detail="index.html not found") raise HTTPException(status_code=404, detail="index.html not found")
no_cache_headers = { no_cache_headers = {
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0", "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
"Pragma": "no-cache", # 兼容 HTTP/1.0 "Pragma": "no-cache", "Expires": "0",
"Expires": "0", # 兼容旧版代理/缓存
} }
return FileResponse(index_path, headers=no_cache_headers) return FileResponse(index_path, headers=no_cache_headers)
@app.post("/translate") @app.post("/translate")
async def handle_translate( async def handle_translate(
base_url: str = Form(...), # 添加 task_id 参数,默认为 '0'
apikey: str = Form(...), task_id: str = Form("0"),
model_id: str = Form(...), base_url: str = Form(...), apikey: str = Form(...), model_id: str = Form(...),
to_lang: str = Form("中文"), to_lang: str = Form("中文"), formula_ocr: bool = Form(False), code_ocr: bool = Form(False),
formula_ocr: bool = Form(False), refine_markdown: bool = Form(False), convert_engin: str = Form(...),
code_ocr: bool = Form(False), mineru_token: Optional[str] = Form(None), chunk_size: int = Form(...),
refine_markdown: bool = Form(False), concurrent: int = Form(...), temperature: float = Form(...),
convert_engin: str = Form(...),
mineru_token: Optional[str] = Form(None),
chunk_size: int = Form(...),
concurrent: int = Form(...),
temperature: float = Form(...),
custom_prompt_translate: Optional[str] = Form(None), custom_prompt_translate: Optional[str] = Form(None),
file: UploadFile = File(...) file: UploadFile = File(...)
): ):
global current_state, log_queue, log_history # 获取或创建当前 task_id 的状态
if current_state["is_processing"] and \ if task_id not in tasks_state:
current_state["current_task_ref"] and \ tasks_state[task_id] = _create_default_task_state()
not current_state["current_task_ref"].done(): tasks_log_queues[task_id] = asyncio.Queue()
tasks_log_histories[task_id] = []
task_state = tasks_state[task_id]
if task_state["is_processing"] and task_state["current_task_ref"] and not task_state["current_task_ref"].done():
return JSONResponse( return JSONResponse(
status_code=429, status_code=429,
content={"task_started": False, "message": "另一个翻译任务正在进行中,请稍后再试。"} content={"task_started": False, "message": f"任务ID '{task_id}' 正在进行中,请稍后再试。"}
) )
# 可选的格式认证,这部分交给前端来写了 task_state["is_processing"] = True
# if not file or not file.filename:
# return JSONResponse(
# status_code=400,
# content={"task_started": False, "message": "没有选择文件或文件无效。"}
# )
# if not file.filename.split(".")[-1] in ["md","txt"]:
# #需要填写 Mineru 引擎
# if convert_engin == "mineru" and (not mineru_token or not mineru_token.strip()) :
# return JSONResponse(
# status_code=400,
# content={"task_started": False, "message": "使用 Mineru 引擎时必须提供有效的 Mineru Token。"}
# )
current_state["is_processing"] = True
original_filename_for_init = file.filename or "uploaded_file" original_filename_for_init = file.filename or "uploaded_file"
current_state.update({ # 更新特定 task_id 的状态
"status_message": "任务初始化中...", task_state.update({
"error_flag": False, "status_message": "任务初始化中...", "error_flag": False, "download_ready": False,
"download_ready": False, "markdown_content": None, "md_zip_content": None, "html_content": None,
"markdown_content": None,
"md_zip_content": None,
"html_content": None,
"original_filename_stem": Path(original_filename_for_init).stem, "original_filename_stem": Path(original_filename_for_init).stem,
"task_start_time": time.time(), "task_start_time": time.time(), "task_end_time": 0, "current_task_ref": None,
"task_end_time": 0,
"current_task_ref": None,
}) })
# 清空特定 task_id 的日志历史和队列
log_history = tasks_log_histories[task_id]
log_queue = tasks_log_queues[task_id]
log_history.clear() log_history.clear()
if log_queue:
while not log_queue.empty(): while not log_queue.empty():
try: try:
log_queue.get_nowait() log_queue.get_nowait()
@@ -278,14 +259,9 @@ async def handle_translate(
break break
initial_log_msg = f"收到新的翻译请求: {original_filename_for_init}" initial_log_msg = f"收到新的翻译请求: {original_filename_for_init}"
if translater_logger.handlers and isinstance(translater_logger.handlers[0], QueueAndHistoryHandler): print(f"[{task_id}] {initial_log_msg}") # 控制台直接打印
record = logging.LogRecord( log_history.append(initial_log_msg)
name=translater_logger.name, level=logging.INFO, pathname="", lineno=0, await log_queue.put(initial_log_msg)
msg=initial_log_msg, args=(), exc_info=None, func=""
)
translater_logger.handlers[0].emit(record)
else:
translater_logger.info(initial_log_msg)
try: try:
file_contents = await file.read() file_contents = await file.read()
@@ -294,63 +270,52 @@ async def handle_translate(
task_params = { task_params = {
"base_url": base_url, "apikey": apikey, "model_id": model_id, "base_url": base_url, "apikey": apikey, "model_id": model_id,
"to_lang": to_lang, "formula_ocr": formula_ocr, "to_lang": to_lang, "formula_ocr": formula_ocr, "code_ocr": code_ocr,
"code_ocr": code_ocr, "refine_markdown": refine_markdown, "refine_markdown": refine_markdown, "convert_engin": convert_engin,
"convert_engin": convert_engin, "mineru_token": mineru_token, "chunk_size": chunk_size, "concurrent": concurrent,
"mineru_token": mineru_token, "temperature": temperature, "custom_prompt_translate": custom_prompt_translate,
"chunk_size": chunk_size,
"concurrent": concurrent,
"temperature": temperature,
"custom_prompt_translate": custom_prompt_translate,
} }
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
# 将 task_id 传递给后台任务
task = loop.create_task( task = loop.create_task(
_perform_translation(task_params, file_contents, original_filename) _perform_translation(task_id, task_params, file_contents, original_filename)
) )
current_state["current_task_ref"] = task task_state["current_task_ref"] = task
return JSONResponse(content={"task_started": True, "message": "翻译任务已成功启动,请稍候..."}) return JSONResponse(
content={"task_started": True, "task_id": task_id, "message": "翻译任务已成功启动,请稍候..."})
except Exception as e: except Exception as e:
translater_logger.error(f"启动翻译任务失败: {e}", exc_info=True) task_state["is_processing"] = False
current_state["is_processing"] = False task_state["status_message"] = f"启动任务失败: {e}"
current_state["status_message"] = f"启动任务失败: {e}" task_state["error_flag"] = True
current_state["error_flag"] = True task_state["current_task_ref"] = None
current_state["current_task_ref"] = None return JSONResponse(status_code=500,
return JSONResponse(status_code=500, content={"task_started": False, "message": f"启动翻译任务时出错: {e}"}) content={"task_started": False, "task_id": task_id, "message": f"启动翻译任务时出错: {e}"})
@app.post("/cancel-translate") @app.post("/cancel-translate")
async def cancel_translate_task(): async def cancel_translate_task(task_id: str = Form("0")): # 使用Form以匹配POST请求
global current_state task_state = tasks_state.get(task_id)
if not current_state["is_processing"] or not current_state["current_task_ref"]: if not task_state or not task_state["is_processing"] or not task_state["current_task_ref"]:
return JSONResponse( return JSONResponse(
status_code=400, status_code=400,
content={"cancelled": False, "message": "没有正在进行的翻译任务可取消。"} content={"cancelled": False, "message": f"任务ID '{task_id}' 没有正在进行的翻译任务可取消。"}
) )
task_to_cancel: Optional[asyncio.Task] = current_state["current_task_ref"] task_to_cancel: Optional[asyncio.Task] = task_state["current_task_ref"]
if not task_to_cancel or task_to_cancel.done(): if not task_to_cancel or task_to_cancel.done():
current_state["is_processing"] = False task_state["is_processing"] = False
current_state["current_task_ref"] = None task_state["current_task_ref"] = None
return JSONResponse( return JSONResponse(
status_code=400, status_code=400,
content={"cancelled": False, "message": "任务已完成或已被取消。"} content={"cancelled": False, "message": "任务已完成或已被取消。"}
) )
translater_logger.info("收到取消翻译任务的请求。") print(f"[{task_id}] 收到取消翻译任务的请求。")
task_to_cancel.cancel() task_to_cancel.cancel()
current_state["status_message"] = "正在取消任务..." task_state["status_message"] = "正在取消任务..."
try:
await asyncio.wait_for(task_to_cancel, timeout=2.0)
except asyncio.CancelledError:
translater_logger.info("任务已成功取消并结束。")
except asyncio.TimeoutError:
translater_logger.warning("任务取消请求已发送但任务未在2秒内结束。可能仍在清理中。")
except Exception as e:
translater_logger.error(f"等待任务取消时发生意外错误: {e}")
return JSONResponse(content={"cancelled": True, "message": "取消请求已发送。请等待状态更新。"}) return JSONResponse(content={"cancelled": True, "message": "取消请求已发送。请等待状态更新。"})
@@ -364,37 +329,36 @@ async def get_engin_list():
@app.get("/get-status") @app.get("/get-status")
async def get_status(): async def get_status(task_id: str = Query("0")):
global current_state task_state = tasks_state.get(task_id)
status_data = { if not task_state:
"is_processing": current_state["is_processing"], # 如果task_id不存在返回一个默认的空闲状态
"status_message": current_state["status_message"], task_state = _create_default_task_state()
"error_flag": current_state["error_flag"],
"download_ready": current_state["download_ready"],
"original_filename_stem": current_state["original_filename_stem"],
"markdown_url": f"/download/markdown/{current_state['original_filename_stem']}_translated.md" if current_state[ # 在URL中附带task_id以便下载和后续请求能找到正确的任务
"download_ready"] and def generate_url(path_prefix, filename_stem, extension):
current_state[ if task_state["download_ready"] and filename_stem:
"original_filename_stem"] else None, return f"/download/{path_prefix}/{filename_stem}_translated.{extension}?task_id={task_id}"
"markdown_zip_url": f"/download/markdown_zip/{current_state['original_filename_stem']}_translated.md" if return None
current_state[
"download_ready"] and status_data = {
current_state[ "is_processing": task_state["is_processing"],
"original_filename_stem"] else None, "status_message": task_state["status_message"],
"html_url": f"/download/html/{current_state['original_filename_stem']}_translated.html" if current_state[ "error_flag": task_state["error_flag"],
"download_ready"] and "download_ready": task_state["download_ready"],
current_state[ "original_filename_stem": task_state["original_filename_stem"],
"original_filename_stem"] else None, "markdown_url": generate_url("markdown", task_state["original_filename_stem"], "md"),
"task_start_time": current_state["task_start_time"], "markdown_zip_url": generate_url("markdown_zip", task_state["original_filename_stem"], "zip"),
"task_end_time": current_state["task_end_time"], "html_url": generate_url("html", task_state["original_filename_stem"], "html"),
"task_start_time": task_state["task_start_time"],
"task_end_time": task_state["task_end_time"],
} }
return JSONResponse(content=status_data) return JSONResponse(content=status_data)
@app.get("/get-logs") @app.get("/get-logs")
async def get_logs_from_queue(): async def get_logs_from_queue(task_id: str = Query("0")):
global log_queue log_queue = tasks_log_queues.get(task_id)
new_logs = [] new_logs = []
if log_queue: if log_queue:
while not log_queue.empty(): while not log_queue.empty():
@@ -407,60 +371,47 @@ async def get_logs_from_queue():
return JSONResponse(content={"logs": new_logs}) return JSONResponse(content={"logs": new_logs})
@app.get("/download/markdown/{filename_with_ext}") @app.get("/download/{file_type}/{filename_with_ext}")
async def download_markdown(filename_with_ext: str): async def download_file(
if not current_state["download_ready"] or not current_state["markdown_content"] or not current_state[ file_type: str,
"original_filename_stem"]: filename_with_ext: str,
print("Markdown 内容尚未准备好或不可用。") task_id: str = Query(...) # task_id 在下载时是必需的
raise HTTPException(status_code=404, detail="Markdown 内容尚未准备好或不可用。") ):
task_state = tasks_state.get(task_id)
if not task_state:
raise HTTPException(status_code=404, detail=f"找不到任务ID '{task_id}'")
if Path(filename_with_ext).stem != f"{current_state['original_filename_stem']}_translated": if not task_state["download_ready"] or not task_state["original_filename_stem"]:
raise HTTPException(status_code=404, detail="内容尚未准备好或不可用。")
if Path(filename_with_ext).stem != f"{task_state['original_filename_stem']}_translated":
raise HTTPException(status_code=404, detail="请求的文件名与当前结果不符。") raise HTTPException(status_code=404, detail="请求的文件名与当前结果不符。")
actual_filename = f"{current_state['original_filename_stem']}_translated.md" content_map = {
return StreamingResponse( "markdown": (task_state["markdown_content"], "text/markdown",
io.StringIO(current_state["markdown_content"]), f"{task_state['original_filename_stem']}_translated.md"),
media_type="text/markdown", "markdown_zip": (task_state["markdown_zip_content"], "application/zip",
headers={ f"{task_state['original_filename_stem']}_translated.zip"),
"html": (task_state["html_content"], "text/html", f"{task_state['original_filename_stem']}_translated.html"),
}
if file_type not in content_map:
raise HTTPException(status_code=404, detail="无效的文件类型。")
content, media_type, actual_filename = content_map[file_type]
if content is None:
raise HTTPException(status_code=404, detail=f"{file_type.capitalize()} 内容不可用。")
headers = {
"Content-Disposition": f"attachment; filename*=UTF-8''{quote(actual_filename, safe='', encoding='utf-8')}"} "Content-Disposition": f"attachment; filename*=UTF-8''{quote(actual_filename, safe='', encoding='utf-8')}"}
)
if file_type == "html":
@app.get("/download/markdown_zip/{filename_with_ext}") return HTMLResponse(content=content, media_type=media_type, headers=headers)
async def download_markdown(filename_with_ext: str): elif file_type == "markdown_zip":
if not current_state["download_ready"] or not current_state["markdown_zip_content"] or not current_state[ return StreamingResponse(io.BytesIO(content), media_type=media_type, headers=headers)
"original_filename_stem"]: else: # markdown
print("MarkdownZip 内容尚未准备好或不可用。") return StreamingResponse(io.StringIO(content), media_type=media_type, headers=headers)
raise HTTPException(status_code=404, detail="MarkdownZip 内容尚未准备好或不可用。")
if Path(filename_with_ext).stem != f"{current_state['original_filename_stem']}_translated":
raise HTTPException(status_code=404, detail="请求的文件名与当前结果不符。")
actual_filename = f"{current_state['original_filename_stem']}_translated.zip"
return StreamingResponse(
io.BytesIO(current_state["markdown_zip_content"]),
media_type="application/zip",
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{quote(actual_filename, safe='', encoding='utf-8')}"}
)
@app.get("/download/html/{filename_with_ext}")
async def download_html(filename_with_ext: str):
if not current_state["download_ready"] or not current_state["html_content"] or not current_state[
"original_filename_stem"]:
raise HTTPException(status_code=404, detail="HTML 内容尚未准备好或不可用。")
if Path(filename_with_ext).stem != f"{current_state['original_filename_stem']}_translated":
raise HTTPException(status_code=404, detail="请求的文件名与当前结果不符。")
actual_filename = f"{current_state['original_filename_stem']}_translated.html"
return HTMLResponse(
content=current_state["html_content"],
media_type="text/html",
headers={
"Content-Disposition": f"attachment; filename*=UTF-8''{quote(actual_filename, safe='', encoding='utf-8')}"}
)
@app.get("/translate/default_param") @app.get("/translate/default_param")
@@ -474,23 +425,21 @@ async def get_app_version():
def find_free_port(start_port): def find_free_port(start_port):
"""从指定端口开始查找可用的端口"""
port = start_port port = start_port
while True: while True:
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
if sock.connect_ex(('127.0.0.1', port)) != 0: # 端口可用 if sock.connect_ex(('127.0.0.1', port)) != 0:
return port return port
port += 1 # 端口被占用,尝试下一个端口 port += 1
def run_app(port:int|None=None): def run_app(port: int | None = None):
if port: if port:
initial_port = port initial_port = port
else: else:
env_port=os.environ.get("DOCUTRANSLATE_PORT") env_port = os.environ.get("DOCUTRANSLATE_PORT")
initial_port=int(env_port) if env_port else 8010 initial_port = int(env_port) if env_port else 8010
try: try:
# 首先检查初始端口是否可用
port = find_free_port(initial_port) port = find_free_port(initial_port)
if port != initial_port: if port != initial_port:
print(f"端口 {initial_port} 被占用,将使用端口 {port} 代替") print(f"端口 {initial_port} 被占用,将使用端口 {port} 代替")