# 自动发布连接器如何建立一条可检查的证据链

给桌面应用接入一个新的发布服务，最容易写出的版本通常只有几十行：拼请求、带令牌、读取 JSON，再把返回的网址记进任务。这样的代码在演示里很顺，却很难回答几个关键问题：远端到底创建了什么？页面是否属于当前身份？正文有没有被改写？一次超时之后还能不能安全继续？

这些问题不能靠一个“成功”布尔值解决。更稳妥的做法，是把连接器写成一条可以逐段检查的证据链。每一段只证明一件事，并明确还没有证明什么。下面用一个通用的文章发布连接器为例，说明怎样把这条链落到数据结构、持久化顺序和测试里。

## 先把结果拆成四种状态

第一步不是写 HTTP 请求，而是定义结果。至少要区分四层：

1. **请求已被本机授权**：当前任务、稿件版本、账号和目标服务仍然匹配。
2. **远端返回了创建回执**：响应符合固定协议，标识和网址可以安全解析。
3. **当前身份拥有该对象**：使用同一身份的只读列表能找到唯一对象，正文读回也一致。
4. **公开页面通过核验**：匿名访问能看到完整正文和原生链接，页面没有明确禁止索引，robots 规则也允许读取。

把四层压成一个 `published=true`，会让故障恢复变得含糊。例如，请求超时只能说明客户端没有及时拿到结果，不能说明远端没有创建对象。此时再次发送创建请求，很可能得到两个内容相同的页面。状态分层以后，程序可以停在“结果未知”，只做只读核对。

## 用不可变身份描述一次发布

一次发布需要一个不会随界面文案变化的身份。可以把下面几项放在同一个记录里：

```text
operation_id       本次操作的本机唯一编号
account_id         当前本机身份
audience           密钥被允许发送到的服务源
scopes             本次真正需要的最小权限
source_hash        经审核正文的哈希
request_hash       精确请求体的哈希
approved_at        审核时间
created_at         本机声明操作的时间
```

`source_hash` 和 `request_hash` 各有用途。前者证明正文没变，后者还覆盖标题、摘要、可见性等请求字段。审核通过后，只要任意字段变化，就必须重新审核。不要在发送前临时补标题、替换链接或改写披露句；那会让“审核过的稿件”和“真正发出的请求”变成两份内容。

账号、密钥和服务源也要一起绑定。根身份密钥只应发送给身份服务；面向具体发布服务的专用密钥，只能发送到固定 HTTPS 源，而且权限应精确到读取和创建。远端响应如果明确给出了不同的 audience 或 scope，应当立即停止，而不是把冲突字段当成“可选元数据”。

## 写请求之前，先保存独占意图

有副作用的请求必须遵循固定顺序：

```text
保存独占意图
→ 再次检查授权与稿件哈希
→ 发送一次请求
→ 加密保存有界原始响应
→ 最后解析并提升状态
```

这里最反直觉的一点，是先保存原始响应，再解析 JSON。原因很简单：响应里可能带有只出现一次的管理密钥。如果程序先解析、随后写数据库时崩溃，这个密钥就丢了，而远端对象可能已经存在。下次启动既无法管理原对象，又不能安全重发。

原始响应要设严格大小上限，并与操作编号、请求哈希和响应来源绑定。含密钥的响应必须进入操作系统保护的本机密文存储；日志、界面返回值和公开错误信息里只能出现不含秘密的诊断分类。即使解析失败，原密文也应保留，供修复后的同一版本流程恢复。

## 把超时当成“未知”，不要当成“失败”

超时处理最适合用两条并行规则描述：

- 前台等待超过预算后，任务进入未知状态，不再发送写请求。
- 如果迟到的响应随后到达，后台仍先把它加密落盘，但不能借此绕过当前暂停或重新授权任务。

下一次启动时，程序先检查是否已有原始回执。如果有，就在本地重新解析；如果没有，就使用同一身份做有界列表查询。列表恢复必须限制页数、条目数、响应大小和总时间，并拒绝重复游标。只有标题、摘要、创建时间和正文都指向唯一候选时，才能把它接回原任务。零个或多个候选都保持未知。

这套规则也适用于注册和专用密钥创建。身份创建结果不明时，不能悄悄再建一个账号；密钥创建结果不明时，也不能连续铸造新密钥。稳定身份比“自动把错误消掉”更重要。

## 公开核验必须使用匿名读取

拥有者接口能返回正确正文，并不代表普通读者能看到页面。最终核验应分成两组请求：

- 使用专用读取密钥，核对对象属于当前身份、正文逐字一致、可见性为公开，并确认保留字段的真实含义。
- 不带任何密钥访问公开页面，检查完整可见正文、目标链接、canonical、响应头、页面 robots 指令和站点 robots 规则。

可空字段尤其需要区分“字段缺失”和“字段为 null”。可选且可空的 schema 只说明这两种形态合法，本身没有定义保留期的业务语义。只有服务提供方的明确协议把 `expires_at: null` 定义为“没有计划到期”，并且当前身份的拥有者读回实际返回 null，连接器才能记录“当前没有计划到期”；字段缺失始终只能记为未知。即便满足这两个条件，也不能承诺平台永久保留内容，因为服务仍可能按规则删除、暂停或停止运营。

公开页面检查也不能只搜索网址字符串。链接可能藏在脚本、注释、不可见元素或导航模板中。验证器应限定正文容器，比较渲染后的完整语义，并确认目标链接是读者可以看到和点击的原生锚点。允许抓取只是一项当前技术证据，不等于已经被搜索引擎发现，更不等于排名或流量。

## 用反例测试连接器的边界

正常响应测试只能说明顺利路径可运行。真正能暴露设计缺口的是反例：

- 服务返回 403 时，连接器是否会改用 Cookie 或浏览器会话绕过？
- 响应声明的 audience 或 scope 不同，是否仍被保存为可用密钥？
- `expires_at` 缺失时，是否被错误转换成 null？
- 拥有者列表还有下一页但查询预算已用完，是否仍把当前候选当成唯一结果？
- 公开页面带 `noindex`，或 robots 禁止该路径，是否仍被记为合格？
- 程序暂停后收到迟到回执，是否会把已暂停任务直接恢复成运行中？
- 日志和界面结果中，是否能搜到密钥、授权头或原始秘密响应？

这些测试最好直接断言网络次数和请求方法。对于未知写入，重启测试的 POST 次数必须保持不变；恢复阶段只能增加 GET。这样可以把“我们认为不会重发”变成机器可验证的约束。

## 给后续维护者留下清晰接口

连接器不应把协议解析、任务调度和数据库字段混在一个函数里。一个易维护的分层方式是：协议层负责固定 URL、请求体、限额和严格解析；发布层负责稿件身份、只读核验和恢复；持久化层负责事务、密文和阶段转换；控制器只决定何时允许进入下一步。

这种分层让新平台的真实差异留在适配器里，也让共享调度规则保持稳定。维护者可以单独冻结协议测试、替换公开页面选择器，或在不发送新请求的情况下重放已加密回执。

如果你希望参考一个把任务状态、本机凭据和公开核验分开的桌面工具实现，可以查看 [Linkflow Desktop 项目](https://github.com/xiaogan123/linkflow-desktop)。阅读时可以重点关注：发布前如何绑定稿件版本，以及结果未知时为什么要保留原意图并转为只读恢复。

披露：本文由 Linkflow Desktop 的维护参与方整理，内容来自项目中的连接器设计与测试实践。AI 协助了提纲组织和措辞修改，事实边界、示例、项目关系与最终文本由维护方复核。上面的项目链接与教程主题直接相关，不构成第三方背书，也不承诺任何托管平台的可用性、收录或排名。
