IR 技术:在网页视觉转换中的应用
# 1. 背景:大模型设计的 UI/UE 如何落地私有框架 **核心问题**:大模型已经能高质量地"设计"UI/UE——直接产出可运行的 HTML 原型 (我们的全部原型即由 LLM 生成)。但企业内部的落地目标往往是**私有技术框架** (自研组件引擎、私有组件库),二者之间隔着组件模型、样式体系与布局引擎的巨大差异。 "设计产出"到"框架程序"之间的转换效率,决定了设计迭代能否当天生效。 # 1.1 常用技术路径对比 |路径|做法|问题| | --- | --- | --- | |代码直译 / 转义翻译|LLM 把 HTML 逐段"翻译"成私有框架代码|每次生成不可复现、token 成本高、样式靠猜、无法回归审计;框架 API 一变全部重来| |样式解析 + 规则引擎|解析 CSS/内联样式,按规则映射到框架组件与属性|确定性最好,但规则要落在一份稳定的结构化数据上——直接对 DOM 写规则会与 DOM 细节耦合| |多模态反推|模型看设计稿截图直接生成框架代码|视觉损失大(字号/颜色/间距全是估的),不可审计,无法批量| |设计规范适配层|约束设计稿使用标准组件,再一一映射|牺牲设计自由度;LLM 生成的原型恰恰是无约束的 HTML/CSS| # 1.2 最终选择:IR(中间表示) 这是编译器的经典思路:**前端语言无关,后端目标无关**。 任何来源(HTML/CSS 原型、未来的设计稿格式)都编译成同一份 IR; IR 再由各自的代码生成器输出到任意私有框架。选它的三个决定性理由: 1. **确定性**:转换是纯函数式的编译过程,同一输入永远同一输出——LLM 只负责"设计",落地交给工具链; 1. **可审计**:IR 与产物同构,质量指标(文本承载率、样式保真率)可自动对账、可接 CI; 1. **可演进**:引擎 API 变化只影响"IR → 框架"一层的映射表,前端提取层完全不动。 # 1.3 OODUI 框架简介 OODUI 是自研的移动端优先 UI 引擎,本管的转换目标即它: - **组件 = 虚拟节点树**:每个组件(ood.UI.Block / Label / Button / Gallery / List / ToolBar…) 由模板定义若干**虚拟节点**(KEY 根节点、CAPTION 文本、ICON、BORDER、PANEL 内层容器…), 样式与内容必须"投递"到正确的虚拟节点才会生效; - **样式 = CS**:组件级自定义样式,按虚拟节点键下发(对 KEY/BORDER 可靠,个别键有坑,见 §3); - **布局**:列布局 conLayoutColumns + dock 停靠 + 绝对定位三件套, 与浏览器 flex **不等价**; - **载体**:组件树以 .cls(JSON)描述,服务端反序列化后由 showModule 渲染进 DOM。 # 1.4 为什么还需要"一层"IR,而不是直转 直转器(HTML 直接映射组件)很快撞上三堵墙: - **样式真相分散**——外部 CSS、 块、内联 style 三处叠加,还要经过级联与特异性排序; - **无法审计**——没有与产物同构的中间表示,"字号保真率多少"这类问题无法回答; - **判定无落脚点**——"6 个同构卡片应聚合成 Gallery"这类判断需要同时看结构、样式与内容。 IR(中间表示)把这三件事**一次性编译完成**:先物化出每个节点的计算样式与规范化结构, 后续所有语义判定、组件映射、审计对账都只依赖 IR、只读不猜。 # 2. 管线总览  图 1 | 五级管线与三条保障带、审计闭环 提取层 html2ir.mjs 把 CSS 级联 + 内联 style 展开为每节点的计算样式; 语义层 semantic2ouc.mjs 完成角色推断、组件映射与样式注入,产出 .ouc.json, 服务端反序列化为 .cls 组件树交引擎渲染。 # 2.1 IR 节点结构(技术细节)  图 2 | IRNode 字段与三大消费方 ``` { "tag": "section", "classes": ["gt-stat-grid"], "children": [ /* 规范化 DOM 树 */ ], "icon": "ri-user-line", "src": null, "text": ["872", "刷题总数"], "style": { "layout": { "display": "grid", "position": "absolute", "top": "0" }, "paint": { "background": "linear-gradient(...)", "color": "#fff" }, "typography": { "font-size": "1.667em", "font-weight": "700" }, "box": { "width": "394px", "border-radius": "1em" } } } ``` **级联展开算法**:收集所有命中节点的规则(外部 CSS 按 specificity × 出现序、 次之、**内联 style 最高**),排序后逐属性覆盖写入 flat, 再按属性分类归入四组。这一步等价于浏览器 getComputedStyle 的离线实现, 是整条管线的地基——初版漏掉内联 style 时,字号/颜色覆盖率长期失真、进度条完全无法识别。 # 3. 角色推断:判定链(顺序即语义)  图 3 | inferRole 从上到下短路命中;三个历史事故全部源于顺序错误 > ⚠ 同构签名 = tag + classes(阈值 ≥3),类名仅用于结构分组、严禁参与语义推断; 复杂子项(item 内层仍是列表)递归拆分为嵌套节点,避免整段内容被压扁丢弃。 # 3.1 组件转换类型对照表 |语义角色|OUC 组件|内容/样式槽位|已定论的关键约束| | --- | --- | --- | --- | |PageHeader / TabBar|ood.UI.ToolBar|items[]|引擎无 bottom 停靠,TabBar 实际渲染在顶部(已知差距)| |HeroBanner|ood.UI.Block|KEY=渐变+display:block;子节点 Label/Button 绝对定位|CS.PANEL 对 Block 不生效 →properties.background='transparent'触发 action 打透;无 IR 高度时须兜底| |StatGroup|ood.UI.Gallery|items[].value / caption;width:100%|VALUE 节点会渲染(旧"不渲染 value"已勘误,勿再双写 comment)| |GridGroup|ood.UI.Gallery|caption / imageClass / comment|图标必须走 imageClass(iconFontCode 会被当文本画出类名)| |NavList / CardList|ood.UI.List|caption / ext / imageClass|List 渲染 {ext} 不渲染 {sub};TreeListItem 无 iconFontCode| |ProgressItem|ood.UI.ProgressBar|value / caption|识别依赖内联样式并入(原型宽度写在 style 上)| |Title / Caption|ood.UI.Label|caption + CS.CAPTION|properties.fontColor 必须是 CSS 变量名,排版走 CS.CAPTION 直注| |Icon|ood.UI.ContentBlock|imageClass|判定必须先于 EmptyState(texts≤2 包含 0)| |EmptyState|ood.UI.ContentBlock|caption / subtext / imageClass|必须写 imageClass,否则产出空壳(曾 29 个,已清零)| |Action|ood.UI.Button|caption + CS.KEY|默认灰底 rgb(245,245,245)+display:block → 须覆盖为 inline-block/透明| |Panel(兜底容器)|ood.UI.Block|conLayoutColumns:1 + KEY|壳层默认 inline-block 会收缩 → 强制 display:block| 对照表各槽位在真实页面上的落地效果——首页集中了 Banner、Gallery 宫格、统计组、List、ToolBar 五类组件:  首页 | 左:HTML 原型,右:产物在 OODUI 引擎渲染(HeroBanner + GridGroup + StatGroup + List + TabBar)  个人中心 | 两组 StatGroup(含彩色数值)与 NavList,"value 会渲染"结论的来源页面 # 4. 布局归一与审计闭环 容器布局只有两种合法形态:**A 自由布局**(conLayoutColumns:1,子节点交引擎列布局)、 **B 绝对定位**(展示型容器,工具从 IR 算出子节点 left/top/height 显式下发)。 配套约束:容器去 padding(折进子坐标防双计)、去 flex/gap 无效属性但 display 必须定为 block。 审计闭环:IR 与产物同构,审计工具逐页量化文本/图标承载与样式保真。 修复一轮后(17 页):文本 81.1%→86.2%、图标 49.5%→80.4%、空壳组件 29→0。 宿主页 ?probe=1 输出组件 computed box+背景、?shot=1 支持无头截图—— 所有观察都可复现。 布局归一的效果在"视频课程"页最为直观:顶部深红渐变 Banner(HeroBanner)满宽上色、 分区卡片(Panel + Gallery)撑满列宽——这两处正是"白 PANEL 盖渐变"与"壳层收缩"两个通病的主战场:  视频课程 | 布局归一后的 Banner 渐变与满宽卡片 # 5. 转换质量实拍:多组件嵌套页面 智能家居首页覆盖了问候区、快捷场景宫格、房间 2×2 宫格与设备列表的嵌套组合, 是多组件协同转换的代表性页面(首页/个人中心/视频课程的截图已按主题分布在前文 §3、§4):  智能家居 | 场景宫格、房间 2×2 宫格、设备列表(左原型 / 右引擎渲染) > 剩余差距:Banner 内头像/图标定位有偏差;无头环境图标字体偶发缺字。均在审计"未解决"清单中。 # 6. 方法论小结 1. 中间表示是审计的前提:没有同构 IR,"转换质量"只能是主观印象; 1. 判定顺序是语义的一部分,要像接口契约一样对待并注释清楚; 1. 对引擎每个字段约定都要追溯到模板源码,猜槽位 = 静默丢数据; 1. 样式真相一次性物化(级联+内联+特异性),之后只读不猜; 1. 不可重现的观察不算证据:缓存、时序、截图都要做成确定性开关。 # 7. 下一步 - 用几何审计(computed box 对比)把 Banner 子元素定位误差量化进指标; - 组件能力缺口(卡片多槽位、列表双图标、圆形、进度条)已提交引擎侧评估; - 审计报告接入 CI,指标回退即阻断合入。 —— 完 ——