AI生成UI的安全防线:用TypeScript构建组件校验门

模型生成可交互UI已成现实,开发者必须在渲染前建立组件白名单校验门,而非直接信任模型的"发明"。
随着OpenAI Intelligent UI、Claude Haiku 5.5等产品让模型具备直接生成可交互界面的能力,一个新的安全命题浮现:模型吐出的按钮、表单和图表,可能指向从未被授权的地址,甚至携带恶意脚本。本文通过一个纯TypeScript的最小示例,阐述了应对这一风险的核心架构:在渲染前设置一道"组件校验门",以封闭的白名单目录(七个组件类型、受限属性集、action白名单)对模型生成的UI树进行解析、校验,至多进行一次修复,然后接受或拒绝——绝不发明新组件类型。整套流程用pass@1、pass@repair、reject rate三个指标量化,并与发布阈值挂钩形成可重复的决策机制。作者强调,厂商提供的智能UI能力无法替代开发者自己的渲染门,涉及资金或数据的action仍须引入人工环节。
当模型开始自己"发明"界面
你的模型返回了一个表单。它带着提交按钮,而按钮指向的 action,是你从未批准过的地址。这不是科幻场景,而是"智能UI"(Intelligent UI)走向主流后,开发者必须面对的现实。
据原文梳理,多家厂商在同一时间窗口推进了相同方向的能力。OpenAI 发布了面向全体用户的 GPT-6 与 Intelligent UI,响应内容可以包含图形、可点击按钮、表单、图表以及交互式体验——官方构建了一套原生可流式组件库,外加一个在模型生成界面时就同步处理的编译器,由模型自主决定布局。Anthropic 同期推出 Claude Haiku 5.5,主打高频、成本敏感的分类与路由任务。Google Cloud 则谈到 agent 直接把成品交付到文档和收件箱流程中。
不同产品,同一方向:厂商正在教模型吐出可交互的 UI。这很强大,但如果你原封不动地渲染模型发明的任何东西,它同样危险。
原文给出的核心命题值得记住:模型发明的组件只是一个"提案",而一个把允许挂载内容列入白名单的"门"(gate),才是产品。

Intelligent UI 的技术背景
Intelligent UI 并非单一产品,而是大语言模型能力向「生成可交互界面」延伸的统称。传统上,模型输出纯文本或结构化 JSON,由前端代码将其转译成界面元素;而 Intelligent UI 的核心转变在于:模型直接输出一棵描述界面结构的组件树(通常是类 JSX 或自定义 DSL),渲染器拿到后可以直接挂载成按钮、表单、图表等可交互元素。这带来了表达力的跃升——模型可以根据上下文动态决定用下拉菜单还是按钮组——但也把安全边界从「文本内容是否合规」推进到了「界面行为是否被授权」。一个精心构造的提示词,完全可能诱导模型生成一个看起来合法、却把表单数据提交到攻击者服务器的 UI 树,而用户在视觉上无从分辨。这正是本文所关注的核心安全隐患。
这个"组件校验门"要解决什么
原文作者用纯 TypeScript 搭了一个最小可运行示例,一条命令即可跑通:
npx tsx gate.ts
没有 API key,没有真实模型,只有 TypeScript。所谓"模型"其实是一张固定的 fixture 表,通过率和发布阈值都是示例输入。真正的价值在于那道"门"的逻辑——它在渲染前,对一棵 UI 树按组件类型、属性和 action 策略进行校验。
需要强调的是,这与"路由决策的 schema gate"是两回事:后者校验 JSON 结构,而这里校验的是一棵 UI 树,在挂载前检查它的组件类型、props 与行为策略。
整个门做四件事:解析树 → 对照目录与 action 策略校验 → 若无效则修复一次(本地剥离未知 props,或把问题反馈给模型)→ 决定接受或拒绝,绝不发明新的组件类型。
目录即契约:七个组件,封闭属性
示例定义了七个白名单组件:Text、Button、Input、Form、Chart、Card 和 Stack。每个组件都有一份封闭的属性清单。按钮和表单只能指向 action 白名单内的目标;action 中的原始 JavaScript 被禁止;超过四层深度的树直接拒绝;图表必须携带数值序列。
const ACTION_ALLOWLIST = new Set([
"https://app.example.com/confirm",
"https://app.example.com/cancel",
"https://app.example.com/submit-feedback",
"app://navigate/home",
"app://navigate/settings",
]);
const MAX_DEPTH = 4;
作者点出关键:OpenAI 的 Intelligent UI 固然提供了原生组件库和编译器,但你的产品仍然需要那份"你愿意挂载什么"的清单。目录模糊,渲染器就会跟着模糊。
校验逻辑本身不依赖 React 或 iframe 沙箱,只做这份目录所需的检查:未知组件直接报错并停止下探,必填属性缺失、属性不在白名单、原始 JS、action 越界、series 非数值等逐项记录为 issue。作者的一句话很到位:没有拒绝路径的校验,不过是一次更漂亮的崩溃。
白名单(Allowlist)与黑名单(Blocklist)的设计哲学差异
「目录即契约」采用的是白名单思路,而非常见的黑名单过滤。黑名单方式试图列举所有危险情形并加以屏蔽,但模型的输出空间几乎无限,攻击者总能找到未被列入黑名单的变体(如用 onclick 替代 javascript:)。白名单方式则相反:只声明允许存在的组件类型和属性,任何未出现在清单中的内容一律拒绝,把「证明合规」的举证责任转移给模型输出本身。这种封闭假设(closed-world assumption)在安全工程中有着悠久传统,也是 Content Security Policy(CSP)、防火墙 ACL 等机制的共同基础。代价是灵活性:每次需要新增组件类型,都必须显式地扩展目录并完成审查,而不能依赖「只要不明显有害就放行」的宽松策略。文中反复强调「绝不发明新组件类型」,本质上就是在贯彻这一封闭世界假设。
模型自由发明时会出现的失败形态
示例用八个 fixture 覆盖了模型放飞时的典型 UI 畸形:目录外的组件、表单提交到未列出的地址、想运行脚本的按钮、缺标题的图表、把树撑爆的嵌套 Stack、把字符串伪装成序列的图表,以及看起来像事件处理器的 props。
其中最关键的是"未知组件"这一例。一个把 MagicWidget 修复成 MagicWidgetV2 的循环,依然是拒绝对象。规则很硬:类型不在目录里,就不要挂载。
一次修复,然后决断
修复流程被刻意设计得很短:解析 → 校验 → 若有问题,做一次受约束的修复 → 再校验 → 停止。
function gate(fixture: Fixture): Outcome {
const firstParsed = parseJson(mockComplete(fixture, "first"));
if (!firstParsed.ok) {
return { status: "reject", pass: "first", issues: [...] };
}
const firstIssues = validateTree(firstParsed.value);
if (firstIssues.length === 0) {
return { status: "accept", pass: "first", value: firstParsed.value };
}
return repairOnce(fixture, firstParsed.value);
}
对于未知属性,harness 直接在本地剥离,而不是让模型去发明 CSS;对于越界 action 和缺标题,fixture 模拟一次模型重试;对于 MagicWidget,重试仍然无效,门直接拒绝。
这里也呼应了多厂商协同的现实:在真实技术栈中,mockComplete 会替换成你的 provider 调用,一个 Haiku 级别的廉价快速模型可以守在"这棵树是否被允许?"的路径上,而更大的模型负责起草 UI。无论如何,目录要留在你自己的 harness 里。作者的收束句同样精炼:一次修复是策略,无限修复是死循环。
指标与发布阈值
门最后输出三个数字:pass@1(首次就通过校验的比例)、pass@repair(一次修复后可挂载的比例)、reject rate(应当回退到文本而非发明 UI 的比例)。
pass@1 0.13 (1/8)
pass@repair 0.88 (7/8)
reject rate 0.13 (1/8)
gate: SHIP (pass@repair >= 0.75 required)
示例中的发布阈值是 0.75,作者明确提醒:阈值要从真实流量里选,而不是从博客里抄。 八个 fixture 里只有一个首次通过,六个经一次修复后变为有效,剩下一个始终未知——门选择拒绝它,而不是去发明一个渲染器。这正是整件事的要义:接受目录内有效的 UI,拒绝其余,并把两者都计数。
pass@k 指标的来源与含义
pass@1 和 pass@repair 的命名来自代码生成领域的标准评估指标 pass@k,最早由 OpenAI 在 HumanEval 基准测试中系统化使用:给模型 k 次生成机会,只要其中有一次通过单元测试,就算「通过」。在本文的语境中,这一思路被迁移到 UI 校验场景:pass@1 衡量模型一次生成就产出合规 UI 的能力,pass@repair 则衡量在给予一次修复机会后的最终合规率。这种分层指标的好处在于,它能把「模型原始质量」和「harness 修复能力」拆开度量,便于判断到底是模型需要提示词优化,还是修复逻辑需要加强。将指标与硬性发布阈值(如 pass@repair >= 0.75)挂钩,使发布决策从主观判断变为可重复验证的流程,是 MLOps 实践向 UI 生成场景的自然延伸。
边界与现实局限
原文难得地花了大段篇幅讲"哪里会失效",这比方案本身更有参考价值。
厂商的 Intelligent UI 不是你的门。 OpenAI 的组件库和编译器服务于 ChatGPT 的聊天体验,但你的产品依然掌握着"要挂载哪些组件、这些组件能触发哪些 action、何时回退到文本"的决定权。
七个组件不是设计系统。 生产环境需要带版本的注册表、可访问性和视觉 QA,但模式不变:封闭类型、封闭属性、封闭 action、一次修复、拒绝。
修复可能制造"看似有效的谎言"。 把错误反馈给模型,它可能发明一个工单里从未出现过的 confirm URL。目录有效 ≠ 语义正确。如果 action 涉及资金或数据删除,就必须给门配上人工环节。
深度上限和序列检查都是启发式。 MAX_DEPTH = 4 是示例,"series 必须是数值"也是。真正的检查矩阵应该从生产故障里长出来。
更大的命题
原文最后把智能 UI 定义为一份契约——介于概率性采样器(模型)与确定性渲染器(你的产品)之间的契约。目录提供类型,校验器提供证据,修复提供一次廉价的二次机会,拒绝路径提供停止点,指标提供发布决策。
当模型越来越擅长"发明"界面,开发者的护城河不再是能不能生成 UI,而是能否清楚地展示:什么被允许、什么被拦截、以及为什么。你的模型刚刚发明了一个表单——让它靠本事去赢得渲染权。
相关推荐

uGAP:统一100+非洲地理数据集的AI平台
uGAP是开普敦大学团队开发的非洲地理空间AI平台,统一120余个非洲数据集并提供Python SDK,支持数据发现、处理与模型训练一站式工作流,已用于气旋洪水制图等实战场景。

Cursor能控制你的电脑了:AI代理可驱动桌面应用测试
Cursor 新增桌面控制能力,AI 代理可驱动 iOS 模拟器等桌面应用测试自己构建的功能。本文分析这一更新对开发工作流的影响、技术实现可能路径及可靠性考量。

Vibecoding自研推理引擎:理想丰满,现实骨感
Reddit开发者分享Vibecoding自研推理引擎的真实经历:用WebGL2和GLSL在浏览器跑GGUF Q4模型,却陷入tokenizer调试地狱。本文分析浏览器端LLM推理的工程难点,以及为何直接clone llama.cpp是更理性的选择。