阶段 当前状态 说明 Phase 0~3 基本完成 Document IR、证据链、人工确认、不可变 PlanData Snapshot、海报/PPT 投影已实现 Phase 4 代码完成 场景、策略、模板版本,校验/发布门禁,确定性 resolver 和严格槽位合并已实现 Phase 5 代码完成 输入冻结、PPTX/海报对账、失败关闭、幂等、心跳、重试和冻结输入重放已实现 Phase 6 基础完成 质量看板、结构化告警、Golden 审批、留存清理、孤儿检查和灰度开关已实现 正式上线 未完成 缺真实样本、生产模板、业务规则签字和灰度观察 目前验证基线: PPT/海报专项测试:107 passed, 1 skipped Vue TypeScript 检查:通过 前端生产构建:通过 代码变更仍在工作区,尚未提交 仓库全量测试仍有既有失败/挂起项,暂时不能宣称全仓测试完全绿色 仍未完成的代码任务主要有: 影子解析差异流水线 目前有灰度开关,但还没有完整的“新旧解析同时运行、字段差异入库、按保司/profile 聚合”的影子比较任务。 自动视觉回归 目前实现的是 DOM 模块、溢出、尺寸、文本和数值检查;还缺基于真实模板和标准图片的像素差异、字体缺失、遮挡和裁切回归。 告警通道接入 后台已经能产生结构化质量告警,但尚未自动推送到邮件、企微或其他通知通道。 留存任务生产化 dry-run、实删服务和失败审计已经具备,但尚未接入周期性 Celery/定时任务,也没有自动重试失败清理批次。 旧链路最终下线 旧 PPT 解析器和海报紧凑解析仍保留为回滚路径。需要全量灰度稳定后才能删除或彻底关闭写入口。 全仓测试收口 需要处理现有无关失败和挂起测试,建立真正全绿的 CI 基线。 仍需外部输入和生产环境完成的事项: 至少 30 份脱敏 Golden PDF,并完成双人标注和精确率验收。 业务专家确认派生公式、缺失值、可比较性和结论策略。 上传并标注真实生产 PPTX 的语义 shape、页面类型和容量。 安装生产字体并建立视觉基准图片。 实际执行数据库迁移 038~040。 完成留存 dry-run、实删演练以及 10% → 30% → 100% 灰度。 观察期通过后开启 SCENARIO_ENGINE_V2,目前它仍默认关闭;真实清理开关也默认关闭。 完整状态记录在 [PPT与海报Phase4至6补充实施记录](D:/work/code/python/coding/baodanagent/docs/PPT与海报Phase4至6补充实施记录_20260802.md)。
157 lines
5.5 KiB
Python
157 lines
5.5 KiB
Python
"""PlanData Snapshot 鉴权 API。"""
|
|
from flask import Blueprint, jsonify, request
|
|
|
|
from insurance.middleware.auth_middleware import jwt_required
|
|
|
|
|
|
plan_snapshot_bp = Blueprint("insurance_plan_snapshots", __name__)
|
|
|
|
|
|
def _response(data=None, message="success", code=0, status=200):
|
|
return jsonify({"code": code, "message": message, "data": data}), status
|
|
|
|
|
|
def _run(action):
|
|
from insurance.plan_data.service import SnapshotError
|
|
|
|
try:
|
|
return action()
|
|
except SnapshotError as exc:
|
|
return _response(
|
|
{"errorCode": exc.code, **exc.data},
|
|
exc.message,
|
|
4201 if exc.status < 500 else 5001,
|
|
exc.status,
|
|
)
|
|
|
|
|
|
@plan_snapshot_bp.route("", methods=["POST"])
|
|
@jwt_required
|
|
def create_plan_snapshot():
|
|
from insurance.plan_data.service import create_draft_from_legacy, create_snapshot
|
|
|
|
user_id = str(getattr(request, "user_id", "guest"))
|
|
payload = request.get_json(silent=True) or {}
|
|
|
|
def action():
|
|
document_id = int(payload.get("documentId") or 0)
|
|
if isinstance(payload.get("planData"), dict):
|
|
snapshot = create_snapshot(document_id, user_id, payload["planData"], status="draft")
|
|
else:
|
|
snapshot = create_draft_from_legacy(
|
|
document_id,
|
|
user_id,
|
|
payload.get("extraction") or {},
|
|
evidence_entries=payload.get("evidence") or [],
|
|
)
|
|
return _response(snapshot.to_public_dict(), status=201)
|
|
|
|
return _run(action)
|
|
|
|
|
|
@plan_snapshot_bp.route("/<int:snapshot_id>", methods=["GET"])
|
|
@jwt_required
|
|
def get_plan_snapshot(snapshot_id: int):
|
|
from insurance.plan_data.service import SnapshotError, get_owned_snapshot
|
|
|
|
user_id = str(getattr(request, "user_id", "guest"))
|
|
|
|
def action():
|
|
snapshot = get_owned_snapshot(snapshot_id, user_id)
|
|
if not snapshot:
|
|
raise SnapshotError("SNAPSHOT_NOT_FOUND", "快照不存在", status=404)
|
|
return _response(snapshot.to_public_dict())
|
|
|
|
return _run(action)
|
|
|
|
|
|
@plan_snapshot_bp.route("/<int:snapshot_id>/evidence", methods=["GET"])
|
|
@jwt_required
|
|
def get_snapshot_evidence(snapshot_id: int):
|
|
from insurance.plan_data.service import list_evidence
|
|
|
|
user_id = str(getattr(request, "user_id", "guest"))
|
|
field_path = str(request.args.get("fieldPath") or "").strip()
|
|
page = max(1, request.args.get("page", 1, type=int))
|
|
page_size = min(100, max(1, request.args.get("pageSize", 50, type=int)))
|
|
return _run(lambda: _response(list_evidence(snapshot_id, user_id, field_path, page, page_size)))
|
|
|
|
|
|
@plan_snapshot_bp.route("/<int:snapshot_id>/draft", methods=["PATCH"])
|
|
@jwt_required
|
|
def update_snapshot_draft(snapshot_id: int):
|
|
from insurance.plan_data.service import patch_draft
|
|
|
|
user_id = str(getattr(request, "user_id", "guest"))
|
|
payload = request.get_json(silent=True) or {}
|
|
return _run(lambda: _response(patch_draft(
|
|
snapshot_id,
|
|
user_id,
|
|
expected_hash=str(payload.get("expectedHash") or ""),
|
|
changes=payload.get("changes") or [],
|
|
).to_public_dict()))
|
|
|
|
|
|
@plan_snapshot_bp.route("/<int:snapshot_id>/validate", methods=["POST"])
|
|
@jwt_required
|
|
def validate_plan_snapshot(snapshot_id: int):
|
|
from insurance.plan_data.service import validate_snapshot
|
|
|
|
user_id = str(getattr(request, "user_id", "guest"))
|
|
return _run(lambda: _response(validate_snapshot(snapshot_id, user_id)))
|
|
|
|
|
|
@plan_snapshot_bp.route("/<int:snapshot_id>/confirm", methods=["POST"])
|
|
@jwt_required
|
|
def confirm_plan_snapshot(snapshot_id: int):
|
|
from insurance.plan_data.service import confirm_snapshot
|
|
|
|
user_id = str(getattr(request, "user_id", "guest"))
|
|
return _run(lambda: _response(confirm_snapshot(snapshot_id, user_id).to_public_dict(), status=201))
|
|
|
|
|
|
@plan_snapshot_bp.route("/<int:snapshot_id>/poster-projection", methods=["GET"])
|
|
@jwt_required
|
|
def poster_projection(snapshot_id: int):
|
|
from insurance.plan_data.projections import to_poster_projection
|
|
from insurance.plan_data.service import SnapshotError, get_owned_snapshot
|
|
|
|
user_id = str(getattr(request, "user_id", "guest"))
|
|
|
|
def action():
|
|
snapshot = get_owned_snapshot(snapshot_id, user_id)
|
|
if not snapshot:
|
|
raise SnapshotError("SNAPSHOT_NOT_FOUND", "快照不存在", status=404)
|
|
if snapshot.status != "confirmed":
|
|
raise SnapshotError("SNAPSHOT_NOT_CONFIRMED", "只有 confirmed 快照可以投影", status=422)
|
|
return _response({
|
|
"snapshotId": snapshot.id,
|
|
"snapshotHash": snapshot.snapshot_hash,
|
|
"projection": to_poster_projection(snapshot.plan_data()),
|
|
})
|
|
|
|
return _run(action)
|
|
|
|
|
|
@plan_snapshot_bp.route("/<int:snapshot_id>/deck-contract", methods=["GET"])
|
|
@jwt_required
|
|
def deck_contract(snapshot_id: int):
|
|
from insurance.plan_data.projections import to_deck_contract
|
|
from insurance.plan_data.service import SnapshotError, get_owned_snapshot
|
|
|
|
user_id = str(getattr(request, "user_id", "guest"))
|
|
|
|
def action():
|
|
snapshot = get_owned_snapshot(snapshot_id, user_id)
|
|
if not snapshot:
|
|
raise SnapshotError("SNAPSHOT_NOT_FOUND", "快照不存在", status=404)
|
|
if snapshot.status != "confirmed":
|
|
raise SnapshotError("SNAPSHOT_NOT_CONFIRMED", "只有 confirmed 快照可以投影", status=422)
|
|
return _response({
|
|
"snapshotId": snapshot.id,
|
|
"snapshotHash": snapshot.snapshot_hash,
|
|
"deckContract": to_deck_contract(snapshot.plan_data(), document_sha256=snapshot.document_sha256),
|
|
})
|
|
|
|
return _run(action)
|