Siftr

发布 Skill

Skill 版本:2026 年 10 月 22 日

把这个页面发给你的 AI Agent,它就能按 Siftr 的内容协议,把 Markdown 简报发布到你自己的坚果云 WebDAV。Skill 与安装包不含任何凭据;应用密码由 Agent 的运行环境注入。

安装

安装包是一个 zip,解压得到完整的 siftr-publish/ 目录(SKILL.md 与两个脚本)。把它放进你 Agent 的 skills 目录即可,常见位置如 ~/.agents/skills/、~/.zcode/skills/、~/.claude/skills/——各平台不同,以你的 Agent 文档为准。

直接把下面这句话发给你的 Agent:

安装这个 Skill:下载 https://chenhk.cn/Siftr/assets/siftr-publish.zip 并解压,把得到的 siftr-publish/ 整个目录放进你的 skills 目录,然后按其中 SKILL.md 的说明为我发布 Siftr 内容。

下载安装包(siftr-publish.zip) · 需要配置的三个环境变量见全文 §2。

Skill 全文

以下与安装包内的 SKILL.md 完全一致(由 scripts/package_skill.py 从 skills/siftr-publish/ 生成,不会漂移)。

Siftr 内容发布(面向 Agent)

0. 给收到这个链接的 Agent

把 Siftr 当成用户手机上的内容交付渠道:你负责生成内容并发布到用户自己的 WebDAV,用户在 Siftr 添加该来源后阅读。适合日报、周报、监控摘要、研究报告、旅行安排,以及单文件 HTML 图表或交互讲解。用户只发来本 Skill 链接时,先简短解释用途并检查是否已配置;链接本身不代表已经授权定时运行或上传测试内容。

  • 一次性报告:生成 Markdown,使用 §4 的元数据与 §5 的发布工具交付。
  • 定时任务产出:用所在平台的定时任务能力执行生成与发布。Siftr 不执行任务,也不保证定时推送通知。确认内容、频率、时间及 IANA 时区(如 Asia/Shanghai),并把发布步骤、Skill/脚本绝对路径、输出路径与凭据注入方式写入任务配置。没有调度能力时说明限制,保留可运行的发布命令,不要声称已设好定时任务。
  • HTML 网页:用于自包含的交互内容,按 §6.1 编写。它是 App 内阅读页面,不是公网网站托管;不要把私有 WebDAV 地址当成可分享的网页链接。用户要公开网站或后端服务时,需要另选托管方案。

0.1 首次使用的顺序

  1. 若只拿到网页说明,下载 完整 Skill 安装包,解压并阅读其中的 SKILL.md。脚本在解压后的 siftr-publish/scripts/;以下命令相对于该目录的父级 Skill 根目录执行。不要假设用户当前目录已有脚本;自动任务使用绝对路径。需要 Python 3.10+,无需 pip 包。
  2. 只检查 §2 的三个环境变量是否存在,不要打印值。未配置时按 §2.1 引导;先在本地准备和校验内容,不反复尝试联网发布。
  3. 已配置后运行 status。ready 表示远端索引可读;waiting_for_first_publish 表示尚无索引或父目录,不证明写权限正常。两者都不证明用户已连接 App,也无法判断 App 版本。
  4. 在已有发布授权下先 validate 再 publish。成功后告诉用户标题、文档 id 与发布结果,让用户在 Siftr 对应来源下拉刷新。只有收到回读确认才能说“已发布”;没有实际查看 App 时不要说“手机已经收到”。
  5. 定时任务必须在它自己的运行环境里取得凭据;当前终端配置不会自动传给后台任务。同一来源的所有任务串行执行,避免重叠。首次定时运行前,用同一执行环境验证一次。

0.2 每期内容与重试

日报使用 ai-daily-2026-10-22,周报使用 weekly-2026-w43;日期以用户时区和本次任务的逻辑周期为准。只有持续更新的同一份报告才用固定 id。重试保留本次正文、元数据、id 和时间戳,不要重新生成一篇或换 id。修订保留首次 published_at,只更新 updated_at。重复任务运行若不断刷新 updated_at,会造成不必要的索引更新。

保留本地产物直到回读确认。发布失败的处理按阶段区分:正文阶段失败时索引未更新;一旦尝试 PUT Manifest,超时或回读失败都可能发生在远端已经写入之后。此时用相同输入重新核对/发布,不要删除远端内容或宣称回滚。脚本每个网络请求最多尝试三次;任务平台也应限制重试次数并串行执行,不要无限重试。

你是 唯一的写入方。Siftr App 只读:它 GET manifest.json,对比后再 GET 需要的正文。App 不会替你总结、配图、推荐、朗读或追问——这些都不做。摘要、分类、标签、标题都由你写好,写进元数据和正文。正文可以是 Markdown 简报,也可以是单文件静态网页(HTML,见 §6.1)。

只想传任意文件到坚果云?看 §5 的 Shell 上传工具。想让 App 读到内容?先读 §1 的逻辑,再用 §5 的工具。

1. 先理解这套逻辑(决定你每次该怎么做)

1.1 两个标识,别混

标识含义谁决定
id这是哪一篇文章。跨修订保持不变(如 ai-daily-2026-10-22、weekly-2026-w43)你
content_sha256这是哪份字节。正文 UTF-8 字节的 SHA-256工具自动算

读者端的已读/收藏记录挂在 id 上;内容变化靠 content_sha256 判断。

1.2 发布前的决策表

你手里的情况怎么做工具返回
全新的一篇起一个新 id,publishpublished
修订已发布的文章(改错字、补充、更新)沿用原 id,改 Markdown,publishpublished(旧版本文件保留,Manifest 指向新文件)
与上次完全一样什么都不用做(再跑也没副作用)unchanged
新 id,但正文与某篇已发布的字节完全相同不要发。这是重复内容,工具直接拒绝写入duplicate(含 duplicate_of,即已存在的 id)
想用旧 id 发一篇完全不同的文章禁止。会覆盖该 id 的阅读状态;换新 id—

重复内容有两层防线:发布工具(duplicate,不写任何文件)和 App(读到相同 Markdown 只显示一份)。别靠 App 兜底——别制造重复。判断「是不是同一篇」用 id,不要用标题。

1.3 读者可以订阅多个 WebDAV 来源

用户可在 Siftr 里添加多个 WebDAV(例如两个坚果云目录、或坚果云加别家 WebDAV)。对你的影响:

  • 一个目录 = 一个来源 = 一份 manifest.json,一个目录只让一个发布方写。Manifest 是「读—改—写」,写前会检查变化,但没有原子锁,两个 Agent 同时写同一个目录会丢更新。想多 Agent,就一人一个目录,让用户在 App 里各添加一次。
  • 同一地址 App 只收一次(大小写、末尾斜杠、默认端口 443、「服务器路径+目录」怎么拆分都视为同一地址)。你不需要、也不应该帮用户在多个目录里重复发同一篇来「保险」。
  • 不同来源里内容完全相同的 Markdown,App 只显示一份(保留较早添加的来源那份,已读/收藏同步到两份上)。所以「同一篇发布到两个目录」没有意义,除非用户明确要求镜像备份。
  • 来源之间 id 互不冲突:甲目录的 daily 和乙目录的 daily 是两篇不同的文章,除非字节也相同。
  • 写入一个目录时只操作这个目录。不要在 SIFTR_DAV_URL 之外读写;不要把一个来源的凭据用到另一个来源。

1.4 读者端如何对待你的文件(决定了「不可变」规则)

  • 正文文件名含全文 SHA-256,一版一文件,永不改、永不删。修订就是新文件 + Manifest 改指新路径。App 要求同一路径下内容不可变,违反会被判定为损坏的 Manifest。
  • Manifest 永远最后写。读者随时可能刷新:正文先落盘并回读校验,Manifest 最后更新,读者看到的状态就总是一致的。
  • App 会把 media/ 图片按路径缓存,不会因为你覆盖同名图片而刷新(见 §7)。

2. 凭据与上传原语

运行环境通过各自的安全管理器注入三个环境变量:

变量含义示例
SIFTR_DAV_URL内容根目录完整 URL(HTTPS)https://dav.jianguoyun.com/dav/Siftr/
SIFTR_DAV_USER账号user@example.com
SIFTR_DAV_APP_PASSWORD第三方应用密码见下

坚果云 WebDAV 不接受登录密码。在网页版「账户信息 → 安全选项 → 第三方应用管理」为每个发布方单独创建应用密码(读者 App、每个 Agent 各一个,互不共用)。凭据只从环境变量读取,绝不写入 Markdown、元数据、仓库、日志、命令行字面值或你的回复。

2.1 用户尚未配置时(先完成这一步)

先说明:“Siftr 需要你自己的 WebDAV 空间来接收内容。你可以使用坚果云;我可以先准备报告,连接后再发布。密码请填写到本机或平台的安全配置入口,不要发到聊天里。”

  1. 引导用户到 坚果云官网 注册或登录,参照 官方第三方应用授权教程 创建应用密码。网页路径为“账户信息 → 安全选项 → 第三方应用管理 → 添加应用密码”。Agent 应先打开官方教程核对当前界面;界面或套餐限制变化时以官方帮助为准。
  2. 确定专用目录,例如 Siftr。坚果云服务器地址为 https://dav.jianguoyun.com/dav/,本工具使用的完整内容根目录为 https://dav.jianguoyun.com/dav/Siftr/。不要填官网网址、分享链接或 manifest.json 地址。初次配置优先使用单层 ASCII 目录;嵌套目录的上级需用户提前创建,工具不会越过内容根目录创建父级。
  3. 在 Agent 平台的秘密管理/环境变量设置里保存 §2 三项。用户填写账号与应用密码;不要使用登录密码、提交到代码库或写进定时任务提示词。平台没有安全输入入口时,由用户在自己的终端配置;不要要求把密码贴回对话。
  4. 在 Siftr 添加 WebDAV 来源:服务器 https://dav.jianguoyun.com/dav/、目录 Siftr,填写同一账号及为 App 单独生成的应用密码。Agent 和 App 的有效目录必须相同,不要重复拼上两次 Siftr。
  5. 运行 status,按结果处理认证、权限或空目录。获得发布授权后上传用户需要的第一篇内容,再请用户在 App 刷新确认;不要为了连接检查擅自留下测试文章。

其他提供商也使用 HTTPS、账号和应用密码(或其官方要求的 WebDAV 密码),以该服务官方文档为准。不要把坚果云特有的 409 行为推断为所有 WebDAV 服务的规则。

任意文件上传可用 §5 的 Shell 工具;它不更新内容索引,上传文件并不等于在 Siftr 发布了文章。

坚果云 WebDAV 行为速查(2026-10 实测)

场景实际返回
认证HTTPS + Basic(realm="nutstore"),HTTP/2
凭据错误401,空响应体
父目录不存在时 GET/PUT409 AncestorsNotFound——不是 404,先 MKCOL(注意:MKCOL 的 URL 不能带尾部斜杠,…/Siftr 成功、…/Siftr/ 返回 409;Python 工具已处理)
GET 目录403;列目录必须 PROPFIND
频率/流量超限429/5xx;指数退避,几次后放弃

3. 云端目录结构(Siftr 内容协议 v2)

$SIFTR_DAV_URL(如 https://dav.jianguoyun.com/dav/Siftr/)
├── manifest.json                      # 索引:App 的唯一入口
├── documents/
│   ├── <id前缀>_<内容SHA-256>.md       # format = markdown,不可变,一版一文件
│   └── <id前缀>_<内容SHA-256>.html     # format = html,同样不可变
└── media/                             # 可选:Markdown 引用的私有图片(App 认证加载)
    └── <名称>.png|.jpg|.jpeg|.gif|.webp

manifest.json 顶层:

字段类型约束
schema_versionint固定 2(读到 1 时 Python 工具会自动升级,见下)
generated_atstring带时区 ISO 8601;整份 ≤ 2 MiB
publisherstring?发布方标识,可选
documentsarray≤ 5000 条,按 published_at 倒序;id 不得重复

documents[] 每条:

字段类型约束
idstringASCII [A-Za-z0-9._-],≤ 200 字符,跨修订不变
formatstring必填:markdown 或 html(工具按正文后缀自动填写)
title / subtitlestring / string?标题非空且 ≤ 200 字符
summarystring≤ 1000 字符;App 列表和搜索都用它,认真写
categorystring非空;App 用它做筛选
tags[string]?可选;App 用它做筛选和搜索
published_at / updated_atstring带时区 ISO 8601(2026-10-13T09:00:00+08:00);published_at 决定排序与按月归档
pathstringdocuments/<id>_<sha256>.<md|html>,由工具生成;后缀必须与 format 一致
content_sha256string正文原始字节的 SHA-256(64 位小写十六进制)
size_bytesint真实字节数,单篇 ≤ 5 MiB(两种格式相同)
reading_minutes / source_countint?非负
format_optionsobject?仅供对应格式的阅读器使用;markdown、html 目前没有定义,不要写

硬性规则:正文是 documents/ 下非空的 UTF-8 文件,≤ 5 MiB;正文与链接里不得出现凭据;时间戳必须带时区。alternates 是协议保留字段,不要写。

版本与旧数据:v1(schema_version: 1,没有 format)只有 Markdown。Python 工具读到 v1 时,会给每条补上 "format": "markdown",下次发布时以 v2 写回(结果里带 upgraded_to_schema_version: 2)。Siftr 1.0.0 只认识 v1,看到 v2 会提示升级 App;所以在用户换成支持 v2 的版本之前,不要往他的目录发布。status 只能检查远端协议,不能判断用户安装的 App 版本;旧目录升级前让用户确认已更新 App。

兼容未来格式:Manifest 里出现你不认识的 format(比如以后的 audio),不要删也不要改,原样保留。Python 工具会自动这样处理。

4. 元数据 JSON(--metadata)

{
  "id": "ai-daily-2026-10-13",
  "title": "AI 每日简报:把信息读成理解",
  "summary": "三句话说清今天发生了什么、为什么重要。",
  "category": "AI 资讯",
  "tags": ["Agent", "模型"],
  "published_at": "2026-10-13T08:30:00+08:00",
  "updated_at": "2026-10-13T08:30:00+08:00",
  "reading_minutes": 3,
  "source_count": 5
}

必填:id、title、summary、category、published_at、updated_at。可选:subtitle、tags、reading_minutes、source_count、publisher。format、path、content_sha256、size_bytes 由工具填,不要手写;如果写了 format,必须和正文格式一致。修订时 published_at 保持首次发布时间,updated_at 写本次时间。

5. 发布路径:按运行环境任选

5.0 Python 3.10+(推荐)

scripts/siftr_publish.py(纯标准库,内置校验、幂等、重复检测、回读与退避,适合无人值守任务):

python3 scripts/siftr_publish.py validate --markdown out/daily.md --metadata out/daily.json   # 完全离线
python3 scripts/siftr_publish.py status                                                        # 远端概况:schema_version、篇数、各格式数量
python3 scripts/siftr_publish.py publish  --markdown out/daily.md --metadata out/daily.json
python3 scripts/siftr_publish.py publish  --html out/sort-demo.html --metadata out/sort-demo.json   # 单文件静态网页
python3 scripts/siftr_publish.py publish  --file out/anything.md --metadata out/meta.json           # 按后缀识别格式

5.1 从空目录验证一条完整链路(不联网)

以下示例创建 out/ 下的 Markdown、HTML 与对应元数据;若同名文件已存在则停止,避免覆盖用户产物。示例只演示格式,正式发布时改成用户实际需要的内容。运行目录为解压后的 siftr-publish/。

python3 - <<'PYTHON'
from pathlib import Path
from datetime import datetime, timezone
import json

out = Path("out")
out.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
samples = {
    "sample-report": ("md", "示例报告", "## 本次结果\n\n这里填写本次任务的结论与来源。\n"),
    "sample-page": ("html", "示例网页", '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>示例网页</title><body><main><h1>本次结果</h1><p>这里填写报告正文。</p></main></body></html>'),
}
files = {}
for doc_id, (suffix, title, text) in samples.items():
    files[out / (doc_id + "." + suffix)] = text
    files[out / (doc_id + ".json")] = json.dumps({
        "id": doc_id, "title": title, "summary": "演示发布格式。",
        "category": "示例", "published_at": stamp, "updated_at": stamp,
    }, ensure_ascii=False, indent=2)
if any(path.exists() for path in files):
    raise SystemExit("示例文件已存在;请复用或换一个工作目录。")
for path, text in files.items():
    path.write_text(text, encoding="utf-8")
PYTHON
python3 scripts/siftr_publish.py validate --markdown out/sample-report.md --metadata out/sample-report.json
python3 scripts/siftr_publish.py validate --html out/sample-page.html --metadata out/sample-page.json

用户授权发布实际内容后,将 validate 改为 publish。不要把这两篇示例自动发布到用户目录,也不要把示例 id 用作正式日报的固定 id。

publish 的 JSON 结果与你该做的事:

result含义你该做的
published新文章或修订已写入并回读确认汇报 id、篇数、generated_at
unchanged远端已是这份内容汇报「无变化」,不重试
duplicate另一个 id(duplicate_of)已有完全相同的正文,什么也没写不要换 id 重发。若本意是修订那一篇,用 duplicate_of 的 id 并修改内容;否则汇报「重复,已跳过」
非零退出 + 错误信息校验、认证、网络或回读失败见 §0.2、§9;Manifest 写入后的失败可能已生效,保留输入核对重试

只在明确授权的发布请求或既定计划任务里执行 publish。

5.2 纯 shell(没有 Python 的主机)

用 scripts/siftr_upload.sh(POSIX sh + curl + sha256sum 或 shasum,创建根目录及其下的父目录并做 SHA-256 回读)。这是低层上传器:会覆盖同名目标,不提供索引合并、去重或不可变保护;文章发布优先使用 Python。上传图片要使用全新文件名,不用此工具覆盖历史正文。每个请求有超时,失败由调用方检查后有限重试:

sh scripts/siftr_upload.sh out/daily_3f2a1b.md documents/daily_3f2a1b.md
sh scripts/siftr_upload.sh chart.jpg media/chart.jpg

manifest.json 由你按 §1、§3 手工编排:GET 旧 Manifest → 本地合并 → 上传正文并确认回读哈希一致 → 最后 PUT manifest.json → GET 复核。合并时务必自己做 Python 版做的事:

  1. 旧 Manifest 是 v1 时:每条补 "format": "markdown",顶层改为 "schema_version": 2。
  2. 同 id 替换旧条目(沿用 id,更新 format、path、content_sha256、size_bytes、updated_at)。同一个 id 从 Markdown 改成 HTML 也属于修订。
  3. 先查重:如果另一个 id 已有相同的 format + content_sha256,就停止,按 duplicate 处理。
  4. 不认识的 format 条目原样保留。
  5. 按 published_at 倒序,刷新 generated_at,Manifest ≤ 2 MiB、≤ 5000 条。

6. Markdown 写作规范(按 App 渲染器实际能力)

支持:h1–h6、粗/斜体、删除线、行内代码、有序/无序列表、任务列表(- [ ] / - [x])、引用块、提示块(引用块首行写 [!NOTE] 等)、表格(横向滚动)、代码块(显示语言名、可一键复制;代码横向滚动,text/markdown 或不标语言的块自动换行)、分隔线、可点击链接、图片(圆角,可点开放大)、脚注([^1],显示为上标数字,正文末尾汇总为「参考」)、LaTeX 公式($…$、$$…$$)。正文 18pt,随系统动态字号;读者可选黑体或宋体。

  • 分节用 ## 与 ###;不要用 #(与文章大标题重复;开头与标题相同的 # 会被 App 自动隐藏)。每个标题都会成为 App 里的目录条目,所以小节标题要简短、有信息、在同一篇里不重复。
  • 首段直接给结论,段落短小;关键引语放引用块,对比信息用表格,命令与代码放代码块并在开头围栏后标注语言(如 python、bash),App 会显示语言名。
  • 需要读者特别留意的一句话用 GitHub 提示块(示例见下),类型只有 NOTE(说明)、TIP(提示)、IMPORTANT(重要)、WARNING(注意)、CAUTION(警告);类型后面可以跟一个自定义标题。每篇最多两三个,滥用会失去重点。
  • 待办或核对清单用任务列表;已完成项会以勾选、弱化显示。
  • 图片独占一段(前后空行)时按整幅配图显示;![替代文字](地址 "图注") 中的引号部分显示为图注,没有图注时显示替代文字。
  • 链接锚文本要有意义,必须是 http(s);App 会拦截指向用户 WebDAV 主机的链接,所以不要链接到你自己的 WebDAV 地址。
  • 引用同一内容空间里的另一篇时写 [上一期](documents/<那篇的 id>.md)(例如 documents/ai-daily-2026-10-13.md);链接到网页就用 .html 后缀,比如 documents/sort-demo.html。App 会在内部直接打开那篇,修订后链接依然有效。该文章必须仍在 Manifest 里,否则读者会看到“这篇文章不在当前列表中”。
  • 段内不折行:一个段落写成一行,段与段之间空一行。段内的硬换行会被渲染成空格,中文里就成了多余的空隙。
  • 全角标点后不加空格:。、,、:、) 等后面直接接下一个字;中文与英文、数字之间是否加空格保持全文一致即可。
  • 写金额直接写 $20。只有整篇的 $ 都能两两配成公式($ 内侧不贴空格、右侧不紧跟数字)时 App 才渲染公式,所以公式和美元金额不要出现在同一篇里;需要时把金额写成「20 美元」或 USD 20。
  • Markdown 里不写 HTML(要做网页就用 §6.1 的 HTML 格式);不要在正文里放凭据。
  • App 支持全文搜索:把读者会搜的专有名词、产品名、人名写进正文(而不只写在图里)。
  • 不要写「相关推荐」「点此提问」「朗读」之类依赖 App 功能的话——这些功能不存在。
> [!TIP] 读这一节的方法
> 先看表格里的价格,再看备注。

6.1 HTML 静态网页(format: html)

适合交互演示、可视化、动画讲解这类用 Markdown 表达不出来的内容。App 用隔离的 WebKit 打开,打开即运行。

  • 单个 .html 文件,CSS 和 JS 写在文件里。不要拆出相对路径的 css、js 或图片,App 不会加载它们。
  • 可以从公共 CDN 引用库、字体和图片,但必须用 https://。CDN 资源由读者手机直接请求,不经过坚果云,也不占坚果云流量。发布工具会拒绝 http:// 资源。
  • 外部库固定版本;不依赖联网也应能看到核心结论。不要把私有报告数据发送给外部脚本或接口。发布校验只做静态检查,不证明 JS 运行正常,也不是完整安全审计;有交互时在浏览器验证操作、窄屏和资源加载,并区分浏览器测试与 Siftr 真机验证。
  • 不要依赖后端:没有服务器,没有登录,表单提交不会有响应。
  • 不要引用你的 WebDAV 地址:App 会拦截所有指向用户 WebDAV 主机的请求,发布工具也会拒绝。私有图片请改用公开地址,或者转成 data URI 内联。
  • 页面里的外链会在系统浏览器里打开,target=_blank 也一样。页内锚点(#step-2)照常可用。
  • 移动端优先:读者在手机上竖屏阅读。写上 <meta name="viewport" content="width=device-width, initial-scale=1">(漏写的话 App 会自动补上),布局要能适应 320–430pt 的宽度,可点控件不小于 44pt。
  • 支持深色模式更好:用 @media (prefers-color-scheme: dark)。App 不会改网页的样式。
  • 搜索:App 会提取页面里的可见文字参与全文搜索,JS 动态生成的文字搜不到。关键名词要写在 HTML 里,同时写进 summary 和 tags。
  • 标题写在元数据的 title 里,列表和阅读页都用它;页面里的 <title> 不会显示。
  • 讲解和演示分开时:讲解写成 Markdown,演示写成 HTML,分别用两个 id 发布。然后在 Markdown 里用 [打开演示](documents/<演示的 id>.html) 链接过去,App 会在站内打开。

7. 图片策略

坚果云流量配额宝贵,按优先级:

  1. 首选公开网络链接:![图表](https://example.com/chart.png),不消耗坚果云流量。
  2. 确需私有图片才上传 media/,且必须先压缩:长边 ≤ 1600px、JPEG 质量 70–80(或 WebP 80),单张目标 ≤ 300 KB。10 MiB 的协议上限只是兜底。
magick in.png -resize '1600x1600>' -quality 75 out.jpg                 # ImageMagick(v6 用 convert)
sips -Z 1600 -s format jpeg -s formatOptions 72 in.png --out out.jpg   # macOS 自带

私有图片用纯相对路径引用:![架构图](media/architecture.png)。App 用该来源的凭据认证加载并缓存。

  • media/ 只允许 png/jpg/jpeg/gif/webp;路径禁止 ..、%、空格与控制字符。发布工具会拒绝违规引用。
  • 图片不进 Manifest,App 按路径按需加载。
  • 缓存以路径为键:覆盖同名图片不会刷新已读设备。改图用新文件名(chart-v2.png)并发布正文修订,旧图保留。

8. 纪律与汇报

  • 发布前先 validate。脚本自带凭据泄露检查(Authorization、URL 内嵌账号密码、SIFTR_DAV_APP_PASSWORD= 等直接拒绝)。
  • 永不删除远端历史文件;永不覆盖已有正文路径。
  • 一个目录只有一个写入方(§1.3)。
  • 汇报只含:命令结果(published/unchanged/duplicate/错误类别)、文档 id、篇数、时间。不含凭据、账号、私有本地路径。

9. 排错速查

现象原因做法
认证失败 / 401用了登录密码,或应用密码已撤销让用户重新创建第三方应用密码
409 AncestorsNotFound父目录不存在MKCOL 后重试(Python 工具把 409 与 404 同样视为「还没有 Manifest」,首次发布会自动初始化目录)
429 / 5xx限流退避重试;仍失败就放弃,下次计划任务再试
远端 Manifest 格式错误;已停止发布远端 manifest.json 损坏或被他人改过不要覆盖。汇报给用户,由人决定如何恢复
Manifest 中存在重复 ID远端被手工改坏同上,不要自行覆盖
正文回读校验失败上传被截断/中途改写Manifest 未改动;直接重试
duplicate内容与别的 id 相同§1.2:不要重发
HTML 不能从 http:// 加载资源页面引用了明文资源改成 https:// 的地址
正文引用了内容空间所在的 WebDAV 主机正文里写了用户的坚果云地址删掉;私有图片改用公开地址或 data URI
不支持的 Manifest 协议版本远端是比工具更新的协议不要覆盖;更新 Skill 后再发布
读者 App 提示“需要更新版本”目录已升级到 v2,读者还在用 1.0.0让用户更新 Siftr
App 里没看到新内容读者需要手动下拉刷新;或用户没添加这个目录确认目录地址与用户在 App 里添加的一致

發布 Skill

Skill 版本:2026 年 10 月 22 日

把這個頁面傳給你的 AI Agent,它就能按 Siftr 的內容協定,把 Markdown 簡報發布到你自己的堅果雲 WebDAV。Skill 與安裝包不含任何憑證;應用程式密碼由 Agent 的執行環境注入。

安裝

安裝包是一個 zip,解壓縮得到完整的 siftr-publish/ 目錄(SKILL.md 與兩個指令碼)。把它放進你 Agent 的 skills 目錄即可,常見位置如 ~/.agents/skills/、~/.zcode/skills/、~/.claude/skills/——各平台不同,以你的 Agent 文件為準。

直接把下面這句話傳給你的 Agent:

安裝這個 Skill:下載 https://chenhk.cn/Siftr/assets/siftr-publish.zip 並解壓縮,把得到的 siftr-publish/ 整個目錄放進你的 skills 目錄,然後按其中 SKILL.md 的說明為我發布 Siftr 內容。

下載安裝包(siftr-publish.zip) · 需要設定的三個環境變數見全文 §2。

Skill 全文

以下與安裝包內的 SKILL.md 完全一致(由 scripts/package_skill.py 從 skills/siftr-publish/ 產生,不會漂移)。

Siftr 内容发布(面向 Agent)

0. 给收到这个链接的 Agent

把 Siftr 当成用户手机上的内容交付渠道:你负责生成内容并发布到用户自己的 WebDAV,用户在 Siftr 添加该来源后阅读。适合日报、周报、监控摘要、研究报告、旅行安排,以及单文件 HTML 图表或交互讲解。用户只发来本 Skill 链接时,先简短解释用途并检查是否已配置;链接本身不代表已经授权定时运行或上传测试内容。

  • 一次性报告:生成 Markdown,使用 §4 的元数据与 §5 的发布工具交付。
  • 定时任务产出:用所在平台的定时任务能力执行生成与发布。Siftr 不执行任务,也不保证定时推送通知。确认内容、频率、时间及 IANA 时区(如 Asia/Shanghai),并把发布步骤、Skill/脚本绝对路径、输出路径与凭据注入方式写入任务配置。没有调度能力时说明限制,保留可运行的发布命令,不要声称已设好定时任务。
  • HTML 网页:用于自包含的交互内容,按 §6.1 编写。它是 App 内阅读页面,不是公网网站托管;不要把私有 WebDAV 地址当成可分享的网页链接。用户要公开网站或后端服务时,需要另选托管方案。

0.1 首次使用的顺序

  1. 若只拿到网页说明,下载 完整 Skill 安装包,解压并阅读其中的 SKILL.md。脚本在解压后的 siftr-publish/scripts/;以下命令相对于该目录的父级 Skill 根目录执行。不要假设用户当前目录已有脚本;自动任务使用绝对路径。需要 Python 3.10+,无需 pip 包。
  2. 只检查 §2 的三个环境变量是否存在,不要打印值。未配置时按 §2.1 引导;先在本地准备和校验内容,不反复尝试联网发布。
  3. 已配置后运行 status。ready 表示远端索引可读;waiting_for_first_publish 表示尚无索引或父目录,不证明写权限正常。两者都不证明用户已连接 App,也无法判断 App 版本。
  4. 在已有发布授权下先 validate 再 publish。成功后告诉用户标题、文档 id 与发布结果,让用户在 Siftr 对应来源下拉刷新。只有收到回读确认才能说“已发布”;没有实际查看 App 时不要说“手机已经收到”。
  5. 定时任务必须在它自己的运行环境里取得凭据;当前终端配置不会自动传给后台任务。同一来源的所有任务串行执行,避免重叠。首次定时运行前,用同一执行环境验证一次。

0.2 每期内容与重试

日报使用 ai-daily-2026-10-22,周报使用 weekly-2026-w43;日期以用户时区和本次任务的逻辑周期为准。只有持续更新的同一份报告才用固定 id。重试保留本次正文、元数据、id 和时间戳,不要重新生成一篇或换 id。修订保留首次 published_at,只更新 updated_at。重复任务运行若不断刷新 updated_at,会造成不必要的索引更新。

保留本地产物直到回读确认。发布失败的处理按阶段区分:正文阶段失败时索引未更新;一旦尝试 PUT Manifest,超时或回读失败都可能发生在远端已经写入之后。此时用相同输入重新核对/发布,不要删除远端内容或宣称回滚。脚本每个网络请求最多尝试三次;任务平台也应限制重试次数并串行执行,不要无限重试。

你是 唯一的写入方。Siftr App 只读:它 GET manifest.json,对比后再 GET 需要的正文。App 不会替你总结、配图、推荐、朗读或追问——这些都不做。摘要、分类、标签、标题都由你写好,写进元数据和正文。正文可以是 Markdown 简报,也可以是单文件静态网页(HTML,见 §6.1)。

只想传任意文件到坚果云?看 §5 的 Shell 上传工具。想让 App 读到内容?先读 §1 的逻辑,再用 §5 的工具。

1. 先理解这套逻辑(决定你每次该怎么做)

1.1 两个标识,别混

标识含义谁决定
id这是哪一篇文章。跨修订保持不变(如 ai-daily-2026-10-22、weekly-2026-w43)你
content_sha256这是哪份字节。正文 UTF-8 字节的 SHA-256工具自动算

读者端的已读/收藏记录挂在 id 上;内容变化靠 content_sha256 判断。

1.2 发布前的决策表

你手里的情况怎么做工具返回
全新的一篇起一个新 id,publishpublished
修订已发布的文章(改错字、补充、更新)沿用原 id,改 Markdown,publishpublished(旧版本文件保留,Manifest 指向新文件)
与上次完全一样什么都不用做(再跑也没副作用)unchanged
新 id,但正文与某篇已发布的字节完全相同不要发。这是重复内容,工具直接拒绝写入duplicate(含 duplicate_of,即已存在的 id)
想用旧 id 发一篇完全不同的文章禁止。会覆盖该 id 的阅读状态;换新 id—

重复内容有两层防线:发布工具(duplicate,不写任何文件)和 App(读到相同 Markdown 只显示一份)。别靠 App 兜底——别制造重复。判断「是不是同一篇」用 id,不要用标题。

1.3 读者可以订阅多个 WebDAV 来源

用户可在 Siftr 里添加多个 WebDAV(例如两个坚果云目录、或坚果云加别家 WebDAV)。对你的影响:

  • 一个目录 = 一个来源 = 一份 manifest.json,一个目录只让一个发布方写。Manifest 是「读—改—写」,写前会检查变化,但没有原子锁,两个 Agent 同时写同一个目录会丢更新。想多 Agent,就一人一个目录,让用户在 App 里各添加一次。
  • 同一地址 App 只收一次(大小写、末尾斜杠、默认端口 443、「服务器路径+目录」怎么拆分都视为同一地址)。你不需要、也不应该帮用户在多个目录里重复发同一篇来「保险」。
  • 不同来源里内容完全相同的 Markdown,App 只显示一份(保留较早添加的来源那份,已读/收藏同步到两份上)。所以「同一篇发布到两个目录」没有意义,除非用户明确要求镜像备份。
  • 来源之间 id 互不冲突:甲目录的 daily 和乙目录的 daily 是两篇不同的文章,除非字节也相同。
  • 写入一个目录时只操作这个目录。不要在 SIFTR_DAV_URL 之外读写;不要把一个来源的凭据用到另一个来源。

1.4 读者端如何对待你的文件(决定了「不可变」规则)

  • 正文文件名含全文 SHA-256,一版一文件,永不改、永不删。修订就是新文件 + Manifest 改指新路径。App 要求同一路径下内容不可变,违反会被判定为损坏的 Manifest。
  • Manifest 永远最后写。读者随时可能刷新:正文先落盘并回读校验,Manifest 最后更新,读者看到的状态就总是一致的。
  • App 会把 media/ 图片按路径缓存,不会因为你覆盖同名图片而刷新(见 §7)。

2. 凭据与上传原语

运行环境通过各自的安全管理器注入三个环境变量:

变量含义示例
SIFTR_DAV_URL内容根目录完整 URL(HTTPS)https://dav.jianguoyun.com/dav/Siftr/
SIFTR_DAV_USER账号user@example.com
SIFTR_DAV_APP_PASSWORD第三方应用密码见下

坚果云 WebDAV 不接受登录密码。在网页版「账户信息 → 安全选项 → 第三方应用管理」为每个发布方单独创建应用密码(读者 App、每个 Agent 各一个,互不共用)。凭据只从环境变量读取,绝不写入 Markdown、元数据、仓库、日志、命令行字面值或你的回复。

2.1 用户尚未配置时(先完成这一步)

先说明:“Siftr 需要你自己的 WebDAV 空间来接收内容。你可以使用坚果云;我可以先准备报告,连接后再发布。密码请填写到本机或平台的安全配置入口,不要发到聊天里。”

  1. 引导用户到 坚果云官网 注册或登录,参照 官方第三方应用授权教程 创建应用密码。网页路径为“账户信息 → 安全选项 → 第三方应用管理 → 添加应用密码”。Agent 应先打开官方教程核对当前界面;界面或套餐限制变化时以官方帮助为准。
  2. 确定专用目录,例如 Siftr。坚果云服务器地址为 https://dav.jianguoyun.com/dav/,本工具使用的完整内容根目录为 https://dav.jianguoyun.com/dav/Siftr/。不要填官网网址、分享链接或 manifest.json 地址。初次配置优先使用单层 ASCII 目录;嵌套目录的上级需用户提前创建,工具不会越过内容根目录创建父级。
  3. 在 Agent 平台的秘密管理/环境变量设置里保存 §2 三项。用户填写账号与应用密码;不要使用登录密码、提交到代码库或写进定时任务提示词。平台没有安全输入入口时,由用户在自己的终端配置;不要要求把密码贴回对话。
  4. 在 Siftr 添加 WebDAV 来源:服务器 https://dav.jianguoyun.com/dav/、目录 Siftr,填写同一账号及为 App 单独生成的应用密码。Agent 和 App 的有效目录必须相同,不要重复拼上两次 Siftr。
  5. 运行 status,按结果处理认证、权限或空目录。获得发布授权后上传用户需要的第一篇内容,再请用户在 App 刷新确认;不要为了连接检查擅自留下测试文章。

其他提供商也使用 HTTPS、账号和应用密码(或其官方要求的 WebDAV 密码),以该服务官方文档为准。不要把坚果云特有的 409 行为推断为所有 WebDAV 服务的规则。

任意文件上传可用 §5 的 Shell 工具;它不更新内容索引,上传文件并不等于在 Siftr 发布了文章。

坚果云 WebDAV 行为速查(2026-10 实测)

场景实际返回
认证HTTPS + Basic(realm="nutstore"),HTTP/2
凭据错误401,空响应体
父目录不存在时 GET/PUT409 AncestorsNotFound——不是 404,先 MKCOL(注意:MKCOL 的 URL 不能带尾部斜杠,…/Siftr 成功、…/Siftr/ 返回 409;Python 工具已处理)
GET 目录403;列目录必须 PROPFIND
频率/流量超限429/5xx;指数退避,几次后放弃

3. 云端目录结构(Siftr 内容协议 v2)

$SIFTR_DAV_URL(如 https://dav.jianguoyun.com/dav/Siftr/)
├── manifest.json                      # 索引:App 的唯一入口
├── documents/
│   ├── <id前缀>_<内容SHA-256>.md       # format = markdown,不可变,一版一文件
│   └── <id前缀>_<内容SHA-256>.html     # format = html,同样不可变
└── media/                             # 可选:Markdown 引用的私有图片(App 认证加载)
    └── <名称>.png|.jpg|.jpeg|.gif|.webp

manifest.json 顶层:

字段类型约束
schema_versionint固定 2(读到 1 时 Python 工具会自动升级,见下)
generated_atstring带时区 ISO 8601;整份 ≤ 2 MiB
publisherstring?发布方标识,可选
documentsarray≤ 5000 条,按 published_at 倒序;id 不得重复

documents[] 每条:

字段类型约束
idstringASCII [A-Za-z0-9._-],≤ 200 字符,跨修订不变
formatstring必填:markdown 或 html(工具按正文后缀自动填写)
title / subtitlestring / string?标题非空且 ≤ 200 字符
summarystring≤ 1000 字符;App 列表和搜索都用它,认真写
categorystring非空;App 用它做筛选
tags[string]?可选;App 用它做筛选和搜索
published_at / updated_atstring带时区 ISO 8601(2026-10-13T09:00:00+08:00);published_at 决定排序与按月归档
pathstringdocuments/<id>_<sha256>.<md|html>,由工具生成;后缀必须与 format 一致
content_sha256string正文原始字节的 SHA-256(64 位小写十六进制)
size_bytesint真实字节数,单篇 ≤ 5 MiB(两种格式相同)
reading_minutes / source_countint?非负
format_optionsobject?仅供对应格式的阅读器使用;markdown、html 目前没有定义,不要写

硬性规则:正文是 documents/ 下非空的 UTF-8 文件,≤ 5 MiB;正文与链接里不得出现凭据;时间戳必须带时区。alternates 是协议保留字段,不要写。

版本与旧数据:v1(schema_version: 1,没有 format)只有 Markdown。Python 工具读到 v1 时,会给每条补上 "format": "markdown",下次发布时以 v2 写回(结果里带 upgraded_to_schema_version: 2)。Siftr 1.0.0 只认识 v1,看到 v2 会提示升级 App;所以在用户换成支持 v2 的版本之前,不要往他的目录发布。status 只能检查远端协议,不能判断用户安装的 App 版本;旧目录升级前让用户确认已更新 App。

兼容未来格式:Manifest 里出现你不认识的 format(比如以后的 audio),不要删也不要改,原样保留。Python 工具会自动这样处理。

4. 元数据 JSON(--metadata)

{
  "id": "ai-daily-2026-10-13",
  "title": "AI 每日简报:把信息读成理解",
  "summary": "三句话说清今天发生了什么、为什么重要。",
  "category": "AI 资讯",
  "tags": ["Agent", "模型"],
  "published_at": "2026-10-13T08:30:00+08:00",
  "updated_at": "2026-10-13T08:30:00+08:00",
  "reading_minutes": 3,
  "source_count": 5
}

必填:id、title、summary、category、published_at、updated_at。可选:subtitle、tags、reading_minutes、source_count、publisher。format、path、content_sha256、size_bytes 由工具填,不要手写;如果写了 format,必须和正文格式一致。修订时 published_at 保持首次发布时间,updated_at 写本次时间。

5. 发布路径:按运行环境任选

5.0 Python 3.10+(推荐)

scripts/siftr_publish.py(纯标准库,内置校验、幂等、重复检测、回读与退避,适合无人值守任务):

python3 scripts/siftr_publish.py validate --markdown out/daily.md --metadata out/daily.json   # 完全离线
python3 scripts/siftr_publish.py status                                                        # 远端概况:schema_version、篇数、各格式数量
python3 scripts/siftr_publish.py publish  --markdown out/daily.md --metadata out/daily.json
python3 scripts/siftr_publish.py publish  --html out/sort-demo.html --metadata out/sort-demo.json   # 单文件静态网页
python3 scripts/siftr_publish.py publish  --file out/anything.md --metadata out/meta.json           # 按后缀识别格式

5.1 从空目录验证一条完整链路(不联网)

以下示例创建 out/ 下的 Markdown、HTML 与对应元数据;若同名文件已存在则停止,避免覆盖用户产物。示例只演示格式,正式发布时改成用户实际需要的内容。运行目录为解压后的 siftr-publish/。

python3 - <<'PYTHON'
from pathlib import Path
from datetime import datetime, timezone
import json

out = Path("out")
out.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
samples = {
    "sample-report": ("md", "示例报告", "## 本次结果\n\n这里填写本次任务的结论与来源。\n"),
    "sample-page": ("html", "示例网页", '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>示例网页</title><body><main><h1>本次结果</h1><p>这里填写报告正文。</p></main></body></html>'),
}
files = {}
for doc_id, (suffix, title, text) in samples.items():
    files[out / (doc_id + "." + suffix)] = text
    files[out / (doc_id + ".json")] = json.dumps({
        "id": doc_id, "title": title, "summary": "演示发布格式。",
        "category": "示例", "published_at": stamp, "updated_at": stamp,
    }, ensure_ascii=False, indent=2)
if any(path.exists() for path in files):
    raise SystemExit("示例文件已存在;请复用或换一个工作目录。")
for path, text in files.items():
    path.write_text(text, encoding="utf-8")
PYTHON
python3 scripts/siftr_publish.py validate --markdown out/sample-report.md --metadata out/sample-report.json
python3 scripts/siftr_publish.py validate --html out/sample-page.html --metadata out/sample-page.json

用户授权发布实际内容后,将 validate 改为 publish。不要把这两篇示例自动发布到用户目录,也不要把示例 id 用作正式日报的固定 id。

publish 的 JSON 结果与你该做的事:

result含义你该做的
published新文章或修订已写入并回读确认汇报 id、篇数、generated_at
unchanged远端已是这份内容汇报「无变化」,不重试
duplicate另一个 id(duplicate_of)已有完全相同的正文,什么也没写不要换 id 重发。若本意是修订那一篇,用 duplicate_of 的 id 并修改内容;否则汇报「重复,已跳过」
非零退出 + 错误信息校验、认证、网络或回读失败见 §0.2、§9;Manifest 写入后的失败可能已生效,保留输入核对重试

只在明确授权的发布请求或既定计划任务里执行 publish。

5.2 纯 shell(没有 Python 的主机)

用 scripts/siftr_upload.sh(POSIX sh + curl + sha256sum 或 shasum,创建根目录及其下的父目录并做 SHA-256 回读)。这是低层上传器:会覆盖同名目标,不提供索引合并、去重或不可变保护;文章发布优先使用 Python。上传图片要使用全新文件名,不用此工具覆盖历史正文。每个请求有超时,失败由调用方检查后有限重试:

sh scripts/siftr_upload.sh out/daily_3f2a1b.md documents/daily_3f2a1b.md
sh scripts/siftr_upload.sh chart.jpg media/chart.jpg

manifest.json 由你按 §1、§3 手工编排:GET 旧 Manifest → 本地合并 → 上传正文并确认回读哈希一致 → 最后 PUT manifest.json → GET 复核。合并时务必自己做 Python 版做的事:

  1. 旧 Manifest 是 v1 时:每条补 "format": "markdown",顶层改为 "schema_version": 2。
  2. 同 id 替换旧条目(沿用 id,更新 format、path、content_sha256、size_bytes、updated_at)。同一个 id 从 Markdown 改成 HTML 也属于修订。
  3. 先查重:如果另一个 id 已有相同的 format + content_sha256,就停止,按 duplicate 处理。
  4. 不认识的 format 条目原样保留。
  5. 按 published_at 倒序,刷新 generated_at,Manifest ≤ 2 MiB、≤ 5000 条。

6. Markdown 写作规范(按 App 渲染器实际能力)

支持:h1–h6、粗/斜体、删除线、行内代码、有序/无序列表、任务列表(- [ ] / - [x])、引用块、提示块(引用块首行写 [!NOTE] 等)、表格(横向滚动)、代码块(显示语言名、可一键复制;代码横向滚动,text/markdown 或不标语言的块自动换行)、分隔线、可点击链接、图片(圆角,可点开放大)、脚注([^1],显示为上标数字,正文末尾汇总为「参考」)、LaTeX 公式($…$、$$…$$)。正文 18pt,随系统动态字号;读者可选黑体或宋体。

  • 分节用 ## 与 ###;不要用 #(与文章大标题重复;开头与标题相同的 # 会被 App 自动隐藏)。每个标题都会成为 App 里的目录条目,所以小节标题要简短、有信息、在同一篇里不重复。
  • 首段直接给结论,段落短小;关键引语放引用块,对比信息用表格,命令与代码放代码块并在开头围栏后标注语言(如 python、bash),App 会显示语言名。
  • 需要读者特别留意的一句话用 GitHub 提示块(示例见下),类型只有 NOTE(说明)、TIP(提示)、IMPORTANT(重要)、WARNING(注意)、CAUTION(警告);类型后面可以跟一个自定义标题。每篇最多两三个,滥用会失去重点。
  • 待办或核对清单用任务列表;已完成项会以勾选、弱化显示。
  • 图片独占一段(前后空行)时按整幅配图显示;![替代文字](地址 "图注") 中的引号部分显示为图注,没有图注时显示替代文字。
  • 链接锚文本要有意义,必须是 http(s);App 会拦截指向用户 WebDAV 主机的链接,所以不要链接到你自己的 WebDAV 地址。
  • 引用同一内容空间里的另一篇时写 [上一期](documents/<那篇的 id>.md)(例如 documents/ai-daily-2026-10-13.md);链接到网页就用 .html 后缀,比如 documents/sort-demo.html。App 会在内部直接打开那篇,修订后链接依然有效。该文章必须仍在 Manifest 里,否则读者会看到“这篇文章不在当前列表中”。
  • 段内不折行:一个段落写成一行,段与段之间空一行。段内的硬换行会被渲染成空格,中文里就成了多余的空隙。
  • 全角标点后不加空格:。、,、:、) 等后面直接接下一个字;中文与英文、数字之间是否加空格保持全文一致即可。
  • 写金额直接写 $20。只有整篇的 $ 都能两两配成公式($ 内侧不贴空格、右侧不紧跟数字)时 App 才渲染公式,所以公式和美元金额不要出现在同一篇里;需要时把金额写成「20 美元」或 USD 20。
  • Markdown 里不写 HTML(要做网页就用 §6.1 的 HTML 格式);不要在正文里放凭据。
  • App 支持全文搜索:把读者会搜的专有名词、产品名、人名写进正文(而不只写在图里)。
  • 不要写「相关推荐」「点此提问」「朗读」之类依赖 App 功能的话——这些功能不存在。
> [!TIP] 读这一节的方法
> 先看表格里的价格,再看备注。

6.1 HTML 静态网页(format: html)

适合交互演示、可视化、动画讲解这类用 Markdown 表达不出来的内容。App 用隔离的 WebKit 打开,打开即运行。

  • 单个 .html 文件,CSS 和 JS 写在文件里。不要拆出相对路径的 css、js 或图片,App 不会加载它们。
  • 可以从公共 CDN 引用库、字体和图片,但必须用 https://。CDN 资源由读者手机直接请求,不经过坚果云,也不占坚果云流量。发布工具会拒绝 http:// 资源。
  • 外部库固定版本;不依赖联网也应能看到核心结论。不要把私有报告数据发送给外部脚本或接口。发布校验只做静态检查,不证明 JS 运行正常,也不是完整安全审计;有交互时在浏览器验证操作、窄屏和资源加载,并区分浏览器测试与 Siftr 真机验证。
  • 不要依赖后端:没有服务器,没有登录,表单提交不会有响应。
  • 不要引用你的 WebDAV 地址:App 会拦截所有指向用户 WebDAV 主机的请求,发布工具也会拒绝。私有图片请改用公开地址,或者转成 data URI 内联。
  • 页面里的外链会在系统浏览器里打开,target=_blank 也一样。页内锚点(#step-2)照常可用。
  • 移动端优先:读者在手机上竖屏阅读。写上 <meta name="viewport" content="width=device-width, initial-scale=1">(漏写的话 App 会自动补上),布局要能适应 320–430pt 的宽度,可点控件不小于 44pt。
  • 支持深色模式更好:用 @media (prefers-color-scheme: dark)。App 不会改网页的样式。
  • 搜索:App 会提取页面里的可见文字参与全文搜索,JS 动态生成的文字搜不到。关键名词要写在 HTML 里,同时写进 summary 和 tags。
  • 标题写在元数据的 title 里,列表和阅读页都用它;页面里的 <title> 不会显示。
  • 讲解和演示分开时:讲解写成 Markdown,演示写成 HTML,分别用两个 id 发布。然后在 Markdown 里用 [打开演示](documents/<演示的 id>.html) 链接过去,App 会在站内打开。

7. 图片策略

坚果云流量配额宝贵,按优先级:

  1. 首选公开网络链接:![图表](https://example.com/chart.png),不消耗坚果云流量。
  2. 确需私有图片才上传 media/,且必须先压缩:长边 ≤ 1600px、JPEG 质量 70–80(或 WebP 80),单张目标 ≤ 300 KB。10 MiB 的协议上限只是兜底。
magick in.png -resize '1600x1600>' -quality 75 out.jpg                 # ImageMagick(v6 用 convert)
sips -Z 1600 -s format jpeg -s formatOptions 72 in.png --out out.jpg   # macOS 自带

私有图片用纯相对路径引用:![架构图](media/architecture.png)。App 用该来源的凭据认证加载并缓存。

  • media/ 只允许 png/jpg/jpeg/gif/webp;路径禁止 ..、%、空格与控制字符。发布工具会拒绝违规引用。
  • 图片不进 Manifest,App 按路径按需加载。
  • 缓存以路径为键:覆盖同名图片不会刷新已读设备。改图用新文件名(chart-v2.png)并发布正文修订,旧图保留。

8. 纪律与汇报

  • 发布前先 validate。脚本自带凭据泄露检查(Authorization、URL 内嵌账号密码、SIFTR_DAV_APP_PASSWORD= 等直接拒绝)。
  • 永不删除远端历史文件;永不覆盖已有正文路径。
  • 一个目录只有一个写入方(§1.3)。
  • 汇报只含:命令结果(published/unchanged/duplicate/错误类别)、文档 id、篇数、时间。不含凭据、账号、私有本地路径。

9. 排错速查

现象原因做法
认证失败 / 401用了登录密码,或应用密码已撤销让用户重新创建第三方应用密码
409 AncestorsNotFound父目录不存在MKCOL 后重试(Python 工具把 409 与 404 同样视为「还没有 Manifest」,首次发布会自动初始化目录)
429 / 5xx限流退避重试;仍失败就放弃,下次计划任务再试
远端 Manifest 格式错误;已停止发布远端 manifest.json 损坏或被他人改过不要覆盖。汇报给用户,由人决定如何恢复
Manifest 中存在重复 ID远端被手工改坏同上,不要自行覆盖
正文回读校验失败上传被截断/中途改写Manifest 未改动;直接重试
duplicate内容与别的 id 相同§1.2:不要重发
HTML 不能从 http:// 加载资源页面引用了明文资源改成 https:// 的地址
正文引用了内容空间所在的 WebDAV 主机正文里写了用户的坚果云地址删掉;私有图片改用公开地址或 data URI
不支持的 Manifest 协议版本远端是比工具更新的协议不要覆盖;更新 Skill 后再发布
读者 App 提示“需要更新版本”目录已升级到 v2,读者还在用 1.0.0让用户更新 Siftr
App 里没看到新内容读者需要手动下拉刷新;或用户没添加这个目录确认目录地址与用户在 App 里添加的一致

發佈 Skill

Skill 版本:2026 年 10 月 22 日

將這個頁面傳俾你的 AI Agent,佢就可以按 Siftr 嘅內容協定,將 Markdown 簡報發佈到你自己的堅果雲 WebDAV。Skill 同安裝包唔含任何憑證;應用程式密碼由 Agent 嘅執行環境注入。

安裝

安裝包係一個 zip,解壓得到完整嘅 siftr-publish/ 目錄(SKILL.md 同兩個指令碼)。將佢放進你 Agent 嘅 skills 目錄就得,常見位置如 ~/.agents/skills/、~/.zcode/skills/、~/.claude/skills/——各平台唔同,以你 Agent 嘅文件為準。

直接將下面呢句話傳俾你 Agent:

安裝呢個 Skill:下載 https://chenhk.cn/Siftr/assets/siftr-publish.zip 並解壓,將得到嘅 siftr-publish/ 成個目錄放入你嘅 skills 目錄,然後按入面 SKILL.md 嘅說明為我發佈 Siftr 內容。

下載安裝包(siftr-publish.zip) · 需要設定嘅三個環境變數見全文 §2。

Skill 全文

以下同安裝包入面嘅 SKILL.md 完全一致(由 scripts/package_skill.py 從 skills/siftr-publish/ 產生,唔會漂移)。

Siftr 内容发布(面向 Agent)

0. 给收到这个链接的 Agent

把 Siftr 当成用户手机上的内容交付渠道:你负责生成内容并发布到用户自己的 WebDAV,用户在 Siftr 添加该来源后阅读。适合日报、周报、监控摘要、研究报告、旅行安排,以及单文件 HTML 图表或交互讲解。用户只发来本 Skill 链接时,先简短解释用途并检查是否已配置;链接本身不代表已经授权定时运行或上传测试内容。

  • 一次性报告:生成 Markdown,使用 §4 的元数据与 §5 的发布工具交付。
  • 定时任务产出:用所在平台的定时任务能力执行生成与发布。Siftr 不执行任务,也不保证定时推送通知。确认内容、频率、时间及 IANA 时区(如 Asia/Shanghai),并把发布步骤、Skill/脚本绝对路径、输出路径与凭据注入方式写入任务配置。没有调度能力时说明限制,保留可运行的发布命令,不要声称已设好定时任务。
  • HTML 网页:用于自包含的交互内容,按 §6.1 编写。它是 App 内阅读页面,不是公网网站托管;不要把私有 WebDAV 地址当成可分享的网页链接。用户要公开网站或后端服务时,需要另选托管方案。

0.1 首次使用的顺序

  1. 若只拿到网页说明,下载 完整 Skill 安装包,解压并阅读其中的 SKILL.md。脚本在解压后的 siftr-publish/scripts/;以下命令相对于该目录的父级 Skill 根目录执行。不要假设用户当前目录已有脚本;自动任务使用绝对路径。需要 Python 3.10+,无需 pip 包。
  2. 只检查 §2 的三个环境变量是否存在,不要打印值。未配置时按 §2.1 引导;先在本地准备和校验内容,不反复尝试联网发布。
  3. 已配置后运行 status。ready 表示远端索引可读;waiting_for_first_publish 表示尚无索引或父目录,不证明写权限正常。两者都不证明用户已连接 App,也无法判断 App 版本。
  4. 在已有发布授权下先 validate 再 publish。成功后告诉用户标题、文档 id 与发布结果,让用户在 Siftr 对应来源下拉刷新。只有收到回读确认才能说“已发布”;没有实际查看 App 时不要说“手机已经收到”。
  5. 定时任务必须在它自己的运行环境里取得凭据;当前终端配置不会自动传给后台任务。同一来源的所有任务串行执行,避免重叠。首次定时运行前,用同一执行环境验证一次。

0.2 每期内容与重试

日报使用 ai-daily-2026-10-22,周报使用 weekly-2026-w43;日期以用户时区和本次任务的逻辑周期为准。只有持续更新的同一份报告才用固定 id。重试保留本次正文、元数据、id 和时间戳,不要重新生成一篇或换 id。修订保留首次 published_at,只更新 updated_at。重复任务运行若不断刷新 updated_at,会造成不必要的索引更新。

保留本地产物直到回读确认。发布失败的处理按阶段区分:正文阶段失败时索引未更新;一旦尝试 PUT Manifest,超时或回读失败都可能发生在远端已经写入之后。此时用相同输入重新核对/发布,不要删除远端内容或宣称回滚。脚本每个网络请求最多尝试三次;任务平台也应限制重试次数并串行执行,不要无限重试。

你是 唯一的写入方。Siftr App 只读:它 GET manifest.json,对比后再 GET 需要的正文。App 不会替你总结、配图、推荐、朗读或追问——这些都不做。摘要、分类、标签、标题都由你写好,写进元数据和正文。正文可以是 Markdown 简报,也可以是单文件静态网页(HTML,见 §6.1)。

只想传任意文件到坚果云?看 §5 的 Shell 上传工具。想让 App 读到内容?先读 §1 的逻辑,再用 §5 的工具。

1. 先理解这套逻辑(决定你每次该怎么做)

1.1 两个标识,别混

标识含义谁决定
id这是哪一篇文章。跨修订保持不变(如 ai-daily-2026-10-22、weekly-2026-w43)你
content_sha256这是哪份字节。正文 UTF-8 字节的 SHA-256工具自动算

读者端的已读/收藏记录挂在 id 上;内容变化靠 content_sha256 判断。

1.2 发布前的决策表

你手里的情况怎么做工具返回
全新的一篇起一个新 id,publishpublished
修订已发布的文章(改错字、补充、更新)沿用原 id,改 Markdown,publishpublished(旧版本文件保留,Manifest 指向新文件)
与上次完全一样什么都不用做(再跑也没副作用)unchanged
新 id,但正文与某篇已发布的字节完全相同不要发。这是重复内容,工具直接拒绝写入duplicate(含 duplicate_of,即已存在的 id)
想用旧 id 发一篇完全不同的文章禁止。会覆盖该 id 的阅读状态;换新 id—

重复内容有两层防线:发布工具(duplicate,不写任何文件)和 App(读到相同 Markdown 只显示一份)。别靠 App 兜底——别制造重复。判断「是不是同一篇」用 id,不要用标题。

1.3 读者可以订阅多个 WebDAV 来源

用户可在 Siftr 里添加多个 WebDAV(例如两个坚果云目录、或坚果云加别家 WebDAV)。对你的影响:

  • 一个目录 = 一个来源 = 一份 manifest.json,一个目录只让一个发布方写。Manifest 是「读—改—写」,写前会检查变化,但没有原子锁,两个 Agent 同时写同一个目录会丢更新。想多 Agent,就一人一个目录,让用户在 App 里各添加一次。
  • 同一地址 App 只收一次(大小写、末尾斜杠、默认端口 443、「服务器路径+目录」怎么拆分都视为同一地址)。你不需要、也不应该帮用户在多个目录里重复发同一篇来「保险」。
  • 不同来源里内容完全相同的 Markdown,App 只显示一份(保留较早添加的来源那份,已读/收藏同步到两份上)。所以「同一篇发布到两个目录」没有意义,除非用户明确要求镜像备份。
  • 来源之间 id 互不冲突:甲目录的 daily 和乙目录的 daily 是两篇不同的文章,除非字节也相同。
  • 写入一个目录时只操作这个目录。不要在 SIFTR_DAV_URL 之外读写;不要把一个来源的凭据用到另一个来源。

1.4 读者端如何对待你的文件(决定了「不可变」规则)

  • 正文文件名含全文 SHA-256,一版一文件,永不改、永不删。修订就是新文件 + Manifest 改指新路径。App 要求同一路径下内容不可变,违反会被判定为损坏的 Manifest。
  • Manifest 永远最后写。读者随时可能刷新:正文先落盘并回读校验,Manifest 最后更新,读者看到的状态就总是一致的。
  • App 会把 media/ 图片按路径缓存,不会因为你覆盖同名图片而刷新(见 §7)。

2. 凭据与上传原语

运行环境通过各自的安全管理器注入三个环境变量:

变量含义示例
SIFTR_DAV_URL内容根目录完整 URL(HTTPS)https://dav.jianguoyun.com/dav/Siftr/
SIFTR_DAV_USER账号user@example.com
SIFTR_DAV_APP_PASSWORD第三方应用密码见下

坚果云 WebDAV 不接受登录密码。在网页版「账户信息 → 安全选项 → 第三方应用管理」为每个发布方单独创建应用密码(读者 App、每个 Agent 各一个,互不共用)。凭据只从环境变量读取,绝不写入 Markdown、元数据、仓库、日志、命令行字面值或你的回复。

2.1 用户尚未配置时(先完成这一步)

先说明:“Siftr 需要你自己的 WebDAV 空间来接收内容。你可以使用坚果云;我可以先准备报告,连接后再发布。密码请填写到本机或平台的安全配置入口,不要发到聊天里。”

  1. 引导用户到 坚果云官网 注册或登录,参照 官方第三方应用授权教程 创建应用密码。网页路径为“账户信息 → 安全选项 → 第三方应用管理 → 添加应用密码”。Agent 应先打开官方教程核对当前界面;界面或套餐限制变化时以官方帮助为准。
  2. 确定专用目录,例如 Siftr。坚果云服务器地址为 https://dav.jianguoyun.com/dav/,本工具使用的完整内容根目录为 https://dav.jianguoyun.com/dav/Siftr/。不要填官网网址、分享链接或 manifest.json 地址。初次配置优先使用单层 ASCII 目录;嵌套目录的上级需用户提前创建,工具不会越过内容根目录创建父级。
  3. 在 Agent 平台的秘密管理/环境变量设置里保存 §2 三项。用户填写账号与应用密码;不要使用登录密码、提交到代码库或写进定时任务提示词。平台没有安全输入入口时,由用户在自己的终端配置;不要要求把密码贴回对话。
  4. 在 Siftr 添加 WebDAV 来源:服务器 https://dav.jianguoyun.com/dav/、目录 Siftr,填写同一账号及为 App 单独生成的应用密码。Agent 和 App 的有效目录必须相同,不要重复拼上两次 Siftr。
  5. 运行 status,按结果处理认证、权限或空目录。获得发布授权后上传用户需要的第一篇内容,再请用户在 App 刷新确认;不要为了连接检查擅自留下测试文章。

其他提供商也使用 HTTPS、账号和应用密码(或其官方要求的 WebDAV 密码),以该服务官方文档为准。不要把坚果云特有的 409 行为推断为所有 WebDAV 服务的规则。

任意文件上传可用 §5 的 Shell 工具;它不更新内容索引,上传文件并不等于在 Siftr 发布了文章。

坚果云 WebDAV 行为速查(2026-10 实测)

场景实际返回
认证HTTPS + Basic(realm="nutstore"),HTTP/2
凭据错误401,空响应体
父目录不存在时 GET/PUT409 AncestorsNotFound——不是 404,先 MKCOL(注意:MKCOL 的 URL 不能带尾部斜杠,…/Siftr 成功、…/Siftr/ 返回 409;Python 工具已处理)
GET 目录403;列目录必须 PROPFIND
频率/流量超限429/5xx;指数退避,几次后放弃

3. 云端目录结构(Siftr 内容协议 v2)

$SIFTR_DAV_URL(如 https://dav.jianguoyun.com/dav/Siftr/)
├── manifest.json                      # 索引:App 的唯一入口
├── documents/
│   ├── <id前缀>_<内容SHA-256>.md       # format = markdown,不可变,一版一文件
│   └── <id前缀>_<内容SHA-256>.html     # format = html,同样不可变
└── media/                             # 可选:Markdown 引用的私有图片(App 认证加载)
    └── <名称>.png|.jpg|.jpeg|.gif|.webp

manifest.json 顶层:

字段类型约束
schema_versionint固定 2(读到 1 时 Python 工具会自动升级,见下)
generated_atstring带时区 ISO 8601;整份 ≤ 2 MiB
publisherstring?发布方标识,可选
documentsarray≤ 5000 条,按 published_at 倒序;id 不得重复

documents[] 每条:

字段类型约束
idstringASCII [A-Za-z0-9._-],≤ 200 字符,跨修订不变
formatstring必填:markdown 或 html(工具按正文后缀自动填写)
title / subtitlestring / string?标题非空且 ≤ 200 字符
summarystring≤ 1000 字符;App 列表和搜索都用它,认真写
categorystring非空;App 用它做筛选
tags[string]?可选;App 用它做筛选和搜索
published_at / updated_atstring带时区 ISO 8601(2026-10-13T09:00:00+08:00);published_at 决定排序与按月归档
pathstringdocuments/<id>_<sha256>.<md|html>,由工具生成;后缀必须与 format 一致
content_sha256string正文原始字节的 SHA-256(64 位小写十六进制)
size_bytesint真实字节数,单篇 ≤ 5 MiB(两种格式相同)
reading_minutes / source_countint?非负
format_optionsobject?仅供对应格式的阅读器使用;markdown、html 目前没有定义,不要写

硬性规则:正文是 documents/ 下非空的 UTF-8 文件,≤ 5 MiB;正文与链接里不得出现凭据;时间戳必须带时区。alternates 是协议保留字段,不要写。

版本与旧数据:v1(schema_version: 1,没有 format)只有 Markdown。Python 工具读到 v1 时,会给每条补上 "format": "markdown",下次发布时以 v2 写回(结果里带 upgraded_to_schema_version: 2)。Siftr 1.0.0 只认识 v1,看到 v2 会提示升级 App;所以在用户换成支持 v2 的版本之前,不要往他的目录发布。status 只能检查远端协议,不能判断用户安装的 App 版本;旧目录升级前让用户确认已更新 App。

兼容未来格式:Manifest 里出现你不认识的 format(比如以后的 audio),不要删也不要改,原样保留。Python 工具会自动这样处理。

4. 元数据 JSON(--metadata)

{
  "id": "ai-daily-2026-10-13",
  "title": "AI 每日简报:把信息读成理解",
  "summary": "三句话说清今天发生了什么、为什么重要。",
  "category": "AI 资讯",
  "tags": ["Agent", "模型"],
  "published_at": "2026-10-13T08:30:00+08:00",
  "updated_at": "2026-10-13T08:30:00+08:00",
  "reading_minutes": 3,
  "source_count": 5
}

必填:id、title、summary、category、published_at、updated_at。可选:subtitle、tags、reading_minutes、source_count、publisher。format、path、content_sha256、size_bytes 由工具填,不要手写;如果写了 format,必须和正文格式一致。修订时 published_at 保持首次发布时间,updated_at 写本次时间。

5. 发布路径:按运行环境任选

5.0 Python 3.10+(推荐)

scripts/siftr_publish.py(纯标准库,内置校验、幂等、重复检测、回读与退避,适合无人值守任务):

python3 scripts/siftr_publish.py validate --markdown out/daily.md --metadata out/daily.json   # 完全离线
python3 scripts/siftr_publish.py status                                                        # 远端概况:schema_version、篇数、各格式数量
python3 scripts/siftr_publish.py publish  --markdown out/daily.md --metadata out/daily.json
python3 scripts/siftr_publish.py publish  --html out/sort-demo.html --metadata out/sort-demo.json   # 单文件静态网页
python3 scripts/siftr_publish.py publish  --file out/anything.md --metadata out/meta.json           # 按后缀识别格式

5.1 从空目录验证一条完整链路(不联网)

以下示例创建 out/ 下的 Markdown、HTML 与对应元数据;若同名文件已存在则停止,避免覆盖用户产物。示例只演示格式,正式发布时改成用户实际需要的内容。运行目录为解压后的 siftr-publish/。

python3 - <<'PYTHON'
from pathlib import Path
from datetime import datetime, timezone
import json

out = Path("out")
out.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
samples = {
    "sample-report": ("md", "示例报告", "## 本次结果\n\n这里填写本次任务的结论与来源。\n"),
    "sample-page": ("html", "示例网页", '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>示例网页</title><body><main><h1>本次结果</h1><p>这里填写报告正文。</p></main></body></html>'),
}
files = {}
for doc_id, (suffix, title, text) in samples.items():
    files[out / (doc_id + "." + suffix)] = text
    files[out / (doc_id + ".json")] = json.dumps({
        "id": doc_id, "title": title, "summary": "演示发布格式。",
        "category": "示例", "published_at": stamp, "updated_at": stamp,
    }, ensure_ascii=False, indent=2)
if any(path.exists() for path in files):
    raise SystemExit("示例文件已存在;请复用或换一个工作目录。")
for path, text in files.items():
    path.write_text(text, encoding="utf-8")
PYTHON
python3 scripts/siftr_publish.py validate --markdown out/sample-report.md --metadata out/sample-report.json
python3 scripts/siftr_publish.py validate --html out/sample-page.html --metadata out/sample-page.json

用户授权发布实际内容后,将 validate 改为 publish。不要把这两篇示例自动发布到用户目录,也不要把示例 id 用作正式日报的固定 id。

publish 的 JSON 结果与你该做的事:

result含义你该做的
published新文章或修订已写入并回读确认汇报 id、篇数、generated_at
unchanged远端已是这份内容汇报「无变化」,不重试
duplicate另一个 id(duplicate_of)已有完全相同的正文,什么也没写不要换 id 重发。若本意是修订那一篇,用 duplicate_of 的 id 并修改内容;否则汇报「重复,已跳过」
非零退出 + 错误信息校验、认证、网络或回读失败见 §0.2、§9;Manifest 写入后的失败可能已生效,保留输入核对重试

只在明确授权的发布请求或既定计划任务里执行 publish。

5.2 纯 shell(没有 Python 的主机)

用 scripts/siftr_upload.sh(POSIX sh + curl + sha256sum 或 shasum,创建根目录及其下的父目录并做 SHA-256 回读)。这是低层上传器:会覆盖同名目标,不提供索引合并、去重或不可变保护;文章发布优先使用 Python。上传图片要使用全新文件名,不用此工具覆盖历史正文。每个请求有超时,失败由调用方检查后有限重试:

sh scripts/siftr_upload.sh out/daily_3f2a1b.md documents/daily_3f2a1b.md
sh scripts/siftr_upload.sh chart.jpg media/chart.jpg

manifest.json 由你按 §1、§3 手工编排:GET 旧 Manifest → 本地合并 → 上传正文并确认回读哈希一致 → 最后 PUT manifest.json → GET 复核。合并时务必自己做 Python 版做的事:

  1. 旧 Manifest 是 v1 时:每条补 "format": "markdown",顶层改为 "schema_version": 2。
  2. 同 id 替换旧条目(沿用 id,更新 format、path、content_sha256、size_bytes、updated_at)。同一个 id 从 Markdown 改成 HTML 也属于修订。
  3. 先查重:如果另一个 id 已有相同的 format + content_sha256,就停止,按 duplicate 处理。
  4. 不认识的 format 条目原样保留。
  5. 按 published_at 倒序,刷新 generated_at,Manifest ≤ 2 MiB、≤ 5000 条。

6. Markdown 写作规范(按 App 渲染器实际能力)

支持:h1–h6、粗/斜体、删除线、行内代码、有序/无序列表、任务列表(- [ ] / - [x])、引用块、提示块(引用块首行写 [!NOTE] 等)、表格(横向滚动)、代码块(显示语言名、可一键复制;代码横向滚动,text/markdown 或不标语言的块自动换行)、分隔线、可点击链接、图片(圆角,可点开放大)、脚注([^1],显示为上标数字,正文末尾汇总为「参考」)、LaTeX 公式($…$、$$…$$)。正文 18pt,随系统动态字号;读者可选黑体或宋体。

  • 分节用 ## 与 ###;不要用 #(与文章大标题重复;开头与标题相同的 # 会被 App 自动隐藏)。每个标题都会成为 App 里的目录条目,所以小节标题要简短、有信息、在同一篇里不重复。
  • 首段直接给结论,段落短小;关键引语放引用块,对比信息用表格,命令与代码放代码块并在开头围栏后标注语言(如 python、bash),App 会显示语言名。
  • 需要读者特别留意的一句话用 GitHub 提示块(示例见下),类型只有 NOTE(说明)、TIP(提示)、IMPORTANT(重要)、WARNING(注意)、CAUTION(警告);类型后面可以跟一个自定义标题。每篇最多两三个,滥用会失去重点。
  • 待办或核对清单用任务列表;已完成项会以勾选、弱化显示。
  • 图片独占一段(前后空行)时按整幅配图显示;![替代文字](地址 "图注") 中的引号部分显示为图注,没有图注时显示替代文字。
  • 链接锚文本要有意义,必须是 http(s);App 会拦截指向用户 WebDAV 主机的链接,所以不要链接到你自己的 WebDAV 地址。
  • 引用同一内容空间里的另一篇时写 [上一期](documents/<那篇的 id>.md)(例如 documents/ai-daily-2026-10-13.md);链接到网页就用 .html 后缀,比如 documents/sort-demo.html。App 会在内部直接打开那篇,修订后链接依然有效。该文章必须仍在 Manifest 里,否则读者会看到“这篇文章不在当前列表中”。
  • 段内不折行:一个段落写成一行,段与段之间空一行。段内的硬换行会被渲染成空格,中文里就成了多余的空隙。
  • 全角标点后不加空格:。、,、:、) 等后面直接接下一个字;中文与英文、数字之间是否加空格保持全文一致即可。
  • 写金额直接写 $20。只有整篇的 $ 都能两两配成公式($ 内侧不贴空格、右侧不紧跟数字)时 App 才渲染公式,所以公式和美元金额不要出现在同一篇里;需要时把金额写成「20 美元」或 USD 20。
  • Markdown 里不写 HTML(要做网页就用 §6.1 的 HTML 格式);不要在正文里放凭据。
  • App 支持全文搜索:把读者会搜的专有名词、产品名、人名写进正文(而不只写在图里)。
  • 不要写「相关推荐」「点此提问」「朗读」之类依赖 App 功能的话——这些功能不存在。
> [!TIP] 读这一节的方法
> 先看表格里的价格,再看备注。

6.1 HTML 静态网页(format: html)

适合交互演示、可视化、动画讲解这类用 Markdown 表达不出来的内容。App 用隔离的 WebKit 打开,打开即运行。

  • 单个 .html 文件,CSS 和 JS 写在文件里。不要拆出相对路径的 css、js 或图片,App 不会加载它们。
  • 可以从公共 CDN 引用库、字体和图片,但必须用 https://。CDN 资源由读者手机直接请求,不经过坚果云,也不占坚果云流量。发布工具会拒绝 http:// 资源。
  • 外部库固定版本;不依赖联网也应能看到核心结论。不要把私有报告数据发送给外部脚本或接口。发布校验只做静态检查,不证明 JS 运行正常,也不是完整安全审计;有交互时在浏览器验证操作、窄屏和资源加载,并区分浏览器测试与 Siftr 真机验证。
  • 不要依赖后端:没有服务器,没有登录,表单提交不会有响应。
  • 不要引用你的 WebDAV 地址:App 会拦截所有指向用户 WebDAV 主机的请求,发布工具也会拒绝。私有图片请改用公开地址,或者转成 data URI 内联。
  • 页面里的外链会在系统浏览器里打开,target=_blank 也一样。页内锚点(#step-2)照常可用。
  • 移动端优先:读者在手机上竖屏阅读。写上 <meta name="viewport" content="width=device-width, initial-scale=1">(漏写的话 App 会自动补上),布局要能适应 320–430pt 的宽度,可点控件不小于 44pt。
  • 支持深色模式更好:用 @media (prefers-color-scheme: dark)。App 不会改网页的样式。
  • 搜索:App 会提取页面里的可见文字参与全文搜索,JS 动态生成的文字搜不到。关键名词要写在 HTML 里,同时写进 summary 和 tags。
  • 标题写在元数据的 title 里,列表和阅读页都用它;页面里的 <title> 不会显示。
  • 讲解和演示分开时:讲解写成 Markdown,演示写成 HTML,分别用两个 id 发布。然后在 Markdown 里用 [打开演示](documents/<演示的 id>.html) 链接过去,App 会在站内打开。

7. 图片策略

坚果云流量配额宝贵,按优先级:

  1. 首选公开网络链接:![图表](https://example.com/chart.png),不消耗坚果云流量。
  2. 确需私有图片才上传 media/,且必须先压缩:长边 ≤ 1600px、JPEG 质量 70–80(或 WebP 80),单张目标 ≤ 300 KB。10 MiB 的协议上限只是兜底。
magick in.png -resize '1600x1600>' -quality 75 out.jpg                 # ImageMagick(v6 用 convert)
sips -Z 1600 -s format jpeg -s formatOptions 72 in.png --out out.jpg   # macOS 自带

私有图片用纯相对路径引用:![架构图](media/architecture.png)。App 用该来源的凭据认证加载并缓存。

  • media/ 只允许 png/jpg/jpeg/gif/webp;路径禁止 ..、%、空格与控制字符。发布工具会拒绝违规引用。
  • 图片不进 Manifest,App 按路径按需加载。
  • 缓存以路径为键:覆盖同名图片不会刷新已读设备。改图用新文件名(chart-v2.png)并发布正文修订,旧图保留。

8. 纪律与汇报

  • 发布前先 validate。脚本自带凭据泄露检查(Authorization、URL 内嵌账号密码、SIFTR_DAV_APP_PASSWORD= 等直接拒绝)。
  • 永不删除远端历史文件;永不覆盖已有正文路径。
  • 一个目录只有一个写入方(§1.3)。
  • 汇报只含:命令结果(published/unchanged/duplicate/错误类别)、文档 id、篇数、时间。不含凭据、账号、私有本地路径。

9. 排错速查

现象原因做法
认证失败 / 401用了登录密码,或应用密码已撤销让用户重新创建第三方应用密码
409 AncestorsNotFound父目录不存在MKCOL 后重试(Python 工具把 409 与 404 同样视为「还没有 Manifest」,首次发布会自动初始化目录)
429 / 5xx限流退避重试;仍失败就放弃,下次计划任务再试
远端 Manifest 格式错误;已停止发布远端 manifest.json 损坏或被他人改过不要覆盖。汇报给用户,由人决定如何恢复
Manifest 中存在重复 ID远端被手工改坏同上,不要自行覆盖
正文回读校验失败上传被截断/中途改写Manifest 未改动;直接重试
duplicate内容与别的 id 相同§1.2:不要重发
HTML 不能从 http:// 加载资源页面引用了明文资源改成 https:// 的地址
正文引用了内容空间所在的 WebDAV 主机正文里写了用户的坚果云地址删掉;私有图片改用公开地址或 data URI
不支持的 Manifest 协议版本远端是比工具更新的协议不要覆盖;更新 Skill 后再发布
读者 App 提示“需要更新版本”目录已升级到 v2,读者还在用 1.0.0让用户更新 Siftr
App 里没看到新内容读者需要手动下拉刷新;或用户没添加这个目录确认目录地址与用户在 App 里添加的一致

パブリッシャー Skill

Skill バージョン:2026 年 10 月 22 日

このページをあなたの AI エージェントに送れば、Siftr のコンテンツプロトコルに従って Markdown ブリーフィングをあなた自身の Nutstore WebDAV に公開できます。Skill にもパッケージにも資格情報は含まれず、アプリパスワードはエージェントの実行環境から注入されます。

インストール

パッケージは zip で、展開すると完全な siftr-publish/ フォルダ(SKILL.md と 2 つのスクリプト)が入ります。そのままエージェントの skills ディレクトリに置いてください。~/.agents/skills/、~/.zcode/skills/、~/.claude/skills/ などが一般的です。プラットフォームごとに異なるため、エージェントのドキュメントを確認してください。

下の一文をそのままエージェントに送ってください:

この Skill をインストールして: https://chenhk.cn/Siftr/assets/siftr-publish.zip をダウンロードして解凍し、できた siftr-publish/ フォルダをあなたの skills ディレクトリに置き、その SKILL.md の手順に従って Siftr コンテンツを公開してください。

パッケージをダウンロード(siftr-publish.zip) · 必要な 3 つの環境変数は本文 §2 を参照。

Skill 全文

以下はパッケージ内の SKILL.md と完全に同一です(scripts/package_skill.py が skills/siftr-publish/ から生成するため、乖離しません)。

Siftr 内容发布(面向 Agent)

0. 给收到这个链接的 Agent

把 Siftr 当成用户手机上的内容交付渠道:你负责生成内容并发布到用户自己的 WebDAV,用户在 Siftr 添加该来源后阅读。适合日报、周报、监控摘要、研究报告、旅行安排,以及单文件 HTML 图表或交互讲解。用户只发来本 Skill 链接时,先简短解释用途并检查是否已配置;链接本身不代表已经授权定时运行或上传测试内容。

  • 一次性报告:生成 Markdown,使用 §4 的元数据与 §5 的发布工具交付。
  • 定时任务产出:用所在平台的定时任务能力执行生成与发布。Siftr 不执行任务,也不保证定时推送通知。确认内容、频率、时间及 IANA 时区(如 Asia/Shanghai),并把发布步骤、Skill/脚本绝对路径、输出路径与凭据注入方式写入任务配置。没有调度能力时说明限制,保留可运行的发布命令,不要声称已设好定时任务。
  • HTML 网页:用于自包含的交互内容,按 §6.1 编写。它是 App 内阅读页面,不是公网网站托管;不要把私有 WebDAV 地址当成可分享的网页链接。用户要公开网站或后端服务时,需要另选托管方案。

0.1 首次使用的顺序

  1. 若只拿到网页说明,下载 完整 Skill 安装包,解压并阅读其中的 SKILL.md。脚本在解压后的 siftr-publish/scripts/;以下命令相对于该目录的父级 Skill 根目录执行。不要假设用户当前目录已有脚本;自动任务使用绝对路径。需要 Python 3.10+,无需 pip 包。
  2. 只检查 §2 的三个环境变量是否存在,不要打印值。未配置时按 §2.1 引导;先在本地准备和校验内容,不反复尝试联网发布。
  3. 已配置后运行 status。ready 表示远端索引可读;waiting_for_first_publish 表示尚无索引或父目录,不证明写权限正常。两者都不证明用户已连接 App,也无法判断 App 版本。
  4. 在已有发布授权下先 validate 再 publish。成功后告诉用户标题、文档 id 与发布结果,让用户在 Siftr 对应来源下拉刷新。只有收到回读确认才能说“已发布”;没有实际查看 App 时不要说“手机已经收到”。
  5. 定时任务必须在它自己的运行环境里取得凭据;当前终端配置不会自动传给后台任务。同一来源的所有任务串行执行,避免重叠。首次定时运行前,用同一执行环境验证一次。

0.2 每期内容与重试

日报使用 ai-daily-2026-10-22,周报使用 weekly-2026-w43;日期以用户时区和本次任务的逻辑周期为准。只有持续更新的同一份报告才用固定 id。重试保留本次正文、元数据、id 和时间戳,不要重新生成一篇或换 id。修订保留首次 published_at,只更新 updated_at。重复任务运行若不断刷新 updated_at,会造成不必要的索引更新。

保留本地产物直到回读确认。发布失败的处理按阶段区分:正文阶段失败时索引未更新;一旦尝试 PUT Manifest,超时或回读失败都可能发生在远端已经写入之后。此时用相同输入重新核对/发布,不要删除远端内容或宣称回滚。脚本每个网络请求最多尝试三次;任务平台也应限制重试次数并串行执行,不要无限重试。

你是 唯一的写入方。Siftr App 只读:它 GET manifest.json,对比后再 GET 需要的正文。App 不会替你总结、配图、推荐、朗读或追问——这些都不做。摘要、分类、标签、标题都由你写好,写进元数据和正文。正文可以是 Markdown 简报,也可以是单文件静态网页(HTML,见 §6.1)。

只想传任意文件到坚果云?看 §5 的 Shell 上传工具。想让 App 读到内容?先读 §1 的逻辑,再用 §5 的工具。

1. 先理解这套逻辑(决定你每次该怎么做)

1.1 两个标识,别混

标识含义谁决定
id这是哪一篇文章。跨修订保持不变(如 ai-daily-2026-10-22、weekly-2026-w43)你
content_sha256这是哪份字节。正文 UTF-8 字节的 SHA-256工具自动算

读者端的已读/收藏记录挂在 id 上;内容变化靠 content_sha256 判断。

1.2 发布前的决策表

你手里的情况怎么做工具返回
全新的一篇起一个新 id,publishpublished
修订已发布的文章(改错字、补充、更新)沿用原 id,改 Markdown,publishpublished(旧版本文件保留,Manifest 指向新文件)
与上次完全一样什么都不用做(再跑也没副作用)unchanged
新 id,但正文与某篇已发布的字节完全相同不要发。这是重复内容,工具直接拒绝写入duplicate(含 duplicate_of,即已存在的 id)
想用旧 id 发一篇完全不同的文章禁止。会覆盖该 id 的阅读状态;换新 id—

重复内容有两层防线:发布工具(duplicate,不写任何文件)和 App(读到相同 Markdown 只显示一份)。别靠 App 兜底——别制造重复。判断「是不是同一篇」用 id,不要用标题。

1.3 读者可以订阅多个 WebDAV 来源

用户可在 Siftr 里添加多个 WebDAV(例如两个坚果云目录、或坚果云加别家 WebDAV)。对你的影响:

  • 一个目录 = 一个来源 = 一份 manifest.json,一个目录只让一个发布方写。Manifest 是「读—改—写」,写前会检查变化,但没有原子锁,两个 Agent 同时写同一个目录会丢更新。想多 Agent,就一人一个目录,让用户在 App 里各添加一次。
  • 同一地址 App 只收一次(大小写、末尾斜杠、默认端口 443、「服务器路径+目录」怎么拆分都视为同一地址)。你不需要、也不应该帮用户在多个目录里重复发同一篇来「保险」。
  • 不同来源里内容完全相同的 Markdown,App 只显示一份(保留较早添加的来源那份,已读/收藏同步到两份上)。所以「同一篇发布到两个目录」没有意义,除非用户明确要求镜像备份。
  • 来源之间 id 互不冲突:甲目录的 daily 和乙目录的 daily 是两篇不同的文章,除非字节也相同。
  • 写入一个目录时只操作这个目录。不要在 SIFTR_DAV_URL 之外读写;不要把一个来源的凭据用到另一个来源。

1.4 读者端如何对待你的文件(决定了「不可变」规则)

  • 正文文件名含全文 SHA-256,一版一文件,永不改、永不删。修订就是新文件 + Manifest 改指新路径。App 要求同一路径下内容不可变,违反会被判定为损坏的 Manifest。
  • Manifest 永远最后写。读者随时可能刷新:正文先落盘并回读校验,Manifest 最后更新,读者看到的状态就总是一致的。
  • App 会把 media/ 图片按路径缓存,不会因为你覆盖同名图片而刷新(见 §7)。

2. 凭据与上传原语

运行环境通过各自的安全管理器注入三个环境变量:

变量含义示例
SIFTR_DAV_URL内容根目录完整 URL(HTTPS)https://dav.jianguoyun.com/dav/Siftr/
SIFTR_DAV_USER账号user@example.com
SIFTR_DAV_APP_PASSWORD第三方应用密码见下

坚果云 WebDAV 不接受登录密码。在网页版「账户信息 → 安全选项 → 第三方应用管理」为每个发布方单独创建应用密码(读者 App、每个 Agent 各一个,互不共用)。凭据只从环境变量读取,绝不写入 Markdown、元数据、仓库、日志、命令行字面值或你的回复。

2.1 用户尚未配置时(先完成这一步)

先说明:“Siftr 需要你自己的 WebDAV 空间来接收内容。你可以使用坚果云;我可以先准备报告,连接后再发布。密码请填写到本机或平台的安全配置入口,不要发到聊天里。”

  1. 引导用户到 坚果云官网 注册或登录,参照 官方第三方应用授权教程 创建应用密码。网页路径为“账户信息 → 安全选项 → 第三方应用管理 → 添加应用密码”。Agent 应先打开官方教程核对当前界面;界面或套餐限制变化时以官方帮助为准。
  2. 确定专用目录,例如 Siftr。坚果云服务器地址为 https://dav.jianguoyun.com/dav/,本工具使用的完整内容根目录为 https://dav.jianguoyun.com/dav/Siftr/。不要填官网网址、分享链接或 manifest.json 地址。初次配置优先使用单层 ASCII 目录;嵌套目录的上级需用户提前创建,工具不会越过内容根目录创建父级。
  3. 在 Agent 平台的秘密管理/环境变量设置里保存 §2 三项。用户填写账号与应用密码;不要使用登录密码、提交到代码库或写进定时任务提示词。平台没有安全输入入口时,由用户在自己的终端配置;不要要求把密码贴回对话。
  4. 在 Siftr 添加 WebDAV 来源:服务器 https://dav.jianguoyun.com/dav/、目录 Siftr,填写同一账号及为 App 单独生成的应用密码。Agent 和 App 的有效目录必须相同,不要重复拼上两次 Siftr。
  5. 运行 status,按结果处理认证、权限或空目录。获得发布授权后上传用户需要的第一篇内容,再请用户在 App 刷新确认;不要为了连接检查擅自留下测试文章。

其他提供商也使用 HTTPS、账号和应用密码(或其官方要求的 WebDAV 密码),以该服务官方文档为准。不要把坚果云特有的 409 行为推断为所有 WebDAV 服务的规则。

任意文件上传可用 §5 的 Shell 工具;它不更新内容索引,上传文件并不等于在 Siftr 发布了文章。

坚果云 WebDAV 行为速查(2026-10 实测)

场景实际返回
认证HTTPS + Basic(realm="nutstore"),HTTP/2
凭据错误401,空响应体
父目录不存在时 GET/PUT409 AncestorsNotFound——不是 404,先 MKCOL(注意:MKCOL 的 URL 不能带尾部斜杠,…/Siftr 成功、…/Siftr/ 返回 409;Python 工具已处理)
GET 目录403;列目录必须 PROPFIND
频率/流量超限429/5xx;指数退避,几次后放弃

3. 云端目录结构(Siftr 内容协议 v2)

$SIFTR_DAV_URL(如 https://dav.jianguoyun.com/dav/Siftr/)
├── manifest.json                      # 索引:App 的唯一入口
├── documents/
│   ├── <id前缀>_<内容SHA-256>.md       # format = markdown,不可变,一版一文件
│   └── <id前缀>_<内容SHA-256>.html     # format = html,同样不可变
└── media/                             # 可选:Markdown 引用的私有图片(App 认证加载)
    └── <名称>.png|.jpg|.jpeg|.gif|.webp

manifest.json 顶层:

字段类型约束
schema_versionint固定 2(读到 1 时 Python 工具会自动升级,见下)
generated_atstring带时区 ISO 8601;整份 ≤ 2 MiB
publisherstring?发布方标识,可选
documentsarray≤ 5000 条,按 published_at 倒序;id 不得重复

documents[] 每条:

字段类型约束
idstringASCII [A-Za-z0-9._-],≤ 200 字符,跨修订不变
formatstring必填:markdown 或 html(工具按正文后缀自动填写)
title / subtitlestring / string?标题非空且 ≤ 200 字符
summarystring≤ 1000 字符;App 列表和搜索都用它,认真写
categorystring非空;App 用它做筛选
tags[string]?可选;App 用它做筛选和搜索
published_at / updated_atstring带时区 ISO 8601(2026-10-13T09:00:00+08:00);published_at 决定排序与按月归档
pathstringdocuments/<id>_<sha256>.<md|html>,由工具生成;后缀必须与 format 一致
content_sha256string正文原始字节的 SHA-256(64 位小写十六进制)
size_bytesint真实字节数,单篇 ≤ 5 MiB(两种格式相同)
reading_minutes / source_countint?非负
format_optionsobject?仅供对应格式的阅读器使用;markdown、html 目前没有定义,不要写

硬性规则:正文是 documents/ 下非空的 UTF-8 文件,≤ 5 MiB;正文与链接里不得出现凭据;时间戳必须带时区。alternates 是协议保留字段,不要写。

版本与旧数据:v1(schema_version: 1,没有 format)只有 Markdown。Python 工具读到 v1 时,会给每条补上 "format": "markdown",下次发布时以 v2 写回(结果里带 upgraded_to_schema_version: 2)。Siftr 1.0.0 只认识 v1,看到 v2 会提示升级 App;所以在用户换成支持 v2 的版本之前,不要往他的目录发布。status 只能检查远端协议,不能判断用户安装的 App 版本;旧目录升级前让用户确认已更新 App。

兼容未来格式:Manifest 里出现你不认识的 format(比如以后的 audio),不要删也不要改,原样保留。Python 工具会自动这样处理。

4. 元数据 JSON(--metadata)

{
  "id": "ai-daily-2026-10-13",
  "title": "AI 每日简报:把信息读成理解",
  "summary": "三句话说清今天发生了什么、为什么重要。",
  "category": "AI 资讯",
  "tags": ["Agent", "模型"],
  "published_at": "2026-10-13T08:30:00+08:00",
  "updated_at": "2026-10-13T08:30:00+08:00",
  "reading_minutes": 3,
  "source_count": 5
}

必填:id、title、summary、category、published_at、updated_at。可选:subtitle、tags、reading_minutes、source_count、publisher。format、path、content_sha256、size_bytes 由工具填,不要手写;如果写了 format,必须和正文格式一致。修订时 published_at 保持首次发布时间,updated_at 写本次时间。

5. 发布路径:按运行环境任选

5.0 Python 3.10+(推荐)

scripts/siftr_publish.py(纯标准库,内置校验、幂等、重复检测、回读与退避,适合无人值守任务):

python3 scripts/siftr_publish.py validate --markdown out/daily.md --metadata out/daily.json   # 完全离线
python3 scripts/siftr_publish.py status                                                        # 远端概况:schema_version、篇数、各格式数量
python3 scripts/siftr_publish.py publish  --markdown out/daily.md --metadata out/daily.json
python3 scripts/siftr_publish.py publish  --html out/sort-demo.html --metadata out/sort-demo.json   # 单文件静态网页
python3 scripts/siftr_publish.py publish  --file out/anything.md --metadata out/meta.json           # 按后缀识别格式

5.1 从空目录验证一条完整链路(不联网)

以下示例创建 out/ 下的 Markdown、HTML 与对应元数据;若同名文件已存在则停止,避免覆盖用户产物。示例只演示格式,正式发布时改成用户实际需要的内容。运行目录为解压后的 siftr-publish/。

python3 - <<'PYTHON'
from pathlib import Path
from datetime import datetime, timezone
import json

out = Path("out")
out.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
samples = {
    "sample-report": ("md", "示例报告", "## 本次结果\n\n这里填写本次任务的结论与来源。\n"),
    "sample-page": ("html", "示例网页", '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>示例网页</title><body><main><h1>本次结果</h1><p>这里填写报告正文。</p></main></body></html>'),
}
files = {}
for doc_id, (suffix, title, text) in samples.items():
    files[out / (doc_id + "." + suffix)] = text
    files[out / (doc_id + ".json")] = json.dumps({
        "id": doc_id, "title": title, "summary": "演示发布格式。",
        "category": "示例", "published_at": stamp, "updated_at": stamp,
    }, ensure_ascii=False, indent=2)
if any(path.exists() for path in files):
    raise SystemExit("示例文件已存在;请复用或换一个工作目录。")
for path, text in files.items():
    path.write_text(text, encoding="utf-8")
PYTHON
python3 scripts/siftr_publish.py validate --markdown out/sample-report.md --metadata out/sample-report.json
python3 scripts/siftr_publish.py validate --html out/sample-page.html --metadata out/sample-page.json

用户授权发布实际内容后,将 validate 改为 publish。不要把这两篇示例自动发布到用户目录,也不要把示例 id 用作正式日报的固定 id。

publish 的 JSON 结果与你该做的事:

result含义你该做的
published新文章或修订已写入并回读确认汇报 id、篇数、generated_at
unchanged远端已是这份内容汇报「无变化」,不重试
duplicate另一个 id(duplicate_of)已有完全相同的正文,什么也没写不要换 id 重发。若本意是修订那一篇,用 duplicate_of 的 id 并修改内容;否则汇报「重复,已跳过」
非零退出 + 错误信息校验、认证、网络或回读失败见 §0.2、§9;Manifest 写入后的失败可能已生效,保留输入核对重试

只在明确授权的发布请求或既定计划任务里执行 publish。

5.2 纯 shell(没有 Python 的主机)

用 scripts/siftr_upload.sh(POSIX sh + curl + sha256sum 或 shasum,创建根目录及其下的父目录并做 SHA-256 回读)。这是低层上传器:会覆盖同名目标,不提供索引合并、去重或不可变保护;文章发布优先使用 Python。上传图片要使用全新文件名,不用此工具覆盖历史正文。每个请求有超时,失败由调用方检查后有限重试:

sh scripts/siftr_upload.sh out/daily_3f2a1b.md documents/daily_3f2a1b.md
sh scripts/siftr_upload.sh chart.jpg media/chart.jpg

manifest.json 由你按 §1、§3 手工编排:GET 旧 Manifest → 本地合并 → 上传正文并确认回读哈希一致 → 最后 PUT manifest.json → GET 复核。合并时务必自己做 Python 版做的事:

  1. 旧 Manifest 是 v1 时:每条补 "format": "markdown",顶层改为 "schema_version": 2。
  2. 同 id 替换旧条目(沿用 id,更新 format、path、content_sha256、size_bytes、updated_at)。同一个 id 从 Markdown 改成 HTML 也属于修订。
  3. 先查重:如果另一个 id 已有相同的 format + content_sha256,就停止,按 duplicate 处理。
  4. 不认识的 format 条目原样保留。
  5. 按 published_at 倒序,刷新 generated_at,Manifest ≤ 2 MiB、≤ 5000 条。

6. Markdown 写作规范(按 App 渲染器实际能力)

支持:h1–h6、粗/斜体、删除线、行内代码、有序/无序列表、任务列表(- [ ] / - [x])、引用块、提示块(引用块首行写 [!NOTE] 等)、表格(横向滚动)、代码块(显示语言名、可一键复制;代码横向滚动,text/markdown 或不标语言的块自动换行)、分隔线、可点击链接、图片(圆角,可点开放大)、脚注([^1],显示为上标数字,正文末尾汇总为「参考」)、LaTeX 公式($…$、$$…$$)。正文 18pt,随系统动态字号;读者可选黑体或宋体。

  • 分节用 ## 与 ###;不要用 #(与文章大标题重复;开头与标题相同的 # 会被 App 自动隐藏)。每个标题都会成为 App 里的目录条目,所以小节标题要简短、有信息、在同一篇里不重复。
  • 首段直接给结论,段落短小;关键引语放引用块,对比信息用表格,命令与代码放代码块并在开头围栏后标注语言(如 python、bash),App 会显示语言名。
  • 需要读者特别留意的一句话用 GitHub 提示块(示例见下),类型只有 NOTE(说明)、TIP(提示)、IMPORTANT(重要)、WARNING(注意)、CAUTION(警告);类型后面可以跟一个自定义标题。每篇最多两三个,滥用会失去重点。
  • 待办或核对清单用任务列表;已完成项会以勾选、弱化显示。
  • 图片独占一段(前后空行)时按整幅配图显示;![替代文字](地址 "图注") 中的引号部分显示为图注,没有图注时显示替代文字。
  • 链接锚文本要有意义,必须是 http(s);App 会拦截指向用户 WebDAV 主机的链接,所以不要链接到你自己的 WebDAV 地址。
  • 引用同一内容空间里的另一篇时写 [上一期](documents/<那篇的 id>.md)(例如 documents/ai-daily-2026-10-13.md);链接到网页就用 .html 后缀,比如 documents/sort-demo.html。App 会在内部直接打开那篇,修订后链接依然有效。该文章必须仍在 Manifest 里,否则读者会看到“这篇文章不在当前列表中”。
  • 段内不折行:一个段落写成一行,段与段之间空一行。段内的硬换行会被渲染成空格,中文里就成了多余的空隙。
  • 全角标点后不加空格:。、,、:、) 等后面直接接下一个字;中文与英文、数字之间是否加空格保持全文一致即可。
  • 写金额直接写 $20。只有整篇的 $ 都能两两配成公式($ 内侧不贴空格、右侧不紧跟数字)时 App 才渲染公式,所以公式和美元金额不要出现在同一篇里;需要时把金额写成「20 美元」或 USD 20。
  • Markdown 里不写 HTML(要做网页就用 §6.1 的 HTML 格式);不要在正文里放凭据。
  • App 支持全文搜索:把读者会搜的专有名词、产品名、人名写进正文(而不只写在图里)。
  • 不要写「相关推荐」「点此提问」「朗读」之类依赖 App 功能的话——这些功能不存在。
> [!TIP] 读这一节的方法
> 先看表格里的价格,再看备注。

6.1 HTML 静态网页(format: html)

适合交互演示、可视化、动画讲解这类用 Markdown 表达不出来的内容。App 用隔离的 WebKit 打开,打开即运行。

  • 单个 .html 文件,CSS 和 JS 写在文件里。不要拆出相对路径的 css、js 或图片,App 不会加载它们。
  • 可以从公共 CDN 引用库、字体和图片,但必须用 https://。CDN 资源由读者手机直接请求,不经过坚果云,也不占坚果云流量。发布工具会拒绝 http:// 资源。
  • 外部库固定版本;不依赖联网也应能看到核心结论。不要把私有报告数据发送给外部脚本或接口。发布校验只做静态检查,不证明 JS 运行正常,也不是完整安全审计;有交互时在浏览器验证操作、窄屏和资源加载,并区分浏览器测试与 Siftr 真机验证。
  • 不要依赖后端:没有服务器,没有登录,表单提交不会有响应。
  • 不要引用你的 WebDAV 地址:App 会拦截所有指向用户 WebDAV 主机的请求,发布工具也会拒绝。私有图片请改用公开地址,或者转成 data URI 内联。
  • 页面里的外链会在系统浏览器里打开,target=_blank 也一样。页内锚点(#step-2)照常可用。
  • 移动端优先:读者在手机上竖屏阅读。写上 <meta name="viewport" content="width=device-width, initial-scale=1">(漏写的话 App 会自动补上),布局要能适应 320–430pt 的宽度,可点控件不小于 44pt。
  • 支持深色模式更好:用 @media (prefers-color-scheme: dark)。App 不会改网页的样式。
  • 搜索:App 会提取页面里的可见文字参与全文搜索,JS 动态生成的文字搜不到。关键名词要写在 HTML 里,同时写进 summary 和 tags。
  • 标题写在元数据的 title 里,列表和阅读页都用它;页面里的 <title> 不会显示。
  • 讲解和演示分开时:讲解写成 Markdown,演示写成 HTML,分别用两个 id 发布。然后在 Markdown 里用 [打开演示](documents/<演示的 id>.html) 链接过去,App 会在站内打开。

7. 图片策略

坚果云流量配额宝贵,按优先级:

  1. 首选公开网络链接:![图表](https://example.com/chart.png),不消耗坚果云流量。
  2. 确需私有图片才上传 media/,且必须先压缩:长边 ≤ 1600px、JPEG 质量 70–80(或 WebP 80),单张目标 ≤ 300 KB。10 MiB 的协议上限只是兜底。
magick in.png -resize '1600x1600>' -quality 75 out.jpg                 # ImageMagick(v6 用 convert)
sips -Z 1600 -s format jpeg -s formatOptions 72 in.png --out out.jpg   # macOS 自带

私有图片用纯相对路径引用:![架构图](media/architecture.png)。App 用该来源的凭据认证加载并缓存。

  • media/ 只允许 png/jpg/jpeg/gif/webp;路径禁止 ..、%、空格与控制字符。发布工具会拒绝违规引用。
  • 图片不进 Manifest,App 按路径按需加载。
  • 缓存以路径为键:覆盖同名图片不会刷新已读设备。改图用新文件名(chart-v2.png)并发布正文修订,旧图保留。

8. 纪律与汇报

  • 发布前先 validate。脚本自带凭据泄露检查(Authorization、URL 内嵌账号密码、SIFTR_DAV_APP_PASSWORD= 等直接拒绝)。
  • 永不删除远端历史文件;永不覆盖已有正文路径。
  • 一个目录只有一个写入方(§1.3)。
  • 汇报只含:命令结果(published/unchanged/duplicate/错误类别)、文档 id、篇数、时间。不含凭据、账号、私有本地路径。

9. 排错速查

现象原因做法
认证失败 / 401用了登录密码,或应用密码已撤销让用户重新创建第三方应用密码
409 AncestorsNotFound父目录不存在MKCOL 后重试(Python 工具把 409 与 404 同样视为「还没有 Manifest」,首次发布会自动初始化目录)
429 / 5xx限流退避重试;仍失败就放弃,下次计划任务再试
远端 Manifest 格式错误;已停止发布远端 manifest.json 损坏或被他人改过不要覆盖。汇报给用户,由人决定如何恢复
Manifest 中存在重复 ID远端被手工改坏同上,不要自行覆盖
正文回读校验失败上传被截断/中途改写Manifest 未改动;直接重试
duplicate内容与别的 id 相同§1.2:不要重发
HTML 不能从 http:// 加载资源页面引用了明文资源改成 https:// 的地址
正文引用了内容空间所在的 WebDAV 主机正文里写了用户的坚果云地址删掉;私有图片改用公开地址或 data URI
不支持的 Manifest 协议版本远端是比工具更新的协议不要覆盖;更新 Skill 后再发布
读者 App 提示“需要更新版本”目录已升级到 v2,读者还在用 1.0.0让用户更新 Siftr
App 里没看到新内容读者需要手动下拉刷新;或用户没添加这个目录确认目录地址与用户在 App 里添加的一致

Publisher Skill

Skill version: October 22, 2026

Send this page to your AI agent and it can publish Markdown briefings to your own WebDAV cloud (Jianguoyun) in the exact layout the Siftr reader expects. Neither the skill nor the package contains credentials; the app password is injected by your agent's runtime.

Install

The package is a zip that unfolds into a complete siftr-publish/ folder (SKILL.md plus two scripts). Drop it into your agent's skills directory — commonly ~/.agents/skills/, ~/.zcode/skills/, or ~/.claude/skills/; check your agent's documentation.

Send this to your agent:

Install this skill: download https://chenhk.cn/Siftr/assets/siftr-publish.zip and unzip it, place the resulting siftr-publish/ folder into your skills directory, then follow its SKILL.md to publish Siftr content for me.

Download the package (siftr-publish.zip) · The three environment variables it needs are in §2 below.

Full skill text

Identical to the SKILL.md inside the package (generated from skills/siftr-publish/ by scripts/package_skill.py, so it never drifts).

The skill itself is written in Simplified Chinese; agents follow it as-is.

Siftr 内容发布(面向 Agent)

0. 给收到这个链接的 Agent

把 Siftr 当成用户手机上的内容交付渠道:你负责生成内容并发布到用户自己的 WebDAV,用户在 Siftr 添加该来源后阅读。适合日报、周报、监控摘要、研究报告、旅行安排,以及单文件 HTML 图表或交互讲解。用户只发来本 Skill 链接时,先简短解释用途并检查是否已配置;链接本身不代表已经授权定时运行或上传测试内容。

  • 一次性报告:生成 Markdown,使用 §4 的元数据与 §5 的发布工具交付。
  • 定时任务产出:用所在平台的定时任务能力执行生成与发布。Siftr 不执行任务,也不保证定时推送通知。确认内容、频率、时间及 IANA 时区(如 Asia/Shanghai),并把发布步骤、Skill/脚本绝对路径、输出路径与凭据注入方式写入任务配置。没有调度能力时说明限制,保留可运行的发布命令,不要声称已设好定时任务。
  • HTML 网页:用于自包含的交互内容,按 §6.1 编写。它是 App 内阅读页面,不是公网网站托管;不要把私有 WebDAV 地址当成可分享的网页链接。用户要公开网站或后端服务时,需要另选托管方案。

0.1 首次使用的顺序

  1. 若只拿到网页说明,下载 完整 Skill 安装包,解压并阅读其中的 SKILL.md。脚本在解压后的 siftr-publish/scripts/;以下命令相对于该目录的父级 Skill 根目录执行。不要假设用户当前目录已有脚本;自动任务使用绝对路径。需要 Python 3.10+,无需 pip 包。
  2. 只检查 §2 的三个环境变量是否存在,不要打印值。未配置时按 §2.1 引导;先在本地准备和校验内容,不反复尝试联网发布。
  3. 已配置后运行 status。ready 表示远端索引可读;waiting_for_first_publish 表示尚无索引或父目录,不证明写权限正常。两者都不证明用户已连接 App,也无法判断 App 版本。
  4. 在已有发布授权下先 validate 再 publish。成功后告诉用户标题、文档 id 与发布结果,让用户在 Siftr 对应来源下拉刷新。只有收到回读确认才能说“已发布”;没有实际查看 App 时不要说“手机已经收到”。
  5. 定时任务必须在它自己的运行环境里取得凭据;当前终端配置不会自动传给后台任务。同一来源的所有任务串行执行,避免重叠。首次定时运行前,用同一执行环境验证一次。

0.2 每期内容与重试

日报使用 ai-daily-2026-10-22,周报使用 weekly-2026-w43;日期以用户时区和本次任务的逻辑周期为准。只有持续更新的同一份报告才用固定 id。重试保留本次正文、元数据、id 和时间戳,不要重新生成一篇或换 id。修订保留首次 published_at,只更新 updated_at。重复任务运行若不断刷新 updated_at,会造成不必要的索引更新。

保留本地产物直到回读确认。发布失败的处理按阶段区分:正文阶段失败时索引未更新;一旦尝试 PUT Manifest,超时或回读失败都可能发生在远端已经写入之后。此时用相同输入重新核对/发布,不要删除远端内容或宣称回滚。脚本每个网络请求最多尝试三次;任务平台也应限制重试次数并串行执行,不要无限重试。

你是 唯一的写入方。Siftr App 只读:它 GET manifest.json,对比后再 GET 需要的正文。App 不会替你总结、配图、推荐、朗读或追问——这些都不做。摘要、分类、标签、标题都由你写好,写进元数据和正文。正文可以是 Markdown 简报,也可以是单文件静态网页(HTML,见 §6.1)。

只想传任意文件到坚果云?看 §5 的 Shell 上传工具。想让 App 读到内容?先读 §1 的逻辑,再用 §5 的工具。

1. 先理解这套逻辑(决定你每次该怎么做)

1.1 两个标识,别混

标识含义谁决定
id这是哪一篇文章。跨修订保持不变(如 ai-daily-2026-10-22、weekly-2026-w43)你
content_sha256这是哪份字节。正文 UTF-8 字节的 SHA-256工具自动算

读者端的已读/收藏记录挂在 id 上;内容变化靠 content_sha256 判断。

1.2 发布前的决策表

你手里的情况怎么做工具返回
全新的一篇起一个新 id,publishpublished
修订已发布的文章(改错字、补充、更新)沿用原 id,改 Markdown,publishpublished(旧版本文件保留,Manifest 指向新文件)
与上次完全一样什么都不用做(再跑也没副作用)unchanged
新 id,但正文与某篇已发布的字节完全相同不要发。这是重复内容,工具直接拒绝写入duplicate(含 duplicate_of,即已存在的 id)
想用旧 id 发一篇完全不同的文章禁止。会覆盖该 id 的阅读状态;换新 id—

重复内容有两层防线:发布工具(duplicate,不写任何文件)和 App(读到相同 Markdown 只显示一份)。别靠 App 兜底——别制造重复。判断「是不是同一篇」用 id,不要用标题。

1.3 读者可以订阅多个 WebDAV 来源

用户可在 Siftr 里添加多个 WebDAV(例如两个坚果云目录、或坚果云加别家 WebDAV)。对你的影响:

  • 一个目录 = 一个来源 = 一份 manifest.json,一个目录只让一个发布方写。Manifest 是「读—改—写」,写前会检查变化,但没有原子锁,两个 Agent 同时写同一个目录会丢更新。想多 Agent,就一人一个目录,让用户在 App 里各添加一次。
  • 同一地址 App 只收一次(大小写、末尾斜杠、默认端口 443、「服务器路径+目录」怎么拆分都视为同一地址)。你不需要、也不应该帮用户在多个目录里重复发同一篇来「保险」。
  • 不同来源里内容完全相同的 Markdown,App 只显示一份(保留较早添加的来源那份,已读/收藏同步到两份上)。所以「同一篇发布到两个目录」没有意义,除非用户明确要求镜像备份。
  • 来源之间 id 互不冲突:甲目录的 daily 和乙目录的 daily 是两篇不同的文章,除非字节也相同。
  • 写入一个目录时只操作这个目录。不要在 SIFTR_DAV_URL 之外读写;不要把一个来源的凭据用到另一个来源。

1.4 读者端如何对待你的文件(决定了「不可变」规则)

  • 正文文件名含全文 SHA-256,一版一文件,永不改、永不删。修订就是新文件 + Manifest 改指新路径。App 要求同一路径下内容不可变,违反会被判定为损坏的 Manifest。
  • Manifest 永远最后写。读者随时可能刷新:正文先落盘并回读校验,Manifest 最后更新,读者看到的状态就总是一致的。
  • App 会把 media/ 图片按路径缓存,不会因为你覆盖同名图片而刷新(见 §7)。

2. 凭据与上传原语

运行环境通过各自的安全管理器注入三个环境变量:

变量含义示例
SIFTR_DAV_URL内容根目录完整 URL(HTTPS)https://dav.jianguoyun.com/dav/Siftr/
SIFTR_DAV_USER账号user@example.com
SIFTR_DAV_APP_PASSWORD第三方应用密码见下

坚果云 WebDAV 不接受登录密码。在网页版「账户信息 → 安全选项 → 第三方应用管理」为每个发布方单独创建应用密码(读者 App、每个 Agent 各一个,互不共用)。凭据只从环境变量读取,绝不写入 Markdown、元数据、仓库、日志、命令行字面值或你的回复。

2.1 用户尚未配置时(先完成这一步)

先说明:“Siftr 需要你自己的 WebDAV 空间来接收内容。你可以使用坚果云;我可以先准备报告,连接后再发布。密码请填写到本机或平台的安全配置入口,不要发到聊天里。”

  1. 引导用户到 坚果云官网 注册或登录,参照 官方第三方应用授权教程 创建应用密码。网页路径为“账户信息 → 安全选项 → 第三方应用管理 → 添加应用密码”。Agent 应先打开官方教程核对当前界面;界面或套餐限制变化时以官方帮助为准。
  2. 确定专用目录,例如 Siftr。坚果云服务器地址为 https://dav.jianguoyun.com/dav/,本工具使用的完整内容根目录为 https://dav.jianguoyun.com/dav/Siftr/。不要填官网网址、分享链接或 manifest.json 地址。初次配置优先使用单层 ASCII 目录;嵌套目录的上级需用户提前创建,工具不会越过内容根目录创建父级。
  3. 在 Agent 平台的秘密管理/环境变量设置里保存 §2 三项。用户填写账号与应用密码;不要使用登录密码、提交到代码库或写进定时任务提示词。平台没有安全输入入口时,由用户在自己的终端配置;不要要求把密码贴回对话。
  4. 在 Siftr 添加 WebDAV 来源:服务器 https://dav.jianguoyun.com/dav/、目录 Siftr,填写同一账号及为 App 单独生成的应用密码。Agent 和 App 的有效目录必须相同,不要重复拼上两次 Siftr。
  5. 运行 status,按结果处理认证、权限或空目录。获得发布授权后上传用户需要的第一篇内容,再请用户在 App 刷新确认;不要为了连接检查擅自留下测试文章。

其他提供商也使用 HTTPS、账号和应用密码(或其官方要求的 WebDAV 密码),以该服务官方文档为准。不要把坚果云特有的 409 行为推断为所有 WebDAV 服务的规则。

任意文件上传可用 §5 的 Shell 工具;它不更新内容索引,上传文件并不等于在 Siftr 发布了文章。

坚果云 WebDAV 行为速查(2026-10 实测)

场景实际返回
认证HTTPS + Basic(realm="nutstore"),HTTP/2
凭据错误401,空响应体
父目录不存在时 GET/PUT409 AncestorsNotFound——不是 404,先 MKCOL(注意:MKCOL 的 URL 不能带尾部斜杠,…/Siftr 成功、…/Siftr/ 返回 409;Python 工具已处理)
GET 目录403;列目录必须 PROPFIND
频率/流量超限429/5xx;指数退避,几次后放弃

3. 云端目录结构(Siftr 内容协议 v2)

$SIFTR_DAV_URL(如 https://dav.jianguoyun.com/dav/Siftr/)
├── manifest.json                      # 索引:App 的唯一入口
├── documents/
│   ├── <id前缀>_<内容SHA-256>.md       # format = markdown,不可变,一版一文件
│   └── <id前缀>_<内容SHA-256>.html     # format = html,同样不可变
└── media/                             # 可选:Markdown 引用的私有图片(App 认证加载)
    └── <名称>.png|.jpg|.jpeg|.gif|.webp

manifest.json 顶层:

字段类型约束
schema_versionint固定 2(读到 1 时 Python 工具会自动升级,见下)
generated_atstring带时区 ISO 8601;整份 ≤ 2 MiB
publisherstring?发布方标识,可选
documentsarray≤ 5000 条,按 published_at 倒序;id 不得重复

documents[] 每条:

字段类型约束
idstringASCII [A-Za-z0-9._-],≤ 200 字符,跨修订不变
formatstring必填:markdown 或 html(工具按正文后缀自动填写)
title / subtitlestring / string?标题非空且 ≤ 200 字符
summarystring≤ 1000 字符;App 列表和搜索都用它,认真写
categorystring非空;App 用它做筛选
tags[string]?可选;App 用它做筛选和搜索
published_at / updated_atstring带时区 ISO 8601(2026-10-13T09:00:00+08:00);published_at 决定排序与按月归档
pathstringdocuments/<id>_<sha256>.<md|html>,由工具生成;后缀必须与 format 一致
content_sha256string正文原始字节的 SHA-256(64 位小写十六进制)
size_bytesint真实字节数,单篇 ≤ 5 MiB(两种格式相同)
reading_minutes / source_countint?非负
format_optionsobject?仅供对应格式的阅读器使用;markdown、html 目前没有定义,不要写

硬性规则:正文是 documents/ 下非空的 UTF-8 文件,≤ 5 MiB;正文与链接里不得出现凭据;时间戳必须带时区。alternates 是协议保留字段,不要写。

版本与旧数据:v1(schema_version: 1,没有 format)只有 Markdown。Python 工具读到 v1 时,会给每条补上 "format": "markdown",下次发布时以 v2 写回(结果里带 upgraded_to_schema_version: 2)。Siftr 1.0.0 只认识 v1,看到 v2 会提示升级 App;所以在用户换成支持 v2 的版本之前,不要往他的目录发布。status 只能检查远端协议,不能判断用户安装的 App 版本;旧目录升级前让用户确认已更新 App。

兼容未来格式:Manifest 里出现你不认识的 format(比如以后的 audio),不要删也不要改,原样保留。Python 工具会自动这样处理。

4. 元数据 JSON(--metadata)

{
  "id": "ai-daily-2026-10-13",
  "title": "AI 每日简报:把信息读成理解",
  "summary": "三句话说清今天发生了什么、为什么重要。",
  "category": "AI 资讯",
  "tags": ["Agent", "模型"],
  "published_at": "2026-10-13T08:30:00+08:00",
  "updated_at": "2026-10-13T08:30:00+08:00",
  "reading_minutes": 3,
  "source_count": 5
}

必填:id、title、summary、category、published_at、updated_at。可选:subtitle、tags、reading_minutes、source_count、publisher。format、path、content_sha256、size_bytes 由工具填,不要手写;如果写了 format,必须和正文格式一致。修订时 published_at 保持首次发布时间,updated_at 写本次时间。

5. 发布路径:按运行环境任选

5.0 Python 3.10+(推荐)

scripts/siftr_publish.py(纯标准库,内置校验、幂等、重复检测、回读与退避,适合无人值守任务):

python3 scripts/siftr_publish.py validate --markdown out/daily.md --metadata out/daily.json   # 完全离线
python3 scripts/siftr_publish.py status                                                        # 远端概况:schema_version、篇数、各格式数量
python3 scripts/siftr_publish.py publish  --markdown out/daily.md --metadata out/daily.json
python3 scripts/siftr_publish.py publish  --html out/sort-demo.html --metadata out/sort-demo.json   # 单文件静态网页
python3 scripts/siftr_publish.py publish  --file out/anything.md --metadata out/meta.json           # 按后缀识别格式

5.1 从空目录验证一条完整链路(不联网)

以下示例创建 out/ 下的 Markdown、HTML 与对应元数据;若同名文件已存在则停止,避免覆盖用户产物。示例只演示格式,正式发布时改成用户实际需要的内容。运行目录为解压后的 siftr-publish/。

python3 - <<'PYTHON'
from pathlib import Path
from datetime import datetime, timezone
import json

out = Path("out")
out.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
samples = {
    "sample-report": ("md", "示例报告", "## 本次结果\n\n这里填写本次任务的结论与来源。\n"),
    "sample-page": ("html", "示例网页", '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>示例网页</title><body><main><h1>本次结果</h1><p>这里填写报告正文。</p></main></body></html>'),
}
files = {}
for doc_id, (suffix, title, text) in samples.items():
    files[out / (doc_id + "." + suffix)] = text
    files[out / (doc_id + ".json")] = json.dumps({
        "id": doc_id, "title": title, "summary": "演示发布格式。",
        "category": "示例", "published_at": stamp, "updated_at": stamp,
    }, ensure_ascii=False, indent=2)
if any(path.exists() for path in files):
    raise SystemExit("示例文件已存在;请复用或换一个工作目录。")
for path, text in files.items():
    path.write_text(text, encoding="utf-8")
PYTHON
python3 scripts/siftr_publish.py validate --markdown out/sample-report.md --metadata out/sample-report.json
python3 scripts/siftr_publish.py validate --html out/sample-page.html --metadata out/sample-page.json

用户授权发布实际内容后,将 validate 改为 publish。不要把这两篇示例自动发布到用户目录,也不要把示例 id 用作正式日报的固定 id。

publish 的 JSON 结果与你该做的事:

result含义你该做的
published新文章或修订已写入并回读确认汇报 id、篇数、generated_at
unchanged远端已是这份内容汇报「无变化」,不重试
duplicate另一个 id(duplicate_of)已有完全相同的正文,什么也没写不要换 id 重发。若本意是修订那一篇,用 duplicate_of 的 id 并修改内容;否则汇报「重复,已跳过」
非零退出 + 错误信息校验、认证、网络或回读失败见 §0.2、§9;Manifest 写入后的失败可能已生效,保留输入核对重试

只在明确授权的发布请求或既定计划任务里执行 publish。

5.2 纯 shell(没有 Python 的主机)

用 scripts/siftr_upload.sh(POSIX sh + curl + sha256sum 或 shasum,创建根目录及其下的父目录并做 SHA-256 回读)。这是低层上传器:会覆盖同名目标,不提供索引合并、去重或不可变保护;文章发布优先使用 Python。上传图片要使用全新文件名,不用此工具覆盖历史正文。每个请求有超时,失败由调用方检查后有限重试:

sh scripts/siftr_upload.sh out/daily_3f2a1b.md documents/daily_3f2a1b.md
sh scripts/siftr_upload.sh chart.jpg media/chart.jpg

manifest.json 由你按 §1、§3 手工编排:GET 旧 Manifest → 本地合并 → 上传正文并确认回读哈希一致 → 最后 PUT manifest.json → GET 复核。合并时务必自己做 Python 版做的事:

  1. 旧 Manifest 是 v1 时:每条补 "format": "markdown",顶层改为 "schema_version": 2。
  2. 同 id 替换旧条目(沿用 id,更新 format、path、content_sha256、size_bytes、updated_at)。同一个 id 从 Markdown 改成 HTML 也属于修订。
  3. 先查重:如果另一个 id 已有相同的 format + content_sha256,就停止,按 duplicate 处理。
  4. 不认识的 format 条目原样保留。
  5. 按 published_at 倒序,刷新 generated_at,Manifest ≤ 2 MiB、≤ 5000 条。

6. Markdown 写作规范(按 App 渲染器实际能力)

支持:h1–h6、粗/斜体、删除线、行内代码、有序/无序列表、任务列表(- [ ] / - [x])、引用块、提示块(引用块首行写 [!NOTE] 等)、表格(横向滚动)、代码块(显示语言名、可一键复制;代码横向滚动,text/markdown 或不标语言的块自动换行)、分隔线、可点击链接、图片(圆角,可点开放大)、脚注([^1],显示为上标数字,正文末尾汇总为「参考」)、LaTeX 公式($…$、$$…$$)。正文 18pt,随系统动态字号;读者可选黑体或宋体。

  • 分节用 ## 与 ###;不要用 #(与文章大标题重复;开头与标题相同的 # 会被 App 自动隐藏)。每个标题都会成为 App 里的目录条目,所以小节标题要简短、有信息、在同一篇里不重复。
  • 首段直接给结论,段落短小;关键引语放引用块,对比信息用表格,命令与代码放代码块并在开头围栏后标注语言(如 python、bash),App 会显示语言名。
  • 需要读者特别留意的一句话用 GitHub 提示块(示例见下),类型只有 NOTE(说明)、TIP(提示)、IMPORTANT(重要)、WARNING(注意)、CAUTION(警告);类型后面可以跟一个自定义标题。每篇最多两三个,滥用会失去重点。
  • 待办或核对清单用任务列表;已完成项会以勾选、弱化显示。
  • 图片独占一段(前后空行)时按整幅配图显示;![替代文字](地址 "图注") 中的引号部分显示为图注,没有图注时显示替代文字。
  • 链接锚文本要有意义,必须是 http(s);App 会拦截指向用户 WebDAV 主机的链接,所以不要链接到你自己的 WebDAV 地址。
  • 引用同一内容空间里的另一篇时写 [上一期](documents/<那篇的 id>.md)(例如 documents/ai-daily-2026-10-13.md);链接到网页就用 .html 后缀,比如 documents/sort-demo.html。App 会在内部直接打开那篇,修订后链接依然有效。该文章必须仍在 Manifest 里,否则读者会看到“这篇文章不在当前列表中”。
  • 段内不折行:一个段落写成一行,段与段之间空一行。段内的硬换行会被渲染成空格,中文里就成了多余的空隙。
  • 全角标点后不加空格:。、,、:、) 等后面直接接下一个字;中文与英文、数字之间是否加空格保持全文一致即可。
  • 写金额直接写 $20。只有整篇的 $ 都能两两配成公式($ 内侧不贴空格、右侧不紧跟数字)时 App 才渲染公式,所以公式和美元金额不要出现在同一篇里;需要时把金额写成「20 美元」或 USD 20。
  • Markdown 里不写 HTML(要做网页就用 §6.1 的 HTML 格式);不要在正文里放凭据。
  • App 支持全文搜索:把读者会搜的专有名词、产品名、人名写进正文(而不只写在图里)。
  • 不要写「相关推荐」「点此提问」「朗读」之类依赖 App 功能的话——这些功能不存在。
> [!TIP] 读这一节的方法
> 先看表格里的价格,再看备注。

6.1 HTML 静态网页(format: html)

适合交互演示、可视化、动画讲解这类用 Markdown 表达不出来的内容。App 用隔离的 WebKit 打开,打开即运行。

  • 单个 .html 文件,CSS 和 JS 写在文件里。不要拆出相对路径的 css、js 或图片,App 不会加载它们。
  • 可以从公共 CDN 引用库、字体和图片,但必须用 https://。CDN 资源由读者手机直接请求,不经过坚果云,也不占坚果云流量。发布工具会拒绝 http:// 资源。
  • 外部库固定版本;不依赖联网也应能看到核心结论。不要把私有报告数据发送给外部脚本或接口。发布校验只做静态检查,不证明 JS 运行正常,也不是完整安全审计;有交互时在浏览器验证操作、窄屏和资源加载,并区分浏览器测试与 Siftr 真机验证。
  • 不要依赖后端:没有服务器,没有登录,表单提交不会有响应。
  • 不要引用你的 WebDAV 地址:App 会拦截所有指向用户 WebDAV 主机的请求,发布工具也会拒绝。私有图片请改用公开地址,或者转成 data URI 内联。
  • 页面里的外链会在系统浏览器里打开,target=_blank 也一样。页内锚点(#step-2)照常可用。
  • 移动端优先:读者在手机上竖屏阅读。写上 <meta name="viewport" content="width=device-width, initial-scale=1">(漏写的话 App 会自动补上),布局要能适应 320–430pt 的宽度,可点控件不小于 44pt。
  • 支持深色模式更好:用 @media (prefers-color-scheme: dark)。App 不会改网页的样式。
  • 搜索:App 会提取页面里的可见文字参与全文搜索,JS 动态生成的文字搜不到。关键名词要写在 HTML 里,同时写进 summary 和 tags。
  • 标题写在元数据的 title 里,列表和阅读页都用它;页面里的 <title> 不会显示。
  • 讲解和演示分开时:讲解写成 Markdown,演示写成 HTML,分别用两个 id 发布。然后在 Markdown 里用 [打开演示](documents/<演示的 id>.html) 链接过去,App 会在站内打开。

7. 图片策略

坚果云流量配额宝贵,按优先级:

  1. 首选公开网络链接:![图表](https://example.com/chart.png),不消耗坚果云流量。
  2. 确需私有图片才上传 media/,且必须先压缩:长边 ≤ 1600px、JPEG 质量 70–80(或 WebP 80),单张目标 ≤ 300 KB。10 MiB 的协议上限只是兜底。
magick in.png -resize '1600x1600>' -quality 75 out.jpg                 # ImageMagick(v6 用 convert)
sips -Z 1600 -s format jpeg -s formatOptions 72 in.png --out out.jpg   # macOS 自带

私有图片用纯相对路径引用:![架构图](media/architecture.png)。App 用该来源的凭据认证加载并缓存。

  • media/ 只允许 png/jpg/jpeg/gif/webp;路径禁止 ..、%、空格与控制字符。发布工具会拒绝违规引用。
  • 图片不进 Manifest,App 按路径按需加载。
  • 缓存以路径为键:覆盖同名图片不会刷新已读设备。改图用新文件名(chart-v2.png)并发布正文修订,旧图保留。

8. 纪律与汇报

  • 发布前先 validate。脚本自带凭据泄露检查(Authorization、URL 内嵌账号密码、SIFTR_DAV_APP_PASSWORD= 等直接拒绝)。
  • 永不删除远端历史文件;永不覆盖已有正文路径。
  • 一个目录只有一个写入方(§1.3)。
  • 汇报只含:命令结果(published/unchanged/duplicate/错误类别)、文档 id、篇数、时间。不含凭据、账号、私有本地路径。

9. 排错速查

现象原因做法
认证失败 / 401用了登录密码,或应用密码已撤销让用户重新创建第三方应用密码
409 AncestorsNotFound父目录不存在MKCOL 后重试(Python 工具把 409 与 404 同样视为「还没有 Manifest」,首次发布会自动初始化目录)
429 / 5xx限流退避重试;仍失败就放弃,下次计划任务再试
远端 Manifest 格式错误;已停止发布远端 manifest.json 损坏或被他人改过不要覆盖。汇报给用户,由人决定如何恢复
Manifest 中存在重复 ID远端被手工改坏同上,不要自行覆盖
正文回读校验失败上传被截断/中途改写Manifest 未改动;直接重试
duplicate内容与别的 id 相同§1.2:不要重发
HTML 不能从 http:// 加载资源页面引用了明文资源改成 https:// 的地址
正文引用了内容空间所在的 WebDAV 主机正文里写了用户的坚果云地址删掉;私有图片改用公开地址或 data URI
不支持的 Manifest 协议版本远端是比工具更新的协议不要覆盖;更新 Skill 后再发布
读者 App 提示“需要更新版本”目录已升级到 v2,读者还在用 1.0.0让用户更新 Siftr
App 里没看到新内容读者需要手动下拉刷新;或用户没添加这个目录确认目录地址与用户在 App 里添加的一致

Skill публикации

Версия skill: 22 октября 2026 г.

Отправьте эту страницу своему ИИ-агенту — и он сможет публиковать Markdown-брифинги в ваше собственное облако WebDAV (Nutstore) точно в том формате, которого ожидает Siftr. Ни в skill, ни в пакете нет учётных данных; пароль приложения внедряется средой агента.

Установка

Пакет — это zip, разворачивающийся в полную папку siftr-publish/ (SKILL.md и два скрипта). Положите её в каталог skills вашего агента — обычно ~/.agents/skills/, ~/.zcode/skills/ или ~/.claude/skills/; точное место подскажет документация агента.

Просто отправьте агенту эту фразу:

Установи этот skill: скачай https://chenhk.cn/Siftr/assets/siftr-publish.zip и распакуй, полученную папку siftr-publish/ помести в свой каталог skills и следуй её SKILL.md, чтобы публиковать для меня контент Siftr.

Скачать пакет (siftr-publish.zip) · Три нужные переменные окружения — в §2 ниже.

Полный текст skill

Идентичен SKILL.md внутри пакета (генерируется из skills/siftr-publish/ скриптом scripts/package_skill.py, поэтому не расходится).

Siftr 内容发布(面向 Agent)

0. 给收到这个链接的 Agent

把 Siftr 当成用户手机上的内容交付渠道:你负责生成内容并发布到用户自己的 WebDAV,用户在 Siftr 添加该来源后阅读。适合日报、周报、监控摘要、研究报告、旅行安排,以及单文件 HTML 图表或交互讲解。用户只发来本 Skill 链接时,先简短解释用途并检查是否已配置;链接本身不代表已经授权定时运行或上传测试内容。

  • 一次性报告:生成 Markdown,使用 §4 的元数据与 §5 的发布工具交付。
  • 定时任务产出:用所在平台的定时任务能力执行生成与发布。Siftr 不执行任务,也不保证定时推送通知。确认内容、频率、时间及 IANA 时区(如 Asia/Shanghai),并把发布步骤、Skill/脚本绝对路径、输出路径与凭据注入方式写入任务配置。没有调度能力时说明限制,保留可运行的发布命令,不要声称已设好定时任务。
  • HTML 网页:用于自包含的交互内容,按 §6.1 编写。它是 App 内阅读页面,不是公网网站托管;不要把私有 WebDAV 地址当成可分享的网页链接。用户要公开网站或后端服务时,需要另选托管方案。

0.1 首次使用的顺序

  1. 若只拿到网页说明,下载 完整 Skill 安装包,解压并阅读其中的 SKILL.md。脚本在解压后的 siftr-publish/scripts/;以下命令相对于该目录的父级 Skill 根目录执行。不要假设用户当前目录已有脚本;自动任务使用绝对路径。需要 Python 3.10+,无需 pip 包。
  2. 只检查 §2 的三个环境变量是否存在,不要打印值。未配置时按 §2.1 引导;先在本地准备和校验内容,不反复尝试联网发布。
  3. 已配置后运行 status。ready 表示远端索引可读;waiting_for_first_publish 表示尚无索引或父目录,不证明写权限正常。两者都不证明用户已连接 App,也无法判断 App 版本。
  4. 在已有发布授权下先 validate 再 publish。成功后告诉用户标题、文档 id 与发布结果,让用户在 Siftr 对应来源下拉刷新。只有收到回读确认才能说“已发布”;没有实际查看 App 时不要说“手机已经收到”。
  5. 定时任务必须在它自己的运行环境里取得凭据;当前终端配置不会自动传给后台任务。同一来源的所有任务串行执行,避免重叠。首次定时运行前,用同一执行环境验证一次。

0.2 每期内容与重试

日报使用 ai-daily-2026-10-22,周报使用 weekly-2026-w43;日期以用户时区和本次任务的逻辑周期为准。只有持续更新的同一份报告才用固定 id。重试保留本次正文、元数据、id 和时间戳,不要重新生成一篇或换 id。修订保留首次 published_at,只更新 updated_at。重复任务运行若不断刷新 updated_at,会造成不必要的索引更新。

保留本地产物直到回读确认。发布失败的处理按阶段区分:正文阶段失败时索引未更新;一旦尝试 PUT Manifest,超时或回读失败都可能发生在远端已经写入之后。此时用相同输入重新核对/发布,不要删除远端内容或宣称回滚。脚本每个网络请求最多尝试三次;任务平台也应限制重试次数并串行执行,不要无限重试。

你是 唯一的写入方。Siftr App 只读:它 GET manifest.json,对比后再 GET 需要的正文。App 不会替你总结、配图、推荐、朗读或追问——这些都不做。摘要、分类、标签、标题都由你写好,写进元数据和正文。正文可以是 Markdown 简报,也可以是单文件静态网页(HTML,见 §6.1)。

只想传任意文件到坚果云?看 §5 的 Shell 上传工具。想让 App 读到内容?先读 §1 的逻辑,再用 §5 的工具。

1. 先理解这套逻辑(决定你每次该怎么做)

1.1 两个标识,别混

标识含义谁决定
id这是哪一篇文章。跨修订保持不变(如 ai-daily-2026-10-22、weekly-2026-w43)你
content_sha256这是哪份字节。正文 UTF-8 字节的 SHA-256工具自动算

读者端的已读/收藏记录挂在 id 上;内容变化靠 content_sha256 判断。

1.2 发布前的决策表

你手里的情况怎么做工具返回
全新的一篇起一个新 id,publishpublished
修订已发布的文章(改错字、补充、更新)沿用原 id,改 Markdown,publishpublished(旧版本文件保留,Manifest 指向新文件)
与上次完全一样什么都不用做(再跑也没副作用)unchanged
新 id,但正文与某篇已发布的字节完全相同不要发。这是重复内容,工具直接拒绝写入duplicate(含 duplicate_of,即已存在的 id)
想用旧 id 发一篇完全不同的文章禁止。会覆盖该 id 的阅读状态;换新 id—

重复内容有两层防线:发布工具(duplicate,不写任何文件)和 App(读到相同 Markdown 只显示一份)。别靠 App 兜底——别制造重复。判断「是不是同一篇」用 id,不要用标题。

1.3 读者可以订阅多个 WebDAV 来源

用户可在 Siftr 里添加多个 WebDAV(例如两个坚果云目录、或坚果云加别家 WebDAV)。对你的影响:

  • 一个目录 = 一个来源 = 一份 manifest.json,一个目录只让一个发布方写。Manifest 是「读—改—写」,写前会检查变化,但没有原子锁,两个 Agent 同时写同一个目录会丢更新。想多 Agent,就一人一个目录,让用户在 App 里各添加一次。
  • 同一地址 App 只收一次(大小写、末尾斜杠、默认端口 443、「服务器路径+目录」怎么拆分都视为同一地址)。你不需要、也不应该帮用户在多个目录里重复发同一篇来「保险」。
  • 不同来源里内容完全相同的 Markdown,App 只显示一份(保留较早添加的来源那份,已读/收藏同步到两份上)。所以「同一篇发布到两个目录」没有意义,除非用户明确要求镜像备份。
  • 来源之间 id 互不冲突:甲目录的 daily 和乙目录的 daily 是两篇不同的文章,除非字节也相同。
  • 写入一个目录时只操作这个目录。不要在 SIFTR_DAV_URL 之外读写;不要把一个来源的凭据用到另一个来源。

1.4 读者端如何对待你的文件(决定了「不可变」规则)

  • 正文文件名含全文 SHA-256,一版一文件,永不改、永不删。修订就是新文件 + Manifest 改指新路径。App 要求同一路径下内容不可变,违反会被判定为损坏的 Manifest。
  • Manifest 永远最后写。读者随时可能刷新:正文先落盘并回读校验,Manifest 最后更新,读者看到的状态就总是一致的。
  • App 会把 media/ 图片按路径缓存,不会因为你覆盖同名图片而刷新(见 §7)。

2. 凭据与上传原语

运行环境通过各自的安全管理器注入三个环境变量:

变量含义示例
SIFTR_DAV_URL内容根目录完整 URL(HTTPS)https://dav.jianguoyun.com/dav/Siftr/
SIFTR_DAV_USER账号user@example.com
SIFTR_DAV_APP_PASSWORD第三方应用密码见下

坚果云 WebDAV 不接受登录密码。在网页版「账户信息 → 安全选项 → 第三方应用管理」为每个发布方单独创建应用密码(读者 App、每个 Agent 各一个,互不共用)。凭据只从环境变量读取,绝不写入 Markdown、元数据、仓库、日志、命令行字面值或你的回复。

2.1 用户尚未配置时(先完成这一步)

先说明:“Siftr 需要你自己的 WebDAV 空间来接收内容。你可以使用坚果云;我可以先准备报告,连接后再发布。密码请填写到本机或平台的安全配置入口,不要发到聊天里。”

  1. 引导用户到 坚果云官网 注册或登录,参照 官方第三方应用授权教程 创建应用密码。网页路径为“账户信息 → 安全选项 → 第三方应用管理 → 添加应用密码”。Agent 应先打开官方教程核对当前界面;界面或套餐限制变化时以官方帮助为准。
  2. 确定专用目录,例如 Siftr。坚果云服务器地址为 https://dav.jianguoyun.com/dav/,本工具使用的完整内容根目录为 https://dav.jianguoyun.com/dav/Siftr/。不要填官网网址、分享链接或 manifest.json 地址。初次配置优先使用单层 ASCII 目录;嵌套目录的上级需用户提前创建,工具不会越过内容根目录创建父级。
  3. 在 Agent 平台的秘密管理/环境变量设置里保存 §2 三项。用户填写账号与应用密码;不要使用登录密码、提交到代码库或写进定时任务提示词。平台没有安全输入入口时,由用户在自己的终端配置;不要要求把密码贴回对话。
  4. 在 Siftr 添加 WebDAV 来源:服务器 https://dav.jianguoyun.com/dav/、目录 Siftr,填写同一账号及为 App 单独生成的应用密码。Agent 和 App 的有效目录必须相同,不要重复拼上两次 Siftr。
  5. 运行 status,按结果处理认证、权限或空目录。获得发布授权后上传用户需要的第一篇内容,再请用户在 App 刷新确认;不要为了连接检查擅自留下测试文章。

其他提供商也使用 HTTPS、账号和应用密码(或其官方要求的 WebDAV 密码),以该服务官方文档为准。不要把坚果云特有的 409 行为推断为所有 WebDAV 服务的规则。

任意文件上传可用 §5 的 Shell 工具;它不更新内容索引,上传文件并不等于在 Siftr 发布了文章。

坚果云 WebDAV 行为速查(2026-10 实测)

场景实际返回
认证HTTPS + Basic(realm="nutstore"),HTTP/2
凭据错误401,空响应体
父目录不存在时 GET/PUT409 AncestorsNotFound——不是 404,先 MKCOL(注意:MKCOL 的 URL 不能带尾部斜杠,…/Siftr 成功、…/Siftr/ 返回 409;Python 工具已处理)
GET 目录403;列目录必须 PROPFIND
频率/流量超限429/5xx;指数退避,几次后放弃

3. 云端目录结构(Siftr 内容协议 v2)

$SIFTR_DAV_URL(如 https://dav.jianguoyun.com/dav/Siftr/)
├── manifest.json                      # 索引:App 的唯一入口
├── documents/
│   ├── <id前缀>_<内容SHA-256>.md       # format = markdown,不可变,一版一文件
│   └── <id前缀>_<内容SHA-256>.html     # format = html,同样不可变
└── media/                             # 可选:Markdown 引用的私有图片(App 认证加载)
    └── <名称>.png|.jpg|.jpeg|.gif|.webp

manifest.json 顶层:

字段类型约束
schema_versionint固定 2(读到 1 时 Python 工具会自动升级,见下)
generated_atstring带时区 ISO 8601;整份 ≤ 2 MiB
publisherstring?发布方标识,可选
documentsarray≤ 5000 条,按 published_at 倒序;id 不得重复

documents[] 每条:

字段类型约束
idstringASCII [A-Za-z0-9._-],≤ 200 字符,跨修订不变
formatstring必填:markdown 或 html(工具按正文后缀自动填写)
title / subtitlestring / string?标题非空且 ≤ 200 字符
summarystring≤ 1000 字符;App 列表和搜索都用它,认真写
categorystring非空;App 用它做筛选
tags[string]?可选;App 用它做筛选和搜索
published_at / updated_atstring带时区 ISO 8601(2026-10-13T09:00:00+08:00);published_at 决定排序与按月归档
pathstringdocuments/<id>_<sha256>.<md|html>,由工具生成;后缀必须与 format 一致
content_sha256string正文原始字节的 SHA-256(64 位小写十六进制)
size_bytesint真实字节数,单篇 ≤ 5 MiB(两种格式相同)
reading_minutes / source_countint?非负
format_optionsobject?仅供对应格式的阅读器使用;markdown、html 目前没有定义,不要写

硬性规则:正文是 documents/ 下非空的 UTF-8 文件,≤ 5 MiB;正文与链接里不得出现凭据;时间戳必须带时区。alternates 是协议保留字段,不要写。

版本与旧数据:v1(schema_version: 1,没有 format)只有 Markdown。Python 工具读到 v1 时,会给每条补上 "format": "markdown",下次发布时以 v2 写回(结果里带 upgraded_to_schema_version: 2)。Siftr 1.0.0 只认识 v1,看到 v2 会提示升级 App;所以在用户换成支持 v2 的版本之前,不要往他的目录发布。status 只能检查远端协议,不能判断用户安装的 App 版本;旧目录升级前让用户确认已更新 App。

兼容未来格式:Manifest 里出现你不认识的 format(比如以后的 audio),不要删也不要改,原样保留。Python 工具会自动这样处理。

4. 元数据 JSON(--metadata)

{
  "id": "ai-daily-2026-10-13",
  "title": "AI 每日简报:把信息读成理解",
  "summary": "三句话说清今天发生了什么、为什么重要。",
  "category": "AI 资讯",
  "tags": ["Agent", "模型"],
  "published_at": "2026-10-13T08:30:00+08:00",
  "updated_at": "2026-10-13T08:30:00+08:00",
  "reading_minutes": 3,
  "source_count": 5
}

必填:id、title、summary、category、published_at、updated_at。可选:subtitle、tags、reading_minutes、source_count、publisher。format、path、content_sha256、size_bytes 由工具填,不要手写;如果写了 format,必须和正文格式一致。修订时 published_at 保持首次发布时间,updated_at 写本次时间。

5. 发布路径:按运行环境任选

5.0 Python 3.10+(推荐)

scripts/siftr_publish.py(纯标准库,内置校验、幂等、重复检测、回读与退避,适合无人值守任务):

python3 scripts/siftr_publish.py validate --markdown out/daily.md --metadata out/daily.json   # 完全离线
python3 scripts/siftr_publish.py status                                                        # 远端概况:schema_version、篇数、各格式数量
python3 scripts/siftr_publish.py publish  --markdown out/daily.md --metadata out/daily.json
python3 scripts/siftr_publish.py publish  --html out/sort-demo.html --metadata out/sort-demo.json   # 单文件静态网页
python3 scripts/siftr_publish.py publish  --file out/anything.md --metadata out/meta.json           # 按后缀识别格式

5.1 从空目录验证一条完整链路(不联网)

以下示例创建 out/ 下的 Markdown、HTML 与对应元数据;若同名文件已存在则停止,避免覆盖用户产物。示例只演示格式,正式发布时改成用户实际需要的内容。运行目录为解压后的 siftr-publish/。

python3 - <<'PYTHON'
from pathlib import Path
from datetime import datetime, timezone
import json

out = Path("out")
out.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
samples = {
    "sample-report": ("md", "示例报告", "## 本次结果\n\n这里填写本次任务的结论与来源。\n"),
    "sample-page": ("html", "示例网页", '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>示例网页</title><body><main><h1>本次结果</h1><p>这里填写报告正文。</p></main></body></html>'),
}
files = {}
for doc_id, (suffix, title, text) in samples.items():
    files[out / (doc_id + "." + suffix)] = text
    files[out / (doc_id + ".json")] = json.dumps({
        "id": doc_id, "title": title, "summary": "演示发布格式。",
        "category": "示例", "published_at": stamp, "updated_at": stamp,
    }, ensure_ascii=False, indent=2)
if any(path.exists() for path in files):
    raise SystemExit("示例文件已存在;请复用或换一个工作目录。")
for path, text in files.items():
    path.write_text(text, encoding="utf-8")
PYTHON
python3 scripts/siftr_publish.py validate --markdown out/sample-report.md --metadata out/sample-report.json
python3 scripts/siftr_publish.py validate --html out/sample-page.html --metadata out/sample-page.json

用户授权发布实际内容后,将 validate 改为 publish。不要把这两篇示例自动发布到用户目录,也不要把示例 id 用作正式日报的固定 id。

publish 的 JSON 结果与你该做的事:

result含义你该做的
published新文章或修订已写入并回读确认汇报 id、篇数、generated_at
unchanged远端已是这份内容汇报「无变化」,不重试
duplicate另一个 id(duplicate_of)已有完全相同的正文,什么也没写不要换 id 重发。若本意是修订那一篇,用 duplicate_of 的 id 并修改内容;否则汇报「重复,已跳过」
非零退出 + 错误信息校验、认证、网络或回读失败见 §0.2、§9;Manifest 写入后的失败可能已生效,保留输入核对重试

只在明确授权的发布请求或既定计划任务里执行 publish。

5.2 纯 shell(没有 Python 的主机)

用 scripts/siftr_upload.sh(POSIX sh + curl + sha256sum 或 shasum,创建根目录及其下的父目录并做 SHA-256 回读)。这是低层上传器:会覆盖同名目标,不提供索引合并、去重或不可变保护;文章发布优先使用 Python。上传图片要使用全新文件名,不用此工具覆盖历史正文。每个请求有超时,失败由调用方检查后有限重试:

sh scripts/siftr_upload.sh out/daily_3f2a1b.md documents/daily_3f2a1b.md
sh scripts/siftr_upload.sh chart.jpg media/chart.jpg

manifest.json 由你按 §1、§3 手工编排:GET 旧 Manifest → 本地合并 → 上传正文并确认回读哈希一致 → 最后 PUT manifest.json → GET 复核。合并时务必自己做 Python 版做的事:

  1. 旧 Manifest 是 v1 时:每条补 "format": "markdown",顶层改为 "schema_version": 2。
  2. 同 id 替换旧条目(沿用 id,更新 format、path、content_sha256、size_bytes、updated_at)。同一个 id 从 Markdown 改成 HTML 也属于修订。
  3. 先查重:如果另一个 id 已有相同的 format + content_sha256,就停止,按 duplicate 处理。
  4. 不认识的 format 条目原样保留。
  5. 按 published_at 倒序,刷新 generated_at,Manifest ≤ 2 MiB、≤ 5000 条。

6. Markdown 写作规范(按 App 渲染器实际能力)

支持:h1–h6、粗/斜体、删除线、行内代码、有序/无序列表、任务列表(- [ ] / - [x])、引用块、提示块(引用块首行写 [!NOTE] 等)、表格(横向滚动)、代码块(显示语言名、可一键复制;代码横向滚动,text/markdown 或不标语言的块自动换行)、分隔线、可点击链接、图片(圆角,可点开放大)、脚注([^1],显示为上标数字,正文末尾汇总为「参考」)、LaTeX 公式($…$、$$…$$)。正文 18pt,随系统动态字号;读者可选黑体或宋体。

  • 分节用 ## 与 ###;不要用 #(与文章大标题重复;开头与标题相同的 # 会被 App 自动隐藏)。每个标题都会成为 App 里的目录条目,所以小节标题要简短、有信息、在同一篇里不重复。
  • 首段直接给结论,段落短小;关键引语放引用块,对比信息用表格,命令与代码放代码块并在开头围栏后标注语言(如 python、bash),App 会显示语言名。
  • 需要读者特别留意的一句话用 GitHub 提示块(示例见下),类型只有 NOTE(说明)、TIP(提示)、IMPORTANT(重要)、WARNING(注意)、CAUTION(警告);类型后面可以跟一个自定义标题。每篇最多两三个,滥用会失去重点。
  • 待办或核对清单用任务列表;已完成项会以勾选、弱化显示。
  • 图片独占一段(前后空行)时按整幅配图显示;![替代文字](地址 "图注") 中的引号部分显示为图注,没有图注时显示替代文字。
  • 链接锚文本要有意义,必须是 http(s);App 会拦截指向用户 WebDAV 主机的链接,所以不要链接到你自己的 WebDAV 地址。
  • 引用同一内容空间里的另一篇时写 [上一期](documents/<那篇的 id>.md)(例如 documents/ai-daily-2026-10-13.md);链接到网页就用 .html 后缀,比如 documents/sort-demo.html。App 会在内部直接打开那篇,修订后链接依然有效。该文章必须仍在 Manifest 里,否则读者会看到“这篇文章不在当前列表中”。
  • 段内不折行:一个段落写成一行,段与段之间空一行。段内的硬换行会被渲染成空格,中文里就成了多余的空隙。
  • 全角标点后不加空格:。、,、:、) 等后面直接接下一个字;中文与英文、数字之间是否加空格保持全文一致即可。
  • 写金额直接写 $20。只有整篇的 $ 都能两两配成公式($ 内侧不贴空格、右侧不紧跟数字)时 App 才渲染公式,所以公式和美元金额不要出现在同一篇里;需要时把金额写成「20 美元」或 USD 20。
  • Markdown 里不写 HTML(要做网页就用 §6.1 的 HTML 格式);不要在正文里放凭据。
  • App 支持全文搜索:把读者会搜的专有名词、产品名、人名写进正文(而不只写在图里)。
  • 不要写「相关推荐」「点此提问」「朗读」之类依赖 App 功能的话——这些功能不存在。
> [!TIP] 读这一节的方法
> 先看表格里的价格,再看备注。

6.1 HTML 静态网页(format: html)

适合交互演示、可视化、动画讲解这类用 Markdown 表达不出来的内容。App 用隔离的 WebKit 打开,打开即运行。

  • 单个 .html 文件,CSS 和 JS 写在文件里。不要拆出相对路径的 css、js 或图片,App 不会加载它们。
  • 可以从公共 CDN 引用库、字体和图片,但必须用 https://。CDN 资源由读者手机直接请求,不经过坚果云,也不占坚果云流量。发布工具会拒绝 http:// 资源。
  • 外部库固定版本;不依赖联网也应能看到核心结论。不要把私有报告数据发送给外部脚本或接口。发布校验只做静态检查,不证明 JS 运行正常,也不是完整安全审计;有交互时在浏览器验证操作、窄屏和资源加载,并区分浏览器测试与 Siftr 真机验证。
  • 不要依赖后端:没有服务器,没有登录,表单提交不会有响应。
  • 不要引用你的 WebDAV 地址:App 会拦截所有指向用户 WebDAV 主机的请求,发布工具也会拒绝。私有图片请改用公开地址,或者转成 data URI 内联。
  • 页面里的外链会在系统浏览器里打开,target=_blank 也一样。页内锚点(#step-2)照常可用。
  • 移动端优先:读者在手机上竖屏阅读。写上 <meta name="viewport" content="width=device-width, initial-scale=1">(漏写的话 App 会自动补上),布局要能适应 320–430pt 的宽度,可点控件不小于 44pt。
  • 支持深色模式更好:用 @media (prefers-color-scheme: dark)。App 不会改网页的样式。
  • 搜索:App 会提取页面里的可见文字参与全文搜索,JS 动态生成的文字搜不到。关键名词要写在 HTML 里,同时写进 summary 和 tags。
  • 标题写在元数据的 title 里,列表和阅读页都用它;页面里的 <title> 不会显示。
  • 讲解和演示分开时:讲解写成 Markdown,演示写成 HTML,分别用两个 id 发布。然后在 Markdown 里用 [打开演示](documents/<演示的 id>.html) 链接过去,App 会在站内打开。

7. 图片策略

坚果云流量配额宝贵,按优先级:

  1. 首选公开网络链接:![图表](https://example.com/chart.png),不消耗坚果云流量。
  2. 确需私有图片才上传 media/,且必须先压缩:长边 ≤ 1600px、JPEG 质量 70–80(或 WebP 80),单张目标 ≤ 300 KB。10 MiB 的协议上限只是兜底。
magick in.png -resize '1600x1600>' -quality 75 out.jpg                 # ImageMagick(v6 用 convert)
sips -Z 1600 -s format jpeg -s formatOptions 72 in.png --out out.jpg   # macOS 自带

私有图片用纯相对路径引用:![架构图](media/architecture.png)。App 用该来源的凭据认证加载并缓存。

  • media/ 只允许 png/jpg/jpeg/gif/webp;路径禁止 ..、%、空格与控制字符。发布工具会拒绝违规引用。
  • 图片不进 Manifest,App 按路径按需加载。
  • 缓存以路径为键:覆盖同名图片不会刷新已读设备。改图用新文件名(chart-v2.png)并发布正文修订,旧图保留。

8. 纪律与汇报

  • 发布前先 validate。脚本自带凭据泄露检查(Authorization、URL 内嵌账号密码、SIFTR_DAV_APP_PASSWORD= 等直接拒绝)。
  • 永不删除远端历史文件;永不覆盖已有正文路径。
  • 一个目录只有一个写入方(§1.3)。
  • 汇报只含:命令结果(published/unchanged/duplicate/错误类别)、文档 id、篇数、时间。不含凭据、账号、私有本地路径。

9. 排错速查

现象原因做法
认证失败 / 401用了登录密码,或应用密码已撤销让用户重新创建第三方应用密码
409 AncestorsNotFound父目录不存在MKCOL 后重试(Python 工具把 409 与 404 同样视为「还没有 Manifest」,首次发布会自动初始化目录)
429 / 5xx限流退避重试;仍失败就放弃,下次计划任务再试
远端 Manifest 格式错误;已停止发布远端 manifest.json 损坏或被他人改过不要覆盖。汇报给用户,由人决定如何恢复
Manifest 中存在重复 ID远端被手工改坏同上,不要自行覆盖
正文回读校验失败上传被截断/中途改写Manifest 未改动;直接重试
duplicate内容与别的 id 相同§1.2:不要重发
HTML 不能从 http:// 加载资源页面引用了明文资源改成 https:// 的地址
正文引用了内容空间所在的 WebDAV 主机正文里写了用户的坚果云地址删掉;私有图片改用公开地址或 data URI
不支持的 Manifest 协议版本远端是比工具更新的协议不要覆盖;更新 Skill 后再发布
读者 App 提示“需要更新版本”目录已升级到 v2,读者还在用 1.0.0让用户更新 Siftr
App 里没看到新内容读者需要手动下拉刷新;或用户没添加这个目录确认目录地址与用户在 App 里添加的一致

Skill de publication

Version du skill : 22 octobre 2026

Envoyez cette page à votre agent IA et il pourra publier des briefings Markdown dans votre propre cloud WebDAV (Nutstore), exactement au format attendu par Siftr. Ni le skill ni le paquet ne contiennent d’identifiants ; le mot de passe d’app est injecté par l’environnement de l’agent.

Installation

Le paquet est un zip qui se déplie en un dossier siftr-publish/ complet (SKILL.md et deux scripts). Déposez-le dans le répertoire skills de votre agent — souvent ~/.agents/skills/, ~/.zcode/skills/ ou ~/.claude/skills/ ; référez-vous à la documentation de votre agent.

Envoyez simplement cette phrase à votre agent :

Installe ce skill : télécharge https://chenhk.cn/Siftr/assets/siftr-publish.zip et décompresse-le, place le dossier siftr-publish/ obtenu dans ton répertoire skills, puis suis son SKILL.md pour publier du contenu Siftr pour moi.

Télécharger le paquet (siftr-publish.zip) · Les trois variables d’environnement nécessaires sont en §2 ci-dessous.

Texte complet du skill

Identique au SKILL.md du paquet (généré depuis skills/siftr-publish/ par scripts/package_skill.py, donc sans dérive).

Siftr 内容发布(面向 Agent)

0. 给收到这个链接的 Agent

把 Siftr 当成用户手机上的内容交付渠道:你负责生成内容并发布到用户自己的 WebDAV,用户在 Siftr 添加该来源后阅读。适合日报、周报、监控摘要、研究报告、旅行安排,以及单文件 HTML 图表或交互讲解。用户只发来本 Skill 链接时,先简短解释用途并检查是否已配置;链接本身不代表已经授权定时运行或上传测试内容。

  • 一次性报告:生成 Markdown,使用 §4 的元数据与 §5 的发布工具交付。
  • 定时任务产出:用所在平台的定时任务能力执行生成与发布。Siftr 不执行任务,也不保证定时推送通知。确认内容、频率、时间及 IANA 时区(如 Asia/Shanghai),并把发布步骤、Skill/脚本绝对路径、输出路径与凭据注入方式写入任务配置。没有调度能力时说明限制,保留可运行的发布命令,不要声称已设好定时任务。
  • HTML 网页:用于自包含的交互内容,按 §6.1 编写。它是 App 内阅读页面,不是公网网站托管;不要把私有 WebDAV 地址当成可分享的网页链接。用户要公开网站或后端服务时,需要另选托管方案。

0.1 首次使用的顺序

  1. 若只拿到网页说明,下载 完整 Skill 安装包,解压并阅读其中的 SKILL.md。脚本在解压后的 siftr-publish/scripts/;以下命令相对于该目录的父级 Skill 根目录执行。不要假设用户当前目录已有脚本;自动任务使用绝对路径。需要 Python 3.10+,无需 pip 包。
  2. 只检查 §2 的三个环境变量是否存在,不要打印值。未配置时按 §2.1 引导;先在本地准备和校验内容,不反复尝试联网发布。
  3. 已配置后运行 status。ready 表示远端索引可读;waiting_for_first_publish 表示尚无索引或父目录,不证明写权限正常。两者都不证明用户已连接 App,也无法判断 App 版本。
  4. 在已有发布授权下先 validate 再 publish。成功后告诉用户标题、文档 id 与发布结果,让用户在 Siftr 对应来源下拉刷新。只有收到回读确认才能说“已发布”;没有实际查看 App 时不要说“手机已经收到”。
  5. 定时任务必须在它自己的运行环境里取得凭据;当前终端配置不会自动传给后台任务。同一来源的所有任务串行执行,避免重叠。首次定时运行前,用同一执行环境验证一次。

0.2 每期内容与重试

日报使用 ai-daily-2026-10-22,周报使用 weekly-2026-w43;日期以用户时区和本次任务的逻辑周期为准。只有持续更新的同一份报告才用固定 id。重试保留本次正文、元数据、id 和时间戳,不要重新生成一篇或换 id。修订保留首次 published_at,只更新 updated_at。重复任务运行若不断刷新 updated_at,会造成不必要的索引更新。

保留本地产物直到回读确认。发布失败的处理按阶段区分:正文阶段失败时索引未更新;一旦尝试 PUT Manifest,超时或回读失败都可能发生在远端已经写入之后。此时用相同输入重新核对/发布,不要删除远端内容或宣称回滚。脚本每个网络请求最多尝试三次;任务平台也应限制重试次数并串行执行,不要无限重试。

你是 唯一的写入方。Siftr App 只读:它 GET manifest.json,对比后再 GET 需要的正文。App 不会替你总结、配图、推荐、朗读或追问——这些都不做。摘要、分类、标签、标题都由你写好,写进元数据和正文。正文可以是 Markdown 简报,也可以是单文件静态网页(HTML,见 §6.1)。

只想传任意文件到坚果云?看 §5 的 Shell 上传工具。想让 App 读到内容?先读 §1 的逻辑,再用 §5 的工具。

1. 先理解这套逻辑(决定你每次该怎么做)

1.1 两个标识,别混

标识含义谁决定
id这是哪一篇文章。跨修订保持不变(如 ai-daily-2026-10-22、weekly-2026-w43)你
content_sha256这是哪份字节。正文 UTF-8 字节的 SHA-256工具自动算

读者端的已读/收藏记录挂在 id 上;内容变化靠 content_sha256 判断。

1.2 发布前的决策表

你手里的情况怎么做工具返回
全新的一篇起一个新 id,publishpublished
修订已发布的文章(改错字、补充、更新)沿用原 id,改 Markdown,publishpublished(旧版本文件保留,Manifest 指向新文件)
与上次完全一样什么都不用做(再跑也没副作用)unchanged
新 id,但正文与某篇已发布的字节完全相同不要发。这是重复内容,工具直接拒绝写入duplicate(含 duplicate_of,即已存在的 id)
想用旧 id 发一篇完全不同的文章禁止。会覆盖该 id 的阅读状态;换新 id—

重复内容有两层防线:发布工具(duplicate,不写任何文件)和 App(读到相同 Markdown 只显示一份)。别靠 App 兜底——别制造重复。判断「是不是同一篇」用 id,不要用标题。

1.3 读者可以订阅多个 WebDAV 来源

用户可在 Siftr 里添加多个 WebDAV(例如两个坚果云目录、或坚果云加别家 WebDAV)。对你的影响:

  • 一个目录 = 一个来源 = 一份 manifest.json,一个目录只让一个发布方写。Manifest 是「读—改—写」,写前会检查变化,但没有原子锁,两个 Agent 同时写同一个目录会丢更新。想多 Agent,就一人一个目录,让用户在 App 里各添加一次。
  • 同一地址 App 只收一次(大小写、末尾斜杠、默认端口 443、「服务器路径+目录」怎么拆分都视为同一地址)。你不需要、也不应该帮用户在多个目录里重复发同一篇来「保险」。
  • 不同来源里内容完全相同的 Markdown,App 只显示一份(保留较早添加的来源那份,已读/收藏同步到两份上)。所以「同一篇发布到两个目录」没有意义,除非用户明确要求镜像备份。
  • 来源之间 id 互不冲突:甲目录的 daily 和乙目录的 daily 是两篇不同的文章,除非字节也相同。
  • 写入一个目录时只操作这个目录。不要在 SIFTR_DAV_URL 之外读写;不要把一个来源的凭据用到另一个来源。

1.4 读者端如何对待你的文件(决定了「不可变」规则)

  • 正文文件名含全文 SHA-256,一版一文件,永不改、永不删。修订就是新文件 + Manifest 改指新路径。App 要求同一路径下内容不可变,违反会被判定为损坏的 Manifest。
  • Manifest 永远最后写。读者随时可能刷新:正文先落盘并回读校验,Manifest 最后更新,读者看到的状态就总是一致的。
  • App 会把 media/ 图片按路径缓存,不会因为你覆盖同名图片而刷新(见 §7)。

2. 凭据与上传原语

运行环境通过各自的安全管理器注入三个环境变量:

变量含义示例
SIFTR_DAV_URL内容根目录完整 URL(HTTPS)https://dav.jianguoyun.com/dav/Siftr/
SIFTR_DAV_USER账号user@example.com
SIFTR_DAV_APP_PASSWORD第三方应用密码见下

坚果云 WebDAV 不接受登录密码。在网页版「账户信息 → 安全选项 → 第三方应用管理」为每个发布方单独创建应用密码(读者 App、每个 Agent 各一个,互不共用)。凭据只从环境变量读取,绝不写入 Markdown、元数据、仓库、日志、命令行字面值或你的回复。

2.1 用户尚未配置时(先完成这一步)

先说明:“Siftr 需要你自己的 WebDAV 空间来接收内容。你可以使用坚果云;我可以先准备报告,连接后再发布。密码请填写到本机或平台的安全配置入口,不要发到聊天里。”

  1. 引导用户到 坚果云官网 注册或登录,参照 官方第三方应用授权教程 创建应用密码。网页路径为“账户信息 → 安全选项 → 第三方应用管理 → 添加应用密码”。Agent 应先打开官方教程核对当前界面;界面或套餐限制变化时以官方帮助为准。
  2. 确定专用目录,例如 Siftr。坚果云服务器地址为 https://dav.jianguoyun.com/dav/,本工具使用的完整内容根目录为 https://dav.jianguoyun.com/dav/Siftr/。不要填官网网址、分享链接或 manifest.json 地址。初次配置优先使用单层 ASCII 目录;嵌套目录的上级需用户提前创建,工具不会越过内容根目录创建父级。
  3. 在 Agent 平台的秘密管理/环境变量设置里保存 §2 三项。用户填写账号与应用密码;不要使用登录密码、提交到代码库或写进定时任务提示词。平台没有安全输入入口时,由用户在自己的终端配置;不要要求把密码贴回对话。
  4. 在 Siftr 添加 WebDAV 来源:服务器 https://dav.jianguoyun.com/dav/、目录 Siftr,填写同一账号及为 App 单独生成的应用密码。Agent 和 App 的有效目录必须相同,不要重复拼上两次 Siftr。
  5. 运行 status,按结果处理认证、权限或空目录。获得发布授权后上传用户需要的第一篇内容,再请用户在 App 刷新确认;不要为了连接检查擅自留下测试文章。

其他提供商也使用 HTTPS、账号和应用密码(或其官方要求的 WebDAV 密码),以该服务官方文档为准。不要把坚果云特有的 409 行为推断为所有 WebDAV 服务的规则。

任意文件上传可用 §5 的 Shell 工具;它不更新内容索引,上传文件并不等于在 Siftr 发布了文章。

坚果云 WebDAV 行为速查(2026-10 实测)

场景实际返回
认证HTTPS + Basic(realm="nutstore"),HTTP/2
凭据错误401,空响应体
父目录不存在时 GET/PUT409 AncestorsNotFound——不是 404,先 MKCOL(注意:MKCOL 的 URL 不能带尾部斜杠,…/Siftr 成功、…/Siftr/ 返回 409;Python 工具已处理)
GET 目录403;列目录必须 PROPFIND
频率/流量超限429/5xx;指数退避,几次后放弃

3. 云端目录结构(Siftr 内容协议 v2)

$SIFTR_DAV_URL(如 https://dav.jianguoyun.com/dav/Siftr/)
├── manifest.json                      # 索引:App 的唯一入口
├── documents/
│   ├── <id前缀>_<内容SHA-256>.md       # format = markdown,不可变,一版一文件
│   └── <id前缀>_<内容SHA-256>.html     # format = html,同样不可变
└── media/                             # 可选:Markdown 引用的私有图片(App 认证加载)
    └── <名称>.png|.jpg|.jpeg|.gif|.webp

manifest.json 顶层:

字段类型约束
schema_versionint固定 2(读到 1 时 Python 工具会自动升级,见下)
generated_atstring带时区 ISO 8601;整份 ≤ 2 MiB
publisherstring?发布方标识,可选
documentsarray≤ 5000 条,按 published_at 倒序;id 不得重复

documents[] 每条:

字段类型约束
idstringASCII [A-Za-z0-9._-],≤ 200 字符,跨修订不变
formatstring必填:markdown 或 html(工具按正文后缀自动填写)
title / subtitlestring / string?标题非空且 ≤ 200 字符
summarystring≤ 1000 字符;App 列表和搜索都用它,认真写
categorystring非空;App 用它做筛选
tags[string]?可选;App 用它做筛选和搜索
published_at / updated_atstring带时区 ISO 8601(2026-10-13T09:00:00+08:00);published_at 决定排序与按月归档
pathstringdocuments/<id>_<sha256>.<md|html>,由工具生成;后缀必须与 format 一致
content_sha256string正文原始字节的 SHA-256(64 位小写十六进制)
size_bytesint真实字节数,单篇 ≤ 5 MiB(两种格式相同)
reading_minutes / source_countint?非负
format_optionsobject?仅供对应格式的阅读器使用;markdown、html 目前没有定义,不要写

硬性规则:正文是 documents/ 下非空的 UTF-8 文件,≤ 5 MiB;正文与链接里不得出现凭据;时间戳必须带时区。alternates 是协议保留字段,不要写。

版本与旧数据:v1(schema_version: 1,没有 format)只有 Markdown。Python 工具读到 v1 时,会给每条补上 "format": "markdown",下次发布时以 v2 写回(结果里带 upgraded_to_schema_version: 2)。Siftr 1.0.0 只认识 v1,看到 v2 会提示升级 App;所以在用户换成支持 v2 的版本之前,不要往他的目录发布。status 只能检查远端协议,不能判断用户安装的 App 版本;旧目录升级前让用户确认已更新 App。

兼容未来格式:Manifest 里出现你不认识的 format(比如以后的 audio),不要删也不要改,原样保留。Python 工具会自动这样处理。

4. 元数据 JSON(--metadata)

{
  "id": "ai-daily-2026-10-13",
  "title": "AI 每日简报:把信息读成理解",
  "summary": "三句话说清今天发生了什么、为什么重要。",
  "category": "AI 资讯",
  "tags": ["Agent", "模型"],
  "published_at": "2026-10-13T08:30:00+08:00",
  "updated_at": "2026-10-13T08:30:00+08:00",
  "reading_minutes": 3,
  "source_count": 5
}

必填:id、title、summary、category、published_at、updated_at。可选:subtitle、tags、reading_minutes、source_count、publisher。format、path、content_sha256、size_bytes 由工具填,不要手写;如果写了 format,必须和正文格式一致。修订时 published_at 保持首次发布时间,updated_at 写本次时间。

5. 发布路径:按运行环境任选

5.0 Python 3.10+(推荐)

scripts/siftr_publish.py(纯标准库,内置校验、幂等、重复检测、回读与退避,适合无人值守任务):

python3 scripts/siftr_publish.py validate --markdown out/daily.md --metadata out/daily.json   # 完全离线
python3 scripts/siftr_publish.py status                                                        # 远端概况:schema_version、篇数、各格式数量
python3 scripts/siftr_publish.py publish  --markdown out/daily.md --metadata out/daily.json
python3 scripts/siftr_publish.py publish  --html out/sort-demo.html --metadata out/sort-demo.json   # 单文件静态网页
python3 scripts/siftr_publish.py publish  --file out/anything.md --metadata out/meta.json           # 按后缀识别格式

5.1 从空目录验证一条完整链路(不联网)

以下示例创建 out/ 下的 Markdown、HTML 与对应元数据;若同名文件已存在则停止,避免覆盖用户产物。示例只演示格式,正式发布时改成用户实际需要的内容。运行目录为解压后的 siftr-publish/。

python3 - <<'PYTHON'
from pathlib import Path
from datetime import datetime, timezone
import json

out = Path("out")
out.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
samples = {
    "sample-report": ("md", "示例报告", "## 本次结果\n\n这里填写本次任务的结论与来源。\n"),
    "sample-page": ("html", "示例网页", '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>示例网页</title><body><main><h1>本次结果</h1><p>这里填写报告正文。</p></main></body></html>'),
}
files = {}
for doc_id, (suffix, title, text) in samples.items():
    files[out / (doc_id + "." + suffix)] = text
    files[out / (doc_id + ".json")] = json.dumps({
        "id": doc_id, "title": title, "summary": "演示发布格式。",
        "category": "示例", "published_at": stamp, "updated_at": stamp,
    }, ensure_ascii=False, indent=2)
if any(path.exists() for path in files):
    raise SystemExit("示例文件已存在;请复用或换一个工作目录。")
for path, text in files.items():
    path.write_text(text, encoding="utf-8")
PYTHON
python3 scripts/siftr_publish.py validate --markdown out/sample-report.md --metadata out/sample-report.json
python3 scripts/siftr_publish.py validate --html out/sample-page.html --metadata out/sample-page.json

用户授权发布实际内容后,将 validate 改为 publish。不要把这两篇示例自动发布到用户目录,也不要把示例 id 用作正式日报的固定 id。

publish 的 JSON 结果与你该做的事:

result含义你该做的
published新文章或修订已写入并回读确认汇报 id、篇数、generated_at
unchanged远端已是这份内容汇报「无变化」,不重试
duplicate另一个 id(duplicate_of)已有完全相同的正文,什么也没写不要换 id 重发。若本意是修订那一篇,用 duplicate_of 的 id 并修改内容;否则汇报「重复,已跳过」
非零退出 + 错误信息校验、认证、网络或回读失败见 §0.2、§9;Manifest 写入后的失败可能已生效,保留输入核对重试

只在明确授权的发布请求或既定计划任务里执行 publish。

5.2 纯 shell(没有 Python 的主机)

用 scripts/siftr_upload.sh(POSIX sh + curl + sha256sum 或 shasum,创建根目录及其下的父目录并做 SHA-256 回读)。这是低层上传器:会覆盖同名目标,不提供索引合并、去重或不可变保护;文章发布优先使用 Python。上传图片要使用全新文件名,不用此工具覆盖历史正文。每个请求有超时,失败由调用方检查后有限重试:

sh scripts/siftr_upload.sh out/daily_3f2a1b.md documents/daily_3f2a1b.md
sh scripts/siftr_upload.sh chart.jpg media/chart.jpg

manifest.json 由你按 §1、§3 手工编排:GET 旧 Manifest → 本地合并 → 上传正文并确认回读哈希一致 → 最后 PUT manifest.json → GET 复核。合并时务必自己做 Python 版做的事:

  1. 旧 Manifest 是 v1 时:每条补 "format": "markdown",顶层改为 "schema_version": 2。
  2. 同 id 替换旧条目(沿用 id,更新 format、path、content_sha256、size_bytes、updated_at)。同一个 id 从 Markdown 改成 HTML 也属于修订。
  3. 先查重:如果另一个 id 已有相同的 format + content_sha256,就停止,按 duplicate 处理。
  4. 不认识的 format 条目原样保留。
  5. 按 published_at 倒序,刷新 generated_at,Manifest ≤ 2 MiB、≤ 5000 条。

6. Markdown 写作规范(按 App 渲染器实际能力)

支持:h1–h6、粗/斜体、删除线、行内代码、有序/无序列表、任务列表(- [ ] / - [x])、引用块、提示块(引用块首行写 [!NOTE] 等)、表格(横向滚动)、代码块(显示语言名、可一键复制;代码横向滚动,text/markdown 或不标语言的块自动换行)、分隔线、可点击链接、图片(圆角,可点开放大)、脚注([^1],显示为上标数字,正文末尾汇总为「参考」)、LaTeX 公式($…$、$$…$$)。正文 18pt,随系统动态字号;读者可选黑体或宋体。

  • 分节用 ## 与 ###;不要用 #(与文章大标题重复;开头与标题相同的 # 会被 App 自动隐藏)。每个标题都会成为 App 里的目录条目,所以小节标题要简短、有信息、在同一篇里不重复。
  • 首段直接给结论,段落短小;关键引语放引用块,对比信息用表格,命令与代码放代码块并在开头围栏后标注语言(如 python、bash),App 会显示语言名。
  • 需要读者特别留意的一句话用 GitHub 提示块(示例见下),类型只有 NOTE(说明)、TIP(提示)、IMPORTANT(重要)、WARNING(注意)、CAUTION(警告);类型后面可以跟一个自定义标题。每篇最多两三个,滥用会失去重点。
  • 待办或核对清单用任务列表;已完成项会以勾选、弱化显示。
  • 图片独占一段(前后空行)时按整幅配图显示;![替代文字](地址 "图注") 中的引号部分显示为图注,没有图注时显示替代文字。
  • 链接锚文本要有意义,必须是 http(s);App 会拦截指向用户 WebDAV 主机的链接,所以不要链接到你自己的 WebDAV 地址。
  • 引用同一内容空间里的另一篇时写 [上一期](documents/<那篇的 id>.md)(例如 documents/ai-daily-2026-10-13.md);链接到网页就用 .html 后缀,比如 documents/sort-demo.html。App 会在内部直接打开那篇,修订后链接依然有效。该文章必须仍在 Manifest 里,否则读者会看到“这篇文章不在当前列表中”。
  • 段内不折行:一个段落写成一行,段与段之间空一行。段内的硬换行会被渲染成空格,中文里就成了多余的空隙。
  • 全角标点后不加空格:。、,、:、) 等后面直接接下一个字;中文与英文、数字之间是否加空格保持全文一致即可。
  • 写金额直接写 $20。只有整篇的 $ 都能两两配成公式($ 内侧不贴空格、右侧不紧跟数字)时 App 才渲染公式,所以公式和美元金额不要出现在同一篇里;需要时把金额写成「20 美元」或 USD 20。
  • Markdown 里不写 HTML(要做网页就用 §6.1 的 HTML 格式);不要在正文里放凭据。
  • App 支持全文搜索:把读者会搜的专有名词、产品名、人名写进正文(而不只写在图里)。
  • 不要写「相关推荐」「点此提问」「朗读」之类依赖 App 功能的话——这些功能不存在。
> [!TIP] 读这一节的方法
> 先看表格里的价格,再看备注。

6.1 HTML 静态网页(format: html)

适合交互演示、可视化、动画讲解这类用 Markdown 表达不出来的内容。App 用隔离的 WebKit 打开,打开即运行。

  • 单个 .html 文件,CSS 和 JS 写在文件里。不要拆出相对路径的 css、js 或图片,App 不会加载它们。
  • 可以从公共 CDN 引用库、字体和图片,但必须用 https://。CDN 资源由读者手机直接请求,不经过坚果云,也不占坚果云流量。发布工具会拒绝 http:// 资源。
  • 外部库固定版本;不依赖联网也应能看到核心结论。不要把私有报告数据发送给外部脚本或接口。发布校验只做静态检查,不证明 JS 运行正常,也不是完整安全审计;有交互时在浏览器验证操作、窄屏和资源加载,并区分浏览器测试与 Siftr 真机验证。
  • 不要依赖后端:没有服务器,没有登录,表单提交不会有响应。
  • 不要引用你的 WebDAV 地址:App 会拦截所有指向用户 WebDAV 主机的请求,发布工具也会拒绝。私有图片请改用公开地址,或者转成 data URI 内联。
  • 页面里的外链会在系统浏览器里打开,target=_blank 也一样。页内锚点(#step-2)照常可用。
  • 移动端优先:读者在手机上竖屏阅读。写上 <meta name="viewport" content="width=device-width, initial-scale=1">(漏写的话 App 会自动补上),布局要能适应 320–430pt 的宽度,可点控件不小于 44pt。
  • 支持深色模式更好:用 @media (prefers-color-scheme: dark)。App 不会改网页的样式。
  • 搜索:App 会提取页面里的可见文字参与全文搜索,JS 动态生成的文字搜不到。关键名词要写在 HTML 里,同时写进 summary 和 tags。
  • 标题写在元数据的 title 里,列表和阅读页都用它;页面里的 <title> 不会显示。
  • 讲解和演示分开时:讲解写成 Markdown,演示写成 HTML,分别用两个 id 发布。然后在 Markdown 里用 [打开演示](documents/<演示的 id>.html) 链接过去,App 会在站内打开。

7. 图片策略

坚果云流量配额宝贵,按优先级:

  1. 首选公开网络链接:![图表](https://example.com/chart.png),不消耗坚果云流量。
  2. 确需私有图片才上传 media/,且必须先压缩:长边 ≤ 1600px、JPEG 质量 70–80(或 WebP 80),单张目标 ≤ 300 KB。10 MiB 的协议上限只是兜底。
magick in.png -resize '1600x1600>' -quality 75 out.jpg                 # ImageMagick(v6 用 convert)
sips -Z 1600 -s format jpeg -s formatOptions 72 in.png --out out.jpg   # macOS 自带

私有图片用纯相对路径引用:![架构图](media/architecture.png)。App 用该来源的凭据认证加载并缓存。

  • media/ 只允许 png/jpg/jpeg/gif/webp;路径禁止 ..、%、空格与控制字符。发布工具会拒绝违规引用。
  • 图片不进 Manifest,App 按路径按需加载。
  • 缓存以路径为键:覆盖同名图片不会刷新已读设备。改图用新文件名(chart-v2.png)并发布正文修订,旧图保留。

8. 纪律与汇报

  • 发布前先 validate。脚本自带凭据泄露检查(Authorization、URL 内嵌账号密码、SIFTR_DAV_APP_PASSWORD= 等直接拒绝)。
  • 永不删除远端历史文件;永不覆盖已有正文路径。
  • 一个目录只有一个写入方(§1.3)。
  • 汇报只含:命令结果(published/unchanged/duplicate/错误类别)、文档 id、篇数、时间。不含凭据、账号、私有本地路径。

9. 排错速查

现象原因做法
认证失败 / 401用了登录密码,或应用密码已撤销让用户重新创建第三方应用密码
409 AncestorsNotFound父目录不存在MKCOL 后重试(Python 工具把 409 与 404 同样视为「还没有 Manifest」,首次发布会自动初始化目录)
429 / 5xx限流退避重试;仍失败就放弃,下次计划任务再试
远端 Manifest 格式错误;已停止发布远端 manifest.json 损坏或被他人改过不要覆盖。汇报给用户,由人决定如何恢复
Manifest 中存在重复 ID远端被手工改坏同上,不要自行覆盖
正文回读校验失败上传被截断/中途改写Manifest 未改动;直接重试
duplicate内容与别的 id 相同§1.2:不要重发
HTML 不能从 http:// 加载资源页面引用了明文资源改成 https:// 的地址
正文引用了内容空间所在的 WebDAV 主机正文里写了用户的坚果云地址删掉;私有图片改用公开地址或 data URI
不支持的 Manifest 协议版本远端是比工具更新的协议不要覆盖;更新 Skill 后再发布
读者 App 提示“需要更新版本”目录已升级到 v2,读者还在用 1.0.0让用户更新 Siftr
App 里没看到新内容读者需要手动下拉刷新;或用户没添加这个目录确认目录地址与用户在 App 里添加的一致

게시 Skill

Skill 버전: 2026년 10월 22일

이 페이지를 AI 에이전트에게 보내면 Siftr의 콘텐츠 프로토콜에 따라 Markdown 브리핑을 당신의 누터스트 WebDAV에 게시할 수 있어요. Skill과 설치 패키지에는 어떤 자격 증명도 없고, 앱 비밀번호는 에이전트 실행 환경에서 주입됩니다.

설치

패키지는 zip이며 압축을 풀면 완전한 siftr-publish/ 폴더(SKILL.md과 스크립트 2개)가 나와요. 에이전트의 skills 디렉터리에 넣으면 됩니다. 보통 ~/.agents/skills/, ~/.zcode/skills/, ~/.claude/skills/ 등이며 플랫폼마다 다르니 에이전트 문서를 확인하세요.

아래 문장을 그대로 에이전트에게 보내세요:

이 Skill을 설치해 줘: https://chenhk.cn/Siftr/assets/siftr-publish.zip 을(를) 내려받아 압축을 풀고, 나온 siftr-publish/ 폴더 전체를 너의 skills 디렉터리에 넣은 뒤, 그 안의 SKILL.md 지침에 따라 나를 위해 Siftr 콘텐츠를 게시해 줘.

패키지 내려받기(siftr-publish.zip) · 필요한 세 가지 환경 변수는 본문 §2를 참고하세요.

Skill 전문

아래 내용은 패키지 안의 SKILL.md와 완전히 동일합니다(scripts/package_skill.py가 skills/siftr-publish/에서 생성하므로 어긋나지 않아요).

Siftr 内容发布(面向 Agent)

0. 给收到这个链接的 Agent

把 Siftr 当成用户手机上的内容交付渠道:你负责生成内容并发布到用户自己的 WebDAV,用户在 Siftr 添加该来源后阅读。适合日报、周报、监控摘要、研究报告、旅行安排,以及单文件 HTML 图表或交互讲解。用户只发来本 Skill 链接时,先简短解释用途并检查是否已配置;链接本身不代表已经授权定时运行或上传测试内容。

  • 一次性报告:生成 Markdown,使用 §4 的元数据与 §5 的发布工具交付。
  • 定时任务产出:用所在平台的定时任务能力执行生成与发布。Siftr 不执行任务,也不保证定时推送通知。确认内容、频率、时间及 IANA 时区(如 Asia/Shanghai),并把发布步骤、Skill/脚本绝对路径、输出路径与凭据注入方式写入任务配置。没有调度能力时说明限制,保留可运行的发布命令,不要声称已设好定时任务。
  • HTML 网页:用于自包含的交互内容,按 §6.1 编写。它是 App 内阅读页面,不是公网网站托管;不要把私有 WebDAV 地址当成可分享的网页链接。用户要公开网站或后端服务时,需要另选托管方案。

0.1 首次使用的顺序

  1. 若只拿到网页说明,下载 完整 Skill 安装包,解压并阅读其中的 SKILL.md。脚本在解压后的 siftr-publish/scripts/;以下命令相对于该目录的父级 Skill 根目录执行。不要假设用户当前目录已有脚本;自动任务使用绝对路径。需要 Python 3.10+,无需 pip 包。
  2. 只检查 §2 的三个环境变量是否存在,不要打印值。未配置时按 §2.1 引导;先在本地准备和校验内容,不反复尝试联网发布。
  3. 已配置后运行 status。ready 表示远端索引可读;waiting_for_first_publish 表示尚无索引或父目录,不证明写权限正常。两者都不证明用户已连接 App,也无法判断 App 版本。
  4. 在已有发布授权下先 validate 再 publish。成功后告诉用户标题、文档 id 与发布结果,让用户在 Siftr 对应来源下拉刷新。只有收到回读确认才能说“已发布”;没有实际查看 App 时不要说“手机已经收到”。
  5. 定时任务必须在它自己的运行环境里取得凭据;当前终端配置不会自动传给后台任务。同一来源的所有任务串行执行,避免重叠。首次定时运行前,用同一执行环境验证一次。

0.2 每期内容与重试

日报使用 ai-daily-2026-10-22,周报使用 weekly-2026-w43;日期以用户时区和本次任务的逻辑周期为准。只有持续更新的同一份报告才用固定 id。重试保留本次正文、元数据、id 和时间戳,不要重新生成一篇或换 id。修订保留首次 published_at,只更新 updated_at。重复任务运行若不断刷新 updated_at,会造成不必要的索引更新。

保留本地产物直到回读确认。发布失败的处理按阶段区分:正文阶段失败时索引未更新;一旦尝试 PUT Manifest,超时或回读失败都可能发生在远端已经写入之后。此时用相同输入重新核对/发布,不要删除远端内容或宣称回滚。脚本每个网络请求最多尝试三次;任务平台也应限制重试次数并串行执行,不要无限重试。

你是 唯一的写入方。Siftr App 只读:它 GET manifest.json,对比后再 GET 需要的正文。App 不会替你总结、配图、推荐、朗读或追问——这些都不做。摘要、分类、标签、标题都由你写好,写进元数据和正文。正文可以是 Markdown 简报,也可以是单文件静态网页(HTML,见 §6.1)。

只想传任意文件到坚果云?看 §5 的 Shell 上传工具。想让 App 读到内容?先读 §1 的逻辑,再用 §5 的工具。

1. 先理解这套逻辑(决定你每次该怎么做)

1.1 两个标识,别混

标识含义谁决定
id这是哪一篇文章。跨修订保持不变(如 ai-daily-2026-10-22、weekly-2026-w43)你
content_sha256这是哪份字节。正文 UTF-8 字节的 SHA-256工具自动算

读者端的已读/收藏记录挂在 id 上;内容变化靠 content_sha256 判断。

1.2 发布前的决策表

你手里的情况怎么做工具返回
全新的一篇起一个新 id,publishpublished
修订已发布的文章(改错字、补充、更新)沿用原 id,改 Markdown,publishpublished(旧版本文件保留,Manifest 指向新文件)
与上次完全一样什么都不用做(再跑也没副作用)unchanged
新 id,但正文与某篇已发布的字节完全相同不要发。这是重复内容,工具直接拒绝写入duplicate(含 duplicate_of,即已存在的 id)
想用旧 id 发一篇完全不同的文章禁止。会覆盖该 id 的阅读状态;换新 id—

重复内容有两层防线:发布工具(duplicate,不写任何文件)和 App(读到相同 Markdown 只显示一份)。别靠 App 兜底——别制造重复。判断「是不是同一篇」用 id,不要用标题。

1.3 读者可以订阅多个 WebDAV 来源

用户可在 Siftr 里添加多个 WebDAV(例如两个坚果云目录、或坚果云加别家 WebDAV)。对你的影响:

  • 一个目录 = 一个来源 = 一份 manifest.json,一个目录只让一个发布方写。Manifest 是「读—改—写」,写前会检查变化,但没有原子锁,两个 Agent 同时写同一个目录会丢更新。想多 Agent,就一人一个目录,让用户在 App 里各添加一次。
  • 同一地址 App 只收一次(大小写、末尾斜杠、默认端口 443、「服务器路径+目录」怎么拆分都视为同一地址)。你不需要、也不应该帮用户在多个目录里重复发同一篇来「保险」。
  • 不同来源里内容完全相同的 Markdown,App 只显示一份(保留较早添加的来源那份,已读/收藏同步到两份上)。所以「同一篇发布到两个目录」没有意义,除非用户明确要求镜像备份。
  • 来源之间 id 互不冲突:甲目录的 daily 和乙目录的 daily 是两篇不同的文章,除非字节也相同。
  • 写入一个目录时只操作这个目录。不要在 SIFTR_DAV_URL 之外读写;不要把一个来源的凭据用到另一个来源。

1.4 读者端如何对待你的文件(决定了「不可变」规则)

  • 正文文件名含全文 SHA-256,一版一文件,永不改、永不删。修订就是新文件 + Manifest 改指新路径。App 要求同一路径下内容不可变,违反会被判定为损坏的 Manifest。
  • Manifest 永远最后写。读者随时可能刷新:正文先落盘并回读校验,Manifest 最后更新,读者看到的状态就总是一致的。
  • App 会把 media/ 图片按路径缓存,不会因为你覆盖同名图片而刷新(见 §7)。

2. 凭据与上传原语

运行环境通过各自的安全管理器注入三个环境变量:

变量含义示例
SIFTR_DAV_URL内容根目录完整 URL(HTTPS)https://dav.jianguoyun.com/dav/Siftr/
SIFTR_DAV_USER账号user@example.com
SIFTR_DAV_APP_PASSWORD第三方应用密码见下

坚果云 WebDAV 不接受登录密码。在网页版「账户信息 → 安全选项 → 第三方应用管理」为每个发布方单独创建应用密码(读者 App、每个 Agent 各一个,互不共用)。凭据只从环境变量读取,绝不写入 Markdown、元数据、仓库、日志、命令行字面值或你的回复。

2.1 用户尚未配置时(先完成这一步)

先说明:“Siftr 需要你自己的 WebDAV 空间来接收内容。你可以使用坚果云;我可以先准备报告,连接后再发布。密码请填写到本机或平台的安全配置入口,不要发到聊天里。”

  1. 引导用户到 坚果云官网 注册或登录,参照 官方第三方应用授权教程 创建应用密码。网页路径为“账户信息 → 安全选项 → 第三方应用管理 → 添加应用密码”。Agent 应先打开官方教程核对当前界面;界面或套餐限制变化时以官方帮助为准。
  2. 确定专用目录,例如 Siftr。坚果云服务器地址为 https://dav.jianguoyun.com/dav/,本工具使用的完整内容根目录为 https://dav.jianguoyun.com/dav/Siftr/。不要填官网网址、分享链接或 manifest.json 地址。初次配置优先使用单层 ASCII 目录;嵌套目录的上级需用户提前创建,工具不会越过内容根目录创建父级。
  3. 在 Agent 平台的秘密管理/环境变量设置里保存 §2 三项。用户填写账号与应用密码;不要使用登录密码、提交到代码库或写进定时任务提示词。平台没有安全输入入口时,由用户在自己的终端配置;不要要求把密码贴回对话。
  4. 在 Siftr 添加 WebDAV 来源:服务器 https://dav.jianguoyun.com/dav/、目录 Siftr,填写同一账号及为 App 单独生成的应用密码。Agent 和 App 的有效目录必须相同,不要重复拼上两次 Siftr。
  5. 运行 status,按结果处理认证、权限或空目录。获得发布授权后上传用户需要的第一篇内容,再请用户在 App 刷新确认;不要为了连接检查擅自留下测试文章。

其他提供商也使用 HTTPS、账号和应用密码(或其官方要求的 WebDAV 密码),以该服务官方文档为准。不要把坚果云特有的 409 行为推断为所有 WebDAV 服务的规则。

任意文件上传可用 §5 的 Shell 工具;它不更新内容索引,上传文件并不等于在 Siftr 发布了文章。

坚果云 WebDAV 行为速查(2026-10 实测)

场景实际返回
认证HTTPS + Basic(realm="nutstore"),HTTP/2
凭据错误401,空响应体
父目录不存在时 GET/PUT409 AncestorsNotFound——不是 404,先 MKCOL(注意:MKCOL 的 URL 不能带尾部斜杠,…/Siftr 成功、…/Siftr/ 返回 409;Python 工具已处理)
GET 目录403;列目录必须 PROPFIND
频率/流量超限429/5xx;指数退避,几次后放弃

3. 云端目录结构(Siftr 内容协议 v2)

$SIFTR_DAV_URL(如 https://dav.jianguoyun.com/dav/Siftr/)
├── manifest.json                      # 索引:App 的唯一入口
├── documents/
│   ├── <id前缀>_<内容SHA-256>.md       # format = markdown,不可变,一版一文件
│   └── <id前缀>_<内容SHA-256>.html     # format = html,同样不可变
└── media/                             # 可选:Markdown 引用的私有图片(App 认证加载)
    └── <名称>.png|.jpg|.jpeg|.gif|.webp

manifest.json 顶层:

字段类型约束
schema_versionint固定 2(读到 1 时 Python 工具会自动升级,见下)
generated_atstring带时区 ISO 8601;整份 ≤ 2 MiB
publisherstring?发布方标识,可选
documentsarray≤ 5000 条,按 published_at 倒序;id 不得重复

documents[] 每条:

字段类型约束
idstringASCII [A-Za-z0-9._-],≤ 200 字符,跨修订不变
formatstring必填:markdown 或 html(工具按正文后缀自动填写)
title / subtitlestring / string?标题非空且 ≤ 200 字符
summarystring≤ 1000 字符;App 列表和搜索都用它,认真写
categorystring非空;App 用它做筛选
tags[string]?可选;App 用它做筛选和搜索
published_at / updated_atstring带时区 ISO 8601(2026-10-13T09:00:00+08:00);published_at 决定排序与按月归档
pathstringdocuments/<id>_<sha256>.<md|html>,由工具生成;后缀必须与 format 一致
content_sha256string正文原始字节的 SHA-256(64 位小写十六进制)
size_bytesint真实字节数,单篇 ≤ 5 MiB(两种格式相同)
reading_minutes / source_countint?非负
format_optionsobject?仅供对应格式的阅读器使用;markdown、html 目前没有定义,不要写

硬性规则:正文是 documents/ 下非空的 UTF-8 文件,≤ 5 MiB;正文与链接里不得出现凭据;时间戳必须带时区。alternates 是协议保留字段,不要写。

版本与旧数据:v1(schema_version: 1,没有 format)只有 Markdown。Python 工具读到 v1 时,会给每条补上 "format": "markdown",下次发布时以 v2 写回(结果里带 upgraded_to_schema_version: 2)。Siftr 1.0.0 只认识 v1,看到 v2 会提示升级 App;所以在用户换成支持 v2 的版本之前,不要往他的目录发布。status 只能检查远端协议,不能判断用户安装的 App 版本;旧目录升级前让用户确认已更新 App。

兼容未来格式:Manifest 里出现你不认识的 format(比如以后的 audio),不要删也不要改,原样保留。Python 工具会自动这样处理。

4. 元数据 JSON(--metadata)

{
  "id": "ai-daily-2026-10-13",
  "title": "AI 每日简报:把信息读成理解",
  "summary": "三句话说清今天发生了什么、为什么重要。",
  "category": "AI 资讯",
  "tags": ["Agent", "模型"],
  "published_at": "2026-10-13T08:30:00+08:00",
  "updated_at": "2026-10-13T08:30:00+08:00",
  "reading_minutes": 3,
  "source_count": 5
}

必填:id、title、summary、category、published_at、updated_at。可选:subtitle、tags、reading_minutes、source_count、publisher。format、path、content_sha256、size_bytes 由工具填,不要手写;如果写了 format,必须和正文格式一致。修订时 published_at 保持首次发布时间,updated_at 写本次时间。

5. 发布路径:按运行环境任选

5.0 Python 3.10+(推荐)

scripts/siftr_publish.py(纯标准库,内置校验、幂等、重复检测、回读与退避,适合无人值守任务):

python3 scripts/siftr_publish.py validate --markdown out/daily.md --metadata out/daily.json   # 完全离线
python3 scripts/siftr_publish.py status                                                        # 远端概况:schema_version、篇数、各格式数量
python3 scripts/siftr_publish.py publish  --markdown out/daily.md --metadata out/daily.json
python3 scripts/siftr_publish.py publish  --html out/sort-demo.html --metadata out/sort-demo.json   # 单文件静态网页
python3 scripts/siftr_publish.py publish  --file out/anything.md --metadata out/meta.json           # 按后缀识别格式

5.1 从空目录验证一条完整链路(不联网)

以下示例创建 out/ 下的 Markdown、HTML 与对应元数据;若同名文件已存在则停止,避免覆盖用户产物。示例只演示格式,正式发布时改成用户实际需要的内容。运行目录为解压后的 siftr-publish/。

python3 - <<'PYTHON'
from pathlib import Path
from datetime import datetime, timezone
import json

out = Path("out")
out.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
samples = {
    "sample-report": ("md", "示例报告", "## 本次结果\n\n这里填写本次任务的结论与来源。\n"),
    "sample-page": ("html", "示例网页", '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>示例网页</title><body><main><h1>本次结果</h1><p>这里填写报告正文。</p></main></body></html>'),
}
files = {}
for doc_id, (suffix, title, text) in samples.items():
    files[out / (doc_id + "." + suffix)] = text
    files[out / (doc_id + ".json")] = json.dumps({
        "id": doc_id, "title": title, "summary": "演示发布格式。",
        "category": "示例", "published_at": stamp, "updated_at": stamp,
    }, ensure_ascii=False, indent=2)
if any(path.exists() for path in files):
    raise SystemExit("示例文件已存在;请复用或换一个工作目录。")
for path, text in files.items():
    path.write_text(text, encoding="utf-8")
PYTHON
python3 scripts/siftr_publish.py validate --markdown out/sample-report.md --metadata out/sample-report.json
python3 scripts/siftr_publish.py validate --html out/sample-page.html --metadata out/sample-page.json

用户授权发布实际内容后,将 validate 改为 publish。不要把这两篇示例自动发布到用户目录,也不要把示例 id 用作正式日报的固定 id。

publish 的 JSON 结果与你该做的事:

result含义你该做的
published新文章或修订已写入并回读确认汇报 id、篇数、generated_at
unchanged远端已是这份内容汇报「无变化」,不重试
duplicate另一个 id(duplicate_of)已有完全相同的正文,什么也没写不要换 id 重发。若本意是修订那一篇,用 duplicate_of 的 id 并修改内容;否则汇报「重复,已跳过」
非零退出 + 错误信息校验、认证、网络或回读失败见 §0.2、§9;Manifest 写入后的失败可能已生效,保留输入核对重试

只在明确授权的发布请求或既定计划任务里执行 publish。

5.2 纯 shell(没有 Python 的主机)

用 scripts/siftr_upload.sh(POSIX sh + curl + sha256sum 或 shasum,创建根目录及其下的父目录并做 SHA-256 回读)。这是低层上传器:会覆盖同名目标,不提供索引合并、去重或不可变保护;文章发布优先使用 Python。上传图片要使用全新文件名,不用此工具覆盖历史正文。每个请求有超时,失败由调用方检查后有限重试:

sh scripts/siftr_upload.sh out/daily_3f2a1b.md documents/daily_3f2a1b.md
sh scripts/siftr_upload.sh chart.jpg media/chart.jpg

manifest.json 由你按 §1、§3 手工编排:GET 旧 Manifest → 本地合并 → 上传正文并确认回读哈希一致 → 最后 PUT manifest.json → GET 复核。合并时务必自己做 Python 版做的事:

  1. 旧 Manifest 是 v1 时:每条补 "format": "markdown",顶层改为 "schema_version": 2。
  2. 同 id 替换旧条目(沿用 id,更新 format、path、content_sha256、size_bytes、updated_at)。同一个 id 从 Markdown 改成 HTML 也属于修订。
  3. 先查重:如果另一个 id 已有相同的 format + content_sha256,就停止,按 duplicate 处理。
  4. 不认识的 format 条目原样保留。
  5. 按 published_at 倒序,刷新 generated_at,Manifest ≤ 2 MiB、≤ 5000 条。

6. Markdown 写作规范(按 App 渲染器实际能力)

支持:h1–h6、粗/斜体、删除线、行内代码、有序/无序列表、任务列表(- [ ] / - [x])、引用块、提示块(引用块首行写 [!NOTE] 等)、表格(横向滚动)、代码块(显示语言名、可一键复制;代码横向滚动,text/markdown 或不标语言的块自动换行)、分隔线、可点击链接、图片(圆角,可点开放大)、脚注([^1],显示为上标数字,正文末尾汇总为「参考」)、LaTeX 公式($…$、$$…$$)。正文 18pt,随系统动态字号;读者可选黑体或宋体。

  • 分节用 ## 与 ###;不要用 #(与文章大标题重复;开头与标题相同的 # 会被 App 自动隐藏)。每个标题都会成为 App 里的目录条目,所以小节标题要简短、有信息、在同一篇里不重复。
  • 首段直接给结论,段落短小;关键引语放引用块,对比信息用表格,命令与代码放代码块并在开头围栏后标注语言(如 python、bash),App 会显示语言名。
  • 需要读者特别留意的一句话用 GitHub 提示块(示例见下),类型只有 NOTE(说明)、TIP(提示)、IMPORTANT(重要)、WARNING(注意)、CAUTION(警告);类型后面可以跟一个自定义标题。每篇最多两三个,滥用会失去重点。
  • 待办或核对清单用任务列表;已完成项会以勾选、弱化显示。
  • 图片独占一段(前后空行)时按整幅配图显示;![替代文字](地址 "图注") 中的引号部分显示为图注,没有图注时显示替代文字。
  • 链接锚文本要有意义,必须是 http(s);App 会拦截指向用户 WebDAV 主机的链接,所以不要链接到你自己的 WebDAV 地址。
  • 引用同一内容空间里的另一篇时写 [上一期](documents/<那篇的 id>.md)(例如 documents/ai-daily-2026-10-13.md);链接到网页就用 .html 后缀,比如 documents/sort-demo.html。App 会在内部直接打开那篇,修订后链接依然有效。该文章必须仍在 Manifest 里,否则读者会看到“这篇文章不在当前列表中”。
  • 段内不折行:一个段落写成一行,段与段之间空一行。段内的硬换行会被渲染成空格,中文里就成了多余的空隙。
  • 全角标点后不加空格:。、,、:、) 等后面直接接下一个字;中文与英文、数字之间是否加空格保持全文一致即可。
  • 写金额直接写 $20。只有整篇的 $ 都能两两配成公式($ 内侧不贴空格、右侧不紧跟数字)时 App 才渲染公式,所以公式和美元金额不要出现在同一篇里;需要时把金额写成「20 美元」或 USD 20。
  • Markdown 里不写 HTML(要做网页就用 §6.1 的 HTML 格式);不要在正文里放凭据。
  • App 支持全文搜索:把读者会搜的专有名词、产品名、人名写进正文(而不只写在图里)。
  • 不要写「相关推荐」「点此提问」「朗读」之类依赖 App 功能的话——这些功能不存在。
> [!TIP] 读这一节的方法
> 先看表格里的价格,再看备注。

6.1 HTML 静态网页(format: html)

适合交互演示、可视化、动画讲解这类用 Markdown 表达不出来的内容。App 用隔离的 WebKit 打开,打开即运行。

  • 单个 .html 文件,CSS 和 JS 写在文件里。不要拆出相对路径的 css、js 或图片,App 不会加载它们。
  • 可以从公共 CDN 引用库、字体和图片,但必须用 https://。CDN 资源由读者手机直接请求,不经过坚果云,也不占坚果云流量。发布工具会拒绝 http:// 资源。
  • 外部库固定版本;不依赖联网也应能看到核心结论。不要把私有报告数据发送给外部脚本或接口。发布校验只做静态检查,不证明 JS 运行正常,也不是完整安全审计;有交互时在浏览器验证操作、窄屏和资源加载,并区分浏览器测试与 Siftr 真机验证。
  • 不要依赖后端:没有服务器,没有登录,表单提交不会有响应。
  • 不要引用你的 WebDAV 地址:App 会拦截所有指向用户 WebDAV 主机的请求,发布工具也会拒绝。私有图片请改用公开地址,或者转成 data URI 内联。
  • 页面里的外链会在系统浏览器里打开,target=_blank 也一样。页内锚点(#step-2)照常可用。
  • 移动端优先:读者在手机上竖屏阅读。写上 <meta name="viewport" content="width=device-width, initial-scale=1">(漏写的话 App 会自动补上),布局要能适应 320–430pt 的宽度,可点控件不小于 44pt。
  • 支持深色模式更好:用 @media (prefers-color-scheme: dark)。App 不会改网页的样式。
  • 搜索:App 会提取页面里的可见文字参与全文搜索,JS 动态生成的文字搜不到。关键名词要写在 HTML 里,同时写进 summary 和 tags。
  • 标题写在元数据的 title 里,列表和阅读页都用它;页面里的 <title> 不会显示。
  • 讲解和演示分开时:讲解写成 Markdown,演示写成 HTML,分别用两个 id 发布。然后在 Markdown 里用 [打开演示](documents/<演示的 id>.html) 链接过去,App 会在站内打开。

7. 图片策略

坚果云流量配额宝贵,按优先级:

  1. 首选公开网络链接:![图表](https://example.com/chart.png),不消耗坚果云流量。
  2. 确需私有图片才上传 media/,且必须先压缩:长边 ≤ 1600px、JPEG 质量 70–80(或 WebP 80),单张目标 ≤ 300 KB。10 MiB 的协议上限只是兜底。
magick in.png -resize '1600x1600>' -quality 75 out.jpg                 # ImageMagick(v6 用 convert)
sips -Z 1600 -s format jpeg -s formatOptions 72 in.png --out out.jpg   # macOS 自带

私有图片用纯相对路径引用:![架构图](media/architecture.png)。App 用该来源的凭据认证加载并缓存。

  • media/ 只允许 png/jpg/jpeg/gif/webp;路径禁止 ..、%、空格与控制字符。发布工具会拒绝违规引用。
  • 图片不进 Manifest,App 按路径按需加载。
  • 缓存以路径为键:覆盖同名图片不会刷新已读设备。改图用新文件名(chart-v2.png)并发布正文修订,旧图保留。

8. 纪律与汇报

  • 发布前先 validate。脚本自带凭据泄露检查(Authorization、URL 内嵌账号密码、SIFTR_DAV_APP_PASSWORD= 等直接拒绝)。
  • 永不删除远端历史文件;永不覆盖已有正文路径。
  • 一个目录只有一个写入方(§1.3)。
  • 汇报只含:命令结果(published/unchanged/duplicate/错误类别)、文档 id、篇数、时间。不含凭据、账号、私有本地路径。

9. 排错速查

现象原因做法
认证失败 / 401用了登录密码,或应用密码已撤销让用户重新创建第三方应用密码
409 AncestorsNotFound父目录不存在MKCOL 后重试(Python 工具把 409 与 404 同样视为「还没有 Manifest」,首次发布会自动初始化目录)
429 / 5xx限流退避重试;仍失败就放弃,下次计划任务再试
远端 Manifest 格式错误;已停止发布远端 manifest.json 损坏或被他人改过不要覆盖。汇报给用户,由人决定如何恢复
Manifest 中存在重复 ID远端被手工改坏同上,不要自行覆盖
正文回读校验失败上传被截断/中途改写Manifest 未改动;直接重试
duplicate内容与别的 id 相同§1.2:不要重发
HTML 不能从 http:// 加载资源页面引用了明文资源改成 https:// 的地址
正文引用了内容空间所在的 WebDAV 主机正文里写了用户的坚果云地址删掉;私有图片改用公开地址或 data URI
不支持的 Manifest 协议版本远端是比工具更新的协议不要覆盖;更新 Skill 后再发布
读者 App 提示“需要更新版本”目录已升级到 v2,读者还在用 1.0.0让用户更新 Siftr
App 里没看到新内容读者需要手动下拉刷新;或用户没添加这个目录确认目录地址与用户在 App 里添加的一致

Publisher-Skill

Skill-Version: 22. Oktober 2026

Schick diese Seite an deinen KI-Agenten, und er kann Markdown-Briefings genau im Format, das Siftr erwartet, in deinen eigenen WebDAV-Space (Nutstore) veröffentlichen. Weder Skill noch Paket enthalten Zugangsdaten; das App-Passwort injiziert die Laufzeitumgebung des Agenten.

Installation

Das Paket ist ein Zip und entfaltet sich zu einem vollständigen Ordner siftr-publish/ (SKILL.md plus zwei Skripte). Lege ihn in das skills-Verzeichnis deines Agenten — üblich sind ~/.agents/skills/, ~/.zcode/skills/ oder ~/.claude/skills/; maßgeblich ist die Dokumentation deines Agenten.

Schick deinem Agenten einfach diesen Satz:

Installiere diesen Skill: Lade https://chenhk.cn/Siftr/assets/siftr-publish.zip herunter und entpacke es, lege den entstehenden Ordner siftr-publish/ in dein skills-Verzeichnis und folge dann dessen SKILL.md, um für mich Siftr-Inhalte zu veröffentlichen.

Paket laden (siftr-publish.zip) · Die drei nötigen Umgebungsvariablen stehen in §2 unten.

Vollständiger Skill-Text

Identisch mit der SKILL.md im Paket (erzeugt aus skills/siftr-publish/ durch scripts/package_skill.py, daher ohne Drift).

Siftr 内容发布(面向 Agent)

0. 给收到这个链接的 Agent

把 Siftr 当成用户手机上的内容交付渠道:你负责生成内容并发布到用户自己的 WebDAV,用户在 Siftr 添加该来源后阅读。适合日报、周报、监控摘要、研究报告、旅行安排,以及单文件 HTML 图表或交互讲解。用户只发来本 Skill 链接时,先简短解释用途并检查是否已配置;链接本身不代表已经授权定时运行或上传测试内容。

  • 一次性报告:生成 Markdown,使用 §4 的元数据与 §5 的发布工具交付。
  • 定时任务产出:用所在平台的定时任务能力执行生成与发布。Siftr 不执行任务,也不保证定时推送通知。确认内容、频率、时间及 IANA 时区(如 Asia/Shanghai),并把发布步骤、Skill/脚本绝对路径、输出路径与凭据注入方式写入任务配置。没有调度能力时说明限制,保留可运行的发布命令,不要声称已设好定时任务。
  • HTML 网页:用于自包含的交互内容,按 §6.1 编写。它是 App 内阅读页面,不是公网网站托管;不要把私有 WebDAV 地址当成可分享的网页链接。用户要公开网站或后端服务时,需要另选托管方案。

0.1 首次使用的顺序

  1. 若只拿到网页说明,下载 完整 Skill 安装包,解压并阅读其中的 SKILL.md。脚本在解压后的 siftr-publish/scripts/;以下命令相对于该目录的父级 Skill 根目录执行。不要假设用户当前目录已有脚本;自动任务使用绝对路径。需要 Python 3.10+,无需 pip 包。
  2. 只检查 §2 的三个环境变量是否存在,不要打印值。未配置时按 §2.1 引导;先在本地准备和校验内容,不反复尝试联网发布。
  3. 已配置后运行 status。ready 表示远端索引可读;waiting_for_first_publish 表示尚无索引或父目录,不证明写权限正常。两者都不证明用户已连接 App,也无法判断 App 版本。
  4. 在已有发布授权下先 validate 再 publish。成功后告诉用户标题、文档 id 与发布结果,让用户在 Siftr 对应来源下拉刷新。只有收到回读确认才能说“已发布”;没有实际查看 App 时不要说“手机已经收到”。
  5. 定时任务必须在它自己的运行环境里取得凭据;当前终端配置不会自动传给后台任务。同一来源的所有任务串行执行,避免重叠。首次定时运行前,用同一执行环境验证一次。

0.2 每期内容与重试

日报使用 ai-daily-2026-10-22,周报使用 weekly-2026-w43;日期以用户时区和本次任务的逻辑周期为准。只有持续更新的同一份报告才用固定 id。重试保留本次正文、元数据、id 和时间戳,不要重新生成一篇或换 id。修订保留首次 published_at,只更新 updated_at。重复任务运行若不断刷新 updated_at,会造成不必要的索引更新。

保留本地产物直到回读确认。发布失败的处理按阶段区分:正文阶段失败时索引未更新;一旦尝试 PUT Manifest,超时或回读失败都可能发生在远端已经写入之后。此时用相同输入重新核对/发布,不要删除远端内容或宣称回滚。脚本每个网络请求最多尝试三次;任务平台也应限制重试次数并串行执行,不要无限重试。

你是 唯一的写入方。Siftr App 只读:它 GET manifest.json,对比后再 GET 需要的正文。App 不会替你总结、配图、推荐、朗读或追问——这些都不做。摘要、分类、标签、标题都由你写好,写进元数据和正文。正文可以是 Markdown 简报,也可以是单文件静态网页(HTML,见 §6.1)。

只想传任意文件到坚果云?看 §5 的 Shell 上传工具。想让 App 读到内容?先读 §1 的逻辑,再用 §5 的工具。

1. 先理解这套逻辑(决定你每次该怎么做)

1.1 两个标识,别混

标识含义谁决定
id这是哪一篇文章。跨修订保持不变(如 ai-daily-2026-10-22、weekly-2026-w43)你
content_sha256这是哪份字节。正文 UTF-8 字节的 SHA-256工具自动算

读者端的已读/收藏记录挂在 id 上;内容变化靠 content_sha256 判断。

1.2 发布前的决策表

你手里的情况怎么做工具返回
全新的一篇起一个新 id,publishpublished
修订已发布的文章(改错字、补充、更新)沿用原 id,改 Markdown,publishpublished(旧版本文件保留,Manifest 指向新文件)
与上次完全一样什么都不用做(再跑也没副作用)unchanged
新 id,但正文与某篇已发布的字节完全相同不要发。这是重复内容,工具直接拒绝写入duplicate(含 duplicate_of,即已存在的 id)
想用旧 id 发一篇完全不同的文章禁止。会覆盖该 id 的阅读状态;换新 id—

重复内容有两层防线:发布工具(duplicate,不写任何文件)和 App(读到相同 Markdown 只显示一份)。别靠 App 兜底——别制造重复。判断「是不是同一篇」用 id,不要用标题。

1.3 读者可以订阅多个 WebDAV 来源

用户可在 Siftr 里添加多个 WebDAV(例如两个坚果云目录、或坚果云加别家 WebDAV)。对你的影响:

  • 一个目录 = 一个来源 = 一份 manifest.json,一个目录只让一个发布方写。Manifest 是「读—改—写」,写前会检查变化,但没有原子锁,两个 Agent 同时写同一个目录会丢更新。想多 Agent,就一人一个目录,让用户在 App 里各添加一次。
  • 同一地址 App 只收一次(大小写、末尾斜杠、默认端口 443、「服务器路径+目录」怎么拆分都视为同一地址)。你不需要、也不应该帮用户在多个目录里重复发同一篇来「保险」。
  • 不同来源里内容完全相同的 Markdown,App 只显示一份(保留较早添加的来源那份,已读/收藏同步到两份上)。所以「同一篇发布到两个目录」没有意义,除非用户明确要求镜像备份。
  • 来源之间 id 互不冲突:甲目录的 daily 和乙目录的 daily 是两篇不同的文章,除非字节也相同。
  • 写入一个目录时只操作这个目录。不要在 SIFTR_DAV_URL 之外读写;不要把一个来源的凭据用到另一个来源。

1.4 读者端如何对待你的文件(决定了「不可变」规则)

  • 正文文件名含全文 SHA-256,一版一文件,永不改、永不删。修订就是新文件 + Manifest 改指新路径。App 要求同一路径下内容不可变,违反会被判定为损坏的 Manifest。
  • Manifest 永远最后写。读者随时可能刷新:正文先落盘并回读校验,Manifest 最后更新,读者看到的状态就总是一致的。
  • App 会把 media/ 图片按路径缓存,不会因为你覆盖同名图片而刷新(见 §7)。

2. 凭据与上传原语

运行环境通过各自的安全管理器注入三个环境变量:

变量含义示例
SIFTR_DAV_URL内容根目录完整 URL(HTTPS)https://dav.jianguoyun.com/dav/Siftr/
SIFTR_DAV_USER账号user@example.com
SIFTR_DAV_APP_PASSWORD第三方应用密码见下

坚果云 WebDAV 不接受登录密码。在网页版「账户信息 → 安全选项 → 第三方应用管理」为每个发布方单独创建应用密码(读者 App、每个 Agent 各一个,互不共用)。凭据只从环境变量读取,绝不写入 Markdown、元数据、仓库、日志、命令行字面值或你的回复。

2.1 用户尚未配置时(先完成这一步)

先说明:“Siftr 需要你自己的 WebDAV 空间来接收内容。你可以使用坚果云;我可以先准备报告,连接后再发布。密码请填写到本机或平台的安全配置入口,不要发到聊天里。”

  1. 引导用户到 坚果云官网 注册或登录,参照 官方第三方应用授权教程 创建应用密码。网页路径为“账户信息 → 安全选项 → 第三方应用管理 → 添加应用密码”。Agent 应先打开官方教程核对当前界面;界面或套餐限制变化时以官方帮助为准。
  2. 确定专用目录,例如 Siftr。坚果云服务器地址为 https://dav.jianguoyun.com/dav/,本工具使用的完整内容根目录为 https://dav.jianguoyun.com/dav/Siftr/。不要填官网网址、分享链接或 manifest.json 地址。初次配置优先使用单层 ASCII 目录;嵌套目录的上级需用户提前创建,工具不会越过内容根目录创建父级。
  3. 在 Agent 平台的秘密管理/环境变量设置里保存 §2 三项。用户填写账号与应用密码;不要使用登录密码、提交到代码库或写进定时任务提示词。平台没有安全输入入口时,由用户在自己的终端配置;不要要求把密码贴回对话。
  4. 在 Siftr 添加 WebDAV 来源:服务器 https://dav.jianguoyun.com/dav/、目录 Siftr,填写同一账号及为 App 单独生成的应用密码。Agent 和 App 的有效目录必须相同,不要重复拼上两次 Siftr。
  5. 运行 status,按结果处理认证、权限或空目录。获得发布授权后上传用户需要的第一篇内容,再请用户在 App 刷新确认;不要为了连接检查擅自留下测试文章。

其他提供商也使用 HTTPS、账号和应用密码(或其官方要求的 WebDAV 密码),以该服务官方文档为准。不要把坚果云特有的 409 行为推断为所有 WebDAV 服务的规则。

任意文件上传可用 §5 的 Shell 工具;它不更新内容索引,上传文件并不等于在 Siftr 发布了文章。

坚果云 WebDAV 行为速查(2026-10 实测)

场景实际返回
认证HTTPS + Basic(realm="nutstore"),HTTP/2
凭据错误401,空响应体
父目录不存在时 GET/PUT409 AncestorsNotFound——不是 404,先 MKCOL(注意:MKCOL 的 URL 不能带尾部斜杠,…/Siftr 成功、…/Siftr/ 返回 409;Python 工具已处理)
GET 目录403;列目录必须 PROPFIND
频率/流量超限429/5xx;指数退避,几次后放弃

3. 云端目录结构(Siftr 内容协议 v2)

$SIFTR_DAV_URL(如 https://dav.jianguoyun.com/dav/Siftr/)
├── manifest.json                      # 索引:App 的唯一入口
├── documents/
│   ├── <id前缀>_<内容SHA-256>.md       # format = markdown,不可变,一版一文件
│   └── <id前缀>_<内容SHA-256>.html     # format = html,同样不可变
└── media/                             # 可选:Markdown 引用的私有图片(App 认证加载)
    └── <名称>.png|.jpg|.jpeg|.gif|.webp

manifest.json 顶层:

字段类型约束
schema_versionint固定 2(读到 1 时 Python 工具会自动升级,见下)
generated_atstring带时区 ISO 8601;整份 ≤ 2 MiB
publisherstring?发布方标识,可选
documentsarray≤ 5000 条,按 published_at 倒序;id 不得重复

documents[] 每条:

字段类型约束
idstringASCII [A-Za-z0-9._-],≤ 200 字符,跨修订不变
formatstring必填:markdown 或 html(工具按正文后缀自动填写)
title / subtitlestring / string?标题非空且 ≤ 200 字符
summarystring≤ 1000 字符;App 列表和搜索都用它,认真写
categorystring非空;App 用它做筛选
tags[string]?可选;App 用它做筛选和搜索
published_at / updated_atstring带时区 ISO 8601(2026-10-13T09:00:00+08:00);published_at 决定排序与按月归档
pathstringdocuments/<id>_<sha256>.<md|html>,由工具生成;后缀必须与 format 一致
content_sha256string正文原始字节的 SHA-256(64 位小写十六进制)
size_bytesint真实字节数,单篇 ≤ 5 MiB(两种格式相同)
reading_minutes / source_countint?非负
format_optionsobject?仅供对应格式的阅读器使用;markdown、html 目前没有定义,不要写

硬性规则:正文是 documents/ 下非空的 UTF-8 文件,≤ 5 MiB;正文与链接里不得出现凭据;时间戳必须带时区。alternates 是协议保留字段,不要写。

版本与旧数据:v1(schema_version: 1,没有 format)只有 Markdown。Python 工具读到 v1 时,会给每条补上 "format": "markdown",下次发布时以 v2 写回(结果里带 upgraded_to_schema_version: 2)。Siftr 1.0.0 只认识 v1,看到 v2 会提示升级 App;所以在用户换成支持 v2 的版本之前,不要往他的目录发布。status 只能检查远端协议,不能判断用户安装的 App 版本;旧目录升级前让用户确认已更新 App。

兼容未来格式:Manifest 里出现你不认识的 format(比如以后的 audio),不要删也不要改,原样保留。Python 工具会自动这样处理。

4. 元数据 JSON(--metadata)

{
  "id": "ai-daily-2026-10-13",
  "title": "AI 每日简报:把信息读成理解",
  "summary": "三句话说清今天发生了什么、为什么重要。",
  "category": "AI 资讯",
  "tags": ["Agent", "模型"],
  "published_at": "2026-10-13T08:30:00+08:00",
  "updated_at": "2026-10-13T08:30:00+08:00",
  "reading_minutes": 3,
  "source_count": 5
}

必填:id、title、summary、category、published_at、updated_at。可选:subtitle、tags、reading_minutes、source_count、publisher。format、path、content_sha256、size_bytes 由工具填,不要手写;如果写了 format,必须和正文格式一致。修订时 published_at 保持首次发布时间,updated_at 写本次时间。

5. 发布路径:按运行环境任选

5.0 Python 3.10+(推荐)

scripts/siftr_publish.py(纯标准库,内置校验、幂等、重复检测、回读与退避,适合无人值守任务):

python3 scripts/siftr_publish.py validate --markdown out/daily.md --metadata out/daily.json   # 完全离线
python3 scripts/siftr_publish.py status                                                        # 远端概况:schema_version、篇数、各格式数量
python3 scripts/siftr_publish.py publish  --markdown out/daily.md --metadata out/daily.json
python3 scripts/siftr_publish.py publish  --html out/sort-demo.html --metadata out/sort-demo.json   # 单文件静态网页
python3 scripts/siftr_publish.py publish  --file out/anything.md --metadata out/meta.json           # 按后缀识别格式

5.1 从空目录验证一条完整链路(不联网)

以下示例创建 out/ 下的 Markdown、HTML 与对应元数据;若同名文件已存在则停止,避免覆盖用户产物。示例只演示格式,正式发布时改成用户实际需要的内容。运行目录为解压后的 siftr-publish/。

python3 - <<'PYTHON'
from pathlib import Path
from datetime import datetime, timezone
import json

out = Path("out")
out.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
samples = {
    "sample-report": ("md", "示例报告", "## 本次结果\n\n这里填写本次任务的结论与来源。\n"),
    "sample-page": ("html", "示例网页", '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>示例网页</title><body><main><h1>本次结果</h1><p>这里填写报告正文。</p></main></body></html>'),
}
files = {}
for doc_id, (suffix, title, text) in samples.items():
    files[out / (doc_id + "." + suffix)] = text
    files[out / (doc_id + ".json")] = json.dumps({
        "id": doc_id, "title": title, "summary": "演示发布格式。",
        "category": "示例", "published_at": stamp, "updated_at": stamp,
    }, ensure_ascii=False, indent=2)
if any(path.exists() for path in files):
    raise SystemExit("示例文件已存在;请复用或换一个工作目录。")
for path, text in files.items():
    path.write_text(text, encoding="utf-8")
PYTHON
python3 scripts/siftr_publish.py validate --markdown out/sample-report.md --metadata out/sample-report.json
python3 scripts/siftr_publish.py validate --html out/sample-page.html --metadata out/sample-page.json

用户授权发布实际内容后,将 validate 改为 publish。不要把这两篇示例自动发布到用户目录,也不要把示例 id 用作正式日报的固定 id。

publish 的 JSON 结果与你该做的事:

result含义你该做的
published新文章或修订已写入并回读确认汇报 id、篇数、generated_at
unchanged远端已是这份内容汇报「无变化」,不重试
duplicate另一个 id(duplicate_of)已有完全相同的正文,什么也没写不要换 id 重发。若本意是修订那一篇,用 duplicate_of 的 id 并修改内容;否则汇报「重复,已跳过」
非零退出 + 错误信息校验、认证、网络或回读失败见 §0.2、§9;Manifest 写入后的失败可能已生效,保留输入核对重试

只在明确授权的发布请求或既定计划任务里执行 publish。

5.2 纯 shell(没有 Python 的主机)

用 scripts/siftr_upload.sh(POSIX sh + curl + sha256sum 或 shasum,创建根目录及其下的父目录并做 SHA-256 回读)。这是低层上传器:会覆盖同名目标,不提供索引合并、去重或不可变保护;文章发布优先使用 Python。上传图片要使用全新文件名,不用此工具覆盖历史正文。每个请求有超时,失败由调用方检查后有限重试:

sh scripts/siftr_upload.sh out/daily_3f2a1b.md documents/daily_3f2a1b.md
sh scripts/siftr_upload.sh chart.jpg media/chart.jpg

manifest.json 由你按 §1、§3 手工编排:GET 旧 Manifest → 本地合并 → 上传正文并确认回读哈希一致 → 最后 PUT manifest.json → GET 复核。合并时务必自己做 Python 版做的事:

  1. 旧 Manifest 是 v1 时:每条补 "format": "markdown",顶层改为 "schema_version": 2。
  2. 同 id 替换旧条目(沿用 id,更新 format、path、content_sha256、size_bytes、updated_at)。同一个 id 从 Markdown 改成 HTML 也属于修订。
  3. 先查重:如果另一个 id 已有相同的 format + content_sha256,就停止,按 duplicate 处理。
  4. 不认识的 format 条目原样保留。
  5. 按 published_at 倒序,刷新 generated_at,Manifest ≤ 2 MiB、≤ 5000 条。

6. Markdown 写作规范(按 App 渲染器实际能力)

支持:h1–h6、粗/斜体、删除线、行内代码、有序/无序列表、任务列表(- [ ] / - [x])、引用块、提示块(引用块首行写 [!NOTE] 等)、表格(横向滚动)、代码块(显示语言名、可一键复制;代码横向滚动,text/markdown 或不标语言的块自动换行)、分隔线、可点击链接、图片(圆角,可点开放大)、脚注([^1],显示为上标数字,正文末尾汇总为「参考」)、LaTeX 公式($…$、$$…$$)。正文 18pt,随系统动态字号;读者可选黑体或宋体。

  • 分节用 ## 与 ###;不要用 #(与文章大标题重复;开头与标题相同的 # 会被 App 自动隐藏)。每个标题都会成为 App 里的目录条目,所以小节标题要简短、有信息、在同一篇里不重复。
  • 首段直接给结论,段落短小;关键引语放引用块,对比信息用表格,命令与代码放代码块并在开头围栏后标注语言(如 python、bash),App 会显示语言名。
  • 需要读者特别留意的一句话用 GitHub 提示块(示例见下),类型只有 NOTE(说明)、TIP(提示)、IMPORTANT(重要)、WARNING(注意)、CAUTION(警告);类型后面可以跟一个自定义标题。每篇最多两三个,滥用会失去重点。
  • 待办或核对清单用任务列表;已完成项会以勾选、弱化显示。
  • 图片独占一段(前后空行)时按整幅配图显示;![替代文字](地址 "图注") 中的引号部分显示为图注,没有图注时显示替代文字。
  • 链接锚文本要有意义,必须是 http(s);App 会拦截指向用户 WebDAV 主机的链接,所以不要链接到你自己的 WebDAV 地址。
  • 引用同一内容空间里的另一篇时写 [上一期](documents/<那篇的 id>.md)(例如 documents/ai-daily-2026-10-13.md);链接到网页就用 .html 后缀,比如 documents/sort-demo.html。App 会在内部直接打开那篇,修订后链接依然有效。该文章必须仍在 Manifest 里,否则读者会看到“这篇文章不在当前列表中”。
  • 段内不折行:一个段落写成一行,段与段之间空一行。段内的硬换行会被渲染成空格,中文里就成了多余的空隙。
  • 全角标点后不加空格:。、,、:、) 等后面直接接下一个字;中文与英文、数字之间是否加空格保持全文一致即可。
  • 写金额直接写 $20。只有整篇的 $ 都能两两配成公式($ 内侧不贴空格、右侧不紧跟数字)时 App 才渲染公式,所以公式和美元金额不要出现在同一篇里;需要时把金额写成「20 美元」或 USD 20。
  • Markdown 里不写 HTML(要做网页就用 §6.1 的 HTML 格式);不要在正文里放凭据。
  • App 支持全文搜索:把读者会搜的专有名词、产品名、人名写进正文(而不只写在图里)。
  • 不要写「相关推荐」「点此提问」「朗读」之类依赖 App 功能的话——这些功能不存在。
> [!TIP] 读这一节的方法
> 先看表格里的价格,再看备注。

6.1 HTML 静态网页(format: html)

适合交互演示、可视化、动画讲解这类用 Markdown 表达不出来的内容。App 用隔离的 WebKit 打开,打开即运行。

  • 单个 .html 文件,CSS 和 JS 写在文件里。不要拆出相对路径的 css、js 或图片,App 不会加载它们。
  • 可以从公共 CDN 引用库、字体和图片,但必须用 https://。CDN 资源由读者手机直接请求,不经过坚果云,也不占坚果云流量。发布工具会拒绝 http:// 资源。
  • 外部库固定版本;不依赖联网也应能看到核心结论。不要把私有报告数据发送给外部脚本或接口。发布校验只做静态检查,不证明 JS 运行正常,也不是完整安全审计;有交互时在浏览器验证操作、窄屏和资源加载,并区分浏览器测试与 Siftr 真机验证。
  • 不要依赖后端:没有服务器,没有登录,表单提交不会有响应。
  • 不要引用你的 WebDAV 地址:App 会拦截所有指向用户 WebDAV 主机的请求,发布工具也会拒绝。私有图片请改用公开地址,或者转成 data URI 内联。
  • 页面里的外链会在系统浏览器里打开,target=_blank 也一样。页内锚点(#step-2)照常可用。
  • 移动端优先:读者在手机上竖屏阅读。写上 <meta name="viewport" content="width=device-width, initial-scale=1">(漏写的话 App 会自动补上),布局要能适应 320–430pt 的宽度,可点控件不小于 44pt。
  • 支持深色模式更好:用 @media (prefers-color-scheme: dark)。App 不会改网页的样式。
  • 搜索:App 会提取页面里的可见文字参与全文搜索,JS 动态生成的文字搜不到。关键名词要写在 HTML 里,同时写进 summary 和 tags。
  • 标题写在元数据的 title 里,列表和阅读页都用它;页面里的 <title> 不会显示。
  • 讲解和演示分开时:讲解写成 Markdown,演示写成 HTML,分别用两个 id 发布。然后在 Markdown 里用 [打开演示](documents/<演示的 id>.html) 链接过去,App 会在站内打开。

7. 图片策略

坚果云流量配额宝贵,按优先级:

  1. 首选公开网络链接:![图表](https://example.com/chart.png),不消耗坚果云流量。
  2. 确需私有图片才上传 media/,且必须先压缩:长边 ≤ 1600px、JPEG 质量 70–80(或 WebP 80),单张目标 ≤ 300 KB。10 MiB 的协议上限只是兜底。
magick in.png -resize '1600x1600>' -quality 75 out.jpg                 # ImageMagick(v6 用 convert)
sips -Z 1600 -s format jpeg -s formatOptions 72 in.png --out out.jpg   # macOS 自带

私有图片用纯相对路径引用:![架构图](media/architecture.png)。App 用该来源的凭据认证加载并缓存。

  • media/ 只允许 png/jpg/jpeg/gif/webp;路径禁止 ..、%、空格与控制字符。发布工具会拒绝违规引用。
  • 图片不进 Manifest,App 按路径按需加载。
  • 缓存以路径为键:覆盖同名图片不会刷新已读设备。改图用新文件名(chart-v2.png)并发布正文修订,旧图保留。

8. 纪律与汇报

  • 发布前先 validate。脚本自带凭据泄露检查(Authorization、URL 内嵌账号密码、SIFTR_DAV_APP_PASSWORD= 等直接拒绝)。
  • 永不删除远端历史文件;永不覆盖已有正文路径。
  • 一个目录只有一个写入方(§1.3)。
  • 汇报只含:命令结果(published/unchanged/duplicate/错误类别)、文档 id、篇数、时间。不含凭据、账号、私有本地路径。

9. 排错速查

现象原因做法
认证失败 / 401用了登录密码,或应用密码已撤销让用户重新创建第三方应用密码
409 AncestorsNotFound父目录不存在MKCOL 后重试(Python 工具把 409 与 404 同样视为「还没有 Manifest」,首次发布会自动初始化目录)
429 / 5xx限流退避重试;仍失败就放弃,下次计划任务再试
远端 Manifest 格式错误;已停止发布远端 manifest.json 损坏或被他人改过不要覆盖。汇报给用户,由人决定如何恢复
Manifest 中存在重复 ID远端被手工改坏同上,不要自行覆盖
正文回读校验失败上传被截断/中途改写Manifest 未改动;直接重试
duplicate内容与别的 id 相同§1.2:不要重发
HTML 不能从 http:// 加载资源页面引用了明文资源改成 https:// 的地址
正文引用了内容空间所在的 WebDAV 主机正文里写了用户的坚果云地址删掉;私有图片改用公开地址或 data URI
不支持的 Manifest 协议版本远端是比工具更新的协议不要覆盖;更新 Skill 后再发布
读者 App 提示“需要更新版本”目录已升级到 v2,读者还在用 1.0.0让用户更新 Siftr
App 里没看到新内容读者需要手动下拉刷新;或用户没添加这个目录确认目录地址与用户在 App 里添加的一致

Skill de publicación

Versión del skill: 22 de octubre de 2026

Envía esta página a tu agente de IA y podrá publicar boletines en Markdown en tu propio cloud WebDAV (Nutstore), exactamente en el formato que Siftr espera. Ni el skill ni el paquete contienen credenciales; la contraseña de app la inyecta el entorno del agente.

Instalación

El paquete es un zip que se despliega en una carpeta siftr-publish/ completa (SKILL.md y dos scripts). Suéltala en el directorio skills de tu agente — habitualmente ~/.agents/skills/, ~/.zcode/skills/ o ~/.claude/skills/; consulta la documentación de tu agente.

Envía simplemente esta frase a tu agente:

Instala este skill: descarga https://chenhk.cn/Siftr/assets/siftr-publish.zip y descomprímelo, coloca la carpeta siftr-publish/ resultante en tu directorio skills y sigue su SKILL.md para publicar contenido de Siftr para mí.

Descargar el paquete (siftr-publish.zip) · Las tres variables de entorno necesarias están en §2 abajo.

Texto completo del skill

Idéntico al SKILL.md del paquete (generado desde skills/siftr-publish/ por scripts/package_skill.py, sin deriva).

Siftr 内容发布(面向 Agent)

0. 给收到这个链接的 Agent

把 Siftr 当成用户手机上的内容交付渠道:你负责生成内容并发布到用户自己的 WebDAV,用户在 Siftr 添加该来源后阅读。适合日报、周报、监控摘要、研究报告、旅行安排,以及单文件 HTML 图表或交互讲解。用户只发来本 Skill 链接时,先简短解释用途并检查是否已配置;链接本身不代表已经授权定时运行或上传测试内容。

  • 一次性报告:生成 Markdown,使用 §4 的元数据与 §5 的发布工具交付。
  • 定时任务产出:用所在平台的定时任务能力执行生成与发布。Siftr 不执行任务,也不保证定时推送通知。确认内容、频率、时间及 IANA 时区(如 Asia/Shanghai),并把发布步骤、Skill/脚本绝对路径、输出路径与凭据注入方式写入任务配置。没有调度能力时说明限制,保留可运行的发布命令,不要声称已设好定时任务。
  • HTML 网页:用于自包含的交互内容,按 §6.1 编写。它是 App 内阅读页面,不是公网网站托管;不要把私有 WebDAV 地址当成可分享的网页链接。用户要公开网站或后端服务时,需要另选托管方案。

0.1 首次使用的顺序

  1. 若只拿到网页说明,下载 完整 Skill 安装包,解压并阅读其中的 SKILL.md。脚本在解压后的 siftr-publish/scripts/;以下命令相对于该目录的父级 Skill 根目录执行。不要假设用户当前目录已有脚本;自动任务使用绝对路径。需要 Python 3.10+,无需 pip 包。
  2. 只检查 §2 的三个环境变量是否存在,不要打印值。未配置时按 §2.1 引导;先在本地准备和校验内容,不反复尝试联网发布。
  3. 已配置后运行 status。ready 表示远端索引可读;waiting_for_first_publish 表示尚无索引或父目录,不证明写权限正常。两者都不证明用户已连接 App,也无法判断 App 版本。
  4. 在已有发布授权下先 validate 再 publish。成功后告诉用户标题、文档 id 与发布结果,让用户在 Siftr 对应来源下拉刷新。只有收到回读确认才能说“已发布”;没有实际查看 App 时不要说“手机已经收到”。
  5. 定时任务必须在它自己的运行环境里取得凭据;当前终端配置不会自动传给后台任务。同一来源的所有任务串行执行,避免重叠。首次定时运行前,用同一执行环境验证一次。

0.2 每期内容与重试

日报使用 ai-daily-2026-10-22,周报使用 weekly-2026-w43;日期以用户时区和本次任务的逻辑周期为准。只有持续更新的同一份报告才用固定 id。重试保留本次正文、元数据、id 和时间戳,不要重新生成一篇或换 id。修订保留首次 published_at,只更新 updated_at。重复任务运行若不断刷新 updated_at,会造成不必要的索引更新。

保留本地产物直到回读确认。发布失败的处理按阶段区分:正文阶段失败时索引未更新;一旦尝试 PUT Manifest,超时或回读失败都可能发生在远端已经写入之后。此时用相同输入重新核对/发布,不要删除远端内容或宣称回滚。脚本每个网络请求最多尝试三次;任务平台也应限制重试次数并串行执行,不要无限重试。

你是 唯一的写入方。Siftr App 只读:它 GET manifest.json,对比后再 GET 需要的正文。App 不会替你总结、配图、推荐、朗读或追问——这些都不做。摘要、分类、标签、标题都由你写好,写进元数据和正文。正文可以是 Markdown 简报,也可以是单文件静态网页(HTML,见 §6.1)。

只想传任意文件到坚果云?看 §5 的 Shell 上传工具。想让 App 读到内容?先读 §1 的逻辑,再用 §5 的工具。

1. 先理解这套逻辑(决定你每次该怎么做)

1.1 两个标识,别混

标识含义谁决定
id这是哪一篇文章。跨修订保持不变(如 ai-daily-2026-10-22、weekly-2026-w43)你
content_sha256这是哪份字节。正文 UTF-8 字节的 SHA-256工具自动算

读者端的已读/收藏记录挂在 id 上;内容变化靠 content_sha256 判断。

1.2 发布前的决策表

你手里的情况怎么做工具返回
全新的一篇起一个新 id,publishpublished
修订已发布的文章(改错字、补充、更新)沿用原 id,改 Markdown,publishpublished(旧版本文件保留,Manifest 指向新文件)
与上次完全一样什么都不用做(再跑也没副作用)unchanged
新 id,但正文与某篇已发布的字节完全相同不要发。这是重复内容,工具直接拒绝写入duplicate(含 duplicate_of,即已存在的 id)
想用旧 id 发一篇完全不同的文章禁止。会覆盖该 id 的阅读状态;换新 id—

重复内容有两层防线:发布工具(duplicate,不写任何文件)和 App(读到相同 Markdown 只显示一份)。别靠 App 兜底——别制造重复。判断「是不是同一篇」用 id,不要用标题。

1.3 读者可以订阅多个 WebDAV 来源

用户可在 Siftr 里添加多个 WebDAV(例如两个坚果云目录、或坚果云加别家 WebDAV)。对你的影响:

  • 一个目录 = 一个来源 = 一份 manifest.json,一个目录只让一个发布方写。Manifest 是「读—改—写」,写前会检查变化,但没有原子锁,两个 Agent 同时写同一个目录会丢更新。想多 Agent,就一人一个目录,让用户在 App 里各添加一次。
  • 同一地址 App 只收一次(大小写、末尾斜杠、默认端口 443、「服务器路径+目录」怎么拆分都视为同一地址)。你不需要、也不应该帮用户在多个目录里重复发同一篇来「保险」。
  • 不同来源里内容完全相同的 Markdown,App 只显示一份(保留较早添加的来源那份,已读/收藏同步到两份上)。所以「同一篇发布到两个目录」没有意义,除非用户明确要求镜像备份。
  • 来源之间 id 互不冲突:甲目录的 daily 和乙目录的 daily 是两篇不同的文章,除非字节也相同。
  • 写入一个目录时只操作这个目录。不要在 SIFTR_DAV_URL 之外读写;不要把一个来源的凭据用到另一个来源。

1.4 读者端如何对待你的文件(决定了「不可变」规则)

  • 正文文件名含全文 SHA-256,一版一文件,永不改、永不删。修订就是新文件 + Manifest 改指新路径。App 要求同一路径下内容不可变,违反会被判定为损坏的 Manifest。
  • Manifest 永远最后写。读者随时可能刷新:正文先落盘并回读校验,Manifest 最后更新,读者看到的状态就总是一致的。
  • App 会把 media/ 图片按路径缓存,不会因为你覆盖同名图片而刷新(见 §7)。

2. 凭据与上传原语

运行环境通过各自的安全管理器注入三个环境变量:

变量含义示例
SIFTR_DAV_URL内容根目录完整 URL(HTTPS)https://dav.jianguoyun.com/dav/Siftr/
SIFTR_DAV_USER账号user@example.com
SIFTR_DAV_APP_PASSWORD第三方应用密码见下

坚果云 WebDAV 不接受登录密码。在网页版「账户信息 → 安全选项 → 第三方应用管理」为每个发布方单独创建应用密码(读者 App、每个 Agent 各一个,互不共用)。凭据只从环境变量读取,绝不写入 Markdown、元数据、仓库、日志、命令行字面值或你的回复。

2.1 用户尚未配置时(先完成这一步)

先说明:“Siftr 需要你自己的 WebDAV 空间来接收内容。你可以使用坚果云;我可以先准备报告,连接后再发布。密码请填写到本机或平台的安全配置入口,不要发到聊天里。”

  1. 引导用户到 坚果云官网 注册或登录,参照 官方第三方应用授权教程 创建应用密码。网页路径为“账户信息 → 安全选项 → 第三方应用管理 → 添加应用密码”。Agent 应先打开官方教程核对当前界面;界面或套餐限制变化时以官方帮助为准。
  2. 确定专用目录,例如 Siftr。坚果云服务器地址为 https://dav.jianguoyun.com/dav/,本工具使用的完整内容根目录为 https://dav.jianguoyun.com/dav/Siftr/。不要填官网网址、分享链接或 manifest.json 地址。初次配置优先使用单层 ASCII 目录;嵌套目录的上级需用户提前创建,工具不会越过内容根目录创建父级。
  3. 在 Agent 平台的秘密管理/环境变量设置里保存 §2 三项。用户填写账号与应用密码;不要使用登录密码、提交到代码库或写进定时任务提示词。平台没有安全输入入口时,由用户在自己的终端配置;不要要求把密码贴回对话。
  4. 在 Siftr 添加 WebDAV 来源:服务器 https://dav.jianguoyun.com/dav/、目录 Siftr,填写同一账号及为 App 单独生成的应用密码。Agent 和 App 的有效目录必须相同,不要重复拼上两次 Siftr。
  5. 运行 status,按结果处理认证、权限或空目录。获得发布授权后上传用户需要的第一篇内容,再请用户在 App 刷新确认;不要为了连接检查擅自留下测试文章。

其他提供商也使用 HTTPS、账号和应用密码(或其官方要求的 WebDAV 密码),以该服务官方文档为准。不要把坚果云特有的 409 行为推断为所有 WebDAV 服务的规则。

任意文件上传可用 §5 的 Shell 工具;它不更新内容索引,上传文件并不等于在 Siftr 发布了文章。

坚果云 WebDAV 行为速查(2026-10 实测)

场景实际返回
认证HTTPS + Basic(realm="nutstore"),HTTP/2
凭据错误401,空响应体
父目录不存在时 GET/PUT409 AncestorsNotFound——不是 404,先 MKCOL(注意:MKCOL 的 URL 不能带尾部斜杠,…/Siftr 成功、…/Siftr/ 返回 409;Python 工具已处理)
GET 目录403;列目录必须 PROPFIND
频率/流量超限429/5xx;指数退避,几次后放弃

3. 云端目录结构(Siftr 内容协议 v2)

$SIFTR_DAV_URL(如 https://dav.jianguoyun.com/dav/Siftr/)
├── manifest.json                      # 索引:App 的唯一入口
├── documents/
│   ├── <id前缀>_<内容SHA-256>.md       # format = markdown,不可变,一版一文件
│   └── <id前缀>_<内容SHA-256>.html     # format = html,同样不可变
└── media/                             # 可选:Markdown 引用的私有图片(App 认证加载)
    └── <名称>.png|.jpg|.jpeg|.gif|.webp

manifest.json 顶层:

字段类型约束
schema_versionint固定 2(读到 1 时 Python 工具会自动升级,见下)
generated_atstring带时区 ISO 8601;整份 ≤ 2 MiB
publisherstring?发布方标识,可选
documentsarray≤ 5000 条,按 published_at 倒序;id 不得重复

documents[] 每条:

字段类型约束
idstringASCII [A-Za-z0-9._-],≤ 200 字符,跨修订不变
formatstring必填:markdown 或 html(工具按正文后缀自动填写)
title / subtitlestring / string?标题非空且 ≤ 200 字符
summarystring≤ 1000 字符;App 列表和搜索都用它,认真写
categorystring非空;App 用它做筛选
tags[string]?可选;App 用它做筛选和搜索
published_at / updated_atstring带时区 ISO 8601(2026-10-13T09:00:00+08:00);published_at 决定排序与按月归档
pathstringdocuments/<id>_<sha256>.<md|html>,由工具生成;后缀必须与 format 一致
content_sha256string正文原始字节的 SHA-256(64 位小写十六进制)
size_bytesint真实字节数,单篇 ≤ 5 MiB(两种格式相同)
reading_minutes / source_countint?非负
format_optionsobject?仅供对应格式的阅读器使用;markdown、html 目前没有定义,不要写

硬性规则:正文是 documents/ 下非空的 UTF-8 文件,≤ 5 MiB;正文与链接里不得出现凭据;时间戳必须带时区。alternates 是协议保留字段,不要写。

版本与旧数据:v1(schema_version: 1,没有 format)只有 Markdown。Python 工具读到 v1 时,会给每条补上 "format": "markdown",下次发布时以 v2 写回(结果里带 upgraded_to_schema_version: 2)。Siftr 1.0.0 只认识 v1,看到 v2 会提示升级 App;所以在用户换成支持 v2 的版本之前,不要往他的目录发布。status 只能检查远端协议,不能判断用户安装的 App 版本;旧目录升级前让用户确认已更新 App。

兼容未来格式:Manifest 里出现你不认识的 format(比如以后的 audio),不要删也不要改,原样保留。Python 工具会自动这样处理。

4. 元数据 JSON(--metadata)

{
  "id": "ai-daily-2026-10-13",
  "title": "AI 每日简报:把信息读成理解",
  "summary": "三句话说清今天发生了什么、为什么重要。",
  "category": "AI 资讯",
  "tags": ["Agent", "模型"],
  "published_at": "2026-10-13T08:30:00+08:00",
  "updated_at": "2026-10-13T08:30:00+08:00",
  "reading_minutes": 3,
  "source_count": 5
}

必填:id、title、summary、category、published_at、updated_at。可选:subtitle、tags、reading_minutes、source_count、publisher。format、path、content_sha256、size_bytes 由工具填,不要手写;如果写了 format,必须和正文格式一致。修订时 published_at 保持首次发布时间,updated_at 写本次时间。

5. 发布路径:按运行环境任选

5.0 Python 3.10+(推荐)

scripts/siftr_publish.py(纯标准库,内置校验、幂等、重复检测、回读与退避,适合无人值守任务):

python3 scripts/siftr_publish.py validate --markdown out/daily.md --metadata out/daily.json   # 完全离线
python3 scripts/siftr_publish.py status                                                        # 远端概况:schema_version、篇数、各格式数量
python3 scripts/siftr_publish.py publish  --markdown out/daily.md --metadata out/daily.json
python3 scripts/siftr_publish.py publish  --html out/sort-demo.html --metadata out/sort-demo.json   # 单文件静态网页
python3 scripts/siftr_publish.py publish  --file out/anything.md --metadata out/meta.json           # 按后缀识别格式

5.1 从空目录验证一条完整链路(不联网)

以下示例创建 out/ 下的 Markdown、HTML 与对应元数据;若同名文件已存在则停止,避免覆盖用户产物。示例只演示格式,正式发布时改成用户实际需要的内容。运行目录为解压后的 siftr-publish/。

python3 - <<'PYTHON'
from pathlib import Path
from datetime import datetime, timezone
import json

out = Path("out")
out.mkdir(exist_ok=True)
stamp = datetime.now(timezone.utc).isoformat()
samples = {
    "sample-report": ("md", "示例报告", "## 本次结果\n\n这里填写本次任务的结论与来源。\n"),
    "sample-page": ("html", "示例网页", '<!doctype html><html lang="zh-CN"><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>示例网页</title><body><main><h1>本次结果</h1><p>这里填写报告正文。</p></main></body></html>'),
}
files = {}
for doc_id, (suffix, title, text) in samples.items():
    files[out / (doc_id + "." + suffix)] = text
    files[out / (doc_id + ".json")] = json.dumps({
        "id": doc_id, "title": title, "summary": "演示发布格式。",
        "category": "示例", "published_at": stamp, "updated_at": stamp,
    }, ensure_ascii=False, indent=2)
if any(path.exists() for path in files):
    raise SystemExit("示例文件已存在;请复用或换一个工作目录。")
for path, text in files.items():
    path.write_text(text, encoding="utf-8")
PYTHON
python3 scripts/siftr_publish.py validate --markdown out/sample-report.md --metadata out/sample-report.json
python3 scripts/siftr_publish.py validate --html out/sample-page.html --metadata out/sample-page.json

用户授权发布实际内容后,将 validate 改为 publish。不要把这两篇示例自动发布到用户目录,也不要把示例 id 用作正式日报的固定 id。

publish 的 JSON 结果与你该做的事:

result含义你该做的
published新文章或修订已写入并回读确认汇报 id、篇数、generated_at
unchanged远端已是这份内容汇报「无变化」,不重试
duplicate另一个 id(duplicate_of)已有完全相同的正文,什么也没写不要换 id 重发。若本意是修订那一篇,用 duplicate_of 的 id 并修改内容;否则汇报「重复,已跳过」
非零退出 + 错误信息校验、认证、网络或回读失败见 §0.2、§9;Manifest 写入后的失败可能已生效,保留输入核对重试

只在明确授权的发布请求或既定计划任务里执行 publish。

5.2 纯 shell(没有 Python 的主机)

用 scripts/siftr_upload.sh(POSIX sh + curl + sha256sum 或 shasum,创建根目录及其下的父目录并做 SHA-256 回读)。这是低层上传器:会覆盖同名目标,不提供索引合并、去重或不可变保护;文章发布优先使用 Python。上传图片要使用全新文件名,不用此工具覆盖历史正文。每个请求有超时,失败由调用方检查后有限重试:

sh scripts/siftr_upload.sh out/daily_3f2a1b.md documents/daily_3f2a1b.md
sh scripts/siftr_upload.sh chart.jpg media/chart.jpg

manifest.json 由你按 §1、§3 手工编排:GET 旧 Manifest → 本地合并 → 上传正文并确认回读哈希一致 → 最后 PUT manifest.json → GET 复核。合并时务必自己做 Python 版做的事:

  1. 旧 Manifest 是 v1 时:每条补 "format": "markdown",顶层改为 "schema_version": 2。
  2. 同 id 替换旧条目(沿用 id,更新 format、path、content_sha256、size_bytes、updated_at)。同一个 id 从 Markdown 改成 HTML 也属于修订。
  3. 先查重:如果另一个 id 已有相同的 format + content_sha256,就停止,按 duplicate 处理。
  4. 不认识的 format 条目原样保留。
  5. 按 published_at 倒序,刷新 generated_at,Manifest ≤ 2 MiB、≤ 5000 条。

6. Markdown 写作规范(按 App 渲染器实际能力)

支持:h1–h6、粗/斜体、删除线、行内代码、有序/无序列表、任务列表(- [ ] / - [x])、引用块、提示块(引用块首行写 [!NOTE] 等)、表格(横向滚动)、代码块(显示语言名、可一键复制;代码横向滚动,text/markdown 或不标语言的块自动换行)、分隔线、可点击链接、图片(圆角,可点开放大)、脚注([^1],显示为上标数字,正文末尾汇总为「参考」)、LaTeX 公式($…$、$$…$$)。正文 18pt,随系统动态字号;读者可选黑体或宋体。

  • 分节用 ## 与 ###;不要用 #(与文章大标题重复;开头与标题相同的 # 会被 App 自动隐藏)。每个标题都会成为 App 里的目录条目,所以小节标题要简短、有信息、在同一篇里不重复。
  • 首段直接给结论,段落短小;关键引语放引用块,对比信息用表格,命令与代码放代码块并在开头围栏后标注语言(如 python、bash),App 会显示语言名。
  • 需要读者特别留意的一句话用 GitHub 提示块(示例见下),类型只有 NOTE(说明)、TIP(提示)、IMPORTANT(重要)、WARNING(注意)、CAUTION(警告);类型后面可以跟一个自定义标题。每篇最多两三个,滥用会失去重点。
  • 待办或核对清单用任务列表;已完成项会以勾选、弱化显示。
  • 图片独占一段(前后空行)时按整幅配图显示;![替代文字](地址 "图注") 中的引号部分显示为图注,没有图注时显示替代文字。
  • 链接锚文本要有意义,必须是 http(s);App 会拦截指向用户 WebDAV 主机的链接,所以不要链接到你自己的 WebDAV 地址。
  • 引用同一内容空间里的另一篇时写 [上一期](documents/<那篇的 id>.md)(例如 documents/ai-daily-2026-10-13.md);链接到网页就用 .html 后缀,比如 documents/sort-demo.html。App 会在内部直接打开那篇,修订后链接依然有效。该文章必须仍在 Manifest 里,否则读者会看到“这篇文章不在当前列表中”。
  • 段内不折行:一个段落写成一行,段与段之间空一行。段内的硬换行会被渲染成空格,中文里就成了多余的空隙。
  • 全角标点后不加空格:。、,、:、) 等后面直接接下一个字;中文与英文、数字之间是否加空格保持全文一致即可。
  • 写金额直接写 $20。只有整篇的 $ 都能两两配成公式($ 内侧不贴空格、右侧不紧跟数字)时 App 才渲染公式,所以公式和美元金额不要出现在同一篇里;需要时把金额写成「20 美元」或 USD 20。
  • Markdown 里不写 HTML(要做网页就用 §6.1 的 HTML 格式);不要在正文里放凭据。
  • App 支持全文搜索:把读者会搜的专有名词、产品名、人名写进正文(而不只写在图里)。
  • 不要写「相关推荐」「点此提问」「朗读」之类依赖 App 功能的话——这些功能不存在。
> [!TIP] 读这一节的方法
> 先看表格里的价格,再看备注。

6.1 HTML 静态网页(format: html)

适合交互演示、可视化、动画讲解这类用 Markdown 表达不出来的内容。App 用隔离的 WebKit 打开,打开即运行。

  • 单个 .html 文件,CSS 和 JS 写在文件里。不要拆出相对路径的 css、js 或图片,App 不会加载它们。
  • 可以从公共 CDN 引用库、字体和图片,但必须用 https://。CDN 资源由读者手机直接请求,不经过坚果云,也不占坚果云流量。发布工具会拒绝 http:// 资源。
  • 外部库固定版本;不依赖联网也应能看到核心结论。不要把私有报告数据发送给外部脚本或接口。发布校验只做静态检查,不证明 JS 运行正常,也不是完整安全审计;有交互时在浏览器验证操作、窄屏和资源加载,并区分浏览器测试与 Siftr 真机验证。
  • 不要依赖后端:没有服务器,没有登录,表单提交不会有响应。
  • 不要引用你的 WebDAV 地址:App 会拦截所有指向用户 WebDAV 主机的请求,发布工具也会拒绝。私有图片请改用公开地址,或者转成 data URI 内联。
  • 页面里的外链会在系统浏览器里打开,target=_blank 也一样。页内锚点(#step-2)照常可用。
  • 移动端优先:读者在手机上竖屏阅读。写上 <meta name="viewport" content="width=device-width, initial-scale=1">(漏写的话 App 会自动补上),布局要能适应 320–430pt 的宽度,可点控件不小于 44pt。
  • 支持深色模式更好:用 @media (prefers-color-scheme: dark)。App 不会改网页的样式。
  • 搜索:App 会提取页面里的可见文字参与全文搜索,JS 动态生成的文字搜不到。关键名词要写在 HTML 里,同时写进 summary 和 tags。
  • 标题写在元数据的 title 里,列表和阅读页都用它;页面里的 <title> 不会显示。
  • 讲解和演示分开时:讲解写成 Markdown,演示写成 HTML,分别用两个 id 发布。然后在 Markdown 里用 [打开演示](documents/<演示的 id>.html) 链接过去,App 会在站内打开。

7. 图片策略

坚果云流量配额宝贵,按优先级:

  1. 首选公开网络链接:![图表](https://example.com/chart.png),不消耗坚果云流量。
  2. 确需私有图片才上传 media/,且必须先压缩:长边 ≤ 1600px、JPEG 质量 70–80(或 WebP 80),单张目标 ≤ 300 KB。10 MiB 的协议上限只是兜底。
magick in.png -resize '1600x1600>' -quality 75 out.jpg                 # ImageMagick(v6 用 convert)
sips -Z 1600 -s format jpeg -s formatOptions 72 in.png --out out.jpg   # macOS 自带

私有图片用纯相对路径引用:![架构图](media/architecture.png)。App 用该来源的凭据认证加载并缓存。

  • media/ 只允许 png/jpg/jpeg/gif/webp;路径禁止 ..、%、空格与控制字符。发布工具会拒绝违规引用。
  • 图片不进 Manifest,App 按路径按需加载。
  • 缓存以路径为键:覆盖同名图片不会刷新已读设备。改图用新文件名(chart-v2.png)并发布正文修订,旧图保留。

8. 纪律与汇报

  • 发布前先 validate。脚本自带凭据泄露检查(Authorization、URL 内嵌账号密码、SIFTR_DAV_APP_PASSWORD= 等直接拒绝)。
  • 永不删除远端历史文件;永不覆盖已有正文路径。
  • 一个目录只有一个写入方(§1.3)。
  • 汇报只含:命令结果(published/unchanged/duplicate/错误类别)、文档 id、篇数、时间。不含凭据、账号、私有本地路径。

9. 排错速查

现象原因做法
认证失败 / 401用了登录密码,或应用密码已撤销让用户重新创建第三方应用密码
409 AncestorsNotFound父目录不存在MKCOL 后重试(Python 工具把 409 与 404 同样视为「还没有 Manifest」,首次发布会自动初始化目录)
429 / 5xx限流退避重试;仍失败就放弃,下次计划任务再试
远端 Manifest 格式错误;已停止发布远端 manifest.json 损坏或被他人改过不要覆盖。汇报给用户,由人决定如何恢复
Manifest 中存在重复 ID远端被手工改坏同上,不要自行覆盖
正文回读校验失败上传被截断/中途改写Manifest 未改动;直接重试
duplicate内容与别的 id 相同§1.2:不要重发
HTML 不能从 http:// 加载资源页面引用了明文资源改成 https:// 的地址
正文引用了内容空间所在的 WebDAV 主机正文里写了用户的坚果云地址删掉;私有图片改用公开地址或 data URI
不支持的 Manifest 协议版本远端是比工具更新的协议不要覆盖;更新 Skill 后再发布
读者 App 提示“需要更新版本”目录已升级到 v2,读者还在用 1.0.0让用户更新 Siftr
App 里没看到新内容读者需要手动下拉刷新;或用户没添加这个目录确认目录地址与用户在 App 里添加的一致