764981c086
# Conflicts: # agent-run-gateway.test.mjs # capabilities.mjs # package.json # server.mjs
972 lines
39 KiB
JavaScript
972 lines
39 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.orchestratorConfigService
|
|
* @param {object|null} deps.orchestratorObservabilityService
|
|
* @param {object|null} deps.skillRuntimeConfigService
|
|
* @param {object|null} deps.systemDisclosurePolicyService
|
|
* @param {object|null} deps.agentCodeRunPolicyService
|
|
* @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,
|
|
orchestratorConfigService,
|
|
orchestratorObservabilityService,
|
|
mindSearchConfigService,
|
|
skillRuntimeConfigService,
|
|
systemDisclosurePolicyService,
|
|
agentCodeRunPolicyService,
|
|
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.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/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('/orchestrator/config', requireAdmin, async (_req, res) => {
|
|
if (!orchestratorConfigService?.getAdminConfig) {
|
|
return res.status(503).json({ message: 'Orchestrator 配置服务未启用' });
|
|
}
|
|
return res.json(await orchestratorConfigService.getAdminConfig());
|
|
});
|
|
|
|
const updateOrchestratorConfig = async (req, res) => {
|
|
if (!orchestratorConfigService?.updateAdminConfig) {
|
|
return res.status(503).json({ message: 'Orchestrator 配置服务未启用' });
|
|
}
|
|
const result = await orchestratorConfigService.updateAdminConfig(
|
|
req.body?.config ?? req.body ?? {},
|
|
{ updatedBy: req.currentUser.id },
|
|
);
|
|
return res.json(result);
|
|
};
|
|
|
|
adminApi.put('/orchestrator/config', requireAdmin, updateOrchestratorConfig);
|
|
adminApi.patch('/orchestrator/config', requireAdmin, updateOrchestratorConfig);
|
|
|
|
adminApi.get('/orchestrator/runtime', requireAdmin, async (_req, res) => {
|
|
if (!orchestratorConfigService?.getRuntimeState) {
|
|
return res.status(503).json({ message: 'Orchestrator 配置服务未启用' });
|
|
}
|
|
return res.json(await orchestratorConfigService.getRuntimeState({ probe: true }));
|
|
});
|
|
|
|
adminApi.get('/orchestrator/shadow-runs', requireAdmin, async (req, res) => {
|
|
if (!orchestratorObservabilityService?.listShadowRuns) {
|
|
return res.status(503).json({ message: 'Orchestrator 观测服务未启用' });
|
|
}
|
|
return res.json(await orchestratorObservabilityService.listShadowRuns({
|
|
hours: req.query.hours,
|
|
limit: req.query.limit,
|
|
status: req.query.status,
|
|
}));
|
|
});
|
|
|
|
adminApi.get('/orchestrator/execution-plans', requireAdmin, async (req, res) => {
|
|
if (!orchestratorObservabilityService?.listExecutionPlans) {
|
|
return res.status(503).json({ message: 'Orchestrator Dry-run 观测服务未启用' });
|
|
}
|
|
return res.json(await orchestratorObservabilityService.listExecutionPlans({
|
|
hours: req.query.hours,
|
|
limit: req.query.limit,
|
|
selection: req.query.selection,
|
|
}));
|
|
});
|
|
|
|
adminApi.get('/orchestrator/canary-readiness', requireAdmin, async (_req, res) => {
|
|
if (!orchestratorObservabilityService?.getCanaryReadiness) {
|
|
return res.status(503).json({ message: 'Orchestrator 观测服务未启用' });
|
|
}
|
|
return res.json(await orchestratorObservabilityService.getCanaryReadiness());
|
|
});
|
|
|
|
adminApi.get('/orchestrator/shadow-runs/:runId', requireAdmin, async (req, res) => {
|
|
if (!orchestratorObservabilityService?.getShadowRun) {
|
|
return res.status(503).json({ message: 'Orchestrator 观测服务未启用' });
|
|
}
|
|
const result = await orchestratorObservabilityService.getShadowRun(req.params.runId);
|
|
if (!result) return res.status(404).json({ message: 'Shadow run 不存在' });
|
|
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('/system-disclosure-policy/config', requireAdmin, async (_req, res) => {
|
|
if (!systemDisclosurePolicyService?.getAdminConfig) {
|
|
return res.status(503).json({ message: 'System Disclosure Policy 服务未启用' });
|
|
}
|
|
return res.json(await systemDisclosurePolicyService.getAdminConfig());
|
|
});
|
|
|
|
const updateSystemDisclosurePolicy = async (req, res) => {
|
|
if (!systemDisclosurePolicyService?.updateAdminConfig) {
|
|
return res.status(503).json({ message: 'System Disclosure Policy 服务未启用' });
|
|
}
|
|
const result = await systemDisclosurePolicyService.updateAdminConfig(req.body?.config ?? req.body ?? {}, {
|
|
updatedBy: req.currentUser.id,
|
|
});
|
|
return res.json(result);
|
|
};
|
|
|
|
adminApi.put('/system-disclosure-policy/config', requireAdmin, updateSystemDisclosurePolicy);
|
|
adminApi.patch('/system-disclosure-policy/config', requireAdmin, updateSystemDisclosurePolicy);
|
|
|
|
adminApi.get('/system-disclosure-policy/runtime', requireAdmin, async (_req, res) => {
|
|
if (!systemDisclosurePolicyService?.getRuntimeState) {
|
|
return res.status(503).json({ message: 'System Disclosure Policy 服务未启用' });
|
|
}
|
|
return res.json(systemDisclosurePolicyService.getRuntimeState());
|
|
});
|
|
|
|
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.get('/agent-code-run/config', requireAdmin, async (_req, res) => {
|
|
if (!agentCodeRunPolicyService?.getAdminConfig) {
|
|
return res.status(503).json({ message: 'Agent Code Run 配置服务未启用' });
|
|
}
|
|
return res.json(await agentCodeRunPolicyService.getAdminConfig());
|
|
});
|
|
|
|
const updateAgentCodeRunConfig = async (req, res) => {
|
|
if (!agentCodeRunPolicyService?.updateAdminConfig) {
|
|
return res.status(503).json({ message: 'Agent Code Run 配置服务未启用' });
|
|
}
|
|
try {
|
|
const result = await agentCodeRunPolicyService.updateAdminConfig(req.body?.config ?? req.body ?? {}, {
|
|
updatedBy: req.currentUser.id,
|
|
});
|
|
return res.json(result);
|
|
} catch (err) {
|
|
if (err?.code === 'CODE_RUN_POLICY_ENV_LOCKED') {
|
|
return res.status(409).json({ message: err.message });
|
|
}
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
adminApi.put('/agent-code-run/config', requireAdmin, updateAgentCodeRunConfig);
|
|
adminApi.patch('/agent-code-run/config', requireAdmin, updateAgentCodeRunConfig);
|
|
|
|
adminApi.get('/agent-code-run/runtime', requireAdmin, async (_req, res) => {
|
|
if (!agentCodeRunPolicyService?.getRuntimeState) {
|
|
return res.status(503).json({ message: 'Agent Code Run 配置服务未启用' });
|
|
}
|
|
return res.json(await agentCodeRunPolicyService.getRuntimeState());
|
|
});
|
|
|
|
adminApi.get('/agent-code-run/runs', requireAdmin, async (req, res) => {
|
|
if (!agentCodeRunPolicyService?.listRecentPageDataDevRuns) {
|
|
return res.status(503).json({ message: 'Agent Code Run 配置服务未启用' });
|
|
}
|
|
const limit = Number(req.query?.limit ?? 50);
|
|
return res.json(await agentCodeRunPolicyService.listRecentPageDataDevRuns({ limit }));
|
|
});
|
|
|
|
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;
|
|
}
|