Extract memind_adm admin server, add local dev tooling, and remove image-generation.
Split platform admin and ops APIs into standalone admin-server.mjs with network guards; simplify billing to RMB token pricing, refactor user auth, and add rsync deploy plus local-test scripts and docs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+43
-546
@@ -21,7 +21,8 @@ import {
|
||||
userSessionCookie,
|
||||
} from './user-auth.mjs';
|
||||
import { createWikiAuth } from './wiki-auth.mjs';
|
||||
import { PUBLISH_ROOT_DIR, PUBLISH_KEY_UUID, PUBLIC_ZONE_DIR, resolvePublicBaseUrl, resolvePublishDir } from './user-publish.mjs';
|
||||
import { isLocalDevHostname } from './scripts/local-test-config.mjs';
|
||||
import { PUBLISH_ROOT_DIR, PUBLISH_KEY_UUID, PUBLIC_ZONE_DIR, resolvePublishDir } from './user-publish.mjs';
|
||||
import { ensureWorkspaceHtmlThumbnail, startWorkspaceThumbnailWatcher, workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
|
||||
import { startWorkspaceAssetSyncWatcher } from './mindspace-workspace-sync.mjs';
|
||||
import { attachRequestId, sendData, sendError } from './api-response.mjs';
|
||||
@@ -31,7 +32,6 @@ import { createMindSpaceService } from './mindspace.mjs';
|
||||
import { createAssetService } from './mindspace-assets.mjs';
|
||||
import { createPageService, pageInternals } from './mindspace-pages.mjs';
|
||||
import { createPageLiveEditService } from './mindspace-page-live-edit.mjs';
|
||||
import { createImageGenerationService } from './image-generation.mjs';
|
||||
import { createPageEditSessionService } from './mindspace-page-edit-session.mjs';
|
||||
import { suggestCoverMetaWithAi } from './mindspace-cover-ai.mjs';
|
||||
import { createPublicationService } from './mindspace-publications.mjs';
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
import { createPlazaRedis, createNoopPlazaRedis } from './plaza-redis.mjs';
|
||||
import { startPlazaTasks, writebackPublications } from './plaza-tasks.mjs';
|
||||
import { createPlazaSeoService } from './plaza-seo.mjs';
|
||||
import { createPlazaOpsService, hasOpsRole } from './plaza-ops.mjs';
|
||||
import { createPlazaOpsService } from './plaza-ops.mjs';
|
||||
import {
|
||||
preparePublicationHtmlForEmbed,
|
||||
isPlazaEmbedRequest,
|
||||
@@ -55,6 +55,7 @@ import { createCleanupService } from './mindspace-cleanup.mjs';
|
||||
import { createAgentJobService } from './mindspace-agent-jobs.mjs';
|
||||
import { createMindSpaceAgentRunner } from './mindspace-agent-runner.mjs';
|
||||
import {
|
||||
analyzeChatMessageForSave,
|
||||
buildChatSavePreviewFrameUrl,
|
||||
buildChatSaveThumbnailUrl,
|
||||
buildWorkspaceAssetUrl,
|
||||
@@ -62,6 +63,7 @@ import {
|
||||
buildWorkspaceThumbnailUrl,
|
||||
injectHtmlBaseHref,
|
||||
resolveChatSaveAnalysis,
|
||||
resolveStaticHtmlContent,
|
||||
} from './mindspace-chat-save.mjs';
|
||||
import { generateHtmlThumbnail } from './mindspace-thumbnails.mjs';
|
||||
import { scanContent } from './mindspace-content-scan.mjs';
|
||||
@@ -99,8 +101,10 @@ const API_TARGET = process.env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006';
|
||||
const API_SECRET = process.env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret';
|
||||
const INTERNAL_AGENT_SECRET = process.env.MINDSPACE_INTERNAL_AGENT_SECRET ?? API_SECRET;
|
||||
const ACCESS_PASSWORD = process.env.H5_ACCESS_PASSWORD;
|
||||
// 无状态前端节点(如 105,MindSpace 经 rclone 挂载)需设 MEMIND_WORKSPACE_MAINTENANCE=0,
|
||||
// 否则启动时对挂载树做 readdir/递归 fs.watch 会占满 libuv 线程池导致 boot 卡死。
|
||||
const WORKSPACE_MAINTENANCE_ENABLED = process.env.MEMIND_WORKSPACE_MAINTENANCE !== '0';
|
||||
const USERS_ROOT = process.env.H5_USERS_ROOT ?? path.join(__dirname, 'users');
|
||||
const PUBLIC_BASE_URL = resolvePublicBaseUrl(process.env);
|
||||
|
||||
const app = express();
|
||||
app.set('trust proxy', 1);
|
||||
@@ -120,10 +124,6 @@ app.use((req, res, next) => {
|
||||
next();
|
||||
});
|
||||
|
||||
function isLoopbackHostname(hostname) {
|
||||
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
||||
}
|
||||
|
||||
function csrfOriginCheck(req, res, next) {
|
||||
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
|
||||
const host = req.get('host');
|
||||
@@ -137,7 +137,7 @@ function csrfOriginCheck(req, res, next) {
|
||||
const source = new URL(value);
|
||||
if (source.host === host) return true;
|
||||
return (
|
||||
isLoopbackHostname(requestHostname) && isLoopbackHostname(source.hostname)
|
||||
isLocalDevHostname(requestHostname) && isLocalDevHostname(source.hostname)
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
@@ -176,7 +176,6 @@ let mindSpaceAssets = null;
|
||||
let mindSpaceAudit = null;
|
||||
let mindSpacePages = null;
|
||||
let mindSpacePageLiveEdit = null;
|
||||
let imageGeneration = null;
|
||||
let mindSpacePageEditSession = null;
|
||||
let mindSpacePublications = null;
|
||||
let plazaPosts = null;
|
||||
@@ -226,18 +225,6 @@ async function bootstrapUserAuth() {
|
||||
return rows[0]?.user_id ?? null;
|
||||
},
|
||||
});
|
||||
imageGeneration = createImageGenerationService({
|
||||
pool,
|
||||
h5Root: __dirname,
|
||||
publicBaseUrl: PUBLIC_BASE_URL,
|
||||
resolveUserIdForAgentSession: async (sessionId) => {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT user_id FROM h5_user_sessions WHERE agent_session_id = ? LIMIT 1`,
|
||||
[sessionId],
|
||||
);
|
||||
return rows[0]?.user_id ?? null;
|
||||
},
|
||||
});
|
||||
mindSpacePublications = createPublicationService(pool, {
|
||||
storageRoot:
|
||||
process.env.MINDSPACE_STORAGE_ROOT ?? path.join(__dirname, 'data', 'mindspace'),
|
||||
@@ -311,25 +298,29 @@ async function bootstrapUserAuth() {
|
||||
agentJobService: mindSpaceAgentJobs,
|
||||
});
|
||||
mindSpaceAudit = createMindSpaceAuditWriter(pool);
|
||||
startWorkspaceThumbnailWatcher(path.join(__dirname, PUBLISH_ROOT_DIR));
|
||||
startWorkspaceAssetSyncWatcher({
|
||||
publishRoot: path.join(__dirname, PUBLISH_ROOT_DIR),
|
||||
syncUserWorkspaceByDirKey: async (dirKey, options) => {
|
||||
let userId = dirKey;
|
||||
if (!PUBLISH_KEY_UUID.test(dirKey)) {
|
||||
const [rows] = await pool.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [
|
||||
dirKey,
|
||||
]);
|
||||
userId = rows[0]?.id;
|
||||
}
|
||||
if (!userId) return;
|
||||
await mindSpaceAssets.syncWorkspaceAssets(userId, options);
|
||||
},
|
||||
});
|
||||
void mindSpaceAssets.expireStaleUploads().catch(() => {});
|
||||
setInterval(() => {
|
||||
void mindSpaceAssets?.expireStaleUploads().catch(() => {});
|
||||
}, 5 * 60 * 1000).unref?.();
|
||||
if (WORKSPACE_MAINTENANCE_ENABLED) {
|
||||
startWorkspaceThumbnailWatcher(path.join(__dirname, PUBLISH_ROOT_DIR));
|
||||
startWorkspaceAssetSyncWatcher({
|
||||
publishRoot: path.join(__dirname, PUBLISH_ROOT_DIR),
|
||||
syncUserWorkspaceByDirKey: async (dirKey, options) => {
|
||||
let userId = dirKey;
|
||||
if (!PUBLISH_KEY_UUID.test(dirKey)) {
|
||||
const [rows] = await pool.query(`SELECT id FROM h5_users WHERE username = ? LIMIT 1`, [
|
||||
dirKey,
|
||||
]);
|
||||
userId = rows[0]?.id;
|
||||
}
|
||||
if (!userId) return;
|
||||
await mindSpaceAssets.syncWorkspaceAssets(userId, options);
|
||||
},
|
||||
});
|
||||
void mindSpaceAssets.expireStaleUploads().catch(() => {});
|
||||
setInterval(() => {
|
||||
void mindSpaceAssets?.expireStaleUploads().catch(() => {});
|
||||
}, 5 * 60 * 1000).unref?.();
|
||||
} else {
|
||||
console.log('Workspace maintenance daemons disabled (MEMIND_WORKSPACE_MAINTENANCE=0)');
|
||||
}
|
||||
await userAuth.ensureAdminUser();
|
||||
llmProviderService = createLlmProviderService(pool, {
|
||||
apiTarget: API_TARGET,
|
||||
@@ -449,16 +440,7 @@ app.post('/auth/login', jsonBody, async (req, res) => {
|
||||
return res.status(401).json({ message: result.message });
|
||||
}
|
||||
setUserLoginCookies(res, req, result.token);
|
||||
const row = await userAuth.getUserById(result.user.id);
|
||||
const capabilityState = row ? await userAuth.resolveUserCapabilities(row) : null;
|
||||
return res.json({
|
||||
authenticated: true,
|
||||
user: result.user,
|
||||
mode: 'user',
|
||||
capabilities: capabilityState?.capabilities,
|
||||
grantedSkills: capabilityState?.grantedSkills ?? [],
|
||||
unrestricted: capabilityState?.unrestricted,
|
||||
});
|
||||
return res.json({ authenticated: true, user: result.user, mode: 'user' });
|
||||
}
|
||||
|
||||
if (!legacyAuth) {
|
||||
@@ -758,348 +740,13 @@ app.post('/auth/logout', async (req, res) => {
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
// ============ Admin API ============
|
||||
|
||||
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) => {
|
||||
await userAuthReady;
|
||||
if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
|
||||
const me = await userAuth.getMe(userToken(req));
|
||||
if (!me) return res.status(401).json({ message: '未登录' });
|
||||
req.currentUser = me;
|
||||
next();
|
||||
});
|
||||
|
||||
adminApi.get('/users', requireAdmin, async (_req, res) => {
|
||||
const users = await userAuth.listUsers();
|
||||
res.json({ users });
|
||||
});
|
||||
|
||||
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/summary', requireAdmin, async (req, res) => {
|
||||
const userId = typeof req.query.userId === 'string' ? req.query.userId : null;
|
||||
const summary = await userAuth.getUsageSummary({ userId });
|
||||
if (userId && !summary) return res.status(404).json({ message: '用户不存在' });
|
||||
res.json({ summary });
|
||||
});
|
||||
|
||||
adminApi.get('/usage', requireAdmin, async (req, res) => {
|
||||
const userId = typeof req.query.userId === 'string' ? req.query.userId : null;
|
||||
const limit = Number(req.query.limit ?? 20);
|
||||
const records = await userAuth.listUsageRecords({ userId, limit });
|
||||
res.json({ records });
|
||||
});
|
||||
|
||||
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 limit = Number(req.query.limit ?? 50);
|
||||
const entries = await userAuth.listBillingLedger({ userId, limit });
|
||||
res.json({ entries });
|
||||
});
|
||||
|
||||
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 },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/auth/usage/summary', async (req, res) => {
|
||||
await userAuthReady;
|
||||
if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
|
||||
const me = await userAuth.getMe(userToken(req));
|
||||
if (!me) return res.status(401).json({ message: '未登录' });
|
||||
const summary = await userAuth.getUsageSummary({ userId: me.id });
|
||||
res.json({ summary });
|
||||
});
|
||||
|
||||
app.get('/auth/usage', async (req, res) => {
|
||||
await userAuthReady;
|
||||
if (!userAuth) return res.status(503).json({ message: '未启用用户系统' });
|
||||
const me = await userAuth.getMe(userToken(req));
|
||||
if (!me) return res.status(401).json({ message: '未登录' });
|
||||
const limit = Number(req.query.limit ?? 20);
|
||||
const records = await userAuth.listUsageRecords({ userId: me.id, limit });
|
||||
const records = await userAuth.listUsageRecords({ userId: me.id, limit: 30 });
|
||||
res.json({ records });
|
||||
});
|
||||
|
||||
@@ -1206,7 +853,6 @@ app.post('/webhooks/wechat-pay/notify', wechatNotifyBody, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.use('/admin-api', adminApi);
|
||||
|
||||
// ============ Wiki API ============
|
||||
|
||||
@@ -1311,7 +957,6 @@ api.use(async (req, res, next) => {
|
||||
if (req.path === '/status') return next();
|
||||
if (req.path.startsWith('/internal/agent/')) return next();
|
||||
if (req.path === '/agent/mindspace_page_patch') return next();
|
||||
if (req.path === '/agent/generate_image') return next();
|
||||
|
||||
const plazaPublic = isPlazaPublicRead(req.path, req.method);
|
||||
|
||||
@@ -1448,124 +1093,6 @@ function plazaClientIp(req) {
|
||||
return req.ip;
|
||||
}
|
||||
|
||||
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('/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,
|
||||
});
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
function mindSpaceError(res, req, error) {
|
||||
const statusByCode = {
|
||||
@@ -1942,7 +1469,7 @@ api.get('/mindspace/v1/assets/:assetId/thumbnail', async (req, res) => {
|
||||
try {
|
||||
const svg = await mindSpaceAssets.renderAssetThumbnail(req.currentUser.id, req.params.assetId);
|
||||
res.set('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.set('Cache-Control', 'private, no-cache');
|
||||
res.set('Cache-Control', 'private, max-age=300');
|
||||
res.setHeader('X-Request-Id', req.requestId);
|
||||
return res.send(svg);
|
||||
} catch (error) {
|
||||
@@ -2091,7 +1618,6 @@ async function resolveChatSaveBundle(user, h5Root, input = {}) {
|
||||
username: user.username,
|
||||
h5Root,
|
||||
selectedLinkIndex,
|
||||
publicBaseUrl: PUBLIC_BASE_URL,
|
||||
});
|
||||
return {
|
||||
source,
|
||||
@@ -2248,13 +1774,12 @@ api.post('/mindspace/v1/pages/save-from-chat', async (req, res) => {
|
||||
req.body?.session_id,
|
||||
req.body?.message_id,
|
||||
);
|
||||
const { analysis, resolvedHtml } = await resolveChatSaveAnalysis({
|
||||
const analysis = analyzeChatMessageForSave({
|
||||
content: source.content,
|
||||
userId: req.currentUser.id,
|
||||
username: req.currentUser.username,
|
||||
h5Root: __dirname,
|
||||
selectedLinkIndex: Number(req.body?.selected_link_index ?? 0),
|
||||
publicBaseUrl: PUBLIC_BASE_URL,
|
||||
});
|
||||
const snapshot = {
|
||||
session_name: source.session.name,
|
||||
@@ -2262,9 +1787,13 @@ api.post('/mindspace/v1/pages/save-from-chat', async (req, res) => {
|
||||
role: source.message.role,
|
||||
content_mode: analysis.contentMode,
|
||||
public_url: analysis.previewUrl,
|
||||
relative_path: resolvedHtml?.relativePath ?? analysis.relativePath,
|
||||
relative_path: analysis.relativePath,
|
||||
};
|
||||
|
||||
let resolvedHtml = null;
|
||||
if (analysis.contentMode === 'static_html') {
|
||||
resolvedHtml = await resolveStaticHtmlContent(analysis).catch(() => null);
|
||||
}
|
||||
const privacyScan = scanContent(resolvedHtml?.content ?? source.content);
|
||||
assertPrivateSaveAllowed(categoryCode, privacyScan, req.body?.acknowledged_finding_ids);
|
||||
|
||||
@@ -2330,7 +1859,7 @@ api.post('/mindspace/v1/pages/save-from-chat', async (req, res) => {
|
||||
|
||||
if (analysis.contentMode === 'static_html' && resolvedHtml?.content && analysis.relativePath) {
|
||||
const publishDir = resolvePublishDir(__dirname, req.currentUser);
|
||||
void ensureWorkspaceHtmlThumbnail(publishDir, analysis.relativePath, resolvedHtml.content, {
|
||||
await ensureWorkspaceHtmlThumbnail(publishDir, analysis.relativePath, resolvedHtml.content, {
|
||||
title: pageInput.title,
|
||||
subtitle: pageInput.summary,
|
||||
}).catch(() => {});
|
||||
@@ -2578,43 +2107,12 @@ api.post('/agent/mindspace_page_patch', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/agent/generate_image', async (req, res) => {
|
||||
if (!imageGeneration) {
|
||||
return res.status(503).json({ message: '图片生成未启用' });
|
||||
}
|
||||
try {
|
||||
const result = await imageGeneration.generateForAgentSession(req.body ?? {});
|
||||
return sendData(res, req, {
|
||||
relative_path: result.relativePath,
|
||||
public_url: result.publicUrl,
|
||||
mime_type: result.mimeType,
|
||||
size: result.size,
|
||||
style: result.style,
|
||||
quality: result.quality,
|
||||
revised_prompt: result.revisedPrompt,
|
||||
prompt: result.prompt,
|
||||
});
|
||||
} catch (error) {
|
||||
const code = error?.code ?? 'internal_error';
|
||||
const statusByCode = {
|
||||
image_gen_unavailable: 503,
|
||||
image_gen_not_configured: 503,
|
||||
invalid_request: 400,
|
||||
invalid_output_path: 400,
|
||||
forbidden: 403,
|
||||
image_gen_failed: 502,
|
||||
};
|
||||
const status = statusByCode[code] ?? 500;
|
||||
return sendError(res, req, status, code, error?.message || '图片生成失败', error?.details);
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/mindspace/v1/pages/:pageId/thumbnail', async (req, res) => {
|
||||
if (!mindSpacePages) return res.status(503).json({ message: 'MindSpace 未启用' });
|
||||
try {
|
||||
const svg = await mindSpacePages.renderThumbnail(req.currentUser.id, req.params.pageId);
|
||||
res.set('Content-Type', 'image/svg+xml; charset=utf-8');
|
||||
res.set('Cache-Control', 'private, no-cache');
|
||||
res.set('Cache-Control', 'private, max-age=300');
|
||||
res.setHeader('X-Request-Id', req.requestId);
|
||||
return res.send(svg);
|
||||
} catch (error) {
|
||||
@@ -3252,7 +2750,6 @@ api.use(async (req, res, next) => {
|
||||
return tkmindProxy.proxyFallback(req, res);
|
||||
});
|
||||
|
||||
api.use('/ops/v1', opsApi);
|
||||
|
||||
api.use(
|
||||
createProxyMiddleware({
|
||||
|
||||
Reference in New Issue
Block a user