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

@@ -1277,6 +1277,63 @@ export interface paths {
patch?: never;
trace?: never;
};
"/qualifications/{qualification_id}/files/{file_id}/previews": {
parameters: {
query?: never;
header?: never;
path: {
qualification_id: components["parameters"]["QualificationId"];
file_id: components["schemas"]["Id"];
};
cookie?: never;
};
/** 附件的预览图(图片一张,PDF 每页一张,最多 10 页),详情和大图浏览用 */
get: operations["getQualificationFilePreviews"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/qualification-scans": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** 上传证书扫描件识别(后台识别,轮询结果;不扣额度,每个企业每天限 100 次) */
post: operations["createQualificationScan"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/qualification-scans/{scan_id}": {
parameters: {
query?: never;
header?: never;
path: {
scan_id: components["schemas"]["Id"];
};
cookie?: never;
};
/** 识别结果 */
get: operations["getQualificationScan"];
put?: never;
post?: never;
/** 丢弃识别结果(连同原件) */
delete: operations["deleteQualificationScan"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/qualifications/{qualification_id}/files/{file_id}": {
parameters: {
query?: never;
@@ -1890,6 +1947,8 @@ export interface components {
quote: string;
quads: components["schemas"]["Quad"][];
created_by_kind: components["schemas"]["Origin"];
/** @description 原句是扫描页或插图上 OCR 识别出来的,可能有错字(界面标「识别文字」) */
ocr: boolean;
};
/** @description 给用户看的失败原因;不含文档内容 */
JobError: {
@@ -2331,6 +2390,8 @@ export interface components {
/** @description 界面上必填(查标时用它核对附件),接口可空,以后批量导入时不一定读得全 */
cert_no?: string | null;
issuer?: string | null;
/** @description 证书主体(企业名、著作权人、持证人) */
holder?: string | null;
/** Format: date */
valid_from?: string | null;
/**
@@ -2339,8 +2400,93 @@ export interface components {
*/
valid_to?: string | null;
};
/** @description 新增、修改资质。从识别结果保存时带上 scan:记下识别值,原件挂成附件(同一个原件不重复挂) */
QualificationSave: components["schemas"]["QualificationInput"] & {
scan?: {
scan_id: components["schemas"]["Id"];
/** @description 识别结果里的第几张证书(从 0 数) */
index: number;
} | null;
};
/** @description 一次证书识别(vision-plan 第 2 步)。没保存的 24 小时后删除 */
QualificationScan: {
id: components["schemas"]["Id"];
filename: string;
/** @enum {string} */
status: "queued" | "running" | "succeeded" | "failed";
error: null | components["schemas"]["ScanError"];
pages: {
page: number;
/** @description 识别用的页面图宽(像素),高亮框按它换算成比例 */
width: number;
height: number;
/** @description OCR 转正用的角度(0 / 90 / 180 / 270) */
rotation: number;
/** @description 页面图的短时签名地址 */
url: string;
}[];
certificates: components["schemas"]["ScanCertificate"][];
created_at: components["schemas"]["Timestamp"];
};
ScanError: {
code: string;
message: string;
};
ScanCertificate: {
page: number;
/** @enum {string} */
category: "enterprise_qualification" | "system_cert" | "software_copyright" | "product" | "personnel" | "business_license" | "other";
kind: components["schemas"]["QualificationKind"];
/** @description 建议的资质名称:证书标题,后面括注软件名或专业 */
name: string;
/**
* @description 证书编号取哪个字段(软著取登记号)
* @enum {string}
*/
cert_no_field: "cert_no" | "reg_no";
/** @description 字段名 → 识别结果。name、holder、level、specialty、software、cert_no、reg_no、issuer、issued_on、valid_from、valid_to、validity_text */
fields: {
[key: string]: components["schemas"]["ScanField"];
};
/** @description 证书上有、但被打码或看不清的字段,要手动填写 */
masked: string[];
/** @description 推算的建议值,用户点了才填。valid_to:证书只写了有效期长度(如「五年」)时按发证日期推算 */
suggest: {
/** Format: date */
valid_to?: string;
};
notes: {
/** @enum {string} */
code: "personnel" | "product" | "business_license" | "holder_mismatch";
holder?: string;
tenant?: string;
}[];
/** @description 资质库里证书编号相同的一条(换证时建议更新它) */
existing: null | {
id: components["schemas"]["Id"];
name: string;
};
};
ScanField: {
/** @description 证书上的原样文字 */
value: string;
/**
* @description ok / date_ok 才自动填;low_conf(马赛克、模糊)、mismatch(识别结果和 OCR 对不上)等只作建议
* @enum {string}
*/
check: "ok" | "date_ok" | "low_conf" | "mismatch" | "bad_line" | "bad_format";
/**
* Format: date
* @description 日期字段换算成的日期
*/
day?: string;
/** @description 所引 OCR 行在页面图上的位置(像素)[x0, y0, x1, y1] */
boxes: number[][];
};
Qualification: components["schemas"]["QualificationInput"] & {
id: components["schemas"]["Id"];
/** @description 是从识别结果保存的(识别值留痕) */
recognized: boolean;
files: {
id: components["schemas"]["Id"];
filename: string;
@@ -4717,7 +4863,7 @@ export interface operations {
};
requestBody: {
content: {
"application/json": components["schemas"]["QualificationInput"];
"application/json": components["schemas"]["QualificationSave"];
};
};
responses: {
@@ -4744,7 +4890,7 @@ export interface operations {
};
requestBody: {
content: {
"application/json": components["schemas"]["QualificationInput"];
"application/json": components["schemas"]["QualificationSave"];
};
};
responses: {
@@ -4809,6 +4955,117 @@ export interface operations {
};
};
};
getQualificationFilePreviews: {
parameters: {
query?: never;
header?: never;
path: {
qualification_id: components["parameters"]["QualificationId"];
file_id: components["schemas"]["Id"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
file_id: components["schemas"]["Id"];
filename: string;
pages: {
page: number;
width: number;
height: number;
/** @description 预览图(JPEG)的短时签名地址 */
url: string;
}[];
};
};
};
};
};
createQualificationScan: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
upload_id: components["schemas"]["Id"];
};
};
};
responses: {
/** @description 已排队 */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["QualificationScan"];
};
};
429: components["responses"]["RateLimited"];
/** @description `ocr_unavailable`:扫描件识别服务没有开或连不上,可以先手动填写 */
503: {
headers: {
[name: string]: unknown;
};
content: {
"application/problem+json": components["schemas"]["Problem"];
};
};
};
};
getQualificationScan: {
parameters: {
query?: never;
header?: never;
path: {
scan_id: components["schemas"]["Id"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["QualificationScan"];
};
};
};
};
deleteQualificationScan: {
parameters: {
query?: never;
header?: never;
path: {
scan_id: components["schemas"]["Id"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description 已删除 */
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
getQualificationFileUrl: {
parameters: {
query?: never;