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