阶段 当前状态 说明 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)。
133 lines
6.9 KiB
Python
133 lines
6.9 KiB
Python
"""统一生成任务模型。"""
|
||
import uuid
|
||
from sqlalchemy import BigInteger, Column, String, Text, Integer, TIMESTAMP, func
|
||
from insurance.db.compat import db
|
||
|
||
|
||
class GenerationTask(db.Model):
|
||
"""统一生成任务表(PPT + 海报)。
|
||
|
||
职责分离:
|
||
- 工作区(PptSession/PosterRecord)保存可编辑数据
|
||
- 任务表保存一次执行的不可变快照、状态和结果
|
||
"""
|
||
__tablename__ = "insurance_generation_tasks"
|
||
|
||
id = Column(String(36), primary_key=True, default=lambda: uuid.uuid4().hex)
|
||
user_id = Column(String(64), nullable=False, comment="用户 ID")
|
||
artifact_type = Column(String(10), nullable=False, comment="ppt/poster")
|
||
operation = Column(String(10), nullable=False, comment="parse/generate")
|
||
workspace_id = Column(String(36), nullable=False, comment="PptSession.id 或 PosterRecord.id")
|
||
title_snapshot = Column(String(200), default="", comment="提交时任务名称")
|
||
status = Column(String(20), nullable=False, default="queued",
|
||
comment="queued/running/done/failed/cancelled")
|
||
stage = Column(String(30), default="", comment="当前执行阶段")
|
||
progress = Column(Integer, default=0, comment="进度 0-100")
|
||
message = Column(String(500), default="", comment="用户可读进度")
|
||
error_code = Column(String(50), default="", comment="结构化错误代码")
|
||
error_message = Column(Text, nullable=True, comment="用户可读错误")
|
||
input_revision = Column(Integer, default=1, comment="本次生成使用的草稿版本")
|
||
input_snapshot_json = Column(Text, nullable=True, comment="不可变输入快照")
|
||
snapshot_id = Column(BigInteger, nullable=True, comment="主 PlanData 快照 ID")
|
||
snapshot_hash = Column(String(64), nullable=True, comment="单个或组合快照哈希")
|
||
scenario_version_id = Column(BigInteger, nullable=True)
|
||
scenario_hash = Column(String(64), nullable=True)
|
||
policy_version_id = Column(BigInteger, nullable=True)
|
||
policy_hash = Column(String(64), nullable=True)
|
||
template_version_id = Column(BigInteger, nullable=True)
|
||
template_hash = Column(String(64), nullable=True)
|
||
asset_sha256 = Column(String(64), nullable=True)
|
||
renderer_version = Column(String(100), nullable=True)
|
||
submit_revision = Column(Integer, nullable=True)
|
||
output_reconciliation_json = Column(Text, nullable=True)
|
||
visual_check_json = Column(Text, nullable=True)
|
||
result_state = Column(String(20), nullable=False, default="current")
|
||
output_json = Column(Text, nullable=True, comment="输出路径、页数、预览等")
|
||
idempotency_key = Column(String(100), nullable=True, comment="防止重复提交")
|
||
celery_task_id = Column(String(200), nullable=True, comment="Celery ID")
|
||
attempt_count = Column(Integer, default=0, comment="重试次数")
|
||
heartbeat_at = Column(TIMESTAMP, nullable=True, comment="Worker 心跳")
|
||
started_at = Column(TIMESTAMP, nullable=True, comment="开始时间")
|
||
finished_at = Column(TIMESTAMP, nullable=True, comment="完成时间")
|
||
viewed_at = Column(TIMESTAMP, nullable=True, comment="用户查看时间")
|
||
dock_hidden_at = Column(TIMESTAMP, nullable=True, comment="从任务坞隐藏时间")
|
||
created_at = Column(TIMESTAMP, server_default=func.now())
|
||
updated_at = Column(TIMESTAMP, server_default=func.now(), onupdate=func.now())
|
||
|
||
def to_dict(self):
|
||
import json
|
||
from insurance.utils.public_payload import strip_server_paths
|
||
|
||
input_snapshot = json.loads(self.input_snapshot_json) if self.input_snapshot_json else None
|
||
output = json.loads(self.output_json) if self.output_json else None
|
||
return {
|
||
"id": self.id,
|
||
"userId": self.user_id,
|
||
"artifactType": self.artifact_type,
|
||
"operation": self.operation,
|
||
"workspaceId": self.workspace_id,
|
||
"titleSnapshot": self.title_snapshot,
|
||
"status": self.status,
|
||
"stage": self.stage,
|
||
"progress": self.progress,
|
||
"message": self.message,
|
||
"errorCode": self.error_code,
|
||
"errorMessage": self.error_message,
|
||
"inputRevision": self.input_revision,
|
||
"inputSnapshot": strip_server_paths(input_snapshot),
|
||
"snapshotId": self.snapshot_id,
|
||
"snapshotHash": self.snapshot_hash,
|
||
"scenarioVersionId": self.scenario_version_id,
|
||
"scenarioHash": self.scenario_hash,
|
||
"policyVersionId": self.policy_version_id,
|
||
"policyHash": self.policy_hash,
|
||
"templateVersionId": self.template_version_id,
|
||
"templateHash": self.template_hash,
|
||
"assetSha256": self.asset_sha256,
|
||
"rendererVersion": self.renderer_version,
|
||
"submitRevision": self.submit_revision or self.input_revision,
|
||
"outputReconciliation": json.loads(self.output_reconciliation_json) if self.output_reconciliation_json else None,
|
||
"visualCheck": json.loads(self.visual_check_json) if self.visual_check_json else None,
|
||
"resultState": self.result_state or "current",
|
||
"output": strip_server_paths(output),
|
||
"idempotencyKey": self.idempotency_key,
|
||
"celeryTaskId": self.celery_task_id,
|
||
"attemptCount": self.attempt_count,
|
||
"heartbeatAt": self.heartbeat_at.isoformat() if self.heartbeat_at else None,
|
||
"startedAt": self.started_at.isoformat() if self.started_at else None,
|
||
"finishedAt": self.finished_at.isoformat() if self.finished_at else None,
|
||
"viewedAt": self.viewed_at.isoformat() if self.viewed_at else None,
|
||
"dockHiddenAt": self.dock_hidden_at.isoformat() if self.dock_hidden_at else None,
|
||
"createdAt": self.created_at.isoformat() if self.created_at else None,
|
||
"updatedAt": self.updated_at.isoformat() if self.updated_at else None,
|
||
}
|
||
|
||
@staticmethod
|
||
def create_task(user_id: str, artifact_type: str, operation: str,
|
||
workspace_id: str, title: str = "",
|
||
input_snapshot: dict = None, idempotency_key: str = None,
|
||
input_revision: int = None):
|
||
"""创建新任务(幂等检查)。"""
|
||
import json
|
||
if idempotency_key:
|
||
existing = GenerationTask.query.filter_by(
|
||
user_id=user_id,
|
||
idempotency_key=idempotency_key,
|
||
).filter(GenerationTask.status.in_(["queued", "running", "done"])).first()
|
||
if existing:
|
||
return existing
|
||
|
||
task = GenerationTask(
|
||
user_id=user_id,
|
||
artifact_type=artifact_type,
|
||
operation=operation,
|
||
workspace_id=workspace_id,
|
||
title_snapshot=title,
|
||
input_snapshot_json=json.dumps(input_snapshot, ensure_ascii=False) if input_snapshot else None,
|
||
input_revision=input_revision or 1,
|
||
idempotency_key=idempotency_key,
|
||
)
|
||
db.session.add(task)
|
||
db.session.commit()
|
||
return task
|