1115 lines
46 KiB
JavaScript
1115 lines
46 KiB
JavaScript
import fs from 'node:fs';
|
|
import crypto from 'node:crypto';
|
|
import path from 'node:path';
|
|
import { spawn } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
import express from 'express';
|
|
import { listUsagePaged, listLedgerPaged } from './pagination.mjs';
|
|
|
|
const projectRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
|
|
const RESTART_ACTIONS = {
|
|
local_restart: {
|
|
label: 'local_restart',
|
|
script: path.join(projectRoot, 'scripts', 'local_restart.sh'),
|
|
logFile: path.join(projectRoot, 'adm-local-restart.log'),
|
|
detached: true,
|
|
},
|
|
pro_restart: {
|
|
label: 'pro_restart',
|
|
script: path.join(projectRoot, 'scripts', 'pro_restart.sh'),
|
|
logFile: path.join(projectRoot, 'adm-pro-restart.log'),
|
|
detached: false,
|
|
},
|
|
};
|
|
|
|
function isRestartAction(value) {
|
|
return value === 'local_restart' || value === 'pro_restart';
|
|
}
|
|
|
|
function spawnDetachedScript({ script, logFile, delaySeconds = 0 }) {
|
|
const stdout = fs.openSync(logFile, 'a');
|
|
const stderr = fs.openSync(logFile, 'a');
|
|
const command = delaySeconds > 0 ? `sleep ${delaySeconds}; exec "${script}"` : `exec "${script}"`;
|
|
const child = spawn('bash', ['-lc', command], {
|
|
cwd: projectRoot,
|
|
detached: true,
|
|
stdio: ['ignore', stdout, stderr],
|
|
env: process.env,
|
|
});
|
|
child.unref();
|
|
return child.pid ?? null;
|
|
}
|
|
|
|
function runManagedScript({ script, logFile }) {
|
|
return new Promise((resolve, reject) => {
|
|
const stdout = fs.openSync(logFile, 'a');
|
|
const stderr = fs.openSync(logFile, 'a');
|
|
let combined = '';
|
|
const child = spawn('bash', [script], {
|
|
cwd: projectRoot,
|
|
env: process.env,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
|
|
child.stdout.on('data', (chunk) => {
|
|
const text = chunk.toString();
|
|
combined += text;
|
|
fs.writeSync(stdout, text);
|
|
});
|
|
child.stderr.on('data', (chunk) => {
|
|
const text = chunk.toString();
|
|
combined += text;
|
|
fs.writeSync(stderr, text);
|
|
});
|
|
child.on('error', reject);
|
|
child.on('close', (code) => {
|
|
fs.closeSync(stdout);
|
|
fs.closeSync(stderr);
|
|
resolve({ code: code ?? 0, output: combined.trim() });
|
|
});
|
|
});
|
|
}
|
|
|
|
function asyncHandler(handler) {
|
|
return (req, res, next) => Promise.resolve(handler(req, res, next)).catch(next);
|
|
}
|
|
|
|
function wrapRouterAsync(router) {
|
|
for (const method of ['get', 'post', 'put', 'patch', 'delete']) {
|
|
const original = router[method].bind(router);
|
|
router[method] = (path, ...handlers) =>
|
|
original(
|
|
path,
|
|
...handlers.map((handler) =>
|
|
typeof handler === 'function' && handler.length < 4 ? asyncHandler(handler) : handler,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
export function createAdminApp(services) {
|
|
const {
|
|
userAuth,
|
|
llmProviderService,
|
|
assetGatewayConfigService,
|
|
pool,
|
|
ready,
|
|
wechatAdmin,
|
|
loadMindSpaceConfig,
|
|
updateMindSpaceConfig,
|
|
memoryV2ConfigService,
|
|
mindSearchConfigService,
|
|
personalMemoryCandidateStore,
|
|
skillRuntimeConfigService,
|
|
wechatScheduleLlmConfigService,
|
|
adminSystemTestService,
|
|
systemTestAccountService,
|
|
wordFilterService,
|
|
planCatalogService,
|
|
subscriptionService,
|
|
planSyncService,
|
|
} = services;
|
|
const app = express();
|
|
app.set('trust proxy', 1);
|
|
|
|
const isSecureRequest = (req) =>
|
|
req.secure || req.get('x-forwarded-proto')?.split(',')[0]?.trim() === 'https';
|
|
const jsonBody = express.json({ limit: '1mb' });
|
|
|
|
let cookieHelpers = null;
|
|
const cookieReady = ready.then(async () => {
|
|
cookieHelpers = {
|
|
parseCookies: services.parseCookies,
|
|
USER_COOKIE: services.USER_COOKIE,
|
|
userLoginCookies: services.userLoginCookies,
|
|
clearUserSessionCookie: services.clearUserSessionCookie,
|
|
resolveCookieDomainForRequest: services.resolveCookieDomainForRequest,
|
|
};
|
|
});
|
|
|
|
function userToken(req) {
|
|
const { parseCookies, USER_COOKIE } = cookieHelpers;
|
|
return parseCookies(req.get('cookie'))[USER_COOKIE];
|
|
}
|
|
|
|
function setUserLoginCookies(res, req, token) {
|
|
const { userLoginCookies, resolveCookieDomainForRequest } = cookieHelpers;
|
|
res.set(
|
|
'Set-Cookie',
|
|
userLoginCookies(token, isSecureRequest(req), resolveCookieDomainForRequest(req)),
|
|
);
|
|
}
|
|
|
|
function clearUserLoginCookies(res, req) {
|
|
const { clearUserSessionCookie, resolveCookieDomainForRequest } = cookieHelpers;
|
|
res.set(
|
|
'Set-Cookie',
|
|
clearUserSessionCookie(isSecureRequest(req), resolveCookieDomainForRequest(req)),
|
|
);
|
|
}
|
|
|
|
app.get('/health', (_req, res) => {
|
|
res.json({ ok: true, service: 'memind_adm' });
|
|
});
|
|
|
|
app.get('/healthz', (_req, res) => {
|
|
res.json({ ok: true, service: 'memind_adm' });
|
|
});
|
|
|
|
app.get('/auth/status', asyncHandler(async (req, res) => {
|
|
await cookieReady;
|
|
await ready;
|
|
const me = await userAuth.getMe(userToken(req));
|
|
if (!me) return res.json({ authenticated: false, mode: 'user' });
|
|
return res.json({ authenticated: true, user: me, mode: 'user' });
|
|
}));
|
|
|
|
app.post('/auth/login', jsonBody, asyncHandler(async (req, res) => {
|
|
await cookieReady;
|
|
await ready;
|
|
const { username, password } = req.body ?? {};
|
|
if (!username || !password) {
|
|
return res.status(400).json({ message: '用户名和密码不能为空' });
|
|
}
|
|
const result = await userAuth.login({ username, password, ip: req.ip });
|
|
if (!result.ok) {
|
|
if (result.retryAfterMs > 0) {
|
|
res.set('Retry-After', String(Math.ceil(result.retryAfterMs / 1000)));
|
|
return res.status(429).json({ message: result.message });
|
|
}
|
|
return res.status(401).json({ message: result.message });
|
|
}
|
|
setUserLoginCookies(res, req, result.token);
|
|
return res.json({ authenticated: true, user: result.user, mode: 'user' });
|
|
}));
|
|
|
|
app.post('/auth/logout', asyncHandler(async (req, res) => {
|
|
await cookieReady;
|
|
await ready;
|
|
await userAuth.revoke(userToken(req));
|
|
clearUserLoginCookies(res, req);
|
|
res.status(204).end();
|
|
}));
|
|
|
|
const adminApi = express.Router();
|
|
wrapRouterAsync(adminApi);
|
|
adminApi.use(jsonBody);
|
|
|
|
adminApi.use(asyncHandler(async (req, res, next) => {
|
|
await cookieReady;
|
|
await ready;
|
|
const me = await userAuth.getMe(userToken(req));
|
|
if (!me) return res.status(401).json({ message: '未登录' });
|
|
req.currentUser = me;
|
|
next();
|
|
}));
|
|
|
|
const requireAdmin = (req, res, next) => {
|
|
if (!req.currentUser || req.currentUser.role !== 'admin') {
|
|
res.status(403).json({ message: '需要管理员权限' });
|
|
return;
|
|
}
|
|
next();
|
|
};
|
|
|
|
function resolveSessionUser(req, res, next) {
|
|
if (!req.currentUser) {
|
|
res.status(401).json({ message: '未授权,请重新登录' });
|
|
return;
|
|
}
|
|
next();
|
|
}
|
|
|
|
adminApi.get('/analytics/sso', requireAdmin, async (req, res) => {
|
|
const sharedSecret = process.env.MEMIND_UMAMI_SSO_SECRET?.trim();
|
|
if (!sharedSecret) return res.status(503).json({ message: '未配置 Umami 单点登录密钥' });
|
|
const config = loadMindSpaceConfig ? await loadMindSpaceConfig(pool) : null;
|
|
const baseUrl = String(config?.analytics?.analyticsUrl || process.env.UMAMI_URL || 'http://127.0.0.1:3100').replace(/\/$/, '');
|
|
const username = process.env.UMAMI_SSO_USERNAME?.trim() || 'admin';
|
|
const encoded = Buffer.from(JSON.stringify({ username, exp: Math.floor(Date.now() / 1000) + 60, nonce: crypto.randomUUID() })).toString('base64url');
|
|
const signature = crypto.createHmac('sha256', sharedSecret).update(encoded).digest('base64url');
|
|
res.json({ url: `${baseUrl}/auth/memind?ticket=${encoded}.${signature}` });
|
|
});
|
|
|
|
adminApi.get('/analytics/rybbit-sso', requireAdmin, async (_req, res) => {
|
|
const sharedSecret = process.env.MEMIND_RYBBIT_SSO_SECRET?.trim();
|
|
if (!sharedSecret) return res.status(503).json({ message: '未配置 Rybbit 单点登录密钥' });
|
|
const email = process.env.RYBBIT_SSO_EMAIL?.trim().toLowerCase();
|
|
if (!email) return res.status(503).json({ message: '未配置 Rybbit 单点登录账号' });
|
|
const baseUrl = String(process.env.RYBBIT_URL || 'https://rybbit.tkmind.cn').replace(/\/$/, '');
|
|
const encoded = Buffer.from(JSON.stringify({
|
|
email,
|
|
exp: Math.floor(Date.now() / 1000) + 60,
|
|
nonce: crypto.randomUUID(),
|
|
})).toString('base64url');
|
|
const signature = crypto.createHmac('sha256', sharedSecret).update(encoded).digest('base64url');
|
|
res.json({ url: `${baseUrl}/api/auth/memind?ticket=${encoded}.${signature}` });
|
|
});
|
|
|
|
adminApi.get('/users', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.listUsers({
|
|
page: Number(req.query.page) || 1,
|
|
pageSize: Number(req.query.pageSize) || 20,
|
|
search: typeof req.query.search === 'string' ? req.query.search.trim() : '',
|
|
role: typeof req.query.role === 'string' ? req.query.role.trim() : '',
|
|
status: typeof req.query.status === 'string' ? req.query.status.trim() : '',
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/users/:userId', requireAdmin, async (req, res) => {
|
|
const user = await userAuth.getUserPublic(req.params.userId);
|
|
if (!user) return res.status(404).json({ message: '用户不存在' });
|
|
res.json({ user });
|
|
});
|
|
|
|
adminApi.get('/summary', requireAdmin, async (_req, res) => {
|
|
const summary = await userAuth.getAdminSummary();
|
|
let llm = null;
|
|
if (llmProviderService) {
|
|
const keys = await llmProviderService.listKeys();
|
|
const global = await llmProviderService.getGlobalSettings();
|
|
const selected = keys.find((key) => key.isSelected);
|
|
llm = {
|
|
keyCount: keys.length,
|
|
selectedKeyName: selected?.name ?? null,
|
|
globalModel: global?.model ?? null,
|
|
};
|
|
}
|
|
res.json({ summary: { ...summary, llm } });
|
|
});
|
|
|
|
adminApi.post('/users', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.createUser(req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.status(201).json({ user: result.user });
|
|
});
|
|
|
|
adminApi.patch('/users/:userId', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.updateUser(req.params.userId, req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ user: result.user });
|
|
});
|
|
|
|
adminApi.post('/users/:userId/recharge', requireAdmin, async (req, res) => {
|
|
const amountCents = Number(req.body?.amountCents);
|
|
const note = typeof req.body?.note === 'string' ? req.body.note : '';
|
|
const result = await userAuth.recharge(
|
|
req.params.userId,
|
|
amountCents,
|
|
req.currentUser.id,
|
|
note,
|
|
);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ user: result.user });
|
|
});
|
|
|
|
adminApi.get('/usage', requireAdmin, async (req, res) => {
|
|
const result = await listUsagePaged(pool, req.query);
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/ledger', requireAdmin, async (req, res) => {
|
|
const result = await listLedgerPaged(pool, req.query);
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/wechat/summary', requireAdmin, async (_req, res) => {
|
|
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
|
|
const summary = await wechatAdmin.getSummary();
|
|
const scheduleLlmConfig = wechatScheduleLlmConfigService
|
|
? await wechatScheduleLlmConfigService.getConfig()
|
|
: { scheduleLlmEnabled: false };
|
|
res.json({
|
|
...summary,
|
|
config: {
|
|
...summary.config,
|
|
scheduleLlmEnabled: scheduleLlmConfig.scheduleLlmEnabled,
|
|
},
|
|
});
|
|
});
|
|
|
|
adminApi.patch('/wechat/schedule-llm-config', requireAdmin, async (req, res) => {
|
|
if (!wechatScheduleLlmConfigService) {
|
|
return res.status(503).json({ message: '服务号提醒 LLM 配置未启用' });
|
|
}
|
|
const result = await wechatScheduleLlmConfigService.updateConfig(req.body ?? {}, {
|
|
updatedBy: req.currentUser.id,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/mindspace/config', requireAdmin, async (_req, res) => {
|
|
if (!loadMindSpaceConfig) return res.status(503).json({ message: 'MindSpace 配置未启用' });
|
|
const config = await loadMindSpaceConfig(pool);
|
|
res.json({
|
|
config,
|
|
});
|
|
});
|
|
|
|
adminApi.patch('/mindspace/config', requireAdmin, async (req, res) => {
|
|
if (!updateMindSpaceConfig) return res.status(503).json({ message: 'MindSpace 配置未启用' });
|
|
const result = await updateMindSpaceConfig(pool, {
|
|
publicPageLimit: req.body?.publicPageLimit,
|
|
analytics: req.body?.analytics,
|
|
});
|
|
res.json({ config: result });
|
|
});
|
|
|
|
adminApi.get('/asset-gateway/config', requireAdmin, async (_req, res) => {
|
|
if (!assetGatewayConfigService?.getConfig) {
|
|
return res.status(503).json({ message: '资产能力配置服务未启用' });
|
|
}
|
|
res.json(await assetGatewayConfigService.getConfig());
|
|
});
|
|
|
|
adminApi.put('/asset-gateway/config', requireAdmin, async (req, res) => {
|
|
if (!assetGatewayConfigService?.updateGlobalConfig) {
|
|
return res.status(503).json({ message: '资产能力配置服务未启用' });
|
|
}
|
|
res.json(await assetGatewayConfigService.updateGlobalConfig(req.body ?? {}, {
|
|
updatedBy: req.currentUser.id,
|
|
}));
|
|
});
|
|
|
|
adminApi.put('/asset-gateway/plugins/:pluginId', requireAdmin, async (req, res) => {
|
|
if (!assetGatewayConfigService?.updatePluginConfig) {
|
|
return res.status(503).json({ message: '资产能力配置服务未启用' });
|
|
}
|
|
const result = await assetGatewayConfigService.updatePluginConfig(req.params.pluginId, req.body ?? {}, {
|
|
updatedBy: req.currentUser.id,
|
|
});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/memory-v2/config', requireAdmin, async (_req, res) => {
|
|
if (!memoryV2ConfigService) return res.status(503).json({ message: 'Memory V2 配置未启用' });
|
|
res.json(await memoryV2ConfigService.getAdminConfig());
|
|
});
|
|
|
|
adminApi.patch('/memory-v2/config', requireAdmin, async (req, res) => {
|
|
if (!memoryV2ConfigService) return res.status(503).json({ message: 'Memory V2 配置未启用' });
|
|
const result = await memoryV2ConfigService.updateAdminConfig(req.body ?? {}, {
|
|
updatedBy: req.currentUser.id,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/mindsearch/config', requireAdmin, async (_req, res) => {
|
|
if (!mindSearchConfigService?.getAdminConfig) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
|
res.json(await mindSearchConfigService.getAdminConfig());
|
|
});
|
|
|
|
adminApi.patch('/mindsearch/config', requireAdmin, async (req, res) => {
|
|
if (!mindSearchConfigService?.updateAdminConfig) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
|
res.json(await mindSearchConfigService.updateAdminConfig(req.body ?? {}, { updatedBy: req.currentUser.id }));
|
|
});
|
|
|
|
adminApi.put('/mindsearch/config', requireAdmin, async (req, res) => {
|
|
if (!mindSearchConfigService?.updateAdminConfig) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
|
res.json(await mindSearchConfigService.updateAdminConfig(req.body ?? {}, { updatedBy: req.currentUser.id }));
|
|
});
|
|
|
|
adminApi.get('/mindsearch/runtime', requireAdmin, async (_req, res) => {
|
|
if (!mindSearchConfigService?.getRuntimeState) return res.status(503).json({ message: 'MindSearch 配置未启用' });
|
|
res.json(await mindSearchConfigService.getRuntimeState());
|
|
});
|
|
adminApi.post('/mindsearch/services/:serviceId/test', requireAdmin, async (req, res) => {
|
|
if (!mindSearchConfigService?.testService) return res.status(503).json({ message: 'MindSearch 服务测试未启用' });
|
|
try {
|
|
return res.json(await mindSearchConfigService.testService(req.params.serviceId));
|
|
} catch (error) {
|
|
if (error?.code === 'SEARCH_SERVICE_NOT_FOUND') return res.status(404).json({ message: error.message });
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
adminApi.get('/memory-v2/status', requireAdmin, async (_req, res) => {
|
|
const portalBaseUrl = `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`;
|
|
try {
|
|
const response = await fetch(`${portalBaseUrl}/api/runtime/status`, {
|
|
headers: { accept: 'application/json' },
|
|
signal: AbortSignal.timeout(2500),
|
|
});
|
|
if (!response.ok) {
|
|
return res.status(502).json({
|
|
ok: false,
|
|
message: `Memory V2 运行时状态返回 HTTP ${response.status}`,
|
|
});
|
|
}
|
|
const payload = await response.json();
|
|
return res.json({
|
|
ok: true,
|
|
checkedAt: Date.now(),
|
|
memory: payload?.memory ?? null,
|
|
});
|
|
} catch (err) {
|
|
return res.status(503).json({
|
|
ok: false,
|
|
checkedAt: Date.now(),
|
|
message: err instanceof Error ? err.message : 'Memory V2 运行时不可用',
|
|
});
|
|
}
|
|
});
|
|
|
|
adminApi.get('/memory-v2/candidates', requireAdmin, async (req, res) => {
|
|
if (!personalMemoryCandidateStore?.listCandidates) {
|
|
return res.status(503).json({ message: '候选记忆存储未启用' });
|
|
}
|
|
try {
|
|
const status = String(req.query.status ?? 'candidate');
|
|
const userId = String(req.query.userId ?? '').trim() || null;
|
|
const limit = Number(req.query.limit ?? 50);
|
|
const [items, counts] = await Promise.all([
|
|
personalMemoryCandidateStore.listCandidates({ status, userId, limit }),
|
|
personalMemoryCandidateStore.countByStatus(),
|
|
]);
|
|
return res.json({ items, counts });
|
|
} catch (err) {
|
|
const missingTable = err?.code === 'ER_NO_SUCH_TABLE';
|
|
return res.status(missingTable ? 503 : 400).json({
|
|
message: missingTable ? '候选记忆表尚未执行本地迁移' : (err instanceof Error ? err.message : '候选记忆读取失败'),
|
|
});
|
|
}
|
|
});
|
|
|
|
adminApi.post('/memory-v2/candidates/:id/accept', requireAdmin, async (req, res) => {
|
|
if (!personalMemoryCandidateStore?.reviewCandidate) {
|
|
return res.status(503).json({ message: '候选记忆存储未启用' });
|
|
}
|
|
const result = await personalMemoryCandidateStore.reviewCandidate(req.params.id, 'accepted', {
|
|
reviewedBy: req.currentUser.id,
|
|
});
|
|
if (!result.updated) return res.status(409).json({ message: '候选记忆已处理或不存在' });
|
|
return res.json(result);
|
|
});
|
|
|
|
adminApi.post('/memory-v2/candidates/:id/reject', requireAdmin, async (req, res) => {
|
|
if (!personalMemoryCandidateStore?.reviewCandidate) {
|
|
return res.status(503).json({ message: '候选记忆存储未启用' });
|
|
}
|
|
const result = await personalMemoryCandidateStore.reviewCandidate(req.params.id, 'rejected', {
|
|
reviewedBy: req.currentUser.id,
|
|
});
|
|
if (!result.updated) return res.status(409).json({ message: '候选记忆已处理或不存在' });
|
|
return res.json(result);
|
|
});
|
|
|
|
adminApi.get('/skill-runtime/config', requireAdmin, async (_req, res) => {
|
|
if (!skillRuntimeConfigService?.getAdminConfig) {
|
|
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
|
}
|
|
res.json(await skillRuntimeConfigService.getAdminConfig());
|
|
});
|
|
|
|
const updateSkillRuntimeConfig = async (req, res) => {
|
|
if (!skillRuntimeConfigService?.updateAdminConfig) {
|
|
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
|
}
|
|
const result = await skillRuntimeConfigService.updateAdminConfig(req.body?.config ?? req.body ?? {}, {
|
|
updatedBy: req.currentUser.id,
|
|
});
|
|
res.json(result);
|
|
};
|
|
|
|
adminApi.put('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig);
|
|
adminApi.patch('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig);
|
|
|
|
adminApi.get('/skill-runtime/catalog', requireAdmin, async (_req, res) => {
|
|
if (!skillRuntimeConfigService?.listCatalogSummary) {
|
|
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
|
}
|
|
res.json({ catalog: await skillRuntimeConfigService.listCatalogSummary() });
|
|
});
|
|
|
|
adminApi.get('/skill-runtime/runtime', requireAdmin, async (_req, res) => {
|
|
if (!skillRuntimeConfigService?.getPublicRuntimeConfig) {
|
|
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
|
|
}
|
|
res.json(await skillRuntimeConfigService.getPublicRuntimeConfig());
|
|
});
|
|
|
|
adminApi.get('/system-tests/accounts', requireAdmin, async (_req, res) => {
|
|
if (!systemTestAccountService) {
|
|
return res.status(503).json({ message: '系统测试账号服务未启用' });
|
|
}
|
|
const accounts = await systemTestAccountService.listAccounts();
|
|
res.json({ accounts });
|
|
});
|
|
|
|
adminApi.post('/system-tests/accounts', requireAdmin, async (req, res) => {
|
|
if (!systemTestAccountService) {
|
|
return res.status(503).json({ message: '系统测试账号服务未启用' });
|
|
}
|
|
const result = await systemTestAccountService.createAccount({
|
|
label: req.body?.label,
|
|
username: req.body?.username,
|
|
password: req.body?.password,
|
|
updatedBy: req.currentUser.id,
|
|
});
|
|
if (!result.ok) {
|
|
return res.status(400).json({ message: result.message ?? '保存测试账号失败' });
|
|
}
|
|
res.status(201).json({ account: result.account });
|
|
});
|
|
|
|
adminApi.delete('/system-tests/accounts/:accountId', requireAdmin, async (req, res) => {
|
|
if (!systemTestAccountService) {
|
|
return res.status(503).json({ message: '系统测试账号服务未启用' });
|
|
}
|
|
const result = await systemTestAccountService.deleteAccount(req.params.accountId);
|
|
if (!result.ok) {
|
|
return res.status(404).json({ message: result.message ?? '测试账号不存在' });
|
|
}
|
|
res.status(204).end();
|
|
});
|
|
|
|
adminApi.post('/system-tests/skill-validation', requireAdmin, async (req, res) => {
|
|
if (!adminSystemTestService?.runSkillValidation) {
|
|
return res.status(503).json({ message: '系统测试服务未启用' });
|
|
}
|
|
const accountId = String(req.body?.accountId ?? '').trim();
|
|
let username = String(req.body?.username ?? '').trim();
|
|
let password = String(req.body?.password ?? '');
|
|
const skillName = String(req.body?.skillName ?? '').trim();
|
|
if (accountId) {
|
|
if (!systemTestAccountService) {
|
|
return res.status(503).json({ message: '系统测试账号服务未启用' });
|
|
}
|
|
const account = await systemTestAccountService.getAccountSecret(accountId);
|
|
if (!account) {
|
|
return res.status(404).json({ message: '所选测试账号不存在,请刷新后重试' });
|
|
}
|
|
username = account.username;
|
|
password = account.password;
|
|
}
|
|
if (!username || !password) {
|
|
return res.status(400).json({ message: '请输入账号和密码' });
|
|
}
|
|
const result = await adminSystemTestService.runSkillValidation({
|
|
username,
|
|
password,
|
|
skillName,
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/wechat/bindings', requireAdmin, async (req, res) => {
|
|
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
|
|
res.json(await wechatAdmin.listBindings(req.query));
|
|
});
|
|
|
|
adminApi.get('/wechat/messages', requireAdmin, async (req, res) => {
|
|
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
|
|
res.json(await wechatAdmin.listMessages(req.query));
|
|
});
|
|
|
|
adminApi.get('/wechat/digests', requireAdmin, async (req, res) => {
|
|
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
|
|
res.json(await wechatAdmin.listDigests(req.query));
|
|
});
|
|
|
|
adminApi.get('/wechat/deliveries', requireAdmin, async (req, res) => {
|
|
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
|
|
res.json(await wechatAdmin.listDeliveries(req.query));
|
|
});
|
|
|
|
adminApi.get('/wechat/web-notifications', requireAdmin, async (req, res) => {
|
|
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
|
|
res.json(await wechatAdmin.listWebNotifications(req.query));
|
|
});
|
|
|
|
adminApi.post('/wechat/web-notifications', requireAdmin, async (req, res) => {
|
|
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
|
|
const result = await wechatAdmin.createWebNotification(req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message ?? '发送失败' });
|
|
res.status(201).json(result);
|
|
});
|
|
|
|
adminApi.post('/wechat/users/:userId/route/clear', requireAdmin, async (req, res) => {
|
|
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
|
|
const result = await wechatAdmin.clearRouteForUser(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message ?? '清除失败' });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.post('/wechat/digests/:id/cancel', requireAdmin, async (req, res) => {
|
|
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
|
|
const result = await wechatAdmin.cancelDigest(req.params.id);
|
|
if (!result.ok) return res.status(404).json({ message: '订阅不存在或已暂停' });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.post('/wechat/digests/:id/resume', requireAdmin, async (req, res) => {
|
|
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
|
|
const result = await wechatAdmin.resumeDigest(req.params.id);
|
|
if (!result.ok) return res.status(404).json({ message: result.message ?? '恢复失败' });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/capabilities/catalog', requireAdmin, (_req, res) => {
|
|
res.json({ catalog: userAuth.capabilityCatalog });
|
|
});
|
|
|
|
adminApi.get('/capabilities/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
if (role === 'admin') {
|
|
return res.json({
|
|
role,
|
|
capabilities: Object.fromEntries(
|
|
userAuth.capabilityCatalog.map((item) => [item.key, true]),
|
|
),
|
|
unrestricted: true,
|
|
});
|
|
}
|
|
res.json(await userAuth.getRoleCapabilities('user'));
|
|
});
|
|
|
|
adminApi.put('/capabilities/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
const result = await userAuth.setRoleCapabilities(role, req.body?.capabilities ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/users/:userId/capabilities', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.getUserCapabilities(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.put('/users/:userId/capabilities', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.setUserCapabilities(
|
|
req.params.userId,
|
|
req.body?.capabilities ?? {},
|
|
);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.delete('/users/:userId/capabilities', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.clearUserCapabilityOverrides(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/policies/catalog', requireAdmin, (_req, res) => {
|
|
res.json({ catalog: userAuth.policyCatalog });
|
|
});
|
|
|
|
adminApi.get('/policies/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
if (role === 'admin') {
|
|
return res.json({ role, policies: {}, unrestricted: true });
|
|
}
|
|
res.json(await userAuth.getRolePolicies('user'));
|
|
});
|
|
|
|
adminApi.put('/policies/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
const result = await userAuth.setRolePolicies(role, req.body?.policies ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/users/:userId/policies', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.getUserPolicies(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.put('/users/:userId/policies', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.setUserPolicies(req.params.userId, req.body?.policies ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.delete('/users/:userId/policies', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.clearUserPolicyOverrides(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/skills/catalog', requireAdmin, (_req, res) => {
|
|
res.json({ catalog: userAuth.skillCatalog });
|
|
});
|
|
|
|
adminApi.get('/skills/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
if (role === 'admin') {
|
|
return res.json({
|
|
role,
|
|
skills: Object.fromEntries(userAuth.skillCatalog.map((item) => [item.name, true])),
|
|
});
|
|
}
|
|
res.json(await userAuth.getRoleSkills('user'));
|
|
});
|
|
|
|
adminApi.put('/skills/role/:role', requireAdmin, async (req, res) => {
|
|
const role = req.params.role === 'admin' ? 'admin' : 'user';
|
|
const result = await userAuth.setRoleSkills(role, req.body?.skills ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/users/:userId/skills', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.getUserSkills(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.put('/users/:userId/skills', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.setUserSkills(req.params.userId, req.body?.skills ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.delete('/users/:userId/skills', requireAdmin, async (req, res) => {
|
|
const result = await userAuth.clearUserSkillOverrides(req.params.userId);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/llm-providers/catalog', requireAdmin, (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
res.json({
|
|
catalog: llmProviderService.catalog,
|
|
executors: llmProviderService.executorCatalog ?? [],
|
|
});
|
|
});
|
|
|
|
adminApi.get('/llm-providers/keys', requireAdmin, async (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
res.json({ keys: await llmProviderService.listKeys() });
|
|
});
|
|
|
|
adminApi.post('/llm-providers/keys', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.createKey(req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.status(201).json({ key: result.key });
|
|
});
|
|
|
|
adminApi.patch('/llm-providers/keys/:keyId', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.updateKey(req.params.keyId, req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ key: result.key });
|
|
});
|
|
|
|
adminApi.post('/llm-providers/keys/:keyId/select', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.selectKey(req.params.keyId);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ key: result.key });
|
|
});
|
|
|
|
adminApi.delete('/llm-providers/keys/:keyId', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.deleteKey(req.params.keyId);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
adminApi.post('/llm-providers/sync', requireAdmin, async (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
try {
|
|
res.json(await llmProviderService.syncSelectedToGoosed());
|
|
} catch (err) {
|
|
res.status(500).json({ message: err instanceof Error ? err.message : '同步失败' });
|
|
}
|
|
});
|
|
|
|
adminApi.get('/llm-providers/global', requireAdmin, async (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
res.json({ global: await llmProviderService.getGlobalSettings() });
|
|
});
|
|
|
|
adminApi.get('/llm-providers/vision', requireAdmin, async (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
res.json({ vision: await llmProviderService.getVisionSettings() });
|
|
});
|
|
|
|
adminApi.put('/llm-providers/vision', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.setVisionKey(req.body?.keyId, req.body?.model);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.delete('/llm-providers/vision', requireAdmin, async (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.clearVisionKey();
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/llm-providers/executor-bindings', requireAdmin, async (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
res.json({ bindings: await llmProviderService.listExecutorBindings() });
|
|
});
|
|
|
|
adminApi.put('/llm-providers/executor-bindings/:executor', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.setExecutorBinding(req.params.executor, req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/llm-providers/executor-runtime', requireAdmin, async (_req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
res.json({ runtimes: await llmProviderService.listExecutorRuntimeConfigs() });
|
|
});
|
|
|
|
adminApi.get('/llm-providers/executor-launch-plan', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const plans = await llmProviderService.listExecutorLaunchPlans({
|
|
purpose: typeof req.query.purpose === 'string' ? req.query.purpose : 'default',
|
|
mode: typeof req.query.mode === 'string' ? req.query.mode : 'serve',
|
|
cwd: typeof req.query.cwd === 'string' ? req.query.cwd : undefined,
|
|
instruction: typeof req.query.instruction === 'string' ? req.query.instruction : '',
|
|
includeSecret: false,
|
|
});
|
|
res.json({ plans });
|
|
});
|
|
|
|
adminApi.get('/llm-providers/executor-launch-status', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const launches = await llmProviderService.listExecutorLaunchStates({
|
|
purpose: typeof req.query.purpose === 'string' ? req.query.purpose : 'default',
|
|
});
|
|
res.json({ launches });
|
|
});
|
|
|
|
adminApi.post('/llm-providers/executor-launch/:executor', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.launchExecutor(req.params.executor, req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message, launch: result });
|
|
res.json({ launch: result.launch ?? result });
|
|
});
|
|
|
|
adminApi.post('/llm-providers/executor-stop/:executor', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.stopExecutor(req.params.executor, req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ launch: result.launch });
|
|
});
|
|
|
|
adminApi.post('/llm-providers/executor-restart/:executor', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.restartExecutor(req.params.executor, req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message, launch: result });
|
|
res.json({ launch: result.launch ?? result });
|
|
});
|
|
|
|
adminApi.post('/service-restart/:action', requireAdmin, async (req, res) => {
|
|
const action = req.params.action;
|
|
if (!isRestartAction(action)) {
|
|
return res.status(400).json({ message: '仅支持 local_restart 或 pro_restart' });
|
|
}
|
|
const target = RESTART_ACTIONS[action];
|
|
if (!fs.existsSync(target.script)) {
|
|
return res.status(500).json({ message: `${target.label} 脚本不存在: ${target.script}` });
|
|
}
|
|
|
|
if (target.detached) {
|
|
const pid = spawnDetachedScript({ script: target.script, logFile: target.logFile, delaySeconds: 1 });
|
|
return res.status(202).json({
|
|
ok: true,
|
|
action,
|
|
message: '已触发本机重启,当前页面会在几秒内短暂断开,请稍后刷新。',
|
|
pid,
|
|
logFile: target.logFile,
|
|
});
|
|
}
|
|
|
|
const result = await runManagedScript({ script: target.script, logFile: target.logFile });
|
|
if (result.code !== 0) {
|
|
return res.status(500).json({
|
|
ok: false,
|
|
action,
|
|
message: `${action} 执行失败`,
|
|
output: result.output,
|
|
logFile: target.logFile,
|
|
});
|
|
}
|
|
return res.json({
|
|
ok: true,
|
|
action,
|
|
message: '远程服务已重启',
|
|
output: result.output,
|
|
logFile: target.logFile,
|
|
});
|
|
});
|
|
|
|
adminApi.put('/llm-providers/global', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
const result = await llmProviderService.setGlobalModel(req.body?.model);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.post('/llm-providers/test', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
try {
|
|
res.json(await llmProviderService.testDraft(req.body ?? {}));
|
|
} catch (err) {
|
|
res.status(500).json({ ok: false, message: err instanceof Error ? err.message : '联通测试失败' });
|
|
}
|
|
});
|
|
|
|
adminApi.post('/llm-providers/keys/:keyId/test', requireAdmin, async (req, res) => {
|
|
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
|
|
try {
|
|
res.json(await llmProviderService.testKey(req.params.keyId, req.body?.model));
|
|
} catch (err) {
|
|
res.status(500).json({ ok: false, message: err instanceof Error ? err.message : '联通测试失败' });
|
|
}
|
|
});
|
|
|
|
// ── Blocked words ─────────────────────────────────────────────────────────
|
|
|
|
adminApi.get('/blocked-words', requireAdmin, async (_req, res) => {
|
|
if (!wordFilterService) return res.status(503).json({ message: '词语过滤未启用' });
|
|
const words = await wordFilterService.listBlockedWords();
|
|
res.json({ words });
|
|
});
|
|
|
|
adminApi.post('/blocked-words', requireAdmin, async (req, res) => {
|
|
if (!wordFilterService) return res.status(503).json({ message: '词语过滤未启用' });
|
|
const result = await wordFilterService.createBlockedWord(req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.status(201).json({ blockedWord: result.blockedWord });
|
|
});
|
|
|
|
adminApi.patch('/blocked-words/:id', requireAdmin, async (req, res) => {
|
|
if (!wordFilterService) return res.status(503).json({ message: '词语过滤未启用' });
|
|
const result = await wordFilterService.updateBlockedWord(req.params.id, req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.json({ blockedWord: result.blockedWord });
|
|
});
|
|
|
|
adminApi.delete('/blocked-words/:id', requireAdmin, async (req, res) => {
|
|
if (!wordFilterService) return res.status(503).json({ message: '词语过滤未启用' });
|
|
const result = await wordFilterService.deleteBlockedWord(req.params.id);
|
|
if (!result.ok) return res.status(404).json({ message: result.message });
|
|
res.json({ ok: true });
|
|
});
|
|
|
|
// ── Subscription Plan Catalog ───────────────────────────────────────────────
|
|
|
|
adminApi.get('/subscriptions/plans', requireAdmin, async (_req, res) => {
|
|
if (!planCatalogService) return res.status(503).json({ message: '套餐服务未启用' });
|
|
const plans = await planCatalogService.listPlans({ includeInactive: true });
|
|
res.json({ plans });
|
|
});
|
|
|
|
const shouldSkipPlanSync = (req) => req.get('x-plan-sync-hop') === '1';
|
|
|
|
adminApi.post('/subscriptions/plans', requireAdmin, async (req, res) => {
|
|
if (!planCatalogService) return res.status(503).json({ message: '套餐服务未启用' });
|
|
const { planType, ...data } = req.body ?? {};
|
|
if (!planType) return res.status(400).json({ message: '缺少 planType' });
|
|
const result = await planCatalogService.upsertPlan(planType, data);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
const sync = shouldSkipPlanSync(req) ? undefined : await planSyncService?.syncPlanUpsert(planType, result.plan);
|
|
res.status(201).json({ plan: result.plan, sync });
|
|
});
|
|
|
|
adminApi.put('/subscriptions/plans/:planType', requireAdmin, async (req, res) => {
|
|
if (!planCatalogService) return res.status(503).json({ message: '套餐服务未启用' });
|
|
const result = await planCatalogService.upsertPlan(req.params.planType, req.body ?? {});
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
const sync = shouldSkipPlanSync(req) ? undefined : await planSyncService?.syncPlanUpsert(req.params.planType, result.plan);
|
|
res.json({ plan: result.plan, sync });
|
|
});
|
|
|
|
adminApi.delete('/subscriptions/plans/:planType', requireAdmin, async (req, res) => {
|
|
if (!planCatalogService) return res.status(503).json({ message: '套餐服务未启用' });
|
|
const result = await planCatalogService.deletePlan(req.params.planType);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
const sync = shouldSkipPlanSync(req) ? undefined : await planSyncService?.syncPlanDelete(req.params.planType);
|
|
res.json({ ok: true, sync });
|
|
});
|
|
|
|
adminApi.post('/subscriptions/plans/sync-production', requireAdmin, async (_req, res) => {
|
|
if (!planCatalogService) return res.status(503).json({ message: '套餐服务未启用' });
|
|
if (!planSyncService?.enabled) {
|
|
return res.status(503).json({ message: planSyncService?.reason || '未配置生产套餐同步' });
|
|
}
|
|
const plans = await planCatalogService.listPlans({ includeInactive: true });
|
|
const sync = await planSyncService.syncAllPlans(plans);
|
|
res.json({ sync });
|
|
});
|
|
|
|
// ── Subscription Management ─────────────────────────────────────────────────
|
|
|
|
adminApi.get('/subscriptions', requireAdmin, async (req, res) => {
|
|
if (!subscriptionService) return res.status(503).json({ message: '套餐服务未启用' });
|
|
const result = await subscriptionService.listSubscriptions({
|
|
userId: req.query.userId || null,
|
|
status: req.query.status || null,
|
|
page: Number(req.query.page) || 1,
|
|
pageSize: Math.min(Number(req.query.pageSize) || 20, 100),
|
|
});
|
|
res.json(result);
|
|
});
|
|
|
|
adminApi.get('/users/:userId/subscription', requireAdmin, async (req, res) => {
|
|
if (!subscriptionService) return res.status(503).json({ message: '套餐服务未启用' });
|
|
const subscription = await subscriptionService.getActiveSubscription(req.params.userId);
|
|
res.json({ subscription });
|
|
});
|
|
|
|
adminApi.post('/users/:userId/subscription', requireAdmin, async (req, res) => {
|
|
if (!subscriptionService) return res.status(503).json({ message: '套餐服务未启用' });
|
|
const { planType, durationDays, note } = req.body ?? {};
|
|
if (!planType) return res.status(400).json({ message: '缺少 planType' });
|
|
const result = await subscriptionService.grantSubscription(
|
|
req.params.userId,
|
|
planType,
|
|
durationDays ? Number(durationDays) : undefined,
|
|
req.currentUser.id,
|
|
note || '',
|
|
);
|
|
if (!result.ok) return res.status(400).json({ message: result.message });
|
|
res.status(201).json({ subscription: result.subscription });
|
|
});
|
|
|
|
adminApi.delete('/users/:userId/subscription', requireAdmin, async (req, res) => {
|
|
if (!subscriptionService) return res.status(503).json({ message: '套餐服务未启用' });
|
|
const result = await subscriptionService.cancelSubscription(
|
|
req.params.userId,
|
|
req.currentUser.id,
|
|
);
|
|
res.json(result);
|
|
});
|
|
|
|
app.use('/admin-api', adminApi);
|
|
|
|
if (services.createOpsApi) {
|
|
const opsApi = express.Router();
|
|
wrapRouterAsync(opsApi);
|
|
opsApi.use(asyncHandler(async (req, res, next) => {
|
|
await cookieReady;
|
|
await ready;
|
|
const me = await userAuth.getMe(userToken(req));
|
|
if (!me) return res.status(401).json({ message: '未授权,请重新登录' });
|
|
req.currentUser = me;
|
|
next();
|
|
}));
|
|
opsApi.use(resolveSessionUser);
|
|
opsApi.use('/ops/v1', services.createOpsApi({ jsonBody, plazaOps: services.plazaOps }));
|
|
app.use('/api', opsApi);
|
|
}
|
|
|
|
app.use((err, _req, res, _next) => {
|
|
console.error('Admin API request failed:', err);
|
|
if (res.headersSent) return;
|
|
res.status(500).json({ message: err instanceof Error ? err.message : '管理后台请求失败' });
|
|
});
|
|
|
|
return app;
|
|
}
|