中文EN

@max-null/dsh-jubensha

fun已测通过npmMIT

一个人的剧本杀:AI 当主持人,AI 填满其余的椅子(各自只知道自己的角色本),留一个位子给你 —— 带阶段条、按座位归集的时间线与搜证页,而真相到复盘才解封。

下载量 / Downloads
1,107
依赖体积 / node_modules
12.13 MB
测试时间 / Tested
2026-11-06
DSH 版本 / Version
0.2.2-rc.1

安装

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

链接

GitHub · Homepage · npm

同类插件