设计原则
这一页先讲 Covel 的产品理念,再回答“Covel 为什么这样设计”:什么该放进内核,什么该交给插件。想给框架加功能,或者拿不准一段逻辑该写在哪里的人,先读这一页。
别的问题去别的页面:
产品理念
这几条是做取舍的依据。代码和它们不一致时,要改的是代码。
先让玩家玩得顺
- Covel 面向普通玩家和开发者。最要紧的,是让玩家方便地玩到别人搭好的世界,或者自己建的世界。
- 扩展玩法和世界观的,是开发者、有经验的玩家,或者替他们干活的 agent。
- 框架提供的是 agent 和玩家之间的调度:玩家说一句话,框架决定由哪些插件、按什么顺序、带着什么内容来回应。在此之上,框架在加载、调度、存储、界面每一层都为世界包和插件服务。
插件一律平等
- 框架的每一项能力都为所有插件服务,不只为内置插件。
- 内置插件只是随应用附带、用来演示框架能力的例子。它们和社区插件用同一套接口,不能用别的插件拿不到的东西。两者只在信任上不同:内置插件随应用发布,不用玩家批准就能运行。
- 框架代码不为任何具体插件做适配,见下面“核心:内核提供原语,插件承载玩法”一节。把所有插件都移除,框架也要能正常运行。
- 只接一个叙事插件,就要能完整地玩一个世界。其他插件都是为了丰富玩法。
- 内置的四个世界也是示范。雾港、遥风学园、提灯古冢各示范一种玩法:传统叙事、舞台模式、跑团规则。Emberback 用英文写成,给英文玩家,也示范一个世界怎样带多种语言。
缺东西也要能玩
- 插件要的字段没出现,就忽略这些字段。世界包里有没用到的字段,游戏也照常运行。
- 缺一个插件或一份数据,只少对应的功能,不能挡住开局,也不能让剧情停下。
- 问题要说出来:校验工具把它报给作者,界面告诉玩家少了什么。
几种部署形态,一套核心
| 形态 | 谁部署 | 玩家数据存在哪里 | 现在 |
|---|---|---|---|
| 桌面客户端 | 玩家在自己的电脑上 | 本机的 SQLite 文件;也可以接 PostgreSQL(目前只能用环境变量设置) | 重点打磨 |
| 自部署网页版 | 玩家或团队自己 | 服务器上的 SQLite 文件,或者自己的 PostgreSQL | 重点打磨 |
| 官方网页版 | Covel 项目 | 玩家自己的浏览器里(IndexedDB)。服务器只在处理请求时经手,不能保存玩家数据 | 还不支持,以后做 |
- 现在先把桌面客户端和自部署网页版的细节做好。官方网页版还不支持,以后再做。
- 底层保持通用:存储、鉴权、模型接入这些核心代码不为某一种形态写特例,形态之间的差别应当集中在一处配置里。这样以后加官方网页版,只是多一种配置,不用重写核心。
- 普通玩家用 SQLite 就够了,换不换数据库对他们几乎没有影响。PostgreSQL 必须支持,但它面向专业开发者和有特殊需求的用户。两个后端在行为上的任何差别都算缺陷,由共享的契约测试守住。
- 各形态的存储方式见存储。
- 商业版现在不做,本仓库也不为它更新。以后要做,也从同一套核心上搭。
写法要简单
- 一个插件可以只用函数、只用 agent,或者两者混用,见下面“两个原语 + 组合”一节。agent 调用工具完成复杂的事,函数负责要求精确的事。
- 插件的界面用统一的声明描述(目前是 json-render),由框架来渲染。框架不为某个插件写专门的界面。
- 写一个插件或世界包,要写的字段越少越好。能推导出来的、有合理默认值的,都不该要求作者写。加字段之前,先看现有的契约、扩展点和数据空间能不能表达。
现在是快速开发阶段
现在还不是稳定版本。契约和定义可以直接改,不为旧数据、旧字段留兼容分支,见 AGENTS.md 的 “Current contract only”。改了契约,就在发布说明里写清楚哪些数据要重建。
基本目标
任何创作者都能把一个游戏创意按规范写成声明,框架就能把它编排成可以玩的场景。创作者可以会写代码,可以不会,也可以是一个 AI Agent。
规范约束“怎么接进框架”,不约束“能玩什么”。
一次赛博潜入、一场法庭辩论、一回视觉小说里的表白,对框架是同一件事:一组按契约声明好的插件和一个 World,被 Kernel(内核)一回合一回合地编排。
核心:内核提供原语,插件承载玩法
Covel 的一切都从这一条出发。仓库根目录的 AGENTS.md 把它定为强制规则,名字是 “Framework ↔ Plugin Isolation Rule”。
- Kernel(
packages/、apps/server/src/、apps/web/src/)只提供五种基本积木和编排。它不认识任何具体玩法。 - 插件承载玩法逻辑。它声明四件事:何时触发、看什么 Context、用哪种 Runtime 执行、产出什么 Proposal。
五种基本积木(原语)是:
| 原语 | 一句话 |
|---|---|
| Runtime | 插件里一段可以运行的东西:一段提示词或一个函数 |
| Tool | 模型或函数可以调用的一个确定的操作 |
| Hook | 在固定时机插进来检查、改写或记录的处理函数 |
| Context | 一个 Runtime 运行时能看到的内容 |
| Proposal | 一条“请保存这个”的请求,由内核检查后写入 |
编排指三件事:调度、组装上下文、校验并提交。
一个直觉的比喻:插件长出标准的“插头”,也就是上面那四项声明,Kernel 就能供电,也就是编排、组合、复用。插头后面接什么“电器”,Kernel 一概不碰。电器是你的逻辑、叙事、判断规则和世界观。
一条裁决规则
判断一件事该不该进 Kernel,问一句:它是“插头”(接口、契约),还是“电器”(内容、逻辑)?
| 是什么 | 例子 | 归谁 |
|---|---|---|
| 插头 | “插件怎么声明触发条件”“有哪几种 Proposal 类型” | Kernel 管,要有规范,写进参考 |
| 电器 | “这个 NPC 该不该相信玩家” | Kernel 不碰,交给插件 |
由此得到一条推论,AGENTS.md 已经强制执行:框架代码永不硬编码具体的插件 ID,也永不为单个玩法修改内核。
一个合理的场景表达不出来时,正确的做法是给 Kernel 加一根通用的针脚:新的 Proposal 类型、新的 Hook 事件或新的触发方式。不要写死一段逻辑。这样任何插件都可以被替换,而框架不用动。
两个原语 + 组合 = 创作者的"三种写法"
每个 Runtime 只有两种:
| 种类 | 怎样写 | 适合什么 |
|---|---|---|
| agent runtime | 用自然语言把规则写进 PLUGIN.md。模型读规则和上下文,自己判断、自己叙述 | 需要理解、裁量、生成文本的玩法,也就是让模型当裁判 |
| function runtime | 一段 JS handler,确定地读上下文、计算、产出 | 需要精确、公平、可复现、零成本的事。例如掷骰的随机数,不能交给模型 |
创作者常把它们叫作“模式”,还会提到第三种“混合”。但 Covel 没有 "hybrid" 原语。所谓混合,是把两种 Runtime 拼起来的组合,靠两种粘合:
- Tool:agent 调用一个由函数实现的工具,稳定地触发确定的逻辑。
- io.inputs:把一个 runtime 的产出送进另一个 runtime 的 Context。
这样 Kernel 只需要两块积木,创作者却能表达任意“既有规则、又有叙事”的玩法。掷骰的范例和具体写法见带本地代码的插件。
NOTE
这里的“写法 / 模式”指执行方式。术语表里的 Trigger mode 是另一回事,它指 runtime 何时运行:auto / manual / event。不要混用。
可表达的边界
一个场景只要能拆成下面四步,框架就支持。它可以是任何回合制的、由叙事或 agent 驱动的场景:
何时触发 → 看什么 Context → 用哪种 Runtime 判断 → 产出什么 Proposal
拆不进这个范式的不是 Covel 的目标,例如实时动作和像素级物理。完整的回合管线见 flow.md。
失败隔离(为什么一律走 Proposal)
runtime 不直接写数据库。它产出 Proposal,由 Kernel 校验并提交。工具调用成功,只说明这次操作完成了;要等提交结果才能确认存档已更新。
这条边界带来三种约束:
- 有效故事可以保留,辅助写入可以单独丢弃。 有故事的执行按顶层 runtime 划分提交单元;嵌套调用与父调用属于同一单元。可选单元在 savepoint 内提交,失败时连同依赖其结果或未提交数据的辅助下游一起丢弃。故事或 setup 单元的写入被拒,以及基础设施故障,仍会回滚整次执行。
- 并发修改有明确的规则。 操作式写入在提交时作用于最新值;角色和维度更新检查版本,冲突会被拒绝。它们不会静默覆盖其他写入,作者仍需处理失败。
- 插件私有数据隔离,公共领域对象共享。 插件只能直接读写自己的 namespace;角色、座位、维度等通过公共 World Model 接口读取,领域写入仍须校验。
没有故事的执行整体提交或整体回滚,例如手动触发、后台执行和开局准备。插件 action RPC 的 scoped store 写入是另一种已存在的边界:写入立即生效,handler 后续失败不会自动撤回;多条领域写入需要一起成败时,调用手动 function runtime。详见通信方式和事务。
数据库回滚不能撤销已经发出的外部请求等副作用。插件作者应让这类操作可重复、可取消,并明确区分外部操作和存档提交。