dsh-design-audit
DSH 的 UI/UX 审查与优化闭环:截图取证 → 确定性设计判据 → CSS 修复 → 复测验证。
为什么做这个(生态定位)
2026-09 的 DSH 插件生态调研结论:
| 方向 | 现状 |
|---|---|
| 设计生成(从 0 到 1) | 已被 deepseek-design(★978,含 Studio/模板市场)覆盖 |
| 截图 → UI 还原 | 已被 agent-vision-toolkit(★1195)覆盖 |
| 设计知识清单 | uckkk 系列 5 个插件,0–2★(纯参考、无工具化) |
| 审查与优化闭环 | 空白 ← 本插件做这个 |
不做生成器、不做还原器:只做"量出问题 → 给出可粘贴的修复 → 证明修好了"。
核心设计决定:测量与判据分离
页面内取证(collect.js) 判据与阈值(checks.js)
· 几何 / 计算样式 · WCAG 条款号
· 对比度比值(页面内算) · 平台指南(44×44)
· 可访问名称 / 遮挡命中测试 · 分级 + 具体 CSS 修复
= 全是"量出来的" = 全是"定下来的"
测量错、判据错可以分别审计,互不掩盖。
机器只做可测量的判断,审美交给模型看图 —— 工具不越界假扮评审官:它把客观证据与截图路径摆好,把"视觉层级/一致性/品牌感/呼吸感"留给模型的视觉复核(报告里显式列为"未覆盖")。
三个工具
| 工具 | 作用 |
|---|---|
design_capture |
无头 Chrome 打开 URL/本地 HTML,多视口整页截图 + 采集客观测量值。产物落 <工作区>/.design-audit/<时间戳>/ |
design_review |
跑确定性判据,出分级问题 + 逐元素 CSS 修复 + 自包含 HTML 报告;带 compare 时输出复测差值 |
design_fix |
把问题转成可粘贴的 CSS 补丁(可选直接追加到样式文件,自动备份原文件) |
判据覆盖(13 组)
- 无障碍:对比度(1.4.3)、焦点可见(2.4.7)、目标尺寸(2.5.8)、可访问名称(4.1.2)、图片替代文本(1.1.1)、表单标签(3.3.2)、页面语言(3.1.1)、标题层级、地标区域
- 排版:字号下限、行高(1.4.12)、行长(measure)、字号/字族阶梯
- 布局/交互:横向溢出、间距基准网格、色彩数量、图片尺寸预留(CLS)、浮层遮挡(可见 ≠ 可点)
- 动效(2.3.3,两条独立判据):
interaction/reduced-motion—— CSS 动效(animation/transition)缺@media (prefers-reduced-motion: reduce)兜底interaction/js-driven-motion—— JS 驱动的动效(GSAP 默认机制=逐帧写内联transform/opacity、WAAPI、手写 rAF)在 reduce 偏好下仍在动。这类动效不产生任何 CSS 动画,上面那条 CSS 兜底对它无效(animation-duration: 0.01ms !important管不到 JS 写的内联样式),所以修复建议直接给 JS 侧写法:检测到 GSAP 时给gsap.matchMedia()分支,否则给window.matchMedia分支
闭环怎么用
design_capture target=... label=before
design_review # 出报告 + 问题清单
read_image <首屏截图> # 模型做视觉复核(工具刻意不做)
design_fix apply=true cssFile=styles.css # 生成并应用补丁(备份原文件)
design_capture label=after
design_review compare=<before 目录> # 差值:已修复 / 仍存在 / 新引入
report.md 中 ## 视觉复核 之后的段落在复测时会被保留,便于把模型批注累加进报告;
不传 compare 时,自动基线只认同一个目标的上一次审计(跨目标比较毫无意义)。
实测证据
端到端闭环测试(node test/e2e.mjs,42/42 PASS):
- before 检出 16 条问题(对比度 / 目标尺寸 / alt / label / lang / 焦点 / 溢出 / 字号 / 行高 / 行长 / 间距 / CLS / 遮挡)
- 应用自动补丁后:修复 7 条、新引入 0 条,剩余为需改标记的项
- 横向溢出只在 mobile 视口标出(多视口确实在工作)
- HTML 类修复被分流,不会污染
.css - JS 驱动动效必须报出:单变量 fixture(只有 rAF 动效、没有任何 CSS 动效)报出
interaction/js-driven-motion,同时 CSS 兜底规则不得跟着误报 - 反向用例不得误报:脚本里已处理 reduced-motion 的页面零告警;断言同时钉住"捕获环境处于 reduce 偏好"这一前提,Chrome 若改了默认值会立刻失败而不是语义悄悄漂移
- 不许假阴性:单元层直接喂真实 evidence、只翻转"声明过兜底"这一位 —— 声明了却仍有元素在动,必须报出(low 并说明"兜底没盖住"),而不是查到字符串就闭嘴
真实项目验证(BookRank3,4 个页面,数据库指向副本、真实库零改动):
| 页面 | 首审 | 修复后 |
|---|---|---|
/ |
11(高1/中3/低7) | 7(全 low) |
/awards |
8(高1/中2/低5) | 5(全 low) |
/new-books |
7(高1/中2/低4) | 4(全 low) |
/publishers |
7(高2/中1/低4) | 4(全 low) |
最低对比度 3.78–3.95:1 → 4.57–4.74:1(越过 AA)。
真实项目揪出的 3 处遮挡(全为既有缺陷,A/B 实测已排除自身改动归因):搜索建议浮层盖住视图切换器、移动端 #sidebar-toggle 整块被顶栏压住、new-books 面包屑被固定顶栏覆盖 —— 三者的 display/opacity/rect 全部"正常",只有元素中心命中测试能发现。
开发过程中被真实使用反推修掉的缺陷
修复只覆盖最严重实例 → 改逐元素展开
<a><img alt="x">被误报无可访问名称 → accname 的 name-from-content 包含内嵌图 alt代码块被套用散文行长判据 →
pre/code排除行长按容器宽度折算(中文高估约一倍、宽容器短文本必误报)→ 改实测「字符数 ÷ 渲染行数」
同一选择器在补丁里重复 8 次(相对父级生成导致塌缩)→ 按选择器去重,补丁 11.2KB → 7.6KB
复测重写
report.md会抹掉批注 → 保留## 视觉复核段落自动基线取到无关页面 → 只认同目标
可见 ≠ 可点(盲点)→ 新增遮挡命中测试
差值误报:同一规则"最差元素"漂移时被判成"已修复 + 新引入"(假回归)→ 二次归类为"选择器漂移",单独列出前后元素
补丁重复块:跨 finding 的同一选择器生成多份(含注释的复合段会绕过归并)→ 全局按选择器归并(
flattenCssBlocks),实测 4 条修复 → 2 个选择器 + 1 段原样规则动效盲区(假阴性):取证脚本原来只统计计算样式里的
animation/transition,于是 GSAP 那类"逐帧写内联 transform"的页面一条都不报 —— 实测同一页面(整屏 rAF 无限位移、完全无视prefers-reduced-motion)拿到"问题总数 0",而它旁边只多一个 CSS 过渡元素的对照页却报了 1 条且归因给了那个 CSS 元素。修法:跨时间采样(隔 250ms 比较几何/透明度变化,排除有 CSS 来源的元素),并把该类动效的修复建议从纯 CSS 改成 JS 侧matchMedia/gsap.matchMedia()
已知边界
- 只覆盖可测量判据;审美、交互态(hover/focus 视觉)、内容与信息架构、真实性能采样不在范围内(报告里显式列为"未覆盖")
- SPA 需传
waitFor指定标志性选择器;元素数 < 20 时会告警"可能只抓到空壳" - 遮挡判据用元素中心点命中测试:部分遮挡但中心可点的目标不会报(保守取值,避免误报)
- 动效判据需要跨时间采样:每次取证多花约 250ms(两视口约 0.5s);采样窗内没动 = 不报,短于采样窗且不重复的一次性动画可能采不到
- 无头 Chrome 默认就是
prefers-reduced-motion: reduce:因此"观察到在动"实际等价于"该元素无视了 reduce 偏好"——这正是要找的违规。证据里记录该状态(motion.prefersReducedMotion),报告实测值里也会标注 - 脚本里已经声明 reduce 分支、却仍有元素在动时,判据不会静默放过(那会造成假阴性):降级为 low 并提示"兜底没盖住这些元素"(GSAP 常见原因:补间建在
matchMedia分支之外) - 依赖本机 Chrome/Edge(
DSH_DESIGN_BROWSER可覆盖);零 npm 依赖,只用 Node 内置模块 + 全局 WebSocket(Node ≥ 22) - 被审项目若采用"哈希名 + 运行中进程缓存构建清单"的打包方式(如 Flask +
dist_url),改完 CSS/JS 都必须重启其服务,否则页面仍加载旧资源(本插件的复测会如实显示"已修复 0")
安装
# 已安装 dsh 命令
dsh plugin --profile web add dsh-design-audit
# 或不经全局安装
npx @deepseek-ai/dsh plugin --profile web add dsh-design-audit
本包是 bundle 形态:package.json 声明 dsh.bundle.patch: ./cordis.patch.yml,
patch 里以包名自引用(name: dsh-design-audit)插入插件。
dsh plugin add 会把它写进 profile 的 dependencies + bundles,启动时由 bundles 组合装配。
开发期(本机源码)
dev_inject_plugin dir=D:\plugins\dsh-design-audit # 运行时注入(改源码即生效)
dev_reload_package packageName=dsh-design-audit # 源码改动后热重载
一条踩坑记录(留给后来者)
本插件尚未声明 dsh.bundle 时,曾被误写入 dsh.profile.bundles → profile 下次启动硬崩:
dsh-app-boot 会对每个 bundles 条目校验 dsh.bundle 声明,缺失即 throw(报错原文:
profile bundle "..." declares no dsh.bundle in its package.json)。
现在包已声明 dsh.bundle,bundles 就是正确路径 —— 但这条教训对所有"纯 host 工具型"插件仍然成立:
要么声明 bundle,要么用 patch 装配,不要只往 bundles 里塞名字。
依赖解析:node_modules/@deepseek-ai/dsh-tools 是指向当前 dsh 安装体的 junction(与 dsh-skill-audit / dsh-agent-frugality 同一物化方式)。纯 ESM 直出,无构建步骤。
结构
lib/cdp.js 无头浏览器驱动(CDP over WebSocket,零依赖;含渲染就绪门控)
lib/collect.js 页面内取证脚本(自包含,序列化后注入页面;含遮挡命中测试与动效跨时间采样)
lib/checks.js 判据引擎(纯函数,可单测)
lib/report.js Markdown / 自包含 HTML 报告(首屏图内嵌可读、整页长图链接存档)
lib/index.js 三个工具的注册与编排
test/e2e.mjs 端到端闭环验证(42 断言,含 JS 动效正/反用例与判据分支单元断言)
test/smoke.mjs CDP 通路冒烟
test/fixtures/ 有意植入缺陷的靶站(含遮挡用例、JS 动效用例、已处理 reduced-motion 的反向用例)
No comments yet. Be the first to write one.