EmDash 插件存储实战指南:Storage 集合、Settings 加密与 KV 状态全解析
CMS后端前端插件系统【免费下载链接】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 CMSWordPress 的精神继承者其沙箱化插件系统为插件提供了三套插件级数据 API可查询的记录集合ctx.storage、用户可配置的设置ctx.settings含加密密钥以及键值状态存储ctx.kv。本文以仓库内插件开发技能参考文档 storage.md 为骨架结合packages/core、packages/plugin-types、packages/cloudflare、packages/workerd中的真实实现系统讲解集合声明、CRUD 与批量写入、基于修订号的 CAS 并发控制、谓词守卫原子更新、索引查询以及密钥轮换读完即可在沙箱插件中安全、正确地持久化数据。三套存储 API 概览沙箱插件Sandboxed plugin拥有三套插件作用域的数据 API全部落在宿主数据库中按运行时插件 ID 隔离且都不需要声明任何 capabilityAPI用途ctx.storage.collection在emdash-plugin.jsonc中声明的可查询记录集合ctx.settings用户可配置的设置支持声明为secret的加密密钥ctx.kv游标、缓存值以及其他键值状态这套接口类型定义在 packages/plugin-types/src/index.ts清单侧与 packages/core/src/plugins/types.ts运行时侧中。从源码结构看三套 API 共享同一套宿主存储后端集合走_plugin_storage数据表、设置走options仓库隔离边界由plugin:pluginId:settings:等命名前缀与运行时插件 ID 共同实现。声明存储集合每个集合及其查询索引都必须在清单中显式声明{ storage: { submissions: { indexes: [formId, status, createdAt, [formId, createdAt]], uniqueIndexes: [externalId], }, }, }indexes中的每条目既可以是单个字段名单列索引也可以是字段名数组复合索引uniqueIndexes携带 UNIQUE 约束字段形状与indexes相同uniqueIndexes中的字段本身已可查询不要再重复放进indexes未声明的集合会被沙箱桥sandbox bridge拒绝。对应的清单类型定义StorageCollectionConfig/PluginStorageConfig见 packages/plugin-types/src/index.ts清单是 bundler写入方与运行时读取方共同遵守的线格式wire contract。集合操作完整可移植 API每个已声明集合在原生、Cloudflare 沙箱、Node/workerd 沙箱三种执行环境中都暴露同一套可移植 API接口签名见 packages/core/src/plugins/types.tsinterface StorageCollectionT unknown { get(id: string): PromiseT | null; put(id: string, data: T): Promisevoid; delete(id: string): Promiseboolean; exists(id: string): Promiseboolean; getVersioned(id: string): Promise{ value: T; revision: string } | null; compareAndSet( id: string, expectedRevision: string | null, data: T, ): Promise{ applied: true; revision: string } | { applied: false }; compareAndDelete(id: string, expectedRevision: string): Promise{ applied: boolean }; updateIf(id: string, args: UpdateIfArgsT): PromiseUpdateIfResultT; getMany(ids: string[]): PromiseMapstring, T; putMany(items: Array{ id: string; data: T }): Promisevoid; deleteMany(ids: string[]): Promisenumber; query(options?: QueryOptions): Promise{ items: Array{ id: string; data: T }; cursor?: string; hasMore: boolean; }; count(where?: WhereClause): Promisenumber; }注意Node/workerd 包装器还包含内容批量方法content batch methods但它们不属于StorageCollection上表中存储批量方法在两个运行时之间都是可移植的。基本写入与批量写入const submissions ctx.storage.submissions as StorageCollectionSubmission; await submissions.put(sub_123, { formId: contact, status: pending, createdAt: new Date().toISOString(), }); const item await submissions.get(sub_123); const exists await submissions.exists(sub_123); const items await submissions.getMany([sub_123, sub_456]); await submissions.putMany([ { id: sub_456, data: { formId: contact, status: pending } }, { id: sub_789, data: { formId: sales, status: pending } }, ]); const deleted await submissions.deleteMany([sub_456, sub_789]);getMany()返回Map跨越任一条沙箱桥后仍保持该语义因此可以放心地以Map.get(id)方式读取批量结果。基于修订号的比较并交换CAS当并发请求可能替换同一个完整值时使用getVersioned()、compareAndSet()与compareAndDelete()const current await submissions.getVersioned(sub_123); if (!current) throw new Error(Submission not found); const result await submissions.compareAndSet(sub_123, current.revision, { ...current.value, status: processing, }); if (!result.applied) { // 另一个请求已修改或删除该值先重新读取再重试。 }各操作的前置条件操作行为getVersioned(key)返回{ value, revision }仅当键不存在时返回nullcompareAndSet(key, null, value)仅在键不存在时创建compareAndSet(key, revision, value)仅当当前修订号匹配时替换compareAndDelete(key, revision)仅当当前修订号匹配时删除几点关键语义存储的 JSONnull仍会返回版本化信封即getVersioned返回{ value: null, revision }而非null只有“行不存在”才返回null这一点在 packages/core/src/plugins/types.ts 有明确注释每次成功写入都会改变修订号包括写入相同值的put()/set()修订号是不透明的、按键隔离的值原样回传即可冲突返回applied: false非法输入、权限失败、唯一索引冲突、数据库故障则以 reject 形式抛出冲突后请重新读取、重新计算并保持重试次数有界丢失的响应可能使写入结果未知因此CAS 不是外部副作用的恰好一次exactly-once机制。版本化方法同样适用于ctx.kvconst current await ctx.kv.getVersionednumber(state:completed); const next (current?.value ?? 0) 1; const result await ctx.kv.compareAndSet(state:completed, current?.revision ?? null, next);谓词守卫更新 updateIfupdateIf()在存储数据匹配守卫时修改既有文档的字段。守卫、字段替换与整数增量对该记录原子执行是不超卖no-oversell的原语守卫与算术在同一个UPDATE … RETURNING语句中完成N 个并发守卫递减会被正确串行化见 packages/core/src/plugins/types.ts 的注释。const result await submissions.updateIf(sub_123, { where: { status: pending, attempts: { lt: 3 } }, set: { status: processing, lastAttemptAt: new Date().toISOString() }, delta: { attempts: { inc: 1 } }, }); if (result.applied) { ctx.log.info(Claimed submission, { submission: result.data }); }updateIf()在以下情况返回{ applied: false }行不存在、守卫失败、存储文档不是对象、整数运算不安全。它从不插入。规则where必填显式{}表示匹配任意既有行set替换提供的顶层字段其余字段保持不变delta每个字段恰好包含一个安全整数inc或dec缺失或为null的计数器从零开始同一字段不能同时出现在set与delta中set或delta中至少要保留一个已定义字段需要保证值非负时请将dec: n与gte: n守卫配对使用。参数校验逻辑在 packages/core/src/plugins/storage-update.ts守卫操作数必须是字符串或有限数值delta必须是恰好一个inc/dec的安全整数字段名经过 JSON 字段名校验。格式错误的参数会在不写入的情况下 reject。串行化失败与死锁在原生 PostgreSQL 执行中串行化失败与死锁会抛出StorageSerializationError其code为STORAGE_SERIALIZATION_FAILURE、retryable为true定义见 packages/core/src/plugins/storage-query.ts。两种触发场景40001serialization_failure仅在高于 READ COMMITTED 的隔离级别出现失败方无法基于更新的快照重读40P01deadlock_detected任意隔离级别下事务按相反顺序获取行锁时出现。沙箱传输层会保留安全字段但不保证instanceof关系因此跨沙箱时请检查code与retryable字段。重试整个显式事务前请先重启该事务。无论哪种情况失败的写入者永远不会应用其更新——要么得到{ applied: false }要么抛出该错误这是不超卖安全不变量的保证。索引查询只有已声明的索引字段才能被过滤或排序const result await submissions.query({ where: { formId: contact, status: { in: [pending, processing] }, createdAt: { gte: 2026-01-01 }, }, orderBy: { createdAt: desc }, limit: 100, cursor, });支持的过滤器类型精确值、{ in: [...] }、{ startsWith: ... }、范围对象使用gt、gte、lt或lte。范围过滤器至少需要一个已定义的边界。query()默认返回 50 条每页最多 100 条当hasMore为 true 时沿cursor继续翻页count(where)接受同样的索引过滤器复合索引顺序决定可用的查询形态[formId, createdAt]支持按formId过滤并按createdAt排序但不能替代省略formId时所需的独立createdAt索引。查询校验与 SQL 构建在 packages/core/src/plugins/storage-query.ts 中实现对非索引字段执行过滤或排序会抛出StorageQueryError其suggestion会直接提示你将该字段加入清单的storage.collection.indexes。startsWith会先对%与_等 LIKE 元字符做转义escapeLikePattern见 storage-query.ts避免前缀被当作通配符数值比较在 PostgreSQL 上通过类型守卫的::numeric强制转换避免词法排序9 10为真的陷阱SQLite 则天然返回类型化数值。范围过滤器若无任何已定义边界会被直接拒绝storage-query.ts避免在条件写操作中意外剥掉守卫。KV 操作KV 支持无条件、版本化、删除与前缀列举操作interface KVAccess { getT(key: string): PromiseT | null; set(key: string, value: unknown): Promisevoid; delete(key: string): Promiseboolean; list(prefix?: string): PromiseArray{ key: string; value: unknown }; getVersionedT(key: string): Promise{ value: T; revision: string } | null; compareAndSet( key: string, expectedRevision: string | null, value: unknown, ): Promise{ applied: true; revision: string } | { applied: false }; compareAndDelete(key: string, expectedRevision: string): Promise{ applied: boolean }; }运行时类型KVAccess定义见 packages/core/src/plugins/types.ts。约定state:*存放内部插件状态不展示给用户cache:*存放可复用的计算结果或远端数据。用稳定的前缀让内部 KV 键可被发现await ctx.settings.set(webhookUrl, url); await ctx.kv.set(state:lastRun, new Date().toISOString()); await ctx.kv.set(cache:summary, summary); const settings await ctx.settings.list();设置Settings与加密密钥插件 CLI 会序列化admin.settingsSchema两条沙箱桥都将ctx.settings路由到与生成的 admin 表单相同的 options 记录上因此表单中保存的值可以通过ctx.settings.get(key)读到。完整的设置 API 支持set、delete、list、getVersioned、compareAndSet、compareAndDelete运行时实现见 packages/core/src/plugins/settings.ts键前缀为plugin:pluginId:settings:。secret 字段的 AES-GCM 信封声明为secret的字段使用带版本的 AES-GCM 信封加密**插件 ID 与设置键作为认证数据additional data**参与运算packages/core/src/plugins/settings.ts因此密文无法被复制到其他插件或键名下解密。信封结构为{ $emdash: plugin-setting, v: 1, kid, iv, ciphertext }settings.ts。EMDASH_ENCRYPTION_KEY可包含逗号分隔的轮换列表第一个键加密新值信封中的kid选择读取时使用的键缺失、错误或被篡改的密钥一律失败关闭fail closed不泄露明文既有明文密钥仍可读取再次保存时会被加密。kid是解码后的密钥字节的 SHA-256 前 8 个字符packages/core/src/config/secrets.ts解析逻辑要求每个条目形如emdash_...前缀加固定长度 base64url 主体并可用emdash secrets generate生成。密钥轮换、校验、去重与启动时验证也都集中在该文件中。运维提醒请将完整密钥列表随操作备份一起保留。如果数据库恢复到不包含其加密设置所引用的全部密钥这些值将无法读取。此外ctx.kv.get(settings:key)在整个 EmDash 0.x 中仍是兼容别名新插件应改用ctx.settingspackages/core/src/plugins/types.ts 亦注明该约定。总结集合ctx.storage清单声明 索引约束驱动提供 CRUD、批量、CAS、updateIf原子守卫与索引分页查询跨原生 / Cloudflare / workerd 三种执行环境可移植设置ctx.settings与 admin 表单同源的 options 存储secret字段走 AES-GCM 信封加密支持密钥轮换与 fail-closedKVctx.kv轻量键值状态建议用state:/cache:前缀组织同样支持版本化并发原语。三者共用宿主数据库、按插件 ID 隔离且无需 capability。在并发场景下优先使用compareAndSet/updateIf而非读-改-写三步操作并始终把重试次数控制得有界——这是从本仓库的存储实现中提炼出的最重要的一条工程经验。赞分享CMS后端前端插件系统【免费下载链接】emdashEmDash is a full-stack TypeScript CMS based on Astro; the spiritual successor to WordPress项目地址https://gitcode.com/gh_mirrors/emdas/emdash点击查看免费下载相关推荐EmDash 插件存储指南Storage、Settings 与 KV 的声明、读写与并发控制EmDash 插件存储指南Storage、Settings 与 KV 的声明、读写与并发控制 EmDash 为沙箱化插件提供了三套插件级数据 API可查询的CMS后端前端插件系统EmDash 插件存储指南基于 Sandboxed 插件的 Collection、Settings 与 KV 数据 API 实战EmDash 插件存储指南基于 Sandboxed 插件的 Collection、Settings 与 KV 数据 API 实战 导读 本文是 EmDashCMS后端前端插件系统EmDash 插件存储与 KV 完全指南ctx.storage、ctx.settings 与 ctx.kv 的声明、并发控制与加密实践EmDash 插件存储与 KV 完全指南ctx.storage、ctx.settings 与 ctx.kv 的声明、并发控制与加密实践 沙盒化插件SandbCMS后端前端插件系统创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

卡瓦三合一CT维修手册:准直器拆装与校准要点解析

卡瓦三合一CT维修手册:准直器拆装与校准要点解析

简介:卡瓦三合一CT X-TREND维修技术手册面向医疗影像设备维修工程师,系统讲解准直器板、辅助准直器组件、CEPH管头、管头扫描机械装置、测颅管头电缆及测颅臂直线部分等核心部件的拆卸、更换与校准流程。资源为1个PDF文件,大小3.12MB&#xf…

2026/9/23 21:36:30 阅读更多 →
老摄像头新内核:sn9c20x驱动移植实战与避坑指南

老摄像头新内核:sn9c20x驱动移植实战与避坑指南

简介:sn9c20x.rar是一份面向Sonix SN9C201/SN9C202 USB摄像头芯片的驱动开发参考资源,适合嵌入式开发者、Linux/Windows驱动工程师,以及需要在项目中快速接入低成本模拟视频转USB方案的技术人员使用。压缩包内仅含1个C语言源文件,…

2026/9/23 21:36:30 阅读更多 →
CANN ops-nn 量化分组矩阵乘累加算子 QuantMatmulReduceSum 深度解析与 aclnn 调用实战

CANN ops-nn 量化分组矩阵乘累加算子 QuantMatmulReduceSum 深度解析与 aclnn 调用实战

人工智能算子库深度学习CANNAscend 【免费下载链接】ops-nn 本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。 项目地址: https://gitcode.com/cann/ops-nn 点击查看 免费下载 QuantMatmulReduceSum 是 CANN ops-nn 神经网络算子库中…

2026/9/23 21:36:30 阅读更多 →

最新新闻

Apache DolphinScheduler 文档贡献完整指南:环境搭建、本地构建验证与文档 Pull Request 提交规范

Apache DolphinScheduler 文档贡献完整指南:环境搭建、本地构建验证与文档 Pull Request 提交规范

任务调度大数据后端前端 【免费下载链接】dolphinscheduler Apache DolphinScheduler is the modern data orchestration platform. Agile to create high performance workflow with low-code 项目地址: https://gitcode.com/gh_mirrors/do/dolphinscheduler 点击查…

2026/9/23 22:58:10 阅读更多 →
你的课程论文,为什么写到一半就想删了重写?

你的课程论文,为什么写到一半就想删了重写?

官网:www.shujiangce.com | 微信 公众号 :书匠策AI 各位同学好,我是那个总在教你们写论文、但自己当年写课程论文也差点把键盘砸了的博主。 今天我们不聊那些听起来很爽的“一键生成万字长文”。那种东西你用一次就知道了——生成出来的文…

2026/9/23 22:58:10 阅读更多 →
答辩前夜,你打开PPT,新建了空白文档——书匠策AI说:别慌,先把“视觉剧本”写出来

答辩前夜,你打开PPT,新建了空白文档——书匠策AI说:别慌,先把“视觉剧本”写出来

官网:www.shujiangce.com | 微信 公众号 :书匠策AI 一个很少被提及的事实 论文写完了,答辩PPT没做完,这是一种比论文写不完更隐秘的崩溃。 因为你以为最难的部分已经过去了。文献综述写了,数据分析跑了&#xff…

2026/9/23 22:58:10 阅读更多 →
基于IPFS、Ethereum与ABE的区块链安全数据共享系统解析

基于IPFS、Ethereum与ABE的区块链安全数据共享系统解析

简介:一套结合IPFS、Ethereum与ABE(基于属性加密)的区块链安全数据共享系统设计源码,面向区块链开发者和数据安全研究人员,适用于金融、医疗、法律等对数据保护要求较高的场景。包内含2000个文件,压缩包约6…

2026/9/23 22:58:10 阅读更多 →
论文降AIGC,其实是在跟“太完美”作对

论文降AIGC,其实是在跟“太完美”作对

官网:www.shujiangce.com | 微信 公众号 :书匠策AI 你有没有想过一个问题:为什么检测器能认出AI写的东西? 不是因为它读懂了你的论文。不是因为它理解了你的论证。是因为AI写的东西,太“干净”了。 你写论文的时…

2026/9/23 22:58:09 阅读更多 →
substrate 内嵌的 cloud.google.com/go/compute/metadata 全解析:Google Cloud 实例元数据服务的 Go 客户端库

substrate 内嵌的 cloud.google.com/go/compute/metadata 全解析:Google Cloud 实例元数据服务的 Go 客户端库

人工智能AI AgentAgent 沙箱云原生容器运行时零信任 【免费下载链接】substrate Agent Substrate: the core system 项目地址&#xff1a; https://gitcode.com/GitHub_Trending/substrate7/substrate 点击查看 免费下载 <导读> 本文围绕 substrate 仓库&#xff08;Age…

2026/9/23 22:57:08 阅读更多 →

日新闻

3招搞定手机怎么下载微信面试难题实战项目解析

3招搞定手机怎么下载微信面试难题实战项目解析

3招搞定手机怎么下载微信面试难题实战项目解析 面试被问“手机怎么下载微信”背后的原理,90%的人答不上来。别笑,这看似弱智的问题,实则是考察你对移动应用分发机制、安全校验及网络协议理解的试金石。我带过不少校招新人,他们背了八股文,却连一个A…

2026/9/23 0:00:23 阅读更多 →
2k显示屏性能优化踩坑:版本升级后API全变了,这份源码解析救了我

2k显示屏性能优化踩坑:版本升级后API全变了,这份源码解析救了我

2k显示屏性能优化踩坑:版本升级后API全变了,这份源码解析救了我 刚把开发环境的显示器从1080P换到2K,跑老项目直接报错,版本升级后 API…

2026/9/23 0:01:25 阅读更多 →
3步搞定美眉图实战项目,告别官方文档抓不住重点

3步搞定美眉图实战项目,告别官方文档抓不住重点

3步搞定美眉图实战项目,告别官方文档抓不住重点 官方文档翻了三遍还是云里雾里?别急,美眉图在实战项目中常被用来做数据可视化,但它的原理比你想的简单。今天咱们直接上手,用一个完整的小项目把美眉图跑通,不再死磕那些冗长的理论说明。…

2026/9/23 0:01:25 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事&#xff1a;用Flutter给OpenHarmony做一款游戏集合类的App&#xff0c;说白了就是把若干小游戏塞进一个壳里&#xff0c;用统一入口分发。这个方向本身不算新鲜&#xff0c;真正让我花了不少心思的&#xff0c;是首页那堆游戏卡…

2026/9/23 4:55:02 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档&#xff0c;最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事&#xff1a;今天在表后面多加了两个空白行&#xff0c;明天给客户交稿前发现整个章节的编号全部错位&#xff0c;光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/23 4:49:06 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年&#xff0c;说实话&#xff0c;第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年&#xff0c;流量惨淡、功能臃肿、代码自己都懒得看第二遍之后&#xff0c;我才慢慢琢磨明白一个道理&#xff1a;第一个网站是练手&…

2026/9/23 9:53:41 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践&#xff1a;原型怎样变成可用功能分类&#xff1a;[AI/大模型]细分主题&#xff1a;AI 增强型 CI/CD 流水线自动化与 GitOps 实践&#xff1a;Agent 工作流、工具调用与任务拆解&#xff1a;从原型到生产的验收清单很多团队在尝试用大…

2026/9/23 9:53:40 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战&#xff1a;复盘记录怎样真正派上用场分类&#xff1a;[工程技术]细分主题&#xff1a;Kubernetes 生产环境运维与排障实战&#xff1a;可复制的项目复盘模板与决策记录大部分团队的事故复盘报告&#xff0c;最后都变成了躺在 Confluence 或钉…

2026/9/23 9:53:40 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理&#xff1a;核心链路应该先拆哪一步分类&#xff1a;[工程技术]细分主题&#xff1a;Docker 容器化技术与镜像安全管理&#xff1a;核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用&#xff08;包含 Web 接口、后台…

2026/9/23 9:53:40 阅读更多 →