@max-null/dsh-jubensha
一个人的剧本杀:AI 当主持人,AI 填满其余的椅子(各自只知道自己的角色本),留一个位子给你 —— 带阶段条、按座位归集的时间线与搜证页,而真相到复盘才解封。
安装
dsh plugin --profile web add @max-null/dsh-jubensha
沙箱测试结果
| 阶段 | 结果 |
|---|---|
| 1 安装 | ✓ |
| 2 列装 | ✓ |
| 3 挂载 | ✓ |
| 4 启动 | — |
四阶段在隔离沙箱里跑:安装(有网)→ 列装 → 挂载 → 启动(后三步断网)。
README
@max-null/dsh-jubensha
剧本杀(jubensha):一个人也能开局的推理本与情感本 —— AI 当 DM,AI 填满整张桌子。
名字用拼音不是偷懒:剧本杀没有对得上的英文词。murder mystery 是西式推理小说、
LARP是实况角色扮演、social deduction是狼人杀那类纯规则游戏 —— 都不对。
用拼音等于拒绝一个错误的类比:这是中文圈的东西,不是舶来品。
它解决什么
剧本杀的真实门槛不是钱,是人:四到六个玩家加一个 DM,凑齐一次比票价难得多。
本插件把这个门槛拆掉 —— 你一个人就能开局:AI 坐 DM 位(它本来就该全知),AI 填满其余的椅子
(它们各自只知道自己的角色本)。留下来的问题是:AI 玩家像不像人。这一点已经用七个本子在真实
DSH 里一局一局实测过(见 cases/),而不是设想。
截图
「团队」标签 —— 与「对话 / 轨迹 / 上下文」并列的那个房间。左栏是这一局的人(座位、人物关系、
时间线),中栏是剧本正文(默认收起)与当前阶段那一页,右栏是桌上说了什么。便签可以留在任意
位置(右键),跟着标签走、按会话存。
人物关系点开是大图 —— 左栏那 258px 里放的是列表(那个宽度里列表好读),点一下弹窗,里面才
是连线图:死者在正中、四个角色在四角、关系写在线上。窄栏里画不下那张图,而它本来就需要宽度。
窄屏下三栏叠起来 —— 窗口窄的时候那三栏不是挤在一起,而是各自成块往下排(每块自己有滚动)。
一个人在笔记本上把它挂在旁边看的时候是这一副样子。
设置页在插件管理里 —— 侧栏「插件」→ 点开 @max-null/dsh-jubensha 那张卡片,演员池(名字、
怎么玩、跨局印象)、本子上哪儿找、加目录都在那一页上。它挂的是插件管理给别的插件留的
plugins.bundle.config 槽(key 是包名)。
装完还会多出什么(行为效果):
变化 · 说明
一个会话视图标签 · 叫「团队」—— 对话形态唯一的面板给不了的东西,就是「看起来在干别的」
一个只读端点 · GET /jubensha/room?session=<id>,围栏是 loopback + 同源
三个写端点 · 演员池、便签、本子目录;便签按会话存,另两个跟着这台机器上的人走
多一个工具面 · 模型能开一局、推进阶段、查局面,而不用把这些记在对话里
阶段与线索有机器可读的记录 · 复盘直接读事件日志,不再靠 DM 回忆
玩家 agent 的工具面被收窄 · 它们能说话,但读不到本子文件 —— 由 jubensha_player 在创建时收窄
状态
骨架与玩家上桌都已跑通,并在真实 DSH 里验证过。
- L1:
typecheck/test(155 条)/build全绿 - L2:在隔离实例里加载成功,四个工具都被模型真实调用过
jubensha_state:先start再show,参数正确、返回局面正确jubensha_player:玩家经 Team 上桌并与 DM 对话五轮;它的模型请求里只有一种工具面
(10 个,跨 5 次请求不变),继承层原有的 146 个(pwsh / read / edit …)已被清掉,
实际能用的只有 send_message
jubensha_case:照着本子让玩家上桌;封存期取truth被拒(报错带出路)、book只给
引用而不给正文,而玩家确实拿到了角色本——见下
jubensha_case action="clue":三条线索以「可直接贴到桌上」的格式返回,而**只写在
supports 里的那句话,在整个会话里一次都没出现过**;同一局里 truth 在复盘前被拒、
推进到 reveal 之后解封(返回里带 culprit)
jubensha_player action="relay":验到它取回的是那位玩家的原话(判据见下)jubensha_actor action="draft":返回的是「该怎么写性格」的两条判据,**不是一段现成的
性格**;带上 actor 时多列那位已有的印象(那是它与「无差别生成一段」的区别所在)
- 三个正式本子都
load过,而且dir给的是目录——测的是<dir>/case.yml那个约定:
case-01 报一条 warn(林默没有「现在」,见下),case-02 与 case-03 零问题
jubensha_actor:建演员、记印象、列池子、看档案全通;重启进程之后仍在——
落盘在 $DSH_HOME/storages/jubensha.json。带演员上桌也验到了:在新会话里 spawn
成功,而玩家子会话收到的第一条消息里,演员段(名字、怎么玩、上局的印象)**排在角色本
之前**,引用已解成正文(判据取自子会话自己的日志,不是 spawn 的返回值)
工具面四项:jubensha_state(看局面 / 开一局 / 推进阶段 / 公布线索 / 记一条行踪)、
jubensha_player(看桌上都有谁 / 让玩家上桌 / 对某位玩家或全桌说话 /
把某位玩家的话转达给其他人 / 请他下桌)、
jubensha_case(新会话第一步:这本都装了哪些本子 / 这本有哪几个座位 / 取某个角色的角色本 /
取线索的牌桌原文 / 取本子的某一段)、
jubensha_actor(池子里有谁 / 招一个演员 / 取一份「怎么写性格」的骨架 / 改他怎么玩 /
换头像 / 记一条跨局印象 / 看某人的档案)。
局面按会话存(Map<sessionId, GameState>)—— 那个标签显示的永远是「这个会话」的那一局,
压缩也不会让别的会话看见或提前解封。
演员与角色是两层:角色来自本子,人来自演员池,而演员池跨会话活着。插件自带六位
(老猫 / 阿珍 / 老孙 / 小何 / 铁哥 / 白姐)——空池子里的「随机」没有意义,而让新用户先自己写六个
性格,门槛比这个插件该有的高得多。补齐只补缺的:已经存在的一个字段都不动,那是他自己的东西。
开一局时那一个「随机排」把整桌重排,所以同一个人每局可能坐不同的位子、拿不同的本子——而这正是
跨局印象值得记的理由。同一个演员下一局
可以换座位、换角色本,但他记得前面几局发生过什么——所以档案里记的是「上局被谁骗过」,
不记座位号。机制与那次 cannot get property "storage" without inject 的排查见
[docs/设计/2026-10-05-演员与跨局记忆.md](docs/设计/2026-10-05-演员与跨局记忆.md)。
房间是会话视图里的一个标签,叫「团队」(同一个包的浏览器半边):与「对话 / 轨迹 / 上下文」
并列。显示当前局面、座位表、演员池(含按 id 生成的头像),并能**选本子、给每个位子挑演员、
生成一段开局指令。它只生成文本、不自己调工具**:一旦会发号施令,它就从「另一块屏」变成了
「第二条控制通道」,而「界面是附加层」那条约束是靠这个守住的。
标签名是刻意的——对话形态唯一的面板给不了的东西,就是「看起来在干别的」(用户原话
「极其隐蔽,适合摸鱼」)。局面按会话存(Map<sessionId, GameState>),因为标签显示的永远
是「这个会话」的那一局;数据走插件自己的只读端点(GET /jubensha/room?session=<id>,围栏是
loopback + 同源)。机制、几处判据与踩过的坑见
[docs/设计/2026-10-05-房间面板.md](docs/设计/2026-10-05-房间面板.md)。
界面已经落到代码里:设计在
[docs/设计/2026-10-05-房间与设置-界面设计.md](docs/设计/2026-10-05-房间与设置-界面设计.md),
三栏 + 五阶段 + 便签层都在,截图见上。那份文档逐块写了「数据从端点哪个字段来」,而**答不上来的
那几块仍明说没接**(时间线要按座位归集他说过的时段、问话与投票两页的内容、设置页在思灵里的入口)
——那是刻意的:画一个空壳比空着更糟。它还记了读会话内容那条路的完整链路(§5.1–5.3)与三个
踩过的坑(§5.4 ctx.sessions 的两面性、§5.5 values() 而不是 turnDataSource、§5.6 探针抄漏了
一行)。
创意工坊只到预研:本子是不可信输入,而且是会进模型上下文的不可信输入。攻击面与三层建议
(最要紧的是让来源可见)见
[docs/设计/2026-10-05-创意工坊预研.md](docs/设计/2026-10-05-创意工坊预研.md)——一行代码没动。
性格可以自己写,也可以让模型给一版:action="draft" 返回的不是一段现成的性格,而是
该照着什么写——两条判据来自设计方案 §2.2(写成决策偏好而不是形容词列表;写到「他想选
什么,但实际做成了什么」)。写出来的那条要跨局,所以必须给人过目再落库,插件自己不去调
模型。
头像是生成的:没配图时按演员 id 画一个内联 SVG 几何图形(同一个 id 永远同一张脸,
所以它是这个人在桌上的记号);想换就 action="avatar" 给一张图,传空串换回来。
生成而不是留空,是因为"不填就没有"等于没做。
桌面上所有人听得见所有人 —— 而那是程序送过去的,不是主持人转述的。
2026-10-07 之后,真人与 AI 玩家说的每一句话都由宿主自己逐句原样广播给桌上其余人
(署名准确、原文一字不改)。在这之前它靠主持人手打 relay,而实测里那种转述会出四种错:
署名不准、地址被摘(真人说「回阿May:你说你进门的时候我才走」,广播版成了
「你说你进门的时候我才走」,而另一个也「进过门」的人会以为在说她)、「你可以接」、
以及最要命的忘了转。机制、四条判据的实测(338 对逐字一致)与量出来的上下文代价见
[docs/设计/2026-10-07-桌上说话-公共信道.md](docs/设计/2026-10-07-桌上说话-公共信道.md)。
action="say" 仍是主持人的嘴(转场、提问、宣布阶段);action="relay" 留着兼容,
而实测里它已经一次都没被调过。那条「玩家一个字都发不出来」的排查与 Team 名册的硬上限见
[docs/设计/2026-10-05-桌面与信道.md](docs/设计/2026-10-05-桌面与信道.md)。
带答案的东西不经过 DM 的上下文:DM 的思考与工具操作条对玩家是可展开的,所以
「不给」写在工具层——真相与整段线索封存到复盘(白名单八段随时可取),角色本走
引用(全文留在插件里,上桌时由程序自己解开送进玩家的首条消息)。机制、边界与一次
实测失败的记录见
[docs/设计/2026-10-05-真相保险箱.md](docs/设计/2026-10-05-真相保险箱.md)。
玩家「只能说话」由两层机制保证,不靠 prompt 纪律:tools.restrict 管看不看得见
(继承层),tools/pre-execute 管准不准执行(含 agent 自己那层,内核的委派工具就在那里)。
机制、源码位置,以及三次迭代踩过的两个时序坑,见
[docs/设计/2026-10-05-spawn_player-可行方案.md](docs/设计/2026-10-05-spawn_player-可行方案.md)。
本子是可校验的数据:格式见 [schema/case.schema.yml](schema/case.schema.yml)(v1.0),
加载器与它的判据见
[docs/设计/2026-10-05-本子格式与加载器.md](docs/设计/2026-10-05-本子格式与加载器.md)。
七个本子都已转成 YAML,各住在 cases/<编号>-<名字>/case.yml;Markdown 手册留着
(那是人写本子时的形态),YAML 是机器读的那一份。
转换不是抄一遍——填不下去的地方就是本子的缺口。case-01 就填出了一处:真人那位角色
(林默)没有「现在」,而另外三张角色卡都自带身份。那正是第三局玩家当场问住的同一件事
(「我人物资料呢?我不知道自己现在是在做什么」),而它在这本最早写的本子里就存在。
转换没有替它编一句补上:本子该修,但不该由抄的人顺手圆过去——**那会把「本子缺东西」
变成「看起来不缺」**。加载器对它的判定是一条 warn,测试里钉着。
这一版到哪儿了:本子已经能一局一局真开(`cases
…(truncated)
以上内容来自 npm 上的 README,我们只做格式化,不改内容。
版本
npm 上共发布:3,最后更新 2026-10-17