import { defineStore, acceptHMRUpdate } from "pinia"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; import { ensureAppConfigReady, ensureLlmConfigReady, validateShareText, DEFAULT_REWRITE_CONFIG, } from "../config/videoPipeline.js"; import { getVoiceDisplayLabel } from "../utils/voiceLabel.js"; import { executeGenerateSpeech, selectAudioHistory, } from "../services/voiceGenerate.js"; import { executeGenerateTalkingVideo, selectVideoHistory, } from "../services/talkingVideoGenerate.js"; import { executeAutoProcessVideo, pickGreenScreenBackground, } from "../services/videoEditProcess.js"; import { executeGenerateTitleTags } from "../services/titleTagsGenerate.js"; import { chooseBgm, executeGenerateSubtitleAndBgm, previewBgm, } from "../services/subtitleBgmGenerate.js"; import { chooseCoverMaterial, executeGenerateCover, } from "../services/coverGenerate.js"; import { getCoverTemplateById } from "../services/coverTemplateCatalog.js"; import { executeOneClickPipeline, stopOneClickPipeline, } from "../services/oneClickPipeline.js"; import { createDefaultPublishPlatforms, executePublishVideo, checkPublishLoginStatus, openPublishAccount, loginPublishPlatform, refreshPublishAccounts, } from "../services/videoPublish.js"; import { SUBTITLE_TEMPLATES } from "../config/subtitleTemplates.js"; import { listGeneratedAudios } from "../services/audioDb.js"; import { listGeneratedVideos } from "../services/videoDb.js"; import { localAudioToPlayableUrl } from "../services/localAudio.js"; import { localVideoToPlayableUrl } from "../services/localVideo.js"; const PIPELINE_SCRIPT = "douyin_pipeline.js"; const SCRIPT_GENERATE = "script_generate.js"; const SCRIPT_LEGAL_CHECK = "script_legal_check.js"; const IP_BRAIN_COLLECT = "ip_brain_collect.js"; const IP_TOPIC_REWRITE = "ip_topic_rewrite.js"; const NODEJS_EVENT = "nodejs:event"; export const useWorkflowStore = defineStore("workflow", { state: () => ({ // 01 IP 深度学习 ipMode: "ipBrain", ipBrainModalVisible: false, ipBrainProfileUrl: "", ipBrainCollecting: false, ipBrainCollectProgress: "", ipBenchmarks: [], /** 当前选中对标的选题(与 benchmark.topicLibrary 同步引用) */ ipTopics: [], ipBrainSelectedBenchmarkId: null, ipBrainSelectedTopicId: null, ipTopicsRewriting: false, ipTopicRewriteProgress: "", /** 选题库标题仿写提示词(对齐 zhenqianba topicRewritePrompt) */ topicRewritePrompt: "请仿写以下标题:{{content}}。要求保持原意和风格,但用不同的表达方式,更加吸引人。", /** 标题标签关键词提示词(对齐 zhenqianba titleTagPrompt) */ titleTagPrompt: "你是短视频标题与标签助手。分析文案内容 {{content}},生成5个吸引眼球的标题建议、10个相关标签、4个分组关键词(每组:重点词/成语词、描述词、行动词、情感词,每个分组只要0-2个词不要多)。", videoLinkModalVisible: false, videoShareText: "", videoRewriting: false, videoRewriteAbortRequested: false, scriptGenerating: false, /** 爆款文案(customPrompt 模式)生成中 */ hitScriptGenerating: false, /** 爆款文案方案列表(最多 3 条) */ hitScriptOptions: [], hitScriptSelectedIndex: 0, hitScriptModalVisible: false, legalChecking: false, legalModalVisible: false, /** @type {null | { hasViolations: boolean, risks: Array<{word:string,recommendation?:string,reason?:string}>, analysis: string, highlightedText: string, fixedText: string, totalCount: number }} */ legalReport: null, /** 流水线进度文案(来自 quickjs:event progress) */ rewriteProgress: "", recognizedContent: "", /** 自定义仿写提示词(可选,对应 zhenqianba rewritePrompt) */ rewritePrompt: "", videoType: "口播文案", copyType: "人设型", industryPersona: "", productBusiness: "", sellingPrice: "", otherRequirements: "", targetWords: 300, // 视频文案编辑 scriptContent: "", editLanguage: "中文(普通话)", editWordCount: 300, // 02 音视频生成 /** 选中音色 id:sys_* 系统音色 或 clone_* 克隆音色 */ selectedVoiceId: "sys_Cherry", voiceManageModalVisible: false, voiceEmotion: "自然", speechRate: 1, voiceLanguage: "中文(普通话)", avatarSelect: null, speechGenerating: false, generatedAudioPath: "", generatedAudioSrc: "", currentAudioId: null, /** @type {Array<{ id: number, name: string, filePath: string, createdAt: string, audioSrc?: string }>} */ audioHistory: [], videoGenerating: false, generatedVideoPath: "", generatedVideoSrc: "", currentVideoId: null, /** @type {Array<{ id: number, name: string, filePath: string, avatarId: number | null, audioId: number | null, createdAt: string, videoSrc?: string }>} */ videoHistory: [], // 03 视频编辑 pipInPicture: false, autoCutBreath: false, greenScreen: false, videoProcessing: false, /** 绿幕替换背景图本地路径 */ greenScreenBackgroundPath: "", /** 画中画混剪设置弹窗 */ mixCutModalVisible: false, /** 步骤 03 编辑后视频(独立于步骤 02 原始口播,可反复重做编辑) */ processedVideoPath: "", processedVideoSrc: "", /** 剪气口参数(对齐 Electron settings) */ silenceThreshold: -40, silenceDuration: 1, minPauseDuration: 0.15, // 04 标题标签关键词 titleTagsGenerating: false, titleGenerated: "", tagsGenerated: "", keywordTab: "0", keywordsFocus: "", keywordsDescribe: "", keywordsAction: "", keywordsEmotion: "", // 05 字幕和音乐 autoSubtitle: true, smartSubtitle: true, bgmEnabled: false, bgmVolume: 30, /** 字幕样式模板 id(系统模板见 subtitleTemplateCatalog) */ subtitleTemplateId: "template_system_11", /** 当前选中的完整字幕模板(模板弹窗选用后写入) */ subtitleTemplate: null, subtitleBgmGenerating: false, subtitleSrtPath: "", /** 步骤 05 字幕/BGM 预览(临时目录,不入库) */ subtitlePreviewVideoPath: "", subtitlePreviewVideoSrc: "", bgmPath: "", bgmPreviewSrc: "", // 06 封面制作 coverGenerating: false, coverImagePath: "", coverImageSrc: "", /** 高级封面步骤预览图路径列表(Python 返回) */ coverPreviewImages: [], coverTemplateId: "cover_electron_portrait_center", /** 当前选中的完整封面模板(封面设置弹窗应用后写入) */ coverTemplate: null, /** 封面素材视频/图片(可选,覆盖口播视频抽帧) */ coverCustomVideoPath: "", // 07 视频发布 publishPlatforms: createDefaultPublishPlatforms(), publishing: false, publishLoggingIn: "", publishStatus: "", publishResults: [], publishScheduledAt: null, publishScheduleTimerId: null, // 一键自动 oneClickRunning: false, oneClickCancelRequested: false, oneClickStatus: "", oneClickProgress: 0, }), getters: { ipModes: () => [ { label: "IP学习", value: "ipBrain" }, { label: "视频学习", value: "videoWrite" }, { label: "爆款文案", value: "customPrompt" }, ], videoTypes: () => ["口播文案", "剧情文案", "带货文案"], copyTypes: () => ["人设型", "卖点型", "故事型"], voiceEmotions: () => ["自然", "热情", "沉稳"], languages: () => ["中文(普通话)", "中文(粤语)", "English"], avatarOptions: () => [{ label: "请选择形象", value: null }], selectedVoiceLabel: (state) => getVoiceDisplayLabel(state.selectedVoiceId), keywordCountFocus: (state) => countLines(state.keywordsFocus), keywordCountDescribe: (state) => countLines(state.keywordsDescribe), keywordCountAction: (state) => countLines(state.keywordsAction), keywordCountEmotion: (state) => countLines(state.keywordsEmotion), subtitleTemplateLabel: (state) => { if (state.subtitleTemplate?.name) return state.subtitleTemplate.name; const legacy = SUBTITLE_TEMPLATES.find( (t) => t.id === (state.subtitleTemplateId || "template_system_11"), ); if (legacy) return legacy.label; return state.subtitleTemplateId || "未选择"; }, coverTemplateLabel: (state) => { if (state.coverTemplate?.name) return state.coverTemplate.name; const tpl = getCoverTemplateById(state.coverTemplateId || "default"); return tpl?.name || "未选择"; }, coverMaterialFileName: (state) => { const p = String(state.coverCustomVideoPath || "").trim(); if (!p) return ""; const parts = p.replace(/\\/g, "/").split("/"); return parts[parts.length - 1] || p; }, bgmFileName: (state) => { const p = state.bgmPath; if (!p) return ""; return p.split(/[/\\]/).pop() || p; }, oneClickBusy: (state) => state.oneClickRunning, step01Processing: (state) => state.ipBrainCollecting || state.ipTopicsRewriting || state.videoRewriting || state.scriptGenerating || state.hitScriptGenerating || state.legalChecking, step02Processing: (state) => state.speechGenerating || state.videoGenerating, step03Processing: (state) => state.videoProcessing, step04Processing: (state) => state.titleTagsGenerating, step05Processing: (state) => state.subtitleBgmGenerating, step06Processing: (state) => state.coverGenerating, step07Processing: (state) => state.publishing || Boolean(state.publishLoggingIn), ipBenchmarkCount: (state) => state.ipBenchmarks.length, selectedBenchmark: (state) => state.ipBenchmarks.find((b) => b.id === state.ipBrainSelectedBenchmarkId) ?? null, selectedTopic: (state) => { if (!state.ipBrainSelectedTopicId) return null; return ( state.ipTopics.find((t) => t.id === state.ipBrainSelectedTopicId) ?? null ); }, /** 已有仿写标题的选题(用于「改写后」列表) */ rewrittenTopics: (state) => state.ipTopics.filter((t) => String(t.rewritten || "").trim()), }, actions: { openIpBrainDialog() { this.ipBrainModalVisible = true; }, closeIpBrainDialog() { this.ipBrainModalVisible = false; }, async startIpBrainCollect() { return executeIpBrainCollect(this); }, selectBenchmark(id) { this.ipBrainSelectedBenchmarkId = id; const bm = this.ipBenchmarks.find((b) => b.id === id); this.ipTopics = bm?.topicLibrary ? [...bm.topicLibrary] : []; this.ipBrainSelectedTopicId = null; }, selectRewrittenTopic(topicId) { const topic = this.ipTopics.find((t) => t.id === topicId); if (!topic?.rewritten?.trim()) return; this.ipBrainSelectedTopicId = topicId; }, async rewriteIpTopics() { return executeIpTopicRewrite(this); }, removeBenchmark(id) { const idx = this.ipBenchmarks.findIndex((b) => b.id === id); if (idx < 0) return; this.ipBenchmarks.splice(idx, 1); if (this.ipBrainSelectedBenchmarkId === id) { const next = this.ipBenchmarks[0]; if (next) this.selectBenchmark(next.id); else { this.ipBrainSelectedBenchmarkId = null; this.ipTopics = []; this.ipBrainSelectedTopicId = null; } } }, openVideoLinkDialog() { this.videoLinkModalVisible = true; }, closeVideoLinkDialog() { this.videoLinkModalVisible = false; }, async startVideoRewrite() { return executeVideoRewrite(this); }, async generateScript() { return executeGenerateScript(this); }, async generateHitScript() { return executeGenerateHitScript(this); }, applyHitScriptOption(index) { return applyHitScriptOptionToEditor(this, index); }, closeHitScriptModal() { this.hitScriptModalVisible = false; }, async runLegalCheck() { return executeLegalCheck(this); }, applyLegalFix() { return applyLegalFixToScript(this); }, closeLegalModal() { this.legalModalVisible = false; }, openVoiceManageDialog() { this.voiceManageModalVisible = true; }, closeVoiceManageDialog() { this.voiceManageModalVisible = false; }, openMixCutDialog() { this.mixCutModalVisible = true; }, closeMixCutDialog() { this.mixCutModalVisible = false; }, selectVoice(voiceId) { this.selectedVoiceId = voiceId; this.voiceManageModalVisible = false; }, async generateSpeech() { return executeGenerateSpeech(this); }, async refreshAudioHistory() { try { const rows = await listGeneratedAudios(); this.audioHistory = await Promise.all( rows.map(async (row) => { let audioSrc = ""; try { audioSrc = await localAudioToPlayableUrl(row.filePath); } catch { audioSrc = ""; } return { ...row, audioSrc }; }), ); } catch { this.audioHistory = []; } }, async pickAudioHistory(audioId) { await selectAudioHistory(this, audioId); }, async generateTalkingVideo() { return executeGenerateTalkingVideo(this); }, async refreshVideoHistory() { try { const rows = await listGeneratedVideos(); this.videoHistory = rows.map((row) => ({ ...row, videoSrc: localVideoToPlayableUrl(row.filePath), })); } catch { this.videoHistory = []; } }, async pickVideoHistory(videoId) { await selectVideoHistory(this, videoId); }, async autoProcessVideo() { return executeAutoProcessVideo(this); }, async chooseGreenScreenBackground() { const path = await pickGreenScreenBackground(); if (!path) return { ok: false, message: "" }; this.greenScreenBackgroundPath = path; return { ok: true, message: "已选择绿幕背景图" }; }, async choosePipMaterial() { this.openMixCutDialog(); return { ok: true, message: "" }; }, async generateTitleTags() { return executeGenerateTitleTags(this); }, async generateSubtitleAndBgm() { return executeGenerateSubtitleAndBgm(this); }, async chooseBgm() { return chooseBgm(this); }, async previewBgm() { return previewBgm(this); }, async generateCover() { return executeGenerateCover(this); }, async chooseCoverMaterial() { return chooseCoverMaterial(this); }, async refreshPublishAccounts() { return refreshPublishAccounts(this); }, async loginPublishPlatform(platformKey, accountId = undefined) { return loginPublishPlatform(this, platformKey, accountId); }, async checkPublishLoginStatus(platformKey, accountId = null, checkBrowser = true) { return checkPublishLoginStatus(this, platformKey, accountId, checkBrowser); }, async openPublishAccount(platformKey, accountId = null) { return openPublishAccount(this, platformKey, accountId); }, async publishVideo(options) { return executePublishVideo(this, options); }, clearPublishSchedule() { if (this.publishScheduleTimerId) { clearTimeout(this.publishScheduleTimerId); this.publishScheduleTimerId = null; } this.publishScheduledAt = null; }, schedulePublishVideo(scheduledAt) { this.clearPublishSchedule(); const target = scheduledAt instanceof Date ? scheduledAt : new Date(scheduledAt); const delayMs = target.getTime() - Date.now(); if (delayMs <= 0) { return { ok: false, message: "发布时间必须晚于当前时间" }; } this.publishScheduledAt = target.toISOString(); this.publishScheduleTimerId = setTimeout(async () => { this.publishScheduleTimerId = null; this.publishScheduledAt = null; await executePublishVideo(this, { autoPublish: true }); }, delayMs); return { ok: true, message: `已设置定时发布:${target.toLocaleString("zh-CN")}`, }; }, async startOneClickAuto() { return executeOneClickPipeline(this); }, stopOneClickAuto() { return stopOneClickPipeline(this); }, resetAll() { this.$reset(); this.publishPlatforms = createDefaultPublishPlatforms(); this.clearPublishSchedule(); }, clearData() { this.resetAll(); }, }, }); /** * 一键仿写:Node.js douyin_pipeline(对齐 zhenqianba ipAgent:processDouyinShare) * @param {ReturnType} store */ export async function executeVideoRewrite(store) { if (store.videoRewriting) { return { ok: false, message: "仿写任务正在执行中,请稍候" }; } const parsed = validateShareText(store.videoShareText); if (!parsed.ok) { return { ok: false, message: parsed.message }; } const unsupported = detectUnsupportedPlatform(parsed.value); if (unsupported) { return { ok: false, message: unsupported }; } const cfgReady = await ensureAppConfigReady(); if (!cfgReady.ok) { return { ok: false, message: cfgReady.message }; } store.videoRewriting = true; store.videoRewriteAbortRequested = false; store.rewriteProgress = "正在解析和仿写..."; let unlisten = null; try { try { unlisten = await listen(NODEJS_EVENT, (e) => { const p = e.payload; if (store.videoRewriteAbortRequested) { return; } if (p?.type === "progress" && typeof p.status === "string") { store.rewriteProgress = p.status; } else if (p?.type === "log") { const prefix = `[nodejs/${p.scriptName || PIPELINE_SCRIPT}]`; const text = p.fields != null ? `${p.msg} ${JSON.stringify(p.fields)}` : p.msg; if (p.level === "error") console.error(prefix, text); else if (p.level === "warn") console.warn(prefix, text); else console.info(prefix, text); } }); } catch { /* 非 Tauri 环境 */ } const result = await invoke("run_nodejs_script", { scriptName: PIPELINE_SCRIPT, params: { shareText: parsed.value, rewriteConfig: { ...DEFAULT_REWRITE_CONFIG }, customPrompt: store.rewritePrompt?.trim() || null, }, }); if (store.videoRewriteAbortRequested) { return { ok: false, cancelled: true, message: "已取消仿写任务" }; } if (!result || result.success !== true) { const err = (result && (result.error || result.message)) || "解析或仿写失败,请检查链接与模型配置"; return { ok: false, message: String(err) }; } const original = result.original || {}; const rewritten = result.rewritten || {}; store.recognizedContent = original.description || ""; store.scriptContent = rewritten.fullContent || rewritten.description || ""; if (rewritten.title) { store.titleGenerated = rewritten.title; } if (original.hashtags?.length) { store.tagsGenerated = original.hashtags.join(" "); } store.videoShareText = ""; store.videoLinkModalVisible = false; return { ok: true, message: "仿写完成,已为您生成新文案", data: result, }; } catch (err) { const msg = err instanceof Error ? err.message : String(err || "未知错误"); if (store.videoRewriteAbortRequested || String(msg).includes("任务已取消")) { return { ok: false, cancelled: true, message: "已取消仿写任务" }; } return { ok: false, message: `仿写失败: ${msg}` }; } finally { if (typeof unlisten === "function") { unlisten(); } store.videoRewriting = false; store.videoRewriteAbortRequested = false; store.rewriteProgress = ""; } } /** * IP 大脑采集(对齐 Electron ipAgent:analyzeDouyinAccount + fi) * @param {ReturnType} store */ export async function executeIpBrainCollect(store) { const url = store.ipBrainProfileUrl?.trim(); if (!url) { return { ok: false, message: "请输入账号主页或分享链接" }; } if (store.ipBenchmarks.length >= 5) { return { ok: false, message: "最多添加 5 个对标账号" }; } store.ipBrainCollecting = true; store.ipBrainCollectProgress = "正在打开浏览器并采集数据,请稍候..."; let unlisten = null; try { try { unlisten = await listen(NODEJS_EVENT, (e) => { const p = e.payload; if ( p?.type === "progress" && p.scriptName === IP_BRAIN_COLLECT && typeof p.status === "string" ) { store.ipBrainCollectProgress = p.status; } }); } catch { /* 非 Tauri */ } const result = await invoke("run_nodejs_script", { scriptName: IP_BRAIN_COLLECT, params: { url }, }); if (!result?.success) { return { ok: false, message: String(result?.error || "采集失败,请检查链接与 Chrome/Edge 是否已安装"), }; } const titles = Array.isArray(result.videoTitles) ? result.videoTitles : []; const accountName = String(result.accountName || "").trim(); const pageUrl = String(result.pageUrl || url).trim(); const label = accountName || truncateUrl(pageUrl); const topicLibrary = titles.map((title, i) => ({ id: `topic_${Date.now()}_${i}`, original: title, title, rewritten: "", isRewriting: false, })); const benchmark = { id: `bm_${Date.now()}`, url: pageUrl, name: accountName, label, topicLibrary, }; store.ipBenchmarks.push(benchmark); store.selectBenchmark(benchmark.id); store.ipBrainProfileUrl = ""; store.ipBrainModalVisible = false; return { ok: true, message: `采集完成!已添加「${label}」,共 ${titles.length} 个视频标题`, }; } catch (err) { const msg = err instanceof Error ? err.message : String(err || "未知错误"); return { ok: false, message: `采集失败: ${msg}` }; } finally { if (typeof unlisten === "function") unlisten(); store.ipBrainCollecting = false; store.ipBrainCollectProgress = ""; } } function syncBenchmarkTopicLibrary(store) { const bm = store.ipBenchmarks.find( (b) => b.id === store.ipBrainSelectedBenchmarkId, ); if (bm) { bm.topicLibrary = store.ipTopics.map((t) => ({ ...t })); } } /** * 选题库一键仿写标题(对齐 Electron ipAgent:rewriteTitles) * @param {ReturnType} store */ export async function executeIpTopicRewrite(store) { if (!store.ipBrainSelectedBenchmarkId) { return { ok: false, message: "请先选择一个对标" }; } if (!store.ipTopics.length) { return { ok: false, message: "选题库为空,请先采集对标账号" }; } const cfgReady = await ensureLlmConfigReady(); if (!cfgReady.ok) { return { ok: false, message: cfgReady.message }; } const originals = store.ipTopics.map((t) => String(t.original || t.title || "").trim(), ).filter(Boolean); if (!originals.length) { return { ok: false, message: "没有可仿写的原始标题" }; } const bm = store.selectedBenchmark; const accountName = bm?.name || bm?.label || ""; store.ipTopicsRewriting = true; store.ipTopicRewriteProgress = "正在一键仿写标题..."; try { const result = await invoke("run_nodejs_script", { scriptName: IP_TOPIC_REWRITE, params: { titles: originals, accountName, rewritePrompt: store.topicRewritePrompt?.trim() || null, }, }); if (!result?.success || !Array.isArray(result.rewrittenTitles)) { return { ok: false, message: String(result?.error || "标题仿写失败,请重试"), }; } result.rewrittenTitles.forEach((rewritten, i) => { const topic = store.ipTopics[i]; if (!topic) return; topic.rewritten = String(rewritten || "").trim(); topic.isRewriting = false; }); syncBenchmarkTopicLibrary(store); const first = store.rewrittenTopics[0]; store.ipBrainSelectedTopicId = first?.id ?? null; return { ok: true, message: `已生成 ${result.rewrittenTitles.length} 条仿写标题`, }; } catch (err) { const msg = err instanceof Error ? err.message : String(err || "未知错误"); return { ok: false, message: `标题仿写失败: ${msg}` }; } finally { store.ipTopicsRewriting = false; store.ipTopicRewriteProgress = ""; } } const DEFAULT_SCRIPT_PROMPT = `请基于以下视频原文案,创作一个新的仿写文案: 原文案:{{content}} 要求: 1. 保持原文案的核心内容和风格 2. 适当改变表达方式和用词 3. 确保新文案通顺自然 4. 长度约{{count}}字 请直接输出仿写后的文案:`; /** * 解析爆款文案模型返回(对齐 zhenqianba ii) * @param {string} raw * @returns {string[]} */ export function parseHitScriptOptions(raw) { const text = String(raw || ""); const md = text.match(/```(?:json)?\s*([\s\S]*?)```/i); const body = (md?.[1] || text).trim(); try { const parsed = JSON.parse(body); const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.options) ? parsed.options : Array.isArray(parsed?.data) ? parsed.data : []; const options = list .map((item) => typeof item === "string" ? item : item?.content || item?.text || "", ) .map((s) => String(s).trim()) .filter(Boolean); if (options.length) { return Array.from(new Set(options)).slice(0, 3); } } catch (err) { console.warn("[爆款文案] JSON 解析失败,转用文本拆分:", err); } const fallback = body .split(/\n(?=(?:方案|选项)\s*[一二三123])|\n{2,}/) .map((line) => line.replace(/^(?:方案|选项)\s*[一二三123][::.\s-]*/g, "").trim(), ) .filter(Boolean); return Array.from(new Set(fallback)).slice(0, 3); } /** * 构建爆款文案提示词(对齐 zhenqianba Ti) * @param {ReturnType} store */ export function buildHitScriptPrompt(store) { const lines = []; lines.push( "你是一个爆款短视频文案专家,擅长创作高播放量、高互动的短视频口播文案。", ); lines.push("请根据以下信息输出 3 个不同方向的爆款短视频文案方案。"); lines.push(`【视频类型】${store.videoType || "口播文案"}`); lines.push(`【文案类型】${store.copyType || "人设型"}`); const persona = String(store.industryPersona || "").trim(); if (persona) lines.push(`【行业+人设】${persona}`); const product = String(store.productBusiness || "").trim(); if (product) lines.push(`【产品/业务】${product}`); const selling = String(store.sellingPrice || "").trim(); if (selling) lines.push(`【卖点+价格】${selling}`); const other = String(store.otherRequirements || "").trim(); if (other) lines.push(`【其他要求】${other}`); const count = store.targetWords || 300; lines.push(""); lines.push("要求:"); lines.push("1. 开头3秒必须有强吸引力的钩子,一开口就能留住用户"); lines.push(`2. 内容贴合「${store.copyType || "人设型"}」风格特点`); lines.push("3. 语言自然口语化,适合短视频口播"); lines.push("4. 结尾要有引导互动的话术(关注、点赞、评论、收藏)"); lines.push("5. 不使用表情符号和特殊符号"); lines.push(`6. 字数控制在${count}字左右`); lines.push("7. 3 个方案要明显区分角度,不要只改几个词"); lines.push( '8. 只返回 JSON,格式必须是 {"options":["方案1","方案2","方案3"]},不要输出 markdown,不要补充解释', ); return lines.join("\n"); } /** * 生成爆款文案:调用 LLM 产出 3 个方案(对齐 zhenqianba Ti) * @param {ReturnType} store */ export async function executeGenerateHitScript(store) { const cfgReady = await ensureLlmConfigReady(); if (!cfgReady.ok) { return { ok: false, message: cfgReady.message }; } const prompt = buildHitScriptPrompt(store); store.hitScriptGenerating = true; store.hitScriptOptions = []; try { const result = await invoke("run_nodejs_script", { scriptName: SCRIPT_GENERATE, params: { messages: [{ role: "user", content: prompt }], temperature: 0.8, max_tokens: 4000, }, }); if (!result?.success || !result?.content) { const err = result?.error || "文案生成失败,请重试"; return { ok: false, message: String(err) }; } const options = parseHitScriptOptions(result.content); if (!options.length) { return { ok: false, message: "模型未返回可用文案方案" }; } store.hitScriptOptions = options; store.hitScriptSelectedIndex = 0; store.hitScriptModalVisible = true; return { ok: true, message: `已生成 ${options.length} 个爆款文案方案,请在弹窗中选择`, }; } catch (err) { const msg = err instanceof Error ? err.message : String(err || "未知错误"); return { ok: false, message: `生成文案失败:${msg}` }; } finally { store.hitScriptGenerating = false; } } /** * 将所选爆款文案填入编辑区(对齐 zhenqianba Bn) * @param {ReturnType} store * @param {number} index */ export function applyHitScriptOptionToEditor(store, index) { const i = Number(index); const option = store.hitScriptOptions[i]; if (!option) { return { ok: false, message: "请选择文案方案" }; } store.scriptContent = String(option).trim(); store.hitScriptModalVisible = false; return { ok: true, message: "已将所选文案填入编辑区" }; } /** * 构建撰写文案提示词(对齐 zhenqianba generateScriptFromVideoContent) * @param {ReturnType} store */ export function buildScriptGeneratePrompt(store) { const content = String(store.recognizedContent || "").trim(); const count = store.editWordCount || 300; let prompt = String(store.rewritePrompt || "").trim(); if (!prompt) { prompt = DEFAULT_SCRIPT_PROMPT; } prompt = prompt .replace(/\{\{content\}\}/g, content) .replace(/\{content\}/g, content) .replace(/\{\{count\}\}/g, String(count)) .replace(/\{count\}/g, String(count)); const lang = store.editLanguage || ""; if (lang && lang !== "中文(普通话)") { prompt = `请用${lang}输出文案。\n\n${prompt}`; } return prompt; } /** * 撰写文案:基于识别的原始内容调用 LLM(对齐 Electron ki) * @param {ReturnType} store */ export async function executeGenerateScript(store) { const content = String(store.recognizedContent || "").trim(); if (!content) { return { ok: false, message: "请先使用智能视频仿写功能识别视频内容" }; } const cfgReady = await ensureLlmConfigReady(); if (!cfgReady.ok) { return { ok: false, message: cfgReady.message }; } const userPrompt = buildScriptGeneratePrompt(store); store.scriptGenerating = true; try { const result = await invoke("run_nodejs_script", { scriptName: SCRIPT_GENERATE, params: { messages: [ { role: "system", content: "你是专业的短视频文案改写助手。请把用户提供的文案改写,保持原文的风格和主题,但使用不同的表达方式。", }, { role: "user", content: userPrompt }, ], temperature: 0.8, max_tokens: 2000, }, }); if (!result?.success || !result?.content) { const err = result?.error || "文案仿写失败,请重试"; return { ok: false, message: String(err) }; } store.scriptContent = String(result.content).trim(); return { ok: true, message: "文案仿写完成" }; } catch (err) { const msg = err instanceof Error ? err.message : String(err || "未知错误"); return { ok: false, message: `文案仿写失败: ${msg}` }; } finally { store.scriptGenerating = false; } } /** * 高亮原文中的违禁词(对齐 zhenqianba Ci) * @param {string} text * @param {Array<{ word: string }>} risks */ export function highlightLegalViolations(text, risks) { let html = String(text || ""); const sorted = [...(risks || [])].sort( (a, b) => (b.word?.length || 0) - (a.word?.length || 0), ); for (const item of sorted) { const word = item?.word; if (!word) continue; const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); html = html.replace( new RegExp(escaped, "g"), `${word}`, ); } return html; } /** * AI 法务审核(对齐 zhenqianba _i / handleLegalCheck) * @param {ReturnType} store */ export async function executeLegalCheck(store) { const content = String(store.scriptContent || "").trim(); if (!content) { return { ok: false, message: "请先输入文案内容" }; } const cfgReady = await ensureLlmConfigReady(); if (!cfgReady.ok) { return { ok: false, message: cfgReady.message }; } store.legalChecking = true; store.legalModalVisible = true; store.legalReport = null; try { const result = await invoke("run_nodejs_script", { scriptName: SCRIPT_LEGAL_CHECK, params: { content }, }); if (!result?.success) { store.legalModalVisible = false; return { ok: false, message: String(result?.error || "AI 审核请求失败"), }; } const risks = Array.isArray(result.risks) ? result.risks : []; const hasViolations = result.hasViolations === true || risks.length > 0; store.legalReport = { hasViolations, risks, analysis: result.analysis || "", fixedText: result.fixedText || "", highlightedText: highlightLegalViolations(content, risks), totalCount: risks.length, }; if (!hasViolations) { return { ok: true, message: "未检测到违规内容" }; } const words = [...new Set(risks.map((r) => r.word).filter(Boolean))]; return { ok: true, message: `检测到 ${risks.length} 处违规内容${words.length ? `:${words.join("、")}` : ""}`, warn: true, }; } catch (err) { store.legalModalVisible = false; const msg = err instanceof Error ? err.message : String(err || "未知错误"); return { ok: false, message: `检测失败:${msg}` }; } finally { store.legalChecking = false; } } /** * 采用优化文案或按建议替换违禁词(对齐 zhenqianba $i) * @param {ReturnType} store */ export function applyLegalFixToScript(store) { const report = store.legalReport; if (!report) { return { ok: false, message: "暂无审核结果" }; } if (report.fixedText?.trim()) { store.scriptContent = report.fixedText.trim(); store.legalModalVisible = false; return { ok: true, message: "已采用 AI 优化后的文案" }; } const risks = report.risks || []; if (risks.length === 0) { return { ok: false, message: "没有需要修正的内容" }; } let text = String(store.scriptContent || ""); const sorted = [...risks].sort( (a, b) => (b.word?.length || 0) - (a.word?.length || 0), ); for (const item of sorted) { const word = item?.word; if (!word) continue; const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); text = text.replace(new RegExp(escaped, "g"), item.recommendation || ""); } store.scriptContent = text; store.legalModalVisible = false; return { ok: true, message: "已自动修正所有违规内容" }; } const UNSUPPORTED_PLATFORM_RULES = [ { pattern: /kuaishou\.com|ksurl\.cn|gifshow\.com/i, name: "快手" }, { pattern: /xiaohongshu\.com|xhslink\.com/i, name: "小红书" }, { pattern: /bilibili\.com|b23\.tv/i, name: "B站" }, { pattern: /channels\.weixin\.qq\.com|finder\.video\.qq\.com/i, name: "视频号" }, ]; function detectUnsupportedPlatform(text) { for (const { pattern, name } of UNSUPPORTED_PLATFORM_RULES) { if (pattern.test(text)) { return `${name}分享链接暂不支持,请使用抖音分享文本或视频链接`; } } return null; } function truncateUrl(url, max = 36) { if (url.length <= max) return url; return `${url.slice(0, max)}…`; } function countLines(text) { if (!text?.trim()) return 0; return text.split("\n").filter((line) => line.trim()).length; } if (import.meta.hot) { import.meta.hot.accept(acceptHMRUpdate(useWorkflowStore, import.meta.hot)); }