首页
/ last30days 可分享 HTML Brief 生成与托管发布完整指南:save-html-brief 从触发到上线的全流程解析

last30days 可分享 HTML Brief 生成与托管发布完整指南:save-html-brief 从触发到上线的全流程解析

2026-09-07 18:03:36作者:袁立春Spencer

本文系统拆解 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_PYTHONLAST30DAYS_MEMORY_DIRSKILL_ROOT)由 SKILL.md 顶部的 Runtime Preflight 块在技能会话开始时解析并固定,本流程直接引用。

关键点拆解:

  1. Slug 化主题TOPIC 全小写、非 a-z0-9 字符替换为 -、去掉首尾连字符,得到安全的文件名片段。
  2. 路径约定:产物落在 LAST30DAYS_MEMORY_DIR 下,命名 <slug>-brief.htmlLAST30DAYS_MEMORY_DIR 未设置时默认 ~/Documents/Last30Days/(SKILL.md 的 Configuration 章节约定;hook 脚本 hooks/scripts/check-config.sh 会在会话启动时自动创建该目录)。这是规范位置,不得擅自改变
  3. 撞名守卫:引擎不会--emit=html 重定向流自动加日期——它的日期后缀逻辑只作用于 --save-dir 的原始文件。因此若干净文件名 <slug>-brief.html 已存在,脚本需先把目标改为 <slug>-brief-YYYY-MM-DD.html,避免覆盖先前同主题的 brief。
  4. 回放相同的 scope flags:引擎在同一主题的后续轮会复用结构化缓存 ~/.config/last30days/last-report.json 来构建 badge 元数据与 footer,而不必重跑各源抓取器。该缓存刻意短命:默认 1 小时,可用环境变量 LAST30DAYS_REPORT_CACHE_TTL_SECONDS 调整,置 0 则禁用复用(对应实现见 lib/doctor.pyREPORT_CACHE_FILENAMEDEFAULT_REPORT_CACHE_TTL_SECONDS = 3600;变量注册见 lib/env.py)。若缓存过期、缺失或属于不同主题,stderr 会出现 "No matching cached report data",引擎退回完整重跑;此时相同 scope flags 才能保证回退结果与合成稿正文数据集一致。对 --hiring-signals 这种有作用域的运行,HTML footer 必须反映 jobs 作用域的看板而非通用爬取,所以 --hiring-signals 必须在最终命令中再次出现。
  5. 输出重定向:最后用 >| "$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 决策流程

文档强调:先本地保存,后按需发布。发布必须以用户显式选择为前提,且要尊重既有的发布偏好。

决策顺序

  1. 保存本地 HTML 文件;
  2. 展示绝对保存路径;
  3. 主动呈现下一步选项:
    1. 打开 HTML 文件
    2. 发布到 <首选/已配置服务>;若展示 ht-ml.app,说明其支持可选密码保护
    3. 暂时完成(Done for now)
  4. 用户选定发布选项之前不得上传

若用户已有偏好的发布服务或内部分享工作流,优先提供该选项;多个发布选项可用时,每个独立呈现,并把 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/sitespublish_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.pyrender_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 learnedKEY PATTERNS from the research:## Key patterns from the research 的提升映射(lib/html_render.py)。
  • 所有 name 引用渲染为 <a> 标签。注意渲染器带有一套安全链接白名单:只允许 httphttpsmailto 与无 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.pylib/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.pyrender_for_html_comparison 实现,元数据行会聚合对比实体数及实体名单)。

临时合成文件中仍应写入你在聊天里写好的对比形态合成稿

  • ## Quick Verdict
  • ## {Entity}(每个被对比实体一节)
  • ## Head-to-Head 表格
  • ## The Bottom Line
  • ## The emerging stack(LAW 4 对比例外允许的段落)

这些 ## 标题在合成稿中直接透传,由 HTML 转换器渲染为各级标题。

八、后续回合(Follow-up turn)的处理

文档区分两种后续场景:

  1. 引用已可见的合成稿:用户正常跑完 /last30days,在聊天里看到合成稿后,明确指回该可见合成稿("save that as HTML"、"make this shareable"、"turn the above into HTML")。此时不要重新研究——合成稿已在会话历史中,直接把它写入临时文件并调用引擎(--emit=html --synthesis-file),随后走「普通报告 + HTML 副本」的工件块交接。
  2. 新的 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/

十一、源码索引与延伸阅读

想从实现层深入,可按以下文件顺藤摸瓜:

整体设计呈现出一条清晰的「内容治理」思路:把面向用户的综合稿与面向引擎的诊断严格隔离——引擎负责 badge/footer/colophon 与安全过滤,Agent 只负责逐字提供合成稿,从而保证任何一份被分享的 HTML brief 都在口吻、引文与数据完整性上等价于原始报告。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388