last30days 可分享 HTML Brief 生成与托管发布完整指南:save-html-brief 从触发到上线的全流程解析
本文系统拆解 last30days Skill 中「把研究报告导出为可分享 HTML 简报(shareable HTML brief)」的完整实现:从 --emit=html 等触发信号如何被识别,到综合稿逐字落盘、引擎渲染、本地规范化命名,再到可选的 ht-ml.app 托管发布与密码保护。读完你会掌握在 /last30days 实际会话中正确执行 HTML 导出的每一步命令、两种输出模式的聊天交接规范,以及从渲染源码看产物边界的设计原理,可直接用于 Slack、邮件、Notion 等场景的成果分享。
文章以关联文档 skills/last30days/references/save-html-brief.md 为骨架,辅以引擎 CLI 与渲染源码佐证。
一、参考文档的定位:SKILL.md 的「按需加载」实现
save-html-brief.md 不是一份独立的手册,而是一份由主契约按需加载的 reference 文件。它由 skills/last30days/SKILL.md 在检测到用户意图需要 HTML 产物时加载——这样设计是为了让最常见的「纯文本报告」主路径保持简短,只有真正需要 HTML 时才引入这段较长的实现说明。
触发信号分为两类,含义不同:
- 命令行形态的提示词参数:
--emit=html、--emit:html、--html。需要注意:这些是 SKILL.md 用于识别用户意图的提示词信号,不是完整的 Python CLI 参数契约。引擎 CLI 真正接受的参数是--emit=html(见下文 CLI 章节),--html、--emit:html这类形态是在技能层完成的意图识别。 - 自然语言表述:如 "give me a shareable HTML brief"、"give it to me in HTML"、"for Slack"、"for Notion"、"export as HTML" 等。
从 SKILL.md 中的检测逻辑可以推断:技能在解析用户输入后,若命中 HTML 意图,就在同一轮中加载本文档并按其流程执行,而非将 HTML 特判逻辑全部堆在 SKILL.md 正文里。
二、两种模式的输出契约
文档将 HTML 流程划分为两种模式,两者决定「主要产物是聊天文本还是 HTML 文件」,这是后续所有交接话术的分叉点:
| 模式 | 触发 | 主要产物 | 合成稿流向 |
|---|---|---|---|
| HTML 即交付物(HTML as the requested deliverable) | --emit=html、--emit:html、--html,或 "give it to me in HTML" 等 |
HTML 工件 | 把合成稿写入临时文件 → 渲染 HTML → 聊天里只做简短的工件交接,不再重复粘贴整份 Markdown 报告 |
| 普通报告 + HTML 副本(Normal report plus HTML copy) | 用户要普通报告并额外要一份 HTML 副本 | 聊天中的合成稿 | 聊天正文照常输出;HTML 作为额外工件保存到磁盘用于分享;两者在同一轮完成 |
何时触发(When to fire)
- 普通报告 + HTML 副本模式:已经输出完完整的聊天响应之后——包含 badge、"What I learned:"(或对比标题)、带引用的加粗引导段、KEY PATTERNS 列表、引擎 footer 透传块、邀请块。
- HTML 即交付物模式:在把进入 HTML 的合成稿起草完成之后、输出最终聊天响应之前。
- 两种模式都必须在 "等待用户回复"(WAIT FOR USER'S RESPONSE)暂停之前完成。
- 只有用户主动要求才保存 HTML。用户没要求时不要保存,因为无合成的稀疏模式(sparse mode)会产出内容单薄的文件,不适合作为可分享件。
三、核心执行流程:逐字落盘 + 引擎渲染
文档给出了三步式 shell 流程。下面是其完整逻辑与每一步的技术要点。
步骤 1:把合成稿逐字(VERBATIM)写入临时文件
临时文件的路径与写入方式都有讲究:
SYNTHESIS_FILE="/tmp/last30days-synthesis-${CLAUDE_SESSION_ID}.md"
cat >| "$SYNTHESIS_FILE" <<'SYNTHESIS_EOF'
What I learned:
**{First headline}** - {body with name inline citations}
**{Second headline}** - {body}
**{Third headline}** - {body}
KEY PATTERNS from the research:
1. {pattern} - per @handle
2. {pattern} - per r/sub
3. {pattern} - per @handle
SYNTHESIS_EOF
要点:
>|而非>:同一次会话内重复运行时,固定路径可能已经存在;技能 shell 运行在set -o noclobber之下,普通>遇到已存在文件会被拒绝,>|显式允许覆盖。- 单引号 heredoc(
<<'SYNTHESIS_EOF'):引住定界符可以阻止任何 shell 展开,合成稿里的引号、$、反引号、&等任意字符都原样通过,天然处理含 shell 特殊字符的主题。 - 写入内容边界:只写合成稿散文本身。不要把 badge(
🌐 last30days vX.Y.Z · synced ...)或引擎 footer 写进临时文件——渲染 HTML 时由引擎统一添加,避免重复。数据质量警告文本也严禁进入临时文件(见第七章)。 - 逐字不 paraphrased:HTML 必须在口吻和引文上与预期的报告读起来一致,所以两种模式都不允许改写、压缩或重排合成稿。
内容模板中的 What I learned:、KEY PATTERNS from the research: 是通用报告的标准 prose label;对比模式则写入对比形态的合成稿(见第八章)。
步骤 2:调用引擎,把合成稿渲染为自包含 HTML
SLUG=$(echo "$TOPIC" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9' '-' | sed 's/^-//;s/-$//')
HTML_PATH="${LAST30DAYS_MEMORY_DIR}/${SLUG}-brief.html"
if [ -f "$HTML_PATH" ]; then
HTML_PATH="${LAST30DAYS_MEMORY_DIR}/${SLUG}-brief-$(date +%F).html"
fi
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" "${TOPIC}" \
--emit=html \
--synthesis-file "$SYNTHESIS_FILE" \
"${SCOPE_FLAGS[@]}" \
>| "$HTML_PATH"
其中 SCOPE_FLAGS 与首次运行保持一致,例如:
SCOPE_FLAGS=(--hiring-signals --plan "$QUERY_PLAN_FILE" --x-handle=acme)
这里的若干变量(LAST30DAYS_PYTHON、LAST30DAYS_MEMORY_DIR、SKILL_ROOT)由 SKILL.md 顶部的 Runtime Preflight 块在技能会话开始时解析并固定,本流程直接引用。
关键点拆解:
- Slug 化主题:
TOPIC全小写、非a-z0-9字符替换为-、去掉首尾连字符,得到安全的文件名片段。 - 路径约定:产物落在
LAST30DAYS_MEMORY_DIR下,命名<slug>-brief.html。LAST30DAYS_MEMORY_DIR未设置时默认~/Documents/Last30Days/(SKILL.md 的 Configuration 章节约定;hook 脚本hooks/scripts/check-config.sh会在会话启动时自动创建该目录)。这是规范位置,不得擅自改变。 - 撞名守卫:引擎不会为
--emit=html重定向流自动加日期——它的日期后缀逻辑只作用于--save-dir的原始文件。因此若干净文件名<slug>-brief.html已存在,脚本需先把目标改为<slug>-brief-YYYY-MM-DD.html,避免覆盖先前同主题的 brief。 - 回放相同的 scope flags:引擎在同一主题的后续轮会复用结构化缓存
~/.config/last30days/last-report.json来构建 badge 元数据与 footer,而不必重跑各源抓取器。该缓存刻意短命:默认 1 小时,可用环境变量LAST30DAYS_REPORT_CACHE_TTL_SECONDS调整,置0则禁用复用(对应实现见 lib/doctor.py 的REPORT_CACHE_FILENAME与DEFAULT_REPORT_CACHE_TTL_SECONDS = 3600;变量注册见 lib/env.py)。若缓存过期、缺失或属于不同主题,stderr 会出现 "No matching cached report data",引擎退回完整重跑;此时相同 scope flags 才能保证回退结果与合成稿正文数据集一致。对--hiring-signals这种有作用域的运行,HTML footer 必须反映 jobs 作用域的看板而非通用爬取,所以--hiring-signals必须在最终命令中再次出现。 - 输出重定向:最后用
>| "$HTML_PATH"而非>——在撞名守卫已经查过的路径上,noclobber 规则仍会拒绝普通>。
引擎 CLI 侧,相关参数在 scripts/last30days.py 定义:--output(渲染结果保存的精确路径)、--synthesis-file(嵌入 --emit=html 输出的 Markdown 合成稿)、--publish-html、--publish-password。其中 --synthesis-file 仅在 --emit=html 时生效,其他 emit 模式会被忽略并给出 stderr 警告(见 scripts/last30days.py 的读取与校验逻辑)。
步骤 3:按模式完成聊天交接
shell 块结束后,不要在 shell 里打印保存路径——聊天交接是用户唯一可见的完成消息(见第六章)。
四、可选托管发布:ht-ml.app 决策流程
文档强调:先本地保存,后按需发布。发布必须以用户显式选择为前提,且要尊重既有的发布偏好。
决策顺序
- 保存本地 HTML 文件;
- 展示绝对保存路径;
- 主动呈现下一步选项:
- 打开 HTML 文件
- 发布到
<首选/已配置服务>;若展示 ht-ml.app,说明其支持可选密码保护 - 暂时完成(Done for now)
- 用户选定发布选项之前不得上传。
若用户已有偏好的发布服务或内部分享工作流,优先提供该选项;多个发布选项可用时,每个独立呈现,并把 ht-ml.app 作为其中之一(标注支持可选密码保护)。没有既有偏好时,ht-ml.app 是默认的兜底发布选项。
ht-ml.app 的两个分支
选定 ht-ml.app 后需追问第二个问题:
- 公开链接(Public link):无密码发布。
- 密码保护链接(Password-protected link):请用户以自由形式输入共享密码,然后用该密码发布。
在给出 ht-ml.app 选项前,必须告知用户:公开页面可能被爬取或索引,且可选密码保护。若选密码保护,必须使用用户为本份报告提供的唯一共享密码,绝不能用其自有账号密码。
命令形态:--publish-html 与 --output
选择内置 ht-ml.app 路径时,在同一个 --emit=html 命令上追加 --publish-html,并用 --output "$HTML_PATH" 取代 shell 重定向,这样引擎才能在本地 HTML 旁写出 .publish.json 伴生元数据:
LAST30DAYS_PUBLISH_PASSWORD="${PUBLISH_PASSWORD:-}" \
"${LAST30DAYS_PYTHON}" "${SKILL_ROOT}/scripts/last30days.py" "${TOPIC}" \
--emit=html \
--synthesis-file "$SYNTHESIS_FILE" \
--output "$HTML_PATH" \
--publish-html \
"${SCOPE_FLAGS[@]}" \
>/dev/null
密码走环境变量 LAST30DAYS_PUBLISH_PASSWORD,不要在命令行传 --publish-password,以免密码暴露在进程列表中(CLI 层对 --publish-password 的注释也明确提示「prefer LAST30DAYS_PUBLISH_PASSWORD」,见 scripts/last30days.py)。
发布结果的处理约定:
- 托管 URL 出现在 stderr,形如
[last30days] Published HTML to https://...,用该 URL 向用户确认结果; - 若选择密码保护,需复述用户所选共享密码,便于其把 URL 与密码一同发出;
- 引擎把 URL 元数据写入
<HTML_PATH>.publish.json; - 提供方可能返回
update_key,必须视为机密:引擎刻意不把它写进 stdout、HTML 工件或.publish.json伴生元数据。
关于发布机制本身,文档要求 Agent 在需要时自行发现所选服务的当前发布机制(必要时访问服务站点),而不是在聊天中硬编码过细的服务专属指令。
发布实现层面,lib/html_publish.py 定义默认端点 https://api.ht-ml.app/v1/sites,publish_html 通过 POST 提交 {"html_content": ..., "password": ...},并对空内容、非 JSON 响应、无效 URL 分别抛出明确的 HtmlPublishError。发布前渲染层还会做一次 scrub_publishable_digit_runs 清理(见第七章),因为 ht-ml.app 的安全扫描会拒绝含 13-19 位连续数字串(类卡号形态)的页面。
五、保存后的聊天交接(Chat handoff)
交接话术必须匹配请求模式。两条硬约束:HTML 即交付物模式下不要在保存工件后把整份 Markdown 报告再粘贴回聊天(否则运行体感就像普通报告附了个附件);而保存的绝对路径行要留在聊天里。
HTML 即交付物模式
🌐 last30days v{VERSION} · synced {YYYY-MM-DD}
📎 Shareable brief saved to <absolute HTML path>
What do you want to do next?
1. Open HTML file
2. Publish to <available HTML publishing service> (<service-specific note, e.g. ht-ml.app supports optional password protection>)
3. Done for now
若用户选择打开:宿主编译环境能安全打开本地文件时直接打开,保留路径行,并补一句 Opened locally.。让宿主自己选择正确的操作系统机制,不要打印一堆 shell 命令菜单。若打开失败或宿主无头(headless),不视为报告失败——展示路径并说明文件随时可用浏览器打开。
普通报告 + HTML 副本模式
保持完整聊天合成稿不变,在邀请块之后追加工件块:
📎 Shareable brief saved to <absolute HTML path>
What do you want to do next?
1. Open HTML file
2. Publish to <available HTML publishing service> (<service-specific note, e.g. ht-ml.app supports optional password protection>)
3. Done for now
此流程中用户未选发布选项前不得上传。
六、HTML 产物里到底有什么:渲染管线拆解
参考 lib/html_render.py,引擎的 --emit=html 渲染器组装出以下结构(渲染入口见 render_html,先走 lib/render.py 的 render_for_html 得到目标 Markdown,再做 HTML 化处理):
- Badge 位于顶部:
🌐 last30days vX.Y.Z · synced YYYY-MM-DD(render 层的_render_badge)。 - 单行内联元数据位于 badge 下方:
{date range} · {active sources}。实现上以<!-- META: ... -->注释标记输出(render_for_html内_render_html_metadata),HTML 化后由_promote_meta_marker提升为带样式的<div class="meta">,从而绕过 Markdown 转换器的转义。 - 你的合成稿逐字呈现:prose label 被提升为
<h2>,加粗引导段(bold lead-ins)原样保留。PROSE_LABELS定义了What I learned:→## What I learned、KEY PATTERNS from the research:→## Key patterns from the research的提升映射(lib/html_render.py)。 - 所有
name引用渲染为<a>标签。注意渲染器带有一套安全链接白名单:只允许http、https、mailto与无 scheme 的相对 URL(lib/html_render.py),防住 LLM 合成文本中可能残留的javascript:/data:存储型 XSS 向量——这正是「HTML 会被浏览器打开」这一分享场景的防御性设计。 - 引擎 footer(
✅ All agents reported back!树)逐字保留,并置于等宽字体的.engine-footer块内。 - Colophon:包含主题与重跑提示(
Re-run for fresh data: /last30days {topic}),见_build_colophon。
同时,渲染器会剥除不属于可分享工件的一切引擎内部噪音:
# last30days vX.Y.Z: TOPIC调试文件头;- 面向模型的
> Safety note:引用块; I'm now an expert on X邀请块(_strip_invitation);<!-- EVIDENCE FOR SYNTHESIS -->证据草稿区(_strip_evidence_block)与 canonical 边界标记(_strip_canonical_boundary)。
数据质量警告(degraded run、thin evidence 等)绝不进入分享文件。它们由 collect_html_warnings 汇聚后路由到引擎 stderr(lib/render.py 与 lib/render.py 的注释均明确:HTML 用于分享,接收者没有要求技术性运行说明,生成者通过 stderr 看到同一批警告即可)。样式方面,产物是内嵌 CSS 的自包含单文件,支持 prefers-color-scheme 明暗主题与打印样式(A4 分页、链接显示 href),移动端亦有响应式布局(见 lib/html_render.py 的 CSS 定义)。
一个实践细节:HTML 化的 Markdown 内,表格、标题(#{1,4})、引用、有序/无序列表、代码围栏(```)都由手写的迷你转换器处理(_markdown_to_html / _render_table),因此对比模式里的 Head-to-Head 表格也能被正确渲染成 <table>。
七、对比模式(Comparison mode)
当主题是 X vs Y(或 X vs Y vs Z)时,流程完全一样,无需额外特殊处理:引擎内部自动走 render_for_html_comparison(渲染入口在 lib/html_render.py,底层由 lib/render.py 的 render_for_html_comparison 实现,元数据行会聚合对比实体数及实体名单)。
临时合成文件中仍应写入你在聊天里写好的对比形态合成稿:
## Quick Verdict## {Entity}(每个被对比实体一节)## Head-to-Head表格## The Bottom Line## The emerging stack(LAW 4 对比例外允许的段落)
这些 ## 标题在合成稿中直接透传,由 HTML 转换器渲染为各级标题。
八、后续回合(Follow-up turn)的处理
文档区分两种后续场景:
- 引用已可见的合成稿:用户正常跑完
/last30days,在聊天里看到合成稿后,明确指回该可见合成稿("save that as HTML"、"make this shareable"、"turn the above into HTML")。此时不要重新研究——合成稿已在会话历史中,直接把它写入临时文件并调用引擎(--emit=html --synthesis-file),随后走「普通报告 + HTML 副本」的工件块交接。 - 新的 HTML 交付请求:用户要求的是全新 HTML 交付物("give it to me in HTML"、
--emit=html、--html),而非指回已见报告——按「HTML 即交付物」模式处理。
两种情况下,第二次调用时若仍在 LAST30DAYS_REPORT_CACHE_TTL_SECONDS(默认 1 小时)窗口内,引擎会尝试复用 ~/.config/last30days/last-report.json:若 stderr 显示正在复用缓存报告数据,正常继续;若 stderr 显示无匹配缓存,说明缓存可能过期——只有你提供了与原始运行相同的 scope flags 时才放行命令跑完,否则应停下并用原始 flags 重跑,以免 HTML footer 描述的是一份不同数据集。
九、红线清单:What NOT to do
- 不要在用户未要求时保存 HTML(稀疏模式产物单薄,不适合分享)。
- 不要在临时文件中添加合成稿以外的内容:badge、footer、colophon 都由引擎负责。
- 不要改变文件路径约定:
${LAST30DAYS_MEMORY_DIR}/${SLUG}-brief.html是规范位置。 - 不要静默覆盖既有文件。
--emit=html输出经 shell 重定向(>| "$HTML_PATH")写出并会覆盖撞名守卫查过的路径——必须用>|而非>,因为set -o noclobber在文件已存在时会拒绝普通>。撞名守卫处理同主题重跑:若{slug}-brief.html已存在则加日期后缀{slug}-brief-YYYY-MM-DD.html。交接时始终报告重定向实际使用的那个路径。 - 不要把数据质量警告文本写进临时文件或最终聊天行:警告是引擎 stderr 的事,不是工件的事。
- 不要在本地保存流程中把 HTML 发布、上传或发送给任何第三方服务。
- 不要仅仅因为请求了 HTML 就发布到任何服务:先展示保存路径与下一步选项,发布须等用户选定发布选项。
- 不要在没有用户显式请求托管 URL 时,让本地导出阻塞在托管决策上。
- 不要把
update_key粘贴或存储在聊天、Markdown、HTML、原始输出或伴生元数据中。
十、边界情况(Edge cases)
- 含 shell 特殊字符的主题(引号、&):临时文件名使用 slug 化版本,但引擎收到的是原始主题;
cat <<'SYNTHESIS_EOF'带引号的 heredoc 形式能不加展开地处理任意内容,合成稿可含任何字符。 - 超长合成稿:无上限。引擎能处理长 Markdown 正文,直接逐字粘贴即可。
- 含图片或非 ASCII 的合成稿:emoji 与 Unicode 正常透传;
...图片标签以原始 HTML 形态透传,渲染器不转换。若聊天正文里没配图,就不要在合成稿里加图。 ${LAST30DAYS_MEMORY_DIR}未设置:按 SKILL.md 的 Configuration 章节默认到~/Documents/Last30Days/。
十一、源码索引与延伸阅读
想从实现层深入,可按以下文件顺藤摸瓜:
- skills/last30days/references/save-html-brief.md - 本文档骨架:双模式契约、触发时机、完整命令流程、交接话术、红线与边界。
- skills/last30days/SKILL.md - 主契约:HTML 意图识别、Runtime Preflight 变量(
LAST30DAYS_PYTHON、LAST30DAYS_MEMORY_DIR等)、Configuration 章节的内存目录默认值。 - skills/last30days/scripts/last30days.py - CLI 入口:
--emit(含html)、--output、--synthesis-file、--publish-html、--publish-password参数及合成稿读取逻辑;--publish-html仅在--emit=html下合法(tests/test_drill_mode.py 专门校验该约束)。 - skills/last30days/scripts/lib/render.py -
render_for_html/render_for_html_comparison:badge、META 标记、合成稿嵌入、footer 追加,以及「数据质量警告只进 stderr 不进工件」的分流。 - skills/last30days/scripts/lib/html_render.py - 自包含 HTML 模板与内嵌 CSS、prose-label 提升、引擎 footer 保护与回填、安全链接白名单、表格渲染、
scrub_publishable_digit_runs防卡号形态数字串清理。 - skills/last30days/scripts/lib/html_publish.py - ht-ml.app 发布客户端:默认端点、密码负载、错误消息规范化。
- skills/last30days/scripts/lib/doctor.py -
last-report.json缓存文件名与默认 TTL(3600 秒),是 HTML 二次渲染复用的数据集来源。 - skills/last30days/scripts/lib/env.py -
LAST30DAYS_REPORT_CACHE_TTL_SECONDS、LAST30DAYS_MEMORY_DIR等环境变量的注册。
整体设计呈现出一条清晰的「内容治理」思路:把面向用户的综合稿与面向引擎的诊断严格隔离——引擎负责 badge/footer/colophon 与安全过滤,Agent 只负责逐字提供合成稿,从而保证任何一份被分享的 HTML brief 都在口吻、引文与数据完整性上等价于原始报告。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00