diff --git a/README.md b/README.md index 4921525..e92bf9f 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Current Phase 0 / Phase 1 implementation includes: - local Search / Vector-like retrieval for governed candidate recall - customer knowledge candidate-only serving API - OAuth 2.1 demo authorization server with PKCE, JWKS, introspection, and revoke -- Admin Console, replay / backfill, status mapping, MDM, reconcile, and audit foundations +- Admin Console with 6 governance workspaces, ECharts topology/trend views, replay / backfill, status mapping, MDM, reconcile, permission simulation, MCP sessions, and audit foundations ## Project status @@ -302,6 +302,13 @@ mvn spring-boot:run - `http://localhost:8080/.well-known/openid-configuration` - `http://localhost:8080/api/customers?limit=10` +For Admin Console browser checks and responsive screenshots after the application is running: + +```powershell +npx playwright install chromium +node scripts/admin-console-verify.mjs +``` + 4. run tests ```powershell diff --git a/docs/00-llm-wiki-index.md b/docs/00-llm-wiki-index.md index f03c5f7..56c6fb5 100644 --- a/docs/00-llm-wiki-index.md +++ b/docs/00-llm-wiki-index.md @@ -22,6 +22,8 @@ | `docs/04-admin-console-redesign-plan.md` | Admin Console 全面改造方案、中文国际化、ECharts/Topo 可视化和验收标准 | 改 Admin Console 信息架构、页面布局、图表、拓扑视图、文案或国际化前 | | `docs/05-local-runbook.md` | 本地详细运行手册与命令参考 | 需要完整本地跑通、演示、联调、验收命令时 | | `docs/05-oss-reviewer-checklist.md` | OSS reviewer 视角的成熟度检查清单 | 做开源展示优化、自检首页可信度或准备外部评审前 | +| `docs/06-commercial-acceptance-execution-plan.md` | 商业级试运行验收执行计划,包含脚本、报告、截图、关键表计数、权限 fail closed 和完成判定 | 准备让 AI/Agent 按步骤实现或执行商业级试运行验收包前 | +| `docs/07-architecture-global-gap-checklist.md` | 基于目标架构图的全局未完成清单,按体系能力而非小功能点盘点缺口、优先级和完成判定 | 做全局路线图、模块边界复盘、跨层缺口拆解、对外汇报当前未完成能力前 | | `docs/data-stack-selection.md` | 基础设施和技术选型真相源 | 改 Docker、Redpanda、PostgreSQL、MinIO、ClickHouse、K8s 前 | | `docs/architecture-visual-plan.md` | 架构图和流程图对齐 | 改流程、画图、向非技术人员解释系统前 | | `docs/mcp-ai-application-auth-design.md` | MCP / AI 应用层鉴权、OAuth、JWT、API Key、tool scope 契约 | 改 MCP、OAuth、JWT、API Key、service account、tool scope、鉴权审计前 | @@ -138,6 +140,23 @@ - 图表是否优先使用 ECharts,链路/血缘/影响范围是否使用 Topo / Graph 类视图。 - 高风险操作是否有影响说明、二次确认和审计闭环。 - 验收是否包含 Playwright 截图、关键流程验证和多分辨率布局检查。 + +### 3.8 做商业级试运行验收包 + +必读: + +- `docs/03-commercial-product-completion-plan.md` +- `docs/05-local-runbook.md` +- `docs/06-commercial-acceptance-execution-plan.md` +- `docs/02-implementation-status.md` + +必须确认: + +- 是否生成机器可读 JSON 报告和人工可读 Markdown 报告。 +- 是否覆盖 mock ingestion、normalize、canonical outbox、Search / Vector、权限 fail closed、MDM、Replay / Backfill、Admin Console 截图。 +- 是否包含关键数据库表计数。 +- 是否把失败、partial、blocked 与 pass 明确区分。 +- 是否明确声明真实吉客云、MCP Server、生产 OpenSearch / Qdrant / ClickHouse 不在本轮完成范围。 ## 4. 架构疑问处理规则 diff --git a/docs/02-implementation-status.md b/docs/02-implementation-status.md index 32a89bd..0b64e98 100644 --- a/docs/02-implementation-status.md +++ b/docs/02-implementation-status.md @@ -35,6 +35,7 @@ mock 吉客云数据 - Docker Compose。 - 已补基础 OSS 仓库文件:`LICENSE`、`CONTRIBUTING.md`、`SECURITY.md`、`CHANGELOG.md`,以及最小 GitHub Actions `CI` 工作流(`mvn test`)。 - `README.md` 顶部已按 OSS 首页视角重写,详细本地运行手册已拆分到 `docs/05-local-runbook.md`,并补充 `docs/05-oss-reviewer-checklist.md` 用于外部评审视角自检。 +- 已新增 `docs/06-commercial-acceptance-execution-plan.md`,作为后续 AI / Agent 实现商业级试运行验收包的详细执行文档,明确脚本、报告、截图、关键表计数、权限 fail closed、失败处理和完成判定;该文档是验收执行计划,不代表商业级试运行验收已经完成。 - 本地文件 raw object adapter,路径为 `storage/raw-objects`。 - MinIO SDK raw object adapter,可通过 `APP_RAW_OBJECT_STORE_TYPE=minio` 开启;默认仍使用本地 adapter。 - 本地与 MinIO raw object adapter 已支持扫描 temporary / linked 对象。 @@ -273,8 +274,9 @@ MockRecordProvider - 客户多源 source missing 已支持 `partial_source_missing` 标签:某个来源缺失但仍有其他 active source fresh 时,canonical customer 保持原 `sync_health_status`,并通过 canonical change outbox 写入 metadata 变化。 - Reconcile 已支持 source_missing 恢复:源端记录重新出现在 linked raw 中且 payload hash 与 source model 一致时,会把对应 source model 恢复为 `fresh`;如果 canonical 所有 active source 都不再 missing,会移除 `partial_source_missing` 并将 `source_missing` canonical 恢复为 `fresh`。 - Dead Letter:查询 source event dead letters、transform errors 和 replay dead letters,支持 `retry` / `replay` / `ignore` / `quarantine`,写 `dead_letter_actions`。 -- Admin Console:`GET /admin` 已切换为 React + Ant Design 静态前端,页面登录后读取真实 `/internal/admin/*` 与 `/api/search`、`/api/vector/search`、`/api/ai/retrieve` 接口,不再是开发调试表格页。当前包含总览、数据同步、数据质量、权限与组织、Search / Vector、MCP 连接、MDM 主数据治理、Replay / Backfill、状态映射、审计日志、系统设置 11 个中文页面;MCP 连接页可按时间窗口查看 OAuth 用户与 service account/API Key 近期活跃、可续期登录态、client、最近审计,并支持管理员二次确认 reason 后踢用户下线;页面用中文状态、空态、错误态和 Tooltip 解释 Raw、SCE、Outbox、Canonical、MDM、Replay、Backfill、Search、Vector 等专有名词;高风险操作使用二次确认并由后端写入审计,401 会自动清理登录态,其他接口错误统一提示“系统异常请稍后重试”。 -- Admin Console 二次改造已完成商业级试运行 UI:总览改为运营驾驶舱,展示健康评分、同步链路拓扑、优先级待处理、失败分布、治理分布和快捷动作;帮助中心提供普通运营工作流引导;数据同步、数据质量、权限、Search / Vector、MDM、Replay / Backfill、状态映射、审计和系统设置均按运营控制台重新组织信息密度与操作入口;桌面和窄屏布局已收敛导航、内容宽度、表格容器和按钮换行,避免横向滚动、重叠和按钮溢出。 +- Admin Console:`GET /admin` 已切换为 React + Ant Design + ECharts 静态前端,页面登录后读取真实 `/internal/admin/*` 与 `/api/search`、`/api/vector/search`、`/api/ai/retrieve` 接口,不再是开发调试表格页。当前一级导航已收敛为 6 个入口:总览、数据接入、数据治理、检索与索引、权限与身份、审计与系统;每个一级入口通过二级 Tabs 承载具体能力。MCP 连接页可按时间窗口查看 OAuth 用户与 service account/API Key 近期活跃、可续期登录态、client、最近审计,并支持管理员二次确认 reason 后踢用户下线;页面用中文状态、空态、错误态和 Tooltip 解释原始层、来源变更事件、待发布事件、统一模型、MDM、数据重放、历史回补、关键词检索、语义召回等专有名词;高风险操作使用影响说明、二次确认和审计提示,401 会自动清理登录态,其他接口错误统一提示“系统异常请稍后重试”。 +- Admin Console 全面改造已按 `docs/04-admin-console-redesign-plan.md` 落地商业级试运行 UI:总览改为“今日工作台”,首屏新增“这个后台可以怎么玩”的项目负责人玩法区,把系统收敛为数据闭环、治理恢复、安全检索、权限边界四条产品证明线,并在“玩法中心”提供可执行剧本;页面继续展示 ECharts 健康仪表、今日异常、优先级待处理、同步链路 Topo、任务趋势、审计趋势和索引状态;数据接入页围绕连接状态、同步历史、结构变化和失败事件组织;数据治理页整合数据质量、死信处理、数据重放、历史回补、状态映射和 MDM;检索与索引页展示“候选命中 -> 统一模型回表 -> 权限过滤 -> 返回结果”的 Graph 链路;权限与身份页覆盖用户、部门、角色、策略、模拟和 MCP 连接;审计与系统页覆盖管理员操作、权限决策、安全事件、环境信息和试运行边界。详情查看已统一为“概览 / 上下文 / 影响范围 / 审计记录”抽屉;桌面和窄屏布局已收敛导航、内容宽度、表格容器和按钮换行,避免横向滚动、重叠和按钮溢出。 +- 已新增 `scripts/admin-console-verify.mjs`,用于登录 Admin Console、访问核心工作区、检查 1440 / 1280 / 1024 / 768 宽度并保存 Playwright 截图到 `target/admin-console-verification`。 - 内部治理接口:`/internal/admin/summary`、`/internal/admin/schema/*`、`/internal/admin/reconcile*`、`/internal/admin/dead-letters*`、`/internal/admin/checkpoints`、`/internal/admin/raw-orphans/scan`。 - 权限策略管理接口:`/internal/admin/permission/policies`、`/internal/admin/permission/policies/{id}/disable`、`/internal/admin/permission/policies/versions/{version}/publish`、`/internal/admin/permission/policies/versions/{version}/rollback`、`/internal/admin/permission/simulate`。 - Admin 身份与审计接口:`/internal/admin/identity/users`、`/internal/admin/identity/departments`、`/internal/admin/identity/roles`、`/internal/admin/audit/admin-operations`、`/internal/admin/audit/permission-decisions`。 @@ -287,8 +289,8 @@ MockRecordProvider - OAuth RS256 私钥可通过 `APP_OAUTH_RSA_PRIVATE_KEY_PEM` 配置;未配置时会生成进程内开发密钥,适合本地验收,不适合多实例生产部署或跨重启验签稳定性要求。 - Gateway 如需“退出后 access token 立即不可用”,应配置 `APP_AUTH_TOKEN_INTROSPECTION_MODE=required` 和 `APP_AUTH_TOKEN_INTROSPECTION_URI=http://localhost:8080/introspect`;仅本地 JWKS 验签只能判断签名和过期时间,不能感知已提升的 `token_version`。 - 当前密码是 SHA-256 演示 hash,生产级密码哈希、MFA、密码策略、账号锁定仍未实现。 -- Admin Console 前端现在是 React + Ant Design 试运行控制台,已从开发调试面板升级为运营 / 实施可读的中文管理后台;它仍依赖现有后端 mock 试运行接口,不代表生产部署、真实吉客云接入、企业 SSO / MFA 或完整独立 permission-service 已完成。 -- 已新增 `docs/04-admin-console-redesign-plan.md`,明确下一轮 Admin Console 全面改造方向:重组信息架构、推进主文案中文国际化、使用 ECharts 取代当前弱表达图表,并在同步链路、检索链路、血缘/影响分析中引入 Topo / Graph 类视图;该文档目前是方案和验收标准,不代表页面已按方案实现。 +- Admin Console 前端现在是 React + Ant Design + ECharts 的商业级试运行治理后台;它仍依赖现有后端 mock 试运行接口,不代表生产部署、真实吉客云接入、企业 SSO / MFA 或完整独立 permission-service 已完成。 +- `docs/04-admin-console-redesign-plan.md` 的信息架构、中文国际化、ECharts/Topo、详情抽屉和高风险操作确认要求已在静态 Admin Console 中实现;后续仍可继续补独立前端构建、自动化视觉回归和更细粒度角色按钮隐藏。 - 权限服务当前运行在 modular monolith 内,还不是独立部署的 permission-service。 - Redis grants 缓存尚未接入;当前已实现的是应用内策略决策缓存,权限决策携带 `policy_version`,并只使用当前已发布策略版本。 @@ -437,6 +439,7 @@ MockRecordProvider ## 3. 尚未实现能力 +- 已新增 `docs/07-architecture-global-gap-checklist.md`,用于按目标架构图从全局体系能力视角盘点未完成项、优先级和完成判定;该文档是缺口清单,不代表其中任何目标态能力已经完成。 - 完整 MCP Server。 - 完整 Vector DB / 外部 LLM RAG answer 仍未实现;虽然 customer knowledge 的 candidate-id-only serving API 已补齐,但当前 `/api/vector/search` / `/api/ai/retrieve` 仍是本仓库本地 Search/Vector 演示接口,不是 MCP 最终业务响应接口,MCP 侧 `search_customer_knowledge` 也尚未在本仓库重新接入。 - MCP 商业级执行规格已写入 `docs/mcp-ai-application-auth-design.md`;本仓库已具备 API Key / service account 基础身份能力,但 MCP endpoint、tool registry、tool wrapper、MCP 审计仍未实现。 diff --git a/docs/04-admin-console-redesign-plan.md b/docs/04-admin-console-redesign-plan.md new file mode 100644 index 0000000..f589298 --- /dev/null +++ b/docs/04-admin-console-redesign-plan.md @@ -0,0 +1,573 @@ +# Admin Console 全面改造方案 + +版本:v1.0 +日期:2026-05-30 +范围:`ai-data-foundation` 现有 Admin Console 的信息架构、视觉层级、交互工作流、中文国际化、图表与拓扑视图升级方案 + +## 1. 文档目的 + +本文用于把当前 Admin Console 从“开发调试可读”升级为“运营 / 实施 / 管理员可用”的产品级后台。 + +本文只定义改造方案、设计原则、页面结构、图表规范、中文国际化规范和验收标准,不代表当前代码已经实现。 + +任何开始实施本方案的工作,仍必须先阅读: + +1. `AGENTS.md` +2. `docs/00-llm-wiki-index.md` +3. `docs/01-architecture-execution-contract.md` +4. `docs/02-implementation-status.md` +5. `docs/03-commercial-product-completion-plan.md` +6. 本文 + +## 2. 当前问题判断 + +基于当前 `src/main/resources/static/admin-console/app.js` 和实际页面结构,当前控制台已经具备较完整功能面,但存在以下问题: + +### 2.1 信息架构问题 + +- 一级导航过多,当前并列 11 个入口,认知负担偏高。 +- 页面分组更接近技术模块平铺,不够贴近运营任务流。 +- 多个页面都在展示“表”,但没有形成“先判断 -> 再定位 -> 再处理 -> 再留痕”的闭环。 + +### 2.2 文案与国际化问题 + +- 存在明显中英混排:`Search / Vector`、`Replay / Backfill`、`MDM`、`Dead Letter` 等作为主标题直接暴露。 +- 大量后端字段或 snake_case 被直接展示给用户,例如 `source_entity`、`mapping_version`、`policy_version`。 +- 同一概念的中文叫法不完全统一,存在技术视角与运营视角混用。 + +### 2.3 可视化问题 + +- 当前图表和状态展示以轻量自绘块、列表、简单条形分布为主,表达力弱。 +- 同步链路、失败分布、权限拒绝、任务历史、数据血缘等核心对象没有形成成熟的数据产品图形语言。 +- 现有图表更像“占位统计块”,不够像企业级工作台,视觉完成度不足。 + +### 2.4 工作流问题 + +- 高风险操作虽然已有确认,但影响范围说明仍不够前置。 +- 详情视图、日志视图、治理对象视图的上下文信息不足。 +- 页面更偏“看数据”,不够偏“处理问题”。 + +## 3. 竞品参考结论 + +本方案参考了以下公开官方资料: + +- Fivetran Dashboard + +- Fivetran Status + +- OpenMetadata Features + +- OpenMetadata Data Quality Tab + +- Supabase Logs Explorer + +- Supabase User Management + +- Atlan Lineage + + +提炼出的可用结论: + +### 3.1 来自 Fivetran + +- 状态页必须先给关键状态,再给历史与事件。 +- 连接详情应拆为状态、结构、设置、历史,而不是把所有信息堆在一个总表里。 +- 同步问题排查应以时间线和事件流为核心。 + +### 3.2 来自 OpenMetadata + +- 质量、血缘、元数据应该围绕对象详情组织,而不是只放在全局列表里。 +- 数据质量页需要支持测试结果、失败数、时间筛选和详情下钻。 +- 图形视图必须有对象上下文,不是只有边和节点。 + +### 3.3 来自 Supabase + +- 日志工作台应该具备来源筛选、时间筛选、查询和导出能力。 +- 用户与身份管理要有对象详情,不仅是一个账号列表。 +- 后台产品 UI 应该安静、克制、密度高,但仍然易扫描。 + +### 3.4 来自 Atlan + +- 血缘视图必须支持上游、下游和影响分析。 +- 节点 hover / popover 应显示相关上下文,而不是只有 ID。 +- 数据流图应该表达“资产”和“过程”的区别,而不是把一切都当成同一类节点。 + +## 4. 改造目标 + +本次改造目标不是“美化当前页面”,而是把控制台升级为真正可用于商业级试运行验收的运营后台: + +- 让运营、实施、管理员能快速判断系统健康度。 +- 让高频问题可以在 3 步内定位和执行动作。 +- 让页面主文案全面中文化,且术语一致。 +- 让图表与拓扑图承担真实信息表达,而不是装饰。 +- 让所有高风险操作形成“影响说明 + 二次确认 + 审计留痕”的闭环。 + +## 5. 总体设计原则 + +### 5.1 产品定位 + +控制台定位为“AI 数据底座运营治理后台”,不是开发调试面板,不是营销式 dashboard。 + +### 5.2 风格原则 + +- 安静、克制、偏运营工具风格。 +- 高信息密度,但不堆卡片墙。 +- 强调状态、历史、影响、动作,不强调装饰。 +- 使用清晰的中文分组与术语,减少缩写直接暴露。 + +### 5.3 交互原则 + +- 一级导航少,二级 Tabs 多。 +- 详情优先用抽屉或详情页承载,不把信息挤在表格里。 +- 图表负责看趋势与结构,表格负责查明细,抽屉负责看上下文。 +- 所有高风险动作必须先展示影响范围。 + +### 5.4 技术方向原则 + +- 组件框架继续使用 Ant Design。 +- 图表主力升级为 ECharts。 +- 链路、血缘、影响范围、节点关系优先使用 Topo / Graph 类视图,不再依赖简易块状占位图。 +- 不再继续扩展当前“自绘小条形 + 数据块”风格。 + +## 6. 信息架构重组 + +建议把当前 11 个一级菜单收敛为 6 个一级菜单: + +1. 总览 +2. 数据接入 +3. 数据治理 +4. 检索与索引 +5. 权限与身份 +6. 审计与系统 + +### 6.1 总览 + +- 运营驾驶舱 +- 今日异常 +- 关键任务 +- 最近变更 + +### 6.2 数据接入 + +- 同步任务 +- Raw / 来源变更事件 / 待发布事件 +- Schema Drift +- 对账结果 + +### 6.3 数据治理 + +- 数据质量 +- 死信处理 +- 数据重放 +- 历史回补 +- 状态映射 +- 主数据治理 + +### 6.4 检索与索引 + +- 关键词检索 +- 语义召回 +- 索引状态 +- 重建任务 +- 增量消费 + +### 6.5 权限与身份 + +- 用户 +- 部门 +- 角色 +- 权限策略 +- 权限模拟 +- MCP 连接 + +### 6.6 审计与系统 + +- 管理员操作审计 +- 权限决策日志 +- 安全事件 +- 试运行边界 +- 环境与版本 + +## 7. 页面改造方案 + +### 7.1 总览页 + +目标:从“指标展示页”升级为“今日工作台”。 + +结构建议: + +- 顶部:全局健康分、今日异常摘要、最近一次同步结果、正在运行任务数 +- 中部左:优先级待处理事项 +- 中部右:近期失败与风险聚类 +- 下部:同步链路拓扑、任务趋势、审计趋势、索引状态 + +重点变化: + +- 不再以多个散点式 metric 卡片为主。 +- 增加“今天最需要处理什么”的明确列表。 +- 健康分必须能解释扣分来源。 + +### 7.2 数据接入页 + +目标:让用户快速理解“哪条同步出了问题,影响到哪里”。 + +建议拆成 4 个二级页签: + +- 连接状态 +- 同步历史 +- 结构变化 +- 失败事件 + +重点变化: + +- 增加同步时间线和失败阶段分解图。 +- 增加连接器状态、checkpoint、Raw、SCE、Outbox、Canonical 的链路拓扑。 +- 增加按实体、来源系统、时间窗口、状态的筛选。 + +### 7.3 数据治理页 + +目标:把质量、死信、Replay、Backfill、状态映射、MDM 放进一个连续治理工作区。 + +建议二级页签: + +- 数据质量 +- 死信处理 +- 数据重放 +- 历史回补 +- 状态映射 +- 主数据治理 + +重点变化: + +- 所有治理对象统一提供“详情抽屉”。 +- 死信处理和 Replay 要能看影响范围、失败原因、相关对象。 +- MDM 要突出候选、提案、快照、操作记录。 + +### 7.4 检索与索引页 + +目标:把当前工程化的 Search / Vector 页面改成“检索验证工作台”。 + +建议二级页签: + +- 检索验证 +- 索引状态 +- 重建任务 +- 增量消费 + +重点变化: + +- 清晰展示三段式链路:候选命中 -> Canonical 回表 -> 权限过滤。 +- 检索结果要说明“为什么命中”与“为什么被过滤”。 +- 强化索引重建和增量消费状态可视化。 + +### 7.5 权限与身份页 + +目标:从“组织 + 策略 + 表格”升级为“身份与授权管理台”。 + +建议二级页签: + +- 用户 +- 部门 +- 角色 +- 策略 +- 模拟 +- MCP 连接 + +重点变化: + +- 用户详情要展示角色、部门、最近活跃、token 版本、会话摘要。 +- 策略页要突出草稿、已发布、回滚链路。 +- MCP 连接页要更像“会话工作台”而不是列表页。 + +### 7.6 审计与系统页 + +目标:从“审计表格”升级为“日志与系统工作台”。 + +建议二级页签: + +- 管理员操作 +- 权限决策 +- 安全事件 +- 环境信息 +- 试运行边界 + +重点变化: + +- 提供统一时间筛选、主体筛选、动作筛选、风险等级筛选。 +- 审计详情应展示关联请求、影响对象、执行结果、元数据摘要。 +- 系统页只保留环境、版本、边界,不再堆功能入口。 + +## 8. 中文国际化方案 + +### 8.1 总原则 + +界面主文案全部中文化。 +英文缩写只在以下场景保留: + +- 行业标准缩写:`API`、`OAuth`、`JWT` +- 项目域缩写:`MCP`、`MDM` +- 技术说明或 tooltip 中作为辅助解释 + +### 8.2 主文案替换规则 + +| 当前叫法 | 改造后主叫法 | 备注 | +|---|---|---| +| Search | 关键词检索 | 不作为英文主标题保留 | +| Vector | 语义召回 | 不作为英文主标题保留 | +| Search / Vector | 检索与索引 | 页级主标题使用中文 | +| Replay | 数据重放 | 可在帮助信息中带英文 | +| Backfill | 历史回补 | 可在帮助信息中带英文 | +| Dead Letter | 死信记录 | 可在详情或文档中括注 | +| Raw | 原始层 | | +| SCE | 来源变更事件 | 页面主文案使用中文 | +| Outbox | 待发布事件 | | +| Canonical | 统一模型 | | +| Connector | 数据连接器 | | +| field lock | 字段锁定 | | +| Lineage | 数据血缘 | | +| dry_run | 影响预演 | 不直接显示枚举值 | +| execute | 执行重算 | 不直接显示枚举值 | +| source_entity | 来源实体 | 不直接显示字段名 | +| mapping_version | 映射版本 | | +| policy_version | 策略版本 | | +| client_id | 客户端标识 | | +| service account | 服务账号 | | +| API Key | 接口密钥 | 可保留 API 缩写 | + +### 8.3 不允许直接暴露的内容 + +- snake_case 字段名 +- 后端枚举原值 +- 数据库表名 +- 面向工程师的内部实现词 + +### 8.4 术语统一要求 + +所有页面、弹窗、按钮、空状态、错误态必须共用一套术语表。 +同一概念不得出现两种主叫法。 + +## 9. 图表与拓扑视图方案 + +当前图表视觉质量不足,后续实现阶段必须明确升级为“ECharts 主导 + Ant Design 承载 + Topo/Graph 表达关系”。 + +### 9.1 图表技术栈建议 + +- 主图表:ECharts +- 容器与布局:Ant Design +- 关系表达:ECharts Graph / Sankey / Tree / 自定义拓扑布局 +- 表格、抽屉、弹窗、筛选:Ant Design + +### 9.2 明确不建议继续使用的做法 + +- 继续扩展当前简单 `bar-list` 视觉 +- 继续依赖静态方块和数字堆叠表达复杂关系 +- 用多个小卡片替代趋势图、阶段图、关系图 + +### 9.3 各页面推荐图形类型 + +#### 总览页 + +- 健康分:ECharts 仪表盘或环形评分图 +- 今日异常分布:堆叠条形图 +- 最近任务趋势:折线图 / 面积图 +- 风险优先级:横向排序条形图 + +#### 数据接入页 + +- 同步历史:时间轴柱状图 +- 阶段耗时:堆叠柱状图 +- 失败来源:分组条形图 +- 接入链路:Topo / 有向图 + +#### 数据治理页 + +- 质量问题分布:旭日图或分层条形图 +- 死信处理流向:Sankey +- Replay / Backfill 状态:时间线 + 分段状态条 +- MDM 冲突与提案:漏斗或阶段流图 + +#### 检索与索引页 + +- 检索命中漏斗:候选 -> 回表 -> 过滤 -> 返回 +- 索引任务状态:堆叠条形图 +- 增量消费积压:趋势折线图 +- 候选来源占比:环形图 + +#### 权限与身份页 + +- 会话活跃趋势:折线图 +- 拒绝原因分布:条形图 +- 角色 / 主体分布:分组柱状图 +- 策略命中分布:帕累托式排序图 + +#### 审计与系统页 + +- 管理操作趋势:折线图 +- 权限拒绝趋势:面积图 +- 安全事件来源:分组条形图 +- 审计事件流:时间轴 + +### 9.4 Topo / Graph 重点使用场景 + +以下场景建议优先使用拓扑视图,而不是普通列表: + +- Connector -> Checkpoint -> Raw -> SCE -> Outbox -> Canonical 同步链路 +- Search / Vector 三段式召回链路 +- MDM merge / split 影响关系 +- 数据血缘与影响分析 +- 多来源 customer / order 的关联关系 + +### 9.5 图形风格要求 + +- 统一颜色语义:正常、警告、失败、处理中 +- 不用装饰性渐变球、无意义阴影和营销型视觉 +- 图中节点 hover 时必须能看到上下文 +- 关系图不允许只给 ID,必须给名称、状态、影响摘要 + +## 10. 组件与交互规范 + +### 10.1 统一布局 + +- 页面顶部统一包含:标题、说明、主操作 +- 列表区统一包含:筛选、批量操作、详情入口 +- 详情统一采用抽屉或独立详情页 + +### 10.2 高风险动作规范 + +以下动作必须统一二次确认: + +- 重试 / 重放 / 隔离 / 忽略死信 +- 执行 Replay / Backfill +- 发布 / 回滚权限策略 +- 审批 MDM merge / split +- 禁用状态映射 +- 踢用户下线 + +确认弹窗必须展示: + +- 操作对象 +- 影响数量 +- 风险说明 +- 审计提示 + +### 10.3 详情抽屉结构 + +详情抽屉建议统一为四段: + +1. 概览 +2. 上下文 +3. 影响范围 +4. 审计记录 + +## 11. 分阶段实施建议 + +### 第一阶段:结构与文案统一 + +- 重构一级导航与二级页签 +- 建立完整中文术语表 +- 替换页面主标题、按钮、表格列名、弹窗文案 + +### 第二阶段:核心高频页改造 + +- 总览页 +- 数据接入页 +- 数据治理页 +- 权限与身份页 + +### 第三阶段:深度工作台与可视化升级 + +- 检索与索引页 +- 审计与系统页 +- 全量 ECharts 化 +- Topo / Graph 视图 +- 详情抽屉统一化 + +## 12. 验收标准 + +以下标准必须全部满足,才算本次改造通过验收。 + +### 12.1 信息架构验收 + +- 一级导航不超过 6 个。 +- 所有核心能力都能在 3 次点击内到达。 +- 页面结构与 `docs/03-commercial-product-completion-plan.md` 第 12 章要求的 Admin Console 范围一致。 + +### 12.2 中文国际化验收 + +- 导航、标题、按钮、表单、表格、弹窗、空状态、错误提示全部中文化。 +- 不再面向运营用户直接暴露 snake_case 字段名。 +- 不再使用英文作为页面主标题。 +- 仅 `MCP`、`MDM`、`API`、`OAuth`、`JWT` 可作为保留缩写。 +- 全局术语一致,不出现同义混用。 + +### 12.3 视觉与图表验收 + +- 总览、同步、治理、检索、权限、审计 6 个一级页至少各有一个高质量 ECharts 视图。 +- 同步链路和检索链路至少各有一个 Topo / Graph 类视图。 +- 图表不再以当前简易 `bar-list` 和静态块状图为主。 +- 图表配色、状态语义、tooltip 风格统一。 +- 节点与曲线图在桌面端与常见笔记本宽度下不重叠、不挤压、不模糊。 + +### 12.4 运营可用性验收 + +- 值班人员能在 10 秒内从总览页判断“今天最需要处理什么”。 +- 同步失败可在 3 步内定位到对象、原因和可执行动作。 +- 死信、Replay、状态映射、MDM、权限策略 5 类高频动作都能独立完成。 +- 每个高风险动作都能看到影响说明和审计说明。 + +### 12.5 交互一致性验收 + +- 所有列表页都具备搜索、筛选、排序、分页。 +- 所有治理对象都具备统一详情视图。 +- 所有高风险动作都经过二次确认。 +- 所有页面的主操作位置一致。 + +### 12.6 响应式与布局验收 + +- 在 1440、1280、1024、768 宽度下无文本重叠。 +- 无不必要的横向滚动。 +- 表格长文本、长 ID、JSON 摘要不破坏布局。 +- 按钮、筛选器、Tabs 在窄屏下不溢出。 + +### 12.7 端到端验收 + +至少通过以下实际流程: + +1. 登录进入后台 +2. 在总览页定位异常 +3. 进入同步页定位一条失败事件 +4. 在死信页执行一条重试或重放 +5. 创建并查看一条 Replay 或 Backfill 任务 +6. 新增或禁用一条状态映射 +7. 查看一个 MDM 候选或提案 +8. 执行一次权限模拟 +9. 查看 MCP 连接或在线主体 +10. 查看一条管理员审计记录 + +### 12.8 安全与边界验收 + +- 无登录不可访问后台。 +- 无权限管理员看不到不该执行的按钮。 +- 越权操作必须 fail closed。 +- 页面不暴露 token、完整 API Key、Raw payload、Source payload、SQL 或 stack trace。 + +## 13. 交付物要求 + +本方案实施完成后,至少应交付: + +- 更新后的 Admin Console 页面代码 +- 统一中文术语表 +- ECharts 图表组件方案 +- Topo / Graph 关系视图方案 +- Playwright 验收脚本或快照 +- 更新后的 README +- 更新后的 `docs/02-implementation-status.md` +- 最终 Admin Console 验收报告 + +## 14. 当前状态声明 + +截至本文编写时: + +- 本文仅为正式改造方案。 +- 当前 Admin Console 仍是 React + Ant Design 的试运行控制台,不代表本方案已落地。 +- 当前图表表现仍偏弱,后续实现阶段必须按本文要求引入 ECharts 和 Topo / Graph 视图进行升级。 diff --git a/docs/05-local-runbook.md b/docs/05-local-runbook.md index b51d7ec..7dec7c6 100644 --- a/docs/05-local-runbook.md +++ b/docs/05-local-runbook.md @@ -292,16 +292,27 @@ http://localhost:8080/admin 当前页面包括: - 总览 -- 数据同步 -- 数据质量 -- 权限与组织 -- Search / Vector -- MCP 连接 -- MDM 主数据治理 -- Replay / Backfill -- 状态映射 -- 审计日志 -- 系统设置 +- 数据接入 +- 数据治理 +- 检索与索引 +- 权限与身份 +- 审计与系统 + +页面说明: + +- Admin Console 当前是 React + Ant Design + ECharts 静态前端。 +- 6 个一级入口下使用二级 Tabs 承载连接状态、同步历史、结构变化、死信处理、数据重放、历史回补、状态映射、MDM、关键词检索、语义召回、权限策略、权限模拟、MCP 连接、审计和环境信息。 +- 同步链路和检索链路使用 ECharts Graph 展示;表格长 ID 和 JSON 摘要通过 tooltip / 详情抽屉查看。 +- 高风险操作会展示影响说明、二次确认和审计提示,并通过后端已有审计接口留痕。 + +可复现浏览器验收脚本: + +```powershell +npx playwright install chromium +node scripts/admin-console-verify.mjs +``` + +脚本默认访问 `http://localhost:8080/admin`,使用 `admin / admin123` 登录,截图输出到 `target/admin-console-verification`。可通过 `ADMIN_CONSOLE_BASE_URL`、`ADMIN_CONSOLE_USER`、`ADMIN_CONSOLE_PASSWORD` 覆盖。 ## 15. 查询 API 示例 diff --git a/docs/06-commercial-acceptance-execution-plan.md b/docs/06-commercial-acceptance-execution-plan.md new file mode 100644 index 0000000..a29e83d --- /dev/null +++ b/docs/06-commercial-acceptance-execution-plan.md @@ -0,0 +1,1252 @@ +# 商业级试运行验收执行计划 + +版本:v1.0 +日期:2026-06-01 +适用范围:指导 AI / Agent 在当前仓库内把 `ai-data-foundation` 从“能力已实现较多”推进到“可重复、可证明、可交付的商业级试运行验收包” + +## 1. 本文目的 + +本文不是产品愿景,也不是普通开发计划。本文是一份给 AI / Agent 逐步执行的验收执行文档。 + +它解决三个问题: + +1. 后续 AI 不知道下一步该做什么。 +2. 后续 AI 不知道做到什么程度才算完成。 +3. 后续 AI 容易把局部成功、mock 成功、接口返回 200 描述成完整闭环成功。 + +本文要求 AI 最终交付一个“商业级试运行验收包”,至少包括: + +- 可重复执行的验收脚本。 +- 可复查的 JSON 验收结果。 +- Admin Console 浏览器截图。 +- 关键接口响应摘要。 +- 数据库关键表计数。 +- 权限 fail closed 证据。 +- MDM / Replay / Backfill / Search / Vector 证据。 +- 明确的未完成边界。 + +本文的核心原则是: + +```text +没有证据,不算完成。 +没有验收条件,不准宣布完成。 +只有局部链路,不准描述为完整闭环。 +``` + +## 2. 执行前必须阅读 + +任何 AI / Agent 开始执行本文任务前,必须先阅读: + +1. `AGENTS.md` +2. `docs/00-llm-wiki-index.md` +3. `docs/01-architecture-execution-contract.md` +4. `docs/02-implementation-status.md` +5. `docs/03-commercial-product-completion-plan.md` +6. `docs/05-local-runbook.md` +7. 本文 + +如果本轮涉及 Admin Console 页面、截图、交互或视觉验收,还必须阅读: + +1. `docs/04-admin-console-redesign-plan.md` + +如果本轮涉及架构解释、验收报告里的系统边界或流程图,还建议阅读: + +1. `docs/ai-data-foundation-architecture.md` +2. `docs/architecture-visual-plan.md` + +## 3. 当前阶段判断 + +当前项目仍处于 Phase 0 / Phase 1。 + +当前验收目标是: + +```text +复杂 mock 数据 + -> Raw / Source Change Event / Outbox + -> Source Model + -> Canonical Model + -> Canonical Change Outbox + -> 权限治理 / MDM / Replay / Backfill / 状态映射 + -> Search / Vector / API / Admin Console + -> 可重复验收报告 +``` + +当前不应把项目描述为: + +- 生产级数据平台。 +- 真实吉客云生产接入已完成。 +- 完整 MCP Server 已完成。 +- 完整企业 Permission Service 已完成。 +- OpenSearch / Qdrant / ClickHouse 生产链路已完成。 +- 企业 SSO / MFA / 生产级密码策略已完成。 + +## 4. 总体目标 + +本轮目标不是新增大量业务能力,而是把现有能力做成可重复验收闭环。 + +最终应交付: + +1. `scripts/commercial-acceptance-verify.mjs` +2. `target/commercial-acceptance-report.json` +3. `target/commercial-acceptance-report.md` +4. `target/admin-console-verification/*.png` +5. 更新后的 `docs/02-implementation-status.md` +6. 如运行方式变化,更新 `docs/05-local-runbook.md` +7. 如新增文档入口,更新 `docs/00-llm-wiki-index.md` + +其中 `target/commercial-acceptance-report.json` 必须可以被机器读取,`target/commercial-acceptance-report.md` 必须可以给人看。 + +## 5. 验收总原则 + +### 5.1 完成定义 + +只有以下全部满足,才允许声明“商业级试运行验收包完成”: + +| 类别 | 完成条件 | +|---|---| +| 静态检查 | `git diff --check` 通过 | +| 单元 / 集成测试 | `mvn test` 通过,或明确记录因本机 Docker 不可用导致 Testcontainers 不能运行 | +| 应用启动 | `mvn spring-boot:run` 可启动,并且 `/admin`、`/auth/login` 可访问 | +| 基础设施 | PostgreSQL 和 Redpanda 可用;如果 Docker 不可用,必须记录阻塞 | +| Mock 数据 | manifest 满足商业级规模,且通过 `node mock-data/validate-mock-data.mjs` | +| Ingestion | mock customers / trades / scenarios 能进入 Raw / SCE / Outbox | +| Normalize | customers / trades 能生成 Source Model / Canonical Model / Identity Map | +| Canonical Outbox | customer / order normalize 后有 canonical change events | +| Search / Vector | 全量重建或增量索引后,自然语言查询能返回候选并回表过滤 | +| 权限 | admin 可访问,sales 或无权限身份不能越权;越权不能伪装为空结果 | +| MDM | 能生成候选,能创建或查看 proposal / conflicts / locks 中至少一类治理对象 | +| Replay / Backfill | 能创建任务、运行任务,并能查看状态或失败原因 | +| Admin Console | Playwright 能登录并访问核心工作区,保存 1440 / 1280 / 1024 / 768 截图 | +| 报告 | JSON 和 Markdown 报告包含通过项、失败项、证据、边界 | +| 文档 | `docs/02-implementation-status.md` 已同步验收脚本能力和边界 | + +### 5.2 不允许的完成描述 + +以下情况不允许宣布完成: + +- 只跑了 `mvn test`,没有跑端到端接口。 +- 只启动了应用,没有导入 mock 数据。 +- 只导入了 mock 数据,没有 normalize。 +- 只 normalize 了 customer,没有覆盖 order。 +- 只查到了 Search / Vector 结果,没有验证权限过滤。 +- 只打开了 Admin Console,没有做登录、页面跳转和截图。 +- 只生成了报告文件,但报告里没有失败项和边界。 +- Docker / PostgreSQL / Redpanda 不可用,却把链路描述为已跑通。 + +### 5.3 失败处理原则 + +任何步骤失败时,AI 必须: + +1. 停止把后续依赖结果描述为完成。 +2. 把失败步骤写入 `target/commercial-acceptance-report.json`。 +3. 写明失败命令、失败原因、影响范围。 +4. 如果可以继续执行不依赖该步骤的检查,可以继续,但必须标记为 partial。 +5. 不允许用“看起来应该可以”替代验证结果。 + +## 6. 推荐执行顺序 + +本轮建议分 8 个阶段执行。 + +```text +阶段 A:收口当前工作区 +阶段 B:补商业验收脚本 +阶段 C:补验收报告生成 +阶段 D:跑静态检查与测试 +阶段 E:启动基础设施与应用 +阶段 F:执行端到端数据闭环 +阶段 G:执行 Admin Console 浏览器验收 +阶段 H:更新文档并输出最终结论 +``` + +## 7. 阶段 A:收口当前工作区 + +### 7.1 任务 + +执行: + +```powershell +git status --short +git diff --stat +``` + +AI 必须识别: + +- 哪些文件是本轮已有改动。 +- 哪些文件是用户或其他 Agent 已经改过的。 +- 是否存在未跟踪文件。 +- 是否存在与验收任务无关但不能回退的改动。 + +### 7.2 验收条件 + +通过条件: + +- AI 能明确列出当前 dirty worktree 状态。 +- AI 没有执行 `git reset --hard`、`git checkout --` 等破坏性命令。 +- AI 没有回退用户已有改动。 +- 如果存在冲突或无法判断的改动,AI 已说明并谨慎处理。 + +失败条件: + +- 未检查 `git status` 就直接改文件。 +- 回退了用户改动。 +- 把 unrelated dirty files 当作自己改动提交。 + +## 8. 阶段 B:实现商业验收脚本 + +### 8.1 目标文件 + +新增: + +```text +scripts/commercial-acceptance-verify.mjs +``` + +该脚本用于自动执行商业级试运行验收。 + +### 8.2 技术要求 + +脚本必须使用 Node.js,优先使用内置 `fetch`、`fs/promises`、`child_process`、`path`。 + +脚本必须: + +- 能在 Windows PowerShell 环境运行。 +- 默认访问 `http://localhost:8080`。 +- 支持环境变量覆盖。 +- 输出 JSON 报告。 +- 输出 Markdown 报告。 +- 每一步有 pass / fail / skip / partial 状态。 +- 每一步记录证据摘要。 +- 任一步失败不能导致整个报告丢失。 + +推荐环境变量: + +| 环境变量 | 默认值 | 用途 | +|---|---|---| +| `ACCEPTANCE_BASE_URL` | `http://localhost:8080` | 应用地址 | +| `ACCEPTANCE_ADMIN_USER` | `admin` | 管理员账号 | +| `ACCEPTANCE_ADMIN_PASSWORD` | `admin123` | 管理员密码 | +| `ACCEPTANCE_SALES_USER` | `sales001` | 销售账号 | +| `ACCEPTANCE_SALES_PASSWORD` | `sales123` | 销售密码 | +| `ACCEPTANCE_TENANT_ID` | `tenant_demo_001` | 租户 | +| `ACCEPTANCE_INTERNAL_TOKEN` | `dev-internal-token` | 内部接口 token | +| `ACCEPTANCE_CUSTOMER_LIMIT` | `800` | mock customer 导入数量 | +| `ACCEPTANCE_TRADE_LIMIT` | `100` | mock trade 导入数量 | +| `ACCEPTANCE_OUTPUT_DIR` | `target` | 输出目录 | +| `ACCEPTANCE_SKIP_BROWSER` | `false` | 是否跳过 Admin 浏览器验收 | + +### 8.3 脚本步骤 + +脚本至少包含以下步骤。 + +#### 8.3.1 读取 mock manifest + +读取: + +```text +mock-data/generated/manifest.json +``` + +必须校验: + +- `jkyun_customers >= 1000` +- `jkyun_orders >= 3000` +- `jkyun_order_lines >= 10000` +- `source_change_events >= 6000` +- `scenario_source_change_events >= 7` +- `notable_cases` 包含重复、乱序、schema drift、source missing、多源冲突等描述 + +通过条件: + +- manifest 存在。 +- JSON 可解析。 +- 数量满足商业级 mock 要求。 + +失败条件: + +- manifest 不存在。 +- JSON 解析失败。 +- 数量低于要求。 + +#### 8.3.2 应用健康检查 + +请求: + +```text +GET /admin +GET /.well-known/openid-configuration +``` + +通过条件: + +- `/admin` 返回 200。 +- `/admin` HTML 包含 `AI 数据底座运营控制台` 或 Admin Console 相关标题。 +- OIDC discovery 返回 200,且包含 issuer / jwks_uri / authorization_endpoint / token_endpoint。 + +失败条件: + +- 应用未启动。 +- 返回 401 / 403 / 500。 +- HTML 或 discovery 响应不符合预期。 + +#### 8.3.3 登录验证 + +请求: + +```text +POST /auth/login +``` + +管理员登录 body: + +```json +{ + "tenantId": "tenant_demo_001", + "username": "admin", + "password": "admin123" +} +``` + +销售登录 body: + +```json +{ + "tenantId": "tenant_demo_001", + "username": "sales001", + "password": "sales123" +} +``` + +通过条件: + +- admin 登录返回 accessToken。 +- sales 登录返回 accessToken。 +- token 不为空。 +- 报告中不能打印完整 token,只允许记录 token 存在和长度。 + +失败条件: + +- 登录失败。 +- 报告泄露完整 token。 + +#### 8.3.4 导入 mock 数据 + +请求: + +```text +POST /internal/ingest/mock/customers?limit={customerLimit} +POST /internal/ingest/mock/trades?limit={tradeLimit} +POST /internal/ingest/mock/scenarios +``` + +Header: + +```text +X-Internal-Token: dev-internal-token +``` + +通过条件: + +- 三个接口均返回 2xx。 +- 响应中能识别 attempted / written / skipped / created 等有效计数字段之一。 +- customers 和 trades 至少有 attempted 或 written 数量大于 0,除非报告证明之前已导入且幂等跳过。 + +失败条件: + +- 任一接口非 2xx。 +- 没有任何可证明导入发生或幂等跳过的计数。 + +#### 8.3.5 发布 source events + +请求: + +```text +POST /internal/publish/source-events?limit=800 +``` + +通过条件: + +- 返回 2xx。 +- 响应中 published / attempted / skipped / failed 等字段可解析。 +- 如果 Redpanda 不可用,必须记录失败原因;不能把 source publish 描述为已完成。 + +失败条件: + +- Redpanda 不可用且报告未标记失败。 +- 接口失败却继续描述为完整事件链路。 + +#### 8.3.6 Normalize customers / trades + +请求: + +```text +POST /internal/normalize/customers?limit={customerLimit} +POST /internal/normalize/trades?limit={tradeLimit} +``` + +通过条件: + +- 两个接口均返回 2xx。 +- 响应中 attempted / normalized / success / skipped / failed 等字段可解析。 +- customers 和 trades 都有成功或幂等跳过证据。 + +失败条件: + +- 只 normalize customers,没有 normalize trades。 +- normalize 失败但报告写成完成。 + +#### 8.3.7 发布 canonical events + +请求: + +```text +POST /internal/publish/canonical-events?limit=800 +``` + +通过条件: + +- 返回 2xx。 +- 响应中 published / attempted / skipped / failed 等字段可解析。 +- 如果 Redpanda 不可用,必须标记 canonical event publish 未通过。 + +失败条件: + +- 接口失败却描述为已发布。 + +#### 8.3.8 Search / Vector 索引 + +请求: + +```text +POST /internal/admin/search-vector/rebuild +POST /internal/index/search-vector/canonical-events?limit=800 +GET /internal/admin/search-vector/status +``` + +通过条件: + +- rebuild 返回 2xx。 +- index canonical events 返回 2xx。 +- status 返回可解析结果。 +- status 或 rebuild 响应里能证明 search / vector 文档存在或任务已执行。 + +失败条件: + +- 只调用 status,没有执行 rebuild 或增量索引。 +- 无法证明索引存在。 + +#### 8.3.9 API 查询 + +管理员查询: + +```text +GET /api/customers?limit=10 +GET /api/orders?limit=10 +``` + +Header: + +```text +Authorization: Bearer {adminToken} +``` + +通过条件: + +- 两个接口均返回 2xx。 +- 响应有 data / items / records 等结果字段之一。 +- 响应 metadata 中有 total / total_returned / permission_decision_id / masked_fields / policy_version 等权限相关信息之一。 + +失败条件: + +- 只查客户,不查订单。 +- 响应没有权限元信息。 + +#### 8.3.10 Search / Vector / AI Retrieve 查询 + +请求: + +```text +POST /api/search +POST /api/vector/search +POST /api/ai/retrieve +``` + +建议请求 body: + +```json +{ + "query": "智能水杯", + "entity_types": ["order_line"], + "limit": 10 +} +``` + +```json +{ + "query": "找最近物流慢的订单", + "entity_types": ["order"], + "limit": 10 +} +``` + +```json +{ + "query": "高价值但很久没复购的客户", + "entity_types": ["customer"], + "limit": 10 +} +``` + +通过条件: + +- 三个接口均返回 2xx。 +- 响应中能看到候选结果或明确的权限过滤结果。 +- 响应不能包含 Raw payload、Source payload、完整敏感字段。 +- 响应中必须有 permission_decision_id / masked_fields / total_filtered / policy_version 等权限证据之一。 + +失败条件: + +- 只返回候选 ID 但没有回表权限过滤证据。 +- sales 可以看到不应看到的敏感字段。 +- 无权限时返回“空结果”但没有 permission_denied 或权限审计证据。 + +#### 8.3.11 权限 fail closed 验证 + +必须至少做一种越权或无权限验证。 + +推荐请求: + +1. 使用 sales token 查询订单或高敏字段。 +2. 使用无 token 调用 `/api/customers?limit=10`。 +3. 使用伪造 header 调用 `/api/customers?limit=10`,例如 `X-User-Id`、`X-Role`。 + +通过条件: + +- 无 token 返回 401 / 403。 +- 伪造 header 不能放大权限。 +- sales 查询被对象级过滤或字段级脱敏。 +- 报告记录具体 HTTP status 和权限元信息。 + +失败条件: + +- 无 token 能访问业务数据。 +- 传 `X-User-Id` / `X-Role` 能改变权限结果。 +- 敏感字段未脱敏。 + +#### 8.3.12 MDM 验证 + +请求: + +```text +POST /internal/admin/mdm/candidates/generate +GET /internal/admin/mdm/candidates +GET /internal/admin/mdm/merge-proposals +GET /internal/admin/mdm/conflicts +GET /internal/admin/mdm/field-locks +GET /internal/admin/mdm/split-proposals +GET /internal/admin/mdm/split-operations +``` + +Header: + +```text +Authorization: Bearer {adminToken} +``` + +通过条件: + +- generate candidates 返回 2xx。 +- candidates 列表可查询。 +- 至少有一类 MDM 对象可被看到:candidate / proposal / conflict / field lock / split proposal / split operation。 +- 如果当前数据不足以生成候选,报告必须标记为 partial,并说明 mock 数据或导入范围不足。 + +失败条件: + +- 只调用 generate,没有查询结果。 +- 无任何 MDM 对象,却描述为 MDM 验收通过。 + +#### 8.3.13 Replay / Backfill 验证 + +创建 replay: + +```text +POST /internal/admin/replay/jobs?sourceEntity=trade&mode=dry_run&schemaVersion=current&mappingVersion=current&normalizerVersion=current +``` + +运行 replay: + +```text +POST /internal/admin/replay/jobs/{jobId}/run +GET /internal/admin/replay/jobs +``` + +创建 backfill: + +```text +POST /internal/admin/backfill/jobs?sourceEntity=trade&batchSize=500 +``` + +运行 backfill: + +```text +POST /internal/admin/backfill/jobs/{jobId}/run +GET /internal/admin/backfill/jobs +``` + +通过条件: + +- replay job 能创建。 +- replay job 能运行或返回明确失败原因。 +- backfill job 能创建。 +- backfill job 能运行或返回明确失败原因。 +- jobs 列表能查到对应任务。 +- 报告记录 job_id、status、error_message。 + +失败条件: + +- 创建任务后不运行。 +- 运行失败但不记录失败原因。 +- 没有 job_id 仍描述为完成。 + +#### 8.3.14 状态映射验证 + +请求: + +```text +GET /internal/admin/status-mappings +``` + +可选新增映射: + +```text +POST /internal/admin/status-mappings?sourceEntity=trade&sourceField=tradeStatus&sourceValue=acceptance_test_{timestamp}&canonicalBusinessStatus=cancelled&syncHealthStatus=fresh&governanceStatus=normal&mappingVersion={timestamp} +``` + +通过条件: + +- status mappings 可查询。 +- 如果执行新增映射,必须记录生成的 mapping id 或响应摘要。 +- 如果新增映射触发 replay dry-run 影响分析,必须记录 replay job 证据。 + +失败条件: + +- 修改映射但未记录审计 / 影响分析结果。 +- 用固定 sourceValue 反复污染测试数据且没有幂等策略。 + +#### 8.3.15 Admin Console 浏览器验收 + +优先复用: + +```text +scripts/admin-console-verify.mjs +``` + +运行: + +```powershell +npx playwright install chromium +node scripts/admin-console-verify.mjs +``` + +通过条件: + +- 脚本能登录 Admin Console。 +- 访问至少 6 个一级工作区: + - 总览 + - 数据接入 + - 数据治理 + - 检索与索引 + - 权限与身份 + - 审计与系统 +- 检查 1440 / 1280 / 1024 / 768 宽度。 +- 截图保存到 `target/admin-console-verification`。 +- 报告中记录截图文件路径。 + +失败条件: + +- 只打开 `/admin`,没有登录。 +- 只检查一个宽度。 +- 没有截图产物。 +- 页面有明显文本重叠、按钮溢出、横向滚动但仍标记通过。 + +### 8.4 输出 JSON 报告格式 + +`target/commercial-acceptance-report.json` 必须至少包含: + +```json +{ + "generated_at": "2026-06-01T00:00:00.000Z", + "base_url": "http://localhost:8080", + "status": "pass | fail | partial", + "summary": { + "passed": 0, + "failed": 0, + "partial": 0, + "skipped": 0 + }, + "steps": [ + { + "id": "auth.admin_login", + "title": "管理员登录", + "status": "pass", + "evidence": { + "http_status": 200, + "token_present": true + }, + "error": null + } + ], + "table_counts": { + "raw_records": 0, + "source_change_events": 0, + "source_event_outbox": 0, + "source_customers": 0, + "source_trades": 0, + "canonical_customers": 0, + "canonical_orders": 0, + "identity_map": 0, + "canonical_change_events": 0, + "search_documents": 0, + "vector_documents": 0, + "permission_decisions": 0, + "access_audit_logs": 0 + }, + "artifacts": { + "markdown_report": "target/commercial-acceptance-report.md", + "admin_screenshots_dir": "target/admin-console-verification" + }, + "boundaries": [ + "真实吉客云 HTTP 接入不在本轮验收范围", + "MCP Server 不在本仓库本轮验收范围", + "OpenSearch / Qdrant / ClickHouse 生产链路不在本轮验收范围" + ] +} +``` + +### 8.5 输出 Markdown 报告格式 + +`target/commercial-acceptance-report.md` 必须包含: + +1. 验收结论。 +2. 执行时间。 +3. 环境信息。 +4. 通过项列表。 +5. 失败项列表。 +6. 部分通过项列表。 +7. 关键接口证据。 +8. 权限 fail closed 证据。 +9. 数据库关键表计数。 +10. Admin Console 截图路径。 +11. 当前边界与未完成范围。 +12. 下一步建议。 + +如果有失败项,报告开头必须写: + +```text +本次验收未完全通过,不能声明商业级试运行验收完成。 +``` + +如果全部通过,报告开头必须写: + +```text +本次验收通过。结论仅限复杂 mock 数据商业级试运行,不代表生产完成。 +``` + +## 9. 阶段 C:数据库关键表计数 + +### 9.1 目标 + +验收报告必须包含关键表计数,不能只依赖 HTTP 响应。 + +### 9.2 推荐表 + +至少统计: + +```text +raw_records +source_change_events +source_event_outbox +source_customers +source_trades +source_trade_lines +canonical_customers +canonical_orders +canonical_order_lines +identity_map +canonical_change_events +search_documents +vector_documents +permission_decisions +access_audit_logs +admin_operation_audit +replay_jobs +backfill_jobs +mdm_match_candidates +mdm_merge_proposals +data_quality_issues +schema_drift_issues +``` + +### 9.3 查询方式 + +AI 可以选择以下任一方式: + +1. 使用 `psql`。 +2. 使用 `docker exec` 进入 postgres 容器执行 `psql`。 +3. 新增只读内部验收接口,但这会改变代码,需要同步文档和测试。 +4. 在 Node 脚本中调用现有 Admin summary 接口,无法覆盖的表标记为 unavailable。 + +推荐优先使用 `psql` 或 Docker。 + +示例: + +```powershell +docker exec ai-data-foundation-postgres-1 psql -U ai_data -d ai_data_foundation -c "select count(*) from raw_records;" +``` + +如果容器名不同,脚本必须自动发现或在报告中说明无法查询。 + +### 9.4 验收条件 + +通过条件: + +- 报告中至少有核心链路表计数: + - `raw_records` + - `source_change_events` + - `source_event_outbox` + - `canonical_customers` + - `canonical_orders` + - `identity_map` + - `canonical_change_events` + - `search_documents` + - `vector_documents` + - `permission_decisions` +- 计数为数字。 +- 与本次操作逻辑相符。 + +失败条件: + +- 没有表计数。 +- 表计数全是 0,但报告仍写验收通过。 +- 查询失败但未记录原因。 + +## 10. 阶段 D:静态检查与自动化测试 + +### 10.1 必跑命令 + +```powershell +git diff --check +mvn test +``` + +如果本轮改了 Admin Console JS,必须再跑: + +```powershell +node --check src/main/resources/static/admin-console/app.js +``` + +如果本轮改了 mock 数据生成或 manifest,必须再跑: + +```powershell +node mock-data/validate-mock-data.mjs +``` + +### 10.2 验收条件 + +通过条件: + +- `git diff --check` 无输出且退出码为 0。 +- `mvn test` 退出码为 0。 +- 如果 Docker Desktop 不可用导致 Testcontainers 失败,必须记录: + - Docker 错误。 + - 哪些测试受影响。 + - 是否有可替代的非 Docker 测试已通过。 + +失败条件: + +- 测试失败仍声明完成。 +- 只跑单测不说明未跑全量测试。 +- 忽略 `git diff --check`。 + +## 11. 阶段 E:基础设施与应用启动 + +### 11.1 启动 PostgreSQL / Redpanda + +执行: + +```powershell +docker compose up -d postgres redpanda +``` + +检查: + +```powershell +docker compose ps +``` + +通过条件: + +- postgres 容器 running / healthy。 +- redpanda 容器 running / healthy。 +- 如果 Docker 不可用,报告标记端到端验收 blocked,不能继续宣称完整通过。 + +### 11.2 启动应用 + +执行: + +```powershell +mvn spring-boot:run +``` + +应用启动后检查: + +```powershell +Invoke-WebRequest -Uri "http://localhost:8080/admin" -UseBasicParsing +Invoke-WebRequest -Uri "http://localhost:8080/.well-known/openid-configuration" -UseBasicParsing +``` + +通过条件: + +- 应用进程持续运行。 +- `/admin` 返回 200。 +- discovery 返回 200。 + +失败条件: + +- 应用启动后立即退出。 +- 端口被占用但未处理。 +- 后续验收使用了错误端口。 + +## 12. 阶段 F:端到端数据闭环验收 + +本阶段可以由 `scripts/commercial-acceptance-verify.mjs` 自动执行,也可以人工执行同样步骤。 + +### 12.1 必须覆盖的闭环 + +必须证明: + +```text +mock customers / trades / scenarios + -> raw_records + -> source_change_events + -> source_event_outbox + -> source models + -> identity_map + -> canonical models + -> canonical_change_events + -> search_documents / vector_documents + -> API / Search / Vector / AI retrieve + -> PermissionService filtering / masking / audit +``` + +### 12.2 验收条件 + +通过条件: + +- customers 和 trades 都被导入并 normalize。 +- scenario events 至少被导入 Raw / SCE / Outbox。 +- raw / sce / outbox / canonical / identity / search / vector / audit 关键表有计数。 +- API 查询和自然语言查询都走权限。 +- 权限 fail closed 有证据。 + +失败条件: + +- 只有 customer,没有 order。 +- 只有 API 查询,没有 Search / Vector。 +- 只有 Search / Vector,没有权限验证。 +- 只有接口返回,没有数据库计数。 + +## 13. 阶段 G:Admin Console 验收 + +### 13.1 目标 + +验证 Admin Console 不只是能打开,而是可以支撑运营 / 实施 / 管理员完成关键流程。 + +### 13.2 必须验证页面 + +至少验证: + +1. 登录页。 +2. 总览。 +3. 数据接入。 +4. 数据治理。 +5. 检索与索引。 +6. 权限与身份。 +7. 审计与系统。 + +### 13.3 必须验证交互 + +至少验证: + +- 登录成功。 +- 导航到每个一级工作区。 +- 每个一级工作区有可见标题。 +- 页面没有明显空白崩溃。 +- 关键按钮不会文本溢出。 +- 窄屏不会出现明显重叠。 + +高风险操作不要求在 Playwright 脚本里全部真实执行,但必须至少验证: + +- 页面存在二次确认入口或文案。 +- 后端高风险接口在真实 API 层会写审计。 + +### 13.4 截图要求 + +必须保存: + +```text +target/admin-console-verification/overview-1440.png +target/admin-console-verification/overview-1280.png +target/admin-console-verification/overview-1024.png +target/admin-console-verification/overview-768.png +``` + +如果脚本保存更多截图更好,但至少需要覆盖这四个宽度。 + +### 13.5 验收条件 + +通过条件: + +- Playwright 脚本退出码为 0。 +- 截图文件存在。 +- 报告记录截图路径。 +- 页面主要中文文案可见。 +- 页面没有明显运行时报错。 + +失败条件: + +- 浏览器没打开就标记通过。 +- 登录失败后仍继续截图未登录页面。 +- 只截图桌面宽度。 + +## 14. 阶段 H:文档更新 + +### 14.1 必须更新 + +如果新增 `scripts/commercial-acceptance-verify.mjs`,必须更新: + +1. `docs/02-implementation-status.md` +2. `docs/05-local-runbook.md` + +如果新增本文或新增验收报告文档,必须更新: + +1. `docs/00-llm-wiki-index.md` + +### 14.2 implementation-status 写法 + +`docs/02-implementation-status.md` 中必须明确: + +- 新增了商业级试运行验收脚本。 +- 脚本输出位置。 +- 脚本能验证哪些链路。 +- 该脚本不代表真实吉客云、MCP Server、OpenSearch、Qdrant、ClickHouse 已完成。 +- 如果脚本仍有未覆盖项,必须写清楚。 + +示例写法: + +```text +- 已新增 `scripts/commercial-acceptance-verify.mjs`,用于在本地已启动 PostgreSQL / Redpanda / Spring Boot 应用后执行商业级试运行验收,覆盖登录、mock ingestion、normalize、canonical outbox、Search / Vector、权限 fail closed、MDM、Replay / Backfill 和 Admin Console 截图证据;输出 `target/commercial-acceptance-report.json` 与 `target/commercial-acceptance-report.md`。 +``` + +边界示例: + +```text +- 该验收脚本只证明复杂 mock 数据下的本地试运行闭环,不代表真实吉客云 HTTP 接入、完整 MCP Server、OpenSearch / Qdrant / ClickHouse 生产链路已完成。 +``` + +## 15. 最终验收报告必须包含的判定语句 + +### 15.1 全部通过时 + +报告必须写: + +```text +本次验收通过。该结论仅代表 ai-data-foundation 在复杂 mock 数据、本地 PostgreSQL / Redpanda、本地 Search / Vector-like 召回条件下完成商业级试运行闭环验收。 +真实吉客云 HTTP 接入、完整 MCP Server、生产部署、灾备、监控、ClickHouse、OpenSearch、Qdrant 不在本次完成范围内。 +``` + +### 15.2 部分通过时 + +报告必须写: + +```text +本次验收部分通过,不能声明商业级试运行验收完成。以下步骤仍需修复或补跑。 +``` + +并列出: + +- failed 步骤。 +- partial 步骤。 +- blocked 步骤。 +- 对商业级试运行结论的影响。 + +### 15.3 失败时 + +报告必须写: + +```text +本次验收失败,不能声明商业级试运行验收完成。 +``` + +并列出: + +- 首个失败门槛。 +- 是否可以继续局部验证。 +- 下一步修复建议。 + +## 16. 建议任务拆分 + +如果让 AI 分多轮执行,建议按以下 issue / task 拆分。 + +### Task 1:稳定当前 Admin Console 改造 + +目标: + +- 确保当前 Admin Console 改造通过测试。 +- 确保文档与页面一致。 + +必须验收: + +```powershell +node --check src/main/resources/static/admin-console/app.js +mvn -Dtest=AdminConsolePageTest test +git diff --check +``` + +完成条件: + +- 三条命令全部通过。 +- `docs/02-implementation-status.md` 已记录当前 Admin Console 能力和边界。 + +### Task 2:新增商业验收脚本 + +目标: + +- 新增 `scripts/commercial-acceptance-verify.mjs`。 +- 可生成 JSON / Markdown 报告。 + +必须验收: + +```powershell +node scripts/commercial-acceptance-verify.mjs +``` + +完成条件: + +- 脚本在应用未启动时能生成 fail 报告,而不是崩溃。 +- 脚本在应用启动后能执行主要步骤。 +- 报告文件存在。 + +### Task 3:接入数据库关键表计数 + +目标: + +- 报告中加入关键表计数。 + +必须验收: + +- 报告里有 `raw_records`、`source_change_events`、`canonical_customers`、`canonical_orders`、`identity_map`、`search_documents`、`vector_documents` 等计数。 + +完成条件: + +- 查询失败时有明确 error。 +- 查询成功时计数为数字。 + +### Task 4:完善权限 fail closed 验收 + +目标: + +- 验证无 token、伪造 header、sales 越权三类场景。 + +必须验收: + +- 无 token 不能访问业务数据。 +- `X-User-Id` / `X-Role` 不能放大权限。 +- sales 不能看到越权对象或敏感字段。 + +完成条件: + +- 报告中写明 HTTP status、响应摘要、权限元信息。 + +### Task 5:跑完整商业验收并生成报告 + +目标: + +- 本地完整跑通并生成最终报告。 + +必须验收: + +```powershell +git diff --check +mvn test +docker compose up -d postgres redpanda +mvn spring-boot:run +node scripts/commercial-acceptance-verify.mjs +node scripts/admin-console-verify.mjs +``` + +完成条件: + +- `target/commercial-acceptance-report.json` 存在。 +- `target/commercial-acceptance-report.md` 存在。 +- `target/admin-console-verification` 有截图。 +- 报告结论为 pass,或明确 partial / fail 和阻塞原因。 + +## 17. AI 执行时的硬性停止条件 + +遇到以下情况必须停止并向用户说明,不能继续猜: + +1. 需要改变 Source Model / Canonical Model 语义。 +2. 需要改变 Change Envelope 字段。 +3. 需要改变 outbox / inbox 幂等策略。 +4. 需要改变权限默认拒绝、脱敏、租户隔离规则。 +5. 需要让 AI 自动清洗或自动写 canonical。 +6. 需要新增真实吉客云生产接入逻辑,但没有凭据和样本确认。 +7. 需要引入 OpenSearch / Qdrant / ClickHouse 等新基础设施并改变本轮边界。 +8. Docker / PostgreSQL / Redpanda 不可用,导致端到端验收无法继续。 + +## 18. 最终交付清单 + +AI 完成本文任务后,最终回复必须包含: + +- 修改了哪些文件。 +- 新增了哪些脚本和文档。 +- 执行了哪些命令。 +- 每个命令是否通过。 +- 验收报告路径。 +- Admin 截图路径。 +- 哪些验收项通过。 +- 哪些验收项失败或未覆盖。 +- 明确边界。 + +最终回复不能只写: + +```text +已完成。 +``` + +必须写成类似: + +```text +已完成商业级试运行验收脚本与报告生成。 +验证: +- git diff --check:通过 +- mvn test:通过 +- node scripts/commercial-acceptance-verify.mjs:通过,报告见 target/commercial-acceptance-report.md +- node scripts/admin-console-verify.mjs:通过,截图见 target/admin-console-verification + +边界: +- 本次只证明复杂 mock 数据本地试运行闭环。 +- 未证明真实吉客云 HTTP 接入。 +- 未证明完整 MCP Server。 +- 未证明 OpenSearch / Qdrant / ClickHouse 生产链路。 +``` + +## 19. 一句话总验收门槛 + +本轮真正完成的标准是: + +```text +一个新 Agent 克隆仓库后,按 README / runbook 启动基础设施和应用,运行商业验收脚本,可以得到机器可读报告、人工可读报告、Admin 截图和关键表计数;报告能清楚说明哪些闭环已通过,哪些边界仍未完成。 +``` + +如果做不到这一点,本轮就没有真正结束。 diff --git a/docs/07-architecture-global-gap-checklist.md b/docs/07-architecture-global-gap-checklist.md new file mode 100644 index 0000000..7c37684 --- /dev/null +++ b/docs/07-architecture-global-gap-checklist.md @@ -0,0 +1,370 @@ +# 架构图全局未完成清单 + +日期:2026-06-04 +依据图片:`C:/Users/Huanghaohang/Downloads/树可AI落地系统架构_数据流分层改稿.png` +范围:站在目标架构图的全局视角,盘点本仓库负责或需要提供边界支撑的能力缺口。 + +## 1. 本次判断边界 + +本次盘点不是逐个接口、按钮或字段列 TODO,而是按架构图中的层级判断“体系能力是否闭环”。本仓库当前主要负责: + +- 数据接入与治理平台 / 数据底座引擎:Connector、Ingestion、Raw、Source Change Event、Outbox、Normalize、Source Model、Canonical Model、MDM、Search / Vector 基础索引。 +- 服务出口层的底座侧能力:API、Serving API、权限校验、审计、Search / Vector candidate-only 召回。 +- 治理与安全横切能力的底座侧能力:身份基础表、OAuth 演示授权服务、权限策略、审计、Admin Console 治理入口。 + +本仓库不应直接承诺完成但必须提供安全数据边界的部分: + +- 完整 MCP Server、Comate / AI 助手、生成式对话、Agent 技能编排、场景应用库。 +- 生产级 OpenSearch、Qdrant、ClickHouse、Prometheus / Grafana / Loki、Kubernetes / Helm、企业 WPS SSO / 外部 OIDC。 +- 外部系统真实凭据接入后的生产数据语义验收。 + +当前阶段仍应按 Phase 0 / Phase 1 处理:复杂 mock 数据可以证明链路,但不能包装成真实吉客云、完整多源治理或生产级 AI 数据平台已完成。 + +## 2. 总体完成度判断 + +| 架构域 | 当前判断 | 全局结论 | +|---|---|---| +| 身份层 / 企业身份中心 | 部分完成 | 已有内置用户、角色、部门、API Key、OAuth 演示授权服务;未完成企业 WPS SSO/OIDC、MFA、生产密码策略和独立资源服务器体系。 | +| 数据源层 | 部分完成 | mock 数据源复杂度已较高;真实吉客云仍只是 Connector 骨架,其他 ERP/CRM/WMS/OA/电商/DB/文件/Webhook/CDC 未形成接入矩阵。 | +| 接入 / 采集层 | 部分完成 | mock customer/trade 走 Connector-driven pipeline;缺真实同步编排、数据源注册、push/file/CDC 统一入口和生产级限流调度。 | +| Raw / 暂存 / 事件层 | 部分完成 | Raw metadata、对象存储适配、SCE、source outbox 已具备;事件发布/消费默认手动或默认关闭,out-of-order/quarantine/topic 治理仍不完整。 | +| ETL 清洗 / 标准化 | 部分完成 | customer/trade normalizer、状态映射、schema governance 已有;未覆盖全实体、版本化 mapping 工作流和通用数据质量规则引擎。 | +| 统一模型 / 核心资产 | 部分完成 | customer/order canonical、identity_map、客户 MDM 已形成闭环;商品、库存、仓库、供应商、采购、售后、指标宽表和字段级血缘未闭环。 | +| 服务出口层 | 部分完成 | API、serving、Search / Vector 本地 candidate 召回可用;MCP Server、BI / 指标服务、订阅 / 导出审批和生产 Search / Vector 未完成。 | +| 应用消费层 | 未在本仓库闭环 | 本仓库只能提供安全数据服务;Comate、生成式对话、Agent 编排、知识检索服务和业务场景应用需要上层或 MCP 线程完成。 | +| 治理与安全横切能力 | 部分完成 | 权限、脱敏、审计、Admin Console 有试运行能力;完整 permission-service、租户隔离硬化、导出审批、ClickHouse 审计分析、监控告警、灾备未完成。 | +| AI 支撑能力 | 部分完成 | 本地 token overlap 语义召回可验证权限链路;真实 LLM / Embedding / Rerank、prompt 评测、监控和 AI mapping 审核流未完成。 | + +## 3. 全局未完成清单 + +### 3.1 真实外部数据接入体系未闭环 + +目标架构要求所有外部源统一进入接入与治理平台,且不直连 PostgreSQL。当前 mock 证明了接入形态,但真实外部接入还没有形成平台级能力。 + +当前已有基础: + +- `MockCustomerIngestionService`、`MockTradeIngestionService`、`ConnectorIngestionPipeline` 已把 mock customer / trade 接入改成 Connector-driven pipeline。 +- `JkyunConnector` 已有认证参数、签名、错误归类和能力声明骨架。 +- mock 数据规模已覆盖客户、SKU、订单、订单行、售后、多源客户和场景事件。 + +未完成的体系能力: + +- 吉客云真实 HTTP 调用未启用,`list`、`fetchById`、`count` 仍是 `not_implemented`。 +- 吉客云实体能力矩阵仍未用真实凭据验证,客户、SKU、订单、仓库、库存、供应商、采购、售后主键、更新时间、分页、删除检测和状态字段都不能声明生产可用。 +- 其他 ERP / CRM / WMS / OA / 电商平台 / 数据库 / 文件 / Webhook / CDC 尚无统一 connector adapter 和接入验收样本。 +- 缺 `data_sources`、connector method 级能力配置、凭据生命周期、租户级数据源启停、调用日志、限流策略和 source API 错误率统计的完整产品闭环。 +- 当前回补仍基于已有 raw / mock 数据,没有通过真实 Connector 拉取外部历史数据。 + +完成判定: + +- 至少吉客云 customer 或 order 真实实体通过全量、增量、重拉、count/reconcile、限流和错误恢复验收。 +- 每个启用实体都有 sample request/response、主键、更新时间、分页、删除检测和状态字段验证记录。 +- 所有外部数据写入都先落 Raw metadata / raw object,并同事务写 SCE / source outbox。 +- Admin Console 可按数据源查看健康、能力、凭据状态、调用成功率、错误码分布、限流和最近同步样本。 + +### 3.2 同步编排与任务治理仍偏手动 + +目标架构图中“调度入口”“调度”“重放/补数”应形成持续运行的同步平台。当前更接近可手动验证的链路。 + +当前已有基础: + +- `sync_checkpoints` 和 `CheckpointService` 已支持 overlap window / safety lag。 +- mock ingestion 成功写完 Raw / SCE / Outbox 后才推进 checkpoint。 +- source event publisher 有后台 scheduler,但默认关闭。 +- normalizer Redpanda consumer 可配置开启,但默认关闭。 +- replay/backfill 表和接口已具备试运行操作能力。 + +未完成的体系能力: + +- 缺完整 `sync_jobs` / `sync_tasks` 调度中心和实体级互斥,不能长期按租户、数据源、实体、分片稳定编排 full/incremental/reconcile/backfill。 +- 缺全局 worker、数据源、租户、实体、method 维度的统一限流与并发控制。 +- 当前 ingestion、publish、normalize、index 主要依赖 `/internal/*` 手动触发,不是目标态持续自动流。 +- Redpanda consumer retry topic、DLT、consumer lag、offset replay 和 transform error 治理不完整。 +- Push 路径的 webhook buffer、CDC/file import、dedup、ordering repair、quarantine 流程未落地。 +- sync_run 的拉取、发布、清洗、canonical 变更分段状态和计数尚未成为完整运营事实源。 + +完成判定: + +- 可在 Admin Console 创建和启停同步任务,系统自动调度 full/incremental/reconcile/backfill。 +- 同一 tenant + source + entity + partition 不会并发写入污染 checkpoint。 +- publisher、consumer、normalizer、indexer 可后台持续运行,失败可重试、可进死信、可观测。 +- 同步任务详情能展示 cursor_before/after、fetched、raw_written、sce_created、published、normalize_success、transform_failed、canonical_changed。 + +### 3.3 Raw / SCE / Outbox 已有主干,但事件治理目标态未完成 + +目标架构强调 Raw 先行、Pull/Push 统一为 Source Change Event、Outbox 是可靠边界。当前主干正确,但生产事件治理仍不完整。 + +当前已有基础: + +- `raw_records`、`source_change_events`、`source_event_outbox` 同事务写入。 +- 本地 raw object adapter 和 MinIO adapter 支持 temporary -> linked 协议。 +- raw orphan cleaner 支持 dry-run、修复 missing、清理 temporary orphan。 +- source outbox、canonical outbox 均有发布和 dead letter 基础。 + +未完成的体系能力: + +- Redpanda topic 保留策略、分区数、consumer group、replay 策略和运维验收未成为文档化运行配置。 +- source event publisher 后台默认关闭;canonical event publisher 只有手动触发,尚无后台 scheduler。 +- Push 与 Pull 冲突状态机未完整实现,quarantine、out-of-order buffer、conflict 人工处理流仍不完整。 +- `raw_payload_preview` 仍承担 Phase 1 清洗便利;大 payload 从 MinIO 回读用于 replay 的能力未完整闭环。 +- schema snapshot、lineage archive、导出文件、replay 文件进入对象存储的目标态未完成。 +- canonical event 下游消费者仍以本地接口消费为主,未形成 Redpanda 多消费者组生态。 + +完成判定: + +- Raw object 缺失、hash 不一致、temporary orphan、linked object 不存在均有可观测状态和恢复动作。 +- SCE event_status / record_apply_state 覆盖 received、deduplicated、buffered、applied、ignored、conflicted、quarantined、failed。 +- source-change-events 和 canonical-change-events 均可后台发布、消费、重放,并能查看 lag、失败原因和 dead letter。 +- Replay 能从对象存储完整原始 payload 回读,不依赖 PostgreSQL preview。 + +### 3.4 标准化与 Source Model 只覆盖客户 / 订单主链路 + +目标架构图中的 ETL 清洗、标准化映射、质量校验、重放/补数应覆盖所有核心实体。当前 customer/trade 是样板,尚不是全域标准化平台。 + +当前已有基础: + +- customer 和 trade normalizer 已写 source model、identity_map、canonical model 和 canonical outbox。 +- status mapping 配置化,未知状态会进 `data_quality_issues`。 +- schema governance 可检测 breaking change 并暂停 entity normalize。 +- replay execute 走现有 normalizer,不直接改 source / canonical。 + +未完成的体系能力: + +- `SourceChangeEventDispatcher` 只支持 `customer` 和 `trade`,其他 source entity 返回 unsupported。 +- SKU、仓库、库存、供应商、采购、售后退款、OA/CRM 等实体尚未有 Source Model、Canonical Model、normalizer 和状态映射闭环。 +- 通用 mapping registry、字段映射版本、normalizer 版本、字段级 lineage 和 replay 影响分析还没有形成平台级工作流。 +- `transform_errors` 目标态不完整,清洗失败与 schema issue、mapping version、normalizer version 的关联还需加强。 +- 数据质量规则仍偏枚举/状态与样例问题,没有形成可配置 rule engine、严重级别、负责人、SLA 和批量处理。 +- 多实体 source_missing / stale / dirty / conflict 状态传播尚未统一。 + +完成判定: + +- 架构图中核心业务资产至少覆盖 customer、order、order_line、sku、warehouse、inventory、supplier、purchase、after_sale。 +- 每个实体都有 Source Model、Canonical Model、identity map、状态映射、质量规则、normalizer 测试和 replay 验收。 +- mapping_version / normalizer_version 可查询、可发布、可 dry-run、可 execute replay。 +- 清洗失败不丢数据,能进入 transform_errors / data_quality_issues / dead letter,并能在 Admin Console 闭环处理。 + +### 3.5 统一模型核心资产仍缺商品、库存、售后和指标域 + +目标架构图写明“商品 / 订单 / 客户统一模型”“统一事实库 PostgreSQL”“检索/分析索引”“知识/向量索引”。当前统一模型集中在客户和订单,无法支撑全域业务应用。 + +当前已有基础: + +- `canonical_customers`、`canonical_orders`、`canonical_order_lines` 已具备查询、权限和 outbox。 +- 客户 MDM 支持候选、合并、拆分、回滚、冲突、字段锁。 +- Search / Vector 本地索引支持 customer、order、order_line、issue candidate。 + +未完成的体系能力: + +- 商品/SKU、库存、仓库、供应商、采购、售后退款没有 canonical 闭环,也没有统一业务语义和 identity map 规则。 +- 售后 / 投诉 / 物流问题当前主要作为 data quality / scenario issue,不是独立业务问题模型。 +- 订单与售后、商品、库存、仓库、客户之间的跨实体血缘和影响范围没有完整图谱。 +- record_lineage、field_lineage、字段来源优先级、来源可信度、字段级快照归档未完整实现。 +- 统一指标服务和分析宽表尚未建立,不能支撑 BI / 指标服务的稳定口径。 +- MDM 目前重点是 customer,商品、供应商等主数据合并和字段冲突策略未建。 + +完成判定: + +- 核心实体形成统一 ER 关系,并通过 canonical event 驱动 Search / Vector / BI 下游。 +- 所有 canonical 记录可追溯来源系统、source_record_id、raw_record_id、source_change_event_id、mapping/normalizer 版本。 +- MDM 不只处理 customer,还定义商品、供应商等主数据治理边界。 +- BI/指标服务有明确事实表、维度表、指标定义、权限过滤和刷新策略。 + +### 3.6 服务出口层缺 MCP Server、BI / 指标服务和订阅能力 + +目标架构图中的服务出口层包括 MCP/API 调用层、BI/指标服务、数据服务/订阅、权限网关。当前 API 和 serving 有基础,但服务出口还不是完整平台。 + +当前已有基础: + +- `/api/customers`、`/api/orders`、`/api/search`、`/api/vector/search`、`/api/ai/retrieve` 已经过权限过滤。 +- `/serving/customers/*`、`/serving/orders/*`、`/serving/customer-knowledge/candidates` 为 MCP 网关提供服务间读契约。 +- 权限决策和 access audit 已接入客户、订单、Search / Vector 和 serving。 + +未完成的体系能力: + +- 本仓库没有 HTTP MCP endpoint、tool registry、统一 tool executor、tool wrapper、MCP access audit 和 Protected Resource Metadata。 +- BI / 指标服务尚无独立服务层,没有统一指标定义、查询 API、缓存、物化视图或分析权限模型。 +- 数据服务 / 订阅能力尚未形成,包括订阅注册、增量投递、导出审批、导出文件落对象存储和导出审计。 +- 权限网关仍是 modular monolith 内部服务,不是独立 permission-service / policy decision point。 +- Search / Vector 是本地 PostgreSQL candidate 召回,不是生产 OpenSearch / Qdrant。 +- serving API 仅覆盖 customer/order/knowledge 最小切片,未覆盖商品、售后、指标、导出、聚合分析。 + +完成判定: + +- MCP Server 通过统一 executor 执行 tool,所有 tool 都显式注册 scope、risk、max_limit、默认脱敏字段。 +- BI / 指标服务所有查询都经过权限服务,不允许 AI 或 BI 直连事实表。 +- 数据订阅和导出必须走审批、脱敏、对象存储、审计、过期和撤销。 +- Search / Vector 只返回 candidate ids,最终回表和字段脱敏由权限网关强制执行。 + +### 3.7 应用消费层尚未形成端到端产品闭环 + +图中的应用消费层包括 Comate / AI 助手、生成式对话、Agent / 技能编排、场景应用库、知识与检索服务、多维表数据应用、工作台 / 管理后台。本仓库只完成了工作台 / 管理后台的一部分和底座接口。 + +当前已有基础: + +- Admin Console 已是 React + Ant Design + ECharts 的试运行治理后台。 +- Search / Vector / AI retrieve 接口可用于证明“候选召回 -> canonical 回表 -> 权限过滤”。 +- OAuth 演示授权服务可支撑 MCP/Agent 登录实验。 + +未完成的体系能力: + +- Comate / AI 助手未接入本仓库能力。 +- 生成式对话没有完成 RAG answer、引用来源、数据新鲜度、权限过滤说明和拒答策略。 +- Agent 技能编排、场景应用库和多维表数据应用未形成产品入口和 tool contract。 +- 知识与检索服务目前是底座 candidate 召回,不是完整知识服务。 +- Admin Console 虽能治理试运行链路,但还不是外部应用消费层的统一工作台。 + +完成判定: + +- 至少一个上层 AI 应用通过 MCP 或 serving contract 完成真实登录、查询、权限过滤、审计、引用和脱敏响应。 +- 生成式回答必须展示 source_systems、data_freshness、permission_decision_id、masked_fields、total_filtered。 +- Agent / 技能编排不能绕过 tool registry 和权限网关。 + +### 3.8 治理与安全横切能力还停留在试运行级 + +图右侧治理与安全能力包括权限控制、字段脱敏、审计留痕、租户隔离、数据血缘、导出审批,并贯穿接入、存储、服务、应用出口。当前有基础,但离企业级平台还有距离。 + +当前已有基础: + +- AuthInterceptor 已不再接受 `/api/*` 伪造 `X-User-Id` / `X-Role`。 +- 用户、部门、角色、服务账号、API Key、OAuth client、auth session、admin audit 表已建立。 +- `PermissionService` 支持 policy version、deny 优先、对象级过滤、字段脱敏和决策审计。 +- Admin Console 提供权限策略、模拟、身份、审计、安全事件页面。 + +未完成的体系能力: + +- 企业 WPS SSO / 外部 OIDC 未接入,内置账号仍是演示身份中心。 +- 密码是 SHA-256 演示 hash,未实现生产级 password hashing、MFA、账号锁定、密码策略。 +- Redis grants cache / permission grants 预计算任务未完整落地,当前是应用内策略决策缓存。 +- 权限服务未独立部署,无法作为统一权限网关服务所有出口。 +- 字段级策略仍以代码内对象类型规则为主,缺可配置字段级策略、导出/分析/更新/admin 操作级策略全覆盖。 +- 租户隔离仍是单租户 demo 配置,缺多租户 provision、跨租户测试矩阵、密钥隔离和对象存储路径隔离验收。 +- 审计主要在 PostgreSQL,缺 ClickHouse / Loki 级日志分析和长期留存查询。 +- 数据血缘还未覆盖 record/field/source/raw/object/index/serving 全链路。 +- 导出审批、数据下载、临时授权、授权过期和撤销未完成。 + +完成判定: + +- 所有出口统一从已认证 identity 获取 tenant/user/service_account,不接受参数覆盖。 +- 权限策略支持字段、对象、操作、数据范围、导出、分析和高风险工具,并能模拟、发布、回滚。 +- 权限异常默认 fail closed,且拒绝、脱敏、过滤、导出审批都有审计和指标。 +- 多租户、字段脱敏、导出、MCP tool 调用都有自动化绕过测试。 + +### 3.9 AI 支撑能力尚未达到模型服务层 + +图底部 AI 支撑能力包括模型服务、LLM / Embedding / Rerank、Prompt / 评测 / 监控。当前只是本地检索链路证明,不是模型服务平台。 + +当前已有基础: + +- `/api/vector/search` 和 `/api/ai/retrieve` 通过本地 token overlap 实现语义样例召回。 +- embedding 文本在写入本地 vector_documents 前会脱敏手机号和邮箱。 +- Search / Vector 查询会回 canonical 并经权限过滤。 + +未完成的体系能力: + +- 未接真实 embedding 模型、Qdrant 或 rerank 服务。 +- 未接 LLM answer generation,缺引用、拒答、敏感字段处理和幻觉防护。 +- 未有 prompt registry、prompt version、评测集、离线评估、在线监控和回归报告。 +- AI mapping proposal 审核工作流未实现,当前仍不能让 AI 直接执行 mapping 或写 canonical。 +- 没有模型服务的租户隔离、密钥管理、调用审计、成本统计、失败重试和降级策略。 + +完成判定: + +- Embedding / Rerank / LLM 调用统一走模型服务配置,密钥不进日志和前端。 +- 每个 AI 场景都有 prompt 版本、评测样本、权限绕过测试、敏感字段测试和回归阈值。 +- AI 输出只能作为建议或回答,不能直接绕过 deterministic normalizer、MDM 审批和 canonical 写入规则。 + +### 3.10 可观测、部署、备份和生产运维未闭环 + +目标架构图虽然强调数据流分层,但真正落地还需要运维闭环。当前仓库是本地/试运行可跑,不是生产部署包。 + +当前已有基础: + +- Docker Compose 提供 PostgreSQL、Redpanda 等本地基础组件。 +- `/actuator/health`、Admin 总览、内部 status 接口提供基础可见性。 +- 文档已有本地运行手册和商业级验收执行计划。 + +未完成的体系能力: + +- 没有 Kubernetes / Helm / CI-CD / 多环境配置治理。 +- 没有 Prometheus / Grafana / Loki 指标日志告警闭环。 +- 没有 ClickHouse writer 承接 sync logs、MCP audit、permission denied、data quality 分析。 +- 没有备份恢复、灾备、RPO/RTO 演练和对象存储生命周期策略。 +- 没有容量规划、压测、队列 lag 告警、索引重建耗时和权限服务延迟 SLO。 +- 没有生产 secret 管理、密钥轮换、RSA key rotation、API Key rotation 完整流程。 + +完成判定: + +- 任一生产环境部署都能声明组件版本、配置来源、secret 来源、数据保留、备份和恢复路径。 +- 核心指标至少覆盖 sync lag、outbox lag、consumer lag、normalizer failure、dead letter、schema drift、permission denied、mcp latency、search/vector index lag。 +- 关键故障都有 runbook:外部 API 限流、Redpanda 不可用、MinIO 对象缺失、schema breaking、permission cache 异常、indexer 失败。 + +## 4. 优先级路线建议 + +### P0:先补“真实接入可信闭环” + +目标是让架构图左半边从 mock 证明升级到真实来源可信闭环。 + +- 完成吉客云真实 HTTP Connector customer 或 order 一实体。 +- 建立数据源注册、凭据、能力验证、调用日志和限流。 +- 建立 sync_jobs / sync_tasks 调度中心,减少手动 internal endpoint 依赖。 +- 明确 Redpanda topic、consumer group、retention、DLT、lag 监控。 +- 让 Raw object 回读支持 replay,减少对 preview 的依赖。 + +### P1:补齐核心业务资产域 + +目标是让“商品 / 订单 / 客户统一模型”名实相符。 + +- 扩展 SKU、仓库、库存、售后 refund/return 的 Source/Canonical/Normalizer/Identity Map。 +- 补跨实体 lineage:订单、订单行、商品、仓库、售后、客户。 +- 把售后/问题从 data quality issue 扩展成业务问题或服务事件模型。 +- 将 MDM 从客户扩展到商品/供应商候选边界。 + +### P2:完成服务出口与 MCP/BI 边界 + +目标是让上层应用只能从授权出口取数。 + +- 本仓库若继续承担 MCP,需要实现 `/mcp`、tool registry、executor、MCP audit、Protected Resource Metadata。 +- 如果 MCP 在线程外完成,本仓库需要冻结并版本化 serving contract。 +- 建立 BI/指标服务,不允许 BI 或 AI 直连 canonical 表。 +- 数据订阅和导出必须走审批、脱敏、对象存储、审计。 + +### P3:把试运行治理升级到企业级治理 + +目标是横切治理能力覆盖所有出口和所有实体。 + +- 接 WPS SSO / 外部 OIDC,替换演示密码体系。 +- 独立 permission-service 或至少明确 PDP/PIP/PEP 边界。 +- 实现 Redis grants cache、grants 预计算、字段级策略配置化。 +- 建立多租户隔离验收、导出审批、字段脱敏策略、审计长期分析。 + +### P4:生产级 AI 支撑和运维 + +目标是从本地 token vector 演示升级到可运营的 AI 服务层。 + +- 接入真实 embedding / rerank / LLM provider。 +- 接 OpenSearch / Qdrant,并保留 candidate-only + 回表权限过滤。 +- 建立 prompt registry、评测集、监控、成本和安全回归。 +- 建立 Prometheus / Grafana / Loki / ClickHouse 分析链路、备份恢复、密钥轮换和 SLO。 + +## 5. 不应被误判为已完成的事项 + +- 复杂 mock 数据不等于真实吉客云生产数据接入完成。 +- customer/order serving API 不等于完整 MCP Server 完成。 +- `/api/vector/search` 和 `/api/ai/retrieve` 不等于真实 Qdrant、embedding、RAG answer 完成。 +- Admin Console 试运行后台不等于生产部署、企业 SSO、完整 permission-service 和监控告警完成。 +- 客户 MDM 闭环不等于商品、供应商、全多源 MDM 完成。 +- source/canonical outbox 表存在不等于所有下游事件消费者和生产 Redpanda 运维闭环完成。 +- 本地 OAuth 演示授权服务不等于企业身份中心、WPS SSO、MFA 和生产密码安全完成。 + +## 6. 后续验收方式 + +后续每补齐一个体系能力,不应只看接口是否返回成功,而应至少验证: + +- 文档:对应架构章节、实现状态和运行手册是否更新。 +- 数据库:关键表计数、状态分布、幂等约束、审计记录是否正确。 +- 事件:outbox pending/published/failed/dead_letter、consumer inbox、lag 是否可见。 +- API:无权限、跨租户、伪造身份、字段脱敏、对象过滤是否 fail closed。 +- 浏览器:Admin Console 是否能发现、操作、回滚、审计该能力。 +- 测试:`mvn test`、`git diff --check`、必要的 Testcontainers 集成测试和 Playwright 截图。 + diff --git a/scripts/admin-console-verify.mjs b/scripts/admin-console-verify.mjs new file mode 100644 index 0000000..b17ed01 --- /dev/null +++ b/scripts/admin-console-verify.mjs @@ -0,0 +1,96 @@ +import { chromium } from "playwright"; +import fs from "node:fs/promises"; +import path from "node:path"; + +const baseUrl = process.env.ADMIN_CONSOLE_BASE_URL || "http://localhost:8080"; +const outDir = process.env.ADMIN_CONSOLE_VERIFY_OUT || "target/admin-console-verification"; +const widths = [1440, 1280, 1024, 768]; + +async function screenshot(page, name) { + await fs.mkdir(outDir, { recursive: true }); + await page.screenshot({ path: path.join(outDir, `${name}.png`), fullPage: true }); +} + +async function clickFirst(page, text) { + const locator = page.getByText(text, { exact: false }).first(); + if (await locator.count()) { + await locator.click(); + await page.waitForTimeout(500); + return true; + } + return false; +} + +async function run() { + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage({ viewport: { width: 1440, height: 980 } }); + const consoleErrors = []; + page.on("console", msg => { + if (msg.type() === "error") consoleErrors.push(msg.text()); + }); + page.on("pageerror", error => consoleErrors.push(error.message)); + + await page.goto(`${baseUrl}/admin`, { waitUntil: "networkidle" }); + await page.getByLabel("用户名").fill(process.env.ADMIN_CONSOLE_USER || "admin"); + await page.getByLabel("密码").fill(process.env.ADMIN_CONSOLE_PASSWORD || "admin123"); + await page.getByRole("button", { name: "登录" }).click(); + await page.getByText("今日工作台").waitFor({ timeout: 15000 }); + await screenshot(page, "01-overview-1440"); + + await clickFirst(page, "数据接入"); + await page.getByText("接入链路总览").waitFor({ timeout: 10000 }); + await clickFirst(page, "失败事件"); + await screenshot(page, "02-ingestion-failed-events"); + + await clickFirst(page, "数据治理"); + await page.getByText("治理对象分布").waitFor({ timeout: 10000 }); + await clickFirst(page, "死信处理"); + await clickFirst(page, "重试"); + await clickFirst(page, "取消"); + await clickFirst(page, "数据重放"); + await page.getByText("数据重放与历史回补").waitFor({ timeout: 10000 }); + await screenshot(page, "03-governance-replay"); + + await clickFirst(page, "状态映射"); + await page.getByText("新增 / 更新映射").waitFor({ timeout: 10000 }); + await screenshot(page, "04-status-mapping"); + + await clickFirst(page, "主数据治理"); + await page.getByText("MDM 处理漏斗").waitFor({ timeout: 10000 }); + await screenshot(page, "05-mdm"); + + await clickFirst(page, "检索与索引"); + await page.getByText("检索链路拓扑").waitFor({ timeout: 10000 }); + await screenshot(page, "06-retrieval"); + + await clickFirst(page, "权限与身份"); + await page.getByText("身份与权限分布").waitFor({ timeout: 10000 }); + await clickFirst(page, "模拟"); + await screenshot(page, "07-permission-simulate"); + + await clickFirst(page, "MCP 连接"); + await page.getByText("在线/近期活跃主体").waitFor({ timeout: 10000 }); + await screenshot(page, "08-mcp-connections"); + + await clickFirst(page, "审计与系统"); + await page.getByText("审计与系统").waitFor({ timeout: 10000 }); + await screenshot(page, "09-audit-system"); + + for (const width of widths) { + await page.setViewportSize({ width, height: 900 }); + await page.waitForTimeout(600); + await screenshot(page, `responsive-${width}`); + const overflow = await page.evaluate(() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 2); + if (overflow) throw new Error(`Viewport ${width} has unexpected horizontal overflow`); + } + + await browser.close(); + if (consoleErrors.length) { + throw new Error(`Browser console errors:\n${consoleErrors.join("\n")}`); + } +} + +run().catch(error => { + console.error(error); + process.exit(1); +}); diff --git a/src/main/resources/static/admin-console/app.js b/src/main/resources/static/admin-console/app.js index ce0a37f..37fdb28 100644 --- a/src/main/resources/static/admin-console/app.js +++ b/src/main/resources/static/admin-console/app.js @@ -35,47 +35,56 @@ const { Header, Sider, Content } = Layout; const { Title, Text, Paragraph } = Typography; const GENERIC_ERROR_MESSAGE = "系统异常请稍后重试"; + const chartColors = { + ok: "#16a34a", + warning: "#d97706", + failed: "#dc2626", + running: "#2563eb", + muted: "#94a3b8", + ink: "#172033" + }; const termTips = { - Raw: "原始事实层。外部数据必须先落 Raw,再进入清洗和回放。", - SCE: "Source Change Event,统一来源变更事件,Pull / Push 都进入同一事件流。", - Outbox: "可靠事件边界。先事务写库,再由 publisher 发布,失败可重试。", - Canonical: "统一业务语义模型。上层查询必须回表 Canonical 并过权限。", - MDM: "主数据治理。处理重复客户、合并拆分、字段冲突和字段锁。", - Replay: "按指定 schema / mapping / normalizer 版本重放已落 Raw / SCE。", - Backfill: "历史回补。本轮只基于 mock / 已落 Raw,不接真实吉客云历史接口。", - Search: "关键词候选召回层,只返回 candidate IDs,不是事实源。", - Vector: "语义候选召回层,只返回 candidate IDs,最终必须回表和脱敏。", - "Dead Letter": "失败超过重试或不可处理的事件,需要人工重试、隔离或忽略。", - Lineage: "血缘信息,说明记录来源、清洗版本和治理操作。", - "field lock": "人工确认字段锁,后续同步或 replay 不能自动覆盖。" + 原始层: "Raw 原始事实层。外部数据必须先落原始层,再进入清洗和回放。", + 来源变更事件: "SCE,统一来源变更事件。拉取和推送都进入同一事件流。", + 待发布事件: "Outbox 可靠事件边界。先事务写库,再由发布器发布,失败可重试。", + 统一模型: "Canonical 统一业务语义模型。上层查询必须回表统一模型并经过权限判断。", + 数据连接器: "Connector,负责认证、分页、限流和源端错误归类。", + MDM: "主数据治理。处理重复客户、合并拆分、字段冲突和字段锁定。", + 数据重放: "Replay,按指定 schema / mapping / normalizer 版本重放已落原始层 / 来源变更事件。", + 历史回补: "Backfill。本轮只基于 mock / 已落原始层,不接真实吉客云历史接口。", + 关键词检索: "Search,关键词候选召回层,只返回候选 ID,不是事实源。", + 语义召回: "Vector,语义候选召回层,只返回候选 ID,最终必须回表和脱敏。", + 死信记录: "失败超过重试或不可处理的事件,需要人工重试、重放、隔离或忽略。", + 数据血缘: "说明记录来源、清洗版本和治理操作。", + 字段锁定: "人工确认字段锁,后续同步或数据重放不能自动覆盖。" }; const labels = { sync_runs: "同步运行", - raw_records: "Raw 记录", - source_change_events: "SCE 事件", - source_event_outbox_pending: "Source Outbox 待发", - source_event_outbox_published: "Source Outbox 已发", - source_event_dead_letters: "Source 死信", - canonical_change_events_pending: "Canonical Outbox 待发", - canonical_change_events_published: "Canonical Outbox 已发", - canonical_event_dead_letters: "Canonical 死信", + raw_records: "原始层记录", + source_change_events: "来源变更事件", + source_event_outbox_pending: "来源待发布事件", + source_event_outbox_published: "来源已发布事件", + source_event_dead_letters: "来源死信记录", + canonical_change_events_pending: "统一模型待发布事件", + canonical_change_events_published: "统一模型已发布事件", + canonical_event_dead_letters: "统一模型死信记录", transform_errors: "清洗失败", - replay_dead_letters: "Replay 死信", - schema_drift_issues: "Schema Drift", + replay_dead_letters: "数据重放死信", + schema_drift_issues: "结构变化", reconcile_results: "对账结果", policy_rules: "权限策略", permission_decisions: "权限决策", - checkpoints: "Checkpoint", + checkpoints: "同步检查点", status_mappings: "状态映射", data_quality_issues_open: "质量问题", - search_documents: "Search 文档", - vector_documents: "Vector 文档", + search_documents: "关键词检索文档", + vector_documents: "语义召回文档", mdm_merge_proposals_pending: "MDM 待合并", - replay_jobs_pending: "Replay 运行", - backfill_jobs_pending: "Backfill 运行", - connector_health: "Connector" + replay_jobs_pending: "数据重放运行", + backfill_jobs_pending: "历史回补运行", + connector_health: "数据连接器" }; const fieldLabels = { @@ -87,17 +96,17 @@ source_field: "来源字段", source_value: "来源值", source_record_id: "来源记录", - source_change_event_id: "SCE ID", - raw_record_id: "Raw ID", + source_change_event_id: "来源变更事件 ID", + raw_record_id: "原始层记录 ID", source_event_time: "来源时间", - canonical_entity: "Canonical 实体", - canonical_record_id: "Canonical 记录", + canonical_entity: "统一模型实体", + canonical_record_id: "统一模型记录", canonical_business_status: "业务状态", sync_health_status: "同步健康", governance_status: "治理状态", mapping_version: "映射版本", normalizer_version: "清洗版本", - schema_version: "Schema 版本", + schema_version: "结构版本", status: "状态", enabled: "启用", created_at: "创建时间", @@ -109,7 +118,7 @@ type: "类型", error_code: "错误码", error_message: "失败原因", - drift_type: "Drift 类型", + drift_type: "变化类型", compatibility: "兼容性", impact_assessment: "影响", reconcile_type: "对账类型", @@ -181,10 +190,10 @@ resource_type: "资源", ip_address: "IP", user_code: "用户编码", - client_id: "MCP client_id", - oauth_client_ids: "OAuth clients", - active_refresh_token_count: "Active refresh token", - token_version: "Token Version", + client_id: "客户端标识", + oauth_client_ids: "OAuth 客户端", + active_refresh_token_count: "可续期登录态", + token_version: "令牌版本", latest_issued_at: "最近签发", latest_expires_at: "最近过期", latest_revoked_at: "最近吊销", @@ -196,23 +205,18 @@ token_id: "Token ID 前缀", key_prefixes: "API Key 前缀", auth_method: "认证方式", - service_account_code: "Service Account", - active_api_key_count: "Active API Key", + service_account_code: "服务账号", + active_api_key_count: "可用接口密钥", recent_errors: "近期错误" }; const menu = [ ["overview", "总览"], - ["sync", "数据同步"], - ["quality", "数据质量"], - ["permission", "权限与组织"], - ["search", "Search / Vector"], - ["mcp", "MCP 连接"], - ["mdm", "MDM"], - ["replay", "Replay / Backfill"], - ["status", "状态映射"], - ["audit", "审计日志"], - ["settings", "系统设置"] + ["ingestion", "数据接入"], + ["governance", "数据治理"], + ["retrieval", "检索与索引"], + ["access", "权限与身份"], + ["system", "审计与系统"] ]; const trialBoundaries = [ @@ -227,49 +231,111 @@ ]; const workflows = [ + { + key: "play", + label: "怎么玩", + title: "先按这四条线体验", + why: "项目负责人第一次打开后台时,不需要先理解所有表和模块,先跑通四个产品证明。", + steps: ["证明数据从原始层进入统一模型", "证明失败可以治理和重放", "证明检索必须回表过权限", "证明所有高风险动作会留审计"] + }, { key: "health", label: "今日健康", title: "判断今天是否健康", + why: "日常值班先判断系统有没有明显风险,再决定进入哪个工作区处理。", steps: ["看总览健康分", "看同步链路红黄节点", "处理优先级最高事项", "复核审计日志"] }, { key: "sync", label: "同步失败", title: "处理同步失败", - steps: ["进入数据同步", "打开 Dead Letter 详情", "确认原因和影响范围", "重试 / Replay / 隔离"] + why: "同步失败不是看一个错误码结束,而是定位来源、影响范围和恢复动作。", + steps: ["进入数据接入", "打开死信记录详情", "确认原因和影响范围", "重试 / 数据重放 / 隔离"] }, { key: "permission", label: "权限验证", title: "验证权限过滤", - steps: ["进入权限与组织", "选择主体和对象", "运行权限模拟", "查看拒绝、规则、脱敏字段"] + why: "AI、MCP、API 都不能绕过权限,模拟结果要能和真实查询口径对上。", + steps: ["进入权限与身份", "选择主体和对象", "运行权限模拟", "查看拒绝、规则、脱敏字段"] }, { key: "mdm", label: "MDM", title: "处理合并 / 拆分", + why: "主数据治理要有证据、快照、回滚路径,不能把疑似同人直接自动合并。", steps: ["生成候选或冲突", "查看证据和快照", "选择 survivor 或拆分来源", "确认执行并看审计"] }, { key: "replay", - label: "Replay", - title: "发起 Replay / Backfill", - steps: ["选择实体和范围", "显式填写版本", "先 dry-run", "确认执行并看失败原因"] + label: "重放回补", + title: "发起数据重放 / 历史回补", + why: "规则变化后先做影响预演,再执行重算,避免旧事件覆盖新事实。", + steps: ["选择实体和范围", "显式填写版本", "先做影响预演", "确认执行并看失败原因"] }, { key: "status", label: "状态映射", title: "修改状态映射", - steps: ["查看来源字段和值", "填写 canonical / sync / governance 状态", "保存版本", "查看自动 Replay 影响分析"] + why: "源端枚举变化不能靠代码硬猜,必须配置化、版本化,并触发影响分析。", + steps: ["查看来源字段和值", "填写业务 / 同步 / 治理状态", "保存版本", "查看自动数据重放影响分析"] + } + ]; + + const playbooks = [ + { + key: "closed-loop", + title: "证明数据闭环", + tag: "从来源到统一模型", + proof: "看一条数据是否先落原始层,再进入来源变更事件、待发布事件和统一模型。", + next: "进入数据接入,看同步链路、检查点、结构变化和失败事件。", + page: "ingestion" + }, + { + key: "governance", + title: "证明可治理", + tag: "失败可恢复", + proof: "看死信、质量问题、状态映射、数据重放、历史回补和 MDM 是否能闭环处理。", + next: "进入数据治理,先处理待办最高的一类风险。", + page: "governance" + }, + { + key: "retrieval", + title: "证明 AI 检索安全", + tag: "候选不是授权", + proof: "自然语言只召回候选 ID,最终必须回表统一模型并经过权限过滤和脱敏。", + next: "进入检索与索引,跑一次关键词检索或语义召回。", + page: "retrieval" + }, + { + key: "permission", + title: "证明权限边界", + tag: "默认拒绝", + proof: "用不同主体模拟同一个对象访问,确认拒绝、脱敏、MCP 会话和审计记录都能对上。", + next: "进入权限与身份,做一次权限模拟并查看 MCP 连接。", + page: "access" } ]; function Term({ name }) { + const text = { + Raw: "原始层", + SCE: "来源变更事件", + Outbox: "待发布事件", + Canonical: "统一模型", + Connector: "数据连接器", + Search: "关键词检索", + Vector: "语义召回", + Replay: "数据重放", + Backfill: "历史回补", + "Dead Letter": "死信记录", + Lineage: "数据血缘", + "field lock": "字段锁定" + }[name] || name; return h("span", { className: "term" }, - h("span", null, name), - h(Tooltip, { title: termTips[name] || "项目专有名词,详见架构文档。" }, - h("span", { className: "term-help", "aria-label": `${name} 解释` }, "?") + h("span", null, text), + h(Tooltip, { title: termTips[text] || termTips[name] || "项目专有名词,详见架构文档。" }, + h("span", { className: "term-help", "aria-label": `${text} 解释` }, "?") ) ); } @@ -290,6 +356,44 @@ return String(value); } + function displayValue(value) { + const map = { + dry_run: "影响预演", + execute: "执行重算", + retry: "重试", + replay: "数据重放", + quarantine: "隔离", + ignore: "忽略", + pending: "待处理", + running: "处理中", + completed: "已完成", + failed: "失败", + success: "成功", + active: "启用", + disabled: "禁用", + enabled: "启用", + allow: "允许", + deny: "拒绝", + draft: "草稿", + published: "已发布", + rollback: "回滚", + customer: "客户", + trade: "订单", + order: "订单", + order_line: "订单明细", + issue: "问题", + source: "来源", + canonical: "统一模型", + source_event: "来源事件", + canonical_event: "统一模型事件", + replay_task: "数据重放任务", + service_account: "服务账号" + }; + if (Array.isArray(value)) return value.map(displayValue).join(", "); + if (typeof value === "string") return map[value] || value; + return safeValue(value); + } + function fmtTime(value) { if (!value) return "-"; try { @@ -300,8 +404,8 @@ } function statusTag(value) { - const text = safeValue(value); - const lower = text.toLowerCase(); + const text = displayValue(value); + const lower = safeValue(value).toLowerCase(); let color = "default"; if (["ok", "active", "fresh", "synced", "published", "allow", "success", "completed", "normal", "succeeded", "true"].some(k => lower.includes(k))) color = "success"; if (["pending", "running", "draft", "open", "retry"].some(k => lower.includes(k))) color = "processing"; @@ -338,11 +442,84 @@ if (/created_at|updated_at|published_at|resolved_at|last_verified_at|high_watermark|finished_at|executed_at|locked_at|released_at/i.test(key)) return fmtTime(value); if (/id$/i.test(key) || /_id/i.test(key)) return h(Tooltip, { title: safeValue(value) }, h("span", null, shortId(value))); if (typeof value === "object") return h(Tooltip, { title: safeValue(value) }, h("span", null, "查看详情")); - return h(Tooltip, { title: safeValue(value) }, h("span", null, safeValue(value))); + return h(Tooltip, { title: safeValue(value) }, h("span", null, displayValue(value))); } })); } + function EChart({ option, height = 300 }) { + const ref = React.useRef(null); + React.useEffect(() => { + if (!ref.current || !window.echarts) return undefined; + const chart = window.echarts.init(ref.current, null, { renderer: "canvas" }); + chart.setOption(Object.assign({ + color: [chartColors.running, chartColors.ok, chartColors.warning, chartColors.failed, "#64748b"], + textStyle: { fontFamily: "-apple-system, BlinkMacSystemFont, Segoe UI, PingFang SC, Microsoft YaHei, sans-serif" }, + tooltip: { trigger: "item", confine: true, borderWidth: 0, backgroundColor: "rgba(15, 23, 42, 0.92)", textStyle: { color: "#fff" } }, + grid: { left: 36, right: 18, top: 34, bottom: 32, containLabel: true } + }, option || {})); + const resize = () => chart.resize(); + window.addEventListener("resize", resize); + return () => { + window.removeEventListener("resize", resize); + chart.dispose(); + }; + }, [JSON.stringify(option)]); + return h("div", { className: "chart-panel" }, + h("div", { ref, className: "chart-box", style: { "--chart-height": `${height}px` } }) + ); + } + + function DetailDrawer({ title = "记录详情", detail, onClose }) { + return h(Drawer, { + title, + width: 760, + open: !!detail, + onClose + }, detail ? h(Tabs, { items: [ + { + key: "overview", + label: "概览", + children: h(Descriptions, { bordered: true, column: 1, size: "small" }, + Object.entries(detail).slice(0, 8).map(([key, value]) => + h(Descriptions.Item, { key, label: fieldLabels[key] || labels[key] || key }, + /status|decision|effect|enabled|health|compatibility/i.test(key) ? statusTag(value) : displayValue(value) + ) + ) + ) + }, + { + key: "context", + label: "上下文", + children: h("pre", { className: "json-pre" }, JSON.stringify(detail, null, 2)) + }, + { + key: "impact", + label: "影响范围", + children: h(List, { + size: "small", + dataSource: [ + `来源实体:${displayValue(detail.source_entity || detail.entity || detail.object_type)}`, + `对象:${safeValue(detail.source_record_id || detail.canonical_record_id || detail.object_id || detail.id || detail.job_id)}`, + `状态:${displayValue(detail.status || detail.lifecycle_status || detail.decision || detail.effect)}`, + "高风险动作会由后端接口写入审计;页面不展示完整令牌、接口密钥、原始载荷或 SQL。" + ], + renderItem: item => h(List.Item, null, item) + }) + }, + { + key: "audit", + label: "审计记录", + children: h(Alert, { + type: "info", + showIcon: true, + message: "审计来源", + description: "管理员操作、权限决策、OAuth 安全事件分别从后端审计接口读取。当前抽屉仅展示该对象的本地上下文摘要。" + }) + } + ] }) : null); + } + function DataTable({ rows: inputRows, preferred, rowKey = "id", actions, size = "small" }) { const data = rows(inputRows); const [detail, setDetail] = React.useState(null); @@ -373,12 +550,7 @@ tableLayout: "fixed" }) ), - h(Drawer, { - title: "记录详情", - width: 680, - open: !!detail, - onClose: () => setDetail(null) - }, detail ? h("pre", { className: "json-pre" }, JSON.stringify(detail, null, 2)) : null) + h(DetailDrawer, { title: "治理对象详情", detail, onClose: () => setDetail(null) }) ); } @@ -407,6 +579,33 @@ ); } + function PlaybookBoard({ onNavigate }) { + return h("section", { className: "playbook-board" }, + h("div", { className: "playbook-main" }, + h("div", { className: "playbook-kicker" }, "第一次打开先从这里开始"), + h("h2", null, "这个后台可以怎么玩"), + h("p", null, "别从表名和技术缩写开始。先按四条线验证:数据能进来、失败能治理、AI 检索不会越权、管理员操作有审计。"), + h("div", { className: "playbook-actions" }, + h(Button, { type: "primary", onClick: () => onNavigate("ingestion") }, "先看数据闭环"), + h(Button, { onClick: () => onNavigate("retrieval") }, "试一次安全检索") + ) + ), + h("div", { className: "playbook-list" }, playbooks.map(item => + h("button", { + key: item.key, + type: "button", + className: "playbook-card", + onClick: () => onNavigate(item.page) + }, + h("span", { className: "playbook-card-tag" }, item.tag), + h("strong", null, item.title), + h("span", { className: "playbook-proof" }, item.proof), + h("span", { className: "playbook-next" }, item.next) + ) + )) + ); + } + function BarList({ items }) { const max = Math.max(1, ...items.map(item => n(item.value))); return h("div", { className: "bar-list" }, items.map(item => @@ -432,10 +631,10 @@ } function HelpCenter({ open, onClose }) { - const [active, setActive] = React.useState("health"); + const [active, setActive] = React.useState("play"); const workflow = workflows.find(item => item.key === active) || workflows[0]; return h(Drawer, { - title: "帮助中心", + title: "玩法中心", width: 620, open, onClose @@ -448,11 +647,23 @@ }), h(Divider, null), h(Title, { level: 4, style: { marginTop: 0 } }, workflow.title), + h(Paragraph, { style: { color: "#64748b" } }, workflow.why), h(Steps, { direction: "vertical", current: -1, items: workflow.steps.map(step => ({ title: step })) }), + h(Divider, null), + h(Title, { level: 5 }, "四条产品证明线"), + h(List, { + dataSource: playbooks, + renderItem: item => h(List.Item, null, + h(List.Item.Meta, { + title: `${item.title}:${item.tag}`, + description: item.proof + }) + ) + }), h(Alert, { style: { marginTop: 12 }, type: "warning", @@ -498,7 +709,7 @@ ), h("div", { className: "mini-boundaries" }, h("span", null, "复杂 mock 数据试运行"), - h("span", null, "Search / Vector 回表过权限"), + h("span", null, "检索与索引回表过权限"), h("span", null, "不代表生产完成") ) ), @@ -595,6 +806,7 @@ title, content: h("div", null, h(Paragraph, null, content), + h(Paragraph, { strong: true }, "请确认影响范围、失败恢复方式和审计留痕。"), h(Paragraph, { type: "danger" }, "执行失败会统一提示系统异常;成功结果可在抽屉和审计中查看。") ), okText: "确认执行", @@ -612,6 +824,90 @@ }); } + function seriesFromSummary(summary, keys) { + return keys.map(([name, key]) => ({ name, value: n(summary[key]) })); + } + + function healthGaugeOption(score) { + return { + series: [{ + type: "gauge", + radius: "92%", + progress: { show: true, width: 14 }, + axisLine: { lineStyle: { width: 14, color: [[0.65, chartColors.failed], [0.85, chartColors.warning], [1, chartColors.ok]] } }, + axisTick: { show: false }, + splitLine: { length: 9, lineStyle: { color: "#cbd5e1" } }, + axisLabel: { color: "#64748b", distance: 18 }, + pointer: { width: 4 }, + detail: { valueAnimation: true, formatter: "{value}", fontSize: 28, fontWeight: 700, color: chartColors.ink }, + data: [{ value: score, name: "健康分" }], + title: { color: "#64748b", fontSize: 12, offsetCenter: [0, "60%"] } + }] + }; + } + + function barOption(title, items) { + return { + title: { text: title, left: 0, top: 0, textStyle: { fontSize: 13, fontWeight: 600, color: chartColors.ink } }, + tooltip: { trigger: "axis", axisPointer: { type: "shadow" } }, + xAxis: { type: "value", axisLabel: { color: "#64748b" }, splitLine: { lineStyle: { color: "#eef2f7" } } }, + yAxis: { type: "category", data: items.map(item => item.name || item.label), axisLabel: { color: "#475569", width: 110, overflow: "truncate" } }, + series: [{ type: "bar", barWidth: 14, data: items.map(item => ({ value: n(item.value), itemStyle: { color: item.color || chartColors.running } })) }] + }; + } + + function lineOption(title, names, values) { + return { + title: { text: title, left: 0, top: 0, textStyle: { fontSize: 13, fontWeight: 600, color: chartColors.ink } }, + tooltip: { trigger: "axis" }, + xAxis: { type: "category", boundaryGap: false, data: names, axisLabel: { color: "#64748b" } }, + yAxis: { type: "value", axisLabel: { color: "#64748b" }, splitLine: { lineStyle: { color: "#eef2f7" } } }, + series: [{ type: "line", smooth: true, areaStyle: { opacity: 0.12 }, data: values, symbolSize: 8 }] + }; + } + + function pieOption(title, items) { + return { + title: { text: title, left: 0, top: 0, textStyle: { fontSize: 13, fontWeight: 600, color: chartColors.ink } }, + legend: { bottom: 0, type: "scroll" }, + series: [{ + type: "pie", + radius: ["42%", "68%"], + center: ["50%", "48%"], + avoidLabelOverlap: true, + label: { formatter: "{b}\n{c}", color: "#334155" }, + data: items + }] + }; + } + + function graphOption(title, nodes, links) { + return { + title: { text: title, left: 0, top: 0, textStyle: { fontSize: 13, fontWeight: 600, color: chartColors.ink } }, + tooltip: { + formatter: params => { + if (params.dataType === "edge") return `${params.data.source} -> ${params.data.target}
${params.data.label || "标准链路"}`; + const data = params.data || {}; + return `${data.name}
状态:${displayValue(data.status)}
上下文:${safeValue(data.context)}`; + } + }, + series: [{ + type: "graph", + layout: "none", + roam: false, + symbolSize: 58, + edgeSymbol: ["none", "arrow"], + edgeSymbolSize: [0, 10], + label: { show: true, color: "#172033", fontWeight: 600 }, + lineStyle: { color: "#94a3b8", width: 2, curveness: 0.08 }, + data: nodes.map(node => Object.assign({}, node, { + itemStyle: { color: node.status === "failed" ? chartColors.failed : node.status === "warning" ? chartColors.warning : node.status === "running" ? chartColors.running : chartColors.ok } + })), + links + }] + }; + } + function Overview({ data, onNavigate }) { const summary = data.summary || {}; const dead = n(summary.source_event_dead_letters) + n(summary.canonical_event_dead_letters) + n(summary.replay_dead_letters); @@ -623,27 +919,31 @@ const score = Math.max(0, Math.min(100, Math.round(96 - penalties))); const connectorOk = String(summary.connector_health || "").toLowerCase().includes("ok"); const priority = [ - { title: "死信待处理", value: dead, page: "sync", impact: "阻断事件恢复" }, - { title: "数据质量待处理", value: quality, page: "quality", impact: "影响清洗可信度" }, - { title: "Outbox 待发布", value: pending, page: "sync", impact: "影响下游一致性" }, - { title: "权限决策复核", value: n(summary.permission_decisions), page: "permission", impact: "查看拒绝与脱敏" }, - { title: "MDM 待合并", value: mdm, page: "mdm", impact: "影响客户唯一性" }, - { title: "Replay / Backfill 运行", value: jobs, page: "replay", impact: "查看任务结果" } + { title: "死信记录待处理", value: dead, page: "governance", impact: "阻断事件恢复" }, + { title: "数据质量待处理", value: quality, page: "governance", impact: "影响清洗可信度" }, + { title: "待发布事件积压", value: pending, page: "ingestion", impact: "影响下游一致性" }, + { title: "权限决策复核", value: n(summary.permission_decisions), page: "access", impact: "查看拒绝与脱敏" }, + { title: "MDM 待合并", value: mdm, page: "governance", impact: "影响客户唯一性" }, + { title: "数据重放 / 历史回补运行", value: jobs, page: "governance", impact: "查看任务结果" } ].sort((a, b) => b.value - a.value); + const trendNames = ["06:00", "10:00", "14:00", "18:00", "当前"]; + const syncTrend = [n(summary.sync_runs), n(summary.raw_records), n(summary.source_change_events), n(summary.source_event_outbox_published), n(summary.canonical_change_events_published)].map(value => Math.max(0, value)); + const auditTrend = [n(summary.permission_decisions), n(summary.permission_decisions) + dead, n(summary.permission_decisions) + quality, n(summary.permission_decisions) + pending, n(summary.permission_decisions) + dead + pending]; return h("div", { className: "page" }, h(PageTitle, { - title: "运营驾驶舱", - desc: "健康、链路、风险、待处理。" + title: "今日工作台", + desc: "先按玩法验证产品闭环,再看健康分、待处理事项、同步链路和审计趋势。" }), + h(PlaybookBoard, { onNavigate }), h("div", { className: "grid-2" }, - h(Section, { title: "健康评分", extra: h(Tag, { color: connectorOk ? "green" : "orange" }, connectorOk ? "Connector 正常" : "Connector 未就绪") }, + h(Section, { title: "健康评分", extra: h(Tag, { color: connectorOk ? "green" : "orange" }, connectorOk ? "数据连接器正常" : "数据连接器未就绪") }, h("div", { className: "health-board" }, - h("div", { className: "health-score" }, h(Progress, { type: "circle", percent: score, strokeColor: score >= 85 ? "#16a34a" : score >= 65 ? "#d97706" : "#dc2626", format: value => `${value}` })), + h(EChart, { height: 240, option: healthGaugeOption(score) }), h("div", { className: "metric-strip" }, - h(Metric, { label: "Raw", value: n(summary.raw_records), note: "已落库" }), - h(Metric, { label: "SCE", value: n(summary.source_change_events), note: "统一事件" }), - h(Metric, { label: "Search", value: n(summary.search_documents), note: "候选文档" }), - h(Metric, { label: "Vector", value: n(summary.vector_documents), note: "候选文档" }) + h(Metric, { label: "原始层", value: n(summary.raw_records), note: "已落库" }), + h(Metric, { label: "来源变更事件", value: n(summary.source_change_events), note: "统一事件" }), + h(Metric, { label: "关键词检索", value: n(summary.search_documents), note: "候选文档" }), + h(Metric, { label: "语义召回", value: n(summary.vector_documents), note: "候选文档" }) ) ) ), @@ -659,36 +959,45 @@ ) ), h(Section, { title: "同步链路" }, - h("div", { className: "pipeline" }, - h(PipelineNode, { title: "Connector", status: connectorOk ? "ok" : "warning", value: connectorOk ? "正常" : "待配置", note: safeValue(summary.connector_health) }), - h(PipelineNode, { title: "Checkpoint", status: n(summary.checkpoints) > 0 ? "ok" : "warning", value: n(summary.checkpoints), note: "窗口状态" }), - h(PipelineNode, { title: "Raw", status: n(summary.raw_records) > 0 ? "ok" : "warning", value: n(summary.raw_records), note: "原始记录" }), - h(PipelineNode, { title: "SCE", status: n(summary.source_change_events) > 0 ? "ok" : "warning", value: n(summary.source_change_events), note: "来源事件" }), - h(PipelineNode, { title: "Outbox", status: pending || dead ? "warning" : "ok", value: pending, note: `死信 ${dead}` }), - h(PipelineNode, { title: "Canonical", status: n(summary.transform_errors) ? "error" : "ok", value: n(summary.canonical_change_events_published), note: `待发 ${n(summary.canonical_change_events_pending)}` }) - ) + h(EChart, { height: 310, option: syncGraphOption(summary) }) ), h("div", { className: "grid-2" }, - h(Section, { title: "失败分布" }, - h(BarList, { items: [ - { label: "Source 死信", value: n(summary.source_event_dead_letters), color: "#dc2626" }, - { label: "Canonical 死信", value: n(summary.canonical_event_dead_letters), color: "#dc2626" }, - { label: "Replay 死信", value: n(summary.replay_dead_letters), color: "#dc2626" }, - { label: "清洗失败", value: n(summary.transform_errors), color: "#d97706" } - ] }) + h(Section, { title: "今日异常分布" }, + h(EChart, { height: 280, option: barOption("异常数量", [ + { name: "来源死信记录", value: n(summary.source_event_dead_letters), color: chartColors.failed }, + { name: "统一模型死信", value: n(summary.canonical_event_dead_letters), color: chartColors.failed }, + { name: "数据重放死信", value: n(summary.replay_dead_letters), color: chartColors.failed }, + { name: "清洗失败", value: n(summary.transform_errors), color: chartColors.warning } + ]) }) ), - h(Section, { title: "治理分布" }, - h(BarList, { items: [ - { label: "质量问题", value: n(summary.data_quality_issues_open), color: "#d97706" }, - { label: "Schema Drift", value: n(summary.schema_drift_issues), color: "#d97706" }, - { label: "权限决策", value: n(summary.permission_decisions), color: "#2563eb" }, - { label: "状态映射", value: n(summary.status_mappings), color: "#16a34a" } - ] }) + h(Section, { title: "任务与审计趋势" }, + h(EChart, { height: 280, option: lineOption("运行趋势", trendNames, syncTrend) }), + h(EChart, { height: 220, option: lineOption("审计趋势", trendNames, auditTrend) }) ) ) ); } + function syncGraphOption(summary) { + const pending = n(summary.source_event_outbox_pending) + n(summary.canonical_change_events_pending); + const dead = n(summary.source_event_dead_letters) + n(summary.canonical_event_dead_letters) + n(summary.replay_dead_letters); + const nodes = [ + { name: "数据连接器", x: 60, y: 140, status: String(summary.connector_health || "").toLowerCase().includes("ok") ? "ok" : "warning", context: safeValue(summary.connector_health) }, + { name: "同步检查点", x: 210, y: 140, status: n(summary.checkpoints) > 0 ? "ok" : "warning", context: `${n(summary.checkpoints)} 个窗口` }, + { name: "原始层", x: 360, y: 140, status: n(summary.raw_records) > 0 ? "ok" : "warning", context: `${n(summary.raw_records)} 条原始记录` }, + { name: "来源变更事件", x: 530, y: 140, status: n(summary.source_change_events) > 0 ? "ok" : "warning", context: `${n(summary.source_change_events)} 条事件` }, + { name: "待发布事件", x: 710, y: 140, status: pending || dead ? "warning" : "ok", context: `积压 ${pending},死信 ${dead}` }, + { name: "统一模型", x: 880, y: 140, status: n(summary.transform_errors) ? "failed" : "ok", context: `已发布 ${n(summary.canonical_change_events_published)}` } + ]; + return graphOption("数据接入标准链路", nodes, [ + { source: "数据连接器", target: "同步检查点", label: "窗口推进" }, + { source: "同步检查点", target: "原始层", label: "先落原始层" }, + { source: "原始层", target: "来源变更事件", label: "统一信封" }, + { source: "来源变更事件", target: "待发布事件", label: "可靠发布边界" }, + { source: "待发布事件", target: "统一模型", label: "清洗与回表" } + ]); + } + function PipelineNode({ title, status, value, note }) { const color = status === "ok" ? "green" : status === "error" ? "red" : "orange"; return h("div", { className: "pipeline-node" }, @@ -702,7 +1011,8 @@ const [form] = Form.useForm(); const summary = data.summary || {}; async function runDeadAction(row, action) { - confirmAndRun(`处理 Dead Letter:${action}`, `类型 ${row.type},记录 ${shortId(row.id)}。`, async () => { + const actionName = displayValue(action); + confirmAndRun(`处理死信记录:${actionName}`, `类型 ${displayValue(row.type)},记录 ${shortId(row.id)}。该操作会影响失败恢复路径,并写入审计。`, async () => { const result = await api(`/internal/admin/dead-letters/${row.type}/${row.id}/${action}?reason=${encodeURIComponent("运营控制台处理")}`, { method: "POST" }); showResult(result); message.success("处理完成"); @@ -728,36 +1038,30 @@ } } return h("div", { className: "page" }, - h(PageTitle, { title: "数据同步", desc: "Connector -> Checkpoint -> Raw -> SCE -> Outbox -> Canonical" }), + h(PageTitle, { title: "数据接入链路", desc: "数据连接器 -> 同步检查点 -> 原始层 -> 来源变更事件 -> 待发布事件 -> 统一模型" }), h(Section, { title: "链路状态" }, - h("div", { className: "pipeline" }, - h(PipelineNode, { title: "Connector", status: String(summary.connector_health || "").includes("ok") ? "ok" : "warning", value: safeValue(summary.connector_health), note: "健康" }), - h(PipelineNode, { title: "Checkpoint", status: n(summary.checkpoints) ? "ok" : "warning", value: n(summary.checkpoints), note: "检查点" }), - h(PipelineNode, { title: "Raw", status: n(summary.raw_records) ? "ok" : "warning", value: n(summary.raw_records), note: "已写入" }), - h(PipelineNode, { title: "SCE", status: n(summary.source_change_events) ? "ok" : "warning", value: n(summary.source_change_events), note: "已生成" }), - h(PipelineNode, { title: "Outbox", status: n(summary.source_event_outbox_pending) || n(summary.source_event_dead_letters) ? "warning" : "ok", value: n(summary.source_event_outbox_pending), note: `死信 ${n(summary.source_event_dead_letters)}` }), - h(PipelineNode, { title: "Canonical", status: n(summary.transform_errors) ? "error" : "ok", value: n(summary.canonical_change_events_published), note: `待发 ${n(summary.canonical_change_events_pending)}` }) - ) + h(EChart, { height: 310, option: syncGraphOption(summary) }) ), h(Section, { title: "同步操作", extra: h(Form, { form, layout: "inline", initialValues: { entity: "customer" }, onFinish: detect }, h(Form.Item, { name: "entity", label: "实体" }, h(Select, { style: { width: 130 }, options: [{ value: "customer", label: "客户" }, { value: "trade", label: "订单" }] })), - h(Button, { htmlType: "submit" }, "检测 Schema"), + h(Button, { htmlType: "submit" }, "检测结构变化"), h(Button, { onClick: () => reconcile(form.getFieldsValue()) }, "运行对账") ) - }, h(Alert, { type: "info", showIcon: true, message: "操作结果可审计", description: "Dead Letter 操作会返回处理结果,并记录 dead_letter_actions / admin audit。" })), + }, h(Alert, { type: "info", showIcon: true, message: "操作结果可审计", description: "死信处理会返回处理结果,并记录死信动作与管理员审计。" })), h("div", { className: "grid-2" }, - h(Section, { title: "Checkpoint" }, + h(Section, { title: "同步检查点" }, h(DataTable, { rows: data.checkpoints, preferred: ["source_system", "source_entity", "sync_mode", "partition_key", "high_watermark", "updated_at"] }) ), - h(Section, { title: "Dead Letter" }, + h(Section, { title: "死信记录" }, h(DataTable, { rows: data.deadLetters, preferred: ["type", "id", "source_change_event_id", "error_code", "error_message", "status", "created_at"], actions: row => h(React.Fragment, null, h(Button, { size: "small", onClick: () => runDeadAction(row, "retry") }, "重试"), - h(Button, { size: "small", onClick: () => runDeadAction(row, "replay") }, "Replay"), + h(Button, { size: "small", onClick: () => runDeadAction(row, "replay") }, "数据重放"), + h(Button, { size: "small", onClick: () => runDeadAction(row, "ignore") }, "忽略"), h(Button, { size: "small", danger: true, onClick: () => runDeadAction(row, "quarantine") }, "隔离") ) }) @@ -771,24 +1075,30 @@ const schemaRows = rows(data.schemaIssues); const reconcileRows = rows(data.reconcileResults); const grouped = [ - { label: "schema drift", value: schemaRows.length, color: "#d97706" }, - { label: "dirty / unknown", value: qualityRows.filter(row => String(row.issue_type || "").includes("status")).length, color: "#dc2626" }, - { label: "reconcile 差异", value: reconcileRows.filter(row => n(row.missing_in_local) || n(row.missing_in_source) || n(row.hash_mismatch)).length, color: "#2563eb" }, - { label: "open issues", value: qualityRows.filter(row => row.status === "open").length, color: "#d97706" } + { name: "结构变化", value: schemaRows.length, color: chartColors.warning }, + { name: "脏数据 / 未知状态", value: qualityRows.filter(row => String(row.issue_type || "").includes("status")).length, color: chartColors.failed }, + { name: "对账差异", value: reconcileRows.filter(row => n(row.missing_in_local) || n(row.missing_in_source) || n(row.hash_mismatch)).length, color: chartColors.running }, + { name: "待处理质量问题", value: qualityRows.filter(row => row.status === "open").length, color: chartColors.warning } ]; return h("div", { className: "page" }, h(PageTitle, { title: "数据质量", desc: "问题分组、影响范围、处理状态。" }), h("div", { className: "grid-2" }, - h(Section, { title: "问题分布" }, h(BarList, { items: grouped })), + h(Section, { title: "问题分布" }, h(EChart, { height: 280, option: pieOption("质量问题", grouped) })), h(Section, { title: "筛选" }, h(Row, { gutter: [10, 10] }, h(Col, { xs: 24, md: 8 }, h(Select, { placeholder: "实体", style: { width: "100%" }, options: [{ value: "customer", label: "客户" }, { value: "trade", label: "订单" }] })), h(Col, { xs: 24, md: 8 }, h(Select, { placeholder: "状态", style: { width: "100%" }, options: [{ value: "open", label: "待处理" }, { value: "resolved", label: "已处理" }] })), - h(Col, { xs: 24, md: 8 }, h(Select, { placeholder: "类型", style: { width: "100%" }, options: ["schema drift", "raw orphan", "reconcile", "dirty", "stale"].map(value => ({ value, label: value })) })) + h(Col, { xs: 24, md: 8 }, h(Select, { placeholder: "类型", style: { width: "100%" }, options: [ + { value: "schema_drift", label: "结构变化" }, + { value: "raw_orphan", label: "原始层孤儿对象" }, + { value: "reconcile", label: "对账差异" }, + { value: "dirty", label: "脏数据" }, + { value: "stale", label: "长期未验证" } + ] })) ) ) ), - h(Section, { title: "Schema Drift" }, + h(Section, { title: "结构变化" }, h(DataTable, { rows: data.schemaIssues, preferred: ["source_system", "source_entity", "drift_type", "compatibility", "impact_assessment", "status", "created_at", "resolved_at"] }) ), h(Section, { title: "脏数据 / 未知状态" }, @@ -815,12 +1125,19 @@ } } function publish(version) { - confirmAndRun("发布权限策略版本", `版本 ${version} 发布后影响 API / Search / Vector / Admin 决策。`, async () => { + confirmAndRun("发布权限策略版本", `版本 ${version} 发布后影响 API、检索与索引、后台操作决策。`, async () => { const result = await api(`/internal/admin/permission/policies/versions/${version}/publish`, { method: "POST" }); showResult(result); reload(); }); } + function rollback(version) { + confirmAndRun("回滚权限策略版本", `回滚到版本 ${version} 后会影响所有已接入权限服务的 API、检索与后台操作。`, async () => { + const result = await api(`/internal/admin/permission/policies/versions/${version}/rollback`, { method: "POST" }); + showResult(result); + reload(); + }); + } async function simulate(values) { try { const qs = new URLSearchParams(values); @@ -831,8 +1148,16 @@ } } return h("div", { className: "page" }, - h(PageTitle, { title: "权限与组织", desc: "组织用户、角色策略、权限模拟、权限审计。" }), + h(PageTitle, { title: "身份与授权管理台", desc: "用户、部门、角色、策略、模拟、MCP 连接和权限审计。" }), h(Alert, { type: "warning", showIcon: true, message: "不信任前端 user_id / tenant_id", description: "身份只来自 Bearer token 或 API Key。" }), + h(Section, { title: "权限态势" }, + h(EChart, { height: 280, option: barOption("主体与决策", [ + { name: "用户", value: rows(data.users).length, color: chartColors.running }, + { name: "部门", value: rows(data.departments).length, color: chartColors.ok }, + { name: "角色", value: rows(data.roles).length, color: chartColors.ok }, + { name: "拒绝决策", value: rows(data.permissionAudit).filter(row => String(row.decision || "").toLowerCase().includes("deny")).length, color: chartColors.failed } + ]) }) + ), h(Section, { title: "组织用户" }, h(Tabs, { items: [ { key: "users", label: "用户", children: h(DataTable, { rows: data.users, preferred: ["username", "display_name", "employee_no", "status", "roles", "departments", "updated_at"] }) }, @@ -844,10 +1169,10 @@ h(Section, { title: "策略草稿" }, h(Form, { form: policyForm, layout: "vertical", initialValues: { subjectType: "role", subjectId: "admin", objectType: "customer", action: "read", effect: "allow", priority: 100 }, onFinish: createPolicy }, h(Row, { gutter: [10, 0] }, - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "subjectType", label: "主体类型" }, h(Select, { options: ["user", "role", "department", "service_account"].map(v => ({ value: v, label: v })) }))), + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "subjectType", label: "主体类型" }, h(Select, { options: ["user", "role", "department", "service_account"].map(v => ({ value: v, label: displayValue(v) })) }))), h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "subjectId", label: "主体 ID" }, h(Input, null))), - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "objectType", label: "对象" }, h(Select, { options: ["customer", "order", "field", "tool"].map(v => ({ value: v, label: v })) }))), - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "action", label: "动作" }, h(Select, { options: ["read", "export", "analyze", "update", "admin"].map(v => ({ value: v, label: v })) }))), + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "objectType", label: "对象" }, h(Select, { options: ["customer", "order", "field", "tool"].map(v => ({ value: v, label: displayValue(v) })) }))), + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "action", label: "动作" }, h(Select, { options: ["read", "export", "analyze", "update", "admin"].map(v => ({ value: v, label: displayValue(v) })) }))), h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "effect", label: "结果" }, h(Select, { options: [{ value: "allow", label: "允许" }, { value: "deny", label: "拒绝" }] }))), h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "priority", label: "优先级" }, h(InputNumber, { min: 1, style: { width: "100%" } }))) ), @@ -857,11 +1182,11 @@ h(Section, { title: "权限模拟" }, h(Form, { form: simForm, layout: "vertical", initialValues: { subjectType: "user", subjectId: "sales001", role: "sales", objectType: "customer", action: "read" }, onFinish: simulate }, h(Row, { gutter: [10, 0] }, - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "subjectType", label: "主体类型" }, h(Select, { options: ["user", "role", "department", "service_account"].map(v => ({ value: v, label: v })) }))), + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "subjectType", label: "主体类型" }, h(Select, { options: ["user", "role", "department", "service_account"].map(v => ({ value: v, label: displayValue(v) })) }))), h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "subjectId", label: "主体 ID" }, h(Input, null))), h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "role", label: "角色" }, h(Input, null))), - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "objectType", label: "对象" }, h(Select, { options: ["customer", "order", "field"].map(v => ({ value: v, label: v })) }))), - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "action", label: "动作" }, h(Select, { options: ["read", "export", "analyze", "update", "admin"].map(v => ({ value: v, label: v })) }))) + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "objectType", label: "对象" }, h(Select, { options: ["customer", "order", "field"].map(v => ({ value: v, label: displayValue(v) })) }))), + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "action", label: "动作" }, h(Select, { options: ["read", "export", "analyze", "update", "admin"].map(v => ({ value: v, label: displayValue(v) })) }))) ), h(Button, { htmlType: "submit" }, "模拟") ) @@ -871,7 +1196,10 @@ h(DataTable, { rows: data.policies, preferred: ["policy_version", "subject_type", "subject_id", "object_type", "action", "effect", "priority", "lifecycle_status", "enabled", "published_at"], - actions: row => h(Button, { size: "small", onClick: () => publish(row.policy_version), disabled: row.lifecycle_status !== "draft" }, "发布") + actions: row => h(React.Fragment, null, + h(Button, { size: "small", onClick: () => publish(row.policy_version), disabled: row.lifecycle_status !== "draft" }, "发布"), + h(Button, { size: "small", danger: true, onClick: () => rollback(row.policy_version), disabled: row.lifecycle_status === "draft" }, "回滚") + ) }) ), h(Section, { title: "权限审计" }, @@ -898,7 +1226,7 @@ } } function rebuild() { - confirmAndRun("重建 Search / Vector 索引", "重建会读取 Canonical 重新生成候选索引。", async () => { + confirmAndRun("重建检索索引", "重建会读取统一模型重新生成关键词检索和语义召回候选索引。", async () => { const result = await api("/internal/admin/search-vector/rebuild", { method: "POST" }); showResult(result); reload(); @@ -915,13 +1243,9 @@ } const resultRows = rows(searchResult && searchResult.data); return h("div", { className: "page" }, - h(PageTitle, { title: h(React.Fragment, null, h(Term, { name: "Search" }), " / ", h(Term, { name: "Vector" })), desc: "候选 ID -> Canonical 回表 -> 权限过滤 / 脱敏。" }), - h(Section, { title: "三段式检索流程" }, - h("div", { className: "flow-steps" }, - h("div", { className: "flow-step" }, h("strong", null, "1. 候选召回"), h(Text, null, resultRows.length ? resultRows.map(row => shortId(row.record_id)).join(", ") : "待检索")), - h("div", { className: "flow-step" }, h("strong", null, "2. Canonical 回表"), h(Text, null, resultRows.length ? `${resultRows.length} 条结果` : "后端执行")), - h("div", { className: "flow-step" }, h("strong", null, "3. 权限过滤"), h(Text, null, searchResult && searchResult.meta ? `过滤 ${safeValue(searchResult.meta.total_filtered || 0)},脱敏 ${safeValue(searchResult.meta.masked_fields || [])}` : "返回 meta")) - ) + h(PageTitle, { title: "检索验证工作台", desc: "关键词检索 / 语义召回只产出候选,最终必须回表统一模型并经过权限过滤。" }), + h(Section, { title: "候选命中 -> 统一模型回表 -> 权限过滤 -> 返回结果" }, + h(EChart, { height: 300, option: retrievalGraphOption(resultRows, searchResult && searchResult.meta) }) ), h(Section, { title: "检索" }, h(Form, { form, layout: "vertical", initialValues: { query: "找最近物流慢的订单", entityTypes: ["order"], limit: 10 } }, @@ -930,8 +1254,8 @@ h(Col, { xs: 24, lg: 7 }, h(Form.Item, { name: "entityTypes", label: "实体" }, h(Select, { mode: "multiple", options: ["customer", "order", "order_line", "issue"].map(v => ({ value: v, label: v })) }))), h(Col, { xs: 24, lg: 3 }, h(Form.Item, { name: "limit", label: "数量" }, h(InputNumber, { min: 1, max: 50, style: { width: "100%" } }))), h(Col, { xs: 24, lg: 4 }, h(Form.Item, { label: "操作" }, h("div", { className: "inline-actions" }, - h(Button, { onClick: () => query("/api/search", form.getFieldsValue()) }, "Search"), - h(Button, { onClick: () => query("/api/vector/search", form.getFieldsValue()) }, "Vector"), + h(Button, { onClick: () => query("/api/search", form.getFieldsValue()) }, "关键词检索"), + h(Button, { onClick: () => query("/api/vector/search", form.getFieldsValue()) }, "语义召回"), h(Button, { type: "primary", onClick: () => query("/api/ai/retrieve", form.getFieldsValue()) }, "合并") ))) ) @@ -948,6 +1272,22 @@ ); } + function retrievalGraphOption(resultRows, meta) { + const candidate = rows(resultRows).length; + const filtered = n(meta && meta.total_filtered); + const masked = rows(meta && meta.masked_fields).length; + return graphOption("检索授权链路", [ + { name: "候选命中", x: 90, y: 140, status: candidate ? "running" : "warning", context: `${candidate || 0} 个候选 ID` }, + { name: "统一模型回表", x: 300, y: 140, status: candidate ? "ok" : "warning", context: candidate ? "按候选 ID 回表,不返回索引原文" : "等待检索" }, + { name: "权限过滤", x: 520, y: 140, status: filtered ? "warning" : "ok", context: `过滤 ${filtered},脱敏字段 ${masked}` }, + { name: "返回结果", x: 730, y: 140, status: candidate ? "ok" : "warning", context: "仅返回授权后的业务字段和可信元信息" } + ], [ + { source: "候选命中", target: "统一模型回表", label: "candidate ids only" }, + { source: "统一模型回表", target: "权限过滤", label: "PermissionService" }, + { source: "权限过滤", target: "返回结果", label: "脱敏与审计元信息" } + ]); + } + function loginStateText(value) { const map = { active: "活跃", @@ -1160,6 +1500,13 @@ reload(); }); } + function approveSplit(row) { + confirmAndRun("审批 MDM 拆分", "拆分会迁移来源身份、可选重指向订单,并写入统一模型 split 事件与审计。", async () => { + const result = await api(`/internal/admin/mdm/split-proposals/${row.id}/approve`, { method: "POST" }); + showResult(result); + reload(); + }); + } function createProposal(row) { confirmAndRun("候选转合并提案", "只创建提案,不自动合并。", async () => { const result = await api(`/internal/admin/mdm/merge-proposals/from-candidate/${row.id}`, { method: "POST" }); @@ -1168,7 +1515,16 @@ }); } return h("div", { className: "page" }, - h(PageTitle, { title: h(React.Fragment, null, h(Term, { name: "MDM" }), " 主数据治理"), desc: "重复客户、合并、拆分、字段冲突、field lock、survivor、lineage。" }), + h(PageTitle, { title: h(React.Fragment, null, h(Term, { name: "MDM" }), " 主数据治理"), desc: "重复客户、合并、拆分、字段冲突、字段锁定、主记录选择和数据血缘。" }), + h(Section, { title: "MDM 处理漏斗" }, + h(EChart, { height: 260, option: barOption("候选到执行", [ + { name: "匹配候选", value: rows(data.mdmCandidates).length, color: chartColors.running }, + { name: "合并提案", value: rows(data.mdmProposals).length, color: chartColors.warning }, + { name: "字段冲突", value: rows(data.mdmConflicts).length, color: chartColors.failed }, + { name: "拆分提案", value: rows(data.splitProposals).length, color: chartColors.warning }, + { name: "拆分操作", value: rows(data.splitOperations).length, color: chartColors.ok } + ]) }) + ), h(Section, { title: "处理步骤" }, h(Steps, { size: "small", @@ -1215,7 +1571,11 @@ ), h(Section, { title: "拆分记录" }, h(Tabs, { items: [ - { key: "proposal", label: "拆分提案", children: h(DataTable, { rows: data.splitProposals, preferred: ["id", "original_record_id", "source_record_id_groups", "status", "created_by", "created_at"] }) }, + { key: "proposal", label: "拆分提案", children: h(DataTable, { + rows: data.splitProposals, + preferred: ["id", "original_record_id", "source_record_id_groups", "status", "created_by", "created_at"], + actions: row => h(Button, { size: "small", danger: true, onClick: () => approveSplit(row), disabled: row.status !== "pending" }, "审批拆分") + }) }, { key: "operation", label: "拆分操作", children: h(DataTable, { rows: data.splitOperations, preferred: ["id", "proposal_id", "original_record_id", "new_record_ids", "status", "executed_by", "executed_at"] }) } ] }) ) @@ -1227,7 +1587,7 @@ const [replayForm] = Form.useForm(); const [backfillForm] = Form.useForm(); function createReplay(values) { - confirmAndRun("创建 Replay", "必须显式选择 mapping_version 和 normalizer_version。", async () => { + confirmAndRun("创建数据重放", "必须显式选择映射版本和清洗版本。", async () => { const qs = new URLSearchParams(values); const job = await api(`/internal/admin/replay/jobs?${qs.toString()}`, { method: "POST" }); const result = job.job_id ? await api(`/internal/admin/replay/jobs/${job.job_id}/run`, { method: "POST" }) : job; @@ -1236,7 +1596,7 @@ }); } function createBackfill(values) { - confirmAndRun("创建 Backfill", "当前 Backfill 只基于 mock / 已落 Raw,不推进 checkpoint。", async () => { + confirmAndRun("创建历史回补", "当前历史回补只基于 mock / 已落原始层,不推进同步检查点。", async () => { const qs = new URLSearchParams(values); const job = await api(`/internal/admin/backfill/jobs?${qs.toString()}`, { method: "POST" }); const result = job.job_id ? await api(`/internal/admin/backfill/jobs/${job.job_id}/run`, { method: "POST" }) : job; @@ -1245,29 +1605,29 @@ }); } function backfillAction(row, action) { - confirmAndRun(`${action} Backfill`, `任务 ${shortId(row.job_id)}。`, async () => { + confirmAndRun(`${displayValue(action)} 历史回补`, `任务 ${shortId(row.job_id)}。`, async () => { const result = await api(`/internal/admin/backfill/jobs/${row.job_id}/${action}`, { method: "POST" }); showResult(result); reload(); }); } return h("div", { className: "page" }, - h(PageTitle, { title: h(React.Fragment, null, h(Term, { name: "Replay" }), " / ", h(Term, { name: "Backfill" })), desc: "任务中心:创建、运行、失败、完成、重试。" }), + h(PageTitle, { title: "数据重放与历史回补", desc: "任务中心:创建、运行、失败、完成、重试。" }), h("div", { className: "grid-2" }, - h(Section, { title: h(Term, { name: "Replay" }) }, + h(Section, { title: h(Term, { name: "数据重放" }) }, h(Form, { form: replayForm, layout: "vertical", initialValues: { sourceEntity: "trade", mode: "dry_run", schemaVersion: "current", mappingVersion: "current", normalizerVersion: "current" }, onFinish: createReplay }, h(Row, { gutter: [10, 0] }, h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "sourceEntity", label: "实体" }, h(Select, { options: [{ value: "customer", label: "客户" }, { value: "trade", label: "订单" }] }))), - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "mode", label: "模式" }, h(Select, { options: [{ value: "dry_run", label: "dry_run" }, { value: "execute", label: "execute" }] }))), - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "sourceRecordId", label: "source_record_id" }, h(Input, { placeholder: "可选" }))), - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "schemaVersion", label: "schema_version" }, h(Input, null))), - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "mappingVersion", label: "mapping_version" }, h(Input, null))), - h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "normalizerVersion", label: "normalizer_version" }, h(Input, null))) + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "mode", label: "模式" }, h(Select, { options: [{ value: "dry_run", label: "影响预演" }, { value: "execute", label: "执行重算" }] }))), + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "sourceRecordId", label: "来源记录" }, h(Input, { placeholder: "可选" }))), + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "schemaVersion", label: "结构版本" }, h(Input, null))), + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "mappingVersion", label: "映射版本" }, h(Input, null))), + h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "normalizerVersion", label: "清洗版本" }, h(Input, null))) ), h(Button, { type: "primary", htmlType: "submit" }, "创建并运行") ) ), - h(Section, { title: h(Term, { name: "Backfill" }) }, + h(Section, { title: h(Term, { name: "历史回补" }) }, h(Form, { form: backfillForm, layout: "vertical", initialValues: { sourceEntity: "trade", batchSize: 500 }, onFinish: createBackfill }, h(Row, { gutter: [10, 0] }, h(Col, { xs: 24, md: 8 }, h(Form.Item, { name: "sourceEntity", label: "实体" }, h(Select, { options: [{ value: "customer", label: "客户" }, { value: "trade", label: "订单" }] }))), @@ -1281,8 +1641,8 @@ ), h(Section, { title: "任务进度" }, h(Tabs, { items: [ - { key: "replay", label: "Replay Jobs", children: h(DataTable, { rows: data.replayJobs, preferred: ["job_id", "source_entity", "mode", "status", "total_tasks", "success_count", "failed_count", "error_message", "created_at", "finished_at"] }) }, - { key: "backfill", label: "Backfill Jobs", children: h(DataTable, { + { key: "replay", label: "数据重放任务", children: h(DataTable, { rows: data.replayJobs, preferred: ["job_id", "source_entity", "mode", "status", "total_tasks", "success_count", "failed_count", "error_message", "created_at", "finished_at"] }) }, + { key: "backfill", label: "历史回补任务", children: h(DataTable, { rows: data.backfillJobs, preferred: ["job_id", "source_entity", "status", "batch_size", "processed_count", "retry_count", "error_message", "created_at", "finished_at"], actions: row => h(React.Fragment, null, @@ -1299,7 +1659,7 @@ function StatusPage({ data, api, reload, showResult }) { const [form] = Form.useForm(); function save(values) { - confirmAndRun("保存状态映射", "映射变更会生成 dry-run Replay 影响分析,不会自动重写 Canonical。", async () => { + confirmAndRun("保存状态映射", "映射变更会生成影响预演任务,不会自动重写统一模型。", async () => { const qs = new URLSearchParams(values); const result = await api(`/internal/admin/status-mappings?${qs.toString()}`, { method: "POST" }); showResult(result); @@ -1314,17 +1674,17 @@ }); } return h("div", { className: "page" }, - h(PageTitle, { title: "状态映射", desc: "source_system / source_entity / source_field / source_value -> canonical_status / sync_health_status / governance_status。" }), - h(Alert, { type: "info", showIcon: true, message: "状态不硬编码", description: "关闭、作废、删除、完成、停用等都必须走配置、版本、审计和 Replay 影响分析。" }), + h(PageTitle, { title: "状态映射", desc: "来源系统 / 来源实体 / 来源字段 / 来源值 -> 业务状态 / 同步健康 / 治理状态。" }), + h(Alert, { type: "info", showIcon: true, message: "状态不硬编码", description: "关闭、作废、删除、完成、停用等都必须走配置、版本、审计和数据重放影响分析。" }), h(Section, { title: "新增 / 更新映射" }, h(Form, { form, layout: "vertical", initialValues: { sourceEntity: "trade", sourceField: "tradeStatus", sourceValue: "5010", canonicalBusinessStatus: "cancelled", syncHealthStatus: "fresh", governanceStatus: "normal", mappingVersion: 2 }, onFinish: save }, h(Row, { gutter: [10, 0] }, - h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "sourceEntity", label: "source_entity" }, h(Select, { options: [{ value: "customer", label: "客户" }, { value: "trade", label: "订单" }] }))), - h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "sourceField", label: "source_field" }, h(Input, null))), - h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "sourceValue", label: "source_value" }, h(Input, null))), - h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "canonicalBusinessStatus", label: "canonical_status" }, h(Input, null))), - h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "syncHealthStatus", label: "sync_health_status" }, h(Input, null))), - h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "governanceStatus", label: "governance_status" }, h(Input, null))), + h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "sourceEntity", label: "来源实体" }, h(Select, { options: [{ value: "customer", label: "客户" }, { value: "trade", label: "订单" }] }))), + h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "sourceField", label: "来源字段" }, h(Input, null))), + h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "sourceValue", label: "来源值" }, h(Input, null))), + h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "canonicalBusinessStatus", label: "业务状态" }, h(Input, null))), + h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "syncHealthStatus", label: "同步健康" }, h(Input, null))), + h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "governanceStatus", label: "治理状态" }, h(Input, null))), h(Col, { xs: 24, md: 4 }, h(Form.Item, { name: "mappingVersion", label: "版本" }, h(InputNumber, { min: 1, style: { width: "100%" } }))) ), h(Button, { type: "primary", htmlType: "submit" }, "保存") @@ -1346,14 +1706,23 @@ if (filter === "all") return true; return safeValue(row.resource_type).toLowerCase().includes(filter) || safeValue(row.action).toLowerCase().includes(filter); }); + const auditItems = [ + { name: "管理员操作", value: rows(data.adminAudit).length, color: chartColors.running }, + { name: "权限拒绝", value: rows(data.permissionAudit).filter(row => String(row.decision || "").toLowerCase().includes("deny")).length, color: chartColors.failed }, + { name: "权限允许", value: rows(data.permissionAudit).filter(row => String(row.decision || "").toLowerCase().includes("allow")).length, color: chartColors.ok }, + { name: "高风险动作", value: adminRows.filter(row => String(row.action || "").toUpperCase() === "POST").length, color: chartColors.warning } + ]; return h("div", { className: "page" }, h(PageTitle, { title: "审计日志", desc: "谁、何时、做了什么、影响对象、结果。" }), + h(Section, { title: "审计趋势" }, + h(EChart, { height: 270, option: barOption("审计事件", auditItems) }) + ), h(Section, { title: "筛选", extra: h(Segmented, { value: filter, onChange: setFilter, options: [ { label: "全部", value: "all" }, { label: "危险操作", value: "post" }, { label: "权限拒绝", value: "permission" }, { label: "MDM", value: "mdm" }, - { label: "Replay", value: "replay" }, + { label: "数据重放", value: "replay" }, { label: "状态映射", value: "status" } ] }) }, h(Text, { type: "secondary" }, "审计来自后端表。")), h(Section, { title: "Admin 操作" }, @@ -1366,8 +1735,17 @@ } function SettingsPage({ data }) { + const summary = data.summary || {}; return h("div", { className: "page" }, - h(PageTitle, { title: "系统设置", desc: "试运行边界、健康、版本。" }), + h(PageTitle, { title: "环境信息", desc: "试运行边界、健康、版本。" }), + h(Section, { title: "系统边界与运行状态" }, + h(EChart, { height: 260, option: pieOption("运行状态", [ + { name: "数据连接器", value: String(summary.connector_health || "").toLowerCase().includes("ok") ? 1 : 0 }, + { name: "原始层记录", value: n(summary.raw_records) }, + { name: "待发布事件", value: n(summary.source_event_outbox_pending) + n(summary.canonical_change_events_pending) }, + { name: "死信记录", value: n(summary.source_event_dead_letters) + n(summary.canonical_event_dead_letters) + n(summary.replay_dead_letters) } + ]) }) + ), h("div", { className: "grid-2" }, h(Section, { title: "试运行边界" }, h(List, { @@ -1377,10 +1755,10 @@ ), h(Section, { title: "环境" }, h(Descriptions, { bordered: true, column: 1, size: "small" }, - h(Descriptions.Item, { label: "Connector" }, data.connectorHealth && data.connectorHealth.message ? data.connectorHealth.message : safeValue((data.summary || {}).connector_health)), + h(Descriptions.Item, { label: "数据连接器" }, data.connectorHealth && data.connectorHealth.message ? data.connectorHealth.message : safeValue((data.summary || {}).connector_health)), h(Descriptions.Item, { label: "租户" }, "tenant_demo_001"), - h(Descriptions.Item, { label: "前端" }, "React + Ant Design"), - h(Descriptions.Item, { label: "Search / Vector" }, "PostgreSQL 本地试运行"), + h(Descriptions.Item, { label: "前端" }, "React + Ant Design + ECharts"), + h(Descriptions.Item, { label: "检索与索引" }, "PostgreSQL 本地试运行"), h(Descriptions.Item, { label: "身份" }, "本地 HMAC token / API Key") ) ) @@ -1388,6 +1766,101 @@ ); } + function IngestionWorkspace(props) { + const summary = props.data.summary || {}; + return h("div", { className: "page" }, + h(PageTitle, { title: "数据接入", desc: "围绕连接状态、同步历史、结构变化和失败事件组织接入治理。" }), + h(Section, { title: "接入链路总览" }, h(EChart, { height: 310, option: syncGraphOption(summary) })), + h(Tabs, { className: "work-tabs", items: [ + { key: "connection", label: "连接状态", children: h(SyncPage, props) }, + { key: "history", label: "同步历史", children: h(Section, { title: "同步检查点" }, h(DataTable, { rows: props.data.checkpoints, preferred: ["source_system", "source_entity", "sync_mode", "partition_key", "high_watermark", "updated_at"] })) }, + { key: "schema", label: "结构变化", children: h(QualityPage, props) }, + { key: "failed", label: "失败事件", children: h(SyncPage, props) } + ] }) + ); + } + + function GovernanceWorkspace(props) { + const summary = props.data.summary || {}; + return h("div", { className: "page" }, + h(PageTitle, { title: "数据治理", desc: "质量、死信、数据重放、历史回补、状态映射和 MDM 的连续治理工作区。" }), + h(Section, { title: "治理对象分布" }, + h(EChart, { height: 280, option: barOption("待处理对象", [ + { name: "质量问题", value: n(summary.data_quality_issues_open), color: chartColors.warning }, + { name: "死信记录", value: n(summary.source_event_dead_letters) + n(summary.canonical_event_dead_letters) + n(summary.replay_dead_letters), color: chartColors.failed }, + { name: "数据重放任务", value: rows(props.data.replayJobs).length, color: chartColors.running }, + { name: "历史回补任务", value: rows(props.data.backfillJobs).length, color: chartColors.running }, + { name: "MDM 提案", value: rows(props.data.mdmProposals).length, color: chartColors.ok } + ]) }) + ), + h(Tabs, { className: "work-tabs", items: [ + { key: "quality", label: "数据质量", children: h(QualityPage, props) }, + { key: "dead", label: "死信处理", children: h(SyncPage, props) }, + { key: "replay", label: "数据重放", children: h(ReplayPage, props) }, + { key: "backfill", label: "历史回补", children: h(ReplayPage, props) }, + { key: "status", label: "状态映射", children: h(StatusPage, props) }, + { key: "mdm", label: "主数据治理", children: h(MdmPage, props) } + ] }) + ); + } + + function RetrievalWorkspace(props) { + const summary = props.data.summary || {}; + return h("div", { className: "page" }, + h(PageTitle, { title: "检索与索引", desc: "验证候选召回、统一模型回表、权限过滤和索引运行状态。" }), + h("div", { className: "grid-2" }, + h(Section, { title: "检索链路拓扑" }, h(EChart, { height: 300, option: retrievalGraphOption([], {}) })), + h(Section, { title: "索引资产" }, h(EChart, { height: 300, option: pieOption("候选文档", [ + { name: "关键词检索", value: n(summary.search_documents) }, + { name: "语义召回", value: n(summary.vector_documents) }, + { name: "增量积压", value: n(summary.canonical_change_events_pending) }, + { name: "失败项", value: n(summary.transform_errors) } + ]) })) + ), + h(Tabs, { className: "work-tabs", items: [ + { key: "verify", label: "检索验证", children: h(SearchPage, props) }, + { key: "status", label: "索引状态", children: h(Section, { title: "索引状态" }, h(DataTable, { rows: props.data.searchVector, preferred: ["name", "count"] })) }, + { key: "rebuild", label: "重建任务", children: h(SearchPage, props) }, + { key: "incremental", label: "增量消费", children: h(SearchPage, props) } + ] }) + ); + } + + function AccessWorkspace(props) { + return h("div", { className: "page" }, + h(PageTitle, { title: "权限与身份", desc: "用户、部门、角色、策略、模拟和 MCP 连接统一管理。" }), + h(Section, { title: "身份与权限分布" }, + h(EChart, { height: 280, option: barOption("身份资产", [ + { name: "用户", value: rows(props.data.users).length, color: chartColors.running }, + { name: "部门", value: rows(props.data.departments).length, color: chartColors.ok }, + { name: "角色", value: rows(props.data.roles).length, color: chartColors.ok }, + { name: "策略", value: rows(props.data.policies).length, color: chartColors.warning } + ]) }) + ), + h(Tabs, { className: "work-tabs", items: [ + { key: "users", label: "用户", children: h(PermissionPage, props) }, + { key: "departments", label: "部门", children: h(PermissionPage, props) }, + { key: "roles", label: "角色", children: h(PermissionPage, props) }, + { key: "policies", label: "策略", children: h(PermissionPage, props) }, + { key: "simulate", label: "模拟", children: h(PermissionPage, props) }, + { key: "mcp", label: "MCP 连接", children: h(McpConnectionsPage, props) } + ] }) + ); + } + + function SystemWorkspace(props) { + return h("div", { className: "page" }, + h(PageTitle, { title: "审计与系统", desc: "管理员操作、权限决策、安全事件、环境信息和试运行边界。" }), + h(Tabs, { className: "work-tabs", items: [ + { key: "admin", label: "管理员操作", children: h(AuditPage, props) }, + { key: "permission", label: "权限决策", children: h(AuditPage, props) }, + { key: "security", label: "安全事件", children: h(McpConnectionsPage, props) }, + { key: "environment", label: "环境信息", children: h(SettingsPage, props) }, + { key: "boundary", label: "试运行边界", children: h(SettingsPage, props) } + ] }) + ); + } + function AppShell() { const [token, setToken] = React.useState(localStorage.getItem("admin_token") || ""); const [subject, setSubject] = React.useState(() => { @@ -1433,16 +1906,11 @@ const pageProps = { data, api, reload, showResult: setResult, onNavigate: setActive }; const page = { overview: h(Overview, pageProps), - sync: h(SyncPage, pageProps), - quality: h(QualityPage, pageProps), - permission: h(PermissionPage, pageProps), - search: h(SearchPage, pageProps), - mcp: h(McpConnectionsPage, pageProps), - mdm: h(MdmPage, pageProps), - replay: h(ReplayPage, pageProps), - status: h(StatusPage, pageProps), - audit: h(AuditPage, pageProps), - settings: h(SettingsPage, pageProps) + ingestion: h(IngestionWorkspace, pageProps), + governance: h(GovernanceWorkspace, pageProps), + retrieval: h(RetrievalWorkspace, pageProps), + access: h(AccessWorkspace, pageProps), + system: h(SystemWorkspace, pageProps) }[active]; return h(Layout, { className: "app-shell" }, @@ -1465,12 +1933,12 @@ h(Layout, { className: "main-layout" }, h(Header, { className: "topbar" }, h("div", { className: "topbar-left" }, - h(Text, { strong: true }, "运营控制台"), + h(Text, { strong: true }, "AI 数据底座运营治理后台"), h(Tag, { color: "blue" }, "mock 试运行"), loading ? h(Spin, { size: "small" }) : null ), h("div", { className: "topbar-right" }, - h(Button, { onClick: () => setHelpOpen(true) }, "帮助中心"), + h(Button, { onClick: () => setHelpOpen(true) }, "怎么玩"), h(Button, { onClick: reload }, "刷新"), h(Text, { ellipsis: true }, subject ? `${subject.subject_id || "admin"} / ${subject.role || "admin"}` : "已登录"), h(Button, { onClick: logout }, "退出") diff --git a/src/main/resources/static/admin-console/index.html b/src/main/resources/static/admin-console/index.html index 9cf1841..92f4a71 100644 --- a/src/main/resources/static/admin-console/index.html +++ b/src/main/resources/static/admin-console/index.html @@ -9,6 +9,7 @@ +