mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
289 lines
14 KiB
JavaScript
289 lines
14 KiB
JavaScript
// Public API barrel — all DB functions
|
|
import { getAdapter } from "./driver.js";
|
|
import { stringifyJson, parseJson } from "./helpers/jsonCol.js";
|
|
import { normalizeCliToolConfig, isPersistableCliTool } from "@/shared/constants/cliToolConfig.js";
|
|
import {
|
|
USER_TOKEN_LIMIT_PROVIDER_IDS,
|
|
USER_TOKEN_LIMIT_WINDOW_IDS,
|
|
} from "open-sse/config/userTokenLimits.js";
|
|
|
|
const userTokenLimitProviderSet = new Set(USER_TOKEN_LIMIT_PROVIDER_IDS);
|
|
const userTokenLimitWindowSet = new Set(USER_TOKEN_LIMIT_WINDOW_IDS);
|
|
|
|
// Settings
|
|
export {
|
|
getSettings, updateSettings, updateComboStrategy, isCloudEnabled, getCloudUrl, exportSettings,
|
|
} from "./repos/settingsRepo.js";
|
|
|
|
// Users
|
|
export {
|
|
getUsers, getUserById, getUserByUsername, createUser, updateUser, deleteUser,
|
|
countActiveAdmins, verifyUserCredentials, verifyUserPassword, resetAdminPassword,
|
|
} from "./repos/usersRepo.js";
|
|
|
|
// Provider connections
|
|
export {
|
|
getProviderConnections, getProviderConnectionById,
|
|
createProviderConnection, updateProviderConnection,
|
|
deleteProviderConnection, deleteProviderConnectionsByProvider,
|
|
reorderProviderConnections, cleanupProviderConnections, countProviderConnectionsByOwnerId,
|
|
} from "./repos/connectionsRepo.js";
|
|
|
|
// Provider nodes
|
|
export {
|
|
getProviderNodes, getProviderNodeById,
|
|
createProviderNode, updateProviderNode, deleteProviderNode,
|
|
} from "./repos/nodesRepo.js";
|
|
|
|
// Proxy pools
|
|
export {
|
|
getProxyPools, getProxyPoolById,
|
|
createProxyPool, updateProxyPool, deleteProxyPool,
|
|
} from "./repos/proxyPoolsRepo.js";
|
|
|
|
// API keys
|
|
export {
|
|
getApiKeys, getApiKeysByOwnerId, getApiKeyById, getApiKeyByIdAndOwnerId,
|
|
getApiKeyByKey, createApiKey, updateApiKey, deleteApiKey, validateApiKey,
|
|
} from "./repos/apiKeysRepo.js";
|
|
|
|
// Combos
|
|
export {
|
|
getCombos, getComboById, getComboByName,
|
|
createCombo, updateCombo, deleteCombo,
|
|
} from "./repos/combosRepo.js";
|
|
|
|
// Per-user CLI tool configurations
|
|
export {
|
|
getCliToolConfig, getCliToolConfigsByOwnerId,
|
|
upsertCliToolConfig, deleteCliToolConfigsByOwnerId,
|
|
} from "./repos/cliToolConfigsRepo.js";
|
|
|
|
// Per-user provider token limits
|
|
export {
|
|
createEmptyUserTokenLimits, getUserTokenLimits,
|
|
replaceUserTokenLimits, getUserProviderTokenUsageSince,
|
|
getUserProviderEarliestTokenUsageSince,
|
|
} from "./repos/userTokenLimitsRepo.js";
|
|
|
|
// Aliases (model + custom + mitm)
|
|
export {
|
|
getModelAliases, setModelAlias, deleteModelAlias,
|
|
getCustomModels, addCustomModel, deleteCustomModel,
|
|
getMitmAlias, setMitmAliasAll,
|
|
} from "./repos/aliasRepo.js";
|
|
|
|
// Pricing
|
|
export {
|
|
getPricing, getPricingForModel, updatePricing, resetPricing, resetAllPricing,
|
|
} from "./repos/pricingRepo.js";
|
|
|
|
// Disabled models
|
|
export {
|
|
getDisabledModels, getDisabledByProvider, disableModels, enableModels,
|
|
} from "./repos/disabledModelsRepo.js";
|
|
|
|
// Permanently deleted models
|
|
export {
|
|
getDeletedModels, isDeletedModel, isDeletedModelReference, deleteModelPermanently,
|
|
} from "./repos/deletedModelsRepo.js";
|
|
|
|
// Usage
|
|
export {
|
|
statsEmitter, trackPendingRequest, getActiveRequests,
|
|
saveRequestUsage, getUsageHistory, getUsageStats, getChartData,
|
|
appendRequestLog, getRecentLogs,
|
|
} from "./repos/usageRepo.js";
|
|
|
|
// Request details
|
|
export {
|
|
saveRequestDetail, getRequestDetails, getRequestDetailById, getDistinctProviders,
|
|
purgeRequestDetailBuffer,
|
|
} from "./repos/requestDetailsRepo.js";
|
|
|
|
// Export/import full DB
|
|
export async function exportDb() {
|
|
const db = await getAdapter();
|
|
const { exportSettings } = await import("./repos/settingsRepo.js");
|
|
|
|
const out = {
|
|
settings: await exportSettings(),
|
|
users: db.all(`SELECT id, username, password, role, isActive, createdAt, updatedAt FROM users`).map((r) => ({ ...r, isActive: r.isActive === 1 || r.isActive === true })),
|
|
providerConnections: db.all(`SELECT * FROM providerConnections`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, provider: r.provider, authType: r.authType, name: r.name, email: r.email, ownerId: r.ownerId, priority: r.priority, isActive: r.isActive === 1, createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
|
providerNodes: db.all(`SELECT * FROM providerNodes`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, type: r.type, name: r.name, createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
|
proxyPools: db.all(`SELECT * FROM proxyPools`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, isActive: r.isActive === 1, testStatus: r.testStatus, createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
|
apiKeys: db.all(`SELECT * FROM apiKeys`).map((r) => ({ id: r.id, key: r.key, name: r.name, machineId: r.machineId, ownerId: r.ownerId, isActive: r.isActive === 1, createdAt: r.createdAt })),
|
|
combos: db.all(`SELECT * FROM combos`).map((r) => ({ id: r.id, name: r.name, ownerId: r.ownerId, kind: r.kind, models: parseJson(r.models, []), createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
|
cliToolConfigs: db.all(`SELECT * FROM cliToolConfigs`).map((r) => ({ ownerId: r.ownerId, toolId: r.toolId, config: parseJson(r.data, {}), createdAt: r.createdAt, updatedAt: r.updatedAt })),
|
|
userTokenLimits: db.all(`SELECT userId, provider, windowType, tokenLimit, createdAt, updatedAt FROM userTokenLimits`),
|
|
modelAliases: {},
|
|
customModels: [],
|
|
mitmAlias: {},
|
|
pricing: {},
|
|
disabledModels: {},
|
|
deletedModels: {},
|
|
};
|
|
|
|
for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'modelAliases'`)) out.modelAliases[r.key] = parseJson(r.value);
|
|
for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'customModels'`)) out.customModels.push(parseJson(r.value));
|
|
for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'mitmAlias'`)) out.mitmAlias[r.key] = parseJson(r.value);
|
|
for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'pricing'`)) out.pricing[r.key] = parseJson(r.value);
|
|
for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'disabledModels'`)) out.disabledModels[r.key] = parseJson(r.value, []);
|
|
for (const r of db.all(`SELECT key, value FROM kv WHERE scope = 'deletedModels'`)) out.deletedModels[r.key] = parseJson(r.value, []);
|
|
|
|
return out;
|
|
}
|
|
|
|
export async function importDb(payload) {
|
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
throw new Error("Invalid database payload");
|
|
}
|
|
if (Array.isArray(payload.users)) {
|
|
const validUsers = payload.users.filter(
|
|
(user) => user?.id && user?.username && user?.password && ["admin", "user"].includes(user.role)
|
|
);
|
|
if (validUsers.length !== payload.users.length) throw new Error("Invalid users data in database payload");
|
|
if (!validUsers.some((user) => user.role === "admin" && user.isActive !== false)) {
|
|
throw new Error("Database import requires at least one active administrator");
|
|
}
|
|
}
|
|
const db = await getAdapter();
|
|
|
|
db.transaction(() => {
|
|
// Wipe all tables (keep _meta)
|
|
db.run(`DELETE FROM settings`);
|
|
db.run(`DELETE FROM cliToolConfigs`);
|
|
db.run(`DELETE FROM userTokenLimits`);
|
|
// Old backups predate multi-user authentication. Preserve the local
|
|
// administrator unless the payload explicitly carries a users array.
|
|
if (Array.isArray(payload.users)) db.run(`DELETE FROM users`);
|
|
db.run(`DELETE FROM providerConnections`);
|
|
db.run(`DELETE FROM providerNodes`);
|
|
db.run(`DELETE FROM proxyPools`);
|
|
db.run(`DELETE FROM apiKeys`);
|
|
db.run(`DELETE FROM combos`);
|
|
db.run(`DELETE FROM kv WHERE scope IN ('modelAliases', 'customModels', 'mitmAlias', 'pricing', 'disabledModels', 'deletedModels')`);
|
|
|
|
// Settings
|
|
if (payload.settings) {
|
|
db.run(`INSERT INTO settings(id, data) VALUES(1, ?) ON CONFLICT(id) DO UPDATE SET data = excluded.data`, [stringifyJson(payload.settings)]);
|
|
}
|
|
|
|
if (Array.isArray(payload.users)) {
|
|
for (const user of payload.users) {
|
|
if (!user?.id || !user?.username || !user?.password || !["admin", "user"].includes(user.role)) continue;
|
|
db.run(
|
|
`INSERT OR REPLACE INTO users(id, username, password, role, isActive, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?)`,
|
|
[user.id, user.username, user.password, user.role, user.isActive === false ? 0 : 1, user.createdAt || new Date().toISOString(), user.updatedAt || new Date().toISOString()]
|
|
);
|
|
}
|
|
}
|
|
|
|
const importedUserIds = new Set(db.all(`SELECT id FROM users`).map((user) => user.id));
|
|
for (const limit of payload.userTokenLimits || []) {
|
|
if (!importedUserIds.has(limit?.userId)) continue;
|
|
if (!userTokenLimitProviderSet.has(limit.provider) || !userTokenLimitWindowSet.has(limit.windowType)) continue;
|
|
const tokenLimit = Number(limit.tokenLimit);
|
|
if (!Number.isSafeInteger(tokenLimit) || tokenLimit <= 0) continue;
|
|
db.run(
|
|
`INSERT OR REPLACE INTO userTokenLimits(userId, provider, windowType, tokenLimit, createdAt, updatedAt)
|
|
VALUES(?, ?, ?, ?, ?, ?)`,
|
|
[limit.userId, limit.provider, limit.windowType, tokenLimit, limit.createdAt || new Date().toISOString(), limit.updatedAt || new Date().toISOString()],
|
|
);
|
|
}
|
|
|
|
const adminOwnerIds = new Set(
|
|
db.all(`SELECT id FROM users WHERE role = 'admin'`).map((user) => user.id),
|
|
);
|
|
const fallbackOwnerId = db.get(`SELECT id FROM users WHERE role = 'admin' ORDER BY createdAt ASC LIMIT 1`)?.id || null;
|
|
if (!fallbackOwnerId) throw new Error("Database import requires an administrator owner for provider connections");
|
|
for (const c of payload.providerConnections || []) {
|
|
const { id, provider, authType, name, email, ownerId, priority, isActive, createdAt, updatedAt, ...rest } = c;
|
|
if (ownerId && !adminOwnerIds.has(ownerId)) continue;
|
|
db.run(
|
|
`INSERT OR REPLACE INTO providerConnections(id, provider, authType, name, email, ownerId, priority, isActive, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
[id, provider, authType || "oauth", name || null, email || null, ownerId || fallbackOwnerId, priority || null, isActive === false ? 0 : 1, stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
|
|
);
|
|
}
|
|
for (const n of payload.providerNodes || []) {
|
|
const { id, type, name, createdAt, updatedAt, ...rest } = n;
|
|
db.run(
|
|
`INSERT OR REPLACE INTO providerNodes(id, type, name, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?)`,
|
|
[id, type || null, name || null, stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
|
|
);
|
|
}
|
|
for (const p of payload.proxyPools || []) {
|
|
const { id, isActive, testStatus, createdAt, updatedAt, ...rest } = p;
|
|
db.run(
|
|
`INSERT OR REPLACE INTO proxyPools(id, isActive, testStatus, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?)`,
|
|
[id, isActive === false ? 0 : 1, testStatus || "unknown", stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
|
|
);
|
|
}
|
|
const defaultKeyOwner = db.get(`SELECT id FROM users WHERE role = 'admin' ORDER BY createdAt ASC LIMIT 1`)?.id || null;
|
|
for (const k of payload.apiKeys || []) {
|
|
db.run(
|
|
`INSERT OR REPLACE INTO apiKeys(id, key, name, machineId, ownerId, isActive, createdAt) VALUES(?, ?, ?, ?, ?, ?, ?)`,
|
|
[k.id, k.key, k.name || null, k.machineId || null, k.ownerId || defaultKeyOwner, k.isActive === false ? 0 : 1, k.createdAt || new Date().toISOString()]
|
|
);
|
|
}
|
|
for (const c of payload.combos || []) {
|
|
db.run(
|
|
`INSERT OR REPLACE INTO combos(id, name, ownerId, kind, models, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?)`,
|
|
[c.id, c.name, c.ownerId || fallbackOwnerId, c.kind || null, stringifyJson(c.models || []), c.createdAt || new Date().toISOString(), c.updatedAt || new Date().toISOString()]
|
|
);
|
|
}
|
|
const userOwnerIds = new Set(db.all(`SELECT id FROM users`).map((user) => user.id));
|
|
const apiKeyOwners = new Map(db.all(`SELECT id, ownerId FROM apiKeys`).map((key) => [key.id, key.ownerId]));
|
|
for (const row of payload.cliToolConfigs || []) {
|
|
if (!row?.ownerId || !userOwnerIds.has(row.ownerId) || !isPersistableCliTool(row.toolId)) continue;
|
|
try {
|
|
const config = normalizeCliToolConfig(row.toolId, row.config);
|
|
if (config.apiKeyMode === "managed" && config.apiKeyId && apiKeyOwners.get(config.apiKeyId) !== row.ownerId) continue;
|
|
const timestamp = new Date().toISOString();
|
|
db.run(
|
|
`INSERT OR REPLACE INTO cliToolConfigs(ownerId, toolId, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?)`,
|
|
[row.ownerId, row.toolId, stringifyJson(config), row.createdAt || timestamp, row.updatedAt || timestamp],
|
|
);
|
|
} catch {
|
|
// Skip malformed or secret-bearing configuration rows from backups.
|
|
}
|
|
}
|
|
for (const [a, m] of Object.entries(payload.modelAliases || {})) {
|
|
db.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('modelAliases', ?, ?)`, [a, stringifyJson(m)]);
|
|
}
|
|
for (const m of payload.customModels || []) {
|
|
const k = `${m.providerAlias}|${m.id}|${m.type || "llm"}`;
|
|
db.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('customModels', ?, ?)`, [k, stringifyJson(m)]);
|
|
}
|
|
for (const [tool, mappings] of Object.entries(payload.mitmAlias || {})) {
|
|
db.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('mitmAlias', ?, ?)`, [tool, stringifyJson(mappings || {})]);
|
|
}
|
|
for (const [provider, models] of Object.entries(payload.pricing || {})) {
|
|
db.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('pricing', ?, ?)`, [provider, stringifyJson(models || {})]);
|
|
}
|
|
for (const [providerAlias, modelIds] of Object.entries(payload.disabledModels || {})) {
|
|
const validModelIds = Array.isArray(modelIds)
|
|
? modelIds.filter((modelId) => typeof modelId === "string" && modelId)
|
|
: [];
|
|
if (providerAlias && validModelIds.length > 0) {
|
|
db.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('disabledModels', ?, ?)`, [providerAlias, stringifyJson([...new Set(validModelIds)])]);
|
|
}
|
|
}
|
|
for (const [providerAlias, modelIds] of Object.entries(payload.deletedModels || {})) {
|
|
const validModelIds = Array.isArray(modelIds)
|
|
? modelIds.filter((modelId) => typeof modelId === "string" && modelId)
|
|
: [];
|
|
if (providerAlias && validModelIds.length > 0) {
|
|
db.run(`INSERT OR REPLACE INTO kv(scope, key, value) VALUES('deletedModels', ?, ?)`, [providerAlias, stringifyJson([...new Set(validModelIds)])]);
|
|
}
|
|
}
|
|
});
|
|
|
|
return await exportDb();
|
|
}
|
|
|
|
// Eager init helper (optional)
|
|
export async function initDb() {
|
|
await getAdapter();
|
|
}
|