fix(bioflow): 部署期 P1 双缺陷修复——context 值导入脱钩 init_context 重绑定(路由层动态取模块全局)+BFF 反代非 SSE 透传 bytes 500(改纯透传 Response)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,3 +16,4 @@ coverage/
|
||||
|
||||
.DS_Store
|
||||
tooling/build/
|
||||
bioflow_data
|
||||
|
||||
@@ -25,7 +25,8 @@ from fastapi import APIRouter, Query
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
|
||||
from ..config import get_settings
|
||||
from ..context import Context, context, load_record
|
||||
from .. import context as context_mod
|
||||
from ..context import Context, context, load_record # context 保留供 conftest monkeypatch
|
||||
from ..schemas import ArchiveCreateRequest, ArchiveRunRequest
|
||||
from ..services.bioflow import store
|
||||
from ..services.bioflow.archive import (
|
||||
@@ -89,7 +90,7 @@ async def reports_register(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
async def create_archive(body: ArchiveCreateRequest) -> dict[str, Any]:
|
||||
"""存档(FR-BIO-020/021):打包任务目录 + 落 bio_archives 行。"""
|
||||
record = await job_or_404(body.job_id)
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
name = normalize_archive_name(body.name)
|
||||
# 重名 409(TC-BIO-API-098 两态断言取拒绝态)
|
||||
existing = await store.find_archive_by_name(ctx.db, name)
|
||||
@@ -137,14 +138,14 @@ async def list_archives(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
) -> dict[str, Any]:
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
items, total = await store.list_archives(ctx.db, page=page, page_size=page_size)
|
||||
return {"ok": True, "data": {"items": items, "total": total, "page": page, "page_size": page_size}}
|
||||
|
||||
|
||||
@router.get("/archives/{archive_id}")
|
||||
async def get_archive(archive_id: str) -> dict[str, Any]:
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
archive = await store.get_archive(ctx.db, archive_id)
|
||||
if archive is None:
|
||||
return _err("NOT_FOUND", "存档不存在", 404)
|
||||
@@ -153,7 +154,7 @@ async def get_archive(archive_id: str) -> dict[str, Any]:
|
||||
|
||||
@router.delete("/archives/{archive_id}", status_code=204)
|
||||
async def delete_archive(archive_id: str) -> JSONResponse:
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
archive = await store.get_archive(ctx.db, archive_id)
|
||||
if archive is None:
|
||||
return _err("NOT_FOUND", "存档不存在", 404)
|
||||
@@ -166,7 +167,7 @@ async def delete_archive(archive_id: str) -> JSONResponse:
|
||||
@router.post("/archives/{archive_id}/run", status_code=201)
|
||||
async def archive_run(archive_id: str, body: ArchiveRunRequest) -> dict[str, Any]:
|
||||
"""复用发起(FR-BIO-023):装载阶段链+参数 → 新建任务(表单预填存档值)。"""
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
archive = await store.get_archive(ctx.db, archive_id)
|
||||
if archive is None:
|
||||
return _err("ARCHIVE_NOT_FOUND", f"存档不存在: {archive_id}", 404)
|
||||
@@ -191,7 +192,7 @@ async def archive_run(archive_id: str, body: ArchiveRunRequest) -> dict[str, Any
|
||||
@router.post("/archives/run", status_code=201)
|
||||
async def archive_run_by_name(body: ArchiveRunRequest) -> dict[str, Any]:
|
||||
"""按名称匹配复用(FR-BIO-023):精确优先;模糊给候选;无匹配 404。"""
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
name = (body.name or "").strip()
|
||||
if not name:
|
||||
return _err("VALIDATION", "缺少归档名 name", 422)
|
||||
@@ -206,7 +207,7 @@ async def archive_run_by_name(body: ArchiveRunRequest) -> dict[str, Any]:
|
||||
@router.get("/archives/{archive_id}/download")
|
||||
async def archive_download(archive_id: str) -> FileResponse:
|
||||
"""归档打包下载(zip;TC-BIO-API-141)。"""
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
archive = await store.get_archive(ctx.db, archive_id)
|
||||
if archive is None:
|
||||
return _err("NOT_FOUND", "存档不存在", 404)
|
||||
@@ -225,7 +226,7 @@ async def archive_download(archive_id: str) -> FileResponse:
|
||||
@router.get("/archives/{archive_id}/report")
|
||||
async def archive_report(archive_id: str) -> FileResponse:
|
||||
"""归档报告单独取(HTML;FR-BIO-022 详情回看)。"""
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
archive = await store.get_archive(ctx.db, archive_id)
|
||||
if archive is None:
|
||||
return _err("NOT_FOUND", "存档不存在", 404)
|
||||
|
||||
@@ -20,7 +20,7 @@ from fastapi import APIRouter, Depends, Query, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
|
||||
from ..config import get_settings
|
||||
from ..context import Context, context, load_record
|
||||
from ..context import Context, context, load_record # context 保留供 conftest monkeypatch
|
||||
from ..db import Database, get_db
|
||||
from ..schemas import ChatStreamRequest, FlowAskRequest
|
||||
from ..services.bioflow import store
|
||||
@@ -45,7 +45,12 @@ def _err(code: str, message: str, status: int) -> JSONResponse:
|
||||
|
||||
|
||||
def _ctx() -> Context:
|
||||
return context
|
||||
# 动态取模块全局:init_context() 用「重绑定」更新全局,模块加载时的
|
||||
# 值导入会拿到旧 Context(catalog=None → 端点 500)。测试的 monkeypatch
|
||||
# 是属性替换所以没暴露;这里按属性读取与两者兼容。
|
||||
from .. import context as context_mod
|
||||
|
||||
return context_mod.context
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -30,7 +30,8 @@ from fastapi import APIRouter, Depends, File, HTTPException, Query, Request, Upl
|
||||
from fastapi.responses import FileResponse, JSONResponse, StreamingResponse
|
||||
|
||||
from ..config import get_settings
|
||||
from ..context import Context, context, load_record
|
||||
from .. import context as context_mod
|
||||
from ..context import Context, context, load_record # context 保留供 conftest monkeypatch
|
||||
from ..db import Database, get_db
|
||||
from ..schemas import (
|
||||
ArchiveRunRequest,
|
||||
@@ -75,7 +76,7 @@ def _err(code: str, message: str, status: int) -> JSONResponse:
|
||||
@router.post("/jobs", status_code=201)
|
||||
async def create_job(body: JobCreateRequest) -> dict[str, Any]:
|
||||
"""创建任务(FR-BIO-004;未知阶段 422;复用归档 TC-BIO-API-029)。"""
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
params: dict[str, dict[str, Any]] = dict(body.params or {})
|
||||
archive_id = body.archive_id
|
||||
if body.archive_id:
|
||||
@@ -130,7 +131,7 @@ async def list_jobs(
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=20, ge=1, le=100),
|
||||
) -> dict[str, Any]:
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
items, total = await store.list_jobs(
|
||||
ctx.db, session_id=session_id, status=status, page=page, page_size=page_size,
|
||||
)
|
||||
@@ -158,7 +159,7 @@ async def delete_job(job_id: str) -> JSONResponse:
|
||||
return _err("NOT_FOUND", "任务不存在", 404)
|
||||
if record.status in (JOB_RUNNING,):
|
||||
return _err("JOB_RUNNING", "任务执行中不可删除", 409)
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
await store.delete_job(ctx.db, job_id) # bio_stages 级联删除
|
||||
ctx.records.pop(job_id, None)
|
||||
return JSONResponse(status_code=204, content=None)
|
||||
@@ -185,7 +186,7 @@ async def upload_file(
|
||||
大文件超时单独调大:本端点自身逐块流式写盘(不整读内存,TC-BIO-API-041)。
|
||||
"""
|
||||
record = await job_or_404(job_id)
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
settings = get_settings()
|
||||
input_dir = record.input_dir
|
||||
input_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -293,7 +294,7 @@ def _stage_exists(record: Any, stage: str) -> None:
|
||||
async def confirm_stage(job_id: str, stage: str, body: StageConfirmRequest) -> dict[str, Any]:
|
||||
record = await job_or_404(job_id)
|
||||
_stage_exists(record, stage)
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
try:
|
||||
await ctx.runner.confirm_stage(record, stage, dict(body.params or {}))
|
||||
except RunnerError as exc:
|
||||
@@ -310,7 +311,7 @@ async def confirm_stage(job_id: str, stage: str, body: StageConfirmRequest) -> d
|
||||
async def rerun_stage(job_id: str, stage: str, body: StageRerunRequest) -> dict[str, Any]:
|
||||
record = await job_or_404(job_id)
|
||||
_stage_exists(record, stage)
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
try:
|
||||
await ctx.runner.rerun_stage(record, stage, dict(body.params) if body.params else None)
|
||||
except RunnerError as exc:
|
||||
@@ -326,7 +327,7 @@ async def rerun_stage(job_id: str, stage: str, body: StageRerunRequest) -> dict[
|
||||
async def skip_stage(job_id: str, stage: str) -> dict[str, Any]:
|
||||
record = await job_or_404(job_id)
|
||||
_stage_exists(record, stage)
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
try:
|
||||
await ctx.runner.skip_stage(record, stage)
|
||||
except RunnerError as exc:
|
||||
@@ -340,7 +341,7 @@ async def skip_stage(job_id: str, stage: str) -> dict[str, Any]:
|
||||
@router.post("/jobs/{job_id}/pause")
|
||||
async def pause_job(job_id: str) -> dict[str, Any]:
|
||||
record = await job_or_404(job_id)
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
try:
|
||||
await ctx.runner.pause_job(record)
|
||||
except RunnerError as exc:
|
||||
@@ -351,7 +352,7 @@ async def pause_job(job_id: str) -> dict[str, Any]:
|
||||
@router.post("/jobs/{job_id}/resume")
|
||||
async def resume_job(job_id: str) -> dict[str, Any]:
|
||||
record = await job_or_404(job_id)
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
try:
|
||||
await ctx.runner.resume_job(record)
|
||||
except RunnerError as exc:
|
||||
@@ -405,7 +406,7 @@ async def job_events(
|
||||
TC-BIO-API-081:任务终态(done/failed/archived)且回放完成后服务端正常关闭连接。
|
||||
"""
|
||||
record = await job_or_404(job_id)
|
||||
ctx: Context = context
|
||||
ctx: Context = context_mod.context
|
||||
|
||||
async def gen():
|
||||
# 1. 快照首帧(TC-BIO-SSE-022)
|
||||
|
||||
@@ -18,7 +18,7 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import JSONResponse, StreamingResponse
|
||||
from fastapi.responses import JSONResponse, Response, StreamingResponse
|
||||
|
||||
from ..config import get_settings
|
||||
from ..db import Database, get_db
|
||||
@@ -96,9 +96,10 @@ async def _proxy(request: Request, path: str) -> Any:
|
||||
payload, status = envelope("UPSTREAM_DOWN", message="生信服务响应中断",
|
||||
degraded=True, status=502)
|
||||
return JSONResponse(status_code=status, content=payload)
|
||||
# 非 SSE:纯透传(content 是 bytes;不能 JSONResponse——bytes 序列化会 500)
|
||||
media_type = upstream.headers.get("content-type") or "application/json"
|
||||
return JSONResponse(status_code=upstream.status_code, content=content,
|
||||
media_type=media_type)
|
||||
return Response(status_code=upstream.status_code, content=content,
|
||||
media_type=media_type)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user