EmDash CLI 内容编辑流程深度指南:Portable Text 转换、`_rev` 并发令牌与自动发布机制
2026/9/23 22:10:02 网站建设 项目流程
  • CMS
  • 后端
  • 前端
  • 插件系统

【免费下载链接】emdash

EmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress

项目地址:https://gitcode.com/gh_mirrors/emdas/emdash
点击查看免费下载

EmDash(templates/marketing-cloudflare 等模板内置的 Astro CMS)提供了面向 Agent 的 CLI 编辑链路,让你完全脱离后台界面,用 Markdown 写富文本、用_rev令牌安全地更新条目,并在读写后立即获得一致结果。读完本文,你将掌握 EmDash CLI 的 Portable Text 自动转换规则、--raw原样模式、--draft/自动发布语义、_rev乐观并发控制与ENTRY_LOCKED编辑锁的处理方法,并能在脚本与 CI 中安全地批量编辑内容。

本文的主体基于仓库内 EDITING-FLOW.md 与 SKILL.md,并辅以 CLI 命令实现 content.ts 与转换层 portable-text.ts 的源码佐证。

一、Portable Text 与 Markdown:双向自动转换

EmDash 将富文本以 Portable Text(PT)格式存储——这是一种结构化的 JSON 数组格式。为了让 Agent 和脚本以熟悉的纯文本工作,CLI 会在读取与写入时自动完成 PT 与 Markdown 之间的转换。

1.1 自动转换的方向与规则

  • 读取时(On read)portableText字段中的 PT 数组会被转换为 Markdown 字符串;
  • 写入时(On write)portableText字段中的 Markdown 字符串会被转换回 PT 数组;
  • 非 PT 字段(string、text、number 等)原样透传,不做任何转换。

关键在于:CLI 并不是盲目转换,而是先获取集合的字段 schema,只对声明为portableText类型的字段执行转换。这在源码中有直接体现——转换层提供了两个 schema 感知的辅助函数:

  • convertDataForRead(data, fields, raw):仅当field.type === "portableText"且字段值为数组时,才调用portableTextToMarkdown(portable-text.ts);
  • convertDataForWrite(data, fields):仅当field.type === "portableText"且字段值为字符串时,才调用markdownToPortableText(portable-text.ts)。

1.2 支持的 Markdown 语法

标准块级语法可以无损往返(round-trip):

MarkdownPT block
# Heading######h1–h6 blocks
普通段落normal block
> Quoteblockquote
- item/* itembullet list(用 2 空格缩进实现嵌套)
1. itemnumbered list(用 2 空格缩进实现嵌套)
```lang```code block(带语言标记)
altimage block

行内标记(inline marks):

MarkdownPT mark
**bold**strong
_italic_em
`code`code
~~strike~~strikethrough
textlink annotation

源码中的解析正则印证了上述能力:HEADING_PATTERN^(#{1,6})\s+(.+)$)、UNORDERED_LIST_PATTERNORDERED_LIST_PATTERNIMAGE_PATTERNINLINE_MARKDOWN_PATTERN(覆盖**bold**_italic_`code`text~~strike~~)全部定义在 portable-text.ts。列表嵌套的实现方式是:按缩进空格数整除 2 计算levelMath.floor(indent.length / 2) + 1),序列化时再按level反推两空格缩进(portable-text.ts)。

1.3 未知块:Opaque Fences(不透明围栏)

转换器无法识别的块(自定义块、embed 等)会被序列化为 HTML 注释形式的"围栏":

<!--ec:block {"_type":"callout","level":"warning","text":"Be careful"} -->

这些围栏能无损地存活于往返转换中:你可以看到它、移动它,但直接编辑其中的 JSON 有损坏风险。写入时,CLI 会通过OPAQUE_FENCE_PATTERN/^<!--ec:block (.+) -->$/)识别并反序列化,将原始 PT 块原样拼接回内容数组(portable-text.ts)。转换层注释将其定位为"Tier 3:未知块 → 不透明围栏(保留,不可编辑)",与标准块(Tier 1)和未来的 Markdown 指令(Tier 2)区分开。

1.4 Raw Mode:跳过转换,直取 PT JSON

当你需要完全掌控 PT 结构时,用--raw跳过 Markdown 转换:

npx emdash content get posts 01ABC123 --raw

建议使用 raw mode 的场景:

  • 需要对 PT 结构做精确控制;
  • 正在处理自定义块类型;
  • 需要在条目之间原样复制 PT(不做任何变换)。

注意--raw只作用于get;且从源码看,当存在待发布草稿且未加--published时,get会用compare接口把草稿数据叠加到返回结果上,并重新应用 PT→Markdown 转换(除非--raw)(content.ts)。

1.5 写入内容的字段检查

创建或更新内容时,每个字段都会按如下规则检查:

  • portableText字段 +字符串值→ 写入前把 Markdown 转换为 PT;
  • portableText字段 +数组值→ 视为原始 PT,直接发送、不做转换;
  • 其他任何字段类型→ 原样发送。
# Markdown 字符串 —— 自动转换为 PT npx emdash content create posts --data '{"title": "Hello", "body": "# Welcome\n\nThis is **bold**."}' # 原始 PT 数组 —— 原样透传 npx emdash content create posts --data '{"title": "Hello", "body": [{"_type": "block", "children": [{"_type": "span", "text": "Welcome"}]}]}'

二、Auto-Publishing:为 Agent 设计的读写一致性

EmDash CLI 的设计初衷是服务于自动化 Agentcreateupdate默认自动发布,让 Agent 获得读写后一致性(read-after-write consistency),无需自己管理草稿/发布生命周期。

2.1 各命令的自动发布行为

  • create—— 创建条目后立即发布,返回的条目处于published状态。源码实现是:client.create之后,若未传--draft,随即调用client.publish,再重新get一次返回当前状态(content.ts);
  • update—— 更新条目。如果集合启用了 revisions 且更新产生了草稿修订(draft revision),则自动发布以将草稿提升到内容表;返回的条目反映更新后的数据。源码中的条件是if (!args.draft && updated.draftRevisionId)——只有真的产生了草稿修订才发布,未启用修订的集合不会多做一次无谓发布(content.ts);
  • get—— 返回最新状态。如果存在待发布草稿(例如有人在后台管理界面编辑过但未发布),则返回草稿数据而不是已发布数据;加--published可只看已发布数据。

使用--draft可以跳过自动发布:

# 创建后保留为草稿 npx emdash content create posts --draft --data '{"title": "Draft post", "body": "..."}' # 更新后保留为草稿 npx emdash content update posts 01ABC123 --rev MToyMDI2... --draft --data '{"title": "Draft update"}'

2.2 为什么要自动发布?

EmDash 集合可以支持草稿修订(draft revisions)。一旦启用,update写入的是草稿修订而非内容表。如果没有自动发布,Agent 更新完条目后紧接着get,看到的将是陈旧的已发布数据——自己刚刚做的修改"消失"了。自动发布从根本上消除了这种读写不一致的困惑。

需要注意的是,草稿修订的底层机制对 Agent 是透明的:无论集合是否使用 revisions,Agent 都不需要感知差异,CLI 会自动处理(SKILL.md)。

三、Read-Before-Write:_rev令牌与乐观并发控制

更新操作使用_rev令牌实现乐观并发控制(optimistic concurrency)——原理与"文件编辑工具要求你先读文件才能编辑"完全相同:你必须先看到你要覆盖的内容

3.1 类比:像编辑文件一样编辑内容

可以把这想象成文件系统编辑工具:

  1. 读取文件,看到当前内容;
  2. 你决定要改什么;
  3. 写入,并带上你读到的版本引用。

如果在你读取和写入之间,别人修改了文件,写入就会失败——你不能覆盖自己没见过的更改。_rev令牌就是"你已见过当前状态"的证明。

3.2 工作机制

  1. content get返回条目,输出中带有_rev令牌;
  2. 把该_rev通过--rev传给content update
  3. 服务端校验:如果条目自你读取后发生过变化,返回409 Conflict
  4. 更新成功后会返回一个新的_rev,供后续编辑使用。

3.3_rev令牌是什么?

一段不透明的 base64 字符串。不要解析它,原样回传即可。它在源码中的角色是client.update(collection, id, { data, _rev: args.rev, ... })的入参(content.ts)。

3.4 CLI 工作流

CLI 在update命令上强制要求--rev(参数声明为required: true,描述为"Revision token from get (prevents overwriting unseen changes)",见 content.ts)。典型流程:

# 1. 读取条目 —— 记下输出中的 _rev npx emdash content get posts 01ABC123 # 输出包含: _rev: MToyMDI2LTAyLTE0... # 2. 用收到的 _rev 更新 —— 默认自动发布 npx emdash content update posts 01ABC123 \ --rev MToyMDI2LTAyLTE0... \ --data '{"title": "New Title"}' # 输出显示更新后的条目及新的 _rev

如果你尝试不带--rev更新,CLI 会直接拒绝该命令。这确保你永远清楚自己在覆盖什么。

3.5 冲突处理

如果在读取与写入之间条目被他人更新,你会看到:

EmDashApiError: Content has been modified since last read (version conflict) status: 409 code: CONFLICT

解决方式:用get重新读取,检查新状态,然后用新的_rev再次update。相关错误码定义在 errors.ts 中,CONFLICTENTRY_LOCKED共同出现在 mutation 冲突 schema 的code示例里(schemas/entry-lock.ts)。

四、Locked Entries:ENTRY_LOCKED--override-lock

收到 409 并不总是意味着内容被修改。如果有人在后台打开了该条目(编辑锁定开启时),写入会被拒绝并返回不同的错误码:

EmDashApiError: Ada is holding this entry status: 409 code: ENTRY_LOCKED

重新读取无法解决这个错误——条目本身没有变化,所以拿到的新_rev依然会被拒绝。重试前务必先检查code。两种处理方式:

  • 等待:编辑者关闭条目后锁即释放;
  • 强制写入:加--override-lock参数,忽略锁继续写。

4.1 锁的生命周期

  • 编辑者关闭条目时释放锁;
  • 崩溃标签页遗留的锁,在最后一次心跳后 7 分钟自动过期。这个常量在服务端源码中有明确定义:ENTRY_LOCK_LEASE_MS = 7 * 60 * 1000,注释说明"足够长以扛过打字停顿,足够短以让关闭的标签页在喝杯咖啡的工夫内释放条目"(handlers/entry-lock.ts)。

4.2 覆盖锁的注意事项

覆盖写入不会拿走锁。编辑者仍然持有它,因此他下一次保存会被当作版本冲突拒绝。所以:除非你确定对方已经离开,否则请等待。

4.3--override-lock的适用范围

--override-lock被以下命令接受:content updatecontent deletecontent publishcontent unpublishcontent schedule。从 content.ts 可以看到,这五个子命令都声明了该布尔参数,并透传到对应的客户端方法。

反之,关闭了编辑锁定的集合永远不会返回ENTRY_LOCKED——锁的启用与否由集合的edit_locking字段控制(handlers/entry-lock.ts)。

五、哪些操作需要_rev

只有update需要。其余操作要么幂等、要么非破坏性:

Command--rev需要?原因
content create尚不存在任何内容
content update覆盖已存在的数据
content delete软删除,可恢复
content publish幂等的状态变更
content unpublish幂等的状态变更
content schedule只修改元数据
content restore从回收站恢复

六、在脚本与 CI 中的实战组合

结合 SKILL.md 的补充能力,可以把上述编辑流程编排成可靠的自动化脚本。

6.1 用--json输出驱动脚本

所有远程命令都支持--json机器可读输出,stdout 被管道接管时自动启用:

# 读取后用 jq 提取 _rev 再更新 REV=$(npx emdash content get posts 01ABC123 --json | jq -r '._rev') npx emdash content update posts 01ABC123 --rev "$REV" \ --data '{"title": "Scripted title"}' --json # 创建后立即拿到 id 继续后续操作 ID=$(npx emdash content create posts --data '{"title":"Hello"}' --json | jq -r '.id')

6.2 冲突重试模式

在并发环境下(多个 Agent 或人机协作),更新前先读取、冲突后重读重试:

# 简化示例:失败后重新读取最新状态 npx emdash content get posts 01ABC123 --json > state.json REV=$(jq -r '._rev' state.json) npx emdash content update posts 01ABC123 --rev "$REV" --data '{"title":"v2"}' \ || { echo "409 conflict — re-read and retry"; }

6.3 判断 409 的错误码再决定策略

脚本中遇到 409 时,务必区分code

  • CONFLICT→ 重读、取新_rev、重试;
  • ENTRY_LOCKED→ 重读没用,需等待锁过期(最多约 7 分钟)或携带--override-lock强制写入。

七、小结

EmDash CLI 的编辑流程围绕三个设计支柱展开:schema 感知的 Portable Text ↔ Markdown 双向转换(含--raw原样模式与 opaque fence 无损保真)、面向 Agent 的自动发布(配合--draft保留草稿)、_rev为核心的读-写前校验(配合ENTRY_LOCKED编辑锁与--override-lock强制写入)。这些机制让脚本化、Agent 化的内容编辑既保持了对富文本结构的完整控制,又通过乐观并发避免了"覆盖未见过更改"的数据丢失风险。

想深入底层实现,可以继续阅读:

  • 转换层完整实现:portable-text.ts
  • CLIcontent子命令全部参数与自动发布逻辑:content.ts
  • 编辑锁的服务端租期与 holder 逻辑:handlers/entry-lock.ts
  • 编辑锁相关 schema 与错误结构:schemas/entry-lock.ts
  • 编辑锁集成测试:entry-lock.test.ts
  • CLI 完整命令参考与认证方式:SKILL.md
  • CMS
  • 后端
  • 前端
  • 插件系统

【免费下载链接】emdash

EmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress

项目地址:https://gitcode.com/gh_mirrors/emdas/emdash
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询