起草失败单独记一种状态,别再停在「起草中」

没有可用模型、模型回复读不懂时,草稿此前一直留在 drafting,界面会以为还在跑。现在置 failed
并把原因写进 error;补上真指标后 PATCH 仍能把它推回正常流程。
This commit is contained in:
ThearchyHelios
2026-11-03 23:07:01 +08:00
parent f697ab7ed2
commit 43a38e926c
6 changed files with 17 additions and 9 deletions
+7 -2
View File
@@ -518,11 +518,13 @@ async def _extract_in_background(draft_id: str) -> None:
}
except docdraft.DraftUnavailable as e:
draft.error = str(e) or "AI 起草不可用"
draft.status = "failed"
await session.commit()
return
except Exception as e: # noqa: BLE001 - 后台出错要写回这一行,不然界面永远停在「起草中」
logger.exception("从文档起草口径出错(草稿 %s)", draft_id)
draft.error = f"起草出错:{e}"
draft.status = "failed"
await session.commit()
return
draft.payload = payload
@@ -575,7 +577,7 @@ async def draft_extract(payload: DraftExtractIn, background: BackgroundTasks,
@router.get("/drafts")
async def list_drafts(status: str | None = Query(default=None, description="按状态筛:drafting / questions_open / "
"path_pending / attached / rejected"),
"path_pending / attached / rejected / failed"),
session: AsyncSession = Depends(get_session)) -> dict[str, Any]:
"""口径草稿列表,最近改过的在前。只给计数和状态,正文用 GET /api/ledger/drafts/{id} 看。"""
from app.api.deps import ensure_role
@@ -658,7 +660,10 @@ async def patch_draft(draft_id: str, payload: DraftPatchIn, session: AsyncSessio
if not (isinstance(q, str) and q.strip() in drop)]
if payload.metrics is not None or payload.resolved is not None:
draft.payload = payload_data
if draft.status in ("drafting", "questions_open", "path_pending"):
metrics = payload_data.get("metrics")
# 起草失败的草稿补上真指标后要能往前走;没有指标时不重算,免得空草稿被当成「待接数据路径」
if isinstance(metrics, list) and metrics and draft.status in ("drafting", "questions_open",
"path_pending", "failed"):
draft.status = _status_for(payload_data)
await session.commit()
return await _draft_view(session, draft)
+2 -1
View File
@@ -22,7 +22,8 @@ from collections.abc import Iterable, Mapping
from typing import Any
#: 草稿的状态。path_pending 不是合法图,attached 之后取数路径才来自一次真实运行。
STATUSES = ("drafting", "questions_open", "path_pending", "attached", "rejected")
#: failed 是起草这一步没跑成(没有可用模型、模型回复读不懂),和 rejected(人判它不是口径)不是一回事。
STATUSES = ("drafting", "questions_open", "path_pending", "attached", "rejected", "failed")
#: 一次发给模型的总字数上限(按 chunk 边界截断,见 extract)
MAX_INPUT_CHARS = 40000
+1 -1
View File
@@ -220,7 +220,7 @@ async def test_a_model_that_is_unavailable_leaves_the_draft_with_an_error(client
monkeypatch.setattr(docdraft, "extract", unavailable)
body = (await client.post("/api/ledger/drafts/extract", json={"documents": [doc]})).json()
made = (await client.get(f"/api/ledger/drafts/{body['draft_id']}")).json()
assert made["status"] == "drafting" and made["error"] == "还没有接入可用的起草模型"
assert made["status"] == "failed" and made["error"] == "还没有接入可用的起草模型"
assert made["metric_count"] == 0 and not made.get("metrics")
+3 -1
View File
@@ -230,13 +230,14 @@ export const LEDGER_UI = {
subtitle: '文档里写的是业务说法,这里整理成定义草稿;取数路径接上一次真实运行',
back: '返回口径目录',
refresh: '刷新',
/** 列表按状态筛:全部加四种落点,起草中的只出现在全部里 */
/** 列表按状态筛:全部加五种落点,起草中的只出现在全部里 */
filter: {
all: '全部',
questions_open: '待确认问题',
path_pending: '待接数据路径',
attached: '已接上',
rejected: '已驳回',
failed: '起草失败',
} as Record<string, string>,
status: {
drafting: '起草中',
@@ -244,6 +245,7 @@ export const LEDGER_UI = {
path_pending: '待接数据路径',
attached: '已接上',
rejected: '已驳回',
failed: '起草失败',
} as Record<string, string>,
filterLabel: '按状态筛',
summary: (metrics: number, questions: number) => `${formatNumber(metrics)} 个指标 · ${
+3 -3
View File
@@ -28,12 +28,12 @@ const D = LEDGER_UI.drafts
/** 列表的状态筛:全部加四种落点,起草中的只出现在全部里 */
type Filter = '' | Exclude<CaliberDraftStatus, 'drafting'>
const FILTERS: Filter[] = ['', 'questions_open', 'path_pending', 'attached', 'rejected']
const FILTERS: Filter[] = ['', 'questions_open', 'path_pending', 'attached', 'rejected', 'failed']
/** 状态标签:起草中 / 待确认问题 / 待接数据路径 / 已接上 / 已驳回 */
/** 状态标签:起草中 / 待确认问题 / 待接数据路径 / 已接上 / 已驳回 / 起草失败 */
function StatusPill({ status }: { status: CaliberDraftStatus }) {
const color = status === 'attached' ? 'var(--ok)' : status === 'rejected' ? 'var(--text-faint)'
: status === 'drafting' ? 'var(--accent)' : 'var(--warn)'
: status === 'drafting' ? 'var(--accent)' : status === 'failed' ? 'var(--err)' : 'var(--warn)'
return (
<span className="shrink-0 rounded border px-1.5 py-0.5 text-[10px]"
style={{ color, borderColor: `color-mix(in srgb, ${color} 40%, var(--border))` }}>
+1 -1
View File
@@ -444,7 +444,7 @@ export interface CaliberTrialOut {
* 草稿的状态(app/ledger/docdraft.STATUSES):起草中 → 有待确认的问题 / 待接数据路径 → 已接上;
* 另有已驳回。path_pending 不是合法图,取数路径只能来自一次真实运行。
*/
export type CaliberDraftStatus = 'drafting' | 'questions_open' | 'path_pending' | 'attached' | 'rejected'
export type CaliberDraftStatus = 'drafting' | 'questions_open' | 'path_pending' | 'attached' | 'rejected' | 'failed'
/** 一条引用:逐字原文、原文所在段落(chunk)、span 是原文里的 [start, end) */
export interface DraftCitation {