feat(admin): add News Engine console and image-make config API
Expose memind-news-engine health, collect, rank, and scheduler controls in md.tkmind.cn admin, proxied via MEMIND_NEWS_ENGINE_URL. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+125
@@ -7,6 +7,7 @@ import express from 'express';
|
||||
import { listUsagePaged, listLedgerPaged, getUsageStats, getUsageSummary } from './pagination.mjs';
|
||||
import { fetchMemindDiscoveryPages } from './umami-analytics.mjs';
|
||||
import { importMemind } from './lib-path.mjs';
|
||||
import { fetchNewsEngine } from './news-engine-admin.mjs';
|
||||
|
||||
const projectRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
@@ -95,6 +96,7 @@ export function createAdminApp(services) {
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
imageMakeAdminConfigService,
|
||||
pool,
|
||||
ready,
|
||||
wechatAdmin,
|
||||
@@ -624,6 +626,64 @@ export function createAdminApp(services) {
|
||||
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 },
|
||||
fal_ai: providers.fal_ai.enabled
|
||||
? {
|
||||
enabled: true,
|
||||
model: providers.fal_ai.model,
|
||||
apiBase: providers.fal_ai.apiBase,
|
||||
numInferenceSteps: providers.fal_ai.numInferenceSteps,
|
||||
apiKeyConfigured: Boolean(providers.fal_ai.apiKey),
|
||||
}
|
||||
: { enabled: false },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
adminApi.get('/memory-v2/config', requireAdmin, async (_req, res) => {
|
||||
if (!memoryV2ConfigService) return res.status(503).json({ message: 'Memory V2 配置未启用' });
|
||||
res.json(await memoryV2ConfigService.getAdminConfig());
|
||||
@@ -674,6 +734,71 @@ export function createAdminApp(services) {
|
||||
}
|
||||
});
|
||||
|
||||
adminApi.get('/news-engine/health', requireAdmin, async (_req, res) => {
|
||||
const result = await fetchNewsEngine('/health');
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 不可用' });
|
||||
});
|
||||
|
||||
adminApi.get('/news-engine/config', requireAdmin, async (_req, res) => {
|
||||
const result = await fetchNewsEngine('/v1/config');
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 不可用' });
|
||||
});
|
||||
|
||||
adminApi.get('/news-engine/collections', requireAdmin, async (req, res) => {
|
||||
const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 20));
|
||||
const result = await fetchNewsEngine(`/v1/collections?limit=${limit}`);
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 不可用' });
|
||||
});
|
||||
|
||||
adminApi.get('/news-engine/collections/latest', requireAdmin, async (_req, res) => {
|
||||
const result = await fetchNewsEngine('/v1/collections/latest');
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 不可用' });
|
||||
});
|
||||
|
||||
adminApi.get('/news-engine/collections/:id', requireAdmin, async (req, res) => {
|
||||
const result = await fetchNewsEngine(`/v1/collections/${encodeURIComponent(req.params.id)}`);
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 不可用' });
|
||||
});
|
||||
|
||||
adminApi.post('/news-engine/collect', requireAdmin, async (req, res) => {
|
||||
const result = await fetchNewsEngine('/v1/collect', { method: 'POST', body: req.body ?? {} });
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 采集失败' });
|
||||
});
|
||||
|
||||
adminApi.post('/news-engine/collections/:id/score', requireAdmin, async (req, res) => {
|
||||
const result = await fetchNewsEngine(`/v1/collections/${encodeURIComponent(req.params.id)}/score`, {
|
||||
method: 'POST',
|
||||
body: req.body ?? {},
|
||||
});
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 评分失败' });
|
||||
});
|
||||
|
||||
adminApi.post('/news-engine/rank', requireAdmin, async (req, res) => {
|
||||
const result = await fetchNewsEngine('/v1/rank', { method: 'POST', body: req.body ?? {} });
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 排序失败' });
|
||||
});
|
||||
|
||||
adminApi.get('/news-engine/runs', requireAdmin, async (req, res) => {
|
||||
const limit = Math.min(50, Math.max(1, Number(req.query.limit) || 30));
|
||||
const result = await fetchNewsEngine(`/v1/runs?limit=${limit}`);
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 不可用' });
|
||||
});
|
||||
|
||||
adminApi.get('/news-engine/runs/:id', requireAdmin, async (req, res) => {
|
||||
const result = await fetchNewsEngine(`/v1/runs/${encodeURIComponent(req.params.id)}`);
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 不可用' });
|
||||
});
|
||||
|
||||
adminApi.get('/news-engine/scheduler/status', requireAdmin, async (_req, res) => {
|
||||
const result = await fetchNewsEngine('/v1/scheduler/status');
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 不可用' });
|
||||
});
|
||||
|
||||
adminApi.post('/news-engine/scheduler/trigger', requireAdmin, async (req, res) => {
|
||||
const result = await fetchNewsEngine('/v1/scheduler/trigger', { method: 'POST', body: req.body ?? {} });
|
||||
return res.status(result.ok ? 200 : result.status || 502).json(result.data ?? { message: 'News Engine 采集触发失败' });
|
||||
});
|
||||
|
||||
adminApi.get('/orchestrator/config', requireAdmin, async (_req, res) => {
|
||||
if (!orchestratorConfigService?.getAdminConfig) {
|
||||
return res.status(503).json({ message: 'Orchestrator 配置服务未启用' });
|
||||
|
||||
@@ -41,6 +41,10 @@ export async function bootstrapAdminServices() {
|
||||
const { createSkillRuntimeAdminConfigService } = await importMemind('skill-runtime-admin-config.mjs');
|
||||
const { createBillingAdminConfigService } = await importMemind('billing-admin-config.mjs');
|
||||
const { createAssetGatewayConfigService } = await importMemind('asset-gateway.mjs');
|
||||
const {
|
||||
createImageMakeAdminConfigService,
|
||||
ensureImageMakeAdminConfigSchema,
|
||||
} = await importMemind('image-make-admin-config.mjs');
|
||||
const { ensureAssetGatewaySchema } = await importMemind('db.mjs');
|
||||
const { createWechatScheduleLlmConfigService } = await importMemind('wechat-schedule-llm-config.mjs');
|
||||
const { createWechatNewsMorningDraftService } = await importMemind('wechat-news-morning-draft.mjs');
|
||||
@@ -105,7 +109,12 @@ export async function bootstrapAdminServices() {
|
||||
apiSecret,
|
||||
});
|
||||
await ensureAssetGatewaySchema(pool);
|
||||
await ensureImageMakeAdminConfigSchema(pool);
|
||||
const assetGatewayConfigService = createAssetGatewayConfigService(pool, { llmProviderService });
|
||||
const imageMakeAdminConfigService = createImageMakeAdminConfigService(pool, {
|
||||
env: process.env,
|
||||
llmProviderService,
|
||||
});
|
||||
const memoryV2ConfigService = createMemoryV2AdminConfigService(pool, {
|
||||
env: process.env,
|
||||
});
|
||||
@@ -197,6 +206,7 @@ export async function bootstrapAdminServices() {
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
imageMakeAdminConfigService,
|
||||
plazaOps,
|
||||
createOpsApi,
|
||||
wechatAdmin,
|
||||
|
||||
@@ -99,6 +99,7 @@ ready
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
imageMakeAdminConfigService,
|
||||
plazaOps,
|
||||
createOpsApi,
|
||||
wechatAdmin,
|
||||
@@ -132,6 +133,7 @@ ready
|
||||
userAuth,
|
||||
llmProviderService,
|
||||
assetGatewayConfigService,
|
||||
imageMakeAdminConfigService,
|
||||
plazaOps,
|
||||
createOpsApi,
|
||||
wechatAdmin,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
export function resolveNewsEngineBaseUrl(env = process.env) {
|
||||
return String(env.MEMIND_NEWS_ENGINE_URL ?? 'http://127.0.0.1:8092').trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
export async function fetchNewsEngine(path, {
|
||||
env = process.env,
|
||||
method = 'GET',
|
||||
body = null,
|
||||
fetchImpl = fetch,
|
||||
} = {}) {
|
||||
const baseUrl = resolveNewsEngineBaseUrl(env);
|
||||
if (!baseUrl) {
|
||||
return { ok: false, status: 503, data: { message: '未配置 MEMIND_NEWS_ENGINE_URL' } };
|
||||
}
|
||||
const token = String(env.MEMIND_NEWS_ENGINE_API_TOKEN ?? '').trim();
|
||||
const response = await fetchImpl(`${baseUrl}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
...(body ? { 'content-type': 'application/json' } : {}),
|
||||
...(token ? { authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal: AbortSignal.timeout(Number(env.MEMIND_NEWS_ENGINE_TIMEOUT_MS ?? 120000) || 120000),
|
||||
});
|
||||
const text = await response.text().catch(() => '');
|
||||
let data = null;
|
||||
try {
|
||||
data = text ? JSON.parse(text) : null;
|
||||
} catch {
|
||||
data = { message: text.slice(0, 500) || `HTTP ${response.status}` };
|
||||
}
|
||||
return { ok: response.ok, status: response.status, data };
|
||||
}
|
||||
Reference in New Issue
Block a user