Skip to content

[Feature] 人设卡绑会话:用会话 id 作寻址键,让每个 bot 会话有自己的卡(并容纳 scenario / mes_example) #606

Closed

Description

@modusensus

问题

人格卡现在只有一个寻址键:session.header.agentPreset(src/inject.js:315-324 → src/scope.js:79-80 → src/user-settings.js:54-62 的 resolvePersonaCard)。在 DS Bot 这类宿主里这个键是常量,所以 byAgent 永远只能命中同一张卡,做不到「一个 bot 一张卡」。

实测证据(2026-11-07,真实会话头)

扫 .dsh/sessions/ 下 50 个 bot 工作区会话的 session.v4.jsonl.zstd 首行:

字段 值
agentPreset standard 43 / minimal 3 / ptc 4
主 bot 会话(Amarre / Ephor / Loom / Persis 四条) agentPreset 全部是 standard
各 bot 的 id 各不相同(session-d530e132…、session-389faaa7…、session-e0843aae…、session-ba2974ad…)

即:agentPreset 分不开 bot,session.id 能分开。 面板按 bot 名字建卡永远命中不了(这也是 #587 那条「byAgent 整体为空时回落 default」的真实成因)。

设计

1. 存储:多一个维度,不动老形

user_settings.persona 从 { default, byAgent } 扩成 { default, byAgent, bySession }:

{
  "default":  { ...卡... },
  "byAgent":  { "<agentPreset>": { ...卡... } },
  "bySession":{ "<sessionId>":   { ...卡... } }
}
  • 老数据零迁移:没有 bySession 键读成 {}(parsePersonaStore 的兼容层已经在做同类判断,src/settings.js:57-79)。
  • serializePersonaStore(src/settings.js:81-93)同步带上第三维。
  • 新增 setPersonaForSession(sessionId, card) / removePersonaForSession(sessionId) / listPersonaSessions(),形状照抄既有的 setPersonaFor / removePersona / listPersonas(src/settings.js:549-574)。

2. 寻址:会话 > 身份 > default

resolvePersonaCard 增加一个 sessionId 参数,优先级:

  1. bySession[sessionId](且 enabled === true)
  2. byAgent[agentId](且 enabled === true)
  3. 严格寻址判定(见下)
  4. default

理由:会话是比预设更具体的寻址单位。DS Bot 里 agentPreset 恒为 standard,若 byAgent 优先,则谁给 standard 建了一张卡就会盖掉所有按会话建的卡。

严格寻址(personaStrictAddressing)沿用现有语义,但两个维度合起来看:任一维建过卡就拒绝兜底,一维都没建时仍回落 default(保住 #587 的口径)。

3. 身份解析:一次取全

src/inject.js 的 sessionAgentId(ctx) 改成 sessionIdentity(ctx),返回 { agentId, sessionId },两条取值各自护住(宿主 session 可能是会抛错的 accessor,sessionKeyOf 就是为同一类宿主写的)。renderUserSettingsBlock 相应收 sessionId。

4. 卡字段:补 scenario / mes_example

PERSONA_FIELDS(src/settings.js:37)现在是 ["name","personality","replyStyle","enabled","provenance"]。加 scenario 与 mesExample,渲染时排在 replyStyle 之后——未设置时零输出,所以老用户注入文本逐字节不变。

这两个字段是 #602 里「agent-persona 层要能容纳 scenario / mes_example」的最小落点:先有数据模型与渲染位,拆块本身仍归 #602。

验收

  • bySession 存取往返:写入 → 读回 → 删除,getPersona() 形状正确
  • 老数据(只有 {default, byAgent},或 v1 单对象)读回后 bySession 为 {},注入文本逐字节不变
  • 寻址优先级:会话卡 > 身份卡 > default,各有独立用例
  • 严格寻址:任一维建过卡时不兜底;两维都空时仍回落 default([人格] byAgent 卡按 agentPreset 寻址,而 DS Bot 所有 Bot 会话都是 standard:一人一张卡做不到,严格寻址下连唯一那张也被废掉 #587 回归)
  • scenario / mesExample 未设置时输出与当前逐字节一致;设置后出现在 replyStyle 之后
  • 注入链路端到端:构造带 header.agentPreset + id 的 ctx,断言渲染用的是会话卡
  • 面板预览路由 GET /api/dsh-mneme/persona?agent=… 支持 &session=…
  • 全量测试基线绿;npm run sync 后 lib/ 与 src/ 仍逐字节一致

不做(留后续)

Activity

  1. modusensus commented on Nov 7, 2026

    @modusensus
    MemberAuthor

    判定:待定(设计成立,考期冻结令下顺延)

    先把 issue 里的行号断言逐条核实了一遍,全部成立(基线 main @ 041e8f30):

    • dsh-mneme/src/settings.js:37 const PERSONA_FIELDS = ["name", "personality", "replyStyle", "enabled", "provenance"];
    • dsh-mneme/src/settings.js:57-79 parsePersonaStore:坏值回 { default: {}, byAgent: {} }、"default" in value || "byAgent" in value 判多卡形、旧形整体作 default —— 兼容层在,但没有 bySession 分支。
    • dsh-mneme/src/settings.js:81-93 serializePersonaStore 只写 { default, byAgent }。
    • dsh-mneme/src/user-settings.js:54-62 resolvePersonaCard 的四步(byAgent → enabled 命中 → strict 判定 → default),resolveFactsCard :71-78 同构且共用 Object.keys(store.byAgent).length > 0。
    • dsh-mneme/src/user-settings.js:88 renderUserSettingsBlock({ settings, config, agentId = null, factsProvider = null }) —— 签名里确实没有 sessionId。
    • dsh-mneme/src/inject.js:315-324 sessionAgentId(ctx) 读 ctx?.agent?.session?.header?.agentPreset,:336 直接把它当 agentId 传进去。

    补三条 issue 没写、但这一版必须一起改的点

    1. 面板保存会把 bySession 整段抹掉(静默数据丢失)。dsh-mneme/lib/client.js:4426-4433 的 norm(c) 只认 name / personality / replyStyle / enabled 四个字段 → 就算后端加了 scenario / mesExample,过一遍面板就没了;dsh-mneme/lib/client.js:4642-4648 的 savePersonaCards 是全量 PUT { persona: { default, byAgent } } → 只要面板存一次,库里 bySession 整段被覆盖掉。这条比"面板 UI 交互"更硬:它不需要新 UI 也会发生。
    2. preview 路由要加一个 searchParam:dsh-mneme/src/api.js:598-605 的 preview 现在只读 url.searchParams.get("agent"),要加 &session= 才能预览按会话寻址的结果。
    3. 文案键缺两条:dsh-mneme/src/lang.js:134-176 里 personaNameLine / personaPersonalityLine / personaReplyStyleLine 齐备,但没有 scenario / mesExample 的键,中英各要新增一条。

    验收补一条

    8 条验收里"bySession 优先于 byAgent"建议加一条反向锁:byAgent 有 enabled 卡、bySession 也有 enabled 卡时,必须命中 bySession 且 source 报 bySession(不是 byAgent)——否则优先级写反了测试也不会红。

    顺带一个可复用口径

    dsh-mneme/src/scope.js:144-153 的 sessionKeyOf(exec) 已经给出 sessionId 的取值口径:const id = sessionOf(exec)?.id; return id == null ? null : String(id);,整体包在 try 里。sessionIdentity(ctx) 照抄这一条即可,不要另起一套。

    排期

    考前只修会耽误复习的故障 → 本项顺延考后,先把设计留在 issue 里。

  2. added a commit that references this issue on Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions