拓冰建站拓冰建站
首页 / 资讯中心 / 正文

EmDash 沙箱插件评论管理:读懂 `comments:read` 与 `comments:moderate` 能力的设计与实现

EmDash 沙箱插件评论管理读懂comments:read与comments:moderate能力的设计与实现【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址: https://gitcode.com/gh_mirrors/emdas/emdash导读EmDash基于 Astro 的全栈 TypeScript CMS为沙箱插件新增了两项评论管理能力comments:read与comments:moderate。前者让插件以只读方式查询非回收站评论及其个人数据后者在隐含读权限的基础上允许插件在approved、pending、spam三种状态间安全流转并以内置的预期状态校验optimistic concurrency防止并发误操作。读完本文你将掌握这两项能力的声明方式、ctx.comments完整 API、并发冲突语义COMMENT_STATUS_CONFLICT/COMMENT_MODERATION_IN_PROGRESS、底层核心审核链路以及隐私边界与功能边界。本变更记录于仓库 .changeset/calm-comments-moderate.md涉及emdash-cms/admin、emdash-cms/cloudflare、emdash-cms/plugin-cli、emdash-cms/plugin-test、emdash-cms/plugin-types、emdash-cms/registry-lexicons、emdash-cms/sandbox-workerd、emdash等多个包均为 minor 变更。变更概览插件第一次获得“评论审核权”在引入这两项能力之前评论的创建、审核与删除完全由 EmDash 管理后台与核心运行时掌控沙箱插件只能通过评论相关 hook如comment:beforeCreate、comment:moderate、comment:afterModerate被动参与。本次 minor 变更补上了缺失的一环让沙箱插件能够主动查询并修改已存储评论的审核状态。具体来说新增了两项能力comments:read授予ctx.comments.get()、ctx.comments.list()、ctx.comments.count()三个只读方法覆盖所有非回收站non-trashed评论。返回内容包含评论正文、作者姓名与邮箱、伪匿名 IP 哈希、User-Agent、审核元数据与状态等不包含与评论关联的 EmDash 用户账户 ID。comments:moderate隐含comments:read额外授予ctx.comments.setStatus()允许插件在approved、pending、spam之间转换评论状态。调用方必须提供其先前观察到的状态expectedStatus由核心审核路径做并发校验。从源码类型定义看这两项能力被收录进插件能力联合类型与信任契约packages/plugin-types/src/index.ts 中的能力枚举comments:read | comments:moderate并实现comments:moderate自动附带comments:read的隐含推导packages/core/src/plugins/types.ts 中的CommentAccess接口定义了ctx.comments的形状。在插件清单中声明能力沙箱插件默认被隔离任何超出自身 KV 与存储的操作都必须在emdash-plugin.jsonc清单中显式声明能力。评论审核同样遵循这一规则{ slug: plugin-comment-moderator, // ...identity profile... capabilities: [comments:read, comments:moderate] }由于comments:moderate隐含comments:read见 packages/core/src/plugins/context.ts 的能力推导逻辑只做审核的插件可以只声明comments:moderate一项。如果插件还需要把评论作者映射回系统用户则必须额外声明users:read—— 评论读取结果本身不携带用户账户 ID两者是独立的授权维度。两个值得注意的运维事实安装/更新需操作者同意。无论是新安装还是更新到请求了这两项能力的版本站点操作者都会在同意对话框中看到能力声明新增能力会以“能力差异”形式要求重新批准。按需声明。comments:read属于个人数据访问作者邮箱、IP 哈希等声明得越少安装审批负担越轻。从仓库文档 docs/src/content/docs/plugins/creating-plugins/capabilities.mdx 可看到能力表的完整描述comments:read授予ctx.comments.get() / list() / count()及评论个人数据comments:moderate授予带预期状态并发控制的setStatus()。读取评论get/list/count与返回数据形状ctx.comments的三个只读方法对应 packages/core/src/plugins/types.ts 中的CommentAccess方法签名说明get(id)(id: string) PromisePluginComment \| null按 ID 读取单条非回收站评论list(options?)(options?: CommentListOptions) PromisePaginatedResultPluginComment游标分页列表最新评论在前count(options?)(options?: CommentCountOptions) Promisenumber按相同过滤条件计数不分页list()与count()支持的过滤参数一致status按approved/pending/spam过滤collection按评论所属内容集合过滤contentId按目标内容 ID 过滤list()另有limit范围 1100默认 50与cursor游标分页。从仓库实现看这些过滤与分页逻辑位于 packages/core/src/database/repositories/comment.ts 的findForPlugin()/countForPlugin()查询恒定附加status ! trash条件回收站评论对插件不可见游标基于created_at与id双重排序先按时间倒序再按 ID 倒序编码保证分页稳定内部查询limit 1条以探测是否还有下一页返回nextCursor/hasMore。单条评论PluginComment的数据形状见 packages/core/src/plugins/types.tsinterface PluginComment { id: string; collection: string; contentId: string; parentId: string | null; authorName: string; authorEmail: string; body: string; status: approved | pending | spam; ipHash: string | null; // 伪匿名 IP 哈希 userAgent: string | null; moderationMetadata: Recordstring, unknown | null; createdAt: string; updatedAt: string; }隐私边界能看什么不能看什么comments:read暴露的是评论方的个人数据正文、作者姓名、作者邮箱、伪匿名 IP 哈希、User-Agent 以及审核元数据moderationMetadata即创建时 hook 写入的扩展信息。它刻意不暴露关联的 EmDash 用户账户 ID即authorUserId位于数据库内部字段插件视图中被剥离回收站trash状态下的评论 —— 仓库实现中toPluginComment()对 trash 评论直接抛错get()对 trash 返回null。在 packages/core/src/plugins/context.ts 的createCommentAccess()与toPluginComment()中可以看到这一映射数据库行里的authorUserId不会出现在PluginComment上。因此若插件需要把评论作者关联到用户账户必须再声明users:read并通过邮箱等其他字段自行匹配。状态转换setStatus与预期状态并发控制comments:moderate的落地方法是ctx.comments.setStatus()。它的独特之处在于调用方必须提供其之前观察到的状态const comment await ctx.comments!.setStatus!(commentId, approved, { expectedStatus: pending, });这段代码的语义是“仅当该评论现在仍然是pending时才将其置为approved”。这是典型的 compare-and-swapCAS模式防止两个审核者基于同一份过期快照互相覆盖。COMMENT_STATUS_CONFLICT状态已过期如果另一名审核者或管理员、其他插件在插件读取之后修改了状态setStatus()会以COMMENT_STATUS_CONFLICT拒绝。正确做法是重新读取评论 → 基于最新状态重新评估审核决定 → 再决定是否重试。底层实现是 packages/core/src/database/repositories/comment.ts 中的updateStatusIf()它先读取当前行若existing.status ! expectedStatus直接返回conflict并携带currentStatus随后用带WHERE status expectedStatus条件的 UPDATE 语句原子更新把“检查 修改”收敛为单条数据库操作从根本上避免检查与写入之间的竞态窗口。COMMENT_MODERATION_IN_PROGRESS转换尚未可见若一次状态转换正在进行、新状态尚未对后续请求可见时另一个重叠的转换请求会被COMMENT_MODERATION_IN_PROGRESS拒绝。此时应等待前一次转换完成再读取当前状态后重试。该守卫位于 packages/core/src/emdash-runtime.ts 的moderateCommentWithOrigin()运行时维护一个commentModerationInProgress集合按评论 ID转换期间将 ID 加入集合finally中移除重叠请求若发现该 ID 在集合中先核对最新状态若与期望状态不符则抛COMMENT_STATUS_CONFLICT否则抛COMMENT_MODERATION_IN_PROGRESS。无操作转换幂等且无副作用将评论设置为它当前已经是的状态例如setStatus(id, approved, { expectedStatus: approved })会被识别为“无变化”updateStatusIf返回unchanged不触发comment:afterModerate也不会重复发送批准通知。核心审核路径一次成功的转换发生了什么插件发起的状态转换并未绕开 EmDash 的审核体系而是复用与管理员完全相同的核心审核路径moderateComment()位于 packages/core/src/comments/service.tsCAS 更新通过repo.updateStatusIf(id, newStatus, expectedStatus)原子修改状态返回not_found/conflict/unchanged/updated四类结果批准通知若新状态为approved调用onApproved回调发送与管理员批准一致的作者通知触发comment:afterModerate仅在一次真实转换成功后触发一次事件携带previousStatus即调用方提供的expectedStatus与newStatusmoderator标识插件来源时为{ id: pluginId, name: null }origin字段{ source: plugin, pluginId }管理员路径则为{ source: admin, userId }。这正是变更记录所强调的“成功转换以调用插件的 origin 运行一次comment:afterModerate并保留批准通知”。这意味着审核插件可以通过comment:afterModerate感知到任何来源管理员或插件的状态变更且不会被重复通知。递归审核防护moderateComment()内部通过AsyncLocalStorage维护一个“正在审核中的评论 ID 集合”若在comment:afterModerate等钩子里对同一评论再次发起审核会直接抛出“Recursive comment moderation is not allowed”防止钩子间无限递归。对应代码见 packages/core/src/comments/service.ts 的moderateComment()。与管理后台 REST 路径的关系管理后台的PUT /_emdash/api/admin/comments/:id/statuspackages/core/src/astro/routes/api/admin/comments/[id]/status.ts也走handleCommentModerate→moderateCommentWithOrigin这条同一核心链路只是 origin 为{ source: admin, userId }。也就是说插件审核与管理后台审核在事务一致性、冲突语义、通知与钩子行为上完全对齐 —— 插件路径并非旁路实现。能力门控与沙箱边界沙箱桥接层packages/cloudflare/src/sandbox/bridge.ts对每个评论方法都做能力门控commentGet/commentList/commentCount未声明comments:read时直接抛Missing capability: comments:readcommentSetStatus未声明comments:moderate时抛Missing capability: comments:moderate同时校验status与expectedStatus必须是approved/pending/spam三者之一否则返回COMMENT_STATUS_INVALID仓库实现COMMENT_STATUSES集合。沙箱侧错误会映射成携带__emdashCommentError标记的结构化错误对象把COMMENT_STATUS_CONFLICT含currentStatus与COMMENT_MODERATION_IN_PROGRESS原样透传给插件其余异常继续抛出。Node.js 侧的 workerd 包装层packages/workerd/src/sandbox/wrapper.ts与 Cloudflare 侧保持一致的语义。能力声明同样会在打包时校验emdash-plugin bundle/publish会拒绝未识别的能力名拼写错误直接构建失败详见 docs/src/content/docs/plugins/creating-plugins/capabilities.mdx。明确不提供的操作变更记录与文档skills/creating-plugins/references/comments.md都明确了两条边界不提供硬删除hard deletectx.comments没有删除方法。评论的物理删除仍是管理后台与核心运行时的职责插件只能调整审核状态将不想要的评论置为spam是推荐做法回收站操作同样不开放给插件不提供批量状态替换setStatus一次只处理一条评论且强制要求expectedStatus。仓库虽然存在面向管理员的bulkUpdateStatus()见 packages/core/src/database/repositories/comment.ts但它只暴露给内部管理路径插件 API 不包含等价方法。这两条边界的意图一致插件审核权限被刻意设计为“单条、显式、可追溯”任何可能造成大规模数据变更的操作都被排除在沙箱能力之外。测试与验证仓库为这两项能力提供了多层测试覆盖可作为实现行为的权威参考桥接层集成测试packages/cloudflare/tests/sandbox/bridge-comments.test.ts 验证沙箱桥接的评论读取与状态转换行为能力门控测试packages/core/tests/integration/plugins/capabilities.test.ts 与 packages/plugin-types/tests/capabilities.test.ts 覆盖能力声明、隐含关系与门控workerd 运行器测试packages/workerd/test/bridge-handler.test.ts、packages/workerd/test/wrapper-context.test.ts 验证 Node.js 沙箱运行器下的错误码与上下文行为插件测试宿主emdash-cms/plugin-test的 packages/plugin-test/test/host.test.ts 覆盖了运行时测试宿主中的评论能力冲突、通知、钩子 origin、递归防护等场景官方推荐用运行时测试宿主验证个人数据形状与真实 HTTP/运行时路径。小结comments:read与comments:moderate是 EmDash 沙箱插件体系在“审核工作流自动化”方向上的关键拼图通过显式能力声明 操作者同意把评论个人数据的读取与审核状态的变更安全地交到插件手中通过expectedStatus预期状态与核心审核链路保证插件审核与管理员审核在同一套一致性、通知与钩子语义下并发安全地工作。如果你的插件需要实现自动垃圾评论过滤、争议评论队列或“待审→批准→通知作者”的自动化流程ctx.comments就是官方提供的标准入口。【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址: https://gitcode.com/gh_mirrors/emdas/emdash创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门