3113479cd2
Settings → AI tools → Profiles holds one tab per tool: Claude Code (the cc-profiles panel, moved out of the Claude Code page's modal — its button now links here), Codex (the whole $CODEX_HOME/auth.json) and Oh My Pi (`omp auth export` / `omp auth import -`, secrets on stdin; omp's credential tables are emptied first because import merges, and a failed import puts the outgoing set back). Antigravity is listed as not switchable: agy keeps its login in the OS keyring. server/lib/tool-profiles.js + /api/tool-profiles keep cc-profiles' rules (one login = one profile, the outgoing login is re-saved before a switch, an unsaved one refuses it, the live login's profile is not deletable) and never return a token; /tool-profiles joins ADMIN_WRITE_PREFIXES and STAFF_ONLY_PREFIXES, writes are same-origin only. The account pool takes a saved Codex profile too (moved in like a Claude one, GET /api/accounts/profiles?tool=codex). Each Claude/Codex profile has a "Move to pool" link (/admin?poolAdd=&poolProfile= opens the pool's Add dialog on it), Settings shows the pool's size, and the pool card links back.
84 lines
2.5 KiB
JavaScript
84 lines
2.5 KiB
JavaScript
/**
|
|
* @file tool-profiles.js
|
|
* @description `/api/tool-profiles` — the login profiles of the AI tools other
|
|
* than Claude Code (server/lib/tool-profiles.js; Claude's stay under
|
|
* `/api/cc-config/profiles`, and Settings → AI tools → Profiles shows both).
|
|
* `GET /` lists every covered tool (`supported: false` + `reason` for one that
|
|
* cannot be switched); `POST /:tool` saves the live login, `PATCH /:tool`
|
|
* renames, `POST /:tool/activate` switches, `DELETE /:tool` deletes — the
|
|
* profile name always in the body. Identity metadata only, never a token. The
|
|
* writes change the HOST's own login, so `/tool-profiles` is in
|
|
* ADMIN_WRITE_PREFIXES and STAFF_ONLY_PREFIXES like `/cc-config`.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const { Router } = require("express");
|
|
const profiles = require("../lib/tool-profiles");
|
|
const { sameOriginGuard } = require("../lib/same-origin");
|
|
|
|
const router = Router();
|
|
// Every write swaps a live login: never from a cross-site form.
|
|
router.use((req, res, next) => (req.method === "GET" ? next() : sameOriginGuard(req, res, next)));
|
|
|
|
const STATUS = {
|
|
EBADNAME: 400,
|
|
EBADLABEL: 400,
|
|
EBADTOOL: 404,
|
|
ENOTFOUND: 404,
|
|
EUNSUPPORTED: 409,
|
|
ENOTINSTALLED: 409,
|
|
EBADJSON: 409,
|
|
EACTIVE: 409,
|
|
EUNSAVED: 409,
|
|
ENOAUTH: 409,
|
|
EDUPLICATE: 409,
|
|
ELABELTAKEN: 409,
|
|
EOMP: 502,
|
|
};
|
|
|
|
const handle = (fn) => async (req, res) => {
|
|
try {
|
|
res.json(await fn(req));
|
|
} catch (e) {
|
|
res
|
|
.status(STATUS[e.code] || 500)
|
|
.json({ error: { code: e.code || "EINTERNAL", message: e.message } });
|
|
}
|
|
};
|
|
|
|
router.get(
|
|
"/",
|
|
handle(() => ({
|
|
tools: profiles.tools().map((t) => {
|
|
if (!t.supported) return { ...t, items: [], active: null };
|
|
try {
|
|
return { ...t, ...profiles.listProfiles(t.tool), error: null };
|
|
} catch (e) {
|
|
// One tool's failure (omp not installed) never hides the others.
|
|
return { ...t, items: [], active: null, error: { code: e.code, message: e.message } };
|
|
}
|
|
}),
|
|
}))
|
|
);
|
|
|
|
const name = (req) => (req.body || {}).name;
|
|
|
|
router.post(
|
|
"/:tool",
|
|
handle((req) => profiles.saveProfile(req.params.tool, name(req)))
|
|
);
|
|
router.patch(
|
|
"/:tool",
|
|
handle((req) => profiles.renameProfile(req.params.tool, name(req), (req.body || {}).label))
|
|
);
|
|
router.post(
|
|
"/:tool/activate",
|
|
handle((req) => profiles.activateProfile(req.params.tool, name(req)))
|
|
);
|
|
router.delete(
|
|
"/:tool",
|
|
handle((req) => profiles.deleteProfile(req.params.tool, name(req)))
|
|
);
|
|
|
|
module.exports = router;
|