feat(platform): close AI expense value loop
Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
@@ -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` 时会按设计拒绝真实回写。
|
||||
Reference in New Issue
Block a user