单文件 HTML 工作台

SKILL开发
开发 未标注权限 许可证不明确
安装这个技能

往既有的单文件 HTML 工作台(如 个人工作台\index.html,3MB 级、内联全部数据)注入新业务模块,并用 Edge 无头 + 注入 harness 做回归验证;也覆盖「给工作台加本机桥接能力」(拉起程序、统一浏览器开外链、平台内联网检索)。当用户说「给工作台加个模块」「把 XX 接入平台」「工作台回归测试」「再开一条业务线」「工作台里查一下 XX / 不要开外部浏览器,结果展示在平台」时使用。 支持X402协议支付调用,按次付费。

一句话选型结论

按资料字段整理

单文件 HTML 工作台是开发类的 Agent Skill,安装方式未标注,权限级别未标注,许可证不明确,有可直接复制的安装命令。技能本质是给模型读的说明书,效果取决于模型与客户端;装完先用一个小任务验证,再放进正式流程。

分类开发 安装方式未标注 权限级别未标注权限 许可证许可证不明确
累计安装 未标注 本周 未标注
GitHub 星标 未标注 仓库 未标注
下载量 未标注 技能市场下载数
权限级别 未标注权限 安装前请确认风险范围
许可证 许可证不明确 未说明

安装

未标注
在支持 Skills 的客户端里执行,装完问一句「你有哪些技能」验证
skillhub install user_b24dc7bf/single-file-html-workbench
  1. 1
    确认客户端支持

    确认你的客户端支持 Skills(Claude、Codex、Cursor 等),并升级到较新的版本。

  2. 2
    拿到技能文件

    执行上面的命令安装;也可以把仓库里的 SKILL.md 手动放进客户端的 skills 目录。

  3. 3
    刷新技能列表

    重新打开客户端,或刷新技能列表,让新放进来的技能被加载。

  4. 4
    验证是否生效

    问一句「你有哪些技能」,确认列表里出现它,再拿一个小任务试跑。

技能档案

开发
字段来自技能仓库与公开索引,中文说明由本站标注
作者 / 维护方user_b24dc7bf仓库 未标注 · 版本 未标注
宿主客户端未标注技能可被这些客户端加载
安装方式未标注安装包 community
权限级别未标注权限安装前请确认风险范围
许可证未说明许可证不明确,商用前请自行确认条款
仓库可用性available技能仓库当前是否可访问
最近核验2026-11-06上游最后核验时间 · 数据更新 2026-11-06
正文体量32.6 KBSKILL.md 原文大小,越大说明说明书写得越细

典型使用场景

3 类
按分类整理的常见用法,具体以 SKILL.md 为准
写代码与改 bug

按需求补代码、定位并修掉报错。

读仓库与生成改动

让模型读懂项目结构,再给出可落地的改动。

跑测试与排查故障

按现象排查问题,补测试用例并复跑。

选择建议

装之前先看这几条
按条目字段给出的判断,不替你决定用不用

值得选的地方

  • 有可直接复制的安装命令,不用自己翻文档找入口。

先想清楚的地方

  • 没有标注权限级别,实际能做什么要装好后确认。
  • 许可证不明确,商用或二次分发前先到仓库确认条款。
  • 没有做站内核验,内容以作者仓库为准。
  • 技能只是说明书,实际效果取决于模型与客户端,装完先用小任务试一遍。

更适合

  • 需要在开发场景里给模型补一套做法的人。
  • 愿意先读一遍 SKILL.md、确认它会让模型做什么的人。

不太适合

  • 对许可证要求很严、必须明确可商用的人。
  • 只想装完就自动变强、不打算看说明书的人。

同类对比

6 个同类
同分类里挑最接近的几条,字段以各自详情页为准
技能权限级别安装方式累计安装宿主客户端安装命令
单文件 HTML 工作台当前 未标注权限 未标注 未标注 未标注 有
技能审查员 浏览器控制 ClawHub 市场 877.9 万 OpenClaw 有
Vercel React 最佳实践 浏览器控制 GitHub 仓库 849.1 万 Codex、Claude、Cursor 有
GitHub 代码协作 联网访问 ClawHub 市场 763.3 万 OpenClaw 有
网页设计指南 联网访问 GitHub 仓库 668.6 万 Codex、Claude、Cursor 有
本体论 只读 ClawHub 市场 663.7 万 OpenClaw 有

SKILL.md 全文

32.6 KB
模型实际读到的说明书原文,来自技能仓库

单文件 HTML 工作台 · 模块注入与回归验证

适用于 <本地路径> 这类单文件、零外链、localStorage 持久化、
体积 3MB 级
的工作台。核心难点:文件太大不能整体重写,必须做定点注入;改完必须验证,
否则一个吞掉的函数声明能让整个页面白屏。

一、注入前:先摸清五个挂点

新增一个业务模块(以「可研审核 → nav key = fs」为例)需要同时挂到 5 处,缺一个就残废:

挂点 位置 说明
导航项 侧栏 data-nav="fs" 与 PAGES 的 key 一致,否则点击无反应
PAGES 映射 var PAGES = {...} 加 fs:'page-fs';决定 hash 路由
页面容器 <section class="page" id="page-fs" hidden> 用 hidden 属性,不是 class
数据块 /*XX_START*/ … /*XX_END*/ 成对标记,便于下次脚本重建时整块替换
渲染入口 refreshAll() 里加 renderXxxAll() 否则刷新后新模块是空的

状态持久化另需:boot() 里调 xxxLoad();localStorage key 统一前缀 wb_。

二、注入方法:写 Python 脚本,不要手改

3MB 文件不适合用 Edit 反复改大段。写一次性脚本(_add_xxx.py),用成对标记做幂等替换:

p = r'<本地路径>'
s = open(p, encoding='utf-8').read()

def inject(s, start, end, body):
    """标记存在→整块替换;不存在→在 end 锚点前插入"""
    ...

s = inject(s, '/*FS_DATA_START*/', '/*FS_DATA_END*/', data_js)
open(p, 'w', encoding='utf-8').write(s)

铁律:注入前先 open(p,encoding='utf-8').read() 并 cp 一份 index.bak.html。

三、三个高频坑(都已踩过)

  1. 吞掉函数声明 — 插入 var FS_DEMO={...} 时若少了换行,会把下一行的
    function renderFsCalc(){ 吃掉,页面全白。注入后必查:
    node --check + grep 确认每个 function xxx 仍独立成行。
  2. 重复 const 声明 — 曾经同时存在 const KB 与 var KB,文件涨到 6.09MB。
    数据块一律用 var,且全文件只允许一处声明。
  3. 事件委托误插 — 把新增的 fs-* 分支写进了 $('#fileInput') 的 if 块里,
    导致永远不触发。文件级事件是统一委托(closestAttr(el,'data-act')),
    新分支必须加到主 switch/映射上,不要嵌套进别的元素判断里。
  4. Word/WPS 导出的 PDF 会把字符「逐字定位」,提取文本变成 项 目 概 况、5 0 0 0 2 平方米、
    3 . 1 4。任何关键词检索/数字正则都会失配(章节全报「缺失」、数字只读到末位 2平方米)。
    解法是在检索层统一归一,不要指望解析层永远完美:
    - 汉字:删「汉字/中文标点之间」的空格(fsNormCjk),保留换行。
    - 数字:删「处于数字/小数点之间的空格」(fsNormNumText),5 0 0 0 2→50002。
    - 归一必须区分「逐字拆散」与「表格相邻列」:只合并左右各自仅 1 个字符的空格
    (逐字拆散=每段 1 字);若任一侧是多字符完整数字(表格列 44,000.00 100.00)绝不合并,
    否则会把金额列与占比列粘成 44,000.00100,抽取值整体错位。
    参考实现:(?<![0-9.,])([0-9.])[ \t\u00a0\u3000]+(?=[0-9.](?![0-9.,])) + 最多 8 遍循环。
    - 同步把复合单位并进正则(万平方米|万平米|万㎡),并做量纲折算(万×1e4、公顷×1e4、亩≈666.67)
    统一到基准单位,否则「5万平方米」与「50000平方米」无法勾稽。
    - 回归要同时锁两个方向:拆散文本「能读全」,表格文本「不粘连」。
  5. 表头 / 表号 / 行号会冒充数值,比「读不全」更隐蔽。真实投资估算表踩过的:
    - 表头「占建设总投资比例(%)」→ 关键词「总投资」被当取值标签,抓到下一行的序号 1;
    - 标题「建设投资估算 表11-1」→ 抓到 11;
    - 反向兜底(数字→标签)抓到行号 6(「6 建设期利息」)。
    在「取值构造函数」(如 fsMkHit)里加闸门:① 数字后紧跟 - 的排除(表/图编号);
    ② 反向匹配里「无单位的小整数(1–2 位)」排除(行号);③ 钱/面积类指标,标签与数字之间的 gap
    含 占|比例|比重 或单位是 % → 排除(表头/比率句)。
    关键教训:判据只能用「标签↔数字」之间的 gap,绝不能用相邻字符窗口——否则会被上一行的 %
    (表头前一行 6.81%)或括号里的参数(FNPV(ic=6%))误伤,把正确值也杀掉。
    另外:金额要让 v>=0——投资表里「建设期利息 0 / 增值税 0」是合规写法,视作缺失会误报「未检索到建设期利息」。
  6. 能读表就顺手做「表内勾稽 + 规模匹配」,价值远高于单点抽值:
    逐行解析「序号 名称 金额 占比%」→ 校验 分项合计=合计行、各占比=金额/合计、基本预备费=费率×工程费用;
    再按「工程规模与投资相匹配」提示可能错误的数字并给修改建议
    (如工业项目「设备购置费 < 安装工程费」多为少写一位;单位建筑面积投资偏离常规区间)。
    解析行要容许「同行」与「每列各占一行」两种 PDF 提取形态,名称允许带括号注释。
  7. 抽 XML 文本的正则要限定标签名边界:&lt;w:t[^&gt;]*&gt; 会先匹配到 &lt;w:tblPr&gt; / &lt;w:tcPr&gt;,
    于是把整段表格标记当正文吞进去。必须写 &lt;w:t(?:\s[^&gt;]*)?&gt;(标签名后紧跟空白或 &gt;)。
    通用版:&#x27;&lt;&#x27; + tag + &#x27;(?:\\s[^&gt;]*)?&gt;([\\s\\S]*?)&lt;/&#x27; + tag + &#x27;&gt;&#x27;。
  8. 解 zip 里的条目要用 DecompressionStream(&#x27;deflate-raw&#x27;),不是 &#x27;deflate&#x27;
    (zip 存的是裸 deflate,无 zlib 头)。用错会静默拿到空串。
  9. Edge 无头 --dump-dom 会把内联 JS 源码一并 dump,于是脚本里的
    class=&quot;sk-card&quot;、data-act=&quot;…&quot; 等字符串也会被 grep 命中,计数比真实 DOM 多。
    核对渲染数量时,要么选只存在于 DOM 的锚(如 id=&quot;…&quot;),要么先扣掉脚本里的字面量。

三 bis、需要「启动本机程序」时:加一个本地桥接服务

浏览器安全沙箱不允许 file:// 页面拉起本机进程(会静默失败或直接报 CORS)。
当工作台要「运行本机 Python 脚本 / 看控制台输出 / 打开生成的报告」时,
正确解法是旁边放一个零依赖的本机 HTTP 桥接,而不是试图在页面里做。

服务端(agent_bridge.py,纯标准库)

  • 仅绑 127.0.0.1,端口被占则从 8777 自动顺延;同时支持 CORS 头和 JSONP。
  • 接口:/api/status(探测项目路径、脚本、解释器、报告清单)、/api/run(拉子进程)、
    /api/tail?id=&amp;from=N(增量取 console 输出,threading.Condition.wait(0.9) 兼顾实时与省线程)、
    /api/stop、/api/report(返回 HTML 原文给 iframe 直接 src)。
  • 安全底线:只能跑「白名单项目 × 白名单参数」,shell=False,不拼接任何用户传入的自由文本;
    报告路径只取 basename 再校验所在目录,杜绝 ../ 穿越。同一项目同时只允许一个实例。
  • 子进程务必设 PYTHONIOENCODING=utf-8、PYTHONUNBUFFERED=1,并用 python -u,
    否则 Windows 下中文乱码且输出不会实时流出。解释器的 pandas 等依赖要保证齐全,
    不要假设 PATH 上的 python 就有包——要逐个候选跑 python -c &quot;import pandas&quot; 实测。
  • 「停止」要 proc.kill() + taskkill /PID &lt;pid&gt; /T /F 双保险,否则残留进程。

前端

  • 所有 GET 走 JSONP(动态 &lt;script&gt; + window[cb]),不走 fetch。
    理由:file:// 的 origin 是 null,某些浏览器收紧策略时 CORS 响应会被吞掉;
    &lt;script&gt; 标签不受同源策略限制,实测可行——这是唯一能保证「怎么都能用」的通道。
  • iframe 指向 http://127.0.0.1:PORT/api/report?agent=xxx 即可在平台内嵌看报告。
  • 必须优雅降级:桥接探测失败时,运行按钮自动换成「打开文件夹」链接,页面零报错;
    并提供「未启动怎么办」弹窗(含可复制的启动命令)。
  • 刷新后要能接管:长任务动辄十几分钟,用户很可能刷新页面。
    启动/探测时若发现服务端仍在跑某任务,自动重建控制台并续接 tail,同时把卡片置为「运行中」并禁用按钮。
  • 「外链统一用某个浏览器打开」也走桥接(2026-10-05 实测):
    file:// 页面无法指定用哪个浏览器开外链(只能交给系统默认),
    但可以让桥接加一个 GET /api/open?url=...,用 subprocess.Popen([firefox_exe, url], shell=False)
    拉起指定浏览器。前端用捕获阶段的全局 click 监听统一接管:

js document.addEventListener(&#x27;click&#x27;, onExternalLinkClick, true); // 捕获:先于 onClick 的 data-act 分发

必须守住的边界(缺一条就会踩坑):
- 只接管 ^https?://;file:// / mailto: / 相对路径 / 带 download 的链接原样放行
—— 工作台里「打开文件夹」「打开原文」都是 file://,误接管会直接坏功能。
- 能力不满足时绝不 preventDefault(桥接离线 / 未装目标浏览器)——回退系统默认,
否则点链接会「毫无反应」,比不接管还糟。
- 桥接侧把「本机是否装了该浏览器」放进 /api/status(如 browser:{firefox,path}),
前端据此决定是否接管;先探测再接管,不要盲拦。
- 忽略修饰键(metaKey/ctrlKey/shiftKey/altKey)与中键 —— 尊重用户自己的开窗习惯。
- 打开失败要可见提示 + 回退 window.open,不能静默失败。
- 服务端只放行 http/https,且用 [exe, url] 数组传参(不拼字符串),路径可用环境变量覆盖。

三 ter、需要「在工作台里联网检索」时:桥接内做多引擎后台检索

同一类需求的第二面(2026-10-05 实测):用户要「找项目、把结果直接展示在平台里,
不要在外部打开浏览器
」。file:// 页面不能自己跨域抓取,所以联网这件事同样得落到桥接里,
再让前端把结果渲染成平台内的卡片(而不是给一堆外链让人自己点)。

关键设计:一次查询 = 多引擎并发 + 硬预算

单一搜索引擎不可靠(会限流、会走代理隧道失败、会弹安全验证)。做法是四路并发、谁快用谁:

_BID_ENGINES = ((&quot;360搜索&quot;, _so_search), (&quot;中国政府采购网&quot;, _ccgp_search),
                (&quot;百度&quot;, _baidu_search), (&quot;必应&quot;, _bing_rss))
_BID_BUDGET = 9.0        # 硬预算:到点未回的源直接标「超过 N 秒预算,本轮跳过」
futs = {ex.submit(fn, q, n): name for name, fn in _BID_ENGINES}
done, not_done = wait(list(futs), timeout=_BID_BUDGET)   # 不等齐,够时间就出榜
ex.shutdown(wait=False)
  • 别串行、别死等:政采网响应 0.5s~20s 还会限流,串行会把整轮拖成十几秒。
  • 每个源独立降级:_baidu_search 遇 &lt;title&gt;百度安全验证 直接 raise,
    前端把该源显示成 百度 ✗——如实报错,绝不假装"没搜到"(否则用户以为全网无结果)。
  • 统一打分再排序,把「招标公告」顶到前面:_bid_score(招标+4/采购+2/公告+2/中标成交+2/
    项目+1/投标标段询价比选+1,封顶 10);_norm_key(忽略 www/query/末尾斜杠)跨源去重。
  • only_bid 时真过滤([x for x in merged if x[&quot;score&quot;]&gt;=3]),不要「全不命中就回退」——
    那等于没过滤,用户会以为筛选坏了。

抓取层的四个必备细节

  1. _http_get 返回三元组 (正文, Content-Type, final_url) —— final_url(r.geturl())
    是跳转后的真实地址,后续「看正文」要用它,不能用请求时的壳地址。
  2. 先删强调标签再去标签(_clean_label):搜索结果标题常带 &lt;em&gt;关键词&lt;/em&gt; 高亮,
    直接 re.sub(&#x27;&lt;[^&gt;]+&gt;&#x27;,&#x27;&#x27;) 会把 天津厂房 两边的标签变成空格 →
    中交建筑九公司 天津厂房 项目。必须先 re.sub(r&#x27;&lt;/?(?:em|b|strong|span)[^&gt;]*&gt;&#x27;,&#x27;&#x27;,s) 再清标签。
  3. 跳转壳延后解析(_is_link_wrapper + _resolve_redirect):百度/搜狗的结果链接是
    .../link?url=... 壳,不要在每个引擎里同步解析(慢且重复);等榜单排序定下、只对
    前 8 条壳做一次 _resolve_redirect。360 则优先用它给的 data-mdurl 免跳转直取。
  4. 垂直源要剥 site:(_strip_site):把「百度结果里的 site:ccgp.gov.cn」原样丢给
    政采网自己的站内搜索,只会搜不到。
  5. 出口要过 SSRF 闸门(_safe_url:仅 http/https 且解析到公网)——但注意
    它会让离线单测失败(假域名 DNS 解析不了),测试里必须 patch 掉它。

前端侧:结果落在平台内,正文就地看

  • 删掉一切检索外链:&lt;button data-act=&quot;query-project&quot;&gt;查询项目▸&lt;/button&gt; 代替
    &lt;a target=&quot;_blank&quot;&gt;。弹窗里零 &lt;a&gt;、零 target=&quot;_blank&quot;——可用一条
    !/qpModal[\s\S]{0,20000}target=&quot;_blank&quot;/.test(html) 断言锁死。
  • 新增 qpViewBody(i):就地展开/收起正文,bridgeCall(&#x27;/api/fetch&#x27;,{url},25000),
    正文块用 white-space:pre-wrap + max-height + 截断提示(如 1600 字)。
  • 标题用纯文本节点(&lt;div class=&quot;qp-t&quot;&gt;)而不是链接——从渲染层堵死"变成外链"。
  • 显示各源健康状态(360搜索 7 | 百度 ✗ | 必应 6):让人一眼知道哪路挂了、是没结果还是没搜成。
  • 接口参数升级成 _api_search 支持 n(夹取 1–30)与 only=bid,前端传 {q, n:15}。

验收(关键:证明"没开浏览器")

  • 端到端起桥接 → curl /api/search 拿到真实公告 → 检索前后 tasklist | grep -c firefox 数字不变
    ⚠️ 这才是"只在后台跑、不开浏览器"的硬证据,不要只用"没报错"敷衍。
  • 前端四套:test_project_query.js(去外链 + qpViewBody)、test_bid_links.js(第三项改按钮)、
    test_query_headless.js(场景 D:桩桥接出结果 + 就地看正文,断言 dump > 4MB)、
    test_bridge_search.py(纯离线 unittest,桩 _http_get 和 _safe_url)。

三 quater、让「操作员在页面里就能启动服务」:自定义协议 + 静默启动器

> 触发场景:页面提示「请双击 xxx.bat 启动服务」,但页面里没有入口,操作员无从下手。
> 根因是 file:// 页面不能拉进程;光改提示文案没用,要给一条真能点的通路。

三件套

组件 作用
静默启动器 &lt;name&gt;_silent_launch.py 幂等:先探端口范围的探活接口,命中即退 0;否则 pythonw + CREATE_NO_WINDOW 隐藏拉起
自定义协议 HKCU\Software\Classes\&lt;proto&gt; 页面点一下 → Windows 去执行启动器(免管理员、仅当前用户)
Startup 目录 .lnk 登录后自动常驻,日常无感

启动器要点

  • 探活必须校验服务自述(如 svc == &quot;agent-bridge&quot;),只看端口通不通会把别的服务当成自己。
  • 命名互斥体(ctypes.WinDLL(&quot;kernel32&quot;).CreateMutexW + ERROR_ALREADY_EXISTS)防并发;
    拿到锁后再复检一次端口,否则两个并发调用会各起一个。
  • creationflags = DETACHED_PROCESS \| CREATE_NO_WINDOW \| CREATE_NEW_PROCESS_GROUP。
  • 脚本内不 print、不 input(pythonw 下 sys.stdout 是 None),一律写文件日志。
  • 退出码约定化(0 已跑/成功、2 无解释器、3 拉起后未就绪、4 异常),便于排查。

前端要点

  • 用动态 anchor + .click(),绝不用 location.href=&#x27;proto://...&#x27;:
    协议未注册时后者会把整个页面导航走(白屏),前者只会静默失败。
  • 触发后轮询探活(如 500ms × 12s),不要假设「点了就成功」;失败要落到可操作的帮助弹窗。
  • 状态提示只留一条:长说明放结果区,按钮旁只放「已连接/未连接」短状态,
    同屏出现 3 条「未连接」是典型的重复文案问题。
  • 新按钮走既有的全局 data-act 委托;不要在弹窗里覆写结果区去塞按钮(会打破别的场景的 DOM 断言)。

四个坑(都实测过)

  1. 沙箱屏蔽 reg.exe / wmic.exe → 注册表一律用 Python winreg 或 PowerShell,
    且只写 HKCU。写前先读:若协议已被别的应用占用,报冲突而不是覆盖。
  2. 单测会污染生产日志:被测模块写文件日志时,setUp 必须先把日志路径重定向到临时文件,
    tearDown 恢复并删除。否则跑一次单测就往真实日志里塞一堆假记录。
  3. 中文 Windows 的 netstat 输出是 GBK:subprocess.run(..., text=True) 会整段解码失败、
    stdout 变 None(表现为"找不到进程")。要 capture_output=True 后手动 .decode(&#x27;gbk&#x27;,&#x27;replace&#x27;)。
  4. 并发排序要带兜底键:多引擎并发合并时若只按分数排序,同分条目顺序会随线程完成顺序抖动
    (flaky 测试的根因)。加 key=lambda x: (-score, url) 让输出确定。

验收

  • os.startfile(&#x27;proto://start&#x27;)(或点页面按钮)→ 端口 LISTENING、无黑窗;日志有 banner。
  • 幂等:已跑时再触发 → pythonw 进程数不变,日志 already running ... exit 0。
  • ⚠️ 端到端能否探活取决于进程怎么被拉起:ShellExecute / 协议拉起的不受沙箱网络限制,
    可从脚本内直接验证;而 Bash 直接 python -c 派生的进程探活可能被沙箱拦(表现为"端口开着也连不上"),
    别据此误判成 bug ——用 netstat + tasklist 佐证。

四、回归验证:Edge 无头 + DOMContentLoaded harness

1. 语法校验(必做,5 秒)

// _check.js —— 抽出全部内联 script 后 node --check
const re=/&lt;script(?![^&gt;]*\ssrc=)[^&gt;]*&gt;([\s\S]*?)&lt;\/script&gt;/g;

> 注意:只用 --check 不做静态分析,避免误报。

2. 交互回归(关键)

生成 _test.html = 原文件 + &lt;/body&gt; 前注入 harness。必须挂 DOMContentLoaded:
脚本在解析期执行,boot() 还没跑,直接取 DOM 会全空;挂在 boot 之后(后注册的
监听器后执行)才能拿到渲染结果。

window.addEventListener(&#x27;DOMContentLoaded&#x27;, function(){
  var out=[], __err=[];
  // 逐页导航:任何一页 hidden 或抛错都记下来
  Object.keys(PAGES).forEach(k=&gt;{ nav(k); ... });
  // 事件模拟:click 之后必须再 dispatch change,否则委托监听不到
  cb.click(); cb.dispatchEvent(new Event(&#x27;change&#x27;,{bubbles:true}));
  document.getElementById(&#x27;TESTOUT&#x27;).setAttribute(&#x27;data-r&#x27;,&#x27;[&#x27;+out.join(&#x27;][&#x27;)+&#x27;]&#x27;);
});

运行(Git Bash):

EDGE=&quot;/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe&quot;
&quot;$EDGE&quot; --headless=new --disable-gpu --no-sandbox \
  --virtual-time-budget=8000 --dump-dom &quot;file:///F:/.../_test.html&quot; &gt; _dump.html
grep -o &#x27;data-r=&quot;\[.*\]&quot;&#x27; _dump.html | head -1 | sed &#x27;s/\]\[/\n/g&#x27;

3. 必备断言清单

  • PAGES 数量与逐页 hidden 状态(导航回归)
  • 新模块容器 innerHTML.length &gt; 0
  • 至少 1 个勾选类交互:断言前后文本变化(0/58 → 1/58),不要只断言不报错
  • window.onerror 收集:最终必须是 ERRORS=none
  • 统计卡 foot 文案正确(暴露汇总逻辑错误)

4. harness 选择器要先核实

教训:写 .lib-filters select 得到 NONE,真实是 #libFilter;.pcard 在库列表里
类名不同。先 grep 出真实 id/class 再写断言,否则假阴性会让你误判功能坏了。

5. 注入 harness 的时序坑:别嵌套 setTimeout、别覆盖页面全局函数

2026-10-05 实测(项目看板新增「查询项目」按钮,需要"点开弹窗再点弹窗内按钮"两级交互):
同一份 HTML,注入方式不同,结果天差地别——

  • load + 单层 setTimeout(..., 400) 点击 → 弹窗正常打开,断言全绿;
  • 改成嵌套 setTimeout(先开弹窗、再点里面的「开始查询」),
    或在 load 回调里覆盖页面全局函数(想桩掉桥接:window.detectBridge=()=&gt;Promise.resolve(false))
    → 页面只渲染了一部分,dump 体积从 4.1MB 掉到 2.75MB,弹窗根本没开,
    且 stderr 里一条报错都没有
    。表现像"功能坏了",实际是页面被 dump 在半渲染状态。

原因:--virtual-time-budget 下虚拟时钟会被页面自身 setInterval 快进,
多层定时器 / 中途改写全局函数会打乱 boot 的推进节奏。

对策:
- 注入脚本只保留一层 setTimeout,一次只测一个动作;多级交互拆成多次运行;
- 需要桩掉某函数(桥接探测、随机数、当前时间)时不要覆盖全局——
该分支交给 Node 侧 vm 桩测试(本次「桥接离线降级」就是 test_project_query.js 覆盖的),
浏览器侧只验"能开、能渲染、无报错";
- 浏览器侧判据三样:class=&quot;mask on&quot;(弹窗真开了)、目标容器内有真实渲染内容、
stderr 过滤 Uncaught|not defined|not a function|SyntaxError 为空。

> 当晚更正:上面「别嵌套 setTimeout」的结论过强了。
> 同日补做「无项目也能查询」三场景回归(个人工作台/项目看板-测试/test_query_headless.js),
> 用的就是两层嵌套 setTimeout(500ms 开弹窗 → 700ms 点弹窗内按钮),
> 三场景 22/22 全绿、dump 稳定 4.16MB。真正的杀手是 虚拟时钟预算被真实网络探测吃光:
> 流程里一旦触发对 127.0.0.1:8777 的桥接探测,--virtual-time-budget 就被这个"慢"请求耗完,
> dump 停在半渲染状态(约 2.75MB)。所以:
> - 判据补一条 dump 体积——正常约 4.1MB,掉到 2.7MB 左右即半渲染,别急着怀疑功能坏了;
> - 流程会碰桥接探测的,--virtual-time-budget 给到 12000 以上;
> - 想彻底绕开就用 Node 侧 vm 桩覆盖该分支,浏览器侧只验"能开、能渲染、无报错"。

8. 「2.7MB 半渲染」还有第二个成因:读 dump 太早(比虚拟时钟更常见)

同晚再查:把上面那套 harness 改成 execFileSync(EDGE, ..., {stdio:[&#x27;ignore&#x27;, outFd, errFd]})
(Edge 直接写文件、Node 用 fd 打开)后,出现同一份文件
Node 读到 2,759,822 字节 → 几秒后同一路径变成 4,166,231 字节。
即:Edge 的启动进程已退出、execFileSync 已返回,但它派生的子进程仍在往同一个 fd 里续写 dump。
读早了就拿到"看着像半渲染、其实只是没写完"的 DOM——&lt;html&gt; 结尾还完整,极难察觉。

对策(写进 test_query_headless.js 的 waitStable()):

/* 等文件大小连续 3 次不变再解析;别用睡眠猜时长 */
let last=-1, same=0, t0=Date.now();
while(Date.now()-t0 &lt; 8000){
  const size = fs.statSync(file).size;
  if(size&gt;0 &amp;&amp; size===last){ if(++same&gt;=3) break; } else { same=0; }
  last = size; /* 同步小睡 100ms */
}

并且给 dump 加一条体积下限断言(本工作台正常 ≈4.1MB,断 size &gt; 4000000),
一旦真出现半渲染会立刻红,而不是让后续断言产生莫名其妙的假阴性。

四、已落地模块示例:总图概念设计(nav key = siteplan)

2026-10-06 接入,挂在建筑设计板块入口卡 + 侧栏 nav siteplan。

五个挂点对照(与上文「可研 fs」同构,但走的是"前端 UI + 独立 site-planning 技能判定"混合形态):

挂点 实现
导航项 &lt;button class=&quot;navi sub&quot; data-nav=&quot;siteplan&quot;&gt;(建筑设计按钮子级)
PAGES 映射 siteplan:{title:&#x27;总图概念设计&#x27;,sub:&#x27;…&#x27;}(挂在 cad 之后)
页面容器 &lt;section class=&quot;page&quot; id=&quot;page-siteplan&quot; hidden&gt;
数据/JS 块 SITEPLAN_JS 整段注入到 function boot(){ 前(用 function initSiteplan(){ 作存在性守卫)
渲染入口 boot() 内 initSiteplan();;事件委托 data-act=&quot;sp-generate/sp-cad/sp-reveal/siteplan-open&quot;

配套后端:~/.workbuddy/skills/site-planning/(Python 引擎 + agent_bridge.py 的 /api/siteplan 五端点 + 中望 CAD /b 布图.scr 拉起)。

重注入坑(务必记牢)

_add_siteplan.py 用 function initSiteplan(){ 已存在则跳过整段 JS 重注入。
所以改过 SITEPLAN_JS 后重跑脚本不会生效——必须直接改 index.html 里已注入的对应函数,
并同步改 _add_siteplan.py 源,让两者一致(将来在干净 index 上重跑才正确)。

该模块的功能要点

  • 输入三类:规划条件 / 甲方需求楼栋(名称·面积㎡·层数·高度m)/ 地形(假设矩形 | 手动红线坐标 | DXF红线"后续接入"占位)。
  • 单体楼栋可上传外轮廓 DXF:前端每栋行加 input[type=file],spGenerate 用 FileReader.readAsText 读 DXF 文本随 payload 提交;
    后端 sp_bld_dxf.parse_outline() 取面积最大的闭合多段线,mm/m 单位换算,按真实轮廓布图与核算基底面积
    (布图仍用轴对齐外接框占位,与道路栅格模型自洽)。
  • 指标口径:HTML 报告 / DXF 图签 / SVG 图签 三处统一调 sp_metrics.fmt_num(),确保数字同源同格式。
  • 回归:Python tests/ 全套(含 test_consistency.py、test_bld_dxf.py)+ test_siteplan_pure.js(node --check)+ test_siteplan_headless.js(Edge 无头)。

9. 无头测试里别「真点」会导航的链接 —— 它会把 dump 毁掉

2026-10-05 实例(工作台外链改用 Firefox):为了验证「file:// 链接不被接管」,
在 harness 里对 &lt;a href=&quot;file:///F:/workbuddy&quot;&gt; 派发了真实
dispatchEvent(new MouseEvent(&#x27;click&#x27;,{bubbles:true,cancelable:true}))。
结果是页面真的导航走了(这恰恰证明它「没被接管」是正确行为),
dump 只剩 318KB、&lt;title&gt; 变成了那个 file URL,harness 的结论全丢,看着像功能坏了。

对策:
- 正向用例(期望被接管、preventDefault 已生效的链接)可以真点——不会导航;
- 反向用例(期望不被接管的 file:// / 非 http 链接)只发合成事件对象
({target:a, button:0, preventDefault(){this.prevented=true}} 直接调处理器),
或用 e.preventDefault 是否被调用作为判据,永远不要真的 dispatch 一次会导航的点击;
- 同理,浏览器级用例里桩 window.bridgeCall 而不是让桥接真去开浏览器——
否则每跑一次测试就在用户屏幕上弹一个窗口。真实拉起的验证放在 curl 端到端里做一次即可。

7. 用户报的 BUG,可能正被自己的测试断言保护着

2026-10-05 实例:用户反馈「项目看板里查询项目需要先新增一个项目」。根因是
openProjectQuery() 首行 if(!state.projects.length){ toast(&#x27;还没有项目,请先新增一条&#x27;); return; },
而上一轮我自己写的 test_project_query.js 里赫然断言了这个行为:

ck(&#x27;无项目时不打开弹窗&#x27;, api.modals.length === 0);
ck(&#x27;无项目时给出提示&#x27;, /新增/.test(...));

单测全绿、回归跑多少遍都发现不了——BUG 被"测试固化"成了期望行为。

所以:拿到用户报的 BUG,第一步不是读实现,而是先 grep 测试文件里有没有把该行为写成断言。
找到后先翻转断言(改成「无项目也必须能打开」),跑一遍确认变红,再动实现,
这样才真正证明了「改前红、改后绿」,而不是改完顺手把断言也一起改了。

6. 改"默认值"做无头测试前,先搜全部出现处

同一次实测:想把看板默认切到 kanban 视图,只替换了 state 字面量与
load(KEY.view,&#x27;table&#x27;) 两处,漏了 initState() 里 seed 分支的赋值语句
state.projView = &#x27;table&#x27;; → 无头跑出来仍是表格视图,白白怀疑实现有问题。

改默认值前先 grep -n 该字面量,把字面量与赋值语句一起替换,并打印残留数确认清零。

五、收尾

  • 删除 _check.js / _tmp_all.js / _mkharness.js / _test.html / _dump.html
    (单个 rm -f,不要用 rm -rf,会触发沙箱批量删除拦截)
  • Node 里 execFileSync 调 Edge 时,收尾绝不要 fs.rmSync(profileDir,{recursive:true}):
    Edge profile 目录有几百个缓存文件,递归删除会被沙箱拦截,
    表现为整个 node 进程被 SIGTERM 杀掉、连最后那行汇总都来不及打印
    (表面上像"测试挂了",其实前面所有 PASS 都已正常执行完)。
    对策:多个场景共用一个 profile 目录且不删;只逐个 rm -f 自己造的夹具/dump;
    并且先打印汇总、后做清理。
  • 保留 index.bak.html
  • 用 present_files 把 index.html 呈现给用户(HTML 会自动开预览面板)

六、配色扩展约定

新业务线新增一个 CSS 变量三元组,不要复用:

--rev:#6a4a93; --rev-bg:#f4effa; --rev-line:#dccfed;   /* 可研审核 */
.t-rev{color:var(--rev)}
.biz-c .biz-head{background:var(--rev-bg);border-bottom-color:var(--rev-line)}
.badge.b-rev{background:var(--rev-bg);border-color:var(--rev-line);color:var(--rev)}

既有:建筑 --arch 深蓝 / 投资 --inv / 红涨 --red:#c0392b / 绿跌 --green:#27865a。

<!-- PAYGATE:BEGIN -->

付费与交付(X402 微信 Agent Pay · 微信支付 AI 专属卡 · ¥0.01/次)

费用由用户直接支付给服务提供方。SkillHub 仅提供展示、投诉处理及平台服务,不作为收款方。请在支付前确认服务内容、计费单元、单价与退款规则。

  • 单价:¥0.01 / 次(以支付网关价目表与 SkillHub 后台配置为准)
  • 计费单元:每次完整服务需求,支付确认后开始服务
  • 免费操作:支付前的需求沟通、订单状态查询

Agent 行为准则

  • 首次调用返回 402 时,原样向用户出示支付信息并申请授权;不得自行判定支付成功,不得改用免费方式替代履约。
  • 牢记 out_trade_no(订单号),支付后用同一订单号重试,不会重复扣费。

红线

  1. 未拿到 status=paid 收据前,严禁执行任何实质性服务、严禁产出结果。
  2. 所有支付只通过 node scripts/client.js,严禁手动调用微信支付接口。
  3. 严禁向用户展示/朗读 payment_code、transaction_id、私钥等敏感字段。
  4. 设备不满足要求时只输出引导并停止,严禁绕过支付。

付费四步(X402 协议)

  1. 前置检查:当前 Agent 是否具备 weixinpay_pay 能力,不具备则终止并提示。
  2. 请求资源:node scripts/client.js require --skill &quot;single-file-html-workbench&quot; --subject &quot;&lt;需求简述&gt;&quot;,未付返回 402,含 WeixinPay-Required 与 X-Out-Trade-No。
  3. 同时保存“支付码”和“订单号”;系统自动弹出微信支付 AI 专属卡,用户手机授权扣款(Agent 不得自行判断成功)。
  4. 原样重试 require(CLI 自动带 X-Out-Trade-No),服务端查单到账返回 status=paid 收据后,才开始履约。

订单状态免费查询(免费、幂等):node scripts/client.js status --out-trade-no &quot;&lt;订单号&gt;&quot;
<!-- PAYGATE:END -->

常见问题

5 问
答案由本站依据条目字段整理
单文件 HTML 工作台怎么安装?

在支持 Skills 的客户端里执行:skillhub install user_b24dc7bf/single-file-html-workbench。安装方式为未标注。装完刷新技能列表或重开客户端。

单文件 HTML 工作台支持哪些客户端?

资料没有标注宿主客户端,但只要客户端支持 Skills(Claude、Codex、Cursor 等),一般都能加载。

单文件 HTML 工作台权限高吗,安全吗?

权限级别为未标注权限。安装前先看它会读取或写入哪些内容。

单文件 HTML 工作台收费吗?

本站不代收费用,也没有标注具体价格。是否收费取决于技能本身和所用的模型或客户端,正式使用前按作者说明确认。

单文件 HTML 工作台用什么许可证?

许可证标注为 未说明(许可证不明确)。商用或二次分发前请到仓库确认条款原文。

热门专题

按场景挑技能
全部专题 →

技能内容以作者仓库为准;分类、权限级别与许可证状态的中文说明为中文整理,最近核验 2026-11-06。