feat(ocr): 扫描件与图片识别——OCR 进文字层、资质库上传识别、大图浏览

计划见 docs/architecture/vision-plan.md(VS1–VS13,docs/decisions.md)。

- 验证:OCR(RapidOCR + PP-OCRv6 small,CPU)和 DeepSeek Flash 看图在国动投标文件
  和 7 张证书上初测(tech-validation §7,代码 spikes/s5-ocr/)
- OCR 服务 services/ocr(镜像、Compose 服务):整页转正(含 180°)、红章附近按红色通道重读
- 扫描页和插图识别后并入文字层 v6,句子标 source=ocr;出处标「OCR」(迁移 0013);
  依据识别文字的问题降级,规则不查扫描附件;国动端到端见 check-eval §16
- 资质库上传识别:添加资质对话框最上面传证书照片,识别后填表,只预填和 OCR 文字
  对得上的字段;识别值留痕、照片挂成附件(迁移 0014,提示词 vision.cert_fields v1)
- 资质库改为查看详情(只读键值 + 附件缩略图),附件用大图浏览控件看,不开新标签页
  (设计规范 §7.23);PDF 附件按页出预览图

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
YANG JIANKUAN
2026-11-05 17:50:13 +08:00
parent 0b783ababa
commit 3344f09563
63 changed files with 4973 additions and 345 deletions

View File

@@ -0,0 +1,60 @@
"""资质库上传识别(vision-plan 第 2 步):上传证书扫描件 → OCR + 视觉模型识别 → 用户核对后保存成资质。
- qualification_scan:一次识别。原件按企业级密钥加密存对象存储;result 是识别结果(每张证书的字段、引用的 OCR 行、
校验结论)。没保存的识别 24 小时后连同原件删除(worker 定时任务);
- qualification.holder:证书主体(企业名、著作权人、持证人);
- qualification.ai_values:保存时的识别值快照(识别值和用户最终值都留痕,同清单项的 ai_values);
- ai_call.job_type 加 qualification_scan。
Revision ID: 0014
"""
from __future__ import annotations
from alembic import op
revision = "0014"
down_revision = "0013"
def upgrade() -> None:
op.get_bind().exec_driver_sql("""
ALTER TABLE qualification ADD COLUMN holder text;
ALTER TABLE qualification ADD COLUMN ai_values jsonb;
CREATE TABLE qualification_scan (
id uuid PRIMARY KEY,
tenant_id uuid NOT NULL REFERENCES tenant (id),
filename text NOT NULL,
object_key text NOT NULL,
sha256 bytea NOT NULL,
size_bytes bigint NOT NULL,
media_type text NOT NULL,
status text NOT NULL DEFAULT 'queued' CHECK (status IN ('queued', 'running', 'succeeded', 'failed')),
error_code text,
result jsonb,
created_by uuid NOT NULL REFERENCES user_account (id),
created_at timestamptz NOT NULL DEFAULT now(),
finished_at timestamptz,
UNIQUE (tenant_id, id)
);
CREATE INDEX ON qualification_scan (tenant_id, created_at);
ALTER TABLE qualification_scan ENABLE ROW LEVEL SECURITY;
ALTER TABLE qualification_scan FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON qualification_scan
USING (tenant_id = current_tenant()) WITH CHECK (tenant_id = current_tenant());
ALTER TABLE ai_call DROP CONSTRAINT ai_call_job_type_check;
ALTER TABLE ai_call ADD CONSTRAINT ai_call_job_type_check
CHECK (job_type IN ('parse_job', 'check_run', 'qualification_scan'));
""")
def downgrade() -> None:
op.get_bind().exec_driver_sql("""
-- ai_call 只追加、不能删行:job_type 的约束保持三种取值
DROP TABLE qualification_scan;
ALTER TABLE qualification DROP COLUMN ai_values;
ALTER TABLE qualification DROP COLUMN holder;
""")