diff --git a/scripts/nodejs/title_tags_generate.js b/scripts/nodejs/title_tags_generate.js index e2ab3f3..f81f689 100644 --- a/scripts/nodejs/title_tags_generate.js +++ b/scripts/nodejs/title_tags_generate.js @@ -8,17 +8,14 @@ const desktopConfig = require("./desktop_config.js"); const KEYWORD_GROUPS = ["重点词/成语词", "描述词", "行动词", "情感词"]; -const DEFAULT_TITLE_TAG_PROMPT = `你是短视频标题与标签助手。分析以下文案内容,生成: -1. 一个最吸引人的标题(不超过30字,若需多个建议可写在 title 字段内用换行分隔) -2. 8-10 个相关话题标签(带 # 号) -3. 四个分组关键词,每组 0-2 个词:重点词/成语词、描述词、行动词、情感词 +const ELECTRON_DEFAULT_TITLE_TAG_PROMPT = + "你是短视频标题与标签助手。分析文案内容 {{content}},生成5个吸引眼球的标题建议、10个相关标签、4个分组关键词(每组:重点词/成语词、描述词、行动词、情感词,每个分组只要0-2个词不要多)。"; -文案内容: -{{content}} +const JSON_OUTPUT_SUFFIX = ` 请只返回 JSON,不要 markdown,格式如下: { - "title": "标题", + "title": "最推荐标题(多个标题建议可用换行分隔)", "tags": ["#标签1", "#标签2"], "keywords": { "重点词/成语词": ["词1"], @@ -28,12 +25,28 @@ const DEFAULT_TITLE_TAG_PROMPT = `你是短视频标题与标签助手。分析 } }`; +function logInfo(tag, payload) { + if (payload === undefined) { + console.info(tag); + return; + } + if (typeof payload === "string") { + console.info(`${tag} - ${payload}`); + return; + } + console.info(`${tag} - ${JSON.stringify(payload)}`); +} + function buildPrompt(template, content) { let prompt = String(template || "").trim(); - if (!prompt) prompt = DEFAULT_TITLE_TAG_PROMPT; - return prompt + if (!prompt) prompt = ELECTRON_DEFAULT_TITLE_TAG_PROMPT; + prompt = prompt .replace(/\{\{content\}\}/g, content) .replace(/\{content\}/g, content); + if (!/json|JSON/.test(prompt)) { + prompt += JSON_OUTPUT_SUFFIX; + } + return prompt; } function cleanJsonText(raw) { @@ -76,6 +89,16 @@ function normalizeKeywordGroup(val) { return ""; } +function normalizeTitle(val) { + if (Array.isArray(val)) { + return val + .map((t) => String(t || "").trim()) + .filter(Boolean) + .join("\n"); + } + return String(val || "").trim(); +} + function parseTitleTagsContent(content) { let title = ""; let tags = ""; @@ -83,7 +106,8 @@ function parseTitleTagsContent(content) { try { const parsed = JSON.parse(cleanJsonText(content)); - title = String(parsed.title || "").trim(); + logInfo("ipAgent.generateTitleTags.cleanedContent", cleanJsonText(content)); + title = normalizeTitle(parsed.title || parsed.titles); tags = normalizeTags(parsed.tags); const kw = parsed.keywords; if (kw && typeof kw === "object" && !Array.isArray(kw)) { @@ -95,7 +119,12 @@ function parseTitleTagsContent(content) { } else if (typeof kw === "string") { keywords["重点词/成语词"] = normalizeKeywordGroup(kw); } - } catch { + logInfo("ipAgent.generateTitleTags.parsed", { title, tags, keywords }); + } catch (err) { + logInfo("ipAgent.generateTitleTags.jsonParseFailed", { + error: err instanceof Error ? err.message : String(err), + content, + }); const lines = String(content).split("\n"); for (const line of lines) { if (/标题|Title/i.test(line)) { @@ -121,6 +150,16 @@ function parseTitleTagsContent(content) { return { title, tags, keywords }; } +function validateLlmConfig(modelConfig) { + if (!modelConfig.apiKey) { + return { ok: false, error: "缺少配置 LLM_API_KEY(desktop_configs 表)" }; + } + if (!modelConfig.apiUrl) { + return { ok: false, error: "缺少配置 LLM_BASE_URL(或 LLM_API_URL)" }; + } + return { ok: true }; +} + globalThis.__nodejsMain = async function main(params) { const p = params || {}; const content = String(p.content || p.script || "").trim(); @@ -129,12 +168,39 @@ globalThis.__nodejsMain = async function main(params) { } const modelConfig = desktopConfig.buildModelConfig(); - const check = desktopConfig.validateModelConfig(modelConfig); + const check = validateLlmConfig(modelConfig); if (!check.ok) { return { success: false, error: check.error }; } const prompt = buildPrompt(p.titleTagPrompt, content); + const requestPayload = { + model: modelConfig.apiModelId, + messages: [{ role: "user", content: prompt }], + }; + + logInfo("ipAgent.generateTitleTags", { + scriptLength: content.length, + modelConfig: { + providerId: modelConfig.providerId, + modelId: modelConfig.apiModelId, + apiUrl: modelConfig.apiUrl, + apiHost: "", + apiKey: modelConfig.apiKey, + type: modelConfig.type, + }, + }); + logInfo("ipAgent.generateTitleTags.REQUEST", { + apiUrl: modelConfig.apiUrl, + model: modelConfig.apiModelId, + promptLength: prompt.length, + hasApiKey: Boolean(modelConfig.apiKey), + }); + logInfo("ipAgent.generateTitleTags.REQUEST_FULL_PROMPT", prompt); + logInfo( + "ipAgent.generateTitleTags.REQUEST_FULL_PAYLOAD", + JSON.stringify(requestPayload, null, 2), + ); globalThis.__native?.emitProgress?.("正在生成标题、标签和关键词…"); @@ -142,24 +208,35 @@ globalThis.__nodejsMain = async function main(params) { apiUrl: modelConfig.apiUrl, apiKey: modelConfig.apiKey, model: modelConfig.apiModelId, - messages: [{ role: "user", content: prompt }], + messages: requestPayload.messages, temperature: typeof p.temperature === "number" ? p.temperature : 0.7, max_tokens: p.max_tokens || 2000, }); if (!chat.success || !chat.content) { + logInfo("ipAgent.generateTitleTags.api.error", chat.error || "模型返回为空"); return { success: false, error: chat.error || "模型返回为空" }; } + logInfo("ipAgent.generateTitleTags.RESPONSE_CONTENT", chat.content); + logInfo("ipAgent.generateTitleTags.rawContent", { content: chat.content }); + const parsed = parseTitleTagsContent(chat.content); if (!parsed) { + logInfo("ipAgent.generateTitleTags.emptyResult", { content: chat.content }); return { success: false, - error: "解析结果为空,请检查模型返回格式", + error: "解析结果为空,AI返回内容: " + chat.content, rawContent: chat.content, }; } + logInfo("ipAgent.generateTitleTags.success", { + title: parsed.title, + tags: parsed.tags, + keywords: parsed.keywords, + }); + return { success: true, title: parsed.title,