feat: complete unified plugin admin v1.1.0
This commit is contained in:
@@ -5,12 +5,15 @@
|
||||
| AUTH-01 | 鉴权 | Core 管理员登录控制面 | `plugins/plugin-admin/main_test.go:TestAdminLoginDoesNotExposeCoreTokens`;本地浏览器登录 | passed |
|
||||
| AUTH-02 | 鉴权 | Core 2FA 登录 | challenge 一次性消费,成功创建会话 | passed |
|
||||
| AUTH-03 | 鉴权 | 普通用户登录和 API | `plugins/plugin-admin/main_test.go:TestOrdinaryCoreUserIsRejected` | passed |
|
||||
| AUTH-03A | 鉴权 | Core CAPTCHA 配置与一次性 proof | `TestCaptchaConfigReturnsOnlyPublicFields`、`TestLoginForwardsCaptchaProof`;登录页按 provider 渲染挑战 | passed |
|
||||
| AUTH-04 | 会话 | 过期、撤销、登出和刷新 | `plugins/plugin-admin/main_test.go:TestRefreshRevalidatesAdminRole` | passed |
|
||||
| AUTH-05 | CSRF | 所有写请求 | `plugins/plugin-admin/main_test.go:TestMutationRequiresCSRFAndIdempotency` | passed |
|
||||
| SEC-01 | 秘密 | 浏览器、URL、HTML、JS、LocalStorage、下载、日志 | 登录/配置测试断言 token 和 secret 不回显;浏览器 DOM 未出现 Core token | passed |
|
||||
| SEC-02 | 出站 | Core URL、重定向、代理和 SSRF | `TestHealthProbeRejectsRedirectAndRequiresReadiness`;loopback URL 校验 | passed |
|
||||
| SEC-02A | 市场出站 | 索引/归档 HTTPS、精确主机 allowlist、DNS 私网拒绝、体积和重定向门禁 | `main_test.go:TestRemoteMarketplaceRequiresAllowlistAndExpiry`;`marketplace.go` 出站策略 | passed |
|
||||
| SEC-03 | 脱敏 | Core 响应和错误 | token/password/secret/cookie 不出现在响应和日志 | passed |
|
||||
| DEPLOY-01 | 发布完整性 | 生产安装提交 pin | `deploy/install.sh` 对 tag 要求 `PLUGIN_COMMIT_SHA`,并校验检出 commit;脚本可从 immutable commit URL 获取 | passed |
|
||||
| DEPLOY-02 | 路径安全 | 安装/卸载根目录与父路径 | 安装路径拒绝根目录、`.`/`..` 和任一级符号链接父路径 | passed |
|
||||
| MAN-01 | 清单 | 未知字段、尾随 JSON、路径跳转 | `plugins/plugin-admin/internal/manifest/manifest_test.go`;包上传 smoke | passed |
|
||||
| MAN-02 | 签名 | Ed25519、key ID、哈希 | `manifest_test.go:TestSignatureAndKeyID`;生产不受信发布者路径 | passed |
|
||||
| MAN-03 | 兼容 | Core baseline、tested versions、capability | `manifest_test.go:TestCompatibility`;上传卡片显示 compatible | passed |
|
||||
@@ -21,7 +24,7 @@
|
||||
| LIFE-04 | 卸载 | 先停用再卸载 | 先提交注册表删除,成功后再清理插件资源,不删除 Core 数据 | passed |
|
||||
| LIFE-04A | 删除接口 | `DELETE /api/plugins/{id}` 与卸载语义一致 | 市场生命周期测试覆盖标准 DELETE | passed |
|
||||
| MENU-01 | 菜单 | preview/apply 自有 `custom_menu_items` | `main_test.go:TestMenuPreviewAndApplyPreserveOtherMenuItems` | passed |
|
||||
| MENU-02 | 嵌入 | iframe 和新窗口 | 本地控制面三视口登录/刷新;插件提供独立登录和新窗口入口 | passed |
|
||||
| MENU-02 | 嵌入 | iframe 和新窗口 | Plugin Admin 统一登录/刷新;订阅模块从统一控制面入口进入,iframe/新窗口均复用同一会话,不提供第二个登录页或 Cookie | passed |
|
||||
| API-01 | allowlist | 未声明路径和查询参数 | `allowedCorePath` 单元路径门禁;业务插件自身 allowlist 测试 | passed |
|
||||
| API-02 | Core 错误 | 401/403/409/429/5xx | 失败关闭、刷新一次、错误脱敏和请求 ID 传播 | passed |
|
||||
| UI-01 | 响应式 | 425px、900px、1440px | 本地 Browser 验收:三个视口 `scrollWidth == innerWidth`,插件卡片可见 | passed |
|
||||
@@ -40,6 +43,7 @@ go vet ./...
|
||||
node --check <all-ui-scripts>
|
||||
production build
|
||||
manifest verification
|
||||
bash -n <all-shell-scripts>
|
||||
git diff --check
|
||||
```
|
||||
|
||||
@@ -49,10 +53,10 @@ git diff --check
|
||||
|
||||
- `plugins/plugin-admin` 和 `plugins/subscription-admin`:`go test -race ./...`、`go vet ./...`、`node --check ui/app.js` 均通过。
|
||||
- `plugins/subscription-admin/package.sh` 生成的 `.s2plugin` 已通过 `unzip -t`,并通过控制面真实上传接口进入 `disabled` 状态。
|
||||
- 本地浏览器登录后,控制面首页显示“已登记插件”与订阅插件卡片;425、900、1440 视口均无横向溢出。
|
||||
- 本地浏览器登录后,控制面首页、插件市场和操作记录可访问;普通登录与 Turnstile 模拟登录在 425、900、1440 视口均通过,无横向溢出或敏感响应字段。
|
||||
- 仍需部署环境追加:真实生产签名密钥、跨实例共享会话、真实 Core iframe 刷新和跨节点升级演练;这些属于部署级验证,不改变本地 V1 控制面契约。
|
||||
|
||||
截图证据保存在 `.playwright-cli/plugin-admin-v1-final/`、`.playwright-cli/plugin-admin-v1-sensitive/`、`.playwright-cli/subscription-admin-v1-final/` 和 `.playwright-cli/subscription-admin-v1-sensitive/`,每组包含 425px、900px、1440px 三种视口。
|
||||
浏览器脚本通过 `PLUGIN_SCREENSHOT_DIR` 输出 425px、900px、1440px 截图;本轮使用系统 Chrome 运行普通控制面、Turnstile fixture 和订阅模块三套验收,截图保留在本机临时证据目录。
|
||||
|
||||
## 本地生命周期硬化证据
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ flowchart TD
|
||||
B[管理员浏览器] --> C[Core custom_menu_items]
|
||||
C --> I[Core /custom/:id sandbox iframe]
|
||||
I --> P[反向代理 /extensions/:plugin-id/]
|
||||
P --> M[Plugin Control Plane]
|
||||
M --> S[独立业务插件服务]
|
||||
S --> A[Typed Core API Adapter]
|
||||
P --> M[Plugin Control Plane / TDesign Shell]
|
||||
M --> S[订阅业务模块 BFF / 独立服务]
|
||||
M --> A[Typed Core API Adapter]
|
||||
A --> K[Core Auth/Admin API]
|
||||
K --> D[Core 权威账本与审计]
|
||||
M --> R[Plugin Registry / Revisions / Audit]
|
||||
@@ -23,15 +23,16 @@ sequenceDiagram
|
||||
participant B as Browser
|
||||
participant P as Plugin BFF
|
||||
participant C as Core
|
||||
B->>P: POST /login
|
||||
B->>P: POST /login(唯一登录入口)
|
||||
P->>C: POST /api/v1/auth/login
|
||||
C-->>P: access/refresh 或 2FA challenge
|
||||
P->>C: POST /api/v1/auth/login/2fa (按需)
|
||||
P->>C: GET /api/v1/auth/me
|
||||
C-->>P: role=admin
|
||||
P-->>B: HttpOnly plugin session + CSRF token
|
||||
B->>P: GET /api/plugins
|
||||
P-->>B: 唯一 HttpOnly plugin session + CSRF token
|
||||
B->>P: GET /api/plugins 或 /modules/subscription/*
|
||||
P->>C: Bearer Core JWT + X-Request-ID
|
||||
P->>S: 复用同一管理员会话调用订阅模块
|
||||
P-->>B: 脱敏业务数据
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Business Plugin V1 边界
|
||||
|
||||
> **前端架构修订(2026-08-30)**:Plugin Admin 是唯一的插件管理控制面和登录入口。订阅属于已安装业务模块,复用控制面会话、导航和 CSRF;订阅后端可以独立进程运行,但订阅前端不得再提供独立登录页、Cookie 或管理员身份。
|
||||
|
||||
## Core 负责
|
||||
|
||||
- 用户身份、密码、2FA、TokenVersion、会话撤销和管理员角色;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
状态:Accepted Contract / V1 参考实现已完成本地验收
|
||||
|
||||
本文定义与 Sub2API Core 解耦的通用业务插件框架。业务插件是独立服务、独立端口、独立版本和独立 UI;它可以通过现有管理员自定义菜单嵌入 Core,也可以在新窗口运行。订阅管理只是一个可选业务插件,不能成为框架后台、Core 热路径或插件生命周期的固定组成部分。
|
||||
本文定义与 Sub2API Core 解耦的通用业务插件框架。业务插件的后端服务、端口和版本独立;浏览器端由统一的 Plugin Admin 控制面承载,业务插件 UI 以模块方式挂载到同一个管理员 Shell 中。订阅管理只是一个可选业务模块,不能成为框架后台、Core 热路径或插件生命周期的固定组成部分。
|
||||
|
||||
> **前端架构修订(2026-08-30)**:此前“独立 UI/独立管理员后台”的表述仅指后端服务可以独立部署,不表示业务模块要再次登录。Plugin Admin 负责唯一的管理员登录、会话、导航和 CSRF;订阅模块安装并启用后才出现在控制面导航中,继承同一会话,不提供第二个登录页或第二套 Cookie。
|
||||
|
||||
## 1. 目标
|
||||
|
||||
@@ -14,7 +16,7 @@ V1 需要提供一个独立的插件控制面,负责:
|
||||
- 保存插件版本、服务地址、运行状态、菜单声明和操作审计;
|
||||
- 通过 Core 现有管理员鉴权复用操作者身份;
|
||||
- 将已启用插件的管理员菜单注入 `custom_menu_items`;
|
||||
- 让每个业务插件仅通过自己的 BFF 调用 Core 明确允许的 API。
|
||||
- 让每个业务插件仅通过控制面或其受控模块 BFF 调用 Core 明确允许的 API。
|
||||
|
||||
V1 不改变 Core Go/Vue、数据库迁移、现有鉴权、前端路由或 `.s2plugin` transport ABI。控制面自己的注册表、安装目录、进程和配置存储属于独立服务;它不连接 Core PostgreSQL、Redis 或宿主业务表。
|
||||
|
||||
@@ -25,7 +27,7 @@ V1 不改变 Core Go/Vue、数据库迁移、现有鉴权、前端路由或 `.s2
|
||||
| 运行方式 | Core 子进程 + gRPC | 独立服务 + HTTP/BFF |
|
||||
| 能力 | `openai.oauth.outbound_transport.v1` | 由清单声明的业务能力 |
|
||||
| 生命周期 | Core `PluginManager` | 独立 Plugin Control Plane |
|
||||
| UI | 配置 iframe + UI Bridge | 业务后台/用户工具页面 |
|
||||
| UI | 配置 iframe + UI Bridge | 统一控制面中的业务模块页面 |
|
||||
| 数据边界 | Core 负责账号转发与计费 | Core 负责权威业务数据,插件只读/投影 |
|
||||
| 菜单 | Core 固定插件管理页 | `custom_menu_items` 管理员入口 |
|
||||
|
||||
@@ -60,7 +62,7 @@ Sub2API Core 现有鉴权、Admin API 和领域账本
|
||||
- `plugin_operator`:查看状态、日志摘要和健康诊断,不改变包或凭据。
|
||||
- `plugin_readonly`:只读查看已登记插件。
|
||||
|
||||
V1 的控制面只允许 Core `role=admin` 登录。插件不创建第二套 Core 用户表;插件会话只保存 `plugin_id`、`admin_user_id`、角色、会话版本和过期时间。UI 隐藏按钮不等于授权,控制面和 Core API 均须重新校验权限。
|
||||
V1 的 Plugin Admin 控制面只允许 Core `role=admin` 登录。插件不创建第二套 Core 用户表;控制面会话保存 `admin_user_id`、角色、会话版本和过期时间,订阅等业务模块直接继承该会话。模块不得创建自己的登录页、Cookie 或独立权限入口。UI 隐藏按钮不等于授权,控制面、模块后端和 Core API 均须重新校验权限。
|
||||
|
||||
## 5. 管理面契约
|
||||
|
||||
@@ -157,9 +159,16 @@ Core 返回 `401` 时,一次用户请求最多 refresh 一次;并发 refresh
|
||||
|
||||
## 9. 会话和嵌入
|
||||
|
||||
插件登录调用 Core 现有 `/auth/login`、按需 `/auth/login/2fa`,再调用 `/auth/me` 校验管理员角色。Core token 只存插件服务端会话,浏览器只持有 HttpOnly、Secure、SameSite Cookie 和插件 CSRF token。
|
||||
Plugin Admin 登录调用 Core 现有 `/auth/login`、按需 `/auth/login/2fa`,再调用 `/auth/me` 校验管理员角色。Core token 只存控制面服务端会话,浏览器只持有控制面的 HttpOnly、Secure、SameSite Cookie 和 CSRF token;订阅模块请求沿用这套会话。
|
||||
|
||||
Core 的自定义页面当前使用 sandbox iframe,且不会自动继承 Core `localStorage` 登录态。因此 V1 必须同时提供新窗口入口;iframe 首屏显示插件登录页是已知行为。真正无感 SSO 需要 V1.1 的一次性 code/state 或受控 `postMessage` 交接,不得把 JWT 放在 URL。
|
||||
会话和进程是两个独立的生命周期:插件作为常驻服务运行,Core access token
|
||||
到期时由后端按需 refresh,不要求重启插件。默认会话空闲 30 分钟、绝对上限
|
||||
8 小时;refresh 失败或 Core 撤销管理员后清除会话并要求重新登录。V1 会话
|
||||
默认只在插件进程内存中保存,所以控制面重启后需要重新登录一次,但已启用
|
||||
插件会按 registry 恢复。跨实例或跨重启免登录必须接入插件自有加密共享会话
|
||||
存储,不能把 Core token 放入浏览器或 Core 数据库。
|
||||
|
||||
Core 的自定义页面当前使用 sandbox iframe,且不会自动继承 Core `localStorage` 登录态。因此 iframe 首屏由 Plugin Admin 显示一次登录页;进入订阅模块时不再追加登录。真正无感 Core SSO 仍需要 V1.1 的一次性 code/state 或受控 `postMessage` 交接,不得把 JWT 放在 URL。
|
||||
|
||||
菜单注入使用 Core 现有 `custom_menu_items`:
|
||||
|
||||
@@ -167,7 +176,7 @@ Core 的自定义页面当前使用 sandbox iframe,且不会自动继承 Core
|
||||
{
|
||||
"id": "DOMAIN_PLUGIN_ID",
|
||||
"label": "DOMAIN_PLUGIN_LABEL",
|
||||
"url": "https://CORE_ORIGIN/extensions/DOMAIN_PLUGIN_ID/",
|
||||
"url": "https://PLUGIN_PUBLIC_ORIGIN/extensions/qiu.plugin-admin/admin/#/modules/DOMAIN_MODULE/overview",
|
||||
"visibility": "admin",
|
||||
"sort_order": 200
|
||||
}
|
||||
@@ -200,7 +209,7 @@ Core 始终是用户身份、余额、订阅、订单、用量、权限、计费
|
||||
|
||||
### Phase 2:业务插件适配
|
||||
|
||||
提供 `DOMAIN_PLUGIN_ID` 级别的 SDK/模板和契约测试。订阅管理作为首个独立业务插件接入,只实现自身领域页面和 Core 只读 API,不改变控制面。
|
||||
提供 `DOMAIN_PLUGIN_ID` 级别的 SDK/模板和契约测试。订阅管理作为首个业务模块接入统一控制面,只实现自身领域页面和 Core 只读 API,不复制控制面的登录、导航和会话。
|
||||
|
||||
### Phase 3:生产增强
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# 插件控制面 UI 信息架构
|
||||
|
||||
## 目标
|
||||
|
||||
插件控制面是统一的插件运维后台,不是 Core 主站的复制品。订阅是安装后的业务模块,不是第二个后台系统。界面按任务拆分页面,避免把安装、版本、配置、菜单、订阅和审计动作堆在同一张卡片或同一个长页面中。
|
||||
|
||||
## 页面层级
|
||||
|
||||
```text
|
||||
插件管理
|
||||
├── 概览 # 控制面总览,不执行高风险操作
|
||||
├── 已安装插件 # 插件摘要列表,只保留常用主操作
|
||||
│ └── 插件详情/:id
|
||||
│ ├── 运行概况 # 健康、状态、端点、兼容性
|
||||
│ ├── 版本与升级 # revision、升级、回滚
|
||||
│ ├── 配置 # 服务地址、菜单地址、密钥提示
|
||||
│ ├── 菜单接入 # 预览和应用管理员菜单
|
||||
│ └── 操作历史 # 当前插件的审计记录
|
||||
├── 插件市场 # 受控目录元数据和入库入口
|
||||
└── 操作记录 # 全局操作审计和操作详情
|
||||
|
||||
已安装并启用的业务模块
|
||||
└── 订阅管理 # 复用控制面会话,不提供第二个登录页
|
||||
├── 概览
|
||||
├── 套餐
|
||||
├── 用户订阅
|
||||
└── 操作记录
|
||||
```
|
||||
|
||||
页面使用 hash 路由,便于刷新、复制链接和从 Core 自定义菜单直接打开:
|
||||
|
||||
```text
|
||||
#/overview
|
||||
#/plugins
|
||||
#/plugins/{plugin_id}/overview
|
||||
#/plugins/{plugin_id}/revisions
|
||||
#/plugins/{plugin_id}/config
|
||||
#/plugins/{plugin_id}/menu
|
||||
#/plugins/{plugin_id}/operations
|
||||
#/marketplace
|
||||
#/operations
|
||||
#/modules/subscription/overview
|
||||
#/modules/subscription/plans
|
||||
#/modules/subscription/subscriptions
|
||||
#/modules/subscription/audit
|
||||
```
|
||||
|
||||
hash 只表达页面位置,不承载凭据、Core JWT、Admin Key 或服务密钥。
|
||||
|
||||
## 一级页面职责
|
||||
|
||||
### 概览
|
||||
|
||||
只展示已登记、运行中、待启用和需要关注的数量,以及最近操作和生命周期提示。概览不直接承载上传、启停、回滚或卸载按钮,避免误操作;通过“管理插件”和“查看全部”进入专门页面。
|
||||
|
||||
### 已安装插件
|
||||
|
||||
每个插件条目只显示名称、版本、状态、Core 兼容性、活动 revision 和更新时间。条目保留一个生命周期主操作(启用或停用)和“查看详情”;升级、回滚、菜单和卸载进入“更多操作”菜单,防止按钮挤压或误触。
|
||||
|
||||
### 插件详情
|
||||
|
||||
详情页的每个二级页签只有一个任务:
|
||||
|
||||
- **运行概况**:判断当前是否健康、是否已配置、是否可以启用;错误仅展示脱敏后的最近错误。
|
||||
- **版本与升级**:查看 revision、活动版本和校验时间;上传升级包、选择回滚版本。
|
||||
- **配置**:读取和保存服务/菜单地址;敏感配置仍由服务端加密,页面不回显原值。
|
||||
- **菜单接入**:查看声明的菜单元数据,先预览再应用,不把菜单操作混在生命周期按钮中。
|
||||
- **操作历史**:只看当前插件关联的操作,操作 ID 可以打开详情。
|
||||
|
||||
### 插件市场
|
||||
|
||||
市场只显示受控索引提供的名称、版本、发布者、兼容性、发布时间和归档哈希。安装动作的语义是“下载并校验后入库”,完成后插件处于待启用状态;已经登记的插件跳转到版本页,不在市场卡片上直接覆盖现有版本。
|
||||
|
||||
### 操作记录
|
||||
|
||||
提供按插件和操作类型筛选的全局审计视图。列表展示操作、插件、结果和时间;操作 ID 进入详情弹窗,详情中包含请求 ID、状态和脱敏错误。
|
||||
|
||||
### 订阅业务模块
|
||||
|
||||
订阅模块只有在 `qiu.subscription-admin` 安装、启用并通过健康检查后才出现在导航中。它沿用 Plugin Admin 的管理员会话、CSRF、主题和响应式 Shell;模块页面不显示登录表单、不创建 Cookie,也不重复调用 Core 登录。套餐、用户订阅和余额数据仍从模块 BFF 读取,Core 继续是权威来源。
|
||||
|
||||
## 导航与响应式规则
|
||||
|
||||
- 桌面端使用左侧一级导航,内容区只显示当前路由页面;导航项同时显示页面名称和一句职责提示。
|
||||
- 移动端(宽度不超过 760px)将一级导航变为抽屉,通过“菜单”按钮打开;打开导航不会改变页面宽度,也不会让整个页面依赖横向滚动。
|
||||
- 详情页签只允许页签条自身横向滚动,内容区和页面主体始终保持 `scrollWidth <= innerWidth`。
|
||||
- 桌面端统计卡片四列,中等宽度两列,手机端仍使用两列但缩小内边距;内容面板在中等宽度以下改为单列。
|
||||
- 列表卡片的次要动作进入操作菜单;移动端按钮按可用宽度换行,不使用固定宽度挤压文字。
|
||||
- 审计表格在手机端保留最小可读列宽,仅表格容器横向滚动,页面本身不横向溢出。
|
||||
- 页面、卡片和表单使用统一 4px/6px 圆角、36px 控件高度和统一边距;页面不会用额外的局部颜色覆盖 Core 主题。
|
||||
|
||||
## 数据加载与权限
|
||||
|
||||
- 页面数据按路由按需加载:概览加载插件和审计,市场加载插件和市场索引,操作记录加载插件和审计,详情加载指定插件和审计;订阅模块只在启用后加载自己的数据。
|
||||
- 所有请求继续通过插件后端会话和 CSRF;浏览器不会直接调用 Core,也不会收到 Core token 或 Admin Key。
|
||||
- 订阅模块请求只能沿用控制面会话,不允许出现第二个登录 endpoint 或模块级会话。
|
||||
- 页面上的按钮隐藏只改善交互,真正的管理员权限、生命周期状态和幂等校验仍由插件后端负责。
|
||||
- mutation 返回 operation ID 后,前端短暂读取操作详情,再刷新当前路由;操作记录页面始终保留最终审计结果。
|
||||
|
||||
## 验收标准
|
||||
|
||||
1. 登录后默认进入概览,导航可进入控制面页面和已启用的业务模块,浏览器刷新后 hash 路由不丢失。
|
||||
2. 已安装列表不再展示所有生命周期按钮;插件详情的五个二级页签分别承担单一职责。
|
||||
3. 市场入库、配置、启停、升级、回滚、菜单和卸载接口行为与现有后端契约一致。
|
||||
4. 425px、900px、1440px 视口没有页面级横向溢出;移动端导航、详情页签和操作菜单可触达。
|
||||
5. 页面 DOM、JSON 响应、URL、错误提示和操作详情不出现 Core JWT、Admin Key、密码或服务密钥。
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
状态:V1 通用插件控制面参考实现已落库;订阅业务插件只读适配与 Core Host Adapter/写操作仍为 Draft
|
||||
|
||||
本文规划一种不改动 Sub2API 核心代码、数据库和现有插件 ABI 的独立业务插件框架。当前 V1 控制面参考实现位于 `plugins/plugin-admin`,它负责插件清单、签名、插件市场、下载入库、启停、升级、回滚、卸载、配置、审计和菜单注入;控制面本身不是订阅后台。每个业务插件(包括独立的 `plugins/subscription-admin`)作为可选的独立服务运行在自己的端口,通过控制面安装后再由部署层反向代理和现有“管理员可见自定义菜单”嵌入 Sub2API 页面。插件登录直接调用 Core 的现有鉴权,普通账号没有访问权限,也不复制 Core 用户表。
|
||||
本文规划一种不改动 Sub2API 核心代码、数据库和现有插件 ABI 的独立业务插件框架。当前 V1 控制面参考实现位于 `plugins/plugin-admin`,它负责插件清单、签名、插件市场、下载入库、启停、升级、回滚、卸载、配置、审计和菜单注入;订阅不是第二个后台,而是安装到控制面后的业务模块。订阅后端可以作为独立服务运行在自己的端口,但浏览器端统一由 Plugin Admin Shell 承载,所有业务模块共享一次管理员登录、会话、导航和 CSRF,不创建第二个登录页或 Cookie。插件登录直接调用 Core 的现有鉴权,普通账号没有访问权限,也不复制 Core 用户表。
|
||||
|
||||
> **前端架构修订(2026-08-30)**:文档中“独立服务”表示部署和进程边界,不表示每个业务模块都是独立产品。订阅模块只能在插件控制面会话内访问;模块入口由插件清单的 capability/menu 声明决定,未安装或未启用时不显示。
|
||||
|
||||
V1 已实现范围以 `plugins/plugin-admin` 控制面和本文“当前实现范围”章节为准;本文中的订阅业务插件只是首个适配样例。Core Host Adapter、短时 Plugin Access Token、无感 SSO 和余额写操作仍是后续版本设计,不代表当前 Core 已提供这些接口。
|
||||
|
||||
@@ -19,7 +21,7 @@ V1 已实现范围以 `plugins/plugin-admin` 控制面和本文“当前实现
|
||||
- 插件只在服务端调用 Core 现有 API,浏览器不持有 `x-api-key` 或 Core JWT;
|
||||
- 第一阶段只搭框架、登录、权限、健康检查、嵌入和只读联调,不实现订阅购买写操作。
|
||||
|
||||
现有自定义菜单可以完成“把插件页面显示在 Sub2API 管理页面内”,但现有 iframe 使用 sandbox,且 Core 前端 JWT 保存在 `localStorage`,不会自动注入跨端口 iframe。因此在完全不改 Core 的前提下,V1 的登录方式是:插件登录页把凭据转交给插件后端,插件后端调用 Core 现有登录和二次验证接口,确认 `role=admin` 后只保留短时插件会话及服务端 Core token;这复用同一套 Core 用户和角色,不复制用户表,但不是无感知的当前页面会话共享。
|
||||
现有自定义菜单可以完成“把插件控制面显示在 Sub2API 管理页面内”,但现有 iframe 使用 sandbox,且 Core 前端 JWT 保存在 `localStorage`,不会自动注入跨端口 iframe。因此在完全不改 Core 的前提下,V1 的登录方式是:Plugin Admin 登录页把凭据转交给插件后端,插件后端调用 Core 现有登录和二次验证接口,确认 `role=admin` 后只保留短时控制面会话及服务端 Core token;这复用同一套 Core 用户和角色,不复制用户表。订阅模块继承控制面会话,不得再次调用 Core 登录。
|
||||
|
||||
如果以后要求“已登录 Core 后打开 iframe 立即无感登录”,需要一个很小的 Core 一次性登录交接接口或前端 `postMessage` 适配;这属于 V1.1,不应通过 URL 明文传递 JWT。反向代理只改变网络路径,不改变这一认证边界。
|
||||
|
||||
@@ -37,12 +39,13 @@ Core `/custom/PLUGIN_ID`
|
||||
反向代理 `/extensions/PLUGIN_ID/*`
|
||||
│ 转发到独立服务 `127.0.0.1:PLUGIN_PORT`
|
||||
▼
|
||||
业务插件后台
|
||||
├─ `/login` 接收管理员登录请求
|
||||
Plugin Admin 控制面(统一 TDesign Shell)
|
||||
├─ `/login` 唯一登录入口
|
||||
├─ 服务端调用 Core `/api/v1/auth/login`(需要时调用 `/login/2fa`)
|
||||
├─ 调用 Core `/api/v1/auth/me`,确认 `role=admin`
|
||||
├─ 建立插件 HttpOnly 会话,Core access/refresh token 只在服务端保存
|
||||
└─ 通过 Bearer Core JWT 调用现有 Core Admin API
|
||||
├─ 建立唯一的插件 HttpOnly 会话,Core access/refresh token 只在服务端保存
|
||||
├─ 挂载已启用业务模块(订阅等),模块不再登录
|
||||
└─ 通过受控 BFF/内部服务调用业务模块和 Core Admin API
|
||||
▼
|
||||
Sub2API Core 现有认证、Admin API 和订阅/余额账本
|
||||
```
|
||||
@@ -53,7 +56,7 @@ Sub2API Core 现有认证、Admin API 和订阅/余额账本
|
||||
|
||||
### 1.2 V1 的默认范围
|
||||
|
||||
- 插件拥有独立后台和独立发布版本。
|
||||
- 插件后端拥有独立服务和独立发布版本;前端由统一控制面承载。
|
||||
- 只允许管理员登录;普通用户访问插件后台一律拒绝。
|
||||
- 首版支持管理员登录、权限校验、健康检查、嵌入和只读订阅数据展示。
|
||||
- 余额购买命令暂不实现,待框架验收后再复用现有订阅逻辑设计原子入口。
|
||||
@@ -78,8 +81,8 @@ Sub2API Core 现有认证、Admin API 和订阅/余额账本
|
||||
| 术语 | 定义 |
|
||||
|---|---|
|
||||
| Core | Sub2API 主服务,拥有用户、余额、Group、Key、订阅和用量账本。 |
|
||||
| Business Plugin | 独立部署的后台服务,提供一个业务域的管理 UI 和 BFF。 |
|
||||
| Plugin UI | 由 Business Plugin 提供的页面,只调用自己的后端。 |
|
||||
| Business Plugin | 独立部署的后台服务或业务模块,提供一个业务域的 UI 和 BFF。 |
|
||||
| Plugin UI | 由统一控制面挂载的业务模块页面,只调用模块 BFF。 |
|
||||
| Plugin Backend | Business Plugin 的服务端,保存插件配置、会话和操作幂等记录。 |
|
||||
| Core API Adapter | V1 插件后端对 Core 现有 REST API 的服务端客户端,只允许访问明确的认证、管理员和只读业务端点。 |
|
||||
| Future Host Adapter | V1.1 以后、需要修改 Core 才能提供的版本化业务 API;不属于本次无 Core 改动的实现范围。 |
|
||||
@@ -91,27 +94,27 @@ Sub2API Core 现有认证、Admin API 和订阅/余额账本
|
||||
|
||||
### 4.1 管理员登录
|
||||
|
||||
V1 采用“插件会话 + Core 现有登录”的两层模型,不创建插件用户表:
|
||||
V1 采用“控制面会话 + Core 现有登录”的两层模型,不创建插件用户表;所有业务模块继承控制面会话:
|
||||
|
||||
1. 浏览器打开插件 `/login`,只向插件后端提交 Core 管理员凭据和必要的 2FA 信息。
|
||||
1. 浏览器打开 Plugin Admin `/login`,只向控制面后端提交 Core 管理员凭据和必要的 2FA 信息。
|
||||
2. 插件后端服务端调用 Core `POST /api/v1/auth/login`;需要二次验证时继续调用 `POST /api/v1/auth/login/2fa`。
|
||||
3. 插件后端用返回的 Core access token 调用 `GET /api/v1/auth/me`,确认用户状态正常且 `role=admin`。
|
||||
4. 插件后端建立自己的短时 HttpOnly 会话;Core access/refresh token 只保存在插件服务端的会话存储中,不回传浏览器。
|
||||
5. 插件业务请求只携带插件会话 Cookie,插件后端再用对应管理员的 Core Bearer token 调用允许的 Core API。
|
||||
4. 控制面建立唯一的短时 HttpOnly 会话;Core access/refresh token 只保存在控制面服务端的会话存储中,不回传浏览器。
|
||||
5. 订阅等业务模块请求只携带控制面会话 Cookie,由控制面 BFF 或受控内部转发调用模块和允许的 Core API。
|
||||
|
||||
```text
|
||||
浏览器 -> Plugin /login(插件会话 Cookie 尚未建立)
|
||||
Plugin -> Core /api/v1/auth/login
|
||||
Plugin -> Core /api/v1/auth/login/2fa(按 Core 返回的要求)
|
||||
Plugin -> Core /api/v1/auth/me(确认 role=admin)
|
||||
Plugin <- 建立 HttpOnly 插件会话
|
||||
浏览器 -> Plugin /admin/*(只带插件会话 Cookie)
|
||||
Plugin -> Core /api/v1/admin/*(只在服务端带 Bearer Core JWT)
|
||||
浏览器 -> Plugin Admin /login(控制面会话 Cookie 尚未建立)
|
||||
Plugin Admin -> Core /api/v1/auth/login
|
||||
Plugin Admin -> Core /api/v1/auth/login/2fa(按 Core 返回的要求)
|
||||
Plugin Admin -> Core /api/v1/auth/me(确认 role=admin)
|
||||
Plugin Admin <- 建立唯一 HttpOnly 控制面会话
|
||||
浏览器 -> Plugin Admin /admin/* 与 /modules/*(只带同一会话 Cookie)
|
||||
Plugin Admin -> 订阅模块 BFF / Core /api/v1/admin/*(只在服务端带 Bearer Core JWT)
|
||||
```
|
||||
|
||||
这不是独立账号,也不是把 Core 用户复制到插件;密码只用于一次 Core 登录请求,插件不落库。插件会话可使用内存存储;多实例部署时使用插件自己的 Redis/会话存储,不连接 Core 数据库。Core 继续负责密码、2FA、限流、TokenVersion、撤销和管理员角色校验。
|
||||
这不是独立账号,也不是把 Core 用户复制到插件;密码只用于一次 Core 登录请求,控制面不落库。插件会话可使用内存存储;多实例部署时使用插件自己的 Redis/会话存储,不连接 Core 数据库。订阅等业务模块直接复用控制面会话,不创建模块级会话。Core 继续负责密码、2FA、限流、TokenVersion、撤销和管理员角色校验。
|
||||
|
||||
当前 Core 没有给自定义 iframe 提供 token handoff,因此“Core 已登录后打开 iframe 自动登录”不属于 V1。V1 允许在 iframe 内显示插件登录页;由于 sandbox/第三方 Cookie 策略可能让嵌入会话在刷新后失效,插件必须提供“新窗口打开插件”入口作为稳定登录路径。以后如需真正无感 SSO,另行设计一次性 code + state 或 `postMessage` 交接机制,JWT 不应放在 URL。
|
||||
当前 Core 没有给自定义 iframe 提供 token handoff,因此“Core 已登录后打开 iframe 自动登录”不属于 V1。V1 允许在 iframe 内显示一次 Plugin Admin 登录页;进入订阅模块时不再追加登录。由于 sandbox/第三方 Cookie 策略可能让嵌入会话在刷新后失效,控制面必须提供“新窗口打开”入口。以后如需真正无感 SSO,另行设计一次性 code + state 或 `postMessage` 交接机制,JWT 不应放在 URL。
|
||||
|
||||
### 4.2 权限判定
|
||||
|
||||
@@ -131,6 +134,14 @@ Plugin -> Core /api/v1/admin/*(只在服务端带 Bearer Core JWT)
|
||||
- 独立 origin 嵌入时按浏览器策略使用 `SameSite=None; Secure`,同源反代优先使用 `Lax`;必须在目标浏览器验证刷新、退出和第三方 Cookie 行为。
|
||||
- 插件 UI 不把 Core JWT、Admin Key 或服务凭据写入 LocalStorage、URL、HTML、日志或错误提示。
|
||||
|
||||
会话有效期与插件进程生命周期相互独立。插件作为常驻 HTTP 服务运行,Core
|
||||
access token 过期时由后端按需调用 `/auth/refresh` 并更新服务端会话,不需要
|
||||
重启插件。只有 refresh 失败、管理员被 Core 撤销、插件会话达到空闲/绝对
|
||||
TTL,或管理员主动退出时,浏览器才需要重新登录。V1 默认使用内存会话,因
|
||||
此控制面进程重启会使现有插件会话失效一次;这不影响 registry 中已登记插件
|
||||
的恢复。需要跨重启免登录时,使用插件自有的加密共享会话存储,不改变 Core
|
||||
身份权威,也不把 refresh token 下发给浏览器。
|
||||
|
||||
## 5. Admin Key 与服务凭据安全
|
||||
|
||||
### 5.1 绝对禁止的做法
|
||||
@@ -145,7 +156,7 @@ Plugin -> Core /api/v1/admin/*(只在服务端带 Bearer Core JWT)
|
||||
|
||||
| 环境 | 凭据 | 用途 | 约束 |
|
||||
|---|---|---|---|
|
||||
| 开发 | Core 管理员的临时登录凭据 | 调用 Core `/auth/login` 联调 | 只由开发者输入到插件登录页,不写入代码、配置和日志。 |
|
||||
| 开发 | Core 管理员的临时登录凭据 | 调用 Core `/auth/login` 联调 | 只由开发者输入到 Plugin Admin 登录页,不写入代码、配置和日志。 |
|
||||
| 测试 | Core 返回的 access/refresh token | 建立插件服务端会话 | 只存插件服务端会话存储,短 TTL,测试 Core 与测试管理员专用。 |
|
||||
| 生产 | Core 返回的 access/refresh token | 代表实际登录的 Core 管理员调用现有 Admin API | 服务端加密保存或内存保存,按 Core TokenVersion/撤销结果失效;不回传浏览器。 |
|
||||
|
||||
@@ -155,8 +166,8 @@ V1.1 如需在不保存 Core refresh token 的情况下运行,再设计 Core
|
||||
|
||||
### 5.3 凭据生命周期
|
||||
|
||||
1. 插件后端接收管理员登录请求,但不保存密码。
|
||||
2. 插件后端调用 Core 登录/2FA,保存返回 token 到服务端会话,并绑定 `admin_user_id`。
|
||||
1. Plugin Admin 后端接收管理员登录请求,但不保存密码。
|
||||
2. Plugin Admin 后端调用 Core 登录/2FA,保存返回 token 到唯一的服务端会话,并绑定 `admin_user_id`。
|
||||
3. 每次 Core 请求都使用 TLS 和 Core Bearer token;Core 继续校验签名、TokenVersion、会话绑定和角色。
|
||||
4. access token 过期时只使用对应 refresh token 调用 Core `/auth/refresh`;刷新失败就销毁插件会话并要求重新登录。
|
||||
5. 登出、Core 管理员撤销会话、停用插件或发现泄露时,立即删除插件会话;Admin Key 过渡模式由运维轮换并撤销。
|
||||
@@ -302,11 +313,11 @@ Core `/custom/example.subscription`
|
||||
└── sandbox iframe -> 反向代理 -> `127.0.0.1:PLUGIN_PORT`
|
||||
```
|
||||
|
||||
`visibility=admin` 只负责隐藏普通账号的菜单入口,插件后端仍必须独立鉴权。现有 iframe 的 sandbox、跨端口 origin 和 Core JWT `localStorage` 使其不会自动共享 Core 登录态;因此 iframe 首屏显示插件登录页是 V1 的预期行为。插件也应提供“新窗口打开”,便于登录后保持自身会话。
|
||||
`visibility=admin` 只负责隐藏普通账号的菜单入口,Plugin Admin 后端仍必须鉴权。现有 iframe 的 sandbox、跨端口 origin 和 Core JWT `localStorage` 使其不会自动共享 Core 登录态;因此 iframe 首屏显示一次控制面登录页是 V1 的预期行为。订阅模块显示在控制面内部,不再出现独立登录页。控制面也应提供“新窗口打开”,便于登录后保持自身会话。
|
||||
|
||||
生产部署建议把插件外部地址挂在与 Core 相同的 HTTPS 站点下,由 Nginx/Caddy 按路径反代到独立端口;这只减少浏览器跨域问题,不改变插件必须登录和服务端调用 Core 的事实。若使用独立 origin,必须在 Core CORS 中精确加入该 origin,禁止 `*`,并仅允许必要的 `Authorization` 请求头。
|
||||
|
||||
插件页面只调用自己的 `/plugin-api/*`,由插件后端调用 Core 现有认证和管理员 API。V1 不允许插件动态注入主应用 Vue 路由、修改核心菜单组件或覆盖全局 CSS;主应用只提供一个受权限控制的“业务插件”入口,插件内部菜单由插件自己管理。
|
||||
插件模块页面只调用控制面提供的 `/plugin-api/*` 或受控模块 BFF,由控制面调用 Core 现有认证和管理员 API。V1 不允许插件动态注入 Core Vue 路由、修改 Core 菜单组件或覆盖全局 CSS;Plugin Admin 只挂载已安装、已启用且管理员可见的业务模块。
|
||||
|
||||
## 10. 威胁模型与处置
|
||||
|
||||
@@ -345,7 +356,7 @@ Core `/custom/example.subscription`
|
||||
|
||||
### 11.3 浏览器和部署测试
|
||||
|
||||
- 管理员可登录,普通用户无法登录或访问任何后台 API;
|
||||
- 管理员在 Plugin Admin 完成一次登录后可进入订阅模块,普通用户无法登录或访问任何后台 API;
|
||||
- 不同屏幕下页面无横向泄露和敏感字段;
|
||||
- 浏览器 DevTools 的请求、下载和页面源中没有 Admin Key;
|
||||
- HTTPS、反向代理、容器低权限、secret 文件权限和日志脱敏;
|
||||
@@ -355,7 +366,7 @@ Core `/custom/example.subscription`
|
||||
|
||||
### Phase 0:冻结部署契约
|
||||
|
||||
- 选择外部服务部署方式(推荐同源反向代理 + 独立服务);
|
||||
- 选择外部服务部署方式(推荐同源反向代理 + 独立服务),并冻结统一控制面模块挂载方式;
|
||||
- 冻结插件端口、反向代理路径、`custom_menu_items` 字段和健康检查;
|
||||
- 冻结允许调用的现有 Core API 路径、字段、分页和错误处理;
|
||||
- 明确插件登录通过 Core `/auth/login`/`/login/2fa`,不创建用户表;
|
||||
@@ -369,10 +380,10 @@ Core `/custom/example.subscription`
|
||||
- 插件健康检查、启停、操作审计和同源入口;
|
||||
- Core API allowlist、contract test 与本地示例插件。
|
||||
|
||||
### Phase 2:订阅插件试验
|
||||
### Phase 2:订阅业务模块试验
|
||||
|
||||
- 只读套餐、用户余额和订阅列表;
|
||||
- 管理员操作页面、查询缓存和审计;
|
||||
- 在统一控制面内挂载只读套餐、用户余额和订阅列表模块;
|
||||
- 复用控制面管理员会话、查询缓存和审计;
|
||||
- 使用测试 Core 和测试账户,不连接生产余额;
|
||||
- 余额购买、续费和撤销暂不实现,等待 Core 原子接口冻结。
|
||||
|
||||
|
||||
+3
-1
@@ -8,12 +8,14 @@
|
||||
- [Manifest V1](BUSINESS_PLUGIN_MANIFEST_V1.md)
|
||||
- [Boundaries](BUSINESS_PLUGIN_BOUNDARIES.md)
|
||||
- [Architecture](BUSINESS_PLUGIN_ARCHITECTURE.md)
|
||||
- [Plugin Admin UI Information Architecture](PLUGIN_ADMIN_UI_INFORMATION_ARCHITECTURE.md)
|
||||
- [TDesign Frontend Migration Assessment](TDESIGN_FRONTEND_MIGRATION_ASSESSMENT.md)
|
||||
- [Acceptance](BUSINESS_PLUGIN_ACCEPTANCE.md)
|
||||
- [Development](BUSINESS_PLUGIN_DEVELOPMENT.md)
|
||||
|
||||
## 领域插件
|
||||
|
||||
- `subscription-admin`:独立管理员只读订阅插件。它是框架的第一个领域样例,不是通用插件后台,也不负责安装或管理其他插件。实现与运行方式见 [`../plugins/subscription-admin/README.md`](../plugins/subscription-admin/README.md)。
|
||||
- `subscription-admin`:订阅业务模块后端。它是框架的第一个领域样例,不是通用插件后台,也不负责安装或管理其他插件;前端由 Plugin Admin 统一挂载并共享一次登录。实现与运行方式见 [`../plugins/subscription-admin/README.md`](../plugins/subscription-admin/README.md)。
|
||||
|
||||
## 现有 `.s2plugin`
|
||||
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
|
||||
状态:V1 只读试验实现已落库;余额购买、续费、撤销仍为 V1.1 Draft
|
||||
|
||||
本文定义基于 [`PLUGIN_FRAMEWORK_V1_RFC.md`](./PLUGIN_FRAMEWORK_V1_RFC.md) 的第一个业务插件试验,对应实现为 `plugins/subscription-admin`。插件是独立端口的管理员后台,复用 Sub2API Core 的管理员鉴权,不创建 Core 用户表,也不直接连接 Core 数据库。当前实现已完成框架、管理员会话、只读套餐/余额/订阅/审计联调;余额购买、续费和撤销写操作后置到 Core 原子接口冻结之后。
|
||||
本文定义基于 [`PLUGIN_FRAMEWORK_V1_RFC.md`](./PLUGIN_FRAMEWORK_V1_RFC.md) 的第一个业务模块试验,对应后端实现为 `plugins/subscription-admin`。订阅后端可以独立端口运行,但它不是第二个管理员后台:浏览器入口由 `plugins/plugin-admin` 统一控制面承载,复用同一套管理员会话、导航、CSRF 和权限,不创建 Core 用户表,也不直接连接 Core 数据库。当前实现已完成框架、管理员会话、只读套餐/余额/订阅/审计联调;余额购买、续费和撤销写操作后置到 Core 原子接口冻结之后。
|
||||
|
||||
> **前端架构修订(2026-08-30)**:本 RFC 中原有“订阅插件登录页/独立后台”描述由本条覆盖。订阅模块不得提供第二个 `/login`、独立 Cookie、独立管理员身份或重复的 Core 登录;安装并启用后才在 Plugin Admin 导航中出现。
|
||||
|
||||
## 1. 目标和范围
|
||||
|
||||
### 1.1 V1 目标
|
||||
|
||||
- 提供独立的管理员订阅后台,普通账号拒绝登录和访问;
|
||||
- 插件登录调用 Core 现有 `/api/v1/auth/login`、`/api/v1/auth/login/2fa` 和 `/api/v1/auth/me`,不维护第二套用户密码;
|
||||
- 提供挂载在统一 Plugin Admin 控制面内的管理员订阅模块,普通账号拒绝登录和访问;
|
||||
- 由 Plugin Admin 统一调用 Core 现有 `/api/v1/auth/login`、`/api/v1/auth/login/2fa` 和 `/api/v1/auth/me`;订阅模块不维护第二套用户密码或登录态;
|
||||
- 展示 Core 中的套餐、用户余额和订阅实例状态;
|
||||
- 通过现有 `custom_menu_items` 的 `visibility=admin` 入口嵌入 Core 页面,也支持新窗口打开;
|
||||
- 插件可独立升级、停用和回滚,不影响 Core 网关、余额账本和已有订阅;
|
||||
@@ -42,14 +44,15 @@
|
||||
Core 管理后台
|
||||
└─ custom_menu_items (visibility=admin)
|
||||
└─ sandbox iframe / 新窗口
|
||||
└─ 反向代理 -> Plugin `127.0.0.1:PLUGIN_PORT`
|
||||
├─ Plugin /login -> Core /api/v1/auth/login (+ /login/2fa)
|
||||
├─ Plugin /auth/me -> 只允许 role=admin
|
||||
├─ HttpOnly 插件会话(不建 Core 用户表)
|
||||
└─ 反向代理 -> Plugin Admin `127.0.0.1:PLUGIN_PORT`
|
||||
├─ Plugin Admin /login -> Core /api/v1/auth/login (+ /login/2fa)
|
||||
├─ Plugin Admin /auth/me -> 只允许 role=admin
|
||||
├─ 唯一 HttpOnly 控制面会话(不建 Core 用户表)
|
||||
├─ 挂载订阅模块 /modules/subscription/*(不重复登录)
|
||||
└─ 服务端 Bearer Core JWT -> 现有 Core Admin API(V1 只读)
|
||||
```
|
||||
|
||||
插件 UI 只访问自己的 BFF;Core JWT、refresh token 和 Admin API Key 只在插件服务端会话或 secret 中出现。iframe 不会自动继承 Core `localStorage` 登录态,因此 V1 首屏显示插件登录页属于预期行为。sandbox 或第三方 Cookie 策略可能导致嵌入会话刷新后失效,插件必须提供新窗口登录路径;登录密码只在一次转发请求中经过插件后端,不落库、不写日志。
|
||||
订阅模块 UI 只访问控制面的模块 BFF;Core JWT、refresh token 和 Admin API Key 只在控制面服务端会话或 secret 中出现。iframe 不会自动继承 Core `localStorage` 登录态,因此 V1 首屏显示一次 Plugin Admin 登录页属于预期行为;进入订阅模块时不再登录。sandbox 或第三方 Cookie 策略可能导致嵌入会话刷新后失效,控制面必须提供新窗口入口;登录密码只在一次转发请求中经过控制面后端,不落库、不写日志。
|
||||
|
||||
## 4. 权威边界
|
||||
|
||||
@@ -65,7 +68,7 @@ Core 管理后台
|
||||
|
||||
### 4.2 插件可以负责
|
||||
|
||||
- 管理员登录代理、插件会话和插件内角色;
|
||||
- 控制面负责管理员登录代理、统一插件会话和插件内角色;订阅模块只消费经过授权的模块会话;
|
||||
- 套餐、用户和订阅的分页筛选与展示;
|
||||
- 只读缓存、操作结果页和管理员审计视图;
|
||||
- 未来写操作的确认表单,但提交必须调用 Core 原子命令;
|
||||
@@ -210,7 +213,7 @@ accepted -> processing -> completed
|
||||
|
||||
### 11.3 浏览器和部署
|
||||
|
||||
- 管理员可在 iframe 和新窗口完成登录;普通用户菜单不可见且 API 拒绝;
|
||||
- 管理员在 Plugin Admin 的 iframe 或新窗口完成一次登录即可进入订阅模块;普通用户菜单不可见且 API 拒绝;
|
||||
- 425px、900px、1440px 下无横向溢出、遮挡或敏感字段泄露;
|
||||
- DevTools 请求、下载、页面源和日志中没有 secret;
|
||||
- 反向代理、HTTPS、低权限运行、停用、升级和回滚流程可恢复。
|
||||
@@ -247,7 +250,7 @@ accepted -> processing -> completed
|
||||
|
||||
- [ ] 插件只允许 Core `role=admin` 登录,且不创建用户表。
|
||||
- [ ] 独立端口、反向代理路径和 `visibility=admin` 菜单入口已确定。
|
||||
- [ ] iframe 登录页和新窗口登录页均可用,已知晓 V1 不提供无感 SSO。
|
||||
- [ ] Plugin Admin 统一登录入口在 iframe 和新窗口场景均可用;订阅模块不提供独立登录,V1 不提供无感 SSO。
|
||||
- [ ] 只读 Core API allowlist、分页、字段脱敏和缓存策略已冻结。
|
||||
- [ ] 同档位多实例、单独续费、用户不可取消和管理员撤销规则已确认。
|
||||
- [ ] 余额购买写操作明确等待 Core 原子接口,不使用现有多个接口拼接。
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# TDesign 插件前端重构评估
|
||||
|
||||
## 1. 范围与结论
|
||||
|
||||
本次只重构独立插件仓库的前端,不修改官方 Sub2API Core 的 Go、Vue、迁移、鉴权或构建配置。
|
||||
|
||||
独立插件仓库当前工作目录:`/tmp/sub2api-add-repo.pzrg17`
|
||||
|
||||
官方 Core 当前工作目录:`/Users/qiu/Desktop/Sub2API`
|
||||
|
||||
建议把 TDesign starter 作为统一插件控制面的 UI 基座,而不是把 starter 合并到 Core 的 `frontend/`。插件控制面只登录一次;订阅是安装后挂载到控制面里的业务模块,不是第二个后台系统。
|
||||
|
||||
## 2. Starter 基线
|
||||
|
||||
已拉取:`/Users/qiu/Desktop/tdesign-vue-next-starter-v1`
|
||||
|
||||
```text
|
||||
仓库: https://github.com/Tencent/tdesign-vue-next-starter
|
||||
分支: develop
|
||||
提交: d6f8fafad9c1596cac8dfac8d52a88d0ca692acc
|
||||
版本: package.json 0.14.0
|
||||
```
|
||||
|
||||
已确认的技术栈:
|
||||
|
||||
- Vue 3.5、TypeScript、Vite 8、Pinia 3、Vue Router 5
|
||||
- `tdesign-vue-next` `^1.20.2`
|
||||
- `tdesign-icons-vue-next` `^0.4.4`
|
||||
- ECharts `^6.1.0`
|
||||
- Node.js `>=22.12.0`
|
||||
|
||||
Starter 的基线构建已通过:`npm ci --ignore-scripts`、`npm run build`。
|
||||
|
||||
ECharts 已在 starter 的 `src/hooks/index.ts` 中通过 `echarts/core` 初始化。starter 没有 `line-icons` 或 `lineicon` 依赖;TDesign Icons 本身是线性图标风格,后续统一使用 `tdesign-icons-vue-next`,避免混用多个图标系统。
|
||||
|
||||
## 3. 当前插件页面清单
|
||||
|
||||
### 3.1 Plugin Admin
|
||||
|
||||
入口:`plugins/plugin-admin/ui/index.html`、`plugins/plugin-admin/ui/app.js`
|
||||
|
||||
这是通用插件控制面,不是订阅业务页面。现有职责应拆成以下路由:
|
||||
|
||||
```text
|
||||
#/overview
|
||||
#/plugins
|
||||
#/plugins/:plugin_id/overview
|
||||
#/plugins/:plugin_id/revisions
|
||||
#/plugins/:plugin_id/config
|
||||
#/plugins/:plugin_id/menu
|
||||
#/plugins/:plugin_id/operations
|
||||
#/marketplace
|
||||
#/operations
|
||||
```
|
||||
|
||||
页面职责:
|
||||
|
||||
| 页面 | 主要内容 | TDesign 组件方向 |
|
||||
| --- | --- | --- |
|
||||
| 登录 | Core 管理员账号登录、2FA、会话过期 | `TForm`、`TInput`、`TButton`、`TAlert` |
|
||||
| 概览 | 已登记、运行中、待启用、需关注、最近操作 | `TCard`、`TStatistic`、`TTag`、`TTimeline` |
|
||||
| 已安装插件 | 摘要列表、生命周期主操作、查看详情 | `TTable` 或响应式 `TCard`、`TDropdown` |
|
||||
| 运行概况 | 健康、端点、兼容性、活动 revision | `TDescriptions`、`TProgress`、`TTag` |
|
||||
| 版本与升级 | revision、校验、升级包、回滚 | `TTable`、`TUpload`、`TDialog` |
|
||||
| 配置 | 服务地址、菜单地址、敏感配置提示 | `TForm`、`TInput`、`TAlert` |
|
||||
| 菜单接入 | 菜单声明、预览、应用 | `TDescriptions`、`TDialog`、`TButton` |
|
||||
| 操作历史 | 当前插件或全局审计记录、详情 | `TTable`、`TDrawer`、`TTag` |
|
||||
| 插件市场 | 受控索引、版本、兼容性、哈希、入库 | `TCard`、`TTag`、`TButton` |
|
||||
|
||||
现有接口保持不变:
|
||||
|
||||
```text
|
||||
GET /api/plugins
|
||||
POST /api/plugins/upload
|
||||
POST /api/plugins/:id/enable
|
||||
POST /api/plugins/:id/disable
|
||||
POST /api/plugins/:id/test
|
||||
POST /api/plugins/:id/ui-session
|
||||
DELETE /api/plugins/:id
|
||||
GET/PUT /api/plugins/:id/config
|
||||
GET /api/marketplace
|
||||
GET /api/audit
|
||||
```
|
||||
|
||||
### 3.2 订阅业务模块
|
||||
|
||||
入口:`plugins/subscription-admin/ui/index.html`、`plugins/subscription-admin/ui/app.js`
|
||||
|
||||
订阅管理是可选业务模块,由 Plugin Admin 安装、启用和卸载。它复用控制面的登录态、导航、CSRF 和管理员权限,不再出现第二个登录页。模块后端可以继续作为独立进程运行,但浏览器只访问控制面提供的同源模块路由。
|
||||
|
||||
```text
|
||||
#/modules/subscription/overview
|
||||
#/modules/subscription/plans
|
||||
#/modules/subscription/subscriptions
|
||||
#/modules/subscription/audit
|
||||
```
|
||||
|
||||
页面职责:
|
||||
|
||||
| 页面 | 主要内容 | TDesign 组件方向 |
|
||||
| --- | --- | --- |
|
||||
| 概览 | Core 连接状态、套餐数量、订阅数量、余额查询 | `TStatistic`、`TCard`、`TAlert` |
|
||||
| 套餐 | Core 返回的套餐目录和覆盖分组 | `TTable`、`TTag`、`TEmpty` |
|
||||
| 用户订阅 | 用户、状态、分组、服务端分页和筛选 | `TForm`、`TSelect`、`TTable`、`TPagination` |
|
||||
| 操作记录 | 插件会话和只读查询审计 | `TTable`、`TDrawer` |
|
||||
| 模块设置 | 运行模式、allowlist、凭据状态 | 插件详情的“配置/运行概况”页,不重复做模块登录 |
|
||||
|
||||
订阅模块现有 Core 代理接口、分页参数、状态枚举和只读边界不变。余额购买、续费、撤销等后续能力仍应通过模块后端 API 增量加入,不能在此次 UI 换肤时偷偷改变业务语义。
|
||||
|
||||
## 4. 推荐前端目录
|
||||
|
||||
不要复用官方 Core 的 `frontend/` 目录。建议在独立插件仓库新增一个统一 TDesign 控制面,并把订阅前端作为可挂载模块构建:
|
||||
|
||||
```text
|
||||
plugins/
|
||||
├── plugin-admin/
|
||||
│ ├── ui/ # 统一 TDesign 应用、登录、导航和模块路由
|
||||
│ └── ui-modules/ # 订阅等业务模块的 Vue/TS 源码
|
||||
└── subscription-admin/
|
||||
├── service/ # 可继续独立运行的业务后端
|
||||
└── ui-module/ # 被 plugin-admin 挂载的订阅模块,不包含登录页
|
||||
```
|
||||
|
||||
订阅模块可以独立打包和回滚,但它的浏览器入口由 Plugin Admin 统一托管。Go 的 `embed`、静态文件路径和部署脚本必须在插件仓库内同步,不与 Core 构建耦合。
|
||||
|
||||
共享但不跨 Core 的内容:
|
||||
|
||||
- TDesign 主题 token、字体、间距、移动端断点
|
||||
- 请求封装、插件会话/CSRF、错误提示
|
||||
- ECharts 按需注册和 resize composable
|
||||
- TDesign Icons 的图标命名约定
|
||||
|
||||
## 5. 不可改变的系统边界
|
||||
|
||||
```text
|
||||
浏览器
|
||||
↓ 一次登录:Plugin Admin Session Cookie + CSRF
|
||||
统一 Plugin Admin TDesign Shell
|
||||
├─ 插件管理页面
|
||||
└─ 订阅业务模块路由
|
||||
↓ 控制面同源 BFF / 内部服务调用
|
||||
独立插件 Go 服务(订阅模块)
|
||||
↓ 服务端 allowlist + Bearer Core token
|
||||
官方 Sub2API Core REST API
|
||||
```
|
||||
|
||||
- 浏览器不能拿到 Core access token、refresh token、Admin Key 或服务密钥。
|
||||
- 所有模块前端只能使用统一控制面的会话和模块 API,不能直连 Core API、PostgreSQL 或 Redis。
|
||||
- 订阅模块不得再实现 `/login`、独立 Cookie 或第二套管理员会话。
|
||||
- `custom_menu_items` 和 Core `/custom/:id` iframe 注入方式保持不变。
|
||||
- Plugin Admin 的配置 iframe `postMessage` bridge、`ui-session`、step-up 和来源校验保持不变。
|
||||
- Core 的源码仓库、版本文件、数据库迁移和前端页面不在本次改动范围内。
|
||||
|
||||
## 6. 分阶段实施
|
||||
|
||||
### 阶段 A:基座复制与边界固定
|
||||
|
||||
从 starter 复制应用骨架,替换 demo 路由、mock 数据、示例登录和品牌资源;保留 TDesign Layout、主题切换、Pinia、Vue Router、ECharts 基础能力。先让统一控制面独立构建,并定义模块注册契约。
|
||||
|
||||
### 阶段 B:Plugin Admin
|
||||
|
||||
先完成登录和应用壳,再按“概览 → 已安装 → 详情页签 → 市场 → 审计”迁移。所有 mutation 仍由现有 Go endpoint 执行,前端只负责表单、状态和操作确认。
|
||||
|
||||
### 阶段 C:订阅业务模块
|
||||
|
||||
订阅 UI 作为控制面的一个模块挂载,复用同一套会话、导航、视觉 token 和错误处理。先迁移概览、套餐、订阅列表和操作记录,服务端分页/筛选参数保持原样。订阅后端是否独立进程不影响前端只有一次登录。
|
||||
|
||||
### 阶段 D:图表与响应式
|
||||
|
||||
只在概览和需要趋势的页面引入 ECharts;图表容器使用固定最小高度、`resize` observer 和按需导入,避免页面被超长 canvas 撑开。列表在移动端只允许表格容器横向滚动,页面主体不得横向溢出。
|
||||
|
||||
### 阶段 E:接入验收
|
||||
|
||||
验证统一控制面一次登录后进入插件管理和订阅模块、Core 菜单 iframe、会话过期、CSRF、管理员权限、插件启停、配置 bridge、市场入库和审计链路。确认官方 Core 工作区没有任何变更。
|
||||
|
||||
## 7. 主要风险与处理
|
||||
|
||||
1. **starter 自带 mock/示例权限**:全部删除,改为 Plugin Admin 会话和管理员权限;不把 starter 的演示用户带入生产。
|
||||
2. **旧 UI 是原生 HTML/JS**:不要强行在同一页面混用原生模板和 TDesign;按应用整体迁移,减少样式优先级冲突。
|
||||
3. **配置 iframe bridge**:只能替换外层视觉,消息名称、token、来源校验、超时和 step-up 语义不变。
|
||||
4. **表格移动端**:使用固定列/可滚动列的明确容器,禁止给 `body` 或整个页面设置横向滚动。
|
||||
5. **图标包选择**:统一 `tdesign-icons-vue-next`;它提供线性图标,不再额外引入未知的 `line-icons` 包。
|
||||
6. **模块登录分裂**:订阅模块不得复制登录页、Cookie 或权限判断;模块调用统一控制面 BFF,由控制面把管理员身份传给订阅服务。
|
||||
7. **Core 更新兼容性**:插件只依赖已声明的 HTTP allowlist 和响应 DTO,Core 更新时只做 API 契约兼容检查。
|
||||
|
||||
## 8. 第一版验收标准
|
||||
|
||||
- 官方 Core `/Users/qiu/Desktop/Sub2API` 保持干净,版本和源码不被修改。
|
||||
- 统一 Plugin Admin 前端能独立 `npm run build`,不依赖 Core 的 Vite 配置;订阅模块以模块产物或受控动态入口挂载。
|
||||
- 管理员只登录一次即可访问插件控制面和已启用订阅模块;直接刷新模块路由仍保持同一插件会话,不把凭据写入 URL。
|
||||
- 425px、768px、1440px 下无页面级横向溢出;表格需要横向查看时只滚动表格容器。
|
||||
- 所有管理员 mutation 仍经过插件后端的会话、CSRF、step-up 和幂等校验。
|
||||
- DOM、网络响应、日志和 URL 不出现 Core token、Admin Key、密码或服务密钥。
|
||||
- ECharts 图表只展示有数据的系列,容器尺寸稳定,窗口变化后可重绘。
|
||||
|
||||
## 9. 当前状态
|
||||
|
||||
已完成:
|
||||
|
||||
- 拉取官方 TDesign Vue Next starter。
|
||||
- 固定 starter 基线和依赖版本。
|
||||
- 完成 starter 基线构建检查。
|
||||
- 完成独立插件页面、API、组件和迁移边界评估。
|
||||
- 根据反馈修正页面边界:订阅从“独立后台”改为统一插件控制面内的业务模块,不再单独登录。
|
||||
|
||||
已完成第一版落地:
|
||||
|
||||
- `plugins/plugin-admin/ui-vue` 基于 TDesign Vue Next starter 建立独立 Vue 3/Vite 应用。
|
||||
- Plugin Admin 登录、统一 Shell、概览、插件列表/详情、市场和审计页面已迁移,图标统一使用 `tdesign-icons-vue-next`,概览趋势图使用 ECharts。
|
||||
- 订阅作为 Shell 内的 `/modules/subscription/*` 业务模块挂载,不再渲染第二个登录页;控制面新增同源、allowlist 约束的订阅只读 BFF。
|
||||
- Go 静态资源支持挂载路径、SPA 回退和路径穿越拒绝;`build-ui.sh` 负责可重复构建并复制嵌入产物。
|
||||
- 已完成 TypeScript/Vite、Go 单元、API 契约和 425/900/1440 多视口浏览器验收;下一轮可继续补充真实业务写操作,但不改变 Core 边界。
|
||||
Reference in New Issue
Block a user