我让 AI 操作了 8 个内容平台,才发现 UI 里藏着多少「只有人看得懂」的约束

简介: 图形界面同时干着两件事——交互和约束。人用的时候感觉不到区别,因为约束是画给你看的:按钮灰了、选项变浅、提示条弹一下。当你让程序去操作同一个界面时,交互那部分它拿得到,约束那部分它几乎全看不见。

先给结论:图形界面同时干着两件事——交互和约束。人用的时候感觉不到区别,因为约束是画给你看的:按钮灰了、选项变浅、提示条弹一下。当你让程序去操作同一个界面时,交互那部分它拿得到,约束那部分它几乎全看不见。

于是就出现了这一类最难查的故障:操作返回成功,但什么都没发生。

这篇记录我把八个内容平台的发布流程自动化时踩到的坑。不讲怎么做自动化,讲的是界面到底把哪些信息只编码在了视觉层,以及这对任何「让程序驱动为人设计的系统」的场景都成立的一条原则。


一、最典型的一类:禁用态

某平台的标签选择器,上限是 2 个。加满之后,剩下的候选项会变成浅灰色。

人看到浅灰色就知道点不动了。程序看到的是什么?

// 加满之后,对一个"禁用"的候选项调用
el.click();     // 不抛错,不报警,正常返回

它不抛错、不返回 false、DOM 上也没有 disabled 属性——只是 CSS 把它画浅了,并且点击回调里加了个提前 return。

所以我的代码是这么写的:

if click_option(page, tag):
    print(f"✅ 标签 {tag} 已添加")

三个标签,三次 ✅。实际只加上了两个。

日志、返回值、异常,三条通道全部显示成功——只有截图能看出问题。

正确的判据不在操作端,在状态端

后来改成读平台自己算的那个数:

# 页面上有一行「你还能添加 N 个标签」
left = read_remaining_quota(page)   # 加之前
click_option(page, tag)
now = read_remaining_quota(page)    # 加之后
ok = now < left                     # 额度真的减少了,才算加上

这个数是平台自己维护的业务状态,不是我对操作结果的推断。

这是整篇最重要的一条:不要用「操作有没有执行」判断「状态有没有改变」。


二、第二类:视觉上存在,DOM 里不存在

另一个平台的发布按钮,截图上清清楚楚一个红色「发布」。但:

// 全页扫描,清洗掉零宽字符和空白后精确匹配
Array.from(document.querySelectorAll('*'))
     .filter(e => clean(e.innerText) === '发布')
// → 只找到侧边栏的「发布笔记」和一个「定时发布」开关,没有那个按钮

排查绕了很久,因为它伪装成好几种常见问题:先怀疑按钮文本里夹了零宽字符(清洗了还是找不到),再怀疑在 iframe 里(只有主文档),再怀疑 DOM 查询被截断了。

最后是用坐标反查一步定位的:

document.elementFromPoint(743, 855)
// → <xhs-publish-btn submit-text="发布" save-text="暂存离开" submit-disabled="false">

按钮文字放在 HTML 属性上,真正的按钮渲染在 closed shadow root 里。 element.shadowRoot 返回 null,querySelectorAll 穿不进去,innerText 是空串。

所有基于文本的定位——包括各种测试框架的 getByText——在这里结构上不可能成功,不是选择器没写对。

但它照常接收鼠标事件

所以解法是按坐标点。这又带出一个更阴的问题:

我一开始按容器宽度的比例算坐标,落在了按钮右侧的空白上。而 closed shadow root 让 elementFromPoint 无论点容器里哪一处都返回同一个元素——看着像命中了,一切"正常",就是不发布。

后来改成按容器中心的固定偏移算(按钮尺寸是固定的,容器宽度随窗口变),才稳定。

教训:当一个查询无论如何都返回同一个结果时,它就不再有区分能力,也就不能拿来当校验。


三、第三类:同一个界面,两套语义

某平台的文章编辑页,URL 里带着文章 ID,标题和正文都正确加载了原文。页面上有个「导入 md 文件」的入口——发布新文章时我一直用它,因为它能顺带解决标题栏的一个问题。

在编辑页用同样的方式导入、提交,返回的成功页长这样:

https://.../creation/success/164183558

而我编辑的那篇文章 ID 是 164152491。

它新建了一篇重复文章,原文纹丝不动。

「导入」这个动作在产品语义里始终是「导入成一篇新文章」,跟你当前打开的是不是编辑页无关。而成功页的 URL 格式一模一样——不比对 ID 根本发现不了。

后来改成剪贴板粘贴(直接改当前文章的编辑器内容),并加了一道硬校验:

if returned_id != original_id:
    print(f"🔴 新建了一篇重复文章({returned_id},原文是 {original_id})")
    return 1

同一类问题在另一个平台的变体

编辑已发布内容时,按钮的名字会变:

平台 新建时 编辑时
A 发布 更新
B 发布 → 确定并发布 更新 → 确定并更新
C 发布 保存修改

C 那个最坑:页面上同时还有一个「发布」文本元素(在别处)。我的宽匹配点到了它,返回 True,然后什么也没发生。

判据当时写的是「URL 里的文章 ID 没变」——它确实没变,因为压根没提交。校验条件和失败模式正交,等于没校验。

最后加的规矩是:保存后重新加载页面,回读内容比对。这是唯一不会被骗的判据。


四、第四类:工具本身的副作用

这一类不怪界面,怪我。

某平台的标签框里已经有两个标签 chip,我想清空搜索输入框,写了:

input.fill("")

结果两个 chip 一起没了。

fill 的实现是「聚焦 → 全选 → 替换」。在这类复合组件里,全选的选区包含了 chip,插入空文本等于把它们删掉。而 fill("非空") 就没这个问题——它替换的是选区内容。

更麻烦的是这个 bug 和第一节那个「判据不可靠」叠在一起:判据永远返回 False → 每次都走清空分支 → chip 被删光 → 判据继续 False。表现是「截图里明明有标签,脚本却说一个都没加上」。

两个各自不致命的问题叠在一起,症状会指向一个不存在的第三个原因。


五、还有一类:删除操作的多步确认

删一篇文章,我以为点了「删除」→「确定」就完事了。

页签数字确实变了:已发布 10 → 9。但另一个数字我没看:回收站 0 → 1。

删除只是移到了回收站,回收站里还有一步「彻底删除」,弹窗文案是「删除后无法恢复」。

我当时的判据是「页面上还有没有这篇文章的链接」——回收站里的文章链接仍在列表页 DOM 里,所以判据显示"仍在",我以为删除失败了。判据错了两次,方向还相反。

真正可靠的信号是平台自己的计数:全部 12 → 11、回收站 1 → 0。


六、一条通用原则

把上面五类放在一起看,共同点很清楚:

界面把「能不能做」这件事,大量地只编码在了视觉层。

按钮灰了、选项浅了、提示条弹了、菜单项禁用了——人一眼就懂,程序拿到的却是一个正常返回的 click()。

所以让程序驱动为人设计的系统时,有一条我认为绕不开的规矩:

永远不要用「操作的返回值」判断「状态是否改变」。要回读状态本身。

具体到实践:

不要 要
看 click() 有没有抛错 读平台自己维护的计数、额度、状态标签
看返回的 URL 长得对不对 比对 URL 里的业务 ID 是不是原来那个
看提交后页面有没有跳转 重新加载页面,回读内容
看某个元素在不在 看那个元素能不能真的起作用

最后一条尤其重要:当一个检查无论如何都返回相同结果时,它已经没有区分能力了,留着只会给你虚假的安全感。


七、这件事的适用范围比自动化大得多

我做的只是内容发布这么个小场景,但同样的结构在别处也成立。

当 Agent 开始替人操作各种系统时,它面对的正是同一个问题:过去很多业务约束是靠界面来实施的——某个字段你看不到是因为页面没渲染,某个操作你做不了是因为按钮灰着,某个流程必须走审批是因为页面强制你提交。

界面被绕开之后,这些约束不会自动跟着走。它们要么被显式地表达出来(在接口层、在权限层、在状态机里),要么就变成一堆「调用成功但什么都没发生」的静默失败。

我这一天遇到的每一个坑,本质上都是同一句话:约束没有被表达,只是被画了出来。


小结

症状 真正的原因 可靠的判据
操作返回成功,状态没变 禁用态只在 CSS 里 读平台自己的计数/额度
元素肉眼可见,代码找不到 closed shadow root 坐标反查定位
提交成功,改的却是别的对象 同一界面两套语义 比对返回的业务 ID
截图有内容,脚本说没有 两个 bug 叠加 每个环节独立回读
删除后还在 多步确认(回收站) 看平台的分类计数

如果只记一句:回读状态,别信返回值。


本文的实测来自把八个内容平台的发布流程自动化的过程,工具与踩坑记录由 Code2AI 团队维护。

相关文章
|
13天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8001 15
|
11天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1772 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
12天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
1923 12
|
10天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
6天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
25天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
3823 10
|
20天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
2108 1