test(frontend): add provider admin acceptance coverage
Add a dedicated acceptance script for providers.html, cover it in the local real-host script regression suite, and document the current frontend review baseline, closure audit, providers action matrix, and remediation task board. This keeps the frontend acceptance boundary explicit: providers.html now has a repeatable verification entry point for its page-level actions, while non-UI provider operations remain documented as backend-only capabilities.
This commit is contained in:
301
docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md
Normal file
301
docs/2026-05-31-FRONTEND_CLOSURE_AUDIT.md
Normal file
@@ -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` 只有部分闭环证据
|
||||
- 当前轮没有做新的公网复验,因此不应把“历史已闭环”说成“本轮已重新证明正常”
|
||||
598
docs/2026-05-31-FRONTEND_REMEDIATION_TASK_BOARD.md
Normal file
598
docs/2026-05-31-FRONTEND_REMEDIATION_TASK_BOARD.md
Normal file
@@ -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 运维能力
|
||||
- 一旦这页边界与验收收口,整套前端审查口径会立刻稳定很多
|
||||
354
docs/2026-05-31-FRONTEND_REVIEW_CHECKLIST.md
Normal file
354
docs/2026-05-31-FRONTEND_REVIEW_CHECKLIST.md
Normal file
@@ -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 的仓库审查,当前最合理的统一口径是:
|
||||
|
||||
- 管理端前端**不是空白状态**,已经有多页真实静态资产和后端接口接线
|
||||
- 但仓库**还没有形成前端专项验收体系**
|
||||
- 因此当前不能只凭执行板就断言“已完成前端功能全部正常”
|
||||
- 后续凡是声称“前端已完成”的条目,都应至少引用这份清单中的页面级闭环证据
|
||||
269
docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md
Normal file
269
docs/2026-05-31-PROVIDERS_ACTION_ACCEPTANCE_MATRIX.md
Normal file
@@ -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` 已支持的前端闭环能力
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
## 统一质量门禁
|
||||
|
||||
378
scripts/acceptance/verify_provider_admin_actions.sh
Executable file
378
scripts/acceptance/verify_provider_admin_actions.sh
Executable file
@@ -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"
|
||||
@@ -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 '<html><title>Provider Admin</title><body>Provider Admin</body></html>'
|
||||
;;
|
||||
"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
|
||||
|
||||
Reference in New Issue
Block a user