- CMS
- 后端
- 前端
- 插件系统
【免费下载链接】emdash
EmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress
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):
| Markdown | PT block |
|---|---|
# Heading至###### | h1–h6 blocks |
| 普通段落 | normal block |
> Quote | blockquote |
- item/* item | bullet list(用 2 空格缩进实现嵌套) |
1. item | numbered list(用 2 空格缩进实现嵌套) |
```lang``` | code block(带语言标记) |
alt | image block |
行内标记(inline marks):
| Markdown | PT mark |
|---|---|
**bold** | strong |
_italic_ | em |
`code` | code |
~~strike~~ | strikethrough |
text | link annotation |
源码中的解析正则印证了上述能力:HEADING_PATTERN(^(#{1,6})\s+(.+)$)、UNORDERED_LIST_PATTERN、ORDERED_LIST_PATTERN、IMAGE_PATTERN、INLINE_MARKDOWN_PATTERN(覆盖**bold**、_italic_、`code`、text、~~strike~~)全部定义在 portable-text.ts。列表嵌套的实现方式是:按缩进空格数整除 2 计算level(Math.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 的设计初衷是服务于自动化 Agent:create与update默认自动发布,让 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 类比:像编辑文件一样编辑内容
可以把这想象成文件系统编辑工具:
- 你读取文件,看到当前内容;
- 你决定要改什么;
- 你写入,并带上你读到的版本引用。
如果在你读取和写入之间,别人修改了文件,写入就会失败——你不能覆盖自己没见过的更改。_rev令牌就是"你已见过当前状态"的证明。
3.2 工作机制
content get返回条目,输出中带有_rev令牌;- 把该
_rev通过--rev传给content update; - 服务端校验:如果条目自你读取后发生过变化,返回409 Conflict;
- 更新成功后会返回一个新的
_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 中,CONFLICT与ENTRY_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 update、content delete、content publish、content unpublish和content 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
- CLI
content子命令全部参数与自动发布逻辑: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
相关推荐
EmDash CLI 内容编辑流程详解:Portable Text 转换、`_rev` 乐观并发与自动发布机制
EmDash CLI 内容编辑流程详解:Portable Text 转换、 _rev 乐观并发与自动发布机制 本文围绕 EmDash 仓库中 EDITING F
CMS后端前端插件系统EmDash CLI 内容编辑流程全解:Portable Text 转换、`_rev` 乐观并发与自动发布机制
EmDash CLI 内容编辑流程全解:Portable Text 转换、 _rev 乐观并发与自动发布机制 EmDash 是一个基于 Astro 的全栈 Ty
CMS后端前端插件系统EmDash CLI 内容编辑流程完全指南:Portable Text 转换、`_rev` 乐观并发与自动发布机制
EmDash CLI 内容编辑流程完全指南:Portable Text 转换、 _rev 乐观并发与自动发布机制 本文以 EmDash 仓库中 EDITING
CMS后端前端插件系统
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考