diff --git a/docs/changelog/2026-06-21-btpanel-batch-bootstrap-ui.md b/docs/changelog/2026-06-21-btpanel-batch-bootstrap-ui.md new file mode 100644 index 00000000..d5a33385 --- /dev/null +++ b/docs/changelog/2026-06-21-btpanel-batch-bootstrap-ui.md @@ -0,0 +1,29 @@ +# Changelog — 宝塔批量 SSH 获取 API(2026-06-21) + +**日期**:2026-06-21 + +## 摘要 + +服务器列表页增加批量/全量「SSH 获取 API」;未配置 API 时禁用一键登录(避免 404 临时链)。 + +## 动机 + +- 2000+ 子机需先写入 `api.json` 才能稳定一键登录 +- 原批量入口仅在连接配置页,不易发现 +- `all_unconfigured` 服务端解析未配置列表,突破请求体 500 台上限 + +## 涉及文件 + +- `server/api/btpanel_schemas.py` — `all_unconfigured` +- `server/application/services/btpanel_service.py` +- `frontend/src/pages/btpanel/BtPanelLoginPage.vue` +- `frontend/src/pages/btpanel/BtPanelSettingsPage.vue` +- `frontend/src/api/btpanel.ts` + +## 验证 + +```bash +.venv/bin/pytest tests/test_btpanel_*.py -q +cd frontend && npx vite build +# 服务器列表 → 全部未配置 / 勾选 → 查看批量任务进度 +``` diff --git a/frontend/src/api/btpanel.ts b/frontend/src/api/btpanel.ts index b021fa9f..638afdd6 100644 --- a/frontend/src/api/btpanel.ts +++ b/frontend/src/api/btpanel.ts @@ -85,10 +85,16 @@ export function bootstrapBtServer(serverId: number) { return api(`/btpanel/servers/${serverId}/bootstrap`, { method: 'POST', body: '{}' }) } -export function batchBootstrapBtServers(serverIds: number[]) { +export function batchBootstrapBtServers( + serverIds: number[], + options?: { allUnconfigured?: boolean }, +) { return api('/btpanel/servers/batch-bootstrap', { method: 'POST', - body: JSON.stringify({ server_ids: serverIds }), + body: JSON.stringify({ + server_ids: serverIds, + all_unconfigured: options?.allUnconfigured ?? false, + }), }) } diff --git a/frontend/src/pages/btpanel/BtPanelLoginPage.vue b/frontend/src/pages/btpanel/BtPanelLoginPage.vue index 834100fe..f176d92c 100644 --- a/frontend/src/pages/btpanel/BtPanelLoginPage.vue +++ b/frontend/src/pages/btpanel/BtPanelLoginPage.vue @@ -4,10 +4,30 @@

宝塔 · 服务器列表

- 选择服务器一键打开宝塔面板(临时链接约 3 小时有效) + 先批量 SSH 获取 API,再一键登录;未配置 API 时登录链接易 404

+ + 全部未配置({{ unconfiguredCount }}) + + + 选中获取 API({{ selected.length }}) + + + + + +

+ 将对 {{ batchTargetCount }} 台服务器执行 SSH 引导:开启子机 + api.json、写入 Nexus 加密凭据、追加中心 IP 白名单。 +

+

+ 前提:子机 root SSH 可达、已装宝塔(未装会标记为安装中并每 5 分钟重试)。 + 连接配置中需已设置「中心调用 IP」。 +

+
+ + + 取消 + 开始 + +
+
diff --git a/frontend/src/pages/btpanel/BtPanelSettingsPage.vue b/frontend/src/pages/btpanel/BtPanelSettingsPage.vue index 439bc6a4..605215d3 100644 --- a/frontend/src/pages/btpanel/BtPanelSettingsPage.vue +++ b/frontend/src/pages/btpanel/BtPanelSettingsPage.vue @@ -311,11 +311,10 @@ async function runBootstrap(id: number) { } async function confirmBatchBootstrap() { - const ids = unconfiguredIds.value - if (!ids.length) return + if (!unconfiguredIds.value.length) return batchLoading.value = true try { - const job = await batchBootstrapBtServers(ids) + const job = await batchBootstrapBtServers([], { allUnconfigured: true }) batchConfirmOpen.value = false window.$snackbar?.(`已启动批量任务 #${job.job_id}`, 'success') } catch (e: unknown) { diff --git a/server/api/btpanel.py b/server/api/btpanel.py index 85c2c8b9..2de839d4 100644 --- a/server/api/btpanel.py +++ b/server/api/btpanel.py @@ -151,6 +151,7 @@ async def batch_bootstrap_bt_servers( try: return await _svc(db).batch_bootstrap( body.server_ids, + all_unconfigured=body.all_unconfigured, admin_username=admin.username, ip_address=_client_ip(request), ) diff --git a/server/api/btpanel_schemas.py b/server/api/btpanel_schemas.py index bb540449..8ff3afba 100644 --- a/server/api/btpanel_schemas.py +++ b/server/api/btpanel_schemas.py @@ -2,7 +2,7 @@ from typing import Optional -from pydantic import BaseModel, Field +from pydantic import BaseModel, Field, model_validator class BtPanelConfigUpdate(BaseModel): @@ -18,7 +18,14 @@ class BtPanelGlobalSettingsUpdate(BaseModel): class BtPanelBatchBootstrap(BaseModel): - server_ids: list[int] = Field(..., min_length=1, max_length=500) + server_ids: list[int] = Field(default_factory=list, max_length=500) + all_unconfigured: bool = False + + @model_validator(mode="after") + def _require_targets(self) -> "BtPanelBatchBootstrap": + if not self.all_unconfigured and not self.server_ids: + raise ValueError("server_ids 不能为空,或设置 all_unconfigured=true") + return self class BtPanelSiteAction(BaseModel): diff --git a/server/application/services/btpanel_service.py b/server/application/services/btpanel_service.py index 969db4da..7675921f 100644 --- a/server/application/services/btpanel_service.py +++ b/server/application/services/btpanel_service.py @@ -330,14 +330,19 @@ class BtPanelService: self, server_ids: list[int], *, + all_unconfigured: bool = False, admin_username: str, ip_address: str | None, ) -> dict[str, Any]: from server.application.services.server_batch_service import start_batch_job - ids = [int(x) for x in server_ids if x] + if all_unconfigured: + statuses = await self.list_server_statuses() + ids = [int(s["id"]) for s in statuses if not s.get("configured")] + else: + ids = [int(x) for x in server_ids if x] if not ids: - raise ValueError("server_ids 不能为空") + raise ValueError("没有可批量初始化的服务器(均已配置或未录入)") return await start_batch_job( op="bt-panel-bootstrap", server_ids=ids,