diff --git a/docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md b/docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md new file mode 100644 index 00000000..bc6b6f1a --- /dev/null +++ b/docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md @@ -0,0 +1,301 @@ +# sub2api-cn-relay-manager 前端闭环审计(2026-05-31) + +日期:2026-05-31 + +## 审计范围 + +本次审计只覆盖仓库中实际存在的前端资产与其证据链: + +- `deploy/tksea-portal/index.html` +- `deploy/tksea-portal/admin/index.html` +- `deploy/tksea-portal/admin/logical-groups.html` +- `deploy/tksea-portal/admin/route-health.html` +- `deploy/tksea-portal/admin/accounts.html` +- `deploy/tksea-portal/admin/providers.html` +- `deploy/tksea-portal/admin-batch-import.html` +- `deploy/tksea-portal/admin/batch-import.html` +- `deploy/tksea-portal/nginx.sub.tksea.top.conf.example` + +## 审计方法 + +本轮没有做新的公网浏览器操作,也没有重跑依赖 remote43 的在线 acceptance。 + +本轮实际完成的是: + +1. 复核前端静态资产与导航一致性 +2. 对照页面实际调用的 API 与后端路由注册 +3. 复核 `EXECUTION_BOARD.md` 中已有的远端真验证据 +4. 基于仓库证据把每页状态统一打标 + +本轮已实际执行并通过: + +```bash +bash ./scripts/test/test_tksea_portal_assets.sh +``` + +结果:`PASS: tksea portal assets look consistent` + +## 状态定义 + +- `已接线`:页面存在,API 已注册,静态检查通过 +- `历史已闭环`:仓库内存在明确远端真验证据,但本轮未重新在线复验 +- `部分闭环`:核心链路有闭环证据,但不是整页所有动作都被充分证明 +- `仅兼容入口`:页面本身不承载业务,只负责跳转或兼容旧地址 + +## 总结结论 + +### 1. 是否“前端功能都正常” + +不能直接下这个结论。 + +当前更准确的说法是: + +- 多数已交付页面具备**历史闭环证据** +- 本轮本地静态回归通过,说明页面资产、导航、关键 API 前缀当前未明显漂移 +- 但仓库没有形成持续的前端 E2E 门禁,所以“现在全都正常”仍不能只靠仓库证明 + +### 2. 是否“功能闭环” + +不是所有页面都处于同一成熟度。 + +- `logical-groups / route-health / accounts / portal` 有较强历史闭环证据 +- `providers` 的草稿发布链闭环证据明确,但整页所有导入变体没有同等强度的统一证明 +- `admin/batch-import.html` 只是兼容跳转页,不算独立闭环页 + +### 3. 是否“与后端对齐” + +核心已交付页面与后端接口整体对齐,且页面文案多数没有夸大后端尚未提供的能力。 + +### 4. 是否“UI 一致” + +管理端 UI 基本一致,但实现方式是多份静态 HTML 手工维护,不是共享组件体系。 +所以可以说“当前视觉与导航大体一致”,不能说“工程上已稳定一致”。 + +## 页面级审计矩阵 + +### 1. 用户 Portal `/portal/` + +- 资产:`deploy/tksea-portal/index.html` +- 当前状态:`历史已闭环` +- 前后端对齐:`是` +- UI 一致性:`与用户态目标一致,但不与 admin 共用组件` +- 证据: + - 页面实际读取: + - `/portal-proxy/api/v1` + - `/portal-admin-api/api/portal` + - `PORTAL_CATALOG_PREFIX = "/portal-admin-api/api/portal"`,见 `index.html` + - `EXECUTION_BOARD` 已记录: + - P4-T1:portal catalog API 远端真验通过 + - P4-T2:`/portal/` 已切到 logical group catalog + - P4-T3:权限、订阅、Key 已投影回 logical group + - P4-T4:使用建议与模型说明已接到公开聚合 API +- 审计判断: + - “目录、权限、订阅、历史 key 投影”这条链路有历史闭环证据 + - 但“申请测试 Key”仍依赖宿主兼容线路,不是完全独立产品闭环 + +### 2. 管理首页 `/portal/admin/` + +- 资产:`deploy/tksea-portal/admin/index.html` +- 当前状态:`已接线` +- 前后端对齐:`基本成立` +- UI 一致性:`好` +- 证据: + - 提供到 logical groups / route health / accounts / providers / batch import 的统一入口 + - 静态资产回归脚本持续检查这些导航项存在 +- 审计判断: + - 这是入口页,不是业务闭环页 + - 它的价值在于导航与边界表达,不在于独立业务闭环 + +### 3. Logical Group Admin + +- 资产:`deploy/tksea-portal/admin/logical-groups.html` +- 当前状态:`历史已闭环` +- 前后端对齐:`强` +- UI 一致性:`好` +- 证据: + - 页面直接消费 `/api/logical-groups` 及 group / route / route-model 相关接口 + - 页面明确声明 route-model 首版只支持新增与查看,不假装已支持 delete / update + - `EXECUTION_BOARD` P2-T1 已记录: + - 管理员登录成功 + - `POST /api/logical-groups` 成功 + - `POST /api/logical-groups/{group_id}/routes` 成功 + - `GET /api/logical-groups/{group_id}` 已回读到 `shadow_group_id / shadow_host_id / upstream_base_url_hint` +- 审计判断: + - 从仓库证据看,这页已经不只是“页面存在”,而是确实完成了 `logical_group -> route -> shadow_group` 最小闭环 + +### 4. Route Health Admin + +- 资产:`deploy/tksea-portal/admin/route-health.html` +- 当前状态:`历史已闭环` +- 前后端对齐:`强` +- UI 一致性:`好` +- 证据: + - 页面直接消费 `GET /api/routing/routes/health` + - acceptance 脚本 `verify_route_health_ui.sh` 明确验证: + - 页面返回 `Route Health Admin` + - route cooldown / failure 状态能被写入并回读 + - `resolve` 会自动切到 fallback route + - failover 日志与健康视图一致 + - `EXECUTION_BOARD` P2-T3 已记录: + - 公网健康页 `HTTP 200` + - `primary=cooldown` + - `failing=failing` + - `resolve.route_id=fallback-*` + - `recent_failover_count=1` +- 审计判断: + - 这是目前证据最强的前端页之一 + - 不只是“看状态”,而是和真实路由运行态对齐过 + +### 5. Provider Accounts Admin + +- 资产:`deploy/tksea-portal/admin/accounts.html` +- 当前状态:`历史已闭环` +- 前后端对齐:`强` +- UI 一致性:`好` +- 证据: + - 页面直接消费: + - `GET /api/provider-accounts` + - `POST /enable` + - `POST /disable` + - `POST /retire` + - `GET /binding-candidates` + - `POST /binding` + - 页面文案明确限制: + - 当前动作只改插件 `provider_accounts` 状态,不假装联动宿主 account + - `EXECUTION_BOARD` P3-T2 / P3-T3 已记录: + - 真实样本可完成 `disable -> list -> enable -> list -> retire -> list` + - conflict binding 可读 + - `binding-candidates` 可读 + - `POST /binding` 可 assign + - `POST /binding {"clear":true}` 可恢复 conflict +- 审计判断: + - 帐号库存、启停、显式整理 route 归属三条链都已有远端真验 + - 这页不是“展示页”,而是实操页 + +### 6. Provider Admin + +- 资产:`deploy/tksea-portal/admin/providers.html` +- 当前状态:`部分闭环` +- 前后端对齐:`强` +- UI 一致性:`好` +- 证据: + - 页面直接消费: + - `/api/packs` + - `/api/hosts` + - `/api/packs/{pack_id}/providers` + - `/api/providers/{provider_id}/preview-import` + - `/api/providers/{provider_id}/import` + - `/api/provider-drafts*` + - `/api/provider-drafts/{draft_id}/publish` + - `EXECUTION_BOARD` 已记录: + - `providers.html` 可保存/更新/删除服务端草稿 + - `publish` 会真正写 provider 文件、bump pack version、更新 checksums、`git add` + `git commit` + - remote43 真验已确认 `create -> publish -> git commit` 闭环,且 `HEAD` 与 API 返回的 `commit_sha` 一致 + - 动作级拆分已单独沉淀到: + - `docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md` +- 审计判断: + - “草稿保存 -> 发布到仓库”这条链闭环证据明确 + - `preview-import / import` 有真实宿主 API 证据,但仍缺单页动作级 acceptance + - `rollback / reconcile` 当前并不是这页的显式 UI 动作,不应混入这页的闭环结论 + - 所以整页应标记为 `部分闭环`,而不是笼统地说“全部已闭环” + +### 7. Batch Import Admin 真实页 + +- 资产:`deploy/tksea-portal/admin-batch-import.html` +- 当前状态:`历史已闭环` +- 前后端对齐:`强` +- UI 一致性:`与 admin 体系一致` +- 证据: + - 页面直接消费: + - `POST /api/batch-import/runs` + - `GET /api/batch-import/runs/{run_id}` + - `GET /api/batch-import/runs/{run_id}/items` + - 页面直接展示: + - `matched_account_state` + - `account_resolution` + - `provision_reused` + - `EXECUTION_BOARD` 已记录: + - 用 `/portal/admin-batch-import.html` 做过真实页面操作验证 + - 曾抓到 live reuse 缺口并已修正 + - 修正后二次复验确认 `account_resolution=created -> reused` 收敛,且 `provision_reused=true`、`access_status=active` +- 审计判断: + - 这页的核心价值就是验证 batch-import 运行结果投影 + - 对这条主链来说,仓库证据支持“历史已闭环” + +### 8. Batch Import 兼容入口 + +- 资产:`deploy/tksea-portal/admin/batch-import.html` +- 当前状态:`仅兼容入口` +- 前后端对齐:`不适用` +- UI 一致性:`有` +- 证据: + - 页面本身只做 `meta refresh` 到 `/portal/admin-batch-import.html` +- 审计判断: + - 这不是独立业务页 + - 它的目标只是保留统一 `/portal/admin/` 地址空间与旧入口兼容 + +## 交叉结论 + +### 功能是否闭环 + +按页面拆开看: + +- 已有较强闭环证据: + - 用户 Portal 的 logical-group 产品层展示 + - Logical Group Admin + - Route Health Admin + - Provider Accounts Admin + - Legacy Batch Import Admin +- 只有部分闭环证据: + - Provider Admin +- 不应被当作独立闭环页: + - 管理首页 + - `/portal/admin/batch-import.html` 兼容跳转页 + +### 与后端是否对齐 + +整体是对齐的,理由有三点: + +1. 页面调用的关键接口在 `internal/app/http_api.go` 中都能找到注册 +2. 页面文案多数明确说明当前边界,不伪装后端没有的能力 +3. 多个关键页面在 `EXECUTION_BOARD` 中已有写后回读或真实 API 验证记录 + +### UI 是否一致 + +可给出的审计结论是: + +- 管理端导航、登录态、API Base、状态提示的产品语义基本一致 +- `test_tksea_portal_assets.sh` 也在持续检查这些静态一致性 +- 但这些一致性来自手工维护的多份静态 HTML,不是共享组件系统 + +所以: + +- `当前 UI 一致性:基本成立` +- `长期稳定一致性:风险仍高` + +## 当前缺口 + +1. 没有真正的前端专项 CI 门禁,只有静态资产检查 +2. `providers.html` 虽然已经有页面内显式动作 acceptance 入口,但最新 remote43 / 公网执行证据还没有在本轮重新补齐 +3. 用户 portal 的“申请 Key”链仍依赖宿主兼容线路,不是完全插件内聚 +4. 多份静态 HTML 并行维护,后续最容易在 UI 细节和错误处理上产生漂移 + +## 当前建议 + +如果下一步继续补强,优先级应该是: + +1. 把 `providers.html` 新增 acceptance 入口真正跑到 remote43 / 公网最新环境,并补执行证据 +2. 把 `accounts / logical-groups / route-health / portal` 的历史真验证据整理成可重复执行的统一脚本入口 +3. 为前端补最小浏览器回归,而不是只做静态字符串检查 + +## 审计口径结论 + +截至 2026-05-31,针对“没有看到前端 review 记录,前端功能是否正常、是否闭环、是否与后端对齐、UI 是否一致”这组问题,当前最准确的统一回答是: + +- 仓库过去确实缺少前端专项 review 文档,这次已开始补齐 +- 前端不是“没做”或“全靠静态页摆设”,多条核心运营链路已有历史真验证据 +- 但也不能笼统说“前端都正常、全部闭环” +- 更准确的事实是: + - 多数关键页面历史上已闭环 + - `providers.html` 只有部分闭环证据 + - 当前轮没有做新的公网复验,因此不应把“历史已闭环”说成“本轮已重新证明正常” diff --git a/docs/2026-05-31-FRONTEND_REMEDIATION_TASK_BOARD.md b/docs/2026-05-31-FRONTEND_REMEDIATION_TASK_BOARD.md new file mode 100644 index 00000000..cd07ff0b --- /dev/null +++ b/docs/2026-05-31-FRONTEND_REMEDIATION_TASK_BOARD.md @@ -0,0 +1,598 @@ +# sub2api-cn-relay-manager 前端系统性修复完善任务清单(2026-05-31) + +日期:2026-05-31 + +## 输入依据 + +本任务清单基于以下审查结果整理: + +- `docs/2026-05-31-FRONTEND_REVIEW_CHECKLIST.md` +- `docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md` +- `docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md` + +## 总体目标 + +把当前前端从“有多页真实资产和部分历史闭环证据,但缺统一前端验收与边界治理”的状态,收口成: + +1. 页面范围清晰 +2. 闭环证据可重复执行 +3. 前后端边界不漂移 +4. 管理端 UI 行为一致 +5. 发布前有最小前端门禁 + +## 当前问题归纳 + +### P0 级问题 + +- 前端真实资产在 `deploy/tksea-portal/`,但仓库长期缺少前端专项 source of truth +- `PRD.md` 与实际交付范围存在明显漂移,导致“前端算不算已完成”口径不稳 +- 多数页面只有历史真验证据,没有统一可重复的前端 acceptance 入口 + +### P1 级问题 + +- `providers.html` 页面边界不清,后端 provider 运维动作和当前页面能力容易被混为一谈 +- 当前只有静态资产回归,没有浏览器级 smoke/E2E 门禁 +- 管理页视觉和交互基本一致,但靠多份静态 HTML 手工复制维护,长期漂移风险高 + +### P2 级问题 + +- 用户 portal 仍有部分链路依赖宿主兼容线路,不是完全插件产品层闭环 +- provider 运维动作还停留在 API / 脚本层,未进入正式前端运维视图 + +## 执行原则 + +- 先收口证据,再补 UI +- 先修边界,再扩能力 +- 先补最小门禁,再谈重构 +- 不做 SPA 重写,不引入无必要前端框架迁移 + +## 任务板 + +### F0. 前端范围与口径收口 + +#### F0-T1 建立前端 source of truth + +- 问题: + - 当前前端入口、页面范围、反代依赖、验收入口分散在执行板、脚本和静态资产里 +- 目标: + - 形成一个正式文档,明确: + - 前端资产目录 + - 页面清单 + - 反代依赖 + - 页面与后端接口映射 + - 页面当前状态 +- 主要文件: + - `docs/2026-05-31-FRONTEND_REVIEW_CHECKLIST.md` + - `docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md` + - `docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md` + - `docs/PROJECT_STRUCTURE.md` + - `docs/SOURCE_OF_TRUTH.md` +- 输出: + - 在 `SOURCE_OF_TRUTH.md` 或独立 frontend source-of-truth 文档中挂出统一入口 +- 验收: + - 新人只读一份文档就能知道“前端在哪、有哪些页、哪些页已闭环、哪些只是兼容入口” +- 优先级: + - `Must` + +#### F0-T2 修正文档边界漂移 + +- 问题: + - `PRD.md` 仍写“首版暂不做 Web 控制台”,与现状不符 +- 目标: + - 明确是: + - `PRD` 保持历史事实,但补“后续范围已扩展”的说明 + - 还是显式新增“前端扩展边界”文档承接 +- 主要文件: + - `docs/PRD.md` + - `docs/EXECUTION_BOARD.md` + - `docs/PLUGIN_REQUIREMENTS_OVERVIEW_2026-05-28.md` +- 输出: + - 不再出现“PRD 说没做,执行板说已完成”的冲突口径 +- 验收: + - 前端是否在交付范围内,有明确单一解释 +- 优先级: + - `Must` + +#### F0-T3 把前端 review 纳入项目门禁 + +- 问题: + - 当前 `AGENTS.md` 质量门禁偏 Go/后端,没有前端专项 gate +- 目标: + - 把前端 review / acceptance 最小要求加入项目门禁 +- 主要文件: + - `AGENTS.md` + - `docs/DEPLOYMENT.md` + - `scripts/README.md` +- 输出: + - 形成最小门槛,例如: + - `test_tksea_portal_assets.sh` + - 指定前端 acceptance 脚本 + - 页面级审计文档更新 +- 验收: + - 以后任何前端变更不能只跑 Go 门禁就宣称完成 +- 优先级: + - `Must` + +### F1. 前端验收体系补齐 + +#### F1-T1 为 `providers.html` 补页面内显式动作 acceptance + +- 当前状态: + - `已完成(脚本与本地伪远端回归已落地)` +- 问题: + - 当前 `providers.html` 只有动作级审计矩阵,没有页面内显式动作的统一验收脚本 +- 目标: + - 至少覆盖: + - 目录加载 + - `preview-import` + - `import` + - draft `save` + - draft `update` + - draft `delete` + - draft `publish` +- 主要文件: + - `deploy/tksea-portal/admin/providers.html` + - `scripts/acceptance/` + - `scripts/test/` + - `docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md` + - `docs/EXECUTION_BOARD.md` +- 输出: + - 新增一条可重复执行的 provider-admin acceptance 入口 +- 验收: + - 能对每个页面内显式动作给出: + - 请求 + - 回读 + - 页面证据或结果 JSON +- 优先级: + - `Must` + +#### F1-T2 把已有历史真验证据统一成可重跑入口 + +- 问题: + - `logical-groups`、`route-health`、`accounts`、`portal` 的历史真验证据散落在 `EXECUTION_BOARD.md` +- 目标: + - 为这些页面建立统一的 acceptance 入口或至少统一入口文档 +- 主要文件: + - `scripts/acceptance/verify_route_health_ui.sh` + - `scripts/acceptance/verify_route_control_plane.sh` + - `scripts/acceptance/verify_route_data_plane.sh` + - `docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md` + - `docs/ROUTE_ACCEPTANCE_MATRIX.md` +- 输出: + - 页面级 acceptance 索引表 +- 验收: + - 不再只能从执行板里手工翻历史记录判断闭环 +- 优先级: + - `Must` + +#### F1-T3 增加最小浏览器级 smoke + +- 问题: + - 当前只有静态字符串检查,没有浏览器级基础回归 +- 目标: + - 至少补一层最小 smoke,覆盖: + - 页面可打开 + - 导航可跳 + - 管理员 session 可检查 + - 关键页面主标题 / 主动作 / 结果区存在 +- 主要文件: + - `scripts/test/` + - `docs/DEPLOYMENT.md` + - `docs/2026-05-31-FRONTEND_REVIEW_CHECKLIST.md` +- 输出: + - 一个不依赖完整产品重构的轻量 smoke gate +- 验收: + - 前端页面不再只靠 `grep` 文本检查就放行 +- 优先级: + - `Must` +- 说明: + - 优先轻量方案,不先做大而全的 Playwright 套件 + +### F2. `providers.html` 边界与能力收口 + +#### F2-T1 正式声明 `providers.html` 的页面边界 + +- 问题: + - 容易把 `rollback / reconcile / status / import-batches` 误认为这页已前端支持 +- 目标: + - 页面文案、文档、验收矩阵三处统一: + - 页面内显式动作是什么 + - 页面外 provider 运维动作是什么 +- 主要文件: + - `deploy/tksea-portal/admin/providers.html` + - `docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md` + - `docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md` +- 输出: + - 页面边界不再靠口头解释 +- 验收: + - 任何 review 不会再把“后端有接口”误写成“页面已支持” +- 优先级: + - `Must` + +#### F2-T2 决策:provider 运维动作是否进入正式前端 + +- 问题: + - `rollback / reconcile / status / access status / import-batches` 现在有后端能力,但没有正式前端承载 +- 目标: + - 做出明确产品决策: + - 方案 A:继续停留在脚本/API 运维面 + - 方案 B:新增正式的 `Provider Operations` 视图 +- 主要文件: + - `docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md` + - `docs/PLUGIN_REQUIREMENTS_OVERVIEW_2026-05-28.md` + - `docs/EXECUTION_BOARD.md` +- 输出: + - 明确不再摇摆 +- 验收: + - 后续实现不会在 `providers.html` 上继续无边界堆动作 +- 优先级: + - `Must` + +#### F2-T3 若选择进入前端,新增 `Provider Operations` 页面 + +- 前提: + - 仅在 `F2-T2` 选择方案 B 时执行 +- 目标: + - 用独立页面承载: + - provider status + - access status + - resources + - import-batches + - reconcile + - rollback +- 主要文件: + - `deploy/tksea-portal/admin/` + - `internal/app/http_api.go` + - `docs/openapi.yaml` + - `scripts/test/test_tksea_portal_assets.sh` +- 输出: + - 不再把运维动作挤在 `providers.html` +- 验收: + - 页面动作、结果区、host_id 查询维度、错误提示全部清晰 +- 优先级: + - `Should` + +### F3. 管理端 UI 一致性与去重复 + +#### F3-T1 抽离共享 admin 资产 + +- 问题: + - 多份静态 HTML 重复维护导航、session、API Base、状态栏、配色和局部脚本 +- 目标: + - 提取共享资产,例如: + - `admin-common.css` + - `admin-common.js` + - 共享导航片段约定 +- 主要文件: + - `deploy/tksea-portal/admin/*.html` + - `deploy/tksea-portal/` + - `scripts/test/test_tksea_portal_assets.sh` +- 输出: + - 减少重复代码和样式漂移 +- 验收: + - 修改一个公共导航或 session 行为,不需要在 4-6 个页面重复改 +- 优先级: + - `Should` + +#### F3-T2 统一错误与状态提示语义 + +- 问题: + - 目前各页虽大体一致,但错误提示和状态栏语气、字段名、回读行为仍有分散实现 +- 目标: + - 统一: + - API 错误展示 + - 登录态提示 + - 成功后回读 + - “当前边界”提示文案 +- 主要文件: + - `deploy/tksea-portal/admin/*.html` + - `docs/2026-05-31-FRONTEND_REVIEW_CHECKLIST.md` +- 输出: + - 页面行为一致,不只视觉一致 +- 验收: + - 管理员切换页面时不会遇到完全不同的反馈模型 +- 优先级: + - `Should` + +### F4. 用户 Portal 闭环补强 + +#### F4-T1 显式暴露“申请 Key”链路依赖状态 + +- 问题: + - 当前用户 portal 的产品层已经较完整,但“申请测试 Key”仍依赖宿主兼容线路 +- 目标: + - 页面上明确区分: + - 逻辑分组产品态 + - 宿主兼容线路依赖态 + - 无法申请时的具体原因 +- 主要文件: + - `deploy/tksea-portal/index.html` + - `internal/app/portal_api.go` + - `docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md` +- 输出: + - 用户端不再只看到“失败”,而能看到失败类型 +- 验收: + - `目录可读但申请不可用` 的场景能被明确解释 +- 优先级: + - `Should` + +#### F4-T2 评估是否继续保留宿主兼容泄漏 + +- 问题: + - portal 已转到 logical-group 产品层,但仍残留部分宿主兼容实现细节 +- 目标: + - 明确哪些宿主字段必须保留,哪些应继续下沉 +- 主要文件: + - `deploy/tksea-portal/index.html` + - `docs/PLUGIN_REQUIREMENTS_OVERVIEW_2026-05-28.md` + - `docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md` +- 输出: + - 用户端产品语言进一步收口 +- 验收: + - 普通用户主视角不再被宿主概念主导 +- 优先级: + - `Could` + +### F5. 发布门禁与维护流程 + +#### F5-T1 前端发布前最小检查标准固化 + +- 问题: + - 现在前端放行标准更多靠执行板记录,不够制度化 +- 目标: + - 形成一条最小前端 release checklist +- 主要文件: + - `docs/DEPLOYMENT.md` + - `docs/REAL_HOST_ACCEPTANCE_CHECKLIST.md` + - `docs/2026-05-31-FRONTEND_REVIEW_CHECKLIST.md` +- 输出: + - 发布前必须勾选: + - 静态资产回归 + - 页面级 acceptance + - 文档同步 +- 验收: + - 前端上线不再只凭“页面 200 + 人工看过”放行 +- 优先级: + - `Must` + +#### F5-T2 执行板记录模板标准化 + +- 问题: + - 当前 `EXECUTION_BOARD.md` 信息丰富,但对前端页面证据没有统一模板 +- 目标: + - 固定每个前端条目的记录结构: + - 页面 + - 动作 + - 接口 + - 真实回读 + - 是否留测试垃圾 +- 主要文件: + - `docs/EXECUTION_BOARD.md` +- 输出: + - 后续前端条目结构统一,利于审计 +- 验收: + - 不再需要从长篇执行板里手工提炼页面闭环 +- 优先级: + - `Should` + +## 建议实施顺序 + +### 第一批:立即做 + +1. `F0-T1` 建立前端 source of truth +2. `F0-T2` 修正文档边界漂移 +3. `F0-T3` 把前端 review 纳入门禁 +4. `F1-T1` 为 `providers.html` 补页面内显式动作 acceptance +5. `F1-T3` 增加最小浏览器级 smoke +6. `F2-T1` 正式声明 `providers.html` 页面边界 +7. `F5-T1` 固化前端发布前最小检查标准 + +### 第二批:紧随其后 + +1. `F1-T2` 把已有历史真验证据统一成可重跑入口 +2. `F2-T2` 决策 provider 运维动作是否进入正式前端 +3. `F3-T1` 抽离共享 admin 资产 +4. `F3-T2` 统一错误与状态提示语义 +5. `F5-T2` 标准化执行板记录模板 + +### 第三批:按产品需要推进 + +1. `F2-T3` 新增 `Provider Operations` 页面 +2. `F4-T1` portal 的“申请 Key”依赖状态显式化 +3. `F4-T2` 继续下沉宿主兼容泄漏 + +## 按迭代排期 + +这部分不是重复任务板,而是把上面的任务压成真正可执行的排期顺序。 + +### 本周必须做 + +#### 1. `F0-T1` 建立前端 source of truth + +- 原因: + - 现在已经有 review/checklist/audit/matrix 四份前端文档,但还没有正式挂到项目统一 truth 入口 +- 目标产物: + - 在 `docs/SOURCE_OF_TRUTH.md` 或等价入口中正式引用前端文档 + - 在 `docs/PROJECT_STRUCTURE.md` 明确 `deploy/tksea-portal/` 是前端真实资产目录 +- 本周完成标准: + - 新人进入仓库后,不会再先去 `web/` 找前端 + +#### 2. `F0-T2` 修正文档边界漂移 + +- 原因: + - 这是前端范围所有争议的根源 +- 目标产物: + - `PRD / EXECUTION_BOARD / PLUGIN_REQUIREMENTS_OVERVIEW` 对前端交付范围给出单一解释 +- 本周完成标准: + - 不再出现“PRD 说不做 Web 控制台,但执行板写多页前端已完成”的冲突表述 + +#### 3. `F0-T3` 把前端 review 纳入项目门禁 + +- 原因: + - 现在最大风险不是“没有页面”,而是前端变更没有正式 gate +- 目标产物: + - `AGENTS.md`、`docs/DEPLOYMENT.md`、`scripts/README.md` 明确前端最小门禁 +- 本周完成标准: + - 任何涉及 `deploy/tksea-portal/` 的改动,不能只跑 Go 门禁就算完成 + +#### 4. `F2-T1` 正式声明 `providers.html` 页面边界 + +- 原因: + - 这是当前 review 里最容易被误解的页面 +- 目标产物: + - 页面文案、总审计文档、动作级矩阵三处口径完全一致 +- 本周完成标准: + - `rollback / reconcile / status / import-batches` 不再被误写成这页已支持的前端动作 + +#### 5. `F1-T1` 为 `providers.html` 补页面内显式动作 acceptance + +- 原因: + - 这是当前最关键的实质性验收缺口 +- 目标产物: + - 至少覆盖: + - 目录加载 + - `preview-import` + - `import` + - draft `save / update / delete / publish` +- 本周完成标准: + - `providers.html` 从“部分闭环但口径模糊”升级到“页面内显式动作有独立 acceptance” + +#### 6. `F1-T3` 增加最小浏览器级 smoke + +- 原因: + - 现在只有静态字符串检查,防不了页面级退化 +- 目标产物: + - 一个轻量 smoke,至少覆盖页面可开、导航可跳、主标题/主动作/结果区存在 +- 本周完成标准: + - 发布前不再只靠 `test_tksea_portal_assets.sh` + +#### 7. `F5-T1` 固化前端发布前最小检查标准 + +- 原因: + - 没有 release checklist,就算补了验收也容易再次失效 +- 目标产物: + - 前端 release checklist +- 本周完成标准: + - 发布前必须显式勾选前端检查项 + +### 下一迭代 + +#### 1. `F1-T2` 把已有历史真验证据统一成可重跑入口 + +- 为什么放下一迭代: + - 这项收益很高,但需要先把本周的门禁和边界收口,否则入口会继续漂移 +- 交付目标: + - `logical-groups / route-health / accounts / portal` 不再主要依赖执行板历史记录判断闭环 + +#### 2. `F2-T2` 决策 provider 运维动作是否进入正式前端 + +- 为什么放下一迭代: + - 这是产品/运维边界决策,不该阻塞本周基础收口 +- 交付目标: + - 明确选择: + - 继续留在脚本/API 面 + - 或新增 `Provider Operations` 视图 + +#### 3. `F3-T1` 抽离共享 admin 资产 + +- 为什么放下一迭代: + - 这是减少重复和长期漂移的工程优化,不是当前最急 gate 问题 +- 交付目标: + - 提取共享 `admin-common.css` / `admin-common.js` 或等价方案 + +#### 4. `F3-T2` 统一错误与状态提示语义 + +- 为什么放下一迭代: + - 先把 acceptance 和边界固定,再统一交互反馈更稳 +- 交付目标: + - 管理页统一错误提示、登录态反馈、成功后回读提示和边界说明 + +#### 5. `F5-T2` 标准化执行板记录模板 + +- 为什么放下一迭代: + - 这项会显著提升后续审计效率,但不阻塞本周修复 +- 交付目标: + - 前端条目在 `EXECUTION_BOARD.md` 中采用统一结构记录 + +#### 6. `F4-T1` 显式暴露用户 portal 的“申请 Key”依赖状态 + +- 为什么放下一迭代: + - 用户 portal 当前已有较强历史闭环证据,优先级低于管理端门禁和 provider 页 +- 交付目标: + - `目录可读但申请不可用` 的场景能在用户页明确解释 + +### 暂缓 + +#### 1. `F2-T3` 新增 `Provider Operations` 页面 + +- 暂缓原因: + - 在 `F2-T2` 做出正式产品决策前,不应直接开做 +- 重启条件: + - 明确决定把 `rollback / reconcile / status / access / import-batches` 做成正式前端能力 + +#### 2. `F4-T2` 继续下沉宿主兼容泄漏 + +- 暂缓原因: + - 这是产品化质量提升项,不是当前最核心的稳定性或验收缺口 +- 重启条件: + - 用户 portal 的依赖状态已显式化,且团队确认要继续推进普通用户产品层收口 + +#### 3. 管理端整体重构或前端框架迁移 + +- 暂缓原因: + - 当前主要问题是门禁、边界、闭环证据,不是技术栈本身 +- 重启条件: + - 完成前端最小门禁和 acceptance 体系后,再评估是否值得重构 + +## 排期建议摘要 + +如果按一个正常迭代节奏推进,最合理的执行顺序是: + +### 本周 + +1. 收口文档口径 +2. 收口 `providers.html` 页面边界 +3. 补 `providers.html` 页面内显式动作 acceptance +4. 加最小浏览器 smoke +5. 固化发布前 front-end checklist + +### 下一迭代 + +1. 把已有历史闭环页收成统一 acceptance 入口 +2. 决策 provider 运维动作是否进入正式前端 +3. 做管理端共享资产和交互一致性整理 +4. 规范执行板记录模板 + +### 后续 + +1. 再决定要不要新增 `Provider Operations` +2. 再推进用户 portal 更彻底的产品层收口 + +## 不建议现在做的事 + +- 不建议现在把整套静态页面重写成 SPA +- 不建议先做大而全的前端框架迁移 +- 不建议先做纯视觉重设计 +- 不建议在页面边界未定之前,把 provider 运维动作继续堆进 `providers.html` + +## 完成标准 + +可认为“前端系统性修复完成”的最小标准是: + +1. 前端范围与页面状态有单一 source of truth +2. 项目门禁里正式出现前端 review / acceptance +3. `providers.html` 的页面内显式动作有独立 acceptance +4. 管理端至少有一层最小浏览器级 smoke +5. `PRD / EXECUTION_BOARD / 审计文档` 对前端范围不再互相冲突 + +## 当前最关键的判断 + +如果只能先做一件事,优先做: + +- `F1-T1` 为 `providers.html` 补页面内显式动作 acceptance + +原因: + +- 这是当前审查里边界最模糊、最容易被误判为“全闭环”的页面 +- 它同时连接导入主链、草稿发布链和后端 provider 运维能力 +- 一旦这页边界与验收收口,整套前端审查口径会立刻稳定很多 diff --git a/docs/2026-05-31-FRONTEND_REVIEW_CHECKLIST.md b/docs/2026-05-31-FRONTEND_REVIEW_CHECKLIST.md new file mode 100644 index 00000000..d6a681bf --- /dev/null +++ b/docs/2026-05-31-FRONTEND_REVIEW_CHECKLIST.md @@ -0,0 +1,354 @@ +# sub2api-cn-relay-manager 前端 Review 清单(2026-05-31) + +日期:2026-05-31 + +## 目的 + +这份清单用于补齐当前仓库缺失的前端专项 review 基线,避免继续出现: + +- 页面已经上线,但没有独立前端验收口径 +- 执行板写“已完成”,但无法快速判断是否真正闭环 +- 前端页面、反代配置、后端接口三者存在漂移却没人显式检查 + +这份文档不是 UI 设计稿,也不是测试报告;它是**前端审查与验收入口**。 + +## 审查范围 + +当前仓库内实际存在的前端资产不在 `web/`,而在 `deploy/tksea-portal/`: + +- `deploy/tksea-portal/index.html` +- `deploy/tksea-portal/admin/index.html` +- `deploy/tksea-portal/admin/logical-groups.html` +- `deploy/tksea-portal/admin/route-health.html` +- `deploy/tksea-portal/admin/accounts.html` +- `deploy/tksea-portal/admin/providers.html` +- `deploy/tksea-portal/admin-batch-import.html` +- `deploy/tksea-portal/admin/batch-import.html` +- `deploy/tksea-portal/nginx.sub.tksea.top.conf.example` + +## 当前事实 + +### 已有 + +- 有静态资产一致性检查:`scripts/test/test_tksea_portal_assets.sh` +- 有部分远端 acceptance 脚本: + - `scripts/acceptance/verify_route_health_ui.sh` + - `scripts/acceptance/verify_route_control_plane.sh` + - `scripts/acceptance/verify_route_data_plane.sh` + - `scripts/acceptance/verify_route_acceptance_matrix.sh` +- 管理端页面普遍采用相同入口: + - `/portal-admin-api/` 访问 CRM 管理接口 + - `/api/admin/session` 登录态 + +### 当前缺口 + +- 没有前端 lint / build / browser E2E 门禁 +- 没有逐页“页面 -> API -> 闭环结果 -> 证据”台账 +- 没有一个统一文档说明哪些页面只是静态存在,哪些页面已经过真实在线闭环 +- 用户 portal 与 admin portal 共享部署资产,但不共享组件体系,后续 UI 漂移风险高 + +## 闭环判定标准 + +每个页面都按同一口径判定,不允许只因为“页面能打开”就标记完成。 + +### A. 静态存在 + +- HTML 文件存在 +- 入口链接可达 +- 页面内引用的核心 API 前缀存在 + +### B. 接口对齐 + +- 页面实际调用的 API 在服务端已注册 +- 关键写操作不是伪按钮或本地假数据 +- 页面文案没有夸大后端尚未提供的能力 + +### C. 运行闭环 + +- 登录 / 鉴权通过 +- 请求可从浏览器发到反代入口 +- 关键读操作有真实返回 +- 关键写操作有真实落库或真实副作用 +- 写后可回读 + +### D. UI 一致性 + +- 管理页导航一致 +- 登录态交互一致 +- 状态栏、错误提示、主按钮语义一致 +- 页面不是孤立“拼装页”而能回到统一管理入口 + +只有 A+B 成立,最多算“前后端已接线”。 +只有 A+B+C 成立,才算“功能闭环”。 +A+B+C+D 成立,才算“可认为产品化完成”。 + +## 跨页面公共检查项 + +每次前端 review 必查以下 10 项: + +1. `nginx.sub.tksea.top.conf.example` 中是否同时保留 `/portal/`、`/portal-proxy/`、`/portal-admin-api/` +2. 管理页是否都优先支持管理员 session,而不是只靠手填 token +3. 页面中 API Base 默认值是否仍然指向同域前缀,而不是硬编码远端机器地址 +4. 页面报错时是否给出可读错误,而不是只在控制台抛异常 +5. 页面写操作后是否自动回读,而不是只提示“成功” +6. 页面是否显式说明当前边界,避免前端能力大于后端真实能力 +7. 页面导航是否能互相到达,不出现孤岛页 +8. 页面是否保留旧地址兼容策略 +9. 页面是否把“只改插件状态”与“会改宿主资源”区分清楚 +10. 页面依赖的后端 action 是否允许为空;如果允许,是否有清晰降级提示 + +## 页面级审查矩阵 + +### 1. 用户 portal + +- 页面:`deploy/tksea-portal/index.html` +- 主要用途:普通用户查看产品目录、权限、订阅、历史 key,并申请兼容 key +- 关键前缀: + - `/portal-proxy/api/v1` + - `/portal-admin-api/api/portal` +- 关键读接口: + - `/api/portal/logical-groups` + - `/api/portal/logical-groups/{group_id}` + - `/api/portal/logical-groups/{group_id}/models` + - 宿主侧 `/auth/me` + - 宿主侧 `/groups/available` + - 宿主侧 `/subscriptions` + - 宿主侧 `/keys` +- 必查项: + - 未登录时是否能明确看到“目录可读 / 权限不可读”的状态差异 + - 登录后是否能同时拉到逻辑分组目录和用户自身订阅 + - “新创建 key 对应哪个逻辑分组 / 模型”是否在页面上可见 + - 目录为空时是否降级到清晰提示,而不是空白页 +- 闭环判定: + - 目录可读 + - 用户态接口可读 + - 创建或展示 key 后能回读 +- 当前仓库级判断: + - 已接线 + - 是否闭环依赖 `portal-proxy`、`portal-admin-api` 和宿主用户态 API 联动,必须在线真验 + +### 2. 管理首页 + +- 页面:`deploy/tksea-portal/admin/index.html` +- 主要用途:统一入口,不承载复杂写操作 +- 关键检查: + - 导航是否覆盖逻辑分组、route health、accounts、providers、batch import + - 页面文案是否仍准确反映当前安全边界 + - 旧入口是否仍可跳到新入口 +- 闭环判定: + - 导航全可达 + - 管理态登录可用 +- 当前仓库级判断: + - 结构存在 + - 更像入口页,不是单独业务闭环页 + +### 3. Logical Group Admin + +- 页面:`deploy/tksea-portal/admin/logical-groups.html` +- 关键接口: + - `POST /api/admin/session/login` + - `GET /api/admin/session` + - `POST /api/logical-groups` + - `GET /api/logical-groups` + - `PUT /api/logical-groups/{group_id}` + - `DELETE /api/logical-groups/{group_id}` + - `POST /api/logical-groups/{group_id}/models` + - `GET /api/logical-groups/{group_id}/models` + - `DELETE /api/logical-groups/{group_id}/models/{model}` + - `POST /api/logical-groups/{group_id}/routes` + - `GET /api/logical-groups/{group_id}/routes` + - `PUT /api/logical-groups/{group_id}/routes/{route_id}` + - `DELETE /api/logical-groups/{group_id}/routes/{route_id}` + - `POST /api/logical-groups/{group_id}/routes/{route_id}/models` + - `GET /api/logical-groups/{group_id}/routes/{route_id}/models` +- 必查项: + - 页面是否仍明确声明“route model 首版只支持新增与查看” + - group / route / route model 三层数据能否顺序创建并回读 + - 删除 group 前是否对关联 route 有清晰反馈 + - 编辑 route 后 `shadow_group_id / shadow_host_id` 是否立即反映 +- 闭环判定: + - `logical_group -> route -> route_model` 至少完成一轮 create + read back +- 当前仓库级判断: + - 前后端接口对齐较好 + - 需要远端真验确认删改流程和错误态 + +### 4. Route Health Admin + +- 页面:`deploy/tksea-portal/admin/route-health.html` +- 关键接口: + - `GET /api/admin/session` + - `POST /api/admin/session/login` + - `GET /api/routing/routes/health` +- 必查项: + - 筛选参数是否真正影响后端查询 + - `healthy / cooldown / failing / disabled` 汇总是否和返回数据一致 + - route 详情是否反映 sticky / failover / cooldown 的真实字段 + - 空结果、鉴权失败、后端超时是否都有页面级提示 +- 闭环判定: + - 能登录 + - 能过滤 + - 能稳定看到 route 健康列表和详情 +- 当前仓库级判断: + - 有脚本化验收入口 + - 是当前最接近“可独立验收”的前端页之一 + +### 5. Provider Accounts Admin + +- 页面:`deploy/tksea-portal/admin/accounts.html` +- 关键接口: + - `GET /api/provider-accounts` + - `GET /api/provider-accounts/{account_id}/binding-candidates` + - `POST /api/provider-accounts/{account_id}/binding` + - `POST /api/provider-accounts/{account_id}/enable` + - `POST /api/provider-accounts/{account_id}/disable` + - `POST /api/provider-accounts/{account_id}/retire` +- 必查项: + - 页面是否持续明确“只修改插件 provider_accounts 库存状态” + - 列表过滤是否真的作用于后端而不是前端本地过滤 + - 选中帐号后,binding 候选与详情是否一致 + - enable / disable / retire 后,状态是否能立即回读 + - clear binding 与 assign binding 是否都能真实落库 +- 闭环判定: + - 一条帐号完成 disable -> enable 或 assign -> clear -> read back +- 当前仓库级判断: + - 页面边界表达相对诚实 + - 是运营闭环重要页面,必须补真实在线回归 + +### 6. Provider Admin + +- 页面:`deploy/tksea-portal/admin/providers.html` +- 页面内显式接口: + - `GET /api/packs` + - `GET /api/hosts` + - `GET /api/packs/{pack_id}/providers` + - `POST /api/providers/{provider_id}/preview-import` + - `POST /api/providers/{provider_id}/import` + - `GET /api/provider-drafts` + - `POST /api/provider-drafts` + - `PUT /api/provider-drafts/{draft_id}` + - `DELETE /api/provider-drafts/{draft_id}` + - `POST /api/provider-drafts/{draft_id}/publish` +- 后端相邻能力,但当前不属于页面显式动作: + - `POST /api/providers/{provider_id}/rollback` + - `POST /api/providers/{provider_id}/reconcile` + - `GET /api/providers/{provider_id}/status` + - `GET /api/providers/{provider_id}/access/status` + - `GET /api/providers/{provider_id}/import-batches` +- 必查项: + - pack / host / provider 目录能否顺序加载 + - preview-import 与 import 的前置校验是否一致 + - 草稿 save / update / delete / publish 是否都可回读 + - publish 后 commit 结果是否展示给操作者 + - 当前页面是否仍清楚区分“新增 provider”与“导入 provider account” +- 闭环判定: + - 至少完成一次 draft save -> update -> publish -> 回读结果 +- 当前仓库级判断: + - 页面内显式动作现已存在独立 acceptance 入口:`scripts/acceptance/verify_provider_admin_actions.sh` + - 后端 provider 运维能力仍需单独决策是否进入正式前端 + +### 7. Batch Import Admin + +- 页面: + - `deploy/tksea-portal/admin-batch-import.html` + - `deploy/tksea-portal/admin/batch-import.html` +- 页面语义: + - `admin/batch-import.html` 当前只是跳转兼容页 + - 真正承载功能的是 `admin-batch-import.html` +- 关键接口: + - `POST /api/admin/session/login` + - `GET /api/admin/session` + - `POST /api/batch-import/runs` + - `GET /api/batch-import/runs/{run_id}` + - `GET /api/batch-import/runs/{run_id}/items` +- 必查项: + - 新旧入口跳转是否稳定 + - 发起 run 后,run 状态、items 列表、`matched_account_state / account_resolution / provision_reused` 是否都能回读 + - 失败 run 是否有可读错误,而不是只显示空结果 +- 闭环判定: + - 发起一次 batch run,并能回读 run 与 item 结果 +- 当前仓库级判断: + - 闭环目标清楚 + - 新旧地址并存,回归时必须同时检查 + +## 建议的 review 顺序 + +按风险和闭环价值排序,建议每次 review 按这个顺序执行: + +1. Nginx 反代配置检查 +2. 管理员 session 登录检查 +3. 管理首页导航检查 +4. Logical Group Admin +5. Route Health Admin +6. Provider Accounts Admin +7. Provider Admin +8. Batch Import Admin +9. 用户 portal + +原因: + +- 先检查反代和 session,后面的页面才有审查价值 +- 先看控制面最基础的 group / route,再看观测页、库存页、导入页 +- 用户 portal 依赖最多,放到最后最省排障时间 + +## 最小执行命令集 + +本地静态一致性: + +```bash +bash ./scripts/test/test_tksea_portal_assets.sh +``` + +route 健康页 acceptance: + +```bash +bash ./scripts/acceptance/verify_route_health_ui.sh +``` + +控制面 acceptance: + +```bash +bash ./scripts/acceptance/verify_route_control_plane.sh +``` + +数据面 acceptance: + +```bash +bash ./scripts/acceptance/verify_route_data_plane.sh +``` + +矩阵式 acceptance: + +```bash +bash ./scripts/acceptance/verify_route_acceptance_matrix.sh +``` + +Provider Admin 页面显式动作 acceptance: + +```bash +bash ./scripts/acceptance/verify_provider_admin_actions.sh +``` + +## Review 结论模板 + +每次前端 review 完成后,至少输出以下字段: + +- 审查日期 +- 审查人 +- 审查环境 +- Nginx 反代是否正确 +- 管理态登录是否可用 +- 页面清单 +- 每页状态: + - 静态存在 / 已接线 / 已闭环 / 有缺口 +- 关键缺口列表 +- 是否阻塞发布 +- 证据链接或命令输出位置 + +## 当前建议结论 + +基于 2026-05-31 的仓库审查,当前最合理的统一口径是: + +- 管理端前端**不是空白状态**,已经有多页真实静态资产和后端接口接线 +- 但仓库**还没有形成前端专项验收体系** +- 因此当前不能只凭执行板就断言“已完成前端功能全部正常” +- 后续凡是声称“前端已完成”的条目,都应至少引用这份清单中的页面级闭环证据 diff --git a/docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md b/docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md new file mode 100644 index 00000000..d77e930a --- /dev/null +++ b/docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md @@ -0,0 +1,269 @@ +# providers.html 动作级 Acceptance 矩阵(2026-05-31) + +日期:2026-05-31 + +## 目的 + +这份矩阵专门回答一个此前被混在一起的问题: + +- `providers.html` 当前到底承载了哪些动作 +- 这些动作里哪些已经有历史真验证据 +- 哪些只是后端有接口,但页面并没有显式做成 UI +- 哪些还缺独立页面级 acceptance + +本矩阵只针对 `deploy/tksea-portal/admin/providers.html`。 + +## 先给结论 + +`providers.html` 当前真正显式承载的动作只有两类: + +1. provider 目录 + `preview-import` / `import` +2. provider draft 的 `save / update / delete / publish` + +**它并没有显式承载 `rollback` 或 `reconcile` 按钮。** + +所以: + +- 不能把后端存在 `POST /api/providers/{providerID}/rollback` / `POST /api/providers/{providerID}/reconcile`,直接说成“providers.html 已完成这两个前端动作闭环” +- 更准确的说法是: + - `preview-import / import`:页面显式承载 + - `rollback / reconcile`:后端已提供,real-host acceptance 也有证据,但当前**不属于这页的显式 UI 能力** + +## 页面实际承载范围 + +从 `providers.html` 当前实现看,页面明确包含: + +- 管理员 session 登录 +- pack / host / provider 目录加载 +- `preview-import` +- `import` +- manifest draft 生成 +- 草稿保存到服务端 +- 草稿更新 +- 草稿删除 +- 草稿发布到仓库 + +页面未显式提供: + +- `rollback` +- `reconcile` +- `provider status` +- `provider resources` +- `access status` +- `import-batches` 历史列表 + +这些能力虽然在后端存在,但不是当前 `providers.html` 的显式按钮或结果区。 + +## 动作级矩阵 + +| 动作 | 后端接口 | 当前是否在 `providers.html` 显式承载 | 当前证据强度 | 审计结论 | +| --- | --- | --- | --- | --- | +| 加载 pack / host / provider 目录 | `GET /api/packs` `GET /api/hosts` `GET /api/packs/{pack_id}/providers` | 是 | 代码接线 + 静态回归 | 已接线,缺独立远端页面级矩阵 | +| preview-import | `POST /api/providers/{provider_id}/preview-import` | 是 | 页面接线 + real-host acceptance API 证据 | 部分闭环 | +| import | `POST /api/providers/{provider_id}/import` | 是 | 页面接线 + real-host acceptance 强证据 | 部分闭环 | +| draft save | `POST /api/provider-drafts` | 是 | 执行板 + 页面接线 | 历史已闭环 | +| draft update | `PUT /api/provider-drafts/{draft_id}` | 是 | 执行板 + 页面接线 | 历史已闭环 | +| draft delete | `DELETE /api/provider-drafts/{draft_id}` | 是 | 执行板 + 页面接线 | 历史已闭环 | +| draft publish | `POST /api/provider-drafts/{draft_id}/publish` | 是 | 执行板远端真验强证据 | 历史已闭环 | +| rollback | `POST /api/providers/{provider_id}/rollback` | 否 | 后端路由 + 单测 | 不属于当前页面显式能力 | +| reconcile | `POST /api/providers/{provider_id}/reconcile` | 否 | 后端路由 + real-host acceptance API 证据 | 不属于当前页面显式能力 | +| provider status | `GET /api/providers/{provider_id}/status` | 否 | 后端路由 + acceptance 证据 | 不属于当前页面显式能力 | +| access status | `GET /api/providers/{provider_id}/access/status` | 否 | 后端路由 + acceptance 证据 | 不属于当前页面显式能力 | +| import-batches | `GET /api/providers/{provider_id}/import-batches` | 否 | 后端路由 + OpenAPI | 不属于当前页面显式能力 | + +## 逐项审计 + +### 1. 目录加载 + +- 页面证据: + - `GET /api/packs` + - `GET /api/hosts` + - `GET /api/packs/{pack_id}/providers` +- 当前结论: + - 页面层面接线明确 + - `test_tksea_portal_assets.sh` 只证明页面引用了这些入口,不证明远端目录加载全过程持续可用 +- 状态: + - `已接线` + +### 2. `preview-import` + +- 页面证据: + - 页面有“预检导入”按钮 + - 直接调用 `POST /api/providers/{provider_id}/preview-import` + - preview 结果区直接展示原始 JSON +- 后端证据: + - 路由已注册 + - OpenAPI 已声明 + - `real_host_acceptance.sh` 会固定落盘 `04-preview-import.json` +- 当前结论: + - 这条动作在 API 层有真实宿主证据 + - 但仓库里没有一份专门说明“通过 `providers.html` 点击预检导入完成远端验证”的独立页面级证据 +- 状态: + - `部分闭环` + +### 3. `import` + +- 页面证据: + - 页面有“执行导入”按钮 + - 直接调用 `POST /api/providers/{provider_id}/import` + - import 结果区直接展示原始 JSON +- 后端证据: + - 路由已注册 + - OpenAPI 已声明 + - `real_host_acceptance.sh` 固定落盘 `05-import.json` + - `import_remote43_provider.sh` 有 remote43 定向验收链 +- 当前结论: + - import 主链路本身证据很强 + - 但 `providers.html` 页面级 acceptance 仍与 API acceptance 混在一起,未形成动作级单页矩阵 +- 状态: + - `部分闭环` + +### 4. draft save / update / delete + +- 页面证据: + - 页面提供按钮并直接调用: + - `POST /api/provider-drafts` + - `PUT /api/provider-drafts/{draft_id}` + - `DELETE /api/provider-drafts/{draft_id}` +- 执行板证据: + - 已明确记录: + - 草稿落到 CRM SQLite + - `providers.html` 可保存、回看、更新、删除服务端草稿 +- 当前结论: + - 这些动作已经属于页面显式能力,且历史上已被写成真实交付范围 +- 状态: + - `历史已闭环` + +### 5. draft publish + +- 页面证据: + - 页面提供“发布到仓库”按钮 + - 调用 `POST /api/provider-drafts/{draft_id}/publish` +- 执行板证据: + - remote43 公网真验已记录: + - `draft_id` + - `provider_id` + - `provider_path` + - `pack_version` bump + - `commit_sha` + - 远端 repo `HEAD` 与 API 返回 `commit_sha` 一致 +- 当前结论: + - 这是 `providers.html` 当前证据最强的动作之一 + - 可认为历史上已形成页面驱动的完整闭环 +- 状态: + - `历史已闭环` + +### 6. `rollback` + +- 页面证据: + - 当前 `providers.html` 没有 `rollback` 按钮,也没有结果区对应 `rollback` +- 后端证据: + - 路由已注册:`POST /api/providers/{provider_id}/rollback` + - OpenAPI 已声明 + - `app_test.go` 有 API handler 测试 +- 注意: + - 当前 real-host 主脚本真实验证的是 `POST /api/import-batches/{batch_id}/rollback` + - 这证明“回滚链路存在”,但不是“`providers.html` 已显式支持 provider rollback” +- 当前结论: + - 不能把它算作 `providers.html` 已闭环动作 + - 它属于“后端存在,但页面未显式承载” +- 状态: + - `不属于当前页面显式能力` + +### 7. `reconcile` + +- 页面证据: + - 当前 `providers.html` 没有 `reconcile` 按钮,也没有结果区对应 `reconcile` +- 后端证据: + - 路由已注册:`POST /api/providers/{provider_id}/reconcile` + - OpenAPI 已声明 + - `real_host_acceptance.sh` 固定落盘 `09-reconcile.json` + - runbook 明确把 `reconcile` 视为真实宿主验收链一环 +- 当前结论: + - API 层与 real-host 层证据都存在 + - 但这仍然不能推导出“当前 `providers.html` 已做 reconcile 前端闭环” +- 状态: + - `不属于当前页面显式能力` + +## 为什么 `providers.html` 当前只能判为“部分闭环” + +因为这页混合了承担两类完全不同的事情: + +1. provider 引导与导入前置动作 +2. provider draft 的仓库发布动作 + +其中: + +- draft 发布链已经有很强的远端真验证据 +- `preview-import / import` 有真实宿主 API 证据,但缺单页动作级 acceptance +- `rollback / reconcile` 根本不是当前页面显式 UI + +因此把整页粗暴标成“已闭环”会夸大页面能力;标成“未闭环”又会抹掉 draft publish 这条已经很扎实的链。 + +最准确的结论只能是: + +- `providers.html`:`部分闭环` + +当前仓库已新增专用脚本入口: + +```bash +bash ./scripts/acceptance/verify_provider_admin_actions.sh +``` + +它的职责是把页面内显式动作收成可重复执行的 acceptance;是否在某个具体环境真正跑成“全部通过”,仍取决于该环境的 host、provider key、repo root 与发布权限配置。 + +## 建议的下一步验收拆法 + +如果要把 `providers.html` 继续收口成真正的动作级 acceptance,建议拆成两组: + +### A. 页面内显式动作 + +必须补页面级 acceptance 的动作: + +1. 加载目录 +2. `preview-import` +3. `import` +4. draft `save` +5. draft `update` +6. draft `delete` +7. draft `publish` + +这组动作可以直接定义为“`providers.html` 页面 acceptance”。 + +### B. 后端 provider 运维动作 + +当前不应强行算到 `providers.html` 的动作: + +1. `rollback` +2. `reconcile` +3. `provider status` +4. `access status` +5. `import-batches` + +这组动作有两条路: + +- 要么新增一个 “Provider Operations” 视图 +- 要么明确归到脚本/API 运维面,而不是当前前端页 + +## 推荐的补强顺序 + +如果继续做,优先级建议是: + +1. 先给 `providers.html` 补页面内显式动作的 acceptance 记录 +2. 再决定要不要把 `rollback / reconcile / status / import-batches` 真正做进一个可视化运维页 + +原因很简单: + +- 现在的主要问题不是后端没有能力 +- 而是“这页到底承载什么”没有被正式写清楚 +- 先把边界写清,再补页面级 acceptance,性价比最高 + +## 审计口径结论 + +截至 2026-05-31,针对 `providers.html` 最准确的口径是: + +- 页面内显式动作: + - `preview-import / import`:已接线,且有真实宿主 API 证据,但仍缺单页动作级 acceptance + - `draft save / update / delete / publish`:历史闭环证据充分,其中 `publish` 证据最强 +- 页面外后端动作: + - `rollback / reconcile / status / access / import-batches`:后端已提供,但当前不应宣称为 `providers.html` 已支持的前端闭环能力 diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index d3501570..4e6677da 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -55,6 +55,7 @@ SUB2API_CRM_ADMIN_TOKEN=change-me-before-production SUB2API_CRM_LISTEN_ADDR=127. - Nginx 示例:`deploy/tksea-portal/nginx.sub.tksea.top.conf.example` - 部署脚本:`scripts/deploy/deploy_tksea_portal.sh` - 资产回归:`scripts/test/test_tksea_portal_assets.sh` +- Provider Admin 页面验收:`scripts/acceptance/verify_provider_admin_actions.sh` 当前正式入口: @@ -104,6 +105,24 @@ SUB2API_CRM_ADMIN_TOKEN=change-me-before-production SUB2API_CRM_LISTEN_ADDR=127. - `DELETE /api/provider-drafts/{draft_id}` - `POST /api/provider-drafts/{draft_id}/publish` +`providers.html` 页面内显式动作当前推荐独立验收: + +- `GET /api/packs` +- `GET /api/hosts` +- `GET /api/packs/{pack_id}/providers` +- `POST /api/providers/{provider_id}/preview-import` +- `POST /api/providers/{provider_id}/import` +- `POST /api/provider-drafts` +- `PUT /api/provider-drafts/{draft_id}` +- `DELETE /api/provider-drafts/{draft_id}` +- `POST /api/provider-drafts/{draft_id}/publish` + +对应脚本: + +```bash +bash ./scripts/acceptance/verify_provider_admin_actions.sh +``` + `publish` 的运行前提: - CRM 进程必须配置 `SUB2API_CRM_REPO_ROOT` diff --git a/scripts/README.md b/scripts/README.md index e5d16c67..399e2ec1 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -20,6 +20,7 @@ - 例如: - `real_host_acceptance.sh` - `import_remote43_provider.sh` + - `verify_provider_admin_actions.sh` - `check_deepseek_completion_split.sh` - `scripts/test/` - 脚本自身的回归与资产检查 @@ -42,6 +43,7 @@ bash ./scripts/test/test_tksea_portal_assets.sh bash ./scripts/test/verify_quality_gates.sh scripts/deploy/build_local_image.sh bash ./scripts/acceptance/real_host_acceptance.sh +bash ./scripts/acceptance/verify_provider_admin_actions.sh ``` ## 统一质量门禁 diff --git a/scripts/acceptance/verify_provider_admin_actions.sh b/scripts/acceptance/verify_provider_admin_actions.sh new file mode 100755 index 00000000..003ced44 --- /dev/null +++ b/scripts/acceptance/verify_provider_admin_actions.sh @@ -0,0 +1,378 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck disable=SC1091 +source "$ROOT_DIR/scripts/acceptance/route_acceptance_lib.sh" + +PROVIDER_ADMIN_ROOT="${PROVIDER_ADMIN_ROOT:-$ROOT_DIR/artifacts/provider-admin-matrix}" +PROVIDER_ADMIN_PAGE_URL="${PROVIDER_ADMIN_PAGE_URL:-https://sub.tksea.top/portal/admin/providers.html}" +TS="${TS:-$(timestamp_token)}" +ARTIFACT_DIR="${ARTIFACT_DIR:-$PROVIDER_ADMIN_ROOT/${TS}_provider_admin_actions}" + +PACK_ID="${PACK_ID:-}" +HOST_ID="${HOST_ID:-}" +PACK_PATH="${PACK_PATH:-}" +PROVIDER_ID="${PROVIDER_ID:-}" +MODE="${MODE:-partial}" +ACCESS_MODE="${ACCESS_MODE:-self_service}" +ACCESS_API_KEY="${ACCESS_API_KEY:-}" +PROVIDER_KEYS="${PROVIDER_KEYS:-}" +SUBSCRIPTION_USERS="${SUBSCRIPTION_USERS:-}" +SUBSCRIPTION_DAYS="${SUBSCRIPTION_DAYS:-30}" +VERIFY_PUBLISH="${VERIFY_PUBLISH:-0}" + +crud_provider_id="${CRUD_PROVIDER_ID:-provider-admin-draft-$TS}" +crud_display_name="${CRUD_DISPLAY_NAME:-Provider Admin Draft $TS}" +crud_platform="${CRUD_PLATFORM:-openai}" +crud_base_url="${CRUD_BASE_URL:-https://draft-$TS.example.com/v1}" +crud_smoke_model="${CRUD_SMOKE_MODEL:-provider-admin-draft-$TS}" +crud_supported_models="${CRUD_SUPPORTED_MODELS:-provider-admin-draft-$TS,provider-admin-draft-mini-$TS}" +crud_notes="${CRUD_NOTES:-provider-admin draft acceptance $TS}" +crud_updated_display_name="${CRUD_UPDATED_DISPLAY_NAME:-Provider Admin Draft Updated $TS}" +crud_updated_base_url="${CRUD_UPDATED_BASE_URL:-https://draft-updated-$TS.example.com/v1}" +crud_updated_smoke_model="${CRUD_UPDATED_SMOKE_MODEL:-provider-admin-draft-updated-$TS}" +crud_updated_supported_models="${CRUD_UPDATED_SUPPORTED_MODELS:-provider-admin-draft-updated-$TS}" +crud_updated_notes="${CRUD_UPDATED_NOTES:-provider-admin draft acceptance updated $TS}" + +publish_provider_id="${PUBLISH_PROVIDER_ID:-provider-admin-publish-$TS}" +publish_display_name="${PUBLISH_DISPLAY_NAME:-Provider Admin Publish $TS}" +publish_platform="${PUBLISH_PLATFORM:-openai}" +publish_base_url="${PUBLISH_BASE_URL:-https://publish-$TS.example.com/v1}" +publish_smoke_model="${PUBLISH_SMOKE_MODEL:-provider-admin-publish-$TS}" +publish_supported_models="${PUBLISH_SUPPORTED_MODELS:-provider-admin-publish-$TS}" +publish_notes="${PUBLISH_NOTES:-provider-admin publish acceptance $TS}" +publish_commit_message="${PUBLISH_COMMIT_MESSAGE:-feat(pack): publish provider admin draft $publish_provider_id}" + +require_var CRM_BASE +require_var ACCESS_API_KEY +require_var PROVIDER_KEYS + +crm_auth_init +ensure_artifact_dir +curl_status_to_file "$PROVIDER_ADMIN_PAGE_URL" "$ARTIFACT_DIR/00-provider-admin.html" + +json_field_from_file() { + local file="$1" + local path="$2" + python3 - "$file" "$path" <<'PY' +import json +import sys + +file_path, path = sys.argv[1:3] +value = json.load(open(file_path, "r", encoding="utf-8")) +for part in path.split("."): + if isinstance(value, dict): + value = value.get(part) + else: + value = None + break +if value is None: + raise SystemExit(2) +if isinstance(value, (dict, list)): + print(json.dumps(value, ensure_ascii=False)) +else: + print(value) +PY +} + +first_collection_field_from_file() { + local file="$1" + local collection="$2" + local field="$3" + python3 - "$file" "$collection" "$field" <<'PY' +import json +import sys + +file_path, collection, field = sys.argv[1:4] +payload = json.load(open(file_path, "r", encoding="utf-8")) +items = payload.get(collection) or [] +if not items: + raise SystemExit(2) +value = items[0].get(field) +if value in ("", None): + raise SystemExit(2) +print(value) +PY +} + +normalize_csv_json() { + local raw="$1" + python3 - "$raw" <<'PY' +import json +import sys + +raw = sys.argv[1] +values = [] +for line in raw.replace("\r", "\n").split("\n"): + for item in line.split(","): + item = item.strip() + if item: + values.append(item) +print(json.dumps(values, ensure_ascii=False)) +PY +} + +build_preview_payload() { + local host_id="$1" + local pack_path="$2" + local provider_id="$3" + local mode="$4" + local keys_json="$5" + python3 - "$host_id" "$pack_path" "$provider_id" "$mode" "$keys_json" <<'PY' +import json +import sys + +host_id, pack_path, provider_id, mode, keys_json = sys.argv[1:6] +print(json.dumps({ + "host_id": host_id, + "pack_path": pack_path, + "provider_id": provider_id, + "keys": json.loads(keys_json), + "mode": mode, +}, ensure_ascii=False)) +PY +} + +build_import_payload() { + local host_id="$1" + local pack_path="$2" + local provider_id="$3" + local mode="$4" + local access_mode="$5" + local access_api_key="$6" + local keys_json="$7" + local subscription_users_json="$8" + local subscription_days="$9" + python3 - "$host_id" "$pack_path" "$provider_id" "$mode" "$access_mode" "$access_api_key" "$keys_json" "$subscription_users_json" "$subscription_days" <<'PY' +import json +import sys + +host_id, pack_path, provider_id, mode, access_mode, access_api_key, keys_json, subscription_users_json, subscription_days = sys.argv[1:10] +payload = { + "host_id": host_id, + "pack_path": pack_path, + "provider_id": provider_id, + "keys": json.loads(keys_json), + "mode": mode, + "access_mode": access_mode, + "access_api_key": access_api_key, +} +if access_mode == "subscription": + payload["subscription_users"] = json.loads(subscription_users_json) + payload["subscription_days"] = int(subscription_days) +print(json.dumps(payload, ensure_ascii=False)) +PY +} + +build_draft_payload() { + local pack_id="$1" + local provider_id="$2" + local display_name="$3" + local platform="$4" + local base_url="$5" + local smoke_model="$6" + local supported_models_json="$7" + local source_host_id="$8" + local notes="$9" + python3 - "$pack_id" "$provider_id" "$display_name" "$platform" "$base_url" "$smoke_model" "$supported_models_json" "$source_host_id" "$notes" <<'PY' +import json +import sys + +pack_id, provider_id, display_name, platform, base_url, smoke_model, supported_models_json, source_host_id, notes = sys.argv[1:10] +supported_models = json.loads(supported_models_json) +manifest = { + "provider_id": provider_id, + "display_name": display_name, + "platform": platform, + "base_url": base_url, + "smoke_test_model": smoke_model, + "supported_models": supported_models, +} +print(json.dumps({ + "pack_id": pack_id, + "provider_id": provider_id, + "display_name": display_name, + "platform": platform, + "base_url": base_url, + "smoke_test_model": smoke_model, + "supported_models": supported_models, + "source_host_id": source_host_id, + "notes": notes, + "manifest": manifest, +}, ensure_ascii=False)) +PY +} + +crm_curl_status() { + local method="$1" + local path="$2" + local payload="${3:-}" + local -a curl_args + curl_args=(-fsS -o /dev/null -w '%{http_code}' -X "$method") + if [[ -n "${crm_token:-}" ]]; then + curl_args+=(-H "Authorization: Bearer $crm_token") + elif [[ -n "${crm_cookie_jar:-}" ]]; then + curl_args+=(-b "$crm_cookie_jar" -c "$crm_cookie_jar") + else + echo "missing CRM auth: set CRM_ADMIN_TOKEN or CRM_ADMIN_USERNAME/CRM_ADMIN_PASSWORD" >&2 + exit 2 + fi + if [[ -n "$payload" ]]; then + curl_args+=(-H 'Content-Type: application/json' "${CRM_BASE%/}${path}" -d "$payload") + else + curl_args+=("${CRM_BASE%/}${path}") + fi + curl "${curl_args[@]}" +} + +provider_keys_json="$(normalize_csv_json "$PROVIDER_KEYS")" +subscription_users_json="$(normalize_csv_json "$SUBSCRIPTION_USERS")" +crud_supported_models_json="$(normalize_csv_json "$crud_supported_models")" +crud_updated_supported_models_json="$(normalize_csv_json "$crud_updated_supported_models")" +publish_supported_models_json="$(normalize_csv_json "$publish_supported_models")" + +save_json 01-packs "$(crm_curl_json GET "/api/packs")" +if [[ -z "$PACK_ID" ]]; then + PACK_ID="$(first_collection_field_from_file "$ARTIFACT_DIR/01-packs.json" packs pack_id)" +fi + +save_json 02-hosts "$(crm_curl_json GET "/api/hosts")" +if [[ -z "$HOST_ID" ]]; then + HOST_ID="$(first_collection_field_from_file "$ARTIFACT_DIR/02-hosts.json" hosts host_id)" +fi + +if [[ -z "$PACK_PATH" ]]; then + PACK_PATH="/app/packs/$PACK_ID" +fi + +save_json 03-pack-providers "$(crm_curl_json GET "/api/packs/$PACK_ID/providers")" +if [[ -z "$PROVIDER_ID" ]]; then + PROVIDER_ID="$(first_collection_field_from_file "$ARTIFACT_DIR/03-pack-providers.json" providers provider_id)" +fi + +preview_payload="$(build_preview_payload "$HOST_ID" "$PACK_PATH" "$PROVIDER_ID" "$MODE" "$provider_keys_json")" +save_json 04-preview-import "$(crm_curl_json POST "/api/providers/$PROVIDER_ID/preview-import" "$preview_payload")" + +import_payload="$(build_import_payload "$HOST_ID" "$PACK_PATH" "$PROVIDER_ID" "$MODE" "$ACCESS_MODE" "$ACCESS_API_KEY" "$provider_keys_json" "$subscription_users_json" "$SUBSCRIPTION_DAYS")" +save_json 05-import "$(crm_curl_json POST "/api/providers/$PROVIDER_ID/import" "$import_payload")" + +crud_create_payload="$(build_draft_payload "$PACK_ID" "$crud_provider_id" "$crud_display_name" "$crud_platform" "$crud_base_url" "$crud_smoke_model" "$crud_supported_models_json" "$HOST_ID" "$crud_notes")" +save_json 06-create-draft "$(crm_curl_json POST "/api/provider-drafts" "$crud_create_payload")" +crud_draft_id="$(json_field_from_file "$ARTIFACT_DIR/06-create-draft.json" draft.draft_id)" + +save_json 07-list-drafts-before-delete "$(crm_curl_json GET "/api/provider-drafts?pack_id=$PACK_ID")" +save_json 08-get-draft "$(crm_curl_json GET "/api/provider-drafts/$crud_draft_id")" + +crud_update_payload="$(build_draft_payload "$PACK_ID" "$crud_provider_id" "$crud_updated_display_name" "$crud_platform" "$crud_updated_base_url" "$crud_updated_smoke_model" "$crud_updated_supported_models_json" "$HOST_ID" "$crud_updated_notes")" +save_json 09-update-draft "$(crm_curl_json PUT "/api/provider-drafts/$crud_draft_id" "$crud_update_payload")" + +delete_status="$(crm_curl_status DELETE "/api/provider-drafts/$crud_draft_id")" +save_text 10-delete-draft.status "$delete_status" +save_json 11-list-drafts-after-delete "$(crm_curl_json GET "/api/provider-drafts?pack_id=$PACK_ID")" + +publish_verified="false" +if [[ "$VERIFY_PUBLISH" == "1" ]]; then + publish_create_payload="$(build_draft_payload "$PACK_ID" "$publish_provider_id" "$publish_display_name" "$publish_platform" "$publish_base_url" "$publish_smoke_model" "$publish_supported_models_json" "$HOST_ID" "$publish_notes")" + save_json 12-create-publish-draft "$(crm_curl_json POST "/api/provider-drafts" "$publish_create_payload")" + publish_draft_id="$(json_field_from_file "$ARTIFACT_DIR/12-create-publish-draft.json" draft.draft_id)" + publish_payload="$(python3 - "$publish_commit_message" <<'PY' +import json +import sys +print(json.dumps({"commit_message": sys.argv[1]}, ensure_ascii=False)) +PY +)" + save_json 13-publish-draft "$(crm_curl_json POST "/api/provider-drafts/$publish_draft_id/publish" "$publish_payload")" + publish_verified="true" +fi + +python3 - \ + "$ARTIFACT_DIR" \ + "$PACK_ID" \ + "$HOST_ID" \ + "$PACK_PATH" \ + "$PROVIDER_ID" \ + "$crud_draft_id" \ + "$crud_updated_display_name" \ + "$publish_verified" \ + "$publish_provider_id" \ + >"$ARTIFACT_DIR/99-summary.json" <<'PY' +import json +import sys +from pathlib import Path + +( + artifact_dir, + pack_id, + host_id, + pack_path, + provider_id, + crud_draft_id, + crud_updated_display_name, + publish_verified, + publish_provider_id, +) = sys.argv[1:10] + +art = Path(artifact_dir) +page = (art / "00-provider-admin.html").read_text(encoding="utf-8") +packs = json.loads((art / "01-packs.json").read_text(encoding="utf-8")) +hosts = json.loads((art / "02-hosts.json").read_text(encoding="utf-8")) +providers = json.loads((art / "03-pack-providers.json").read_text(encoding="utf-8")) +preview = json.loads((art / "04-preview-import.json").read_text(encoding="utf-8")) +import_result = json.loads((art / "05-import.json").read_text(encoding="utf-8")) +create_draft = json.loads((art / "06-create-draft.json").read_text(encoding="utf-8"))["draft"] +list_before = json.loads((art / "07-list-drafts-before-delete.json").read_text(encoding="utf-8"))["provider_drafts"] +get_draft = json.loads((art / "08-get-draft.json").read_text(encoding="utf-8"))["draft"] +update_draft = json.loads((art / "09-update-draft.json").read_text(encoding="utf-8"))["draft"] +delete_status = (art / "10-delete-draft.status").read_text(encoding="utf-8").strip() +list_after = json.loads((art / "11-list-drafts-after-delete.json").read_text(encoding="utf-8"))["provider_drafts"] + +assert "Provider Admin" in page +assert packs["packs"] +assert hosts["hosts"] +assert providers["providers"] +assert pack_id +assert host_id +assert pack_path +assert provider_id +assert int(preview["accepted_keys_count"]) >= 1 +assert int(import_result["batch_id"]) > 0 +assert import_result["batch_status"] +assert create_draft["draft_id"] == crud_draft_id +assert any(item["draft_id"] == crud_draft_id for item in list_before) +assert get_draft["draft_id"] == crud_draft_id +assert update_draft["display_name"] == crud_updated_display_name +assert delete_status == "204" +assert not any(item["draft_id"] == crud_draft_id for item in list_after) + +summary = { + "page_title_seen": "Provider Admin" in page, + "pack_id": pack_id, + "host_id": host_id, + "pack_path": pack_path, + "provider_id": provider_id, + "preview_accepted_keys_count": int(preview["accepted_keys_count"]), + "import_batch_id": int(import_result["batch_id"]), + "import_batch_status": import_result["batch_status"], + "crud_draft_id": crud_draft_id, + "crud_updated_display_name": update_draft["display_name"], + "crud_delete_status": delete_status, + "publish_verified": publish_verified == "true", +} + +if publish_verified == "true": + create_publish = json.loads((art / "12-create-publish-draft.json").read_text(encoding="utf-8"))["draft"] + publish_result = json.loads((art / "13-publish-draft.json").read_text(encoding="utf-8"))["publish"] + assert create_publish["provider_id"] == publish_provider_id + assert publish_result["provider_id"] == publish_provider_id + assert publish_result["commit_sha"] + summary["publish_draft_id"] = create_publish["draft_id"] + summary["publish_provider_id"] = publish_result["provider_id"] + summary["publish_commit_sha"] = publish_result["commit_sha"] +else: + summary["publish_skipped_reason"] = "VERIFY_PUBLISH=0" + +print(json.dumps(summary, ensure_ascii=False, indent=2)) +PY + +cat "$ARTIFACT_DIR/99-summary.json" diff --git a/scripts/test/test_real_host_scripts.sh b/scripts/test/test_real_host_scripts.sh index cc66c561..70eb183b 100755 --- a/scripts/test/test_real_host_scripts.sh +++ b/scripts/test/test_real_host_scripts.sh @@ -830,6 +830,131 @@ EOF assert_contains "$payloads" '"subscription_user_id": "36"' } +run_test_verify_provider_admin_actions_script() { + local tmpdir fakebin artifact_dir state_dir stdout_file + tmpdir="$(mktemp -d)" + trap 'rm -rf "$tmpdir"' RETURN + fakebin="$tmpdir/bin" + artifact_dir="$tmpdir/artifacts" + state_dir="$tmpdir/state" + stdout_file="$tmpdir/verify_provider_admin_actions.stdout.txt" + mkdir -p "$fakebin" "$artifact_dir" "$state_dir" + + cat > "$fakebin/curl" <<'EOF' +#!/usr/bin/env bash +set -euo pipefail +method="GET" +url="" +payload="" +output_file="" +write_out="" +prev="" +for arg in "$@"; do + case "$prev" in + -X) method="$arg"; prev=""; continue ;; + -d|--data) payload="$arg"; prev=""; continue ;; + -o) output_file="$arg"; prev=""; continue ;; + -w) write_out="$arg"; prev=""; continue ;; + esac + case "$arg" in + -X|-d|--data|-o|-w) prev="$arg"; continue ;; + http://*|https://*) url="$arg" ;; + esac +done +state_dir="${FAKE_STATE_DIR:?missing FAKE_STATE_DIR}" +write_body() { + local body="$1" + if [[ -n "$output_file" ]]; then + printf '%s\n' "$body" > "$output_file" + else + printf '%s\n' "$body" + fi +} +write_status() { + local status="$1" + if [[ -n "$write_out" ]]; then + printf '%s' "$status" + fi +} +case "$method $url" in + "GET http://portal.example.com/providers.html") + write_body 'Provider AdminProvider Admin' + ;; + "GET http://crm.example.com/api/packs") + write_body '{"packs":[{"pack_id":"openai-cn-pack","version":"1.1.9"}]}' + ;; + "GET http://crm.example.com/api/hosts") + write_body '{"hosts":[{"host_id":"remote43-current-host","base_url":"http://host.example.com","host_version":"0.1.129"}]}' + ;; + "GET http://crm.example.com/api/packs/openai-cn-pack/providers") + write_body '{"providers":[{"provider_id":"deepseek","display_name":"DeepSeek","platform":"openai","host_overlays":1}]}' + ;; + "POST http://crm.example.com/api/providers/deepseek/preview-import") + write_body '{"accepted_keys_count":1,"host_overlays":1,"names":["DeepSeek"],"decisions":[{"action":"accept"}]}' + ;; + "POST http://crm.example.com/api/providers/deepseek/import") + write_body '{"batch_id":321,"batch_status":"succeeded","provider_status":"ready","access_status":"self_service_ready","accepted_keys_count":1,"accounts_count":1,"host_overlays":1,"group":{"id":"g1"},"channel":{"id":"c1"},"plan":{"id":"p1"},"gateway":{"id":"gw1"}}' + ;; + "POST http://crm.example.com/api/provider-drafts") + if [[ "$payload" == *'provider-admin-publish-1700000003'* ]]; then + printf '%s\n' present > "$state_dir/publish-draft.present" + write_body '{"draft":{"draft_id":"draft_publish_001","pack_id":"openai-cn-pack","provider_id":"provider-admin-publish-1700000003","display_name":"Provider Admin Publish 1700000003","platform":"openai","base_url":"https://publish-1700000003.example.com/v1","smoke_test_model":"provider-admin-publish-1700000003","supported_models":["provider-admin-publish-1700000003"]}}' + else + printf '%s\n' present > "$state_dir/crud-draft.present" + write_body '{"draft":{"draft_id":"draft_crud_001","pack_id":"openai-cn-pack","provider_id":"provider-admin-draft-1700000003","display_name":"Provider Admin Draft 1700000003","platform":"openai","base_url":"https://draft-1700000003.example.com/v1","smoke_test_model":"provider-admin-draft-1700000003","supported_models":["provider-admin-draft-1700000003","provider-admin-draft-mini-1700000003"]}}' + fi + ;; + "GET http://crm.example.com/api/provider-drafts?pack_id=openai-cn-pack") + if [[ -f "$state_dir/crud-draft.present" ]]; then + write_body '{"provider_drafts":[{"draft_id":"draft_crud_001","pack_id":"openai-cn-pack","provider_id":"provider-admin-draft-1700000003","display_name":"Provider Admin Draft 1700000003","platform":"openai"}]}' + else + write_body '{"provider_drafts":[]}' + fi + ;; + "GET http://crm.example.com/api/provider-drafts/draft_crud_001") + write_body '{"draft":{"draft_id":"draft_crud_001","pack_id":"openai-cn-pack","provider_id":"provider-admin-draft-1700000003","display_name":"Provider Admin Draft 1700000003","platform":"openai","base_url":"https://draft-1700000003.example.com/v1","smoke_test_model":"provider-admin-draft-1700000003","supported_models":["provider-admin-draft-1700000003","provider-admin-draft-mini-1700000003"]}}' + ;; + "PUT http://crm.example.com/api/provider-drafts/draft_crud_001") + write_body '{"draft":{"draft_id":"draft_crud_001","pack_id":"openai-cn-pack","provider_id":"provider-admin-draft-1700000003","display_name":"Provider Admin Draft Updated 1700000003","platform":"openai","base_url":"https://draft-updated-1700000003.example.com/v1","smoke_test_model":"provider-admin-draft-updated-1700000003","supported_models":["provider-admin-draft-updated-1700000003"]}}' + ;; + "DELETE http://crm.example.com/api/provider-drafts/draft_crud_001") + rm -f "$state_dir/crud-draft.present" + write_status "204" + ;; + "POST http://crm.example.com/api/provider-drafts/draft_publish_001/publish") + write_body '{"publish":{"draft_id":"draft_publish_001","pack_id":"openai-cn-pack","provider_id":"provider-admin-publish-1700000003","provider_path":"packs/openai-cn-pack/providers/provider-admin-publish-1700000003.json","pack_version_before":"1.1.9","pack_version_after":"1.1.10","publish_mode":"created","commit_message":"feat(pack): publish provider admin draft provider-admin-publish-1700000003","commit_sha":"abc1234","repo_root":"/srv/sub2api-cn-relay-manager"}}' + ;; + *) + echo "unexpected curl request: $method $url payload=$payload" >&2 + exit 1 + ;; +esac +EOF + chmod +x "$fakebin/curl" + + PATH="$fakebin:$PATH" \ + FAKE_STATE_DIR="$state_dir" \ + CRM_BASE="http://crm.example.com" \ + CRM_ADMIN_TOKEN="token" \ + PROVIDER_ADMIN_PAGE_URL="http://portal.example.com/providers.html" \ + TS="1700000003" \ + ACCESS_API_KEY="sk-access" \ + PROVIDER_KEYS="sk-provider-1" \ + VERIFY_PUBLISH="1" \ + ARTIFACT_DIR="$artifact_dir" \ + bash "$ROOT_DIR/scripts/acceptance/verify_provider_admin_actions.sh" >"$stdout_file" + + local summary + summary="$(cat "$artifact_dir/99-summary.json")" + assert_contains "$summary" '"page_title_seen": true' + assert_contains "$summary" '"provider_id": "deepseek"' + assert_contains "$summary" '"preview_accepted_keys_count": 1' + assert_contains "$summary" '"import_batch_id": 321' + assert_contains "$summary" '"crud_delete_status": "204"' + assert_contains "$summary" '"publish_verified": true' + assert_contains "$summary" '"publish_commit_sha": "abc1234"' +} + run_test_verify_route_health_ui_script() { local tmpdir fakebin artifact_dir stdout_file tmpdir="$(mktemp -d)" @@ -1056,6 +1181,7 @@ run_test_import_remote43_provider_subscription_prep run_test_migrate_historical_artifacts run_test_verify_route_control_plane_script run_test_verify_route_data_plane_script +run_test_verify_provider_admin_actions_script run_test_verify_route_health_ui_script run_test_remote43_patched_stack_renderers run_test_setup_remote43_patched_stack_dry_run