【免费下载链接】answer-me-with-html
Answer me with HTML — an agent skill that answers hard questions with a one-page HTML you can actually read. 让 AI Agent 用一页 HTML 回答复杂问题。
本文以仓库自带的示例草稿 examples/architecture.md 为主线,拆解 answer-me-with-html 的核心架构:模型只写"内容稿"(扩展 Markdown),CLI 负责解析、排版、配色、暗黑模式、SVG 自动布局和写作风格检查。读完后你会掌握它的渲染管线(parse → STE lint → 组件渲染 → dagre 布局 → 模板组装)、各模块的源码位置,以及出错时"错误 + 正确示例"的自我修复闭环。
1. 设计起点:模型与 CLI 的分工
示例文档开篇就明确了整条管线的分工边界(原文面板 B):
| 工作 | 谁来做 |
|---|---|
| 决定讲什么、分几个面板 | 模型 |
| 写关系、数据和说明文字 | 模型 |
| 排版、配色、暗黑模式 | CLI |
| 计算图形坐标 | CLI(dagre) |
| 检查写作风格 | CLI(STE lint) |
结果:模型不再输出 CSS、JS 和 SVG 坐标。一次 Bash 调用就能拿到成品页面。(原文面板 B 的 callout)
这条分工线在仓库多处可以得到印证:
- package.json 的 description 就是这句话的实现版:"the model writes a short Markdown draft; the bundled CLI handles templates, SVG auto-layout and an STE controlled-writing check"。
- Agent 技能文件 skills/answer-me-with-html/SKILL.md 对模型侧的约束非常直白:"You write only thecontent draft(extended Markdown). The
amCLI does all layout, colours, dark mode and diagram coordinates.Do not hand-write HTML / CSS / SVG." - 依赖面极小:
dependencies只有 @dagrejs/dagre(图布局)和marked(Markdown 正文),说明"排版、配色、暗黑模式"确实全部由 CLI 自带的模板与主题系统完成。
理解了这个边界,示例文档里的每一张图(流程图、目录树、时序图)都只是 CLI 管线的可视化表达,下文逐一展开。
2. 一次 render 的全过程
原文面板 A 用项目自己的flow组件语法画出了主管线(这是一段可以直接交给am render渲染的草稿语法,fence 语言即组件名):
(Markdown 稿件) -> parse: frontmatter + 面板 parse -> STE lint & 组件渲染 STE lint --> 终端警告 组件渲染 -> dagre 布局: flow 组件渲染 & dagre 布局 -> 模板与插槽 模板与插槽 -> *[(单文件 HTML)] group render.js: parse, STE lint, 组件渲染, dagre 布局, 模板与插槽这张图的 group 框就标在render.js上——五个环节全部发生在 src/render.js 的renderDoc()里,CLI 入口 src/cli.js 的cmdRender只是读文件、调它、落盘。按源码顺序拆解:
2.1 parse:从 frontmatter 到面板与块
解析逻辑在 src/parse.js 的parseDoc(),文件头注释一句话概括:"frontmatter → meta;##headings → panels (slots); panel bodies → markdown blocks and fenced blocks",且"只做结构切分,不做渲染"。要点:
- frontmatter 白名单:src/parse.js 的
CHOICES定义了合法取值——template: sheet | doc | video、theme: auto | <主题名>、style: off | 80 | strict、mode: auto | light | dark。示例草稿用了template: doc+theme: shadcn,分别对应"单列线性讲解页(面板 ≥3 时带目录)"和卡片风格主题,渲染截图左侧的 A/B/C/D 目录就是 doc 模板的目录栏。 - 面板切分:每个
## 标题开一个面板,字母 ID(A、B、C…)可省略、自动分配(assignIds,src/parse.js);## A 标题 {span=2}形式的面板属性也在这里解析。 - 块切分:正文按 Markdown 块(
md)与 fence 块(fence,带语言与参数)交替切分,行号一律按源文件 1-based 记录——这就是后面所有报错都能精确到L14的基础。 - 默认值:
DEFAULT_META给出template: sheet、theme: auto、style: 80、mode: auto、cols: 3(src/parse.js),命令行参数通过applyOverrides覆盖草稿与配置(src/render.js)。
2.2 主题解析与 STE lint
解析之后,renderDoc先处理主题:theme: auto会被pickTheme落成一个真实主题——页面场景下,doc 模板或不含任何图形块的草稿选paper,否则选blueprint(src/themes/registry.js)。落定的主题会写进页面,供页内主题切换器使用。
接着是 STE 受控写作检查(详见第 4 节):style: off直接跳过;style: strict且存在警告时抛LintError,不写页面(src/render.js);默认的80只产生警告、随输出一起打印。
2.3 组件渲染
每个 fence 块按语言名查组件注册表 src/components/index.js。从源码结构看,当前注册了9 个组件:callout、kv、timeline、annot、tree、limits、sequence、flow、ask(示例文档目录树里写的是 8 个,可视为早期版本的快照)。语言是html/svg的 fence 走 escape hatch 原样嵌入;其他语言则是代码块(支持src=引用真实文件、diff语法等,见am help code)。组件的render()返回 HTML,异常统一包装成带line/component/example字段的RenderError(src/render.js),为第 6 节的错误闭环提供数据。
2.4 dagre 布局
涉及图形的块(示例里的flow)在这一阶段由 dagre 计算坐标并画出 SVG,详见第 3 节。
2.5 模板与插槽:产出单文件 HTML
最后renderDoc用 src/templates/ 里的模板(sheet/doc)把 intro 与面板 HTML 填进"插槽",再由 src/render.js 的shell()组装外壳。所谓"单文件"体现在:
<style>里内联整套主题 CSS(pageCss,含暗黑模式 token);- 页脚内联 runtime JS(主题/明暗切换、lightbox 放大、复制按钮);
- 原稿保存在隐藏的
#am-source中(sourceTag),这正是am patch能对成品页就地替换单个面板的前提(src/cli.js); - 图片被内联为 data URI,代码块引用真实文件后一并嵌入——页面离线可开、不依赖任何网络资源。
输出默认落在~/.answer-me-with-html/pages/(可用环境变量AM_HOME移动),文件名由标题 slug 加时间戳构成(src/cli.js)。
3. 图形层:模型只写关系,坐标全部交给 dagre
示例面板 A 的图就是flow组件画的。flow的契约在 src/components/flow.js:
- 方向参数
TB | LR | BT | RL(默认 TB),如flow LR; A -> B: label实线、A --> B虚线,A -> B & C扇出,行尾: label是边标签;- 方括号决定形状:
(round)圆角矩形、{diamond}菱形、[(cylinder)]圆柱(数据库)、[rect]矩形;*节点高亮; group 组名: B, C画分组框(对应示例图里包住五个环节的group render.js: ...)。
layout()的实现(src/components/flow.js)说明了"模型不写坐标"是怎么落地的:
- 节点尺寸不是拍脑袋的常量:
nodeSize()先用文本测量(measure/wrap,上限 150 字符换行)算出宽高中再乘形状系数(菱形要放大 1.5/1.6 倍)。 - 建图:
new dagre.graphlib.Graph({ compound: groups.length > 0, multigraph: true }),setGraph({ rankdir, nodesep: 36, ranksep: 46, ... });分组用setParent挂成 compound cluster。 dagre.layout(g)之后,节点读g.node()的中心坐标、边读g.edge().points,再用clipEnds()修正菱形端点(否则箭头会悬浮在菱形斜边外)。- 最终输出的 SVG 里,每条边/节点带
data-key与(视频模式下)按源行号的data-step,支持页面内的变更标记(+/-/~增删改)与视频逐行步进。
这就是分工表里"计算图形坐标 | CLI(dagre)"的完整含义:草稿里只出现parse -> STE lint & 组件渲染这样的关系描述,x/y一个都不写。
4. STE 受控写作检查:CLI 替读者把关文风
分工表最后一行"检查写作风格 | CLI(STE lint)"对应 src/lint/ste.js。它只约束稿件里的说明性文字,核心规则(src/lint/ste.js):
- 句长:中文陈述句 ≤45 字、程序性句子(编号列表项)≤35 字;英文分别为 25 词 / 20 词;
- 段长:每段最多 6 句(
MAX_SENTENCES); - 英语:查未收录词表(src/lint/wordlist.en.js)并给出替换建议,正则识别被动语态;
- 中文:查轻动词、"的"字链和套话(src/lint/wordlist.zh.js);
- 日语(含假名)只查句长与段长;其他语言只用语言中性的长度规则;
- 代码、行内代码、标题、带
no状态的表格行、除callout外的组件内容全部跳过(src/lint/ste.js 文件头注释)。
严格度由 frontmatter 的style控制,三档行为不同:off不检查;80(默认)只警告,页面照写,警告随输出打印;strict下任何警告都会让渲染失败(LintError,"no page was written",src/render.js、src/render.js)。也可以单独跑 src/cli.js 的cmdLint(am lint <file> [--style off|80|strict])只查不渲染,strict下有警告时退出码为 1。
5. 目录结构
原文面板 C 用tree组件给出了精简目录树(同样是草稿语法,可直接渲染):
answer-me-with-html `bin/am.js` | CLI 入口 `src/` `parse.js` | 稿件 → 面板与块 `render.js` | 主流程 `components/` | 8 个组件 `lint/` | STE 受控写作检查 `themes/` | blueprint 与 shadcn 两套主题 `skills/answer-me-with-html/` | Agent Skill对照当前仓库,这棵树的主干依旧准确,但有两处演进(以当前源码为准):
components/现在注册了 9 个组件(新增ask等),见 src/components/index.js;themes/的内置主题有 4 套:blueprint、shadcn、paper、3b1b(src/themes/registry.js),其中3b1b面向视频;此外还有src/languages/(页面语言与字体)、src/runtime/(浏览器端运行时)、src/video/(讲解视频管线)等示例树未列出的模块。
Agent Skill 一侧,skills/answer-me-with-html/ 除SKILL.md外还带references/(settings、video)与scripts/am.mjs,是模型侧的入口;CLI 侧 bin/am.js 注册为am与answer-me-with-html两个可执行命令(package.json)。
6. 出错时怎么办:带"正确示例"的自修复闭环
原文面板 D 用时序图描述了 Agent 视角的错误处理:
模型 -> am: render 稿件 am --> 模型: ✗ L14 [flow] 错误 + 正确示例 模型 -> 模型: 按示例修正 模型 -> am: 再次 render am --> 模型: ✓ 页面路径这个闭环的每一环都有源码支撑:
- 错误从哪来:组件解析失败抛
ComponentError,被 src/render.js 捕获后连同组件自带的example(每个组件注册时都提供了最小正确示例,见 src/components/flow.js)包装成RenderError。 - 怎么报给模型:src/cli.js 的
reportError打印三行——✗ L14 [flow] <message>、缩进的Correct example:正确示例、Full syntax: am help flow指向完整语法文档,退出码 1。模型拿到的是可直接粘贴的修法,而不是一句"语法错误"。 - 另一类失败:
style: strict下的 lint 失败走LintError分支,列出全部警告并声明"no page was written"(src/cli.js);frontmatter 取值非法则走ParseError(如Invalid theme value …)。 - 成功路径:
emit()打印✓ <文件路径>加一行摘要(doc · shadcn · 4 panels · flow×1),并列出内嵌的代码文件与各类警告(src/cli.js)。
因为RenderError强制携带line与component,且所有解析器从第一天起就维护 1-based 行号(src/parse.js 头注释),"第几行、哪个组件、改成什么样"三要素齐备,模型按示例修正后再次 render 即可收敛——这正是时序图最后一帧✓ 页面路径的来源。
7. 自己跑一遍这个示例
前置条件是 Node 20+(package.json 的engines),安装方式见 INSTALL.md。然后直接以仓库内这份草稿为输入:
am render examples/architecture.md # 渲染出 doc 模板 + shadcn 主题的单文件页面 am lint examples/architecture.md # 只跑 STE 检查 am list # 列模板、主题、组件 am help flow # 查看 flow 组件语法与示例输出默认写入~/.answer-me-with-html/pages/,终端打印✓ <路径>并(按配置)自动打开浏览器。页面顶部的主题/明暗切换器说明"配色、暗黑模式"确实由 CLI 负责:切换只是运行时换 token,页面本身已经内联了所有主题的 CSS。
小结:answer-me-with-html 的架构本质是一次职责切分——模型侧的产物被收窄为一份结构化的 Markdown 内容稿(frontmatter +##面板 + 组件 fence),而 src/parse.js 的解析、src/lint/ste.js 的文风检查、src/components/flow.js 的 dagre 布局、src/templates/ 的模板组装全部收敛在 src/render.js 一条管线里,最终产出可离线打开、保留原稿可 patch 的单文件 HTML。理解了这条管线,就能解释为什么"模型不写 CSS、JS 和 SVG 坐标"依然能稳定产出排版一致的讲解页。
【免费下载链接】answer-me-with-html
Answer me with HTML — an agent skill that answers hard questions with a one-page HTML you can actually read. 让 AI Agent 用一页 HTML 回答复杂问题。
相关推荐
用专属 Agent 维护演讲演示文稿:presentation-claude-gemini 如何守护 52 页单文件 HTML 幻灯片
用专属 Agent 维护演讲演示文稿:presentation claude gemini 如何守护 52 页单文件 HTML 幻灯片 本指南围绕 claude
文档教程AI 技能html-anything resume-modern 技能解析:从一份 Markdown 简历到 A4 单页极简简历 HTML
html anything resume modern 技能解析:从一份 Markdown 简历到 A4 单页极简简历 HTML 本文围绕 html anyth
AI 应用人工智能AI AgentAI 写作媒体生成Quartz 插件详解:ContentPage 页面类型插件如何为 Markdown 内容生成完整 HTML 页面
Quartz 插件详解:ContentPage 页面类型插件如何为 Markdown 内容生成完整 HTML 页面 ContentPage 是 Quartz v
前端开发工具CLI
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考