merge: feature/sync-wip-2026-09-13 — container env config and chat refresh docs
CI / go-vet-and-short-test (push) Failing after 1s
CI / backup-restore-drill (push) Failing after 0s
CI / web (push) Failing after 0s

This commit is contained in:
weichen
2026-09-13 02:55:31 +00:00
22 changed files with 568 additions and 109 deletions
@@ -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)
@@ -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` 并走显式兼容策略。
+8
View File
@@ -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)
@@ -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)
@@ -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)
@@ -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`
+1
View File
@@ -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) | 症状→原因→方案速查 |
+9 -2
View File
@@ -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 与前端人工抽检**。
+2 -2
View File
@@ -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` |
+2
View File
@@ -3,6 +3,8 @@
.doc
docs
data
.regulations
.tmp
web/node_modules
web/dist
web/.vite
+17 -14
View File
@@ -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
+3 -19
View File
@@ -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
+8 -10
View File
@@ -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=
+1 -4
View File
@@ -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
+2 -6
View File
@@ -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
+22 -4
View File
@@ -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:
+24 -5
View File
@@ -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:
+2 -2
View File
@@ -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:
+24 -4
View File
@@ -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:
+42 -23
View File
@@ -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"
}
+41
View File
@@ -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")
}
}
+15 -14
View File
@@ -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}"