# 演员与跨局记忆 —— 让「上局被谁骗过」活下来 > 2026-10-05。对应设计方案 §3.4 第 3 条(跨局记忆)与 §2.2 的第二层人设(玩家性格)。 ## 0. 一句话 **跨局的连续性只能靠落盘的档案**——teammate 是会话内的成员,一局打完就散;而「一局一个会话」 拍板之后,这条路再没有第二个地方可放。 ## 1. 为什么它从「锦上添花」变成了必须 `spawn_teammate` 建出来的玩家挂在一个 Team 上,而 Team 属于**那一局的会话**。会话一结束, 玩家连同它这一局学到的任何东西一起消失。 在此之前,这条缺口还能靠"反正都在同一个会话里"糊过去:同一会话连开两局,teammate 名册还在, 上下文也还在。但名册的上限(8 位,且条目不可移除)让连开两局本身就走不通——于是 **一局一换会话**成了约定,而那条糊法也就彻底没有了。 **这是设计方案的 §2.2 第二次被推到台前**:它把人设分两层,第一层是角色人设(本子给的), 第二层是**玩家性格**(这个人怎么玩游戏)——"同一份剧本换个人玩,玩法完全不同"。第二层要 成立,就需要一个不随会话消失的载体。 ## 2. 判据一:演员 ≠ 座位 演员是**跨局存在的那个人**,座位是**这一局他坐哪儿**。两者刻意分开: | | 属于谁 | 换一局还在吗 | |---|---|---| | 角色本 | 本子 | 不在了(换本子就换人) | | 座位(p1 / p2) | 这一局 | 不在了(同一个人下局可能坐别处) | | **演员** | 那个人 | **在** | 所以档案里记的是「上一局被人连珠追问逼出过话」这种,而不是「上一局我演林小满的时候……」。 **座位号进不了档案**,理由在 §4。 ## 3. 判据二:落盘走宿主的 storage,不自己写文件 演员池用 `ctx.storage` 的域(`@deepseek-ai/dsh-storage-domain`)+ json 后端,与 `dsh-memory` 的 `memory.json` 同一个根目录: ``` $DSH_HOME/storages/jubensha.json ``` **为什么不自己 `writeFileSync`**:域有 zod schema 校验,**一条不合格的记录会让整个打开失败** (`invalid-record`,fail loud);手写的 JSON 坏掉时是静默半坏——少一条、多一个 `undefined`, 读的地方照样跑,只是数据不对。而这份数据是**跨局**的:坏一次会跟着之后每一局。 **为什么放 `$DSH_HOME` 而不是 `/.dsh/storages`**:那个根随 git 分享,而演员池是私人的。 更要紧的是它**该跟着人走,不跟着工作区走**——换个目录开一局,坐下来的还是同一批人。 ## 4. 判据三:上台说明里那段不写座位 演员那一段告诉玩家三件事:**谁在演**、**他怎么玩**、**他记得什么**。它不写座位号,因为 同一批演员换位子是常事——写了反而像在说"你换人了"。 顺序也是判据:演员段排在**角色本之前**。先知道"谁在玩",再看他"这次演谁"。 印象只带**最近三条**进上台说明(`NOTES_IN_BRIEF`)。上限不是省事:印象在池子里会一直长, 而 brief 每局都要重发一遍——三条够他认出"上次栽在谁手里",多出来的只会把角色本挤到后面。 ## 5. 实测 隔离实例 + 会话日志,驱动脚本 `jubensha-drive.mjs` 的 step 11 / 12。 ### 5.1 第一次跑:五个动作全挂 `jubensha_actor` 的 `add` / `note` / `list` / `show` **五个调用全部**返回: ``` Error: cannot get property "storage" without inject ``` **根因**:演员池要 `new DomainFacility(ctx, …)`,而它内部是按**属性**访问 `ctx.storage` 的—— 属性代理只认**声明过的注入**。`actor.ts` 里写的是 `ctx.get('storage')`(那也是 DSH 的正规写法), 但那只解决「我自己怎么读这个服务」,解决不了「**我交给别人的那个 ctx 怎么读**」。 **连带伤害更值得记**:给 `spawn` 传 `actor` 会触发同一个错,**把整次上桌一起拖失败**;同一个 会话里不传 `actor` 的 `spawn` 一直正常。所以那个错看起来像「演员功能坏了」,实际是「插件没声明 自己依赖的服务」。 **修法**:`export const inject = ['tools', 'storage']`。 ### 5.2 修好之后 | 判据 | 结果 | seq | |---|---|---| | 建两个演员 | 两次 `add` 都成 | 1204 / 1206 | | 记一条印象 | `note` 成 | 1212 | | 列池子 | `amay 阿May|印象 0 条`、`laozhou 老周|印象 1 条` | 1218 | | 看档案 | 名字 + 风格 + 那条印象 | 1224 | | **落盘** | `$DSH_HOME/storages/jubensha.json`,657 字节,unit 结构 | — | | **跨进程** | **重启之后 `list` 仍然是** `laozhou 老周|印象 1 条` | **1276** | **最后一条才是这一块存在的理由**:同一个进程里列得出来不算数,重启之后还在才算。 ### 5.3 带演员上桌 上一轮它卡在 `Team member limit 8 reached`——那个会话已经上过 8 位 teammate。而按当天定的 约定(**一局一个会话**,见 [2026-10-05-桌面与信道.md](2026-10-05-桌面与信道.md) §5.3), 本来就该换一个新会话再跑。新会话里: | 判据 | 结果 | seq | |---|---|---| | 池子跨会话还在 | `amay 阿May|印象 0 条`、`laozhou 老周|印象 1 条` | 20 | | `spawn` 带 `actor` | **成功**,入参是 `roleBook: "book:p1@02"` 加 `actor: "laozhou"` | 43 | | `say` 送达 | `delivered: "吴国强"` | 49 | **最硬的那条证据在玩家自己的会话里**(子会话 `005c387b`,`agent/inbox/spliced` seq 3)。 它收到的第一条消息完整包含: ``` --- 谁在玩这个角色 --- 这一局由「老周」来演。你怎么玩这个游戏,跟你这次拿到什么角色无关: 藏拙型:能不说就不说,被追问才答,答得短。等别人先暴露,自己最后表态。 你还记得这些(都是**前面几局**的事,跟这一局的人无关): - 上一局被人连珠追问,把藏了整局的事说了出来——下次遇到压迫型要更早闭嘴。 ``` 三件事一次到位:演员段在**角色本之前**、引用被**解开成正文**、通道写的是 `lead`。 **为什么这条判据要下到子会话去取**:`spawn` 成功只说明调用没报错;要证明「那段话真的到了 他手上」,只有读他被投喂的那条消息本身。中间层的任何返回都不够。 ## 6. 头像与性格:演员这一层的两块展示与录入 **2026-10-05 用户提,同一天定形。** ### 6.1 头像:默认生成,可以换成图 | 形态 | 怎么来 | 什么时候用 | |---|---|---| | `svg`(默认) | 按演员的名字与性格生成一个**内联 SVG** 几何图形 | 什么都没配的时候 | | `image` | 用户给的一张图(存路径) | 想给某个演员一张"就是这张脸"的图 | **默认必须是生成,不能是空**:空着的时候桌上就没有头像,而一个不填就没有的东西等于没做。 生成用的是内联 SVG,不是 emoji——界面图标一律内联 SVG 是这个工作区既有的约定 (字符当图标渲染出来就像 emoji 标记,曾被用户当场指出)。 **为什么生成也够用**:对话形态下头像只有几十像素大,它的作用是让"桌上坐着几个人、谁在说话" 变成一眼能看出来的东西,不是肖像画。同一个演员每局都长同一张脸才是要紧的——所以 SVG 由 **演员 id 决定**(而不是每局随机),于是它成了这个人在桌上的记号。 ### 6.2 性格:用户自己捏,或者让 LLM 照着几句话生成 **两条路都要**(用户原话是"自定义**或**由 llm 帮助设置"):用户能直接写,也能只给几句提示 让模型生成一版,看过再落库。 **判据是设计方案 §2.2 那两条**,它们比"生成得像不像"要紧得多: - **写成决策偏好,不写成形容词列表**。「性格火爆、心直口快」一出手就被识破是表面功夫, 也影响不到任何决策;写成「抓到一点就往前压,宁可压错」,它才会在票型上留下痕迹。 - **写到「他想选什么,但实际做成了什么」**——即欲望与能力的缺口。这条是进阶版: 原型是那个"平时嘴上没把门、愿望偏偏是守口如瓶一次"的胖厨子。 **生成的那一版一定要给人过目再落库**:它落进去就是跨局的(这个演员以后每局都这么玩), 而"生成得不对但已经写进档案"比"没有性格"更难发现。 **落点**:`jubensha_actor action="draft"` 返回一份**可填的骨架**(把上面两条判据摆出来, 外加这个演员已有的印象),由主持人接着用户的话写成具体的一段,再走 `action="add"` 或 `action="style"`。**插件自己不去调模型**——它没有那条通道,也不该有;写的动作属于主持人。 ### 5.4 探针的一个局限 `jubensha-drive.mjs` 用「消息锚点数量变化」判断模型是否动过,而**重启后的新会话**里那个 计数与刚发的消息对不上——`step 12` 因此报了「锚点始终没变」,但工具调用其实发生了 (日志里 seq 1275/1276 俱在)。**界面锚点不是可靠判据时,会话日志才是。**