# 说明书 · 自建前端 给自己写聊天 app 的人。你能改前后端源码,就能把档案嵌进自己的页面:AI 用道具时聊天里出一张档案小卡,侧边栏、菜单里放一个入口打开整本图鉴,用户自己也能在档案里点 Use。 用别人做好的 app(RikkaHub、Kelivo、Cherry Studio……)的看 [apps.md](apps.md)。 --- ## 一共三块 | 文件 | 是什么 | 跑在哪 | |---|---|---| | `engine/play.js` | 引擎:机关、存量、问号、档案数据,状态存在 `PLAY_STATE_DIR/play.json` | 你的服务器(Node 18+,零依赖) | | `engine/mcp-play.js` | 给 AI 的六个工具(play_use / play_reveal / play_train / play_report / play_release / play_status) | 同上 | | `web/vault.js` + `web/vault.css` | 档案界面:图鉴、档案页、聊天小卡 | 你的前端(纯 ES5,不依赖框架) | `engine/play.md` 是给 AI 的说明书,放进人设或者系统提示词。 三块共用一个状态目录。AI 写进去的,档案页马上看得到。 --- ## 一、服务器 在你的 Node HTTP 服务里挂上档案的接口: ```js process.env.PLAY_STATE_DIR = "/路径/state"; // 先设,再 import const play = await import("./engine/play.js"); // 请求处理里,鉴权之后: if (await play.route(req, url, readBody, res)) return; // req / res 是 node:http 的;url 是 new URL(req.url, "http://x"); // readBody(req) 返回一个 Promise,解出 JSON 对象 ``` `route` 只认这四条,别的路径回 `false` 让你接着处理: | 方法 | 路径 | 做什么 | |---|---|---| | GET | `/play` | 整本档案(目录、每样的状态、标记、报告……),档案界面就靠它 | | POST | `/play/use` | 用户在档案里亲手用一样:`{item, on, branch, data, told}`。机关拒了回 `{ok:false, error}`(200) | | POST | `/play/prefs` | 开关:`{off: ["hook"]}` 关掉几样;`{furry: "pets"}` Furry 只留猫狗 | | POST | `/play/seen` | 用户看过了(入口上的红点消掉) | 不是 Node 的后端:单独跑一份 `serve.js`(它就是上面这几行加一个静态目录),你的前端 `base` 指过去。 --- ## 二、让 AI 调到工具 看你的后端怎么跟模型说话,三选一。 ### 1. 后端本来就是 MCP 客户端 比如用 Claude Agent SDK、`claude -p --mcp-config`、LangChain 的 MCP 适配器,或者任何 MCP 客户端库: - 同机:stdio,命令 `node engine/mcp-play.js`,环境变量 `PLAY_STATE_DIR` - 隔着网络:开着 `serve.js`,连 `http://地址:8800/mcp`(Streamable HTTP,`Authorization: Bearer `) 工具名会带前缀(`mcp__play__play_use` 这种),原样交给前端就行。 ### 2. 自己调 API、自己跑工具循环 直接调 OpenAI 兼容接口或者 Anthropic 接口的,不用起 MCP,把工具当函数用: ```js const mcp = await import("./engine/mcp-play.js"); // 被 import 时不会去读 stdin // 拿工具定义 const { result: { tools } } = await mcp.handle({ jsonrpc: "2.0", id: 1, method: "tools/list" }); // OpenAI 兼容格式 const openaiTools = tools.map((t) => ({ type: "function", function: { name: t.name, description: t.description, parameters: t.inputSchema } })); // Anthropic 格式 const anthropicTools = tools.map((t) => ({ name: t.name, description: t.description, input_schema: t.inputSchema })); // 模型要调的时候 async function runTool(name, args) { const r = await mcp.handle({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name, arguments: args } }); return { text: r.result.content[0].text, isError: r.result.isError }; } ``` 把 `text` 当工具结果还给模型。`isError` 为真的是「空了」「没电」「要歇」这类机关拒绝,照样还给模型,让它照着演。 ### 3. 不让 AI 自己调 也可以完全由你的代码决定什么时候用:直接调 `play.use(id, on, data, branch)`、`play.reveal(...)`,返回 `{ok, text}` 或 `{error}`。 --- ## 三、用户亲手用了,怎么告诉 AI 用户在档案里点 Use,引擎会生成一句给 AI 的话(「〔道具:她亲手用了……〕」)。两种送法: - **马上送**:前端配了 `sendToAI`、而且 AI 没在说话(`busy()` 回 false),档案替用户发一条 - **攒着**:AI 正在说话,或者没配 `sendToAI`,这句存在服务器上。你的后端拼用户下一条消息时: ```js const notes = play.drainNotes(); // 没有就是空字符串;取一次就清掉 if (notes) userText += "\n" + notes; ``` `drainNotes` 也会带上「哪道标记快消了」这种提醒,建议每条用户消息都调一次。 --- ## 四、前端 ```html ``` `VAULT_CONFIG` 全部选项: | 键 | 必填 | 说明 | |---|---|---| | `base` | 是 | 挂着 `/play` 的服务器地址。同源就写 `""` | | `token` | | 有口令就填,请求会带 `Authorization: Bearer` | | `chat` | | 聊天列表的容器。用户亲手用了,她那张小卡摆在这里(靠右)。不填就不摆 | | `sendToAI(line)` | | 替用户发一条消息给 AI。不填就攒在服务器上(见上一节) | | `busy()` | | AI 正在输出时回 `true`,这时不打断、攒着 | | `tap(kind)` | | 震动反馈:`light` / `medium` / `heavy`,壳里接原生震动 | | `onRecord(rec)` | | 用户亲手用了一样,交出一条记录 `{who: "me", at, play: [...]}`,存进你的聊天记录,重开时用 `Vault.mine` 摆回去 | | `iconsUrl` | | `icons.json` 的地址,默认同目录 | `window.Vault`: | 调用 | 做什么 | |---|---| | `Vault.open()` / `Vault.open("wax")` | 打开图鉴;给了道具 id 就直接翻到那一页 | | `Vault.card(容器, 工具名, 工具参数, 结果文字)` | AI 的工具调用回来、没报错时调:在容器里摆一张档案小卡。工具名带不带 `mcp__play__` 前缀都行 | | `Vault.mine(容器, rec.play)` | 把用户亲手用的那张卡摆回去(重开页面时) | | `Vault.widget()` | 一颗现成的入口卡(带新动静红点),不想自己画入口就用它 | | `Vault.load()` | 重新拉一遍档案(别处改了状态以后) | | `Vault.icons()` | 拿二十几枚 24×24 铅笔线工具图标(`{pl_wax: "", …}`),想在思考过程里每个工具一枚就用 | **小卡什么时候摆**:拿到工具结果、`isError` 为假时,在 AI 那一轮的气泡附近放一个容器,调 `Vault.card`。`play_status` 不出卡,`play_use` / `play_train` / `play_report` 出卡。重开页面时照聊天记录里存的工具调用再调一遍就行,数据从服务器拿,不会重复记账。 **日夜**:`` 是白天,别的值都按夜里画。 **配色**:`vault.css` 开头那几个变量(`--ink`、`--ground-hi`、`--mine-bub-fill`……)覆盖掉就换成你的颜色。按钮跟 `--mine-bub-fill` 走,设成你家用户气泡的颜色最顺。 --- ## 五、改称呼 文案默认「她」是用户、「他」是 AI。不一样的话改两处: - `engine/play.md`:给 AI 看的说明 - `engine/play.js` 里 `herLine` 和机关回报的那几句 ## 六、加道具、关道具 看包里 README 的「自己加道具」「关掉不要的」:放一个 `play-custom.json` 就能加,不用改代码。 ## 七、检查清单 - [ ] `PLAY_STATE_DIR` 服务器、MCP、`serve.js` 三边一致 - [ ] `GET /play` 带上口令能拿到 JSON - [ ] `play.md` 进了 AI 的提示词 - [ ] 工具结果没报错时调了 `Vault.card` - [ ] 每条用户消息拼上 `play.drainNotes()` - [ ] 想要用户亲手用的卡重开还在:接 `onRecord` + `Vault.mine` --- The Vault by **Lei**。转载、二改公开发布请保留署名,不许商用,见 [LICENSE](../LICENSE)。