Compare commits
32 Commits
9a84e125d0
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
638b757138 | ||
|
|
b185e58f31 | ||
|
|
2a1a5991cb | ||
|
|
2a9b3e3273 | ||
|
|
7a3755ac55 | ||
|
|
aa6a7d1250 | ||
|
|
208e1bed59 | ||
|
|
140efd1a2a | ||
|
|
74fafc86d3 | ||
|
|
15da295963 | ||
|
|
044a5669fe | ||
|
|
c59990dc35 | ||
|
|
372e35d62a | ||
|
|
07241b4648 | ||
|
|
787bc3a481 | ||
|
|
242d68c36f | ||
|
|
28b834edd3 | ||
|
|
4940ebc419 | ||
|
|
ee88a36baf | ||
|
|
6bdf65bc24 | ||
|
|
ae3f02c35a | ||
|
|
54754b5502 | ||
|
|
211f85d981 | ||
|
|
5b24630710 | ||
|
|
a662cfe6c3 | ||
|
|
5ed34c2b8f | ||
|
|
11275e4ba6 | ||
|
|
1347366b95 | ||
|
|
22669a9071 | ||
|
|
a616b30cb2 | ||
|
|
653eda0596 | ||
|
|
661990b27b |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -31,6 +31,8 @@ server/logs/
|
||||
server/storage/expense_claims/
|
||||
server/storage/finance_reports/
|
||||
server/storage/receipt_folder/
|
||||
server/storage/knowledge/platform/
|
||||
server/storage/knowledge/tenants/
|
||||
test-results/
|
||||
.codex-remote-attachments/
|
||||
tmp-*.png
|
||||
|
||||
@@ -1,36 +1,4 @@
|
||||
services:
|
||||
main:
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DB: "${POSTGRES_DB:-x_financial}"
|
||||
POSTGRES_USER: "${POSTGRES_USER:-x_financial}"
|
||||
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-x_financial}"
|
||||
DATABASE_URL: "postgresql+psycopg://${POSTGRES_USER:-x_financial}:${POSTGRES_PASSWORD:-x_financial}@postgres:5432/${POSTGRES_DB:-x_financial}"
|
||||
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg17
|
||||
container_name: x-financial-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: "${POSTGRES_DB:-x_financial}"
|
||||
POSTGRES_USER: "${POSTGRES_USER:-x_financial}"
|
||||
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-x_financial}"
|
||||
ports:
|
||||
- "${POSTGRES_HOST_PORT:-55432}:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
networks:
|
||||
- financial-internal
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
# PostgreSQL 已并入默认 docker-compose.yml。
|
||||
# 保留此空覆盖文件,兼容历史启动命令:
|
||||
# docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d
|
||||
services: {}
|
||||
|
||||
@@ -3,6 +3,13 @@ services:
|
||||
image: x-financial-dev:latest
|
||||
container_name: local-x-financial-linux
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
onlyoffice:
|
||||
condition: service_healthy
|
||||
qdrant:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
WEB_HOST: 0.0.0.0
|
||||
WEB_PORT: "${WEB_PORT:-5173}"
|
||||
@@ -10,12 +17,18 @@ services:
|
||||
SERVER_PORT: "${SERVER_PORT:-8000}"
|
||||
SERVER_RELOAD: "${SERVER_RELOAD:-true}"
|
||||
SERVER_VENV_DIR: /tmp/x-financial-server-venv
|
||||
X_FINANCIAL_PREFER_ENV_FILE: "true"
|
||||
ONLYOFFICE_ENABLED: "${ONLYOFFICE_ENABLED:-false}"
|
||||
ONLYOFFICE_PUBLIC_URL: "${ONLYOFFICE_PUBLIC_URL:-}"
|
||||
ONLYOFFICE_BACKEND_URL: "${ONLYOFFICE_BACKEND_URL:-}"
|
||||
X_FINANCIAL_PREFER_ENV_FILE: "false"
|
||||
POSTGRES_HOST: postgres
|
||||
POSTGRES_PORT: "5432"
|
||||
POSTGRES_DB: "${LOCAL_POSTGRES_DB:-x_financial}"
|
||||
POSTGRES_USER: "${LOCAL_POSTGRES_USER:-x_financial}"
|
||||
POSTGRES_PASSWORD: "${LOCAL_POSTGRES_PASSWORD:-x_financial}"
|
||||
DATABASE_URL: "postgresql+psycopg://${LOCAL_POSTGRES_USER:-x_financial}:${LOCAL_POSTGRES_PASSWORD:-x_financial}@postgres:5432/${LOCAL_POSTGRES_DB:-x_financial}"
|
||||
ONLYOFFICE_ENABLED: "true"
|
||||
ONLYOFFICE_PUBLIC_URL: "${LOCAL_ONLYOFFICE_PUBLIC_URL:-http://127.0.0.1:${ONLYOFFICE_PORT:-8082}}"
|
||||
ONLYOFFICE_BACKEND_URL: "${LOCAL_ONLYOFFICE_BACKEND_URL:-http://main:${SERVER_PORT:-8000}}"
|
||||
ONLYOFFICE_JWT_SECRET: "${ONLYOFFICE_JWT_SECRET:-x-financial-onlyoffice-dev-secret}"
|
||||
QDRANT_URL: "${QDRANT_URL:-}"
|
||||
QDRANT_URL: "http://qdrant:6333"
|
||||
LIGHTRAG_WORKSPACE: "x_financial_knowledge"
|
||||
ports:
|
||||
- "${WEB_PORT:-5173}:${WEB_PORT:-5173}"
|
||||
@@ -65,6 +78,67 @@ services:
|
||||
networks:
|
||||
- financial-internal
|
||||
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg17
|
||||
container_name: x-financial-local-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: "${LOCAL_POSTGRES_DB:-x_financial}"
|
||||
POSTGRES_USER: "${LOCAL_POSTGRES_USER:-x_financial}"
|
||||
POSTGRES_PASSWORD: "${LOCAL_POSTGRES_PASSWORD:-x_financial}"
|
||||
ports:
|
||||
- "127.0.0.1:${LOCAL_POSTGRES_HOST_PORT:-55432}:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U \"$${POSTGRES_USER}\" -d \"$${POSTGRES_DB}\""]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
networks:
|
||||
- financial-internal
|
||||
|
||||
onlyoffice:
|
||||
image: onlyoffice/documentserver:latest
|
||||
container_name: x-financial-onlyoffice
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
JWT_ENABLED: "true"
|
||||
JWT_SECRET: "${ONLYOFFICE_JWT_SECRET:-x-financial-onlyoffice-dev-secret}"
|
||||
ports:
|
||||
- "127.0.0.1:${ONLYOFFICE_PORT:-8082}:80"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1/healthcheck >/dev/null || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 60s
|
||||
networks:
|
||||
- financial-internal
|
||||
|
||||
qdrant:
|
||||
image: qdrant/qdrant:latest
|
||||
container_name: x-financial-qdrant
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:${QDRANT_HTTP_PORT:-6333}:6333"
|
||||
- "127.0.0.1:${QDRANT_GRPC_PORT:-6334}:6334"
|
||||
volumes:
|
||||
- qdrant-storage:/qdrant/storage
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "bash -lc 'exec 3<>/dev/tcp/127.0.0.1/6333' || exit 1"]
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
networks:
|
||||
- financial-internal
|
||||
|
||||
networks:
|
||||
financial-internal:
|
||||
name: financial-internal
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
qdrant-storage:
|
||||
|
||||
17
document/development/2026-07-08/work-logs.med
Normal file
17
document/development/2026-07-08/work-logs.med
Normal file
@@ -0,0 +1,17 @@
|
||||
# 2026-07-08 综合工作日志
|
||||
|
||||
生成时间:2026-07-08 17:01:43 CST
|
||||
来源:`feature/` 功能点文档与 `dev-logs/bugs/` bug 记录
|
||||
|
||||
## 今日功能点
|
||||
|
||||
- 今日未发现功能点文档。
|
||||
|
||||
## 今日 Bugs
|
||||
|
||||
- 今日未发现 bug 修复记录。
|
||||
|
||||
## 综合分析
|
||||
|
||||
- 今日目录下暂无功能点或 bug 记录。
|
||||
- 后续复盘优先看本文件,再回到对应功能点或 bug 文件追溯证据。
|
||||
17
document/development/2026-07-09/work-logs.med
Normal file
17
document/development/2026-07-09/work-logs.med
Normal file
@@ -0,0 +1,17 @@
|
||||
# 2026-07-09 综合工作日志
|
||||
|
||||
生成时间:2026-07-09 17:01:25 CST
|
||||
来源:`feature/` 功能点文档与 `dev-logs/bugs/` bug 记录
|
||||
|
||||
## 今日功能点
|
||||
|
||||
- 今日未发现功能点文档。
|
||||
|
||||
## 今日 Bugs
|
||||
|
||||
- 今日未发现 bug 修复记录。
|
||||
|
||||
## 综合分析
|
||||
|
||||
- 今日目录下暂无功能点或 bug 记录。
|
||||
- 后续复盘优先看本文件,再回到对应功能点或 bug 文件追溯证据。
|
||||
17
document/development/2026-07-10/work-logs.med
Normal file
17
document/development/2026-07-10/work-logs.med
Normal file
@@ -0,0 +1,17 @@
|
||||
# 2026-07-10 综合工作日志
|
||||
|
||||
生成时间:2026-07-10 17:00:31 CST
|
||||
来源:`feature/` 功能点文档与 `dev-logs/bugs/` bug 记录
|
||||
|
||||
## 今日功能点
|
||||
|
||||
- 今日未发现功能点文档。
|
||||
|
||||
## 今日 Bugs
|
||||
|
||||
- 今日未发现 bug 修复记录。
|
||||
|
||||
## 综合分析
|
||||
|
||||
- 今日目录下暂无功能点或 bug 记录。
|
||||
- 后续复盘优先看本文件,再回到对应功能点或 bug 文件追溯证据。
|
||||
17
document/development/2026-07-11/work-logs.med
Normal file
17
document/development/2026-07-11/work-logs.med
Normal file
@@ -0,0 +1,17 @@
|
||||
# 2026-07-11 综合工作日志
|
||||
|
||||
生成时间:2026-07-11 17:09:02 CST
|
||||
来源:`feature/` 功能点文档与 `dev-logs/bugs/` bug 记录
|
||||
|
||||
## 今日功能点
|
||||
|
||||
- 今日未发现功能点文档。
|
||||
|
||||
## 今日 Bugs
|
||||
|
||||
- 今日未发现 bug 修复记录。
|
||||
|
||||
## 综合分析
|
||||
|
||||
- 今日目录下暂无功能点或 bug 记录。
|
||||
- 后续复盘优先看本文件,再回到对应功能点或 bug 文件追溯证据。
|
||||
17
document/development/2026-07-12/work-logs.med
Normal file
17
document/development/2026-07-12/work-logs.med
Normal file
@@ -0,0 +1,17 @@
|
||||
# 2026-07-12 综合工作日志
|
||||
|
||||
生成时间:2026-07-12 17:11:32 CST
|
||||
来源:`feature/` 功能点文档与 `dev-logs/bugs/` bug 记录
|
||||
|
||||
## 今日功能点
|
||||
|
||||
- 今日未发现功能点文档。
|
||||
|
||||
## 今日 Bugs
|
||||
|
||||
- 今日未发现 bug 修复记录。
|
||||
|
||||
## 综合分析
|
||||
|
||||
- 今日目录下暂无功能点或 bug 记录。
|
||||
- 后续复盘优先看本文件,再回到对应功能点或 bug 文件追溯证据。
|
||||
@@ -0,0 +1,9 @@
|
||||
## 修复记录
|
||||
|
||||
- 15:33:记录 bug 修复:AI 新建费用申请直接提交绕过统一事务。
|
||||
- Git 提交检查:`git fetch --all --prune` 后未发现 upstream 新提交;本地 ahead 2 个既有提交,分别为 `653eda05 feat(auth): add opaque bearer sessions`(不透明 Bearer 会话与认证收口)和 `661990b2 feat(expenses): add transactional expense case events`(Expense Case 与事务业务事件基础)。
|
||||
- 修改:`user_agent_application.py` 将 AI 新建并直接提交改为先创建草稿,再统一调用 `ExpenseClaimService.submit_claim`,删除入口内手写提交状态和平台风险评估;提交异常或未找到单据时主动回滚,避免外层工具日志提交失败草稿。
|
||||
- 修改:`budget.py` 的预算就绪检查改为复用当前 Session 连接执行 metadata 检查,避免 Engine 连接隐式提交已经 `flush` 的申请和预算数据;`test_reimbursement_endpoints.py`、`test_user_agent_service.py` 补齐成功提交、事件失败整体回滚、保存草稿无副作用和必填部门上下文回归。
|
||||
- 操作:所有后端验证均在 `x-financial-local-linux` 容器内执行,单条命令使用 `timeout 60s`;没有修改数据库结构,没有对持久化开发数据库执行迁移,也没有重启服务。
|
||||
- 验证:AI 直接提交/失败回滚/保存草稿 3 项、申请提交主链路 5 项、预算与 Expense Case 13 项,共 21 项通过;相关 Python 文件 `ruff --select F,I` 通过。整份 `test_reimbursement_endpoints.py` 运行结果为 13 项通过、3 项未通过,失败分别位于既有附件风险等级断言、申请审批路由断言和中文测试请求头编码,不属于本次 AI 提交事务断言。
|
||||
- 影响:AI 一键新建申请现在与编辑重提共用同一提交语义,预算预占、提交校验、申请风险标记、Expense Case 事件和审批状态保持一致;业务事件写入失败时,申请、预算额度、预算流水、预算预占和 Case 关联不会残留部分成功数据。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 22:59:记录 bug 修复:AI 工作台重复保存请求会创建多张申请草稿和多条事件。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`,未发现 upstream 新提交;本地 ahead 4 个既有提交,分别为 `22669a90 feat(expenses): show unified expense event timeline`(真实费用事件时间线)、`a616b30c fix(expenses): unify AI application submission transaction`(AI 申请提交事务)、`653eda05 feat(auth): add opaque bearer sessions`(不透明 Bearer 会话)和 `661990b2 feat(expenses): add transactional expense case events`(Expense Case 事务事件基础)。
|
||||
- 修改:新增 `expense_application_draft_events.py`,以租户、操作人、run ID 和稳定草稿快照生成固定长度幂等键及 UUID5 聚合 ID;`user_agent_application.py` 在新建前复用已完成动作,并在并发主键竞争后回滚、重查已提交事件。持久化兜底的当前时间不参与指纹,原始申请时间仍通过申请详情快照区分。
|
||||
- 操作:先在容器中分别复现“带日期相同请求”和“缺日期半成品相同请求”均生成两个不同 `claim_id`,再补动作级幂等和数据库并发仲裁路径;业务单号继续使用既有随机格式,没有修改数据库结构或执行迁移。
|
||||
- 验证:相同 HTTP 保存请求连续执行两次返回同一 `claim_id` 和 `claim_no`,数据库只有一张申请草稿与一条 `claim_draft_created`;同一 run 的不同快照仍分别留痕,事件失败时草稿、Case、Link 和事件整体回滚。容器内受影响后端定向回归 36 项、前端时间线兼容测试 9 项及 Python `ruff --select F,I` 通过。
|
||||
- 影响:网络重试、用户重复点击或客户端重放不会制造重复申请草稿;并发请求由稳定聚合主键仲裁。真实 PostgreSQL 双会话并发集成测试仍是后续可补的非阻断验证,不影响当前顺序重放与事务契约。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 22:59:记录 bug 修复:AI 申请预览入口可保留请求体伪造的身份和管理员权限。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`,未发现 upstream 新提交;本地 ahead 4 个既有提交,分别为 `22669a90 feat(expenses): show unified expense event timeline`(真实费用事件时间线)、`a616b30c fix(expenses): unify AI application submission transaction`(AI 申请提交事务)、`653eda05 feat(auth): add opaque bearer sessions`(不透明 Bearer 会话)和 `661990b2 feat(expenses): add transactional expense case events`(Expense Case 事务事件基础)。
|
||||
- 修改:`reimbursements.py` 将 `user_id`、租户、角色、管理员标记、用户名、姓名、部门、职位、职级、员工编号和直属经理全部强制绑定到服务端当前会话,不再对请求体同名字段使用 `setdefault`;`test_reimbursement_endpoints.py` 增加伪造管理员与他人身份编辑退回申请的对抗用例。
|
||||
- 操作:先在容器中复现修复前接口返回 200 且允许修改他人申请,再完成服务端身份覆盖;没有修改数据库结构,没有执行迁移或重启服务。
|
||||
- 验证:修复后恶意请求返回 400,目标申请的事由、状态和审批节点保持不变,也没有新增费用事件;正常保存草稿与直接提交回归通过。本轮受影响后端定向回归共 36 项、前端时间线兼容测试 9 项和 Python `ruff --select F,I` 均在容器内通过。
|
||||
- 影响:AI 工作台快速申请入口不能再通过伪造 `user_id`、`is_admin`、角色或员工编号绕过申请所有权检查,授权事实与其他受保护接口统一来自服务端会话。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 14:43:记录 bug 修复:客户端身份头可伪造且已初始化系统仍存在匿名重配置入口。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;未发现 upstream 新提交,本地 ahead 1 个提交 `661990b2 feat(expenses): add transactional expense case events`,它是本任务上一切片的费用事件事务提交,当前认证改造继续建立在该提交之上。
|
||||
- 修改:新增 `AuthSession` 不透明 Bearer 会话和摘要存储,后端从服务端会话及员工表解析身份/角色;移除生产 `X-Auth-*` 授权来源,保护 Bootstrap、Settings、模型连通性、缓存、员工、分析、Agent、风险观测、审计及日志接口;前端统一 Bearer、`sessionStorage`、集中 `401` 和原子登出;Vite Setup 桥在初始化完成后锁定重配置。
|
||||
- 操作:新增 `20260713_0002_auth_sessions.py` 并只生成 upgrade/downgrade 离线 SQL,没有对持久化数据库执行迁移或重启;领域回归测试使用仅存在于测试目录的身份依赖覆盖,生产代码没有兼容伪造头。测试过程中发现规则初始化用例会重写 5 份 Excel 工件,已停止继续运行该类测试并保持这些文件不进入本次提交。
|
||||
- 验证:容器 `x-financial-local-linux` 内认证/Bootstrap/费用事件/OpenAPI 21 项通过,受保护端点 13 项通过,全量收集 805 项成功;前端 17 项通过,生产构建成功;认证安全核心文件 Ruff 通过,`git diff --check` 通过,生产源码未检出 `X-Auth-*`;Alembic 离线升级包含 `CREATE TABLE auth_sessions`,离线降级包含 `DROP TABLE auth_sessions`。
|
||||
- 影响:用户登录后必须使用服务端签发的短期 Bearer 会话,伪造用户名、角色或管理员头不再生效;业务经理不能冒充平台管理员,已初始化系统的基础设施信息与重配置入口不再向匿名请求开放。租户级查询守卫、会话清理/全部退出、登录限流和 SSO 仍按 P0 后续任务推进。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 11:57:记录 bug 修复:关联申请归档/解绑时嵌套审计提前提交业务事务。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、upstream 与 local-ahead 检查,未发现 upstream 新提交或本地 ahead 新提交;工作区原有规则表和历史开发文档改动已保留。
|
||||
- 修改:`AuditLogService` 与 `AuditLogRepository` 增加由调用方控制的提交方式;`ExpenseClaimApplicationHandoffMixin` 的关联申请归档和解绑审计改为 `flush`,由付款/删除外层事务统一提交。
|
||||
- 操作:新增费用事件事务回滚测试;在付款状态与结构化 Outbox 事件之间制造失败,确认付款、申请归档和嵌套审计能够整体回滚。
|
||||
- 验证:容器 `x-financial-local-linux` 内新增及核心费用流程共 29 项测试通过,静态检查、OpenAPI 校验、启动脚本语法和 Alembic upgrade/downgrade 离线 SQL 验证通过。
|
||||
- 影响:付款或关联解绑中途失败时,不再留下“申请已归档但付款/事件未提交”的半完成状态,为事务 Outbox 提供可靠边界。
|
||||
@@ -0,0 +1,628 @@
|
||||
# AI 费用闭环与价值证明 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
文档路径:document/development/2026-07-13/feature/ai-expense-closed-loop-and-value-proof/CONCEPT.md
|
||||
|
||||
## 功能一句话
|
||||
|
||||
把申请、消费、票据、报销、审批、付款、入账、分析和持续学习连接成同一个费用事件,让员工少填表、审批人只处理例外、财务能确认真实节省,并让 AI 在可控边界内越用越准确。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
- 当前现状:项目已经具备费用申请、AI 建单、票据夹、OCR、预算检查、风险预审、动态审批、员工画像、财务看板、知识库、Agent trace、反馈表和 few-shot 样本等较完整的功能骨架。容器运行时 OpenAPI 已覆盖 156 个业务操作,功能广度已经足够。
|
||||
- 用户痛点:申请、票据、报销、审批和分析仍以多个页面、多个服务和多套状态存在。用户需要在不同入口之间理解系统,而不是由系统主动接住费用事件;移动端主流程仍有 mock;审批通过后的真实付款、回执、ERP 凭证和对账没有形成完整闭环。
|
||||
- AI 痛点:当前系统能够记录会话、运行轨迹、风险反馈和员工画像,但没有统一记录“AI 建议了什么、用户改了什么、后来是否退回、最终是否付款或产生节省”。AI 能看到历史,却不能稳定把业务结果转化为下一次的个性化、自动化和风险校准。
|
||||
- 企业痛点:现有看板主要回答花了多少、预算用了多少、发现了多少风险,尚不能可信回答省了多少钱、为什么省、哪些建议被执行、哪些费用仍可优化。
|
||||
- 工程问题:旧 `ReimbursementRequest` 与当前 `ExpenseClaim` 能力重叠,申请与报销又复用部分字段和 JSON 标记;审批、预算、付款、归档和关系引用部分混入 `risk_flags_json`,不利于事务一致性、事件追溯和持续演进。
|
||||
- 商业影响:如果不能持续证明处理效率、风险改善和真实节省,产品只能按“报销工具”定价;如果能形成价值闭环,则可以扩展为智能风控、预算经营、价值洞察和企业集成平台。
|
||||
- 为什么现在需要做:现有模块已经足够支撑一条完整费用闭环,下一阶段继续横向新增页面会扩大编排和数据割裂。应先收口费用领域、业务事件、AI 决策、学习记忆和节省归因,再逐步开放自动化。
|
||||
|
||||
相关既有方案:
|
||||
|
||||
- `document/development/2026-07-03/feature/ai-data-flywheel/CONCEPT.md`:作为本功能的 AI 样本回流、评测门禁、Prompt 版本和 Canary 子能力,不重复建设。
|
||||
- `document/development/AI意图规划器/UNIFIED_GATE_PIPELINE.md`:作为 AI 场景识别和后端唯一编排入口的架构前置,不在前端继续增加影子门控。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- [G1] 建立统一 `Expense Case`,把一次费用从需求表达、申请、票据、报销、审批、付款、入账到归档视为同一业务事件。
|
||||
- [G2] 建立零录入报销体验,自动匹配申请、预算、票据、费用类型、项目、成本中心和历史偏好,让用户主要核对异常项。
|
||||
- [G3] 建立按动作授权的安全自动化等级,从解释、建议、预填、可逆自动化逐步升级到低风险直通。
|
||||
- [G4] 建立“AI 决策 → 用户反馈 → 工作流结果 → 分层记忆 → 再决策”的学习闭环。
|
||||
- [G5] 建立节省机会、执行任务、实际结果和财务确认组成的 Savings Ledger,区分风险暴露、预计节省和已实现节省。
|
||||
- [G6] 建立从描述性统计到异常归因、预算预测、供应商分析、政策模拟和节省任务的费用经营分析能力。
|
||||
- [G7] 建立企业租户、用量计量、模型成本和客户价值指标,为年度订阅、用量超额和增值模块提供可审计基础。
|
||||
- [G8] 保持高风险动作、资金支付、制度发布和敏感主数据变更的人为授权、审计和回滚能力。
|
||||
|
||||
### 非目标
|
||||
|
||||
- [NG1] 本轮不自建商旅、企业卡、银行或支付供应链网络,只定义标准连接器和业务事件契约。
|
||||
- [NG2] 本轮不一次性替换所有现有报销接口;先建立统一费用事件和旁路事件账本,再分阶段迁移旧模型。
|
||||
- [NG3] 本轮不做模型微调或无监督自训练,优先使用结构化反馈、few-shot、规则学习、Prompt 版本和回归门禁。
|
||||
- [NG4] 本轮不允许 AI 无人值守执行资金支付、高风险批准/驳回、制度发布和收款账户变更。
|
||||
- [NG5] 本轮不同时覆盖所有费用场景,首个试点只选择一个高频、可测量、可闭环的场景。
|
||||
- [NG6] 本轮不做未经授权的跨租户学习和企业间明细 Benchmark;后续只允许严格聚合、隐私保护并获得授权的对标能力。
|
||||
- [NG7] 本轮不把风险关联单据总额、暂缓付款金额或未采纳建议直接计为企业节省。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
### 目标用户
|
||||
|
||||
1. 报销人/申请人:少填字段、少整理票据、少补件、少追问进度。
|
||||
2. 审批人:只处理必要性、例外和高风险问题,快速获得证据与建议。
|
||||
3. 财务运营:减少重复审核、退回、对账和月末整理,集中处理异常。
|
||||
4. CFO/管理层:了解费用增长原因、预算趋势、节省机会和实际 ROI。
|
||||
5. 风控/审计:查看规则依据、风险证据、算法版本、人工覆盖和审计结果。
|
||||
6. 系统管理员/IT:配置租户、组织、权限、连接器、数据保留、模型成本和自动化上限。
|
||||
|
||||
### 使用入口
|
||||
|
||||
- AI 工作台自然语言入口。
|
||||
- 统一费用事件详情页。
|
||||
- 移动端拍票、报销和审批入口。
|
||||
- 票据夹和自动票据收件箱。
|
||||
- 审批例外工作台。
|
||||
- CFO 费用价值看板。
|
||||
- AI 记忆与自动化策略设置。
|
||||
|
||||
### 核心场景
|
||||
|
||||
1. 员工说“下周去上海出差三天”,系统自动生成合规申请、预算影响和可选节省方案,用户核对后提交。
|
||||
2. 消费期间,邮箱发票、拍照票据、企业卡或商旅订单进入统一票据收件箱,系统按时间、金额、地点和商户自动归集到费用事件。
|
||||
3. 行程结束后,系统自动生成报销草稿,只要求用户处理缺票、归属或异常金额。
|
||||
4. 提交前,系统按事实、规则、证据和风险等级给出绿色通过、黄色修正、红色复核结果。
|
||||
5. 审批人看到预算影响、历史相似单、政策依据、风险证据和建议意见,低风险单据进入快速通道。
|
||||
6. 财务完成付款、回执、ERP 凭证和对账,所有结果回写费用事件并形成审计链。
|
||||
7. 月末系统识别预算超支、供应商价格漂移、重复采购、异常路线和流程瓶颈,生成有负责人和目标金额的节省任务。
|
||||
8. 用户修正字段、审批人覆盖建议、财务确认误报或节省结果后,系统更新用户、部门和企业记忆,下次减少重复操作。
|
||||
|
||||
### 异常场景
|
||||
|
||||
- OCR、模型、向量检索或外部连接器不可用时,回退到人工录入或稳定规则,不阻塞草稿保存和主链路。
|
||||
- 票据、申请或费用事件匹配置信度不足时,只展示候选,不自动绑定。
|
||||
- 企业规则与个人偏好冲突时,企业规则优先,必须解释冲突原因。
|
||||
- 高风险、超金额阈值、敏感账户变更或证据不足时,强制人工确认。
|
||||
- 自动化动作失败时必须幂等、可重试、可撤销,并保留错误状态和操作证据。
|
||||
- 预计节省没有财务确认或实际结果时,只能标记为机会,不得计入客户 ROI。
|
||||
|
||||
## 功能能力
|
||||
|
||||
- [C1] 费用事件能力:统一表达申请、消费、票据、报销、审批、付款、入账和归档关系。
|
||||
- [C2] 零录入能力:自动抽取并匹配申请、预算、票据、费用类型、项目、成本中心、参与人和历史偏好。
|
||||
- [C3] 预审与修复能力:输出事实、规则、证据、判断和建议动作,并提供一键补件、修正和解释入口。
|
||||
- [C4] 审批例外能力:风险分级、证据摘要、预算影响、推荐路由、意见草稿、批量处理、委托、转交、加签和超时升级。
|
||||
- [C5] 支付入账能力:以连接器方式接收付款批次、支付回执、ERP 凭证、银行流水和对账结果。
|
||||
- [C6] 学习记忆能力:记录 AI 建议、用户修改、审批覆盖、退回、付款和审计结果,形成用户、部门、企业三级记忆。
|
||||
- [C7] 自动化策略能力:按动作、金额、场景、风险、置信度、证据、可逆性和抽检率配置自动化等级。
|
||||
- [C8] 节省价值能力:把节省机会、建议动作、负责人、目标时间、预计节省、实际节省和财务确认形成闭环。
|
||||
- [C9] 费用经营能力:费用结构、预算预测、异常归因、供应商分析、政策模拟、流程成本和客户 ROI。
|
||||
- [C10] 商业计量能力:企业租户、套餐、配额、用量、模型/OCR 成本、增值模块、客户贡献毛利和价值证明。
|
||||
- [C11] 状态与权限:服务端认证、租户隔离、角色权限、动作授权、审计日志和敏感数据保留策略。
|
||||
- [C12] 边界与降级:外部依赖失败时主链路可用;高风险动作始终保留人工控制;所有学习与自动化可关闭、遗忘或回滚。
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 前端
|
||||
|
||||
#### 统一费用事件
|
||||
|
||||
- 新增或重构费用事件详情容器,不再让用户理解申请单、票据夹、报销草稿、审批单和付款状态之间的内部关系。
|
||||
- 页面按“计划 → 消费 → 报销 → 审批 → 付款/入账 → 复盘”展示时间线和当前待办。
|
||||
- 每个 AI 填充字段显示来源、置信度和修改入口;低置信度字段集中进入“需要确认”区。
|
||||
- 退回、补件和断点续办直接回到对应问题,不要求用户重新开始对话。
|
||||
|
||||
#### 零录入报销首个切片
|
||||
|
||||
- 用户在小财管家上传已完成 OCR 的票据并发送后,附件关联任务必须以当前租户、当前员工和 `Expense Case` 为边界,优先寻找已审批申请自动生成的可编辑报销草稿。
|
||||
- 匹配信号首期使用费用事件关系、申请状态、票据日期、行程城市、费用场景和草稿状态;返回结构化分数、置信度和命中原因,不把模型自由文本作为授权依据。
|
||||
- 只有唯一高置信候选允许自动归集。低置信、多个接近候选、申请缺少系统生成草稿、票据已属于其他单据或证据不足时,任务以 `requires_confirmation=true` 成功返回候选和异常,不修改任何报销单或票据关系。
|
||||
- 自动归集成功后直接返回费用事件、申请、报销草稿、归集数量、跳过数量、风险项、缺失项和可解释原因;前端只展示完成结果或“需要确认”异常,不要求用户再次上传同一票据。
|
||||
- 自动归集是 L3 可逆动作,只允许写入草稿、票据关系和业务事件;不自动提交、审批、付款或重建历史遗留申请缺失的草稿。
|
||||
- 相同租户、用户、票据和目标草稿重试必须幂等;已归集到同一草稿的票据计入跳过数,不重复创建费用明细、附件或业务事件。
|
||||
|
||||
#### 移动端
|
||||
|
||||
- 把拍照、相册选票、报销列表、审批列表和 AI 助手接入真实后端。
|
||||
- 支持离线拍票、失败重试和后台上传状态。
|
||||
- 未实现能力必须隐藏或明确标为不可用,不保留无响应的主按钮。
|
||||
|
||||
#### 审批例外工作台
|
||||
|
||||
- 以风险、金额、预算影响、等待时长和证据完整度排序。
|
||||
- 支持批量处理低风险事项,行内保留真实按钮和键盘可访问入口。
|
||||
- 高风险审批展示模型版本、规则版本、政策依据、证据和人工覆盖原因。
|
||||
- 正式审批任务切片由 `/api/v1/approval-tasks` 返回租户安全的数据库分页投影,优先级由风险、SLA、金额和证据完整度共同计算;任务保存节点进入时间、处理人、乐观版本、批量资格和权威动作集合。旧 `/approval-workbench/items` 仍可承担 advisory-only 摘要,但不再作为“待我审核”的业务动作事实源。
|
||||
- 批准、退回和付款均使用动作协议:客户端在确认时冻结 `request_id`、预期状态和预期审批节点;服务端以租户 + 操作人 + 请求 ID 唯一账本、请求指纹、PostgreSQL advisory lock、Claim 行锁和同事务业务事件确保相同请求可安全重放,不同内容或陈旧快照以 409 拒绝。
|
||||
- 高风险门禁同时读取持久化风险观察和尚未落表的原始风险标记;未处置的严重/高危可行动风险在路由、预算和状态修改前阻断,原始重大风险持久化失败时 fail-closed。仅明确标记为 `route_review` 的路由型风险进入对应审批节点而不冒充已解决。
|
||||
- 门禁只消费与当前单据业务阶段一致的显式风险:申请阶段不会被明确标记为 `reimbursement` 的风险阻断,反之亦然;缺少阶段的未知重大风险仍保守处理。`route_review` 是显式路由语义,审批路由可以继续但风险仍保留在证据和后续复核中。
|
||||
- 风险处置采用判定与生命周期双状态:支持确认风险、误报、补件、开始整改、申请豁免、财务/管理层批准或拒绝豁免,以及完成处置;每个动作绑定请求指纹、预期版本、操作人和只追加事件。申请人不得自批/自拒,仅在职 finance/executive 且非申请人可决定,过期申请 fail-closed。完整证据只对管理员或当前审批人开放,风险池对财务/管理/预算角色开放。
|
||||
- 并发锁顺序固定为 Claim → RiskObservation → RiskDisposition,审批动作、人工处置和 Hermes 扫描共用 Claim 锁。扫描在锁外计算、锁内校验状态与更新时间,过期快照丢弃并等待下一轮,避免旧图结果覆盖刚完成的人工处置。
|
||||
- 所有单据动作首次完整 API 响应写入动作账本;风险处置首次完整响应在 append-only 事件 INSERT 前原子写入。相同请求重放只返回首次版本,风险旧事件无快照时也只从目标版本及以前的审计链重建,不读取当前 Claim、当前处置投影或后续事件。
|
||||
- 审批任务支持低风险批量通过、委托/撤销委托、永久转交、顺序加签、并行会签和手动/调度 SLA 升级;SLA 以当前节点进入时间为基准。前端“待我审核”完全消费服务端分页、筛选、`available_actions` 与版本,筛选和页码可从详情返回恢复。尚未完成的主要边界是统一必要性/政策证据摘要、真实企微/钉钉/邮件触达及外部结果回写。
|
||||
|
||||
#### CFO 价值看板
|
||||
|
||||
- 首页优先展示财务确认现金节省、已核验工时价值、安全智能直通率和重大风险护栏。
|
||||
- 支持按部门、项目、费用类型、供应商、城市、时间和费用事件下钻。
|
||||
- 每项节省可打开来源、基线、建议、执行、确认和去重证据。
|
||||
|
||||
#### AI 记忆与自动化设置
|
||||
|
||||
- 用户可以查看“系统记住了什么、为什么记住、在哪里使用”,并支持修改、忘记和关闭个性化。
|
||||
- 企业管理员配置部门/企业记忆边界、有效期、敏感等级和自动化上限。
|
||||
- 首个个人记忆切片只在申请核对表展示“常用出行方式”来源、证据数量和可编辑状态,并提供“忘记此偏好”;不在本轮新建大型设置页,也不把风险画像当作个人偏好。
|
||||
|
||||
### 后端
|
||||
|
||||
#### 费用领域与编排
|
||||
|
||||
- 新增 `ExpenseCaseService` 作为跨阶段编排入口,避免继续扩大万能 `ExpenseClaimService`。
|
||||
- `ExpenseCaseService` 只负责阶段协调,申请、票据、报销、审批、支付、入账、记忆和节省由独立协作者负责。
|
||||
- 复用统一 AI 场景注册与 LangGraph 编排,前端只按后端返回的 plan/action 渲染,不再增加业务门控。
|
||||
- 零录入票据归集由独立匹配器计算候选和证据,由独立关联编排器执行可逆写入;后台 Job 只负责身份绑定、状态保存和结果投影,不继续堆评分、附件和事件逻辑。
|
||||
- 票据存储命名空间必须至少包含租户与用户;后台任务查看和执行同时校验租户及用户,平台管理员也不能跨租户读取任务结果。
|
||||
|
||||
#### 业务事件与 AI 决策
|
||||
|
||||
- 所有关键动作写入 append-only `business_events`,使用 `correlation_id` 串联同一费用事件、Agent run、审批和外部连接器事件。
|
||||
- 业务状态变更与对应 Outbox 事件必须在同一数据库事务中持久化;消费者按事件 ID 幂等处理,避免审计、学习和 ROI 数据因异步失败永久丢失。
|
||||
- 只有画像刷新、分析聚合和消息通知等可重建派生任务允许异步失败并重试,不得把申请、提交、退回、审批、支付和入账事件降级为可丢弃旁路日志。
|
||||
- 每个 AI 输出写入 `ai_decisions`,记录建议值、置信度、证据、模型、Prompt、规则、政策版本、风险等级和自动化模式。
|
||||
- 用户接受、修改、拒绝或忽略写入 `ai_decision_feedback`;审批、退回、付款和审计写入 `workflow_outcomes`。
|
||||
|
||||
#### 记忆与学习
|
||||
|
||||
- `memory_entries` 支持用户、部门、企业作用域,以及 candidate/active/suppressed 状态。
|
||||
- `memory_evidence_links` 保留记忆与业务事件、决策、反馈、结果的来源关系。
|
||||
- 企业制度优先于部门基线,部门基线优先于个人偏好;冲突时返回解释。
|
||||
- 复用既有 AI 数据飞轮的 few-shot、golden case、Prompt 版本、Canary 和回归门禁。
|
||||
- 首个切片使用关系库精确键检索,只允许低敏枚举 `travel_application.transport_mode`;不复用当前缺少完整租户键的 FewShot/Qdrant 或员工风险画像存储个人记忆。
|
||||
- 记忆证据只从认证动作事务中的服务端核验纠正产生,关联 Decision、Feedback、Outcome 和 Expense Case;草稿、客户端观察、重复请求和同一 Case 多次修改不能重复计票。
|
||||
- 个人记忆只填充当前申请的空白软字段;当前明确输入、服务端 HR/组织主数据、企业制度和规则计算结果都优先于个人记忆。每次应用或抑制都返回可解释原因,不把优先级交给 Prompt 或 LLM 自行判断。
|
||||
- 分层记忆首个组织级切片继续只允许 `travel_application.transport_mode` 的“飞机/火车/轮船”。企业记忆由租户管理员显式确认,部门记忆必须绑定稳定的 `organization_unit_id` 并只把部门名称作为展示标签;隐式证据暂只生成个人 Candidate,不允许少量个人行为自动升级为部门或企业制度。
|
||||
- 分层解析顺序固定为“当前显式输入/规则禁用 > 企业记忆 > 部门记忆 > 个人记忆”。不同层值冲突时只应用最高优先级可用项,并返回候选层级、冲突值、未采用原因、有效期和衰减后置信度;不得静默覆盖当前输入,也不得把优先级交给模型自由判断。
|
||||
- 管理员显式确认的组织记忆可直接激活,但必须记录创建人、确认来源、适用作用域和有效期;隐式个人记忆仍使用 3 个不同 Case、2 次审批通过、证据跨 7 天的最小样本门槛。组织记忆过期、撤销或服务异常时降级到下一层,不阻塞申请预览。
|
||||
- 已确认历史案例接入报销预审前,`few_shot_samples` 与 Qdrant payload 必须补齐租户、业务场景、制度/规则标识、规则版本和 active 状态;每次向量命中还必须回关系库按租户和样本状态二次校验,样本改判时删除或稳定覆盖旧向量,禁止跨租户或陈旧结论进入提示。
|
||||
- 历史案例只能输出结构化 `historical_case_evidence`,明确标记 `advisory_only`、人工确认结论、相似度、制度和版本是否匹配;它不得修改确定性预审的 `decision`、`passed`、`blocking_count`,不得改变预算复核和审批路由。旧版本只可作为降权参考,Qdrant 或 Embedding 不可用时返回空证据并继续规则链。
|
||||
|
||||
#### 节省与价值
|
||||
|
||||
- `savings_opportunities` 记录基线、风险暴露、建议动作、预计节省、负责人和截止时间。
|
||||
- `savings_realizations` 记录执行结果、实际节省、财务确认人、证据、归因状态和去重键。
|
||||
- 风险关联金额、暂缓付款和未采纳建议不得自动转为实际节省。
|
||||
|
||||
#### 连接器
|
||||
|
||||
- 定义统一 `ExpenseConnector` 协议,覆盖票据邮箱、税务验真、企业卡、商旅、支付、银行流水、ERP、HR、SSO、企微/钉钉。
|
||||
- 外部事件必须具备幂等键、来源系统、原始事件 ID、发生时间、处理状态、错误码和重试次数。
|
||||
|
||||
#### 商业计量
|
||||
|
||||
- 以租户、套餐、模块和时间窗口记录模型、OCR、文档、分析任务、连接器与人工实施用量。
|
||||
- 成本事件与客户价值事件分开记账,支持计算客户贡献毛利、实施成本摊销和私有部署成本。
|
||||
- 套餐与配额只控制商业权益,不改变安全规则、租户隔离和高风险人工复核边界。
|
||||
|
||||
### 算法与规则
|
||||
|
||||
#### 自动化决策
|
||||
|
||||
- 自动化等级按动作计算,不按 Agent 整体授权。
|
||||
- 动作风险、模型置信度、证据完整度、历史命中率、金额阈值、可逆性、企业上限和抽检率共同决定是否执行。
|
||||
- L0 只读解释;L1 建议;L2 预填;L3 可逆自动化;L4 低风险直通;L5 资金支付和高风险决策始终保留人工。
|
||||
|
||||
#### 记忆激活
|
||||
|
||||
- 明确偏好后续可由用户认证确认后立即激活;首个切片只实现隐式候选,不把普通字段编辑冒充明确授权。
|
||||
- 隐式出行方式偏好需要同租户、同员工、同场景和同归一化值的 3 个不同 Expense Case 服务端核验纠正,其中至少 2 个出现 `application_approved`,且首尾证据至少跨 7 天;同一 Case、重试、草稿续签只计一票。
|
||||
- `accepted`、`client_observed`、`draft_saved` 和单纯 `application_submitted` 只能作为观察信息,不能单独激活;退回、反向纠正、结果 reversed 或制度冲突会抑制现有记忆。
|
||||
- Candidate 默认 90 天有效,Active 默认 180 天;状态支持 `candidate / active / suppressed / expired / revoked`。相反值先抑制旧记忆并建立新候选,不原地覆盖;用户“忘记”后立即 revoked,清除可恢复值且不可自动复活。
|
||||
- 错误标注、违规习惯、一次性例外、自由文本和敏感字段不能直接变为默认记忆。
|
||||
|
||||
#### 风险与预审
|
||||
|
||||
- 统一输出事实、规则、证据、风险、建议和可执行修复动作。
|
||||
- 报销提交必须先完成服务端预审握手。预审结果至少包含稳定 `review_id`、输入指纹、规则集指纹、流水线版本、`ready / needs_fix / ready_with_review` 决策、结构化发现和修复动作;客户端不得仅凭旧的 `passed` 标记提交。
|
||||
- 输入字段、明细、票据、风险事实或规则集发生变化时,旧预审自动失效并在提交事务内重算;同一输入和规则集重复预审复用相同 `review_id`,避免生成重复事件。
|
||||
- 只有 high/critical 且 `fixable_by_submitter` 的未解决风险阻断提交;预算治理、领导判断和财务复核风险随单进入对应审批角色,不把所有高风险简单放行或全部拦截。
|
||||
- `needs_fix` 必须在预算占用和 `claim_submitted` 之前阻断,保持草稿状态并返回 HTTP 409 结构化整改信息;可提交结果的预审事件与提交事件共享 correlation,并通过 causation 形成可追溯链路。
|
||||
- 使用已确认正/负样本校准误报与漏检;规则或 Prompt 发布前运行 golden case。
|
||||
- 新策略先进入 shadow,再 Canary,最后按动作开放自动化。
|
||||
|
||||
#### 费用分析与节省
|
||||
|
||||
- 描述性分析回答“发生了什么”;诊断分析回答“为什么”;建议分析回答“做什么”;价值闭环回答“是否执行并产生了多少结果”。
|
||||
- 供应商、费用类型、城市、项目和部门基线必须保存数据窗口、样本量、算法版本和政策版本。
|
||||
- 节省归因必须区分现金节省、可释放工时价值和不可货币化效率改善。
|
||||
|
||||
### 数据与契约
|
||||
|
||||
#### 核心数据对象
|
||||
|
||||
- `expense_cases`:统一费用事件和当前阶段。
|
||||
- `expense_case_links`:连接申请、报销、票据、审批、付款、凭证和外部对象。
|
||||
- `business_events`:append-only 业务事件账本。
|
||||
- `ai_application_preview_decisions`:申请落单前由服务端重新规范化并短期签发的预览建议;不创建空 Expense Case,不保存建议明文,动作成功后一次消费。
|
||||
- `ai_decisions`:结构化 AI 建议与版本证据。
|
||||
- `ai_decision_feedback`:接受、修改、拒绝和忽略。
|
||||
- `workflow_outcomes`:退回、补件、审批、付款、审计和最终结果。
|
||||
- `memory_entries`:按租户、作用域主体、场景、字段和值指纹保存候选/激活/抑制/过期/撤销状态,只允许白名单低敏值可恢复。
|
||||
- `memory_evidence_links`:把记忆与服务端 Decision、Feedback、Outcome、Expense Case 和业务结果关联;唯一键保证同一 Case 对同一候选最多计一票。
|
||||
- `automation_policies` / `automation_grants`:动作级自动化策略和授权。
|
||||
- `profile_baseline_snapshots`:持久化员工、部门、供应商、费用和流程基线。
|
||||
- `savings_opportunities` / `savings_realizations`:节省机会和实现结果。
|
||||
- `usage_meter_events`:租户、模块、模型、OCR、文档和分析用量。
|
||||
|
||||
#### 零录入附件关联任务契约
|
||||
|
||||
- 保留现有 `status`、`message`、`claim_id`、`claim_no`、`uploaded_count`、`skipped_count` 字段,追加字段保持前端向后兼容。
|
||||
- 任务追加 `resolution`、`requires_confirmation`、`expense_case_id`、`application_claim_id`、`application_claim_no`、`confidence`、`confidence_score`、`match_reasons`、`exceptions`、`missing_fields`、`risk_items` 和 `candidates`。
|
||||
- `status=succeeded` 只表示匹配流程完成;`resolution=auto_associated` 表示已经完成可逆归集,`resolution=requires_confirmation` 表示没有业务写入,需要用户选择候选或处理异常。
|
||||
- 候选最少返回目标类型、费用事件 ID、申请 ID/编号、草稿 ID/编号、分数、置信度和命中原因;不得返回其他租户或当前员工数据范围之外的候选。
|
||||
- `receipt_received` 和 `attachment_associated` 使用票据 ID 与目标草稿组成稳定幂等键,并与票据 Link、附件写入在同一数据库事务中完成;若文件存储写入失败,数据库事务回滚且任务进入失败态。
|
||||
|
||||
#### 最小事件词典
|
||||
|
||||
- `expense_case_created`
|
||||
- `historical_claim_imported`:迁移前旧单的当前快照;只证明已纳入统一费用事件,不重建或伪造迁移前审批历史。
|
||||
- `application_generated` / `application_submitted` / `application_approved`
|
||||
- `application_pre_review_completed` / `claim_pre_review_completed`
|
||||
- `receipt_received` / `receipt_verified` / `ocr_corrected`
|
||||
- `field_suggested` / `field_accepted` / `field_edited` / `field_rejected`
|
||||
- `attachment_associated` / `draft_saved` / `claim_submitted`
|
||||
- `claim_returned` / `supplement_completed`
|
||||
- `risk_flagged` / `risk_confirmed` / `risk_false_positive`
|
||||
- `route_suggested` / `route_overridden`
|
||||
- `claim_approved` / `payment_requested` / `payment_completed`
|
||||
- `accounting_entry_created` / `reconciliation_completed`
|
||||
- `saving_opportunity_created` / `saving_action_completed` / `saving_confirmed`
|
||||
|
||||
#### 状态枚举
|
||||
|
||||
- Expense Case:`planning` / `approved_to_spend` / `spending` / `claiming` / `reviewing` / `paying` / `accounting` / `closed` / `cancelled`。
|
||||
- AI Decision:`suggested` / `accepted` / `edited` / `rejected` / `ignored` / `executed` / `rolled_back`。
|
||||
- Memory:`candidate` / `active` / `suppressed` / `expired` / `revoked`。
|
||||
- Automation:`explain` / `recommend` / `prefill` / `reversible_auto` / `low_risk_straight_through` / `human_only`。
|
||||
- Savings:`identified` / `accepted` / `in_progress` / `realized` / `verified` / `rejected` / `expired`。
|
||||
|
||||
#### 兼容策略
|
||||
|
||||
- 迁移初期可用 shadow 事件校验现有 `ExpenseClaim` 的映射;某个状态一旦正式纳入事件模型,其业务写入与 Outbox 事件必须进入同一事务。
|
||||
- 迁移前已有 `ExpenseClaim` 通过显式维护命令写入一条 `historical_claim_imported` 快照事件:事件发生时间使用真实回填时间,原创建、发生、提交和更新时间只进入内部 payload;`history_reconstructed=false`,不得按当前状态反推并伪造历史提交、审批或付款动作。
|
||||
- 历史回填默认 dry-run,必须显式提供租户、创建时间边界和数据库目标;apply 还要求精确目标确认、迁移 head、advisory lock 和分批事务。稳定幂等键、源快照指纹、已有 Link 跳过及孤立 Event 冲突拒绝共同保证可审计重跑。
|
||||
- 历史快照事件使用 `delivery_status=suppressed`,供时间线和分析读取,但不进入实时 Outbox 投递;用户态 API 仍只返回事件白名单,不暴露回填批次、源指纹、幂等键和投递状态。
|
||||
- 旧 `ReimbursementRequest` 进入只读兼容和迁移状态,停止新增第二套业务编排。
|
||||
- 现有 `risk_flags_json` 保持读取兼容,新审批、付款、归档和关系事件写入结构化表。
|
||||
- API 新字段优先追加,不在同一阶段破坏现有前端契约。
|
||||
- 从未成功签发 `decision_id` 的旧申请预览,或签发接口失败且服务端没有当前会话有效决策时,保存/提交继续走现有 `client_observed` 路径;当前会话一旦存在有效服务端决策,省略 `decision_id` 必须拒绝,不能主动降级绕过核验。兼容路径不得在用户已经编辑后把最终值反向登记成“原始建议”。
|
||||
- 新预览使用 30 分钟有效的服务端 `decision_id`。保存草稿或提交成功后一次消费;保存草稿会尽力签发基于已保存事实的新 `decision_id`,供后续提交继续核验,但续签属于业务提交后的派生能力,续签失败不能把已成功保存返回为失败。相同动作请求 ID、动作和最终指纹允许安全重放,其他重放拒绝。
|
||||
- AI 工作台、小财管家和通用 Orchestrator 的结构化申请预览统一复用服务端签发/消费工作流。小财管家签发失败时保持可编辑和可重试,但保存、提交必须 fail-closed,不再降级到旧 Orchestrator 副作用;只有没有结构化预览的历史消息保留旧兼容入口。
|
||||
- Orchestrator 将已签发 decision 保存在服务端会话状态,后续保存/提交只消费该状态,不采信浏览器回传的 preview 或 decision。保存草稿返回的新 decision 继续写回会话,服务端消息中的结构化预览可在刷新或换端后恢复。
|
||||
- 迁移桥接期集中维护 migration-owned 表集合;所有 legacy bootstrap 只能创建集合之外的表。标准启动在 `alembic upgrade head` 前只读核对 revision 与自有表集合,发现漂移时拒绝启动,不自动猜测、stamp 或修复。
|
||||
- 所有表结构通过 Alembic 迁移,不继续在请求路径执行 DDL。
|
||||
|
||||
#### 版本与审计
|
||||
|
||||
- 事件记录 actor、tenant、source、run、model、Prompt、rule、policy 和 schema 版本。
|
||||
- 记忆、自动化策略、规则、Prompt 和基线均支持 supersede、有效期和回滚。
|
||||
- 高风险人工覆盖必须记录原因、证据和审批主体。
|
||||
|
||||
### 权限与安全
|
||||
|
||||
- 登录后签发服务端可验证会话或 JWT,服务端从会话和数据库解析用户、租户、角色和数据范围。
|
||||
- 移除客户端 `X-Auth-*` 作为授权事实来源;管理面、Bootstrap、设置和模型连通性接口必须受平台管理员保护。
|
||||
- P0 采用不透明 Bearer 会话:明文 token 仅在登录成功时返回,数据库只保存 SHA-256 摘要;每次请求从服务端会话和员工数据重新解析当前身份与角色,过期、撤销或不存在的 token 统一拒绝。
|
||||
- Web 端只在 `sessionStorage` 保存 token 和过期时间,普通请求、流式请求及页面关闭收尾统一携带 Bearer;任一 `401` 触发本地会话清理,登出时会话指标与 token 撤销在同一事务完成。
|
||||
- 已初始化系统的 Bootstrap 状态只返回脱敏信息,Bootstrap 写入、系统设置、模型连通性、缓存、审计与系统日志等敏感管理面由平台管理员权限保护;Vite 本地 Setup 桥在初始化完成后锁定重新配置入口。
|
||||
- P0 数据契约即引入最小 `tenant_id`、数据库约束、行级过滤、向量库命名空间和对象存储前缀隔离;删除传播、数据导出和私有部署加固可在商业化阶段继续完善。
|
||||
- Expense Case 用户态查询只返回流程摘要:Case 基础状态、关系类型、事件类型、操作人、发生时间及白名单业务载荷;不返回关联资源 ID、幂等键、correlation、causation、聚合标识或 Outbox 投递状态。有权查看入口单据的本人、当前审批人、财务和管理员可查看整 Case 的安全摘要,无权限与跨租户查询继续以 404 隐藏资源存在性。
|
||||
- 申请预览签发与消费同时绑定 `tenant_id`、actor、Bearer 登录会话和会话 ID;动作入口按这些服务端事实锁行校验,不能只凭随机 UUID 授权。浏览器提供的模型来源、`finalValue`、用户和角色声明均不作为可信事实。
|
||||
- 个人记忆主体优先使用服务端 `employee_id`,缺失时才使用规范化用户名;所有唯一键、读取、证据外键和忘记动作都必须以 `tenant_id` 开头。同邮箱或同员工号在不同租户形成完全独立的记忆。
|
||||
- 记忆值采用 deny-by-default 白名单;首个切片仅保存规范枚举“飞机/火车/轮船”。事由、地点、日期、金额、客户/项目自由文本、附件、银行卡、收款人与支付账户均不得进入记忆明文、Prompt、向量 payload 或日志。
|
||||
- 外部 `/orchestrator/run` 的用户消息、定时任务和系统事件都必须携带有效登录会话,不能由请求体中的 `source` 自行声明可信内部来源;其中 `schedule` / `system_event` 只允许平台管理员触发。服务端使用登录态覆盖用户、租户、角色、管理员、员工、审批人和调度操作人别名,并移除客户端 preview/decision 状态。最近会话查询与删除只使用当前登录用户和租户,查询参数中的 `user_id` 仅保留协议兼容,不参与授权。
|
||||
- 预览与学习账本中的字段值使用独立、可版本化的费用申请密钥计算 HMAC-SHA256 指纹,密钥目录和文件权限分别为 `0700`、`0600`;核验旧决策必须使用其签发版本,缺失版本直接拒绝,不静默生成替代密钥。字段指纹同时编码“字段是否存在”,避免新增或删除字段被误判为采纳;训练资格仍保持关闭,直至审批、付款或人工核验闭环完成。
|
||||
- 自动化权限按动作、金额、场景、风险和有效期授予,不使用全局“允许 Agent 自动执行”开关。
|
||||
- 收款账户变更、资金支付、制度发布、高风险驳回和敏感主数据变更执行双人或更高等级复核。
|
||||
|
||||
### 降级策略
|
||||
|
||||
- LLM 不可用:回退到规则和人工填写。
|
||||
- OCR 不可用:文件保留并进入待识别队列,用户可手工补录。
|
||||
- Qdrant/few-shot 不可用:使用 stable Prompt 和基础规则,不阻塞主流程。
|
||||
- 历史案例证据只返回固定的脱敏标签与摘要,不返回样本 ID、费用单号、人工评论或历史结论原文;它只能辅助复核,不参与确定性规则、阻断数量、预算复核和审批路由计算。
|
||||
- 外部支付/ERP/税务连接器不可用:事件进入 retryable 状态,保留人工处理入口和幂等键。
|
||||
- 记忆冲突或可信度不足:只展示建议,不自动应用。
|
||||
- 结构化预览签发超时或不可用:允许继续编辑和重新签发,但保存、提交保持 fail-closed;只有不存在结构化预览的历史兼容入口保留原有降级路径,且只能记录 `client_observed` 行为证据。
|
||||
- 记忆服务不可用、过期或校验失败:回退为“无个人记忆 + 当前规则”,不阻塞预览,也不读取跨租户、旧缓存或无租户向量样本。
|
||||
- Savings 证据不足:保留机会状态,不进入已实现节省。
|
||||
|
||||
## 算法与公式
|
||||
|
||||
### 自动化资格分数
|
||||
|
||||
```text
|
||||
automation_score
|
||||
= w1 * model_confidence
|
||||
+ w2 * evidence_completeness
|
||||
+ w3 * historical_precision
|
||||
+ w4 * reversibility
|
||||
- w5 * action_risk
|
||||
- w6 * amount_risk
|
||||
```
|
||||
|
||||
变量说明:
|
||||
|
||||
- `model_confidence`:模型或规则对当前建议的置信度。
|
||||
- `evidence_completeness`:发票、申请、预算、合同、订单和政策证据完整度。
|
||||
- `historical_precision`:相同租户、场景、动作和版本的历史正确率。
|
||||
- `reversibility`:动作是否可以无损撤销或回滚。
|
||||
- `action_risk`:动作类型风险,支付和制度发布最高。
|
||||
- `amount_risk`:金额相对企业阈值和历史基线的风险。
|
||||
- `w1...w6`:按企业和动作配置的权重。
|
||||
- 适用边界:分数只决定候选自动化等级,仍需满足企业硬性白名单、金额上限、抽检率和人审要求。
|
||||
|
||||
### 记忆激活置信度
|
||||
|
||||
```text
|
||||
memory_confidence
|
||||
= consistent_evidence_weight
|
||||
+ outcome_success_weight
|
||||
+ explicit_confirmation_weight
|
||||
- conflict_weight
|
||||
- age_decay
|
||||
```
|
||||
|
||||
变量说明:
|
||||
|
||||
- `consistent_evidence_weight`:多次一致修改或选择产生的证据。
|
||||
- `outcome_success_weight`:建议最终一次通过、付款或审计确认的权重。
|
||||
- `explicit_confirmation_weight`:用户或管理员明确确认的权重。
|
||||
- `conflict_weight`:与企业制度、部门规则或其他记忆冲突的惩罚。
|
||||
- `age_decay`:记忆随时间衰减。
|
||||
- 适用边界:敏感信息、一次性例外和违规行为不得依赖分数自动激活。
|
||||
|
||||
### 安全智能直通率
|
||||
|
||||
```text
|
||||
safe_straight_through_rate
|
||||
= qualified_completed_cases_without_manual_correction_or_return
|
||||
/ eligible_completed_cases
|
||||
```
|
||||
|
||||
分子还必须满足必要审批完成,且事后抽样审计未发现重大问题。
|
||||
|
||||
### 客户可验证 ROI
|
||||
|
||||
```text
|
||||
verified_value
|
||||
= verified_cash_savings
|
||||
+ verified_releasable_labor_value
|
||||
|
||||
customer_roi
|
||||
= (verified_value - customer_total_cost) / customer_total_cost
|
||||
```
|
||||
|
||||
变量说明:
|
||||
|
||||
- `verified_cash_savings`:客户财务确认的重复支付阻止、超标准调整、采购或预算优化等实际现金节省。
|
||||
- `verified_releasable_labor_value`:基于上线前后人工分钟、单据量和角色完全成本计算,并经客户认可的工时价值。
|
||||
- `customer_total_cost`:订阅、用量、实施、集成和客户内部运营成本。
|
||||
- 适用边界:现金节省和工时价值分开披露;风险暴露、未采纳建议和暂缓付款不得计入。
|
||||
|
||||
### 客户贡献毛利
|
||||
|
||||
```text
|
||||
customer_contribution_margin
|
||||
= subscription_revenue
|
||||
+ usage_revenue
|
||||
+ module_revenue
|
||||
+ verified_savings_share
|
||||
- llm_ocr_storage_cost
|
||||
- third_party_cost
|
||||
- support_cost
|
||||
- amortized_implementation_cost
|
||||
```
|
||||
|
||||
用于判断高收入但高度定制或私有部署客户是否实际盈利。
|
||||
|
||||
## 测试方案
|
||||
|
||||
### 后端
|
||||
|
||||
- Expense Case 状态机、关系绑定、幂等和非法状态跃迁单元测试。
|
||||
- 历史 ExpenseClaim 回填的 dry-run 零写、租户/时间边界、快照真实性、批次回滚、冲突拒绝、重复执行和数据库目标防误连测试。
|
||||
- Business Event、AI Decision、Feedback、Outcome、Memory、Automation Policy、Savings Ledger service 单元测试。
|
||||
- 服务端会话、租户隔离、角色和动作权限的正向/越权测试。
|
||||
- 连接器事件幂等、重试、回执、失败恢复和重复支付防护测试。
|
||||
- Alembic baseline、升级、回滚边界和旧数据迁移测试。
|
||||
- 现有报销、预算、风险、知识库和 Agent 回归测试。
|
||||
|
||||
### 前端
|
||||
|
||||
- 费用事件时间线、异常确认、断点续办和操作反馈视图模型测试。
|
||||
- 移动拍票、上传失败恢复、真实列表和审批 mutation 测试。
|
||||
- 审批例外工作台键盘操作、焦点管理和权限态测试。
|
||||
- CFO 价值看板对预计/实际/确认节省的展示和下钻测试。
|
||||
- AI 记忆查看、修改、忘记和关闭个性化测试。
|
||||
- 生产构建、lint、typecheck 和浏览器关键流程测试。
|
||||
|
||||
### 算法与规则
|
||||
|
||||
- 自动化分数、硬阈值、金额上限、动作白名单和抽检策略测试。
|
||||
- 记忆候选、激活、冲突、过期、撤销和污染样本测试。
|
||||
- 风险规则、Prompt、few-shot 和 golden case 回归门禁测试。
|
||||
- Savings 去重、基线、归因和确认状态测试。
|
||||
|
||||
### 集成
|
||||
|
||||
- 一句话申请 → 票据归集 → 自动报销 → 预审 → 审批 → 付款事件 → 入账 → 归档端到端。
|
||||
- 持久开发库只读流式克隆 → Alembic 升级 → 历史回填 dry-run/apply/重复 apply → 服务端登录 → 旧单时间线查询 → 持久库不变验证。
|
||||
- AI 建议 → 用户修改 → 退回/通过 → 记忆候选 → 下次建议变化闭环。
|
||||
- 常用出行方式 1/2 次纠正保持候选,第 3 个不同 Case 且至少 2 次审批通过、跨 7 天后激活;当前输入覆盖、跨租户拒绝、忘记后不再预填。
|
||||
- 风险命中 → 人工确认/误报 → few-shot → 新版本回放 → Canary/回滚闭环。
|
||||
- 节省机会 → 负责人执行 → 实际结果 → 财务确认 → ROI 看板闭环。
|
||||
- 多租户同名员工、同号单据、向量检索和对象存储隔离测试。
|
||||
|
||||
### 容器验证
|
||||
|
||||
所有后端、集成、数据库迁移和外部依赖验证必须在项目容器内执行,单条测试命令最大超时 60s:
|
||||
|
||||
```bash
|
||||
docker exec -w /app -e SERVER_VENV_DIR=/tmp/x-financial-server-venv \
|
||||
x-financial-local-linux \
|
||||
/tmp/x-financial-server-venv/bin/pytest -q server/tests/test_expense_case_service.py
|
||||
```
|
||||
|
||||
如果本地 Compose 使用不同容器名,先通过 `docker ps` 确认当前主应用容器,不得改为宿主机直接运行后端测试。
|
||||
|
||||
### 手工验证
|
||||
|
||||
- 在真实容器页面完成申请、拍票、报销、退回补件、审批、付款和价值看板流程。
|
||||
- 检查每个 AI 建议是否能展示来源、置信度、修改结果和后续业务结果。
|
||||
- 检查用户能否查看、修改和忘记 AI 记忆。
|
||||
- 检查高风险动作无法绕过人工,低风险可逆动作能够撤销和回放。
|
||||
- 检查每项实际节省能够追溯到基线、建议、执行、确认人和证据。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
以下目标在缺少真实客户基线前均为首轮试点的方向性目标,正式数值必须在试点基线采集后冻结。
|
||||
|
||||
- [A1] 全流程验收:任一费用事件可以从申请追溯到票据、报销、审批、付款、凭证、归档、AI 决策和节省结果。
|
||||
- [A2] 体验指标:首个试点场景报销创建时间 P50 目标不超过 60 秒。
|
||||
- [A3] 自动化指标:可结构化字段的自动填充率方向性目标不低于 85%,且字段修改率持续下降。
|
||||
- [A4] 质量指标:首次提交完整率方向性目标不低于 90%,退回率和每单人工触点低于试点基线。
|
||||
- [A5] 智能指标:AI 字段采纳率、重复纠正率、建议接受率和记忆复用成功率可按租户、场景、版本统计。
|
||||
- [A6] 风险护栏:重大风险漏检率、误报干扰率、人工覆盖率和抽样审计结果可计算;自动化提升不得以护栏恶化为代价。
|
||||
- [A7] 价值指标:每项节省区分机会、预计、执行、实现和财务确认;客户月度 ROI 可回放。
|
||||
- [A8] 商业指标:早期可计算付费试点转年度合同率、持续产生可验证价值的客户率和按客户贡献毛利;成熟后计算 NRR。
|
||||
- [A9] 性能指标:同步页面接口 P95、AI 预填 P95、OCR 队列等待、后台任务恢复和连接器重试达到试点 SLA,具体阈值在基线后确定。
|
||||
- [A10] 安全指标:客户端无法伪造管理员、跨租户访问返回拒绝、敏感动作具备二次授权和审计。
|
||||
- [A11] 可观测性:每个费用事件、AI 决策、工具调用、连接器事件、自动化执行和失败重试使用统一 correlation ID。
|
||||
- [A12] 工程验收:相关后端定向测试、前端测试、lint、typecheck、构建和端到端验证在容器事实环境中通过。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
### 风险
|
||||
|
||||
- 范围过大:费用闭环、AI 学习、价值分析和商业化不能同时全量实现,需要按 P0/P1/P2 阶段交付。
|
||||
- 领域模型迁移:旧 `ReimbursementRequest`、`ExpenseClaim` 和 JSON 状态并存,必须旁路记录、小步迁移和双读校验。
|
||||
- 历史证据边界:旧单快照只表达回填时可确认的当前状态,迁移前逐节点办理过程仍以原单据和既有审计为准;后续分析不得把快照事件误当成历史审批事实。
|
||||
- 认证和租户:当前客户端身份头不适合自动化和 SaaS,多租户、记忆和高风险动作开发前必须修复。
|
||||
- 会话运维:不透明会话已经替代客户端身份头,但仍需补充定时清理、活跃会话查看/全部退出、密钥轮换策略、登录限流和企业 SSO;当前 `tenant_id` 仍是最小契约,不代表跨租户查询守卫已经完成。
|
||||
- Agent 会话租户边界:Orchestrator 与 Steward 动作运行时都把可信 `tenant_id` 写入会话状态;创建、恢复、幂等检查点重放、单条删除和批量删除同时校验租户与用户名,同名用户不能跨租户复用 decision 或动作结果。历史无租户状态的会话在认证入口 fail-closed,不会被恢复或删除。`agent_conversations` 仍缺少独立 `tenant_id` 列和数据库复合约束,后续迁移需要把当前应用层守卫下沉为结构化租户键。
|
||||
- 费用事件读取边界:用户态精简 DTO 和首批 HTTP 权限测试已完成;剩余风险是同一 URL 若已有外部客户端依赖旧内部字段会产生契约变更,且未来新增敏感 payload 字段必须继续显式进入白名单,不能恢复任意字典透传。
|
||||
- 反馈投毒:一次点击或违规习惯不能直接成为记忆,需要候选态、最小样本、制度约束和结果权重。
|
||||
- 记忆值恢复:现有学习账本只保存 HMAC 指纹,无法恢复具体偏好值;必须由独立记忆表在认证事务中只提取白名单枚举,不能为了复用把自由文本重新塞回账本。
|
||||
- 记忆与制度冲突:首个切片的三种交通方式当前均只有估价和舱等规则,没有“禁止某种交通方式”的制度维度;后续一旦增加按职级、路线或金额禁用交通方式的规则,必须先提供服务端允许性判定、抑制原因和回归用例,再允许个人记忆参与预填。
|
||||
- 自动化失控:高准确率不代表高风险动作可以无人值守,必须按动作授权并支持 shadow、Canary、抽检和回滚。
|
||||
- 虚假节省:风险暴露金额、暂缓付款和工时估算容易被夸大,必须由客户财务确认并执行去重。
|
||||
- 外部依赖:税务、支付、银行、商旅、ERP 和消息平台连接器存在可用性、资质和交付周期风险。
|
||||
- 移动端成熟度:拍票基础已有,但主业务仍有 mock,必须先完成真实登录、列表、草稿和审批闭环。
|
||||
- 成本失控:高频 LLM、OCR、向量检索和私有部署可能降低毛利,需要按租户和模块计量成本。
|
||||
- 数据稀疏:本地开发库缺少报销、反馈、风险和 golden 样本,正式目标必须来自真实试点而不是演示数据。
|
||||
|
||||
### 已处理依赖
|
||||
|
||||
- 已有预算、票据夹、OCR、报销草稿、风险规则、审批路由、知识库、员工画像、Agent trace、few-shot 和财务看板可复用。
|
||||
- 已有 AI 数据飞轮概念与阶段 1 few-shot 实现,不重复建设样本检索底座。
|
||||
- 已有统一门控管道设计,可作为 AI 场景收口依据。
|
||||
|
||||
### 待确认
|
||||
|
||||
- 首个目标客户规模和部署形态:中小 SaaS、中大型企业 SaaS 或集团私有部署。
|
||||
- 首个 90 天付费试点场景:差旅报销、业务招待、日常费用或预算风控。
|
||||
- 是否进入企业支付、商旅预订或公司卡领域;如果不进入,应明确聚焦 AI 费控与经营分析。
|
||||
- 客户是否接受工时价值进入 ROI,以及对应角色完全成本口径。
|
||||
- L3/L4 自动化允许的动作、金额阈值、抽检率和授权主体。
|
||||
- 税务验真、银行、ERP、企微/钉钉等连接器的首批合作范围。
|
||||
- SaaS 定价的员工档位、包含用量、超额计价和增值模块边界。
|
||||
|
||||
### 降级策略
|
||||
|
||||
- 首期只选择一个费用场景做完整闭环,其他场景保留现有流程。
|
||||
- 正式切换前允许 shadow 事件用于一致性校验;正式切换后的关键业务状态与 Outbox 事件必须原子提交,失败时整体回滚并向用户返回可重试状态。
|
||||
- 画像刷新、分析聚合、消息通知等可重建派生任务失败时进入重试/死信队列,不阻塞已成功提交的关键业务状态。
|
||||
- 自动化默认停留在 L1/L2,只有试点评估和风险护栏通过后才逐项开放 L3/L4。
|
||||
- 外部连接器未完成前保留人工导入、确认和对账入口,但状态语义和审计事件按正式契约记录。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-16(审批任务正式化):新增 `approval_tasks` 与 append-only `approval_task_events`,报销提交、批准、退回和下一节点推进统一生成/完成任务。支持数据库分页筛选、可解释优先级、当前节点 SLA、委托/撤销、转交、顺序加签、并行会签、手动/调度升级、独立事务批量部分成功、乐观版本和不可变幂等响应;历史回填默认 dry-run。
|
||||
- 2026-07-16(风险豁免决定):豁免申请新增范围、条件与到期时间,只有在职 finance/executive 且非申请人可批准或拒绝;过期 fail-closed。风险变更在同一事务刷新开放审批任务的风险、证据、批量资格和优先级,不重置 SLA 窗口;前端展示完整追加审计链并在到期时刷新权威投影。
|
||||
- 2026-07-16(审批工作台接入):单据中心“审核单”切换到 `ApprovalTaskWorkspace`,移除旧 Claim 预览列表和静态“批量通过 23 条”提示。工作台提供真实批准、退回、委托、转交、加签、会签、升级和批量动作;未筛选 pending 摘要与队列筛选结果分离,详情返回恢复任务筛选/页码,相同负载安全重试、编辑负载自动换新幂等键。
|
||||
- 2026-07-16(阶段验证):容器内相关后端 `122 passed, 4 skipped`、前端审批/单据中心/风险专项 `60 passed`、Ruff 与 Vite 构建通过;一次性 PostgreSQL 17 空库迁移循环 `1 passed`、审批任务和风险并发 `3 passed`。完整前端套件未新增真实回归,仍有 31 项既有陈旧断言/质量债待后续硬化阶段清理。
|
||||
- 2026-07-16(审批动作协议):批准、退回和付款统一接入租户化幂等账本、请求指纹、乐观状态/节点前置条件、advisory lock 与 Claim 行锁;完全相同的网络重试不重复扣减预算、写审批、生成事件或付款,不同内容和陈旧状态稳定返回 409。前端确认流在用户确认时冻结请求和前置条件,并保持服务端状态与展示标签分离。
|
||||
- 2026-07-16(风险门禁与处置):高危/严重可行动风险在任何审批副作用前阻断;类型化风险处置覆盖确认、误报、补件、整改、豁免申请和完成处置,具备乐观版本、权限守卫和只追加审计。门禁合并持久观察与未匹配原始风险,重大观察落库失败 fail-closed;审批、处置和 Hermes 扫描通过 Claim 公共锁协调,扫描过期快照不会覆盖新事实。
|
||||
- 2026-07-16(审批工作台):审批中心接入可解释优先级投影,展示风险、预算、金额、等待时长、证据完整度和 advisory-only AI 建议;风险证据卡读取真实持久字段、显示生命周期和明确动作,但不提供绕过风险门禁的直接审批入口。
|
||||
- 2026-07-16(审批与风险验证):容器内审批/风险/费用服务组合 174 项、迁移/所有权 54 项、前端 73 项及 Vite 生产构建通过;一次性 tmpfs PostgreSQL 17 完成 13 项迁移和 1 项真实并发验证,临时数据库自动清理且持久开发库未修改。变更 Python 文件 Ruff F/I 与 `git diff --check` 通过。
|
||||
- 2026-07-16(严格响应重放):修复审批和风险动作只保证副作用幂等、却在重放时返回当前投影的问题。审批账本保存完整 `ExpenseClaimRead`;风险 append-only 事件通过 `20260716_0012` 保存完整处置响应。单据或风险后续推进后,旧请求只返回首次快照和当时事件,不暴露后续状态;历史无快照事件按目标版本安全重建。门禁同步增加申请/报销阶段匹配和显式 `route_review` 路由语义。容器内审批、费用 Case、路由、风险和严格重放组合 182 项通过,一次性 PostgreSQL 17 迁移与并发共 17 项通过。
|
||||
- 2026-07-16(分层组织记忆):`travel_application.transport_mode` 已形成当前输入/规则 > 企业 > 部门 > 个人的解析链。租户管理员可维护 30-365 天有效的企业/部门低敏记忆;同级冲突 fail-closed,低级覆盖、过期和撤销均返回可解释但脱敏的状态。创建、更新、撤销使用作用域锁、稳定幂等键和请求指纹,响应丢失可安全重放,请求内容变化返回冲突。
|
||||
- 2026-07-16(租户化历史案例):风险观测、人工反馈、few-shot 关系数据和 Qdrant 向量统一绑定租户、场景、制度和规则版本;Hermes 按租户分别构建图与历史,风险规则生成不再回落到默认租户。报销预审与 AI 助手接入只读 `historical_case_evidence`,旧版本显式标 stale,检索故障降级为空证据。
|
||||
- 2026-07-16(历史证据隐私边界):公开协议仅返回“历史已确认/历史误报,仅供复核”的固定摘要,不暴露样本 ID、费用单号、人工评论和历史结论原文;历史命中不改变 review ID、规则 findings、passed、blocking count、预算复核或审批路由。
|
||||
- 2026-07-16(分层学习验证):容器内分层与组织记忆 44 项、历史案例/风险/预审/规则生成 90 项、报销接口与费用服务 137 项、Golden 与历史注入 28 项、迁移/模型/租户组合 70 项通过;前端 18 项和 Vite 生产构建通过。一次性 tmpfs PostgreSQL 17 完整迁移循环 9 项通过,临时容器自动清理且开发数据库未修改;变更 Python 文件 Ruff F/I、compileall 与 `git diff --check` 通过。
|
||||
- 2026-07-16(P0 提交前预审握手):新增稳定预审决策契约、输入/规则/动态 findings 指纹和结构化整改动作;申请与报销提交前均由服务端重新计算完整风险上下文,可整改重大风险在预算占用前返回 409,握手期间风险变化返回 `PRE_REVIEW_CHANGED`,预算治理和人工判断风险继续进入领导与 P8。
|
||||
- 2026-07-16(P0 费用事件续接):新增 `application_pre_review_completed` / `claim_pre_review_completed` 时间线语义,预审与提交共享 correlation/causation;票据归集完成后刷新预审,申请批准生成空报销草稿时 Expense Case 保持 `approved_to_spend`,首张票据归集后再进入 `claiming`。
|
||||
- 2026-07-16(租户与路由修复):AI 草稿、申请预览、Steward 和差旅测算透传真实租户;Claim 核心访问和关联草稿后台任务按 CaseLink 与服务端租户隔离;仅未整改的人工判断/预算治理高风险申请进入同部门 P8,已解决风险不再重复升级。
|
||||
- 2026-07-16(结构化风险回填):409 findings 保留原始 source、business stage、risk domain 与 actionability,前端即使详情刷新失败也能生成可见整改卡,不再被 `ai_pre_review` 汇总过滤规则丢弃。
|
||||
- 2026-07-16(工程拆分):提取申请关联、职级标准调整、预审端点、风险清单指纹与租户查询范围职责,所有本轮触及的核心服务、端点和前端提交模块均回到 800 行硬上限以内。
|
||||
- 2026-07-16(验证):容器内费用服务与审批路由 126 项、费用事件/接口/租户/票据关联 63 项、报销接口全量 20 项和前端预审/时间线 24 项通过;Vite 生产构建、Python compileall、关键未定义符号检查与 `git diff --check` 通过。
|
||||
|
||||
- 2026-07-13:完成产品能力、端到端费用旅程、AI 学习飞轮、KPI 和商业模式分析,形成总功能概念文档。
|
||||
- 2026-07-13(规划阶段):只沉淀功能规划,没有修改业务代码、数据库结构或现有接口。
|
||||
- 2026-07-13(P0 基础切片):新增 `ExpenseCase`、`ExpenseCaseLink`、`BusinessEvent` 模型与 `ExpenseCaseService`,提供 `/api/v1/expense-cases/by-claim/{claim_id}` 时间线查询接口。
|
||||
- 2026-07-13(P0 基础切片):草稿、提交、退回、审批、申请转报销、付款和申请归档开始旁写结构化事件;事件具备租户字段、correlation、causation、幂等键和待投递状态,并与业务状态在同一事务提交。
|
||||
- 2026-07-13(事务修复):关联申请归档/解绑的内部审计改为 `flush`,由外层业务事务统一提交,避免审计日志提前提交付款或归档状态。
|
||||
- 2026-07-13(迁移桥接):新增第一条 migration-owned schema revision;服务启动先执行 Alembic,旧 `create_all` 明确排除三张迁移表。完整历史 schema baseline、正式数据库 upgrade/rollback 和停止旧 DDL 仍未完成。
|
||||
- 2026-07-13(验证):容器内新增测试与既有差旅主链路回归共 24 项通过;Alembic PostgreSQL upgrade/downgrade 离线 SQL、启动脚本语法、OpenAPI 路由和新增文件静态检查通过。未对持久化开发数据库执行迁移。
|
||||
- 2026-07-13(P0 认证安全切片):新增 `AuthSession` 不透明 Bearer 会话及 `20260713_0002_auth_sessions.py`,登录 token 只返回一次、数据库只保存摘要;`/auth/me`、会话结束和登出均从服务端会话解析身份,登出与使用指标收尾原子提交。
|
||||
- 2026-07-13(管理面收口):移除生产代码中的 `X-Auth-*` 授权来源,保护 Settings、模型连通性、缓存、员工、分析、Agent 运行/轨迹、风险观测、审计及系统日志;已初始化 Bootstrap 返回脱敏状态并拒绝匿名重配置,Vite Setup 桥同步锁定。
|
||||
- 2026-07-13(前端会话):Web 请求、流式响应和页面关闭收尾统一使用 Bearer,token 与过期时间只保存在 `sessionStorage`,集中处理 `401`、空闲过期和服务端过期;业务经理不再被前端视为平台管理员。
|
||||
- 2026-07-13(认证验证):容器内认证/Bootstrap/费用事件/OpenAPI 定向测试 21 项通过,受保护业务端点回归 13 项通过,全量测试收集 805 项成功;前端会话、请求、Setup 锁和权限测试 17 项通过,生产构建通过。迁移仅生成并检查 upgrade/downgrade 离线 SQL,未写入持久化开发数据库。
|
||||
- 2026-07-13(P0 提交一致性补缝):AI 新建费用申请并直接提交时,先持久化为草稿,再统一委托 `ExpenseClaimService.submit_claim`;提交校验、预算预占、申请提交风险标记、`application_submitted` 事件和审批状态不再由 AI 入口分别维护。
|
||||
- 2026-07-13(事务边界修复):预算表运行时就绪检查改为复用当前 Session 连接,避免通过 Engine 执行 metadata 检查时隐式提交已 `flush` 的申请;即使预算预占后 Expense Case 事件写入失败,申请、预算额度、预算流水、预占和 Case 数据也会整体回滚。
|
||||
- 2026-07-13(提交一致性验证):容器内 AI 申请直接提交、失败回滚、保存草稿副作用、申请提交主链路及预算/Expense Case 回归共 21 项通过,`ruff --select F,I` 通过;未修改数据库结构,未对持久化开发数据库执行迁移。
|
||||
- 2026-07-13(P0 前端时间线):申请/报销详情在原有横向进度下接入真实 `/api/v1/expense-cases/by-claim/{claim_id}`,新增稳定排序、中文事件语义、状态迁移、退回原因、操作人和发生时间展示;未知事件使用通用文案,不暴露 correlation、幂等键或 Outbox 投递状态。
|
||||
- 2026-07-13(前端兼容与验证):单据切换时清空旧时间线并通过请求序列防止乱序覆盖;未纳入 Expense Case 的 404 显示非阻断兼容态,其他接口错误支持重试且不影响原有进度与单据操作。容器内 99 条定向前端测试和 Vite 生产构建通过。
|
||||
- 2026-07-13(P0 用户态事件契约):Expense Case 响应收口为 Case 基础状态、关系类型和事件安全摘要,事件载荷采用递归白名单;关联单据 ID、聚合信息、幂等与关联链、Outbox 投递字段及未知嵌套内部字段不再通过用户接口返回。
|
||||
- 2026-07-13(P0 草稿事件补缝):AI 工作台与小财管家新建/更新费用申请草稿分别写入 `claim_draft_created` / `claim_draft_updated`;租户从服务端身份透传,事件、Case、Link 与草稿同事务提交,失败整体回滚。动作幂等组合租户、操作人、run ID 与稳定草稿快照:完全相同的 HTTP 保存重放复用同一草稿和事件,并发竞争由稳定聚合 ID 与数据库主键仲裁,同一 run 内的真实内容变化仍保留独立事件。
|
||||
- 2026-07-13(AI 申请身份边界):申请预览快速入口不再接受请求体中的 `user_id`、管理员标记、角色、员工编号或其他身份字段作为授权事实,全部强制绑定服务端会话;伪造管理员身份编辑他人退回申请会返回 400,且原申请与费用事件不发生变化。
|
||||
- 2026-07-13(安全与事务验证):容器内受影响后端定向回归 36 项、Expense Case 前端兼容测试 9 项和 Python `ruff --select F,I` 通过;覆盖本人、当前审批人、财务、管理员、无权限、跨租户、整 Case 安全摘要、草稿新建/更新、失败回滚、HTTP 创建重放、相同快照去重、不同快照留痕、Steward 重放及伪造身份越权。未修改数据库结构,未执行持久化开发数据库迁移。
|
||||
- 2026-07-13(联调边界):当前持久化开发数据库尚无 `auth_sessions`、`expense_cases`、`expense_case_links` 和 `business_events` 表,浏览器登录无法获得有效认证凭证,因此本轮未声称完成真实页面端到端联调,也未擅自执行数据库迁移。计划、消费/票据、入账、对账和复盘事件仍待后续补齐。
|
||||
- 2026-07-14(迁移所有权加固):新增统一 `schema_ownership.py`,七个运行时初始化入口只创建 legacy 表;标准启动在 Alembic upgrade 前执行只读漂移预检,revision 与 migration-owned 表集合不一致时 fail-fast,且不会自动 stamp 或修改数据库。
|
||||
- 2026-07-14(真实迁移验证):在主应用容器连接的一次性 tmpfs PostgreSQL 17 中完成空库升级、重复升级、关键约束/索引、真实外键级联、降级到 base、legacy 哨兵保留、无版本自有表漂移拒绝和再次升级,`test_alembic_migrations.py` 4 项通过,最终 revision 为 `20260713_0002`;临时容器已自动清理,持久化开发数据库复查仍未迁移。
|
||||
- 2026-07-14(剩余边界):当前两条 revision 只覆盖 Expense Case、Business Event 和 Auth Session,完整 legacy schema baseline 及停止其余运行时 DDL 仍未完成;本轮受影响服务回归 46 项通过,既有员工目录历史部门归一化用例仍单独失败,未混入本次迁移安全范围。
|
||||
- 2026-07-14(历史旧单接入):新增独立 `ExpenseCaseLegacyBackfillService` 和 `backfill_legacy_expense_claim_cases.py`。命令只读取显式 `DATABASE_URL`,默认 dry-run;apply 强制目标核验、精确确认、迁移 head、advisory lock 和批次事务。每张旧单只创建一条 `historical_claim_imported` 系统快照,保留源时间和指纹、明确不重建历史,并以 `suppressed` 阻止实时投递。
|
||||
- 2026-07-14(克隆库联调):将持久开发库以只读 `pg_dump` 流式恢复到一次性 tmpfs PostgreSQL 17,原 40 张表、4 张费用单、105 名员工、248 条预算和 62 个 Agent 资产完整保留。升级到 `20260713_0002` 后首次 dry-run 识别 4 张旧单,apply 创建 4 组 Case/Link/Event,重复 apply 创建 0 条;隔离后端完成登录、`/auth/me`、旧单时间线 200 和登出,内部回填字段未出现在 API。临时数据库和隔离进程已清理,持久库复查仍为原数据签名且没有 migration-owned 表。
|
||||
- 2026-07-14(安全复核与质量验证):交叉审查后补齐超长租户幂等键稳定哈希、Link/Case 租户一致性、孤立 Case 冲突、URL 路由参数覆盖防护和部分批次失败进度摘要;前端将快照语义明确为“纳入时状态/节点”。容器内 65 项后端定向测试、11 项前端测试、Ruff 和 Vite 生产构建通过。全库代码体积门禁仍被本次未修改的 `RiskRuleGenerationService` 807 行存量问题阻断,未混入当前功能提交。
|
||||
- 2026-07-14(AI 行为闭环首个切片):新增 `AIDecision`、`AIDecisionFeedback` 和 `WorkflowOutcome` 三类独立事实,把申请预填建议、用户确认或显式字段纠正、草稿保存或申请提交结果通过稳定 `decision_id` 关联。只有已认证的申请预览快速入口显式传入服务端 `CurrentUserContext` 时才写账本;通用 User Agent、模板预览和单据详情编辑路径均不写入,避免伪造身份和非 AI 操作污染样本。
|
||||
- 2026-07-14(字段纠正与隐私边界):申请表编辑器在当前会话中维护首次建议值与最终显式编辑值,多次修改保留最初建议,改回原值会移除差异;日期调整同时记录 `time` 与派生 `days`。服务端最终值始终从解析后的申请 facts 重建,不信任客户端 `finalValue`;持久化层只保存字段名和带密钥版本的 HMAC-SHA256 值指纹,不复制事由、地点、人员、金额等原始值,原始对话、Prompt、模型全文、附件和支付敏感信息也不进入学习账本。
|
||||
- 2026-07-14(可信度边界):浏览器编辑轨迹仍标记为 `verification_status=client_observed`、`trust_level=behavioral_analytics_only` 且 `training_eligible=false`,只能用于受限产品统计;服务端签发预览消费后提升为 `server_verified`,仅证明“服务端签发快照与最终业务事实的差异已核验”,不证明浏览器声称的模型来源,也仍不直接训练模型、自动放行风险或激活企业记忆。
|
||||
- 2026-07-14(事务与一致性):学习三表与申请、预算及 Business Event 使用同一 Session,在最终 commit 前 `flush`,失败整体回滚;提交场景通过提交服务的事务内回调关联真实 `application_submitted` 事件。Case、Business Event 和 Decision 使用包含 `tenant_id + expense_case_id` 的复合外键,既拒绝跨租户,也拒绝同租户跨 Case 串联。前端估算使用消息级请求版本和输入指纹,只合并估算派生字段,旧响应不会覆盖后续编辑。
|
||||
- 2026-07-14(0003 迁移与验证):新增 `20260714_0003_ai_learning_loop.py`,三张学习账本表纳入集中迁移所有权;AgentRun 与旧 ExpenseClaim 保持带索引软引用以兼容空库迁移顺序。一次性 tmpfs PostgreSQL 17 的升级、重复升级、跨租户/同租户跨 Case 外键拒绝、降级和再次升级 4 项通过;容器内学习账本、入口边界和迁移所有权定向 29 项,日期联动及异步估算关键场景 5 项通过。持久开发库只读复查仍为 `40|4|105|248|62`,7 张 migration-owned 表数量为 0。
|
||||
- 2026-07-14(服务端预览决策):新增认证的 `POST /api/v1/reimbursements/application-previews`。服务端从原始申请文本和当前登录身份重新解析字段、重跑规则测算,再签发 30 分钟有效的 `decision_id`;浏览器返回的模型来源和建议值不被直接认证。预览绑定租户、actor、登录会话和 conversation,字段只保存独立版本密钥生成的 HMAC 指纹;字段存在性也纳入指纹,新增、删除和改值都能由服务端识别。
|
||||
- 2026-07-14(消费与续签):快速保存/提交显式携带动作类型、`decision_id` 和稳定请求 ID。服务端锁行校验后,在 Claim、Expense Case、Business Event、AIDecision、Feedback、Outcome 的同一事务中一次消费;当前会话已有有效决策时禁止省略 ID 降级。保存草稿提交成功后再尽力签发基于服务端事实的新 `decision_id`,续签失败只返回无新 ID,不反转已成功业务动作;相同请求安全重放不会重复建单或重复写学习账本。
|
||||
- 2026-07-14(0004 迁移与验证):新增 `20260714_0004_ai_application_preview_decisions.py`,预览决策表和正式 Decision 复合租户外键纳入集中迁移所有权。一次性隔离 PostgreSQL 17 完整 upgrade、重复 upgrade、约束/索引、downgrade 和再次 upgrade 4 项通过;容器内新增决策安全用例 9 项、旧快速保存/提交 4 项、申请学习账本 9 项、迁移与所有权 26 项通过且条件型 PostgreSQL 用例 1 项跳过,3 组前端定向测试及 Vite 生产构建通过。持久开发库只读确认 8 张 migration-owned 表数量仍为 0。
|
||||
- 2026-07-14(多入口统一决策闭环):抽取 `ExpenseApplicationPreviewWorkflow`,认证签发、锁行消费、动作幂等、学习落账和草稿续签不再由 HTTP 端点重复编排。Steward、小财管家结构化预览和通用 Orchestrator 均复用该工作流;用户纠正字段后仍消费原始 decision,使服务端能够把建议值与最终业务事实记为 `server_verified` 接受或纠正证据。
|
||||
- 2026-07-14(Orchestrator 身份与会话边界):外部用户消息、定时任务和系统事件统一强制认证,`schedule` / `system_event` 进一步要求平台管理员;登录态覆盖请求中的身份、租户和调度操作人别名,堵住匿名或普通用户伪造来源触发 Hermes 管理任务的路径。会话创建、恢复和删除同时绑定可信租户与用户名,历史无租户状态会话 fail-closed。申请 decision 只从服务端会话恢复,客户端 preview/decision 在进入编排前被移除;保存草稿后的 next decision 回写会话并支持刷新/换端恢复结构化预览。
|
||||
- 2026-07-14(小财管家 fail-closed):完整预览展示前先使用稳定 request ID 签发 canonical decision;签发失败时允许继续编辑和以同一 ID 重试,但禁止保存/提交且不回退旧副作用链路。保存/提交失败重试复用稳定 action request ID,保存成功同步草稿信息与 next decision。
|
||||
- 2026-07-14(decision 生命周期加固):同一租户、用户、登录会话、conversation 和 HMAC 快照即使使用不同签发 request ID,也复用当前 active decision,避免多个并行 decision 命中草稿幂等捷径后遗留未消费状态。动作发现 decision 过期、已消费或不可用时,前端清空旧 ID、生成新的签发 request ID 并开放重新签发。
|
||||
- 2026-07-14(跨租户检查点加固):Steward 动作会话创建显式写入服务端租户;租户 A/B 即使使用相同用户名、conversation 和 trace,也会获得独立会话与 decision,不再跨租户返回幂等结果或敏感预览内容。
|
||||
- 2026-07-14(统一闭环验证):容器内服务端预览、Steward 动作/图运行、Orchestrator 外部来源授权及决策消费组合回归 50 项通过,Python Ruff F/I 通过;前端结构化动作、会话恢复、工作台路由、富确认和动作脚本共 18 项通过,Vite 生产构建通过。并行读取用户正在变动的规则工作簿曾触发 `openpyxl` ZIP 句柄竞争,相关套件改为串行后全部通过,未修改规则工作簿。
|
||||
- 2026-07-14(个人出行方式记忆):新增租户化 `memory_entries` 与 `memory_evidence_links`,首个切片只存储 `travel_application.transport_mode` 的“飞机/火车/轮船”。同一租户、员工、场景和值需要 3 个不同 Expense Case 的服务端核验纠正、至少 2 个当前审批通过且证据跨 7 天才激活;Candidate/Active 默认有效期分别为 90/180 天。
|
||||
- 2026-07-14(证据信任边界):记忆证据严格绑定当前 tenant、actor、employee、Case owner 和 Claim owner,并要求 Decision、Feedback、Outcome 指向同一申请、同一 Case 和同一个真实 `application_submitted` 业务事件;事件聚合必须是对应 `expense_claim`。同 Case 重放、invalidated/reversed 事实、最新退回、反向纠正和非白名单纠正不会继续支持 active 记忆。
|
||||
- 2026-07-14(应用、抑制与遗忘):个人记忆只补空白出行方式,任何当前显式值都优先。命中后服务端重新计算交通与总额估算再签发 canonical decision;记忆异常降级为无记忆,不阻塞预览。反向或非白名单纠正抑制 active,用户忘记后状态改为 revoked、清空可恢复值和指纹,旧请求不可复活。
|
||||
- 2026-07-14(前端记忆解释):申请核对表新增独立 `TravelReimbursementMemoryPanel`,展示已应用的常用出行方式、证据数量、学习回执和“忘记此偏好”入口;本地会话快照支持跨刷新恢复,忘记偏好不会篡改当前申请字段。
|
||||
- 2026-07-14(个人记忆验证):容器内个人记忆专项 16 项、记忆/预览决策/迁移/所有权组合回归 56 项通过且 1 项条件跳过、前端申请与记忆组合 83 项通过;一次性 PostgreSQL 迁移循环、Ruff、Vite 生产构建和 `git diff --check` 均通过。当前规则中心尚不存在交通方式禁用维度,制度冲突守卫作为后续规则扩展的前置门禁保留,不以恒真占位判断冒充已实现。
|
||||
- 2026-07-16(零录入票据首个闭环):附件后台任务从“大而全”的内存编排中拆出只读 `ExpenseReceiptMatcher` 与可逆 `ExpenseReceiptAssociationService`。系统按当前租户、员工、Expense Case、已审批申请、票据日期、城市、路线和场景选择草稿;只有唯一高置信草稿自动归集,低置信、多候选、无草稿及仅有申请时均以需要确认的安全终态返回并保持零业务写入。
|
||||
- 2026-07-16(事务、幂等与租户边界):票据 Link、`receipt_received`、附件写入和 `attachment_associated` 由同一关联编排器提交,数据库快照与文件目录备份共同补偿中途失败,事件或文件元数据异常时恢复 Claim/Case/Link/Event/票据及旧附件目录;相同票据任务按租户、owner 和票据集合持久去重。票据目录加入无碰撞租户摘要,任务、候选 Claim 和票据同时绑定租户与本人,平台管理员也不能跨租户读取任务。
|
||||
- 2026-07-16(持久任务与并发):新增 migration-owned `attachment_association_jobs` 和 `20260716_0006`。任务状态、结构化结果、owner 上下文、attempt、租约与 generation 写入数据库;GET 可恢复 queued 或租约过期任务,`attempt_count + running` 作为栅栏阻止旧 worker 或迟到回调覆盖新终态。同票据和同 Claim 分别使用进程锁与 PostgreSQL advisory lock 串行化,Claim 锁内清理旧事务并重新匹配,避免不同票据并发选择同一空明细。待确认或失败任务保留原代历史,再次发起创建新 generation 并重新评估;已自动关联成功的代际继续幂等复用。评分改为纯只读查询,每份票据必须独立达到最小证据,避免无关票据被同批强证据带入。
|
||||
- 2026-07-16(小财管家交互与验证):任务结果新增 Case、申请、置信度、原因、异常、缺失项、风险项、候选和草稿载荷;前端可跨会话恢复,自动完成直接查看草稿,仅申请候选查看申请,待确认不伪装成功;幂等重放显示“已关联”,成功结果仍展示风险和复核要求。容器内后端归集专项 20 项、归集与相邻服务/迁移所有权组合回归 71 项、前端关联链路组合回归 29 项、Ruff F/I/UP 和 Vite 生产构建通过;一次性 tmpfs PostgreSQL 17 的 0006 完整迁移循环 4 项通过并已清理,持久开发库未修改。既有大型报销服务与接口套件仍存在旧审批、删除和风控断言失败,未把这些基线问题误报为已解决。
|
||||
- 2026-07-16(保留边界):预算、项目、成本中心和个人记忆偏好尚未接入票据候选评分;任务已持久化并支持租约恢复,但尚未建设独立消息队列、运维重试面板和死信治理;完整 G2 与平台级异步任务治理仍未完成。
|
||||
- 2026-07-17(费用价值闭环):新增 Savings Ledger、CFO 价值分析和财务确认/冲回链路;现金、工时、风险暴露和预计机会严格分账。住宿标准调整从服务端锁定原金额创建机会,付款/生产连接器回执产生 actual realization,独立财务确认后才进入现金 KPI,退款以负向追加事实冲回。
|
||||
- 2026-07-17(财务连接器):建立配置生命周期、HMAC、防重放、事件幂等、对账、ERP 入账、退款/冲回、operational event 和 mock/test/staging 隔离。端到端以本地自签的 production-mode 事件验证申请、票据、报销、预审、审批、回执、ERP、归档、Savings 与商业价值契约;它不代表真实银行/ERP 回执,模拟回执也不会改变核心财务事实。
|
||||
- 2026-07-17(真实发布遥测):规则发布从真实 observation、可信 disposition/reviewer label、独立盲审负样本和保守 recall 进入 Monitor/Guard;collecting、积压、聚合失败或低 precision 均不晋级并保持 stable。0023 提供 audit sample、双人票、append-only 和 PostgreSQL 并发保护。
|
||||
- 2026-07-17(商业闭环):新增套餐、订阅、账期、权益、配额、用量、成本、ROI、毛利和定价走廊;中央 Orchestrator、Runtime Chat、OCR、金融连接器和附件源文件写入按权威数量预占、结算或释放,客户价值、平台收入和内部成本不混账。
|
||||
- 2026-07-17(全链租户安全):新增 0025-0028,统一 Tenant、Employee、Claim、Agent Asset、Knowledge、Ontology、Hermes、Finance Report、Qdrant、文件和缓存作用域;ONLYOFFICE 改为数据库一次性会话与 SSRF/重放保护。审批员工解析按认证企业或结构化 Claim 企业首层过滤,跨租户身份和相同名称不再命中。
|
||||
- 2026-07-17(工程收口验证):176 个后端测试文件在主应用容器内分片或专项通过,费用主服务 121 项通过;fresh PostgreSQL 迁移/并发总探针 `87 passed / 0 skipped / 0 failed`,head 为 0028;Web 全量 `815 passed / 0 failed` 与 Vite build 通过;Mobile lint/typecheck、197 个新增 Python 文件 Ruff、目标 compileall、受门禁核心类/组件 800 行检查和 `git diff --check` 通过。
|
||||
- 2026-07-17(完成边界):工程代码和容器验收已收口;生产 ONLYOFFICE DNS/TLS、备份副本迁移演练、逐租户 SMTP、真实 provider/会计规则、移动/浏览器实机联调、30/90 天企业试点、客户财务签字和合同定价仍必须使用目标环境与真实业务证据完成,不能由 mock 或本地测试替代。总验收见 `document/development/2026-07-17/feature/engineering-closure-and-production-readiness/`。
|
||||
@@ -0,0 +1,316 @@
|
||||
# AI 费用闭环与价值证明 开发 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
文档路径:document/development/2026-07-13/feature/ai-expense-closed-loop-and-value-proof/TODO.md
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 每个 TODO 必须对应 `CONCEPT.md` 中的目标、能力、方案或验收点。
|
||||
- 只有完成实现并验证后,才能把 `[ ]` 改成 `[x]`。
|
||||
- 已完成项必须补充文件、接口、命令、容器测试或真实页面证据。
|
||||
- 如果需求发生变化,先更新 `CONCEPT.md`,再调整本 TODO。
|
||||
- 实施顺序遵循:P0 费用闭环与数据基础 → P1 安全自动化与学习 → P2 费用经营与价值证明 → P3 商业化复制。
|
||||
- 所有后端、集成、迁移和依赖外部服务的验证只允许在项目容器内运行,单条测试命令最大超时 60s。
|
||||
|
||||
## 1. 调研与边界
|
||||
|
||||
- [x] [CONCEPT: 背景与问题] 盘点申请、票据、报销、预算、风险、审批、付款、分析、Agent trace、反馈和 AI 数据飞轮现状。
|
||||
证据:`server/src/app/api/v1/endpoints`、`server/src/app/services`、`web/src`、`mobile/app/src`、容器运行时 OpenAPI 共 156 个业务操作。
|
||||
- [x] [CONCEPT: 背景与问题] 确认当前主要断点是消费/取票、真实支付入账、结果回流、节省归因和商业计量,而不是页面数量不足。
|
||||
证据:`CONCEPT.md`“背景与问题”;`expense_claim_approval_flow.py` 付款为状态更新;本地容器关键反馈/样本表聚合结果为 0。
|
||||
- [x] [CONCEPT: 目标与非目标] 确认本功能采用一个总功能点分阶段实施,不一次性替换所有现有接口,不自建支付和商旅供应链网络。
|
||||
证据:`CONCEPT.md`“目标与非目标”。
|
||||
- [x] [CONCEPT: 风险与开放问题] 记录认证、租户、旧模型迁移、反馈污染、虚假节省、自动化失控、连接器和数据稀疏风险。
|
||||
证据:`CONCEPT.md`“风险与开放问题”。
|
||||
## 2. 开发前置门禁:试点与基线
|
||||
|
||||
- [ ] [CONCEPT: 待确认] 确认首个目标客户规模、部署形态、购买决策人和最小租户边界。
|
||||
- [ ] [CONCEPT: 待确认] 确认首个 90 天试点费用场景,只允许一个场景进入 P0 开发。
|
||||
- [ ] [CONCEPT: 待确认] 确认是否进入企业支付、商旅预订或公司卡领域,并冻结首期连接器范围。
|
||||
- [ ] [CONCEPT: 待确认] 确认客户 ROI 中现金节省与工时价值的口径、基线窗口和签字人。
|
||||
- [ ] [CONCEPT: 指标与验收] 在 P0 开发前采集人工分钟、退回率、完成周期、人工触点、风险反馈、费用金额和付款结果基线。
|
||||
- [ ] [CONCEPT: 指标与验收] 为 P0/P1/P2 分别冻结一条可纵向跑通的闭环验收,不以“模块代码已完成”代替业务结果。
|
||||
|
||||
## 3. 契约与设计
|
||||
|
||||
- [x] [CONCEPT: 费用领域与编排] 定义 `Expense Case`、申请、票据、报销、审批、付款、凭证和归档的领域边界与迁移关系。
|
||||
证据:`expense_cases.py`、Expense Case/Link/Event 模型、连接器付款/ERP/归档与 Savings 冲回 E2E。
|
||||
- [x] [CONCEPT: 数据与契约] 定义 `expense_cases`、`expense_case_links` 和最小状态机,明确非法状态跃迁。
|
||||
证据:`20260713_0001_expense_case_business_events.py`、`ExpenseCaseService` 与状态/事件回归。
|
||||
- [x] [CONCEPT: 数据与契约] 定义 `business_events` 事件信封、事件词典、correlation ID、幂等键和版本策略。
|
||||
证据:`BusinessEvent` schema/service、业务事件词典及申请/报销/审批/支付/ERP/Savings 回归。
|
||||
- [x] [CONCEPT: 业务事件与 AI 决策] 定义 `ai_decisions`、`ai_decision_feedback` 和 `workflow_outcomes` 契约。
|
||||
证据:`ai_learning.py`、`expense_application_learning.py`、`20260714_0003_ai_learning_loop.py`;三类事实分别表达 AI 建议、用户采纳/显式字段纠正和业务结果,技术执行成功、用户反馈与工作流结果不复用同一状态。
|
||||
- [x] [CONCEPT: 业务事件与 AI 决策] 定义落单前服务端预览决策的签发、授权、过期、一次消费、重放和续签契约。
|
||||
证据:`ai_application_preview.py`、`expense_application_preview_decisions.py`、`expense_application_snapshot.py`;预览不创建空 Case,绑定租户、actor、登录会话与 conversation,30 分钟过期,保存/提交成功后一次消费;保存草稿在业务提交后尽力返回基于服务端事实的新 `decision_id`,续签失败不反转已成功动作。
|
||||
- [x] [CONCEPT: 记忆与学习] 定义并实现 `memory_entries`、`memory_evidence_links`、优先级、有效期、敏感白名单、撤销和遗忘契约;首个切片仅覆盖 `travel_application.transport_mode`。
|
||||
证据:`ai_memory.py`、`expense_application_memory.py`、`expense_application_memory_evidence.py`、`20260714_0005_ai_memory.py`;记忆使用租户、主体、场景、字段和值指纹精确隔离,值只允许“飞机/火车/轮船”,服务异常降级为不应用记忆。
|
||||
- [x] [CONCEPT: 自动化决策] 定义动作风险、金额阈值、置信度、证据完整度、可逆性、抽检率和企业授权策略。
|
||||
证据:`automation_eligibility.py` 与 `test_automation_eligibility.py`;资金、制度和高风险动作始终人控。
|
||||
- [x] [CONCEPT: 节省与价值] 定义 Savings Ledger 的机会、执行、实现、确认、去重和归因状态。
|
||||
证据:Savings 模型、0015 迁移、状态服务与并发/E2E 测试。
|
||||
- [ ] [CONCEPT: 连接器] 定义票据邮箱、税务、企业卡、商旅、支付、银行、ERP、消息和 SSO 连接器协议。
|
||||
- [x] [CONCEPT: 权限与安全] 定义服务端会话、租户数据范围、角色和动作级授权契约。
|
||||
证据:不透明 Bearer Session、`CurrentUserContext`、租户访问策略和 endpoint role dependencies;0025-0028 多租户安全迁移。
|
||||
- [ ] [CONCEPT: 指标与验收] 完成首个试点指标字典、基线采集方案、分子分母、数据源和负责人。
|
||||
- [x] [CONCEPT: 方案设计] 完成分阶段架构评审,确认新增 service 不继续堆入 `ExpenseClaimService`、`UserAgentService` 或大型前端 composable。
|
||||
证据:访问策略、员工解析、附件、预审、Savings、商业、连接器、发布遥测及前端 composable 均按职责拆分;受门禁核心类/组件 800 行检查通过。
|
||||
|
||||
## 4. P0 后端实现:费用闭环与数据基础
|
||||
|
||||
- [x] [CONCEPT: 权限与安全] 实现服务端可验证会话/JWT,移除客户端身份头作为授权事实来源。
|
||||
证据:`auth_sessions.py`、`auth_session.py`、`deps.py`、`auth.py`、`authSessionStorage.js`;登录签发不透明 Bearer token,数据库仅保存 SHA-256 摘要,生产代码不再信任 `X-Auth-*`,过期/撤销/伪造会话回归测试通过。
|
||||
- [x] [CONCEPT: 权限与安全] 为管理面、Bootstrap、Settings、模型连通性、缓存、审计和系统日志补齐平台管理员保护。
|
||||
证据:`bootstrap.py`、`settings.py`、`audit_logs.py`、`agent_traces.py`、`system_logs.py`、`vite.config.js`;已初始化 Bootstrap 脱敏且拒绝匿名重配,平台管理员/业务经理权限边界和 Vite Setup 锁测试通过。
|
||||
- [x] [CONCEPT: 权限与安全] 继续盘点并收口风险规则发布、制度发布及其他尚未纳入本轮的敏感动作,按动作定义平台管理员或双人复核权限。
|
||||
证据:Agent Asset/Rule Editor/Reviewer、发布盲审双人票、风险豁免独立决定、平台资产只读和 ONLYOFFICE 写入权限均有服务端守卫与安全测试。
|
||||
- [x] [CONCEPT: 权限与安全] 为本轮新增表及 Claim、Employee、Agent Asset、Knowledge、Ontology、Hermes 和 Report 等纳入范围的共享核心数据补齐 `tenant_id`、约束、查询守卫和默认租户迁移。
|
||||
证据:0025-0028 迁移、复合租户外键、首层 SQL 过滤和 tenant security 测试;fresh PostgreSQL 迁移总探针通过。
|
||||
- [ ] [CONCEPT: 权限与安全] 将 `agent_conversations` 等仍依赖 JSON tenant 的 legacy 状态迁移到结构化租户列和数据库约束。
|
||||
证据要求:后继迁移、历史回填、复合约束和跨租户回归;当前只能由服务层校验 `state_json.tenant_id`,不得表述为数据库边界已完成。
|
||||
- [x] [CONCEPT: 权限与安全] 为 Expense Case 查询提供面向用户的精简事件 DTO,移除幂等键、correlation、causation 和 Outbox 投递字段,并明确审批意见、退回原因和操作人可见范围。
|
||||
证据:`expense_case.py`、`test_expense_case_endpoints.py`;用户态响应只保留安全流程摘要,关联资源 ID 和未知嵌套 payload 被递归过滤,本人、当前审批人、财务、管理员、无权限及跨租户边界测试通过。
|
||||
- [x] [CONCEPT: 权限与安全] 为 Qdrant collection/namespace、对象存储前缀和缓存键补齐租户隔离回归测试。
|
||||
证据:Knowledge tenant scope、RAG workspace、Qdrant namespace、票据/附件路径、运行缓存和财务快照 tenant fingerprint 回归。
|
||||
- [x] [CONCEPT: 数据与契约] 建立 Alembic baseline 和正式迁移链,停止请求路径运行 DDL。
|
||||
证据:migration ownership/preflight 与 0001-0028 正式链;fresh PostgreSQL 完整 upgrade/downgrade/re-upgrade 通过,请求路径不再创建 migration-owned 表。
|
||||
- [x] [CONCEPT: 兼容策略] 集中 migration-owned 表所有权并在标准启动迁移前执行只读漂移预检,禁止 legacy bootstrap 越权建表。
|
||||
证据:`schema_ownership.py`、`migration_preflight.py`、`server_start.sh`、`test_migration_preflight.py`、`test_schema_ownership.py`;无版本自有表、缺表、多表、未知/多 revision 均 fail-fast,不自动 stamp 或修改数据库。
|
||||
- [x] [CONCEPT: 费用领域与编排] 新增 `ExpenseCaseService` 和费用事件查询接口,保持编排与具体职责分离。
|
||||
证据:`server/src/app/services/expense_cases.py`、`server/src/app/api/v1/endpoints/expense_cases.py`、`GET /api/v1/expense-cases/by-claim/{claim_id}`;容器 OpenAPI 校验通过。
|
||||
- [x] [CONCEPT: 数据与契约] 新增 `expense_cases`、`expense_case_links` 和 `business_events` 表及迁移。
|
||||
证据:`20260713_0001_expense_case_business_events.py`;一次性 PostgreSQL 17 已验证表、唯一约束、复合索引、外键级联、降级和再次升级。
|
||||
- [x] [CONCEPT: 业务事件与 AI 决策] 新增 `ai_decisions`、`ai_decision_feedback`、`workflow_outcomes` 表及迁移。
|
||||
证据:`20260714_0003_ai_learning_loop.py`、`schema_ownership.py`、`migration_preflight.py`;三表具备租户幂等约束,Case/Event/Decision 使用包含费用 Case 的复合租户外键。一次性 PostgreSQL 17 完整升级、重复升级、跨租户及同租户跨 Case 外键拒绝、降级和再次升级 4 项通过,持久开发库未迁移。
|
||||
- [x] [CONCEPT: 业务事件与 AI 决策] 新增服务端预览决策表、认证签发接口和正式 Decision 关联迁移。
|
||||
证据:`20260714_0004_ai_application_preview_decisions.py`、`expense_application_previews.py`、`reimbursements.py`;动作消费与 Claim、Case、Business Event 和学习三表同事务,跨登录会话重放被拒绝;独立版本密钥生成的 HMAC 指纹不保存低熵字段明文,并编码字段存在性。只有从未成功签发的旧调用可无 ID 降级,当前会话已有有效决策时省略 ID 会返回 409。
|
||||
- [ ] [CONCEPT: 数据与契约] 为申请、票据、草稿、提交、退回、审批、付款和归档接入统一 correlation ID。
|
||||
当前进度:报销显式预审与提交已共享 correlation/causation,AI 草稿和票据归集已透传租户与稳定幂等键;申请、审批、付款和外部连接器仍需继续统一。
|
||||
- [ ] [CONCEPT: 业务事件与 AI 决策] 建立事务 Outbox:申请、提交、退回、审批、支付和入账状态与事件同事务提交,消费端按事件 ID 幂等处理。
|
||||
- [x] [CONCEPT: 业务事件与 AI 决策] 完成首批草稿、提交、退回、审批、申请转报销、付款和申请归档事件旁写,具备 correlation、causation、幂等键及事务回滚。
|
||||
证据:`expense_claim_draft_flow.py`、`expense_claims.py`、`expense_claim_approval_flow.py`、`test_expense_case_service.py`;容器测试覆盖同 Case 关联、重复事件去重及 Outbox 失败整体回滚。
|
||||
- [x] [CONCEPT: 风险与预审] 建立申请与报销提交前服务端预审握手,使用稳定 review ID、输入/规则/动态结果指纹、三态决策和结构化整改动作阻止旧预审复用。
|
||||
证据:`expense_claim_pre_review_decision.py`、`expense_claim_pre_review.py`、`expense_claims.py`、`reimbursement_pre_review.py`;提交前始终重算完整风险上下文,`needs_fix` 在预算占用前保持草稿并返回结构化 409,握手期间风险变化返回 `PRE_REVIEW_CHANGED`,相同输入、规则和 findings 重试只保留一条预审事件。申请与报销使用同一契约:提交人可整改风险必须先处理,人工判断或预算治理风险才进入领导与 P8。
|
||||
- [x] [CONCEPT: 统一费用事件] 补齐票据归集 → 预审 → 提交切片的阶段与事件衔接,并修复非默认租户在 AI 草稿、预览和 Steward 链路回落到 default 的问题。
|
||||
证据:`expense_receipt_association.py`、`expense_claim_approval_flow.py`、`expense_claim_draft_flow.py`、`expense_claim_review_preview.py`、`steward_action_executor.py`;申请批准生成空报销草稿后 Case 保持 `approved_to_spend`,票据关联后进入 `claiming`,跨租户 Case/Event 不能串线。
|
||||
- [x] [CONCEPT: 权限与安全] 将 Claim 核心查询和关联报销后台任务纳入服务端租户范围,拒绝同用户名跨租户读取、修改、审批或查询任务。
|
||||
证据:`expense_claim_tenant_scope.py`、`expense_claim_access_policy.py`、`linked_reimbursement_draft_jobs.py`;默认租户仅为无 CaseLink 的历史单保留兼容,后台任务强制覆盖客户端 tenant,关联申请按 CaseLink 与租户共同校验。
|
||||
- [x] [CONCEPT: 业务事件与 AI 决策] 收口 AI 新建申请直接提交入口,统一复用申请提交事务,并消除预算 metadata 检查对外层事务的隐式提交。
|
||||
证据:`user_agent_application.py`、`budget.py`、`test_reimbursement_endpoints.py`、`test_user_agent_service.py`;容器测试覆盖有效预算预占、`application_submitted` 事件、提交风险标记、事件失败整体回滚及保存草稿无提交副作用。
|
||||
- [x] [CONCEPT: 业务事件与 AI 决策] 为 AI 工作台和小财管家申请草稿补齐新建/更新事件、租户透传、事务回滚和动作幂等。
|
||||
证据:`expense_application_draft_events.py`、`user_agent_application.py`、`reimbursements.py`、`steward_action_executor.py`、`test_user_agent_application_draft_events.py`、`test_reimbursement_endpoints.py`、`test_steward_action_executor.py`;完全相同 HTTP 保存重放复用同一草稿和事件,同一 run 内不同快照分别留痕,事件失败后草稿与 Case 数据整体回滚。
|
||||
- [x] [CONCEPT: 权限与安全] 将 AI 申请预览快速入口的用户、租户、角色与管理员身份强制绑定到服务端会话,拒绝请求体伪造身份编辑他人申请。
|
||||
证据:`reimbursements.py`、`test_reimbursement_endpoints.py`;对抗用例修复前返回 200,修复后返回 400,且目标申请和费用事件保持不变。
|
||||
- [x] [CONCEPT: 权限与安全] 收口通用 Orchestrator 用户消息和会话管理的认证边界,拒绝客户端身份与 decision 注入。
|
||||
证据:`orchestrator.py`、`agent_conversations.py`、`orchestrator_expense_application_workflow.py`、`steward_graph_action_runtime.py`、`test_orchestrator_auth_endpoints.py`、`test_steward_action_executor.py`、`test_orchestrator_review_flow.py`;用户消息、定时任务和系统事件无认证均返回 401,普通用户触发 `schedule` / `system_event` 返回 403,登录态覆盖请求身份和调度操作人别名;Orchestrator 会话创建/恢复/删除及 Steward 幂等检查点同时校验租户与用户名,申请动作只消费服务端会话 decision。
|
||||
- [x] [CONCEPT: 兼容策略] 建立迁移桥接:服务启动先执行 Alembic,旧 metadata bootstrap 排除 migration-owned 表。
|
||||
证据:`server_start.sh`、`schema_ownership.py`、`migration_preflight.py`、`20260713_0001_expense_case_business_events.py`、`20260713_0002_auth_sessions.py`;容器 Shell/静态检查及一次性 PostgreSQL 完整 upgrade/downgrade/re-upgrade 通过。
|
||||
- [x] [CONCEPT: 兼容策略] 为迁移前已有 `ExpenseClaim` 提供显式、幂等且不伪造办理历史的费用事件快照回填。
|
||||
证据:`expense_case_legacy_backfill.py`、`maintenance_database_target.py`、`backfill_legacy_expense_claim_cases.py`;默认 dry-run,apply 强制租户/截止时间、精确目标、迁移 head、advisory lock 和批次事务,事件使用真实回填时间、`history_reconstructed=false` 与 `delivery_status=suppressed`。一次性克隆库首次创建 4 组 Case/Link/Event,重复 apply 创建 0 条。
|
||||
- [ ] [CONCEPT: 兼容策略] 正式切换前以 shadow 事件校验现有 `ExpenseClaim` 映射;切换后禁止关键事件可丢弃写入。
|
||||
- [ ] [CONCEPT: 兼容策略] 制定旧 `ReimbursementRequest` 只读兼容、迁移和停止新增编排的计划。
|
||||
- [ ] [CONCEPT: 兼容策略] 把新增审批、付款、归档和关系事件移出 `risk_flags_json`,保留旧数据读取兼容。
|
||||
- [ ] [CONCEPT: 数据与契约] 补齐撤回、取消、驳回、作废、补件、支付失败、对账异常和归档状态。
|
||||
- [x] [CONCEPT: 连接器] 实现财务连接器统一事件信封、认证、幂等、重试、错误状态和回执事件。
|
||||
证据:`financial_connector_auth.py`、`financial_connector_ingestion.py`、`payment_reconciliation.py`、配置生命周期及 operational events;服务/API/并发测试通过。
|
||||
- [ ] [CONCEPT: 连接器] 实现支付批次、回执、重复付款防护、ERP 凭证和对账的内部契约,首期允许 mock connector 但不得再只写单一“已付款”状态。
|
||||
- [x] [CONCEPT: 降级策略] 将附件关联和关联报销草稿后台任务迁为可持久化、可恢复、可幂等的任务状态。
|
||||
证据:新增 migration-owned `attachment_association_jobs` 与 `20260716_0006`;任务使用租户、owner、票据集合和 generation 去重,运行态带租约与 `attempt_count + running` 栅栏,GET 可恢复 queued/租约过期任务。同票据和同 Claim 均由进程锁与 PostgreSQL advisory lock 串行化,Claim 锁内重新匹配;待确认或失败历史保留原代并以新 generation 重新评估,自动关联成功代继续幂等复用。进程状态清空、租约过期、旧 worker 回写、不同票据并发同 Claim 和代际重评估回归通过。
|
||||
- [x] [CONCEPT: 零录入附件关联任务契约] 扩展附件关联任务的向后兼容结果契约,区分自动完成与需要确认,并返回 Case、申请、置信度、原因、异常、缺失项、风险项和候选。
|
||||
证据:`attachment_association_job.py` 与 `attachmentAssociationJobModel.js` 保留旧字段并追加结构化结果;`status=succeeded + resolution=requires_confirmation` 明确表示匹配完成但零业务写入。
|
||||
- [x] [CONCEPT: 零录入报销首个切片] 新增独立票据匹配器,按当前租户、员工、Expense Case、已审批申请、日期、城市和场景计算候选;唯一高置信才允许自动归集。
|
||||
证据:`expense_receipt_matcher.py` 只读评分;覆盖唯一高置信、相近候选、无草稿、仅有已审批申请和已关联重放,申请缺少系统草稿时只返回 `approved_application` 候选。
|
||||
- [x] [CONCEPT: 零录入报销首个切片] 新增独立归集编排器,把票据 Link、`receipt_received`、附件写入和 `attachment_associated` 纳入同一事务,并保证同票据重试不重复建明细或事件。
|
||||
证据:`expense_receipt_association.py` 统一编排数据库写入,文件系统使用元数据快照和附件目录补偿;稳定幂等键保证相同票据重试只增加跳过数。事件写入失败回归证明 Claim、Case、Link、Event、票据元数据和附件最终状态均回退。
|
||||
- [x] [CONCEPT: 权限与安全] 将票据夹存储命名空间和附件关联后台任务授权同时绑定租户与用户,补充同用户名跨租户隔离回归。
|
||||
证据:`receipt_folder.py` 使用租户与用户联合命名空间,任务状态绑定 `owner_tenant_id + owner_username`;普通用户及平台管理员跨租户查询均返回 404,同用户名跨租户不能读取票据。
|
||||
|
||||
## 5. P0 前端实现:一键报销与真实移动端
|
||||
|
||||
- [x] [CONCEPT: 统一费用事件] 在申请/报销详情接入已有真实 Expense Case 事件时间线,展示申请提交、报销提交、退回、审批、自动生成报销草稿、付款和归档事件。
|
||||
证据:`TravelRequestExpenseCaseTimeline.vue`、`useExpenseCaseTimeline.js`、`expenseCases.js`、`expenseCaseTimeline.js`;保留原有横向进度,404 与接口异常均安全降级,容器内 99 条定向前端回归和 Vite 生产构建通过。
|
||||
- [ ] [CONCEPT: 统一费用事件] 补齐计划、消费/票据、入账、对账和复盘事件,并在真实迁移数据库上跑通完整时间线。
|
||||
- [ ] [CONCEPT: 统一费用事件] 自动匹配申请、预算、票据、费用类型、项目、成本中心和常用字段。
|
||||
首个切片先交付申请、票据、费用事件、日期、城市和费用场景匹配;预算、项目、成本中心和记忆偏好随后接入同一候选契约。
|
||||
- [ ] [CONCEPT: 统一费用事件] 增加“需要确认”区,只展示低置信、缺失或冲突字段。
|
||||
首个切片在小财管家附件关联结果中复用统一异常模型;后续再收口全屏助手和票据夹入口。
|
||||
- [x] [CONCEPT: 零录入报销首个切片] 在小财管家实现“上传并发送 → 自动归集已审批申请生成的草稿 → 查看草稿”,不再次上传票据;低置信时只展示候选和异常卡片。
|
||||
证据:附件任务 composable 将待确认视为安全终态,不发送伪成功通知;自动完成提供“查看草稿”,申请候选提供“查看候选申请”,不伪造报销草稿。
|
||||
- [x] [CONCEPT: 零录入附件关联任务契约] 会话刷新后恢复新任务结果、候选和异常状态,旧任务结果仍可正常展示。
|
||||
证据:`workbenchAiMessageModel.js` 持久化新协议,规范化层兼容旧字段;容器内前端票据关联、协议、卡片、幂等重放、风险复核和会话恢复组合回归 29 项通过。
|
||||
- [x] [CONCEPT: 风险与预审] 报销详情提交前先执行真实预审,`needs_fix` 直接展示结构化风险并停止提交,其他结果携带 review ID 与输入指纹完成握手。
|
||||
证据:`reimbursements.js`、`api.js`、`useTravelRequestDetailRiskSubmit.js`;错误响应保留 status/code/detail/review,风险可即时回填并触发详情刷新,前端预审、409、请求头、时间线组合测试 24 项和 Vite 生产构建通过。
|
||||
- [ ] [CONCEPT: 风险与预审] 风险卡提供一键补件、修正、解释和重新预审入口。
|
||||
- [ ] [CONCEPT: 统一费用事件] 退回后直接定位问题字段,支持断点续办,不要求重新发起对话。
|
||||
- [ ] [CONCEPT: 移动端] 接通真实登录恢复、路由守卫、报销列表、详情、草稿、上传和审批 API。
|
||||
- [ ] [CONCEPT: 移动端] 完成拍照/相册 → OCR → 票据夹 → 费用事件 → 报销草稿真实闭环。
|
||||
- [ ] [CONCEPT: 移动端] 删除或禁用没有真实行为的报销、审批、发送和语音主按钮。
|
||||
- [ ] [CONCEPT: 前端] 修复表格整行点击键盘不可达、弹窗焦点管理、焦点环和移动触控目标。
|
||||
- [ ] [CONCEPT: 前端] 移除页面级全局 `transform: scale()`,使用真实响应式布局承载费用闭环页面。
|
||||
|
||||
## 6. P1 算法与规则实现:安全自动化与持续学习
|
||||
|
||||
- [x] [CONCEPT: 自动化决策] 实现 L0-L5 动作级自动化等级和资格计算器。
|
||||
证据:`AutomationEligibilityCalculator` 将 L0-L4 资格与 L5 人控分开计算,不直接执行动作;单元测试覆盖保守降级。
|
||||
- [x] [CONCEPT: 自动化决策] 为每个可自动动作实现硬白名单、金额上限、证据要求、抽检率和企业上限。
|
||||
证据:系统硬白名单与企业白名单取交集;资金、制度和高风险动作永远人控,金额、置信度、证据、历史精度、样本和抽检任一不足即降级。
|
||||
- [x] [CONCEPT: 风险与预审] 统一风险输出为事实、规则、证据、判断、建议动作和降级原因。
|
||||
证据:结构化 pre-review findings、平台风险投影、整改动作、historical evidence 和 fail-closed 原因已用于申请/报销提交与审批风险卡。
|
||||
- [x] [CONCEPT: 记忆激活] 为首个个人出行方式切片实现 candidate/active/suppressed/expired/revoked 记忆状态机。
|
||||
证据:Candidate/Active 默认有效期分别为 90/180 天;反向或非白名单纠正抑制已激活记忆,过期后新证据创建新 generation,忘记后清值并阻止旧请求复活。
|
||||
- [x] [CONCEPT: 记忆激活] 实现用户、部门、企业记忆优先级、冲突解释、时间衰减和最小样本要求。
|
||||
证据:首个切片限定 `travel_application.transport_mode`,按当前输入/规则 > 企业 > 部门 > 个人解析;组织记忆由管理员显式激活并受 30-365 天有效期约束,个人记忆继续执行 3 Case、2 次审批通过、跨 7 天门槛和时间衰减。同优先级不同值 fail-closed,低优先级覆盖关系返回脱敏解释,过期、撤销和并发换代均不会继续应用。
|
||||
- [x] [CONCEPT: 记忆与学习] 新增租户化个人记忆与证据链接迁移,唯一键保证同一 Expense Case 对同一候选最多计一票。
|
||||
证据:记忆、证据、Decision、Feedback、Outcome、Expense Case 和 Claim 采用租户复合外键;证据同时校验 actor、员工和 Case/单据 owner,跨租户或跨主体关系不能计票。
|
||||
- [x] [CONCEPT: 记忆激活] 为常用出行方式实现隐式激活门槛:3 个不同 Case 的一致服务端纠正、至少 2 次审批通过、证据跨 7 天;草稿、client_observed、accepted 和重试不激活。
|
||||
证据:只有当前有效的 `server_verified` 编辑、`application_submitted` 结果和最新审批状态参与计算;提交事件还必须绑定对应 `expense_claim` 与 Claim ID,invalidated/reversed 事实、同 Case 重放和旧请求均被排除。
|
||||
- [x] [CONCEPT: 记忆与学习] 在服务端申请预览签发前只为空白出行方式应用 active 个人记忆,并返回 memory id、证据数与来源;当前输入、主数据和企业规则优先。
|
||||
证据:`expense_application_preview_workflow.py` 应用记忆后重新执行规则测算和签名;仅缺出行方式时前端仍请求服务端,命中记忆后可直接生成完整核对表。
|
||||
- [x] [CONCEPT: AI 记忆与自动化设置] 提供当前用户记忆查询与“忘记此偏好”接口;撤销后清除可恢复值并在后续预览中不再应用。
|
||||
证据:`GET /api/v1/expense-application-memories/me` 与 `DELETE /api/v1/expense-application-memories/{id}` 均绑定当前租户和主体,越权以 404 隐藏资源存在性。
|
||||
- [ ] [CONCEPT: 记忆与学习] 当规则中心新增按职级、路线或金额禁用交通方式的制度维度时,在个人记忆应用前接入显式允许性判定、冲突抑制原因和回归测试。
|
||||
- [x] [CONCEPT: 记忆与学习] 为 AI 申请预填记录用户原样采纳、显式字段修改和草稿/提交结果证据。
|
||||
证据:`expenseApplicationDecisionFeedback.js`、`useApplicationPreviewEditor.js`、`expense_application_learning.py`、`expense_application_preview_decisions.py`;改回原建议会清除字段差异,日期联动同步记录天数,最终值由服务端 facts 重建。旧预览保持 `client_observed`;服务端签发预览由版本化 HMAC 快照与最终 facts 逐字段比对,字段新增、删除或改值都标记为 `server_verified` 编辑。两者均保持 `training_eligible=false`,尚不直接训练模型或激活记忆。
|
||||
- [x] [CONCEPT: 记忆与学习] 将小财管家、Steward 与通用 Orchestrator 的申请预览切换到认证签发与消费链路,并把结构化预览失败策略收口为 fail-closed。
|
||||
证据:`expense_application_preview_workflow.py`、`orchestrator_expense_application_workflow.py`、`steward_action_executor.py`、`useTravelReimbursementApplicationPreviewActions.js`;签发/动作请求 ID 可稳定重试,草稿续签回写服务端会话,字段接受/纠正按 `server_verified` 落账,未签发结构化预览不能保存或提交。
|
||||
- [ ] [CONCEPT: 记忆与学习] 从字段接受/修改/拒绝、退回、审批覆盖、付款和审计结果生成记忆证据。
|
||||
- [x] [CONCEPT: 记忆与学习] 将已确认 few-shot 扩展到报销预审和审批辅助,并按租户、场景、制度版本过滤。
|
||||
证据:`few_shot_ingestion.py`、`few_shot_retrieval.py`、`expense_claim_historical_evidence.py`、`expense_claim_pre_review.py`;样本关系库和 Qdrant 同时绑定租户、场景、制度与规则版本,检索命中后再由关系库校验,旧版本标记 stale,依赖异常返回空证据。公开 `historical_case_evidence` 与 AI 助手只显示“历史已确认/历史误报,仅供复核”的固定脱敏摘要,不暴露 sample ID、单号、人工评论或结论原文,也不改变确定性结论、阻断数量、预算复核和审批路由。
|
||||
- [x] [CONCEPT: 风险与预审] 完成 golden case、Prompt/规则版本、Canary、回归门禁和自动回滚的工程闭环。
|
||||
证据:Golden evaluator、版本化资产、真实 observation/label、盲审负样本、Release Monitor/Guard、Canary 与 stable 回滚组合测试通过;生产效果仍由试点章节单独验收。
|
||||
- [x] [CONCEPT: 自动化决策] 实现默认 shadow、按动作开放 L3、L4 单独受控的代码策略。
|
||||
证据:Automation Policy 的 `release_stage`、企业上限、硬白名单和 L4 资格检查;资金/制度/高风险动作不进入自动执行。
|
||||
- [ ] [CONCEPT: 自动化决策] 在真实企业按周运行 shadow 并依据签字阈值逐项开放 L3/L4。
|
||||
证据要求:生产样本、抽检、回滚演练和企业授权;本地代码测试不得替代。
|
||||
- [x] [CONCEPT: 降级策略] 对模型、OCR、Qdrant、连接器和记忆服务实现稳定降级和可观测状态。
|
||||
证据:Runtime Chat attempt、OCR worker、Knowledge RAG、连接器 operational events/health、记忆 fail-closed 与发布告警均有明确失败状态和回归。
|
||||
|
||||
## 7. P1 前端实现:审批例外与 AI 记忆
|
||||
|
||||
- [x] [CONCEPT: 审批例外工作台] 按风险、金额、预算影响、等待时长和证据完整度排序审批事项。
|
||||
证据:`GET /api/v1/approval-workbench/items` 由 `ApprovalWorkbenchService` 统一生成可解释优先级、风险/SLA/预算/金额/证据分项和排序原因;审批中心使用该投影,不再用前端临时状态覆盖服务端乐观并发状态。
|
||||
- [x] [CONCEPT: 审批例外工作台] 展示预算压力、风险证据、材料完整度、历史标签和简短 AI 复核建议。
|
||||
证据:工作台返回证据完整度、缺失证据、预算影响、风险摘要、历史确认标签及 `advisory_only` AI 建议;详情页风险卡读取持久化证据、政策依据、贡献度和决策轨迹,并明确禁止从风险卡直接审批。
|
||||
- [ ] [CONCEPT: 审批例外工作台] 补齐统一必要性摘要、完整政策/相似单证据和可编辑审批意见草稿。
|
||||
- [x] [CONCEPT: 审批例外工作台] 为批准、退回和付款建立幂等动作协议与乐观前置条件。
|
||||
证据:动作请求冻结 `request_id`、`expected_status` 和 `expected_approval_stage`;服务端以租户 + 操作人 + 请求 ID 唯一账本、请求指纹、advisory lock、Claim 行锁和同事务事件保证同请求安全重放,不同内容或过期快照返回 409。首次完整 API 响应以不可变快照落账,单据后续推进后重放仍只返回首次结果,不读取或泄露后续状态。
|
||||
- [x] [CONCEPT: 审批例外工作台] 为风险处置动作建立不可变原响应快照和历史安全重放。
|
||||
证据:`20260716_0012` 为 append-only 风险事件增加 `response_json`;新事件在 INSERT 前原子写入完整响应,重放只返回目标事件版本及以前的事件。0012 前无快照事件从 `after_json` 与 `version <= target` 的审计链安全重建,不读取当前处置投影。
|
||||
- [x] [CONCEPT: 审批例外工作台] 建立高风险门禁与类型化风险处置生命周期。
|
||||
证据:严重/高危可行动风险在任何审批副作用前阻断;确认风险、误报、补件、整改、豁免申请和完成处置具有乐观版本、请求指纹、租户权限、只追加事件和 Claim 共用锁。未持久化的原始高风险仍保守阻断,重大风险持久化失败 fail-closed。
|
||||
- [x] [CONCEPT: 审批例外工作台] 补齐批量审批、委托、转交、加签、会签和超时升级交互。
|
||||
证据:`approval_tasks`/`approval_task_events`、`/api/v1/approval-tasks`、`ApprovalTaskWorkspace.vue` 与 `20260716_0013`;支持数据库分页筛选、节点进入时间 SLA、委托/撤销、永久转交、顺序加签、并行会签、手动/调度升级和逐项独立事务批量结果。服务端动作集合与版本为唯一权限事实,筛选/页码可从详情返回恢复。
|
||||
- [x] [CONCEPT: 审批例外工作台] 完成风险豁免申请、批准、拒绝、到期和审计展示闭环。
|
||||
证据:`20260716_0014`、`risk_waiver_decision_policy.py`、风险读投影与 `RiskWaiverActionDialog.vue`/`RiskWaiverRecord.vue`;申请人不能决定自己的申请,仅在职 finance/executive 可决定,过期 fail-closed,全部事件保留版本、人员、请求 ID、原因和关键前后快照。
|
||||
- [ ] [CONCEPT: 审批例外工作台] 接入真实企微/钉钉/邮件触达和处理结果回写。
|
||||
- [ ] [CONCEPT: AI 记忆与自动化设置] 新增“我的 AI 记忆”,支持来源解释、修改、忘记和关闭个性化。
|
||||
- [x] [CONCEPT: AI 记忆与自动化设置] 在申请核对表展示常用出行方式的记忆来源、证据数量和“忘记此偏好”,并在保存/提交后区分候选记录与已应用回执。
|
||||
证据:`TravelReimbursementMemoryPanel.vue` 与独立样式分片承载记忆解释、学习回执和可访问的忘记入口;会话快照可跨刷新恢复已应用记忆。
|
||||
- [x] [CONCEPT: AI 记忆与自动化设置] 新增企业/部门出行方式记忆管理首个低敏切片。
|
||||
证据:租户管理员可创建、查看、更新和撤销企业/部门记忆,配置 30-365 天有效期;创建、更新、撤销使用请求指纹和稳定幂等键,作用域锁与数据库唯一约束阻止并发静默换代。接口只接受“飞机/火车/轮船”,自由文本、金额、客户、项目、附件和支付信息保持禁用。
|
||||
- [ ] [CONCEPT: AI 记忆与自动化设置] 扩展可配置保留策略、敏感等级目录和动作级自动化上限管理;在新增字段前先完成制度允许性与数据分级评审。
|
||||
- [ ] [CONCEPT: 前端] 展示自动化动作、执行依据、撤销入口、抽检状态和版本信息。
|
||||
|
||||
## 8. P2 实现:费用经营与价值证明
|
||||
|
||||
- [x] [CONCEPT: 节省与价值] 新增 Savings baseline、opportunity、realization、evidence、event 和去重/确认表结构。
|
||||
证据:`models/savings.py`、`20260716_0015_savings_value_ledger.py` 与模型/迁移测试。
|
||||
- [x] [CONCEPT: 节省与价值] 实现预计机会、接受、执行中、实际结果、财务确认、拒绝、过期和冲回的严格状态转换。
|
||||
证据:Savings actions/realization 服务、版本/指纹、canonical benefit 去重与 PostgreSQL 并发测试;风险暴露保持独立护栏,不混入节省金额。
|
||||
- [x] [CONCEPT: 费用分析与节省] 持久化员工、部门、费用类型、城市、项目和流程基线,记录窗口和样本量。
|
||||
证据:`savings_baseline_generation.py` 与 baseline/insight 测试;维度、窗口、样本、算法版本、查询指纹和质量等级均冻结。
|
||||
- [ ] [CONCEPT: 费用分析与节省] 接入租户化供应商、合同价、数量和单位价格事实后生成供应商基线。
|
||||
证据要求:真实供应商主数据和合同/采购事实;当前明确返回 `supplier_dimension_unavailable`,不从模拟应付数据伪造。
|
||||
- [x] [CONCEPT: 费用分析与节省] 实现预算预测、异常归因、重复小额浪费和只读政策模拟准备项。
|
||||
证据:Savings insight budget/analysis/attribution;缺正式反事实时 `estimated_savings=None` 且不创建机会。
|
||||
- [ ] [CONCEPT: 费用分析与节省] 接入真实供应商价格漂移分析。
|
||||
证据要求:租户化合同价、数量、单位价和供应商证据;当前保持 coverage gap。
|
||||
- [x] [CONCEPT: CFO 价值看板] 展示现金节省、工时价值、直通率、风险护栏、节省来源和责任人。
|
||||
证据:CFO API/analytics/dashboard;现金、工时、风险和机会分卡,缺数据使用 collecting/unavailable。
|
||||
- [x] [CONCEPT: CFO 价值看板] 实现部门、项目、费用类型、供应商、城市、时间和单据下钻。
|
||||
证据:CFO filters、`cfoValueSourceLinks.js`、URL 状态恢复与前端回归;供应商无事实时显示缺口而非零。
|
||||
- [x] [CONCEPT: CFO 价值看板] 每项节省支持查看基线、建议、执行、实际结果、确认人和证据。
|
||||
证据:`CfoValueOpportunityDrawer.vue` 与 Savings detail projection;无证据时不开放实际结果登记。
|
||||
- [ ] [CONCEPT: 指标与验收] 生成客户月度 ROI 报告,现金节省与工时价值分开披露。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 建立节省归因复核、重复收益去重和客户财务签字流程。
|
||||
|
||||
## 9. P3 实现:商业化与规模复制
|
||||
|
||||
- [x] [CONCEPT: 商业计量] 在 P0 最小租户隔离基础上新增套餐、订阅、账期、配额、用量和增值模块授权模型。
|
||||
证据:商业模型、0016/0019/0021/0024 迁移、管理/查询 API 与商业工作台。
|
||||
- [x] [CONCEPT: 商业计量] 按客户和权威 meter 记录模型、OCR、附件存储、连接器、分析任务与模块用量/成本。
|
||||
证据:Orchestrator、Runtime Chat、OCR、附件、连接器 permit/预占/结算;已识别运行入口资源组合 63 项通过。
|
||||
- [x] [CONCEPT: 商业计量] 建立客户 ROI、平台贡献毛利、成本明细和私有部署成本输入模型。
|
||||
证据:`commercial_analytics.py`、商业价值/成本前端;客户价值、平台收入和内部成本分账,多币种不合并。
|
||||
- [ ] [CONCEPT: 目标与非目标] 定义年度基础订阅、用量超额、智能风控、预算经营、价值洞察、企业集成和私有部署包。
|
||||
- [x] [CONCEPT: 目标与非目标] 定价引擎仅允许对财务确认的已实现节省计算可选价值分享上限。
|
||||
证据:`commercial_pricing.py` 使用 confirmed value、成本下限和成功费封顶;实际合同仍需客户签署。
|
||||
- [ ] [CONCEPT: 连接器] 建立 ERP、HR、SSO、支付、电子档案和消息平台标准实施模板。
|
||||
- [ ] [CONCEPT: 权限与安全] 完成删除传播、数据导出、审计增强和私有部署安全验收,不把基础租户隔离留到本阶段。
|
||||
|
||||
## 10. 测试与验证
|
||||
|
||||
- [x] [CONCEPT: 测试方案] 完成 Expense Case/Link、业务事件幂等、租户边界、同事务回滚、申请转报销同 Case 和付款归档事件首批测试。
|
||||
证据:容器内 `pytest -q server/tests/test_expense_case_service.py` 7 项通过;联合差旅主链路定向回归共 24 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 为 AI 新建申请直接提交补充统一事务回归,覆盖提交成功、事件失败回滚和仅保存草稿三条边界。
|
||||
证据:容器内直接提交定向测试 3 项、申请提交回归 5 项、预算与 Expense Case 回归 13 项通过;相关 Python 文件 `ruff --select F,I` 通过。
|
||||
- [x] [CONCEPT: 测试方案] 为 Expense Case 状态机、事件账本、AI 决策、结果、记忆、自动化和节省服务补充单元测试。
|
||||
证据:Case/Event、AI learning/memory、`test_automation_eligibility.py`、Savings model/service/API/E2E 与全量后端分片均通过。
|
||||
- [x] [CONCEPT: 测试方案] 为服务端会话、管理员保护和 Bootstrap 重配置补充首批安全回归测试。
|
||||
证据:`test_auth_session_endpoints.py`、`test_auth_service.py`、`test_bootstrap_security.py`;容器定向测试覆盖 token 摘要、伪造身份头、过期/撤销、登出原子收尾、业务经理越权和初始化后匿名重配置拒绝。
|
||||
- [x] [CONCEPT: 测试方案] 为租户隔离、跨租户访问、规则/制度发布和双人复核等敏感动作补充安全回归测试。
|
||||
证据:Tenant Identity、Agent Asset、Knowledge、Ontology/Employee、Hermes/Report、Steward、Finance Dashboard、Approval、Release Review 与 ONLYOFFICE 安全测试通过。
|
||||
- [x] [CONCEPT: 测试方案] 为 Expense Case GET 接口补充 owner、审批人、财务、管理员、无权限用户和整 Case 关联事件可见范围的 HTTP 权限测试。
|
||||
证据:`test_expense_case_endpoints.py` 容器内 8 项通过,覆盖跨租户、无 Case、申请与报销关联摘要以及内部字段递归过滤。
|
||||
- [x] [CONCEPT: 测试方案] 为 Alembic baseline、升级、旧数据迁移和回滚边界补充 PostgreSQL 集成测试。
|
||||
证据:fresh PostgreSQL 最终总探针 `87 passed / 0 skipped / 0 failed`,覆盖 62 项迁移、完整降级/再升级、旧结构迁移、复合租户约束、append-only 和有事实回滚保护,head 为 0028。
|
||||
- [x] [CONCEPT: 测试方案] 为当前 migration-owned schema 切片补充一次性 PostgreSQL 集成测试和危险 URL 防误连门禁。
|
||||
证据:`test_alembic_migrations.py` 默认无显式 URL 时跳过,主机和库名必须带 disposable 标记;当前 0012 Head 在 tmpfs PostgreSQL 17 中通过完整迁移验证,覆盖空库升级、重复升级、审批动作账本、风险处置复合租户约束、只追加事件触发器、不可变响应快照及有数据降级拒绝、组织 active 脏数据升级前拒绝、版本化 few-shot 无损降级拒绝、外键级联、base 降级、legacy 哨兵保留、漂移拒绝和再次升级;临时容器自动清理,持久化开发库未被修改。完整 legacy baseline 仍保留在上一条未完成项中。
|
||||
- [x] [CONCEPT: 测试方案] 验证审批动作幂等、任务编排、风险门禁、豁免决定权限和共同锁顺序。
|
||||
证据:容器内本阶段后端回归 `122 passed, 4 skipped`、前端审批/单据中心/风险专项 `60 passed`、Ruff 与生产构建通过;一次性 PostgreSQL 17 空库迁移循环 `1 passed`、审批任务和风险并发 `3 passed`,证明同 request ID 并发只生成一个事件、陈旧版本只有一个胜者、风险重新打开与审批竞争时按 Claim 公共锁读取最新事实。
|
||||
- [x] [CONCEPT: 测试方案] 为连接器幂等、重试、回执、失败恢复、重复付款和对账补充测试。
|
||||
证据:连接器 service/endpoint/config/operational/concurrency 测试覆盖签名、防重放、冲突、错配、失败、ERP、退款和非生产隔离。
|
||||
- [x] [CONCEPT: 测试方案] 跑通申请 → 票据 → 报销 → 预审 → 审批 → 付款 → 入账 → 归档端到端。
|
||||
证据:`test_expense_financial_value_chain_e2e.py` 使用测试密钥自签 production-mode 事件,覆盖 HMAC、ERP posted、独立财务确认、Savings、商业价值和退款冲回契约;不声明真实 provider 或真实现金。
|
||||
- [x] [CONCEPT: 测试方案] 跑通首个个人出行方式切片的 AI 建议 → 用户修改 → 工作流结果 → 记忆激活 → 下次建议变化闭环。
|
||||
证据:容器内记忆、预览决策、迁移与所有权组合回归 56 项通过、1 项条件跳过;一次性 PostgreSQL 迁移循环 1 项通过;前端申请快速预览、个人记忆、Steward 与会话恢复组合 83 项通过,Vite 生产构建通过,Python Ruff F/I 与 `git diff --check` 通过。
|
||||
- [x] [CONCEPT: 测试方案] 跑通首个行为采集切片:AI 申请预填 → 用户接受/显式修改 → 草稿或提交结果同事务落账。
|
||||
证据:`test_user_agent_application_draft_events.py`、`test_reimbursement_endpoints.py`、`expense-application-decision-feedback.test.mjs`、`expense-application-fast-preview.test.mjs`;覆盖可信入口、模板/详情排除、隐私指纹、同事务事件关联、日期联动及异步乱序响应。容器内学习账本与迁移所有权定向 29 项、一次性 PostgreSQL 迁移 4 项和前端关键场景 5 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 跑通风险反馈 → few-shot → golden case → Canary → 回滚工程闭环。
|
||||
证据:风险处置学习、Golden evaluator、发布 runtime/telemetry/review/recall/monitor 与 PostgreSQL 并发测试;低 precision 和失败路径恢复 stable。
|
||||
- [x] [CONCEPT: 测试方案] 跑通节省机会 → 执行 → 实现 → 财务确认 → ROI 看板闭环。
|
||||
证据:`test_savings_value_e2e.py`、`test_expense_financial_value_chain_e2e.py` 和 CFO analytics/frontend 测试。
|
||||
- [x] [CONCEPT: 测试方案] 为已有 Expense Case 事件时间线补充视图模型、404 降级、详情页接入及相关响应式回归,并完成前端生产构建。
|
||||
证据:容器内 `node --test` 定向执行 99 项通过;`npm --prefix web run build` 通过。一次性克隆迁移库上的隔离后端已完成真实登录、身份读取和旧单时间线 200 联调;持久开发库仍未迁移,因此日常本地页面仍保持兼容提示。
|
||||
- [x] [CONCEPT: 测试方案] 为 AI 申请草稿事件补充事务失败回滚、同快照幂等、同 run 多版本留痕和 Steward 重放回归。
|
||||
证据:本轮受影响后端定向回归 36 项、Expense Case 前端兼容测试 9 项和 Python `ruff --select F,I` 在容器内通过。
|
||||
- [x] [CONCEPT: 测试方案] 在现有开发数据的只读一次性克隆上验证迁移、历史回填和真实认证时间线链路。
|
||||
证据:源库与克隆初始签名均为 40 张表、4 张费用单、105 名员工、248 条预算、62 个 Agent 资产;克隆升级后 dry-run 为 eligible=4,apply created=4,重复 apply created=0,四条事件均为 system/suppressed;登录、`/auth/me`、旧单时间线和登出均返回 200,持久库复查不变且仍无 migration-owned 表。
|
||||
- [ ] [CONCEPT: 测试方案] 补充其余前端组件、键盘操作、移动真实接口和完整浏览器关键流程验证。
|
||||
- [x] [CONCEPT: 测试方案] 验证服务端预览决策签发、字段新增/改值差异判定、跨会话拒绝、无 ID 降级防绕过、安全重放、独立密钥权限、续签失败容错、旧路径兼容、会话恢复和真实 PostgreSQL 迁移。
|
||||
证据:容器内新增决策安全用例 9 项、旧快速保存/提交 4 项、申请学习账本 9 项、迁移与所有权 26 项通过且条件型 PostgreSQL 用例 1 项跳过;一次性 PostgreSQL 17 迁移 4 项、前端定向 3 组及 Vite 生产构建已通过。临时 PostgreSQL 已清理,持久开发库 8 张 migration-owned 表数量仍为 0。
|
||||
- [x] [CONCEPT: 测试方案] 验证小财管家、Steward、通用 Orchestrator 的统一预览闭环、认证绑定、fail-closed、稳定重试和跨刷新恢复。
|
||||
证据:容器内后端组合回归 50 项通过,覆盖服务端预览、Steward 动作/图运行、跨租户 checkpoint、Orchestrator 匿名/普通用户/管理员来源授权及决策消费,Python Ruff F/I 通过;前端结构化动作、会话恢复、工作台路由、富确认和 `ai-application-preview-actions` 共 18 项通过,Vite 生产构建通过。共享规则工作簿相关套件按容器内串行执行,避免并行读取正在变动的 XLSX 产生非业务性 ZIP 竞争。
|
||||
- [x] [CONCEPT: 测试方案] 所有后端、集成和迁移测试在当前主应用容器内执行,单条命令最大超时 60s。
|
||||
证据:176 个后端测试文件按有界分片或专项执行;PostgreSQL 探针由应用容器运行,未在宿主机安装替代 venv。
|
||||
- [x] [CONCEPT: 指标与验收] 记录测试、lint、typecheck、构建、端到端和未覆盖风险证据。
|
||||
证据:后端分片与费用主服务通过;PostgreSQL `87/0/0`;Web `815/0` 与 Vite build;Mobile lint/typecheck;新增 Python Ruff、compileall、受门禁核心类/组件 800 行检查和 `git diff --check`。未覆盖的生产/试点项保留在第 11-12 节。
|
||||
|
||||
## 11. 分阶段试点与价值验证
|
||||
|
||||
- [ ] [CONCEPT: 指标与验收] 确认开发前置门禁已完成,试点场景、客户群体、基线和纵向验收未发生未经评审的漂移。
|
||||
- [ ] [CONCEPT: 自动化决策] 前四周只运行建议和 shadow,不开放高风险自动化。
|
||||
- [ ] [CONCEPT: 指标与验收] 验证报销创建时间、自动填充率、首次提交完整率、人工触点和风险护栏。
|
||||
- [ ] [CONCEPT: 节省与价值] 由客户财务确认现金节省、工时价值、归因口径和去重结果。
|
||||
- [ ] [CONCEPT: 商业计量] 计算试点客户的订阅、模型、OCR、实施、支持和集成贡献毛利。
|
||||
- [ ] [CONCEPT: 指标与验收] 输出 90 天 ROI 报告和年度合同/扩展模块建议。
|
||||
|
||||
## 12. 文档收尾
|
||||
|
||||
- [ ] [CONCEPT: 指标与验收] 将试点实际基线替换方向性目标,冻结正式验收阈值。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 更新目标客户、试点场景、支付边界、连接器范围和自动化授权结论。
|
||||
- [x] [CONCEPT: 本轮实现记录] 每个工程阶段完成后补充实现文件、迁移、接口和容器测试证据。
|
||||
证据:本 CONCEPT/TODO、2026-07-16/17 分功能文档与 `engineering-closure-and-production-readiness` 总验收文档。
|
||||
- [x] [CONCEPT: 功能一句话] 确认最终工程实现持续服务于“不用填表、少被退回、真正省钱”的核心结果。
|
||||
证据:零录入票据、服务端预审、审批例外、可信学习、Savings/CFO 与商业计量形成同一费用闭环;真实成效仍由第 11 节企业试点验证。
|
||||
19
document/development/2026-07-13/work-logs.med
Normal file
19
document/development/2026-07-13/work-logs.med
Normal file
@@ -0,0 +1,19 @@
|
||||
# 2026-07-13 综合工作日志
|
||||
|
||||
生成时间:2026-07-13 17:01:12 CST
|
||||
来源:`feature/` 功能点文档与 `dev-logs/bugs/` bug 记录
|
||||
|
||||
## 今日功能点
|
||||
|
||||
- AI 费用闭环与价值证明 概念文档:把申请、消费、票据、报销、审批、付款、入账、分析和持续学习连接成同一个费用事件,让员工少填表、审批人只处理例外、财务能确认真实节省,并让 AI 在可控边界内越用越准确。(TODO 完成 15 项,未完成 98 项;目录:`ai-expense-closed-loop-and-value-proof`)
|
||||
|
||||
## 今日 Bugs
|
||||
|
||||
- bugs:影响:AI 一键新建申请现在与编辑重提共用同一提交语义,预算预占、提交校验、申请风险标记、Expense Case 事件和审批状态保持一致;业务事件写入失败时,申请、预算额度、预算流水、预算预占和 Case 关联不会残留部分成功数据。(文件:`ai-application-submit-bypasses-transaction.md`)
|
||||
- bugs:影响:用户登录后必须使用服务端签发的短期 Bearer 会话,伪造用户名、角色或管理员头不再生效;业务经理不能冒充平台管理员,已初始化系统的基础设施信息与重配置入口不再向匿名请求开放。租户级查询守卫、会话清理/全部退出、登录限流和 SSO 仍按 P0 后续任务推进。(文件:`client-auth-forgery-and-bootstrap-reconfiguration.md`)
|
||||
- bugs:影响:付款或关联解绑中途失败时,不再留下“申请已归档但付款/事件未提交”的半完成状态,为事务 Outbox 提供可靠边界。(文件:`nested-audit-premature-commit.md`)
|
||||
|
||||
## 综合分析
|
||||
|
||||
- 功能侧沉淀了 1 个功能点,问题侧记录了 3 个 bug 修复。
|
||||
- 后续复盘优先看本文件,再回到对应功能点或 bug 文件追溯证据。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 迁移自有表被运行时 bootstrap 越权创建
|
||||
|
||||
日期:2026-07-14
|
||||
文档路径:document/development/2026-07-14/dev-logs/bugs/migration-owned-table-bootstrap-drift.md
|
||||
|
||||
## 修复记录
|
||||
- 09:21:记录 bug 修复:迁移自有表被运行时 bootstrap 越权创建。(bug-log:1347366b)
|
||||
- Git 提交检查:fetch 成功;upstream `origin/main`;upstream 新提交:未发现;本地 ahead 提交:1347366b (HEAD -> main) feat(expenses): secure timeline and draft events;22669a90 feat(expenses): show unified expense event timeline;a616b30c fix(expenses): unify AI application submission transaction;653eda05 feat(auth): add opaque bearer sessions;661990b2 feat(expenses): add transactional expense case events。
|
||||
- 修改:新增 `schema_ownership.py` 统一四张 migration-owned 表的所有权边界,把 Agent Foundation、员工、预算、设置、系统看板、数字员工看板和演示数据初始化中的全量 `create_all` 改为只创建 legacy 表;新增 `migration_preflight.py`,在启动迁移前只读核对 Alembic revision 与自有表集合,遇到无版本表、缺表、多表、未知版本或多版本时直接拒绝,不自动 stamp 或修复。
|
||||
- 操作:调整 `server_start.sh` 的启动顺序和 `alembic.ini` 的脚本绝对定位方式;在无端口、无持久卷、tmpfs 数据目录的一次性 PostgreSQL 17 容器中执行空库升级、重复升级、降级到 base、漂移拦截和再次升级,结束后自动删除临时容器;没有执行持久化开发数据库迁移。
|
||||
- 验证:迁移与预检定向测试 `19 passed, 1 skipped`;一次性 PostgreSQL 真实迁移测试 `4 passed`,最终 revision 为 `20260713_0002`,关键唯一约束、复合索引和外键级联均通过,legacy 哨兵跨降级/再升级保留;受影响服务回归 `46 passed, 1 failed`,唯一失败为既有员工目录历史部门归一化用例,单独运行同样失败,与本次建表 helper 无关;新增文件 Ruff 全规则、旧服务 F/I/B/UP、Shell 语法、Alembic heads 和 `git diff --check` 均通过。
|
||||
- 影响:运行时 bootstrap 不再可能越权重建 Alembic 管理的表,启动会在可能破坏数据前暴露版本/表漂移;完整 legacy schema baseline 仍未建立,后续迁移旧表时必须继续小步验证。
|
||||
@@ -0,0 +1,13 @@
|
||||
# Orchestrator 未认证与费用申请决策绕过
|
||||
|
||||
日期:2026-07-14
|
||||
文档路径:document/development/2026-07-14/dev-logs/bugs/orchestrator-auth-preview-decision-bypass.md
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 15:43:记录 bug 修复:Orchestrator 未认证与费用申请决策绕过。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功,`origin/main` 没有新提交;本地 ahead 9 条,依次为 `5b246307` 服务端申请预览决策、`a662cfe6` 申请反馈账本、`5ed34c2b` 历史费用 Case 回填、`11275e4b` 迁移所有权安全、`1347366b` 安全时间线与草稿事件、`22669a90` 统一费用时间线、`a616b30c` AI 申请提交事务、`653eda05` Bearer 会话、`661990b2` 事务型费用事件;工作树为 dirty,未执行合并或变基。
|
||||
- 修改:`orchestrator.py` 的所有外部来源改为强制认证,`schedule` / `system_event` 进一步要求平台管理员;`OrchestratorService` 用登录态覆盖客户端身份与调度操作人别名并丢弃 preview/decision 注入。`agent_conversations.py` 与 Steward 动作运行时使用可信租户状态隔离会话创建、恢复、幂等检查点和删除;统一申请预览工作流要求 Steward、工作台和小财管家先签发再保存/提交,并复用同会话同快照的 active decision。
|
||||
- 操作:把 Orchestrator 申请 decision 保存到服务端会话,草稿保存后的 next decision 继续回写;工作台 Steward 申请动作改为先打开结构化签发预览。过期/已消费 decision 会清空旧 ID 并进入可重新签发状态;租户 A/B 使用相同用户名、conversation 和 trace 时创建独立会话与 decision。所有验证均在 `x-financial-local-linux` 容器 `/app` 内执行,未修改七个用户规则工作簿。
|
||||
- 验证:后端组合回归 50 项通过,覆盖服务端预览、Steward 动作/图运行、跨租户 checkpoint、Orchestrator 匿名/普通用户/管理员来源授权与 decision 消费,Python Ruff F/I 通过;前端结构化动作、会话恢复、工作台动作路由、富确认和动作脚本 18 项通过,Vite 生产构建通过。共享工作簿并行读取曾触发 `openpyxl` ZIP 竞争,串行复跑相关套件后全部通过。
|
||||
- 影响:未登录用户不能再通过伪造 `source` 调用 Orchestrator,普通用户也不能伪装调度来源触发 Hermes 管理任务;同名用户不能跨租户恢复、删除或重放 Steward 会话检查点。申请保存/提交只消费当前登录会话签发的 decision,重复签发、过期和失败均有确定的安全处理。
|
||||
@@ -0,0 +1,9 @@
|
||||
## 修复记录
|
||||
|
||||
- 18:14:记录 bug 修复:Agent Run 详情绕过财务看板权限暴露跨租户快照与工具响应。
|
||||
- Git 提交检查:18:13 执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交,本地比 `origin/main` ahead 17 个提交,范围为 `661990b2 feat(expenses): add transactional expense case events` 至 `242d68c3 feat(approval): add task workflow and waiver decisions`,本次未合并或改写这些提交。
|
||||
- 修改:新增 `agent_run_access_policy.py`,识别 `finance_dashboard_snapshot` 运行记录并同时核对 route/ontology 中的 `tenant_id`、`data_scope` 及当前用户财务角色;两份范围标签缺失、不一致、跨租户或与当前数据范围不符时一律 fail-closed。
|
||||
- 修改:`agent_runs.py` 在列表返回前过滤无权查看的财务快照,在详情返回 `snapshot_payload`、工具请求和工具响应前执行同一访问策略;跨租户(包括其他租户 admin)按 404 处理,同租户普通用户按 403 处理,只有同租户 `finance`、`executive` 或 admin 能读取完整快照。
|
||||
- 操作:在 `test_finance_dashboard_tenant_security.py` 构造 tenant-a、tenant-b、无租户旧快照和损坏 data scope 快照,覆盖列表、详情、普通用户、财务用户和跨租户管理员路径;所有命令均在 `local-x-financial-linux` 容器执行。
|
||||
- 验证:Ruff 检查与格式检查通过;财务看板、Agent Run 服务和 Ontology 端点定向回归共 17 个测试通过,证明合法同租户详情仍可读取,跨租户载荷、无范围旧记录和损坏范围记录均不可见。
|
||||
- 影响:已登录用户不能再通过猜测或复用 `run_id` 绕过财务看板领域权限读取其他租户的报销金额快照和工具调用明细,列表入口也不会泄露这些快照的摘要记录。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 22:33:记录 bug 修复:Agent Run 轻量列表遗漏语义解析结果。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..@{u}` 为空,未发现 upstream 新提交;本地 `main` ahead 17。审批链相关提交为 `242d68c3`、`28b834ed`、`4940ebc4`,AI 报销学习与申请链相关提交为 `ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`,费用事件与事务链相关提交为 `5ed34c2b`、`1347366b`、`22669a90`、`a616b30c`、`661990b2`,另有迁移安全 `11275e4b` 与会话鉴权 `653eda05`;这些均为当前任务开始前已有的本地提交,本次没有改写或合并。
|
||||
- 修改:在 `agent_run.py` 的轻量仓储查询中按列表已经筛选出的 run_id 批量读取每个 Run 的首条语义解析;在 `agent_runs.py` 中恢复 `AgentRunRead.semantic_parse` 的既有列表契约,同时保留工具调用和大 JSON 字段的轻量预览策略。
|
||||
- 操作:先在容器复现 seeded trace 用例失败,再核对 foundation seed、详情序列化和前端消费字段;没有修改 seed fixture 来掩盖列表序列化断层。
|
||||
- 验证:容器内 `test_agent_runs_service.py`、`test_agent_run_tenant_security.py`、`test_agent_trace_service.py` 与 OnlyOffice 定向测试共 11 项通过;`test_agent_asset_service.py` 全部 28 项通过;相关实现文件 Ruff 与 `git diff --check` 通过。
|
||||
- 影响:Agent Run 列表重新携带真实语义解析摘要,seeded trace、运行轨迹界面和依赖 `semantic_parse` 的流程可继续使用;补充查询仅以租户过滤后的 run_id 为输入,不扩大数据作用域。
|
||||
@@ -0,0 +1,11 @@
|
||||
## 修复记录
|
||||
|
||||
- 22:10:记录 bug 修复:Agent Run 普通日志跨租户读取与统计泄露。
|
||||
- Git 提交检查:22:10 执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交,本地比 `origin/main` ahead 17 个提交,范围为 `661990b2 feat(expenses): add transactional expense case events` 至 `242d68c3 feat(approval): add task workflow and waiver decisions`,本次未合并或改写这些既有提交。
|
||||
- 修改:`agent_run.py` repository 将 `route_json.tenant_id` 与 `ontology_json.tenant_id` 的双重一致性条件下推到 SQL,在排序和 `limit` 前完成租户过滤;任一标记缺失、空作用域或两处标记冲突的记录均 fail-closed,详情也在数据库查询阶段按租户收窄。
|
||||
- 修改:`agent_runs.py` service 新增显式的租户级列表、统计和详情入口,保留带注释的受信任内部跨作用域入口;`agent_run_access_policy.py` 增加返回前二次租户校验,并把财务快照的角色与 `data_scope` 门禁同步下推,防止不可见快照挤占列表和统计窗口。
|
||||
- 修改:Agent Run API 的列表、统计和详情统一使用当前认证租户;普通 run 的跨租户详情、无作用域旧记录和冲突标记记录均返回 404,跨租户管理员也没有旁路,工具 `request_json`/`response_json`、语义 `raw_query` 与错误统计不会跨租户暴露。
|
||||
- 修改:`create_run` 支持显式 `tenant_id` 并同时写入 route/ontology,后续整体更新或 route 合并会保留已验证的双重标记,发现调用方已有冲突标记时拒绝写入;Orchestrator、Ontology、知识同步和财务快照的认证/租户感知创建路径已传入可信 tenant,知识同步的活动任务复用也改为租户内查询。
|
||||
- 操作:新增 `test_agent_run_tenant_security.py`,构造 tenant-a、tenant-b、无作用域、冲突作用域和同租户财务快照,覆盖 limit 前过滤、列表、统计、详情、跨租户 admin 及敏感载荷反向断言;全部命令均在 `local-x-financial-linux` 容器内以 60 秒超时执行。
|
||||
- 验证:Agent Run、财务快照、Ontology 端点、Orchestrator 认证和知识服务定向回归 31 个测试通过;核心变更文件 Ruff、`git diff --check`、PostgreSQL 方言 SQL 编译与代码行数检查通过,最大核心文件 `agent_runs.py` 为 760 行,低于 800 行硬上限。额外 Ontology 全文件回归共 82 个通过、4 个既有业务信号识别用例失败,失败堆栈位于未由本次改动触碰的 `_has_supported_business_signal` 判定。
|
||||
- 影响:Agent Run 日志现在以认证租户为强边界,其他租户、历史无归属记录及损坏作用域记录不再进入列表、统计或详情响应,同时保留内部后台任务按明确可信入口读取的兼容性。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 19:19:修复 AI 报销申请预检的阻断提示与既有交互文案契约不一致问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交;本地相对 upstream ahead 17 个既有提交,依次为 `242d68c3`、`28b834ed`、`4940ebc4`、`ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`、`5ed34c2b`、`11275e4b`、`1347366b`、`22669a90`、`a616b30c`、`653eda05`、`661990b2`;本次未合并、覆盖或改写共享工作树中的其他变更。
|
||||
- 修改:`aiApplicationPrecheckModel.js` 将受限状态和证据快照提示统一为“请先检查”“请先核对”,恢复动作前置关系并与页面测试契约一致。
|
||||
- 操作:检查在 `local-x-financial-linux` 容器内执行,未修改财务规则 XLSX。
|
||||
- 验证:AI 申请预检模型定向前端回归 `4 passed`。
|
||||
- 影响:用户在提交前能清楚理解必须先完成的检查动作,避免因提示语义弱化而误以为可直接继续。
|
||||
@@ -0,0 +1,9 @@
|
||||
## 修复记录
|
||||
|
||||
- 19:01:修复一个 AI 决策关联多条工作流结果后,原申请动作幂等重放可能命中多行的问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交;本地相对 upstream ahead 17 个既有提交,从 `661990b2 feat(expenses): add transactional expense case events` 到 `242d68c3 feat(approval): add task workflow and waiver decisions`,其中包含认证、费用 Case、AI 反馈/记忆、迁移安全、预审、风险处置和审批任务能力;本次未拉取、合并或改写这些历史。
|
||||
- 修改:`ExpenseApplicationLearningService._find_existing()` 不再按 `decision_id` 使用可返回多行的无序 scalar 查询,而是根据原始动作的租户、幂等键和稳定 UUID 精确取回首次 Feedback/Outcome,并二次校验租户与 Decision 关联。
|
||||
- 修改:工作流结果桥接只关联事件发生前最新的已提交 AI Decision;原动作重放只回填同 correlation 的预审结论,不会把后续重新提交的退回或审批结果污染到旧决策。
|
||||
- 操作:在 `local-x-financial-linux` 容器中使用 `/tmp/x-financial-server-venv` 运行 Ruff 和 AI 预览/工作流学习回归;未在宿主机执行 Python 或 pytest。
|
||||
- 验证:`test_expense_application_preview_decisions.py` 与 `test_expense_workflow_learning.py` 组合回归 `17 passed`;新用例验证第二次提交后的退回仅关联最新 Decision,多 Outcome/Feedback 不影响原请求重放。
|
||||
- 影响:申请动作重试不再因后续付款、退回或审计结果增多而出现多行异常,也不会将新一轮流程结果错标到历史 AI 建议上。
|
||||
@@ -0,0 +1,15 @@
|
||||
## 修复记录
|
||||
|
||||
- 2026-07-16 15:10:42 CST:修复审批、退回和付款动作缺少稳定请求标识、预期状态与预期审批节点校验的问题。新增租户 + 操作人 + 请求 ID 唯一账本、请求指纹、PostgreSQL advisory lock 与 Claim 行锁;完全相同的重试返回原结果,请求内容变化或状态已过期返回 409,不重复扣减预算、写审批记录、生成业务事件或付款结果。
|
||||
- 2026-07-16 15:10:42 CST:修复高风险观察仅展示但不阻止审批的问题。审批流程在任何路由、预算和业务状态变更之前检查同租户高危/严重风险;只有已判定误报或完成处置的观察允许继续,阻断响应返回稳定错误码和观察状态,整笔动作账本随事务回滚。
|
||||
- 2026-07-16 15:10:42 CST:前端确认弹窗冻结请求 ID、预期单据状态和审批节点,网络结果不确定时复用同一请求,避免用户重试造成重复业务副作用。
|
||||
- 2026-07-16 15:10:42 CST:验证在 `local-x-financial-linux` 容器完成:费用服务 116 项、审批动作/风险门禁/风险处置组合 26 项、报销接口回归 22 项;一次性 tmpfs PostgreSQL 17 迁移循环 13 项通过。变更 Python 文件 Ruff F/I 与格式检查通过。
|
||||
- 2026-07-16 15:10:42 CST:Git 拉取检查已执行;上游没有本地尚未包含的新提交,当前分支相对上游 ahead 14。已有本地提交包括 `ee88a36b` 分层费用学习、`6bdf65bc` 权威预审、`ae3f02c3` 零录入票据闭环等,本次修复在这些能力之上继续演进。
|
||||
- 2026-07-16 15:33:03 CST:交叉审查后修复三项并发绕过:审批动作不再调用会隐式提交的读取修复逻辑,列表/分页 GET 也不再修改审批节点;审批中心保留服务端原始状态并单独生成展示标签;门禁将持久化风险观察与未匹配原始高风险合并,重大观察持久化失败时 fail-closed。
|
||||
- 2026-07-16 15:33:03 CST:审批、风险处置和 Hermes 扫描统一以 Claim 行锁为公共协调点,锁顺序固定为 Claim → Observation → Disposition;Hermes 锁外计算、锁内刷新并核对状态和更新时间,旧快照被丢弃。一次性 PostgreSQL 并发用例证明风险重新打开与审批竞争时,审批等待后读取最新风险并阻断,Claim 节点和动作账本均保持无副作用。
|
||||
- 2026-07-16 15:33:03 CST:最终容器回归为审批/风险/费用服务 174 项、迁移/所有权 54 项、前端审批/风险 73 项通过,Vite 生产构建通过;一次性 tmpfs PostgreSQL 17 上 13 项迁移和 1 项真实并发测试通过,临时数据库自动清理。再次执行 Git 拉取检查,上游仍无新增提交,本地 ahead 14。
|
||||
- 2026-07-16 15:44:39 CST:提交后交叉审查发现“副作用幂等”不等于“响应幂等”:动作账本虽然已有 `response_json`,重放却重新读取当前 Claim,后续节点变化会返回新状态。现改为首次动作在同一事务保存完整 `ExpenseClaimRead`,首次和重放均从该快照构造响应;快照缺失、单据/状态/节点身份不一致时 fail-closed 返回冲突,不再退回当前投影。
|
||||
- 2026-07-16 15:44:39 CST:新增“首次批准 → 单据继续推进并写入后续内部状态 → 旧 request_id 重放”服务和 HTTP 回归,确认响应逐字段等于首次结果、后续风险字段不泄露、数据库当前状态不被回退;审批动作与报销接口组合 29 项通过。
|
||||
- 2026-07-16 15:44:39 CST:再次完成 Git 拉取检查,上游无新增提交,本地 ahead 15,新增 ahead 为 `4940ebc4` 审批风险安全底座;用户维护的财务规则工作簿和历史日志继续保持未暂存。
|
||||
- 2026-07-16 15:48:38 CST:扩大回归范围后发现两个兼容边界:首次动作若直接返回 Pydantic 快照会破坏既有服务层 ORM 可变语义,因此首次执行继续返回已提交并刷新的 ORM,只有重放从不可变快照构造;HTTP 首次与重放仍逐字段一致。另为风险门禁增加申请/报销阶段匹配,显式错阶段风险不阻断当前流程,未知阶段重大风险仍 fail-closed。
|
||||
- 2026-07-16 15:48:38 CST:路由型重大风险统一使用显式 `route_review`,它允许当前审批进入预算/财务复核,但不会被标记为已解决;普通无 actionability 的原始重大风险继续阻断。容器内费用服务、审批路由、租户/Case、动作协议、接口和风险处置组合 182 项通过。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 2026-07-16 16:50:44 CST:修复 SQLAlchemy Session 关闭 autoflush 后的两处审批任务状态错误。根任务完成后先显式 flush 再生成下一节点,避免仍被查询为开放任务并误标 superseded;SLA 扫描每次升级后 flush,避免同一事务重复扫描生成相同版本事件。
|
||||
- 2026-07-16 16:50:44 CST:修复审批队列前后端 SLA 与权限动作契约不一致。前端统一使用 `due_soon`,服务端正式支持 `escalated` 并拒绝未知筛选;批准/退回仍要求 `can_act`,委托撤销、管理员转交和 SLA 升级则严格按服务端 `available_actions` 展示,不再被前端错误隐藏。
|
||||
- 2026-07-16 16:50:44 CST:修复任务筛选总数污染全局待办数、批量审批后摘要不刷新、队列错误与动作错误相互覆盖,以及详情返回丢失任务筛选/页码的问题。工作台独立查询未筛选 pending 摘要,批量成功聚合回流单据更新,审核路由保存专属风险/SLA/关键词/页码状态,旧响应由请求代次丢弃。
|
||||
- 2026-07-16 16:50:44 CST:修复审批动作网络失败后允许编辑负载却继续复用旧幂等键的问题。单项、委托/转交、加签/会签和批量审批均对请求负载生成稳定指纹;完全相同的重试复用 request ID,理由、候选人、参与人或批量意见变化时自动生成新 ID,避免同键不同内容冲突被误解为操作失败。
|
||||
- 2026-07-16 16:50:44 CST:验证全部在 `local-x-financial-linux` 容器完成。审批/风险/迁移相关后端 `122 passed, 4 skipped`,变更 Python 文件 Ruff 通过;审批、单据中心和风险前端专项 `60 passed`,Vite 生产构建通过;一次性 PostgreSQL 17 空库迁移循环 `1 passed`,审批任务与风险锁并发 `3 passed`,探针数据库均已销毁。
|
||||
- 2026-07-16 16:50:44 CST:执行 `git fetch --all --prune`、工作区状态和上下游提交差异检查;上游无新增提交,本地相对 `origin/main` ahead 16,最近两个检查点为 `28b834ed` 不可变审批响应重放和 `4940ebc4` 安全风险处置。本次继续排除 7 个用户维护的财务规则工作簿及历史未跟踪开发文档。
|
||||
@@ -0,0 +1,9 @@
|
||||
## 修复记录
|
||||
|
||||
- 19:09:修复 CFO 价值看板允许发起无证据手工实际结果、与后端证据门禁冲突的问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交;本地相对 upstream ahead 17 个既有提交,依次为 `242d68c3`、`28b834ed`、`4940ebc4`、`ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`、`5ed34c2b`、`11275e4b`、`1347366b`、`22669a90`、`a616b30c`、`653eda05`、`661990b2`;本次未合并、覆盖或改写共享工作树中的其他变更。
|
||||
- 修改:`CfoValueOpportunityDrawer.vue` 过滤 `record_realization` 空证据入口,并明确提示实际结果必须来自平台付款事件或可追溯的支付、银行、ERP 凭证;`CfoValueActionDialog.vue` 删除不可履行证据要求的手工金额表单;`useCfoValueDashboard.js` 增加防御性拒绝,避免旁路重新提交空证据。
|
||||
- 修改:`cfo-value-dashboard.test.mjs` 将序列化样例改为带外部凭证的请求,并增加前端不暴露空证据登记入口的回归断言。
|
||||
- 操作:全部检查均在 `local-x-financial-linux` 容器内执行,未修改财务规则 XLSX 或历史开发文档。
|
||||
- 验证:CFO/应用壳/财务看板组合前端回归 `22 passed`;Vite 生产构建成功(`2227 modules transformed`,`built in 5.08s`);`git diff --check -- web` 通过。
|
||||
- 影响:用户不会再遇到“页面允许登记、后端必然拒绝”的假动作;在真实连接器或凭证上传入口接入前,现金节省仍只能依靠可信付款事件落账并由独立财务确认。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 21:40:记录 bug 修复:定价毛利率边界舍入越过后端上限。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新增提交;本地 `main` ahead 17 个既有提交,分别为 `242d68c3` 审批任务流、`28b834ed` 不可变动作回放、`4940ebc4` 风险处置、`ee88a36b` 分层费用学习、`6bdf65bc` 权威预审、`ae3f02c3` 零录入票据关联、`54754b55` 个人申请记忆、`211f85d9` 统一申请流、`5b246307` 申请预览决策、`a662cfe6` 申请反馈台账、`5ed34c2b` 历史申请回填、`11275e4b` 迁移归属校验、`1347366b` 费用时间线与草稿事件、`22669a90` 统一费用事件时间线、`a616b30c` AI 申请事务、`653eda05` bearer 会话和 `661990b2` 费用案例事件;均非本次修复产生。
|
||||
- 修改:`commercialWorkspaceModel.js` 先把百分比量化为后端允许的六位小数,再校验目标贡献毛利率严格小于 `0.95`;`CommercialPricingScenarioPanel.vue` 同步把可输入上限收紧到 `94.9999%`,并在模型测试中覆盖舍入临界值。
|
||||
- 操作:在容器 `local-x-financial-linux` 内执行边界探针、商业模型与组件定向测试、全量前端测试、code-size 门禁和 Vite production build。
|
||||
- 验证:临界探针确认 `94.9999%` 序列化为 `0.949999`,`94.999999%` 在请求前被拒绝;商业定向测试 35 项通过,修复后的模型与组件回归 28 项通过,全量前端 802 项通过,code-size 通过,Vite 完成 2246 个模块转换。
|
||||
- 影响:管理员无法再提交一个表面小于 95%、但六位小数量化后等于后端禁值 `0.95` 的场景,避免无意义的 422 往返,同时不改变合法目标毛利率的精度。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 21:09:记录 bug 修复:未配置硬配额被前端误显示为剩余 0。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新增提交;本地 `main` ahead 17 个既有提交,分别为 `242d68c3` 审批任务流、`28b834ed` 不可变动作回放、`4940ebc4` 风险处置、`ee88a36b` 分层费用学习、`6bdf65bc` 权威预审、`ae3f02c3` 零录入票据关联、`54754b55` 个人申请记忆、`211f85d9` 统一申请流、`5b246307` 申请预览决策、`a662cfe6` 申请反馈台账、`5ed34c2b` 历史申请回填、`11275e4b` 迁移归属校验、`1347366b` 费用时间线与草稿事件、`22669a90` 统一费用事件时间线、`a616b30c` AI 申请事务、`653eda05` bearer 会话和 `661990b2` 费用案例事件;均非本次修复产生。
|
||||
- 修改:`commercialWorkspaceModel.js` 的数值规范化显式把 `null`、`undefined` 和空字符串保留为“未知”,不再依赖 JavaScript 的 `Number(null) === 0` 隐式转换。
|
||||
- 操作:在容器 `local-x-financial-linux` 内运行商业服务、模型和 Vue 组件定向 Node 测试。
|
||||
- 验证:商业服务、模型和组件定向测试共 28 项通过;全量 `web/tests/*.test.mjs` 共 795 项通过;`code-size-limits` 通过;Vite production build 完成 2233 个模块转换。权益测试同时覆盖不限量、未配置硬上限和失败关闭状态。
|
||||
- 影响:真实硬配额为 0 时仍显示 0;未配置硬配额时显示“未配置硬上限”,避免管理员把未知配置误判为已耗尽配额。
|
||||
@@ -0,0 +1,11 @@
|
||||
## 修复记录
|
||||
|
||||
- 19:37:修复商业分析 ROI 口径、商业合同可见角色和已消费权益历史可变三类问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交;本地相对 upstream ahead 17 个既有提交,依次为 `242d68c3`、`28b834ed`、`4940ebc4`、`ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`、`5ed34c2b`、`11275e4b`、`1347366b`、`22669a90`、`a616b30c`、`653eda05`、`661990b2`;本次未合并、覆盖或改写共享工作树中的其他变更。
|
||||
- 修改:`commercial_analytics.py` 将客户现金 ROI 统一为“(财务确认现金节省-客户合同收费代理值)/ 客户合同收费代理值”,并把 ratio numerator 改为净收益;禁止截止时间早于窗口开始及无时区分析时间。
|
||||
- 修改:`commercial_access_policy.py` 移除直属经理对套餐、订阅和配额的默认读取权限,仅保留财务、executive 和平台管理员。
|
||||
- 修改:`commercial_admin.py` 在权益已有用量后禁止回改配额、计价配置和有效期,只允许暂停/恢复;完全相同的 PUT 不再无意义增加版本。
|
||||
- 修改:`commercial.py` 强制套餐、订阅、权益、用量和成本事实时间显式带时区,避免跨时区账期歧义。
|
||||
- 操作:全部检查均在 `local-x-financial-linux` 容器内执行,未修改财务规则 XLSX 或历史开发文档。
|
||||
- 验证:商业模型、服务与 HTTP 定向回归 `10 passed`;相关 Ruff 检查通过。
|
||||
- 影响:商业 ROI 与既定合同口径一致,普通直属经理不能查看敏感商业合同,历史用量不会因事后回改权益配置而改变含义。
|
||||
@@ -0,0 +1,23 @@
|
||||
## 修复记录
|
||||
|
||||
- 21:56:记录 bug 修复:非成功 Agent 工具调用可能被商业账本误计量。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新增提交;本地 `main` ahead 17 个既有提交,分别为 `242d68c3` 审批任务流、`28b834ed` 不可变动作回放、`4940ebc4` 风险处置、`ee88a36b` 分层费用学习、`6bdf65bc` 权威预审、`ae3f02c3` 零录入票据关联、`54754b55` 个人申请记忆、`211f85d9` 统一申请流、`5b246307` 申请预览决策、`a662cfe6` 申请反馈台账、`5ed34c2b` 历史申请回填、`11275e4b` 迁移归属校验、`1347366b` 费用时间线与草稿事件、`22669a90` 统一费用事件时间线、`a616b30c` AI 申请事务、`653eda05` bearer 会话和 `661990b2` 费用案例事件;均非本次修复产生。
|
||||
- 修改:`commercial_runtime_policy.py` 把工具调用状态明确分为可计量、采集中和不可计量;`commercial_runtime_metering.py` 只允许 `succeeded/success/ok/completed` 的真实终态调用进入用量与成本账本,`running/pending/queued` 保留待终态补偿语义,`blocked/failed/skipped/cancelled` 不再产生商业事实。同步用 `commercial_runtime_bridge.py` 将未配置租户兼容放行、显式配置后的执行前配额门禁、成功调用后的幂等追加和故障补偿接入真实 `AgentToolCall` 生命周期。
|
||||
- 操作:在 `AgentRunService` 的创建与终态更新后触发桥接计量;在中央工具执行器调用真实 executor 前执行权益预检;将工具执行职责拆到 `orchestrator_tool_execution.py`,让核心编排文件回落到 762 行;所有命令均在 `local-x-financial-linux` 容器内运行并设置 60 秒超时。
|
||||
- 验证:商业运行计量 17 项通过,商业模型/服务/接口/运行计量合计 28 项通过,AgentRun 与 Orchestrator 鉴权相关回归合计 34 项通过;范围内 Ruff、`compileall`、diff 检查和类级 800 行检查通过。全库 code-size 门禁仍被本次范围外的 `RiskRuleGenerationService` 817 行阻断,未在本修复中改动该类。
|
||||
- 影响:未成功完成的工具调用不会再消耗客户配额或形成内部成本;未配置商业计量的既有租户继续执行;已配置租户在执行前受配额约束。计量系统故障时真实工具调用记录保持不变,返回 `requires_reconciliation` 并可按同一工具调用 ID 幂等重试,避免把计量失败伪装成已入账。
|
||||
|
||||
- 22:31:修复执行前只读配额在并发下可超卖、直接调用绕过预占及补偿误判问题。
|
||||
- Git 提交检查:再次执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`;上游无新增提交,本地仍 ahead 17 个既有提交:`242d68c3`、`28b834ed`、`4940ebc4`、`ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`、`5ed34c2b`、`11275e4b`、`1347366b`、`22669a90`、`a616b30c`、`653eda05`、`661990b2`,均非本次修复产生。
|
||||
- 修改:新增 `commercial_runtime_reservations` 运营占位及 `0019` 迁移;在订阅、权益行锁内按“已用量 + 有效预占 + 本次预占”原子校验硬配额。中央 Orchestrator 先生成稳定 tool call ID 并预占,再执行真实工具;成功且真实量不超过预占才追加用量/成本并提交,失败或阻断释放,变量基准没有执行器 hard max 时失败关闭。
|
||||
- 修改:新增持久 `reconciliation_required` 和过期补偿器。缺少执行前预占的旧直接调用不再静默补写正常用量,而是冻结对应容量;补偿器仅在真实工具成功、失败或运行已终止且无调用时结算/释放,运行中或来源不确定继续保留。修复历史过期订阅/权益错误启用门禁、无租户旧运行错误进入补偿、相同预占重试被自身占位判定为额度耗尽三个边界。
|
||||
- 操作:拆出运行时成本解析、周期键、标量校验、预占和补偿模块,相关核心类均低于 800 行;更新模型注册、迁移所有权、前置检查、商业配额投影、AgentRun/中央工具调用点和迁移/并发/补偿测试。所有 Python、Alembic 和 PostgreSQL 验证均在 `local-x-financial-linux` 容器内以 60 秒超时执行。
|
||||
- 验证:运行时定向 25 项通过;商业、Agent、权限、迁移组合 140 项通过;一次性 PostgreSQL 17 商业并发 4 项通过,两个线程竞争一份硬配额时只有一个预占成功;全新 PostgreSQL 0019→0020 完整 Alembic 升降级循环 1 项通过;范围内 Ruff、`compileall`、`git diff --check` 和相关类 800 行检查通过。Orchestrator review 套件保持既有 5 项本体/申请流失败、11 项通过;全库 code-size 仍仅被范围外 `RiskRuleGenerationService` 817 行阻断。
|
||||
- 影响:商业硬配额从“执行前提示”升级为数据库原子 permit,真实工具并发不能再穿透上限;成功事实可幂等结算,计量或成本故障保留可补偿状态且不伪造成功;未配置 runtime meter 的路径继续兼容,历史失效配置不会误拦截用户。
|
||||
|
||||
- 22:39:修复“用量已提交、成本写入失败”缺少持久补偿身份的问题。
|
||||
- Git 提交检查:再次执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`;上游仍无新增提交,本地仍 ahead 17 个既有提交:`242d68c3`、`28b834ed`、`4940ebc4`、`ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`、`5ed34c2b`、`11275e4b`、`1347366b`、`22669a90`、`a616b30c`、`653eda05`、`661990b2`,均非本次修复产生。
|
||||
- 修改:为运行时预占增加 `committed_reconciliation_required` 状态。真实用量已经追加但内部成本失败时,保留真实数量、结算时间和失败原因并进入补偿队列;按同一 tool call 重试只幂等补写成本,成功后恢复 committed。该状态不再计入有效预占,避免真实用量和冻结量重复消耗配额。
|
||||
- 修改:直连调用发生后若当前唯一匹配合同已暂停,允许补偿记录绑定原订阅/权益快照并进入 `reconciliation_required`;正常执行前 reserve 仍严格要求 active/trialing 订阅和 active 权益,不放宽真实执行许可。
|
||||
- 验证:容器内运行时定向 27 项、商业/Agent/权限/迁移组合 183 项通过,另 1 项因未显式配置外部迁移库跳过;全新 PostgreSQL 完整迁移循环 1 项、商业并发 4 项通过。成本故障用例验证 `used=1`、`reserved=0`,重试后用量仍为 1 且只新增 1 条成本;范围内 Ruff 通过。
|
||||
- 影响:成本账本短暂故障不再只依赖日志发现,也不会让客户额度被重复扣减;暂停发生与工具终态竞态时,已发生业务仍有持久、租户隔离的补偿证据。
|
||||
@@ -0,0 +1,15 @@
|
||||
## 修复记录
|
||||
|
||||
- 2026-07-16 11:03 CST:执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`。上游没有新增提交;本地相对 `origin/main` ahead 12,依次包含 `ae3f02c3` 零录入票据关联、`54754b55` 个人申请记忆、`211f85d9` 认证申请工作流、`5b246307` 预览决策签发、`a662cfe6` 申请反馈账本、`5ed34c2b` 历史费用 Case 回填、`11275e4b` 迁移所有权安全、`1347366b` 安全时间线与草稿事件、`22669a90` 统一费用时间线、`a616b30c` AI 申请提交事务、`653eda05` Bearer 会话和 `661990b2` 事务费用事件。
|
||||
- 2026-07-16 11:03 CST:修复报销提交只检查旧 `ai_pre_review` 标记、前端不执行真实预审、规则或单据变化后仍可复用过期结果的问题。新增稳定 review ID、输入/规则指纹、流水线版本、`ready / needs_fix / ready_with_review` 决策、结构化发现与整改动作;高危且可由提交人修复的风险在预算占用前保持草稿并返回结构化 409,预算治理和人工复核风险继续进入审批。
|
||||
- 2026-07-16 11:03 CST:修复预审和提交事件关联断裂、票据归集后预审未刷新、申请批准生成空报销草稿过早把 Expense Case 推入 `claiming` 的问题。预审与提交事件共享 correlation/causation,同输入和规则重试只保留一条预审事件;Case 在申请批准后保持 `approved_to_spend`,票据关联后再进入 `claiming`。
|
||||
- 2026-07-16 11:03 CST:修复 AI 报销草稿、申请预览、Steward 和差旅测算链路未透传非默认租户、事件回落到 `default` 的问题;新增租户隔离与 Case 阶段回归,阻止跨租户 Case/Event 串线。
|
||||
- 2026-07-16 11:03 CST:修复高风险费用申请未进入同部门 P8 预算审批、缺失 P8 未报错、P8 直属领导合并审批缺少审计标记及预算预占提前转移的问题;高风险恢复动态预算路由,中等普通预算预警与低风险申请仍保持快速流转。
|
||||
- 2026-07-16 11:03 CST:修复职责拆分后发票去重助手未接回 `ExpenseClaimService`、英文 reimbursement/travel application 意图被财务门禁拒绝,以及 OCR 成功返回空文档时普通图片被降为“待识别中风险”的问题;复用统一票据键提取器,补齐英文财务信号,并把无有效 OCR 信号附件进入高风险校验。
|
||||
- 2026-07-16 11:03 CST:容器内验证费用服务与审批路由 126 项、费用事件/接口/租户/票据关联 63 项、报销接口全量 20 项和前端预审/时间线 24 项通过;Vite 生产构建、Python compileall、关键未定义符号检查及 `git diff --check` 通过。影响范围为报销预审与提交、费用事件、申请预算路由、租户隔离、票据去重、英文财务意图和空 OCR 附件风险分级,不修改用户维护的财务规则工作簿。
|
||||
- 2026-07-16 11:39 CST:再次执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`。上游仍无新增提交;本地相对 `origin/main` ahead 12,提交摘要与 11:03 检查一致,当前未提交改动继续排除 7 个用户维护的财务规则工作簿和历史未跟踪开发文档。
|
||||
- 2026-07-16 11:39 CST:根据独立审查修复旧 `ready` 预审未覆盖动态风险上下文的问题。提交前申请与报销均重算完整风险,review ID 新增动态 findings 指纹;风险在握手期间变化时返回 `PRE_REVIEW_CHANGED`,新增重复发票/历史风险变化和嵌套无序集合回归,实际提交事件始终关联本次有效预审事件。
|
||||
- 2026-07-16 11:39 CST:修复结构化 409 findings 被前端 `ai_pre_review` 汇总过滤规则丢弃、首票归集后 Case 未进入 `claiming`、申请可整改风险可绕过前端直接 API 提交,以及已解决高风险仍被路由至 P8 的问题。findings 现在保留来源、业务阶段、风险域和 actionability;申请与报销统一在预算前阻断提交人可整改风险,仅人工判断和预算治理风险进入领导/P8。
|
||||
- 2026-07-16 11:39 CST:完成 Claim 核心访问、关联报销后台任务、草稿 ID/单号/最近候选查询和历史风险统计的租户隔离。同用户名、同员工 ID 在不同租户下不能读取、修改、关联或污染预审/P8;默认租户仅对无 CaseLink 历史单保留兼容。新增租户范围 helper 和跨租户对抗测试。
|
||||
- 2026-07-16 11:39 CST:修复规则工作簿损坏或保存中间态导致报销主链抛出 `BadZipFile` / `InvalidFileException` 的问题;单个不可读工作簿现在安全跳过,默认规则和其他已加载规则继续可用。修复只增加运行时容错与临时文件单测,未写入、回滚或替换任何用户 XLSX。
|
||||
- 2026-07-16 11:39 CST:按工程硬约束拆出申请关联、预审端点、职级标准调整、规则清单指纹和租户范围职责;本轮触及的核心服务、端点和前端提交模块均不超过 800 行。容器最终验证核心费用/审批/预审/规则容错 133 项、接口/Case/租户/任务/票据 74 项、前端 25 项通过;Vite 生产构建、全量 Python compileall、变更文件未定义符号检查和 `git diff --check` 通过。
|
||||
@@ -0,0 +1,17 @@
|
||||
## 修复记录
|
||||
|
||||
- 17:11:记录 bug 修复:财务看板跨租户聚合、快照串租户复用与普通用户越权读取。
|
||||
- Git 提交检查:执行 `git fetch --all --prune` 后未发现 `HEAD..origin/main` 新提交;本地比 `origin/main` ahead 17 个提交,最新为 `242d68c3 feat(approval): add task workflow and waiver decisions`,其余为 `28b834ed` 至 `661990b2` 的审批、费用闭环、AI 记忆、迁移安全和认证能力提交,本次没有合并或改写这些历史。
|
||||
- 修改:`finance_dashboard.py` 通过 `ExpenseClaimTenantScopeMixin` 按 Expense Case Link 聚合当前租户报销单;缺少 `tenant_id` 的旧预算表仅允许 `default` 租户读取,其他租户返回带原因的明确空预算;新增 `finance_dashboard_scope.py` 统一声明报销与预算数据范围。
|
||||
- 修改:`finance_dashboard_snapshot.py` 把 `tenant_id`、数据范围和完整时间参数纳入无歧义缓存键,并在 SQL 查询、Agent Run 路由及工具请求中同时校验租户和范围;`finance_dashboard_scheduler.py` 显式固定 `default` 系统租户,非默认租户不能调用默认定时快照入口。
|
||||
- 修改:新增 `finance_dashboard_access_policy.py`,`analytics.py` 显式接收 `CurrentUserContext`,仅允许 `finance`、`executive` 或 admin 只读访问财务看板,普通用户和仅有 `budget_monitor` 角色的用户返回 403。
|
||||
- 操作:所有检查均在 `local-x-financial-linux` 容器内执行;新增租户 A/B、default 历史单、旧预算、快照缓存与接口权限测试,没有触碰受保护的财务规则 XLSX 和历史开发文档。
|
||||
- 验证:Ruff 对本次 7 个 Python 模块及新增测试检查通过;`test_finance_dashboard_tenant_security.py` 与 `test_finance_dashboard_service.py` 共 8 个测试通过。补充运行财务报告与数字员工回归时 4 个测试通过、1 个既有财务周报用例失败,原因是用例将“当前时间减 2 天”的数据断言进“上一完整周”窗口,和本次租户过滤无关。
|
||||
- 影响:财务看板不再读取其他租户的报销单或把 default 旧预算暴露给非默认租户;相同时间参数的租户快照不会互相命中,后台默认快照和前台读取权限也有了可审计的显式边界。
|
||||
|
||||
- 18:14:补齐财务看板租户 fail-closed 边界与模块拆分验证。
|
||||
- Git 提交检查:18:13 再次执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交,本地仍比 `origin/main` ahead 17 个提交,范围为 `661990b2 feat(expenses): add transactional expense case events` 至 `242d68c3 feat(approval): add task workflow and waiver decisions`,未合并、改写或覆盖这些提交及工作区内其他智能体改动。
|
||||
- 修改:`finance_dashboard_access_policy.py` 对空白租户上下文直接拒绝,避免异常认证上下文回落到 default;将预算摘要、预算卡片和预算瓶颈投影提取到 `finance_dashboard_budget.py`,`finance_dashboard.py` 从 920 行降至 746 行,保持租户过滤和原 API 不变。
|
||||
- 操作:只在 `local-x-financial-linux` 容器内执行 Ruff、财务看板服务与接口测试、Agent Run 服务回归和 Ontology 端点回归;未触碰财务规则 XLSX、Savings 迁移或前端文件。
|
||||
- 验证:Ruff 检查与格式检查通过;财务看板、Agent Run 服务和 Ontology 端点定向回归共 17 个测试通过。另跑数字员工、系统看板和财务报告回归时 6 个通过、1 个既有周报窗口用例失败;该用例在周四写入“当前时间减 2 天”的单据,却断言它属于“上一完整周”,失败与本次改动无关。
|
||||
- 影响:租户身份缺失时财务看板不再隐式读取 default 数据;预算展示职责被独立封装,后续继续扩展财务指标时不会把核心聚合模块推过项目 800 行硬上限。
|
||||
@@ -0,0 +1,11 @@
|
||||
## 修复记录
|
||||
|
||||
- 22:29:记录 bug 修复:非生产财务回执会进入核心付款状态机,连接器配置缺少版本化生命周期审计,normalized payload 冗余保存完整单号。
|
||||
- Git 提交检查:执行 `git fetch --all --prune` 后未发现 `HEAD..origin/main` 新提交;当前 `main` 相对 `origin/main` ahead 17,范围为 `661990b2..242d68c3`,包含认证、费用事件、AI 学习、审批任务/风险处置和迁移所有权等既有基础提交,本轮未合并或改写这些提交。
|
||||
- 修改:`financial_connector_ingestion.py` 与新增 `financial_connector_simulation.py` 将 test/mock/staging 六类事件收口为 `simulation_only` 只读事实,禁止修改 Claim、对账、ERP、Business Event、归档和 Savings;生产 origin 查询只接受同来源生产事实,不能引用模拟结算触发冲回。
|
||||
- 修改:`financial_connector_config_lifecycle.py`、`financial_connector_config_audit.py`、配置 schema/API 和 `FinancialConnectorConfigEvent` 新增带 expected version、认证 actor、request ID、reason 的 activate/disable/rotate 状态机;新配置只能 disabled 创建,激活/轮换前解析服务端 `secret_ref` 并校验 HMAC 密钥强度,审计前后快照不保存密钥引用或明文。
|
||||
- 修改:`20260716_0020_financial_connector_config_lifecycle.py` 基于 0019 增加配置 version、复合租户约束和 PostgreSQL append-only 审计 trigger;受控移除历史 connector event normalized payload 中的完整 `claim_reference`,保留金额/币种、必要尾号和内容指纹,并把可能已有旧非生产副作用的响应标记为 `legacy_nonproduction_effect_unknown`,不伪装为新策略下的无副作用模拟事实。
|
||||
- 修改:保留并验证 HMAC v2 对 tenant/provider/key version/timestamp/method/path 的绑定,拒绝共享密钥跨来源重放;ERP 回执按协议只要求 origin、Claim、金额和币种,不再错误强制重复支付参考号;空白密钥即使长度足够也按强度不足失败关闭。
|
||||
- 操作:在独立一次性 PostgreSQL 17 容器完成空库升级到 0020、schema/约束/trigger 探针、并发激活单版本胜者、0019→0020 历史脱敏探针和完整升降级循环;验证完成后删除临时容器。同步更新模型注册、迁移所有权、preflight、HEAD revision、迁移断言及连接器 CONCEPT/TODO。
|
||||
- 验证:容器内连接器/配置/费用价值链/迁移组合回归 `140 passed, 1 skipped`;PostgreSQL 连接器并发 `3 passed`;全新 PostgreSQL 完整迁移循环 `1 passed`,商业迁移 agent 在另一空库复跑同样 `1 passed`;Ruff、全树 `git diff --check` 均通过,连接器核心文件最大 509 行,低于 800 行硬上限。
|
||||
- 影响:模拟、测试和预发布环境现在可以安全演练完整事件协议而不会改账;只有 `production_verified` 且精确匹配的回执能够推进付款、ERP 与 Savings。管理员可以安全激活、停用和轮换密钥,所有配置动作可追溯且不泄露密钥或完整单号。
|
||||
@@ -0,0 +1,9 @@
|
||||
## 修复记录
|
||||
|
||||
- 19:01:修复风险规则 Golden 评测的 FP/FN 统计颠倒、修订版本未进门禁以及异常/空用例默认放行问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交;本地相对 upstream ahead 17 个既有提交,依次为 `242d68c3`、`28b834ed`、`4940ebc4`、`ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`、`5ed34c2b`、`11275e4b`、`1347366b`、`22669a90`、`a616b30c`、`653eda05`、`661990b2`;本次未合并、覆盖或改写共享工作树中的其他变更。
|
||||
- 修改:`risk_rule_golden_evaluator.py` 将 false positive 改为“期望不命中但实际命中”,false negative 改为“期望命中但实际未命中”,Precision/Recall 分母恢复正确。
|
||||
- 修改:初次发布和 revision 发布均强制执行 Golden 门禁;缺规则文档、缺 rule code、缺 active Golden case 或评测异常均 fail-closed。仅当 `GOLDEN_SET_GATE_ENABLED=false` 被显式配置时允许跳过,且仍写入 status=skipped 的 `AgentAssetTestRun`。
|
||||
- 操作:在 `local-x-financial-linux` 容器中运行 Golden、发布、修订和安全自动化定向回归,并执行 Ruff;未修改财务规则 XLSX。
|
||||
- 验证:Golden、release guard 和自动化资格组合回归 `29 passed`;并行定向发布/修订回归纳入总计 `62 passed`,评测异常、空用例和缺配置均留下 failed 记录并拦截发布。
|
||||
- 影响:误报不再被错计为漏报,Precision/Recall 可用于可信的 Canary 与回滚判断;新规则和修订规则不能因评测器失败或没有黄金用例而静默上线。
|
||||
@@ -0,0 +1,7 @@
|
||||
## 修复记录
|
||||
|
||||
- 2026-07-16 13:42:57 CST:在分层记忆回归测试中发现,AI 新建非 `default` 租户费用申请后会立即调用 `submit_claim()`,但此时尚未建立租户化 Expense Case Link;提交服务重新查询单据时按 Case Link 执行租户过滤,因此刚创建的申请返回不可见并报“未找到可提交的申请单”。
|
||||
- 2026-07-16 13:42:57 CST:在 `UserAgentApplicationSlotMixin._create_expense_application_record()` 完成 Claim `flush` 后、进入草稿或提交分支前,使用当前认证租户同步建立 Expense Case 与 Claim Link,使新建记录、租户归属和后续提交保持在同一数据库事务;失败时仍由既有外层回滚,不留下半成品 Case 或 Link。
|
||||
- 2026-07-16 13:42:57 CST:按项目规范执行 `git fetch --all --prune`、上下游状态与提交差异检查;当前分支相对 `origin/main` ahead 13,未发现新的上游提交,本地 ahead 包含最近的费用闭环、个人记忆、零录入票据和权威预审阶段提交。
|
||||
- 2026-07-16 13:42:57 CST:在 Docker 容器 `local-x-financial-linux` 内运行原失败用例、分层记忆服务与组织记忆接口测试,结果 `9 passed`;验证非默认租户申请可完成预览提交并生成学习回执,组织记忆生命周期和租户隔离未受影响。
|
||||
- 影响:修复 AI 助手、Steward 和 Orchestrator 共用申请创建链路在非默认租户下无法直接提交的问题;默认租户与既有草稿事件继续复用同一 Case,不改变确定性预审、预算和审批规则。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 22:33:记录 bug 修复:OnlyOffice 回调测试仍 patch 已拆分前的网络符号。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..@{u}` 为空,未发现 upstream 新提交;本地 `main` ahead 17。审批链相关提交为 `242d68c3`、`28b834ed`、`4940ebc4`,AI 报销学习与申请链相关提交为 `ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`,费用事件与事务链相关提交为 `5ed34c2b`、`1347366b`、`22669a90`、`a616b30c`、`661990b2`,另有迁移安全 `11275e4b` 与会话鉴权 `653eda05`;这些均为当前任务开始前已有的本地提交,本次没有改写或合并。
|
||||
- 修改:把 `test_onlyoffice_callback_summary.py` 的网络 patch 目标切换到实际查找符号的 `agent_asset_onlyoffice` 模块,并删除对旧版本元数据方法及“回调自行生成 change_note”的过期假设;测试现在验证下载内容、回调用户和 `onlyoffice` 来源被完整委托给统一上传流程。
|
||||
- 操作:没有在 `agent_assets` 中恢复底层 `urlopen` 兼容导出,因为该符号不是公共 API,且兼容别名也无法拦截拆分模块中的真实调用;差异摘要仍由 `upload_rule_spreadsheet` 统一生成和审计,已有服务测试覆盖其工作表/单元格统计。
|
||||
- 验证:容器内回调定向测试通过;包含该测试的 Agent Run/租户/轨迹组合共 11 项通过,`test_agent_asset_service.py` 全部 28 项通过;测试文件 Ruff 与 `git diff --check` 通过。
|
||||
- 影响:OnlyOffice 回调测试重新拦截真实网络边界,不会发出外部请求,也不会因内部模块拆分误报;生产回调与摘要生成职责保持不变。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 2026-07-16 14:29:13 CST:组织记忆创建在“检查 active → 新建 generation”之间存在竞态,后到请求可能静默压制并发产生的新版本;创建、更新的幂等键也没有绑定请求内容,撤销操作则无法在响应丢失后安全重试。
|
||||
- 2026-07-16 14:29:13 CST:为企业/部门记忆增加租户与作用域级事务锁、幂等请求锁、请求 payload 指纹和数据库唯一约束;创建、更新、撤销现在都满足“同键同内容重放、同键不同内容 409”,新 generation 激活前严格核对预期 active 集合。锁协调职责拆到 `organization_memory_locks.py`,主服务从接近 800 行降至 761 行。
|
||||
- 2026-07-16 14:29:13 CST:加固 `20260716_0009` 迁移:active 唯一索引只约束企业/部门作用域,DDL 前先检测重复 active 并 fail-fast,避免个人学习记忆换代受到影响;非 PostgreSQL 在任何结构变更前明确拒绝,避免 SQLite 留下半迁移列。
|
||||
- 2026-07-16 14:29:13 CST:执行 `git fetch --all --prune`、工作区状态和上下游提交差异检查;`origin/main` 没有新增提交,当前分支 ahead 13,最近本地检查点为权威提交前预审、持久化零录入票据和个人申请记忆,本次未改写这些提交。
|
||||
- 2026-07-16 14:29:13 CST:全部验证在 `local-x-financial-linux` 内执行并设置 60 秒超时。分层与组织记忆回归 44 项、迁移/模型/租户组合 70 项通过;前端记忆与申请链路 18 项通过,Vite 生产构建通过,变更 Python 文件 Ruff F/I 与 compileall 通过。一次性 tmpfs PostgreSQL 17 完整迁移循环 9 项通过,临时容器自动清理,开发数据库未修改。
|
||||
- 影响:管理员并发维护组织记忆时不再发生静默覆盖,网络重试不会产生重复版本或把不同请求伪装成成功;个人记忆状态机保持原有最小样本与换代语义,迁移遇到脏数据时在写 DDL 前停止并给出明确错误。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 报销审批非参与者错误映射与陈旧测试契约
|
||||
|
||||
日期:2026-07-16
|
||||
文档路径:document/development/2026-07-16/dev-logs/bugs/reimbursement-approval-task-access-error-mapping.md
|
||||
|
||||
## 修复记录
|
||||
- 21:38:记录 bug 修复:报销审批非参与者错误映射与陈旧测试契约。(bug-log:242d68c3)
|
||||
- Git 提交检查:已手工执行 `git fetch --all --prune`,upstream `origin/main` 无本地尚未包含的新提交;当前分支 ahead 17 且工作区已有其他智能体和用户的未提交改动,因此未自动合并或变基。ahead 包括审批任务与风险链 `242d68c3`、`28b834ed`、`4940ebc4`,AI/费用学习与预审链 `ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`,Expense Case/时间线与事务链 `5ed34c2b`、`1347366b`、`22669a90`、`a616b30c`、`661990b2`,以及迁移安全 `11275e4b`、认证会话 `653eda05`。
|
||||
- 修改:`reimbursement_approval_actions.py` 将审批任务访问策略抛出的 `LookupError` 显式映射为 404,避免非任务参与者通过报销审批/退回接口触发 500;`test_expense_claim_service.py` 与 `test_reimbursement_endpoints.py` 同步到正式审批任务的资源隐藏与任务冲突契约,并增加申请人审批、退回均无状态和动作账本副作用的 HTTP 回归。
|
||||
- 操作:完整读取 `agent-change-log` Skill,沿 `ExpenseClaimActionProtocolMixin → ApprovalTaskLifecycleService → ApprovalTaskAccessPolicy` 诊断调用链;保留“非参与者不可读取任务”的权限语义,没有放宽审批人、管理员、申请人或租户边界;随后运行日志 helper 创建本记录并补齐实际证据。
|
||||
- 验证:在 `local-x-financial-linux` 容器中,原失败用例与新增 HTTP 用例 2 项通过;审批任务、审批路由和报销接口组合 65 项通过;费用服务审批、退回和付款相关筛选回归 30 项通过;相关 Python 文件 Ruff F/I 检查通过。
|
||||
- 影响:申请人或其他非任务参与者调用审批动作时稳定返回 404,不再出现服务端 500,也不会创建动作账本、审批事件或修改 Claim;可见但不可操作的任务仍按既有策略返回 403,状态/版本冲突继续返回 409。
|
||||
@@ -0,0 +1,27 @@
|
||||
# AI 发布门禁全局资产越权与快照损坏静默放行
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 19:48:记录 AI 分阶段发布的租户越权与运行时完整性失效修复。
|
||||
- Git 提交检查:执行 `git fetch --all --prune` 后未发现 `HEAD..origin/main` 上游新提交;本地 `main` 比上游 ahead 17,最新为 `242d68c3 feat(approval): add task workflow and waiver decisions`,其余为审批安全、AI 费用学习、报销预审、迁移安全与会话认证等既有检查点,本次未合并或改写这些提交。
|
||||
- 修改:`agent_asset_release_guard.py` 为所有发布查询、启动、评测、晋级和回滚入口增加平台全局资产管理边界;未绑定租户的共享资产只允许平台管理员管理,租户级 manager 统一返回不可见。`agent_asset_releases.py` 与既有风险规则发布入口只从认证上下文传递平台管理员权限,不能由请求体或自报 actor 绕过。
|
||||
- 修改:`expense_claim_risk_rule_loader.py` 在候选快照与稳定快照均无法通过 SHA-256 完整性校验时生成强制阻断信号,`expense_claim_platform_risk.py` 将该信号转换为 critical/block 风险,而不是把损坏规则当成“未命中”静默跳过。
|
||||
- 操作:仅通过 `apply_patch` 修改源码和测试;保护现有财务规则 XLSX、历史未跟踪开发目录及其他智能体改动,未执行提交、推送或数据库破坏操作。
|
||||
- 验证:在 `local-x-financial-linux` 容器内运行 Ruff 定向检查通过;`test_agent_asset_release_guard.py` 与 `test_agent_asset_release_runtime.py` 共 12 项测试全部通过,新增覆盖租户 manager 无权管理全局发布资产、平台 admin 可管理,以及双快照损坏后报销自动流转被阻断。
|
||||
- 影响:单租户管理员不再能影响所有企业共享的风险规则;受控发布元数据损坏时系统优先停流并提示平台恢复稳定版本,避免风险规则失效后继续自动审批。
|
||||
|
||||
- 19:53:继续修复发布质量指标可由管理人员手工伪造的问题。
|
||||
- Git 提交检查:再次执行 `git fetch --all --prune`,`HEAD..origin/main` 仍无上游新提交;本地仍 ahead 17,提交范围与 19:48 检查一致,未自动合并、变基或覆盖共享工作区改动。
|
||||
- 修改:新增 `agent_asset_release_monitor_auth.py`,评测请求必须使用至少 32 字节独立密钥,对时间戳、租户、资产、release ID、当前阶段和规范化请求体摘要做 HMAC-SHA256 签名;仅允许 5 分钟时钟窗口并使用常量时间比较。`agent_asset_releases.py` 在写入评测记录前验证签名,密钥未配置返回 503,缺失、过期或错误签名返回 401。
|
||||
- 操作:保留服务层直接写入能力供同进程可信监控使用,但关闭普通 HTTP manager 仅凭自报数字写入“通过”证据的路径;签名绑定 release ID 和阶段,旧阶段请求不能在晋级后重复使用。
|
||||
- 验证:容器内 Ruff 定向检查通过;发布 guard/runtime 共 12 项测试通过,HTTP 用例新增未签名评测返回 401,同时签名监控数据仍可触发 shadow→Canary→active 和指标越界自动回滚。
|
||||
- 影响:人工管理权限与机器监控证据分离,发布门禁不再把未经认证的手工数字当成可信质量结果。
|
||||
|
||||
- 21:51:补齐“签名合法但指标仍可伪造”的第二层真实性修复,并接通真实证据自动回滚。
|
||||
- Git 提交检查:执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 与 `git log @{u}..HEAD`;`origin/main` 无新提交,本地仍 ahead 17,最新提交仍为 `242d68c3 feat(approval): add task workflow and waiver decisions`,其余 ahead 提交为既有审批、AI 费用学习、报销预审、迁移安全和会话认证检查点。本次未合并、变基、提交或推送,也未触碰受保护财务规则 XLSX。
|
||||
- 修改:Release Monitor HTTP 契约改为禁止额外字段的空触发请求,HMAC 继续绑定 tenant/asset/release/stage,但 `total`、`precision` 等质量数字只能由服务端查询 append-only observation/label 聚合;即使签名正确,携带伪造汇总字段也返回 422。
|
||||
- 修改:真实 shadow/Canary/active manifest 执行追加脱敏 observation,类型化 `RiskDispositionEvent` 追加可信标签并即时触发 Guard;相同 release 聚合快照复用同一测试运行,低 precision 或结构化运行失败自动回滚 stable。即时监控失败由租户周期调度补偿,`collecting` 不写虚假 passed。
|
||||
- 修改:新增 `0018` 两张发布遥测表、复合租户/release 外键、幂等唯一约束和 PostgreSQL append-only 触发器,并登记主 metadata、迁移所有权与启动前置检查;将风险处置发布同步拆到独立服务,使核心 `risk_dispositions.py` 保持 800 行以内。
|
||||
- 操作:全部源码与文档通过 `apply_patch` 修改;用独立 `pgvector/pgvector:pg17` 一次性数据库验证完整迁移链及数据库约束,未连接或变更生产数据库。
|
||||
- 验证:容器内发布 Guard/Runtime/Telemetry/Monitor/风险处置组合 44 项通过;调度器、Monitor 与风险处置定向 24 项通过;迁移/schema owner 静态回归 112 项、启动前置检查 77 项、一次性 PostgreSQL 完整迁移循环 1 项通过;PostgreSQL savings/commercial/financial connector/approval 并发探针共 14 项通过。Ruff 定向检查与 `git diff --check` 通过(文档回填后仍需最终全量复验)。
|
||||
- 影响:发布质量门禁不再信任“会签名的调用方”提交的汇总数字,而是由数据库真实执行与可信人工结论生成;遥测暂时失败只会阻止晋级,不会撤销已成功人工处置或关闭现有 stable 保护。
|
||||
@@ -0,0 +1,13 @@
|
||||
## 修复记录
|
||||
|
||||
- 2026-07-16 15:10:42 CST:修复风险观察列表和完整证据链读取范围过宽的问题。风险池仅允许管理员、财务、管理层和预算监控角色访问;单据完整风险证据只允许管理员或当前审批人查看,申请人和无权主体统一以 404 隐藏资源存在性,所有查询继续绑定可信租户。
|
||||
- 2026-07-16 15:10:42 CST:修复风险反馈只有自由文本状态、无法可靠表达处置过程的问题。新增类型化判定与生命周期投影,区分确认风险、误报、补件、整改、豁免申请和完成处置;使用乐观版本、请求指纹、行锁和仅追加事件表保证并发与审计一致性,已完成状态不可重新判定。
|
||||
- 2026-07-16 15:10:42 CST:修复前端读取错误 JSON 字段名导致证据、政策依据、贡献度和决策轨迹可能丢失的问题;风险证据卡改用服务端持久字段并展示处置状态、版本和明确动作,高风险卡不提供直接审批入口。
|
||||
- 2026-07-16 15:10:42 CST:验证在 `local-x-financial-linux` 容器完成:风险观察、风险处置、审批工作台与风险门禁组合 26 项,前端审批/风险/动作协议定向 72 项,Vite 生产构建通过;一次性 PostgreSQL 验证了复合租户外键、唯一约束、仅追加触发器和非空降级保护。
|
||||
- 2026-07-16 15:10:42 CST:Git 拉取检查已执行;上游没有本地尚未包含的新提交,当前分支相对上游 ahead 14。已有 ahead 提交均保留在修复上下文中,未修改用户正在维护的财务规则工作簿和历史未跟踪日志。
|
||||
- 2026-07-16 15:33:03 CST:交叉审查后收紧处置动作语义:误报、补件和豁免申请必须提供服务端校验的说明,完成处置必须提供处理结果;版本冲突返回明确消息并触发前端刷新,已完成生命周期不再展示重复动作。
|
||||
- 2026-07-16 15:33:03 CST:处置服务在 Claim 公共锁内重新确认当前审批人,Observation 和 Disposition 使用租户范围行锁及固定顺序;风险观察写入同样获取 Claim 锁。完整证据、风险池和处置权限分别执行最小授权,跨租户和无权资源继续以 404 隐藏。
|
||||
- 2026-07-16 15:33:03 CST:最终验证全部在 `local-x-financial-linux` 容器完成并设置 60 秒超时;相关后端 174 项、迁移/所有权 54 项、前端 73 项与生产构建通过,一次性 PostgreSQL 17 的结构和并发验证 14 项通过。规则工作簿和历史未跟踪日志未被改动或纳入提交。
|
||||
- 2026-07-16 15:44:39 CST:交叉审查发现风险处置重放在权限复核前读取当前 Disposition 及全部事件,旧 request_id 可能看到首次动作之后的处置状态。新增 `20260716_0012`:完整响应在 append-only Event 的 INSERT 前原子写入 `response_json`,重放只返回该不可变快照并将 `replayed=true`;不得通过 UPDATE 补写审计事件。
|
||||
- 2026-07-16 15:44:39 CST:兼容 0012 前历史事件时,不回读当前 Disposition,只使用目标 Event 的 `after_json` 和 `version <= target` 的只追加事件重建;快照身份、租户、观察、处置、请求和版本任一不一致均 fail-closed。新增后续补件/整改后重放首个裁决的服务与 HTTP 回归,确认只返回版本 1 和当时事件。
|
||||
- 2026-07-16 15:44:39 CST:容器内风险处置、迁移和前置检查 69 项通过、1 项条件跳过,审批快照与报销接口 29 项通过;一次性 tmpfs PostgreSQL 17 的 0012 Head 迁移与审批风险真实并发共 17 项通过,临时数据库自动清理且持久开发库未修改。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 2026-07-16 16:50:44 CST:修复首次风险尚无 disposition 时前端丢弃 Observation 顶层 `available_actions/read_only_reason`、导致“确认风险”等首次处置入口永久隐藏的问题。前端只消费服务端权威投影,缺少 disposition 时构造 version 0 展示模型,不进行角色猜测或本地越权推导。
|
||||
- 2026-07-16 16:50:44 CST:修复页面持续打开跨过豁免到期点后仍显示“豁免有效”的问题。证据卡按 `waiver_expires_at` 注册到期刷新,切换单据和卸载时清理定时器;服务端过期门禁继续 fail-closed,前端不再长期展示过期授权。
|
||||
- 2026-07-16 16:50:44 CST:修复风险豁免界面只展示当前快照、拒绝后重新申请会让上一轮审计信息消失的问题。服务保留 actor、request ID、before/after JSON,记录组件展示申请、批准、拒绝的追加事件、版本、人员、时间、原因、申请关键字段和请求 ID。
|
||||
- 2026-07-16 16:50:44 CST:风险豁免申请、批准、拒绝和到期后的任务投影刷新与风险处置同事务执行,保持 Claim → Observation → Disposition → Task 固定锁顺序;刷新风险、证据、批量资格和优先级时保留 SLA 时间窗与显式升级原因。
|
||||
- 2026-07-16 16:50:44 CST:容器内风险处置、门禁与审批投影定向 `30 passed`,扩展回归 `39 passed, 3 skipped`,前端风险专项 `12 passed`,生产构建通过;一次性 PostgreSQL 并发验证包含风险重新打开与审批竞争,未发现绕过或脏写。
|
||||
- 2026-07-16 16:50:44 CST:Git 拉取检查确认上游无新增提交、本地 ahead 16;本记录基于当前未提交修复,未纳入用户维护的财务规则工作簿和历史开发日志。
|
||||
@@ -0,0 +1,9 @@
|
||||
## 修复记录
|
||||
|
||||
- 19:19:修复商业计量迁移上线后,标准调整 Savings 回填脚本因精确锁定旧版本而错误拒绝执行的问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交;本地相对 upstream ahead 17 个既有提交,依次为 `242d68c3`、`28b834ed`、`4940ebc4`、`ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`、`5ed34c2b`、`11275e4b`、`1347366b`、`22669a90`、`a616b30c`、`653eda05`、`661990b2`;本次未合并、覆盖或改写共享工作树中的其他变更。
|
||||
- 修改:`backfill_standard_adjustment_savings.py` 不再要求当前 revision 必须精确等于 `20260716_0015`,改为遍历 Alembic `down_revision` 祖先链,只有当前迁移确实包含 Savings 数据契约时才允许回填。
|
||||
- 修改:`test_standard_adjustment_savings_backfill.py` 覆盖所需版本本身、后继商业迁移 `0016`、更旧版本和未迁移数据库四类边界。
|
||||
- 操作:全部检查均在 `local-x-financial-linux` 容器内执行,未修改财务规则 XLSX 或历史开发文档。
|
||||
- 验证:Ruff 通过;标准调整 Savings 回填定向回归 `12 passed`。
|
||||
- 影响:数据库升级到 `0016` 及未来合法后继版本后仍可安全执行历史节省回填;更旧、未迁移或不包含目标契约的版本仍会 fail-closed。
|
||||
@@ -0,0 +1,17 @@
|
||||
## 修复记录
|
||||
|
||||
- 19:24:修复历史费用偏离分析可能读取分析截止时间之后才冻结的基线、形成时间穿越的问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交;本地相对 upstream ahead 17 个既有提交,依次为 `242d68c3`、`28b834ed`、`4940ebc4`、`ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`、`5ed34c2b`、`11275e4b`、`1347366b`、`22669a90`、`a616b30c`、`653eda05`、`661990b2`;本次未合并、覆盖或改写共享工作树中的其他变更。
|
||||
- 修改:`savings_insight_analysis.py` 在历史基线查询中增加 `frozen_at <= as_of`,确保候选分析只能使用截止时间当时已经存在的冻结快照。
|
||||
- 修改:`test_savings_baseline_insights.py` 新增未来冻结基线回归;确认这类快照不会产生历史价格偏离候选,并返回基线不可用的数据质量提示。
|
||||
- 操作:全部检查均在 `local-x-financial-linux` 容器内执行,未修改财务规则 XLSX 或历史开发文档。
|
||||
- 验证:Savings 基线/洞察、端点与 CFO 组合回归 `10 passed`;相关 Ruff 检查通过。
|
||||
- 影响:`as_of` 历史回放不再引用未来才生成的知识,CFO 候选洞察和审计结果保持时间一致性。
|
||||
|
||||
- 22:50:继续修复预算预测读取分析窗口或 `as_of` 之后预算配置与核销流水的问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交;本地仍相对 upstream ahead 17 个既有提交(`242d68c3` 至 `661990b2`,内容为审批任务/风险处置、AI 学习与预审、Expense Case、迁移安全和认证等共享能力),未发现新的上游提交,也未合并或改写共享工作树。
|
||||
- 修改:`savings_insight_budget.py` 以 `min(as_of, window_end)` 作为预算分析截止点,只读取当时已经创建且最后更新的预算配置,以及预算期间开始至截止点的核销/回滚事实;部门范围优先使用稳定部门 ID,缺 ID 才使用部门名称或成本中心。
|
||||
- 修改:`test_savings_baseline_insights.py` 增加窗口后核销、截止时间后配置、稳定重放与零机会副作用回归。
|
||||
- 操作:所有 pytest 和 Ruff 均在 `local-x-financial-linux` 容器内以 60 秒超时执行;未接触财务规则 XLSX、商业/连接器/发布迁移或迁移 HEAD。
|
||||
- 验证:Savings/CFO 定向组合 `34 passed, 6 skipped`(6 项为未配置 PostgreSQL 专用测试 URL 的预期跳过),相关 Ruff 检查通过。
|
||||
- 影响:历史预算预测不再使用报告窗口之后才出现的配置或交易,异常归因、政策模拟候选与 CFO 审计回放保持同一时间边界。
|
||||
@@ -0,0 +1,9 @@
|
||||
## 修复记录
|
||||
|
||||
- 18:11:记录 bug 修复:接受住宿职级标准调整时不再信任客户端金额、天数或金额快照。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 未发现 upstream 新提交;本地相对 upstream ahead 17 个既有提交,依次为 `242d68c3 feat(approval): add task workflow and waiver decisions`、`28b834ed fix(approval): replay immutable action responses`、`4940ebc4 feat(approval): add safe risk disposition workflow`、`ee88a36b feat(ai): add tenant-safe hierarchical expense learning`、`6bdf65bc feat(expenses): add authoritative pre-review workflow`、`ae3f02c3 feat(expense): add persistent zero-entry receipt association`、`54754b55 feat(ai): add personal expense application memory`、`211f85d9 feat(ai): unify verified expense application workflow`、`5b246307 feat(ai): issue verified application preview decisions`、`a662cfe6 feat(ai): add expense application feedback ledger`、`5ed34c2b feat(expenses): backfill historical claims into expense cases`、`11275e4b fix(migrations): enforce schema ownership safety`、`1347366b feat(expenses): secure timeline and draft events`、`22669a90 feat(expenses): show unified expense event timeline`、`a616b30c fix(expenses): unify AI application submission transaction`、`653eda05 feat(auth): add opaque bearer sessions`、`661990b2 feat(expenses): add transactional expense case events`;这些提交均早于本次未提交修复,本次没有拉取、合并或覆盖共享工作树。
|
||||
- 修改:`expense_claim_standard_adjustment.py` 只从已锁定的 `ExpenseClaimItem.item_amount` 读取原始金额,只接受服务端差旅规则计算出的最终可报金额;客户端携带的 `application_days`、`original_amount`、`reimbursable_amount` 仅作为旧界面兼容展示字段,不参与计算。服务端快照新增规则名、规则版本(无发布版本时使用内容指纹)、地点、匹配城市、职级、职级档、天数、每日住宿标准、住宿标准总额及计算指纹。
|
||||
- 修改:为标准调整增加 PostgreSQL advisory lock + Claim/Item 行锁和非 PostgreSQL 进程内串行锁;请求支持 `request_id` 幂等键与 `expected_updated_at` 乐观前置条件。相同请求直接重放且不改 `created_at`/计算快照,同请求号改选其他明细会被拒绝;单次调整只替换被选明细的快照,不再误删其他明细已接受的标准调整。
|
||||
- 操作:在 `local-x-financial-linux` 容器及 `/tmp/x-financial-server-venv` 中运行定向、接口全量和服务全量测试;运行 scoped Ruff 与 `git diff --check`,未在宿主机运行 Python/pytest,也未修改规则表或其他用户文件。
|
||||
- 验证:标准调整定向回归(含非 PostgreSQL 同租户同单据锁竞争)`9 passed`;`test_reimbursement_endpoints.py` 全量 `22 passed`;Scoped Ruff 与 `git diff --check` 通过。`test_expense_claim_service.py` 全量为 `112 passed, 8 failed`,8 个失败均位于既有审批任务配置/旧错误文案断言(直属领导任务、费用申请提交、本人审批、重复退回),不经过标准调整实现;本次新增及关联标准调整用例全部通过。
|
||||
- 影响:伪造低原金额、任意可报金额或超长住宿天数不能降低或抬高实际报销额;规则缺失时整次操作失败关闭且不改金额。审批人看到的原额、可报额和差额均可追溯到数据库明细与服务端规则证据,重复点击和并发请求不会重写金额证据。
|
||||
@@ -0,0 +1,9 @@
|
||||
## 修复记录
|
||||
|
||||
- 2026-07-16 14:29:13 CST:历史风险观测、few-shot 样本、Qdrant 向量和规则生成链路原先缺少完整租户、场景及制度版本边界;Hermes 全局扫描还可能把不同租户的 Claim 放进同一风险图,历史检索结果若直接公开也可能泄露样本 ID、费用单号或人工评语。
|
||||
- 2026-07-16 14:29:13 CST:风险观测与 few-shot 关系数据改为租户复合唯一,风险接口、反馈、查询和 Hermes 图构建按认证租户隔离;Qdrant 写入与检索显式携带 tenant、scene、policy_ref、rule_version,使用稳定向量 ID并清理旧向量,命中后再由关系库校验租户、状态和版本。风险规则生成与再生成从认证用户透传租户,缺失租户时禁用历史注入而不回落到 default。
|
||||
- 2026-07-16 14:29:13 CST:报销预审新增只读 `historical_case_evidence`,检索或向量服务异常时降级为空证据。公开协议和 AI 助手只返回固定的“历史已确认/历史误报,仅供复核”标签与脱敏摘要,不返回 sample ID、claim_no、人工评论或历史结论原文;历史证据不参与 review ID、确定性 findings、passed、blocking count、预算复核或审批路由计算。
|
||||
- 2026-07-16 14:29:13 CST:加固 `20260716_0008` 迁移:`RiskObservation.claim_id` 明确改为旧 Claim 表的 view-only 软引用,使空库与旧表采用路径的 Head 结构一致;降级若发现非默认租户或非空 policy/rule 版本数据则在 DDL 前 fail-fast,禁止静默丢失隔离和版本信息;非 PostgreSQL 在变更前明确拒绝。
|
||||
- 2026-07-16 14:29:13 CST:执行 `git fetch --all --prune`、工作区状态和上下游提交差异检查;`origin/main` 没有新增提交,当前分支 ahead 13,最近本地检查点为权威提交前预审、持久化零录入票据和个人申请记忆,本次未改写这些提交。
|
||||
- 2026-07-16 14:29:13 CST:全部验证在 `local-x-financial-linux` 内执行并设置 60 秒超时。历史案例、风险观测、风险图、预审和规则生成组合 90 项,报销接口与费用服务 137 项,补充 Golden/规则历史注入 28 项通过;迁移/模型/租户组合 70 项通过,Ruff F/I、compileall 和 `git diff --check` 通过。一次性 tmpfs PostgreSQL 17 完整迁移循环 9 项通过,临时容器自动清理,开发数据库未修改。
|
||||
- 影响:同名用户、相同样本键或相似向量不能跨租户读取、覆盖或进入 Prompt;历史案例开始帮助预审与复核,但只以脱敏、可降级、非决策证据存在,不会扩大 AI 自动放行或审批权限。
|
||||
@@ -0,0 +1,9 @@
|
||||
## 修复记录
|
||||
|
||||
- 19:37:修复 Workbench AI 超大运行时导致职责耦合,以及前端回归测试仍绑定旧单体文件和宿主机缺失 Pillow 的问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;`HEAD..origin/main` 无新提交;本地相对 upstream ahead 17 个既有提交,依次为 `242d68c3`、`28b834ed`、`4940ebc4`、`ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`、`5ed34c2b`、`11275e4b`、`1347366b`、`22669a90`、`a616b30c`、`653eda05`、`661990b2`;本次未合并、覆盖或改写共享工作树中的其他变更。
|
||||
- 修改:将会话滚动、流式响应、持久化/重置提取到 `useWorkbenchAiConversationRuntime.js`,把模型意图规划、低置信确认和多任务衔接提取到 `useWorkbenchAiIntentExecution.js`;`usePersonalWorkbenchAiMode.js` 降至 768 行。
|
||||
- 修改:Workbench、会话删除、报销关联与快速申请预览测试改为联合审计入口与职责模块;视觉验证复用容器已有 ImageMagick,保留像素和动画断言,不再依赖未安装的 Pillow。
|
||||
- 操作:全部 node 测试和构建均在 `local-x-financial-linux` 容器内执行,未修改后端或财务规则 XLSX。
|
||||
- 验证:Workbench AI/会话组合 `85 passed`,快速申请预览 `67 passed`;Vite 生产构建成功(2229 modules);`git diff --check` 通过。
|
||||
- 影响:会话清理、详情智能录入、关联门禁和申请预览的回归测试不再因内部职责迁移误报,核心运行时恢复到项目 800 行硬上限内。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 零录入票据归集跨租户隔离与失败回滚
|
||||
|
||||
日期:2026-07-16
|
||||
文档路径:document/development/2026-07-16/dev-logs/bugs/zero-entry-receipt-tenant-and-rollback.md
|
||||
|
||||
## 修复记录
|
||||
- 09:35:记录 bug 修复:零录入票据归集跨租户隔离与失败回滚。(bug-log:54754b55)
|
||||
- Git 提交检查:2026-07-16 09:35 CST 执行 `git fetch --all --prune` 成功;`origin/main` 没有远端新提交;本地 ahead 11 条,依次是 `54754b55` 个人费用申请记忆、`211f85d9` 统一核验申请工作流、`5b246307` 申请预览决策、`a662cfe6` 申请反馈账本、`5ed34c2b` 历史费用单回填、`11275e4b` 迁移所有权安全、`1347366b` 费用时间线与草稿事件安全、`22669a90` 统一费用事件时间线、`a616b30c` AI 申请提交事务、`653eda05` Bearer 会话、`661990b2` 事务化费用事件;这些提交均为本轮开始前已有的本地检查点,本次未合并或改写历史。
|
||||
- 修改:`receipt_folder.py` 把票据命名空间从仅用户名收口为租户与用户联合边界,并用稳定摘要消除有损清洗碰撞;`attachment_association_jobs.py` 同时校验任务租户和 owner,平台管理员也不能跨租户读取。`expense_receipt_matcher.py` 改用纯只读本人 Claim 查询并要求每份票据独立满足证据门槛。`expense_receipt_association.py` 将 Claim、Case、Link、业务事件、票据元数据和新旧附件目录纳入统一失败补偿,避免事件或文件中途异常留下半关联状态。
|
||||
- 操作:新增持久化 `attachment_association_jobs` 模型、仓储和 `20260716_0006` 迁移;任务按租户、owner、票据集合和 generation 去重,运行态使用租约、`attempt_count + running` 栅栏、进程锁和 PostgreSQL advisory lock。同票据与同 Claim 分别串行化,Claim 锁内清理旧事务并重新匹配;待确认或失败任务保留原代审计历史,以新 generation 重新评估,自动关联成功代继续幂等复用。低置信、相近候选、混入无关票据、无草稿和仅有申请统一返回需要确认的零写入结果;同步扩展前端任务协议、会话恢复、候选卡片、幂等重放和成功风险复核提示,并更新功能 CONCEPT/TODO。
|
||||
- 验证:所有后端验证均在项目主容器内执行并设置 60 秒超时。后端归集专项 20 项、归集与相邻服务及迁移所有权组合回归 71 项通过,覆盖纯只读确认、逐票据证据、事件失败、文件元数据失败、既有申请 Case 保留、同票据并发、不同票据并发同 Claim、租约过期恢复、旧 worker/迟到回调栅栏、待确认重评估、失败代际历史保留、普通用户及管理员跨租户隔离;前端关联链路组合回归 29 项通过;相关 Python 文件 `ruff --select F,I,UP` 通过;Vite 生产构建通过。一次性 tmpfs PostgreSQL 17 中完整迁移循环 4 项通过,临时数据库已清理,持久开发库未修改。
|
||||
- 影响:同用户名在不同租户的票据、任务和候选 Claim 不再互相可见;自动归集中途失败不会留下已改 Claim、孤立 Case/Link/Event、错误票据状态或被覆盖的旧附件目录;低证据和混合票据批次只提示确认,不扩大自动化权限;任务重启或多 worker 后仍可查询和恢复。非默认租户原先落在旧用户名目录中的历史票据改为 fail-closed,不做未经确认的跨目录迁移。
|
||||
@@ -0,0 +1,333 @@
|
||||
# AI 分阶段发布真实遥测 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
把真实 shadow/Canary 规则执行、正负样本盲审真值和服务端保守聚合串成可审计证据链,只有精确率、召回率下界与运行质量同时满足门禁时才允许晋级,越界时自动回滚到稳定版本。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
风险规则已经具备 Golden Case、`shadow → canary → active → rolled_back` 状态机、稳定流量路由和自动回滚能力,但线上评测仍有一个关键证据缺口:`ReleaseEvaluationInput` 由外部调用方直接提交 `total`、`failure_count`、`precision` 和 `baseline_precision` 汇总数字,Release Guard 无法证明这些数字来自哪一次真实规则执行、哪一个租户、哪一个 release,也无法证明精度分子和分母来自可信人工结论。
|
||||
|
||||
现有运行与学习链路的事实边界如下:
|
||||
|
||||
- `ExpenseClaimRiskRuleLoader` 能按租户和稳定路由键选择 stable、shadow 或 Canary 候选版本,并在快照损坏时保守阻断。
|
||||
- `evaluate_platform_risk_rules()` 会返回 shadow 候选的 `asset_id / rule_code / rule_version / release_stage / hit / severity`;Canary 命中会进入带版本和阶段的风险 flag,但当前返回值不会持久化成发布样本。
|
||||
- `RiskDispositionEvent` 是类型化、租户化、只追加的人工处置事实,`confirm` 和 `false_positive` 可作为正例命中的可信真值来源。
|
||||
- `RiskObservationFeedback` 的自由评论、`AIDecisionFeedback` 和 `WorkflowOutcome` 可以支持业务学习,但它们没有同时绑定 `asset_id + release_id + stage + version`,不能直接作为某次发布的精度标签。
|
||||
- Release Monitor 的 HMAC 能认证请求来自持有共享密钥的调用方,并限制传输重放;它不能证明请求体中的汇总数字由真实数据库 observation 和人工 label 计算得出。
|
||||
|
||||
因此,线上门禁必须从“相信外部汇总数字”改为“服务端从只追加事实计算指标”。本能力是 `ai-data-flywheel` 在线质量闭环和 `ai-expense-closed-loop-and-value-proof` 分阶段发布目标的证据层,不替代离线 Golden Case。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- [G1] 为每次真实候选规则执行记录租户、资产、release、阶段、版本、规则、命中、基线命中和结构化运行状态。
|
||||
- [G2] 将专用发布复核或数据库中可信 `RiskDispositionEvent` 转换成只追加的正例判断;将独立盲审转换成与模型预测语义分离的 `risk_present / risk_absent` 真值。
|
||||
- [G3] 使用稳定幂等键处理同一执行或人工动作的安全重放,不同内容复用同一来源时拒绝冲突。
|
||||
- [G4] 从真实 observation 和最新可信 label 聚合运行总量、运行失败、候选 precision 和基线 precision。
|
||||
- [G5] 未标注候选命中保持 `collecting`,不得把“没有人工结论”当成成功或正确。
|
||||
- [G6] 聚合结果可转换成现有 `ReleaseEvaluationInput`,供 shadow/Canary 门禁、自动晋级和自动回滚使用。
|
||||
- [G7] 全链路租户隔离、数据脱敏、append-only,并拒绝跨租户和陈旧 release/stage/version 标签。
|
||||
- [G8] 对候选未命中人群实施分层盲审:基线命中分歧样本全量复核,其余负样本按不可预测稳定分数随机抽检;证据不足时 recall/FN 仍显式不可用。
|
||||
- [G9] 使用抽样漏检率估计总体 FN,并以 Wilson 上界反推保守召回率下界;发布门禁只使用下界,不把点估计冒充确定事实。
|
||||
|
||||
### 非目标
|
||||
|
||||
- [NG1] 不用线上遥测替代离线 Golden Case;前者验证真实分布,后者验证覆盖明确预期的回归集合。
|
||||
- [NG2] 不把候选未命中直接判为 false negative,也不从“后续未退回”反推规则正确。
|
||||
- [NG3] 不保存报销事由、票据内容、人工评论、单号、操作者账号原值或原始风险 payload。
|
||||
- [NG4] 不接受客户端、浏览器或普通管理员提交的 precision/recall 作为发布真值。
|
||||
- [NG5] 不因为 HMAC 校验通过就信任汇总数字;HMAC 只解决传输来源和重放,不解决数据生成真实性。
|
||||
- [NG6] 不让通过质量门禁的规则绕过审批、风险处置或资金动作的人类控制。
|
||||
- [NG7] 不把遥测或自动回滚做成绕开共享风险循环、迁移所有权、审批控制或稳定版本保护的旁路。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
### 用户
|
||||
|
||||
1. 风险运营/规则管理员:查看某个 release 的真实样本数、待标注数、误报率和基线比较,决定是否继续采集或人工回滚。
|
||||
2. 财务审计/审批人:在既有风险处置或专用发布复核队列中给出类型化确认/误报结论,不接触发布汇总公式。
|
||||
3. Release Monitor:只从数据库聚合当前 release,满足证据条件后把服务端计算结果交给 Release Guard。
|
||||
4. 平台审计员:按租户、release、版本和来源指纹回放 observation、label、评测与状态转换,不读取业务正文。
|
||||
|
||||
### 核心场景
|
||||
|
||||
1. shadow 阶段同时执行 stable 和候选规则;记录候选 hit/miss,并记录同一单据上 stable 是否命中。
|
||||
2. 候选或 stable 命中进入人工复核;`confirm` 表示风险事实成立,`false_positive` 表示该次正向命中是误报。
|
||||
3. 每条成功 observation 同步决定盲审层:候选命中全量入队、候选未命中但基线命中全量入队、双方均未命中按发布时冻结比例随机入队。
|
||||
4. 复核队列混排正负样本,只提供业务单据、规则和业务阶段,不返回 candidate/baseline hit;复核人只回答“存在真实风险/确认无该风险”。
|
||||
5. 所有候选未命中样本必须由两个不同复核人给出一致结论;同一人的重复动作不增加法定票数,冲突时继续 collecting。
|
||||
6. 候选命中仍有任何未标注项时,聚合状态保持 `collecting`,Release Monitor 不提交通过评测。
|
||||
7. 候选正例全部标注后,服务端计算 candidate precision;基线正例也全部标注时才计算 baseline precision。
|
||||
8. 负样本达到最小独立复核量后,服务端分别披露实际观察 FN、总体估计 FN、FN 置信上界、recall 点估计和 recall 置信下界。
|
||||
9. Canary 路由中的候选 hit、miss 和结构化运行失败继续追加;错误率、precision 或 recall 下界越界时 Release Guard 自动回滚稳定快照。
|
||||
10. release 已晋级、回滚或被新 release 替代后,旧 observation/sample/label 仍保留,但不能再接收新标签或作为当前晋级输入。
|
||||
|
||||
## 功能能力
|
||||
|
||||
- [C1] 运行样本生产:消费现有 `shadow_evaluations`、Canary/active flag,并提供 manifest 执行循环级 hook 记录 Canary 未命中和结构化失败。
|
||||
- [C2] 可信标签:支持认证发布复核动作和数据库中真实 `RiskDispositionEvent`;不接受评论文本作为标签。
|
||||
- [C3] 保守聚合:分别计算 observation、completed、runtime failure、candidate hit/labeled/pending、baseline hit/labeled/pending。
|
||||
- [C4] 门禁转换:仅 `ready` 聚合可生成 `ReleaseEvaluationInput`;`collecting` 调用转换时明确拒绝。
|
||||
- [C5] 证据隔离:运行来源、标签来源、操作者均只保存租户内、带密钥版本的
|
||||
HMAC-SHA-256 指纹;原始 claim、事件和账号值不进入遥测表。
|
||||
- [C6] 幂等与冲突:同一租户、release、阶段、版本、规则和来源形成稳定键;相同重放复用首次记录,不同载荷冲突。
|
||||
- [C7] 历史不可变:observation 与 label 均只追加;标签纠正追加新 label,聚合取最新可信标签,不原地覆盖旧事实。
|
||||
- [C8] 盲审抽样:候选正例和候选/基线分歧样本全量入队,其余负样本按发布策略的 `negative_sample_percent` 与 HMAC 来源伪名生成稳定随机分数。
|
||||
- [C9] 真值盲化:样本表不保存明文单据 ID,业务来源加密保存;API 不返回 candidate/baseline hit,前端也拒绝接收预测字段。
|
||||
- [C10] 保守召回:证据未满足时 `false_negative_count / estimated_false_negative_count / recall / recall_lower_bound` 保持 `null`;满足后分别披露,不用点估计替代下界。
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 证据链与自动判定
|
||||
|
||||
```text
|
||||
[真实 stable / candidate 执行]
|
||||
│
|
||||
▼
|
||||
[append-only Observation]
|
||||
tenant + asset + release + stage + version + hit + runtime status
|
||||
│
|
||||
▼
|
||||
[append-only Audit Sample]
|
||||
正例全量 + 分歧全量 + 其余负例稳定随机抽样
|
||||
│
|
||||
▼
|
||||
[盲化可信 Label]
|
||||
typed disposition / release review / blind release review
|
||||
│
|
||||
▼
|
||||
[服务端 Aggregate]
|
||||
runtime + precision + baseline + sampled FN + recall lower bound
|
||||
│
|
||||
┌───────┴────────┐
|
||||
│ collecting │ ready
|
||||
▼ ▼
|
||||
[继续采集/告警] [ReleaseEvaluationInput]
|
||||
│
|
||||
▼
|
||||
[Release Guard 判定]
|
||||
shadow → canary → active
|
||||
或自动 rolled_back
|
||||
```
|
||||
|
||||
该链路中的每一层只消费上一层可验证的结构化事实。离线 Golden Case 仍是进入 shadow 前的回归门禁;线上 telemetry 是进入 Canary、active 及运行中回滚的分布证据,两者不能互相替代。
|
||||
|
||||
### 前端
|
||||
|
||||
- 发布控制台分别展示运行样本、候选待标注、候选/基线 precision、运行失败、负样本池/抽样/积压、实际 FN、估计 FN、FN 上界、recall 点估计和置信下界。
|
||||
- 盲审队列只接收服务端安全字段;`candidate_hit / baseline_hit` 即使误入响应也不会进入页面状态。
|
||||
- 复核按钮使用中性业务语义“存在真实风险 / 确认无该风险”,不使用“模型命中/误报”暗示预测;来源单据在新标签页打开并隔离 opener。
|
||||
- `null` 指标统一显示“证据不足/暂不可用”,不得渲染成 0;`collecting`、`ready`、`failed/rolled_back` 使用不同状态。
|
||||
- `collecting`、`ready`、`failed/rolled_back` 使用不同状态,不把空样本或缺标签显示为 100%。
|
||||
|
||||
### 后端
|
||||
|
||||
- `AgentAssetReleaseTelemetryService.record_expense_risk_result()` 可从当前风险评测返回值生产 shadow 样本和 Canary/active 命中样本。
|
||||
- `record_manifest_evaluation()` 设计为风险 manifest 执行循环 hook,可记录 Candidate 未命中及 `evaluator_error / artifact_integrity_error / unsupported_evaluator / timeout` 等结构化失败。
|
||||
- `record_review_label()` 只接收类型化 label、认证 actor ID 和 request ID;actor 与 request 进入表前被指纹化。
|
||||
- `AgentAssetReleaseSamplingService` 在 observation 同一事务内完成分层选择与来源加密;加密失败会连同 observation 一起回滚,避免留下无法复核的孤立事实。
|
||||
- `AgentAssetReleaseReviewService` 只查询当前租户、当前 release 的样本,混排后输出去预测队列;发布发起人不可自审,负样本强制两个不同 actor。
|
||||
- `agent_asset_release_aggregation` 将精度和召回证据拆开聚合;`agent_asset_release_recall` 使用随机层漏检率与 Wilson 上界生成总体 FN 估计和 recall 下界。
|
||||
- `record_risk_disposition_label()` 会重新查询数据库中的同租户 `RiskDispositionEvent` 和 `RiskObservation`,校验 action、claim/rule 来源指纹和版本,不信任调用方提供的人工结论副本。
|
||||
- `aggregate()` 只读取同租户、同资产、同 release、同阶段、同版本事实,并验证它仍是资产当前 release。
|
||||
- `to_release_evaluation_input()` 只允许 `ready` 聚合转换;未标注、无候选正例或空样本会抛出 collecting 错误。
|
||||
- `AgentAssetReleaseMonitor` 与周期调度器只在服务端查询事实并调用上述方法;HTTP 入口只接受签名触发,不再接收外部汇总数字。
|
||||
- Agent 资产基础 CRUD/表格/版本接口与风险规则生成、测试、启停和发布子路由分离;
|
||||
资产版本只读投影由独立序列化 mixin 承担,Release Guard 只编排状态与持久化,
|
||||
阈值归一化和质量门禁计算下沉为无数据库副作用的纯策略模块。
|
||||
|
||||
### 算法/规则
|
||||
|
||||
- shadow 同时保留候选与 stable 的命中信息,用同一人工真值分别估计 candidate precision 和 baseline precision。
|
||||
- Canary 使用稳定路由键分流;必须在 manifest 执行循环记录候选 miss,否则只从最终 flag 采集会产生“只有命中样本”的选择偏差。
|
||||
- candidate 正向命中使用 `confirmed / false_positive` 计算 precision;盲审统一使用 `risk_present / risk_absent` 描述业务真值,再在聚合层规范化,不向复核人暴露预测结论。
|
||||
- candidate miss 只有进入服务端抽样表并完成独立双人盲审后才可形成 FN/TN 证据;未抽中的个体不能直接被标签,也不能由“后续无退回”反推正确。
|
||||
- 运行失败与业务误报分开:`failure_count` 表示 evaluator/快照/超时等结构化运行失败,`false_positive_count` 只进入 precision。
|
||||
- 阶段最小样本数、最大错误率、最低 precision 和最大 precision drop 仍由 `ReleaseGuardPolicy` 统一判定。
|
||||
- `ReleaseGuardPolicy.reviewer_quorum` 以 `1..2` 的受控整数随 release 策略固化,
|
||||
telemetry 只统计不同 actor 指纹的最新票;票数不足或不同复核人结论冲突时保持
|
||||
`collecting`,不会把部分意见交给 Release Guard。
|
||||
- 新发布默认开启 recall 门禁,并冻结 `negative_sample_percent / negative_min_reviewed / min_recall / recall_confidence_level`;旧 release 未携带开关时保持兼容,不追溯伪造历史抽样事实。
|
||||
- recall 门禁只比较 `recall_lower_bound` 与 `min_recall`;点估计再高,只要保守下界不足也不能晋级。明显低 precision 可直接失败,不必等待召回样本凑齐。
|
||||
|
||||
### 数据
|
||||
|
||||
#### `agent_asset_release_observations`
|
||||
|
||||
- 身份:`tenant_id / asset_id / release_id / stage / version / rule_code`。
|
||||
- 运行事实:`candidate_hit / baseline_hit / runtime_status / failure_code / business_stage`。
|
||||
- 脱敏来源:`source_kind / source_fingerprint`;不保存 claim ID、单号或业务正文。
|
||||
- 一致性:租户幂等键唯一,保存 payload fingerprint;同一来源不同内容冲突。
|
||||
|
||||
#### `agent_asset_release_labels`
|
||||
|
||||
- 身份复制:tenant、observation、asset、release、stage、version,并通过复合外键绑定原 observation。
|
||||
- 标签:正例判断使用 `confirmed / false_positive`,盲审真值使用 `risk_present / risk_absent`。
|
||||
- 来源:`typed_risk_disposition / release_review / blind_release_review`;数据库组合约束禁止标签语义与来源交叉使用。
|
||||
- 历史:只追加;纠正写新行,不修改或删除旧标签。
|
||||
|
||||
#### `agent_asset_release_audit_samples`
|
||||
|
||||
- 身份复制:tenant、observation、asset、release、stage、version,通过复合外键绑定原 observation。
|
||||
- 分层:`candidate_positive_census / candidate_disagreement_census / candidate_negative_random`。
|
||||
- 抽样事实:保存入样概率和稳定选择分数;同租户 observation 最多一条样本,重放必须匹配 payload fingerprint。
|
||||
- 来源保护:原始单据引用使用 SecretBox 加密,表中不保存明文;只有通过租户与复核角色检查的队列读取才解密。
|
||||
|
||||
三类模型同时具备 ORM 层 UPDATE/DELETE 拒绝。`0018` 建立 observation/label,后继 `0023` 建立 audit sample、扩展标签约束并复用 PostgreSQL append-only 触发器;存在盲审事实或新标签语义时拒绝有损降级。
|
||||
|
||||
### 权限
|
||||
|
||||
- 所有写入、读取和聚合以 `tenant_id` 为第一条件;租户绑定资产不允许其他租户观察或标签。
|
||||
- 平台共享资产可为不同租户分别保存 observation/label,但各租户样本和 precision 不混算。
|
||||
- 标签前重新校验资产当前 `release_id + stage + candidate_version`;旧 release、已晋级阶段或已回滚阶段拒绝新增标签。
|
||||
- 类型化处置标签必须来自数据库中真实存在且同租户的 `RiskDispositionEvent`,action 只允许 `confirm / false_positive`。
|
||||
- 专用复核队列只允许 `manager` 或 `admin`;租户和 actor 全部来自认证上下文,
|
||||
跨租户资产返回 404,非复核角色返回 403,发布发起人自审返回 400。
|
||||
- 标签写入必须携带 `X-Request-Id`;新客户端只发送 `risk_present / risk_absent`,旧客户端的 `confirmed / false_positive` 仅在复核 API 边界映射为盲审真值。标签、
|
||||
actor 指纹和 request 来源只追加保存,客户端不能覆盖 tenant、release 或 actor。
|
||||
- `reviewer_quorum=2` 时必须由两个不同复核人给出相同结论;同一人的重复提交不增加票数,
|
||||
冲突结论进入待仲裁状态并继续阻止晋级。
|
||||
|
||||
### HMAC 与数据真实性边界
|
||||
|
||||
- HMAC 可以证明传输请求由持有密钥的一方生成、请求在允许时间窗口内且签名未被修改。
|
||||
- HMAC 不能证明调用方提交的 `total=100`、`precision=0.99` 真的来自 100 条数据库 observation,也不能证明人工标签存在。
|
||||
- 因此 HMAC 只保留为自动 Monitor 的传输认证和防重放手段;指标必须由接收端使用当前数据库 observation/label 重新计算。
|
||||
- 最终 Monitor 请求应只携带受控 release 触发信息或聚合作业游标,而非可被签名后照单采用的质量汇总数字。
|
||||
- 即使 HMAC 认证失败,也不得影响 stable 规则继续保护业务;应停止晋级、记录安全告警并保持 `collecting`。
|
||||
|
||||
### 降级策略
|
||||
|
||||
- 遥测表或写入暂不可用:不阻断已生效 stable 风险规则和报销主流程,但当前 release 不能晋级,状态保持 collecting 并告警。
|
||||
- 人工标签迟到:保留 observation,待标签追加后重新聚合;不使用默认正确值填补。
|
||||
- 运营端同时展示待标注数量和最早积压时长;超过 24 小时生成结构化逾期告警。
|
||||
- 处置事件与 observation 无法安全关联:拒绝标签,不按相似文本、姓名或评论做模糊匹配。
|
||||
- baseline 标签不完整:`baseline_precision = null`;候选指标可继续采集,但不能声称已完成可靠基线比较。
|
||||
- 负样本未抽中、未完成双人复核或未达到最小复核量:recall、估计 FN 和置信下界保持不可用,继续 collecting 并告警;不会用零填充。
|
||||
- 聚合或 Guard 判定越界:按现有冻结快照恢复 stable;回滚不删除候选 observation 和 label。
|
||||
- 周期聚合异常形成 `release_aggregation_failed` 告警并隔离到单资产;运行失败率、
|
||||
precision 下降、baseline 不可用和自动回滚分别使用独立告警码,稳定版本继续服务。
|
||||
|
||||
## 算法与公式
|
||||
|
||||
### 候选精确率
|
||||
|
||||
```text
|
||||
candidate_precision = candidate_confirmed / (
|
||||
candidate_confirmed + candidate_false_positive
|
||||
)
|
||||
```
|
||||
|
||||
- 分母只包含候选 `candidate_hit=true` 且已有可信最新标签的 observation。
|
||||
- 任一候选正向命中仍未标注时,聚合保持 `collecting`,不得将部分 precision 交给 Release Guard 作为通过证据。
|
||||
|
||||
### 基线精确率
|
||||
|
||||
```text
|
||||
baseline_precision = baseline_confirmed / (
|
||||
baseline_confirmed + baseline_false_positive
|
||||
)
|
||||
```
|
||||
|
||||
- 只使用同一 shadow 样本上 `baseline_hit=true` 的可信标签。
|
||||
- 任一基线正向命中待标注时,baseline precision 显式不可用,不用部分样本制造有利比较。
|
||||
|
||||
### 运行错误率
|
||||
|
||||
```text
|
||||
runtime_error_rate = runtime_failure_count / observed_count
|
||||
```
|
||||
|
||||
- `observed_count` 是该 release/stage/version 的真实运行 observation 数。
|
||||
- `runtime_failure_count` 只统计结构化执行失败,不把业务误报混成技术错误。
|
||||
- 误报通过 precision 体现;运行错误通过 `ReleaseGuardPolicy.max_error_rate` 体现。
|
||||
|
||||
### Recall 与 false negative
|
||||
|
||||
```text
|
||||
recall = TP / (TP + FN)
|
||||
```
|
||||
|
||||
线上负样本分为两层,不能把抽检样本数直接当总体 FN:
|
||||
|
||||
```text
|
||||
disagreement_FN = 全量复核(candidate_hit=false, baseline_hit=true)中的真实风险数
|
||||
random_FN_rate = 随机盲审层真实风险数 / 已完成双人复核的随机样本数
|
||||
estimated_FN = disagreement_FN + random_FN_rate * random_negative_population
|
||||
recall_point = TP / (TP + estimated_FN)
|
||||
|
||||
random_FN_rate_upper = WilsonUpper(random_FN, reviewed, confidence)
|
||||
FN_upper = disagreement_FN + random_FN_rate_upper * random_negative_population
|
||||
recall_lower_bound = TP / (TP + FN_upper)
|
||||
```
|
||||
|
||||
- `false_negative_count` 仅表示已完成法定复核样本中实际观察到的 FN,不等于总体 FN。
|
||||
- `estimated_false_negative_count` 是总体点估计,`false_negative_upper_bound` 是保守上界,二者必须分字段展示。
|
||||
- 随机层存在但尚无已复核样本、仍有抽中样本待审或未达到最小复核量时,上述估计统一保持 `null`。
|
||||
- 没有随机负例人群时可使用全量复核的精确 recall;否则发布门禁只消费 `recall_lower_bound`。
|
||||
- 离线 Golden Case recall 只证明测试集表现,不能冒充线上 recall;线上抽样也不能替代 Golden Case 的边界覆盖。
|
||||
|
||||
## 测试方案
|
||||
|
||||
- 模型:租户/release 复合身份、sample/label 到 observation 复合外键、标签来源组合约束和 append-only。
|
||||
- 样本生产:shadow hit/miss、stable baseline hit、Canary hit、manifest 循环 Canary miss 和结构化运行失败。
|
||||
- 标签:专用复核、真实 `RiskDispositionEvent`、盲审语义、负样本双人法定票、来源/actor 脱敏。
|
||||
- 幂等:相同 observation/label 稳定重放;同一来源不同载荷返回冲突。
|
||||
- 租户与时效:跨租户隐藏、陈旧 release/stage/version 拒绝、错误规则/单据来源拒绝。
|
||||
- 聚合:无样本、无候选正例、无标签、部分标签、完整标签、baseline 部分标签、运行失败和 precision drop。
|
||||
- 运营告警:待标注数量/最早时长、24 小时积压、运行失败率、聚合失败、baseline
|
||||
不可用、precision 下降和自动回滚使用去敏结构化告警。
|
||||
- 证据边界:未抽中负样本拒绝标签;预测字段不进入队列/API;抽样不足时 recall/FN 为 null,充分时点估计与下界分离。
|
||||
- 组合回归:Telemetry 生成的 `ReleaseEvaluationInput` 可被现有 Release Guard 消费,且不改变 shadow/Canary/回滚状态机。
|
||||
- PostgreSQL:0018/0023 upgrade/downgrade、复合外键、标签组合约束、数据库 append-only、并发同幂等键单赢家、同 actor 去重和标签/阶段竞争。
|
||||
- 所有验证在 `local-x-financial-linux` 容器内执行,单条命令最大 60 秒。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
- [A1] 每条线上发布样本可追溯到 tenant、asset、release、stage、version、rule 和来源指纹,且不含业务正文。
|
||||
- [A2] 相同运行/标签重放只保留一条事实;不同载荷复用同一来源 100% 拒绝。
|
||||
- [A3] 任一候选正向命中未标注时状态为 collecting,不能生成 Release Guard 通过输入。
|
||||
- [A4] precision 和 baseline precision 只由真实 observation 与可信类型化标签计算,外部汇总值不作为权威事实。
|
||||
- [A5] recall/FN 证据不足时明确 unavailable;充分时同时披露实际 FN、估计 FN、FN 上界、recall 点估计和置信下界,门禁只使用下界。
|
||||
- [A6] 跨租户、陈旧 release/stage/version、错误处置来源和纯负样本伪标签均被拒绝。
|
||||
- [A7] 质量越界时自动回滚 stable,遥测故障或 HMAC 故障时停止晋级但不关闭既有稳定保护。
|
||||
- [A8] PostgreSQL 迁移、append-only、并发、后端组合回归、Ruff 和 `git diff --check` 全部在容器内通过。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
- 模型注册、真实 manifest hook、类型化处置标签、盲审抽样、服务端聚合、即时 Guard、
|
||||
租户周期调度、发布复核队列和运营控制台已经接通;`0023` 后继迁移与完整 PostgreSQL
|
||||
循环仍须完成最终验证后才能关闭本能力。
|
||||
- 租户调度器只自动处理租户绑定资产。平台共享资产可以按租户保存隔离样本,但在建设跨租户、加权且可审计的聚合口径前,不能由单租户样本自动回滚全局版本。
|
||||
- 线上标签可能集中在高风险或有争议样本,precision 仍可能受人工复核选择偏差影响;控制台必须同时披露 hit、labeled 和 pending 数量。
|
||||
- 规则稀有时可能长期没有候选正例;不能为了晋级降低为“零命中等于 100% precision”,需要延长 shadow 或补充经审核 Golden Case。
|
||||
- 标签纠正采用追加新事实和“每个 actor 最新票”聚合;数据库索引、复合外键和只追加
|
||||
约束已在 PostgreSQL 验证,同 observation/label 的并发单赢家、阶段晋级竞争和调度器
|
||||
advisory leader lease 均有 PostgreSQL 并发验证。
|
||||
- HMAC 密钥泄露会让攻击者通过传输认证,但仍不应允许其伪造数据库 observation/label;服务端重算是不可省略的第二道边界。
|
||||
- 线上 recall 已有分层抽样、预测盲化、双人复核、冲突保持 collecting、最小样本量和置信下界;仍需用试点数据校准抽样比例、人工一致率与业务风险容忍度,不能把默认阈值当行业通用真理。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-16:完成现有 Loader、平台风险评测、shadow_evaluations、风险处置和 AI workflow feedback/outcome 的只读审计,确认 `RiskDispositionEvent` 是当前最可信线上人工标签源,通用学习结果缺少 release 身份不能直接用于门禁。
|
||||
- 2026-07-16:新增独立 observation/label 模型与服务,完成脱敏、append-only、稳定幂等、租户/陈旧 release 拒绝、shadow/Canary 样本生产、可信标签和保守聚合。
|
||||
- 2026-07-16:独立切片阶段新增遥测测试 7 项,与当时 Release Guard/Runtime 组合共 19 项通过;该阶段留下的共享注册、迁移和运行 hook 已在后续记录中完成。
|
||||
- 2026-07-16:完成主模型注册、迁移所有权与 `0018`;一次性 PostgreSQL 17 完整迁移循环、复合外键和数据库 append-only 探针通过。
|
||||
- 2026-07-16:真实 shadow/Canary/active manifest 执行已写 observation;类型化风险处置自动追加标签并即时触发 Guard,相同聚合快照幂等复用测试运行,低 precision 或运行失败自动恢复 stable。
|
||||
- 2026-07-16:Release Monitor HTTP 改为空触发 + HMAC,禁止调用方提交 precision/total;新增租户级周期调度作为即时监控失败的补偿链。发布组合回归 44 项、调度/风险定向回归 24 项通过。
|
||||
- 2026-07-16:完成后端大文件职责拆分:`agent_assets.py` endpoint 降至 714 行、
|
||||
`AgentAssetService` 降至 675 行、`AgentAssetReleaseGuardService` 降至 636 行;
|
||||
风险规则子路由、资产序列化和发布纯策略分别独立,旧路由路径、公开类型导入和状态机行为保持兼容。
|
||||
- 2026-07-16:发布纯策略新增 `reviewer_quorum`(默认 1、范围 1..2)并随 release state 保存,
|
||||
专用去敏复核队列按独立 actor 计票,禁止发布人自审,双人同意前或结论冲突时保持 collecting。
|
||||
- 2026-07-16:发布控制台展示真实样本、命中、待审、最早积压时长、运行失败率、
|
||||
candidate/baseline precision 和 recall 不可用;新增积压、超时、运行故障、聚合失败、
|
||||
precision 下降、baseline 不可用和自动回滚结构化告警。
|
||||
- 2026-07-16:新增 append-only 盲审样本、正例/分歧全量与其余负例稳定随机抽样;来源引用加密保存,observation 与 sample 同事务写入,保护失败整体回滚。
|
||||
- 2026-07-16:发布复核队列改为预测盲化混排,负样本强制两个不同复核人;新增实际/估计 FN、Wilson FN 上界、recall 点估计与保守下界,Release Guard 只消费下界。
|
||||
- 2026-07-16:前端拒绝 prediction hit 字段,使用中性业务真值动作,并显示负样本池、抽样进度、积压及置信方法;证据缺失保持不可用,不渲染为零。
|
||||
- 2026-07-16:新增 `0023` 后继迁移和标签来源组合约束;最终 PostgreSQL 全链升级/降级、并发和全量回归结果待验证后回填。
|
||||
@@ -0,0 +1,120 @@
|
||||
# AI 分阶段发布真实遥测 开发 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 每项必须回链 `CONCEPT.md` 对应章节;没有代码、迁移、接口或容器证据不得勾选。
|
||||
- observation、label、聚合和 Release Guard 输入必须按证据层分开,不能用 HMAC 请求或客户端汇总值替代数据库事实。
|
||||
- `collecting` 不得包装成通过;recall/false-negative 没有达到独立盲审证据阈值时必须保持 unavailable。
|
||||
- 所有后端、迁移和并发验证只在 `local-x-financial-linux` 容器内执行,单条命令最长 60 秒。
|
||||
|
||||
## 1. 调研与边界
|
||||
|
||||
- [x] [CONCEPT: 背景与问题] 审计 Loader、平台风险评测、shadow_evaluations、风险处置和 AI workflow feedback/outcome 链路,确认线上发布评测缺少持久真实样本。
|
||||
证据:`expense_claim_risk_rule_loader.py`、`expense_claim_platform_risk.py`、`risk_dispositions.py`、`expense_workflow_learning.py`、`ai_learning.py` 只读审计。
|
||||
- [x] [CONCEPT: 背景与问题] 确认 `RiskDispositionEvent confirm/false_positive` 是当前可绑定风险命中的可信人工结论,通用 feedback/outcome 缺少完整 release 身份。
|
||||
证据:`risk_disposition.py`、`risk_dispositions.py` 与 `agent_asset_release_telemetry.py` 的可信事件查询和关联校验。
|
||||
- [x] [CONCEPT: HMAC 与数据真实性边界] 冻结 HMAC 只负责传输认证、防篡改和防重放,不证明汇总指标的数据真实性。
|
||||
证据:`CONCEPT.md`“HMAC 与数据真实性边界”;现有 `agent_asset_release_monitor_auth.py` 与 `ReleaseEvaluationInput` 契约对照审计。
|
||||
- [x] [CONCEPT: 目标与非目标] 明确未标注 collecting、敏感数据不落遥测表、线上 recall/FN 证据不足不可用。
|
||||
证据:`CONCEPT.md`“目标与非目标”“Recall 与 false negative”。
|
||||
|
||||
## 2. 契约与设计
|
||||
|
||||
- [x] [CONCEPT: 数据] 定义 observation 与 label 的 tenant/asset/release/stage/version 复合身份、稳定幂等键和只追加边界。
|
||||
证据:`server/src/app/models/agent_asset_release_telemetry.py`。
|
||||
- [x] [CONCEPT: 证据链与自动判定] 定义 observation → trusted label → aggregate → ReleaseEvaluationInput → Release Guard 的证据链。
|
||||
证据:`CONCEPT.md`“证据链与自动判定”、`ReleaseTelemetryAggregate.to_release_evaluation_input()`。
|
||||
- [x] [CONCEPT: 算法/规则] 分开定义运行失败、业务误报、candidate precision、baseline precision 和缺失负样本真值。
|
||||
证据:`agent_asset_release_telemetry.py` 的 `aggregate()`;`test_agent_asset_release_telemetry.py` 的 collecting、baseline 和 FN 不可用断言。
|
||||
- [x] [CONCEPT: 权限] 冻结专用发布复核接口的角色矩阵、双人复核阈值和跨租户 HTTP 错误契约。
|
||||
证据:`require_rule_reviewer_user` 只允许 manager/admin;跨租户 404、非角色 403、
|
||||
发布人自审 400;`reviewer_quorum` 限制 1..2 且按不同 actor 指纹计票。
|
||||
|
||||
## 3. 独立模型与服务
|
||||
|
||||
- [x] [CONCEPT: 数据] 新增 append-only observation/label ORM 模型、复合租户/release 外键和更新/删除拒绝。
|
||||
证据:`server/src/app/models/agent_asset_release_telemetry.py`。
|
||||
- [x] [CONCEPT: 后端] 实现真实 shadow 结果、Canary/active 命中和 manifest 执行循环样本生产器。
|
||||
证据:`AgentAssetReleaseTelemetryService.record_expense_risk_result()`、`record_manifest_evaluation()`。
|
||||
- [x] [CONCEPT: 后端] 实现专用发布复核标签和可信 `RiskDispositionEvent` 标签转换,不接收自由评论。
|
||||
证据:`record_review_label()`、`record_risk_disposition_label()`。
|
||||
- [x] [CONCEPT: 后端] 实现租户隔离、陈旧 release/stage/version 拒绝、来源关联校验和稳定幂等冲突。
|
||||
证据:`_require_current_release()`、`_observation_replay()`、`_label_replay()` 及对应测试。
|
||||
- [x] [CONCEPT: 算法与公式] 实现保守聚合;未标注候选命中保持 collecting,基线标签不完整时 baseline precision 不可用。
|
||||
证据:`ReleaseTelemetryAggregate`、`aggregate()`、`to_release_evaluation_input()`。
|
||||
- [x] [CONCEPT: 数据] 实现 claim、事件、actor 来源指纹化,不保存业务正文、评论和账号原值。
|
||||
证据:模型无自由文本业务字段;`_fingerprint()`;脱敏测试断言。
|
||||
|
||||
## 4. 共享注册、迁移与运行接入
|
||||
|
||||
- [x] [CONCEPT: 数据] 在 `db/base.py` 与 `models/__init__.py` 注册 `AgentAssetReleaseObservation` 和 `AgentAssetReleaseLabel`,保证主应用 metadata 与迁移所有权检查可见。
|
||||
证据:`db/base.py`、`models/__init__.py`、`schema_ownership.py`、`migration_preflight.py` 已登记两张迁移自有表;前置检查 77 项通过。
|
||||
- [x] [CONCEPT: 数据] 新增后继 `20260716_0018` Alembic 迁移,创建两张表、复合租户/release 约束、检查约束、索引和数据库级 append-only UPDATE/DELETE 触发器。
|
||||
证据:`20260716_0018_agent_asset_release_telemetry.py`;一次性 PostgreSQL 17 完整升级/降级循环 1 项通过,静态迁移/schema owner 回归 112 项通过。
|
||||
- [x] [CONCEPT: 算法/规则] 在真实候选 manifest 执行循环接入 `record_manifest_evaluation()`,完整记录 shadow/Canary hit、miss 和结构化执行失败。
|
||||
证据:`expense_claim_platform_risk.py`、`expense_claim_release_telemetry.py`;`test_agent_asset_release_runtime.py` 覆盖 shadow、Canary、active 和损坏快照,`test_agent_asset_release_telemetry.py` 覆盖结构化运行失败。
|
||||
- [x] [CONCEPT: 后端] 在类型化风险处置事务接入 release label,按同租户、同 claim/rule、当前 release 精确关联 observation;关联失败保守拒绝。
|
||||
证据:`agent_asset_release_disposition_labels.py`、`risk_disposition_release_sync.py`;风险确认/误报会追加标签,误报越界自动回滚 stable。
|
||||
- [x] [CONCEPT: 后端] 将现有 Release Monitor 从“提交外部汇总数字”改为“触发服务端聚合”,只在 aggregate ready 时构造 `ReleaseEvaluationInput`。
|
||||
证据:`AgentAssetReleaseMonitor.evaluate_current()` 只接受 release 身份;HTTP body 为禁止额外字段的空触发契约,真实聚合未 ready 时不调用 Guard。
|
||||
- [x] [CONCEPT: HMAC 与数据真实性边界] 保留 HMAC 作为 Monitor 传输认证,但禁止签名请求覆盖数据库聚合结果,并补充签名通过但汇总伪造的反向测试。
|
||||
证据:`agent_asset_releases.py`、`AgentAssetReleaseMonitorTriggerWrite`;带有效签名的伪造 `precision/total` 请求仍返回 422。
|
||||
- [x] [CONCEPT: 降级策略] 遥测持久化或聚合失败时保持 stable 规则、停止晋级并记录结构化告警,不让观测故障阻断正常报销。
|
||||
证据:`ExpenseClaimReleaseTelemetryRecorder`、`risk_disposition_release_sync.py` 将遥测/标签/监控故障隔离并记录日志;未获得 ready 真实聚合不会写 passed,也不会改变 stable 路由。
|
||||
- [x] [CONCEPT: 后端] 按职责拆分 Agent 资产接口、版本只读投影和 Release Guard 纯策略计算,保持公开 API 与状态机行为稳定。
|
||||
证据:`agent_assets.py` endpoint 714 行、`agent_asset_risk_rules.py` 534 行、`agent_assets.py` service 675 行、`agent_asset_serialization.py` 217 行、`agent_asset_release_guard.py` 636 行、`agent_asset_release_policy.py` 201 行,相关核心文件均低于 800 行;纯策略模块保存范围为 1..2 的 `reviewer_quorum`,第 5 节已完成复核执行与运营闭环。
|
||||
|
||||
## 5. 自动聚合、告警与运营闭环
|
||||
|
||||
- [x] [CONCEPT: 证据链与自动判定] 实现按 tenant/asset/release/stage/version 的周期聚合作业与幂等快照。
|
||||
证据:`agent_asset_release_scheduler.py` 按租户有界扫描;`AgentAssetReleaseMonitor._existing_evaluation()` 复用相同 release 聚合快照,不重复生成测试运行。
|
||||
- [x] [CONCEPT: 证据链与自动判定] aggregate ready 后自动调用 Release Guard;collecting 只更新采集状态,不写虚假 passed test run。
|
||||
证据:人工标签提交后即时触发 monitor,后台 scheduler 提供失败补偿;`test_agent_asset_release_monitor.py` 覆盖 collecting 不调用 Guard、低 precision/运行失败自动回滚与相同快照幂等。
|
||||
- [x] [CONCEPT: 降级策略] 增加待标注数量/时长、运行失败率、precision 下降、baseline 不可用、聚合失败和自动回滚告警。
|
||||
证据:`agent_asset_release_alerts.py`、`AgentAssetReleaseMonitor._metrics()`;24 小时
|
||||
待审逾期和单资产聚合失败均返回结构化告警,定向 Monitor/Telemetry/Runtime 29 项通过。
|
||||
- [x] [CONCEPT: 前端] 在发布控制台展示 observed/hit/labeled/pending、候选/基线 precision、运行失败和召回证据。
|
||||
证据:`AuditReleaseMonitorPanel.vue` 展示负样本池、抽样进度、积压、实际/估计 FN、FN 上界、recall 点估计/下界/置信方法;空值不渲染为零。
|
||||
- [x] [CONCEPT: 权限] 实现认证发布复核队列、操作审计和需要时的双人复核,不允许规则发布人独自伪造所有标签。
|
||||
证据:`agent_asset_release_review.py`、`agent_asset_release_label_votes.py`、专用 GET/POST
|
||||
API;`X-Request-Id`、append-only label、actor HMAC 指纹、发布人隔离和独立双人同意均有测试。
|
||||
- [x] [CONCEPT: Recall 与 false negative] 实现独立分层负样本抽样、预测盲化、双人标注、冲突保持 collecting 和保守召回估计。
|
||||
证据:`agent_asset_release_sampling.py`、`agent_asset_release_review.py`、
|
||||
`agent_asset_release_aggregation.py`、`agent_asset_release_recall.py`;前端只发送
|
||||
`risk_present / risk_absent`,负样本要求两个不同 actor,门禁只读取 recall 下界。
|
||||
- [x] [CONCEPT: 数据] 完成 `0023` audit sample 迁移注册、数据库标签组合约束、append-only 触发器和无损降级验证。
|
||||
证据:`20260716_0023_agent_asset_release_blind_audit.py`、`release_telemetry_migration_assertions.py`;fresh PostgreSQL 完整迁移链、约束、触发器及降级/再升级均通过。
|
||||
|
||||
## 6. 测试与验证
|
||||
|
||||
- [x] [CONCEPT: 测试方案] 独立模型/服务测试覆盖 shadow、Canary、baseline、真实 typed disposition、脱敏、幂等、租户、陈旧 release、append-only、collecting 和 FN 不可用。
|
||||
证据:容器内 `test_agent_asset_release_telemetry.py` 7 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 与现有 Release Guard 和 Runtime 组合回归通过。
|
||||
证据:容器内 `test_agent_asset_release_guard.py`、`test_agent_asset_release_runtime.py`、`test_agent_asset_release_telemetry.py` 共 19 项通过;Ruff 通过,`git diff --check` 通过。
|
||||
- [x] [CONCEPT: 测试方案] 新增 0018 upgrade/downgrade、schema owner、复合外键和数据库 append-only PostgreSQL 验证。
|
||||
证据:一次性 `pgvector/pgvector:pg17` 数据库中完整迁移循环 1 项通过;迁移运行探针验证复合租户外键、幂等唯一约束和两张表的数据库级 UPDATE/DELETE 拒绝。
|
||||
- [x] [CONCEPT: 测试方案] 新增 PostgreSQL 并发同 observation、同 label、标签与阶段晋级竞争和幂等冲突测试。
|
||||
证据:一次性 PostgreSQL 17 中 `test_agent_asset_release_telemetry_concurrency_postgres.py` 4 项通过;相同重放只保留一条,不同载荷单赢家,阶段转换持锁后旧标签保守拒绝。
|
||||
- [x] [CONCEPT: 测试方案] 验证 audit sample 并发单赢家、同 actor 重复不增加负样本法定票、第二独立 actor 完成双人复核,以及预测字段不进入前端队列状态。
|
||||
证据:`test_agent_asset_release_telemetry_concurrency_postgres.py` 覆盖单赢家和独立双人票;`test_agent_asset_release_telemetry.py` 与 `agent-release-monitor-panel.test.mjs` 验证队列不暴露 candidate/baseline 命中预测。
|
||||
- [x] [CONCEPT: 测试方案] 跑通真实风险循环 → observation → disposition label → aggregate → Release Guard 回滚端到端。
|
||||
证据:`test_agent_asset_release_runtime.py` 与 `test_risk_dispositions.py` 覆盖真实执行样本、类型化处置标签、服务端聚合、低 precision 自动恢复 stable;发布组合回归 44 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 验证职责拆分后的资产服务、风险规则子路由、Release Guard/Runtime、Monitor/Scheduler/Telemetry 和 API schema 兼容性。
|
||||
证据:容器内资产服务 27 项、Guard/Runtime 13 项、Monitor/Scheduler/Telemetry 22 项、风险规则生成/解释 30 项、修订/反馈 14 项通过;迁移后的既有 publish HTTP 防绕过用例通过,Ruff 与 `git diff --check` 通过。
|
||||
- [x] [CONCEPT: 指标与验收] 验证 HMAC 失败、遥测库失败、标签积压和聚合失败均停止晋级但不破坏 stable 业务保护。
|
||||
证据:`test_agent_asset_release_runtime.py` 覆盖签名失败;`test_agent_asset_release_monitor.py` 覆盖 collecting、积压和聚合失败;`ExpenseClaimReleaseTelemetryRecorder` 隔离遥测持久化异常,stable 报销路径继续服务。
|
||||
- [x] [CONCEPT: 指标与验收] 完成相关后端全量回归、Ruff、迁移完整循环和 `git diff --check`,逐项回填 A1-A8 证据。
|
||||
证据:遥测组合 45 项通过;fresh PostgreSQL 总探针 `87 passed / 0 skipped / 0 failed`,其中发布遥测并发 7 项;Web 全量 815 项及 Vite build 通过;新增 Python 文件 Ruff 与 `git diff --check` 通过。全仓既有 Ruff 基线债不伪装为本轮新增错误。
|
||||
|
||||
## 7. 文档收尾
|
||||
|
||||
- [x] [CONCEPT: 本轮实现记录] 新建独立 CONCEPT/TODO,记录证据边界、已实现切片和共享集成缺口。
|
||||
证据:`document/development/2026-07-16/feature/ai-release-real-telemetry/CONCEPT.md`、`TODO.md`。
|
||||
- [x] [CONCEPT: 风险与开放问题] 共享集成完成后回填 0018、运行 hook、自动作业、PostgreSQL 和端到端证据。
|
||||
证据:本 TODO 第 4-6 节与 `CONCEPT.md`“本轮实现记录”已回填;发布面板已展示积压、失败、precision 和 recall,生产运营效果由下一条真实流量验收单独保留。
|
||||
- [x] [CONCEPT: 指标与验收] 与上位 AI 闭环 TODO 对齐代码与容器验证状态。
|
||||
证据:`document/development/2026-07-13/feature/ai-expense-closed-loop-and-value-proof/TODO.md` 已回填 Golden、Canary、盲审和回滚的工程证据。
|
||||
- [ ] [CONCEPT: 指标与验收] 使用生产真实流量、独立复核样本和试点阈值验证 Golden/Canary/自动回滚运营效果。
|
||||
证据要求:目标企业生产 observation/label、盲审样本、阈值签字和真实回滚演练;本地测试不得替代。
|
||||
@@ -0,0 +1,237 @@
|
||||
# 商业计量、客户 ROI 与可持续定价 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
把套餐、订阅、权益、真实用量、内部成本、客户确认价值和平台毛利拆成可审计事实,并只在成本与价值证据同时成立时给出可持续定价走廊。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
- 平台要成为可长期经营的产品,既要证明客户省了钱,也要知道每个租户消耗了多少 OCR、AI、存储、连接器和支持成本。
|
||||
- “风险金额”“预计节省”“流程耗时”不能直接作为客户 ROI;平台收入、平台内部成本和客户价值也不能混在一个指标里。
|
||||
- 只有套餐创建而没有暂停、取消、历史查询和配额硬门禁,商业后台无法真正运营。
|
||||
- 仅在执行前读取 `SUM(usage)` 再放行无法抵抗并发;两个工具可同时看到剩余额度并一起执行,事后计量再准确也已形成不可逆超卖。
|
||||
- 固定拍一个价格无法适配客户规模、真实成本和价值覆盖。需要先计算平台可持续下限,再计算客户价值可接受上限;没有交集时不能强行报价。
|
||||
- 试点期通常缺少 30/90 天真实成本和财务确认收益,因此系统必须显示“采集中”,而不是用 mock 或估计值伪造单位经济性。
|
||||
|
||||
本能力是 `2026-07-13/feature/ai-expense-closed-loop-and-value-proof` 中商业模式与价值证明部分的实施拆分。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- [G1] 建立租户隔离、版本化的套餐、订阅和权益配置。
|
||||
- [G2] 建立追加式用量与内部成本事实,支持幂等、冲回、配额和并发门禁。
|
||||
- [G3] 把商业权益门禁与安全门禁做收紧式合并,付费不能绕过高风险人工审核。
|
||||
- [G4] 严格分开客户收费、内部成本、平台贡献毛利、财务确认现金节省、客户 ROI 和工时价值。
|
||||
- [G5] 支持套餐/订阅/权益生命周期、历史查询、用量成本查询和数据质量状态。
|
||||
- [G6] 根据真实成本和财务确认节省给出基础费下限、价值上限和可封顶成功费,不自动改合同。
|
||||
- [G7] 形成“试点采集 → 基础订阅 → 基础费 + 封顶成功费”的可验证商业演进路径。
|
||||
- [G8] 用不可变账期承载收费、用量、成本与配额历史,并对商业配置和自动续期保留脱敏追加式审计。
|
||||
|
||||
### 非目标
|
||||
|
||||
- [NG1] 不在代码中硬编码某个客户的最终价格、税率、折扣或合同条款。
|
||||
- [NG2] 不把风险暴露、预计机会、未确认结果、未锁定汇率或未经客户认可的工时估值计入价值定价。
|
||||
- [NG3] 不让套餐或配额放宽审批、风险、租户和人工确认门禁。
|
||||
- [NG4] 不自建开票、税务、收款或第三方订阅扣费网络;只保存受控外部订阅引用。
|
||||
- [NG5] 不跨币种直接求和,也不在缺少同币种成本/价值时生成综合 ROI。
|
||||
- [NG6] 不把定价场景结果自动写成生效套餐,最终合同仍需平台商业负责人审批。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
### 用户
|
||||
|
||||
1. 平台商业管理员:维护套餐版本、订阅、权益、状态和定价场景。
|
||||
2. 平台运营/财务:查看用量、成本、毛利和异常数据质量。
|
||||
3. 客户 CFO/财务负责人:查看自己租户的当前套餐、用量、配额和客户价值口径。
|
||||
4. 产品运行时:在执行 OCR、AI 或连接器能力前检查配额,并写入真实用量。
|
||||
|
||||
### 核心场景
|
||||
|
||||
1. 试点客户先配置 `pilot` 套餐和明确的合同周期,开始采集真实用量、成本与已确认价值。
|
||||
2. 运行时检查某项权益;只有商业配额允许且安全决策为 allow 时才最终允许。
|
||||
3. 同一用量事件重试返回首次结果;不同内容复用幂等键返回冲突。
|
||||
4. 客户暂停、逾期、取消或过期时,商业权益即时失败关闭,历史用量和成本不被删除。
|
||||
5. 商业负责人选择 90 天窗口,输入目标贡献毛利率和客户最大价值分享比例,系统按币种输出定价走廊。
|
||||
6. 成本下限高于价值上限时,系统建议先优化单位经济性或扩大可信价值,不生成强行报价。
|
||||
|
||||
## 功能能力
|
||||
|
||||
- [C1] 套餐版本:subscription、usage、hybrid、pilot、custom,支持生效区间和旧版本退役。
|
||||
- [C2] 订阅快照:合同周期、计费周期、席位、基础费、外部订阅引用和状态历史。
|
||||
- [C3] 权益与配额:feature、metered、unlimited,包含量、硬上限、重置周期和超额策略。
|
||||
- [C4] 用量事实:usage、credit、adjustment、reversal,保存主体、来源、correlation 和首次请求指纹。
|
||||
- [C5] 成本事实:AI、OCR、存储、连接器、支持、实施、基础设施、支付等分类及汇率快照。
|
||||
- [C6] 商业分析:收费、成本、贡献毛利、确认节省、客户 ROI 和工时价值分账展示。
|
||||
- [C7] 定价走廊:最低可持续收费、最高价值对齐收费、最大成功费和证据状态。
|
||||
- [C8] 生命周期与查询:暂停、恢复、逾期、取消、过期,以及套餐/订阅/权益/用量/成本历史。
|
||||
- [C9] 账期与审计:月/季/年自动续期、合同边界失败关闭、账期历史和商业管理追加审计。
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 前端
|
||||
|
||||
- 商业工作台分为“当前账户”“套餐与订阅”“权益与配额”“用量与成本”“价值与定价”五块。
|
||||
- 客户 ROI 与平台贡献毛利必须使用不同卡片、不同说明,不允许用一个“综合收益”混合展示。
|
||||
- 多币种按币种分行;缺成本、缺确认价值、仅有试点数据和证据冲突使用不同状态。
|
||||
- 暂停、取消和定价场景均需要确认;终态操作明确提示不能原地恢复。
|
||||
- 普通客户财务只能查看本租户账户;平台级配置、成本、毛利和定价仅平台管理员可见。
|
||||
|
||||
### 后端
|
||||
|
||||
- `CommercialAdminService` 管理套餐、订阅、权益和状态转换。
|
||||
- `CommercialBillingPeriodService` 签发和定位不可变账期;用量、成本和运行时预占必须绑定真实账期编号。
|
||||
- `CommercialSubscriptionRolloverService` 在订阅行锁内按月/季/年边界幂等续期;合同制或跨越 `ends_at` 时失败关闭。
|
||||
- `CommercialRolloverScheduler` 以 PostgreSQL advisory lock 选举单一执行者,再按订阅行锁串行签发到期账期。
|
||||
- `CommercialAdminAuditService` 只记录字段白名单快照,排除合同正文、配置、外部订阅编号、元数据和凭证类字段。
|
||||
- `CommercialQueryService` 负责租户范围内历史与追加事实查询。
|
||||
- `CommercialEntitlementService` 计算配额和商业/安全合并门禁。
|
||||
- `CommercialMeteringService` 写追加式用量和成本、执行幂等与冲回。
|
||||
- `CommercialRuntimeReservationService` 在订阅/权益锁内完成执行前额度预占,管理 reserved、committed、released、expired、reconciliation_required 和 committed_reconciliation_required 状态。
|
||||
- `CommercialRuntimeBridge` 把可信 AgentRun、中央工具执行、真实 AgentToolCall 和商业事实接成预占—执行—结算链;未配置商业计量时保持兼容。
|
||||
- `CommercialRuntimeReconciler` 只根据可验证的工具/运行终态补偿过期预占,不按超时猜测业务是否发生。
|
||||
- `CommercialAnalyticsService` 按窗口和币种分账聚合,不伪造缺失数据。
|
||||
- `CommercialPricingService` 只读分析真实成本和确认节省,输出价格区间,不写套餐。
|
||||
- HTTP 管理入口仅平台管理员可用;租户账户读取仅 finance、executive 或平台管理员可用。
|
||||
|
||||
### 算法/规则
|
||||
|
||||
- 用量硬配额在数据库锁内计算,最终用量超过限制时整个写入失败。
|
||||
- 真实工具执行前按 `已用量 + 有效预占 + 本次预占 <= 硬上限` 原子判断;成功时真实量不得超过预占,失败/阻断只释放预占,不写用量或成本。
|
||||
- call 基准可直接预占一次;token、duration 等变量基准必须由执行器声明并强制最大量。缺少可信 hard max 时拒绝执行,不用正文长度或估算值代替。
|
||||
- 已有相同 tool call 的预占请求按指纹稳定重放;真实 AgentToolCall 的用量/成本继续使用追加式幂等键。用量已写入但成本失败时进入 `committed_reconciliation_required`,重试只补缺失成本,不重复占用额度或追加用量。
|
||||
- 权益已有用量后,配额、定价和有效期不可回改,只允许暂停/恢复;结构变化必须新建订阅版本。
|
||||
- 客户 ROI 只使用财务确认 canonical 现金节省与客户收费。
|
||||
- 贡献毛利只使用平台收费与内部成本,不混入客户节省。
|
||||
- 定价只在相同币种内计算,并保留成本/价值证据状态。
|
||||
|
||||
### 数据
|
||||
|
||||
- `tenant_commercial_plans`:租户、套餐编码、版本、价格模型、基础费、币种、生效期和合同条款摘要。
|
||||
- `tenant_subscriptions`:租户、套餐、周期、基础费快照、状态、席位、外部引用和版本。
|
||||
- `commercial_entitlements`:租户、订阅、权益键、计量键、配额、状态和有效期。
|
||||
- `usage_meter_events`:追加式用量、冲回引用、幂等键、请求指纹和关联链。
|
||||
- `commercial_cost_events`:追加式内部成本、原币/报告币、汇率、分摊键和冲回引用。
|
||||
- `commercial_runtime_reservations`:工具执行前的可变运营占位,保存 tenant/subscription/entitlement/run/tool call、基准、预占量、真实量、周期、配置快照、状态和补偿原因;它参与配额但不是客户用量事实。
|
||||
- `commercial_billing_periods`:不可变账期签发事实,保存订阅/套餐引用、窗口、顺序、币种、基础费、席位、计价模式和来源快照;PostgreSQL 禁止更新和删除。
|
||||
- `commercial_admin_events`:套餐、订阅、权益、账期和续期动作的脱敏追加审计,保存 tenant、actor、`X-Request-Id`、原因、动作、资源版本和白名单 before/after。
|
||||
- `usage_meter_events.period_key` 表示真实账期键,`quota_period_key` 独立表示权益重置周期;成本与运行时预占同样通过 `billing_period_id` 绑定账期,避免订阅当前周期滚动后污染历史。
|
||||
- 事实表在 PostgreSQL 使用触发器禁止 UPDATE/DELETE,租户复合外键防止跨租户引用。
|
||||
|
||||
运行时占位状态如下:
|
||||
|
||||
```text
|
||||
reserved -> committed # 成功且 actual <= reserved
|
||||
reserved -> released # 工具失败或执行前阻断
|
||||
reserved -> expired # 运行已终止且没有真实工具调用
|
||||
reserved -> reconciliation_required # 已发生调用但缺少执行前预占等人工补偿场景
|
||||
committed -> committed_reconciliation_required -> committed
|
||||
# 用量已提交、成本等后续事实失败,幂等补齐后恢复
|
||||
```
|
||||
|
||||
过期但运行仍在进行、运行记录缺失或工具终态不确定时继续保留额度,不允许补偿器仅因 TTL 到期释放后造成超卖。
|
||||
`committed_reconciliation_required` 已有真实用量事实,因此不再计入有效预占;配额只消费一次,同时保留明确的待补偿队列。
|
||||
|
||||
### 权限
|
||||
|
||||
- 平台管理员可配置所有租户商业账户,但商业配置不能授予财务确认或风险审批能力。
|
||||
- finance/executive 只读本租户当前账户,不读取平台内部成本或其他租户数据。
|
||||
- manager、employee 默认无商业账户与商业分析权限。
|
||||
- 所有管理接口从认证上下文判断平台管理员,目标租户来自受控路径参数。
|
||||
|
||||
### 降级策略
|
||||
|
||||
- 无订阅:返回 unavailable 和明确说明,不自动赠送无限权益。
|
||||
- 订阅非 active/trialing:配额保留展示但最终消费失败关闭。
|
||||
- 缺成本:贡献毛利和可持续价格下限不可用。
|
||||
- 缺财务确认价值:客户 ROI、价值上限和成功费不可用,建议继续试点采集。
|
||||
- 多币种缺少共同币种:分别展示,禁止跨币种净额。
|
||||
- 并发或幂等冲突:返回 409,不覆盖首次事实。
|
||||
- 生产中央工具路径未配置 runtime meter:兼容执行且不生成商业事实;配置只在当前订阅周期和权益有效期内参与门禁,历史过期配置不会误触发 enforcement。
|
||||
- 已发生的旧直接工具路径缺少执行前预占:不补写为正常用量,持久化 `reconciliation_required` 并冻结对应容量,等待受控补偿;即使当前合同已暂停,也保留其唯一可验证的租户、订阅和权益归属。
|
||||
- 用量已追加但内部成本写入失败:持久化 `committed_reconciliation_required`,配额以真实用量为准且预占归零;按相同 tool call 重试只补成本,完成后回到 committed。
|
||||
- 自动续期只处理 `trialing/active + auto_renew`;月、季、年按自然月边界滚动。合同制、缺少可推导边界或下一完整账期越过 `ends_at` 时失败关闭,不创建部分账期或猜测续约。
|
||||
- 调度器即使发生重复扫描或多进程竞争,也先获取 leader lease,再锁定订阅;账期窗口和幂等键的唯一约束保证同一周期最多签发一次。数据库触发器还会按租户与订阅获取事务级 advisory lock,并拒绝任何半开区间重叠账期,防止绕过服务层直接写入破坏时间线。
|
||||
|
||||
## 算法与公式
|
||||
|
||||
### 客户 ROI
|
||||
|
||||
```text
|
||||
customer_roi = (verified_cash_savings - customer_charges) / customer_charges
|
||||
```
|
||||
|
||||
- `verified_cash_savings` 只包含独立财务确认、canonical、已计冲回的现金节省。
|
||||
- `customer_charges` 来自订阅基础费快照和有可信同币种单价的用量计费。
|
||||
- 分母必须大于 0;否则状态为 unavailable。
|
||||
|
||||
### 平台贡献毛利
|
||||
|
||||
```text
|
||||
contribution_margin = customer_charges - internal_costs
|
||||
contribution_margin_rate = contribution_margin / customer_charges
|
||||
```
|
||||
|
||||
- 内部成本来自追加式成本账本,不使用估算页面数字。
|
||||
|
||||
### 可持续定价走廊
|
||||
|
||||
```text
|
||||
minimum_sustainable_charge = internal_costs / (1 - target_margin_rate)
|
||||
maximum_value_aligned_charge = verified_cash_savings * max_value_share
|
||||
maximum_success_fee = max(0, maximum_value_aligned_charge - minimum_sustainable_charge)
|
||||
```
|
||||
|
||||
- 当 `minimum_sustainable_charge <= maximum_value_aligned_charge` 时,建议 hybrid:基础费不低于成本下限,成功费封顶为剩余价值空间。
|
||||
- 只有成本证据时建议 subscription;成本与价值都不足时建议 pilot_collecting。
|
||||
- 成本下限高于价值上限时建议 optimize_unit_economics,不自动提高客户报价。
|
||||
|
||||
## 测试方案
|
||||
|
||||
- 模型/迁移:租户复合外键、状态检查、金额符号、冲回引用和 append-only。
|
||||
- 服务:套餐版本、订阅终态、权益不可回改、配额硬门禁、幂等、冲回和多币种。
|
||||
- 权限:平台管理员、租户财务、manager、employee 与跨租户访问。
|
||||
- 分析:收费/成本/节省/毛利/ROI 分账,缺证据状态和 `as_of` 回放。
|
||||
- 定价:可行区间、成本高于价值、仅成本、完全无证据和多币种。
|
||||
- 前端:状态、权限、操作确认、空态、错误态、币种分组与生产构建。
|
||||
- PostgreSQL:并发配额、同幂等键、成本冲回单赢家和迁移完整性。
|
||||
- PostgreSQL 账期:0020→0021→0020 升降级、账期/审计 UPDATE/DELETE 拒绝、重叠账期 INSERT 拒绝、历史事实账期绑定和双线程续期单赢家。
|
||||
- 运行时预占:无配置兼容、成功结算、失败释放、变量 hard max、真实量超预占、幂等重放、历史配置隔离、直接路径补偿和过期补偿。
|
||||
- 所有命令在 `local-x-financial-linux` 容器内执行,单次最长 60 秒。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
- [A1] 每个商业消费可追溯到租户、订阅、权益、用量事件、来源和 correlation。
|
||||
- [A2] 并发不能突破硬配额;重复事件稳定重放,冲突内容被拒绝。
|
||||
- [A3] 付费状态不能绕过安全或人工审核门禁。
|
||||
- [A4] 客户 ROI、平台毛利、客户节省和工时价值在 API/UI 中不混算。
|
||||
- [A5] 暂停、恢复、取消、过期和历史查询可操作,终态不可原地复活。
|
||||
- [A6] 定价场景只使用真实同币种成本与确认节省;证据不足时不输出虚假价格。
|
||||
- [A7] 相关后端、PostgreSQL、前端、构建、Ruff 与迁移验证在容器内通过。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
- 真实计费仍需把 OCR、LLM、存储、连接器和支持运行事件自动接入用量/成本账本;手工管理员写入只能用于校验,不是最终生产采集。
|
||||
- 中央 Orchestrator 工具已经执行前预占;绕过中央执行器的其他生产入口仍须逐一迁移到 permit 契约,当前只会形成可见补偿积压,不会伪装成正常计量。
|
||||
- 合同制自动续期仍不推断新合同窗口;管理员或外部订阅连接器必须先提供显式续约事实,再创建后继合同/订阅版本。
|
||||
- 首个客户的实际套餐金额、席位、包含量、毛利目标、价值分享比例和折扣需要商业负责人确认。
|
||||
- 发票、税率、回款、坏账、渠道分成与收入确认尚未接入,当前 `customer_charges` 是合同/用量计费基准,不等同已收现金。
|
||||
- 价值分享合同必须定义基线、排除项、冲回、确认人、封顶和争议期。
|
||||
- 工时价值默认不进入现金 ROI,只有客户确认活跃工时基线、角色成本和可释放比例后才单独披露。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-16:完成五张商业事实表与 0016 迁移、套餐/订阅/权益、用量/成本、配额门禁、幂等和冲回服务。
|
||||
- 2026-07-16:完成客户收费、内部成本、贡献毛利、财务确认现金节省、客户 ROI 和工时价值分账分析;修正 ROI 为净收益口径并收紧角色、时区和权益历史不可变边界。
|
||||
- 2026-07-16:补齐订阅暂停/逾期/取消/过期、恢复和套餐/订阅/权益/用量/成本历史查询。
|
||||
- 2026-07-16:完成基于真实成本下限与确认价值上限的定价场景,禁止风险暴露、预计节省或跨币种混入报价。
|
||||
- 2026-07-16:完成商业工作台五块能力、订阅生命周期确认、用量/成本与价值分账、按币种定价场景两步确认;全量前端 802 项、code-size 和生产构建通过。
|
||||
- 2026-07-16:一次性 PostgreSQL 17 并发配额、幂等和成本冲回 3 项通过;真实开票、回款与客户合同参数继续保留为外部试点边界,不以 mock 标记完成。
|
||||
- 2026-07-16:新增 `0019` 运行时预占迁移和中央 Agent 工具 permit;在订阅/权益锁内按已用量加有效预占原子控额,成功按真实 AgentToolCall 结算,失败/阻断释放,变量用量超过预占拒绝写入。
|
||||
- 2026-07-16:新增持久补偿状态和过期补偿器;旧直接调用缺少预占时冻结容量并进入 reconciliation_required,运行终态不确定时不按 TTL 误释放;用量已提交但成本失败进入 committed_reconciliation_required,相同预占与用量/成本均可幂等重试。
|
||||
- 2026-07-16:新增 `0021` 不可变账期和商业管理审计;订阅创建即签发首期,月/季/年到期由 leader 调度器和订阅行锁幂等滚动,合同制与越界周期失败关闭。
|
||||
- 2026-07-16:用量、成本、运行时预占和配额查询改为绑定 `billing_period_id`,并以独立 `quota_period_key` 保留权益重置语义;客户收费基础费改从账期快照聚合,不再读取可变订阅当前周期。
|
||||
- 2026-07-16:容器内商业/迁移前置定向 131 项、前端商业 35 项和 Vite 构建通过;一次性 PostgreSQL 17 商业并发 5 项通过。0021 的升级、模型/约束/触发器(含账期重叠阻断)/运行不变量及 0021→0020 降级通过;全链 44/45 唯一失败来自后继 0023 尚未接管的盲审临时表,不属于 0021。
|
||||
- 2026-07-16:容器内商业/Agent/迁移组合 183 项通过、1 项因未显式配置外部迁移库跳过;运行时定向 27 项、PostgreSQL 商业并发 4 项和 0019→0020 完整迁移循环通过;全局 code-size 仅剩共享 `RiskRuleGenerationService` 817 行既有门禁失败,本轮所有相关核心类低于 800 行。
|
||||
@@ -0,0 +1,87 @@
|
||||
# 商业计量、客户 ROI 与可持续定价 开发 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 每项必须回链 `CONCEPT.md` 对应章节。
|
||||
- 只有代码、接口或容器验证提供证据后才能勾选。
|
||||
- 客户价值、平台收入、内部成本和工时估值必须分账;mock 与手工事件不得标记为生产事实。
|
||||
|
||||
## 1. 调研与边界
|
||||
|
||||
- [x] [CONCEPT: 背景与问题] 明确商业权益、用量、成本、客户 ROI、平台毛利和定价不是同一事实。
|
||||
证据:`CONCEPT.md`“背景与问题”“目标与非目标”。
|
||||
- [x] [CONCEPT: 目标与非目标] 确认不硬编码客户价格、不混算风险暴露、不跨币种求和、不让付费绕过安全门禁。
|
||||
证据:`CONCEPT.md`“目标与非目标”。
|
||||
|
||||
## 2. 契约与设计
|
||||
|
||||
- [x] [CONCEPT: 数据] 定义套餐、订阅、权益、用量和内部成本五类事实及状态。
|
||||
证据:`commercial.py` 模型与 schema、`20260716_0016_commercial_metering.py`。
|
||||
- [x] [CONCEPT: 算法与公式] 定义客户 ROI、贡献毛利和可持续定价走廊公式。
|
||||
证据:`CONCEPT.md`“算法与公式”、`commercial_analytics.py`、`commercial_pricing.py`。
|
||||
- [x] [CONCEPT: 权限] 定义平台配置、租户只读和商业/安全门禁分离。
|
||||
证据:`commercial_access_policy.py`、`commercial_entitlements.py`。
|
||||
|
||||
## 3. 后端实现
|
||||
|
||||
- [x] [CONCEPT: 数据] 新增五张商业表、复合租户约束、幂等、冲回和 append-only 迁移。
|
||||
证据:`models/commercial.py`、`20260716_0016_commercial_metering.py`、迁移/模型测试。
|
||||
- [x] [CONCEPT: 数据] 新增运行时预占运营表、状态约束、复合租户外键、全局 tool call 幂等和迁移所有权。
|
||||
证据:`models/commercial_runtime.py`、`20260716_0019_commercial_runtime_reservations.py`、`commercial_migration_assertions.py`;0019→0020 一次性 PostgreSQL 完整升降级循环通过。
|
||||
- [x] [CONCEPT: 后端] 实现套餐版本、订阅、权益、配额、用量、成本与商业分析服务。
|
||||
证据:`commercial_admin.py`、`commercial_entitlements.py`、`commercial_metering.py`、`commercial_analytics.py`。
|
||||
- [x] [CONCEPT: 生命周期与查询] 实现暂停、逾期、取消、过期、恢复与五类历史查询。
|
||||
证据:`CommercialAdminService.transition_subscription()`、`commercial_queries.py`、`/commercial/admin/tenants/{tenant_id}/...` 分资源接口。
|
||||
- [x] [CONCEPT: 定价走廊] 实现成本下限、确认价值上限、成功费封顶和商业模式建议。
|
||||
证据:`commercial_pricing.py`、`POST /commercial/admin/tenants/{tenant_id}/pricing-scenarios`。
|
||||
- [x] [CONCEPT: 后端] 把中央 Orchestrator 工具接入执行前预占、真实 AgentToolCall 结算和失败释放。
|
||||
证据:`orchestrator_tool_execution.py`、`agent_runs.py`、`commercial_runtime_bridge.py`;无配置兼容,配置后先 reserve 再执行,成功只追加真实用量/成本,失败和阻断不计量。
|
||||
- [x] [CONCEPT: 降级策略] 持久化缺预占和计量故障补偿状态,并安全处理过期预占。
|
||||
证据:`commercial_runtime_reservations.py`、`commercial_runtime_reconciler.py`;直接调用形成 reconciliation_required,运行中/未知终态继续持有额度,终态无调用才过期释放;用量成功但成本失败形成 committed_reconciliation_required,重试只补成本且不重复冻结额度。
|
||||
- [x] [CONCEPT: 风险与开放问题] 把已识别的权威运行入口迁移到 permit 契约。
|
||||
证据:中央 Orchestrator、`ocr_commercial.py`、`runtime_chat_commercial.py`、`financial_connector_commercial.py` 和 `expense_claim_attachment_commercial.py` 已接通预占/结算/释放;资源组合 63 项通过。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 对后续新增的知识库/ONLYOFFICE 存储、实施和支持等资源入口持续执行 meter 盘点,不允许绕过 permit。
|
||||
证据要求:新增真实资源入口时提供权威数量口径、事务边界、成本来源和回归测试;当前不把未发生的未来入口伪装成已计量。
|
||||
- [x] [CONCEPT: 风险与开放问题] 通过不可变 billing period 和幂等 rollover 实现 `auto_renew` 周期滚动。
|
||||
证据:`20260716_0021_commercial_billing_periods.py`、`commercial_billing_periods.py`、`commercial_subscription_rollover.py`、`commercial_rollover_scheduler.py`;月/季/年自动滚动,合同制和 `ends_at` 越界失败关闭,PostgreSQL 双线程仅生成一个账期,数据库触发器拒绝重叠账期。
|
||||
- [x] [CONCEPT: 数据] 让用量、成本、运行时预占和配额历史绑定不可变账期,并分离配额重置键。
|
||||
证据:`UsageMeterEvent`、`CommercialCostEvent`、`CommercialRuntimeReservation` 的 `billing_period_id`,以及 usage/reservation 的 `quota_period_key`;收费分析从账期快照读取基础费和币种。
|
||||
- [x] [CONCEPT: 权限] 为套餐、订阅、权益和续期建立脱敏追加式审计与租户安全历史 API。
|
||||
证据:`commercial_admin_events`、`commercial_admin_audit.py`、`commercial_billing.py`;管理写接口强制 `X-Request-Id` 和原因,普通 finance/executive 只能读本租户账期,审计仅平台管理员可读。
|
||||
- [ ] [CONCEPT: 非目标] 对接真实开票/收款/订阅提供商并区分合同计费、已开票与已收现金。
|
||||
证据:等待目标客户和 provider 选择,不使用 mock 冒充完成。
|
||||
|
||||
## 4. 前端实现
|
||||
|
||||
- [x] [CONCEPT: 前端] 完成商业工作台的账户、套餐/订阅、权益/配额、用量/成本、价值/定价五块。
|
||||
证据:`CommercialWorkspace.vue` 组合账户、生命周期、权益、用量成本、价值分析与定价场景面板。
|
||||
- [x] [CONCEPT: 前端] 接入订阅暂停/取消/恢复、历史查询和操作确认。
|
||||
证据:`useCommercialWorkspace.js`、`CommercialSubscriptionLifecyclePanel.vue`;终态和定价均有确认步骤,操作后按租户重新加载。
|
||||
- [x] [CONCEPT: 前端] 严格分开展示客户 ROI 与平台毛利,并支持多币种和证据缺口状态。
|
||||
证据:`CommercialValueAnalysisPanel.vue`、`CommercialPricingScenarioPanel.vue`、`commercialWorkspaceModel.js`;按币种分组,null/unavailable 显示“不可用”,不跨币种合计。
|
||||
- [x] [CONCEPT: 前端] 接入现有应用入口、权限态、移动端和生产构建。
|
||||
证据:`OverviewView.vue`/顶部导航已接入“商业化管理”;商业定向 35 项、全量 web 802 项、code-size 与 Vite 2246 modules 构建通过。
|
||||
|
||||
## 5. 测试与验证
|
||||
|
||||
- [x] [CONCEPT: 测试方案] 后端模型、服务、HTTP、权限、生命周期、查询和定价回归通过。
|
||||
证据:容器内 Ruff 通过;`test_commercial_models.py`、`test_commercial_services.py`、`test_commercial_endpoints.py` 当前 12 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 一次性 PostgreSQL 并发用量、原子预占、硬配额和成本冲回验证通过并记录当前命令结果。
|
||||
证据:一次性 PostgreSQL 17 中 `test_commercial_concurrency_postgres.py` 4 项通过;两个并发工具竞争 1 份额度时仅一个 reservation 成功。
|
||||
- [x] [CONCEPT: 测试方案] 运行时预占、结算、释放、幂等、变量上限、历史配置和补偿回归通过。
|
||||
证据:`test_commercial_runtime_metering.py`、`test_commercial_runtime_reservations.py` 27 项通过;商业/Agent/权限/迁移相关组合 183 项通过、1 项因未显式配置外部迁移库跳过,Ruff、compileall 和相关类 800 行检查通过。
|
||||
- [x] [CONCEPT: 测试方案] 前端行为测试、全量 web 测试、code-size 门禁和 Vite 构建通过。
|
||||
证据:商业定向 35 项、全量 web 802 项通过;code-size 通过;Vite production build 转换 2246 个模块。
|
||||
- [x] [CONCEPT: 测试方案] 不可变账期、脱敏审计、自动续期和调度器验证通过。
|
||||
证据:容器内商业/迁移前置定向 131 项、前端商业 35 项及 Vite build 通过;PostgreSQL 17 商业并发 5 项通过,含双线程续期单赢家;0021 升级、重叠账期阻断、运行不变量和 0021→0020 降级通过。
|
||||
- [x] [CONCEPT: 指标与验收] 逐项核对 A1-A7,并回填最终文件、接口和容器证据。
|
||||
证据:商业模型/服务/API/前端、硬配额、账期、生命周期、ROI/毛利分账和定价走廊均有回归;资源边界组合 63 项、PostgreSQL 商业并发 5 项、Web 全量 815 项及 Vite build 通过。
|
||||
|
||||
## 6. 商业与试点收尾
|
||||
|
||||
- [ ] [CONCEPT: 风险与开放问题] 用真实试点 30/90 天数据冻结目标毛利率、最大价值分享、包含量、超额策略和封顶。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 确认发票、税率、回款、坏账、渠道和收入确认边界。
|
||||
- [x] [CONCEPT: 本轮实现记录] 同步更新上位闭环文档与工程验收手册,不删除证据不足项。
|
||||
证据:上位 AI 闭环 TODO 与 `engineering-closure-and-production-readiness` CONCEPT/TODO 已区分工程完成、生产上线和真实试点。
|
||||
@@ -0,0 +1,226 @@
|
||||
# 财务连接器与支付对账闭环 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
把经过租户、来源、密钥版本和请求路径共同认证的 production-mode 财务事件契约接入付款、ERP、对账与 Savings 闭环,同时让所有非生产回执严格停留在只读模拟事实层;真实外部现金仍以目标 provider 联调为准。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
当前报销单可以由财务人员在平台内确认“已付款”,并能联动申请归档和 Savings 实现记录,但这只是内部业务状态,不是银行、支付平台或 ERP 的外部现金事实。若继续把单一状态当成真实回执,会留下重复付款、金额/币种错配、回执伪造、凭证缺失、对账异常未处置和虚假现金节省等风险。
|
||||
|
||||
本功能把外部财务系统接入收敛成统一、可审计的连接器契约。首期允许使用 mock adapter 验证协议和流程,但 mock 必须完整模拟签名、幂等、失败、重试、乱序、退款、凭证和对账差异,不以直接写“已付款”代替连接器事实。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- 建立租户隔离、来源可验证、追加式的支付/银行/ERP 事件账本。
|
||||
- 支持支付批次、结算成功、支付失败、退款/冲回、ERP 入账凭证和对账结果。
|
||||
- 只有单据、金额、币种、外部引用和签名均通过校验时,才推进报销付款状态。
|
||||
- 把外部事件与 Expense Case、Business Event、Savings Evidence、归档事件用同一 correlation 链关联。
|
||||
- 对重复、冲突、乱序和不完整事件 fail-closed,并提供可人工处理的对账异常。
|
||||
- 以版本化 activate/disable/rotate 状态机管理连接器密钥,所有配置动作形成不含密钥材料的追加式审计。
|
||||
|
||||
### 非目标
|
||||
|
||||
- 不自建银行清算、企业支付网络、税务开票网络或 ERP 总账。
|
||||
- 不保存银行卡号、银行流水原文、完整付款人账号或连接器密钥明文。
|
||||
- 不允许连接器绕过报销审批、风险门禁、租户权限或财务确认。
|
||||
- 不把 mock 回执标记为生产级外部现金证据;运行环境和证据等级必须显式区分。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
- 财务付款员:提交或查看付款批次,处理失败与待匹配回执。
|
||||
- 财务复核员:复核金额/币种/收款主体和对账异常,确认或拒绝处置。
|
||||
- 财务负责人/CFO:查看已匹配、待对账、失败、退款和未入账金额。
|
||||
- 平台管理员:配置连接器公钥/密钥版本、来源白名单和健康状态,但不能代替财务确认业务结果。
|
||||
- 外部连接器:按租户和来源签名推送支付、银行或 ERP 事件,安全重试并读取幂等结果。
|
||||
|
||||
## 功能能力
|
||||
|
||||
- 连接器注册与密钥版本:来源、环境、允许事件、时钟偏差、启停和轮换状态。
|
||||
- 统一事件信封:tenant、provider、event ID、event type、occurred at、payload hash、correlation、signature version。
|
||||
- 支付批次与回执:批次创建、提交、受理、成功、失败和部分成功。
|
||||
- ERP 入账:凭证号、会计期间、入账时间、受控摘要和原始内容哈希。
|
||||
- 对账:按单据、金额、币种和外部引用自动匹配;差异进入人工处置。
|
||||
- 冲回:退款、撤销和补付使用新事件,不更新或删除原事件。
|
||||
- 可观测性:最近成功时间、失败率、重试次数、积压、签名失败和对账差异。
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 模块职责
|
||||
|
||||
- `financial_connector_auth`:验证来源、签名、时间戳、密钥版本和重放窗口。
|
||||
- `financial_connector_ingestion`:规范化事件、计算指纹、幂等写入、生产/模拟分流和冲突检测。
|
||||
- `financial_connector_simulation`:只读校验 test/mock/staging 回执,不创建或修改 Claim、对账、ERP、Business Event 或 Savings。
|
||||
- `financial_connector_mock_adapter`:平台管理员显式触发的非生产场景适配器;用已激活配置的真实签名链确定性生成成功、失败、乱序、重复、冲突、退款和 ERP 回执,但不提供任意 payload 注入能力。
|
||||
- `financial_connector_observability`:按租户和配置聚合最近成功、失败率、重试、积压、签名失败、冲突和对账异常;只读取最小化事实与追加式运行事件。
|
||||
- `financial_connector_payment_evidence`:从 Claim 的付款事实中提取统一证据 DTO,明确区分 production-mode 外部回执分类、非生产模拟回执和人工付款内部状态;分类本身不证明真实 provider 已接通。
|
||||
- `financial_connector_config_lifecycle`:执行带 expected version 的激活、停用和原子密钥轮换。
|
||||
- `payment_reconciliation`:匹配 Claim、金额、币种和状态,生成 matched / exception 结果。
|
||||
- `financial_connector_actions`:在可信匹配后调用现有付款动作;支付失败、退款和 ERP 入账分别旁写事件。
|
||||
- `financial_connector_projection`:为财务工作台提供脱敏列表、详情、差异和健康度。
|
||||
- 供应商 adapter 只负责供应商字段映射,不直接修改 Claim、Savings 或预算。
|
||||
|
||||
### 数据与契约
|
||||
|
||||
首期新增以下 migration-owned 表,均带 `tenant_id`:
|
||||
|
||||
- `financial_connector_configs`:provider、environment、allowed event types、secret/key version、status、last success/error;只保存密钥引用或不可逆验证材料。
|
||||
- `financial_connector_config_events`:created、activated、disabled、rotation started/replacement created 的追加式配置审计;保存 actor、request、reason、expected version 和脱敏前后状态,不保存 `secret_ref`。
|
||||
- `financial_connector_events`:方向、事件类型、外部事件 ID、请求指纹、原始内容哈希、发生/接收时间、验证等级、处理状态、关联单据/Case 和错误码;UPDATE/DELETE 禁止。
|
||||
- `payment_reconciliation_cases`:Claim、期望/实际金额与币种、匹配状态、差异、处置版本、负责人和最后事件;作为可变投影,历史动作另存事件。
|
||||
- `payment_reconciliation_events`:创建、自动匹配、人工确认、拒绝、重开、退款和关闭的追加式审计事件,保存请求指纹和首次响应。
|
||||
- `financial_connector_operational_events`(`0022`):保存 `replay / auth_failure / payload_conflict` 三类运行事实的 tenant/config/provider/environment、受控原因码、两类 HMAC 指纹、幂等键和发生时间;不保存原始 payload、签名、外部事件 ID、Claim 引用、correlation 或密钥材料。复合租户外键防止跨租户归属,PostgreSQL 触发器禁止 UPDATE/DELETE;存在运行事实时拒绝有损降级。
|
||||
|
||||
关键约束:
|
||||
|
||||
- `(tenant_id, provider, external_event_id)` 唯一。
|
||||
- 配置从 `disabled/version=1` 创建;激活和停用必须命中 expected version,轮换原子地产生新 active key version 并把旧版本置为 rotating。
|
||||
- 同幂等键不同 payload hash 返回冲突,不能覆盖首次事件。
|
||||
- 同一个运行事实 candidate 的补偿写入按 `(tenant_id, idempotency_key)` 幂等;不同 HTTP 尝试即使请求内容相同,也因可信发生时间不同而分别计数,避免把真实重放次数永久压成一次。
|
||||
- Claim、Case、配置和对账记录使用复合租户外键。
|
||||
- 退款/冲回必须引用同租户原结算事件。
|
||||
- 生产事件必须通过已激活密钥验证;test/mock/staging 即使签名和业务字段全部匹配,也只能生成 `projection_scope=simulation_only` 的连接器事实,绝不进入核心财务状态机。
|
||||
|
||||
### 接口
|
||||
|
||||
- `POST /api/v1/integrations/financial-events`:连接器签名事件入口;返回稳定接收/重放结果。
|
||||
- `POST /api/v1/financial-connectors/admin/tenants/{tenant}/configs/{id}/activate|disable|rotate`:带版本、actor、request ID 和 reason 的配置状态机。
|
||||
- `POST /api/v1/financial-connectors/admin/tenants/{tenant}/configs/{id}/simulate`:平台管理员运行确定性非生产场景;production 配置和未激活配置 fail-closed。
|
||||
- `GET /api/v1/financial-connectors/admin/tenants/{tenant}/config-events`:读取不含密钥材料的配置审计时间线。
|
||||
- `GET /api/v1/financial-connectors/admin/tenants/{tenant}/observability`:平台管理员读取目标租户脱敏运行指标;从 config/event/reconciliation/operational event 聚合指定窗口真实值。
|
||||
- `GET /api/v1/financial-connectors/observability`:财务角色读取当前租户脱敏运行指标;返回 `window_started_at / as_of / generated_at / source_revision`,以及 replay、认证失败、签名失败、payload conflict 的真实计数与最近发生时间。`0022` 起四类指标均标记 `available`,没有事实时真实返回零而不是“待采集”占位。
|
||||
- `GET /api/v1/financial-connectors/payment-evidence/{claim}`:有单据读取权限的当前租户用户读取付款证据等级,不返回原始连接器内容。
|
||||
- `GET /api/v1/financial-reconciliation/cases`:财务角色分页查看匹配与异常。
|
||||
- `GET /api/v1/financial-reconciliation/cases/{id}`:查看脱敏证据和追加式时间线。
|
||||
- `POST /api/v1/financial-reconciliation/cases/{id}/confirm`:独立财务确认差异或人工匹配。
|
||||
- `POST /api/v1/financial-reconciliation/cases/{id}/reject`:拒绝错误回执并记录原因。
|
||||
- 连接器配置管理接口仅平台管理员可用,业务确认接口仅财务角色可用,两类权限不互相继承。
|
||||
|
||||
### 匹配算法
|
||||
|
||||
1. 验证 tenant/provider/key version/timestamp/signature 和事件类型;HMAC canonical request 同时绑定固定 HTTP method/path,禁止共享密钥跨 provider 或 key version 重放。
|
||||
2. 以 canonical JSON 生成 payload hash;按外部事件 ID 与幂等键检查首次请求。
|
||||
3. 通过受控引用解析 Claim,不允许仅凭模糊姓名或备注自动匹配。
|
||||
4. 校验 Claim 已完成审批且处于待付款;比较金额、币种、外部业务引用和事件方向。
|
||||
5. 只有 `production_verified` 且完全一致时才自动 matched;非生产来源只生成模拟投影,任何生产差异进入 exception 且不改变 Claim。
|
||||
6. matched 事件在同事务调用付款动作、记录 Business Event,并把外部事件哈希作为 Savings 证据。
|
||||
7. ERP posted 只表示入账,不重复触发付款;refund/reversal 追加负向业务与 Savings 冲回候选。
|
||||
|
||||
### 权限与安全
|
||||
|
||||
- 连接器入口不使用普通用户会话,使用租户绑定的签名认证;普通 Bearer token 不能伪装连接器。
|
||||
- HMAC/签名比较使用常量时间函数,限制时间窗口并记录 nonce/外部事件 ID 防重放。
|
||||
- 激活和轮换前由服务端解析 `secret_ref` 并校验至少 128-bit HMAC 密钥;数据库和审计 DTO 均不返回引用或明文。
|
||||
- 日志、响应和 DTO 最多暴露外部引用后八位或不可逆摘要,不返回原始 payload、签名或密钥。
|
||||
- replay 随成功重放事务提交;认证失败与 payload 冲突先回滚失败业务事务,再独立提交脱敏运行事实,审计写入异常不得覆盖原始 401/409 响应。
|
||||
- 只有服务端按 tenant/provider/key version 解析出 active 配置,并成功解析该配置的服务端密钥后,才构造可归属的 operational context。缺认证头、请求 tenant 不一致、未知/未激活配置或密钥不可用均不接受客户端自报租户,只写不带租户归属的结构化警告;签名、时间窗和事件白名单失败才可安全归入已解析配置。
|
||||
- 运行表只保存 `hmac-sha256:` 请求/外部事件指纹和 `sha256:` 幂等键。HMAC 输入可以包含外部 ID 和业务引用,但这些原值不会进入表、DTO 或错误日志。
|
||||
- 不可变连接器事实的 normalized payload 不再保存完整 `claim_reference`;`0020` 受控迁移会删除历史冗余值,保留内容哈希、受控 Claim ID 与必要尾号。
|
||||
- 跨租户资源统一按 404 隐藏;配置管理员不能确认对账,付款申请人不能确认自己的异常。
|
||||
- 连接器故障、未知密钥、签名异常、金额/币种不一致和数据库异常均 fail-closed。
|
||||
|
||||
### 状态转换
|
||||
|
||||
- 连接器事件:`received → verified → processed`,失败进入 `rejected`;事实本身追加只读。
|
||||
- 连接器配置:`disabled → active → rotating → disabled`;正常启用走 `disabled → active`,轮换时旧 active 原子进入 rotating、新 key version 以 active 创建。
|
||||
- 对账记录:`pending → matched | exception → confirmed | rejected`;退款可从 confirmed 进入 `reopened`,重新处置后关闭。
|
||||
- Claim 仅在可信 `payment_settled + matched` 后从 `pending_payment` 进入 `paid`。
|
||||
- ERP 凭证从 `pending_posting` 进入 `posted | posting_failed`,不反向伪造支付成功。
|
||||
|
||||
### 降级策略
|
||||
|
||||
- 连接器离线:保留待付款,不自动标记已付;显示积压和最后成功时间。
|
||||
- 回执乱序:先保存事实,等待前置事件或进入 pending,不猜测状态。
|
||||
- 回执冲突:保留首次事实并返回 409,生成对账异常。
|
||||
- ERP 未接入:付款事实可进入已付,但“已入账/凭证号”保持待采集。
|
||||
- test/mock/staging:返回 `simulation_only`,只保留追加式 connector fact/response projection,不创建对账 Case、不修改 Claim/ERP/归档/Savings,也不进入生产现金证明。
|
||||
- 显式 mock adapter 只能选择预定义场景和当前租户 Claim;事件 ID、correlation 和受控外部引用由 tenant/config/scenario/request ID 确定性派生。重复执行同一请求只产生稳定重放,不能借模拟接口注入生产配置或任意字段。
|
||||
- 升级前若已有非生产事件且响应曾关联对账 Case,`0020` 将其标记为 `legacy_nonproduction_effect_unknown` 供审计,不伪装成新策略下的无副作用模拟事实,也不在迁移中猜测性冲回历史财务状态。
|
||||
|
||||
### 兼容策略
|
||||
|
||||
- 保留现有人工“确认已付款”作为低等级内部证据;付款证据 DTO 使用 `internal_manual_payment`,生产连接器使用 `external_cash`,非生产连接器使用 `simulated_connector`/`staging_connector`。UI 必须同时展示来源标签和可信等级,不能只显示“已付款”。
|
||||
- 新连接器路径复用现有幂等付款动作、Case 时间线和 Savings 实现服务,不复制第二套状态机。
|
||||
- 现有 `risk_flags_json` 付款摘要继续只读兼容,新连接器事实进入正式事件表。
|
||||
|
||||
## 测试方案
|
||||
|
||||
- 单元:签名、时间窗口、canonical hash、幂等重放、冲突、乱序和字段白名单。
|
||||
- 权限:跨租户、普通用户伪造、管理员越权、申请人自证和密钥停用。
|
||||
- PostgreSQL:复合外键、唯一键、append-only、并发同事件、不同 payload 冲突和安全降级。
|
||||
- 集成:申请 → 票据 → 报销 → 预审 → 审批 → 外部付款 → 对账 → ERP 入账 → 归档 → Savings 待确认。
|
||||
- 反向:支付失败不推进、金额/币种错配不推进、重复回执只写一次、退款追加冲回。
|
||||
- adapter:逐场景验证确定性结果、重复请求、跨租户 Claim 隐藏、production 拒绝,以及 Claim/对账/ERP/Business Event/Savings 零副作用。
|
||||
- 可观测性:验证租户隔离、财务/管理员权限、窗口边界、签名失败与重放计数,并断言 DTO/日志无原始 payload、签名和密钥。
|
||||
- 运行事件:验证同 candidate 并发/补偿重试单赢家、不同请求尝试分别计数、冲突回滚后独立持久化、复合租户外键、HMAC 格式检查和数据库 append-only。
|
||||
- 所有验证只在 `local-x-financial-linux` 容器内执行,每条命令最长 60 秒。
|
||||
|
||||
## 算法与公式
|
||||
|
||||
本能力不做概率预测,核心是确定性门禁:
|
||||
|
||||
```text
|
||||
canonical_effect_allowed = (
|
||||
verification_level == production_verified
|
||||
AND signature_valid
|
||||
AND tenant_provider_key_path_bound
|
||||
AND claim_amount_currency_reference_match
|
||||
)
|
||||
```
|
||||
|
||||
任一条件为假都不得产生核心财务副作用;非生产环境无论其他条件是否为真,`canonical_effect_allowed` 固定为 false。
|
||||
|
||||
运行指标使用确定性窗口聚合,不从应用日志估算:
|
||||
|
||||
```text
|
||||
operational_count(type, window) = COUNT(
|
||||
tenant_id = current_tenant
|
||||
AND event_type = type
|
||||
AND window_started_at <= occurred_at <= as_of
|
||||
)
|
||||
|
||||
operational_idempotency_key = SHA256(
|
||||
source_revision, tenant, config, type, reason,
|
||||
HMAC(request), HMAC(external_event), occurred_at
|
||||
)
|
||||
```
|
||||
|
||||
`source_revision=20260716_0022` 表示当前运行指标的数据源与聚合契约版本;它不是 provider 协议版本。发生时间属于本次接收尝试,因此同一 candidate 重试仍稳定,而新尝试会形成新的真实计数。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
- 100% 外部结算事件具有租户、来源、签名版本、payload hash、外部 ID 和接收时间。
|
||||
- 重复相同事件稳定重放,冲突 payload 100% 拒绝。
|
||||
- 任何金额/币种/Claim/审批状态不一致均不会推进已付款。
|
||||
- 外部支付、ERP 凭证、对账处置、归档和 Savings 证据可由 correlation 链回放。
|
||||
- 连接器离线或异常时不出现虚假“已付款”“已入账”或现金节省。
|
||||
- observability 响应 100% 标明窗口起止和 source revision;三类运行事实按租户真实计数,并提供各类最近发生时间。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
- 首期真实 provider、签名算法、事件字段和 SLA 需要目标客户确认。
|
||||
- 部分 ERP 只有批次级凭证,需要明确批次到单据的拆分和舍入规则。
|
||||
- 多币种付款需要锁定汇率来源和会计期间;本功能不自行猜测汇率。
|
||||
- 退款、补付、员工自担调整是否进入现金节省,仍需客户财务签字口径。
|
||||
- 对账大额阈值及双人复核需按企业策略配置。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-16:完成现有内部付款、申请归档、Business Event、Savings 实现链路盘点,并冻结统一连接器、安全认证、对账状态和完整 E2E 方案;实现与容器证据保留在同目录 TODO 中继续执行。
|
||||
- 2026-07-16:完成 `0017` 四表迁移、HMAC/密钥版本/时间窗认证、租户隔离、追加式事件、幂等冲突和对账投影;一次性 PostgreSQL 17 迁移循环与 2 项并发探针通过。
|
||||
- 2026-07-16:支付结算只在金额、币种、Claim、审批状态与来源全部匹配时复用正式付款动作;ERP、失败、退款/冲回、Savings 失效和财务处置均进入同一 correlation 审计链,连接器服务/HTTP 7 项通过。
|
||||
- 2026-07-16:真实 provider、批次拆分、汇率、会计期间和大额双人复核仍由目标客户确认;当前 mock/test 证据始终标记 simulated,不冒充生产现金事实。
|
||||
- 2026-07-16:补齐 `0020` 配置版本与追加式审计迁移;新配置只能停用创建,激活前校验服务端密钥,轮换原子切换 key version,HTTP 时间线不暴露 `secret_ref`。
|
||||
- 2026-07-16:把 test/mock/staging 六类事件收口到 `simulation_only` 只读投影;生产事件继续完成付款、ERP、归档、Savings 和冲回,生产冲回也不能引用模拟原事件。
|
||||
- 2026-07-16:normalized payload 删除完整 `claim_reference`,历史冗余值由 `0020` 受控脱敏,HMAC v2 继续以内容哈希和 tenant/provider/key version/path 证明请求边界。
|
||||
- 2026-07-16:容器内连接器/配置/费用价值链组合 16 项、PostgreSQL 并发 3 项、迁移静态 124 项及全新 PostgreSQL 17 完整升降级循环通过;独立 0019→0020 探针确认版本回填、历史脱敏和 legacy 不确定性标记符合契约。
|
||||
- 2026-07-16:新增显式非生产 adapter,以 tenant/config/claim/scenario/request ID 派生稳定签名回执,覆盖成功、失败、乱序、重复、冲突、退款和 ERP 场景;HTTP 与服务测试确认 simulation-only 且 Claim、对账、Business Event、ERP 与 Savings 零副作用。
|
||||
- 2026-07-16:新增租户级脱敏可观测性 API 与财务看板面板,现有 config/event/reconciliation 表提供最后成功、失败率、积压和对账异常真实值;retry/auth_failure 在 `0022` 运行事件落地前明确显示 unavailable,不伪造零值。
|
||||
- 2026-07-16:新增付款证据等级 DTO 和 UI 口径,通过 production-mode 签名契约的回执分类为 `external_cash`,人工确认标为低等级 `internal_manual_payment`,模拟/预发布回执明确不进入核心账;当前本地自签测试不作为真实现金或 provider 接通证据。
|
||||
- 2026-07-17:完成 `0022` 追加式运行事实迁移与服务接入,耐久记录 replay、可信归属后的 auth failure 和 payload conflict;请求与外部事件只保存 HMAC 指纹,失败事务回滚后独立补偿提交。
|
||||
- 2026-07-17:可观测性改为返回 `20260716_0022` source revision、明确窗口、真实计数和最近时间;同一 candidate 补偿重试幂等,不同 HTTP 尝试分别计数。迁移总头继续串到既有 `0023`,`0022` 不越界创建后继 AI 表。
|
||||
- 2026-07-17:全新一次性 PostgreSQL 17 完整迁移循环 51 项、连接器并发 4 项、后继盲审并发 7 项通过;验证租户复合外键、HMAC 格式、运行事实 append-only 和并发单赢家。
|
||||
@@ -0,0 +1,76 @@
|
||||
# 财务连接器与支付对账闭环 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 每项必须回链 `CONCEPT.md`;只有代码、迁移、接口或容器验证提供证据后才能勾选。
|
||||
- 外部回执、内部付款状态、ERP 入账和财务确认必须分开,mock 不得伪装成生产现金事实。
|
||||
|
||||
## 1. 契约与安全
|
||||
|
||||
- [x] [CONCEPT: 背景与问题] 盘点内部付款、申请归档、Business Event、Savings 实现和证据边界。
|
||||
证据:`expense_claim_approval_flow.py`、`expense_claim_application_handoff.py`、`expense_cases.py`、`savings_realization.py` 只读审计。
|
||||
- [x] [CONCEPT: 目标与非目标] 冻结签名事件、幂等、对账、ERP 凭证、冲回和 mock 环境边界。
|
||||
证据:`CONCEPT.md`“目标与非目标”“数据与契约”“匹配算法”“降级策略”。
|
||||
- [x] [CONCEPT: 权限与安全] 实现连接器签名认证、密钥版本、时间窗口、来源白名单和防重放。
|
||||
证据:`financial_connector_auth.py`、`financial_connector_ingestion.py`;HMAC 使用常量时间比较,tenant/provider/key version/timestamp/method/path 均进入签名边界,共享密钥跨 provider/key version 重放被拒绝,相同事件幂等重放、冲突 payload 返回 409。
|
||||
- [x] [CONCEPT: 权限与安全] 实现平台配置权限与财务处置权限分离、跨租户 404 和申请人自证拒绝。
|
||||
证据:`financial_connectors.py`、`financial_connector_projection.py`;HTTP/服务回归覆盖普通用户、平台管理员、财务角色、跨租户隐藏与申请人自证拒绝。
|
||||
|
||||
## 2. 数据与迁移
|
||||
|
||||
- [x] [CONCEPT: 数据与契约] 新增配置、外部事件、对账投影和对账事件模型。
|
||||
证据:`models/financial_connector.py`、`schemas/financial_connector.py`。
|
||||
- [x] [CONCEPT: 数据与契约] 新增后继 Alembic 迁移、迁移所有权、复合租户外键、唯一/检查约束和 append-only 触发器。
|
||||
证据:`20260716_0017_financial_connector_reconciliation.py`、`schema_ownership.py`、`migration_preflight.py`;一次性 PostgreSQL 17 完整迁移循环通过。
|
||||
- [x] [CONCEPT: 数据与契约] 实现外部事件与退款/冲回引用、首次响应和 payload 指纹冲突。
|
||||
证据:`financial_connector_ingestion.py`、`payment_reconciliation.py`;同外部事件不同 payload 拒绝,退款/冲回必须绑定同租户已处理结算原事件。
|
||||
- [x] [CONCEPT: 数据与契约] 新增配置 version、追加式配置审计和历史 normalized payload 脱敏迁移。
|
||||
证据:`20260716_0020_financial_connector_config_lifecycle.py`、`FinancialConnectorConfigEvent`;配置审计使用 PostgreSQL append-only trigger,历史 `claim_reference` 从不可变事件的 normalized payload 中受控移除,内容哈希继续保留;全新 PostgreSQL 17 完整升降级循环 1 项通过,另一个独立库验证 0019→0020 历史数据脱敏与 legacy 标识。
|
||||
|
||||
## 3. 服务与接口
|
||||
|
||||
- [x] [CONCEPT: 模块职责] 拆分认证、ingestion、reconciliation、action 和 projection 服务,核心文件不超过 800 行。
|
||||
证据:`financial_connector_auth.py`、`financial_connector_ingestion.py`、`payment_reconciliation.py`、`financial_connector_actions.py`、`financial_connector_projection.py` 职责独立,最大核心文件低于 800 行。
|
||||
- [x] [CONCEPT: 接口] 实现统一事件入口、连接器配置、对账列表/详情、确认和拒绝接口。
|
||||
证据:`api/v1/endpoints/financial_connectors.py`。
|
||||
- [x] [CONCEPT: 接口] 实现带 expected version、actor、request ID、reason 的 activate/disable/rotate 状态机与脱敏审计查询。
|
||||
证据:`financial_connector_config_lifecycle.py`、`financial_connector_config_audit.py`、`FinancialConnectorConfigLifecycleAction`、`FinancialConnectorConfigRotateAction`;激活/轮换前解析服务端密钥并校验强度,轮换原子切换新旧 key version。
|
||||
- [x] [CONCEPT: 匹配算法] 完全匹配时复用现有幂等付款动作;任何金额、币种、单据或审批状态差异不产生付款副作用。
|
||||
证据:`FinancialConnectorActionService` 复用 `ExpenseClaimService.mark_claim_paid_from_connector()`;反向测试验证 mismatch/failure/conflict 无付款副作用。
|
||||
- [x] [CONCEPT: 证据与审计] 把外部事件哈希写入 Expense Case/Business Event/Savings 证据 correlation 链。
|
||||
证据:连接器结算动作写入脱敏内容哈希、verification/evidence classification 与 correlation;服务 E2E 可回放付款、Case、Business Event 和 Savings evidence。
|
||||
- [x] [CONCEPT: 状态转换] 实现支付失败、ERP posted/posting_failed、退款/冲回和对账重开。
|
||||
证据:`PaymentReconciliationService` 对六类生产事件分流;ERP 不重复付款,生产退款/冲回恢复 Claim 并追加 Savings 冲回事实。
|
||||
- [x] [CONCEPT: 降级策略] test/mock/staging 六类事件只写 simulation-only connector fact/response projection,不修改核心财务状态。
|
||||
证据:`financial_connector_simulation.py`、`financial_connector_ingestion.py`;`test_financial_connector_services.py` 参数化覆盖三类非生产环境和六类事件,Claim、申请归档、对账、Business Event、ERP 与 Savings 均保持不变。
|
||||
|
||||
## 4. Mock 与可观测性
|
||||
|
||||
- [x] [CONCEPT: 降级策略] 实现明确标识 test/mock 的 adapter,覆盖成功、失败、乱序、重复、冲突、退款和 ERP 回执。
|
||||
证据:`financial_connector_mock_adapter.py`、`FinancialConnectorSimulationCreate/Read` 与平台管理员 simulate API;仅 active test/mock/staging 可运行,tenant/config/claim/scenario/request ID 确定性派生事件,production、disabled 和跨租户请求 fail-closed;7 场景服务/HTTP 回归通过。
|
||||
- [x] [CONCEPT: 可观测性] 从现有事实输出最后成功时间、失败率、积压和对账异常,并在安全 DTO/UI 声明未采集指标。
|
||||
证据:`financial_connector_observability.py`、当前租户/管理员 observability API、`FinancialConnectorHealthPanel.vue`;所有查询先绑定 tenant,仅聚合 config/event/reconciliation 最小化事实,不返回原始 payload、签名和密钥。
|
||||
- [x] [CONCEPT: 可观测性] 用后继 `0022` 追加式运行事件补齐 replay、auth_failure 和 payload conflict 耐久计数。
|
||||
证据:`20260716_0022_financial_connector_operational_events.py`、`financial_connector_operational_events.py`、`financial_connector_auth.py`、`financial_connector_ingestion.py`、`financial_connector_observability.py`;只有可信配置与服务端密钥解析后才归属认证失败,表内只保存 HMAC 指纹;同 candidate 重试幂等、不同接收尝试分别计数,API 返回窗口、source revision、真实数量和最近时间。
|
||||
- [x] [CONCEPT: 兼容策略] 保留人工付款为低等级内部证据,并在 DTO/UI 区分外部回执和内部确认。
|
||||
证据:`financial_connector_payment_evidence.py`、payment-evidence API、`FinancialPaymentEvidenceRead` 与面板证据口径;人工付款=`internal_manual_payment`,通过 production-mode 契约验证的外部回执分类=`external_cash`,非生产回执=`simulated_connector|staging_connector`。真实 provider 仍待联调。
|
||||
|
||||
## 5. 测试与验收
|
||||
|
||||
- [x] [CONCEPT: 测试方案] 签名、防重放、幂等、冲突、字段白名单和错误恢复单元测试通过。
|
||||
证据:`test_financial_connector_services.py`、`test_financial_connector_endpoints.py`、`test_financial_connector_config_lifecycle.py` 与费用价值链 E2E 共 16 项通过;乱序原事件缺失保守进入 exception,不推进付款。
|
||||
- [x] [CONCEPT: 测试方案] PostgreSQL 迁移、复合租户约束、append-only、并发和安全降级验证通过。
|
||||
证据:fresh PostgreSQL 17 最终迁移总探针 62 项通过;`financial_connector_migration_assertions.py` 验证配置/事件/运行事实 trigger、租户约束、HMAC 格式和 version check;`test_financial_connector_concurrency_postgres.py` 4 项通过,覆盖同事件、冲突补偿事实、配置单版本胜者和 operational candidate 单赢家;最终 head 为 `20260717_0028`。
|
||||
- [x] [CONCEPT: 测试方案] 申请 → 票据 → 报销 → 预审 → 审批 → 外部付款 → 对账 → ERP 入账 → 归档端到端通过。
|
||||
证据:`test_expense_financial_value_chain_e2e.py` 使用测试密钥自签 production-mode HMAC 事件,覆盖申请审批、报销审批、ERP 入账、独立财务确认、Savings、商业价值与退款冲回契约;与连接器定向组合共 16 项通过,不代表真实 provider 回执或现金。
|
||||
- [x] [CONCEPT: 测试方案] 支付失败、金额/币种错配、重复回执和退款反向链路通过。
|
||||
证据:`test_financial_connector_services.py` 覆盖 production-mode 失败/错配无副作用、稳定重放、ERP、reversal/refund 追加 Savings 冲回,以及非生产回执完全不创建 Savings。
|
||||
- [x] [CONCEPT: 容器验证] 相关 pytest、Ruff、前端测试和构建均在 `local-x-financial-linux` 内通过。
|
||||
证据:历史连接器/配置/费用价值链/迁移组合 `140 passed, 1 skipped`;adapter/观测/证据 DTO 与既有连接器回归 `21 passed, 3 skipped`。2026-07-17 的 `0022` 收尾在容器内新增/定向回归 `115 passed`,全新 PostgreSQL 17 完整迁移循环 `51 passed`,连接器并发 `4 passed`,后继 `0023` 并发 `7 passed`;相关 Ruff、文件行数和全树 `git diff --check` 通过。历史连接器前端 3 项与 Vite 生产构建已通过;共享前端曾有 5 项旧路径测试失败,已单独记录且不属于本切片。
|
||||
|
||||
## 6. 客户配置待确认
|
||||
|
||||
- [ ] [CONCEPT: 风险与开放问题] 确认首个 provider、签名算法、字段映射、事件 SLA 和重试窗口。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 确认批次到单据映射、多币种汇率、会计期间、大额双人复核和退款口径。
|
||||
@@ -0,0 +1,373 @@
|
||||
# 节省事实账本与 CFO 经营价值看板 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
把费用优化从“发现风险和预计能省”推进为“执行、实际结果、独立财务确认、可回放冲回”的事实账本,并让 CFO 只看到有来源、有基线、有证据、可去重的企业价值。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
- 现有财务看板能够回答支出、单量、待付款、预算使用和风险分布,但不能可信回答企业已经节省了多少钱。
|
||||
- 风险观察金额、暂缓付款、未使用预算和未执行建议都不是现金节省;如果直接汇总,会形成虚假 ROI。
|
||||
- 当前最接近真实节省的链路是“住宿超标准 → 用户接受职级标准重算 → 审批 → 付款”。它已经真实降低报销金额,但尚未形成独立机会、付款后结果、财务签字、去重和冲回记录。
|
||||
- `ExpenseClaim`、预算和应付旧表没有结构化租户键,Claim 只能通过 `ExpenseCaseLink` 判断租户;旧 `FinanceDashboardService` 仍有全表读取和跨租户快照复用风险,不能作为客户 ROI 的事实源。
|
||||
- 现有 `accept_standard_adjustment` 优先使用客户端传入的原金额,客户端理论上可以放大差额;任何节省计算必须改为只使用服务端锁定的明细金额和政策计算快照。
|
||||
- 当前“已付款”是财务人员在系统内确认的业务状态,尚没有银行流水、支付回执或 ERP 凭证。因此付款只能把机会推进到“实际结果待确认”,不能直接进入财务确认 KPI。
|
||||
- 报销提交时间到同租户首个 `payment_completed` 业务事件可以形成可审计的端到端流程周期,但它包含等待与系统处理,不是人工活跃工时;在没有人工计时基线、角色成本和客户认可释放比例前,工时价值必须显示“待采集”,不能伪造为 0 或现金节省。
|
||||
|
||||
本方案是 `2026-07-13/feature/ai-expense-closed-loop-and-value-proof` 中 P2“费用经营与价值证明”的实施拆分。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- [G1] 建立租户安全的 Savings Ledger,完整区分风险暴露、预计机会、执行中、实际结果、财务确认和冲回。
|
||||
- [G2] 建立不可变基线和证据链,所有金额都能追溯到费用事件、单据明细、政策版本、执行动作、付款事件和确认人。
|
||||
- [G3] 建立经济收益去重键和归因约束,避免同一单据、付款义务或政策差额被多个风险重复计入。
|
||||
- [G4] 建立严格状态机、乐观版本、幂等响应和数据库并发约束,防止重复确认、陈旧操作和跨租户访问。
|
||||
- [G5] 建立 CFO 价值看板,分开展示财务确认现金节省、可释放工时价值、安全智能直通率、经营漏斗和风险护栏。
|
||||
- [G6] 支持部门、项目、费用类型、供应商、城市、时间、负责人、来源和单据下钻,并显示数据覆盖、口径和新鲜度。
|
||||
- [G7] 持久化费用基线快照,记录窗口、样本量、算法版本、政策版本、来源指纹和数据质量。
|
||||
- [G8] 修复旧财务聚合与快照的租户边界,禁止真实接口失败时回退成看似真实的演示数字。
|
||||
|
||||
### 非目标
|
||||
|
||||
- [NG1] 不把风险关联金额、暂缓付款金额、未采纳建议、未使用预算或预计金额计入已确认节省。
|
||||
- [NG2] 首个切片不宣称已具备外部银行或 ERP 付款凭证;后续通过连接器补齐。
|
||||
- [NG3] 首个切片不使用缺少租户、合同价、采购数量和付款凭证的 `AccountsPayableRecord` 计算供应商节省。
|
||||
- [NG4] 不把申请金额与最终报销差额默认归因给 AI;缺少具体 AI 决策、采纳动作和结果链时,AI 归因金额为 0。
|
||||
- [NG5] 不把流程经过时长换算为人工工时,不直接暴露个人薪酬或个人成本。
|
||||
- [NG6] 不跨币种直接求和;没有锁定汇率的金额只按原币展示并进入数据质量提醒。
|
||||
- [NG7] 不删除、覆盖已确认收益;补付、退款、申诉或归因修正使用追加负向冲回事件。
|
||||
- [NG8] 不在本阶段重写整个 Overview,也不把不可信的预算中心模拟数据接入价值看板。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
### 目标用户
|
||||
|
||||
1. CFO/管理层:查看企业已经确认的现金价值、价值兑现速度和风险护栏。
|
||||
2. 财务运营:复核机会、实际结果、重复归因、凭证和冲回事项。
|
||||
3. 费用治理负责人:领取机会、执行动作、补充结果和跟进逾期。
|
||||
4. 预算负责人:只在授权部门或成本中心范围内查看机会与驱动。
|
||||
5. 审计/风控:回放基线、政策、执行、付款、确认、冲回和操作事件。
|
||||
6. 普通员工:仅在自己的费用事件中看到与本人相关的调整说明,不访问企业 CFO 汇总。
|
||||
|
||||
### 核心场景
|
||||
|
||||
1. 员工接受住宿职级标准重算。服务端锁定明细原金额、城市、天数、职级、政策版本和可报销上限,同事务生成唯一节省机会。
|
||||
2. 机会进入执行后仍只展示预计金额;审批未通过、单据取消或超过期限时保留失败/到期事实,不从兑现率分母中消失。
|
||||
3. 单据完成付款业务事件后,系统根据冻结差额记录实际结果,但不进入财务确认 KPI。
|
||||
4. 与机会负责人和结果填报人不同的财务人员检查证据、去重、币种和成本后确认;确认后才计入 CFO 现金节省。
|
||||
5. 后续发生例外补付或申诉时,追加负向冲回并保留原确认,历史月报按报告 `as_of` 可回放。
|
||||
6. CFO 从价值总览下钻到部门、项目、费用类型、城市、负责人和具体单据,查看基线、建议、执行、实际、确认人和证据。
|
||||
7. 费用治理负责人查看异常集中维度和只读政策模拟准备项;历史中位数只能作为异常信号,缺少正式政策反事实时不显示预计节省,也不自动创建机会。
|
||||
|
||||
### 异常场景
|
||||
|
||||
- 服务端政策无法计算、明细金额缺失或差额不为正:原报销流程可继续,但不创建可货币化节省机会。
|
||||
- Claim 没有合法 `ExpenseCaseLink` 或租户不一致:fail-closed,不自动归入默认租户。
|
||||
- 相同请求重复发送:返回首次不可变响应;相同请求 ID 内容不同:409 拒绝。
|
||||
- 陈旧版本、重复付款事件或重复经济收益:通过版本锁、事件唯一键和收益去重键拒绝。
|
||||
- 缺少付款/凭证、汇率、独立确认或证据不完整:停留在实际待确认,不进入主 KPI。
|
||||
- 看板接口失败、无权限、无数据、基线不足或快照过期:分别展示明确状态,绝不使用模拟数字伪装真实指标。
|
||||
|
||||
## 功能能力
|
||||
|
||||
- [C1] 机会发现:从服务端核验的政策调整、后续分析洞察或风险复核创建机会。
|
||||
- [C2] 状态管理:支持 identified、accepted、in_progress、realized、verified、rejected、expired 和 reversed 事实。
|
||||
- [C3] 实现记录:保存实际毛收益、新增执行成本、净收益、发生时间、币种和结果证据。
|
||||
- [C4] 财务确认:独立确认人复核去重、证据、汇率、成本和归因后签字。
|
||||
- [C5] 证据与审计:只追加事件、内容指纹、before/after、首次响应和 correlation 全链路回放。
|
||||
- [C6] 基线快照:按员工、部门、费用类型、供应商、城市、项目和流程持久化窗口、样本量和版本。
|
||||
- [C7] 价值分析:经营漏斗、兑现率、周期、逾期、来源、责任人和数据质量。
|
||||
- [C8] CFO 看板:真实指标、全局筛选、URL 恢复、下钻、移动端和口径抽屉。
|
||||
- [C9] 安全边界:租户、角色、数据范围、自证禁止、管理员业务权限分离和字段白名单。
|
||||
- [C10] 冲回能力:补付、退款、申诉或归因修正只能追加负向记录,不改历史。
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 前端
|
||||
|
||||
- 在现有“分析看板”增加 `value` / “经营价值看板”,复用统一时间筛选,不新增一级导航。
|
||||
- `OverviewView.vue` 只负责挂载独立 `CfoValueDashboard.vue`;价值加载、筛选和展示模型拆到 `useCfoValueDashboard.js`、`cfoValueDashboardModel.js` 与 `analyticsValue.js`,避免继续扩大接近 800 行的 `useOverviewView.js`。
|
||||
- 默认视图从上到下为:主 KPI 与护栏、价值漏斗、现金节省趋势、来源/组织驱动、机会执行表、数据质量和口径说明。
|
||||
- 全局筛选只保留时间、部门、费用类型和价值类型;项目、供应商、城市、负责人、状态和置信度进入高级筛选。
|
||||
- 看板状态同步到 URL query;当前机会使用 `value_opportunity` 保存,刷新、浏览器前进/后退和分享链接能够恢复同一抽屉。非法 ID、403/404、跨租户不可见或不再符合当前筛选/时间窗口的机会会 fail-closed 清理,避免残留上一租户详情。
|
||||
- 机会详情展示基线、建议、执行、实际结果、财务确认、去重与证据时间线;证据只使用服务端可见性 DTO。来源动作由独立 helper 根据 Claim、Expense Case、AI Decision、维度和 Evidence Resource 构造,不在抽屉组件内拼接路由规则。
|
||||
- 单据来源进入 `app-document-detail`;风险来源优先进入关联单据,并只携带风险 focus、观察/决策 ID 与现有锚点。详情返回动作恢复 `dashboard=value`、时间窗口和 `value_*` 查询。
|
||||
- 预算来源进入 `app-budget` 的“预算配置视图”,按授权范围应用部门和费用类型焦点;页面明确说明配置、阈值及当前演示金额不是该机会的真实预算事实。未配置的费用科目显示“未找到配置”,不得解释为预算为零。
|
||||
- 部门、项目、费用类型、供应商、城市、负责人和来源维度可返回 CFO 看板相应筛选;切换维度时移除旧机会 ID,避免筛选与抽屉详情不一致。
|
||||
- 实际结果登记必须具备真实付款事件或可追溯外部凭证;外部凭证上传/连接器尚未接入时,前端隐藏无证据手工登记并解释下一步,不发送必然失败或可能污染价值账本的空证据请求。
|
||||
- 真实为 0、无数据、基线不足、无权限、接口失败、部分数据和快照过期使用不同状态。
|
||||
- 禁止复用 `data/metrics.js`、`BudgetCenterView` 静态种子或遗留 `demoTotals` 作为 CFO 真实回退。
|
||||
|
||||
### 后端
|
||||
|
||||
- `SavingsDiscoveryService` 只负责从可信业务事实发现/创建机会,不提交事务。
|
||||
- `SavingsActionService` 负责机会状态动作、版本、权限、幂等和事件。
|
||||
- `SavingsRealizationService` 负责付款后实际结果、财务确认、拒绝和冲回。
|
||||
- `SavingsQueryService` 负责租户安全分页、详情和可见动作投影。
|
||||
- `SavingsFactScopeReader` 只读取当前租户与授权部门中的已归档报销事实,并以报告窗口和 `as_of` 排除未来单据、修改和完成事件。
|
||||
- `SavingsBaselineGenerationService` 分开冻结金额中位数与流程历时中位数;流程只使用提交时间和首个付款完成业务事件,指标固定为 elapsed minutes。
|
||||
- `SavingsAnomalyAttributionAnalyzer` 只生成描述性异常集中归因和政策模拟准备项,不声称因果,不写 `SavingsOpportunity`。
|
||||
- `CfoValueAnalyticsService` 只从 Savings Ledger、风险事实和明确资格快照聚合,不从 UI mock 或风险金额推导节省。
|
||||
- 标准重算只使用数据库行锁中的 `ExpenseClaimItem.item_amount` 作为原金额;客户端原金额和可报销金额只可作为展示输入,不能成为节省事实。
|
||||
- 标准重算在同一事务内写 Claim 调整、机会、证据、Savings 事件和 `saving_opportunity_created` 业务事件;API 边界统一提交。
|
||||
- 付款动作在 Claim → Opportunity 的固定锁顺序中创建 actual realization 和 `saving_action_completed`,与 `payment_completed` 同事务。
|
||||
- 财务确认写 `saving_confirmed`,拒绝和冲回写对应只追加事件;相同请求安全重放。
|
||||
- 旧 `/analytics/finance-dashboard` 必须接收可信 `CurrentUserContext`,Claim 通过 `ExpenseCaseLink` 限定租户;快照键至少包含租户与数据权限范围,后台任务必须显式指定租户。
|
||||
|
||||
### 算法与规则
|
||||
|
||||
#### 第一条可信机会
|
||||
|
||||
```text
|
||||
server_original_amount = locked ExpenseClaimItem.item_amount
|
||||
policy_target_amount = server policy calculator result
|
||||
estimated_net_saving = max(0, server_original_amount - policy_target_amount)
|
||||
```
|
||||
|
||||
- 仅当政策计算成功、输入快照完整、币种一致、差额大于 0 时创建可货币化机会。
|
||||
- 机会唯一键首期为 `tenant + claim + item + policy_version + policy_input_fingerprint`。
|
||||
- 接受重算表示建议已采纳,机会进入 `in_progress`;付款完成后进入 `realized`,独立财务确认后进入 `verified`。
|
||||
- 员工自行承担差额同时是员工体验护栏,必须跟踪申诉和例外补付率,防止通过不合理转嫁美化节省。
|
||||
|
||||
#### 流程基线、异常归因与政策模拟
|
||||
|
||||
```text
|
||||
workflow_elapsed_minutes
|
||||
= first_tenant_payment_completed_event.occurred_at - claim.submitted_at
|
||||
```
|
||||
|
||||
- 流程窗口按首个付款完成事件归属;提交时间缺失、完成早于提交、跨租户事件、`as_of` 之后完成或截止后被修改的单据全部排除。
|
||||
- 流程快照使用 `median_submission_to_payment_elapsed_minutes`、`minutes` 单位、独立算法版本和来源指纹;证据元数据固定声明 `elapsed_cycle_not_active_labor`。
|
||||
- 异常归因按部门、费用类型、城市和项目聚合质量合格的历史偏离候选,只表示异常集中度,不表示该维度导致支出。
|
||||
- 政策模拟候选只输出版本化政策、生效期、适用范围、限额和例外规则等必需输入;历史中位数不是政策反事实,缺少正式反事实时 `estimated_savings=None`。
|
||||
- 预算预测复用现有预算分配和核销事实,以 `min(as_of, window_end)` 为截止点;旧预算表没有租户字段时仅允许 default 租户,部门权限优先按稳定部门 ID 收紧。
|
||||
- 供应商缺少核验 ID、数量和单位价格时继续返回 unavailable,不读取 `AccountsPayableRecord` 演示或应付种子。
|
||||
|
||||
#### 收益去重
|
||||
|
||||
- `benefit_key` 表达同一个经济结果,不表达同一个风险观察。
|
||||
- 多条风险可指向一个机会;同一发票、付款义务、报销明细或价格变化只能有一个 canonical 确认收益。
|
||||
- 同一收益多个动作的归因比例之和不得超过 1。
|
||||
- 确认后大额、超预计、手工基线、缺外部凭证和归因异常进入二次复核或数据质量队列。
|
||||
|
||||
#### 状态转换
|
||||
|
||||
```text
|
||||
identified -> accepted -> in_progress -> realized -> verified -> reversed
|
||||
| | | |
|
||||
+-------- rejected -------+------------+
|
||||
+-------- expired --------+
|
||||
```
|
||||
|
||||
- `identified`:冻结基线、方法、价值类型、币种、预计净值、负责人、截止时间、去重键和来源证据。
|
||||
- `accepted`:负责人明确采纳。
|
||||
- `in_progress`:保存执行动作、执行人、开始时间和动作证据;预计值不得静默上调。
|
||||
- `realized`:保存实际结果、净值、发生时间、归因和付款/结果证据,但不计主 KPI。
|
||||
- `verified`:完成去重、币种、成本、证据和独立财务确认。
|
||||
- `reversed`:追加负向冲回,原确认不可删除。
|
||||
- `rejected/expired`:保留失败事实,防止只保留成功机会美化兑现率。
|
||||
|
||||
### 数据与契约
|
||||
|
||||
#### `profile_baseline_snapshots`
|
||||
|
||||
- 租户、基线类型、稳定维度 ID、指标、单位和原币。
|
||||
- 基线值、窗口开始/结束、样本量、方法、查询指纹和数据质量。
|
||||
- 算法版本、政策版本、冻结时间/人和有效期。
|
||||
- 历史群组基线强制窗口与样本量;政策反事实基线强制政策版本、生效区间和目标明细。
|
||||
- 金额基线按员工、部门、费用类型、城市和项目分组;流程基线按稳定流程键分组,使用独立 metric/unit,不能与币种金额比较或求和。
|
||||
|
||||
#### `savings_opportunities`
|
||||
|
||||
- 租户、费用事件、Claim 软引用、来源类型/ID、类别和价值类型。
|
||||
- 风险暴露只作护栏;基线、目标、预计毛收益、预计成本、预计净收益和区间分开保存。
|
||||
- 原币、报告币、负责人、截止时间、状态、版本、`benefit_key` 和去重组。
|
||||
- 部门、项目、费用类型、供应商、城市和流程维度使用明确快照字段或受控 JSON。
|
||||
- 唯一约束至少覆盖 `(tenant_id, opportunity_key)`。
|
||||
|
||||
#### `savings_realizations`
|
||||
|
||||
- 租户、机会、费用事件、Claim、BusinessEvent 和实际发生时间。
|
||||
- 实际毛收益、新增执行成本、实际净收益、原币、报告金额和汇率快照。
|
||||
- 归因方法/比例、`benefit_key`、去重状态和 canonical realization。
|
||||
- 财务确认/拒绝/冲回人、时间、说明和证据。
|
||||
- 只追加金额事实;确认投影可更新,但每次变更必须有不可变事件。
|
||||
|
||||
#### `savings_evidence_links` 与 `savings_events`
|
||||
|
||||
- 证据保存实体、证据角色、资源类型/ID、来源系统、外部事件 ID、内容哈希、发生/采集时间和验证状态。
|
||||
- 事件保存动作、请求 ID、操作人、版本、指纹、before/after、首次响应、correlation 和时间。
|
||||
- PostgreSQL 触发器禁止修改或删除 `savings_events`。
|
||||
|
||||
### 权限
|
||||
|
||||
- `finance`、`executive` 可读取租户 CFO 汇总;预算负责人仅看被授权部门/成本中心。
|
||||
- 普通员工、普通经理不得读取 CFO 汇总;只能看到本人费用事件中的最小调整说明。
|
||||
- 机会接受、拒绝和指派需要 finance/executive 或明确负责人权限。
|
||||
- 财务确认必须是 finance/executive,且不能是机会负责人或实际结果填报人。
|
||||
- 只有 `admin` 而没有财务角色时允许运维只读,不允许业务确认。
|
||||
- 所有 API、聚合、快照、导出和后台任务强制 `tenant_id` 与数据范围;不允许默认全表扫描。
|
||||
- Claim 通过 `ExpenseCaseLink` 校验租户;缺 Link 的非默认历史数据不自动猜测归属。
|
||||
|
||||
### 降级策略
|
||||
|
||||
- 政策或基线服务失败:不创建货币化机会,原报销主流程保留人工处理。
|
||||
- 外部付款/ERP 连接器未接入:付款业务事件只能推进到 realized,必须人工财务确认。
|
||||
- 汇率缺失:保留原币明细,不进入跨币种总计。
|
||||
- 工时基线缺失:显示“待采集”,不显示 0,不计扩展 ROI。
|
||||
- 流程历时可用但活跃工时缺失:只展示 elapsed cycle 驱动指标,CFO 工时价值仍保持 collecting。
|
||||
- CFO 聚合失败:显示错误和重试,不加载演示值;旧财务支出看板独立可用。
|
||||
- 快照过期:展示过期提示并触发受控刷新,不能跨租户复用旧快照。
|
||||
|
||||
## 算法与公式
|
||||
|
||||
### 主 KPI 1:财务确认净现金节省
|
||||
|
||||
```text
|
||||
verified_net_cash_savings
|
||||
= sum(actual_gross_saving - incremental_execution_cost + reversal_amount)
|
||||
where value_kind = cash
|
||||
and confirmation_status = finance_confirmed
|
||||
and dedupe_status = canonical
|
||||
and confirmed_at <= report_as_of
|
||||
```
|
||||
|
||||
- 按 `realized_at` 归属业务期间,按 `confirmed_at` 和报告 `as_of` 保证历史可回放。
|
||||
- 风险暴露、预计金额、执行中金额和未确认实际金额不得进入。
|
||||
- 多币种只有存在锁定汇率时才折算;否则按原币分组。
|
||||
|
||||
### 主 KPI 2:财务确认可释放工时价值
|
||||
|
||||
```text
|
||||
verified_releasable_labor_value
|
||||
= max(0, baseline_active_minutes_per_unit - actual_active_minutes_per_unit)
|
||||
* eligible_units
|
||||
* approved_role_cost_per_minute
|
||||
* approved_releasable_ratio
|
||||
```
|
||||
|
||||
- 现金与工时分账、分卡、分报告,默认不相加。
|
||||
- 缺少上线前后活跃分钟、角色完全成本、生效期或客户认可释放比例时不可计算。
|
||||
|
||||
### 主 KPI 3:安全智能直通率
|
||||
|
||||
```text
|
||||
safe_straight_through_rate
|
||||
= qualified_completed_cases_without_manual_correction_or_return
|
||||
and no_major_post_audit_issue
|
||||
/ eligible_completed_cases_frozen_at_creation
|
||||
```
|
||||
|
||||
- 必须保存 eligibility 快照、策略版本、必要审批完成和事后抽检结果。
|
||||
|
||||
### 驱动指标
|
||||
|
||||
- 现金兑现率:同一成熟机会队列的财务确认净现金 / 冻结预计净现金。
|
||||
- 机会到财务确认 P50 天数、逾期负责人占比。
|
||||
- 提交至首个付款完成的端到端 P50 elapsed minutes;它与每单人工活跃分钟分开,后者在采集前保持不可用。
|
||||
- 人工触点、首次提交完整率和 AI 字段采纳率。
|
||||
|
||||
### 风险护栏
|
||||
|
||||
- 开放且已确认的高危/重大风险暴露,按单据或经济义务去重;它不是节省。
|
||||
- 重大风险漏检率、事后审计重大问题率、误报率和人工覆盖率。
|
||||
- 财务确认后冲回率、实际超过预计异常率、去重待复核金额和证据不完整金额。
|
||||
- 标准重算员工申诉/例外补付率。
|
||||
|
||||
## 测试方案
|
||||
|
||||
### 后端
|
||||
|
||||
- 状态机合法/非法转换、确认人独立性、管理员只读和角色权限。
|
||||
- 机会/实现/确认/冲回幂等、请求内容冲突、陈旧版本和租户隔离。
|
||||
- 服务端明细金额锁定、客户端放大原金额无效、政策快照完整性。
|
||||
- 付款事件重复、经济收益去重、跨币种、成本扣除、冲回和归因上限。
|
||||
- 看板按租户、时间、部门、项目、费用类型、城市、来源和负责人聚合对账。
|
||||
- 六维基线验证员工、部门、费用类型、城市、项目和流程的窗口、样本量、算法版本、来源指纹、租户/部门范围与稳定重放。
|
||||
- 洞察验证预算截止点、描述性归因、政策模拟准备项、供应商 unavailable、所有无反事实候选 `estimated_savings=None` 且不会创建机会。
|
||||
- 旧财务看板 Claim、预算和缓存键租户隔离回归。
|
||||
- Alembic 空库升级、重复升级、约束、append-only 触发器、无损降级和 PostgreSQL 并发。
|
||||
|
||||
### 前端
|
||||
|
||||
- API snake/camel 归一化、部分数据和过期快照。
|
||||
- verified、realized、estimated 和 risk exposure 严格分区,不得混算。
|
||||
- loading/error/empty/partial/stale/permission-denied/baseline-missing 状态。
|
||||
- 时间、部门、费用类型、价值类型筛选与 URL 恢复。
|
||||
- 机会详情证据链、可用动作、版本冲突、幂等重放和确认反馈。
|
||||
- 单据、风险、预算和维度下钻参数。
|
||||
- 机会 `value_opportunity` 的恢复、关闭清理、非法格式、403/404、跨筛选和时间窗口清理。
|
||||
- 风险来源最小定位参数、单据返回 CFO、预算配置焦点和“非真实预算金额”口径。
|
||||
- 响应式、键盘操作、44px 触控目标和生产构建。
|
||||
|
||||
### 集成
|
||||
|
||||
- 住宿超标准 → 服务端重算 → 机会 → 审批 → 付款 → actual realization → 独立财务确认 → CFO 看板。
|
||||
- 相同重算/付款/确认并发只产生一个经济收益和一条对应版本事件。
|
||||
- 确认后补付/申诉 → 负向冲回 → 历史报告 `as_of` 可回放。
|
||||
- 多租户同单号、同员工名、相同 request ID 和快照缓存隔离。
|
||||
|
||||
### 容器验证
|
||||
|
||||
所有 pytest、Alembic、PostgreSQL 并发、前端测试和构建必须在 `local-x-financial-linux` 容器内完成,单条命令超时不超过 60 秒。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
- [A1] 任一 verified cash saving 可追溯到费用事件、明细原金额、政策快照、执行动作、付款事件、确认人、去重键和证据。
|
||||
- [A2] 风险暴露、预计、执行中、实际待确认、财务确认和冲回在 API、数据库和 UI 中均不混算。
|
||||
- [A3] 客户端伪造原金额、跨租户访问、陈旧版本、自证确认和重复经济收益被服务端拒绝。
|
||||
- [A4] CFO 三个主 KPI 有口径、时间窗口、来源、新鲜度、数据覆盖和护栏;缺数据时明确“待采集”。
|
||||
- [A4.1] 流程 elapsed cycle 有独立 metric/unit/算法版本/来源指纹,且不会进入工时价值或现金节省。
|
||||
- [A5] 价值看板支持部门、项目、费用类型、供应商、城市、时间、负责人、来源和单据下钻。
|
||||
- [A6] 真实接口失败不出现演示数字;零值、无数据、无权限、错误和过期可区分。
|
||||
- [A7] 迁移在一次性 PostgreSQL 空库完成升级、重复升级、约束验证和安全降级边界测试。
|
||||
- [A8] 相关后端、前端、Ruff、构建、端到端和并发测试全部在容器内通过。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
### 风险
|
||||
|
||||
- 员工承担差额不一定等同企业创造价值;需要跟踪申诉、补付和政策公平性,避免激励扭曲。
|
||||
- 当前付款是人工状态,不是外部现金事实;确认页必须清晰披露证据等级。
|
||||
- 旧 Claim/预算缺租户列,读取必须经过 Case Link 或正式迁移,不能依赖默认租户猜测。
|
||||
- 数据稀疏且包含模拟种子,试点目标值必须在真实基线采集后冻结。
|
||||
- 当前预算中心仍是配置演示视图;CFO 来源入口只用于定位部门/费用类型配置,页面和价值计算均不得把其中金额当成预算事实或节省事实。
|
||||
- 多币种、分摊收益和跨期冲回会显著增加财务口径复杂度,必须保留原始事实和版本。
|
||||
- 工时价值若没有活跃时间采集与客户认可成本率,容易被夸大,因此默认不计现金 ROI。
|
||||
- 历史异常集中不是因果,历史中位数也不是政策反事实;模拟候选必须在正式政策版本和客户确认适用口径补齐后才可货币化。
|
||||
|
||||
### 已处理决策
|
||||
|
||||
- 首条闭环选择“住宿职级标准重算”,不选择缺少付款事实的重复支付阻止。
|
||||
- 价值看板进入现有分析看板,独立拆组件和 composable。
|
||||
- 只设 3 个主 KPI,其余作为驱动和护栏;现金与工时分开披露。
|
||||
- Savings Ledger 直接带结构化租户键,不复用旧财务快照作为价值事实源。
|
||||
- 流程基线采用提交至首个付款完成的端到端 elapsed cycle;人工活跃工时继续作为独立待采集事实。
|
||||
- 异常归因仅做描述性集中度,政策模拟候选保持只读,不自动写入节省机会。
|
||||
|
||||
### 待后续真实客户确认
|
||||
|
||||
- 独立财务确认是否要求双人复核及大额阈值。
|
||||
- 报告币、汇率来源和月末汇率锁定规则。
|
||||
- 工时价值是否进入扩展 ROI、角色成本口径和可释放比例。
|
||||
- 标准重算差额在客户会计政策中属于现金节省、成本避免还是员工自担调整。
|
||||
- 试点 30/90 天目标、CFO 月报签字人和节省分成合同边界。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-16:完成现有财务、预算、风险、付款、标准重算、前端入口与 KPI 口径盘点;确认旧财务聚合租户边界和客户端原金额信任问题。
|
||||
- 2026-07-16:冻结 Savings Ledger、CFO KPI、状态机、权限、去重、证据和首条纵向闭环方案;尚未完成的代码与验证全部保留在同目录 TODO 中继续执行。
|
||||
- 2026-07-16:完成 Savings Ledger 五表与 0015 迁移、租户/权限/幂等/并发/证据/冲回契约,并把住宿标准重算和付款动作接入同事务价值链。
|
||||
- 2026-07-16:完成独立财务确认、证据复核留痕、canonical 收益去重、负向冲回、`as_of` 回放和历史标准调整安全回填。
|
||||
- 2026-07-16:完成 CFO 价值分析 API;只将已确认 canonical 现金结果计入主 KPI,多币种分组,工时、安全直通率和审计事实缺口显式披露,不使用演示数据回退。
|
||||
- 2026-07-16:完成 CFO 经营价值前端入口、URL 筛选恢复、主 KPI、漏斗、趋势、驱动维度、护栏、数据质量、机会证据链与响应式状态;详情明确展示财务确认人、时间和说明。
|
||||
- 2026-07-16:收紧无证据手工登记边界。真实付款事件仍可自动形成待确认实际结果;在支付/银行/ERP 凭证连接器接入前,页面不再提交空证据,并向用户解释必须先完成付款或等待外部回执。
|
||||
- 2026-07-16:完成租户隔离的员工、部门、费用类型、城市和项目五维历史中位数冻结基线,以及预算预测偏差、重复小额模式和历史中位数偏离候选;非政策反事实信号只披露暴露,不生成节省金额,并补齐 `as_of` 基线时间一致性门禁。
|
||||
- 2026-07-16:补齐第六维流程周期基线,以同租户首个付款完成业务事件冻结提交到完成的 elapsed minutes;窗口、样本量、算法版本、来源指纹、质量状态和证据完整保存,明确禁止推算人工活跃工时。
|
||||
- 2026-07-16:新增部门/费用类型/城市/项目异常集中归因和版本化政策模拟准备项;复用预算预测并收紧配置/交易截止点,所有缺少反事实的候选保持 `estimated_savings=None`、零机会副作用,供应商分析继续明确 unavailable。
|
||||
- 2026-07-16:完成机会抽屉 `value_opportunity` 深链、刷新/前进/后退恢复和非法/越权/跨筛选安全清理;来源动作可跳关联单据、风险证据位置、预算配置视图及 CFO 维度筛选,单据返回时恢复经营价值查询状态。
|
||||
- 2026-07-16:预算来源只应用授权部门与费用类型配置焦点;未覆盖科目显示“未找到配置”,所有入口和页面均明确演示预算金额不是当前机会的真实预算事实。
|
||||
@@ -0,0 +1,141 @@
|
||||
# 节省事实账本与 CFO 经营价值看板 开发 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 每条 TODO 必须回链 `CONCEPT.md` 的章节或语义段落。
|
||||
- 只有代码、接口或容器验证提供真实证据后才能勾选 `[x]`。
|
||||
- 现金节省、工时价值、风险暴露和预计机会始终分开,不以演示数据代替缺失事实。
|
||||
- 实施顺序为:安全前置 → 账本 → 首条闭环 → 基线/分析 → CFO 前端 → PostgreSQL/E2E → 文档收口。
|
||||
|
||||
## 1. 调研与边界
|
||||
|
||||
- [x] [CONCEPT: 背景与问题] 盘点财务看板、预算、Claim、Expense Case、Business Event、风险、审批、付款和标准重算事实源。
|
||||
证据:`finance_dashboard.py`、`finance_dashboard_snapshot.py`、`financial_record.py`、`expense_cases.py`、`expense_claim_standard_adjustment.py`、`expense_claim_approval_flow.py` 的只读审计。
|
||||
- [x] [CONCEPT: 目标与非目标] 确认风险金额、未用预算、预计机会和未确认结果不得进入已确认节省。
|
||||
证据:CONCEPT「目标与非目标」「算法与公式」。
|
||||
- [x] [CONCEPT: 用户与场景] 选择住宿职级标准重算作为首条纵向闭环,不选择证据不足的重复支付或供应商议价。
|
||||
证据:现有标准重算和付款业务事件可形成最短可信证据链;`AccountsPayableRecord` 已有租户字段,但仍缺合同价、数量、单位价格和外部付款事实。
|
||||
- [x] [CONCEPT: 风险与开放问题] 识别旧财务聚合全表读取、快照键缺租户、接口缺角色限制和标准重算信任客户端原金额问题。
|
||||
证据:`FinanceDashboardMetricMixin._fetch_claims()`、`_fetch_budget_allocations()`、`FinanceDashboardSnapshotService._cache_key()`、`accept_standard_adjustment()`。
|
||||
|
||||
## 2. 契约与设计
|
||||
|
||||
- [x] [CONCEPT: 数据与契约] 定义 baseline、opportunity、realization、evidence 和 append-only event 的职责与关键字段。
|
||||
证据:CONCEPT「数据与契约」。
|
||||
- [x] [CONCEPT: 算法与规则] 定义 identified → accepted → in_progress → realized → verified → reversed 状态和 rejected/expired 终态。
|
||||
证据:CONCEPT「状态转换」。
|
||||
- [x] [CONCEPT: 算法与规则] 定义 benefit key、canonical realization、归因比例和追加冲回规则。
|
||||
证据:CONCEPT「收益去重」。
|
||||
- [x] [CONCEPT: 权限] 定义租户、finance/executive、预算范围、管理员只读、自证禁止和数据范围。
|
||||
证据:CONCEPT「权限」。
|
||||
- [x] [CONCEPT: 算法与公式] 定义 3 个主 KPI、驱动指标、风险护栏和缺数据边界。
|
||||
证据:CONCEPT「算法与公式」。
|
||||
|
||||
## 3. 安全前置
|
||||
|
||||
- [x] [CONCEPT: 后端] 让旧财务看板显式接收可信租户与数据范围,Claim/预算查询不得全表混算。
|
||||
证据:`finance_dashboard_access_policy.py`、`finance_dashboard_scope.py`、`finance_dashboard_budget.py`、`finance_dashboard.py`;Claim 直接按结构化 `ExpenseClaim.tenant_id` 首层过滤,预算历史兼容仅限 default 租户。
|
||||
- [x] [CONCEPT: 后端] 给财务快照缓存键和后台任务加入租户/数据范围,禁止跨租户复用。
|
||||
证据:`finance_dashboard_snapshot.py`、`finance_dashboard_scheduler.py`;快照键包含 tenant 与 scope fingerprint,调度入口必须显式携带租户。
|
||||
- [x] [CONCEPT: 权限] 给财务与 CFO 分析接口增加后端角色和范围校验,管理员身份不自动获得业务确认权。
|
||||
证据:`finance_dashboard_access_policy.py`、`savings_access_policy.py`、`agent_run_access_policy.py`、`GET /api/v1/analytics/cfo-value`;容器租户/缓存/角色回归 17 项通过。
|
||||
- [x] [CONCEPT: 第一条可信机会] 标准重算只使用锁定数据库明细原金额,客户端原金额不能影响结果和节省。
|
||||
证据:`expense_claim_standard_adjustment.py`;原金额只读锁定 `ExpenseClaimItem.item_amount`,政策结果只由服务端重算,陈旧版本使用内容指纹并降低基线质量等级。
|
||||
- [x] [CONCEPT: 后端] 将标准重算、审计和账本写入收口到同一事务边界。
|
||||
证据:`expense_claim_standard_adjustment.py`以 `commit=False` 写审计,同一 API 事务写 Claim、Baseline、Opportunity、Evidence、SavingsEvent 和 BusinessEvent;事务失败整体回滚。
|
||||
|
||||
## 4. Savings Ledger 后端
|
||||
|
||||
- [x] [CONCEPT: 数据与契约] 新增 `profile_baseline_snapshots`、`savings_opportunities`、`savings_realizations`、`savings_evidence_links` 和 `savings_events` 模型。
|
||||
证据:`savings.py`(模型)及 `test_savings_models.py`;五类事实分离保存基线、机会、结果、证据和不可变操作。
|
||||
- [x] [CONCEPT: 数据与契约] 新增 Alembic 0015、迁移所有权、复合租户外键、唯一键、检查约束、索引和 append-only 触发器。
|
||||
证据:`20260716_0015_savings_value_ledger.py`、`migration_preflight.py`、`schema_ownership.py`;一次性 PostgreSQL 17 空库完整迁移循环通过。
|
||||
- [x] [CONCEPT: 后端] 实现 discovery、action、realization、query、access policy 和 response builder 独立服务。
|
||||
证据:`savings_discovery.py`、`savings_actions.py`、`savings_realization.py`、`savings_query.py`、`savings_access_policy.py`、`savings_read_projection.py`、`savings_protocol.py`。
|
||||
- [x] [CONCEPT: 后端] 实现分页、筛选、排序、详情、可用动作、证据和事件 DTO。
|
||||
证据:`GET /api/v1/savings/opportunities`、`GET /api/v1/savings/opportunities/{id}`、`savings.py`(schema/API)、`test_savings_endpoints.py`。
|
||||
- [x] [CONCEPT: 算法与规则] 实现版本锁、请求指纹、不可变响应重放、收益去重和归因比例约束。
|
||||
证据:`SavingsRequestProtocol`、数据库唯一/检查约束与 `test_savings_concurrency_postgres.py`;同 request 并发仅一次写入,同 benefit 双确认仅一个 canonical winner。
|
||||
- [x] [CONCEPT: 状态转换] 实现 accept/start/record/verify/reject/expire/reverse 合法与非法转换。
|
||||
证据:`savings_actions.py`、`savings_realization.py`、`test_savings_ledger_services.py`;负向 reversal 只追加,不改写原已确认金额。
|
||||
- [x] [CONCEPT: 证据与审计] 把 Savings 关键动作写入同 Case 的 `BusinessEvent` 并保留 correlation/causation。
|
||||
证据:`ExpenseCaseService` 资源链接与 `savings_*` 服务;端到端测试核验 opportunity/payment/action/confirm/reverse 事件同 Case 可回放。
|
||||
- [x] [CONCEPT: 后端] 新增历史标准重算机会 dry-run/apply 回填脚本;缺 Case/租户/政策证据只输出数据质量报告。
|
||||
证据:`savings_standard_adjustment_backfill.py`、`backfill_standard_adjustment_savings.py`;显式租户/目标库、指纹重算、批次锁、稳定键重放,容器直接测试 8 项通过。
|
||||
|
||||
## 5. 首条真实纵向闭环
|
||||
|
||||
- [x] [CONCEPT: 第一条可信机会] 接受住宿标准重算时冻结服务端原金额、政策输入/版本、目标金额、差额、维度和证据。
|
||||
证据:`SavingsDiscoveryService`冻结 `ProfileBaselineSnapshot` 与内容指纹,政式发布版本为 complete,内容指纹版本显式标记 partial。
|
||||
- [x] [CONCEPT: 第一条可信机会] 同事务以稳定 opportunity key 创建或重放机会,并进入 in_progress。
|
||||
证据:稳定键由 tenant + claim + item + policy version + calculation fingerprint 生成;重试返回原机会。
|
||||
- [x] [CONCEPT: 后端] 付款业务事件后同事务创建 actual realization,重复付款事件不重复计入。
|
||||
证据:`expense_claim_approval_flow.py`调用 `realize_paid_claim()`;PostgreSQL 同付款事件并发结果为 `[0, 1]`,仅一条 realization 和一条完成事件。
|
||||
- [x] [CONCEPT: 财务确认] 实现独立财务 verify/reject,未确认实际结果不得进入主 KPI。
|
||||
证据:所有人工实际结果必须至少一条可追溯证据;独立确认人会固化证据复核人/时间,pending 结果在 CFO 口径中为 0。
|
||||
- [x] [CONCEPT: 冲回能力] 实现补付/申诉/归因修正的负向 reversal,原确认不可删除。
|
||||
证据:原 actual 保留 `finance_confirmed`,新增负向 canonical reversal;`as_of` 可回放冲回前结果。
|
||||
- [x] [CONCEPT: 权限] 验证申请人、机会负责人、结果填报人、纯管理员和跨租户用户不能自证或越权。
|
||||
证据:`SavingsAccessPolicy`与 PostgreSQL 并发安全测试;owner/recorder/admin-only 自证拒绝,独立 finance 可确认,跨租户无副作用。
|
||||
|
||||
## 6. 费用基线与经营分析
|
||||
|
||||
- [x] [CONCEPT: 基线快照] 按员工、部门、费用类型、城市、项目和流程从租户安全真实数据生成基线窗口、样本量和版本。
|
||||
证据:`savings_fact_scope.py`、`savings_baseline_generation.py`、`savings_insights.py`;金额五维使用已归档明细中位数,流程维度只使用同租户报销提交时间与首个 `payment_completed` 业务事件,冻结 elapsed minutes、窗口、样本量、算法版本、查询指纹、质量、证据和审计事件;`source_workflow_cycle_count` 明确披露覆盖,活跃工时保持 unavailable。
|
||||
- [x] [CONCEPT: 基线快照] 供应商数据缺少租户/合同事实时显示 coverage gap,不从应付模拟种子生成可信基线。
|
||||
证据:基线和洞察 API 均返回 `supplier_dimension_unavailable` / `supplier_price_drift_unavailable`,要求核验供应商、数量和单位价格;不会读取 `AccountsPayableRecord` 模拟事实或创建货币化机会。
|
||||
- [x] [CONCEPT: 算法与规则] 实现预算预测、描述性异常归因、重复小额浪费、历史偏离和只读政策模拟准备项;证据不足时不自动货币化。
|
||||
证据:`savings_insight_budget.py` 复用预算配置/核销事实并以 `min(as_of, window_end)` 截止;`savings_insight_analysis.py`、`savings_insight_attribution.py` 输出历史偏离、部门/费用类型/城市/项目异常集中和版本化政策模拟必需输入。所有缺少正式反事实的候选 `estimated_savings=None`、`created_opportunity_ids=[]`,稳定重放不写 `SavingsOpportunity`。
|
||||
- [x] [CONCEPT: 后端] 实现 `GET /analytics/cfo-value`,统一时间、维度、币种、状态和 `as_of` 口径。
|
||||
证据:`cfo_value.py`(API/schema)、`cfo_value_analytics.py`;租户、角色、预算范围、时间和维度过滤均在服务端执行。
|
||||
- [x] [CONCEPT: 算法与公式] 实现确认现金、工时待采集、安全直通率资格、漏斗、兑现周期、逾期、来源和护栏聚合。
|
||||
证据:确认现金只求和 finance_confirmed + canonical + cash;工时和安全直通率显式返回 collecting;漏斗、趋势、驱动、逾期、冲回和数据质量不与主 KPI 混算。
|
||||
- [x] [CONCEPT: 降级策略] 多币种、工时、外部付款和审计结果缺失时返回明确数据质量状态,不伪造 0。
|
||||
证据:多币种按原币分组;无人工活跃时间/审计事实时返回 collecting/unavailable 和 coverage gap,不使用 mock 回退。
|
||||
|
||||
## 7. CFO 前端
|
||||
|
||||
- [x] [CONCEPT: 前端] 在分析看板新增 `value` 入口,并把 dashboard 状态同步 URL。
|
||||
证据:`useTopBarOverviewRange.js`、`AppShellRouteView.vue`、`OverviewView.vue`;`dashboard=value` 与价值筛选/页码写入 query,刷新和返回可恢复。
|
||||
- [x] [CONCEPT: 前端] 新增独立 CFO 组件、composable、API service 和展示模型,不继续扩大 `useOverviewView.js`。
|
||||
证据:`CfoValueDashboard.vue`、`CfoValueTrendChart.vue`、`CfoValueOpportunityDrawer.vue`、`CfoValueActionDialog.vue`、`useCfoValueDashboard.js`、`analyticsValue.js`、`cfoValueDashboardModel.js`;业务组件和状态职责已拆分,核心文件均低于 800 行。
|
||||
- [x] [CONCEPT: CFO 看板] 实现主 KPI、护栏、价值漏斗、趋势、来源/组织驱动、机会表和数据质量。
|
||||
证据:`CfoValueDashboard.vue` 与 `cfo-value-dashboard.css`;现金、工时和直通率分卡,趋势按币种切换,预计/实际/确认/冲回不混算,缺数据显式展示。
|
||||
- [x] [CONCEPT: 前端] 实现时间、部门、费用类型、价值类型筛选和项目/供应商/城市/负责人高级筛选。
|
||||
证据:顶部时间窗口与价值 query 联动,`createEmptyValueFilters`、`readValueFiltersFromQuery`、`writeValueFiltersToQuery` 和 API query 白名单覆盖全部筛选字段。
|
||||
- [x] [CONCEPT: 前端] 实现基线、建议、执行、实际、确认、去重和证据详情。
|
||||
证据:`CfoValueOpportunityDrawer.vue` 展示冻结基线、机会状态、实际净值、记录人、canonical 去重、财务确认人/时间/说明、证据索引和不可变事件;无可追溯凭证时不开放手工实际结果登记。
|
||||
- [x] [CONCEPT: 前端] 实现单据、风险、预算、维度下钻与返回状态恢复。
|
||||
证据:`cfoValueSourceLinks.js` 统一构造来源路由;Claim 进入 `app-document-detail`,风险携带最小 focus/观察/决策参数和现有锚点,预算进入 `app-budget` 配置视图并应用部门/费用类型焦点,维度返回 `app-overview?dashboard=value` 相应筛选。`useCfoValueDashboard.js` 以 `value_opportunity` 恢复抽屉,并在非法 ID、403/404、跨租户不可见或不符合当前筛选/时间窗口时安全清除;`useAppShell.js` 从单据详情恢复 CFO 查询。限制:预算中心仍是演示配置视图,页面明确金额不是当前机会的真实预算事实;未覆盖费用科目显示未配置而不是零预算。
|
||||
- [x] [CONCEPT: 降级策略] 区分零、无数据、基线不足、无权限、失败、部分数据和快照过期;删除 CFO 演示回退。
|
||||
证据:`classifyCfoDashboardState`、`buildValueKpis` 与页面状态区;接口失败不读取 `data/metrics.js` 或 demo/fallback 数字。
|
||||
- [x] [CONCEPT: 前端] 完成移动端、键盘、焦点、44px 触控和无障碍状态提示。
|
||||
证据:CFO 样式移动断点、44px 按钮、语义化 `label`/`role=alert`/`aria-live`、抽屉关闭标签及趋势表格降级;生产构建通过。
|
||||
|
||||
## 8. 测试与验证
|
||||
|
||||
- [x] [CONCEPT: 测试方案] 后端状态、金额、权限、租户、幂等、证据、去重、冲回和看板聚合单测通过。
|
||||
证据:容器组合回归 84 项通过;本轮 Savings/CFO 基线、洞察、端点、账本、回填与 E2E 组合 `34 passed, 6 skipped`,6 项为未配置 PostgreSQL 专用 URL 的预期跳过。
|
||||
- [x] [CONCEPT: 测试方案] 标准重算客户端金额伪造、政策失败、Case 缺失和事务回滚测试通过。
|
||||
证据:`test_expense_claim_service.py -k standard_adjustment` 8 项加付款集成 1 项通过,HTTP 标准重算 1 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 前端数据归一化、状态、筛选、URL、证据动作和响应式测试通过。
|
||||
证据:容器内 `cfo-value-dashboard.test.mjs` 14 项通过,新增机会 URL 恢复/清理、筛选上下文、风险/单据/预算/维度链接和预算非事实口径断言;与 App Shell 返回链、路由加载和筛选样式组合回归 37 项通过;带凭证序列化和无证据入口 fail-closed 均有断言。
|
||||
- [x] [CONCEPT: 测试方案] 一次性 PostgreSQL 空库迁移、重复升级、约束、append-only、无损降级和并发测试通过。
|
||||
证据:一次性 PostgreSQL 17 迁移循环通过;`test_savings_concurrency_postgres.py` 6 项通过,覆盖重放、canonical 竞态、跨租户、独立确认、付款单事实和 append-only DB 触发器。
|
||||
- [x] [CONCEPT: 集成] 住宿标准重算 → 机会 → 审批 → 付款 → 实际 → 财务确认 → CFO 看板 E2E 通过。
|
||||
证据:`test_savings_value_e2e.py` 从服务端政策差额、付款动作、待确认排除、独立财务确认到 CFO 金额对账单项通过。
|
||||
- [x] [CONCEPT: 集成] 确认后负向冲回和报告 `as_of` 回放 E2E 通过。
|
||||
证据:`test_savings_value_e2e.py`与 `test_cfo_value_analytics.py`同时验证当前净值归零和冲回前历史金额回放。
|
||||
- [x] [CONCEPT: 容器验证] 相关 pytest、Ruff、前端测试和生产构建均在 `local-x-financial-linux` 内通过。
|
||||
证据:Savings/CFO 相关切片与端到端均通过;fresh PostgreSQL 总探针 `87 passed / 0 skipped / 0 failed`,其中 Savings 并发 6 项;Web 全量 `815 passed / 0 failed` 与 Vite build 通过;新增 Python 文件 Ruff 和 `git diff --check` 通过。
|
||||
|
||||
## 9. 文档收尾
|
||||
|
||||
- [x] [CONCEPT: 指标与验收] 逐项核对 A1-A8,并把文件、接口、迁移、测试和运行结果写回证据。
|
||||
证据:A1 纵向闭环由 `test_savings_value_e2e.py`;A2-A4/A4.1 由 Savings schema、`cfo_value_analytics.py`、基线/分析测试;A5-A6 由 CFO 组件、来源下钻和前端状态测试;A7 由 0015 迁移与 PostgreSQL 并发;A8 由本节最终容器汇总证明。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 记录付款证据等级、汇率、双人复核、工时口径和试点目标的最终边界。
|
||||
证据:
|
||||
- [x] [CONCEPT: 本轮实现记录] 同步更新上位 `ai-expense-closed-loop-and-value-proof` 文档,不删除历史证据。
|
||||
证据:上位 TODO 已回填 Savings Ledger、状态机、CFO 看板、下钻、证据详情和 E2E;历史证据原样保留。
|
||||
@@ -0,0 +1,13 @@
|
||||
## 修复记录
|
||||
|
||||
- 13:08:修复 AgentAsset 全局读写、跨租户子记录注入、风险样本串租户、审核身份伪造和 ONLYOFFICE 匿名/可重放回调问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`;`origin/main` 无新提交,本地 `main` ahead 17,工作区包含多智能体并行未提交变更且未被清理或覆盖。
|
||||
- 本地 ahead 摘要:`242d68c3` 审批任务与豁免、`28b834ed` 审批动作幂等回放、`4940ebc4` 风险处置流程、`ee88a36b` 租户安全分层学习、`6bdf65bc` 权威预审、`ae3f02c3` 零录入票据关联、`54754b55` 个人报销记忆、`211f85d9` 已验证申请工作流、`5b246307` 申请预览决策、`a662cfe6` AI 反馈账本、`5ed34c2b` 历史申请回填、`11275e4b` 迁移 ownership 安全、`1347366b` 时间线与草稿事件安全、`22669a90` 统一费用时间线、`a616b30c` AI 申请事务、`653eda05` 不透明会话、`661990b2` 费用案例事务事件。
|
||||
- 修改:为 AgentAsset、Version、Review、TestRun 和 RuleFeedback 增加结构化 `tenant_id + scope`;所有资产、发布、监控、召回、遥测、调度、foundation 和风险运行时查询接入企业/平台作用域;同编码按企业优先、平台回退解析,跨企业资源统一不可见,平台资产仅平台管理员可写。
|
||||
- 修改:真实风险场景强制显式目标企业并先按 `ExpenseClaim.tenant_id` 过滤;测试证据归目标企业;版本、审核和规则表变更主体改为登录会话中的稳定 employee/username 标识,客户端 actor/reviewer 不能覆盖审计事实。
|
||||
- 修改:新增 DB-backed ONLYOFFICE content/callback 会话和 `active → processing → consumed|failed` 原子状态机;token 绑定租户、资源、资产、document key/version/fingerprint、权限、actor、audience、时间和 JTI;平台只读、跨资产、旧版本、错 key、过期和重放回调均拒绝。
|
||||
- 修改:回调复用安全下载器,限制配置 origin,校验 DNS 全部地址并固定已验证公网 IP,拒绝重定向、超限、错误 MIME、危险 ZIP 和异常 OOXML;新增 `20260717_0026` 迁移并对无法归属的旧数据和有企业事实的 downgrade fail-closed。
|
||||
- 操作:拆出 AgentAsset access/serialization/ONLYOFFICE security 与风险规则字段推断模块;将核心文件控制在 800 行以内;补齐功能 CONCEPT/TODO 和迁移、权限、安全回归测试。
|
||||
- 验证:容器内发布/监控/召回/运行时/调度/遥测/租户安全/ONLYOFFICE 汇总 58 项通过;AgentAsset service/foundation 28 项通过;风险生成/修订/golden 49 项通过;相关文件 Ruff、py_compile、ORM mapper(84 张表)和 `git diff --check` 均通过。
|
||||
- 验证:一次性 PostgreSQL 探针完成旧平台数据升级、同编码多企业、scope/check、跨租户复合外键和有事实 downgrade 保护;新库 `base → head(0028)` 与 `head → 0025 → head` 均成功,AgentAsset 与 Knowledge ONLYOFFICE 会话表正常创建。
|
||||
- 影响:企业只能读取本企业和平台只读资产,无法观察或修改其他企业的资产、版本、审核和测试证据;规则测试不会抽取其他企业费用;审计身份不可由请求伪造;文档回写失败时保持原文件不变,也不会向任意或内部地址发起下载。
|
||||
@@ -0,0 +1,10 @@
|
||||
## 修复记录
|
||||
|
||||
- 13:40:修复 0026 Agent 资产租户安全迁移在真实历史建表路径下无法完整回退的问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、`git status -sb`、`git rev-parse --abbrev-ref --symbolic-full-name @{u}`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`;`origin/main` 无新提交,本地 `main` ahead 17,工作区包含多智能体并行变更,未合并、覆盖或提交。
|
||||
- 本地 ahead 摘要:17 个提交覆盖审批任务与幂等回放(`242d68c3`、`28b834ed`、`4940ebc4`)、租户安全费用学习/预审/票据关联/申请记忆与反馈(`ee88a36b`、`6bdf65bc`、`ae3f02c3`、`54754b55`、`211f85d9`、`5b246307`、`a662cfe6`)、历史回填与迁移安全(`5ed34c2b`、`11275e4b`)、费用时间线与事务(`1347366b`、`22669a90`、`a616b30c`、`661990b2`)及不透明认证会话(`653eda05`)。
|
||||
- 根因:空库先经过 0026 时 Agent 资产旧表尚不存在,迁移会按设计跳过这些表;之后旧表由当前模型补建,PostgreSQL 为列级租户外键生成 `*_tenant_id_fkey`,而 0026 回退硬编码删除 `fk_*_tenant`,首个 `agent_asset_rule_feedback` 约束不存在即中断。
|
||||
- 修改:`20260717_0026_agent_asset_tenant_security.py` 新增带 PostgreSQL 标识符引用的约束安全删除器,0026 回退对它负责的复合外键、租户外键、范围检查和唯一约束统一使用 `DROP CONSTRAINT IF EXISTS`;升级路径与最终升级约束保持不变,模型自动命名的列级外键仍随租户列删除安全清理。
|
||||
- 操作:在 `financial-internal` 网络启动独立 `postgres:16-alpine` 一次性数据库,使用应用容器和项目 venv 执行真实 Alembic 循环;验证结束后停止探针,并确认 `--rm` 已删除容器。
|
||||
- 验证:相关 Ruff 检查通过;PostgreSQL-only 迁移防误用测试 42 项通过;一次性 PostgreSQL 上的完整迁移循环 1 项通过(7.00 秒),覆盖空库升级至 0028、回退至 0008、再次升级、完整回退至 base、最终再次升级至 0028,同时验证约束存在与缺失两种删除路径。
|
||||
- 影响:真实历史路径与当前模型补建路径都能安全回退 0026;缺少迁移命名约束时不再失败,已有约束仍被正常移除,且不会改变 head 升级结构。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 13:25:记录 bug 修复:未配置商业计量器时独立 SQLite Session 回滚调用方事务。
|
||||
- Git 提交检查:执行 `git fetch --all --prune` 后,`HEAD..origin/main` 无新提交;本地 `main` ahead 17 个既有提交,依次为 `242d68c3` 审批任务与豁免、`28b834ed` 审批幂等响应、`4940ebc4` 风险处置、`ee88a36b` 分层费用学习、`6bdf65bc` 权威预审、`ae3f02c3` 票据零入口归集、`54754b55` 个人申请记忆、`211f85d9` 申请流程统一、`5b246307` 预览决策、`a662cfe6` 反馈账本、`5ed34c2b` 历史费用 Case 回填、`11275e4b` migration ownership、`1347366b` 时间线与草稿事件、`22669a90` 费用时间线、`a616b30c` AI 申请提交事务、`653eda05` bearer session、`661990b2` 费用 Case 事务事件;这些提交均早于本轮且未改写。
|
||||
- 修改:`commercial_direct_operation.py` 增加调用方 `lookup_session` 的只读未配置短路,OCR、RuntimeChat、金融连接器和附件 observer 均显式传入当前 Session。这样没有计量器时不再创建第二个 Session,也不会在 SQLite `StaticPool` 共用连接上意外 rollback 已 flush 的费用明细。
|
||||
- 操作:先用附件归集回归复现 `expense_claim_items expected to update 1 row; 0 were matched`,再将未配置判断前移到调用方 Session;保留真正配置计量器时的独立事务预占与结算。
|
||||
- 验证:容器内附件归集与票据夹 `31 passed`;商业资源边界 `9 passed`;报销端点与连接器组合 `38 passed`;相关 Ruff 通过。
|
||||
- 影响:未启用商业计量的开发、测试和兼容租户不再因为商业探测破坏调用方事务;生产 PostgreSQL 的独立商业事务语义保持不变。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 13:25:记录 bug 修复:业务回滚释放预占后相同请求无法安全重试。
|
||||
- Git 提交检查:执行 `git fetch --all --prune` 后,`HEAD..origin/main` 无新提交;本地 `main` ahead 17 个既有提交,依次为 `242d68c3` 审批任务与豁免、`28b834ed` 审批幂等响应、`4940ebc4` 风险处置、`ee88a36b` 分层费用学习、`6bdf65bc` 权威预审、`ae3f02c3` 票据零入口归集、`54754b55` 个人申请记忆、`211f85d9` 申请流程统一、`5b246307` 预览决策、`a662cfe6` 反馈账本、`5ed34c2b` 历史费用 Case 回填、`11275e4b` migration ownership、`1347366b` 时间线与草稿事件、`22669a90` 费用时间线、`a616b30c` AI 申请提交事务、`653eda05` bearer session、`661990b2` 费用 Case 事务事件;这些提交均早于本轮且未改写。
|
||||
- 修改:`commercial_runtime_reservations.py` 在相同指纹、相同订阅/权益/账期下允许 `released → reserved`,重开时重新锁定合同、校验 meter 快照并执行硬配额判断;跨账期重试继续失败关闭。
|
||||
- 操作:补充 Direct operation 的 not-sent 后重试测试,并把金融连接器“业务 rollback 后同一外部事件重试并提交”加入资源边界回归。
|
||||
- 验证:容器内 Direct + 商业资源组合 `22 passed`,商业资源边界 `9 passed`;回滚阶段无 usage,重试提交后仅一个 usage 且预占终态为 committed。
|
||||
- 影响:数据库瞬时失败或显式回滚不再把同一幂等业务请求永久卡在 released;重试仍受当前硬配额和账期约束,不会绕过额度。
|
||||
@@ -0,0 +1,15 @@
|
||||
# 默认 Compose 未统一管理本地 PostgreSQL
|
||||
|
||||
日期:2026-07-17
|
||||
文档路径:document/development/2026-07-17/dev-logs/bugs/default-compose-local-postgres-lifecycle.md
|
||||
|
||||
## 修复记录
|
||||
- 22:47:记录 bug 修复:默认 Compose 未统一管理本地 PostgreSQL。(bug-log:787bc3a4)
|
||||
- Git 提交检查:fetch 成功;upstream `origin/main`;upstream 新提交:未发现;本地 ahead 提交:787bc3a4 (HEAD -> main) feat(platform): close AI expense value loop;242d68c3 feat(approval): add task workflow and waiver decisions;28b834ed fix(approval): replay immutable action responses;4940ebc4 feat(approval): add safe risk disposition workflow;ee88a36b feat(ai): add tenant-safe hierarchical expense learning;6bdf65bc feat(expenses): add authoritative pre-review workflow;ae3f02c3 feat(expense): add persistent zero-entry receipt association;54754b55 feat(ai): add personal expense application memory;... 另有 10 条。
|
||||
- 修改:`docker-compose.yml` 将 PostgreSQL、健康依赖、本机端口和持久卷并入默认启动链路;本地数据库插值改用独立的 `LOCAL_POSTGRES_*` 命名空间,避免根 `.env` 的外部数据库账号污染 Compose;`docker-compose.postgres.yml` 保留为空兼容覆盖文件。
|
||||
- 修改:`start.sh` 与 `server_start.sh` 在读取 `.env` 前保存并在读取后恢复完整 PostgreSQL 运行参数,保证 Compose 注入的 host、port、database、user、password 和 URL 始终优先;`test_env_file_precedence.py` 增加根脚本、后端脚本和默认 Compose 回归。
|
||||
- 操作:先将旧库备份到 `/tmp/x-financial-local-pre-compose-20260717.dump`,再把旧卷只读复制到 Compose 管理的 `x-financial_postgres-data`;旧卷未删除。统一本地开发角色凭据后执行 `docker compose up -d`,迁移前置检查从 `unversioned/base` 通过并升级到 `20260717_0028`。
|
||||
- 过程披露:第一次 Compose 重建暴露环境优先级问题时,主容器仍沿用了根 `.env` 指向的外部数据库,并按既有启动流程将其从 `20260716_0006` 升级到 `20260717_0028`;迁移日志无失败,未对外部库执行回滚。修复后已确认当前主容器只连接本地 PostgreSQL 容器。
|
||||
- 验证:容器内定向测试 `107 passed`;Ruff、格式、两个 shell 脚本语法、默认/兼容 Compose 配置和 `git diff --check` 均通过;重复执行 `docker compose up -d` 只保持两个服务运行,没有创建额外容器。
|
||||
- 验证:`main` 与 `postgres` 均为 healthy;前端代理健康接口返回数据库 `ok=true`;应用实际连接 PostgreSQL 容器地址;本地库为 85 张 public 表、105 条员工数据、Alembic head `20260717_0028`,PostgreSQL 日志无 FATAL/ERROR/PANIC。
|
||||
- 影响:以后在仓库根目录执行一次默认 `docker compose up -d` 即可同时启动应用和本地数据库,并复用 Compose 管理的数据卷;外部数据库 `.env` 配置不再悄悄覆盖本地 Compose 连接。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 员工导入与成员资格部分提交
|
||||
|
||||
日期:2026-07-17
|
||||
文档路径:document/development/2026-07-17/dev-logs/bugs/employee-import-membership-transaction.md
|
||||
|
||||
## 修复记录
|
||||
- 14:07:记录 bug 修复:员工导入与成员资格部分提交。(bug-log:242d68c3)
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、状态、upstream 和双向日志检查;`origin/main` 无新提交,本地 `main` ahead 17。ahead 摘要:17 个提交覆盖审批/风险处置、租户安全费用学习、权威预审、票据关联、个人记忆、历史回填、迁移安全、费用事件事务和不透明认证会话;本轮未改写这些提交。
|
||||
- 根因:`EmployeeImportCoordinator._apply_import_rows()` 在写员工和上级关系后先 commit,`EmployeeService.import_employees()` 才补 `TenantMembership` 并第二次 commit;成员资格失败时接口报错,但员工已永久落库。
|
||||
- 修改:协调器只 flush 员工、角色、组织、上级和变更日志,不再拥有 commit;外层服务仅在成功结果后补齐租户成员资格,并对两部分执行一次统一 commit,任一异常统一 rollback。
|
||||
- 操作:新增成员资格同步注入失败测试,导入新员工后故意抛错并验证员工记录不存在;校验失败结果不触发成员资格或无意义提交。
|
||||
- 验证:容器内员工服务、导入、认证和行为画像 35 项通过;差旅计算器 5 项通过;目标 Ruff、format check、compileall、代码体积门禁和 `git diff --check` 通过。
|
||||
- 影响:员工批量导入现在满足“全部员工数据与认证成员资格一起成功或一起失败”,不会出现接口失败但部分账号已经创建/修改的状态。
|
||||
@@ -0,0 +1,11 @@
|
||||
## 修复记录
|
||||
|
||||
- 13:19:修复用户会话结算测试身份错配、缓存命中后旧部门不再归一化,以及 Excel 导入清空上级时误清空员工租户的问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`;`origin/main` 无新提交,本地 `main` ahead 17,工作区仍包含多智能体并行变更,未做清理、覆盖或提交。
|
||||
- 本地 ahead 摘要:17 个提交覆盖审批任务与幂等回放(`242d68c3`、`28b834ed`、`4940ebc4`)、租户安全费用学习/预审/票据关联/申请记忆与反馈(`ee88a36b` 至 `a662cfe6`)、历史回填与迁移安全(`5ed34c2b`、`11275e4b`)、费用时间线/事务(`1347366b`、`22669a90`、`a616b30c`、`661990b2`)及不透明认证会话(`653eda05`)。
|
||||
- 修改:会话结算正向用例改用会话真实所有者认证,保留服务端 username ownership 校验;新增其他用户不能关闭该会话的反向测试,避免用放宽授权掩盖 `durationMs=0`。
|
||||
- 修改:目录建表/种子初始化继续按 bind 与租户缓存,但缓存命中时仍以租户过滤查询旧部门编码并持久化映射到规范部门;不再因初始化缓存永久跳过外部同步产生的旧编码。
|
||||
- 修改:Employee 与 OrganizationUnit 的复合租户关系只把 `organization_unit_id`、`manager_id` 标记为 SQLAlchemy 可同步外键,`tenant_id` 只参与关联过滤;清空部门或上级不会再把员工租户写成 `NULL`,数据库复合外键仍阻止跨租户关联。
|
||||
- 测试:补充会话所有权反例、导入后 `tenant_id/manager_id` 持久化断言,以及旧部门归一化后租户与数据库组织归属断言。
|
||||
- 验证:容器内三个原失败点与新增反例 4 项通过;员工服务、Excel 导入、行为画像/会话和认证会话相关回归 31 项通过;ORM mapper 确认关系同步列仅为 `organization_unit_id/manager_id`;相关文件 Ruff 与 `git diff --check` 均通过。
|
||||
- 影响:会话时长能在正确登录主体下正常结算,其他用户仍不能关闭该会话;旧组织编码会持续收敛到标准部门;员工导入或资料更新清空上级/部门时不会破坏不可为空的租户归属。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 报销审批身份解析跨租户串读
|
||||
|
||||
日期:2026-07-17
|
||||
文档路径:document/development/2026-07-17/dev-logs/bugs/expense-claim-approver-tenant-isolation.md
|
||||
|
||||
## 修复记录
|
||||
- 13:48:记录 bug 修复:报销审批身份解析跨租户串读。(bug-log:242d68c3)
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、`git status -sb`、upstream 解析及双向日志检查;`origin/main` 无新提交,本地 `main` ahead 17。ahead 摘要:`242d68c3/28b834ed/4940ebc4` 为审批任务、不可变回放和风险处置,`ee88a36b/6bdf65bc/ae3f02c3/54754b55/211f85d9/5b246307/a662cfe6` 为租户安全费用学习、预审、票据关联、个人记忆和申请反馈,`5ed34c2b/11275e4b` 为历史回填与迁移安全,`1347366b/22669a90/a616b30c/661990b2` 为费用时间线及事务,`653eda05` 为不透明会话;本轮未改写这些提交。
|
||||
- 修改:`expense_claim_access_policy.py` 的当前员工、申请人、直属领导、部门预算负责人和财务负责人解析全部先绑定认证用户或报销单的结构化 `tenant_id`;员工、组织和下属子查询增加租户首层谓词,避免相同姓名、邮箱前缀、部门或角色在另一企业命中。
|
||||
- 修改:把身份候选、申请人回填和唯一姓名判断拆入 `expense_claim_employee_resolver.py`,保持公开策略 API 不变,并将访问策略主文件降到 701 行;无可信租户、跨租户关联或结构化归属冲突统一失败关闭。
|
||||
- 操作:同步补齐审批任务、风险并发、层级记忆和报销测试夹具的显式企业归属;没有放宽生产授权,也没有用默认企业兼容掩盖跨租户错误。
|
||||
- 验证:容器内 `test_expense_claim_service.py` 121 项通过,访问策略文件大小与租户作用域定向 10 项通过;审批任务、PostgreSQL 并发与全量后端分片均通过,fresh PostgreSQL 专项最终 `87 passed / 0 skipped / 0 failed`;新 Python 文件 Ruff 和 `git diff --check` 通过。
|
||||
- 影响:审批队列、审批人快照、退回/通过权限和历史回填不会再因另一企业存在同名员工或同名部门而串租户;多租户环境下报销审批保持可解释且失败关闭。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 13:33:记录 bug 修复:财务驾驶舱租户安全测试仍按旧 Case Link 契约构造报销单。
|
||||
- Git 提交检查:`git fetch --all --prune` 后未发现 upstream 新提交;本地相对 `origin/main` ahead 17 个既有提交,分别为 `242d68c3` 审批任务工作流、`28b834ed` 不可变响应重放、`4940ebc4` 风险处置工作流、`ee88a36b` 租户安全分层费用学习、`6bdf65bc` 权威预审、`ae3f02c3` 零录入票据关联、`54754b55` 个人申请记忆、`211f85d9` 申请工作流统一、`5b246307` 申请预览决策、`a662cfe6` 反馈账本、`5ed34c2b` 历史 Expense Case 回填、`11275e4b` 迁移所有权安全、`1347366b` 时间线与草稿事件安全、`22669a90` 统一事件时间线、`a616b30c` AI 申请事务统一、`653eda05` 不透明 bearer 会话、`661990b2` 事务化 Expense Case 事件;本次未改写这些提交。
|
||||
- 修改:`test_finance_dashboard_tenant_security.py` 的 Claim 构造器显式接收并写入 `tenant_id`,租户夹具不再只依赖历史 `ExpenseCaseLink`;新增“结构化 Claim 租户与旧 Link 不一致时以 Claim 为准”的隔离回归。
|
||||
- 操作:先在容器内单跑复现 3 个失败,确认不是测试顺序或全局 monkeypatch 污染,再执行最小夹具修复并串行复跑财务、连接器与 Hermes 相邻测试组。
|
||||
- 验证:单文件 `8 passed`;排序相邻组 `67 passed, 4 skipped`;目标文件 Ruff 检查与 `git diff --check` 均通过。
|
||||
- 影响:财务驾驶舱测试与新的结构化租户模型保持一致,同时固定了旧关联索引不能移动报销单租户归属的安全边界;生产 fail-close 与首条 SQL 租户过滤没有放宽。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 财务连接器运行事件计数与固定时钟偏差
|
||||
|
||||
日期:2026-07-17
|
||||
文档路径:document/development/2026-07-17/dev-logs/bugs/financial-connector-operational-counting-clock.md
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 11:39:记录 bug 修复:财务连接器相同载荷的不同运行尝试被永久合并,固定时钟场景的接收时间偏离观测窗口。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;upstream `origin/main` 无新提交;本地 ahead 17 条,包含 `242d68c3 feat(approval): add task workflow and waiver decisions`、`28b834ed fix(approval): replay immutable action responses`、`4940ebc4 feat(approval): add safe risk disposition workflow`、`ee88a36b feat(ai): add tenant-safe hierarchical expense learning` 等共享工作区既有提交,本次未改写这些提交。
|
||||
- 修改:`financial_connector_operational_events.py` 把规范化 UTC 发生时间纳入运行事实幂等命名空间,使同一 candidate 的补偿重试保持单条、不同 HTTP 尝试分别计数;`financial_connector_ingestion.py` 让注入的可信 `now_epoch` 同时驱动接收时间和配置健康时间;`financial_connector_observability.py` 统一把数据库时间规范为 UTC,避免 SQLite/驱动返回 naive datetime 时窗口结果不稳定。
|
||||
- 操作:补充运行事实服务、HTTP 冲突、可信认证归属、敏感原值不落库和 PostgreSQL 并发探针;所有 Python、pytest、Ruff 与迁移操作均在 `local-x-financial-linux` 容器中执行,PostgreSQL 验证使用新建的一次性 `disposable-probe` 容器,没有连接项目配置中的外部数据库。
|
||||
- 验证:容器内连接器与迁移前置定向 `115 passed`,全新 PostgreSQL 17 完整迁移循环 `51 passed`,连接器并发 `4 passed`,后继 0023 并发 `7 passed`;相关 Ruff 和 `git diff --check` 通过。
|
||||
- 影响:可观测性不会再把多次真实重放压成一次,也不会因测试/模拟可信时钟与实际系统日期不同而漏掉事件;同一补偿 candidate 仍由数据库唯一约束保证幂等。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 13:15:修复本体参考目录、员工画像、Hermes 扫描/看板/提醒、财务报告和内部 Orchestrator 的跨租户读取与错误归属风险。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`;`origin/main` 无新增提交,本地 `main` ahead 17。ahead 摘要:`242d68c3/28b834ed/4940ebc4` 审批与风险处置,`ee88a36b/54754b55/211f85d9/5b246307/a662cfe6` AI 费用学习与申请流程,`6bdf65bc/ae3f02c3/5ed34c2b/1347366b/22669a90/a616b30c/661990b2` 费用预审、费用事件与时间线,`11275e4b` migration ownership,`653eda05` opaque bearer session;本轮未改写这些提交。
|
||||
- 修改:本体解析在模型前创建租户化 Agent Run,并对员工、组织、费用、应收、应付和项目目录首 SQL 过滤;员工画像新增租户字段、复合员工外键和本人/直属领导/财务/高管/管理员访问矩阵;Hermes 风险、画像、线索、提醒和看板逐租户运行;财务报告按企业配置收件人、内容、路径和幂等账本;内部 Orchestrator 缺失、停用、不存在或冲突租户时在建 Run 前拒绝。
|
||||
- 操作:新增 `20260717_0028` 迁移和租户财务报告配置/运行表;upgrade/downgrade 在任何 DDL 前拒绝非 PostgreSQL;完成 fresh、旧结构回填、回滚重升及复合外键 PostgreSQL 探针,验证后停止并自动清理一次性探针容器。
|
||||
- 验证:容器内新增安全/提醒测试 9 项、旧本体与 Orchestrator 88 项、Hermes/看板/财务报告 8 项、画像/鉴权/关联草稿 25 项通过;Alembic 静态回归 61 项通过、1 项因未提供外部 PostgreSQL DSN 跳过;相关文件 Ruff check/format check 和 `git diff --check` 通过。
|
||||
- 影响:企业 A 的模型提示词、员工画像、风险扫描、提醒、看板、报告附件和邮件收件人不再包含企业 B 数据;无可信租户的内部任务不能生成不可归属记录;历史无双租户快照的 Run 不会被企业看板错误统计。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 12:45:修复知识文件、元数据与 LightRAG/Qdrant 全局共享导致的跨租户串读和覆盖风险。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`;upstream 无新提交,本地 `main` ahead 17,包含 `242d68c3`、`28b834ed`、`4940ebc4`、`ee88a36b`、`11275e4b`、`653eda05` 等既有审批、AI、迁移与鉴权提交,本次未改写或合并这些提交。
|
||||
- 修改:新增 tenant/platform 存储作用域,把文件、`.index.json`、`.lightrag`、运行时缓存和 Qdrant workspace 分区;旧全局制度无损复制到平台只读层;知识 API、同步、Orchestrator 查询和后台索引统一从认证用户或数据库 Agent Run 获取可信 tenant。
|
||||
- 操作:按职责拆出 scope、index state 和 RAG scoring 小模块;删除知识文件工具中已废弃的弱 ONLYOFFICE token 代码;所有命令在 Docker 容器 `local-x-financial-linux` 的 `/app` 下运行,未提交、未推送、未删除旧知识资料。
|
||||
- 验证:容器内租户隔离 6 项、既有知识回归 31 项和相关 Agent Run/鉴权 20 项通过;目标 Ruff 与 compileall 通过。
|
||||
- 影响:租户只能看到和操作自己的知识数据,并可读取平台只读制度;RAG 本地状态、缓存和向量 workspace 不再跨企业复用,缺 tenant 时统一 fail-closed。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 12:45:修复匿名 ONLYOFFICE 回调可重放、可跨资源覆盖并可借下载 URL 发起 SSRF 的高风险问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`;upstream 无新提交,本地 `main` ahead 17,既有提交未被改写。
|
||||
- 修改:新增 DB-backed 一次性会话与 `20260717_0027` 迁移;token 绑定租户、资源、文档、key、版本、权限、audience、过期时间和 JTI;预览禁止写入,编辑回调原子 claim 后只能消费一次;下载器拒绝错误 origin、私网/回环/链路本地解析、DNS 重绑定、重定向、超限、错误 MIME 和异常 OOXML。
|
||||
- 操作:把 content/callback 解析与网络下载从主 KnowledgeService 拆出;content token 保持短时,callback session 默认 4 小时;失败会话记录受控原因且原文件保持不变。
|
||||
- 验证:容器内 ONLYOFFICE 租户安全 8 项通过,覆盖平台只读、错 key 不 claim、过期、成功单次回写、重放、SSRF、IP pinning、大小/MIME/OOXML;迁移/preflight/ownership 163 项通过、1 项静态套件因未配置测试 DSN 跳过;共享 PostgreSQL 探针另完成 `base → 0028` 与 `0028 → 0025 → 0028`,Knowledge/AgentAsset 会话表均正常落库。
|
||||
- 影响:未签名、过期、跨租户、跨文档、旧版本、只读和重放回调均不能覆盖文件;文档服务下载失败时安全拒绝,不向内部网络或任意 URL 发请求。
|
||||
@@ -0,0 +1,8 @@
|
||||
## 修复记录
|
||||
|
||||
- 12:45:修复知识索引调度器使用默认租户、后台线程信任传入上下文导致任务错误归属的问题。
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、`git status -sb`、`git log HEAD..@{u}` 和 `git log @{u}..HEAD`;upstream 无新提交,本地 `main` ahead 17,既有提交未被改写。
|
||||
- 修改:调度器查询租户注册表并逐个处理 `status=active` 的租户;无活跃租户时跳过;内部 Hermes 身份显式携带 tenant;索引 worker 从数据库 Agent Run 的 route/ontology 重新取得并比对 tenant,拒绝缺失或冲突上下文。
|
||||
- 操作:同步任务、活跃任务复用、stale 状态回收和 ingest 状态写入均绑定当前 tenant,不再构造无作用域 KnowledgeService。
|
||||
- 验证:容器内 active/suspended 调度隔离、Agent Run tenant 冲突、知识同步和既有知识组合测试通过;扫描全部 `KnowledgeService`/`KnowledgeRagService` 调用点,生产代码仅保留显式 tenant 或显式 platform 构造。
|
||||
- 影响:定时知识任务不会再落入默认企业或把一个租户的文档写进另一个租户的索引;后台参数被篡改或丢失时任务 fail-closed。
|
||||
@@ -0,0 +1,12 @@
|
||||
# 后台报销与 Steward 可信租户上下文缺失
|
||||
|
||||
日期:2026-07-17
|
||||
文档路径:document/development/2026-07-17/dev-logs/bugs/steward-linked-reimbursement-tenant-context.md
|
||||
|
||||
## 修复记录
|
||||
- 02:09:记录 bug 修复:后台报销与 Steward 可信租户上下文缺失。(bug-log:242d68c3)
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`;upstream `origin/main` 无新提交;本地 ahead 17 条,包含 `242d68c3 feat(approval): add task workflow and waiver decisions`、`28b834ed fix(approval): replay immutable action responses`、`4940ebc4 feat(approval): add safe risk disposition workflow`、`ee88a36b feat(ai): add tenant-safe hierarchical expense learning` 等共享工作区既有提交,本次未改写这些提交。
|
||||
- 修改:`linked_reimbursement_draft_jobs.py` 将已认证 `current_user` 显式传给 Orchestrator,避免后台 AgentRun 丢失 tenant;`steward.py` 为 plans、slot-decisions、runtime-decisions、plans/stream 注入认证用户,用登录态覆盖 payload 身份字段,并补充会话归属校验和申请候选单租户过滤;对应测试覆盖未认证、空 tenant、伪造 tenant、跨租户会话及候选单隔离。
|
||||
- 操作:所有静态检查和测试均在 Docker 容器 `local-x-financial-linux` 的 `/app` 下执行;没有修改 commercial、迁移、连接器、AI 发布或 Savings 代码,也没有提交或推送。
|
||||
- 验证:容器内定向 pytest 两组共 54 项通过;Ruff 排除目标旧文件既有 E501 长行后全部规则通过。完整 Ruff 仍报告目标旧文件历史 E501,不涉及本次新增安全逻辑。
|
||||
- 影响:后台报销任务不再因漏传登录态落入缺失/default tenant;四类 Steward AI 入口统一要求登录,客户端伪造 tenant、用户或权限字段不能覆盖服务端身份,跨租户会话和申请候选单读取会 fail-closed。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 差旅计算器员工解析跨租户命中
|
||||
|
||||
日期:2026-07-17
|
||||
文档路径:document/development/2026-07-17/dev-logs/bugs/travel-calculator-employee-tenant-isolation.md
|
||||
|
||||
## 修复记录
|
||||
- 14:07:记录 bug 修复:差旅计算器员工解析跨租户命中。(bug-log:242d68c3)
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、状态、upstream 和双向日志检查;`origin/main` 无新提交,本地 `main` ahead 17。ahead 摘要:`242d68c3/28b834ed/4940ebc4` 为审批与风险处置,`ee88a36b` 至 `a662cfe6` 为费用学习/预审/票据/记忆闭环,`5ed34c2b/11275e4b` 为回填和迁移安全,`1347366b/22669a90/a616b30c/661990b2` 为费用事件与事务,`653eda05` 为认证会话;本轮未改写这些提交。
|
||||
- 根因:差旅计算器按邮箱、工号或姓名查员工时没有租户谓词,也忽略认证上下文的 `employee_id/employee_no`;员工身份现为租户内唯一,不同企业可以合法存在相同值,首条命中会把另一企业职级和办公城市带入计算。
|
||||
- 修改:`travel_reimbursement_calculator.py` 先校验可信 `tenant_id`,将 `employee_id` 加入最高优先候选,所有 ID/邮箱/工号/姓名查询都以 `Employee.tenant_id` 为首层条件。
|
||||
- 操作:新增两企业相同邮箱、工号和姓名的反向测试,故意先插入另一企业员工,确认当前登录企业的 employee ID、职级和地点始终胜出。
|
||||
- 验证:容器内差旅计算器 5 项通过;员工服务、导入、认证和行为画像 35 项通过;目标 Ruff、format check、compileall、代码体积门禁和 `git diff --check` 通过。
|
||||
- 影响:差旅住宿、补助和交通估算不会再读取另一企业员工的职级或办公地点;无合法租户时直接失败关闭。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 差旅计算器只读流程触发规则资产提交
|
||||
|
||||
日期:2026-07-17
|
||||
文档路径:document/development/2026-07-17/dev-logs/bugs/travel-calculator-rule-sync-transaction.md
|
||||
|
||||
## 修复记录
|
||||
- 13:48:记录 bug 修复:差旅计算器只读流程触发规则资产提交。(bug-log:242d68c3)
|
||||
- Git 提交检查:已执行 `git fetch --all --prune`、`git status -sb`、upstream 解析及双向日志检查;`origin/main` 无新提交,本地 `main` ahead 17。ahead 摘要:`242d68c3/28b834ed/4940ebc4` 为审批与风险处置,`ee88a36b` 至 `a662cfe6` 为 AI 费用学习和申请闭环,`5ed34c2b/11275e4b` 为历史回填与迁移安全,`1347366b/22669a90/a616b30c/661990b2` 为费用事件与事务,`653eda05` 为认证会话;本轮未改写这些提交。
|
||||
- 根因:差旅计算器为读取规则先调用 `AgentAssetService.list_assets()`,该入口可能执行资产初始化并提交共享 Session,导致外层报销事务被提前提交;同时规则目录未带认证企业,企业覆盖规则可能退回到错误的全局目录。
|
||||
- 修改:`travel_reimbursement_calculator.py` 删除计算路径中的资产同步调用,改为使用当前 Session 和认证用户 `tenant_id` 直接只读加载 `ExpenseRuleRuntimeService` 目录;规则初始化只保留在启动/管理边界,不再混入金额计算事务。
|
||||
- 操作:保留地点、职级、住宿、补助和交通估算算法,仅收紧规则读取的事务与租户边界;测试夹具同步补齐结构化企业身份。
|
||||
- 验证:容器内差旅计算器 4 项金额、地区拒绝和地点归一化回归通过;AgentAsset service/foundation 28 项、报销服务 121 项及后端全量分片通过;相关新文件 Ruff、compileall 和 `git diff --check` 通过。
|
||||
- 影响:用户计算差旅标准时不会意外提交正在编辑的报销单;企业规则按当前登录企业解析,读取失败会明确报错而不会静默使用另一企业或全局可写状态。
|
||||
@@ -0,0 +1,188 @@
|
||||
# Agent 资产多租户隔离与安全规则编辑 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
让规则、技能、MCP、任务及其版本、审核、测试和编辑会话都拥有可验证的租户归属,并以“企业资产可写、平台资产只读”的双层模型安全贯通规则生成、真实场景验证、审核、发布和 ONLYOFFICE 编辑。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
原 AgentAsset 数据模型没有结构化 `tenant_id` 与 `scope`,部分读取和内部查询可以在没有用户上下文时返回全局资产。版本、审核、测试、反馈和发布链路主要依赖 `asset_id` 或业务约定关联,无法由数据库阻止跨租户子记录注入。同编码资产也不能由不同企业独立维护。
|
||||
|
||||
规则场景测试可以在未声明目标企业时抽取费用数据,审核主体还可能使用客户端传入的 actor/reviewer 字段,导致测试证据和盲审身份缺乏稳定、可追溯的企业边界。
|
||||
|
||||
规则表的 ONLYOFFICE 内容与回调接口原先缺少持久化的一次性会话。回调下载地址、文档 key、版本、租户和编辑权限之间没有不可变绑定,存在匿名读取、跨资产回写、身份伪造、重放和服务端请求伪造风险。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- AgentAsset、Version、Review、TestRun、RuleFeedback 和 ONLYOFFICE Session 均保存结构化租户作用域。
|
||||
- 租户用户只能看到本企业资产和平台资产;同编码时企业资产优先覆盖平台默认资产。
|
||||
- 跨租户详情、版本、审核、发布、测试和反馈统一表现为不存在,避免泄露资源存在性。
|
||||
- 平台资产对所有租户只读,仅平台管理员可新增、修改、发布或编辑。
|
||||
- 所有 HTTP 入口使用认证会话中的 `CurrentUserContext.tenant_id`,不接受客户端覆盖租户。
|
||||
- 写入、版本和审核操作使用 RuleEditor、RuleReviewer 或平台管理员权限,并以稳定身份写入审计证据。
|
||||
- 真实风险场景必须显式声明当前企业 `target_tenant_id`,费用样本的第一层 SQL 条件就是该租户。
|
||||
- ONLYOFFICE 内容和回调使用不同 audience/scope 的签名 token,并绑定数据库一次性会话。
|
||||
- 回调下载拒绝错误 origin、非公网解析、DNS 重绑定、重定向、超限和非安全 OOXML 文件。
|
||||
|
||||
### 非目标
|
||||
|
||||
- 不允许普通企业用户创建或修改平台资产。
|
||||
- 不把所有企业资产放在全局结果集中后仅靠前端过滤。
|
||||
- 不允许平台管理员借普通企业会话修改其他企业的私有资产;跨企业运维需要独立受控流程。
|
||||
- 不提供允许私网、回环或任意下载地址的 ONLYOFFICE 安全降级开关。
|
||||
- 不在本切片中重做 Agent 资产管理前端视觉或商业定价页面。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
- 企业规则编辑者:维护本企业规则资产、上传规则表并创建新版本。
|
||||
- 企业规则审核者:以稳定登录身份进行盲审、驳回或批准规则版本。
|
||||
- 企业风控人员:用本企业真实费用申请生成测试样本和质量证据。
|
||||
- 普通企业用户:读取本企业资产和平台只读资产,但不能写入。
|
||||
- 平台管理员:维护跨企业可见的平台基础规则和模板。
|
||||
- 运行时与调度器:在显式租户范围内加载、测试、监控和召回资产,不进行全局扫描。
|
||||
- ONLYOFFICE 文档服务:使用资源专用 token 读取一次文档,并通过单次 callback session 回写允许编辑的当前版本。
|
||||
|
||||
## 功能能力
|
||||
|
||||
- 双层可见性:`tenant:{tenant_id}` 与 `platform:platform`。
|
||||
- 企业内唯一编码:数据库唯一键为 `(tenant_id, scope, code)`,不同企业可拥有同编码资产。
|
||||
- 确定性覆盖:按编码加载时先找当前企业资产,再回退平台资产。
|
||||
- 服务端可信租户:HTTP 服务由登录会话构造 `AgentAssetAccessScope`;无用户上下文的内部读取只允许平台作用域。
|
||||
- 稳定审计主体:优先记录 `employee:{employee_id}`,没有员工 ID 时记录大小写归一的 `username:{username}`。
|
||||
- 真实样本隔离:场景请求必须传 `target_tenant_id`,且必须等于登录企业;TestRun 记录样本所属企业。
|
||||
- 平台资产可在企业场景中验证,但测试证据仍归目标企业,不能变成平台或其他企业事实。
|
||||
- ONLYOFFICE content token 有效期 15 分钟,callback session 有效期 4 小时。
|
||||
- 回调状态机保证同一个 JTI 最多一次进入写入阶段。
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 前端契约
|
||||
|
||||
- AgentAsset DTO 提供 `tenantId` 与 `scope`,前端可明确标识企业资产和平台资产。
|
||||
- 平台资产在非平台管理员会话中必须隐藏或禁用修改、发布、上传和 ONLYOFFICE 编辑动作。
|
||||
- 场景测试请求必须携带 `targetTenantId`;它是目标企业的显式确认,不是可切换企业的授权参数。
|
||||
- 旧 `X-Actor`/reviewer 头仅为兼容保留,服务端忽略其身份值并使用登录会话主体。
|
||||
- ONLYOFFICE 配置根据权限返回 `view` 或 `edit`;内容和回调 token 只供文档服务使用。
|
||||
|
||||
### 后端职责
|
||||
|
||||
- `agent_asset_scope`:定义 platform/tenant 常量和合法作用域基础规则。
|
||||
- `agent_asset_access`:从可信用户构造访问范围、生成稳定主体并提供可见/可写谓词。
|
||||
- `agent_asset` repository:所有列表、详情、版本、审核、测试和反馈查询都注入结构化租户条件。
|
||||
- `agent_assets`:编排资产 CRUD、版本、审核与序列化,不通过未过滤 ORM 关系返回子记录。
|
||||
- `agent_asset_risk_rule_testing`:校验目标企业、先按租户过滤 ExpenseClaim,再生成测试证据。
|
||||
- 发布、监控、召回、调度、遥测和风险运行时服务:沿资产租户作用域读取和写入,禁止全局 asset id 查询。
|
||||
- `agent_asset_onlyoffice_security`:签发/验证持久化会话并原子消费 callback JTI。
|
||||
- `agent_asset_onlyoffice`:按会话中的租户、资产、key、版本和指纹定位文档,安全下载后创建新版本。
|
||||
- `knowledge_onlyoffice_security`:复用统一安全下载器,执行 origin、DNS/IP、响应和 OOXML 校验。
|
||||
|
||||
### 数据
|
||||
|
||||
`20260717_0026_agent_asset_tenant_security.py` 负责以下结构:
|
||||
|
||||
- `agent_assets`:新增 `tenant_id`、`scope`,建立作用域约束、租户外键和企业内 code 唯一键。
|
||||
- `agent_asset_versions`、`agent_asset_reviews`:新增租户作用域,并以 `(tenant_id, scope, asset_id)` 复合外键绑定父资产。
|
||||
- `agent_asset_test_runs`、`agent_asset_rule_feedback`:保存证据所属企业;平台资产的企业测试/反馈也归企业事实域。
|
||||
- `agent_asset_onlyoffice_sessions`:保存 JTI、租户、资源 scope、asset、document key/version/fingerprint、audience、权限、actor、过期时间和状态。
|
||||
- 旧资产默认回填为平台资产;能从父资产确定的历史版本、审核和证据同步回填。
|
||||
- 无法确认租户的历史数据或不完整旧表结构会 fail-closed,不猜测企业归属。
|
||||
|
||||
### 权限与信任边界
|
||||
|
||||
- 当前企业只来自 `CurrentUserContext.tenant_id`;空值、`platform` 伪企业或请求参数不能构造企业访问范围。
|
||||
- 租户读取谓词是“当前企业或平台”,写入谓词只允许当前企业;平台写入还要求 `is_admin=true`。
|
||||
- 跨租户资源统一返回 404;权限不足的本作用域操作返回受控错误。
|
||||
- RuleEditor 可维护规则和版本,RuleReviewer 执行审核;平台资产的任意写操作额外要求平台管理员。
|
||||
- 版本 created_by、审核 reviewer、规则表变更 actor 都由稳定登录主体生成。
|
||||
- 后台 bootstrap/foundation 按平台作用域精确查找种子资产,不能误改同编码企业资产。
|
||||
- 风险运行时按“企业优先、平台回退”加载发布规则,不扫描其他企业版本。
|
||||
- ORM 关系不是授权边界;对外响应必须经过带 scope 的 repository/service 查询。
|
||||
|
||||
### 资产解析顺序
|
||||
|
||||
```text
|
||||
find_by_code(code, current_tenant):
|
||||
1. tenant_id = current_tenant AND scope = tenant
|
||||
2. tenant_id = platform AND scope = platform
|
||||
3. not found
|
||||
```
|
||||
|
||||
该顺序让企业能够在不修改平台模板的情况下覆盖默认规则,同时保持其他企业和平台资产不受影响。
|
||||
|
||||
### 风险场景测试
|
||||
|
||||
```text
|
||||
认证用户 tenant
|
||||
→ 校验 target_tenant_id == tenant
|
||||
→ 校验目标资产为 tenant 自有或 platform 只读资产
|
||||
→ SQL 第一层条件 ExpenseClaim.tenant_id == target_tenant_id
|
||||
→ 应用时间、费用类型、城市等业务筛选
|
||||
→ 创建 tenant-scoped TestRun
|
||||
```
|
||||
|
||||
没有目标企业、目标企业不一致或资产属于其他企业时均拒绝,不使用 mock 的默认企业或全局样本补齐。
|
||||
|
||||
### ONLYOFFICE 状态机
|
||||
|
||||
```text
|
||||
issue(view) → active ── callback status 2/6 ──拒绝写入
|
||||
issue(edit) → active ── atomic claim ──→ processing
|
||||
├─校验/下载/写入成功→ consumed
|
||||
└─任一步失败────────→ failed
|
||||
|
||||
active -- exp 超时 --> 验证拒绝
|
||||
processing/consumed/failed/revoked -- replay --> 409/拒绝
|
||||
```
|
||||
|
||||
token 同时绑定 issuer、audience、scope、JTI、tenant、resource scope、asset、document key、version、fingerprint、writable、actor、iat/nbf/exp。回调 payload 只能提供状态和下载位置,不能覆盖这些授权事实。
|
||||
|
||||
### 降级与回滚策略
|
||||
|
||||
- 无可信租户:HTTP 请求拒绝;无用户上下文的内部服务只看平台资产,绝不回退全局查询。
|
||||
- 跨租户资产、版本或测试证据:按不存在处理,不尝试平台管理员越权兼容。
|
||||
- 场景样本为空:返回空样本测试事实,不改查其他企业数据。
|
||||
- ONLYOFFICE token、key、版本、指纹、DNS、MIME 或 OOXML 校验失败:拒绝回写并保持原文件不变。
|
||||
- 生产文档服务必须使用配置白名单中的公网 TLS origin,或经满足相同约束的安全代理访问。
|
||||
- migration downgrade 在存在企业资产/证据或 ONLYOFFICE 会话时拒绝有损回滚;必须先通过受控数据迁移清理事实。
|
||||
|
||||
## 测试方案
|
||||
|
||||
- 可见性:两企业同编码资产、企业覆盖平台、平台只读、跨企业详情 404、无上下文仅平台。
|
||||
- 数据完整性:scope/tenant check、企业内 code 唯一、Version/Review 复合父子外键、TestRun 证据企业。
|
||||
- 权限与身份:RuleEditor/RuleReviewer、平台管理员、伪造 actor/reviewer 无效、稳定 employee/username 主体。
|
||||
- 风险场景:必须目标企业、目标不一致拒绝、SQL 租户首过滤、平台资产的企业 TestRun。
|
||||
- 发布链路:发布门禁、监控、召回、运行时、调度和遥测均使用显式租户。
|
||||
- ONLYOFFICE:content/callback scope、tenant/asset/key/version/fingerprint 绑定、平台只读、原子 claim、失败终态和重放拒绝。
|
||||
- SSRF/文件:白名单 origin、全量公网 DNS、固定已校验 IP、拒绝重定向、大小/MIME/ZIP/OOXML 限制。
|
||||
- 迁移:旧平台数据回填、同编码多企业、非法 scope/跨租户外键拒绝、事实存在时 downgrade 拒绝、清理后可回滚。
|
||||
- 所有后端验证只在 `local-x-financial-linux` 容器中执行,单条命令限制 60 秒。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
- 所有 AgentAsset 业务读取都能由 `tenant_id + scope` 确定结果范围。
|
||||
- 跨企业资产、版本、审核、测试、反馈和回调用例 100% 不可见或拒绝。
|
||||
- 平台资产对非平台管理员的写入和回调 100% 无副作用。
|
||||
- 真实费用样本查询 100% 具有服务端校验的企业首过滤条件。
|
||||
- 审计主体 100% 来自稳定登录身份,客户端 actor/reviewer 不能改变事实。
|
||||
- 同一 ONLYOFFICE callback JTI 最多一次进入 `processing`。
|
||||
- 相关 Python 文件通过 Ruff、py_compile 和 ORM mapper 配置;定向发布、安全与风险规则回归通过。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
- 旧测试或内部调用若直接构造没有 `tenant_id` 的 `CurrentUserContext`,会按新信任边界 fail-closed;应由对应调用方补齐真实租户,而不是放宽服务契约。
|
||||
- 两个旧费用风险测试使用尚未持久化、没有 claim tenant 的对象;共享租户作用域已要求先保存并确认归属,需由费用申请切片统一调整测试夹具。
|
||||
- 两个旧风险发布测试手工注入 aggregate 后直接 promote,与当前必须有真实质量 TestRun 的发布门禁不一致;需由发布门禁切片统一口径。
|
||||
- ONLYOFFICE 实际回写依赖部署环境提供公网可解析、可信 TLS 的文档服务或安全代理;开发网络解析到保留/私网地址时会按设计拒绝。
|
||||
- 企业间受控复制、平台资产签名发布和跨企业运维审计属于后续独立能力,不应通过放宽本轮隔离实现。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-17:完成 AgentAsset、Version、Review、TestRun、Feedback 的结构化租户作用域、企业覆盖平台读取和跨企业 fail-closed。
|
||||
- 2026-07-17:完成可信目标企业风险场景、真实费用样本 SQL 首过滤、企业 TestRun 证据和稳定盲审身份。
|
||||
- 2026-07-17:完成 DB-backed ONLYOFFICE 一次性会话、平台只读、文档基线绑定与安全下载。
|
||||
- 2026-07-17:完成 `20260717_0026` 迁移及一次性 PostgreSQL `base → head`、`head → 0025 → head` 验证。
|
||||
- 2026-07-17:完成发布、监控、召回、运行时、调度、遥测、foundation 和风险规则生成链路的租户接线与容器回归。
|
||||
@@ -0,0 +1,67 @@
|
||||
# Agent 资产多租户隔离与安全规则编辑 开发 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
关联方案:[CONCEPT.md](./CONCEPT.md)
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 任务边界、信任模型、数据归属和上线约束以 CONCEPT 对应章节为准。
|
||||
- `[x]` 只表示已有代码或容器验证证据;生产环境尚未验证的项目保持 `[ ]`。
|
||||
- 所有后端测试必须在 `local-x-financial-linux` 容器内执行,单命令最长 60 秒。
|
||||
|
||||
## 1. 调研与边界
|
||||
|
||||
- [x] [CONCEPT: 背景与问题] 盘点 AgentAsset、版本、审核、测试、反馈、发布和规则表编辑中的无租户/弱租户查询。证据:repository、service、endpoint 和 release 全链路调用扫描。
|
||||
- [x] [CONCEPT: 目标与非目标] 冻结“企业资产可写 + 平台资产只读 + 企业覆盖平台”的双层模型。证据:`AgentAssetAccessScope` 与两企业同编码测试。
|
||||
- [x] [CONCEPT: 权限与信任边界] 冻结可信租户只来自登录会话、客户端 tenant/actor/reviewer 不能覆盖事实。证据:认证依赖、稳定主体函数和伪造头测试。
|
||||
|
||||
## 2. 契约与设计
|
||||
|
||||
- [x] [CONCEPT: 数据] 为 Asset、Version、Review、TestRun、Feedback 定义 `tenant_id + scope`、约束和索引。证据:模型与 `20260717_0026`。
|
||||
- [x] [CONCEPT: 资产解析顺序] 定义企业同编码资产优先、平台资产回退的确定性加载顺序。证据:scoped repository 和 runtime loader 测试。
|
||||
- [x] [CONCEPT: ONLYOFFICE 状态机] 定义 `active → processing → consumed|failed`,以及过期、撤销和重放拒绝。证据:Session 模型、迁移与 callback 测试。
|
||||
- [x] [CONCEPT: 风险场景测试] 定义显式 `target_tenant_id` 与 TestRun 证据企业,不允许默认或全局样本。证据:scenario schema/service 测试。
|
||||
|
||||
## 3. 后端实现
|
||||
|
||||
- [x] [CONCEPT: 后端职责] 所有 AgentAsset 列表、详情和 code 查询接入结构化可见谓词。证据:`agent_asset.py` repository 与 `agent_assets.py`。
|
||||
- [x] [CONCEPT: 权限与信任边界] 资产写入、版本和审核接入 RuleEditor/RuleReviewer/平台管理员依赖。证据:AgentAsset 和风险规则 endpoints。
|
||||
- [x] [CONCEPT: 权限与信任边界] 用 `employee:{id}` 或归一化 `username:{name}` 替代请求 actor/reviewer。证据:`stable_user_principal()` 与审计断言。
|
||||
- [x] [CONCEPT: 后端职责] 发布门禁、评审、监控、召回、遥测、调度和处置标签均接入租户作用域。证据:8 个 release 定向测试文件。
|
||||
- [x] [CONCEPT: 后端职责] foundation/bootstrap 只查平台 seed,避免修改同编码企业资产。证据:foundation helper 拆分与回归测试。
|
||||
- [x] [CONCEPT: 后端职责] 风险运行时按企业优先、平台回退加载规则,禁止跨企业版本。证据:expense runtime/loader 改造。
|
||||
- [x] [CONCEPT: 风险场景测试] 场景请求校验目标企业,并将 `ExpenseClaim.tenant_id` 作为 SQL 第一层谓词。证据:真实样本场景与 TestRun 断言。
|
||||
- [x] [CONCEPT: ONLYOFFICE 状态机] 新增持久化 content/callback token、原子 claim 和终态处理。证据:`agent_asset_onlyoffice_security.py`。
|
||||
- [x] [CONCEPT: 权限与信任边界] content/callback 按会话重建 machine scope,拒绝跨企业、跨资产、跨版本和平台只读回写。证据:ONLYOFFICE 安全测试。
|
||||
- [x] [CONCEPT: 降级与回滚策略] 复用安全下载器,拒绝错误 origin、非公网 DNS、重定向、超限及异常 OOXML。证据:下载器单测与 callback 回归。
|
||||
- [x] [CONCEPT: 数据] 新增 `20260717_0026`,完成旧平台数据回填、约束、索引和有事实 downgrade 保护。证据:迁移文件与 PostgreSQL 探针。
|
||||
|
||||
## 4. 代码结构
|
||||
|
||||
- [x] [CONCEPT: 后端职责] 将访问策略、ONLYOFFICE 安全和序列化从大型 AgentAsset service 中拆出。证据:新增小职责模块,核心文件均低于 800 行。
|
||||
- [x] [CONCEPT: 后端职责] 将风险规则字段推断/草稿对齐从生成主服务抽到独立模块。证据:`risk_rule_generation.py` 619 行、`risk_rule_generation_fields.py` 272 行。
|
||||
- [x] [CONCEPT: 后端职责] foundation 按资产 helper、seed、topup、财务规则和电子员工任务拆分。证据:拆分模块与 28 项定向回归。
|
||||
|
||||
## 5. 前端契约
|
||||
|
||||
- [x] [CONCEPT: 前端契约] 后端 DTO 已返回 `tenantId/scope`,客户端可识别平台只读资产。证据:schema 与 AgentAsset API 回归。
|
||||
- [x] [CONCEPT: 前端契约] ONLYOFFICE 配置按权限返回 view/edit,平台资产仅平台管理员可编辑。证据:config 单测与平台回写拒绝测试。
|
||||
- [x] [CONCEPT: 目标与非目标] 本切片不做 Agent 资产管理页面视觉重构。证据:CONCEPT 非目标。
|
||||
|
||||
## 6. 测试与验证
|
||||
|
||||
- [x] [CONCEPT: 测试方案] 完成租户可见性、企业覆盖平台、无上下文平台只读、跨租户 404、目标企业场景和稳定身份测试。证据:`test_agent_asset_tenant_security.py`。
|
||||
- [x] [CONCEPT: 测试方案] 完成发布门禁、监控、召回、运行时、调度、遥测及 ONLYOFFICE callback 汇总回归。证据:容器内 58 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 完成 AgentAsset service 与 foundation 兼容回归。证据:容器内 28 项通过、4 项无关差旅计算器用例主动排除。
|
||||
- [x] [CONCEPT: 测试方案] 完成风险生成、修订和 golden evaluator 回归。证据:容器内 49 项通过,2 项旧发布门禁口径由对应切片处理。
|
||||
- [x] [CONCEPT: 测试方案] 完成费用风险租户接线回归。证据:13 项通过;2 项旧测试使用未保存 claim,已记录为共享测试夹具问题。
|
||||
- [x] [CONCEPT: 测试方案] 完成一次性 PostgreSQL 迁移验证。证据:旧数据升级、同编码多企业、非法约束、跨租户 FK、有事实 downgrade 拒绝,以及 `base → head`、`head → 0025 → head` 均通过。
|
||||
- [x] [CONCEPT: 指标与验收] 相关文件 Ruff 通过、py_compile 通过、ORM mapper 84 张表完成配置、`git diff --check` 通过。
|
||||
|
||||
## 7. 文档与上线
|
||||
|
||||
- [x] [CONCEPT: 本轮实现记录] 完成 CONCEPT、分阶段 TODO 和安全 bug 修复日志。证据:本目录及 `dev-logs/bugs/agent-asset-tenant-isolation-and-onlyoffice-security.md`。
|
||||
- [x] [CONCEPT: 风险与开放问题] 记录无 tenant 旧调用、未保存 claim 测试、旧发布 aggregate 口径和企业间受控复制边界。证据:CONCEPT 风险章节。
|
||||
- [ ] [CONCEPT: 降级与回滚策略] 上线前确认 ONLYOFFICE 下载 origin 在应用容器内解析为公网地址并使用可信 TLS。证据要求:生产白名单配置、容器 DNS/TLS 与实际编辑回写验证。
|
||||
- [ ] [CONCEPT: 降级与回滚策略] 上线前在备份副本执行 `0025 → head`,确认不存在无法归属的历史记录,并演练有事实情况下的受控回滚流程。
|
||||
@@ -0,0 +1,161 @@
|
||||
# 商业资源权威边界闭环 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
只对已经通过可信边界且随业务事务成功持久化的连接器事件和附件源文件写入计量,并在任何拒绝、失败或回滚时先释放额度、恢复文件,不生成虚假用量。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
商业底座已经具备套餐、权益、硬配额、执行前预占和追加式用量/成本事实,但真实资源入口仍存在两处断点:
|
||||
|
||||
- 金融连接器能验证 HMAC、阻断重放与 payload 冲突并持久化事件,却没有把“首次接受且提交成功”接到 `events` 权威计量。
|
||||
- 报销附件会在上传开始时直接删除旧目录再写新文件。若配额在写入后才判断,旧文件已经遭到破坏;若数据库随后回滚,文件系统和商业事实还可能与业务记录分叉。
|
||||
- 独立商业 Session 不能提交调用方业务 Session。反过来,商业结算也不能早于业务提交,否则数据库回滚后仍会留下客户用量。
|
||||
- SQLite 单连接测试环境中,另开 Session 检查“未配置计量器”会回滚同一底层连接上的业务事务;未配置路径必须先在调用方 Session 只读短路。
|
||||
|
||||
因此,本能力把“预占—业务变更—事务终态—结算/释放”定义为统一资源协议,而不是在端点成功返回前后零散补记用量。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- [G1] 金融连接器只对新建、已认证、已提交的 `FinancialConnectorEvent` 计量一个 `events`。
|
||||
- [G2] 鉴权失败、稳定重放、payload 冲突、配额拒绝和业务事务回滚均不产生连接器用量。
|
||||
- [G3] 附件上传按源文件 `bytes` 在覆盖旧文件前执行硬配额预占。
|
||||
- [G4] 附件业务事务回滚时恢复旧文件树、删除未提交的新文件并释放预占;提交后才追加用量。
|
||||
- [G5] 删除单附件、费用明细和整张报销单时,把文件删除绑定到数据库事务;回滚恢复、提交后最终清理。
|
||||
- [G6] 商业事实只保存哈希化操作身份、固定工具维度、数量和安全来源,不保存外部事件原文、关联号、单号、文件名或文件正文。
|
||||
- [G7] 同一已释放操作允许在相同账期重新预占,支持业务回滚后的安全重试。
|
||||
|
||||
### 非目标
|
||||
|
||||
- [NG1] 本切片不把附件 `bytes` 定义为实时磁盘容量;它表示成功持久化的源文件写入流量。
|
||||
- [NG2] 删除附件不追加正向用量,也不自动冲销历史写入事实;容量型 GB-month 计费需要独立快照账本。
|
||||
- [NG3] 不对连接器重放、鉴权失败和冲突运营事件收费。
|
||||
- [NG4] 不在商业事实中复制金融 payload、OCR 文本、文件名、报销事由或客户外部引用。
|
||||
- [NG5] 不在本切片新增商业配置页面或改变套餐价格。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
- 企业员工上传或替换报销附件:平台先检查源文件字节配额,再覆盖文件;数据库提交后才形成用量。
|
||||
- 企业员工删除附件、费用明细或草稿报销单:删除操作不被配额阻止,数据库失败时文件恢复。
|
||||
- 金融连接器发送回执:只有首次通过签名验证、去重并持久化的事件消费一个连接器事件额度。
|
||||
- 连接器因网络重试再次发送相同事件:返回既有业务响应,不重复预占或计量。
|
||||
- 平台运营排查账单:能看到 `connector/financial.ingest/events` 或 `storage/attachment.upload/bytes`,但看不到业务原文和敏感标识。
|
||||
|
||||
## 功能能力
|
||||
|
||||
- 连接器权威观察器:认证和首次事件判定之后预占,业务事务 `after_commit` 后结算。
|
||||
- 附件权威观察器:固定使用 `storage + attachment.upload + bytes`,以 `len(content)` 作为执行前与执行后的同一权威数量。
|
||||
- 事务回调批次:同一 Session 可登记多个资源操作;提交按登记顺序结算,回滚按相反顺序补偿。
|
||||
- 文件暂存事务:旧文件或目录先原子重命名为同目录隐藏备份;提交删除备份,回滚删除新目录并恢复原路径。
|
||||
- 删除事务:单附件、费用明细附件和整张报销单附件树均先暂存,数据库提交后才最终删除。
|
||||
- 已释放预占重开:相同指纹、相同订阅/权益/账期的 `released` 预占可重新进入 `reserved`,并重新执行硬配额判断。
|
||||
- 未配置兼容:调用方 Session 先只读确认没有匹配计量器,直接兼容执行且不创建商业事实。
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 前端
|
||||
|
||||
- 当前不新增页面。
|
||||
- 附件上传端点接受可选 `X-Request-ID`,商业硬配额拒绝返回 HTTP 429;现有 400/404 业务错误保持不变。
|
||||
- 客户端应为同一次上传重试复用 `X-Request-ID`。未提供时,服务端用认证会话、租户、Claim/Item 哈希和内容摘要形成保守幂等身份。
|
||||
|
||||
### 后端
|
||||
|
||||
- `FinancialConnectorCommercialObserver` 使用认证后配置租户、配置摘要和请求指纹构造哈希身份,固定工具维度为 `connector/financial.ingest`。
|
||||
- `FinancialConnectorIngestionService` 先完成认证、外部事件串行化和既有事实判断;只有新事件才申请预占。事件 flush 成功后把完成/释放绑定到调用方 Session。
|
||||
- `CommercialTransactionCallbacks` 把多个独立商业操作绑定到一个业务事务。`after_commit` 执行完成回调,`after_rollback` 逆序执行补偿回调;回调异常只记录日志并保留可补偿预占,不伪装业务提交失败。
|
||||
- `ExpenseClaimAttachmentCommercialObserver` 固定使用 `bytes`,配额拒绝发生在任何旧文件移动、`rmtree`、`unlink` 或新文件写入之前。
|
||||
- `ExpenseClaimAttachmentFileTransaction` 负责旧路径暂存、提交清理和回滚恢复;上传、删除附件、删除费用明细和删除报销单复用同一协议。
|
||||
- `CommercialRuntimeReservationService` 允许同一指纹的 `released` 预占在相同账期重新预占;重新检查当前合同、计量器快照和硬配额。
|
||||
- OCR 与 RuntimeChat 的直接商业桥同样增加调用方 Session 的只读未配置短路,避免 SQLite 单连接环境的独立 Session 回滚调用方事务。
|
||||
|
||||
### 算法/规则
|
||||
|
||||
- 连接器的权威数量恒为一个已提交事件:`actual_events = 1`。
|
||||
- 附件的权威数量为请求中源文件真实字节数:`actual_bytes = len(content)`。
|
||||
- 连接器只允许 `quantity_basis=events`;附件只允许 `quantity_basis=bytes`。错误基准在预占前失败关闭。
|
||||
- 同一业务事务包含多个附件操作时,回滚补偿使用 LIFO,确保同一 Item 连续替换也能恢复到事务开始前状态。
|
||||
|
||||
### 数据
|
||||
|
||||
- 不新增数据库表和迁移。
|
||||
- 复用 `commercial_runtime_reservations`、`usage_meter_events` 和可选 `commercial_cost_events`。
|
||||
- 连接器用量 metadata 只包含固定 meter 版本、哈希化 operation call、固定工具名、数量基准、权益、预占、provider 代码、结果和安全来源。
|
||||
- 附件用量不包含文件名、storage key、Claim/Item 原值、MIME、OCR 文本或文件正文。
|
||||
|
||||
### 权限
|
||||
|
||||
- 连接器租户只来自 HMAC 认证后配置,不能由未验证 header 或 payload 单独决定。
|
||||
- 附件租户只来自 `CurrentUserContext.tenant_id`,Claim 和 Item 已先通过费用单租户访问策略校验。
|
||||
- 付费或配额状态不能放宽现有报销状态、附件可变性、连接器签名、租户或财务对账规则。
|
||||
|
||||
### 降级策略
|
||||
|
||||
- 没有匹配计量器:保持原业务兼容,不写预占、用量或成本。
|
||||
- 商业配置错误、重复计量器、错误数量基准或硬配额不足:在业务/文件副作用前拒绝。
|
||||
- 业务事务回滚:释放预占;连接器不留事件用量,附件恢复原文件。
|
||||
- 商业完成回调异常:业务提交不反转;预占保持可审计状态,由既有补偿流程处理。
|
||||
- 文件恢复失败:记录错误并保留隐藏备份,不把失败删除误报为成功;需要运维根据事务日志和隐藏路径人工恢复。
|
||||
|
||||
## 算法与公式
|
||||
|
||||
### 硬配额判断
|
||||
|
||||
```text
|
||||
allowed = used_quantity + held_quantity + requested_quantity <= hard_limit
|
||||
```
|
||||
|
||||
- 连接器 `requested_quantity = 1 event`。
|
||||
- 附件 `requested_quantity = len(content) bytes`。
|
||||
- 判断在订阅与权益锁内完成;拒绝时真实连接器处理和附件破坏性操作尚未发生。
|
||||
|
||||
### 业务事务终态
|
||||
|
||||
```text
|
||||
permit -> business mutation -> commit -> usage(actual_quantity)
|
||||
-> rollback -> release reservation + restore files
|
||||
```
|
||||
|
||||
### 附件写入计量口径
|
||||
|
||||
```text
|
||||
billable_attachment_bytes = Σ len(source_content)
|
||||
```
|
||||
|
||||
- 只汇总成功随业务事务提交的源文件写入。
|
||||
- OCR 临时文件、预览图、metadata 和隐藏事务备份不进入本 meter。
|
||||
- 删除不产生负数;实时容量需另建周期快照和保留量口径。
|
||||
|
||||
## 测试方案
|
||||
|
||||
- 连接器:首次提交、稳定重放、payload 冲突、签名失败、业务回滚、回滚后重试、硬配额拒绝和敏感 metadata 反向断言。
|
||||
- 附件商业:bytes 提交、回滚释放、错误 basis、硬配额拒绝发生在旧文件移动前、相同事务连续替换逆序恢复。
|
||||
- 附件文件:单附件删除、费用明细删除、整单删除的 commit/rollback 文件终态。
|
||||
- 既有回归:连接器服务/端点/配置生命周期、附件归集任务、票据夹、报销端点、附件专项、OCR 与 RuntimeChat。
|
||||
- 质量:相关 Python 文件 Ruff;核心服务、Mixin 和端点继续低于 800 行。
|
||||
- 所有后端测试只在 `local-x-financial-linux` 容器内执行,单命令超时不超过 60 秒。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
- [A1] 每个首次提交的连接器事件最多一个 `events` 用量;重放、鉴权失败和冲突为零。
|
||||
- [A2] 连接器或附件业务回滚后 `usage_meter_events` 为零,预占为 `released`,相同操作可重新预占。
|
||||
- [A3] 附件配额拒绝时原文件内容和路径完全不变。
|
||||
- [A4] 附件数据库回滚后原文件恢复;提交后新文件/删除结果与数据库一致。
|
||||
- [A5] 用量 metadata 中不存在 external event ID、correlation ID、报销单号、文件名或业务正文。
|
||||
- [A6] 连接器、附件归集、报销端点、OCR、RuntimeChat 和商业直接运行相关回归在容器内通过。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
- 本地文件系统与数据库不是分布式事务。进程在文件暂存后、事务终态回调前被强杀时,隐藏备份可能需要启动恢复器扫描;当前正常异常和显式 commit/rollback 已闭环。
|
||||
- `X-Request-ID` 当前为可选。缺失时采用内容绑定的保守幂等身份,优先避免客户端网络重试重复收费;若未来按每次写请求收费,应把请求 ID 升级为必填并为业务上传建立持久幂等记录。
|
||||
- `bytes` 是源文件成功写入流量,不等于实时保留容量。若商业模式采用存储包或 GB-month,需要新增周期容量快照、删除后容量释放和归档层级计价。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-17:完成连接器首次接受事件的 `events` 预占—提交后结算,并确保重放、鉴权失败、payload 冲突、配额拒绝和回滚不计量。
|
||||
- 2026-07-17:完成附件源文件 `bytes` 执行前硬配额、事务文件暂存、上传/删除/费用明细删除/整单删除的提交与回滚闭环。
|
||||
- 2026-07-17:完成事务回调批次逆序补偿、未配置计量器调用方 Session 短路和已释放预占同账期安全重开。
|
||||
@@ -0,0 +1,59 @@
|
||||
# 商业资源权威边界闭环 开发 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
关联方案:[CONCEPT.md](./CONCEPT.md)
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 任务边界、计量口径和安全约束以 CONCEPT 对应章节为准。
|
||||
- `[x]` 只表示已有代码或容器验证证据;没有证据不得勾选。
|
||||
- 所有后端测试必须在 `local-x-financial-linux` 容器内运行,单命令最长 60 秒。
|
||||
|
||||
## 1. 调研与边界
|
||||
|
||||
- [x] [CONCEPT: 背景与问题] 盘点连接器认证、重放、冲突、业务 commit 和 operational event 边界。证据:`financial_connector_ingestion.py`、`financial_connectors.py` 与连接器服务/端点测试。
|
||||
- [x] [CONCEPT: 背景与问题] 盘点附件上传覆盖、单附件删除、费用明细删除、整单删除和批量归集事务。证据:`expense_claim_attachment_operations.py`、`expense_claims.py`、`expense_receipt_association.py`。
|
||||
- [x] [CONCEPT: 目标与非目标] 冻结连接器 `events` 与附件源文件写入 `bytes` 两个唯一 meter,不把删除伪装成正向用量。证据:两个商业 observer 的固定工具名和 required basis。
|
||||
|
||||
## 2. 契约与设计
|
||||
|
||||
- [x] [CONCEPT: 后端] 定义业务事务提交后结算、回滚释放的回调协议。证据:`commercial_transaction_callbacks.py`。
|
||||
- [x] [CONCEPT: 算法/规则] 定义 `events=1` 与 `bytes=len(content)` 的权威数量,错误 basis 预占前拒绝。证据:`required_quantity_basis` 与资源边界测试。
|
||||
- [x] [CONCEPT: 数据] 定义商业 metadata 脱敏白名单,不复制业务原文与敏感标识。证据:`test_connector_only_meters_new_authenticated_committed_event` 反向断言。
|
||||
|
||||
## 3. 后端实现
|
||||
|
||||
- [x] [CONCEPT: 后端] 连接器在认证和既有事件判断之后预占,事件提交后结算。证据:`financial_connector_commercial.py`、`financial_connector_ingestion.py`。
|
||||
- [x] [CONCEPT: 降级策略] 连接器重放、鉴权失败、冲突和事务回滚不写用量。证据:`test_commercial_resource_boundaries.py`。
|
||||
- [x] [CONCEPT: 后端] 附件在任何旧文件移动或新文件写入前按源文件 bytes 预占。证据:`stage_attachment_replacement()` 调用顺序与配额拒绝测试。
|
||||
- [x] [CONCEPT: 后端] 上传回滚恢复旧目录,提交后清理备份并追加用量。证据:附件 commit/rollback 资源测试。
|
||||
- [x] [CONCEPT: 后端] 单附件、费用明细和整张报销单删除均绑定数据库事务。证据:`stage_attachment_deletion()`、`stage_claim_attachment_deletion()` 与既有删除回归。
|
||||
- [x] [CONCEPT: 后端] 同一 Session 多操作提交顺序结算、回滚逆序恢复。证据:事务批次实现与连续替换 LIFO 测试。
|
||||
- [x] [CONCEPT: 降级策略] 未配置 meter 时在调用方 Session 只读短路,不让独立 SQLite Session 回滚业务。证据:Direct bridge `lookup_session`、附件归集 31 项通过。
|
||||
- [x] [CONCEPT: 后端] 允许相同 released 预占在相同账期重开并重新检查配额。证据:Direct operation released retry 测试与连接器回滚后重试测试。
|
||||
|
||||
## 4. 算法/规则实现
|
||||
|
||||
- [x] [CONCEPT: 硬配额判断] 使用 `used + held + requested <= hard_limit` 的既有锁内配额算法。证据:`CommercialRuntimeReservationService.reserve()` 与资源配额拒绝测试。
|
||||
- [x] [CONCEPT: 附件写入计量口径] 只结算成功持久化的源文件 bytes,不包含 OCR/预览/metadata。证据:observer authoritative quantity 与 metadata。
|
||||
- [x] [CONCEPT: 业务事务终态] 回滚不记 usage/cost,已释放操作可再次 permit。证据:商业直接运行与资源边界回归。
|
||||
|
||||
## 5. 前端实现
|
||||
|
||||
- [x] [CONCEPT: 前端] 上传端点接收 `X-Request-ID` 并把商业拒绝映射为 HTTP 429。证据:`reimbursements.py`。
|
||||
- [x] [CONCEPT: 目标与非目标] 本切片不新增商业或附件页面。证据:CONCEPT 非目标;本轮无前端文件变更。
|
||||
|
||||
## 6. 测试与验证
|
||||
|
||||
- [x] [CONCEPT: 测试方案] 连接器首次提交、重放、冲突、鉴权失败、回滚、重试、配额和脱敏通过。证据:容器内 `test_commercial_resource_boundaries.py` 9 项通过;商业资源、Direct、reservation、OCR、RuntimeChat 与连接器组合最终 `63 passed`。
|
||||
- [x] [CONCEPT: 测试方案] 商业 Direct、OCR、RuntimeChat 相关回归通过。证据:容器内组合 33 项通过;新增 released retry 后 Direct + 资源组合 22 项通过;最终商业/连接器组合 63 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 附件归集和票据夹回归通过。证据:容器内与报销端点最终组合 `54 passed`。
|
||||
- [x] [CONCEPT: 测试方案] 报销端点与附件专项通过。证据:容器内报销/归集组合 54 项、Expense Claim attachment `20 passed, 101 deselected`。
|
||||
- [x] [CONCEPT: 测试方案] 连接器既有服务、端点和配置生命周期回归通过。证据:容器内 15 项通过;最终纳入商业/连接器组合 63 项通过。
|
||||
- [x] [CONCEPT: 指标与验收] 相关 Python 文件 Ruff、compileall 通过,核心文件均低于 800 行。证据:容器检查退出码 0;最大相关文件 `expense_claim_attachment_operations.py` 为 787 行。
|
||||
|
||||
## 7. 文档收尾
|
||||
|
||||
- [x] [CONCEPT: 本轮实现记录] 创建 CONCEPT 与分阶段 TODO,记录 meter、事务、降级和验证口径。证据:本目录两份文档。
|
||||
- [x] [CONCEPT: 风险与开放问题] 记录进程强杀恢复、可选请求 ID 和 GB-month 容量计费边界。证据:CONCEPT 风险章节。
|
||||
@@ -0,0 +1,157 @@
|
||||
# AI 费用闭环工程收口与生产就绪边界
|
||||
|
||||
日期:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
把 X-Financial 收口为一条租户安全、可学习、可解释、可计量的费用闭环:用户从申请、票据、报销、预审、审批到付款归档尽量少填少等,企业能看见风险、节省和真实成本,同时不把模拟数据包装成生产价值。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
此前系统已经有申请、报销、审批、AI 助手和分析页面,但存在四类系统性断点:
|
||||
|
||||
- 业务链路能跑,但申请、票据、审批、支付、ERP、归档和价值事实没有统一闭环。
|
||||
- AI 能给建议,但用户反馈、工作流结果、记忆、few-shot 和发布质量没有形成受控学习链。
|
||||
- 单租户演示可用,但员工、知识、规则资产、Hermes、报告、缓存、向量库和文档编辑仍有跨租户风险。
|
||||
- 能展示费用,却不能严格区分确认现金节省、工时价值、风险暴露、预计机会、平台收入和内部成本。
|
||||
|
||||
本轮工程改造围绕上述断点逐步完成,不以页面数量或 mock 日志作为完成标准。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- 完成申请到付款、ERP、归档和冲回的可验证费用链路。
|
||||
- 让 AI 从可信的字段修改、提交、审批、付款、风险处置和人工标签中学习,并保留解释、撤销和发布门禁。
|
||||
- 对本轮纳入的 Claim、Employee、Agent Asset、Knowledge、Ontology、Hermes、Report 等共享核心数据建立可信会话、显式租户、复合约束、首层查询过滤和跨租户失败关闭;仍以 JSON 保存 tenant 的 legacy 状态继续列为后续迁移。
|
||||
- 建立 Savings Ledger、CFO 价值看板、商业权益、资源计量、客户 ROI、平台成本和定价走廊。
|
||||
- 将大型 Service 按访问策略、身份解析、持久化、规则、附件、计量和投影职责拆分,受代码体积门禁的核心类/组件保持低于 800 行。
|
||||
|
||||
### 非目标
|
||||
|
||||
- 不替客户决定首个支付/ERP provider、签名映射、会计期间、汇率来源、退款口径或大额双签阈值。
|
||||
- 不用 mock 回执冒充真实现金、真实开票、真实回款或真实客户 ROI。
|
||||
- 不在没有生产域名、可信 TLS、备份副本和真实 SMTP 的情况下声称完成生产上线。
|
||||
- 不以一次工程验证替代 30/90 天真实企业试点和商业定价验证。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
- 员工:通过 AI 预填、票据归集、结构化预审和断点续办,减少填表和退回。
|
||||
- 直属领导、预算负责人和财务:在租户安全的任务队列中处理例外、风险、豁免、支付和财务确认。
|
||||
- CFO/管理层:按币种和证据等级查看节省、机会、周期、预算、风险护栏和数据质量。
|
||||
- 租户管理员:管理企业知识、规则资产、记忆、报告配置和商业权益,但不能越权替业务人员自证。
|
||||
- 平台运营:管理套餐、订阅、计量、成本、发布门禁和连接器配置,不能跨租户读取业务正文。
|
||||
|
||||
## 功能能力
|
||||
|
||||
### 费用闭环
|
||||
|
||||
- Expense Case、Link、Business Event 将申请、票据、报销、预审、审批、付款、ERP、归档和冲回串成可回放链路。
|
||||
- 服务端预览决策、预审握手、审批动作和风险处置使用版本、指纹、请求 ID、事务和乐观前置条件防止陈旧重放。
|
||||
- 财务连接器区分 production-mode 外部事件契约、内部人工确认和 test/mock/staging 模拟事实;错误金额、币种、单据或状态不会推进付款,真实外部现金仍须 provider 联调证明。
|
||||
|
||||
### 越用越智能
|
||||
|
||||
- AI Decision、Feedback、Workflow Outcome、Memory Evidence 和 few-shot 按租户、主体、场景、规则版本与证据等级隔离。
|
||||
- 个人及企业/部门低敏偏好可解释、可过期、可撤销;企业规则和当前输入始终高于个人记忆。
|
||||
- 发布遥测从真实 observation、可信人工 label、盲审负样本和保守 recall 进入 Canary/Release Guard;证据不足保持 collecting。
|
||||
|
||||
### 风险与安全
|
||||
|
||||
- 不透明 Bearer 会话是身份事实;请求中的 tenant、actor、reviewer、role 不能覆盖服务端上下文。
|
||||
- Employee、Claim、Agent Asset、Knowledge、Ontology、Hermes、Report、Qdrant、文件路径和缓存键均按租户隔离。
|
||||
- ONLYOFFICE 使用数据库一次性会话、资源绑定 token、DNS/IP 校验、可信 origin、大小/MIME/OOXML 校验和重放拒绝。
|
||||
|
||||
### 节省与商业闭环
|
||||
|
||||
- Savings Ledger 分离 baseline、opportunity、realization、evidence 和 append-only event;未确认结果不进入确认现金 KPI。
|
||||
- CFO 看板将现金、工时、风险暴露和预计机会分开,并显式展示 collecting、unavailable 和 coverage gap。
|
||||
- 商业层分离套餐、订阅、权益、用量、内部成本、账期、客户 ROI、贡献毛利和定价建议。
|
||||
- Orchestrator、OCR、Runtime Chat、连接器和附件源文件写入均接入权威 permit/reserve/commit/release 计量边界。
|
||||
|
||||
## 方案设计
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["申请与票据"] --> B["服务端预览与预审"]
|
||||
B --> C["审批任务与风险处置"]
|
||||
C --> D["支付/ERP 连接器"]
|
||||
D --> E["归档与冲回"]
|
||||
B --> F["反馈、记忆与 few-shot"]
|
||||
C --> F
|
||||
D --> G["Savings Ledger"]
|
||||
E --> G
|
||||
G --> H["CFO 价值与 ROI"]
|
||||
A --> I["商业预占与计量"]
|
||||
B --> I
|
||||
D --> I
|
||||
F --> J["盲审遥测与 Release Guard"]
|
||||
```
|
||||
|
||||
核心边界如下:
|
||||
|
||||
1. 认证层生成可信 `CurrentUserContext`,业务入口不得从请求体补造身份。
|
||||
2. 业务服务以 `tenant_id` 作为第一层 SQL 条件,ORM 复合外键和数据库约束作为第二层保护。
|
||||
3. 状态与业务事件在同一事务提交;外部副作用和资源计量使用稳定请求 ID、追加事实和补偿状态。
|
||||
4. 学习只消费可信服务端事实;自由文本评论、mock 数据和未确认推断不得进入训练或价值 KPI。
|
||||
5. 分析投影只读事实账本,并保留币种、时间窗口、证据等级和数据质量状态。
|
||||
|
||||
## 数据与契约
|
||||
|
||||
- Alembic 正式链从 Expense Case、认证、AI 学习一直升级到 `20260717_0028`,migration-owned 表由启动前置检查统一管理。
|
||||
- `20260716_0015` 至 `0024` 建立 Savings、商业计量、连接器、发布遥测、账期、运行事件、盲审和资源数量口径。
|
||||
- `20260717_0025` 至 `0028` 建立租户身份、Agent Asset、Knowledge、Hermes/Ontology/Report 安全基础。
|
||||
- 关键 append-only 表由数据库 trigger 阻止 UPDATE/DELETE;幂等键和请求指纹区分安全重放与冲突载荷。
|
||||
- production-mode 签名事件、内部确认、模拟回执、确认节省、预计机会、收入和成本使用不同类型,不互相降级替代;本地自签事件不作为真实现金证据。
|
||||
|
||||
## 算法与规则
|
||||
|
||||
- 硬配额:`used + held + requested <= hard_limit`,预占在业务提交后结算,回滚后释放并允许同账期安全重试。
|
||||
- 确认现金节省:仅汇总 `finance_confirmed + canonical + cash` 的 realization,冲回通过负向追加事实抵消。
|
||||
- 客户 ROI:按币种分别计算确认价值与客户费用,不跨币种强行相加;证据不足返回 unavailable。
|
||||
- 贡献毛利:平台收入减去可归属模型、OCR、存储、连接器和其他运行成本;内部成本与客户价值分账。
|
||||
- 定价走廊:成本下限、确认价值上限、成功费封顶和合同约束共同决定建议,系统不自动替客户签订价格。
|
||||
- 发布门禁:只有 observation、独立可信 label、盲审负样本和保守置信下界达到阈值才允许晋级;失败或 collecting 保持 stable。
|
||||
|
||||
## 测试方案
|
||||
|
||||
所有后端、集成、迁移和依赖验证以 Docker 容器 `local-x-financial-linux` 的 `/app` 为唯一事实来源,单命令限制 60 秒。
|
||||
|
||||
- 后端:176 个测试文件按有界分片或专项运行,费用主服务 121 项单独回归;所有检查通过,条件跳过的 PostgreSQL 项随后在真实 PostgreSQL 探针补跑。
|
||||
- PostgreSQL:fresh schema、完整 upgrade/downgrade/re-upgrade、复合租户约束、append-only、迁移保护和并发专项最终 `87 passed / 0 skipped / 0 failed`,最终 head 为 `20260717_0028`。
|
||||
- 前端:Node 全量 `815 passed / 0 failed`,Vite production build 通过;仅保留 chunk size 提示。
|
||||
- 移动端:`npm run lint` 与 `npx tsc --noEmit` 通过;真实移动 API 与设备浏览器链路仍属于上线验收。
|
||||
- 静态质量:所有 197 个新增 Python 文件通过 Ruff;目标模块 compileall、受门禁核心类/组件 800 行检查和 `git diff --check` 通过。全仓 Ruff 仍包含既有基线格式债,不在本轮批量改写用户已有代码。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
### 工程验收
|
||||
|
||||
- A1:申请到支付/ERP/归档/冲回的纵向事实链可通过 E2E 重放。
|
||||
- A2:现金、工时、风险和预计机会分账,缺数据不伪造为 0。
|
||||
- A3:身份、租户、角色、业务范围和双人复核在服务端及数据库层失败关闭。
|
||||
- A4:费用基线、预算、异常归因、节省漏斗和 CFO 下钻使用租户安全事实。
|
||||
- A5:商业权益、配额、账期、用量、成本、ROI、毛利和定价建议可审计。
|
||||
- A6:AI 反馈、记忆、few-shot、盲审、Canary 和回滚均有证据等级和降级路径。
|
||||
- A7:迁移、并发、后端、前端、移动静态检查和差异检查形成可重复验证记录。
|
||||
- A8:文档明确区分工程完成、生产上线和真实商业验证,不用 mock 冒充后两者。
|
||||
|
||||
### 真实试点指标
|
||||
|
||||
以下指标必须由首个企业在 30/90 天试点中建立基线后评估:报销创建时间、自动填充率、首次提交完整率、退回率、人工触点、完成周期、风险反馈、确认现金节省、工时价值、客户 ROI 和平台贡献毛利。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
本轮约定的六个工程步骤已完成:商业资源计量与 EmployeeService 拆分、Expense Claim 访问策略与身份解析拆分、全链租户安全、后端/迁移/并发验证、前端/移动静态验证,以及文档与 bug 记录收口。
|
||||
|
||||
这六步范围内不再存在需要继续编码才能证明的阻断项。生产上线和商业验证仍需要目标环境与客户决策;上位长期路线图中统一 Outbox/legacy 清理、移动实机闭环、供应商事实和高级 AI 管理等扩展能力也没有被本轮文档悄悄标成完成,详见同目录 `TODO.md` 第 6-8 节。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
- ONLYOFFICE 生产下载域名必须在应用容器解析为允许的公网地址并使用可信 TLS;开发网络的保留地址会按设计拒绝。
|
||||
- `0025 → 0028` 必须在生产备份副本演练历史归属和受控回滚,不能用 disposable 空库代替真实数据演练。
|
||||
- 首个支付/ERP provider、字段映射、SLA、会计期间、汇率、批次拆分、退款和大额双签需要客户确认。
|
||||
- SMTP、企业报告收件人和实际投递审计需要逐租户配置。
|
||||
- 消息平台、移动设备真实流程、浏览器关键链路和私有部署安全验收需要目标环境联调。
|
||||
- 30/90 天基线、客户财务签字、目标毛利、价值分享比例、合同、税率、开票和回款边界不能由代码自行完成。
|
||||
- 上位长期路线图仍保留统一 correlation/Outbox、旧模型收敛、完整移动端、供应商事实、消息/SSO 模板、数据导出与高级 AI 管理等产品扩展;它们不阻断本轮六步收口,但属于“完整产品愿景”后续工作。
|
||||
@@ -0,0 +1,81 @@
|
||||
# AI 费用闭环工程收口与生产就绪 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
关联方案:[CONCEPT.md](./CONCEPT.md)
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 每项必须回链 `CONCEPT.md`;没有代码、迁移、接口、容器或真实环境证据不得勾选。
|
||||
- `[x]` 代表本轮工程范围已完成,不代表生产环境或真实商业试点自动完成。
|
||||
- mock/test/staging 只能验证契约和降级,不能证明真实现金、开票、回款、客户 ROI 或生产可用性。
|
||||
|
||||
## 1. 功能闭环
|
||||
|
||||
- [x] [CONCEPT: 费用闭环] 完成申请、票据、报销、预审、审批、付款、ERP、归档和冲回的可回放链路。
|
||||
证据:Expense Case/Business Event、财务连接器、Savings Ledger;`test_expense_financial_value_chain_e2e.py` 与相关服务测试通过。
|
||||
- [x] [CONCEPT: 越用越智能] 完成可信反馈、工作流结果、个人/企业记忆、few-shot、盲审遥测、Canary 和 Release Guard。
|
||||
证据:AI learning/memory、release telemetry/review/recall 模块;相关后端与 PostgreSQL 并发测试通过。
|
||||
- [x] [CONCEPT: 节省与商业闭环] 完成 Savings Ledger、CFO 价值看板、商业权益/计量/成本/ROI/定价建议。
|
||||
证据:0015/0016/0019/0021/0024 迁移,Savings/CFO/Commercial 服务、端点和前端组件。
|
||||
|
||||
## 2. 租户安全与代码结构
|
||||
|
||||
- [x] [CONCEPT: 风险与安全] 收口 Bearer 会话、Claim、Employee、Agent Asset、Knowledge、Ontology、Hermes、Report、Qdrant、文件和缓存租户边界。
|
||||
证据:0025-0028 迁移与 tenant security 测试;生产 `CurrentUserContext` 无缺失 tenant 构造。
|
||||
- [x] [CONCEPT: 风险与安全] 完成 ONLYOFFICE 一次性会话、资源绑定、SSRF/DNS/IP、格式和重放保护。
|
||||
证据:Agent Asset/Knowledge ONLYOFFICE 安全服务与回归测试。
|
||||
- [x] [CONCEPT: 目标与非目标] 完成大型核心模块职责拆分,保持受门禁核心类/组件低于 800 行。
|
||||
证据:Employee 776 行、ExpenseClaimAccessPolicy 701 行;访问策略、身份解析、目录维护、附件计量等均为独立模块。
|
||||
|
||||
## 3. 商业资源权威边界
|
||||
|
||||
- [x] [CONCEPT: 节省与商业闭环] Orchestrator、OCR、Runtime Chat、连接器和附件写入接入 permit/reserve/commit/release。
|
||||
证据:commercial direct/runtime bridge、connector observer、attachment commercial;资源组合 63 项、附件/端点 54 项通过。
|
||||
- [x] [CONCEPT: 算法与规则] 连接器仅按已认证且成功提交的事件计 `events=1`,附件仅按成功持久化源文件计 bytes。
|
||||
证据:`test_commercial_resource_boundaries.py` 覆盖重放、冲突、鉴权失败、回滚、配额、重试和脱敏。
|
||||
- [x] [CONCEPT: 算法与规则] 回滚释放、同账期安全重试、硬配额和未配置兼容均保持事务正确。
|
||||
证据:Direct、reservation、OCR、Runtime Chat、连接器和附件组合回归通过。
|
||||
|
||||
## 4. 容器验证
|
||||
|
||||
- [x] [CONCEPT: 测试方案] 后端 176 个测试文件完成有界分片或专项检查,费用主服务 121 项单独通过。
|
||||
证据:所有分片退出码 0;条件 PostgreSQL 跳过已在真实探针补跑。
|
||||
- [x] [CONCEPT: 测试方案] fresh PostgreSQL 完成迁移、降级、再升级、并发和数据库约束专项。
|
||||
证据:`87 passed / 0 skipped / 0 failed`,最终 head `20260717_0028`,一次性数据库已清理。
|
||||
- [x] [CONCEPT: 测试方案] Web 全量测试和生产构建通过。
|
||||
证据:`815 passed / 0 failed`;Vite production build 通过。
|
||||
- [x] [CONCEPT: 测试方案] Mobile lint 与 TypeScript 静态检查通过。
|
||||
证据:`npm run lint`、`npx tsc --noEmit` 均退出码 0。
|
||||
- [x] [CONCEPT: 测试方案] 新增 Python、编译、文件大小和差异质量门禁通过。
|
||||
证据:197 个新增 Python 文件 Ruff 通过;目标 compileall、受门禁核心类/组件 800 行检查和 `git diff --check` 通过;全仓历史 Ruff 债单独披露。
|
||||
|
||||
## 5. 文档与可追溯性
|
||||
|
||||
- [x] [CONCEPT: 本轮实现记录] 回填 Savings、商业、连接器、发布遥测和上位 AI 闭环 TODO 的真实完成状态。
|
||||
证据:2026-07-13、2026-07-16 对应功能文档及本目录。
|
||||
- [x] [CONCEPT: 本轮实现记录] 为本轮生产 bug 创建独立修复日志,并先完成 upstream/local-ahead 检查。
|
||||
证据:`document/development/2026-07-17/dev-logs/bugs/`;`origin/main` 无新提交,本地 ahead 17 已记录。
|
||||
- [x] [CONCEPT: 指标与验收] 明确工程完成、生产上线、真实试点三种完成口径。
|
||||
证据:CONCEPT“目标与非目标”“指标与验收”“风险与开放问题”。
|
||||
|
||||
## 6. 生产上线(需要目标环境)
|
||||
|
||||
- [ ] [CONCEPT: 风险与开放问题] 配置生产 ONLYOFFICE 允许 origin,并在应用容器验证公网 DNS、可信 TLS 和真实编辑回写。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 在生产备份副本演练 `0025 → 0028`、历史默认企业归属、回滚保护和恢复。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 为每个启用企业配置 SMTP、报告收件人并验证实际投递审计。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 完成真实浏览器、移动设备、消息平台和私有部署环境的关键流程验收。
|
||||
|
||||
## 7. 客户与商业验证(需要业务决策)
|
||||
|
||||
- [ ] [CONCEPT: 风险与开放问题] 确认首个支付/ERP provider、签名、字段映射、SLA、重试、批次、汇率、会计期间、退款和大额双签口径。
|
||||
- [ ] [CONCEPT: 真实试点指标] 采集首个企业 30/90 天基线并验证效率、风险、现金节省、工时价值和客户 ROI。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 由客户财务签字确认节省归因、去重、汇率、工时价值和报告口径。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 冻结目标毛利、包含量、超额策略、价值分享、合同、税率、开票、回款、坏账和收入确认。
|
||||
|
||||
## 8. 长期产品路线图(不属于本轮六步阻断)
|
||||
|
||||
- [ ] [CONCEPT: 风险与开放问题] 统一所有阶段 correlation/事务 Outbox,并完成 `agent_conversations` 结构化租户、旧 `ReimbursementRequest`、`risk_flags_json` 和少见状态迁移。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 完成移动端真实 API、拍照/OCR/票据/草稿实机闭环,以及全浏览器键盘、焦点和响应式验收。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 接入租户化供应商/合同/单位价格事实、真实消息/SSO/电子档案模板、删除传播和数据导出。
|
||||
- [ ] [CONCEPT: 风险与开放问题] 扩展“我的 AI 记忆”、保留/敏感级别/动作上限配置,以及自动化依据、撤销、抽检和版本可视化。
|
||||
@@ -0,0 +1,106 @@
|
||||
# Hermes、本体解析与财务报告多租户安全 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
让本体解析、员工行为画像、Hermes 扫描、数字员工看板和定时财务报告从请求到存储全程绑定可信企业,并在任何租户上下文缺失或冲突时停止运行。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
本体解析曾在建立 `AgentRun` 前调用模型,并从全局员工、组织、客户、供应商、项目和单据字典构造提示词;员工画像详情可按任意员工 ID 查询;Hermes 扫描、看板和财务报告存在全表读取、全局收件人以及跨企业复用存储路径的风险。内部 Orchestrator 调用还可在没有认证用户时继续运行,无法证明 `AgentRun` 的企业归属。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- HTTP 入口只信任 `CurrentUserContext.tenant_id`,内部任务只接受显式、已注册且启用的 `trusted_tenant_id`。
|
||||
- 本体解析在任何模型调用前创建租户化 `AgentRun`,商业运行上下文和失败证据可追溯。
|
||||
- 员工、组织、费用、应收、应付、画像、风险、提醒和看板查询在首个 SQL 中限定租户。
|
||||
- 员工画像仅允许本人、直属领导、财务/高管或管理员读取;越权和跨租户统一返回 404。
|
||||
- 财务报告按租户配置收件人、生成内容、存储文件和幂等运行账本,不使用全局邮件回退。
|
||||
- 所有 Hermes 定时任务逐个枚举 `status=active` 的企业,运行记录同时在 route/ontology 保存租户快照。
|
||||
|
||||
### 非目标
|
||||
|
||||
- 不提供客户端选择或覆盖租户的兼容参数。
|
||||
- 不实现跨企业合并分析、集团穿透报表或平台运营后台。
|
||||
- 不在本切片中重构预算分配等仍属旧模型的全部业务域。
|
||||
|
||||
## 信任边界
|
||||
|
||||
```text
|
||||
HTTP 请求 ── CurrentUserContext.tenant_id ─┐
|
||||
├─→ tenant-scoped AgentRun
|
||||
内部任务 ── active trusted_tenant_id ──────┘ ├─ route_json.tenant_id
|
||||
└─ ontology_json.tenant_id
|
||||
```
|
||||
|
||||
- 客户端 `context_json.tenant_id` 会被可信租户覆盖,不能参与授权。
|
||||
- 同时传入登录用户和内部租户时,两者必须一致。
|
||||
- 内部任务租户必须存在于租户注册表且状态为 active;缺失、停用或不存在均在创建 Run 前拒绝。
|
||||
- 后台消费者从数据库 Run 恢复租户,并校验 route/ontology 两份快照一致。
|
||||
|
||||
## 功能设计
|
||||
|
||||
### 本体解析
|
||||
|
||||
- 接口覆盖请求中的 user id,并把认证企业传给 `SemanticOntologyService`。
|
||||
- `parse()` 先建立 `running/pending_model_analysis` Run,再加载租户字典、商业运行上下文并调用模型。
|
||||
- 模型、规则降级和失败路径都更新同一 Run;失败不会留下无企业、无状态的调用。
|
||||
- 员工、部门、费用申请、应收、应付和项目参考目录均以租户作为第一层 SQL 条件。
|
||||
|
||||
### 员工画像
|
||||
|
||||
- 快照保存 `tenant_id`,并用 `(tenant_id, subject_id)` 复合外键绑定员工。
|
||||
- Profile Service 的快照、费用单和 Agent Run 查询均先限定租户。
|
||||
- 详情接口先在当前企业解析目标员工,再执行本人/直属领导/财务/高管/管理员访问矩阵。
|
||||
- 附带 `claim_id` 时必须同时属于目标员工和当前企业。
|
||||
|
||||
### Hermes 与数字员工
|
||||
|
||||
- 风险扫描、画像扫描、风险线索、提醒扫描和看板均接受显式租户。
|
||||
- 调度器只枚举 active 企业,并为每个企业独立创建任务与结果。
|
||||
- 看板只统计 route/ontology 租户快照均匹配的 Run;缺少或冲突快照的历史记录不会被猜测归属。
|
||||
- 提醒任务的费用单、员工和关联报销查询在首个 SQL 限制当前企业。
|
||||
|
||||
### 财务分析报告
|
||||
|
||||
- `TenantFinanceReportConfig` 保存企业启停状态和经校验的收件人。
|
||||
- `TenantFinanceReportRun` 以 `(tenant_id, idempotency_key)` 防止同企业同周期重复发送。
|
||||
- 报告上下文只读取当前企业的费用、风险、画像和 Agent Run。
|
||||
- 邮件发送只使用当前企业配置;未配置、停用或无有效收件人时 fail-closed。
|
||||
- 文件存储目录使用企业标识哈希,避免路径注入和跨企业文件覆盖。
|
||||
|
||||
## 数据与迁移
|
||||
|
||||
`20260717_0028_hermes_ontology_tenant_security.py` 完成:
|
||||
|
||||
- 员工行为画像、Hermes 任务配置/日志和风险报告新增租户列、索引、唯一约束和复合外键。
|
||||
- 新增租户财务报告配置和周期运行账本。
|
||||
- 旧记录回填为 `default`,再移除 server default,后续写入必须显式确定企业。
|
||||
- 仅支持 PostgreSQL;upgrade/downgrade 在任何 DDL 前检查方言,其他数据库直接拒绝。
|
||||
- downgrade 在存在非默认企业事实或报告账本时拒绝有损回滚。
|
||||
|
||||
## 降级与回滚
|
||||
|
||||
- 租户缺失、停用、不存在或上下文冲突:不创建 Run、不查询业务数据。
|
||||
- 跨企业员工、画像、费用单:统一按不存在处理。
|
||||
- 企业未配置报告收件人:保留失败/跳过证据,不回退环境变量中的全局地址。
|
||||
- 历史 Run 没有双租户快照:看板不纳入企业统计。
|
||||
- 生产升级前必须在数据库备份副本演练 `0027 → 0028 → 0027 → 0028`。
|
||||
|
||||
## 测试与验收
|
||||
|
||||
- 两企业本体目录、画像 IDOR、Hermes 扫描、风险线索、看板和报告内容/收件人/路径/幂等隔离。
|
||||
- Orchestrator 缺租户、停用租户、不存在租户和认证/内部租户冲突均在 Run 前拒绝。
|
||||
- 本体模型调用前存在 running Run,失败后仍保存同企业失败证据。
|
||||
- PostgreSQL 完成 fresh `base → 0028`、`0028 → 0027 → 0028` 及旧表回填/外键验证。
|
||||
- 所有后端测试和 Ruff 均在 `local-x-financial-linux` 容器内执行,单命令不超过 60 秒。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-17:完成本体解析前置 Run、可信商业上下文和租户参考目录。
|
||||
- 2026-07-17:完成员工画像模型、服务和 API 的租户隔离与访问矩阵。
|
||||
- 2026-07-17:完成 Hermes 扫描、提醒、看板、调度和财务报告的逐租户运行。
|
||||
- 2026-07-17:完成 `20260717_0028` PostgreSQL 迁移、反向回滚和安全回归。
|
||||
@@ -0,0 +1,40 @@
|
||||
# Hermes、本体解析与财务报告多租户安全 开发 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
关联方案:[CONCEPT.md](./CONCEPT.md)
|
||||
|
||||
## 1. 信任边界
|
||||
|
||||
- [x] [CONCEPT: 信任边界] HTTP 入口只使用认证用户企业,覆盖请求中的 user/tenant 上下文。
|
||||
- [x] [CONCEPT: 信任边界] Orchestrator 内部调用必须显式传入已注册、启用的 `trusted_tenant_id`。
|
||||
- [x] [CONCEPT: 信任边界] Agent Run 的 route/ontology 同时保存租户快照,冲突时 fail-closed。
|
||||
|
||||
## 2. 本体与画像
|
||||
|
||||
- [x] [CONCEPT: 本体解析] 在模型调用前创建 running Run,并在成功、规则降级和失败路径闭环状态。
|
||||
- [x] [CONCEPT: 本体解析] 员工、组织、费用、应收、应付和项目目录全部首 SQL 租户过滤。
|
||||
- [x] [CONCEPT: 员工画像] 画像模型新增租户字段、复合员工外键和租户索引。
|
||||
- [x] [CONCEPT: 员工画像] 实现本人、直属领导、财务/高管/管理员访问矩阵及 claim 归属校验。
|
||||
|
||||
## 3. Hermes 与报告
|
||||
|
||||
- [x] [CONCEPT: Hermes 与数字员工] 风险扫描、画像扫描、风险线索、提醒和看板接入显式租户。
|
||||
- [x] [CONCEPT: Hermes 与数字员工] 调度器逐 active 企业运行,不创建默认或全局扫描。
|
||||
- [x] [CONCEPT: 财务分析报告] 新增企业报告配置、收件人校验和周期幂等账本。
|
||||
- [x] [CONCEPT: 财务分析报告] 报告上下文、邮件收件人和文件路径按企业隔离。
|
||||
|
||||
## 4. 数据与验证
|
||||
|
||||
- [x] [CONCEPT: 数据与迁移] 完成 `20260717_0028` 数据回填、约束、索引、报告表和 downgrade 保护。
|
||||
- [x] [CONCEPT: 数据与迁移] upgrade/downgrade 在 DDL 前拒绝非 PostgreSQL 方言。
|
||||
- [x] [CONCEPT: 测试与验收] 完成本体、画像、Hermes、报告和 Orchestrator 两企业安全测试。
|
||||
- [x] [CONCEPT: 测试与验收] 完成旧本体 72 项、Orchestrator 16 项及认证/关联草稿 18 项回归。
|
||||
- [x] [CONCEPT: 测试与验收] 完成一次性 PostgreSQL fresh、回滚重升和旧结构迁移探针。
|
||||
- [x] [CONCEPT: 测试与验收] 相关 Python 文件通过容器 Ruff,Git diff 无空白错误。
|
||||
|
||||
## 5. 上线
|
||||
|
||||
- [x] [CONCEPT: 本轮实现记录] 完成概念文档、TODO 和安全修复日志。
|
||||
- [ ] [CONCEPT: 降级与回滚] 上线前在生产备份副本执行 `0027 → 0028 → 0027 → 0028`,确认历史默认企业归属。
|
||||
- [ ] [CONCEPT: 财务分析报告] 为每个启用企业确认 SMTP、报告收件人和实际投递审计,不使用全局兜底地址。
|
||||
@@ -0,0 +1,162 @@
|
||||
# 知识库多租户隔离与安全编辑 概念文档
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
## 功能一句话
|
||||
|
||||
让每个企业只读写自己的知识文件、元数据和 LightRAG/Qdrant 命名空间,同时以平台制度只读层和一次性 ONLYOFFICE 会话安全地贯通知识查询、预览与编辑。
|
||||
|
||||
## 背景与问题
|
||||
|
||||
原知识库把所有企业的文件、`.index.json`、`.lightrag`、运行时缓存和 Qdrant workspace 放在同一全局空间。API、后台索引和定时任务还能在没有可信 `tenant_id` 时继续工作,导致同名文件覆盖、跨租户列表/详情/原文读取、索引串读和错误的默认租户归属风险。
|
||||
|
||||
ONLYOFFICE 原回调仅依赖可伪造或可重放的短 payload,并直接下载回调 URL。匿名请求可以借此访问内部地址、跟随重定向、下载超大或错误格式内容,最终覆盖知识文件。预览会话与编辑会话也没有不可变的租户、资源、文档 key、版本和一次性状态绑定。
|
||||
|
||||
## 目标与非目标
|
||||
|
||||
### 目标
|
||||
|
||||
- 文件、索引 JSON、LightRAG 本地状态、运行时实例和 Qdrant workspace 全部按可信租户隔离。
|
||||
- 旧全局制度资料无损复制到显式 `platform` 空间,并始终以只读方式提供给租户。
|
||||
- 列表、详情、原文、上传、删除、同步和查询均从认证用户或数据库 Agent Run 获取租户,不接受客户端上下文覆盖。
|
||||
- ONLYOFFICE token 同时绑定 tenant、resource scope、document、key、version、editable、audience、expiry 和 JTI。
|
||||
- 编辑回调以数据库状态机实现一次性消费;预览会话永不回写。
|
||||
- 回调下载仅访问配置的文档服务,拒绝重定向、私网/回环/链路本地解析、DNS 重绑定、超限和非 OOXML 内容。
|
||||
- 定时索引只枚举租户注册表中的 `active` 租户,不再使用默认租户。
|
||||
|
||||
### 非目标
|
||||
|
||||
- 不允许租户经普通 API 新建或修改平台制度;平台资料由受控部署流程维护。
|
||||
- 不把租户知识数据合并成一个共享向量集合后再依赖过滤器补救。
|
||||
- 不为私网 ONLYOFFICE 地址提供安全降级开关;不满足公网解析要求时回写必须 fail-closed。
|
||||
- 不在本切片中实现知识内容质量评估、模型微调或新的知识运营前端。
|
||||
|
||||
## 用户与场景
|
||||
|
||||
- 租户管理员:上传、覆盖、删除和触发当前企业的知识同步。
|
||||
- 普通员工:联合查询本企业知识与平台只读制度,预览有权限的原文。
|
||||
- 知识运营人员:在受控编辑模式中修改租户 Office 文档,保存后形成新版本。
|
||||
- Hermes 调度器:逐个处理活跃租户的增量知识,不创建或猜测默认租户。
|
||||
- 平台管理员:通过部署资产提供跨租户可见但不可写的平台制度。
|
||||
|
||||
## 功能能力
|
||||
|
||||
- 租户目录:`storage/knowledge/tenants/{tenant_id}/`。
|
||||
- 平台目录:`storage/knowledge/platform/`,所有业务入口只读。
|
||||
- 每个作用域独立 `.index.json`、`.lightrag`、workspace 和运行时缓存键。
|
||||
- 租户查询采用“租户空间 + 平台只读空间”隔离检索后融合,不在存储层混库。
|
||||
- 旧 `storage/knowledge/{固定目录}` 与旧索引首次启动时复制到平台空间,源文件不删除。
|
||||
- ONLYOFFICE content token 默认 5 分钟;callback session 默认 4 小时、最长 12 小时,并在首次写回前原子进入 `processing`。
|
||||
- 回写成功进入 `consumed`,失败进入 `failed`;已消费、失败、撤销或过期会话不能再次写入。
|
||||
- 后台索引线程从数据库 Agent Run 的 route/ontology 双份租户上下文重新校验,线程参数只能作为一致性断言。
|
||||
|
||||
## 方案设计
|
||||
|
||||
### 前端契约
|
||||
|
||||
- 知识文档新增 `scope` 与 `readOnly` 字段。
|
||||
- 平台文档可以查看、下载和预览,但编辑入口必须隐藏或禁用。
|
||||
- ONLYOFFICE 配置默认 `mode=view`;只有租户管理员显式请求 `editable=true` 时才返回编辑配置。
|
||||
- 回调和 content URL 中的 token 是文档服务专用凭证,不暴露为普通用户授权能力。
|
||||
|
||||
### 后端职责
|
||||
|
||||
- `knowledge_tenant_scope`:校验租户标识,生成文件路径、workspace 和缓存键,执行旧资料到平台空间的无损迁移。
|
||||
- `knowledge`:编排租户/平台文档、权限、文件操作、查询融合和 ONLYOFFICE 配置。
|
||||
- `knowledge_index_state`:管理当前作用域的 JSON 元数据与 ingest 状态。
|
||||
- `knowledge_rag`:只使用当前作用域的 working dir、workspace、缓存实例与 Qdrant 命名空间。
|
||||
- `knowledge_run_scope`:从持久化 Agent Run 的 route/ontology 双份上下文还原并校验后台任务租户。
|
||||
- `knowledge_onlyoffice_security`:签发/验证会话、原子消费 JTI、执行安全网络下载与 OOXML 校验。
|
||||
- `knowledge_onlyoffice_callback`:按 token 中已验证的资源作用域定位文件,校验基线后回写新版本。
|
||||
- `knowledge_scheduler`:从 `tenants.status=active` 枚举租户并创建显式内部身份。
|
||||
|
||||
### 数据
|
||||
|
||||
新增 migration-owned 表 `knowledge_onlyoffice_sessions`:
|
||||
|
||||
- 身份:`jti`、`tenant_id`、`resource_scope`、`document_id`。
|
||||
- 不可变绑定:`document_key`、`document_version`、`audience`、`editable`、`created_by`、`expires_at`。
|
||||
- 生命周期:`active → processing → consumed | failed`,另支持 `revoked`。
|
||||
- 平台会话仍绑定发起租户,但数据库约束要求 `editable=false`。
|
||||
- `tenant_id` 外键指向租户注册表;租户删除时清理其会话。
|
||||
- 存在会话证据时 migration downgrade 拒绝有损删除。
|
||||
|
||||
### 权限与信任边界
|
||||
|
||||
- HTTP 文档 API 只使用 `CurrentUserContext.tenant_id`。
|
||||
- 同步任务只使用认证用户租户;后台线程再与数据库 Agent Run 租户交叉校验。
|
||||
- 平台 scope 必须由服务端显式构造,客户端不能通过参数选择。
|
||||
- 租户文档 ID 在其他租户下按不存在处理;平台文档仅作为只读 fallback。
|
||||
- ONLYOFFICE token 必须同时通过签名、issuer、audience、scope、时间、数据库行和全部资源字段比较。
|
||||
- callback token 只有写入状态 `2/6` 才尝试 claim;key 不匹配时不会消耗会话。
|
||||
- 下载 URL 的 origin 必须等于配置白名单;DNS 解析的全部地址必须为公网地址,实际连接固定到已校验 IP,HTTPS 仍校验原主机证书和 SNI。
|
||||
- 不跟随 3xx;限制响应大小、MIME、ZIP 条目数、解压体积、路径穿越、加密条目和 OOXML 目录结构。
|
||||
|
||||
### 查询算法
|
||||
|
||||
租户查询先在两个物理隔离空间各自检索,再对候选做确定性融合:
|
||||
|
||||
```text
|
||||
tenant_workspace = base + "__tenant_" + SHA256(tenant_id)[0:20]
|
||||
platform_workspace = base + "__platform"
|
||||
|
||||
merged_hits = top_k(
|
||||
deduplicate(tenant_hits ∪ platform_hits, by=code),
|
||||
order_by=score DESC
|
||||
)
|
||||
```
|
||||
|
||||
运行时缓存同样使用租户哈希键,不把原始 tenant ID 放进 Qdrant workspace 名称。
|
||||
|
||||
### ONLYOFFICE 状态机
|
||||
|
||||
```text
|
||||
issue(view) → active ── callback status 2/6 ──拒绝写入
|
||||
issue(edit) → active ── atomic claim ──→ processing
|
||||
├─验证/下载/写入成功→ consumed
|
||||
└─任一步失败────────→ failed
|
||||
|
||||
active -- exp 超时 --> 验证拒绝
|
||||
consumed/failed/revoked -- replay --> 409/拒绝
|
||||
```
|
||||
|
||||
### 降级策略
|
||||
|
||||
- 租户缺失、格式非法或与 Agent Run 冲突:拒绝任务,不回退默认租户。
|
||||
- 平台迁移源不存在:建立空平台作用域,不影响租户空间。
|
||||
- 平台或租户 RAG 不可用:保留已有本地检索降级;不会改查其他租户 workspace。
|
||||
- ONLYOFFICE 地址、JWT、DNS、MIME 或 OOXML 校验失败:返回错误并保持原文件不变。
|
||||
- 当前容器若通过网络代理把文档域名解析为非公网保留地址,真实回写会按安全策略拒绝;部署需提供满足公网解析与 TLS 的文档服务或安全反向代理。
|
||||
|
||||
## 测试方案
|
||||
|
||||
- 服务:两租户同名文件、列表/详情/原文隔离、平台只读 fallback、无 scope fail-closed。
|
||||
- RAG:workspace、缓存键、本地 chunks 和运行签名隔离。
|
||||
- 后台:只枚举 active tenant;worker 从 Agent Run 重取租户并拒绝 route/ontology 冲突。
|
||||
- ONLYOFFICE:tenant/resource/key/version/aud/exp/JTI 绑定,平台只读,会话过期,错 key 不 claim,成功只写一次,重放拒绝。
|
||||
- SSRF:错误 origin、私网 DNS、重定向、IP pinning、超限、错误 MIME、损坏 OOXML。
|
||||
- 迁移:revision/down_revision、表/外键/check、非 PostgreSQL 拒绝、ownership/preflight 和有证据 downgrade 拒绝。
|
||||
- 所有后端验证只在 `local-x-financial-linux` 容器内执行,单命令超时不超过 60 秒。
|
||||
|
||||
## 指标与验收
|
||||
|
||||
- 100% 租户知识文件、索引、LightRAG 与 Qdrant workspace 可由可信 tenant 唯一确定。
|
||||
- 跨租户文档读取、删除、同步和查询用例 100% 拒绝或不可见。
|
||||
- 平台文档 `readOnly=true`,任何编辑回调均不产生文件副作用。
|
||||
- 同一 callback JTI 最多一次进入 `processing`,重复回调 100% 拒绝。
|
||||
- 非白名单 origin、非公网解析、重定向、超限或非 OOXML 回写 100% fail-closed。
|
||||
- 定时任务只为注册表 `active` 租户建任务;无活跃租户时不生成默认数据。
|
||||
- 新增/修改 Python 文件通过 Ruff 与 compileall,定向知识安全、既有知识回归和迁移 ownership 测试全部通过。
|
||||
|
||||
## 风险与开放问题
|
||||
|
||||
- 当前 JSON 索引采用原子文件级写入之外的旧读改写模型;同一租户多进程高并发上传仍应在后续演进为数据库元数据或跨进程锁。
|
||||
- 平台制度的发布、签名和回滚需要独立的受控平台资产流程,本切片只保证业务 API 只读。
|
||||
- 部署必须确认 ONLYOFFICE 下载域名从应用容器解析为真实公网地址;不得为开发便利开放私网通配。
|
||||
- 长时编辑使用 4 小时 callback session;超过时限需重新打开文档生成新会话。
|
||||
|
||||
## 本轮实现记录
|
||||
|
||||
- 2026-07-17:完成租户/平台文件与 RAG 命名空间拆分、旧资料无损平台迁移、API 与后台任务可信租户接线。
|
||||
- 2026-07-17:完成 DB-backed ONLYOFFICE 一次性会话、平台只读预览、文档 key/version 基线校验和 SSRF/大小/MIME/OOXML 防护。
|
||||
- 2026-07-17:完成 `20260717_0027` 迁移、模型 ownership/preflight 注册、定时器 active tenant 枚举及容器定向回归;共享 PostgreSQL 探针完成 `base → 0028` 与 `0028 → 0025 → 0028`,Knowledge/AgentAsset 两类会话表均正常落库。
|
||||
@@ -0,0 +1,63 @@
|
||||
# 知识库多租户隔离与安全编辑 开发 TODO
|
||||
|
||||
更新时间:2026-07-17
|
||||
|
||||
关联方案:[CONCEPT.md](./CONCEPT.md)
|
||||
|
||||
## 使用规则
|
||||
|
||||
- 任务边界、信任模型和上线约束以 CONCEPT 对应章节为准。
|
||||
- `[x]` 只表示已有代码或容器验证证据;运维环境尚未验证的项目保持 `[ ]`。
|
||||
- 所有后端测试必须在 `local-x-financial-linux` 容器内运行,单命令最长 60 秒。
|
||||
|
||||
## 1. 调研与边界
|
||||
|
||||
- [x] [CONCEPT: 背景与问题] 盘点旧文件、index、LightRAG、运行时缓存和 Qdrant 的全局共享路径。证据:`knowledge.py`、`knowledge_rag.py` 调用点扫描与两租户隔离测试。
|
||||
- [x] [CONCEPT: 目标与非目标] 冻结“租户可写 + 平台只读”的双层资源模型,不提供客户端 scope 选择或私网下载降级。证据:`KnowledgeStorageScope` 与 platform 只读反向测试。
|
||||
- [x] [CONCEPT: 用户与场景] 明确 API 用户、平台资料、Hermes 调度器和 ONLYOFFICE 文档服务四类主体。证据:认证端点、scheduler 和 session service 的显式调用契约。
|
||||
|
||||
## 2. 契约与设计
|
||||
|
||||
- [x] [CONCEPT: 功能能力] 定义 `tenants/{tenant}/`、`platform/`、workspace 和 runtime cache key。证据:`knowledge_tenant_scope.py`。
|
||||
- [x] [CONCEPT: 方案设计] 为文档 DTO 增加 `scope/readOnly`,平台文档只能只读 fallback。证据:`schemas/knowledge.py` 与平台文档测试。
|
||||
- [x] [CONCEPT: ONLYOFFICE 状态机] 定义 `active → processing → consumed|failed` 和过期/重放拒绝。证据:`KnowledgeOnlyOfficeSession` 模型、`20260717_0027` 迁移与 replay 测试。
|
||||
|
||||
## 3. 后端实现
|
||||
|
||||
- [x] [CONCEPT: 后端职责] 建立 tenant/platform 文件、index 与 LightRAG 存储作用域。证据:`knowledge_tenant_scope.py`、`knowledge_index_state.py`、`knowledge_rag.py`。
|
||||
- [x] [CONCEPT: 后端职责] 将列表、详情、原文、上传、删除、同步和查询接入可信租户。证据:`knowledge.py`、`endpoints/knowledge.py`、`knowledge_sync.py`。
|
||||
- [x] [CONCEPT: 权限与信任边界] 让 Orchestrator 从数据库 Agent Run 取 tenant,并让索引 worker 比对 route/ontology tenant。证据:`orchestrator_execution.py`、`knowledge_run_scope.py`、`knowledge_index_tasks.py`、Agent Run 冲突测试。
|
||||
- [x] [CONCEPT: 后端职责] 定时器只枚举 `Tenant.status=active`,没有活跃租户时跳过。证据:`knowledge_scheduler.py` 与 active/suspended 调度测试。
|
||||
- [x] [CONCEPT: 后端职责] 迁移旧全局制度到 platform 空间且不删除源文件。证据:`migrate_legacy_library_to_platform()` 与 legacy migration 测试。
|
||||
- [x] [CONCEPT: 权限与信任边界] 新增带数据库状态的 ONLYOFFICE token、平台只读预览和一次性 callback。证据:`knowledge_onlyoffice_security.py`、`knowledge_onlyoffice_callback.py`。
|
||||
- [x] [CONCEPT: 权限与信任边界] 加固下载 origin、DNS/IP pinning、重定向、大小、MIME、ZIP 和 OOXML 校验。证据:`download_onlyoffice_document()` 与 SSRF/格式测试。
|
||||
- [x] [CONCEPT: 数据] 新增 `20260717_0027`,注册模型、schema ownership 和 migration preflight。证据:迁移文件、`db/base.py`、`models/__init__.py`、`migration_preflight.py`。
|
||||
- [x] [CONCEPT: 后端职责] 删除知识文件工具中已废弃的无状态弱 token 实现。证据:`knowledge_file_utils.py` 差异与全仓引用扫描。
|
||||
|
||||
## 4. 算法/规则实现
|
||||
|
||||
- [x] [CONCEPT: 查询算法] 以租户哈希生成不可冲突的 workspace/cache key,再隔离检索 tenant 与 platform 候选。证据:workspace/cache/local chunks 两租户测试。
|
||||
- [x] [CONCEPT: 查询算法] 对两个隔离结果按 code 去重、score 排序并截取 top-k。证据:`_merge_scoped_search_results()`。
|
||||
- [x] [CONCEPT: ONLYOFFICE 状态机] content token 默认 5 分钟,callback session 默认 4 小时且最长 12 小时。证据:JWT exp 差值断言与会话测试。
|
||||
|
||||
## 5. 前端实现
|
||||
|
||||
- [x] [CONCEPT: 前端契约] 后端 DTO 已提供 `scope/readOnly`,现有列表可安全识别平台只读资源。证据:OpenAPI 回归与 `KnowledgeDocumentRead`。
|
||||
- [x] [CONCEPT: 前端契约] ONLYOFFICE 默认返回 view 模式,只有管理员显式 `editable=true` 才获得 edit 权限。证据:config 单元测试与平台编辑拒绝测试。
|
||||
- [x] [CONCEPT: 目标与非目标] 本切片不新增知识运营页面或视觉改版。证据:CONCEPT 非目标;本轮无知识前端文件变更。
|
||||
|
||||
## 6. 测试与验证
|
||||
|
||||
- [x] [CONCEPT: 测试方案] 完成租户文件、平台只读、RAG namespace、scheduler 与 worker trust 测试。证据:容器内 `test_knowledge_tenant_security.py` 6 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 完成 token 绑定、平台只读、错 key、过期、单次回写、重放和 SSRF/OOXML 测试。证据:容器内 `test_knowledge_onlyoffice_tenant_security.py` 8 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 完成既有知识服务、RAG、同步、配置、解析、runtime 与 OpenAPI 回归。证据:容器内 8 个测试文件 31 项通过。
|
||||
- [x] [CONCEPT: 测试方案] 完成 migration、preflight 与 ownership 静态回归。证据:容器内 163 项通过、1 项因无 PostgreSQL 测试 DSN 跳过。
|
||||
- [x] [CONCEPT: 测试方案] 完成相关 Agent Run 与鉴权回归。证据:容器内 20 项通过。
|
||||
- [x] [CONCEPT: 指标与验收] 完成目标 Python 文件 Ruff 和 compileall。证据:容器命令均退出码 0。
|
||||
- [x] [CONCEPT: 测试方案] 在一次性 PostgreSQL 测试数据库执行迁移链。证据:共享迁移探针完成 `base → 0028` 与 `0028 → 0025 → 0028`,`knowledge_onlyoffice_sessions` 和 `agent_asset_onlyoffice_sessions` 均正常落库;有事实 downgrade 仍由各迁移显式拒绝。
|
||||
|
||||
## 7. 文档收尾
|
||||
|
||||
- [x] [CONCEPT: 本轮实现记录] 更新 CONCEPT、分阶段 TODO 和三份 bug 修复日志。证据:本目录两份文档及 `dev-logs/bugs/knowledge-*.md`。
|
||||
- [x] [CONCEPT: 风险与开放问题] 记录 JSON index 多进程竞争、平台发布流程和长时编辑边界。证据:CONCEPT 风险章节。
|
||||
- [ ] [CONCEPT: 降级策略] 上线前确认 ONLYOFFICE 下载域名从应用容器解析为公网地址并使用可信 TLS。证据要求:生产 `ONLYOFFICE_DOWNLOAD_ALLOWED_ORIGINS` 配置与容器 DNS/TLS 验证;当前开发网络解析到 `198.18.0.0/15` 时会按设计拒绝真实回写。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 管理员会话未绑定所选企业导致管理模块不可用
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/admin-tenant-context-management-modules.md
|
||||
|
||||
## 修复记录
|
||||
- 15:00:记录 bug 修复:`admin/admin` 登录后仍落在 `platform` 工作域,数字员工和员工管理无法读取 `default` 企业数据。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main`;upstream 新提交:未发现;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`、`28b834ed fix(approval): replay immutable action responses`、`4940ebc4 feat(approval): add safe risk disposition workflow`、`ee88a36b feat(ai): add tenant-safe hierarchical expense learning`、`6bdf65bc feat(expenses): add authoritative pre-review workflow`、`ae3f02c3 feat(expense): add persistent zero-entry receipt association`,另有 11 条。
|
||||
- 原因:管理员认证成功后固定构造为 `platform` 租户,忽略登录请求中的企业代码;会话恢复也再次回落到平台域,导致企业级资产与员工接口拿不到有效企业上下文。
|
||||
- 修改:`AuthService` 在管理员登录和会话恢复时解析请求企业 ID/企业代码,只接受启用企业,并保持 `is_admin=true`;未选择企业时仍保留平台管理员语义。
|
||||
- 修改:新增管理员 `default` 企业登录、会话恢复、停用/未知企业拒绝测试,以及员工 meta/list 端点的真实管理员租户回归,防止平台域误触发员工初始化。
|
||||
- 验证:容器内后端联合回归 `65 passed`,Ruff 全部通过;使用 `admin/admin` 和 `default` 实际登录后,数字员工技能页显示 9 条技能,员工管理显示 105 人、100 人在职、3 人试用、2 人停用,曹笑竹详情及角色信息可正常打开。
|
||||
- 影响:管理员现在能在所选企业内稳定使用数字员工、员工与组织、企业规则等租户级功能,同时不会扩大普通用户或跨企业访问权限。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 员工年龄反算出生日期偏差一年
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/employee-age-birthdate-conversion.md
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 15:40:记录 bug 修复:员工生日在当年尚未到来时,编辑年龄会反算出少一年的年龄。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新包括 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||
- 修改:`employeeManagementModel.js` 在目标生日尚未到来时向前校正出生年份,并为年龄与出生日期转换函数增加可注入的参考日期;新增 `employee-management-age.test.mjs` 覆盖未过生日和已过生日两个边界。
|
||||
- 操作:在 `local-x-financial-linux` 容器内运行员工年龄、员工历史和规则权限定向测试,并执行 Vite 生产构建与 `git diff --check`。
|
||||
- 验证:以 2026-07-18 为参考日期,30 岁且生日为 12 月 31 日时正确生成 `1995-12-31`,回算年龄仍为 30;定向前端测试 9 项全部通过,前端全量测试 `820/820` 通过,Vite 生产构建成功,差异格式检查通过。
|
||||
- 影响:员工管理中直接修改年龄后,保存与重新加载不再把员工年龄静默减一岁。
|
||||
- 16:03:补齐 0 岁与闰日出生日期边界。
|
||||
- Git 提交检查:沿用本轮 15:54 拉取检查;upstream `origin/main` 无新提交,本地 ahead 19 条。
|
||||
- 修改:出生年按参考日中生日是否已过推导;0 岁且生日未到时使用上一出生年度,避免生成未来日期;2 月 29 日跨非闰年统一按 2 月 28 日计算和保存。
|
||||
- 验证:参考日 2026-07-18、0 岁、保留 12 月 31 日时生成 `2025-12-31` 并回算 0 岁;闰日跨到非闰年生成合法 `2001-02-28` 并回算 23 岁;定向测试 22 项、前端全量测试 `823/823`、生产构建均通过。
|
||||
- 影响:新生儿档案和闰日生日员工的编辑结果保持合法、可保存且年龄一致。
|
||||
@@ -0,0 +1,19 @@
|
||||
# 模型密钥解密失败仍显示已配置
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/model-secret-decryption-status.md
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 15:54:记录 bug 修复:模型密文无法由当前 `settings.key` 解密时,设置页仍根据非空密文显示“已配置”,掩盖实际不可用状态。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||
- 原因:后端配置状态只判断数据库密文字段是否非空,前端本地草稿又可能覆盖服务端状态,均没有验证密文是否能用当前密钥成功解密。
|
||||
- 修改:后端序列化配置时以实际解密结果计算模型、OnlyOffice JWT 与 SMTP 密码状态;解密只读取现有主密钥,不再因 GET 设置请求自动创建新 `settings.key`;前端合并本地草稿后重新应用服务端权威密钥状态;新增密钥轮换、密钥文件缺失和前端状态覆盖回归测试。
|
||||
- 操作:在 `local-x-financial-linux` 容器内执行设置持久化测试、后端联合回归、前端全量回归、Ruff 和生产构建,并使用真实管理员设置页复验。
|
||||
- 验证:主模型和备份模型仍显示“已配置”,当前无法解密的 Embedding 与 Reranker 准确显示“未配置”;后端联合回归 `165 passed`,前端全量回归 `821/821`,生产构建成功。
|
||||
- 影响:管理员不会再被伪阳性状态误导;受影响的两个真实 API Key 仍需重新录入,或恢复与密文配套的旧密钥文件后才能恢复检索模型调用。
|
||||
- 16:20:补齐已有主密钥格式与长度校验。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||
- 修改:加密和解密路径统一要求已有 `settings.key` 解码后为 32 字节;非空但格式错误或长度错误的密钥直接拒绝,不再先生成无法由后续读取路径解密的密文;空文件仍按首次初始化语义生成合法密钥。
|
||||
- 验证:新增非法 Base64、短密钥和空密钥文件回归;设置与管理模块后端联合回归 `176 passed`,Ruff 通过;`admin/admin + default` 实际读取状态为主模型/备份模型/OnlyOffice 已配置,Embedding/Reranker/SMTP 未配置。
|
||||
- 影响:设置保存不会在损坏主密钥下产生新的不可恢复密文,读取设置也不会通过副作用掩盖密钥文件缺失。
|
||||
@@ -0,0 +1,14 @@
|
||||
# 平台工作域误初始化企业员工目录
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/platform-employee-directory-tenant-guard.md
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 15:54:记录 bug 修复:平台管理员未选择企业时访问员工接口,会在 `platform` 工作域初始化并返回企业员工数据。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||
- 原因:员工路由只校验管理员身份,没有区分平台工作域和企业工作域;首次访问还会触发演示员工初始化,造成平台租户数据污染。
|
||||
- 修改:新增企业管理员依赖并应用到员工管理整组接口;平台工作域统一返回 403,前端同步隐藏企业员工目录入口;新增接口拒绝、无数据副作用和导航权限回归测试。
|
||||
- 操作:在 `local-x-financial-linux` 容器内完成先失败后通过的接口测试、后端联合回归、前端全量回归、Ruff 和生产构建。
|
||||
- 验证:平台管理员访问员工 meta/list 均返回 403 且不生成平台员工或组织;`admin/admin + default` 实际访问员工管理正常显示 105 人;后端联合回归 `165 passed`,前端全量回归 `821/821` 通过。
|
||||
- 影响:员工与组织数据只能在明确企业上下文中读取和初始化,避免平台域污染及跨工作域误操作。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 默认 Compose 缺少 Qdrant 运行服务
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/qdrant-compose-runtime-missing.md
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 15:54:记录 bug 修复:默认本地 Compose 未管理 Qdrant,知识检索与向量索引链路无法连接预期服务。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||
- 修改:在 `docker-compose.yml` 中加入持久化 Qdrant 服务、健康检查、本机回环端口和内部网络;主服务显式使用 `http://qdrant:6333`,并等待 Qdrant 健康后启动。
|
||||
- 操作:执行 `docker compose config --quiet`,仅启动 `x-financial-qdrant`,未重建或重启 `local-x-financial-linux`。
|
||||
- 验证:Qdrant 1.18.3 容器健康,主容器内 `qdrant` 域名可解析,`/healthz` 返回 200,`/collections` 返回正常空集合;主容器仍保持原启动时间且重启次数为 0。
|
||||
- 影响:默认本地运行环境具备可持久化的向量存储依赖,后续知识检索不再依赖手工创建的散落容器。
|
||||
@@ -0,0 +1,14 @@
|
||||
# 规则在线表格缺少运行依赖且平台资产下载被拒绝
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/rule-editor-onlyoffice-runtime-platform-access.md
|
||||
|
||||
## 修复记录
|
||||
- 15:00:记录 bug 修复:规则详情无法加载 ONLYOFFICE,补齐容器后平台规则文件仍因机器会话缺少有效企业而下载失败。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main`;upstream 新提交:未发现;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`、`28b834ed fix(approval): replay immutable action responses`、`4940ebc4 feat(approval): add safe risk disposition workflow`、`ee88a36b feat(ai): add tenant-safe hierarchical expense learning`、`6bdf65bc feat(expenses): add authoritative pre-review workflow`、`ae3f02c3 feat(expense): add persistent zero-entry receipt association`,另有 11 条。
|
||||
- 原因:默认 Compose 没有管理 ONLYOFFICE DocumentServer,应用配置仍可能被根 `.env` 覆盖;服务启动后,ONLYOFFICE 内容令牌映射出的合法平台管理员机器会话又被资产访问域统一拒绝。
|
||||
- 修改:默认 Compose 新增带健康检查和 JWT 的 `onlyoffice` 服务,主应用等待 PostgreSQL 与 ONLYOFFICE 均健康后启动,并分别配置浏览器公共地址和 Compose 网络内回调地址;本地运行配置不再被根 `.env` 悄悄覆盖。
|
||||
- 修改:平台资产访问域仅允许真实 `is_admin=true` 的平台会话进入,并把可见范围严格限制为 `scope=platform` 且 `tenant_id=platform`;普通平台会话继续拒绝,企业管理员也不能借此跨企业访问。
|
||||
- 操作:通过 `admin/admin` 在系统设置中保存本地 ONLYOFFICE 地址与匹配 JWT 密钥;Compose 重建后仅保留一个应用容器 `local-x-financial-linux`,依赖容器为 PostgreSQL 和 ONLYOFFICE,三者均为 healthy。
|
||||
- 验证:规则配置、内容下载和回调链路均返回 200;真实页面成功加载公司通信费报销规则工作簿,显示完整工具栏、工作表“通信费报销标准”和 A1 内容“序号”;平台资产相关回归 36 条通过,最终后端联合回归 `65 passed`,`docker compose config --quiet` 与 `git diff --check` 通过。
|
||||
- 影响:在默认 Compose 环境中无需手工启动外部文档服务即可在线查看和编辑规则 Excel,同时平台规则访问仍保持最小权限边界。
|
||||
@@ -0,0 +1,18 @@
|
||||
# 规则管理权限状态未导出导致页面崩溃
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/rule-management-permission-reference-crash.md
|
||||
|
||||
## 修复记录
|
||||
- 15:00:记录 bug 修复:进入规则管理时模板引用未定义的 `isRuleManager`,导致规则列表和操作区整页崩溃。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main`;upstream 新提交:未发现;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`、`28b834ed fix(approval): replay immutable action responses`、`4940ebc4 feat(approval): add safe risk disposition workflow`、`ee88a36b feat(ai): add tenant-safe hierarchical expense learning`、`6bdf65bc feat(expenses): add authoritative pre-review workflow`、`ae3f02c3 feat(expense): add persistent zero-entry receipt association`,另有 11 条。
|
||||
- 原因:`useAuditViewPermissions` 内部已经计算 `isRuleManager`,但未在返回对象中暴露;`AuditView` 同时没有解构该状态,渲染权限按钮时触发未定义引用。
|
||||
- 修改:权限 composable 明确返回 `isRuleManager`,视图层同步解构;新增权限状态导出与视图接线回归测试。
|
||||
- 验证:容器内前端测试 `816/816` 通过,Vite 生产构建成功;使用 `admin/admin` 实际进入规则管理,基础规则 8 条、风险规则列表和 MCP 服务 2 条均正常加载,页面无崩溃。
|
||||
- 影响:规则管理员与系统管理员的规则操作权限可以稳定参与渲染,基础规则、风险规则和 MCP 三个页签恢复可用。
|
||||
- 15:40:追加修复风险规则详情操作状态未向模板暴露的问题。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新包括 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||
- 修改:`AuditView.js` 的 `setup()` 返回对象补齐 `riskRuleHasPublishableRevision`、`canEditRiskRuleDraft` 和 `canCreateRiskRuleRevision`;`audit-view-permissions.test.mjs` 新增模板权限暴露回归。
|
||||
- 操作:在 `local-x-financial-linux` 容器内运行规则权限、风险规则详情和员工模型定向测试,并执行 Vite 生产构建与 `git diff --check`。
|
||||
- 验证:定向前端测试 9 项全部通过,前端全量测试 `820/820` 通过,Vite 生产构建成功,差异格式检查通过。
|
||||
- 影响:已发布风险规则可正常显示“创建修订版本”,未发布草稿和待发布修订也能显示对应的编辑、发布入口。
|
||||
@@ -0,0 +1,14 @@
|
||||
# 规则正文与运行时配置分步保存产生孤儿版本
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/rule-markdown-runtime-atomic-save.md
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 16:20:记录 bug 修复:普通 Markdown 规则先创建版本、再更新运行时 JSON,第二个请求失败时会留下不可重试的孤儿版本和版本号冲突。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||
- 原因:前端将一个业务动作拆成创建 `AgentAssetVersion` 和 PATCH 资产配置两个独立事务,任一网络、审计或持久化异常都可能只完成前半段。
|
||||
- 修改:新增 `POST /agent-assets/{id}/rule-markdown` 单一接口和专用规则保存 mixin;在一个 SQLAlchemy 事务中创建版本、更新 current/working 指针、白名单合并运行时配置并写审计;Markdown 内嵌 `expense-rule` JSON 必须与运行配置一致;表格规则和 JSON 风险规则继续走各自专用接口。
|
||||
- 修改:前端保存 Markdown 和运行时 JSON 均改为一次原子请求;新增服务和路由按职责拆分,主服务文件 703 行、主路由文件 773 行,均低于项目硬上限。
|
||||
- 验证:成功落库、审计失败、响应构建失败、提交失败与审计记录回滚测试通过;管理模块后端联合回归 `176 passed`,前端全量回归 `824/824`,迁移回归 `66 passed, 1 skipped`,Vite 生产构建、Ruff、Compose 配置解析及 `git diff --check` 均通过。
|
||||
- 影响:规则编辑保存现在具备事务一致性,失败后不会留下半成品版本,也不会因为孤儿版本阻塞下一次保存。
|
||||
@@ -0,0 +1,14 @@
|
||||
# 设置草稿覆盖服务端模型集合
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/settings-model-draft-reconciliation.md
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 16:03:记录 bug 修复:设置页本地会话草稿会整体覆盖服务端模型列表,保存任一设置时可能误删服务端新增槽位,或复活已删除槽位。
|
||||
- Git 提交检查:沿用本轮 15:54 拉取检查;upstream `origin/main` 无新提交,本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||
- 原因:加载时先用 session 草稿的 `models` 整体覆盖服务端集合,后续只同步现存行的密钥状态;后端保存完整 `llmForm` 时会把缺失的自定义槽位视为删除。
|
||||
- 修改:以服务端快照的模型集合为权威来源,仅按同一 slot 保留当前内存中未提交且不是掩码的 API Key 输入;服务端新增槽位自动补入,服务端删除槽位不再保留;首次异步加载也保护用户请求期间输入的密钥。
|
||||
- 操作:在 `local-x-financial-linux` 容器内运行新增/删除自定义槽位、未提交输入和权威状态合并测试,并执行前端全量回归与生产构建。
|
||||
- 验证:定向测试 22 项、前端全量测试 `823/823` 全部通过,Vite 生产构建成功,`git diff --check` 通过。
|
||||
- 影响:设置页不会因陈旧浏览器草稿静默改变服务端模型清单,降低误删模型配置和检索能力中断风险。
|
||||
@@ -0,0 +1,14 @@
|
||||
# 员工与组织残留全局唯一索引阻断多租户复用
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/tenant-identity-global-unique-indexes.md
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 15:54:记录数据库迁移修复:员工编号、员工邮箱和组织编码仍保留单列全局唯一索引,第二个企业无法使用相同企业内部标识。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||
- 原因:历史迁移已增加正确的租户复合唯一约束,但三个旧单列唯一索引未被替换,PostgreSQL 仍按全局范围拒绝重复值。
|
||||
- 修改:新增 Alembic `20260718_0029`,在 PostgreSQL 中校验复合唯一约束和现有索引形态后,将三个同名单列唯一索引替换为普通查询索引;降级前检查跨租户重复并在无法安全恢复全局唯一时拒绝降级。
|
||||
- 操作:更新迁移前置检查和迁移测试;只读核验当前数据库仍位于 `20260717_0028`,未执行 live DDL、数据写入或主容器重启。
|
||||
- 验证:容器内迁移测试 `167 passed, 1 skipped`,Ruff 通过,Alembic HEAD 为 `20260718_0029`;跳过项仅因未配置专用一次性 PostgreSQL 迁移测试库,未使用当前业务库替代。
|
||||
- 影响:代码层已具备租户内唯一、跨租户可复用的索引迁移;正式生效会短暂获取索引 DDL 锁,须在用户确认维护窗口后应用。
|
||||
@@ -0,0 +1,14 @@
|
||||
# Uvicorn 热重载监听日志形成反馈循环
|
||||
|
||||
日期:2026-07-18
|
||||
文档路径:document/development/2026-07-18/dev-logs/bugs/uvicorn-reload-log-feedback-loop.md
|
||||
|
||||
## 修复记录
|
||||
|
||||
- 15:54:记录 bug 修复:开发模式的 Uvicorn 热重载监听整个服务目录,应用写入日志会再次触发 reload 检测日志,形成持续反馈循环。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream `origin/main` 无新提交;本地 ahead 19 条,最新为 `07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`,另有 16 条。
|
||||
- 原因:`--reload` 未限定源码目录,`server/logs/app.log` 的每次追加都被 watchfiles 识别为变更,并产生新的日志。
|
||||
- 修改:热重载分支增加 `--reload-dir src`,只监听后端源码;非热重载与多 worker 启动参数保持不变;新增两种启动分支的脚本回归测试。
|
||||
- 操作:在容器内执行 Shell 语法检查、启动依赖测试、后端联合回归、Ruff 和差异格式检查;为避免自动执行待确认的数据库迁移,本轮未重启主容器。
|
||||
- 验证:脚本语法通过,启动脚本定向测试通过,后端联合回归 `165 passed`,Ruff 与 `git diff --check` 通过;代码将在下一次经确认的安全重建后生效。
|
||||
- 影响:生效后日志文件写入不再触发热重载扫描,可停止无效日志增长和持续 CPU/IO 消耗。
|
||||
27
document/development/2026-07-18/work-logs.med
Normal file
27
document/development/2026-07-18/work-logs.med
Normal file
@@ -0,0 +1,27 @@
|
||||
# 2026-07-18 综合工作日志
|
||||
|
||||
生成时间:2026-07-20 10:29:14 CST
|
||||
来源:`feature/` 功能点文档与 `dev-logs/bugs/` bug 记录
|
||||
|
||||
## 今日功能点
|
||||
|
||||
- 今日未发现功能点文档。
|
||||
|
||||
## 今日 Bugs
|
||||
|
||||
- 管理员会话未绑定所选企业导致管理模块不可用:影响:管理员现在能在所选企业内稳定使用数字员工、员工与组织、企业规则等租户级功能,同时不会扩大普通用户或跨企业访问权限。(文件:`admin-tenant-context-management-modules.md`)
|
||||
- 员工年龄反算出生日期偏差一年:影响:新生儿档案和闰日生日员工的编辑结果保持合法、可保存且年龄一致。(文件:`employee-age-birthdate-conversion.md`)
|
||||
- 模型密钥解密失败仍显示已配置:影响:设置保存不会在损坏主密钥下产生新的不可恢复密文,读取设置也不会通过副作用掩盖密钥文件缺失。(文件:`model-secret-decryption-status.md`)
|
||||
- 平台工作域误初始化企业员工目录:影响:员工与组织数据只能在明确企业上下文中读取和初始化,避免平台域污染及跨工作域误操作。(文件:`platform-employee-directory-tenant-guard.md`)
|
||||
- 默认 Compose 缺少 Qdrant 运行服务:影响:默认本地运行环境具备可持久化的向量存储依赖,后续知识检索不再依赖手工创建的散落容器。(文件:`qdrant-compose-runtime-missing.md`)
|
||||
- 规则在线表格缺少运行依赖且平台资产下载被拒绝:影响:在默认 Compose 环境中无需手工启动外部文档服务即可在线查看和编辑规则 Excel,同时平台规则访问仍保持最小权限边界。(文件:`rule-editor-onlyoffice-runtime-platform-access.md`)
|
||||
- 规则管理权限状态未导出导致页面崩溃:影响:已发布风险规则可正常显示“创建修订版本”,未发布草稿和待发布修订也能显示对应的编辑、发布入口。(文件:`rule-management-permission-reference-crash.md`)
|
||||
- 规则正文与运行时配置分步保存产生孤儿版本:影响:规则编辑保存现在具备事务一致性,失败后不会留下半成品版本,也不会因为孤儿版本阻塞下一次保存。(文件:`rule-markdown-runtime-atomic-save.md`)
|
||||
- 设置草稿覆盖服务端模型集合:影响:设置页不会因陈旧浏览器草稿静默改变服务端模型清单,降低误删模型配置和检索能力中断风险。(文件:`settings-model-draft-reconciliation.md`)
|
||||
- 员工与组织残留全局唯一索引阻断多租户复用:影响:代码层已具备租户内唯一、跨租户可复用的索引迁移;正式生效会短暂获取索引 DDL 锁,须在用户确认维护窗口后应用。(文件:`tenant-identity-global-unique-indexes.md`)
|
||||
- Uvicorn 热重载监听日志形成反馈循环:影响:生效后日志文件写入不再触发热重载扫描,可停止无效日志增长和持续 CPU/IO 消耗。(文件:`uvicorn-reload-log-feedback-loop.md`)
|
||||
|
||||
## 综合分析
|
||||
|
||||
- 问题侧记录了 11 个 bug 修复。
|
||||
- 后续复盘优先看本文件,再回到对应功能点或 bug 文件追溯证据。
|
||||
@@ -0,0 +1,14 @@
|
||||
# 对话口语化单据筛选意图识别失败
|
||||
|
||||
日期:2026-07-20
|
||||
文档路径:document/development/2026-07-20/dev-logs/bugs/conversation-document-filter-intent.md
|
||||
|
||||
## 修复记录
|
||||
- 10:19:修复“出差上海的单据请筛选一下”等口语化单据筛选请求在对话中无法识别的问题。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream 为 `origin/main`;未发现 upstream 新提交;本地 ahead 19 个提交:`07241b46 fix(docker): manage local postgres in default compose`、`787bc3a4 feat(platform): close AI expense value loop`、`242d68c3 feat(approval): add task workflow and waiver decisions`、`28b834ed fix(approval): replay immutable action responses`、`4940ebc4 feat(approval): add safe risk disposition workflow`、`ee88a36b feat(ai): add tenant-safe hierarchical expense learning`、`6bdf65bc feat(expenses): add authoritative pre-review workflow`、`ae3f02c3 feat(expense): add persistent zero-entry receipt association`、`54754b55 feat(ai): add personal expense application memory`、`211f85d9 feat(ai): unify verified expense application workflow`、`5b246307 feat(ai): issue verified application preview decisions`、`a662cfe6 feat(ai): add expense application feedback ledger`、`5ed34c2b feat(expenses): backfill historical claims into expense cases`、`11275e4b fix(migrations): enforce schema ownership safety`、`1347366b feat(expenses): secure timeline and draft events`、`22669a90 feat(expenses): show unified expense event timeline`、`a616b30c fix(expenses): unify AI application submission transaction`、`653eda05 feat(auth): add opaque bearer sessions`、`661990b2 feat(expenses): add transactional expense case events`。
|
||||
- 修改:前端会话范围守卫新增“筛选/过滤/查询 + 单据”信号识别,费用会话直接放行,申请会话建议切换到费用查询;引导式报销流程把此类请求识别为新意图,不再误当作当前表单字段答案。
|
||||
- 修改:工作台单据查询统一复用城市名提取,支持从“出差上海的单据”提取 `expense_type=travel`、`location=上海`,并补齐带中文引号的显式关键词解析。
|
||||
- 修改:后端语义入口放宽口语化单据查询硬门禁,补齐“出差”“筛选”“过滤”等规则候选;请求通过门禁后仍执行“规则候选 + 模型语义归一 + 结构化 SQL 筛选”的混合链路,最终按费用类型和地点确定性过滤数据。
|
||||
- 操作:首次端到端回归暴露测试夹具缺少必填 `department_name`,补齐夹具后重新执行;未修改生产数据,也未提交代码。
|
||||
- 验证:前端目标回归 `27/27` 通过;相关前端回归 `121/121` 通过;`npm run build` 成功(2254 modules transformed);容器内本体语义测试 `73/73` 通过;容器内费用查询编排回归 `3/3` 通过;精确端到端用例 `2/2` 通过;`git diff --check` 通过。
|
||||
- 影响:“出差上海的单据请筛选一下”现在会稳定路由到费用查询,识别差旅费与上海并只返回上海相关单据;“上海的单据请筛选一下”等未显式写出费用类型的表达也可进入模型增强的语义解析,不再被判为非财务问题或误填入引导表单。
|
||||
@@ -0,0 +1,15 @@
|
||||
# 费用操作记录图标缺失与企业级样式收敛
|
||||
|
||||
日期:2026-07-20
|
||||
文档路径:document/development/2026-07-20/dev-logs/bugs/expense-operation-record-icon-enterprise-ui.md
|
||||
|
||||
## 修复记录
|
||||
- 09:58:修复费用操作记录 AI 预审图标缺失,并将记录区收敛为企业级审计日志样式。
|
||||
- Git 提交检查:`git fetch --all --prune` 成功;upstream 为 `origin/main`;`HEAD..origin/main` 无新提交;本地相对 upstream ahead 19 个提交。
|
||||
- 本地 ahead 摘要:`07241b46 fix(docker): manage local postgres in default compose`;`787bc3a4 feat(platform): close AI expense value loop`;`242d68c3 feat(approval): add task workflow and waiver decisions`;`28b834ed fix(approval): replay immutable action responses`;`4940ebc4 feat(approval): add safe risk disposition workflow`;`ee88a36b feat(ai): add tenant-safe hierarchical expense learning`;`6bdf65bc feat(expenses): add authoritative pre-review workflow`;`ae3f02c3 feat(expense): add persistent zero-entry receipt association`;`54754b55 feat(ai): add personal expense application memory`;`211f85d9 feat(ai): unify verified expense application workflow`;`5b246307 feat(ai): issue verified application preview decisions`;`a662cfe6 feat(ai): add expense application feedback ledger`;`5ed34c2b feat(expenses): backfill historical claims into expense cases`;`11275e4b fix(migrations): enforce schema ownership safety`;`1347366b feat(expenses): secure timeline and draft events`;`22669a90 feat(expenses): show unified expense event timeline`;`a616b30c fix(expenses): unify AI application submission transaction`;`653eda05 feat(auth): add opaque bearer sessions`;`661990b2 feat(expenses): add transactional expense case events`。
|
||||
- 修改:将 MDI 7.4.47 不存在的 `mdi-shield-search-outline` 替换为 `mdi-shield-check-outline`,并把申请/报销提交图标改为语义更准确的 `mdi-file-send-outline`;补充图标映射回归断言。
|
||||
- 修改:将费用操作记录改为“发生时间 / 事件 / 操作人”三列审计日志,图标并入事件列;移除绿色“最新”成功态误用,改为中性记录数和“最新优先”元数据;统一紧凑行高、方形状态图标、标准字重、语义色与显式响应式网格。
|
||||
- 修改:补充 `aria-labelledby`、`aria-busy`、图标读屏隐藏、操作人完整值提示和刷新按钮键盘焦点样式;事件倒序排序仅对有限时间戳执行数值比较。
|
||||
- 操作:通过项目脚本写入本 bug 日志;生成桌面最终截图、聚焦截图和设计对照证据,并更新根目录 `design-qa.md`。
|
||||
- 验证:`node --test --test-timeout=60000 tests/expense-case-timeline.test.mjs` 通过,13/13;`npm run build` 通过;`git diff --check` 通过;真实申请 `AT4UHTKWU` 浏览器刷新后仍展示两条记录、三列表头与两个有效图标,未进入加载失败状态。
|
||||
- 影响:修复费用申请与报销单 AI 预审事件的空白图标;统一费用操作记录的信息密度、状态语义、可访问性和企业后台视觉风格,不改变后端事件协议或审批业务顺序。
|
||||
@@ -58,14 +58,33 @@ pip install -e .[dev]
|
||||
copy ..\\.env.example ..\\.env
|
||||
```
|
||||
|
||||
3. 启动服务
|
||||
3. 使用标准入口启动服务
|
||||
|
||||
```bash
|
||||
uvicorn app.main:app --reload --app-dir src
|
||||
cd ..
|
||||
./start.sh server
|
||||
```
|
||||
|
||||
标准入口在启动新的 FastAPI 进程前会自动执行 `alembic upgrade head`,迁移成功后才会
|
||||
启动 Uvicorn。`./start.sh all` 在需要启动后端时也会复用同一流程。不要把直接运行
|
||||
`uvicorn` 当作标准启动方式;手工调试 Uvicorn 时,需要先自行完成迁移。
|
||||
|
||||
## 迁移
|
||||
|
||||
```bash
|
||||
alembic upgrade head
|
||||
cd server
|
||||
alembic -c alembic.ini upgrade head
|
||||
```
|
||||
|
||||
一次性 PostgreSQL 迁移测试默认跳过,只有显式提供
|
||||
`MIGRATION_TEST_DATABASE_URL` 时才会执行。为避免误操作开发库,测试会同时要求主机名
|
||||
和数据库名使用 `migration-probe` 或 `disposable-probe` 安全前缀,并要求数据库初始为空。
|
||||
|
||||
```bash
|
||||
cd server
|
||||
MIGRATION_TEST_DATABASE_URL='postgresql+psycopg://migration_probe:migration_probe_pw@x-financial-migration-probe-123:5432/migration_probe' \
|
||||
pytest -q tests/test_alembic_migrations.py
|
||||
```
|
||||
|
||||
该测试覆盖首次升级、重复升级、关键表/约束/索引、外键级联、降级到 `base`、无关旧表
|
||||
及哨兵数据保留,以及再次升级。请只把它指向无持久卷的一次性 PostgreSQL 容器。
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[alembic]
|
||||
script_location = alembic
|
||||
script_location = %(here)s/alembic
|
||||
prepend_sys_path = .
|
||||
path_separator = os
|
||||
sqlalchemy.url = postgresql+psycopg://postgres:postgres@127.0.0.1:5432/x_financial
|
||||
|
||||
[loggers]
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
"""add expense case and transactional business event tables
|
||||
|
||||
This is the first migration-owned schema slice in a legacy database that still
|
||||
bootstraps older tables through SQLAlchemy metadata. The server startup runs
|
||||
this revision before the legacy bootstrap, and the legacy bootstrap explicitly
|
||||
excludes these migration-owned tables.
|
||||
|
||||
Revision ID: 20260713_0001
|
||||
Revises:
|
||||
Create Date: 2026-07-13 11:45:00
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260713_0001"
|
||||
down_revision: str | None = None
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"expense_cases",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("case_no", sa.String(length=80), nullable=False),
|
||||
sa.Column("scene_code", sa.String(length=50), nullable=False),
|
||||
sa.Column("title", sa.String(length=200), nullable=False),
|
||||
sa.Column("owner_employee_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("current_stage", sa.String(length=40), nullable=False),
|
||||
sa.Column("status", sa.String(length=30), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("tenant_id", "case_no", name="uq_expense_cases_tenant_case_no"),
|
||||
)
|
||||
op.create_index("ix_expense_cases_tenant_id", "expense_cases", ["tenant_id"])
|
||||
op.create_index("ix_expense_cases_owner_employee_id", "expense_cases", ["owner_employee_id"])
|
||||
op.create_index(
|
||||
"ix_expense_cases_tenant_stage",
|
||||
"expense_cases",
|
||||
["tenant_id", "current_stage"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_expense_cases_tenant_status",
|
||||
"expense_cases",
|
||||
["tenant_id", "status"],
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"expense_case_links",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("expense_case_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("resource_type", sa.String(length=50), nullable=False),
|
||||
sa.Column("resource_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("relation_type", sa.String(length=50), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(["expense_case_id"], ["expense_cases.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("resource_type", "resource_id", name="uq_expense_case_links_resource"),
|
||||
)
|
||||
op.create_index("ix_expense_case_links_tenant_id", "expense_case_links", ["tenant_id"])
|
||||
op.create_index(
|
||||
"ix_expense_case_links_tenant_case",
|
||||
"expense_case_links",
|
||||
["tenant_id", "expense_case_id"],
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"business_events",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("expense_case_id", sa.String(length=36), nullable=False),
|
||||
sa.Column("aggregate_type", sa.String(length=50), nullable=False),
|
||||
sa.Column("aggregate_id", sa.String(length=100), nullable=False),
|
||||
sa.Column("event_type", sa.String(length=80), nullable=False),
|
||||
sa.Column("event_version", sa.Integer(), nullable=False),
|
||||
sa.Column("idempotency_key", sa.String(length=120), nullable=False),
|
||||
sa.Column("correlation_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("causation_id", sa.String(length=64), nullable=True),
|
||||
sa.Column("actor_id", sa.String(length=120), nullable=False),
|
||||
sa.Column("actor_type", sa.String(length=30), nullable=False),
|
||||
sa.Column("payload_json", sa.JSON(), nullable=False),
|
||||
sa.Column("delivery_status", sa.String(length=20), nullable=False),
|
||||
sa.Column("delivery_attempts", sa.Integer(), nullable=False),
|
||||
sa.Column("last_delivery_error", sa.Text(), nullable=True),
|
||||
sa.Column(
|
||||
"occurred_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("published_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.ForeignKeyConstraint(["expense_case_id"], ["expense_cases.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint(
|
||||
"tenant_id",
|
||||
"aggregate_type",
|
||||
"aggregate_id",
|
||||
"event_type",
|
||||
"idempotency_key",
|
||||
name="uq_business_event_idempotency",
|
||||
),
|
||||
)
|
||||
op.create_index("ix_business_events_tenant_id", "business_events", ["tenant_id"])
|
||||
op.create_index("ix_business_events_event_type", "business_events", ["event_type"])
|
||||
op.create_index("ix_business_events_correlation_id", "business_events", ["correlation_id"])
|
||||
op.create_index(
|
||||
"ix_business_events_tenant_case_time",
|
||||
"business_events",
|
||||
["tenant_id", "expense_case_id", "occurred_at"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_business_events_outbox",
|
||||
"business_events",
|
||||
["delivery_status", "occurred_at"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_business_events_aggregate",
|
||||
"business_events",
|
||||
["aggregate_type", "aggregate_id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_business_events_aggregate", table_name="business_events")
|
||||
op.drop_index("ix_business_events_outbox", table_name="business_events")
|
||||
op.drop_index("ix_business_events_tenant_case_time", table_name="business_events")
|
||||
op.drop_index("ix_business_events_correlation_id", table_name="business_events")
|
||||
op.drop_index("ix_business_events_event_type", table_name="business_events")
|
||||
op.drop_index("ix_business_events_tenant_id", table_name="business_events")
|
||||
op.drop_table("business_events")
|
||||
|
||||
op.drop_index("ix_expense_case_links_tenant_case", table_name="expense_case_links")
|
||||
op.drop_index("ix_expense_case_links_tenant_id", table_name="expense_case_links")
|
||||
op.drop_table("expense_case_links")
|
||||
|
||||
op.drop_index("ix_expense_cases_tenant_status", table_name="expense_cases")
|
||||
op.drop_index("ix_expense_cases_tenant_stage", table_name="expense_cases")
|
||||
op.drop_index("ix_expense_cases_owner_employee_id", table_name="expense_cases")
|
||||
op.drop_index("ix_expense_cases_tenant_id", table_name="expense_cases")
|
||||
op.drop_table("expense_cases")
|
||||
82
server/alembic/versions/20260713_0002_auth_sessions.py
Normal file
82
server/alembic/versions/20260713_0002_auth_sessions.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""add opaque bearer authentication sessions
|
||||
|
||||
Revision ID: 20260713_0002
|
||||
Revises: 20260713_0001
|
||||
Create Date: 2026-07-13 14:20:00
|
||||
"""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "20260713_0002"
|
||||
down_revision: str | None = "20260713_0001"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"auth_sessions",
|
||||
sa.Column("id", sa.String(length=36), nullable=False),
|
||||
sa.Column("token_hash", sa.String(length=64), nullable=False),
|
||||
sa.Column("tenant_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("principal_type", sa.String(length=20), nullable=False),
|
||||
sa.Column("employee_id", sa.String(length=36), nullable=True),
|
||||
sa.Column("username", sa.String(length=255), nullable=False),
|
||||
sa.Column("metric_session_id", sa.String(length=64), nullable=False),
|
||||
sa.Column(
|
||||
"issued_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("revoked_at", sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column(
|
||||
"last_seen_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.func.now(),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("token_hash", name="uq_auth_sessions_token_hash"),
|
||||
)
|
||||
op.create_index("ix_auth_sessions_tenant_id", "auth_sessions", ["tenant_id"])
|
||||
op.create_index("ix_auth_sessions_principal_type", "auth_sessions", ["principal_type"])
|
||||
op.create_index("ix_auth_sessions_employee_id", "auth_sessions", ["employee_id"])
|
||||
op.create_index("ix_auth_sessions_username", "auth_sessions", ["username"])
|
||||
op.create_index("ix_auth_sessions_metric_session_id", "auth_sessions", ["metric_session_id"])
|
||||
op.create_index("ix_auth_sessions_expires_at", "auth_sessions", ["expires_at"])
|
||||
op.create_index("ix_auth_sessions_revoked_at", "auth_sessions", ["revoked_at"])
|
||||
op.create_index(
|
||||
"ix_auth_sessions_principal_active",
|
||||
"auth_sessions",
|
||||
["principal_type", "revoked_at", "expires_at"],
|
||||
)
|
||||
op.create_index(
|
||||
"ix_auth_sessions_tenant_username",
|
||||
"auth_sessions",
|
||||
["tenant_id", "username"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_auth_sessions_tenant_username", table_name="auth_sessions")
|
||||
op.drop_index("ix_auth_sessions_principal_active", table_name="auth_sessions")
|
||||
op.drop_index("ix_auth_sessions_revoked_at", table_name="auth_sessions")
|
||||
op.drop_index("ix_auth_sessions_expires_at", table_name="auth_sessions")
|
||||
op.drop_index("ix_auth_sessions_metric_session_id", table_name="auth_sessions")
|
||||
op.drop_index("ix_auth_sessions_username", table_name="auth_sessions")
|
||||
op.drop_index("ix_auth_sessions_employee_id", table_name="auth_sessions")
|
||||
op.drop_index("ix_auth_sessions_principal_type", table_name="auth_sessions")
|
||||
op.drop_index("ix_auth_sessions_tenant_id", table_name="auth_sessions")
|
||||
op.drop_table("auth_sessions")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user