# 智能质检报告 — AI 导入模板 前端联调方案 > 后端模块:`yudao-module-qcreport` 接口前缀:`/admin-api/qc-report/ai-import` > 本轮范围:**上传一份已有的检验报告文件 → AI 读懂 → 产出模板草稿**。 > **接口不落库**,草稿只是「一串组件 + 各自的属性」;人工在设计器确认后点既有的「保存」才会写成模板版本。 ## 涉及页面 | 页面 | 说明 | 本轮状态 | |---|---|---| | 模板设计器(既有) | 顶栏「AI 导入」按钮 → 弹窗选文件 → 识别 → 预览 → 「生成到画布」 | 已接入 | | 模板列表页 | 本轮**不做**「AI 新建模板」入口,见「注意事项」 | 不做 | ## 业务流程与数据带入 1. **进入设计器**:必须带 `templateId` 进入(`/qc/report-template/designer?id=5`)。 AI 导入只在既有模板的设计器内,`templateId` 是后续「绑附件」与「日志留痕」的主键。 2. **上传**:用户选文件后调 system 模块既有的上传接口拿 `blobId`。 **必须保持用户的选择顺序**——后端按这个顺序理解文档页序。**这一步不发识别请求**,用户点头才发。 3. **绑附件**:拿到 `blobId` 后**立即**调 system 模块既有的附件绑定接口,把文件归属到当前模板 (`application='file'`、`recordType='qc_report_template'`、`recordId=templateId`)。 ⚠ `application` 受后端枚举限制,**只能是 `file` / `image` / `avatar`**,传其它值会 500。 **先绑再识别**:识别即使失败,文件也已归属到模板名下,不会留成没人认领的孤儿文件。 4. **识别**:调本文档的 `draft` 接口,带上 `blobIds`、`templateId`、`schemaVersion`、`catalog`、可选的 `hint`。 **这条请求要单独放宽超时到 180 秒**(单次最长要发 5 次大模型调用,默认超时会在模型返回前就掐断)。 5. **预览**:弹窗列识别出的组件(类型显示名 + 已识别的属性)、`summary`、`warnings`。 这一步**没有任何写操作**,用户可以反复重新上传。 6. **生成到画布**:画布非空时先弹替换确认(**替换后无法用撤销恢复**)→ 前端装配器把草稿编译成画布数据装进去。 7. **保存**:人工在设计器调整后点**既有的**「保存」→ 走既有的版本保存接口。 **这是唯一的落库点,AI 这条路没有第二条保存路径**,因此既有的模板校验(含判定规则)自动生效。 **带入关系要点** - `templateId` 是主线,从模板列表带入设计器、再带入 AI 导入与附件绑定。 - `catalog`(积木清单)**每次请求都从当前前端注册表现算一遍再传**,不要在应用里缓存成常量: 缓存它等于把「注册表是唯一真相来源」这条前提悄悄作废。 - `blobIds` 的顺序即文档页序,多页文件按这个顺序逐页识别、最后合并去重。 ## API | 方法 | 路径 | 说明 | 权限码 | |---|---|---|---| | POST | `/qc-report/ai-import/draft` | 识别文件生成模板草稿。**同步返回、不落库、不建版本行** | `qc-report:template:ai-import` | > ⚠ 本轮**未**往 `system_menu` 插入 `qc-report:template:ai-import` 权限行(与出件接口同一口径)。 > 当前 `admin` 是超管、直接放行,联调不受影响;**前端页面正式落地时需一并补菜单与按钮权限**, > 否则非超管角色调本接口会 403。 ### 请求参数 | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `blobIds` | Long 数组 | **是** | 上传文件对应的 blobId。**顺序即文档页序**;最多 3 个(该上限由后端 `yudao.qcreport.ai-import.max-files` 决定,超限报 `1_070_104_005`),单个文件最大 20MB | | `templateId` | Long | **是** | 目标模板编号,用于日志留痕与附件归属 | | `schemaVersion` | String | **是** | 固定传 `1.1`。与设计器同源,两端不一致时后端直接报错 | | `catalog` | Object 数组 | **是** | 可用组件积木清单,见「`catalog` 的构造方式」;最多 40 项 | | `hint` | String | 否 | 用户补充说明,会拼进提示词,例如「这是 IQC 来料检验报告」 | **`catalog` 一项的结构** | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `type` | String | **是** | 组件类型,如 `QualityTable`。**大小写敏感**,模型会原样抄进结果 | | `label` | String | 否 | 组件显示名,供模型理解语义 | | `category` | String | 否 | 组件分类,如 `basic` / `data` / `result` | | `hint` | String | **是** | 该组件的**选型说明**:用来放什么、不要用来放什么。模型选型以它为准,见「选型指引必须随清单带出」 | | `fields` | Object 数组 | 否 | 该组件允许填写的属性 | > `hint` 标为**是**不是因为后端强制(后端收到空值不报错),而是因为**漏带它 = 模型退回瞎猜**: > 组件之间大量可互相替代(报告抬头可写 `Heading` / `Text` / `ReportHeader`), > 模型只看中文 `label` 时会挑字段最简单、最通用的那个。实测漏带前的表现就是 > 「页眉页脚位置用了文本组件」。 **`catalog[].fields[]` 一项的结构** | 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | `key` | String | **是** | 属性 key,如 `itemsPath` | | `label` | String | 否 | 字段显示名 | | `type` | String | 否 | 字段类型,如 `text` / `number` / `boolean` / `enum` | | `required` | Boolean | 否 | 是否必填;**只置真不置假** | | `bindable` | Boolean | 否 | 是否支持数据绑定;**只置真不置假** | | `enumOptions` | Object 数组 | 否 | 枚举可选值,元素形如 `{ label: '横向', value: 'landscape' }`。**仅在 `type=enum` 时有意义** | ### 响应字段 | 字段 | 类型 | 说明 | |---|---|---| | `components` | Object 数组 | 识别出的组件草稿,按报告从上到下的顺序;已做过合并去重与非法项过滤 | | `components[].type` | String | 组件类型,**保证是 `catalog` 里声明过的 type** | | `components[].props` | Object | 组件属性。清单里未声明的 key 已被后端丢弃 | | `page` | Object | AI 猜测的纸张配置,**可能为空**(为空表示沿用当前模板的纸张) | | `page.size` | String | `A3` / `A4` / `A5` / `Letter` | | `page.orientation` | String | `portrait` / `landscape` | | `page.margin` | Object | `top` / `right` / `bottom` / `left`,单位 mm | | `summary` | String | AI 对这份文档的一句话说明,展示在预览弹窗顶部 | | `warnings` | String 数组 | 软提示:被跳过的页、无法表达的区块、被丢弃的未知组件、**原件带页眉/页脚已被标记并入识别内容**等 | | `durationMs` | Long | 识别总耗时(毫秒),含多次模型调用 | **响应示例** ```json { "code": 0, "data": { "components": [ { "type": "ReportHeader", "props": { "title": "来料检验报告" } }, { "type": "QualityTable", "props": { "itemsPath": "inspectionItems" } }, { "type": "Result", "props": { "conclusion": "{{report.conclusion}}" } } ], "page": { "size": "A4", "orientation": "portrait" }, "summary": "这是一份 IQC 来料检验报告,含表头、检验项目表格与结论栏。", "warnings": ["第 2 页的判定规则说明无法用可用组件表达,已丢弃"], "durationMs": 12400 } } ``` > ⚠ **响应里没有 `rawText` 字段**,这是有意的:完全解析不出结果时接口直接抛错 > (`1_070_104_012`),响应体是错误结构而不是本响应;塞一个恒为空的字段只会让人误以为 > 「解析失败时前端能拿到模型原文」。模型原文写进了服务端日志。 ## `catalog` 的构造方式 `catalog` 不是手写的常量表,而是**从设计器实际使用的组件注册表现算出来的派生视图**。 | 要点 | 说明 | |---|---| | **来源** | 设计器实际使用的那个组件注册表(`src/components/quality`),它是组件清单的**唯一真相来源** | | **何时算** | 每次发起识别请求前现算一遍 | | **必须先去注册** | 注册表**只有显式注册过才有内容**。设计器初始化时已做过一次,弹窗里再补一次是幂等的兜底 | | **剥掉什么** | 剥离不可序列化的实现成员(图标、构建函数、校验函数、默认值),只留「模型需要知道的」:类型、显示名、分类、**选型说明**、字段 | | **为什么不在后端硬编码** | 后端抄一份就是第二个真相来源,且是最坏的:前端加组件/改必填字段时前端立刻生效、后端副本静默过期,模型随即会编出注册表里根本不存在的 `type` | | **边界为什么没被削弱** | 清单进提示词后的唯一出口是「被模型抄成 JSON 字符串」。真正把关的是**前端装配器对着活注册表查**:未注册的 `type` 会被跳过,**永远产不出任意 HTML** | | **前置校验** | 注册表为空时构造函数**直接抛错**,不要发一个空清单出去——后端会判为入参非法,报出来的错和真实原因隔了好几层 | ### 选型指引必须随清单带出 `hint` 是**组件选型的唯一真相来源**,和组件定义同源:改注册表里某个组件的 `hint`,下次请求自动生效, 后端不需要(也不应该)同步任何一份副本。 | 要点 | 说明 | |---|---| | **写在哪** | 每个组件定义的一条 `aiHint` 文案,随清单序列化带出 | | **写什么** | 「用来放什么」+「**不要**用来放什么」。后者才是关键——模型误用的根因是「不知道某个通用组件不该用在这里」 | | **怎么用** | 后端只负责把它原样序列化进提示词,一条抽取规则要求模型「选型时以 hint 为准,清单里存在专门组件就不要退而用通用组件凑数」 | | **不写会怎样** | 模型只看到组件中文名,会挑字段最简单的那个。实测症状就是:报告抬头与落款全落到了通用文本组件上 | | **和设计器界面的关系** | `hint` **不进设计器界面**,它是给模型看的说明,不是组件的展示名或属性提示 | ## 草稿 → 画布:编译契约 草稿**不能直接塞进画布**。它要先经前端的装配器编译成画布数据,再交给设计器载入(与「打开一份已保存的模板」走完全同一条路)。 **装配器(`assembleQualityCanvas`)入参 / 出参** | 方向 | 名字 | 类型 | 说明 | |---|---|---|---| | 入 | `components` | 草稿组件数组 | 即响应里的 `components`;可为空 | | 出 | `grapes` | Object | 画布数据,喂给设计器的载入方法 | | 出 | `skipped` | Object 数组 | 被跳过的组件:`{ type, label, reason }`。**`reason` 是给人看的中文,直接展示** | | 出 | `problems` | String 数组 | 装进去了但有问题:必填缺失、收到结构化数据而回落默认值等 | **逐项行为(与既有「从画布收集语义」互为逆运算)** | 情况 | 行为 | |---|---| | `type` 取不到注册表里的定义 | 进 `skipped`,**继续处理下一个**,不整批失败 | | `type` 为空 / 缺失 | 同上,进 `skipped` | | AI 漏了某些字段 | 用组件声明的默认值补齐(不会出现「组件缺属性」) | | AI 编了清单外的属性 key | **静默丢弃**(属性序列化只输出声明过的 key) | | 某个字段收到对象 / 数组 | 记一条 `problems`,该字段按默认值处理(防止 `[object Object]` 被写进画布) | | 必填字段为空 | 记一条 `problems`,**组件仍然装进画布**(用户可以在设计器里补) | | 一个组件都装不进去 | **不动画布**,只报错。载入动作会清空撤销栈与已有内容,白白搭进去一版画布却什么都换不来 | **载入动作的两个副作用(必须让用户知情)** - **清空撤销栈**:装完之后**无法用撤销恢复**原来的画布。所以画布非空时要先弹确认,并建议用户先保存当前内容。 - **会用传入的判定规则覆盖当前规则**:装配路径**显式沿用当前已编辑的规则**,不会把用户写好的判定规则静默清空。 > **装配产物不能直接落库。** 出参 `grapes` 里每个节点的 `components` 仍是**未解析的 HTML 字符串**——它必须先进画布(GrapesJS 会把它拆成节点树)才能保存。绕过设计器直接把这串写进 `schema_json`,后端渲染器不会解析它,出件时组件会渲成空 div。 > > 同理,若要程序化地把组件放进画布,**要把节点对象交给画布,不要自己拼 HTML 串再解析**:HTML 解析器会把属性名转小写(`data-qc-itemsPath` → `data-qc-itemspath`),属性面板与渲染都会绑不上。设计器内部走的就是「直接传对象」这条路径。 ## AI 可填的模板结构属性(列定义 / 表头跨列 / 样品字段) 原件的检验表**列**常常比组件默认列多(「检测方法」「标准要求」「结论」等),样品信息块的字段也常与默认六项不同。为此新增三个可填属性,AI 会把原件的列与字段写进去。**这三个属性都是纯文本字符串,不是数组**——属性值在画布上是标量,数组存不进去。 | 属性 | 挂在哪个组件 | 作用 | |---|---|---| | `columns` | 分组检验项表 | 覆盖该表的列:列标题 + 每列绑定的数据 | | `headerSpans` | 分组检验项表 | 表头分上下两层时,描述**上面那一层** | | `fields` | 样品信息 | 覆盖样品信息块展示的字段 | ### `columns` 的写法 一行字符串,列与列之间用 `|` 分隔,每列写作「列标题=绑定表达式」,**在第一个 `=` 处切分**(所以列标题里不能出现 `=`)。 - 列标题前加 `#` = 该列要**纵向合并跨住整组**(放组名的那一列),一表只有一列该加。 - **填了 `columns` 整表就按它来**,「显示序号 / 显示检测要求 / 显示实测值 / 显示单位 / 显示判定」这些开关全部失效(它们只管默认列)。 - 列标题与绑定表达式都会做 HTML 转义,用户写什么都不会变成可执行的标签。 **可用的绑定路径是白名单**,写白名单外的路径会在校验时报错:`{{index}}` 行序号、`{{item.itemName}}` 检验项目名、`{{item.group.childName}}` 子项名、`{{item.checkMethod}}` 检测方法、`{{item.requirement}}` 检测要求、`{{item.standardValue}}` 标准值、`{{item.actualValue}}` 实测值、`{{item.unit}}` 单位、`{{item.resultText}}` 判定结论、`{{item.remark}}` 备注。 **给分组表写 `columns` 时必须保留「子项」列**(`子项={{item.group.childName}}`),且它不加 `#`。分组表的默认列本来就有这一列,AI 把列写全时最容易漏掉它——一漏,子项名整列从报告上消失,而子项往往才是标准真正考核的对象。实测:不写明这条时 3/3 轮都把子项列丢掉(只写出 4 列),写明后 5/5 轮保留、且与原件的 6 列逐列对齐。 ### 「#」那一列到底怎么合并的 合并**不在前端算,也画布上看不见**,全部由数据下发、出件时才生效: | 环节 | 谁做 | 做什么 | |---|---|---| | 标出合并列 | 模板 | `columns` 里给该列标题加 `#`,组件据此只在这一格写 `rowspan` / `hidden` 两个绑定 | | 算出合并几行 | 后端 | 组的第一行下发 `span = 本单该组的实际行数`、`hidden = 空串`;组内其余行下发 `span = 1`、`hidden = hidden` | | 渲成视觉合并 | 前端 / Java 渲染器 | 两边都在「求值结果为空串」时不输出该属性。于是组头行的 `hidden` 消失、那一格带着 `rowspan` 显示;其余行的 `hidden="hidden"` 让整格 `display:none`,把位置让给上方的合并格 | 三条由此推出的口径: - **合并行数是「本单该组实际出了几行」,不是指标主数据里该分组挂了多少个子项。** 本单少录两个子项,格子就只跨那几行。 - **该组在本单没有子项行时不合并**(后端按 `span = 1` 下发),独立录入项同理——本来就只有一行,无所谓跨行。 - **`hidden` 只能靠「属性在不在」表达**,所以这个值必须是字符串:`hidden="false"` 照样隐藏,只有空串能表示「不输出该属性 = 这一格可见」。前端不要把它当布尔值处理。 ### 自定列的列宽:长串不许把表格撑出纸张 自定列**不带任何宽度声明**(默认列带:序号 48px、检测要求 30%、实测值 26%……),所以整张表的列宽只能由各列的**最小内容宽度**倒推。而报告里的值是 `{{item.standardValue}}` 这种不含空格的长串,最小内容宽度就是整串长度——几列加起来轻易超过 A4 正文宽度,浏览器只能把表格撑出纸张,最末一列被挤成二十几个像素的竖条。 修法:**只在自定列时**给单元格加 `overflow-wrap:anywhere`。它会参与最小内容宽度计算(`break-word` 不会),列宽因此不再被长串绑架;出件时值本来就短(「烘箱干燥法」「13.2」),折行极少触发。 | 场景(A4 纵向,正文宽 672px) | 修前 | 修后 | |---|---|---| | 自定 6 列,值为占位符 | 整表 810px、溢出 138px、末列 29px | 整表 672px、不溢出、末列 97px | | 自定 6 列,值为真实数据 | 本就 672px | 672px,列宽分布不变 | | **默认列**(分组表 7 列) | 807px、溢出 135px | **保持原样** | 最后一行是刻意为之:默认列的宽度声明已经定住版面,再叠一条折行反而会让没写宽度的「检验项目」「子项」被挤到 30 余像素、行高从 59px 涨到 283px(实测)。因此这条只挂在自定列上,**没有 `columns` 的存量模板产物逐字不变**(探针以 sha256 守着)。 > 自定列的属性是随本轮一起进的,存量模板与已发布版本里一条都没有(全库实测 `data-qc-columns` / `data-qc-headerSpans` / `data-qc-fields` 命中数均为 0),因此这条改动对既有报告零影响。 ### `headerSpans` 的写法 同样是 `|` 分隔的一行字符串,每段写作「标题^跨越列数」,**在最后一个 `^` 处切分**;不写跨越列数时按 1 算。 - **下面那一层由 `columns` 各列的标题自动拼出,不要重复写**——`headerSpans` 只描述上面那一层。 - **各段跨越列数之和必须等于列数**。对不上时整层会被丢弃、退回单层表头(不报错、不崩),所以这条不满足时用户看到的是「两层表头没生效」而不是一条错误。 - 原件最右边那列「结论」/「判定」在原件里不属于上层分组标题时**要单独成段**(`结论^1`)。不点明时模型会图省事把它的列数并进「检验结果」(写成 `检验结果^6`),表头就把结论列画到检验结果底下。 - **某列自己既是列名又是上格时**(原件里是一格纵向合并、占满上下两层表头,提取文本里表现为下层表头那一格写着「↑同上」),仍照写一段、跨越列数写 1,但该段标题必须与 `columns` 里这一列的列标题**一字不差**。两者相同时报告会把它合出一格跨两行(`rowspan`),下层不再重复出这一格;不一致时会被当成另一层的新分组标题,「结论」二字在表头上印两遍。 - 这是本轮修掉的一处真缺陷:模型此前 5/5 轮都写 `结论^1`、列名也叫「结论」,两层各印一次,用户看到的就是「多了一个结论」。补上「两边一字不差 ⇒ 合成一格跨两行」这条判据后,直连 5/5 轮合成为一格。 - 反例见实测:有 1/5 轮模型把 `columns` 里那一列改名成「判定」、`headerSpans` 却仍写着原件上的「结论」,两边对不上又退回多印一个表头。提示词里因此补了一句「不要自作主张把它改名成近义词」并写明后果,复跑 5/5 轮不再出现。 ### `fields` 的写法 语法与 `columns` 完全相同(字段名=绑定表达式,`|` 分隔),但**不允许 `#` 合并标记**(那是分组表标组名的)。每行放几组仍由「每行字段数」属性控制。 绑定表达式只能写报告上下文里确实存在的字段(与 `columns` 同受「不许自造键」约束);原件里有、上下文里没有的栏目(如「产品数量」「土豆品种」「生产日期」「有效日期」)会被丢弃,并在 `summary` 里说明。 字段数不是「每行字段数」的整数倍时(**原件常见**,如原件 5 项、每行放 2 组),末行由最后一个值格横向跨掉余下的列,右侧不会留出只有边框的空格。这是渲染期的固定行为,**不需要在 `fields` 里做任何标记**,也不会因此增减字段格数。 ### 画布显示与出件结果的差异 设计器画布**不求解绑定**——`{{item.group.childName}}` 这类占位符原样留着。由此有一处必须单独处理:分组检验项表的组名格写成 `hidden="{{item.group.hidden}}"`,在浏览器眼里「有 `hidden` 属性」就成立,于是画布里这一格被 `display:none`,该行后面的格整体左移一格,末列只剩表头、被挤成竖排细条——画布上看到的排版与出件结果对不上。 设计器为此只在**画布文档内**注入一条引导样式,把「值还停在占位符」的 `hidden` 还原成可见格。出件时该值已解成空串或 `hidden`,选择器不再命中,报告渲染一个字都不受影响;这条样式只加在画布 iframe 里,不写进 Schema、不随模板保存。 占位符带来的第二处差异是**宽度**:占位符比真值长得多,撑不撑得破纸张在两侧表现不同。这一处不走画布样式,而是把 `overflow-wrap:anywhere` 写进自定列的单元格样式里——两侧用同一套规则,所见即所得(详见上一节)。 ### 谁在什么时候校验这三个属性 只有**保存 / 发布前**的属性校验会检查它们(列定义语法、跨列数合计、`#` 用在样品信息上等),设计过程中的中间态不触发。校验信息走既有的 `problems` 通道(**非阻断**),组件仍会装进画布,用户可以在设计器里改。 > **已知边界**:AI 导入路径会跑这套校验;设计器里**手工编辑**后点保存**目前不跑**(`validateQualityProps` 只在装配器里被调用)。也就是说手工填错的列定义不会在保存时被拦住,只会在出件时以「绑定取不到值」的形式暴露。这是既有缺口,本轮未改。 ## 字段展示规则 | 字段 | 展示位置 | 说明 | |---|---|---| | `summary` | 弹窗顶部 | 成功色提示条。为空时不展示 | | `warnings` | 弹窗中部 | 警告色提示条,逐条 `·` 列出。**必须展示**——它承载「文件里有哪些内容没被识别到」 | | `components` | 弹窗中部列表 | 每行:序号 + 类型显示名 + 类型原始值 + 已识别的属性(`字段显示名=值`)。类型取不到定义时额外打「未注册,将被跳过」标记 | | `components` 条数 | 列表上方 | 「识别出 N 个组件」;有未注册项时追加「M 个组件设计器不认识」标记 | | `durationMs` | 列表上方 | 「耗时 N 秒」 | | `page` | 不进弹窗 | 并入设计器的纸张设置后再载入画布,见「业务规则」 | | `skipped` / `problems` | 载入后弹窗 | 「有 N 处需要留意」,逐条列出 | ## 业务规则说明 | 场景 | 规则 | |---|---| | **只作草稿,不落库** | 接口不建模板、不建版本行、不写画布。**唯一的落库点是设计器既有的「保存」**,所以既有的模板校验(含判定规则)自动生效 | | **多页逐页识别后去重合并** | 扫描件/照片按页各出一次草稿,再按「类型 + 属性」完全相同去重合并。页眉、标题、表头在多页重复出现时只留第一份 | | **`.doc` / `.xls` 旧格式不支持** | 直接报 `1_070_104_003`,提示另存为 `.docx` / `.xlsx`。`.docx` / `.xlsx` / `.pdf` / 图片 / `.txt` / `.csv` 支持 | | **「空白」与「损坏」分开报** | 扩展名合法但内容为空 → `1_070_104_005`;扩展名合法但文件打不开(损坏、带密码) → `1_070_104_015`。两者给用户的动作完全不同,不能合成一条 | | **PDF 走哪条通道由「有没有文本层」决定** | 电子版 PDF(有文本层)走文本通道,整份 1 次调用;扫描版 PDF(无文本层)逐页转图后按图识别 | | **表格只识别结构** | 表格的**具体数据行不会写进模板**,`QualityTable` 的数据源一律是 `inspectionItems`;真实数据在出件时由业务单据提供。**这一点必须向用户说明**,否则用户会以为文件里的数据也导进来了 | | **原件列/字段与默认不同时是覆盖,不是换组件** | 检验表列比默认多(检测方法 / 标准要求 / 结论…)→ 写 `columns`;表头分上下两层 → 写 `headerSpans`;样品信息字段与默认六项不同 → 写 `fields`。**一律不换组件**——换成别的组件只会把多出来的内容丢掉。三者的语法、白名单与校验见上文「AI 可填的模板结构属性」 | | **绑定表达式原样保留** | 文件里出现 `{{report.reportNo}}` 这类占位符时,AI 会原样保留,不会替换成它看到的字面值 | | **AI 不得自造绑定键** | 提示词明确禁止模型发明新的 `{{...}}` 键,并且**把可用的键全量列了出来**:报告级字段取自后端 `ReportFields`(报告号、样品名、规格、批号、检验员、检验日期、结论、合格率、计数等 23 个,反射生成、不手写),检验项字段取自表格组件的绑定路径。**只禁编造、不给可选集合时模型只能靠猜**——实测 5/5 轮都编出了 `{{report.productionDate}}` / `{{report.expiryDate}}` 这类不存在的键,报告上留白并触发「绑定取不到值」告警 | | **`.docx` 的 Word 页眉/页脚会被标记并入内容** | Word 文档的**页眉与页脚**(每页重复的公司抬头、厂址电话等)原本不在正文流里,现已被读取并以 `[页眉]` / `[页脚]` 标记拼进识别内容(页眉在正文前、页脚在正文后),同时往 `warnings` 里加一条提示。**扫描件/照片不受影响**:它们的页眉页脚本来就是页面像素的一部分 | | **表格末行的落款签署行属于落款** | 检验员 / 审核人 / 批准人 / 日期这类签署行**常排在检验表格的最后一行**。它不算「具体数据行」,应被取出来与原件页脚合成放进 `ReportFooter`(`ReportFooter` 与 `QualityTable` 两条选型说明里各自写明了这条边界) | | **签署行的人名与日期留空** | 签署行**保留栏目名、值留空**(形如「检验员:\_\_\_\_ 审核人:\_\_\_\_ 日期:\_\_\_\_」),打印出来由人手填。人名与日期是每一份报告各自的数据,写死原件上的那几个人会让所有报告印成同一个检验员 | | **落款允许多行,换行要真的折行** | 签署行与原件页脚合成一段**多行**文本交给 `ReportFooter`,几行内容**必须折行显示**,不能糊成一片。属性值里保留的是原始换行;**折行发生在渲染产物这一侧**——组件生成的画布 `content` 里是 `
`,渲染引擎按原样输出。`Text` 组件同样支持多行 | | **判定规则与数据绑定不由 AI 生成** | AI 只出组件的静态属性;判定规则、数据绑定一律保留默认值,由人工在设计器里配置 | | **纸张的合并规则** | AI 猜出的纸张并入当前设计器的纸张设置(**按字段合并**,AI 没猜到的字段沿用用户当前设置),再随载入动作一起生效。**不同步的话,保存时会被模板保存接口按旧值写回** | | **AI 猜不到纸张时不覆盖** | `page` 为空时**必须传当前设计器的纸张设置**,不能传空。传空会让载入动作退回默认 A4,把用户已经调好的纸张悄悄改掉 | | **组件数上限** | 识别结果最多 200 个组件,超出截断并在 `warnings` 里提示补录 | | **任何单个组件坏掉都不影响整体** | 未知类型、结构化属性、必填缺失都只进 `warnings` / `problems`,其余组件正常返回。**只有「一个组件都没识别出来」才整体报错** | | **功能可被运维关掉** | 部署环境不能出网时会关掉(`yudao.qcreport.ai-import.enabled=false`),此时接口报 `1_070_104_000`。前端应原样提示,不要当成系统异常 | ### 「正文抬头/落款」与「每页重复的页眉页脚」不是一回事 这条口径容易混淆,前端联调时务必对齐: | | 画布上的 `ReportHeader` / `ReportFooter` | 服务端出件时的页眉页脚 | |---|---|---| | **位置** | **正文流里**的顶部抬头块 / 底部落款块,随正文只出现一次 | 打印时**每页重复**的版式区域 | | **由谁生成** | AI 从文件里识别出来、装进画布,用户可编辑 | 服务端 PDF 出件配置(`PdfPrintOptions`)加的,目前只有页码 | | **本轮改动** | `.docx` 的 Word 页眉被读进来后,模型会把它挪进正文顶部的 `ReportHeader` | **未改动** | 也就是说:把 Word 页眉认成 `ReportHeader`,是**把它搬进正文顶部**,**不是**还原每页重复的打印版式。 真正每页重复的那一层仍由服务端 PDF 出件配置控制。**不要向前端承诺「导入后会得到每页重复的页眉」**。 ## 注意事项 - **不要缓存 `catalog`**:每次请求现算。理由见「`catalog` 的构造方式」。 - **`catalog` 每项必须带 `hint`**:选型指引是它可以被模型看见的唯一出口。注册表里新增组件时, 该组件的 `aiHint` 要一并写全——漏一条,那个组件就基本不会被模型选中。 - **`blobIds` 顺序有意义**:多页文件按数组顺序理解页序,前端要保序,不要用无序集合承载。 - **上传与识别是两步**:用户选完文件**不发**识别请求,等用户点「开始识别」才发。上传在 AntDV 的上传组件里要先关掉它内置的自动上传通道,由按钮统一触发。 - **绑附件要先于识别**:先绑再识别,识别失败文件也已归属到模板,不会留孤儿文件。 - **识别要放宽超时**:这条请求单独设 180 秒。用默认超时会在模型返回前就被掐断,而且错误信息看不出是超时还是模型挂了。 - **不要在弹窗里吞掉识别错误**:后端已按「维度 + 实际值 + 可执行动作」给了精确文案,由请求层统一弹出即可,弹窗里再包一层反而会盖住它。 - **载入时要传当前规则**:装配路径如果不传当前已编辑的判定规则,等于把用户写好的规则静默清空。 - **落款的属性输入框是单行的**:可绑定字段统一用自定义控件(下拉 + 单行输入框)。**AI 导入出来的多行落款值会原样保留、也能正确折行**,但只要用户去编辑那个输入框,换行就会被打掉、重新变回一片。要做「在设计器里也能手工敲多行落款」,得把该控件的输入框换成多行输入框——该控件被全部可绑定字段共用,属于需要单独评估的改动。 - **载入会清空撤销栈**:画布非空时先弹确认并建议先保存;文案要**明说无法用撤销恢复**。 - **`skipped` / `problems` 逐条展示**:不要只报条数,用户要照着改。 - **本轮不做「AI 新建模板」**:设计器必须带 `templateId` 进入,AI 导入只在既有模板的设计器内。想做「上传文件直接造新模板」需先建模板壳,是另一条流程。 - **本轮不做 AI 自动发布版本**,也不做撤销。 - **文件类型在前端只做即时约束**(扩展名白名单 + 拖拽区提示),**权威校验在后端**,前端别把这里的 3 个文件上限当成安全边界。 - **模板删除会连带清理附件**:本轮补齐了「删除模板时清理其名下附件」的动作,AI 导入的源文件不会在模板删除后变成永久孤儿。前端不需要处理,但验收时值得确认一次。 ## 错误码 | 错误码 | 报文 | 触发场景 | |---|---|---| | `1_070_104_000` | AI 导入功能未启用(`yudao.qcreport.ai-import.enabled` 当前为 false),请联系管理员开启后再试 | 运维把开关关成了 `false` | | `1_070_104_001` | 上传的文件(blobId=X)不存在或已被清理,请重新上传后再试 | `blobId` 查不到。文件已被清理或编号传错 | | `1_070_104_002` | 不支持的文件「X」(扩展名:Y)。仅支持 PDF、Word(.docx)、Excel(.xlsx)、图片(png/jpg/jpeg/bmp/gif/webp) 与文本(txt/csv),请转换格式后重新导入 | 扩展名不在白名单内 | | `1_070_104_003` | 不支持的文件「X」(旧版 Office 格式.Y)。请用 Office 打开后「另存为」.docx 或 .xlsx 再重新导入 | 上传了 `.doc` / `.xls` | | `1_070_104_004` | 文件「X」大小 YMB,超过单文件上限 ZMB,请压缩或拆分后重新导入 | 超过 `maxFileSizeMb`(默认 20MB) | | `1_070_104_005` | 文件「X」未解析出任何可用内容(共 N 页)。若是扫描件,请确认分辨率不低于 200 DPI、文字无严重倾斜或遮挡;推荐改用电子版 PDF 或 Word/Excel | 空文件;传真件等编码不受支持的图片;**内容为纯空白的 `.docx` / `.xlsx` / `.txt`**。`.docx` / `.xlsx` 打不开(损坏、带密码)走 `1_070_104_015`,不会落到这里 | | `1_070_104_006` | 一次最多导入 N 个文件,本次提交了 M 个,请分批导入 | 超过 `maxFiles`(默认 3) | | `1_070_104_007` | 文件「X」共 N 页,超过单次识别上限 M 页,请拆分后分批导入 | 超过 `maxPagesPerFile`(默认 5)。**不静默截断** | | `1_070_104_008` | 组件清单参数不合法(原因),请刷新页面后重试 | `catalog` 为空、含没有 `type` 的项、超 40 项、序列化超 32KB | | `1_070_104_009` | 组件清单的 Schema 版本为「X」,服务端只接受「Y」,请刷新页面后重试 | `schemaVersion` 与设计器不一致(通常是前端已更新而后端未发版) | | `1_070_104_010` | AI 识别调用失败(已耗时 Nms):原因。请到「AI 大模型」中确认对话模型已启用且密钥有效,或稍后重试 | 模型服务异常、网络不通、密钥无效 | | `1_070_104_011` | AI 识别超时(本次已等 N 秒)。文件页数或数量较多时耗时更长,请减少文件数量,或改用电子版 PDF / Word / Excel | 模型排队或文件过大。**接口侧超时默认 60 秒**(后端 `yudao.ai.timeout`,与前端那条 axios 的 180 秒是两道独立的闸) | | `1_070_104_012` | AI 返回的内容无法解析为模板草稿(已收到 N 个字符)。若文件内容很长,请拆分后分批导入;若持续失败,请改用手工设计 | 模型输出不是预期 JSON(被截断、夹带解释文字等)。原始输出已写进服务端日志 | | `1_070_104_013` | AI 未能从文件「X」中识别出任何可用组件。请确认该文件是检验报告;若确实是,请改用手工设计模板 | 一个组件都没识别出来。**这是唯一一个「整体失败」的语义** | | `1_070_104_014` | 本次提交的文件合计 N 页,超过单次识别上限 M 页(单文件上限 K 页),请减少文件数量或分批导入 | 每个文件都没超、加起来超 `maxPagesPerRequest`(默认 8) | | `1_070_104_015` | 文件「X」无法作为 .Y 打开,内容已损坏或受密码保护。请先用 Office 打开该文件,确认能正常显示后「另存为」.docx / .xlsx 再重新导入;若文件有打开密码,请先解除密码保护 | `.docx` / `.xlsx` 解析抛异常(POI 打不开)。**「空白」与「损坏」是两回事,报文分开**;此前损坏文件误报 `1_070_104_005`(让用户去查分辨率),已纠正 | > 注:`templateId` **不做存在性校验**,它只用于日志留痕与附件归属。前端务必先用真实存在的模板 id 进设计器。