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:
phamnazage-jpg
2026-06-01 09:58:20 +08:00
parent c588a95c7d
commit 5fbac6ef0b
8 changed files with 2047 additions and 0 deletions

View 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-T1portal 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` 只有部分闭环证据
- 当前轮没有做新的公网复验,因此不应把“历史已闭环”说成“本轮已重新证明正常”

View 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 运维能力
- 一旦这页边界与验收收口,整套前端审查口径会立刻稳定很多

View 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 的仓库审查,当前最合理的统一口径是:
- 管理端前端**不是空白状态**,已经有多页真实静态资产和后端接口接线
- 但仓库**还没有形成前端专项验收体系**
- 因此当前不能只凭执行板就断言“已完成前端功能全部正常”
- 后续凡是声称“前端已完成”的条目,都应至少引用这份清单中的页面级闭环证据

View 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` 已支持的前端闭环能力

View File

@@ -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`

View File

@@ -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
```
## 统一质量门禁

View 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"

View File

@@ -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