编辑 | blame | 历史 | 原始文档

智能质检报告 — 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 接口,带上 blobIdstemplateIdschemaVersioncatalog、可选的 hint
    这条请求要单独放宽超时到 180 秒(单次最长要发 5 次大模型调用,默认超时会在模型返回前就掐断)。
  5. 预览:弹窗列识别出的组件(类型显示名 + 已识别的属性)、summarywarnings
    这一步**没有任何写操作**,用户可以反复重新上传。
  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 识别总耗时(毫秒),含多次模型调用

响应示例

{
  "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-itemsPathdata-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 = 1hidden = 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 里加一条提示。**扫描件/照片不受影响**:它们的页眉页脚本来就是页面像素的一部分
表格末行的落款签署行属于落款 检验员 / 审核人 / 批准人 / 日期这类签署行**常排在检验表格的最后一行**。它不算「具体数据行」,应被取出来与原件页脚合成放进 ReportFooterReportFooterQualityTable 两条选型说明里各自写明了这条边界)
签署行的人名与日期留空 签署行**保留栏目名、值留空**(形如「检验员:____ 审核人:____ 日期:____」),打印出来由人手填。人名与日期是每一份报告各自的数据,写死原件上的那几个人会让所有报告印成同一个检验员
落款允许多行,换行要真的折行 签署行与原件页脚合成一段**多行**文本交给 ReportFooter,几行内容**必须折行显示**,不能糊成一片。属性值里保留的是原始换行;**折行发生在渲染产物这一侧**——组件生成的画布 content 里是 <br>,渲染引擎按原样输出。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 进设计器。