Files
memind/admin-routes.mjs
T
john 4b15610aa8
Memind CI / Test, build, and release guards (pull_request) Successful in 2m47s
feat(image): complete reviewed generation delivery
2026-07-20 20:04:53 +08:00

820 lines
33 KiB
JavaScript

// admin-routes.mjs
//
// Back-office HTTP route factories for the standalone memind_adm service.
//
// Route logic lives here exactly once. The public server (server.mjs) no longer
// serves these surfaces; admin-server.mjs is the sole consumer. Services are
// injected so the same router can be wired against any container (tests, future
// read-replica pool, etc.).
//
// createAdminApi -> /admin-api/* platform super-admin (role === 'admin')
// createOpsApi -> /api/ops/v1/* plaza operations console (ops_role)
import express from 'express';
import { sendData, sendError } from './api-response.mjs';
import { PLAN_CATALOG, tokensToCallsApprox } from './billing-subscription.mjs';
import { hasOpsRole } from './plaza-ops.mjs';
import { mapPlazaError } from './plaza-posts.mjs';
function plazaRouteError(res, req, error) {
const status = mapPlazaError(error);
const code = error?.code ?? 'internal_error';
const message = error instanceof Error ? error.message : 'Plaza 请求失败';
return sendError(res, req, status, code, message, error?.details);
}
/**
* Platform super-admin API (role === 'admin').
* @param {object} deps
* @param {import('express').RequestHandler} deps.jsonBody JSON body parser middleware.
* @param {(req: import('express').Request) => (string|undefined)} deps.getToken Extract the user session token from a request.
* @param {Promise<unknown>} [deps.ready] Optional bootstrap gate awaited before the first request resolves auth.
* @param {object} deps.userAuth
* @param {object|null} deps.llmProviderService
* @param {object|null} deps.memoryV2ConfigService
* @param {object|null} deps.skillRuntimeConfigService
* @param {object|null} deps.adminSystemTestService
* @param {object|null} deps.plazaPosts
* @param {object|null} deps.plazaOps
* @param {object|null} deps.wechatAdmin
*/
export function createAdminApi({
jsonBody,
getToken,
ready,
userAuth,
llmProviderService,
assetGatewayConfigService,
imageMakeAdminConfigService,
memoryV2ConfigService,
mindSearchConfigService,
skillRuntimeConfigService,
adminSystemTestService,
plazaPosts,
plazaOps,
wechatAdmin,
subscriptionService,
}) {
function requireAdmin(req, res, next) {
if (!req.currentUser || req.currentUser.role !== 'admin') {
res.status(403).json({ message: '需要管理员权限' });
return;
}
next();
}
const adminApi = express.Router();
adminApi.use(jsonBody);
adminApi.use(async (req, res, next) => {
if (ready) await ready;
if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
const me = await userAuth.getMe(getToken(req));
if (!me) return res.status(401).json({ message: '未登录' });
req.currentUser = me;
next();
});
adminApi.get('/users', requireAdmin, async (req, res) => {
const { page, pageSize, search, role, status } = req.query;
const result = await userAuth.listUsers({ page, pageSize, search, role, status });
res.json(result);
});
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.get('/asset-gateway/config', requireAdmin, async (_req, res) => {
if (!assetGatewayConfigService?.getConfig) {
return res.status(503).json({ message: '资产能力配置服务未启用' });
}
return res.json(await assetGatewayConfigService.getConfig());
});
adminApi.put('/asset-gateway/config', requireAdmin, async (req, res) => {
if (!assetGatewayConfigService?.updateGlobalConfig) {
return res.status(503).json({ message: '资产能力配置服务未启用' });
}
return 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 });
return res.json(result);
});
adminApi.get('/image-make/config', requireAdmin, async (_req, res) => {
if (!imageMakeAdminConfigService?.getAdminConfig) {
return res.status(503).json({ message: 'image_make 配置服务未启用' });
}
return res.json(await imageMakeAdminConfigService.getAdminConfig());
});
const updateImageMakeConfig = async (req, res) => {
if (!imageMakeAdminConfigService?.updateAdminConfig) {
return res.status(503).json({ message: 'image_make 配置服务未启用' });
}
const result = await imageMakeAdminConfigService.updateAdminConfig(req.body ?? {}, {
updatedBy: req.currentUser.id,
});
if (result.ok === false) return res.status(400).json({ message: result.message });
return res.json(result);
};
adminApi.put('/image-make/config', requireAdmin, updateImageMakeConfig);
adminApi.patch('/image-make/config', requireAdmin, updateImageMakeConfig);
adminApi.get('/image-make/runtime', requireAdmin, async (_req, res) => {
if (!imageMakeAdminConfigService?.getRuntimeConfig) {
return res.status(503).json({ message: 'image_make 配置服务未启用' });
}
const runtime = await imageMakeAdminConfigService.getRuntimeConfig();
if (!runtime.ok) return res.status(503).json({ message: runtime.message ?? 'image_make 运行时配置无效' });
const { providers, ...rest } = runtime;
return res.json({
...rest,
providers: {
mock: { enabled: providers.mock.enabled },
aliyun_bailian: providers.aliyun_bailian.enabled
? {
enabled: true,
model: providers.aliyun_bailian.model,
apiBase: providers.aliyun_bailian.apiBase,
apiKeyConfigured: Boolean(providers.aliyun_bailian.apiKey),
}
: { enabled: false },
comfyui: providers.comfyui.enabled
? { enabled: true, ...providers.comfyui }
: { enabled: false },
},
});
});
adminApi.get('/memory-v2/config', requireAdmin, async (_req, res) => {
if (!memoryV2ConfigService?.getAdminConfig) {
return res.status(503).json({ message: 'Memory V2 配置服务未启用' });
}
const result = await memoryV2ConfigService.getAdminConfig();
return res.json(result);
});
const updateMemoryV2Config = async (req, res) => {
if (!memoryV2ConfigService?.updateAdminConfig) {
return res.status(503).json({ message: 'Memory V2 配置服务未启用' });
}
const result = await memoryV2ConfigService.updateAdminConfig(req.body ?? {}, {
updatedBy: req.currentUser.id,
});
return res.json(result);
};
adminApi.put('/memory-v2/config', requireAdmin, updateMemoryV2Config);
adminApi.patch('/memory-v2/config', requireAdmin, updateMemoryV2Config);
adminApi.get('/mindsearch/config', requireAdmin, async (_req, res) => {
if (!mindSearchConfigService?.getAdminConfig) return res.status(503).json({ message: 'MindSearch 配置服务未启用' });
return res.json(await mindSearchConfigService.getAdminConfig());
});
const updateMindSearchConfig = async (req, res) => {
if (!mindSearchConfigService?.updateAdminConfig) return res.status(503).json({ message: 'MindSearch 配置服务未启用' });
return res.json(await mindSearchConfigService.updateAdminConfig(req.body ?? {}, { updatedBy: req.currentUser.id }));
};
adminApi.put('/mindsearch/config', requireAdmin, updateMindSearchConfig);
adminApi.patch('/mindsearch/config', requireAdmin, updateMindSearchConfig);
adminApi.get('/mindsearch/runtime', requireAdmin, async (_req, res) => {
if (!mindSearchConfigService?.getRuntimeState) return res.status(503).json({ message: 'MindSearch 配置服务未启用' });
return res.json(await mindSearchConfigService.getRuntimeState());
});
adminApi.get('/memory-v2/runtime', requireAdmin, async (_req, res) => {
if (!memoryV2ConfigService?.getRuntimeState) {
return res.status(503).json({ message: 'Memory V2 配置服务未启用' });
}
const result = await memoryV2ConfigService.getRuntimeState();
return res.json(result);
});
adminApi.get('/skill-runtime/config', requireAdmin, async (_req, res) => {
if (!skillRuntimeConfigService?.getAdminConfig) {
return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' });
}
return 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,
});
return 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 配置服务未启用' });
}
return 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 配置服务未启用' });
}
return res.json(await skillRuntimeConfigService.getPublicRuntimeConfig());
});
adminApi.post('/system-tests/skill-validation', requireAdmin, async (req, res) => {
if (!adminSystemTestService?.runSkillValidation) {
return res.status(503).json({ message: '系统测试服务未启用' });
}
const username = String(req.body?.username ?? '').trim();
const password = String(req.body?.password ?? '');
const skillName = String(req.body?.skillName ?? 'service-integration-smoke').trim();
if (!username || !password) {
return res.status(400).json({ message: '请输入测试账号和密码' });
}
const result = await adminSystemTestService.runSkillValidation({ username, password, skillName });
return res.json(result);
});
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 userId = typeof req.query.userId === 'string' ? req.query.userId : null;
const { page, pageSize } = req.query;
const result = await userAuth.listUsageRecords({ userId, page, pageSize });
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('/ledger', requireAdmin, async (req, res) => {
const userId = typeof req.query.userId === 'string' ? req.query.userId : null;
const { page, pageSize } = req.query;
const result = await userAuth.listBillingLedger({ userId, page, pageSize });
res.json(result);
});
adminApi.get('/wechat/summary', requireAdmin, async (_req, res) => {
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
res.json(await wechatAdmin.getSummary());
});
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/balance-alerts', requireAdmin, async (req, res) => {
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
res.json(await wechatAdmin.listBalanceAlerts(req.query));
});
adminApi.post('/wechat/balance-alerts', requireAdmin, async (req, res) => {
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
const result = await wechatAdmin.createBalanceAlert(req.body ?? {});
if (!result.ok) return res.status(400).json({ message: result.message });
res.status(201).json(result);
});
adminApi.post('/wechat/balance-alerts/:id/cancel', requireAdmin, async (req, res) => {
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
const result = await wechatAdmin.cancelBalanceAlert(req.params.id);
if (!result.ok) return res.status(404).json({ message: result.message ?? '取消失败' });
res.json(result);
});
adminApi.post('/wechat/balance-alerts/:id/resume', requireAdmin, async (req, res) => {
if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' });
const result = await wechatAdmin.resumeBalanceAlert(req.params.id);
if (!result.ok) return res.status(404).json({ message: result.message ?? '恢复失败' });
res.json(result);
});
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('/llm-providers/catalog', requireAdmin, (_req, res) => {
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
res.json({ catalog: llmProviderService.catalog });
});
adminApi.get('/llm-providers/keys', requireAdmin, async (_req, res) => {
if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' });
const keys = await llmProviderService.listKeys();
res.json({ keys });
});
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 {
const result = await llmProviderService.syncSelectedToGoosed();
res.json(result);
} 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 配置' });
const global = await llmProviderService.getGlobalSettings();
res.json({ global });
});
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 {
const result = await llmProviderService.testDraft(req.body ?? {});
res.json(result);
} 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 {
const result = await llmProviderService.testKey(req.params.keyId, req.body?.model);
res.json(result);
} catch (err) {
res.status(500).json({
ok: false,
message: err instanceof Error ? err.message : '联通测试失败',
});
}
});
adminApi.get('/plaza/pending', requireAdmin, async (_req, res) => {
if (!plazaPosts) return res.status(503).json({ message: 'Plaza 未启用' });
const posts = await plazaPosts.listPendingPosts();
res.json({ data: { posts } });
});
adminApi.post('/plaza/posts/:id/review', requireAdmin, async (req, res) => {
if (!plazaPosts) return res.status(503).json({ message: 'Plaza 未启用' });
try {
const action = String(req.body?.action ?? 'approve');
const reason = req.body?.reason ?? null;
const result =
plazaOps && req.currentUser?.id
? await plazaOps.reviewPostAsOps(req.currentUser.id, req.params.id, action, { reason })
: await plazaPosts.reviewPost(req.params.id, action, { reason });
res.json({ data: result });
} catch (error) {
const status = mapPlazaError(error);
res.status(status).json({
error: { code: error?.code ?? 'review_failed', message: error.message },
});
}
});
// ── Subscription management ───────────────────────────────────────────────
adminApi.get('/subscriptions', requireAdmin, async (req, res) => {
if (!subscriptionService) return res.status(503).json({ message: '订阅服务未启用' });
const { userId, status, page, pageSize } = req.query;
const result = await subscriptionService.listSubscriptions({ userId, status, page, pageSize });
res.json(result);
});
adminApi.get('/subscriptions/plans', requireAdmin, (_req, res) => {
res.json(
Object.entries(PLAN_CATALOG).map(([planType, def]) => ({
planType,
...def,
approxCalls: tokensToCallsApprox(def.periodTokens),
})),
);
});
adminApi.get('/users/:userId/subscription', requireAdmin, async (req, res) => {
if (!subscriptionService) return res.status(503).json({ message: '订阅服务未启用' });
const sub = await subscriptionService.getActiveSubscription(req.params.userId);
res.json({ subscription: sub });
});
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.json(result);
});
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);
});
adminApi.post('/subscriptions/expire-stale', requireAdmin, async (_req, res) => {
if (!subscriptionService) return res.status(503).json({ message: '订阅服务未启用' });
const count = await subscriptionService.expireStaleSubscriptions();
res.json({ expiredCount: count });
});
return adminApi;
}
/**
* Plaza operations console API (ops_role: reviewer < editor < ops_admin).
* @param {object} deps
* @param {import('express').RequestHandler} deps.jsonBody
* @param {object|null} deps.plazaOps
*/
export function createOpsApi({ jsonBody, plazaOps }) {
function makeRequireOps(minRole = 'reviewer') {
return async (req, res, next) => {
if (!plazaOps) return sendError(res, req, 503, 'plaza_unavailable', 'Plaza 未启用');
if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权,请重新登录');
const role = await plazaOps.loadOperatorRole(req.currentUser.id);
if (!hasOpsRole(role, minRole)) {
return sendError(res, req, 403, 'OPS_PERMISSION_DENIED', '无运营权限');
}
req.opsRole = role;
return next();
};
}
const opsApi = express.Router();
opsApi.use(jsonBody);
opsApi.use(makeRequireOps('reviewer'));
opsApi.get('/categories', async (req, res) => {
try {
return sendData(res, req, { categories: await plazaOps.listCategories() });
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.get('/review/queue', async (req, res) => {
try {
const queue = await plazaOps.listReviewQueue({
status: req.query.status ?? 'pending_review',
cursor: req.query.cursor ?? null,
limit: req.query.limit,
keyword: req.query.keyword ?? null,
category: req.query.category ?? null,
});
return sendData(res, req, queue);
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.post('/review/posts/:id', async (req, res) => {
try {
const result = await plazaOps.reviewPostAsOps(req.currentUser.id, req.params.id, req.body?.action, {
reason: req.body?.reason ?? null,
});
return sendData(res, req, { post: result });
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.post('/review/batch', makeRequireOps('editor'), async (req, res) => {
try {
const result = await plazaOps.batchReviewPosts(req.currentUser.id, req.body ?? {});
return sendData(res, req, result);
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.get('/reports', async (req, res) => {
try {
return sendData(res, req, await plazaOps.listReports({ status: req.query.status ?? 'pending' }));
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.post('/reports/:id/process', async (req, res) => {
try {
const result = await plazaOps.processReport(req.currentUser.id, req.params.id, req.body ?? {});
return sendData(res, req, { report: result });
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.get('/featured', makeRequireOps('editor'), async (req, res) => {
try {
return sendData(res, req, await plazaOps.listFeatured());
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.post('/featured', makeRequireOps('editor'), async (req, res) => {
try {
const result = await plazaOps.setFeatured(req.currentUser.id, req.body ?? {});
return sendData(res, req, { featured: result }, 201);
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.delete('/featured/:id', makeRequireOps('editor'), async (req, res) => {
try {
const result = await plazaOps.removeFeatured(req.currentUser.id, req.params.id);
return sendData(res, req, { featured: result });
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.get('/analytics/overview', makeRequireOps('ops_admin'), async (req, res) => {
try {
return sendData(res, req, await plazaOps.getAnalyticsOverview());
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.get('/creators', async (req, res) => {
try {
return sendData(res, req, await plazaOps.listCreators({ keyword: req.query.keyword ?? null }));
} catch (error) {
return plazaRouteError(res, req, error);
}
});
opsApi.patch('/creators/:userId', async (req, res) => {
try {
const result = await plazaOps.updateCreator(req.currentUser.id, req.params.userId, req.body ?? {});
return sendData(res, req, { creator: result });
} catch (error) {
return plazaRouteError(res, req, error);
}
});
return opsApi;
}