From e380fcfcf43c1ee101c068ee041e3048bedcc688 Mon Sep 17 00:00:00 2001 From: wangzhizhou <824219521@qq.com> Date: Wed, 28 Oct 2026 13:22:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(actions):=20=E5=BC=BA=E5=88=B6=E7=BB=88?= =?UTF-8?q?=E6=AD=A2=E5=AE=9E=E4=BE=8B=EF=BC=88kill=EF=BC=89+=20=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E5=81=9C=E6=AD=A2=E6=98=AF=E5=BC=82=E6=AD=A5=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 面板的「停止」是**异步**的:`/stop` 立即返回 200,实例随后进 `STOPPING`,服务器读完档才退出。 存档很大 / 插件死锁 / JVM 假死时会一直停在 `STOPPING` —— 那时唯一的出路是强杀,而本包之前没封装。 新增 `GET /api/protected_instance/kill`(operationId `killInstance`): - 官方 apidoc 已收录(`api_instance.md` 的 `GET /api/protected_instance/kill`), `check-apidoc-drift.sh` 是 `[ok]`,不需要进白名单; - 面板侧 `router.all` + 前端用 GET;内部转发守护进程 `instance/kill` → `execPreset("kill")`, 面板超时 `INSTANCE_KILL_TIMEOUT = 30000`(30 秒,比其它动作长); - 权限与其它 `protected_instance` 端点一致:**普通用户也能调**(只要实例属于你)。 两处 spec 说明同步修正: - `/stop` 的描述原来写「要强制杀进程用 `kill`(本包未封装)」→ 现在已封装,并补上 「停止是异步的,可能长时间停在 `STOPPING`」这个关键事实; - `/kill` 的 200 **不声明响应体**:面板回的是 `{status,data,time}` 外壳,`data` 结构各版本不一致 (10.18.0 实测是 `{instanceUuid}`),声明具体 schema 会因多/少字段解码失败 (踩过:`typeMismatch String/dictionary`)。我们只关心"有没有被拒绝"(403/500)。 顺带:YAML 描述里含 `{ … }` 必须加引号,否则被当成 flow mapping,`openapi-generator` 直接报 `mapping values are not allowed in this context`。 测试:`lifecycleActionsCarryDaemonAndInstance` 覆盖 open/stop/restart/**kill** 的路径与查询参数。 --- README.md | 1 + Sources/MCSMAPI/openapi.yml | 56 ++++++++++++++++++++- Tests/MCSMAPITests/ClientRequestTests.swift | 3 ++ 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index fc4a92d..6fe192d 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ let text = try MCSMResponseEnvelope.checkedText(raw) // 或 unwrapText(raw) | `GET /api/protected_instance/open` | 实例的主人(**普通用户也行**) | 启动。面板立即返回,状态随后变化 | | `GET /api/protected_instance/stop` | 同上 | 停止(发送 `stop`,不强制杀进程) | | `GET /api/protected_instance/restart` | 同上 | 重启 | +| `GET /api/protected_instance/kill` | 同上 | **强制终止**(守护进程直接杀进程,不给保存机会)。面板侧超时 30 秒 | | `GET /api/protected_instance/command` | 同上 | 命令走查询参数;`/` 开头是 MC 命令,不带 `/` 是控制台命令 | | `GET /api/protected_instance/outputlog` | 同上 | 返回 `text/plain` 的**日志尾部**,可能从半行开始。体里仍是 `{status,data,time}` 外壳 → 用 `MCSMResponseEnvelope` 解 | | `POST /api/protected_instance/stream_channel` | 同上 | 控制台**实时流**的第一步:拿 `addr` + 一次性口令,然后连那台 socket.io 发 `stream/auth`。见下节 | diff --git a/Sources/MCSMAPI/openapi.yml b/Sources/MCSMAPI/openapi.yml index 511ba45..3b0f5b2 100644 --- a/Sources/MCSMAPI/openapi.yml +++ b/Sources/MCSMAPI/openapi.yml @@ -273,7 +273,10 @@ paths: operationId: stopInstance summary: 停止实例 description: | - 对应面板「停止」(会给实例发 `stop`,等它自己退出)。要强制杀进程用 `kill`(本包未封装)。 + 对应面板「停止」(会给实例发 `stop`,等它自己退出)。要强制杀进程用 `kill`。 + + ⚠️ **停止是异步的**:面板立即返回 200,实例随后进入 `STOPPING`;服务器卡住/存档很慢时 + 可能长时间停在 `STOPPING`,此时用 `kill` 强杀。 **普通用户也能调用**(`/protected_instance` 一组只要"这台实例属于你",不要求管理员)。 面板对成功/失败都返回 200 + 空的 JSON 对象或节点回执,所以本 spec 不声明 200 的响应体: @@ -313,6 +316,57 @@ paths: text/plain: schema: type: string + /api/protected_instance/kill: + get: + operationId: killInstance + summary: 强制终止实例(强杀进程) + description: | + 对应面板的「强制终止」:守护进程直接 `kill` 掉服务器进程,**不给服务器保存/退出的机会** + (没存盘的进度会丢)。用于"停止卡住"的场合。 + + 面板源码:`panel/src/app/routers/instance_operate_router.ts` 的 `/kill`(`router.all`, + 前端用 `GET`)→ 守护进程 `instance/kill` → `instance.execPreset("kill")`; + 面板侧超时 `INSTANCE_KILL_TIMEOUT = 30000`(30 秒,注意比普通动作长)。 + + 官方 apidoc 已收录(`api_instance.md` 的 `GET /api/protected_instance/kill`)。 + 权限与其它 `protected_instance` 端点一致:**普通用户也能调**(只要求这台实例属于你)。 + parameters: + - name: daemonId + in: query + required: true + schema: + type: string + - name: uuid + in: query + required: true + description: 实例 UUID(**不是**昵称) + schema: + type: string + responses: + "200": + # 只声明"受理了",不声明 200 的响应体:面板回的是 `{status,data,time}` 外壳, + # 里面的 `data` 结构各版本不一致(实测 10.18.0 是 `{instanceUuid}`), + # 声明成具体 schema 反而会因为多/少字段解码失败(踩过:typeMismatch String/dictionary)。 + # 我们只关心"请求有没有被拒绝"(403/500),与 `stop` / `restart` 一致。 + description: 已受理(面板立即返回;实例状态随后变化,需重新查询) + "403": + description: API Key 权限不足 / 未开启 API Key 功能 / 不是这台实例的主人 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + text/plain: + schema: + type: string + "500": + description: 面板内部错误 / 节点(守护进程)返回了错误 + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + text/plain: + schema: + type: string /api/protected_instance/restart: get: operationId: restartInstance diff --git a/Tests/MCSMAPITests/ClientRequestTests.swift b/Tests/MCSMAPITests/ClientRequestTests.swift index 0957889..f8fa076 100644 --- a/Tests/MCSMAPITests/ClientRequestTests.swift +++ b/Tests/MCSMAPITests/ClientRequestTests.swift @@ -49,6 +49,7 @@ final class ClientRequestTests { ("openInstance", "/api/protected_instance/open"), ("stopInstance", "/api/protected_instance/stop"), ("restartInstance", "/api/protected_instance/restart"), + ("killInstance", "/api/protected_instance/kill"), ] { let capture = RequestCapture() let client = makeClient(capture: capture) @@ -57,6 +58,8 @@ final class ClientRequestTests { _ = try await client.openInstance(query: .init(daemonId: "d1", uuid: "i1")) case "stopInstance": _ = try await client.stopInstance(query: .init(daemonId: "d1", uuid: "i1")) + case "killInstance": + _ = try await client.killInstance(query: .init(daemonId: "d1", uuid: "i1")) default: _ = try await client.restartInstance(query: .init(daemonId: "d1", uuid: "i1")) }