Improve AI batch progress controls

This commit is contained in:
2026-05-27 12:59:15 +08:00
parent 5d96c1de6d
commit b5f41c3dda
2 changed files with 229 additions and 23 deletions

View File

@@ -232,6 +232,7 @@ WORKFLOW_LOCK = threading.Lock()
WORKFLOW_READY = False
AI_JOB_LOCK = threading.Lock()
AI_REVIEW_JOB: dict[str, Any] = {
"kind": "ai_review",
"running": False,
"cancel_requested": False,
"scope": "",
@@ -248,6 +249,19 @@ AI_REVIEW_JOB: dict[str, Any] = {
"last_record_id": None,
"privacy_mode": True,
}
BULK_JOB_LOCK = threading.Lock()
BULK_APPROVE_JOB: dict[str, Any] = {
"kind": "approve_ai_passed",
"running": False,
"total": 0,
"processed": 0,
"updated": 0,
"failed": 0,
"message": "",
"started_at": "",
"finished_at": "",
}
APPROVE_BATCH_SIZE = 500
DEFAULT_STATUS_CHECK_TIME = env("REVIEW_STATUS_CHECK_TIME", "03:00") or "03:00"
DEFAULT_KIMI_API_BASE = env("MOONSHOT_API_BASE", env("KIMI_API_BASE", "https://api.moonshot.cn/v1")) or "https://api.moonshot.cn/v1"
DEFAULT_KIMI_MODEL = env("KIMI_MODEL", "kimi-k2.6") or "kimi-k2.6"
@@ -677,7 +691,7 @@ def ai_action_mode_to_override(mode: str) -> dict[str, Any]:
def ai_scope_allowed(mode: str, scope: str) -> bool:
mode = normalize_kimi_ai_scope_mode(mode)
if mode == "all":
return scope in {"current", "five", "fifty", "all", "ai_pending"}
return scope in {"current", "five", "fifty", "all", "ai_pending", "privacy_blocked"}
if mode == "five":
return scope in {"current", "five"}
if mode == "current":
@@ -2716,8 +2730,8 @@ def apply_ai_review_with_retry(record_id: int, attempts: int = 3, kimi_override:
def ai_target_ids(scope: str, record_id: int | None) -> list[int]:
if scope not in {"current", "five", "fifty", "all", "ai_pending"}:
raise HTTPException(status_code=400, detail="AI核验范围只能是 current/five/fifty/all/ai_pending")
if scope not in {"current", "five", "fifty", "all", "ai_pending", "privacy_blocked"}:
raise HTTPException(status_code=400, detail="AI核验范围只能是 current/five/fifty/all/ai_pending/privacy_blocked")
if scope in {"current", "five", "fifty"} and not record_id:
raise HTTPException(status_code=400, detail="缺少当前记录 ID")
if scope == "current":
@@ -2728,6 +2742,43 @@ def ai_target_ids(scope: str, record_id: int | None) -> list[int]:
cur.execute(query, (AI_PENDING_STATUS,))
rows = cur.fetchall()
return [int(row["id"]) for row in rows]
if scope == "privacy_blocked":
query = sql.SQL(
"""
SELECT id
FROM {table}
WHERE review_status = %s
AND EXISTS (
SELECT 1
FROM jsonb_array_elements(COALESCE(review_logs, '[]'::jsonb)) AS log
WHERE log->>'changed_by' = 'AI'
AND (
log->'ai_result'->>'ai_question' ILIKE %s
OR log->'ai_result'->'parsed'->>'summary' ILIKE %s
OR (log->'ai_result'->'parsed'->'remaining_issues')::text ILIKE %s
)
AND (
(log->'ai_result'->'parsed'->'remaining_issues')::text ILIKE %s
OR (log->'ai_result'->'parsed'->'remaining_issues')::text ILIKE %s
)
)
ORDER BY id
"""
).format(table=table_identifier())
with connect() as conn, conn.cursor() as cur:
cur.execute(
query,
(
AI_PENDING_STATUS,
"%隐私模式本地判定%",
"%隐私模式未上传敏感区域%",
"%隐私模式不上传%",
"%基本信息%",
"%地址联系人%",
),
)
rows = cur.fetchall()
return [int(row["id"]) for row in rows]
where = sql.SQL("review_status = 'needs_review'")
params: list[Any] = []
@@ -2784,6 +2835,7 @@ def run_ai_review_job(scope: str, ids: list[int], kimi_override: dict[str, Any]
privacy_mode = normalize_bool((kimi_override or {}).get("privacy_mode"), True)
stop_event = threading.Event()
update_ai_job(
kind="ai_review",
running=True,
cancel_requested=False,
scope=scope,
@@ -2866,7 +2918,28 @@ def run_ai_review_job(scope: str, ids: list[int], kimi_override: dict[str, Any]
)
def mark_ai_no_issue_reviewed() -> int:
def ai_no_issue_reviewed_ids() -> list[int]:
query = sql.SQL(
"""
SELECT id
FROM {table}
WHERE review_status IN ('AI已处理-OK', 'AI复核-无问题')
OR (
review_status = 'auto_pass'
AND COALESCE(review_logs, '[]'::jsonb) @> jsonb_build_array(jsonb_build_object('changed_by', 'AI'))
)
ORDER BY id
"""
).format(table=table_identifier())
with connect() as conn, conn.cursor() as cur:
cur.execute(query)
rows = cur.fetchall()
return [int(row["id"]) for row in rows]
def mark_ai_no_issue_reviewed_batch(record_ids: list[int]) -> int:
if not record_ids:
return 0
changed_at = datetime.now().isoformat(timespec="seconds")
note = f"批量确认历史AI已通过({datetime.now().strftime('%Y-%m-%d %H:%M:%S')}): 确认为已人工复核"
query = sql.SQL(
@@ -2888,17 +2961,69 @@ def mark_ai_no_issue_reviewed() -> int:
)
)
)
WHERE review_status IN ('AI已处理-OK', 'AI复核-无问题')
WHERE id = ANY(%s)
AND (
review_status IN ('AI已处理-OK', 'AI复核-无问题')
OR (
review_status = 'auto_pass'
AND COALESCE(review_logs, '[]'::jsonb) @> jsonb_build_array(jsonb_build_object('changed_by', 'AI'))
)
)
"""
).format(table=table_identifier())
with connect() as conn, conn.cursor() as cur:
cur.execute(query, (datetime.now().strftime("%Y%m%d%H%M%S%f"), changed_at, note))
cur.execute(query, (datetime.now().strftime("%Y%m%d%H%M%S%f"), changed_at, note, record_ids))
count = cur.rowcount
conn.commit()
refresh_status_snapshot(source="bulk")
return int(count)
def update_bulk_approve_job(**updates: Any) -> dict[str, Any]:
with BULK_JOB_LOCK:
BULK_APPROVE_JOB.update(updates)
return dict(BULK_APPROVE_JOB)
def run_approve_ai_no_issue_job(record_ids: list[int]) -> None:
update_bulk_approve_job(
kind="approve_ai_passed",
running=True,
total=len(record_ids),
processed=0,
updated=0,
failed=0,
message="批量通过AI已通过中",
started_at=datetime.now().isoformat(timespec="seconds"),
finished_at="",
)
try:
updated = 0
for start in range(0, len(record_ids), APPROVE_BATCH_SIZE):
batch = record_ids[start : start + APPROVE_BATCH_SIZE]
updated += mark_ai_no_issue_reviewed_batch(batch)
update_bulk_approve_job(
processed=min(start + len(batch), len(record_ids)),
updated=updated,
message="批量通过AI已通过中",
)
time.sleep(0.05)
refresh_status_snapshot(source="bulk")
update_bulk_approve_job(
running=False,
processed=len(record_ids),
updated=updated,
message=f"批量通过完成,已更新 {updated}",
finished_at=datetime.now().isoformat(timespec="seconds"),
)
except Exception as exc: # noqa: BLE001
update_bulk_approve_job(
running=False,
failed=1,
message=f"批量通过失败:{exc}",
finished_at=datetime.now().isoformat(timespec="seconds"),
)
def submit_reviewed_records() -> int:
changed_at = datetime.now().isoformat(timespec="seconds")
note = f"一键提交已人工复核项目({datetime.now().strftime('%Y-%m-%d %H:%M:%S')}): 已从患者首页复核工作台隐藏"
@@ -3370,6 +3495,12 @@ def ai_review_status():
return dict(AI_REVIEW_JOB)
@app.get("/api/ai/review/approve-no-issue/status")
def approve_ai_no_issue_status():
with BULK_JOB_LOCK:
return dict(BULK_APPROVE_JOB)
@app.post("/api/ai/review/cancel")
def cancel_ai_review():
with AI_JOB_LOCK:
@@ -3381,8 +3512,28 @@ def cancel_ai_review():
@app.post("/api/ai/review/approve-no-issue")
def approve_ai_no_issue():
count = mark_ai_no_issue_reviewed()
return {"ok": True, "updated": count, "status_snapshot": load_local_settings().get("status_snapshot")}
with BULK_JOB_LOCK:
if BULK_APPROVE_JOB.get("running"):
raise HTTPException(status_code=409, detail="已有批量通过任务正在运行")
ids = ai_no_issue_reviewed_ids()
if not ids:
update_bulk_approve_job(
kind="approve_ai_passed",
running=False,
total=0,
processed=0,
updated=0,
failed=0,
message="当前没有AI已通过记录需要批量通过",
started_at=datetime.now().isoformat(timespec="seconds"),
finished_at=datetime.now().isoformat(timespec="seconds"),
)
return dict(BULK_APPROVE_JOB)
thread = threading.Thread(target=run_approve_ai_no_issue_job, args=(ids,), name="approve-ai-passed", daemon=True)
thread.start()
time.sleep(0.1)
with BULK_JOB_LOCK:
return dict(BULK_APPROVE_JOB)
@app.post("/api/ai/review")