diff --git a/.doc/archive/ops/2026-09-13-chat-history-refresh-diagnosis.md b/.doc/archive/ops/2026-09-13-chat-history-refresh-diagnosis.md new file mode 100644 index 00000000..b9b1eead --- /dev/null +++ b/.doc/archive/ops/2026-09-13-chat-history-refresh-diagnosis.md @@ -0,0 +1,54 @@ +--- +title: "刷新后对话正文消失问题定位与整改方案" +status: done +created: 2026-09-13 +updated: 2026-09-13 +owner: codex +branch: dev +worktree: "-" +mutex: chat +--- + +# 刷新后对话正文消失问题定位与整改方案 + +## 目标 + +定位页面刷新后对话正文不显示、但锚点与过滤计数仍存在的根因,并形成可执行、可验证的整改方案。 + +## 范围 + +- 前端对话状态、会话恢复、消息窗口、锚点与类别过滤的数据来源 +- 后端会话/消息持久化与历史读取 API +- 当前 dev 运行态的只读接口与数据一致性验证 +- 审计记录、整改方案与验收标准 + +## 非目标 + +- 本轮不修改业务代码 +- 不执行破坏性数据修复 +- 不把 stub 或仅内存态结果作为最终验收 + +## 步骤 + +1. [x] 复核刷新前后前端状态初始化和会话选择链路 +2. [x] 核对历史消息 API、存储字段及运行态返回 +3. [x] 解释正文、锚点和过滤计数数据源为何分叉 +4. [x] 输出 audit 与 remediation 文档并回链 + +## 验收标准 + +- [x] 根因有源码、接口或运行态证据支撑 +- [x] 明确影响范围、触发条件与风险等级 +- [x] 整改方案覆盖实现、回归测试、数据兼容和验收 + +## 相关链接 + +- Checklist: 待整改实施时建立 +- Handoff: [chat-history-refresh-diagnosis](../../ops/handoffs/2026-09-13-chat-history-refresh-diagnosis.md) +- Test report: 待整改实施并验收后建立 + +## 证据 / 备注 + +- 用户实测:新对话能正常生成文档;刷新后历史对话正文消失,但右侧锚点与顶部过滤仍显示对应消息数量。 +- Audit: [../../audit/2026-09-13-chat-history-refresh-disappears.md](../../audit/2026-09-13-chat-history-refresh-disappears.md) +- Remediation: [../../remediation/2026-09-13-chat-history-refresh-remediation.md](../../remediation/2026-09-13-chat-history-refresh-remediation.md) diff --git a/.doc/audit/2026-09-13-chat-history-refresh-disappears.md b/.doc/audit/2026-09-13-chat-history-refresh-disappears.md new file mode 100644 index 00000000..daf2a4d8 --- /dev/null +++ b/.doc/audit/2026-09-13-chat-history-refresh-disappears.md @@ -0,0 +1,99 @@ +--- +title: "刷新后对话正文消失但统计仍存在" +status: done +created: 2026-09-13 +updated: 2026-09-13 +severity: high +--- + +# 刷新后对话正文消失但统计仍存在 + +## 现象 + +用户在项目会话中可以正常与助手对话并生成文档;页面刷新后,消息正文区域显示为空,但顶部分类过滤和右侧分类锚点仍显示消息数量及首尾消息 ID。 + +结论:**消息没有从数据库删除,而是在刷新后的服务端历史恢复链路中被阶段权限过滤器全部剔除。统计接口没有应用同一权限范围,因此仍返回未过滤的数量和锚点。** + +## 根因分析 + +### 1. 刷新前与刷新后使用不同消息来源 + +- 刷新前,`web/src/App.tsx:1634-1656` 中的组件内 `messages` 保存本轮实时消息,`useChatWindow` 将它们合并到画面。 +- 刷新后,组件内 `messages` 从空数组重新开始,正文依赖 `GET /api/v1/chat/messages/window` 恢复。 +- `web/src/hooks/useChatWindow.ts:112-120` 只要服务端返回非空 `days`,就用返回桶覆盖窗口和本地缓存;即使桶中的 `messages` 已被过滤为空也照常覆盖。 + +因此,实时画面正常不代表历史恢复链路正常。 + +### 2. 文档任务消息没有绑定工作流阶段 + +- 普通聊天路径会在 `internal/chat/session.go:267-274,425-438` 把 `ActiveStageKey` 写入用户与助手消息的 `metadata.stage_key`。 +- 文档生成使用的异步任务路径在 `internal/server/task_run_handlers.go:81-96` 只写入 `project_id`、`client_turn_id`、`task_key`、`task_status`,没有写 `stage_key`、`workflow_instance_id` 或 `stage_instance_id`。 +- 任务完成投影 `internal/server/task_finalize.go:58-85` 仍未补这些作用域字段。 + +这使得文档任务在当前页面可见,但重载后无法通过阶段可见性判定。 + +### 3. 工作流激活前的历史也会被追溯性隐藏 + +- `internal/server/stage_access_helpers.go:135-163` 在项目进入受限工作流后,明确丢弃所有 `metadata.stage_key` 为空的历史消息。 +- `internal/projectinstance/stage_access.go:15-77` 根据当前 live 实例计算可见阶段;策略一旦进入 `Restricted=true`,激活前产生、天然没有阶段键的消息也无法恢复。 +- `StageKeyAllowed("")` 本身在 `internal/projectinstance/stage_access.go:103-108` 返回允许,但外层过滤器在调用它之前已经对空阶段执行 `continue`;现有单测只验证前者,没有覆盖组合行为。 + +这不是合理的“拒绝越权”,而是缺少对 `pre_activation/legacy` 对话作用域的正式建模。 + +### 4. 统计与正文的授权口径分裂 + +- 窗口和按日正文在 `internal/server/chat_window_api.go:48-50,104-106` 应用阶段过滤。 +- 分类与日期统计在同文件 `111-155` 直接查询并返回原始统计,没有会话所有权校验,也没有阶段过滤。 +- `internal/ctxmgr/chat_window.go:622-749` 的 SQL 对会话内全部未删除消息聚合,返回数量及 `MIN/MAX(id)`。 +- 前端 `CategoryFilterBar` 使用这些服务端统计显示数量;`CategoryDock` 还以统计中的首尾 ID 作为锚点兜底。因此正文为空时,数量与锚点仍存在。 + +此外,所有这些端点虽由全局中间件要求登录,但窗口、日期、分类等端点没有统一调用 `assertSessionOwnedByUser`。这会造成私聊会话对象级授权不一致,并可能泄露其他会话的消息存在性、分类数量和消息 ID。 + +## 运行态证据(dev,2026-09-13) + +只读查询 `shuimu-context-pg-dev/agent_system_dev`: + +| 项目/会话 | 结果 | +|---|---| +| project `143` | 创建者 user `9`,live instance `89`,active stage `V0.1_triage` | +| session `sess-p143-u9` | 共 4 条未删除消息,4 条正文长度均大于 0 | +| 激活前消息 `431-432` | 创建于 instance `89` 之前,无 `stage_key` | +| 文档任务消息 `433-434` | 创建于激活之后,`task_key=intake`,仍无 `stage_key` | +| 过滤结果 | 4 条均命中“空 stage_key → 丢弃”,窗口日桶保留但消息数组为空 | +| 分类统计 | 仍按全量 4 条聚合,可返回数量及首尾 ID | + +全库当前有 live 工作流实例的会话消息统计也显示:所有命中的消息均缺少 `stage_key`。这说明不是单条偶发数据,而是写入链路缺字段与历史兼容策略共同造成的系统性问题。 + +## 严重度 + +`high`,并应作为发布 blocker: + +- 核心对话记录在刷新/重新登录后不可用,用户会误认为数据丢失; +- 文档任务的对话留痕无法恢复,破坏可审计性; +- 正文、搜索、日期、统计、锚点的授权集合不一致; +- 分类数量和消息 ID 可在对象级授权不完整时形成侧信道泄露。 + +## 关联整改 + +- Remediation: [../remediation/2026-09-13-chat-history-refresh-remediation.md](../remediation/2026-09-13-chat-history-refresh-remediation.md) +- Short-term: [../ops/short-term/2026-09-13-chat-history-refresh-remediation.md](../ops/short-term/2026-09-13-chat-history-refresh-remediation.md) +- Gap matrix: [../remediation/gap-matrix.md](../remediation/gap-matrix.md) + +## 证据 + +- `web/src/App.tsx:1634-1656,3393-3506` +- `web/src/hooks/useChatWindow.ts:81-170,446-578` +- `web/src/api/chatWindow.ts:73-164` +- `internal/chat/session.go:267-280,425-468` +- `internal/server/task_run_handlers.go:76-108` +- `internal/server/task_finalize.go:58-96` +- `internal/server/stage_access_helpers.go:135-212` +- `internal/server/chat_window_api.go:12-155` +- `internal/ctxmgr/chat_window.go:39-174,622-749` +- dev PostgreSQL 只读查询(未读取或落盘消息正文) +- 相关测试通过但缺场景:`go test -short ./internal/server -run 'TestServer_ChatWindow' -count=1 -v`、`go test -short ./internal/ctxmgr -run 'TestChatWindow' -count=1 -v`、`cd web && npm test` + +## 开放问题 + +- 激活前项目私聊应默认仅对会话所有者可见,还是按项目成员和退出留痕策略共享,需要在实现前固化为策略测试;本方案建议保持现有“每人独立私聊”语义,优先按会话所有者授权。 +- 历史上若同一 `task_key` 在多个工作流实例/阶段重复出现,不能仅按当前模板名称盲目回填,必须结合运行实例、创建时间和不可变实例 ID;无法唯一归属的数据应标记为 `legacy_unscoped` 并走显式兼容策略。 diff --git a/.doc/audit/INDEX.md b/.doc/audit/INDEX.md index 103cf6cf..25b1b06e 100644 --- a/.doc/audit/INDEX.md +++ b/.doc/audit/INDEX.md @@ -13,6 +13,14 @@ ## 核心文档(按阅读顺序推荐) +### 最新专项审计(2026-09-13) + +**文件**: [2026-09-13-chat-history-refresh-disappears.md](./2026-09-13-chat-history-refresh-disappears.md) + +**结论**: 消息仍在数据库;文档任务与激活前消息缺阶段作用域,被刷新后的窗口过滤器剔除,而统计仍返回未过滤数量。 + +**整改**: [对话刷新恢复与阶段授权一致性整改方案](../remediation/2026-09-13-chat-history-refresh-remediation.md) + ### 0️⃣ 当前状态(Live SSOT) **文件**: [gap-matrix.md](../remediation/gap-matrix.md) diff --git a/.doc/ops/handoffs/2026-09-13-chat-history-refresh-diagnosis.md b/.doc/ops/handoffs/2026-09-13-chat-history-refresh-diagnosis.md new file mode 100644 index 00000000..60ab4fe3 --- /dev/null +++ b/.doc/ops/handoffs/2026-09-13-chat-history-refresh-diagnosis.md @@ -0,0 +1,70 @@ +--- +title: "对话刷新后正文消失整改实施交接" +created: 2026-09-13 +from: codex +to: next-agent +status: open +--- + +# 交接:对话刷新后正文消失整改实施 + +## 目标(下一位 Agent 应完成什么) + +按整改方案修复消息作用域、历史读取授权和统计同集问题,并完成历史回填与真实刷新 E2E。 + +## 已完成 + +- 源码链路与 dev PostgreSQL 只读定位完成,确认消息未删除。 +- 确认 project 143/session `sess-p143-u9` 的 4 条消息均缺 `stage_key`,工作流激活后被窗口过滤器全部移除。 +- 确认异步文档任务写入路径缺阶段/实例作用域;分类与日期统计未应用同一过滤。 +- 审计、整改方案、发布 blocker 与 planned short-term 已建立。 +- 现有相关 Go 测试和前端 192 项测试通过,但均未覆盖真实缺陷场景。 + +## 未完成 + +- 未修改业务代码、迁移或运行态数据。 +- 未建立实施 checklist/test-report。 +- 未运行真实 PI 刷新回归和双用户对象级授权负例。 + +## 认领 / 槽位状态 + +| 项 | 值 | +|----|-----| +| mutex 已释放 | yes | +| 集成槽位 holder | idle | +| branch | dev(诊断);建议实施用 `fix/chat-history-refresh` | +| worktree | 建议 `.worktrees/fix-chat-history-refresh` | + +## 关键文件 + +| 路径 | 说明 | +|------|------| +| `.doc/audit/2026-09-13-chat-history-refresh-disappears.md` | 根因与运行态证据 | +| `.doc/remediation/2026-09-13-chat-history-refresh-remediation.md` | 分波次整改与验收 | +| `internal/server/task_run_handlers.go` | 文档任务消息缺阶段作用域 | +| `internal/server/stage_access_helpers.go` | 空阶段消息被丢弃 | +| `internal/server/chat_window_api.go` | 正文与统计授权口径分裂 | +| `web/src/hooks/useChatWindow.ts` | 刷新后服务端空桶覆盖本地窗口 | + +## 如何验证 + +```bash +go test -short ./internal/server -run 'TestServer_ChatWindow' -count=1 -v +go test -short ./internal/ctxmgr -run 'TestChatWindow' -count=1 -v +cd web && npm test +``` + +实施时必须另加真实 PG 场景:激活前对话 → 激活 → `intake` 文档任务 → 刷新窗口,以及非 owner/无阶段职责负例。 + +## 风险 / Blocker + +- 当前为发布 blocker;刷新会造成对话“数据丢失”假象。 +- 不能用“空 `stage_key` 全部放行”或保留旧 localStorage 缓存作修复,否则会扩大阶段/跨用户泄露。 +- 历史回填存在重复 `task_key` 歧义,必须先 dry-run 并按实例与时间唯一匹配。 + +## 相关链接 + +- CURRENT: [CURRENT.md](../CURRENT.md) +- Task: [chat-history-refresh-remediation](../short-term/2026-09-13-chat-history-refresh-remediation.md) +- Audit: [chat-history-refresh-disappears](../../audit/2026-09-13-chat-history-refresh-disappears.md) +- Remediation: [chat-history-refresh-remediation](../../remediation/2026-09-13-chat-history-refresh-remediation.md) diff --git a/.doc/ops/short-term/2026-09-13-chat-history-refresh-remediation.md b/.doc/ops/short-term/2026-09-13-chat-history-refresh-remediation.md new file mode 100644 index 00000000..c1705f15 --- /dev/null +++ b/.doc/ops/short-term/2026-09-13-chat-history-refresh-remediation.md @@ -0,0 +1,57 @@ +--- +title: "修复对话刷新恢复与授权统计不一致" +status: planned +created: 2026-09-13 +updated: 2026-09-13 +owner: unassigned +branch: fix/chat-history-refresh +worktree: ".worktrees/fix-chat-history-refresh" +mutex: chat +--- + +# 修复对话刷新恢复与授权统计不一致 + +## 目标 + +按整改方案修复消息作用域写入、历史兼容、读取授权和前端恢复态,并完成真实 PostgreSQL 与 PI 文档生成后的刷新验收。 + +## 范围 + +- 消息和 task run 的不可变工作流实例/阶段作用域 +- 会话 owner 与阶段可见性的统一服务端策略 +- 窗口、日期、分类、搜索、摘要、未读、导出、锚点的同集查询 +- 现有消息 dry-run/backfill +- 前端窗口错误态和用户隔离缓存 +- 自动化与真实 PI E2E 证据 + +## 非目标 + +- 不放宽现有阶段可见性规则 +- 不用前端缓存替代服务端持久化 +- 不对无法唯一归属的历史消息盲目回填当前阶段 + +## 步骤 + +1. [ ] 先补可稳定复现本缺陷的 PG 集成与前端恢复测试 +2. [ ] 实现统一消息作用域解析与写入 +3. [ ] 实现统一会话/消息授权查询及统计同集 +4. [ ] 实现历史 dry-run/backfill 与对账 +5. [ ] 完善前端恢复状态及用户隔离缓存 +6. [ ] 执行全量门禁、真实 PI 刷新 E2E 和双用户负例 + +## 验收标准 + +- [ ] [整改方案](../../remediation/2026-09-13-chat-history-refresh-remediation.md) 的验收项全部有证据 +- [ ] `CHAT-HISTORY-REFRESH` 在 gap-matrix 中由 FAIL 更新为 PASS +- [ ] 测试报告归档并回链 + +## 相关链接 + +- Checklist: 实施开始时复制模板建立 +- Handoff: [../handoffs/2026-09-13-chat-history-refresh-diagnosis.md](../handoffs/2026-09-13-chat-history-refresh-diagnosis.md) +- Test report: 待实施 + +## 证据 / 备注 + +- Audit: [../../audit/2026-09-13-chat-history-refresh-disappears.md](../../audit/2026-09-13-chat-history-refresh-disappears.md) +- Remediation: [../../remediation/2026-09-13-chat-history-refresh-remediation.md](../../remediation/2026-09-13-chat-history-refresh-remediation.md) diff --git a/.doc/remediation/2026-09-13-chat-history-refresh-remediation.md b/.doc/remediation/2026-09-13-chat-history-refresh-remediation.md new file mode 100644 index 00000000..9dcfebd8 --- /dev/null +++ b/.doc/remediation/2026-09-13-chat-history-refresh-remediation.md @@ -0,0 +1,65 @@ +--- +title: "对话刷新恢复与阶段授权一致性整改方案" +status: planned +created: 2026-09-13 +updated: 2026-09-13 +--- + +# 对话刷新恢复与阶段授权一致性整改方案 + +## 背景 + +审计确认消息正文仍在 PostgreSQL,但文档任务消息和激活前历史缺少工作流阶段作用域;刷新后的窗口接口将其全部过滤,而分类/日期统计仍返回全量结果。详见 [审计记录](../audit/2026-09-13-chat-history-refresh-disappears.md)。 + +## 目标 + +1. 同一登录用户在权限未变化时,刷新、重新登录和服务重启后看到一致的对话正文、数量、日期、搜索和锚点。 +2. 每条新消息在服务端落库时拥有可审计的会话、项目及工作流作用域;客户端不得决定该作用域。 +3. 所有历史读取入口使用同一对象级与阶段级授权规则,统计集合必须与正文集合一致。 +4. 安全回填现有可唯一归属的消息,不丢失正文,不扩大不可确定历史的可见范围。 + +## 分波次计划 + +| 波次 | 内容 | 验收 | +|------|------|------| +| Wave 0:锁定回归 | 增加失败测试:激活前聊天 → 激活工作流 → 运行 `intake` 文档任务 → 刷新拉窗口;同时覆盖非 owner、阶段无职责用户、空阶段消息和统计一致性 | 修复前测试稳定复现“窗口 0 条、分类 4 条”;记录真实 PG 证据 | +| Wave 1:统一服务端作用域 | 在任务创建事务中由服务端从冻结实例解析 `task_key → workflow_instance_id → stage_instance_id/stage_key`;把不可变作用域写入 `task_runs` 和该回合两条 `chat_messages`;普通 chat 同样写实例 ID;finalize 只合并结果,不覆盖作用域 | 新建普通聊天和任务消息均具备正确实例/阶段;伪造客户端 stage 无效;找不到唯一阶段时拒绝启动并给出结构化错误 | +| Wave 2:修正授权查询 | 建立统一的 `AuthorizeSessionRead + visible-message predicate`,窗口、day、dates、categories、search、digest、unread、export、锚点加载全部复用;私聊先校验 owner,再计算阶段范围;统计在 SQL 层对相同可见集合聚合 | 正文、数量、首尾 ID、搜索/导出完全同集;他人 session 返回 403/404;不可见 ID 不能作锚点探测 | +| Wave 3:历史迁移 | 新增 Goose 迁移或一次性受控 backfill:任务消息通过 `task_key + task_run/workflow_run + 时间区间` 回填实例/阶段;激活前 owner 私聊标记 `scope_kind=pre_activation`;歧义数据标记 `legacy_unscoped` 并记录数量,不猜测归属 | dry-run 报告、事务执行、前后计数与 hash 对账;project 143 的 4 条消息可按策略恢复;零正文删除 | +| Wave 4:前端恢复态 | `useChatWindow` 区分 loading / authorized-empty / forbidden / error;不把“带空消息的日桶”静默呈现为空白;缓存键绑定 user/session/authorization revision,权限变化时清除,不用旧缓存绕过服务端 | 刷新不闪现上一用户/无权缓存;错误有可操作提示;服务端正常时首屏稳定落底 | +| Wave 5:全链验收 | 在空库和升级库执行 PG 集成;完成真实 PI 文档生成、浏览器刷新、重登和服务重启恢复;执行双用户/跨阶段负例 | 所有自动化门禁通过并归档 test-report;人工抽检截图/网络响应入证据 | + +## 推荐数据模型与边界 + +- `chat_messages` 增加正式列或受约束字段:`workflow_instance_id`、`stage_instance_id`、`scope_kind`;`stage_key` 可作为展示/索引冗余,不应成为唯一授权依据。 +- `task_runs` 同样保存 `workflow_instance_id`、`stage_instance_id`,确保任务运行期间阶段推进也不会改变该回合归属。 +- `scope_kind` 至少区分 `workflow_stage`、`pre_activation`、`legacy_unscoped`、`non_project_private`。 +- 消息作用域只能由服务端从会话、项目与冻结工作流实例解析;忽略客户端提交的 user/stage/instance 身份字段。 +- 不建议用“空 `stage_key` 一律允许”快速修复,这会绕过阶段隔离;也不建议仅在前端保留本地缓存,这会掩盖服务端错误并产生共享浏览器泄露风险。 + +## 建议修改范围 + +- 后端写入:`internal/server/task_run_handlers.go`、`internal/server/task_finalize.go`、`internal/chat/session.go`、`internal/taskrun/*` +- 后端读取与授权:`internal/server/chat_window_api.go`、`internal/server/chat_search_api.go`、chat digest/unread/export handlers、`internal/server/stage_access_helpers.go`、`internal/ctxmgr/chat_window.go` +- 工作流解析:`internal/projectinstance/*`,增加按冻结实例解析消息作用域的单一入口 +- 前端:`web/src/hooks/useChatWindow.ts`、`web/src/api/chatWindow.ts`、`web/src/components/chat/MessageStream.tsx` +- 数据库:`deploy/postgres/goose/00026_*`(实际序号以实施时仓库为准)及对应 dry-run/rollback 说明 + +## 验收标准 + +- [ ] 创建项目会话并发送普通消息,刷新后用户/助手两条正文、顺序、类别和锚点不变。 +- [ ] 先聊天再激活工作流,刷新后激活前 owner 私聊仍按既定策略可见。 +- [ ] 激活后通过后台任务生成文档,任务用户消息和最终助手消息刷新后完整恢复,artifact 仍可打开。 +- [ ] 工作流推进后,已参与阶段历史按策略可见,未参与阶段消息不可见。 +- [ ] `window/day/dates/categories/search/digest/unread/export` 对同一用户返回同一可见集合;统计总数等于可枚举正文数。 +- [ ] 非会话 owner 即使猜到 `session_id/message_id`,所有读取与锚点入口均返回 403/404,且不泄露计数。 +- [ ] 浏览器刷新、重新登录、切换用户、服务重启均不依赖未授权 localStorage 缓存恢复正文。 +- [ ] 历史迁移 dry-run 与执行报告包含总数、可唯一归属数、歧义数、前后校验;正文删除数为 0。 +- [ ] `go vet ./...`、`go test -short ./...`、PG 集成、`cd web && npm test && npm run build` 全绿。 +- [ ] 真实 PI Agent 文档生成后刷新恢复 E2E 有控制台/网络/数据库证据并归档 `.doc/test-reports/`。 + +## 相关链接 + +- Audit: [../audit/2026-09-13-chat-history-refresh-disappears.md](../audit/2026-09-13-chat-history-refresh-disappears.md) +- Short-term: [../ops/short-term/2026-09-13-chat-history-refresh-remediation.md](../ops/short-term/2026-09-13-chat-history-refresh-remediation.md) +- gap-matrix 行: `CHAT-HISTORY-REFRESH` diff --git a/.doc/remediation/INDEX.md b/.doc/remediation/INDEX.md index 8de77927..c6b78eeb 100644 --- a/.doc/remediation/INDEX.md +++ b/.doc/remediation/INDEX.md @@ -3,6 +3,7 @@ | 文档 | 说明 | |------|------| | [gap-matrix.md](./gap-matrix.md) | 发布缺口 SSOT | +| [2026-09-13-chat-history-refresh-remediation.md](./2026-09-13-chat-history-refresh-remediation.md) | 对话刷新恢复、消息作用域与授权统计一致性 | | [2026-09-12-comprehensive-audit-and-remediation-plan.md](./2026-09-12-comprehensive-audit-and-remediation-plan.md) | 综合缺陷与整改 | | [2026-09-12-remediation-tasks-and-acceptance-checklist.md](./2026-09-12-remediation-tasks-and-acceptance-checklist.md) | 任务拆分与验收 | | [2026-09-12-quick-reference-guide.md](./2026-09-12-quick-reference-guide.md) | 症状→原因→方案速查 | diff --git a/.doc/remediation/gap-matrix.md b/.doc/remediation/gap-matrix.md index 14d67063..99d8ac44 100644 --- a/.doc/remediation/gap-matrix.md +++ b/.doc/remediation/gap-matrix.md @@ -2,7 +2,7 @@ **更新**: 2026-09-13 **分支**: dev -**production_ready**: no(CI 真实 PI E2E / 前端人工抽检未完成;main 本地冒烟已通过) +**production_ready**: no(对话刷新恢复缺陷、CI 真实 PI E2E / 前端人工抽检未完成;main 本地冒烟已通过) **协作焦点**: [../ops/CURRENT.md](../ops/CURRENT.md) **Agent 手册**: [AGENTS.md](../AGENTS.md) · [PROJECT.md](../PROJECT.md) @@ -53,6 +53,12 @@ | DOC-SSOT | 文档叙事漂移 | PASS | 本矩阵 + superseded 标记 + ops | | OPS-AGENTS | 多 Agent 协作规范 | PASS | `AGENTS.md` / `PROJECT.md` / `.doc/ops` | +## 2026-09-13 现场问题 + +| ID | 描述 | 状态 | 证据 | 计划动作 | +|---|---|---|---|---| +| CHAT-HISTORY-REFRESH | 工作流激活后刷新,对话正文被阶段过滤器清空,但分类数量与锚点仍按全量返回 | **FAIL** | [audit](../audit/2026-09-13-chat-history-refresh-disappears.md):project 143/session `sess-p143-u9` 的 4 条正文仍在 DB、全部缺 `stage_key` | [remediation](./2026-09-13-chat-history-refresh-remediation.md):统一作用域写入、对象/阶段授权、同集统计、历史回填和真实 PI 刷新 E2E | + --- ## 发布门禁(test → main) @@ -62,10 +68,11 @@ ☑ go test -short ./... 全绿 □ TEST_DATABASE_URL 下治理 PG 集成全绿(CI) □ 真实 PI Agent 三工作流治理 E2E 实录(本轮 stub/short 已绿,PI 待 CI token) +□ 对话刷新恢复:激活前聊天 + 激活后文档任务 + 刷新/重登/重启可恢复,双用户负例通过 ☑ lifecycle fix SQL 已在 dev 执行 ☑ test/dev health schema_version=25 ☑ main 环境部署冒烟(本地 8000 health/ready schema 25) □ 前端:版本冲突刷新、终止 preview 人工抽检 ``` -**结论**: P0 授权、冷启动序列、Go 门禁、三环境本地冒烟已闭环。**发布 blocker 剩余 CI 真实 PI E2E 与前端人工抽检**。 +**结论**: P0 授权、冷启动序列、Go 门禁、三环境本地冒烟已闭环;新增现场缺陷 `CHAT-HISTORY-REFRESH`。**发布 blocker 包括对话刷新恢复、CI 真实 PI E2E 与前端人工抽检**。 diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index 6222f84f..053d0ab0 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -16,10 +16,10 @@ | 模块 | 能力 | 状态 | 证据 | 缺口/后续 | mutex | |------|------|------|------|-----------|-------| | 意图识别 L1 | 18 类 Intent + 三分支消解 | done | [system-design/intent-recognition](../system-design/intent-recognition/);[test-report 2026-09-07](../test-reports/2026-09-07-three-branch-intent-framework-test-report.md) | — | `intent` | -| 对话 / ctxmgr | 多轮上下文、记忆、摘要 | partial | [system-design/multi-turn-dialogue](../system-design/multi-turn-dialogue/);P0 断链已修 | 语义摘要(D5 backlog);长任务治理 | `chat` | +| 对话 / ctxmgr | 多轮上下文、记忆、摘要 | partial | [system-design/multi-turn-dialogue](../system-design/multi-turn-dialogue/);[刷新恢复专项审计](../audit/2026-09-13-chat-history-refresh-disappears.md) | `CHAT-HISTORY-REFRESH`:作用域写入、同集授权统计、历史回填;语义摘要(D5 backlog) | `chat` | | 智能体小队 | 角色-任务-工作区-黑板 | done | [system-design/agent-squad](../system-design/agent-squad/);[test 2026-09-07](../test-reports/2026-09-07-dynamic-squad-planning-test-report.md) | 非关键步骤降级(A5/A6 backlog) | `squad` | | 工作流治理 | 三正式工作流 + 阶段门禁 | done | [remediation/three-workflows](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[test 2026-09-12](../test-reports/2026-09-12-three-workflows-governance-remediation-test-report.md) | Phase2 设计器 partial | `workflow` | -| IAM / 鉴权 | 项目级 API 鉴权 + RBAC | done | [audit/authorization](../audit/authorization-matrix-v2.md);[test phase1](../test-reports/2026-09-12-phase1-iam-authz-test-report.md) | 旧 API 410 标注(BUG-11 partial) | `authz` | +| IAM / 鉴权 | 项目级 API 鉴权 + RBAC | partial | [audit/authorization](../audit/authorization-matrix-v2.md);[刷新恢复专项审计](../audit/2026-09-13-chat-history-refresh-disappears.md) | 历史读取端点补统一会话 owner + 阶段授权;旧 API 410 标注(BUG-11 partial) | `authz` | | 文档治理 | 法定文档生命周期 + 租约 | done | [test docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md) | — | `docgov` | | 前端工作台 | 三栏 + 块文档 + SSE | partial | [test workbench](../test-reports/2026-09-08-three-column-workbench-and-block-document-test-report.md) | 人工抽检待完成 | `web` | | PI Runner / 配置链 | pi-agent + MiniMax 真实链路 | done | [test piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md) | — | `pi-agent` | diff --git a/.dockerignore b/.dockerignore index a0cf2f22..37225ead 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,8 @@ .doc docs data +.regulations +.tmp web/node_modules web/dist web/.vite diff --git a/.env.example b/.env.example index 43d0a573..d2d9a40d 100644 --- a/.env.example +++ b/.env.example @@ -3,14 +3,12 @@ # # 使用说明: # 1. 复制为 .env 并填写密钥:cp .env.example .env && chmod 600 .env -# 2. 本文件供 Go 后端 (bin/server)、launch.sh 默认 compose 变量替换使用 -# 3. 若通过 ./scripts/env-service.sh dev start 启容器,还需同步密钥到 configs/.env.dev -# (见 configs/.env.dev.example 第 6 节) +# 2. 本文件供 Go 后端、launch.sh 与 env-service.sh 作为公共配置源 +# 3. configs/.env.<环境> 只保存端口、数据库等环境差异,且优先级高于本文件 # ============================================================================= # --- 1. 运行环境与基础目录 --- ENV=dev -COMPOSE_PROJECT_NAME=agent-dev DATA_DIR=./data/dev APP_PUBLISH_HOST=127.0.0.1 HTTP_PORT=8080 @@ -42,7 +40,7 @@ PI_AGENT_RUNNER_PORT=18865 # 宿主 bin/server 直连本机 pi-agent 容器时启用;容器内 app 服务由 compose 注入 http://pi-agent:18865 # PI_AGENT_RUNNER_URL=http://127.0.0.1:18865 # Runner 服务间认证(main 必填;dev 可留空) -# RUNNER_AUTH_TOKEN= +RUNNER_AUTH_TOKEN= # --- 7. 本机 Pi CLI 回退(不使用容器 runner 时)--- PI_AGENT_BIN=/opt/homebrew/bin/pi @@ -54,17 +52,15 @@ PI_AGENT_MODEL=MiniMax-M2.7-highspeed KB_DB_HOST=127.0.0.1 KB_DB_PORT=5432 KB_DB_USER=regkb -KB_DB_PASSWORD=shared_kb_secret_pass +KB_DB_PASSWORD= KB_DB_NAME=regulations -# --- 9. 向量 Embedding / Reranker(Go 宿主 KB 检索;容器 app 当前未透传,宿主模式必填)--- +# --- 9. 向量 Embedding / Reranker(Go 宿主与容器 app 的 KB 检索)--- EMBEDDING_ENDPOINT_URL=https://www.dmxapi.cn/v1/embeddings -EMBEDDING_PROVIDER=openai_compatible EMBEDDING_MODEL=bge-m3 EMBEDDING_API_KEY= RERANKER_ENDPOINT_URL=https://www.dmxapi.cn/v1/rerank -RERANKER_PROVIDER=openai_compatible RERANKER_MODEL=bge-reranker-v2-m3-free RERANKER_API_KEY= @@ -87,8 +83,15 @@ ADMIN_PASSWORD=admin-secret-password-123 AUTH_REGISTRATION_ENABLED=true AUTH_REQUIRE_APPROVAL=false -# --- 12. 可选:CORS / 内存降级 --- -# dev/test 未配置时 CORS 由代码填充 localhost 默认值 -# CORS_ALLOWED_ORIGINS=http://localhost:5173,http://127.0.0.1:8080 -# 生产必须为 false;main 启动时若 true 将拒绝加载 -# ALLOW_IN_MEMORY=false +# --- 12. 安全、缓存与意图路由开关 --- +# 留空时 dev/test 由代码补 localhost;生产留空代表不允许跨 Origin +CORS_ALLOWED_ORIGINS= +# 生产强制 false;main compose 不接受覆盖 +ALLOW_IN_MEMORY=false +CACHE_ENABLED=true +INTENT_V2_SHADOW=false +INTENT_V2_DECISION=false +INTENT_V2_DECISION_TIER=none +WORKFLOW_INTENTS_ENABLED=true +MULTI_INTENT_ENABLED=true +SEMANTIC_FALLBACK_ENABLED=false diff --git a/configs/.env.dev.example b/configs/.env.dev.example index 936c8b14..ecc5c863 100644 --- a/configs/.env.dev.example +++ b/configs/.env.dev.example @@ -1,25 +1,18 @@ # ============================================================================= # 开发环境 Compose 专用配置模板 (configs/.env.dev) -# 作用域:./scripts/env-service.sh dev * 与 docker compose --env-file configs/.env.dev +# 作用域:./scripts/env-service.sh dev * 的环境差异覆盖 # -# 重要:本文件与根目录 .env 分工不同 -# - 根 .env → Go bin/server、launch.sh 默认 compose -# - 本文件 → env-service.sh 启停 dev 容器组 -# 密钥(MINIMAX_API_KEY / TAVILY_API_KEYS 等)须与根 .env 保持一致,否则 pi-agent 容器无 Token! +# 根 .env 提供公共模型、检索、认证配置;本文件只覆盖 dev 差异。 # ============================================================================= ENV=dev -COMPOSE_PROJECT_NAME=agent-dev # 1. 业务 HTTP 服务 HTTP_PORT=8080 APP_PUBLISH_HOST=127.0.0.1 -ADMIN_TOKEN=dev-admin-token-change-me -ALLOW_INSECURE_ADMIN=1 # 2. 对话数据与工作区(禁止跨环境共享) DATA_DIR=./data/dev -AUX_FILE_DSN=file:/var/lib/feishu/outbox_dev.db?_journal_mode=WAL&_busy_timeout=5000 # 3. 开发环境专属数据库 SYSTEM_DB_USER=app @@ -29,7 +22,6 @@ SYSTEM_DB_PUBLISH_PORT=5433 # 4. 共享知识库(容器 app 经 shuimu-kb-shared 访问) KB_DB_USER=regkb -KB_DB_PASSWORD=shared_kb_secret_pass KB_DB_NAME=regulations KB_DB_HOST=shuimu-kb-shared KB_DB_PORT=5432 @@ -40,12 +32,4 @@ PI_AGENT_RUNNER_PORT=18865 AGENT_BACKEND=pi-agent AGENT_TIMEOUT=60m -# 6. Pi-Agent / MiniMax / Tavily(pi-agent 容器必填;从根 .env 复制) -MINIMAX_ENDPOINT_URL=https://api.minimaxi.com/v1 -MINIMAX_API_KEY= -MINIMAX_MODEL_ID=MiniMax-M2.7-highspeed -MINIMAX_API_TYPE=openai-compatible -PI_AGENT_PROVIDER=minimax -PI_AGENT_MODEL=MiniMax-M2.7-highspeed -TAVILY_API_KEYS= -# RUNNER_AUTH_TOKEN= +# 6. Pi-Agent / MiniMax / Tavily / Runner 密钥统一读取根 .env diff --git a/configs/.env.main.example b/configs/.env.main.example index 30a46e6f..63b0aa53 100644 --- a/configs/.env.main.example +++ b/configs/.env.main.example @@ -1,39 +1,37 @@ # ============================================================================= # 生产/主分支环境 (main) 环境变量配置模板 (configs/.env.main) # 作用域:main 分支正式线上发布与生产运营 +# 根 .env 提供公共模型与检索配置;本文件覆盖生产端口、数据库和生产密钥。 # ============================================================================= ENV=prod -COMPOSE_PROJECT_NAME=agent-main # 1. 业务 HTTP 服务 HTTP_PORT=8000 APP_PUBLISH_HOST=0.0.0.0 -ADMIN_TOKEN=main-prod-admin-token-super-secure -ALLOW_INSECURE_ADMIN=0 +AUTH_REGISTRATION_ENABLED=false +AUTH_REQUIRE_APPROVAL=true # 2. 对话数据与工作区隔离目录 (禁止跨环境共享) DATA_DIR=./data/main -AUX_FILE_DSN=file:/var/lib/feishu/outbox_main.db?_journal_mode=WAL&_busy_timeout=5000 # 3. 生产环境专属数据库 (禁止与 dev/test 共享) SYSTEM_DB_USER=app -SYSTEM_DB_PASSWORD=prod_highly_secure_db_pass +SYSTEM_DB_PASSWORD= SYSTEM_DB_NAME=agent_system_main SYSTEM_DB_PUBLISH_PORT=5435 # 4. 共享知识库连接 (只读检索共享数据) KB_DB_USER=regkb -KB_DB_PASSWORD=shared_kb_secret_pass KB_DB_NAME=regulations KB_DB_HOST=shuimu-kb-shared KB_DB_PORT=5432 # 5. Cursor / LLM Agent Runner 服务 CURSOR_RUNNER_PORT=18767 -AGENT_BACKEND=stub +PI_AGENT_RUNNER_PORT=18867 +AGENT_BACKEND=pi-agent AGENT_TIMEOUT=60m # 生产必填(不可使用 shared_kb_secret_pass / admin-secret-password-123 等默认值) -RUNNER_AUTH_TOKEN=change-me-runner-token -KB_DB_PASSWORD=change-me-kb-prod-secret -ADMIN_PASSWORD=change-me-admin-prod-secret +RUNNER_AUTH_TOKEN= +ADMIN_PASSWORD= diff --git a/configs/.env.shared.example b/configs/.env.shared.example index 518679f8..e5b79afe 100644 --- a/configs/.env.shared.example +++ b/configs/.env.shared.example @@ -1,13 +1,10 @@ # ============================================================================= # 共享基础设施环境变量配置模板 (configs/.env.shared) # 作用域:所有分支与环境共享的知识库、向量检索与底层公共网络 +# KB_DB_PASSWORD 统一在根 .env 配置,本文件只覆盖共享服务差异。 # ============================================================================= -COMPOSE_PROJECT_NAME=agent-infra - # 共享知识库 PostgreSQL (带 pgvector) KB_DB_USER=regkb -KB_DB_PASSWORD=shared_kb_secret_pass KB_DB_NAME=regulations KB_PUBLISH_PORT=5432 -KB_DATA_DIR=./data/shared/kb diff --git a/configs/.env.test.example b/configs/.env.test.example index 8ca77b0b..7ac78432 100644 --- a/configs/.env.test.example +++ b/configs/.env.test.example @@ -1,20 +1,17 @@ # ============================================================================= # 测试环境 (test) 环境变量配置模板 (configs/.env.test) # 作用域:test 分支阶段集成、长期部署实测与运维验证 +# 根 .env 提供公共模型、检索、认证配置;本文件只覆盖 test 差异。 # ============================================================================= ENV=test -COMPOSE_PROJECT_NAME=agent-test # 1. 业务 HTTP 服务 HTTP_PORT=8081 APP_PUBLISH_HOST=0.0.0.0 -ADMIN_TOKEN=test-admin-token-secure-key -ALLOW_INSECURE_ADMIN=0 # 2. 对话数据与工作区隔离目录 (禁止跨环境共享) DATA_DIR=./data/test -AUX_FILE_DSN=file:/var/lib/feishu/outbox_test.db?_journal_mode=WAL&_busy_timeout=5000 # 3. 测试环境专属数据库 (禁止与 dev/main 共享) SYSTEM_DB_USER=app @@ -24,7 +21,6 @@ SYSTEM_DB_PUBLISH_PORT=5434 # 4. 共享知识库连接 (只读检索共享数据) KB_DB_USER=regkb -KB_DB_PASSWORD=shared_kb_secret_pass KB_DB_NAME=regulations KB_DB_HOST=shuimu-kb-shared KB_DB_PORT=5432 @@ -32,7 +28,7 @@ KB_DB_PORT=5432 # 5. Cursor / LLM Agent Runner 服务 CURSOR_RUNNER_PORT=18766 PI_AGENT_RUNNER_PORT=18866 -AGENT_BACKEND=stub +AGENT_BACKEND=pi-agent AGENT_TIMEOUT=60m # app 与 pi-agent 容器共享(test 环境建议设置非空值) # RUNNER_AUTH_TOKEN=test-runner-shared-secret diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index afd6ab4f..fa6c6aa4 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -60,7 +60,6 @@ services: MINIMAX_API_TYPE: ${MINIMAX_API_TYPE:-openai-compatible} MINIMAX_PROVIDER: minimax TAVILY_API_KEYS: ${TAVILY_API_KEYS:-} - TAVILY_API_KEY: ${TAVILY_API_KEY:-} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} ports: - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18865}:18865" @@ -94,19 +93,38 @@ services: HTTP_PORT: "${HTTP_PORT:-8080}" HTTP_HOST: "0.0.0.0" AGENT_BACKEND: ${AGENT_BACKEND:-pi-agent} + AGENT_TIMEOUT: ${AGENT_TIMEOUT:-30m} + MAX_CONCURRENT_AGENTS: ${MAX_CONCURRENT_AGENTS:-8} DATA_DIR: /app/data # 独占业务与对话库 APP_DB_DSN: postgres://${SYSTEM_DB_USER:-app}:${SYSTEM_DB_PASSWORD:-dev_secret_db_pass}@system-db:5432/${SYSTEM_DB_NAME:-agent_system_dev}?sslmode=disable # 共享知识库 - POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:-shared_kb_secret_pass}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable + POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:?KB_DB_PASSWORD is required}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable CURSOR_RUNNER_URL: http://cursor-agent:18765 PI_AGENT_RUNNER_URL: http://pi-agent:18865 PI_AGENT_RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} PI_AGENT_PROVIDER: ${PI_AGENT_PROVIDER:-minimax} PI_AGENT_MODEL: ${PI_AGENT_MODEL:-MiniMax-M2.7-highspeed} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} - ALLOW_IN_MEMORY: "false" - AUX_FILE_DSN: file:/var/lib/feishu/outbox_dev.db?_journal_mode=WAL&_busy_timeout=5000 + EMBEDDING_ENDPOINT_URL: ${EMBEDDING_ENDPOINT_URL:-} + EMBEDDING_MODEL: ${EMBEDDING_MODEL:-bge-m3} + EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-} + RERANKER_ENDPOINT_URL: ${RERANKER_ENDPOINT_URL:-} + RERANKER_MODEL: ${RERANKER_MODEL:-bge-reranker-v2-m3-free} + RERANKER_API_KEY: ${RERANKER_API_KEY:-} + ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin-secret-password-123} + AUTH_REGISTRATION_ENABLED: ${AUTH_REGISTRATION_ENABLED:-true} + AUTH_REQUIRE_APPROVAL: ${AUTH_REQUIRE_APPROVAL:-false} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-} + ALLOW_IN_MEMORY: ${ALLOW_IN_MEMORY:-false} + CACHE_ENABLED: ${CACHE_ENABLED:-true} + INTENT_V2_SHADOW: ${INTENT_V2_SHADOW:-false} + INTENT_V2_DECISION: ${INTENT_V2_DECISION:-false} + INTENT_V2_DECISION_TIER: ${INTENT_V2_DECISION_TIER:-none} + WORKFLOW_INTENTS_ENABLED: ${WORKFLOW_INTENTS_ENABLED:-true} + MULTI_INTENT_ENABLED: ${MULTI_INTENT_ENABLED:-true} + SEMANTIC_FALLBACK_ENABLED: ${SEMANTIC_FALLBACK_ENABLED:-false} ports: - "${APP_PUBLISH_HOST:-127.0.0.1}:${HTTP_PORT:-8080}:${HTTP_PORT:-8080}" healthcheck: diff --git a/docker-compose.main.yml b/docker-compose.main.yml index 786a6e62..553c770d 100644 --- a/docker-compose.main.yml +++ b/docker-compose.main.yml @@ -14,7 +14,7 @@ services: command: ["postgres", "-c", "shared_buffers=512MB", "-c", "max_connections=300"] environment: POSTGRES_USER: ${SYSTEM_DB_USER:-app} - POSTGRES_PASSWORD: ${SYSTEM_DB_PASSWORD:-prod_highly_secure_db_pass} + POSTGRES_PASSWORD: ${SYSTEM_DB_PASSWORD:?SYSTEM_DB_PASSWORD is required for main/production} POSTGRES_DB: ${SYSTEM_DB_NAME:-agent_system_main} ports: - "127.0.0.1:${SYSTEM_DB_PUBLISH_PORT:-5435}:5432" @@ -60,7 +60,6 @@ services: MINIMAX_API_TYPE: ${MINIMAX_API_TYPE:-openai-compatible} MINIMAX_PROVIDER: minimax TAVILY_API_KEYS: ${TAVILY_API_KEYS:-} - TAVILY_API_KEY: ${TAVILY_API_KEY:-} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:?RUNNER_AUTH_TOKEN is required for main/production} ports: - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18867}:18867" @@ -92,19 +91,39 @@ services: HTTP_PORT: "${HTTP_PORT:-8000}" HTTP_HOST: "0.0.0.0" AGENT_BACKEND: ${AGENT_BACKEND:-pi-agent} + AGENT_TIMEOUT: ${AGENT_TIMEOUT:-30m} + MAX_CONCURRENT_AGENTS: ${MAX_CONCURRENT_AGENTS:-8} DATA_DIR: /app/data # 独占生产业务与对话库 - APP_DB_DSN: postgres://${SYSTEM_DB_USER:-app}:${SYSTEM_DB_PASSWORD:-prod_highly_secure_db_pass}@system-db:5432/${SYSTEM_DB_NAME:-agent_system_main}?sslmode=disable + APP_DB_DSN: postgres://${SYSTEM_DB_USER:-app}:${SYSTEM_DB_PASSWORD:?SYSTEM_DB_PASSWORD is required for main/production}@system-db:5432/${SYSTEM_DB_NAME:-agent_system_main}?sslmode=disable # 共享知识库 - POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:-shared_kb_secret_pass}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable + POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:?KB_DB_PASSWORD is required for main/production}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable CURSOR_RUNNER_URL: http://cursor-agent:18767 PI_AGENT_RUNNER_URL: http://pi-agent:18867 PI_AGENT_RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:?RUNNER_AUTH_TOKEN is required for main/production} + PI_AGENT_PROVIDER: ${PI_AGENT_PROVIDER:-minimax} + PI_AGENT_MODEL: ${PI_AGENT_MODEL:-${MINIMAX_MODEL_ID:-MiniMax-M3}} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:?RUNNER_AUTH_TOKEN is required for main/production} KB_DB_PASSWORD: ${KB_DB_PASSWORD:?KB_DB_PASSWORD is required for main/production} + ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD is required for main/production} + AUTH_REGISTRATION_ENABLED: ${AUTH_REGISTRATION_ENABLED:-true} + AUTH_REQUIRE_APPROVAL: ${AUTH_REQUIRE_APPROVAL:-false} + EMBEDDING_ENDPOINT_URL: ${EMBEDDING_ENDPOINT_URL:-} + EMBEDDING_MODEL: ${EMBEDDING_MODEL:-bge-m3} + EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-} + RERANKER_ENDPOINT_URL: ${RERANKER_ENDPOINT_URL:-} + RERANKER_MODEL: ${RERANKER_MODEL:-bge-reranker-v2-m3-free} + RERANKER_API_KEY: ${RERANKER_API_KEY:-} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-} ALLOW_IN_MEMORY: "false" - AUX_FILE_DSN: file:/var/lib/feishu/outbox_main.db?_journal_mode=WAL&_busy_timeout=5000 + CACHE_ENABLED: ${CACHE_ENABLED:-true} + INTENT_V2_SHADOW: ${INTENT_V2_SHADOW:-false} + INTENT_V2_DECISION: ${INTENT_V2_DECISION:-false} + INTENT_V2_DECISION_TIER: ${INTENT_V2_DECISION_TIER:-none} + WORKFLOW_INTENTS_ENABLED: ${WORKFLOW_INTENTS_ENABLED:-true} + MULTI_INTENT_ENABLED: ${MULTI_INTENT_ENABLED:-true} + SEMANTIC_FALLBACK_ENABLED: ${SEMANTIC_FALLBACK_ENABLED:-false} ports: - "${APP_PUBLISH_HOST:-0.0.0.0}:${HTTP_PORT:-8000}:${HTTP_PORT:-8000}" healthcheck: diff --git a/docker-compose.shared.yml b/docker-compose.shared.yml index 7cb19a7e..5ee68b40 100644 --- a/docker-compose.shared.yml +++ b/docker-compose.shared.yml @@ -10,7 +10,7 @@ services: container_name: shuimu-kb-shared environment: POSTGRES_USER: ${KB_DB_USER:-regkb} - POSTGRES_PASSWORD: ${KB_DB_PASSWORD:-shared_kb_secret_pass} + POSTGRES_PASSWORD: ${KB_DB_PASSWORD:?KB_DB_PASSWORD is required} POSTGRES_DB: ${KB_DB_NAME:-regulations} ports: - "127.0.0.1:${KB_PUBLISH_PORT:-5432}:5432" @@ -29,7 +29,7 @@ services: networks: shared-net: name: chat-agent-shared-net - driver: bridge + external: true volumes: shared-kb-pgdata: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 6a9604f7..d0d880e4 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -60,7 +60,6 @@ services: MINIMAX_API_TYPE: ${MINIMAX_API_TYPE:-openai-compatible} MINIMAX_PROVIDER: minimax TAVILY_API_KEYS: ${TAVILY_API_KEYS:-} - TAVILY_API_KEY: ${TAVILY_API_KEY:-} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} ports: - "127.0.0.1:${PI_AGENT_RUNNER_PORT:-18866}:18866" @@ -92,17 +91,38 @@ services: HTTP_PORT: "${HTTP_PORT:-8081}" HTTP_HOST: "0.0.0.0" AGENT_BACKEND: ${AGENT_BACKEND:-pi-agent} + AGENT_TIMEOUT: ${AGENT_TIMEOUT:-30m} + MAX_CONCURRENT_AGENTS: ${MAX_CONCURRENT_AGENTS:-8} DATA_DIR: /app/data # 独占测试业务与对话库 APP_DB_DSN: postgres://${SYSTEM_DB_USER:-app}:${SYSTEM_DB_PASSWORD:-test_secret_db_pass}@system-db:5432/${SYSTEM_DB_NAME:-agent_system_test}?sslmode=disable # 共享知识库 - POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:-shared_kb_secret_pass}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable + POSTGRES_SHARED_KB_DSN: postgres://${KB_DB_USER:-regkb}:${KB_DB_PASSWORD:?KB_DB_PASSWORD is required}@shuimu-kb-shared:5432/${KB_DB_NAME:-regulations}?sslmode=disable CURSOR_RUNNER_URL: http://cursor-agent:18766 PI_AGENT_RUNNER_URL: http://pi-agent:18866 PI_AGENT_RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} + PI_AGENT_PROVIDER: ${PI_AGENT_PROVIDER:-minimax} + PI_AGENT_MODEL: ${PI_AGENT_MODEL:-${MINIMAX_MODEL_ID:-MiniMax-M3}} RUNNER_AUTH_TOKEN: ${RUNNER_AUTH_TOKEN:-} - ALLOW_IN_MEMORY: "false" - AUX_FILE_DSN: file:/var/lib/feishu/outbox_test.db?_journal_mode=WAL&_busy_timeout=5000 + EMBEDDING_ENDPOINT_URL: ${EMBEDDING_ENDPOINT_URL:-} + EMBEDDING_MODEL: ${EMBEDDING_MODEL:-bge-m3} + EMBEDDING_API_KEY: ${EMBEDDING_API_KEY:-} + RERANKER_ENDPOINT_URL: ${RERANKER_ENDPOINT_URL:-} + RERANKER_MODEL: ${RERANKER_MODEL:-bge-reranker-v2-m3-free} + RERANKER_API_KEY: ${RERANKER_API_KEY:-} + ADMIN_USERNAME: ${ADMIN_USERNAME:-admin} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin-secret-password-123} + AUTH_REGISTRATION_ENABLED: ${AUTH_REGISTRATION_ENABLED:-true} + AUTH_REQUIRE_APPROVAL: ${AUTH_REQUIRE_APPROVAL:-false} + CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-} + ALLOW_IN_MEMORY: ${ALLOW_IN_MEMORY:-false} + CACHE_ENABLED: ${CACHE_ENABLED:-true} + INTENT_V2_SHADOW: ${INTENT_V2_SHADOW:-false} + INTENT_V2_DECISION: ${INTENT_V2_DECISION:-false} + INTENT_V2_DECISION_TIER: ${INTENT_V2_DECISION_TIER:-none} + WORKFLOW_INTENTS_ENABLED: ${WORKFLOW_INTENTS_ENABLED:-true} + MULTI_INTENT_ENABLED: ${MULTI_INTENT_ENABLED:-true} + SEMANTIC_FALLBACK_ENABLED: ${SEMANTIC_FALLBACK_ENABLED:-false} ports: - "${APP_PUBLISH_HOST:-0.0.0.0}:${HTTP_PORT:-8081}:${HTTP_PORT:-8081}" healthcheck: diff --git a/internal/config/config.go b/internal/config/config.go index 6f9d7412..c6d52240 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,27 +10,27 @@ import ( ) type Config struct { - Env string `json:"env"` - DataDir string `json:"data_dir"` - HTTPHost string `json:"http_host"` - HTTPPort int `json:"http_port"` - AgentBackend string `json:"agent_backend"` - AgentTimeout time.Duration `json:"agent_timeout"` - CursorAPIKey string `json:"cursor_api_key"` - CursorModel string `json:"cursor_model"` - CursorBin string `json:"cursor_bin"` - CursorBridgeBin string `json:"cursor_bridge_bin"` - PiAgentBin string `json:"pi_agent_bin"` - PiAgentBridgeBin string `json:"pi_agent_bridge_bin"` - PiAgentProvider string `json:"pi_agent_provider"` - PiAgentModel string `json:"pi_agent_model"` - PiAgentRunnerURL string `json:"pi_agent_runner_url"` - PiAgentRunnerAuthToken string `json:"pi_agent_runner_auth_token"` - KbDbHost string `json:"kb_db_host"` - KbDbPort int `json:"kb_db_port"` - KbDbUser string `json:"kb_db_user"` - KbDbPassword string `json:"kb_db_password"` - KbDbName string `json:"kb_db_name"` + Env string `json:"env"` + DataDir string `json:"data_dir"` + HTTPHost string `json:"http_host"` + HTTPPort int `json:"http_port"` + AgentBackend string `json:"agent_backend"` + AgentTimeout time.Duration `json:"agent_timeout"` + CursorAPIKey string `json:"cursor_api_key"` + CursorModel string `json:"cursor_model"` + CursorBin string `json:"cursor_bin"` + CursorBridgeBin string `json:"cursor_bridge_bin"` + PiAgentBin string `json:"pi_agent_bin"` + PiAgentBridgeBin string `json:"pi_agent_bridge_bin"` + PiAgentProvider string `json:"pi_agent_provider"` + PiAgentModel string `json:"pi_agent_model"` + PiAgentRunnerURL string `json:"pi_agent_runner_url"` + PiAgentRunnerAuthToken string `json:"pi_agent_runner_auth_token"` + KbDbHost string `json:"kb_db_host"` + KbDbPort int `json:"kb_db_port"` + KbDbUser string `json:"kb_db_user"` + KbDbPassword string `json:"kb_db_password"` + KbDbName string `json:"kb_db_name"` // 专属记忆与多级上下文数据库配置 ContextDbHost string `json:"context_db_host"` @@ -252,9 +252,29 @@ func Load(path string) (*Config, error) { if v := os.Getenv("AGENT_BACKEND"); v != "" { cfg.AgentBackend = v } + if v := os.Getenv("AGENT_TIMEOUT"); v != "" { + if d, err := time.ParseDuration(v); err == nil { + cfg.AgentTimeout = d + } + } + if v := os.Getenv("CURSOR_API_KEY"); v != "" { + cfg.CursorAPIKey = v + } + if v := os.Getenv("CURSOR_MODEL"); v != "" { + cfg.CursorModel = v + } + if v := os.Getenv("CURSOR_BIN"); v != "" { + cfg.CursorBin = v + } + if v := os.Getenv("CURSOR_BRIDGE_BIN"); v != "" { + cfg.CursorBridgeBin = v + } if v := os.Getenv("PI_AGENT_BIN"); v != "" { cfg.PiAgentBin = v } + if v := os.Getenv("PI_AGENT_BRIDGE_BIN"); v != "" { + cfg.PiAgentBridgeBin = v + } if v := os.Getenv("PI_AGENT_PROVIDER"); v != "" { cfg.PiAgentProvider = v } @@ -398,7 +418,7 @@ func validateProductionSecrets(cfg *Config) error { "CONTEXT_DB_PASSWORD": cfg.ContextDbPassword, "ADMIN_PASSWORD": cfg.AdminPassword, } - defaults := []string{"shared_kb_secret_pass", "dev_secret_db_pass", "admin-secret-password-123"} + defaults := []string{"shared_kb_secret_pass", "dev_secret_db_pass", "prod_highly_secure_db_pass", "admin-secret-password-123"} for envKey, val := range weak { if strings.TrimSpace(val) == "" { return fmt.Errorf("%s must be set in %s environment", envKey, cfg.Env) @@ -482,4 +502,3 @@ func (c *Config) KbDSN() string { func (c *Config) ContextDSN() string { return "postgres://" + c.ContextDbUser + ":" + c.ContextDbPassword + "@" + c.ContextDbHost + ":" + strconv.Itoa(c.ContextDbPort) + "/" + c.ContextDbName + "?sslmode=disable" } - diff --git a/internal/config/config_test.go b/internal/config/config_test.go index b126f868..ec789ed0 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -69,6 +69,35 @@ func TestConfigLoadDSNOverrides(t *testing.T) { } } +func TestConfigLoadRuntimeEnvironmentOverrides(t *testing.T) { + t.Setenv("ENV", "dev") + t.Setenv("AGENT_TIMEOUT", "47m") + t.Setenv("CURSOR_API_KEY", "cursor-test-key") + t.Setenv("CURSOR_MODEL", "cursor-test-model") + t.Setenv("CURSOR_BIN", "/test/cursor") + t.Setenv("CURSOR_BRIDGE_BIN", "/test/cursor-bridge.mjs") + t.Setenv("PI_AGENT_BRIDGE_BIN", "/test/pi-bridge.mjs") + t.Setenv("EMBEDDING_MODEL", "embedding-test-model") + t.Setenv("RERANKER_MODEL", "reranker-test-model") + + cfg, err := config.Load("/dev/null") + if err != nil { + t.Fatalf("unexpected error loading config: %v", err) + } + if cfg.AgentTimeout != 47*time.Minute { + t.Fatalf("expected environment AgentTimeout 47m, got %v", cfg.AgentTimeout) + } + if cfg.CursorAPIKey != "cursor-test-key" || cfg.CursorModel != "cursor-test-model" || cfg.CursorBin != "/test/cursor" || cfg.CursorBridgeBin != "/test/cursor-bridge.mjs" { + t.Fatalf("cursor environment overrides were not applied") + } + if cfg.PiAgentBridgeBin != "/test/pi-bridge.mjs" { + t.Fatalf("pi bridge environment override was not applied") + } + if cfg.EmbeddingModel != "embedding-test-model" || cfg.RerankerModel != "reranker-test-model" { + t.Fatalf("retrieval model environment overrides were not applied") + } +} + func TestConfigLoadCORSDefaultsForDev(t *testing.T) { cfg, err := config.Load("/dev/null") if err != nil { @@ -103,3 +132,15 @@ func TestConfigLoadRejectsWeakSecretsOutsideDev(t *testing.T) { t.Fatal("expected config load to fail for missing secrets in main env") } } + +func TestConfigLoadRejectsProductionDatabasePlaceholder(t *testing.T) { + t.Setenv("ENV", "prod") + t.Setenv("KB_DB_PASSWORD", "kb-prod-secret-xyz") + t.Setenv("CONTEXT_DB_PASSWORD", "prod_highly_secure_db_pass") + t.Setenv("ADMIN_PASSWORD", "admin-prod-secret-xyz") + + _, err := config.Load("/dev/null") + if err == nil { + t.Fatal("expected config load to reject the documented production database placeholder") + } +} diff --git a/scripts/env-service.sh b/scripts/env-service.sh index 0c5a0eeb..68571b07 100755 --- a/scripts/env-service.sh +++ b/scripts/env-service.sh @@ -90,44 +90,45 @@ run_compose() { prepare_env_file "${target}" fi - local env_arg="" + local env_args=() + # 根 .env 提供跨环境共用的模型、检索与认证配置;环境文件仅覆盖端口、 + # 数据库和其他环境差异。后声明的 env-file 优先级更高。 + if [[ -f ".env" ]]; then + env_args+=(--env-file ".env") + fi if [[ -f "${env_file}" ]]; then - env_arg="--env-file ${env_file}" + env_args+=(--env-file "${env_file}") fi case "${act}" in start) - if [[ "${target}" != "shared" ]]; then - ensure_shared_network - fi + ensure_shared_network echo "==> [${target}] 启动服务 (${compose_file})..." - docker compose -f "${compose_file}" ${env_arg} up -d + docker compose -f "${compose_file}" "${env_args[@]}" up -d echo "==> [${target}] 服务已启动" ;; stop) echo "==> [${target}] 停止服务..." - docker compose -f "${compose_file}" ${env_arg} stop + docker compose -f "${compose_file}" "${env_args[@]}" stop ;; restart) - if [[ "${target}" != "shared" ]]; then - ensure_shared_network - fi + ensure_shared_network echo "==> [${target}] 重启服务..." - docker compose -f "${compose_file}" ${env_arg} restart + docker compose -f "${compose_file}" "${env_args[@]}" restart ;; status|ps) echo "------------------------------------------------------------" echo "环境: [${target}] (编排: ${compose_file})" echo "------------------------------------------------------------" - docker compose -f "${compose_file}" ${env_arg} ps + docker compose -f "${compose_file}" "${env_args[@]}" ps echo "" ;; logs) - docker compose -f "${compose_file}" ${env_arg} logs -f --tail=100 + docker compose -f "${compose_file}" "${env_args[@]}" logs -f --tail=100 ;; down) echo "==> [${target}] 清理并下线容器..." - docker compose -f "${compose_file}" ${env_arg} down + docker compose -f "${compose_file}" "${env_args[@]}" down ;; *) echo "[错误] 未知操作: ${act}"