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")) }