在 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 并发写,后写的结果直接盖掉前一条;日志或埋点复制了消息正文,导致敏感字段多出一份副本。这三处先用注释标出来,改完再回头确认。
决定会话存前端还是后端
三个条件逐条过,不要凭感觉合并判断:
| 判断条件 | 偏前端 | 偏后端 | 怎么确认 |
|---|---|---|---|
| 多端访问 | 只在一台设备的浏览器里用 | 手机、网页、客户端任一入口都要看到同一会话 | 换一个入口发一条消息,看另一入口是否出现 |
| 刷新保留 | 刷新后能恢复即可,换设备丢失可接受 | 刷新和换设备都必须还在 | 清掉本地存储后刷新,看历史是否还在 |
| 数据敏感度 | 只是本地草稿、无身份信息 | 含订单、内部资料、用户身份等字段 | 检查消息体里是否有需要做访问控制的字段 |
两项选择对应的改动范围不同:
- 存前端:改动集中在前端状态层、请求封装和刷新恢复逻辑,后端可以保持无状态;代价是多端同步要自己补,且清理浏览器数据即丢历史。
- 存后端:改动集中在路由、持久层和会话归属校验,前端只保留 sessionId 与展示状态;后续加多端同步不用再迁移数据,但每次读写都要过一次归属判断。
如果暂时判断不了,可以先按后端存、前端只缓存最近一次会话的方式做,同时在前端注释里写明这是过渡状态,避免以后被当成正式方案继续叠加。
决定工具调用由谁发起、谁校验
工具调用的权限边界只在一个地方算数:服务端分发器入口。前端的工具开关、按钮禁用只能算交互提示,绕过前端直接发请求一样能触发工具,所以不能把前端当校验点。发起方可以是模型、可以是前端按钮,但校验必须由服务端完成。
白名单配置建议按工具名组织,字段保持最少:
{
"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,和权限拒绝分开。
按决策改一处代码并跑通一次完整问答
两项决策定完后,只改一处代码先验证,不要一次把会话迁移和工具校验都铺开。
改动点控制在两处,每处一个文件:
- 会话写入位置按第二节的选择改一处:例如由后端在首次响应时生成并下发 sessionId,前端只保存该值,不再自己生成。
- 在工具分发器入口加一次校验调用,白名单从配置文件读取。
跑通标准是一次完整问答链路能观察到这些:发送问题后响应里带回 sessionId,前端后续请求沿用同一个 sessionId,工具调用日志里出现一条校验通过的记录,刷新页面后的会话行为符合第二节选定的预期。日志里能同时看到校验通过和被拒两类记录,说明校验点只在一处生效。
失败时按下面顺序回退,一次只回一步:
- 先回退工具校验入口,改成只记录不拦截,确认问答主链路恢复。
- 再回退会话写入位置的改动,恢复原来的 sessionId 生成方式。
- 最后回退前端对 sessionId 的读取逻辑。
回退按单文件维度提交,不要和已经跑通的部分混在同一个提交里,否则回退时会把有效的改动一起撤掉。