Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { Agent, fetch as undiciFetch } from 'undici';
|
||||
import { jsonrepair } from 'jsonrepair';
|
||||
import {
|
||||
decryptSecret,
|
||||
normalizeApiUrl,
|
||||
resolveChatCompletionsUrl,
|
||||
} from './llm-providers.mjs';
|
||||
import { normalizeCoverMetaSuggestion, parseMindspaceCoverMeta } from './mindspace-cover-meta.mjs';
|
||||
|
||||
const insecureDispatcher = new Agent({
|
||||
connect: { rejectUnauthorized: false },
|
||||
});
|
||||
|
||||
function resolveEncryptionKey(explicitKey) {
|
||||
const raw =
|
||||
explicitKey ??
|
||||
process.env.H5_SETTINGS_ENCRYPTION_KEY ??
|
||||
process.env.TKMIND_SERVER__SECRET_KEY ??
|
||||
'local-dev-secret';
|
||||
return raw;
|
||||
}
|
||||
|
||||
function parseJsonObject(text) {
|
||||
const source = String(text ?? '').trim();
|
||||
if (!source) return null;
|
||||
try {
|
||||
return JSON.parse(source);
|
||||
} catch {
|
||||
try {
|
||||
return JSON.parse(jsonrepair(source));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extractJsonObject(text) {
|
||||
const direct = parseJsonObject(text);
|
||||
if (direct && typeof direct === 'object') return direct;
|
||||
const source = String(text ?? '').trim();
|
||||
const start = source.indexOf('{');
|
||||
const end = source.lastIndexOf('}');
|
||||
if (start < 0 || end <= start) {
|
||||
throw Object.assign(new Error('AI 未返回有效 JSON'), { code: 'cover_ai_invalid_output' });
|
||||
}
|
||||
const raw = source.slice(start, end + 1);
|
||||
try {
|
||||
return JSON.parse(jsonrepair(raw));
|
||||
} catch (error) {
|
||||
throw Object.assign(new Error('AI 封面 JSON 解析失败'), {
|
||||
code: 'cover_ai_invalid_output',
|
||||
cause: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function buildCoverPrompt({ title, summary, html, instruction, currentCover }) {
|
||||
const excerpt = String(html ?? '')
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
.slice(0, 1200);
|
||||
|
||||
return [
|
||||
'你是 MindSpace 信息流封面设计助手。请根据页面内容生成 mindspace-cover 元数据。',
|
||||
'只返回一个 JSON 对象,不要 Markdown,不要解释。',
|
||||
'字段要求:',
|
||||
'- tag: 2-4 个汉字分类,如 旅行/美食/活动/报告',
|
||||
'- emoji: 1 个与主题高度相关的 emoji',
|
||||
'- accent: 主色 hex,如 #eeb04e',
|
||||
'- accent2: 辅色 hex',
|
||||
'- subtitle: 12-28 字副标题,用于封面底部',
|
||||
'- cover: 可选,若页面已有 hero 图路径可保留,否则留空字符串',
|
||||
'',
|
||||
`页面标题:${title || '未命名页面'}`,
|
||||
`页面摘要:${summary || '无'}`,
|
||||
`当前 cover 元数据:${JSON.stringify(currentCover ?? {})}`,
|
||||
`页面正文摘录:${excerpt || '无'}`,
|
||||
instruction ? `用户补充要求:${instruction}` : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
export async function suggestCoverMetaWithAi(
|
||||
pool,
|
||||
{ title, summary, html, instruction, encryptionKey },
|
||||
) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT * FROM h5_llm_provider_keys WHERE is_selected = 1 AND status = 'active' LIMIT 1`,
|
||||
);
|
||||
const row = rows[0];
|
||||
if (!row) {
|
||||
throw Object.assign(new Error('请先在管理后台配置并启用 LLM'), { code: 'llm_not_configured' });
|
||||
}
|
||||
|
||||
const apiKey = decryptSecret(
|
||||
{
|
||||
ciphertext: row.api_key_ciphertext,
|
||||
iv: row.api_key_iv,
|
||||
tag: row.api_key_tag,
|
||||
},
|
||||
resolveEncryptionKey(encryptionKey),
|
||||
);
|
||||
const apiUrl = normalizeApiUrl(row.api_url);
|
||||
const url = resolveChatCompletionsUrl(apiUrl);
|
||||
if (!url) {
|
||||
throw Object.assign(new Error('LLM API 地址无效'), { code: 'llm_not_configured' });
|
||||
}
|
||||
|
||||
const prompt = buildCoverPrompt({
|
||||
title,
|
||||
summary,
|
||||
html,
|
||||
instruction,
|
||||
currentCover: parseMindspaceCoverMeta(html),
|
||||
});
|
||||
|
||||
const upstream = await undiciFetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: row.default_model,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
stream: false,
|
||||
...(row.relay_provider ? { provider: row.relay_provider } : {}),
|
||||
}),
|
||||
dispatcher: url.startsWith('https://') ? insecureDispatcher : undefined,
|
||||
});
|
||||
|
||||
const text = await upstream.text().catch(() => '');
|
||||
if (!upstream.ok) {
|
||||
throw Object.assign(new Error(`AI 封面生成失败:${text.slice(0, 240) || upstream.status}`), {
|
||||
code: 'cover_ai_failed',
|
||||
});
|
||||
}
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
throw Object.assign(new Error('AI 响应不是 JSON'), { code: 'cover_ai_failed' });
|
||||
}
|
||||
|
||||
const reply =
|
||||
data?.choices?.[0]?.message?.content ??
|
||||
data?.message?.content ??
|
||||
data?.output ??
|
||||
text;
|
||||
const parsed = normalizeCoverMetaSuggestion(extractJsonObject(reply));
|
||||
if (!parsed.tag && !parsed.emoji && !parsed.accent) {
|
||||
throw Object.assign(new Error('AI 未生成有效封面参数'), { code: 'cover_ai_invalid_output' });
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
Reference in New Issue
Block a user