所有已实现的 Covel 插件。本页当前以
plugins/**/PLUGIN.md与对应handler.js / tools/*.js的实现为准。
按 Stage(见 调度阶段)分组,点击直达。
pregame— 游戏初始化 function runtime(stage: setup)char-creator/player-init— 玩家建角 agent runtime(stage: setup)world-init/schema-gen— 世界维度 agent runtime(guard 门控,stage: setup+after: [pregame])scene-stage/seed— 舞台开场种子 function runtime(stage: setup,为叙事不发scene.set兜底)
npc-graph/rag-retriever— NPC 图谱结构化检索dice-check/roller— 骰子判定预掷骰池(function,注入叙事引擎)scene-cast— 对话模式当前场景演员(function)scene-stage/resolver— 场景/昼夜解析(event 触发,消费scene.set,无 stage)
narrator— 主叙事生成器chat-mode-narrator— 对话 / 舞台模式叙事器(与 narrator 互斥)
codex— 知识图鉴 agentguide— 行动引导 agentcore-quest— 任务日志 agentaffinity— 玩家↔NPC 好感度跟踪 agentinventory— 行囊/物品台账 agentnpc-graph/extractor— NPC 关系图抽取 agentchar-creator/character-tracker— NPC 发现与状态跟踪 agentscene-prompts— 对话模式玩家口吻短回复 agentmimo-tts/auto-narrate— 叙事旁白自动 TTS(function,ctx.speech)dice-check/recorder— 判定回执记录(event 触发,消费check.resolved,无 stage)scene-stage/background-gen— 场景背景后台增量生成(event 触发,无 stage,execution: background)
character-blueprint— 可复用角色蓝图 + 世界角色导入character-presence— 角色头像 / 立绘 / 语音媒体living-world-rules— 长期世界规则 → lorebook 注入branch-reply— 回复候选 + 投影历史改写
dashscope-image-gen— DashScope(万相)剧情插图:manual prompt agent + event 生成 followeropenai-image-gen— OpenAI 兼容剧情插图:同样的两段式管线(gpt-image 系)mimo-tts— 小米 MiMo TTS 旁白语音:auto 逐回合朗读 + 消息内「朗读」按钮
UI-only(无 runtime,仅出现在概览表)
memory— 长期记忆摘要面板 + 声明默认核心记忆块(memoryBlocks),不占调度槽位
world.yaml 可以声明 requiredPlugins、recommendedPlugins、excludedPlugins、pluginPolicy 和 worldData。加载后这些值进入 WorldRecord.metadata:
requiredPlugins:准备页锁定启用。recommendedPlugins:准备页默认启用。excludedPlugins:准备页默认关闭。pluginPolicy:描述场景意图和组合包,可包含preset、preferTags、avoidTags、requireCapabilities、requiredPlugins、recommendedPlugins、excludedPlugins和packs。写在顶层的同名三组字段也会被前端并入pluginPolicy。worldData:可选,指向data/world.data.yaml;当前会读取本地 YAML/JSON/Markdown/Text/Media source,生成轻量WorldRecord.metadata.worldData摘要,投影world:metadata.dimensions,并在 session 创建时导入plugin:*/*、plugin:*/*+lorebook、lorebook、characters、media+indexTo。
第三方插件可以把插件数据声明为 schema: plugin://<pluginId>/<namespace> 与 to: plugin:<pluginId>/<namespace>。完整格式见 World Data。
内置组合包由前端提供:traditional-story、dialogue-mode、low-cost。世界可以用 pluginPolicy.preset 引用,也可以在 pluginPolicy.packs 自定义组合包。对话模式世界通常启用 chat-mode-narrator、scene-cast、scene-stage、scene-prompts、character-blueprint、character-presence、living-world-rules、branch-reply,并排除默认 narrator、guide 以及包级旧下游插件。多 runtime 插件当前按包选择;例如 npc-graph/rag-retriever 和 npc-graph/extractor 同属 npc-graph 包,准备页会一起启用或关闭。scene-stage 由 chat-mode-narrator 的 relations.requires 强制拉起(同 scene-cast),即便玩家在准备页手动关闭也会被服务端展开逻辑重新加回——世界包引用 plugin:scene-stage/scenes 的 worldData source 因此总能解析到已激活插件。
🔵 core(pluginType: core-plugin,不可禁用) · ⚪ optional(pluginType: plugin,可禁用) · 🧠 uses LLM(agent runtime) · ⚙ pure function(runtimeType: function,零 token) · 🖼 UI only(只提供面板,无 runtime)
调度以命名 stage(setup / pre-turn / narrative / post-turn / audit)为一级分组,stage 之间是严格屏障——上一 stage 全部 runtime 结束(成功/失败/skip 均计入)才进入下一 stage。同一 stage 内部由 DAG 调度器 依据每个 runtime 的 needs / after / inputs 绑定与 input.inject[].from 推导——无环依赖的 runtime 自动归入同一层并发执行,独立 runtime 之间同层并行,name 只做并列时的稳定排序 tiebreaker。
needs 的每一项可以是 runtime id 字符串(该 runtime 必须本回合成功,缺席=skip,绝不当作成功),或 { capability: <name> }(本回合在场的某个声明该 capability 的 runtime 成功即满足;零个在场提供者=不满足→skip)。capability 形态让一个下游插件按 capability 发现"当前模式的提供者",无需写死具体插件名 —— 例如 guide/scene-prompts 用 { capability: narrative-engine } 同时适配 narrator(传统模式)与 chat-mode-narrator(对话模式)。两个叙事引擎都在 capabilities 里声明了 narrative-engine。
| Stage | Runtime | 说明 |
|---|---|---|
pre-turn |
npc-graph/rag-retriever · dice-check/roller · scene-cast |
narrator 的依赖上游(function runtime,无 LLM) |
narrative |
narrator · chat-mode-narrator |
主叙事生成器(互斥,二选一激活) |
post-turn |
guide · codex · core-quest · affinity · inventory · npc-graph/extractor · char-creator/character-tracker · scene-prompts · mimo-tts/auto-narrate · branch-reply |
除 branch-reply 外都以 { capability: narrative-engine } 依赖当前模式的叙事引擎(branch-reply 无 needs,按 narrativeOutput 非空这一契约自行发现叙事);彼此独立 → 同 stage 内并行执行 |
setup stage(会话 phase === "setup" 时运行)走:pregame → world-init/schema-gen → char-creator/player-init,顺序完全由声明边决定:world-init/schema-gen 声明弱排序 after: [pregame](pregame 失败不拦 schema 生成);char-creator/player-init 声明 turn-scoped needs: [pregame, world-init/schema-gen](needs 既是同一 pass 内的 DAG 边、也是同回合门控),并通过 input.inject 读取 schema-gen output 的 worldSchema。DAG 顺序只保证上游结果可见;未提交的 proposal store write 要到 finalizer transaction 后才可读取,因此同轮数据传递必须使用 runtime output/inputs。三者均为 stage: setup + trigger: auto(maxTriggerCount 为重试预算)。
所有插件单声明 stage + needs/after,无例外;event / manual runtime 不设 stage。
Setup 状态机(插件视角):setup 阶段的每个 runtime,框架按 (session, runtimeId) 维护一个解析状态,插件作者需要知道三种落点:
pending— 本代(generation)尚未解析;若最近一次尝试失败会带上lastError。done— 已解析,resolution分两种:completed(跑到成功、通常伴随 runtime 输出preGameDone: true)或waived(按策略跳过);warning携带非致命提示。blocked— 无法推进(重试预算耗尽、或硬依赖失败),reason说明原因;后续轮次不再自动调度。
重试预算 = maxTriggerCount(不写=无限)。generation 在插件集合 / 插件版本变化时递增,因此上一代的 done 不会压住新一代的重跑。一次执行内产出的 done 对下一次执行可见——这正是 needs(session) 判定所依据的"执行开始时冻结的持久快照"。
| ID | 类型 | Stage | 触发方式 | 模型 slot | 描述 |
|---|---|---|---|---|---|
| pregame | core-plugin | setup |
auto(setup 段一次性,重试预算 1) | — | 游戏初始化(function runtime) |
| world-init/schema-gen | core-plugin | setup · after: [pregame] |
auto(setup 段一次性,重试预算 1) | plugin |
世界维度初始化(guard + agent,setup 第二步) |
| char-creator/player-init | core-plugin | setup |
auto(guard 门控) | plugin |
玩家角色创建(agent runtime;turn-scoped needs 依赖 pregame + schema-gen) |
| npc-graph/rag-retriever | plugin | pre-turn |
scheduled(interval=1,function runtime) | — | NPC 图谱结构化检索器,向 narrator 注入相关关系事实 |
| dice-check/roller | plugin | pre-turn |
scheduled(interval=1,function) | — | 每回合预掷 d20 骰池,向叙事引擎注入 <check-results> 判定规则与骰值 |
| dice-check/recorder | plugin | 无(event,不设 stage) | event(topic: check.resolved) |
— | 记录叙事发回的批量判定回执,驱动消息区 🎲 结果块与「判定记录」面板 |
| scene-cast | plugin | pre-turn |
scheduled(interval=1,function) | — | 对话模式当前场景演员,注入 activeCastContext |
| scene-stage/resolver | plugin | 无(event,不设 stage) | event(topic: scene.set) |
— | 场景/昼夜解析,写 stage/current;未命中注册表时向 background-gen 发内部信令 |
| scene-stage/seed | plugin | setup |
auto(maxTriggerCount=1) | — | 开局把注册表首个场景写入 stage/current,为叙事整局不发 scene.set 兜底 |
| narrator | core-plugin | narrative |
auto | story |
主叙事生成器 |
| chat-mode-narrator | plugin | narrative |
auto | story |
对话 / 舞台模式叙事器(conflicts: narrator,requires 场景/角色子系统) |
| guide | plugin | post-turn |
scheduled(interval=1, cooldown=1) | plugin |
行动引导 + 聊天内建议面 |
| codex | plugin | post-turn |
auto(每轮,紧跟 narrator 之后) | plugin |
知识图鉴系统(agent runtime) |
| core-quest | plugin | post-turn |
auto(needs: capability narrative-engine) | plugin |
任务日志:从叙事登记/推进结构化任务(agent runtime) |
| affinity | plugin | post-turn |
auto(needs: capability narrative-engine) | plugin |
玩家↔NPC 数值好感度跟踪(agent runtime) |
| inventory | plugin | post-turn |
auto(needs: capability narrative-engine) | plugin |
行囊台账:从叙事记录物品得失与装备变化(agent runtime) |
| npc-graph/extractor | plugin | post-turn |
scheduled(interval=1, cooldown=1) | plugin |
NPC 关系图抽取器 |
| char-creator/character-tracker | core-plugin | post-turn |
scheduled(interval=1, cooldown=1) | plugin |
NPC 发现 + 角色状态跟踪 |
| scene-prompts | plugin | post-turn |
scheduled(interval=1) | plugin |
对话模式玩家口吻短回复 |
| character-blueprint | plugin | — | manual(按需 / world-data 导入) | — | 可复用角色蓝图;dataSchemas blueprints/characters 接收世界导入 |
| character-presence | plugin | — | manual(按需 / world-data 导入) | — | 角色头像 / 立绘 / 语音媒体;dataSchemas presence/assets |
| living-world-rules | plugin | — | manual(按需 / world-data 导入) | — | 长期世界规则 → lorebook.upsert 注入叙事;dataSchemas rules |
| branch-reply | plugin | post-turn |
auto(每回合播种)+ manual(重生成/采纳) | — | 回复候选 + prompt-history-rewriter(自动播种叙事原文,重生成走 LLM;投影历史折叠已采纳回合) |
| scene-stage/background-gen | plugin | 无(event,不设 stage) | event(topic: scene-stage.generate.requested,execution: background) |
— | 后台增量生成缺失的场景背景图(ctx.images),产出 asset.generate |
| dashscope-image-gen/prompt-generator | plugin | — | manual(右侧「生成图片」按钮) | default |
剧情插图提示词 agent,发 image.generate.requested 唤醒生成 follower |
| dashscope-image-gen/image-generator | plugin | 无(event,不设 stage) | event(topic: image.generate.requested,execution: background) |
— | DashScope wan2.x 生成插图(ctx.images),产出 asset.generate + 画廊记录 |
| openai-image-gen/prompt-generator | plugin | — | manual(右侧「生成图片」按钮) | default |
剧情插图提示词 agent,发 openai-image.generate.requested |
| openai-image-gen/image-generator | plugin | 无(event,不设 stage) | event(topic: openai-image.generate.requested,execution: background) |
— | OpenAI 兼容生成插图(ctx.images),产出 asset.generate + 画廊记录 |
| mimo-tts/auto-narrate | plugin | post-turn |
auto(needs: capability narrative-engine) |
— | 叙事旁白自动 TTS(ctx.speech,MiMo wire);每回合写「朗读」按钮的消息层锚点 |
| mimo-tts/manual-narrate | plugin | — | manual(消息内「朗读」按钮,execution: background) |
— | 按钮 payload 指定段落的手动 TTS(ctx.speech) |
| memory | core-plugin | — | UI-only(无 runtime) | — | 长期记忆摘要面板 + 通过 memoryBlocks 声明默认核心记忆块(剧情/角色关系/场景/玩家状态) |
| cost-gate | plugin | — | hook-only(opt-in,默认禁用) | — | 跨切面:每会话 token 预算门控(hooks:PostLLMResponse/PreSchedule/TurnStart/SessionEnd) |
| director | plugin | — | hook-only(opt-in,默认禁用) | — | 跨切面:用 PostContextAssembly 给本局所有 story runtime 统一注入导演前言 |
| story-guard | plugin | — | hook-only(opt-in,默认禁用) | — | 跨切面:故事文本红线净化(PostLLMResponse)+ 高危工具拦截(PreToolUse) |
🔵 core · ⚙ pure function
Quick use:如果你要在 session 首轮(先于任何 LLM 调用)跑一段确定性的初始化逻辑——读世界观、发欢迎通知、写 welcome banner——挂这个插件。
路径: plugins/pregame/
| 字段 | 值 |
|---|---|
| pluginType | core-plugin(不可禁用) |
| stage | setup — 仅在 phase === "setup" 时调度,报告完成后不再运行 |
| trigger | auto,maxTriggerCount: 1(重试预算) |
| runtimeType | function(纯函数执行,不调用 LLM) |
| handler | ./handler.js |
| input.inject | 无 |
职责: 游戏开始时第一个执行的插件。读取世界观设定,发送欢迎通知,输出世界观摘要供后续叙事插件(narrator、codex、char-creator)作为上下文引导。
setup 契约: 位于 setup stage(phase === "setup" 期间运行),maxTriggerCount: 1 保证仅在 session 首轮执行。完成后可在 RuntimeOutput 中声明 preGameDone: true,框架据此在 session.setupRuntimes 集合中记录本 runtime 已完成 setup 初始化(API 响应仍会派生出兼容字段 preGameCompleted)。
🔵 core · 🧠 uses LLM(guard 可能跳过)
Quick use:如果你想让 LLM 在首轮根据 WORLD.md 自动派生一套"角色属性 schema + 世界词条"并写进 session lorebook,挂这个插件。已有 schema 时 guard 会直接 skip,零 LLM 开销。
路径: plugins/world-init/
单 runtime 插件,使用 guard 机制实现无 LLM 开销的前置门控。
| 字段 | 值 |
|---|---|
| pluginType | core-plugin(不可禁用) |
| stage | setup · after: [pregame](弱排序:沿用历史串行,pregame 失败不拦本 runtime;player-init 通过 turn-scoped needs 硬依赖本 runtime) |
| trigger | auto,maxTriggerCount: 1(重试预算) |
| model | plugin |
| guard | ../../guard.js |
| capabilities | [world-data-provider] |
| tools.plugin | set-world-schema, set-world-entries-batch |
| tools.builtin | 无(setup 期只写世界 schema,不回读自身 plugin-data) |
| ui.right | ./ui/world-overview.json, ./ui/world-schema.json |
无论 guard 复用已存在 schema、采用世界声明、从 dimensions 派生,还是 agent 生成,成功/完成输出都会携带结构化 worldSchema。下游 setup runtime 可在同一 execution 中通过 runtime inject 消费它;持久 world.schema 仍在 proposal commit 后成为后续 execution 的 store 真值。
Guard 门控: guard.js 在 LLM 调用前执行(纯函数,零 LLM 开销),按优先级决定角色属性 schema,命中任一即返回 { skip: true } 跳过 LLM:
- 当前 session 已有 schema + 词条 → 直接复用。
- 世界声明了
world.yaml的characterAttributes(→metadata.characterAttributes,也接受同义键metadata.schemas)→ 原样写入该 schema(并从 dimensions 导入词条)。这是权威来源:即使同世界已有其他 session,也以世界声明为准,因此编辑characterAttributes会在新 session 生效。 - 世界有 dimensions 但未声明属性 →
deriveSchema(dimensions)从世界数据推断通用属性。 - 以上都没有 → 才进入
schema-genagent,由 LLM 生成。
快路径只读世界包声明与 dimensions,不跨 session 复制 schema 或词条:session plugin-data 可被会话持有者经通用
PUT /plugin-data写入,hosted 层级下来源 session 还可能属于其他用户,复制即泄露 + 投毒。详见 world-data.md。
characterAttributes[*].name / description 支持 I18nText({ "zh-CN": …, "en-US": … }),右栏与 prompt 注入按 locale 解析显示。
Agent 职责: 读取世界观文档,通过专用 local tools 批量生成角色属性 schema 和世界词条。只需 2 次工具调用(set-world-schema + set-world-entries-batch)。
数据存储结构:
- namespace
schema— 维度 schema 定义(plugin_data),通过world.schema注入 prompt。 - session lorebook(
strategy: 'constant')— 世界词条数据,通过world.entries注入 prompt。
set-world-entries-batch 工具写入 session 级 lorebook;每个词条成为一条 constant 类型的 lorebook row,id 按 world-entry:<key> 稳定化,insertionOrder 按批内顺序以 100 为步长递增。
🔵 core · 🧠 uses LLM
Quick use:你想要默认的主叙事引擎——每轮读 {{ player.message }} + 世界观 + 历史,输出 outputKind: story 的第二人称叙事。换掉它就是换掉整个故事基调。
路径: plugins/narrator/
| 字段 | 值 |
|---|---|
| pluginType | core-plugin(不可禁用) |
| stage | narrative(每个主循环轮次都执行) |
| trigger | auto — 每轮 narrative stage 执行 |
| outputKind | story(输出显示在主聊天区) |
| model | story |
| capabilities | [narrative, narrative-engine] |
| tools.builtin | world-dimension-get、emit-event |
| advertiseEvents | true(segment 5 注入 <available-events> 目录) |
| input.inject | npc-graph/rag-retriever → npcContext → <npc-relationships>;dice-check/roller → checkContext → <check-results>(缺席解析为空) |
职责: 根据玩家输入、世界观和历史上下文生成主线叙事。输出 narrativeOutput 字段供其他插件引用;需要精确世界字段时调用 world-dimension-get 按需读取。
上下文变量:
{{ world.lore }}— 世界观全文{{ world.dimensions }}— 世界维度信息{{ world.openingScenario }}— 开场场景(叙事用整段铺垫){{ world.openingHook }}— 可选,会话首屏「扉页大字」(一句话钩子,UI 用){{ world.openingChips }}— 可选,会话首屏的 2-4 个短 tag(UI 用){{ world.tone }}— 叙事风格设定{{ player.message }}— 玩家当前输入{{ player.character }}— 玩家角色数据(CharacterSummary){{ session.turnNumber }}— 当前回合数(全局 turnCount){{ session.status }}— 会话状态(active/paused/ended)
调度说明: Narrator 位于 narrative stage,每个主循环轮次(phase !== "setup")都会执行。是否在首轮发声由 setup stage 的插件流水线决定(例如 char-creator/player-init 处理玩家建角),Narrator 不再通过 phases 自我门控。
⚪ optional · ⚙ pure function(rag-retriever)· 🧠 uses LLM(extractor)
Quick use:你想要一张会话级的 NPC 关系图——叙事里提到的人物、势力、欠债 / 结盟 / 背叛关系自动抽取并持久化,narrator 下轮能沿 2-hop 邻居看到"跟这个人相关的所有事实"。
路径: plugins/npc-graph/
多 runtime 插件。包含两个协作的子 runtime:
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function(无 LLM 调用,纯结构化检索) |
| handler | ./runtimes/rag-retriever/handler.js |
| stage | pre-turn(在 narrative stage 之前) |
| capabilities | [npc-graph, graph-rag] |
| trigger | scheduled,interval: 1 |
每个主循环回合开始时自动运行:从 playerMessage 中匹配 NPC 节点名(含别名,case-insensitive),沿邻接索引做 2-hop BFS,只保留有效区间仍开放的边(invalidAt === undefined;被新版本取代的旧边保留在库里做溯源,但不进 prompt,否则同一对人物会出现两条互相矛盾的事实),按 (validAt, |strength|) 排序后取 top-20,输出 markdown 列表到 npcContext 字段。narrator 通过 input.inject 把这段文本作为 <npc-relationships> 块注入 prompt 末尾。
Phase 3.5 升级路径:当 framework 层向 function handler 暴露 gateway 后,将升级为"先 embed 查询 → vector search → 子图扩展"的混合检索。当前为纯结构化版本。
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | agent(LLM 驱动) |
| stage | post-turn(与 guide / codex / character-tracker 同 stage 并行执行) |
| capabilities | [npc-graph, relationship-tracking] |
| trigger | scheduled,interval: 1,cooldownTurns: 1 |
| needs | [{ capability: narrative-engine }] — 引擎无关(H-04),当前模式的叙事引擎失败时 skip |
| input.inject | narrator + chat-mode-narrator → narrativeOutput → <narrator-output>(双引擎声明,缺席的解析为空);plugin-data[nodes] → <existing-npcs>、plugin-data[edges] → <existing-relations>(format: summary,现有图在构建 prompt 时注入,免去每轮 list-npc-graph 往返 —— 同 codex <existing-entries> 模式;工具 name-first,LLM 只需看见图,不需携带 id) |
| model slot | plugin |
| tools.plugin | upsert-npc-graph(批量写节点+边)、list-npc-graph(现有图已注入,仅在需要某关系完整 fact 时按需调用) |
| ui.right | ./ui/npc-graph-panel.json |
职责: 维护一张会话级的人物-关系图。从叙事文本中抽取 NPC 节点(individual / group / faction)、它们的关系(信任、结盟、欠债、背叛等)以及每条关系的自然语言事实,持久化到 plugin_data 的 nodes、edges、index、meta 四个 namespace。
与
memory.character_relationships的分工(不是重复):npc-graph 负责 NPC↔NPC 的结构化、带类型的有向图(玩家通常不是图中节点);memory 的character_relationships块负责 主角(玩家)↔NPC 的散文式羁绊(好感 / 信任 / 承诺 / 态度)。两者抽取的是不同信号、互补存在——memory 的 extractionHint 已显式限定为「只记录与玩家相关的关系」,避免两套系统重复抽取同一信号。一次只合并(抑制其一)会丢失玩家中心的羁绊连续性,故保留两者、以边界澄清替代合并。
数据模型(packages/shared/src/types/npc-graph.ts):
NpcNode:{id, name, aliases?, type, labels, summary ≤200 字符, firstSeenTurn, lastSeenTurn, attributes?}NpcEdge:{id, source, target, relation (UPPER_SNAKE_CASE), strength [-1..1], fact (完整一句话), validAt, invalidAt?, evidenceTurnIds}NpcGraphOntology:{version, entityTypes, edgeTypes, createdAt, updatedAt}— 本体约束
本体设计(受 MiroFish 启发):
- 节点类型固定三类:
individual | group | faction - 关系类型推荐 10 种
TRUSTS / FEARS / RESPECTS / ALLY_OF / OPPOSES / COMPETES_WITH / WORKS_FOR / SUBORDINATE_OF / OWES_DEBT_TO / KNOWS_ABOUT - LLM 使用
upsert-npc-graph时通过 name 而非 ID 引用节点,工具内部去重并分配短 ID(npc-xxxx、edge-xxxx) - 每条 edge 的
fact必须是完整自然语言句子 —— 这是 Phase 3 Graph-RAG 的检索单元
边的版本化(有效区间):一条边是「带有效区间的事实版本」,不是唯一行。同一 (source, target, relation) 在一个会话里可以有多个版本,其中至多一个是开放的(invalidAt === undefined)。
| 再次提交同一关系 | 行为 |
|---|---|
strength 与 fact 都没变 |
空操作,结果里标记 skipped: "unchanged relation" |
| 任一变化 | 开放版本在当前回合被关闭(写入 invalidAt = 当前回合),同时新开一个版本(validAt = 当前回合),结果里带 supersedes: <旧 edge id> |
validAt / invalidAt / firstSeenTurn / lastSeenTurn 用的都是真实逻辑回合数(context.turnNumber,即玩家消息计数);不在回合上下文中执行时写 -1 表示未知——衰减 / 近期性排序都依赖这个时间语义。
没有 invalidAt 的行一律读作开放版本,因此不带该字段的存量数据照常检索并正常参与取代。
存储布局(plugin_data 表中):
namespace="nodes" key=npcId value=NpcNode
namespace="edges" key=edgeId value=NpcEdge
namespace="index" key=by-source:{npcId} | by-target:{npcId} value=string[] (edge IDs)
namespace="meta" key=ontology value=NpcGraphOntology (Phase 3 wire-up)
Phase 进度: 当前实现已经包含 ui/npc-graph-panel.json 与 GraphCanvas。后续演进点集中在 Graph-RAG 的向量检索部分。
⚪ optional · ⚙ pure function(两个 runtime 均零 LLM)
Quick use:想让"我尝试撬锁"的成败有规则、有随机性、可审计——每回合预掷 d20 骰池注入叙事引擎,叙事按规则判定并发回执,玩家在消息区看到 🎲 判定结果,右栏有完整判定记录。
路径: plugins/dice-check/
多 runtime 插件(根 PLUGIN.md 仅包级摘要)。
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function(无 LLM) |
| handler | ./runtimes/roller/handler.js |
| stage | pre-turn |
| trigger | scheduled,interval: 1 |
| outputKind | system |
| capabilities | [dice-check, check-context] |
职责:每回合用 node:crypto 预掷 3 个 d20,输出 checkContext(骰池 + 判定规则 markdown),narrator / chat-mode-narrator 经 input.inject 注入为 <check-results>(插件缺席时解析为空,叙事按一般逻辑处理);骰池审计轨写 plugin_data[rolls](key = turnId)。判定规则:骰值 + 角色卡数值属性修正 vs 难度 DC(轻松 8 / 普通 12 / 困难 16 / 极难 20),天然 20 大成功、天然 1 大失败。
| 字段 | 值 |
|---|---|
| runtimeType | function(无 LLM) |
| handler | ./runtimes/recorder/handler.js |
| stage | 无(event 触发不设 stage) |
| trigger | event,topic check.resolved |
| events | 消费 check.resolved(批量 payload { checks: [1..3] },schema schemas/check-resolved.event.json) |
| ui.message | check-message.json — 消息区 🎲 判定结果块(骰式 / 成败配色 / critical 强调) |
| ui.right | checks-panel.json — 「判定记录」面板(倒序) |
职责:叙事引擎判定完成后,把整回合全部判定装进 checks 数组、经 emit-event 一次性发射 check.resolved(emit-event 对同 topic 每回合去重,逐次发射会丢第二条——批量是契约而非优化)。recorder 逐项容错落库:无效项跳过、有效项照常,写 checks(key <turnId>-<序号>,含展示字段)与 message(本回合判定数组,消息块数据源)。
⚪ optional · 🧠 uses LLM
Quick use:你想要一本自动更新的世界百科——LLM 读每轮叙事识别新地点/人物/势力/物品,unlock 成卡片;重复出现时补充原有条目而不是新建。
路径: plugins/codex/
| 字段 | 值 |
|---|---|
| pluginType | plugin(可禁用) |
| stage | post-turn |
| runtimeType | agent(默认,LLM 驱动) |
| trigger | auto(每轮触发;needs: [{ capability: narrative-engine }] 保证在当前模式的叙事引擎失败时 skip,不会用空 <narrator-output> 幻觉) |
| model | plugin |
| tools.plugin | unlock-codex-entries, update-codex-entry |
| ui.right | ./ui/codex-panel.json |
| ui.message | ./ui/codex-message.json |
| input.inject | narrator + chat-mode-narrator → narrativeOutput → <narrator-output>(双引擎声明,缺席的解析为空)plugin-data[entries] → <existing-entries>(format: summary,maxEntries: 100) |
职责: 分析叙事文本,识别并登记本轮出现的知识条目(地点 / 人物 / 势力 / 物品 / 技能 / 传闻 / 怪物)。对"没有新发现"的回合直接结束。prompt 里同时看到本轮叙事 <narrator-output> 和已登记条目 <existing-entries>,所以 LLM 一次调用即可决定是 unlock-codex-entries(新增)还是 update-codex-entry(补充已有),无需额外调用 plugin-data-list 往返。
数据持久化: unlock-codex-entries 批量写入 plugin_data[entries];update-codex-entry 读取指定 entryId(就是 plugin-data 的 key,形如 codex-xxx)并按 append-only 语义合并内容、合并标签、可选升级 rarity。
框架能力依赖:input.inject: plugin-data source 由 @covel/context 的 async build 路径提供;当 manifest 声明了任何 kind: plugin-data 注入时,turn-executor 会自动切到异步装配路径并调用 store.listPluginData(sessionId, pluginId, namespace)。同步路径保持零改动,其他插件不受影响。
UI 面板: ui/codex-panel.json 承接完整图鉴,ui/codex-message.json 负责聊天内的本轮新增摘要。框架通过 /api/ui-specs 发现并渲染这两个 surface。
⚪ optional · 🧠 uses LLM
Quick use:自动任务日志——每轮读叙事,把明确出现的任务线索登记/推进成结构化任务,右栏按状态分组、变更出消息提示;世界包可预置主线/支线任务。
路径: plugins/core-quest/
| 字段 | 值 |
|---|---|
| pluginType | plugin(可禁用) |
| stage | post-turn |
| trigger | auto;needs: [{ capability: narrative-engine }] |
| model | plugin |
| outputKind | system |
| entry | ./server/index.js |
| tools.plugin | upsert-quests |
| input.inject | narrator + chat-mode-narrator → narrativeOutput → <narrator-output>(双引擎声明);plugin-data[quests] → <existing-quests>(format: summary,maxEntries: 50) |
| dataSchemas | quests(schemaVersion 1,acceptsWorldData,schema URI plugin://core-quest/quests) |
| ui.right | quest-log-panel.json — 任务面板(进行中含 objectives 勾选清单 / 已完成 / 已失败) |
| ui.message | quest-changes-block.json — 本回合任务变更块 |
职责:只记录叙事明确出现的任务信号——不发明任务、每回合新任务 ≤3、目标勾选与完成/失败必须有叙事证据、无信号回合不调工具。upsert-quests(≤5/次)按 name 归一化合并:已有任务提供字段覆盖,objectives 依次按稳定 id、规范化 text、保守语义匹配更新勾选,命中后保留已有目标原文,未命中才追加;世界预置任务(<existing-quests> 已注入)只推进不重建。变更摘要写 message namespace 驱动消息块。
世界导入:world.data.yaml 用 schema: plugin://core-quest/quests + to: plugin:core-quest/quests + key: id 预置任务,记录形状 { id, name, description, status?, objectives?: [{id?, text, done?}], giver?, reward? };建议世界包为每个 objective 提供任务内稳定的 id(参考 worlds/emberback/data/quests.yaml)。
⚪ optional · 🧠 uses LLM
Quick use:玩家↔NPC 的数值好感度——每轮读叙事记增量,右栏每人一条好感条 + 档位徽标,变更出消息 toast;世界包可预置关键 NPC 初始好感。
路径: plugins/affinity/
| 字段 | 值 |
|---|---|
| pluginType | plugin(可禁用) |
| stage | post-turn |
| trigger | auto;needs: [{ capability: narrative-engine }] |
| model | plugin |
| outputKind | system |
| entry | ./server/index.js |
| tools.plugin | update-affinity |
| input.inject | 双引擎 narrativeOutput → <narrator-output>;plugin-data[affinity] → <existing-affinity>(format: summary,maxEntries: 50) |
| dataSchemas | affinity(schemaVersion 1,acceptsWorldData,schema URI plugin://affinity/affinity) |
| ui.right | affinity-panel.json — 好感面板(score 双向条 + tier 徽标 + 最近变化原因) |
| ui.message | affinity-toast.json — 变更 toast("莉安 +5(你替她挡了债主)"式) |
职责:只对叙事中玩家与 NPC 的明确互动记 delta(日常 ±1..5、重大事件至多 ±20;只为有名字且发生实际互动的 NPC 建条目)。update-affinity(≤5/次)按 name 归一化去重:不存在则以 score=0 创建后应用 delta,score 累计 clamp [-100,100],按阈值派生 6 档 tier(≤-60 敌视 / -59..-20 冷淡 / -19..19 中立 / 20..59 友好 / 60..84 亲密 / ≥85 挚爱,tierLabel 为 I18nText),history 保留最近 10 条。
与 npc-graph / memory 的分工(不是重复):affinity 只管玩家↔NPC 的数值好感;NPC↔NPC 结构化关系归
npc-graph;散文式羁绊叙述归 memory 的character_relationships块。三者抽取不同信号、互补存在。
世界导入:schema: plugin://affinity/affinity + to: plugin:affinity/affinity + key: id,记录形状 { id, name, score, notes? };预置记录不带派生字段(tier/history),首次 delta 时由工具补齐(参考 worlds/emberback/data/affinity.yaml)。
⚪ optional · 🧠 uses LLM
Quick use:行囊台账——叙事里"你捡起了铁剑",自动记成结构化背包(已装备分组 + 数量),得失出消息 toast;世界包可预置开局装备。
路径: plugins/inventory/
| 字段 | 值 |
|---|---|
| pluginType | plugin(可禁用) |
| stage | post-turn |
| trigger | auto;needs: [{ capability: narrative-engine }] |
| model | plugin |
| outputKind | system |
| entry | ./server/index.js |
| tools.plugin | update-inventory |
| rpc | item-op(entry 注册)——玩家侧装备/卸下/丢弃,面板逐物品按钮经 invokePluginAction 触发;丢弃与工具 remove-to-zero 同款墓碑语义 |
| input.inject | 双引擎 narrativeOutput → <narrator-output>;plugin-data[items] → <existing-inventory>(format: summary,maxEntries: 80) |
| dataSchemas | items(schemaVersion 1,acceptsWorldData,schema URI plugin://inventory/items) |
| ui.right | inventory-panel.json — 行囊面板(已装备分组 + 背包列表,数量徽标 + tags pill,alwaysRender) |
| ui.message | inventory-message.json — 得失 toast("+ 铁剑 ×1 / − 火把 ×2"式) |
职责:只记录叙事明确的获得/失去/消耗/装备变化——不发明物品,大宗模糊描述("一堆金币")合理量化并在 description 注明估算,货币也是物品(tag currency)。玩家侧操作刻意只到装备位与丢弃:item-op RPC 管 equip/unequip/drop(装备是玩家的配置选择,不经叙事),而"使用物品"必须走故事输入——静默扣数量的"使用"按钮会绕过叙事引擎。update-inventory(≤8/次)按 name 归一化到稳定短 id:add 叠加数量、remove 减量至 0 时墓碑化(quantity: 0, removed: true——proposal 管线暂无 plugin-data 删除类型,UI 隐藏墓碑,同名再获得复活同一记录)、set 局部更新、equip/unequip 切装备位;对不存在条目的 remove 容错 skip。得失摘要写 message namespace(key = turnId)。
世界导入:schema: plugin://inventory/items + to: plugin:inventory/items + key: id,记录形状 { id, name, quantity, description?, tags?, equipped? }(参考 worlds/emberback/data/items.yaml)。
🔵 core · 🧠 uses LLM(player-init,guard 门控)· 🧠 uses LLM(character-tracker)
Quick use:你要玩家在首轮填一张"角色创建表单"生成主角;并且每轮自动跟踪叙事里出现的 NPC、角色状态变化(受伤、死亡、装备、关系)并写进 characters 表。两个子 runtime 共用同一个 character-panel.json 侧边栏。
路径: plugins/char-creator/
多 runtime 插件。player-init 负责玩家角色创建,character-tracker 负责持续跟踪 NPC 和角色状态变化。两者共用同一个 character-panel.json 侧边栏面板(通过 group: "character" 聚合)。
| 字段 | 值 |
|---|---|
| pluginType | core-plugin(不可禁用) |
| stage | setup |
| runtimeType | agent(默认,LLM 生成开场表单;guard 命中时跳过) |
| trigger | auto(guard 门控) |
| needs | [pregame, world-init/schema-gen](turn-scoped:既是同 pass 的 DAG 边,也是同回合上游门控) |
| input.inject | world-init/schema-gen.worldSchema → <same-turn-world-schema>;同轮结构化 schema 优先,已提交的 world.schema 兜底 |
| guard | ./guard.js — 若 player 已存在或已收到表单提交则 skip LLM |
| model | plugin |
| ui.right | ../../ui/character-panel.json |
两步流程(第 1 步由 LLM agent 完成,第 2 步由 guard.js 确定性完成):
-
第 1 步 - 生成表单(
<player-submission>为空时):- 读取世界 schema
- 直接返回
interaction.request形式的角色创建表单 - 表单字段从
worldSchema.character-attributes.attributes中选取,最多 4 个字段含characterName
-
第 2 步 - 提交创建(
<player-submission>包含表单值时):- 读取最近一次 player input submission
- 合并 schema
defaultValue - 通过 guard 的 execution write buffer 生成
character.upsert与plugin.dataproposals - 输出
preGameDone: true,标记本 runtime 已完成 setup 初始化(框架将其累加到session.setupRuntimes)
当前代码状态: 这一条路径保持在插件包内部,实现位于 runtimes/player-init/guard.js(deterministic 提交分支)。schema defaultValue 在写入边界合并进存库 fields(与 builtin create-character 一致),使右栏显示、模型 get-character 与 prompt 注入读到同一份字段,schema 通过 well-known namespace/key 发现而非硬编码 world-data 插件 id。如果后续希望统一 deterministic runtime 的 trace 与工具链,可以把这条流程收敛到 builtin character tools。
| 字段 | 值 |
|---|---|
| pluginType | core-plugin |
| stage | post-turn(与 guide / codex / extractor 同 stage 并行) |
| trigger | scheduled,interval: 1,cooldownTurns: 1 |
| model | plugin |
| tools.builtin | create-character, update-character, get-character(get-character 仅在注入名册被截断时按需调用;不声明 list-characters——名册已由 <existing-characters> 注入) |
| input.inject | narrator + chat-mode-narrator → narrativeOutput → <narrator-output>(双引擎声明,缺席的解析为空);plugin-data[characters] → <existing-characters>(format: summary,现有角色名册在构建 prompt 时注入,免去每轮 list-characters 往返 —— 同 codex <existing-entries> 模式) |
| needs | [{ capability: narrative-engine }] — 引擎无关(H-04),当前模式的叙事引擎失败时 skip |
职责: 每轮扫描 narrator 输出,发现新的有名字 NPC → create-character(type="npc");检测叙事中的角色状态变化(受伤、死亡、装备、关系)→ update-character(fields: {...})。工作流:
- 查看
<existing-characters>(框架自动注入的现有角色名册,行首即角色 id)避免重复——名册既已注入,本 runtime 不再声明list-characters - 阅读叙事识别新 NPC + 状态变化
- 仅对明确出现的变化调用 create/update 工具(update 用注入名册里的 id;摘要不足以决策时才按需
get-character) - 每次最多创建 5 个 NPC(防止 runaway)
- 不修改玩家角色属性(除非叙事明确描述)
⚪ optional · 🧠 uses LLM
Quick use:你要让 LLM 在每轮叙事之后给玩家提三组行动建议(safe / aggressive / creative)并接入聊天输入框——让 narrator 专注叙事、选择引导交给这个插件。
路径: plugins/guide/
| 字段 | 值 |
|---|---|
| pluginType | plugin(可禁用) |
| stage | post-turn(与 codex / extractor / character-tracker 同 stage 并行) |
| trigger | scheduled,interval: 1,cooldownTurns: 1 |
| model | plugin |
| tools.plugin | generate-guide |
| ui.message | ./ui/action-guide-block.json |
| input.inject | narrator + chat-mode-narrator → narrativeOutput → <narrator-output>(列出两个已知叙事引擎,缺席的解析为空,由在场的那个填充) |
| needs | [{ capability: narrative-engine }] — 按 capability 发现当前模式的叙事引擎,传统模式下解析为 narrator、对话模式下解析为 chat-mode-narrator;该引擎失败时仍 skip。引擎无关,因此 guide 在两种模式下都可用 |
职责: 在叙事推进后,分析当前情境,为玩家生成分风格的行动建议。让 narrator 专注叙事,选择引导交由本插件。引导按 capability 发现叙事引擎,因此在传统模式与对话模式下都能工作(默认仅传统模式启用,玩家可在对话模式手动开启)。
风格分类:
- safe(稳妥) — 低风险、谨慎的选择
- aggressive(激进) — 直接、对抗性的选择
- creative(创意) — 非常规、巧妙的选择
触发逻辑: cooldownTurns: 1 确保首轮不触发(避免与角色创建冲突)。位于 post-turn stage,每轮 narrator 之后执行。如果叙事中没有明显决策点,LLM 不会调用工具。
UI 渲染: 当前 generate-guide 会把 topic 与三组建议写入 plugin_data[message]。ui/action-guide-block.json 读取这些字段,渲染三组策略卡和自定义输入;玩家点击建议后进入待发送区,由底部输入栏统一发送。
⚪ optional · ⚙ hook-only(无可调度 runtime)
Quick use:想给每局对话设一个 token 花费上限——接近上限时自动停掉后台生成(codex / guide / 抽取器),到上限时暂停本回合——启用这个插件。它完全靠生命周期 hook 工作,不进调度、不写库。
路径: plugins/cost-gate/
| 字段 | 值 |
|---|---|
| pluginType | plugin(可禁用;前端 low-cost 组合包默认启用, 其它包 / 世界需手动启用) |
| outputKind | system |
| capabilities | cost-control |
| hooks | PostLLMResponse(计量) · PreSchedule(软上限收窄) · TurnStart(硬上限 abort) · SessionEnd(清理) |
职责: Covel 首个消费 hook 生命周期的「跨切面框架能力插件」示例。维护每会话的进程内 token 计数:
PostLLMResponse(enforce: post)累加每次 LLM 调用的usage,纯观察不改写;PreSchedule在软上限后把本回合 runtime 收窄为仅outputKind: "story"(按字段判定,不硬编码插件 ID),跳过后台 LLM 生成;TurnStart(enforce: pre)在硬上限 abort 整回合,abortReason透传前端;SessionEnd清理该会话的计数桶,防止进程内 Map 泄漏。
setup stage runtime(phase === "setup" 期间)由框架强制保护,PreSchedule 收窄只影响主循环。
配置(per-session userSettings,env 兜底): 两个阈值现已 per-session 可配——hook 经 HookContext.getOwnSettings() 读取本插件解析后的 userSettings(manifest 默认值与玩家保存值合并的冻结快照),玩家可在 设置 > Plugins > cost-gate 按局调整。softTokens(默认 400000)软上限 · hardTokens(默认 600000)硬上限。每次 hook 调用按三级回退链解析:per-session userSettings → env(COST_GATE_SOFT_TOKENS / COST_GATE_HARD_TOKENS)→ 硬编码默认,故只设 env 的旧部署照常工作。软上限须低于硬上限,否则收窄无窗口(cost-gate 一次性告警)。
限制: 计数为进程内、非持久——重启清零,多进程(PG / T3)不共享(单进程 T1/T2 是硬上限,T3 为每进程软信号)。详见 plugins/cost-gate/README.md。
⚪ optional · ⚙ hook-only(无可调度 runtime)
Quick use:想让本局所有叙事(narrator / chat-mode-narrator 等所有 story runtime)共享一致的语气 / 安全 / 风格前言,而不必逐个改它们的 postHistory——启用这个插件。
路径: plugins/director/
| 字段 | 值 |
|---|---|
| pluginType | plugin(可禁用,默认不启用) |
| outputKind | system |
| capabilities | narration-director |
| hooks | PostContextAssembly(turn 级、每 runtime 一次) |
职责: 用 PostContextAssembly 在每个 story runtime 的系统提示末尾追加统一的「导演前言」。仅对 payload.outputKind === "story" 的 runtime 注入(按字段判定,不硬编码插件 ID);非 story runtime 原样放行。前言文本为插件自带静态常量。
为支持这种「只塑形 story」的判定,框架给 PostContextAssembly 的 payload 增加了只读 outputKind 字段(AssembledContextView.outputKind,可选、纯增量、hook 不可改写)。
限制: 前言来自插件包内静态资源;若要「每会话可调」,可配合 HookContext.getOwnSettings()(见 plugin-authoring hooks 段)。详见 plugins/director/README.md。
⚪ optional · ⚙ hook-only(无可调度 runtime)
Quick use:托管 / 多人环境想要一层可插拔的内容安全——对故事文本做确定性红线净化、剥离模型自我暴露 / 选项菜单,并拦截高危工具调用——启用这个插件。
路径: plugins/story-guard/
| 字段 | 值 |
|---|---|
| pluginType | plugin(可禁用,默认不启用) |
| outputKind | system |
| capabilities | content-safety |
| hooks | PostLLMResponse(净化)· PreToolUse(拦高危工具) |
职责: 两道确定性、保守的守卫:
PostLLMResponse对response.content做红线净化(剥离 AI/模型自我暴露样板、Llama 模板标记、部署配置的红线词)+ 选项菜单剥离(连续 ≥2 行的枚举选项 / 带尾冒号的菜单头;孤立的行首缩写如C. S. Lewis不误伤)。完整回填LLMResponse(仅换 content);净化为空时保守放行(绝不把真实叙事清成空白)。PreToolUse对高危工具名(delete-everything/drop-database等 deny-list,可经 env 扩展)返回abort,仅跳过该工具不中断回合。
注意:PreToolUse 的工具名嵌在
payload.toolCall.name,而 frontmattermatch只对顶层 payload key 等值,故 deny-list 判定在 handler 内完成。
配置(env): STORY_GUARD_REDACT_TERMS(额外红线词,逗号分隔)· STORY_GUARD_REDACT_MARK(替换标记,默认 [redacted])· STORY_GUARD_BLOCKED_TOOLS(额外拦截工具名)。
限制: 净化是确定性正则,不替代模型层安全;依赖 M1(resume 路径已接 PostLLMResponse,本批审计已修)才能覆盖挂起→恢复的输出。详见 plugins/story-guard/README.md。
⚪ optional · 🧠 uses LLM
Quick use:对话 / 舞台模式的主叙事器,替代 narrator。读 {{ player.message }} + 世界观 + 当前场景演员 + NPC 关系,输出对话密度可调的 outputKind: story 叙事。
路径: plugins/chat-mode-narrator/
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| stage | narrative(每个主循环轮次) |
| trigger | auto |
| outputKind | story |
| model | story |
| capabilities | [narrative, chat-mode, narrative-engine] |
| tags | mode:dialogue · role:narrator |
| relations | conflicts: narrator;requires: scene-cast, scene-stage, scene-prompts, character-blueprint, character-presence, living-world-rules, branch-reply |
| tools.builtin | emit-event |
| advertiseEvents | true(segment 5 注入 <available-events> 目录) |
| input.inject | scene-cast/activeCastContext → <active-cast>;npc-graph/rag-retriever/npcContext → <npc-relationships>;dice-check/roller/checkContext → <check-results>(缺席解析为空) |
userSettings(世界用 pluginSettings.chat-mode-narrator 预置,玩家可覆盖):
| key | 类型 | 默认 | 范围 / 选项 |
|---|---|---|---|
dialogueRatio |
number | 70 | 30–90(step 5)——对话 / 角色反应占比 |
proseLength |
select | medium |
short / medium / long |
上场角色数由 scene-cast 的
activeSpeakerCount控制(它是实际裁剪 cast 的插件)——userSettings 按声明插件作用域隔离,所以这个旋钮必须挂在 scene-cast 上。chat-mode-narrator 的 prompt 以注入的<active-cast>实际人数为准,不再自带该设置(修复了"narrator 被告知 N、cast 却恒为 2"的分裂大脑)。
职责:启用时 relations.conflicts 自动顶替 narrator,relations.requires 自动拉起整套对话子系统。是 dialogue-mode preset 的核心。
⚪ optional · ⚙ pure function
Quick use:对话模式下追踪"当前在场的角色",从 session 角色里挑出上场演员与说话人,注入 chat-mode-narrator 与 scene-prompts。
路径: plugins/scene-cast/
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| stage | pre-turn(narrator 之前) |
| runtimeType | function(无 LLM) |
| trigger | scheduled,interval: 1 |
| outputKind | system |
| capabilities | [scene-cast] |
| tags | mode:dialogue · role:scene-state · role:character |
| output | activeCastContext → 注入 <active-cast> |
| ui.right | scene-cast-panel.json |
userSettings:
| key | 类型 | 默认 | 范围 / 选项 |
|---|---|---|---|
activeSpeakerCount |
number | 2 | 1–4(step 1)——每个节拍的上场角色数 |
职责:每轮从 characters(含 world-data 导入的角色卡)选出当前场景演员,给对话叙事提供"谁在场"。是 chat-mode-narrator 的上游依赖。上场角色数由本插件的 activeSpeakerCount 决定(声明在真正裁剪 cast 的插件上,避免跨插件 userSettings 无法生效的陷阱)。
⚪ optional · ⚙ pure function
Quick use:统一事件发射层(emit-event)里 scene.set 事件的第一个消费方——解析叙事当前所在的场景与昼夜,写 stage/current 驱动舞台背景;未命中世界注册表时向后台 runtime 请求增量生成。
路径: plugins/scene-stage/
多 runtime 插件。根 PLUGIN.md 只是插件级元信息(名称/描述/关联),不是可执行 runtime;三个真正被发现、调度的 runtime 都在 runtimes/ 下(discoverPlugins 对声明了 runtimes/ 的插件只扫描 runtimes/*/PLUGIN.md,根 PLUGIN.md 不参与调度)。events[].schema 与 dataSchemas.*.schema 仍按插件根目录相对路径解析,因此三个 runtime 共享插件根的 schemas/;只有 handler 与 ui.* 相对各自 runtime 目录。stage/current 记录的构造集中在 lib/stage-data.js(buildStageRecord / makeStageProposal),resolver 与 seed 共用,避免两个写入方在字段上漂移。
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function(无 LLM) |
| handler | ./runtimes/resolver/handler.js |
| stage | 无(event 触发不设 stage,由回合内事件扇出唤起,不受 stage 屏障约束) |
| trigger | event,topic scene.set |
| outputKind | system |
| capabilities | [scene-stage] |
| tags | mode:dialogue · role:scene-state · cost:function · ui:right-panel |
| dataSchemas | scenes(acceptsWorldData,世界包导入的场景注册表,一行文档) |
| events | 消费 scene.set;内部发射 scene-stage.generate.requested(advertise: false,不进 <available-events> 目录) |
| ui.right | ./runtimes/resolver/ui/scene-stage-panel.json |
userSettings:
| key | 类型 | 默认 | 范围 / 选项 |
|---|---|---|---|
autoGenerateScenes |
toggle |
true |
未命中注册表时是否自动请求背景增量生成 |
maxGeneratedScenes |
number |
10 |
0–50(step 1)——单会话增量生成场景数上限 |
职责:narrator(或对话模式叙事器)经 emit-event 发射 scene.set 后,同回合触发本 runtime:读自身 scenes namespace(世界注册表)与 stage/current(上一状态),按精确名 / locationRef → 归一化子串 → 会话内已生成场景的顺序匹配 location。命中写 stage/current(source: "world" | "session");同 sceneId 且同 variant 时 no-op(不写不发 SSE,防抖)——但上一状态为 source: "pending" 时不视为 no-op:生成失败后重发的 scene.set 会重新发内部信令重试(成功场景的重复计费由 background-gen 的已生成检查防住)。未命中按 autoGenerateScenes + maxGeneratedScenes 门控:放行则 source: "pending" 并发内部信令请求 background-gen,门控不过或已达会话帽则 source: "none"。昼夜变体缺夜图时 resolved 回退日图。stage/current 额外写 sourceLabel(I18nText,source 对应的展示文案,如 pending → "背景生成中…")与 variantLabel(I18nText,昼夜文案 day → "白天" / night → "夜晚"),面板直接渲染,无需按枚举值自行翻译。
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function(无 LLM) |
| handler | ./runtimes/seed/handler.js |
| stage | setup |
| trigger | auto(maxTriggerCount: 1 是重试预算) |
| outputKind | system |
| tags | mode:dialogue · role:scene-state · cost:function |
职责:scene.set 的唯一发射方是叙事 LLM——事件目录注入的【必做】指示是提示词约束,不是保证。整局不发时 resolver 作为 event 触发 runtime 永远不跑,舞台恒空。本 runtime 是那条链路的确定性下限:在 setup 阶段跑一次,stage/current 已存在(恢复会话、setup 重试)或世界没有场景注册表时跳过,否则把注册表第一个场景按白天变体写入 stage/current(source: "world")。跑在任何叙事输出之前,因此不与 LLM 的 scene.set 竞争——两者若落在同一回合,事件扇出顺序不定,后写的会盖掉正确场景。叙事之后发的 scene.set 由 resolver 正常覆盖此记录。
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function(无 LLM,调用 ctx.images) |
| handler | ./runtimes/background-gen/handler.js |
| stage | 无(event 触发不设 stage) |
| execution | background(不阻塞回合关键路径) |
| timeoutMs | 360000 |
| trigger | event,topic scene-stage.generate.requested |
| outputKind | plugin |
| capabilities | [image-generation](D 管线 asset 强制校验:必须产出 asset.generate) |
| tags | mode:dialogue · role:scene-state · cost:function |
职责:消费 resolver 的内部信令,用注册表随带的 style 块拼 prompt(prefix + subject + suffix,夜变体加 nightSuffix),subject 优先取事件载荷的 visualHint,缺失回退 location 名;调 ctx.images.generate(尺寸固定 1536x1024,横版背景约定)产出 asset.generate。day 变体先行生成,night 变体首次夜晚请求该场景时才懒生成。完成后更新 stage/generated 会话索引,若 stage/current 仍指向该场景则把 source 从 "pending" 刷新为 "session"(plugin-data.changed SSE 驱动面板换图)。
⚪ optional · 🧠 uses LLM
Quick use:对话模式版的 guide——读 chat-mode-narrator 输出,给玩家三四个"像玩家自己会说的话"的快捷回复,而非系统按钮口吻。
路径: plugins/scene-prompts/
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| stage | post-turn |
| runtimeType | agent(model plugin) |
| trigger | scheduled,interval: 1(无 cooldownTurns——每个玩家回合都出快捷回复) |
| outputKind | system |
| capabilities | [scene-prompts](舞台 choices 层按此能力发现,非硬编码插件 id) |
| tags | mode:dialogue · role:quick-reply |
| input.inject | chat-mode-narrator + narrator → narrativeOutput → <narrator-output> |
| needs | [{ capability: narrative-engine }] — 引擎无关,按 capability 发现当前模式的叙事引擎;两种模式下都可用 |
| tools.plugin | generate-scene-prompts |
| ui.message | scene-prompts-block.json |
⚪ optional · ⚙ pure function
Quick use:可复用角色蓝图库;也是 world 包导入角色卡的目标插件(dataSchemas.blueprints + source 的 effects: characters)。instantiate: true 时 emit character.upsert 实例化为 session 角色。
路径: plugins/character-blueprint/
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function,trigger: manual |
| outputKind | system |
| capabilities | [character-blueprint] |
| dataSchemas | blueprints(acceptsWorldData)· characters(acceptsWorldData,UI 镜像) |
| ui.right | blueprints-panel.json |
职责:蓝图作为参考库存放,effects: [characters] 时实例化为角色。world 包接法见 world-data.md Character Blueprint Import。
⚪ optional · ⚙ pure function
Quick use:给角色关联头像 / 立绘 / 语音媒体,在右侧角色面板与对话立绘中显示。world 包用 media + presence source 交付(按 sha256 内容寻址)。
路径: plugins/character-presence/
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function,trigger: manual |
| outputKind | system |
| capabilities | [character-presence] |
| dataSchemas | presence(acceptsWorldData)· assets(acceptsWorldData,媒体索引) |
| ui.right | character-presence-panel.json |
职责:presence 记录把 characterId 的 avatar / sprite / voice 指向内容寻址的媒体;媒体本体存媒体库,记录只放 { id(sha256), mime, size }。无媒体时优雅降级(面板在、图为空)。交付契约见 world-data.md Character Presence Portraits。
⚪ optional · ⚙ pure function
Quick use:长期世界规则 / 习俗 / 禁忌;world 包用 dataSchemas.rules 导入,规则经 lorebook.upsert 注入 narrator / chat-mode-narrator 提示。
路径: plugins/living-world-rules/
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function,trigger: manual |
| outputKind | system |
| capabilities | [living-world-rules, world-info] |
| dataSchemas | rules(acceptsWorldData) |
| ui.right | living-world-rules-panel.json |
职责:规则 schema 见 schemas/rules.schema.json(kind / category / budgetClass / coordinate / keys / insertionOrder)。world 包用 to: plugin:living-world-rules/rules+lorebook 同时写规则与 lorebook。
kind → lorebook strategy 映射:只有带关键词的 triggered 规则映射为 selective(按 keys 命中才注入);evolving 与 constant 是常驻(constant strategy,每轮注入)。一条 triggered 规则若未填关键词会回退为常驻 constant,而不是静默永不生效——避免"面板显示 enabled、却从不进 prompt"的分裂。
⚪ optional · ⚙ pure function
Quick use:为同一回合生成多个回复候选并记录已采纳版本;作为 prompt-history-rewriter,把已采纳的备选回合折叠进投影历史。舞台模式的"换个说法重试"靠它。
路径: plugins/branch-reply/
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function,trigger: auto,stage: post-turn |
| outputKind | system |
| capabilities | [branch-reply, prompt-history-rewriter] |
| ui.message | branch-reply-block.json |
生命周期(两条路径,按 manualPayload 是否存在区分):
- 自动播种(seed,无
manualPayload):作为trigger: auto、stage: post-turn的 runtime,每个故事回合在叙事引擎(narrativestage)之后运行,从ctx.completedResults读取当前激活叙事引擎的narrativeOutput,把它作为 candidate[0]("原文")写入message/turnsnamespace,并把产出该叙事的runtimeId一并记入turns记录。发现方式与引擎无关:按narrativeOutput非空这一叙事契约识别,不硬编码任何叙事插件 id,因此narrator与chat-mode-narrator通用。播种按turnId幂等(不会重复播种),空回合 / 系统回合不播种。这是该 block 能出现的前提——ui.messageblock 只有在其messagenamespace 被写入后才渲染,纯手动写入者无法自举(这是它此前完全不显示的根因)。 - 手动动作(
manualPayload存在,经 plugin-rpc):createCandidates/acceptCandidate。其中createCandidates(前端"重生成"按钮)通过ctx.gateway调用快速文本 slot 生成 1-2 条同一剧情节拍的真实改写(语言跟随ctx.locale与原文);当宿主无 gateway / 无 slot 或调用失败时,仅返回原文,绝不编造英文填充近似句。createCandidates/acceptCandidate都会把播种时记录的叙事runtimeId透传到turns记录。
职责:buildProjectedPromptHistory 读其 turns namespace,把采纳的备选回合折叠进投影历史;未发现时历史原样透传。由于自动播种本身也会在历史里追加一条 sourceRuntimeId="branch-reply" 的 assistant 消息,改写器用 turns 记录里的 runtimeId 精确命中叙事引擎那条消息,而非 branch-reply 自己的播种消息(runtimeId 由播种时发现得到,非硬编码)。
⚪ optional · 🎨 媒体生成
Quick use:右侧「生成图片」按钮 → prompt agent 整理当前剧情为画面需求(单场景或多格漫画)→ 经 image.generate.requested 事件唤醒后台 follower 调 DashScope 万相(wan2.x)出图,落画廊 + asset.generate。
路径: plugins/dashscope-image-gen/
多 runtime 插件(结构同 scene-stage):
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | agent |
| model | default |
| trigger | manual(右侧 ui/generate-button.json,expectsBackgroundFollower) |
| output | schema: ./output.schema.json — JSON envelope,必须携带 events[].topic = image.generate.requested |
| capabilities | [image-prompt, manual-invoke] |
| input.inject | plugin-data[prompts] → <previous-image-prompts>(ids-only,避免复述旧图) |
| userSettings | composition(single-scene / comic-strip)· comicPanels 等构图选项 |
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function(无 LLM,调用 ctx.images) |
| stage | 无(event 触发不设 stage) |
| execution | background · timeoutMs: 360000 |
| trigger | event,topic image.generate.requested |
| capabilities | [image-generation] |
| userSettings | modelPresetId(默认 image slot)· imageSize · n · negativePrompt 等 |
| ui.right | gallery.json(ImageGallery)· jobs.json(ImageJobs) |
职责:从事件载荷取 prompt,映射 userSettings 后整体交给框架统一图像管线 ctx.images.generate()(DashScope 异步任务提交/轮询、x→* 尺寸转换、OSS URL 摄取进 MediaStore、promptHash 去重都在框架侧的 dashscope-wan wire)。产出逐图 images namespace 记录 + assetGenerations[];失败路径写可见的 failed 卡片。
⚪ optional · 🎨 媒体生成
Quick use:与 dashscope-image-gen 同构的两段式插图管线,走 OpenAI 兼容 Images API(gpt-image 系或任何 OpenAI 兼容第三方);事件 topic 为 openai-image.generate.requested,两插件可并存不串线。
路径: plugins/openai-image-gen/
结构与 dashscope-image-gen 一致(manual prompt agent + event follower),差异点:
| 差异 | 值 |
|---|---|
| 事件 topic | openai-image.generate.requested |
| 默认 preset | openai-image |
| 尺寸归一化 | handler 把 1024*1536 这类 * 分隔写法归一为小写 x 形式(OpenAI 约定) |
| style | 无专用 wire 参数,折叠进 prompt 文本(…, style: watercolor) |
⚪ optional · 🔊 媒体生成
Quick use:把叙事引擎的输出用小米 MiMo TTS 朗读出来——每回合自动产出音轨(可关),右侧 Tab 是 playlist;每条剧情消息下有「朗读」按钮手动重读。
路径: plugins/mimo-tts/
多 runtime 插件;根 PLUGIN.md 声明 entry: ./server/index.js,entry 经 covel.registerWires() 注册 MiMo speech wire(mimo-tts/mimo,api-key header + OpenAI 风格 chat/completions;slot 侧 providerRequestMetadata.speechWire = "mimo-tts/mimo" 启用)。
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function(无 LLM,调用 ctx.speech.generate) |
| stage | post-turn |
| trigger | auto;needs: [{ capability: narrative-engine }] |
| inputs | narrative ← capability narrative-engine 的 /narrativeOutput(引擎无关) |
| capabilities | [tts, narrative-audio] |
| userSettings | enabled(关掉只停自动合成)· modelPresetId · voice · format · maxChars 等 |
| ui.right | audio-tab.json(AudioPlayer playlist) |
职责:读本轮叙事 → ctx.speech.generate()(wire 分发、MediaStore 持久化、promptHash 去重在框架侧)→ 写 tracks 记录 + assetGenerations[]。**每回合(含 enabled: false 时)**写 message namespace 的 { turnId, text } 记录——这是「朗读」按钮的消息层锚点与 payload 来源(消息层 spec 依赖该 namespace 才渲染)。
| 字段 | 值 |
|---|---|
| pluginType | plugin |
| runtimeType | function · execution: background |
| trigger | manual(消息内 ui/play-button.json) |
| capabilities | [tts, narrative-audio, manual-invoke] |
| ui.message | play-button.json — $state 绑定从 message namespace 记录读 /turnId + /text 作为 invoke payload |
职责:朗读按钮 payload 里的段落文本(manual 激活不解析 turn inputs,payload.text 必需——无 inputs 兜底),走同一条 ctx.speech.generate() 管线,tracks 记录标 triggeredBy: manual。
| 插件 | 预期 stage | 描述 |
|---|---|---|
| combat | pre-turn |
回合制战斗(判定原语已由 dice-check 提供) |
inventory与core-quest已落地(见上文注册表);dice-check/affinity一并补齐了 RPG 玩法基座。
当前世界包推荐使用 pluginPolicy 表达插件组合意图。内置前端组合包包括:traditional-story(传统叙事主线 + 行动建议/图鉴/关系图,玩家口吻设置为可选项)、dialogue-mode(对话优先叙事 + 场景演员/短句回复 + 玩家口吻设置)、low-cost(保留核心流程并减少下游 LLM 调用,玩家口吻设置为可选项)。世界可以通过 preset 引用这些组合包,也可以在 packs 中提供自定义组合。
plugins/<plugin-id>/
├── README.md # 必需:给人类 / 开发者看的插件说明
├── PLUGIN.md # 必需:frontmatter 元信息 + Markdown 提示词
├── package.json # 必需:workspace 依赖声明
├── vitest.config.ts # 可选:测试配置
├── server/ # 可选:统一服务端入口(entry 字段指向)
│ └── index.js # export default function (covel) { ... }
├── tools/ # 可选:本地工具实现(由 server/index.js 导入注册)
│ └── my-tool.ts
├── tests/ # 可选:测试文件
│ └── my-plugin.test.ts
└── references/ # 可选:按需加载的参考资料
└── world-lore.md
一个插件可以包含多个子运行时,放在 runtimes/ 目录下。每个子运行时有独立的 PLUGIN.md。name 字段使用 plugin-id/runtime-name 格式(斜杠分隔)。
plugins/<plugin-id>/
├── README.md # 必需:给人类 / 开发者看的插件说明
├── package.json
├── PLUGIN.md # 可选:包级摘要(见下)
├── runtimes/
│ ├── runtime-a/
│ │ ├── PLUGIN.md # name: plugin-id/runtime-a
│ │ └── PLUGIN.en.md # 可选:英文版(只翻译正文与自然语言字段)
│ └── runtime-b/
│ ├── PLUGIN.md # name: plugin-id/runtime-b
│ └── handler.js # function runtime 的 handler
└── tools/ # 可选:所有子运行时共享的工具
真实多 runtime 范例见
plugins/npc-graph/(extractoragent +rag-retrieverfunction)和plugins/char-creator/(player-init首轮 agent +character-tracker持续 agent)。world-init当前是单 runtime(schema-gen)+ 一个guard文件,不算多 runtime。
子运行时之间可通过 input.inject 传递数据(上游输出 → 下游 prompt 注入)。
每个插件根目录都需要 README.md。它不参与 runtime 执行,也不会被当作模型提示词;它服务于插件作者、维护者和代码审核者。建议包含:
- 插件解决什么玩家问题
- 运行时组成:哪些 agent runtime、哪些 function runtime、哪些 UI 面板
- 数据读写:主要 namespace、world-data schema、角色 / lorebook / media 写入
- 主要文件:
handler.js、tools/、ui/、schemas/、tests/ - 测试方式、已知限制和后续计划
displayName 是 frontmatter 顶层的 I18nText 字段,作为插件在插件列表、provider 切换器等 UI 处的友好展示名——与 name(runtime id,用于数据隔离 / 工具作用域 / trace)解耦。单 runtime 与多 runtime 插件都适用;服务器经 PluginSummary.displayName 下发,前端按 locale 解析(缺失时回落到 name,再回落到 plugin id)。
---
name: guide # runtime id(保持小写短横线)
displayName: # I18nText:玩家可见的友好名
zh: 行动引导
en: Action Guide
description: # I18nText:一句话简介
zh: 在每轮故事后给出几种行动建议。
en: Suggests a few actions after each story beat.
---没有 displayName 时,UI 退回显示 plugin id(如 dashscope-image-gen),冗长且不直观。所有插件(含内置与第三方)都建议声明 displayName;23 个内置插件均已声明中英文名。
兼容:多 runtime 插件的包级 PLUGIN.md(根目录仅含摘要 frontmatter、不作为 runtime 加载)若把
name写成 I18nText 对象,仍会作为展示名的回落来源;但新代码应优先用displayName,不要重载name。
| 值 | 含义 |
|---|---|
core-plugin |
核心插件,Session 中不可禁用 |
plugin |
普通插件,可按需启用/禁用 |
| 值 | 含义 |
|---|---|
agent(默认) |
LLM 驱动:构建上下文 → 调用 LLM → 工具循环 → 结果 |
function |
纯函数执行:直接调用 handler 指定的 JS 模块,不调用 LLM,零延迟 |
function 类型 runtime 需要额外声明 handler 字段指向 JS 模块路径。
entry 指向一个插件根目录相对的 JS 模块,default 导出一个工厂函数(同步或异步),接收统一的 PluginAPI facade(约定参数名 covel),在函数体内命令式注册插件的全部服务端能力:
entry: ./server/index.js # 整个插件声明一次(多 runtime 声明同一路径会去重,约定写在根 PLUGIN.md)// server/index.js
export default function (covel) {
// 本地工具(等价旧 tools.local;toolkit 即旧工厂注入包 { tool, z, shortId, shortIdBatch, withPendingProposals, store })
covel.registerTool(
covel.toolkit.tool({
name: "my-tool",
description: "...",
parameters: covel.toolkit.z.object({}),
execute,
}),
);
// 生命周期 hook(等价旧 hooks 字段;16 事件语义不变,options 支持 match 谓词 / timeoutMs / enforce)
covel.on("PostLLMResponse", handler, { enforce: "post" });
// RPC action(等价旧 rpc 字段;handler 内联,信任等级仍按插件来源钳制)
covel.registerRpc("my-action", handler, { description: "..." });
// 媒体 wire(等价旧 wires 字段;仍以 <pluginId>/<wireId> 命名空间注册,SSRF 防护经 covel.http 注入)
covel.registerWires({ image: [myWire] });
}- 类型可导入:
PluginAPI/PluginToolkit/PluginHookOptions/PluginRpcOptions/PluginEntryFactory从@covel/runtime导出(Public Plugin API 的稳定契约)。JS 插件用 JSDoc@param {import('@covel/runtime').PluginAPI} covel标注工厂参数,TS 插件直接import type。服务端实现按同一类型做编译期对齐(buildApi(): PluginAPI),不会与文档 / 作者可见类型漂移。 - 信任门控与 local tools 一致:builtin/official 在启动时执行 entry;community 延迟到插件激活(
ensurePluginEntry,与 runtime 加载同刻)。弃用的hooks字段(下方)同受此门控:community 的 legacy hook handler 在批准+激活前保持休眠(不import()),激活后每次触发还按 session 复核授权(H-03)。 - entry 抛错 / 非函数导出 / 路径逃逸只 warn 跳过,不影响启动;工厂每插件只执行一次(幂等)。
- agent runtime 暴露给 LLM 的工具仍需在各 runtime manifest 声明:entry 注册的工具用
tools.plugin(名字列表)声明可见性,替代旧tools.local的路径列表:
tools:
plugin: # entry 注册的工具名
- my-tool
builtin: # builtin 启用列表(不变)
- plugin-data-get移除说明:
tools.local已移除——声明它会让整个 manifest 加载失败。hooks/rpc/wires三个注册字段仍被接受但已弃用(启动时每插件 warn 一次),请迁移到entry。
wires 指向一个插件根目录相对的 JS 模块,default export { image?, speech?, transcription? } 三组 wire 数组,或一个接受 { fetchWithRetry, validateBaseUrl } 注入的工厂函数。框架加载后把每个 wire 以 <pluginId>/<wireId> 命名空间注册进 @covel/ai-provider 的对应 registry,llm.toml slot 通过 providerRequestMetadata.imageWire / speechWire / transcriptionWire 选中它。
wires: lib/wires.js # 整个插件声明一次即可(多 runtime 声明同一路径会去重)- 信任门控与 local tools 一致:builtin/official 启动即注册,community 在其 runtime 首次加载时注册。
- 路径逃逸 / 文件缺失 / 条目形状错误只 warn 跳过,不影响启动;重复注册幂等。
- 教程与 wire 接口签名见 plugin-authoring-advanced.md § 注册自定义 wire,slot 侧配置见 slots.md。
dataSchemas 声明插件哪些 plugin_data namespace 可以接收 world package 导入数据。world-data session importer 会在创建 session 前做插件启用检查,并用插件包内 JSON Schema 校验 source item。
dataSchemas:
relationships:
schemaVersion: 1
acceptsWorldData: true
schema: ./schemas/relationships.schema.json
description: Importable relationship records.| 字段 | 类型 | 说明 |
|---|---|---|
schemaVersion |
number |
namespace 数据契约版本 |
acceptsWorldData |
boolean |
true 时允许 world-data importer 写入 |
schema |
string |
插件根目录相对 JSON Schema 路径 |
description |
string |
面向作者的简短说明 |
多 runtime 插件可以在多个 runtime 的 PLUGIN.md 中声明同一 namespace;声明完全一致时合并到插件级 registry,冲突时插件注册失败。第三方 world 包引用该 namespace 时使用:
schema: plugin://social-sim/relationships
to: plugin:social-sim/relationships
key: idevents 声明该插件的某个 runtime消费的领域事件契约——通常配合 trigger: { type: event, topic: ... } 让另一个 runtime 的发射触发它。服务端按会话激活插件集聚合所有声明,供内置 emit-event 工具(见 tools.md #emit-event)做 topic 校验与 payload schema 校验。
events:
- topic: quest.updated
schema: ./schemas/quest-updated.event.json
description:
zh: 任务状态更新
en: Quest status updated
advertise: true # 默认 true,可省略| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
topic |
string |
必填 | 点分 kebab-case(domain.verb,如 quest.updated),正则拒绝其他格式 |
schema |
string |
必填 | 插件根目录相对 JSON Schema 路径,校验 data payload(与 dataSchemas 同规则) |
description |
I18nText |
必填 | 目录展示用说明,按 session locale 解析 |
advertise |
boolean |
true |
false 时该 topic 为内部信令:不出现在 <available-events> 目录里,且不进 emit-event 白名单——只能由声明它的插件自己的函数 runtime 经 output.events 结果通道发射(或 trigger: { type: event, topic } 触发消费方),agent 无法经 emit-event 直发。适合两个插件间不希望被通用叙事 runtime 随手调用的内部信令 |
同一 session 内若两个不同插件声明了同一 topic 但 schema 路径不同,服务端按插件激活优先级顺序首胜(保留先声明者的 schema)并 console.warn 一次(同一 (session, topic) 不重复告警)。
顶层 advertiseEvents: true 让该 runtime 在 prompt 段 5 收到当前会话已声明事件(advertise !== false 的那些)的目录文本(<available-events> 块,含 topic、locale 描述与必填字段名);要真正发射还需要在 tools.builtin 里加 emit-event。两者职责分离:advertiseEvents 只控制"是否看得到目录",tools.builtin: [emit-event] 才控制"能不能调用"。
advertiseEvents: true
tools:
builtin:
- emit-event目录为空(当前会话没有任何插件声明可发射事件)时不会注入 <available-events> 块,不占用 prompt 空间。narrator / chat-mode-narrator 已按此接入,作为发射方参考实现;scene-stage/resolver 消费 narrator 发射的 scene.set,是消费方(trigger: { type: event, topic: ... })的参考实现。
Function runtime 和 guard 的 FunctionHandlerContext 暴露:
interface FunctionHandlerContext {
recursiveCall(
delta: RecursiveCallDelta,
opts?: { reason?: string },
): Promise<NestedTurnResult>;
recursionDepth: number;
}recursiveCall() 会用当前 turn 输入作为基底,合并 delta 后重新进入 turn executor。
执行身份由框架持有,插件不可覆盖:
RecursiveCallDelta = Omit<Partial<TurnInput>, "sessionId" | "turnId" | "origin" | "parentTurnId">。这四个字段即使在运行时被传入也会被剥离——嵌套调用必须留在父 session 内(否则已批准的 handler 可读取并写入其他 session,绕过 hosted 的 session-owner 边界),并保留框架签发的子turnId(否则其 execution artifact 无法随父回合结算)。NestedTurnResult = Omit<TurnResult, "completeTurn">。completion barrier 只保留在顶层框架控制面;嵌套调用方若能触发它,就会在父回合 proposal 提交之前发出权威的turn.completed并启动 memory ingestion。 嵌套调用默认深度上限为10,manifest 可用maxRecursionDepth覆盖:
runtimeType: function
handler: ./handler.js
maxRecursionDepth: 5opts.reason 会写入 recursive.calling、recursive.completed、recursive.failed trace payload,方便在 debug timeline 中解释嵌套调用意图。超过上限会抛出 MaxRecursionExceeded,并进入 runtime 的失败路径。
Agent runtime 的前置门控函数。在 LLM 调用前执行(纯函数,零 token 开销),可用于检查前置条件、导入数据等。
guard: ../../guard.jsGuard 函数接收与 function runtime 相同的 FunctionHandlerContext,返回值规则:
{ skip: true, ... }— 跳过 LLM 调用,guard 输出作为 runtime 结果{ skip: false, ... }— 继续执行 LLM agent
Guard 适用于"先检查再决定是否需要 LLM"的场景,替代了之前需要独立 function runtime 做门控的模式。
声明该插件/世界贡献的核心记忆块(Letta 式 in-context memory)。框架的记忆系统(@covel/memory)会聚合所有已加载插件的 memoryBlocks,据此驱动每轮结束后的 LLM 抽取、持久化与 prompt 渲染——块定义因此是纯数据,而非内核硬编码。这正是「插件承载玩法、内核提供原语」在记忆维度的落地:侦探局可声明 clues / suspects / timeline,商战局可声明 deals / rivals,无需 fork 框架包。
builtin memory 插件声明默认的四个通用块(story_state / character_relationships / scene / player_profile)。任意插件或世界包都可追加自己的块;标签重复时按信任层级决胜(builtin > official > community):高信任声明覆盖低信任声明,与发现顺序无关——因此 community 插件无法靠抢先加载来静默覆盖 builtin 默认块的定义(如改写 story_state 的 extractionHint)。同一信任层级内取首次声明(稳定);当同层级的多个插件以不同定义声明同一标签时,框架打印一条 dev 警告。信任层级取自插件的发现来源(加载路径,不可伪造),框架不按具体插件 id 决胜。未声明任何 memoryBlocks 时,框架回退到 @covel/memory 内置的同名通用默认块。
每轮结束后的抽取输入同时包含叙事、工具摘要,以及已提交会话状态中的玩家角色和最近一次表单值。结构化会话事实具有最高事实优先级;player_profile 的首行由框架根据角色记录与世界属性显示名确定性生成,LLM 只维护其后的动态状态摘要。这样后续回合无法翻译、改写或覆盖玩家已确认的姓名与属性值。
世界包在 world.yaml 顶层(而非 PLUGIN.md)声明 memoryBlocks(字段形状相同)。与插件块的全局聚合不同,世界块按 session 解析:记忆系统把该 session 所属世界的块合并到全局插件块之上——基础块(插件 / 框架默认)在标签冲突时优先(builtin 默认受保护),世界只新增未占用的标签。因此侦探世界的会话才会出现 clues / suspects,其它题材会话不受影响。世界侧声明与示例见 world-data.md #世界记忆块memoryblocks。
| 字段 | 类型 | 说明 |
|---|---|---|
label |
string(snake_case) |
块机器标签:working_memory key、prompt XML tag、镜像 plugin-data key |
displayName |
I18nText |
UI 面板与 prompt 块标题的本地化显示名 |
extractionHint |
I18nText |
注入摘要 LLM system prompt 的逐块抽取指引(保持世界中立) |
icon |
string(可选,Lucide) |
UI 面板图标,缺省 Info |
maxChars |
number(可选) |
该块字符上限,覆盖管理器默认值(2000) |
memoryBlocks:
- label: clues
displayName: { zh: 线索, en: Clues }
icon: Search
extractionHint:
zh: 已发现的线索、物证及其与嫌疑人的关联。
en: Discovered clues, physical evidence, and their links to suspects.hooks 声明生命周期处理器。handler 路径相对插件目录解析,首次触发时懒加载。新代码请在 entry 模块里用 covel.on(event, handler, { match?, timeoutMs?, enforce? }) 注册(match 为谓词函数而非浅层等值 map);事件表与执行语义两种方式完全一致。
hooks:
- event: PreToolUse
handler: ./hooks/validate-tool.ts
enforce: pre
timeoutMs: 3000
match:
tool: create-character只要 manifest 声明了 hooks:,loader 会校验每个条目并注册有效 hook。单个 hook 条目格式错误或事件名未知时只跳过该条目并输出 warning,其他有效条目继续生效。
| 字段 | 类型 | 默认值 | 含义 |
|---|---|---|---|
event |
HookEvent |
必填 | 生命周期事件名 |
handler |
string |
必填 | hook 模块路径,默认导出 async 函数 |
enforce |
pre | normal | post |
normal |
排序分组,执行顺序为 pre → normal → post |
timeoutMs |
number |
5000 |
单个 handler 的超时 |
match |
Record<string, string | number> |
无 | payload 浅层等值过滤 |
同一事件内先按 enforce 分组排序;同组内全局 hook 先执行,插件 hook 保持注册顺序。
| Event | Semantic | 行为 |
|---|---|---|
SessionStart |
parallel |
会话级(无回合):会话创建 + 插件激活后触发,payload {sessionId, worldId}。观察型,不能否决创建(对齐 pi 的 session_start) |
TurnStart |
sequential |
回合开始的否决门:任一 handler abort 则整回合中止(无 runtime 运行,返回带 abortReason 的 TurnResult),用于访问控制 / 限流 |
PreCompaction |
sequential |
历史压缩前的否决门:任一 handler abort 则本回合跳过压缩、保留完整历史(对齐 pi 的 session_before_compact 取消路径) |
PostCompaction |
parallel |
并发观察压缩结果(compacted / summaryId);返回值只用于日志和 trace(对齐 pi 的 session_compact) |
PreSchedule |
sequential |
触发选择之后、调度之前观察 / 收窄本回合要跑的 runtime 集;replace.triggered 链式改写(如条件门控 / 成本控制)。严格 filter-only(2026-07-20 审计 H-03):返回列表按稳定 runtime 身份(manifest.name)与原集合对账,框架复用原 manifest 对象——不在原集合的注入项被丢弃并 warn,变造副本无法替换原 manifest。仅能影响主循环 runtime:phase === "setup" 时,框架强制保留被 hook 删掉的 setup stage runtime,避免静默中断会话初始化 |
PreRuntime |
sequential |
链式改写 runtime 输入;replace 会传给下一个 handler;abort 会停止执行 |
PostContextAssembly |
sequential |
turn 级(每 runtime 一次,buildContext 之后、进 loop 之前)改写已装配的 systemPrompt / 投影历史;replace.{systemPrompt,messages} 链式累积(对齐 pi 的 before_agent_start) |
PreLLMCall |
sequential |
每次 LLM 调用前非破坏性改写发往模型的请求;replace.{messages,model,tools} 链式累积。不改写底层 transcript(对齐 pi 的 context)。abort 无意义、视为不变 |
PostLLMResponse |
sequential |
LLM 响应返回后、工具派发前;replace.response 链式改写 content/toolCalls(对齐 pi 的 after_provider_response) |
PostRuntime |
sequential |
链式改写 runtime 输出:replace.result 重写该 runtime 的 RuntimeResult(链式累积),不改则原样。执行身份不可改写:pluginId / runtimeId / runId / turnId 始终被还原为框架实际选中并加载的 manifest 身份——提交阶段按这些字段重绑 proposal,否则已批准的 hook 能把写入重定向到别的插件名下 |
PreToolUse |
sequential |
链式改写 tool call;replace 会传给下一个 handler;abort 会跳过该 tool(不中止回合) |
PostToolUse |
sequential |
链式 patch tool result:replace.result 改写结果、replace.terminate: true 在记录该结果后结束工具循环(对齐 pi 的 tool_result.terminate)。结束循环用 replace.terminate,不要用 abort(PostToolUse 的 abort 不生效,结果原样、循环继续) |
PreStateCommit |
sequential |
链式改写 commit payload;任一 handler 可用 abort 拒绝 commit |
PostStateCommit |
parallel |
并发观察 commit 结果;返回值只用于日志和 trace |
TurnStop |
parallel |
并发观察回合结束;返回值只用于日志和 trace |
SessionEnd |
parallel |
会话级(无回合):会话 PATCH 状态→ended 或 DELETE 时触发,payload {sessionId, reason: "ended"|"deleted"}。仅在进入 ended 的那次触发(不重复),适合清理(对齐 pi 的 session_shutdown) |
PostToolUse为sequential:parallel语义会丢弃replace,因此结果 patch 与terminate必须在顺序链中累积。SessionStart/SessionEnd是会话级 hook(turnId为空):在 server 的 session 路由触发,不属于 turn pipeline。 Session 作用域:hook pipeline 是全局单例,但执行时按当前 session 的激活插件集过滤(hooks/hook-scope.ts,经 AsyncLocalStorage)——插件 hook 只对该插件激活的 session 触发,框架 hook(无pluginId)始终触发。turn hook 的作用域取自activeRuntimes,SessionStart/End 取自session.activePlugins。HookContext.activePluginIds暴露给 handler。
first 和 stream 已作为框架语义保留:first 用于未来的首个命中选择类 hook,stream 用于未来的流式 transform hook。
声明该 runtime 输出在 UI 中的处理方式。框架根据此字段决定消息展示策略,而非硬编码插件 ID。
| 值 | 含义 |
|---|---|
story |
主叙事内容,显示在主聊天流中 |
plugin(默认) |
辅助内容,可能被隐藏在主聊天之外 |
system |
系统级输出,不对玩家展示 |
示例 frontmatter:
outputKind: story仅在通过 POST /api/sessions/:id/plugin-rpc 的 runtimeId 分支手动触发时生效;调度器驱动的 runtime 忽略此字段。
| 值 | 含义 |
|---|---|
sync(默认) |
同步执行:HTTP 请求阻塞到 runtime 完成,返回 runtimeResults 汇总 JSON。适合可以秒级完成的 runtime(prompt 生成、状态校验等) |
background |
后台执行:立即返回 202 + jobId,通过 setImmediate 脱离请求继续跑。框架在 plugin_data 表 _jobs/{jobId} 记录任务生命周期(pending → done / failed),前端通过 plugin-data.changed SSE 感知并渲染 loading/final UI |
使用规则:
_jobs是框架保留命名空间,插件禁止直接写入;框架自动维护 row 生命周期- background 模式下,事件链 chain 仍然生效 —— 手动触发的 runtime emit 的
event.emitproposals 会在同一后台任务里按 stage/DAG 顺序执行下游 runtime - 如果 runtime 通过
input.inject向下游传递结构化数据,background 模式下下游 runtime 会看到最终态(不是增量),就像在 sync 模式下一样 - 不持会话锁执行:后台 follower 的 handler 跑在会话锁外,只有提交阶段(finalize 事务 + auto-snapshot)进锁——否则一次几分钟的出图会把玩家的下一条消息一起堵住。由此带来两条对插件作者可见的约定:
- 同一 runtime 的并发 follower 由框架按
<sessionId>::<runtimeId>串行,所以 handler 里"这张图是不是已经生成过"这类 check-then-act 仍然是原子的,不会重复计费;跨 runtime 不保证。 - handler 执行期间读到的会话数据可能被并发的玩家回合改写。handler 对自己命名空间的读-改-写是安全的(读经 writeBuffer overlay、写在同一事务提交),但如果 handler 把别处的状态读出来再写回去,请假设中途可能已经变了。
- 同一 runtime 的并发 follower 由框架按
- 进程重启不恢复:后台任务由进程内队列驱动,没有持久队列。重启后开机扫描会把上一个进程留下的
pending行标为failed(reason: "orphaned",保留triggerEvent供重试),框架不自动重跑——重跑要再计一次费,且请求级userSettings没有持久化在任务行上。需要"一键重试"的插件自行读这行的triggerEvent提供入口。
示例:
execution: background # wan2.x 文生图需要几十秒,不阻塞 UI详细 RPC 流程见 api.md #post-apisessionsidplugin-rpc。
能力标签数组,框架通过能力标签发现插件,而非硬编码插件 ID。
| 能力标签 | 含义 | 框架用途 |
|---|---|---|
narrative |
主叙事生成器 | 标识主叙事输出源 |
world-data-provider |
世界数据提供者 | 加载世界 schema/entries 到 turn context |
image-generation |
图像生成 | 前端展示「生成配图」按钮 |
memory-panel |
核心记忆面板宿主 | 记忆系统将核心记忆块镜像到该插件的 plugin-data,用于实时 UI 面板更新 |
persona-provider |
玩家人设提供者 | buildSessionContextSnapshot 从该插件的 session-binding / profiles 命名空间加载 activePersona。内置提供者 player-identity 已归档(plugins/_archive/,口吻归角色卡),capability 口子保留给第三方插件。未发现时不加载人设,优雅降级到 {{ player.character }}。 |
prompt-history-rewriter |
prompt 历史改写者 | buildProjectedPromptHistory 读取该插件的 turns 命名空间,把已采纳的备选回合折叠进投影历史(由 branch-reply 声明)。未发现时历史原样透传。 |
上表是插件级能力(匹配整个插件 manifest,对应 FrameworkCapability)。框架还消费一组runtime 级能力(匹配插件内某个具体子 runtime,对应 FrameworkRuntimeCapability),用于多步图像插件的链路发现:
| 能力标签(runtime 级) | 含义 | 框架用途 |
|---|---|---|
image-prompt |
图像提示词入口 runtime | 前端「生成配图」入口:发现声明该能力且 trigger.type === manual 的入口 runtime,经 plugin-rpc 触发并交给后台 follower。 |
image-generator |
图像生成后台 runtime | 图像面板「重跑」:发现声明该能力的后台 runtime,把提示词转成图像 asset。 |
dataSchemas.<namespace>.acceptsWorldData: true同样是一种能力声明:世界角色蓝图导入(blueprintStorageTargets/characterMirrorTargets)据此发现「接受世界蓝图 / 角色镜像」的插件(如character-blueprint),框架不再硬编码character-blueprint/char-creator。
声明 image-generation 的 runtime 在完成态返回 assetGenerations[],每一项包含 { ref: MediaRef, modality: "image", meta? }。图像画廊索引写入 plugin_data.images 时保存 { status, ref, prompt, ... },运行时会把旧 url / base64 / dataUrl 字段记录为 image.generate.plugin_data_inline_media error。
插件可以声明任意自定义能力标签。框架仅依赖上述已定义标签。框架代码(server / runtime / web)引用这些标签时不得使用裸字符串字面量,而应使用 @covel/shared 导出的常量:插件级用 FrameworkCapability(如 FrameworkCapability.WorldDataProvider),runtime 级用 FrameworkRuntimeCapability(如 FrameworkRuntimeCapability.ImageGenerator),这样拼写漂移会变成编译错误而非静默 undefined。两组常量的并集导出为 FRAMEWORK_KNOWN_CAPABILITIES(单一事实源);plugin-loader 在加载 PLUGIN.md 时,对「形似某个框架已知能力但拼错」的声明发 dev 警告(不阻断、不丢弃,自定义能力仍自由声明)。新增框架消费的能力标签时,需同时更新对应常量(packages/shared/src/types/plugin.ts)与本表。
API 暴露: Session plugins API(GET /api/sessions/:id/plugins)在响应中返回每个插件的 capabilities 字段(从所有子 runtime 的 manifest 中聚合),前端可据此发现插件能力。示例响应片段:
{
"id": "world-init",
"pluginType": "core-plugin",
"active": true,
"capabilities": ["world-data-provider"]
}示例 frontmatter:
capabilities: [narrative, world-data-provider]tags 是面向玩家、作者和准备页筛选的目录标签,例如 mode:dialogue、role:narrator、cost:llm。capabilities 保持机器能力契约;框架逻辑依赖 capabilities,准备页和组合包匹配使用 tags。
relations 描述插件目录关系,可包含 provides、requires、conflicts、recommends。每项就是一个字符串——插件 id 或 pluginId/runtimeId,没有对象形式。创建或启用 session 时,服务端会执行 requires 闭包并移除 conflicts 指向的插件;provides 和 recommends 作为目录/准备页信号保留。
provides 是唯一语义例外:它的字符串不是插件 id,而是一个不透明的能力标签,两个插件声明同一个标签即表示彼此可替换(narrator 与 chat-mode-narrator 都 provides: narrative-engine,服务端据此允许其一顶替另一个)。
对象形式已移除(早期文档列过
target/plugin/runtime/capability/tag/type/optional/reason)。前四个是写同一个 id 的四种等价拼法,后三个从来没有任何代码读过,而capability/tag更是写了就被静默忽略。依赖的理由写成条目上方的 YAML 注释即可(内置插件一直是这么做的);requires里的optional: true语义等同于直接写在recommends下。旧写法现在加载报错并提示改写。需要按能力声明调度依赖请用needs/after(见调度声明),那是另一套、确实生效的机制。
tags:
- mode:dialogue
- role:narrator
- cost:llm
relations:
provides:
- narrative-engine
requires:
- world-init
conflicts:
- narrator
recommends:
- scene-castAgent runtime 在调用 LLM 时会受到两个方向的约束:单次调用时长(callTimeoutMs / firstTokenTimeoutMs)和运行总时长(timeoutMs)。框架会自动在 transient 错误、call-timeout、first-token-timeout、tool-call 循环四种情形下重试,并在每次重试时向 prompt 追加一条短 system 提示打破 KV-cache 命中。
LLM 并发闸门:进程内所有 LLM 调用共享一个 FIFO 并发上限(COVEL_LLM_MAX_CONCURRENT,默认 4,0 关闭)——post-turn 阶段多个 agent 并行时不再裸并发打满 provider。排队等槽的时间顺延该 runtime 的 deadline(排队是框架的成本,不占 runtime 预算),流式调用在整个流消费期间持有槽位。实现见 packages/runtime/src/retry/llm-slots.ts。
Function runtime 只消费 timeoutMs:handler 受同一运行总时长硬上限约束(默认 60000ms),超时该 runtime 以 failed 收场、turn 继续。function runtime 没有重试循环,其余字段(maxRetries / callTimeoutMs / firstTokenTimeoutMs / loopDetectionThreshold / requireToolUse)对其无效。注意超时只解除 turn 阻塞,已发出的 handler 调用无法被取消。超时后框架会吊销 handler 的全部副作用能力——store、pluginData、media、images、speech、gateway、utils、recursiveCall、logger、assetProgress——脱离的 handler 再调用会同步抛出 capability ... is revoked,避免它在本次执行已经收场之后仍然写入。吊销挂在超时本身、不挂在任何锁上,因此对持锁与不持锁的执行路径一样有效。协作式 handler 应监听 ctx.signal 主动取消。
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
timeoutMs |
number |
60000 | 运行总时长硬上限。任何情况下都不会超过此值 |
maxRetries |
number |
1 |
transient 错误/超时/循环时的重试次数(不含首次尝试)。0 禁用重试。上限 5 |
callTimeoutMs |
number |
min(60000, floor(timeoutMs / (maxRetries + 1))) |
单次 LLM 调用的总时长。防止一个挂死请求吃掉整轮预算 |
firstTokenTimeoutMs |
number |
30000 |
流式 runtime 的首 token(TTFB)上限;非流式忽略 |
loopDetectionThreshold |
number |
3 |
连续重复相同 (tool name + JSON arguments) 的次数;命中则注入扰动继续。0 关闭 |
requireToolUse |
boolean |
false |
仅 agent runtime。循环在“零成功工具调用”下收场(LLM 只回散文)时,注入一条纠正 system 消息并重试一次;第二次仍零工具则放行并 console.warn(maxSteps 仍兜底)。适合唯一职责就是调某工具、却会漂移成续写正文的 runtime |
requireToolUse 判定:仅当本轮 loop 从未有任何工具成功执行、且 LLM 本次回复无 tool call 时触发;已经成功干过活再收尾的 runtime 不受影响。纠正消息按 input.locale 分支(zh 前缀 → 中文“你没有调用任何工具就结束了……”,其余含无 locale → 英文),记一条 [runtime-retry] <name> ... reason=no-tool-call。内置的 scene-prompts(每回合必须调用 generate-scene-prompts)已启用。
四类重试触发条件:
transient-error:AbortError / network / 5xx /RATE_LIMITED/PROVIDER_ERRORcall-timeout:单次调用超过callTimeoutMsfirst-token-timeout(仅流式):超过firstTokenTimeoutMs仍无任何 text/tool eventtool-loop-detected:外层 tool loop 连续命中相同调用loopDetectionThreshold次
扰动策略:重试时框架在 messages 末尾追加一条 [retry N] ... system 消息,并随 N 递增加入空格 padding,确保 prompt 字节串不同,避免 provider 端 KV-cache 复读同一回应。
与 gateway fallback 的关系:llm.toml 中 fallback = "story" 依然生效。本层的同 preset 重试先跑完后,失败才沿 gateway 的 preset fallback chain 继续尝试下一条。总时长硬上限仍是 timeoutMs。
示例 frontmatter:
timeoutMs: 120000
maxRetries: 2 # 更保守,最多 3 次尝试
callTimeoutMs: 40000 # 每次调用 40s,足够 qwen-flash 但留重试余量
firstTokenTimeoutMs: 20000 # 20s 无首 token 即判定卡死
loopDetectionThreshold: 3 # 默认即可Agent runtime 默认使用 segment-based prompt assembler。插件正文进入 Plugin Instructions 段,authorsNote 与 postHistory 作为高权重消息扩展点参与同一条 context 构建路径。
声明"导演级"指令,插入到消息历史倒数第 depth 条之前。借鉴 SillyTavern / NovelAI 的 author's note 语义 —— 用于在长历史中重新锚定模型的叙事方向。
| 字段 | 类型 | 说明 |
|---|---|---|
content |
string(必填) |
注入文本,支持 {{ template }} 插值(与 PLUGIN.md 正文相同的变量空间) |
depth |
number(可选,默认 4) |
距离消息数组尾部的偏移。4 表示插入到 messages[length - 4] 之前。0 或 <= 0 等价于追加到末尾 |
role |
'system' | 'user' | 'assistant'(可选,默认 system) |
注入消息的角色 |
多个插件的 authorsNote 会按 (stage, name) 顺序聚合,落在同一 (role, depth) 桶内的内容会被合并为一条消息(用空行分隔)。
该字段对所有 agent runtime 生效。
示例 frontmatter:
authorsNote:
content: |
Keep scenes tense and grounded.
Do not reveal {{ userSettings.spoilerName }}.
depth: 4
role: system声明最末端的高权重指令。追加在所有消息(包括 authorsNote)之后,用于提醒模型输出格式、风格约束或硬规则。
| 字段 | 类型 | 说明 |
|---|---|---|
content |
string(必填) |
注入文本,支持 {{ template }} 插值 |
role |
'system' | 'user'(可选,默认 system) |
注入消息的角色 |
多个插件的 postHistory 会按 (stage, name) 顺序聚合;相同 role 的声明会被合并为一条消息。
该字段对所有 agent runtime 生效。
示例 frontmatter:
postHistory:
content: Always respond in valid markdown. Never break character.声明插件暴露给 POST /api/sessions/:id/plugin-rpc 的结构化 action,供前端或外部代理用统一通道调用。每个 entry 是一个 RPC handler 模块的相对路径。新代码请在 entry 模块里用 covel.registerRpc(action, handler, { description?, trustLevel? }) 内联注册;路由、审批门与信任钳制语义不变。
| 字段 | 类型 | 说明 |
|---|---|---|
<action-name> |
string(必须 kebab-case,不可以 framework- 开头) |
action 名,与 pluginId 一起作为路由 key |
<action>.handler |
string(必填) |
handler 模块的插件相对路径,必须 .js / .mjs / .cjs,不允许绝对路径或 .. 段(框架在 schema 与 loader 两层校验) |
<action>.input |
string(可选) |
payload 的 JSON Schema 路径,仅作文档参考,框架不强制 |
<action>.trustLevel |
'builtin' | 'official' | 'community'(可选) |
强制声明此 action 的信任级别,只能比插件源信任更严格(降级);尝试升级会被 clamp 并 warn |
<action>.description |
string(可选) |
一句话描述,会显示在 approval 对话框里 |
已移除:
<action>.streaming不再被 schema 接受。RPC 派发始终同步,长任务走execution: background+plugin-data.changedSSE 汇报进度,从来没有流式协议读取该字段。注意 rpc 块的解析策略:任何一个无法识别的 key 会跳过整个rpc块(而非仅该 action),manifest 仍报告加载成功、只打一条指明文件/行/键名的 warning——所以残留streaming的插件会一次性丢掉全部 rpc action。
handler 模块必须 default export 一个 (payload, context) => Promise<unknown> 函数。context 包含 { sessionId, pluginId, action, store: RpcHandlerStore },其中 store 是窄结构接口(getSession / listTurnMessages / savePlayerInput / 可选 plugin-data 三件套),不暴露完整的 DataStore。
示例 frontmatter:
rpc:
regenerate:
handler: ./rpc/regenerate.js
description: 重新生成上一段叙事
cancel:
handler: ./rpc/cancel.js
trustLevel: community # 即使插件本身是 official,也强制对 cancel 走 community 审批框架默认 actions(无需声明,通过 pluginId: "framework" sentinel 调用):
| Action | 说明 |
|---|---|
submit-form |
绑定已提交 interaction,严格校验并幂等持久化表单 / 选择 / 确认,再填充模板 narrative |
详细 API 说明见 api.md POST /api/sessions/:id/plugin-rpc,作者指南见 ../guide/plugin-authoring.md §2.3.1。
声明"在 LLM 调用前要注入到 system prompt 里的上下文块"。每条 entry 是一个独立的 XML 块,按声明顺序拼接在 PLUGIN.md 正文末尾。支持两种 kind:
读取前序 runtime 的结构化 output 字段。kind: runtime 必须显式声明,避免同一 inject entry 同时存在多种解释。
| 字段 | 类型 | 说明 |
|---|---|---|
kind |
'runtime'(必填) |
runtime-output 注入来源 |
from |
string(必填) |
源 runtime name,可以是 pluginId 或 pluginId/runtimeId |
field |
string(必填) |
从源 runtime output 里取的字段名 |
as |
string(必填) |
包裹 XML 标签,如 "<narrator-output>" |
如果源 runtime 本回合没有执行、失败、或指定字段不存在,该 entry 静默跳过,不会污染其他注入块。字符串按原文注入;数组和对象使用稳定的 JSON 文本渲染,避免出现 "[object Object]"。
在 prompt 构建时调用 store.listPluginData(sessionId, pluginId, namespace) 拿到本插件自己的 plugin-data 记录(跨插件读故意不支持),按声明的 format 序列化后注入。适合"增量维护状态"类插件:codex 先看已有条目再决定增/改,character-tracker 先看已有角色再决定 create/update,等等。
| 字段 | 类型 | 说明 |
|---|---|---|
kind |
'plugin-data'(必填) |
显式 discriminator |
namespace |
string(必填) |
本插件的 plugin-data namespace |
as |
string(必填) |
XML 标签,如 "<existing-entries>" |
format |
'summary' | 'full' | 'ids-only'(可选,默认 'summary') |
序列化方式,见下 |
maxEntries |
number(可选,默认 50,范围 [1, 500]) |
Token 预算保护 |
Format 说明:
| format | 每行结构 | 适用场景 |
|---|---|---|
summary |
- {key} | {updatedAt} | {json-truncated-200} |
默认,够 LLM 判断重复/匹配 |
ids-only |
- {key} |
最省 token,只做 ID 存在性检查 |
full |
- {key}: {full-json} |
调试或小条目集 |
两段式截断:当条目数 > maxEntries 时,框架采用确定性的两段式截断——前半按 createdAt 升序取"最早的锚"(保证老条目永远可见,防止 session 后期 callback 老地点被当成重复 unlock),后半按 updatedAt 倒序取"最近活跃",两段互相去重。超出时追加一行 [总计 N 条,展示 M 条]。
空 namespace:返回 <tag>暂无</tag>,让 LLM 明确知道"空"而不是"被截断了"。
错误路径:store.listPluginData 失败会让 runtime 直接失败,错误走观测通道(runtime_outputs.error + trace),不污染下游任何 runtime 的 context(由 Phase 0 审计保证:失败 runtime 不进入 completedResults,无路径泄漏到 narrator)。
框架能力:当 manifest 声明了任意 kind: plugin-data 注入时,turn-executor 自动切换到 buildContextAsync 路径;其他 runtime 继续走同步 buildContext,零开销零回归。
示例 frontmatter:
input:
inject:
- kind: runtime
from: narrator
field: narrativeOutput
as: "<narrator-output>"
- kind: plugin-data
namespace: entries
as: "<existing-entries>"
format: summary
maxEntries: 100setup ──▶ pre-turn ──▶ narrative ──▶ post-turn ──▶ audit
(游戏初始化)(玩家操作前)(主叙事输出)(操作后处理)(审计,保留位)
会话处于 phase === "setup" 时只运行 setup stage;phase 转为主循环(playing)后,每轮依次跑 pre-turn → narrative → post-turn → audit,stage 之间是严格屏障——上一 stage 全部结束才进入下一 stage。
| Stage | 执行时机 | 说明 |
|---|---|---|
setup |
首次进入时 | 游戏初始化:世界状态、角色属性、动态表单。按 runtime 粒度跟踪——每个 runtime 首次完成后将自身 id 写入 session.setupRuntimes,后续轮次框架不会再调度它。单个 runtime 通过 maxTriggerCount 控制首次阶段内的多步流程 |
pre-turn |
每轮主循环 | 玩家操作后、叙事前的处理 |
narrative |
每轮主循环 | 主叙事模型输出,Turn 的核心产出 |
post-turn |
每轮主循环 | 叙事后处理:状态更新、图像生成、日志 |
audit |
每轮主循环 | 冲突审计(保留位) |
主循环每轮依次跑完 pre-turn → narrative → post-turn → audit 四个 stage;setup 由 phase 控制,默认单次完成后不再触发,无需 phases: [...] 自我门控。同一 stage 内部由 DAG(needs / after / inputs)排序,独立 runtime 并行执行,见 调度层级。
| 类型 | 状态 | 说明 |
|---|---|---|
auto |
✅ 生产可用 | 每个 Turn 自动触发 |
manual |
✅ 生产可用 | 仅玩家手动触发;启用插件只表示该能力可用,不会自动进入每轮调度 |
scheduled |
✅ 生产可用 | 每 N 个逻辑玩家回合触发一次(配合 interval + maxTriggerCount)。基数是逻辑回合号 completedPlayerTurns + 1(interval: 2 在第 2、4、6 个玩家回合触发);manual / follower / recursive 执行不推进 completedPlayerTurns,因此不影响 cadence,setup 阶段的交互轮次同样不占号 |
event |
✅ 生产可用 | 监听特定事件触发(在 Turn 内的事件 fan-out 中由 shouldTrigger 判定) |
trigger 枚举就是上面四种。manifest 输入 schema 对
trigger.type做闭集校验,任何其他取值在加载时被拒绝,不会进入运行时。
event runtime 唯一的触发点是回合内的事件扇出(packages/runtime/src/trigger/turn-event-chain.ts):主调度器用空 topic 列表评估它,topic 匹配必然失败。扇出的语义是「因果反应」而不是「排班的时隙」,因此它故意不套用当前的 stage 屏障——setup stage 里某个 runtime 发出的 topic,同样能唤起主循环 stage 的订阅者,反之亦然。若按 stage 过滤,发射方与订阅方分处两个 stage 时订阅者会被静默丢弃且没有任何诊断信息。
扇出仍然受这些约束:
session.setupRuntimes:已经报告完成的setupstage runtime 不会被后续同名 topic 复活——这是「一次性 setup」契约真正的守卫,也是扇出唯一继承的 stage 相关语义(API 响应仍会派生出兼容字段session.preGameCompleted)。maxDepth(默认 8):限制事件链在单回合内的递归深度。- 回合内去重:本回合已产出结果的 runtime 不会被再次执行;
execution: background的订阅者每回合最多被 defer 一次。
maxTriggerCount / cooldownTurns 这类按会话计的节流由主调度器每回合应用一次,扇出内会带上真实历史一并判定。
| 字段 | 默认 | 含义 |
|---|---|---|
interval |
1 | scheduled 类型每隔 N 轮触发一次 |
cooldownTurns |
— | 上一次触发后多少轮内不可再次触发 |
maxTriggerCount |
— | 整个 session 内最多触发次数(达到后不再触发) |
startTurn |
— | 从第几个主循环轮次起开始介入。基于 completedPlayerTurns(0-based,legacy turnCount 的派生源),与 setup 首轮自动跳过互不冲突。适合"让玩家先熟悉环境再介入"的场景 |
startTurn 用例:
trigger:
type: scheduled
interval: 1
startTurn: 3 # 前三轮让玩家适应,第四轮起开始检查这条配置表达"前三轮玩家先熟悉环境,从第四轮起插件才开始介入"。setup stage 由框架按 session.setupRuntimes 集合(经 phase 门控)决定是否再次触发,与 startTurn 解耦。
CRITICAL: 框架代码中禁止出现任何具体插件 ID 或插件名称。
Covel 的核心设计原则是插件承载游戏逻辑,框架提供原语和编排。为确保任何插件都可以被替换而不修改框架代码,以下规则必须严格遵守:
在框架代码(packages/、apps/server/src/、apps/web/src/)中:
- ❌
pluginId === 'narrator'— 不得通过插件 ID 判断行为 - ❌
store.listPluginData(sessionId, 'world-init', ...)— 不得硬编码数据来源插件 - ❌
p.id === "image"— 不得通过插件 ID 控制 UI - ❌ 在常量集合中列出插件名(如
KNOWN_KEYS.has("codex"))
- ✅ 通过
RuntimeManifest.outputKind判断输出类型(story/plugin/system) - ✅ 通过
RuntimeManifest.capabilities发现插件能力(如world-data-provider) - ✅ 通过
pluginType判断核心/普通插件 - ✅ 测试文件中可以使用具体插件名作为测试数据
当框架需要区分插件行为时,应在 RuntimeManifest 中添加通用字段(如 outputKind、capabilities),而非在框架代码中添加条件分支。
工具白名单较大(>~10 个)的 runtime 可以声明延迟加载,避免每次 LLM 调用都全量预载所有工具 schema:
tools:
plugin: [tool-a, tool-b, tool-c, ...] # entry 注册的工具名
defer: true # true = 延迟整个白名单;或 [tool-a, tool-b] 精确列出被延迟的工具照常注册、照常鉴权,只是不进初始 LLM 工具清单;框架自动注入 search-tools(BM25 检索,中英文均可),LLM 检索命中的工具自下一步起可直接调用,激活状态持续到本 turn 结束。defer 数组中不在白名单内的名字会被忽略——延迟声明永远不能授予未声明的工具。详见 docs/reference/tools.md。
setup stage runtime 可在 RuntimeOutput 中声明:
{ "preGameDone": true }框架在 commit 链上看到该字段为 true 时,会将该 runtimeId 追加到 session.setupRuntimes(API 响应仍会派生出兼容字段 session.preGameCompleted,响应形状不变);后续轮次的调度器会跳过已完成的 setup stage runtime。这是 runtime 粒度的闸门;顶层 session.phase(setup / playing)只是粗粒度的 stage-band 选择器,不下放到单插件的触发条件里,两者互不冲突。