// 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} [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.memoryV2AdminOpsService * @param {object|null} deps.goalRunAdminOpsService * @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 * @param {object|null} deps.wechatIntentRouterConfigService */ export function createAdminApi({ jsonBody, getToken, ready, userAuth, llmProviderService, assetGatewayConfigService, imageMakeAdminConfigService, memoryV2ConfigService, memoryV2AdminOpsService, goalRunAdminOpsService, orchestratorConfigService, orchestratorObservabilityService, mindSearchConfigService, skillRuntimeConfigService, systemDisclosurePolicyService, agentCodeRunPolicyService, adminSystemTestService, plazaPosts, plazaOps, wechatAdmin, wechatIntentRouterConfigService, 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('/memory-v2/status', requireAdmin, async (_req, res) => { const portalBase = String( process.env.SYSTEM_TEST_PORTAL_BASE_URL ?? `http://127.0.0.1:${process.env.H5_PORT ?? 8081}`, ).replace(/\/$/, ''); try { const upstream = await fetch(`${portalBase}/api/runtime/status`, { headers: { accept: 'application/json' }, }); const payload = await upstream.json().catch(() => ({})); if (!upstream.ok) { return res.status(upstream.status).json({ ok: false, message: payload?.message ?? '读取 Portal Memory V2 运行时状态失败', memory: null, }); } return res.json({ ok: Boolean(payload?.ok ?? true), memory: payload?.memory ?? null, }); } catch (err) { return res.status(502).json({ ok: false, message: err instanceof Error ? err.message : '读取 Portal Memory V2 运行时状态失败', memory: null, }); } }); adminApi.get('/memory-v2/metrics', requireAdmin, async (req, res) => { if (!memoryV2AdminOpsService?.getProductMetrics) { return res.status(503).json({ message: 'Memory V2 指标服务未启用' }); } try { const since = String(req.query?.since ?? '7d').trim() || '7d'; const userId = String(req.query?.userId ?? '').trim() || null; const metrics = await memoryV2AdminOpsService.getProductMetrics({ since, userId }); const audit = await memoryV2AdminOpsService.getShadowAudit({ since, userId }); const counts = await memoryV2AdminOpsService.countCandidatesByStatus(); return res.json({ metrics, audit, candidateCounts: counts }); } catch (err) { return res.status(500).json({ message: err instanceof Error ? err.message : '读取 Memory V2 指标失败', }); } }); adminApi.get('/memory-v2/candidates', requireAdmin, async (req, res) => { if (!memoryV2AdminOpsService?.listCandidates) { return res.status(503).json({ message: 'Memory V2 候选服务未启用' }); } try { const status = String(req.query?.status ?? 'candidate').trim() || 'candidate'; const userId = String(req.query?.userId ?? '').trim() || null; const limit = Number(req.query?.limit ?? 50); const offset = Number(req.query?.offset ?? 0); const items = await memoryV2AdminOpsService.listCandidates({ status, userId, limit, offset }); const counts = await memoryV2AdminOpsService.countCandidatesByStatus(); return res.json({ items, status, limit, offset, counts }); } catch (err) { return res.status(400).json({ message: err instanceof Error ? err.message : '读取候选记忆失败', }); } }); adminApi.post('/memory-v2/candidates/auto-review', requireAdmin, async (req, res) => { if (!memoryV2AdminOpsService?.runAutoReview) { return res.status(503).json({ message: 'Memory V2 候选服务未启用' }); } try { const userId = String(req.body?.userId ?? '').trim() || null; const limit = Number(req.body?.limit ?? 200); const result = await memoryV2AdminOpsService.runAutoReview({ userId, limit }); return res.json({ ok: true, ...result }); } catch (err) { return res.status(500).json({ message: err instanceof Error ? err.message : '自动审核候选记忆失败', }); } }); adminApi.post('/memory-v2/candidates/:id/review', requireAdmin, async (req, res) => { if (!memoryV2AdminOpsService?.reviewCandidate) { return res.status(503).json({ message: 'Memory V2 候选服务未启用' }); } const status = String(req.body?.status ?? '').trim(); if (!['accepted', 'rejected'].includes(status)) { return res.status(400).json({ message: 'status 必须是 accepted 或 rejected' }); } try { const result = await memoryV2AdminOpsService.reviewCandidate(req.params.id, status, { reviewedBy: req.currentUser.id, }); if (!result.updated) { return res.status(409).json({ message: '候选不存在或已审核' }); } return res.json({ ok: true, ...result }); } catch (err) { return res.status(400).json({ message: err instanceof Error ? err.message : '审核候选记忆失败', }); } }); adminApi.get('/goal-runs/runtime', requireAdmin, async (_req, res) => { if (!goalRunAdminOpsService?.getRuntime) { return res.status(503).json({ message: 'Goal Run 管理服务未启用' }); } return res.json(goalRunAdminOpsService.getRuntime()); }); adminApi.get('/goal-runs/summary', requireAdmin, async (_req, res) => { if (!goalRunAdminOpsService?.countByStatus) { return res.status(503).json({ message: 'Goal Run 管理服务未启用' }); } try { const counts = await goalRunAdminOpsService.countByStatus(); const runtime = goalRunAdminOpsService.getRuntime(); return res.json({ counts, runtime }); } catch (err) { return res.status(500).json({ message: err instanceof Error ? err.message : '读取 Goal Run 汇总失败', }); } }); adminApi.get('/goal-runs', requireAdmin, async (req, res) => { if (!goalRunAdminOpsService?.listGoals) { return res.status(503).json({ message: 'Goal Run 管理服务未启用' }); } try { const status = String(req.query?.status ?? '').trim() || null; const userId = String(req.query?.userId ?? req.query?.user_id ?? '').trim() || null; const limit = Number(req.query?.limit ?? 50); const offset = Number(req.query?.offset ?? 0); const items = await goalRunAdminOpsService.listGoals({ status, userId, limit, offset }); return res.json({ items, status, limit, offset }); } catch (err) { return res.status(400).json({ message: err instanceof Error ? err.message : '读取 Goal Run 列表失败', }); } }); adminApi.get('/goal-runs/:goalRunId', requireAdmin, async (req, res) => { if (!goalRunAdminOpsService?.getGoalDetail) { return res.status(503).json({ message: 'Goal Run 管理服务未启用' }); } try { const goal = await goalRunAdminOpsService.getGoalDetail(req.params.goalRunId); if (!goal) return res.status(404).json({ message: '目标不存在' }); return res.json({ goal }); } catch (err) { return res.status(500).json({ message: err instanceof Error ? err.message : '读取 Goal Run 详情失败', }); } }); adminApi.post('/goal-runs/:goalRunId/checkpoints/:checkpointId/approve', requireAdmin, async (req, res) => { if (!goalRunAdminOpsService?.approveCheckpoint) { return res.status(503).json({ message: 'Goal Run 管理服务未启用' }); } try { const goal = await goalRunAdminOpsService.approveCheckpoint({ goalRunId: req.params.goalRunId, checkpointId: req.params.checkpointId, feedback: req.body?.feedback ?? null, reviewedBy: req.currentUser.id, }); return res.json({ ok: true, goal }); } catch (err) { const status = err?.code === 'GOAL_RUN_NOT_FOUND' || err?.code === 'GOAL_CHECKPOINT_NOT_FOUND' ? 404 : err?.code === 'GOAL_CHECKPOINT_NOT_APPROVABLE' ? 409 : 500; return res.status(status).json({ message: err instanceof Error ? err.message : '确认阶段失败', code: err?.code ?? null, }); } }); adminApi.post('/goal-runs/:goalRunId/cancel', requireAdmin, async (req, res) => { if (!goalRunAdminOpsService?.cancelGoal) { return res.status(503).json({ message: 'Goal Run 管理服务未启用' }); } try { const goal = await goalRunAdminOpsService.cancelGoal({ goalRunId: req.params.goalRunId, reviewedBy: req.currentUser.id, }); return res.json({ ok: true, goal }); } catch (err) { const status = err?.code === 'GOAL_RUN_NOT_FOUND' ? 404 : err?.code === 'GOAL_RUN_NOT_CANCELLABLE' ? 409 : 500; return res.status(status).json({ message: err instanceof Error ? err.message : '取消目标失败', code: err?.code ?? null, }); } }); 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('/wechat/intent-router/config', requireAdmin, async (_req, res) => { if (!wechatIntentRouterConfigService?.getConfig) { return res.status(503).json({ message: '微信意图路由配置未启用' }); } const config = await wechatIntentRouterConfigService.getConfig(); return res.json({ config }); }); adminApi.get('/wechat/intent-router/runtime', requireAdmin, async (_req, res) => { if (!wechatIntentRouterConfigService?.getRuntimeState) { return res.status(503).json({ message: '微信意图路由配置未启用' }); } return res.json(await wechatIntentRouterConfigService.getRuntimeState()); }); const updateWechatIntentRouterConfig = async (req, res) => { if (!wechatIntentRouterConfigService?.updateConfig) { return res.status(503).json({ message: '微信意图路由配置未启用' }); } const config = await wechatIntentRouterConfigService.updateConfig(req.body ?? {}, { updatedBy: req.currentUser.id, }); return res.json({ config }); }; adminApi.put('/wechat/intent-router/config', requireAdmin, updateWechatIntentRouterConfig); adminApi.patch('/wechat/intent-router/config', requireAdmin, updateWechatIntentRouterConfig); 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; }