@max-null/dsh-jubensha
剧本杀(jubensha):一个人也能开局的推理本与情感本 —— AI 当 DM,AI 填满整张桌子 适合想要独自一人通过AI模拟整桌玩家游玩推理/情感剧本杀的用户。
インストール
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:Max-Null/dsh-jubenshaドキュメント
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=,围栏是 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_messagejubensha_case:照着本子让玩家上桌;封存期取truth被拒(报错带出路)、book只给 引用而不给正文,而玩家确实拿到了角色本——见下jubensha_case action="clue":三条线索以「可直接贴到桌上」的格式返回,而只写在supports里的那句话,在整个会话里一次都没出现过;同一局里truth在复盘前被拒、 推进到reveal之后解封(返回里带culprit)jubensha_player action="relay":验到它取回的是那位玩家的原话(判据见下)jubensha_actor action="draft":返回的是「该怎么写性格」的两条判据,不是一段现成的 性格;带上actor时多列那位已有的印象(那是它与「无差别生成一段」的区别所在)- 三个正式本子都
load过,而且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)—— 那个标签显示的永远是「这个会话」的那一局,
压缩也不会让别的会话看见或提前解封。
演员与角色是两层:角色来自本子,人来自演员池,而演员池跨会话活着。插件自带六位
(老猫 / 阿珍 / 老孙 / 小何 / 铁哥 / 白姐)——空池子里的「随机」没有意义,而让新用户先自己写六个
性格,门槛比这个插件该有的高得多。补齐只补缺的:已经存在的一个字段都不动,那是他自己的东西。
开一局时那一个「随机排」把整桌重排,所以同一个人每局可能坐不同的位子、拿不同的本子——而这正是
跨局印象值得记的理由。同一个演员下一局
可以换座位、换角色本,但他记得前面几局发生过什么——所以档案里记的是「上局被谁骗过」,
不记座位号。机制与那次 cannot get property "storage" without inject 的排查见
docs/设计/2026-10-05-演员与跨局记忆.md。
房间是会话视图里的一个标签,叫「团队」(同一个包的浏览器半边):与「对话 / 轨迹 / 上下文」 并列。显示当前局面、座位表、演员池(含按 id 生成的头像),并能选本子、给每个位子挑演员、 生成一段开局指令。它只生成文本、不自己调工具:一旦会发号施令,它就从「另一块屏」变成了 「第二条控制通道」,而「界面是附加层」那条约束是靠这个守住的。
标签名是刻意的——对话形态唯一的面板给不了的东西,就是「看起来在干别的」(用户原话
「极其隐蔽,适合摸鱼」)。局面按会话存(Map),因为标签显示的永远
是「这个会话」的那一局;数据走插件自己的只读端点(GET /jubensha/room?session=,围栏是
loopback + 同源)。机制、几处判据与踩过的坑见
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——一行代码没动。
性格可以自己写,也可以让模型给一版:action="draft" 返回的不是一段现成的性格,而是
该照着什么写——两条判据来自设计方案 §2.2(写成决策偏好而不是形容词列表;写到「他想选
什么,但实际做成了什么」)。写出来的那条要跨局,所以必须给人过目再落库,插件自己不去调
模型。
头像是生成的:没配图时按演员 id 画一个内联 SVG 几何图形(同一个 id 永远同一张脸,
所以它是这个人在桌上的记号);想换就 action="avatar" 给一张图,传空串换回来。
生成而不是留空,是因为"不填就没有"等于没做。
桌面上所有人听得见所有人 —— 而那是程序送过去的,不是主持人转述的。
2026-10-07 之后,真人与 AI 玩家说的每一句话都由宿主自己逐句原样广播给桌上其余人
(署名准确、原文一字不改)。在这之前它靠主持人手打 relay,而实测里那种转述会出四种错:
署名不准、地址被摘(真人说「回阿May:你说你进门的时候我才走」,广播版成了
「你说你进门的时候我才走」,而另一个也「进过门」的人会以为在说她)、「你可以接」、
以及最要命的忘了转。机制、四条判据的实测(338 对逐字一致)与量出来的上下文代价见
docs/设计/2026-10-07-桌上说话-公共信道.md。
action="say" 仍是主持人的嘴(转场、提问、宣布阶段);action="relay" 留着兼容,
而实测里它已经一次都没被调过。那条「玩家一个字都发不出来」的排查与 Team 名册的硬上限见
docs/设计/2026-10-05-桌面与信道.md。
带答案的东西不经过 DM 的上下文:DM 的思考与工具操作条对玩家是可展开的,所以
「不给」写在工具层——真相与整段线索封存到复盘(白名单八段随时可取),角色本走
引用(全文留在插件里,上桌时由程序自己解开送进玩家的首条消息)。机制、边界与一次
实测失败的记录见
docs/设计/2026-10-05-真相保险箱.md。
玩家「只能说话」由两层机制保证,不靠 prompt 纪律:tools.restrict 管看不看得见
(继承层),tools/pre-execute 管准不准执行(含 agent 自己那层,内核的委派工具就在那里)。
机制、源码位置,以及三次迭代踩过的两个时序坑,见
docs/设计/2026-10-05-spawn_player-可行方案.md。
本子是可校验的数据:格式见 schema/case.schema.yml(v1.0),
加载器与它的判据见
docs/设计/2026-10-05-本子格式与加载器.md。
七个本子都已转成 YAML,各住在 cases/-/case.yml;Markdown 手册留着
(那是人写本子时的形态),YAML 是机器读的那一份。
转换不是抄一遍——填不下去的地方就是本子的缺口。case-01 就填出了一处:真人那位角色 (林默)没有「现在」,而另外三张角色卡都自带身份。那正是第三局玩家当场问住的同一件事 (「我人物资料呢?我不知道自己现在是在做什么」),而它在这本最早写的本子里就存在。 转换没有替它编一句补上:本子该修,但不该由抄的人顺手圆过去——那会把「本子缺东西」 变成「看起来不缺」。加载器对它的判定是一条 warn,测试里钉着。
这一版到哪儿了:本子已经能一局一局真开(cases/ 里那七个都在隔离实例里跑过;另有一个
99 外面的那本,它住在工作区外,用来验「从别的目录读本子」那条路),房间页三栏都接上了数据
(时间线那一页读的就是局面里的 timeline,而自述阶段结束前每个座位都得有一条——那条有闸看着)。
设计方案 §3.4 余下的私聊一侧是故意不做的:真人这一侧只有一个输入框,会话对话就是桌面,
而「对主持人说」与「对桌上说」分开要新增一个界面元素——没有实测需求之前不先加它
(取舍写在那篇设计文档的 §6)。
安装
dsh plugin add @max-null/dsh-jubensha
怎么开第一局:新会话里在标题上连点三下,它会往输入框里填一句「来一局剧本杀」, 回车 —— 对话/轨迹/上下文那一排标签这时才出现,「团队」就在最右边,点进去选本子、给座位挑演员、 点「开一局」。

那个入口是刻意不显眼的(这个面板的用途之一就是「看起来在干别的」,见上面「标签名是刻意的」)。 而它不做别的:只往输入框里填一句话,发送与否、开哪一本都由你决定——它不会替你开局。
本子
自带七本。四人的六本(01 拾光照相馆 / 02 深夜电台 / 03 没拆的那封信 / 04 三支药 /
05 金库的铜带 / 06 年会那晚),加一本六人的 07 夜场 —— 六人那一本的价值不在人多:
四个人的时候,一个人的不在场证明只能靠别人记不记得;六个人里台前与后台是两条天然的对照线,
五个人的位置能互相咬合,而凶手得在这张网里找一个空档。
本子的格式由 schema/case.schema.yml 定义 —— 它的每一条字段注释
都是从三局实测里逼出来的判据,不是先验设计。最要紧的三条:
- 收尾必须有一条物理上不可绕的证据。自检问句:把真凶放到法庭上,检察官能拿出什么? 答案是「他撒谎了」→ 本子不合格(他改口"我记错了"就能脱身)。
- 误导负责制造情绪,不负责制造另一种真相。要给他心理负担,就让他「以为自己做了什么」(主观), 而不是「客观上真的可能做了什么」(那会造出第二个答案)。
- 动机会自己推着玩家走 —— 出口要修在动机的延长线上。你给了他「最好的朋友」,这条动机 必然推他追到底;那就得在那条路上修落点,而不是在旁边另摆一个「共情出口」。
要写新本子(或者让别的模型写)时,读 skill jubensha-write-case —— 它装的是上面那三样
机械检查抓不到的东西:七本里真踩过的五个逻辑坑(每一条都带着是哪一本踩出来的)、
怎么想一个本子的真相与情感落点、四人与六人的差别。写了之后跑这两样:
node scripts/check-case-syntax.mjs # 两条 YAML 坑:引号数、* 开头
pnpm test # 11 条结构检查:角色六段非空、恰好一个凶手、五个阶段 id…
已知限制
- 共享工作目录:所有 AI 玩家与 DM 观察同一个 cwd(Agent Teams 的既有边界)。所以 本子的真相手册不得落在工作区内 —— 开局时写到工作区外,散场后归档。
- 角色本不落盘:只经消息送达对应 agent。落盘就等于任何 agent 都能读到别人的秘密。
- 玩家看得见的比能用的多:白名单只有
send_message一个(src/player.ts的PLAYER_TOOLS), 而模型请求里实际带着 10 个——那是 2026-10-05 抓玩家子会话的请求时数出来的(它是实测数, 不是常量,源码里没有那个 10)。多出来的那些是走 Team 的必然残留:Team 成员工具 9 个 (spawn_teammate/list_agents/wait_agent/team_task_*…)加上内核的subagent, 全都注册在agent 自己那层,而tools.restrict按定义只管继承层。安全上没问题 (执行前的闸全兜住了),代价是占 prompt 空间、可能引它试一次。「进 roster」与「工具面绝对 干净」在机制上不可兼得。 - 一局之后 teammate 名字不释放:Team 名册是跨进程持久的——一个名字在一个 DM 会话里用过
一次,重启之后也不能再用(撞
TEAM_MEMBER_NAME_TAKEN),而名册本身不可移除 (agent-team/src/roster.ts:272)。插件给每个座位缀了时间戳 + 随机后缀(不是单调计数—— 计数器是进程级的,而名册不是,2026-10-05 踩了两轮才看清)绕开它,但团队名册本身是累积的。 - 玩家会出戏:实测里玩家有几次不发台词,改发「进展(林晚侧):已向主持人确认口径…」 这类元层面汇报。上台说明还需要更硬的约束。
- 一个会话打一局:Team 名册一个会话最多 8 位 teammate,而条目不可移除(上限来自
experimental/agent-team-profile/cordis.patch.yml的maxMembers: 8,内核默认是 16), 按每局 3 位 AI 玩家算开不了三局。约定是一局一换会话。(绕开 Team 直接用ctx.subagents.startContinuable也能解,但那会丢掉已验证的 Team 实践,不值。) 推论:跨局的连续性只能靠落盘的玩家档案,不能靠会话上下文——见下。 - 封存不是物理隔离:真相保险箱挡住的是顺手泄漏(发角色本必看全座位秘密那条路),
不是"DM 看不到"。DM 是主 agent,它真要主动
read本子文件,谁也拦不住。判据只有一条: 按正常工作流程带局时,答案不流经它的上下文。 另一半仍未解决——人类玩家这一侧没有 私聊信道,会话对话就是桌面。 - 跨局的东西目前只有演员池:局面、座位、玩家登记都随会话消失,所以「跨局记忆」现在到
「这个人记得什么」为止,还不到「这批人之间的旧账」。演员的印象靠主持人一局结束时写
(
jubensha_actor action="note")——他不写,这一局就什么都不剩。
待做
- 6 人的本子:《夜场》(
cases/07-夜场/)已经写完并在 dev 里完整跑过一局—— 而那一局暴露的六人特有问题还没整理(比如六个人各自那段要能互相咬合,比四人难)。 relay的去留:桌上说话已改由宿主广播(玩家与真人说的每一句原样送到其余人, 不再经主持人手打转达);见docs/设计/2026-10-07-桌上说话-公共信道.md。 而relay一次都没被调过——它现在只作兼容留着,退役之前要确认「主持人在广播之下确实 不再需要转达」。