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:
@@ -24,6 +24,9 @@
|
||||
*
|
||||
* `GET /9router` only says whether a `9router` binary is on the server's PATH,
|
||||
* so the page can offer a link to its own config UI; it never runs it.
|
||||
* `GET|POST /9router/update` reports on / starts the **Update 9router** job
|
||||
* (`server/lib/nine-router-update.js`: shut down, `npm install -g`, start again
|
||||
* hidden) — the host's own 9router, so starting one is an admin's only.
|
||||
*
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
@@ -36,6 +39,7 @@ const { refreshProfileFiles, removeProfileFiles } = require("../lib/proxy-apply"
|
||||
const { sameOriginGuard } = require("../lib/same-origin");
|
||||
const { which } = require("../lib/ai-tools");
|
||||
const { shellPath } = require("../lib/shell-path");
|
||||
const nineRouterUpdate = require("../lib/nine-router-update");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -49,6 +53,13 @@ const ERR_TO_STATUS = {
|
||||
EBUILTIN: 409,
|
||||
ENOTFOUND: 404,
|
||||
EADMIN: 403,
|
||||
EBUSY: 409,
|
||||
ENOTINSTALLED: 409,
|
||||
EUPTODATE: 409,
|
||||
ENOLATEST: 502,
|
||||
ENONPM: 409,
|
||||
ENOPERM: 409,
|
||||
EUNSUPPORTED: 409,
|
||||
};
|
||||
|
||||
const notFound = (name) =>
|
||||
@@ -68,7 +79,9 @@ function assertMayWrite(req, name) {
|
||||
|
||||
function fail(res, e) {
|
||||
const status = ERR_TO_STATUS[e.code] || 500;
|
||||
return res.status(status).json({ error: { code: e.code || "EINTERNAL", message: e.message } });
|
||||
const error = { code: e.code || "EINTERNAL", message: e.message };
|
||||
if (e.command) error.command = e.command;
|
||||
return res.status(status).json({ error });
|
||||
}
|
||||
|
||||
// GET /api/ai-proxies — every profile, built-in `native` first. Never a key.
|
||||
@@ -87,6 +100,29 @@ router.get("/9router", (_req, res) => {
|
||||
res.json({ installed: Boolean(which("9router", pathStr)) });
|
||||
});
|
||||
|
||||
// GET /api/ai-proxies/9router/update — installed vs latest version, whether
|
||||
// it runs (and under what), why an update would be refused now (`blocker`),
|
||||
// and the last/current update job. `?fresh=1` re-asks the npm registry.
|
||||
router.get("/9router/update", async (req, res) => {
|
||||
try {
|
||||
res.json(await nineRouterUpdate.status({ fresh: req.query.fresh === "1" }));
|
||||
} catch (e) {
|
||||
return fail(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/ai-proxies/9router/update — start the job; 202 with it. Poll the
|
||||
// GET above for its steps. Admin only (also in auth.js ADMIN_WRITE_PREFIXES).
|
||||
router.post("/9router/update", sameOriginGuard, async (req, res) => {
|
||||
try {
|
||||
if (req.user && req.user.role !== "admin")
|
||||
throw Object.assign(new Error("only an admin may update 9router"), { code: "EADMIN" });
|
||||
res.status(202).json(await nineRouterUpdate.start());
|
||||
} catch (e) {
|
||||
return fail(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/ai-proxies — body: { label, baseUrl, apiKey?, model, models?,
|
||||
// behavesAs?, scope? }. `scope` is "shared" (an admin's default) or "personal"
|
||||
// (a member's only choice); local mode has no user and always writes shared.
|
||||
|
||||
Reference in New Issue
Block a user