feat(ai-proxy): Update 9router button — shut down, npm update, relaunch hidden

Settings → AI Proxy gains an admin-only Update 9router button beside
Config 9router. One in-memory job (server/lib/nine-router-update.js) stops
9router (its user unit, else this account's own 9router processes), runs
npm install -g --prefix <its prefix> 9router@latest (sudo -n only when
sudoers already allows it, probed before anything stops), then relaunches
it hidden only if it was running. A failed update still relaunches the old
version. Nothing in the request names a command, path or package.
This commit is contained in:
2026-10-26 17:00:33 +07:00
parent 631ea96f19
commit 569b68ec98
13 changed files with 1318 additions and 2 deletions
+24
View File
@@ -324,6 +324,30 @@ const paths = {
},
},
},
"/api/ai-proxies/9router/update": {
get: {
tags: ["AI Proxies"],
summary: "9router update status",
description:
"Installed vs latest npm version of the host's 9router, whether it runs (`managedBy`: the `9router.service` user unit or a plain process), `blocker` — why a start would be refused now (`ENOTINSTALLED`, `EUPTODATE`, `ENOLATEST`, `ENONPM`, `ENOPERM` with the `command` to run by hand, `EUNSUPPORTED`) — and the current/last job. `?fresh=1` re-asks the registry (cached 10 min).",
operationId: "getAiProxy9routerUpdate",
parameters: [{ name: "fresh", in: "query", schema: { type: "string", enum: ["1"] } }],
responses: { 200: json({ type: "object" }, "Status.") },
},
post: {
tags: ["AI Proxies"],
summary: "Update 9router",
description:
"Starts the one in-memory job: shut 9router down (stop the unit, or SIGTERM→SIGKILL its processes until the port is free), `npm install -g --prefix <its prefix> 9router@latest` (under `sudo -n` only when the prefix is not writable and sudo needs no password), then — only if it was running — start it again hidden (`--tray --no-browser --skip-update`). A failed update still relaunches. Admin only; same-origin guarded. Poll the GET for the steps.",
operationId: "updateAiProxy9router",
responses: {
202: json({ type: "object" }, "The job, running."),
403: errResp("`EADMIN`."),
409: errResp("`EBUSY` or the status `blocker` code."),
502: errResp("`ENOLATEST` — the npm registry did not answer."),
},
},
},
"/api/ai-proxies/test": {
post: {
tags: ["AI Proxies"],