先定会话存哪、再定工具谁能调——OpenMuse 最小接入前的两步

文章导读
在 OpenMuse 里做最小接入,真正容易返工的不是调用代码本身,而是两个位置没提前定:会话数据写在哪一层、工具调用的权限校验放在哪里。这两件事一旦进了代码再改,通常要连带动前端状态层、请求封装、后端路由、持久层和工具分发器一起动。建议在写第一行接入代码前,先把这两项定下来,再动手。
📋 目录
  1. 一 列出项目里所有会读写会话数据的位置
  2. 二 决定会话存前端还是后端
  3. 三 决定工具调用由谁发起、谁校验
  4. 四 按决策改一处代码并跑通一次完整问答
A A

在 OpenMuse 里做最小接入,真正容易返工的不是调用代码本身,而是两个位置没提前定:会话数据写在哪一层、工具调用的权限校验放在哪里。这两件事一旦进了代码再改,通常要连带动前端状态层、请求封装、后端路由、持久层和工具分发器一起动。建议在写第一行接入代码前,先把这两项定下来,再动手。

先做两项可验证的决策:一是列出项目里所有读写会话数据的位置,再按多端访问、刷新保留、数据敏感度三个条件决定会话存前端还是后端;二是把工具调用的发起方与校验点钉在服务端分发器入口,白名单只作为服务端配置。是否成立看一次完整问答链路:会话 id 前后一致、工具校验只在一处生效、被拒调用有稳定错误码。具体落点需要结合你的部署方式确认。

列出项目里所有会读写会话数据的位置

第一步不是选方案,是把现状摊开。先检索一遍,把可能出现会话读写的位置列成清单:

grep -rn `--include`="*.ts" `--include`="*.tsx" -E "session|conversation|chat_id|history" src server | head -50

把命中的文件按读写方向标注,标不出来的先记为待确认:

  • 前端状态层,例如 src/store/chat.ts:读写都有,负责暂存当前会话和历史列表,刷新后是否恢复取决于有没有做本地持久化。
  • 请求封装层,例如 src/api/chat.ts:一般只写,往请求头或 body 塞 sessionId,偶尔读用户当前选中的会话。
  • 后端路由与持久层,例如 server/routes/chat.ts、server/db/session.sqlite:读写都有,是消息落库和会话归属判断的实际位置。
  • 日志与埋点调用点:一般只写,但经常顺手把消息正文和工具参数一起写进去。
  • 工具执行器,例如 server/tools/runner.ts:读会话上下文,写工具调用结果。

相互覆盖的风险点主要盯三处:前端本地历史和后端历史同时存在,刷新时以哪份为准没有约定;同一个 session 并发写,后写的结果直接盖掉前一条;日志或埋点复制了消息正文,导致敏感字段多出一份副本。这三处先用注释标出来,改完再回头确认。

决定会话存前端还是后端

三个条件逐条过,不要凭感觉合并判断:

先定会话存哪、再定工具谁能调——OpenMuse 最小接入前的两步
判断条件偏前端偏后端怎么确认
多端访问只在一台设备的浏览器里用手机、网页、客户端任一入口都要看到同一会话换一个入口发一条消息,看另一入口是否出现
刷新保留刷新后能恢复即可,换设备丢失可接受刷新和换设备都必须还在清掉本地存储后刷新,看历史是否还在
数据敏感度只是本地草稿、无身份信息含订单、内部资料、用户身份等字段检查消息体里是否有需要做访问控制的字段

两项选择对应的改动范围不同:

  • 存前端:改动集中在前端状态层、请求封装和刷新恢复逻辑,后端可以保持无状态;代价是多端同步要自己补,且清理浏览器数据即丢历史。
  • 存后端:改动集中在路由、持久层和会话归属校验,前端只保留 sessionId 与展示状态;后续加多端同步不用再迁移数据,但每次读写都要过一次归属判断。

如果暂时判断不了,可以先按后端存、前端只缓存最近一次会话的方式做,同时在前端注释里写明这是过渡状态,避免以后被当成正式方案继续叠加。

决定工具调用由谁发起、谁校验

工具调用的权限边界只在一个地方算数:服务端分发器入口。前端的工具开关、按钮禁用只能算交互提示,绕过前端直接发请求一样能触发工具,所以不能把前端当校验点。发起方可以是模型、可以是前端按钮,但校验必须由服务端完成。

白名单配置建议按工具名组织,字段保持最少:

先定会话存哪、再定工具谁能调——OpenMuse 最小接入前的两步
{
  "tools": [
    {
      "name": "search_docs",
      "enabled": true,
      "allow_roles": ["user"],
      "require_confirm": false,
      "params": { "query": "string" }
    }
  ]
}

替换项:name 用实际工具名,allow_roles 按你项目里已有的角色字段填,require_confirm 控制是否要二次确认,params 只做结构说明,真正的参数校验放在工具函数内部。

校验位置放在分发器入口,顺序是先查配置里是否存在并启用该工具,再查当前会话角色是否在 allow_roles 里。两项不过就直接返回,不进工具函数。校验失败时返回稳定的错误结构,让前端按 code 处理:

{
  "ok": false,
  "error": {
    "code": "TOOL_FORBIDDEN",
    "message": "该工具对当前会话不可用",
    "tool": "search_docs"
  }
}

错误信息里不要带白名单全貌、内部角色名或工具实现细节,只说明这次调用为什么被拒。前端把 TOOL_FORBIDDEN 渲染成一句提示即可,不要自动重试;参数结构不对的情况建议单独给一个 code,和权限拒绝分开。

按决策改一处代码并跑通一次完整问答

两项决策定完后,只改一处代码先验证,不要一次把会话迁移和工具校验都铺开。

先定会话存哪、再定工具谁能调——OpenMuse 最小接入前的两步

改动点控制在两处,每处一个文件:

  1. 会话写入位置按第二节的选择改一处:例如由后端在首次响应时生成并下发 sessionId,前端只保存该值,不再自己生成。
  2. 在工具分发器入口加一次校验调用,白名单从配置文件读取。

跑通标准是一次完整问答链路能观察到这些:发送问题后响应里带回 sessionId,前端后续请求沿用同一个 sessionId,工具调用日志里出现一条校验通过的记录,刷新页面后的会话行为符合第二节选定的预期。日志里能同时看到校验通过和被拒两类记录,说明校验点只在一处生效。

失败时按下面顺序回退,一次只回一步:

  1. 先回退工具校验入口,改成只记录不拦截,确认问答主链路恢复。
  2. 再回退会话写入位置的改动,恢复原来的 sessionId 生成方式。
  3. 最后回退前端对 sessionId 的读取逻辑。

回退按单文件维度提交,不要和已经跑通的部分混在同一个提交里,否则回退时会把有效的改动一起撤掉。