案例 #1772:移除 Viewer 误拦 Agent 对话
PR:#1772 | 4 文件 +66/-109 | 2026-06-23 合入 | lyingbug
结论先行
CustomAgent.RunnableByViewer 在 类型定义、DB 列、Go SDK 中存在,但 运行时从未完整实现;更糟的是 session/qa.go 的 Agent QA gate 仍检查该字段 — builtin agent 内存中未填充该字段,导致 viewer 调用 /api/v1/agent-chat 恒 403,尽管 DB 默认 runnable_by_viewer=true。
修复:删除死字段 + 移除 gate(净删 43 行)— 当目标是 unblock 用户时,删错误逻辑优于补 half-baked RBAC。
适合谁读:改 Agent QA 入口、租户 RBAC、Go handler 权限的贡献者。
链 Ch45 Agent/Token · Ch12 Agentic RAG · 对比 case-1774(删前引用检查)。
1. 问题现象(What)
复现(PR Test plan)
- 以租户 Viewer 角色登录
POST /api/v1/agent-chat/{session_id},agent_id=builtin-smart-reasoning- 修复前:403 Forbidden
- 修复后:200,流式返回正常
产品预期 vs 实际
| 角色 | 预期 | 修复前 |
|---|---|---|
| Viewer | 只读使用已发布 agent | 被 RunnableByViewer gate 拦截 |
| Contributor/Owner | 正常对话 | 正常 |
| knowledge-chat | viewer 可用 | 未改(本 PR 不动) |
1.3 修复前 / 修复后
| 角色 | 修复前 | 修复后 |
|---|---|---|
| Viewer + builtin agent | 403(RunnableByViewer zero value) | 200 流式 |
| Contributor/Owner | 200 | 200(不变) |
| knowledge-chat viewer | 未改 PR scope | 未改 |
2. 定位步骤
rg "RunnableByViewer|agent-chat|AgentQA" internal/handler/session/
rg "RunnableByViewer" internal/types client/
阅读顺序:qa.go agent-chat 分支 → custom_agent.go → client/agent_manage.go
3. 根因分析(Why)
半实现 flag 的危险
flowchart LR
A[DB 列 runnable_by_viewer<br/>default true] --> B[CustomAgent struct 有字段]
B --> C[Go SDK JSON 暴露]
C --> D[qa.go gate 检查字段]
D --> E{builtin agent 内存对象}
E -->|字段未赋值| F[Go zero value = false]
F --> G[403 ❌]
style G fill:#f96
三层不一致:
- DB 默认 true — 运维/前端以为 viewer 可跑
- builtin agent 构造时不 populate — 内存为
false - handler 仍信任内存字段 — 误拦
这是典型的 「有 schema 无行为」 — 比完全没有字段更误导排查。
关键文件
| 文件 | 改动 |
|---|---|
internal/types/custom_agent.go |
移除 RunnableByViewer |
client/agent_manage.go |
SDK 类型同步 |
internal/handler/session/qa.go |
删除 AgentQA RBAC gate |
logger.go |
minor 清理 |
PR Summary 强调:built-in agents never populated this field in memory — 这是 403 的直接触发条件。
PR 四段式
| What | Why | How | Test |
|---|---|---|---|
| Viewer 403 on agent-chat | dead flag + zero value | 删字段与 gate | Viewer POST builtin agent → 200 |
4. 解决方案(How)
为何「删」而不是「补全 RBAC」?
| 方案 | 成本 | 风险 |
|---|---|---|
| 补全:load DB → populate → 前端配置 | 高(全链路 RBAC) | 新 bug 面 |
| 删除死字段 + gate | 低(-109 行) | viewer 与 contributor 在 agent-chat 暂同等 |
Issue 目标是 unblock viewer — 维护者选择 删错误逻辑。完整 viewer 只读 RBAC(含 #1248 feedback 写权限)应 另开设计,不在 dead flag 上补丁。
定位 QA 入口
rg "agent-chat|AgentQA|RunnableByViewer" internal/handler/session/
rg "RunnableByViewer" internal/types client/
读 qa.go 中 agent-chat 与 knowledge-chat 分支 — 本 PR 只动前者。
5. 与 #1248 / 竞赛
| 关联 | 说明 |
|---|---|
| 谁能写 feedback | #1248 需区分 viewer(只读?)vs editor — 不能复制本 PR「删 gate」模式,需 正面设计 |
| dead flag 排查 | grep RunnableByViewer 类字段 — 配置存在但行为 absent |
| handler 读路径 | session/qa.go 是 QA 入口地图 — 与 case-1774 handler 层同级 |
6. 可复用模式
- 半实现比没有更糟 — schema/SDK/handler 三层要 同时 有行为或同时不存在
- builtin 与 custom 对象构造路径不同 — 集成测要覆盖 builtin-smart-reasoning
- 净删行 PR 可高价值 — 降低认知负担
7. 常见误区
错误认知:DB default true 就能保证运行时 true。
正确理解:handler 读的是 内存 struct — 构造链必须赋值。
错误认知:403 一定是 JWT 角色问题。
正确理解:先看 agent 级 gate,再查 tenant RBAC。
错误认知:删 RBAC gate 等于「不做权限」。
正确理解:本 PR 是 移除错误 gate;正确 RBAC 应基于 资源 + 动作 表设计,而非单个未维护 bool。
错误认知:builtin agent 与 custom agent 走同一构造路径,字段一定一致。
正确理解:builtin 在内存中 不 populate RunnableByViewer,Go zero value 为 false — 集成测必须覆盖 builtin-smart-reasoning,不能只看 DB 默认。
8. 思考点
- 若要做「viewer 可读 agent 但不可改配置」,应在 哪一层 校验?(路由 / middleware / service)
- #1248 点赞是否算 写操作?viewer 该不该允许?设计 API 返回码。
- 写一条 回归测试:viewer + builtin agent → 200;如何 mock session?
- 对比 case-1774「删模型前查引用」— 何时该 加检查,何时该 删检查?
章末自检
- 解释 dead flag 为何导致「DB 与行为不一致」
- 写出定位 qa.go 的 rg 命令
- 说明本 PR 未改 knowledge-chat 的原因(scope)