| name | write-fullfront-card |
| description | 设计完全前端化的 SillyTavern 角色卡。当用户要求设计完全前端卡、全前端卡、前端游戏卡、自定义界面卡、HTML 卡时使用。 |
完全前端卡设计 Skill
设计完全前端化的 SillyTavern 角色卡——放弃 ST 原生对话交互,用自定义 HTML/CSS/JS 构建完整的游戏/交互界面。
触发条件
用户要求设计/编写:完全前端卡、全前端卡、前端游戏卡、自定义界面卡、HTML 卡、独立前端卡。
开发工具链
完全前端卡使用 Vite 项目化开发 + st-card-toolkit 共享工具包:
- JS 逻辑:模块化开发,AI 调用/配置/历史/解析/DOM/设置面板全部通过
st-card-toolkit 导入,卡只写 schema + prompt + 游戏逻辑
- 构建:
npm run build → Vite 内联所有资源 → build.cjs 提取片段 + 合并 lorebook → 输出 card.json
- 世界书:lorebook MCP (
lorebook-editor) 保证条目格式正确
AI 调用细节见 docs/toolkit-api.md。叙事类调用优先用 generate() 适配 ST 预设(见「AI 调用模式」章节)。只有 toolkit 不覆盖的场景(流式事件、世界书操作、ordered_prompts / overrides 编排)才需要查阅 docs/tavernhelper-api.md 或 docs/prompt-orchestration.md。
架构原理
完全前端卡利用 SillyTavern 的正则脚本机制,将用户输入的触发词替换为完整的 HTML 应用。核心结构:
first_mes → 开场白/引导界面(HTML 页面)
alternate_greetings[0] → 触发关键词(如 "start_game")
regex_scripts → findRegex 匹配触发词 → replaceString 注入完整 HTML/JS 应用
character_book → 世界规则、AI 行为指令、变量操作规范
description → 可留空或简短说明(前端卡主要靠世界书驱动)
数据流
用户输入触发词 → 正则匹配 → 替换为 HTML 应用 → 应用内用 JS 调用 AI API
→ 应用内用 IndexedDB (Dexie) 存储状态
→ AI 回复带结构化数据(通常经 json_schema 约束)
→ 前端解析数据 → 更新数据库 → 刷新 UI
卡作者责任边界
完全前端卡完全脱离了 SillyTavern 的原生对话/存档机制。ST 在这种卡里几乎只负责两件事:(1)渲染 ```html 代码块为 iframe;(2)通过 TavernHelper 转发 AI 请求(以及在 generate() 模式下应用用户预设)。除此之外的一切——对话状态、提示词组装、上下文压缩、存档读档、设置持久化——都必须由卡自己实现。
设计一张完全前端卡前,先确认下面四个系统都已经规划好:
1. 提示词组装系统
ST 的 Prompt Manager 在 generate() 模式下只负责注入用户预设和 lorebook 的静态部分,每一轮的动态 prompt 拼装完全在卡手中。卡需要决定:
- 每次
callAI() 前要拼哪些层(系统规则 / 长期记忆 / 近期回顾 / 当前状态 / 本轮输入)
- 哪些内容走 lorebook(让 ST 预设按 Prompt Manager 顺序注入)、哪些走代码(每轮重新拼)
- 多通道分别用什么 prompt 模板,避免叙事/逻辑/压缩通道的指令互相污染
generateRaw 通道的 ordered_prompts 顺序、role 分布
generate 通道的 overrides 槽位用法(chat_history / world_info / char_description 等)
src/prompt.js 是卡的提示词中枢,不要把 prompt 拼装散落在调用点。
2. 上下文压缩系统
ST 自己的「聊天历史 → token」机制对完全前端卡不生效——卡用 max_chat_history: 0 把它清空了,所有「记得多少」都由卡自己决定。卡需要规划:
- history:原始多轮记录,谁来 push、何时 reset(参考本 skill「上下文管理系统设计」章节的小总结/大总结两级压缩)
- 小总结:通常作为结构化输出的字段随回复附带,零额外请求
- 大总结:何时触发独立压缩调用(固定轮次 / token 估算 / 阶段切换 / 结案)
- 压缩触发的副作用:压缩成功才能 reset history、压缩失败要回滚,避免对话流断裂
每个使用 callAI() 的通道都要单独考虑压缩策略,不能只管主叙事。
3. 存档与读档系统
完全前端卡的所有运行时状态都在浏览器内存和 IndexedDB 里,ST 的对话保存机制覆盖不到这些数据。用户刷新页面、切换角色、重开聊天时,卡必须能从持久化存储里把整个游戏世界重建出来。卡需要规划:
- 持久化范围:游戏实体(角色/物品/任务/...)、对话 history、recap 队列、大总结、玩家设置、UI 状态(当前打开的面板/页签)
- 写入时机:每次状态变更后写、定时批量写、事件驱动写——选一种并保持一致
- 读档流程:iframe 加载 → 检测 IndexedDB 是否有存档 → 有则 rehydrate(重建 history / recaps / state)→ 无则走新游戏初始化
- 多存档槽位(如有):存档列表 UI、命名、删除、导出/导入
- 版本迁移:schema 升级后老存档怎么兼容(字段补默认值、跑迁移函数)
- 崩溃恢复:写入失败、Dexie 异常、存档损坏的兜底
判断标准:关闭浏览器后重新进入这张卡,能不能 1:1 恢复到之前的状态和 AI 上下文。如果做不到,存档系统就没设计完。
4. 设置持久化系统
API 配置、用户偏好、各通道开关等都属于设置层。createSettingsController() 负责面板渲染和 localStorage 持久化,但面板上有哪些项、设置项之间的依赖、默认值、迁移是卡作者的事。常见漏项:
- 用户切换通道模式(generate ↔ generateRaw)后是否要重置该通道 history
- 切换 API 端点后旧 history 是否还兼容
- 设置变更是否需要触发 UI 刷新
简而言之:ST 给你一个 iframe 和一根 AI 调用通道,剩下的整个「游戏运行时」都得自己造。本 skill 后续章节会分别给出 history、压缩、多通道等子系统的设计模式,但这些模式只是可选实现,责任本身始终在卡作者手里。
JSON 结构规范
{
"name": "卡名",
"description": "",
"personality": "",
"scenario": "",
"first_mes": "<完整 HTML 引导页面>",
"mes_example": "",
"creator_notes": "使用说明:推荐模型、触发方式等",
"system_prompt": "",
"post_history_instructions": "",
"alternate_greetings": ["触发关键词"],
"tags": ["前端卡", "游戏"],
"creator": "作者名",
"character_version": "1.0.0",
"character_book": {
"name": "卡名_worldbook",
"entries": [
]
},
"extensions": {
"regex_scripts": [
{
"scriptName": "卡名 (作者)",
"findRegex": "触发关键词",
"replaceString": "提示文字\n```html\n<!DOCTYPE html>...\n```",
"trimStrings": [],
"placement": [2],
"disabled": false,
"markdownOnly": true,
"promptOnly": false,
"runOnEdit": true,
"substituteRegex": true,
"minDepth": null,
"maxDepth": null
}
]
}
}
正则脚本关键参数
| 参数 | 完全前端卡设置 | 说明 |
|---|
findRegex | 触发关键词字符串 | 用户输入此词时触发界面注入 |
replaceString | 完整 HTML 应用代码 | 必须包裹在 ```html 代码块中(见踩坑清单) |
placement | [2] | 匹配用户输入 |
markdownOnly | true | 仅在显示时替换,不影响发送给 AI 的内容 |
promptOnly | false | 显示侧需要渲染 |
前端应用框架
渲染机制
完全前端卡依赖 SillyTavern 的代码块渲染器(需用户在 ST 设置中开启)。渲染器会将 ```html 代码块放进 iframe 执行,从而绕过 DOMPurify 的 <script> 剥离。
重要前置条件: 用户必须在 ST 设置中开启「启用渲染器 - 启用后,符合条件的代码块将被渲染」。
项目结构(Vite 项目化)
cards/my-card/
├── package.json # dependencies: st-card-toolkit
├── vite.config.js # vite-plugin-singlefile
├── index.html # Vite 入口 HTML(模板)
├── build.cjs # 后处理:Vite 产物 → card.json
├── card.meta.json # 卡元数据
├── src/
│ ├── main.js # 入口 + 将交互函数暴露到 window
│ ├── style.css # CSS
│ ├── schema.js # json_schema 定义
│ ├── prompt.js # 每轮提示词构建
│ ├── state.js # 卡状态管理(config / history / 运行时状态)
│ ├── settings.js # 设置面板(通常基于 createSettingsController)
│ ├── ... # 卡特有模块(游戏逻辑、UI、存档、数据库等)
│ └── first_mes.html # 首条消息
├── lorebook/ # 世界书条目(V2 Spec JSON)
└── dist/card.json # 构建产物
这只是一个典型布局,不是规范。文件拆分粒度视卡的复杂度调整——可以把 state/config/history 合并到一个文件,也可以把游戏逻辑拆成多个模块(动作处理、存档系统、数据库访问等);toolkit 的 callAI 可以直接用,也可以做一层卡内封装(如果需要定制 history 管理或响应解析)。
渲染格式要求
构建产物是 HTML 片段(不含 <!DOCTYPE>/<html>/<head> 外壳),用 <body> 标签包裹以通过 isFrontend() 检测。JS-Slash-Runner 的 createSrcContent() 会自动包一层完整 HTML 文档并注入 FontAwesome、jQuery、Vue 等。
卡不需要自己引入 FontAwesome / jQuery / Vue(框架已注入)。只需引入框架未提供的依赖(如 Dexie)。
资源约束
- 所有 CSS/JS 由 Vite 内联到单文件
- 外部库只能用 CDN(unpkg、cdnjs 等)
- 图片/图标使用 CDN 或 base64 内联
ST 渲染踩坑清单(必读)
以下是在 SillyTavern 中实际部署完全前端卡时的已知坑点:
1. DOMPurify 会剥离直接注入的 HTML 中的 script 和事件属性
问题: 如果 replaceString 不使用 ```html 代码块包裹,而是直接输出 HTML 片段,SillyTavern 的 DOMPurify 净化器会:
- 剥离所有
<script> 标签
- 剥离所有
onclick、onchange 等事件处理器属性
- 剥离
<iframe srcdoc> 属性
表现: HTML/CSS 正常渲染(布局、样式、图标都在),但所有交互完全失效——点击按钮无任何反应,浏览器控制台也无报错(因为事件处理器根本不存在)。
规则: 完全前端卡必须使用 ```html 代码块包裹,依赖 ST 的代码块渲染器(在 iframe 中执行脚本,绕过 DOMPurify)。
调试方法: F12 → Elements 面板,搜索 <script> 或 onclick。如果搜不到,说明被净化器剥离了。
2. first_mes 同理
问题: first_mes 中的 HTML 同样经过 DOMPurify。
规则: 如果 first_mes 需要 JS 交互,必须用 ```html 代码块包裹完整 HTML 文档。纯静态展示页(无 JS)可以直接用 HTML 片段:
// 静态展示(无 JS)—— 直接 HTML 片段即可:
<style>.intro { ... }</style>
<div class="intro">...</div>
// 需要 JS 交互 —— 必须代码块包裹:
` ` `html
<!DOCTYPE html>
<html>...<script>...</script>...</html>
` ` `
3. first_mes 只放触发占位符,所有内容由 JS 渲染
原则: 完全前端卡的 first_mes 不应包含任何实际文字内容。它的唯一职责是提供一个正则匹配的上下文,让 regex_scripts 将其替换为前端应用。所有展示内容(封面、介绍文字、按钮等)都在 replaceString 的 HTML/JS 中实现。
原因: 正则脚本会扫描所有渲染的消息内容。如果 first_mes 中包含任何文字(包括说明文字中提到的触发词),可能被正则意外匹配,导致封面页同时渲染出游戏界面。
推荐做法: first_mes 直接填入触发关键词本身,让正则将其完整替换为封面页 HTML:
{
"first_mes": "start_game",
"alternate_greetings": ["start_game"],
"regex_scripts": [{
"findRegex": "start_game",
"replaceString": "```html\n<!DOCTYPE html>...\n```"
}]
}
如果封面页和游戏主界面不同,可以用两个正则分别匹配不同的触发词:
{
"first_mes": "show_intro",
"alternate_greetings": ["start_game"],
"regex_scripts": [
{ "findRegex": "show_intro", "replaceString": "```html\n...封面HTML...\n```" },
{ "findRegex": "start_game", "replaceString": "```html\n...游戏HTML...\n```" }
]
}
4. 需要用户开启代码块渲染器
前置条件: 用户必须在 ST 设置中开启「启用渲染器」(启用后,符合条件的代码块将被渲染)。未开启时,```html 代码块只会显示为语法高亮的源码,不会执行。
建议在 creator_notes 中注明此要求。
持久化层设计
完全前端卡的所有运行时数据都要自己存——ST 不会帮你保存游戏内的角色、物品、任务、存档快照。主流选择是 IndexedDB(通常通过 Dexie.js 封装):容量大、原生异步、iframe 环境稳定、有 schema 版本迁移机制。轻量的启动配置(API key、主题偏好等)可以直接放 localStorage;其余长期数据走 IndexedDB。
设计要点
- 表结构由卡的需求决定:字段命名风格(纯数字索引 / 命名字段 / auto-increment ID)、主键方式都是作者的取舍,skill 不规定具体形式。围绕「游戏实体 + 存档 dump + AI 读写」三个用途来设计
- 覆盖所有长期数据:游戏实体(NPC/物品/任务/...)、对话 history、小总结/大总结、玩家设置、UI 状态,凡是刷新页面不能丢的都得进 DB
- Schema 迁移是存档兼容的前置条件:加字段或改结构时用
db.version(N).stores({...}).upgrade(tx => ...) 写迁移函数,老存档才能正确升级;没有迁移策略就不要发版本更新
- AI 读写的字段必须在 lorebook 里告诉 AI:字段名、含义、取值范围、枚举值都要讲清楚,AI 才能写出前端可解析的数据
Dexie 的表定义语法、索引、版本迁移等 API 参考 dexie.org。
结构化数据输出
完全前端卡里,AI 的一次回复通常要带两层信息:叙事文本(给玩家看)+ 结构化数据(给前端解析、更新 UI/DB)。让 AI 稳定返回可解析的数据是这一层的核心需求。
主推:json_schema + parseNarrativeAndData
通过 callAI() 的 json_schema 参数约束 AI 输出格式。把叙事正文也放进 schema 的一个字段(通常叫 narrative),一轮调用就完成「叙事 + 结构化数据」混合输出。
const SCENE_SCHEMA = {
name: 'scene_response',
value: {
type: 'object',
properties: {
narrative: { type: 'string', description: '本轮叙事正文' },
},
required: ['narrative' ],
additionalProperties: false,
},
strict: true,
};
const raw = await callAI(prompt, { config, history, json_schema: SCENE_SCHEMA });
const { text, data } = parseNarrativeAndData(raw, 'narrative');
renderNarrative(text);
updateState(data);
这种做法的三个好处:
- 稳定性由 API 层保证:格式错乱、字段缺失、JSON 嵌套挂掉的情况显著减少——模型级约束比 prompt 里讲格式可靠得多
- 工具链原生支持:
parseNarrativeAndData() 由 st-card-toolkit 提供,一行拆出 text 和 data
- CRUD 决策权留在前端:AI 只返回数据快照,什么时候 insert/update/delete 由游戏事件触发(回合结束、任务完成、结案归档等),不交给 AI 动态决定——减少误操作和变量保护负担
schema 设计和 json_schema 完整语法参考 docs/structured-output.md。
历史方案:y_xxx 指令流
json_schema 普及之前,早期完全前端卡让 AI 在叙事文本里嵌入自定义数据操作指令(y_insert({...}) / y_update("ID", {...}) / y_delete("ID") / y_add_json(...)),前端用正则扫描回复、解析指令、执行数据库操作。
这种做法的痛点:
- 稳定性差:AI 容易漏括号、JSON 嵌套挂掉、指令残缺——需要额外写"指令自动修复"逻辑去修补
- 要教 AI 指令格式:lorebook 里得详细列指令语法、使用规则、变量保护约束,占可观 token
- CRUD 决策权交给 AI:AI 决定什么时候插入/更新/删除哪条记录,需要给足约束和白名单,否则容易乱删关键数据
什么时候还用得到:
- 使用的 API 不支持
json_schema(某些自定义端点、老 API)
- 沙盒 / 开放世界场景:AI 一轮回复里要动态新增多种不同类型的实体,schema 列不完
- 和 json_schema 搭配——主数据快照走 schema,偶发的动态操作走指令流
完整的 parseCommands / executeCommands / sanitize / 自动修复参考实现在 docs/data-ops.md。
Function calling(tools 参数)
如果 AI 需要显式调用外部工具或触发副作用(例如"搜索网页"、"发送通知"、"执行脚本"),用 callAI() 的 tools / tool_choice 参数走 function calling 协议。和 json_schema 的区别是:json_schema 约束的是 AI 的输出格式,function calling 是"AI 自主决定要不要调用某个具体函数"。详见 docs/structured-output.md。
默认选择
默认用 json_schema——稳定、简单、工具链原生。只有明确需要 "AI 动态决定 CRUD" 或 "API 不支持 json_schema" 时,才考虑退回到 y_xxx 指令流。
AI 调用模式:generate 还是 generateRaw
完全前端卡有两种 AI 调用方式,选择取决于是否需要 ST 预设参与提示词编排。
ST 预设做什么
SillyTavern 的预设(Prompt Manager)为 AI 编排一套完整的提示词上下文:系统提示(Main Prompt)→ 世界书 → 角色描述 → 聊天历史 → 最终指令(Post-History Instructions)。用户通过预设来丰富 AI 的故事表现、限定文风、设置破限指令。这些都是用户侧的调教,卡作者不需要也不应该替用户做这些事。
选择依据
用 generate() | 用 generateRaw() |
|---|
| 这次调用需要用户的预设 / 文风 / 破限介入 | 这次调用不希望用户预设参与编排 |
| 输出是面向玩家的叙事 / 故事 / 角色扮演 | 输出是卡内自控的数据、子对话、翻译、事件生成等 |
| 卡的静态规则放 lorebook 里,让 ST 预设注入 | 卡完全自控上下文,不依赖 ST 的任何内容 |
| 温度、模型等参数由用户预设控制 | 温度、模型等参数由代码精确控制 |
同一张卡可以混用两种模式。典型做法:主叙事通道用 generate()(用户预设生效),辅助通道(摘要生成、数据提取)用 generateRaw()(不需要预设干扰)。
generate() 模式下的分工
ST 预设 + lorebook → 静态系统规则(世界观、格式规范、认知隔离、文风、破限)
代码(prompt.js) → 每次调用的动态内容(随机种子、状态变量、玩家行动)
overrides → 自管的多轮对话历史(注入到预设的 chat_history 槽位)
lorebook 条目设为 constant: true,ST 预设会自动按 Prompt Manager 顺序注入。代码中的 prompt 不重复写 lorebook 已有的规则——否则 AI 会看到两份相同的指令。
多轮对话通过 overrides.chat_history.prompts 注入,配合 max_chat_history: 0 清空 ST 聊天记录,实现正规的 user/assistant 交替格式。API 细节见 docs/prompt-orchestration.md。
上下文管理系统设计
完全前端卡的 AI 调用由前端 JS 控制,对话历史(history)的管理完全在卡手中。核心问题:随着交互次数增长,原始对话记录会撑爆 token 上限。解决思路是小总结 + 大总结的两级压缩。
核心概念
| 概念 | 存储位置 | 内容 | 生命周期 |
|---|
| history | 内存(createHistory()) | 原始对话记录(user/assistant 逐条) | 每次 callAI() 自动追加,压缩后清空 |
| 小总结 | AI 每次回复的结构化字段 | 本轮发生了什么(一两句话) | 随回复返回,存入 state 或 DB |
| 大总结 | IndexedDB | 多轮小总结的压缩合并 | 定期生成,长期保留 |
| 上下文块 | prompt 拼接时构建 | 大总结 + 近期小总结 + 当前状态 | 每次 callAI() 前临时组装 |
小总结(随回复附带)
在结构化输出的 schema 中加一个 recap 字段,要求 AI 每次回复时顺带生成一句话总结本轮要点。零额外请求——AI 生成叙事的同时就完成了压缩。
前端收到回复后把 recap 存入数组,下次调用时将近期 recap 列表拼入 prompt 作为「近期回顾」。
适用: 所有卡。即使不做大总结,光靠小总结队列就能覆盖大多数短中期交互。
大总结(异步压缩)
每 N 轮(或 history 达到 token 阈值时),发起一次独立的 AI 调用,将累积的小总结压缩为一段精炼的摘要。压缩完成后清空 history 和小总结队列。
关键点:
- 用独立的临时 history(
createHistory()),不污染主对话流
- 如果已有旧的大总结,把它也喂给压缩请求,让新摘要覆盖旧摘要(滚动压缩)
- 压缩后
history.reset() 释放主对话的 token 占用
- 可以不 await,后台异步执行不阻塞 UI
触发时机根据卡的交互模式选择:
| 触发方式 | 适用场景 |
|---|
| 固定轮次(每 N 轮) | 节奏稳定的卡 |
| token 估算 | 对话长度波动大的卡 |
| 阶段切换 | 有章节/关卡的卡,切换时压缩上一阶段 |
| 结案/存档点 | 有明确结束事件的卡 |
上下文组装
每次调用 AI 时,按层级拼装 prompt:
[系统规则] ← 世界观、行为规则、输出格式等
[故事至今] ← 大总结(长期记忆,可选)
[近期回顾] ← 小总结队列,最近 N 条(近期记忆)
[当前状态] ← DB 数据快照(角色属性、物品、任务等)
[当前场景/玩家动作] ← 本轮输入
信息密度从上到下递增:大总结高度压缩,小总结保留细节,当前状态是实时快照。
### 异步压缩时机
大总结的触发方式可以根据卡的交互模式选择:
| 触发方式 | 适用场景 | 实现 |
|---|---|---|
| **固定轮次** | 节奏稳定的卡(每 N 轮压缩) | `if (state.recaps.length >= N)` |
| **token 估算** | 对话长度波动大的卡 | 估算 history token 数,超阈值时压缩 |
| **阶段切换** | 有明确章节/关卡的卡 | 切换阶段时压缩上一阶段 |
| **结案/存档点** | 有明确结束事件的卡 | 事件结束时压缩并归档到 DB |
异步压缩可以在不阻塞 UI 的情况下执行——用 `Promise` 发起压缩请求,压缩期间玩家仍可阅读叙事:
```javascript
// 不阻塞:叙事渲染后台压缩
addMessage('narrator', narrative);
maybeCompress(); // 不 await,后台执行
大小控制策略总结
原始对话 history(临时,callAI 自动管理)
↓ 每轮回复附带 recap 字段(小总结,零额外请求)
↓ 累积 N 轮后,独立 AI 调用压缩(大总结,一次额外请求)
↓ 压缩后清空 history + recaps,大总结存入 state/DB
↓ 下次调用:大总结 + 近期 recaps + 当前状态 → prompt
关键原则:
- 小总结免费:利用 json_schema 让 AI 顺手生成,不增加请求数
- 大总结异步:独立请求 + 临时 history,不阻塞主交互流,不污染对话
- 清空即释放:压缩后
history.reset() 释放 token,大总结接管长期记忆
- DB 是终极存储:大总结和关键状态存 IndexedDB,刷新页面不丢失
- 上下文分层:大总结(远)→ 小总结队列(近)→ 当前状态(即时),AI 看到的信息密度递增
多通道 AI 架构设计
完全前端卡的核心优势:前端 JS 可以同时发起多个独立 AI 调用,每个通道按用途选择 generate 还是 generateRaw,并使用独立的 history。
每个通道创建独立的 createConfig() + createHistory():
const narrativeConfig = createConfig('mycard_narrative');
const narrativeHistory = createHistory();
const logicConfig = createConfig('mycard_logic', { temperature: });
const logicHistory = createHistory();
const [narrativeResult, logicResult] = await Promise.all([
callAI(narrativePrompt, {
config: narrativeConfig, history: narrativeHistory,
mode: 'generate',
}),
callAI(logicPrompt, {
config: logicConfig, history: logicHistory,
mode: 'generateRaw', json_schema: LOGIC_SCHEMA,
}),
]);
通道分工设计
| 通道类型 | 模式 | 温度倾向 | 典型用途 |
|---|
| 主叙事 | generate | 由用户预设决定 | 故事推进、角色扮演 |
| 数据/逻辑 | generateRaw | 偏低(代码控) | 数据操作指令、分类、提取、判定 |
| 压缩/总结 | generateRaw | 中(代码控) | 记忆压缩、大总结 |
| 其他自控通道 | generateRaw | 视用途而定 | 任何不希望 ST 预设介入的场景 |
generateRaw 的适用范围:不只是「数据/逻辑」。只要你不希望用户的预设、文风、破限、聊天历史等参与编排——例如卡内的固定 NPC 对话、自定义的子角色推理、固定模板的翻译/润色、独立的世界事件生成、对外部素材的纯加工等——都应该用 generateRaw 自己拼 ordered_prompts。判断标准只有一个:这一次调用是否需要用户预设介入。需要 → generate;不需要 → generateRaw。
在 parent + generate 模式下,createConfig() 的 temperature 不会被读取——温度、模型、break-jail 等参数全部由用户的 ST 预设接管。卡只负责提供 prompt 和(可选的)overrides。
并发约束
generate() 是单实例并发:同一时刻只能跑一个 generate() 调用,第二个会排队等待。
generateRaw() 可任意并行。
- 安全的并发组合:1 个
generate + N 个 generateRaw 同时跑(上面的示例就是这种)。
- 不要把多个叙事通道都用
generate 然后 Promise.all——它们会被强制串行,得不到并行收益。如果叙事侧也需要并发,把次要的通道改成 generateRaw + 自己拼 prompt。
设置面板
用 createSettingsController() 为每个通道绑定独立设置面板。叙事通道在 generate 模式下,设置面板里的温度/采样参数仅对自定义端点(非 parent)生效,可在面板上加注释提醒用户。
世界书条目设计
完全前端卡的世界书主要用来注入静态的 AI 行为规则——那些每轮都要见效、但不随游戏状态变化的内容。动态内容(当前状态快照、玩家行动、近期记忆)由代码每轮重新拼接,见「AI 调用模式 → generate() 模式下的分工」。
常驻条目(constant=true)
每轮都自动注入的条目。典型内容:
- 核心世界观 / 游戏机制:地理、历史、种族、战斗规则、经济系统等
- 叙事文风与输出格式:描写视角、细节程度、禁止事项;如果用 json_schema 约束结构,这里只补充 schema 之外的文风要求
- 认知隔离:防止 AI 使用训练数据里的元知识(例如角色不知道自己在游戏里)
- 思维链 / 自检规则:强制 AI 在输出前走一遍内部思考或合理性审查
- 数据库字段说明:列出 AI 要读写的表结构——字段名、含义、取值范围、枚举值——让 AI 能正确生成结构化数据
选择性条目(constant=false + keys)
按关键词触发的条目——只有 AI 或玩家提到特定词时才注入。适合:
- 地区 / NPC / 种族 / 物品的详细设定(按需展开)
- 特殊事件的背景信息
- 分支故事线
用关键词触发避免把所有细节塞进常驻条目占用 token 预算。
条目内容组织
- 用结构化标签(XML / JSON / 自定义分段)帮助 AI 解析内容边界——选一种风格在整张卡里保持一致
- 数据规模大时拆分:一个大常驻条目不如多个选择性条目按需触发
- 条目命名:用对你和 AI 都清晰的名字即可。emoji / 前缀 / 纯文字都可以,skill 不规定风格
条目的字段格式(V2 Spec)见 write-lorebook-entry skill。
开场白(first_mes)设计
first_mes 只放触发占位符,不放任何实际文字内容。封面/引导界面的所有展示内容(世界观介绍、角色创建表单、动画效果等)都在 replaceString 的 HTML/JS 中实现,由正则脚本将 first_mes 替换为封面页。
如果封面页和游戏主界面是同一个应用(由 JS 内部切换状态),只需一个正则和一个触发词。如果是独立的两个页面,用两个正则分别匹配(见踩坑清单第 4 条)。
UI 设计原则
硬性约束(不遵守就会坏)
- 根容器固定像素高度(如
height: 800px)——100vh 在部分 ST 环境下会导致 iframe 无限扩展(消息区域不断撑高、输入栏被推出视口)。需要全屏效果用 Fullscreen API 按钮
- 所有资源内联或 CDN:iframe 里不能引用本地文件
- 加载 / 错误状态:AI 调用期间显示 loading,失败时给明确提示——否则用户会以为卡崩了
建议(按需选用,不是硬规定)
- 自适应布局:flexbox/grid 适配宽度,高度在固定根容器内自适应
- 暗色主题:和 ST 默认主题协调,减少视觉违和
- CSS 前缀:iframe 天然隔离样式,加前缀(
.myapp .btn)影响有限但可读性更好
- 可拖动 / 折叠面板:复杂 UI 有多个信息区域时值得考虑,简单卡不必
工作流程
- 需求确认:游戏类型、核心机制、需要的数据表结构
- 创建卡项目:基于 Vite 模板,
npm install st-card-toolkit
- 定义 Schema:
src/schema.js — AI 输出的 JSON Schema
- 编写 Prompt:
src/prompt.js — 提示词构建逻辑
- 编写游戏逻辑:
src/actions.js — 用 callAI() + parseNarrativeAndData() 驱动
- 编写世界书:通过
lorebook-editor MCP 创建条目(格式自动正确)
- 开发 UI:
index.html + src/style.css,设置面板用 createSettingsController()
- 构建:
npm run build → dist/card.json
输出要求
npm run build 产出完整 card.json,可直接导入 SillyTavern
- 根容器使用固定像素高度(如
800px),避免 100vh 导致 iframe 无限扩展
- 用户需在 ST 中开启「代码块渲染器」才能使用完全前端卡