Files
aiclientpureui/src/stores/workflow.js
949036910@qq.com 328ee75be5 11
2026-06-03 15:27:24 +08:00

1472 lines
37 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 音视频生成
/** 选中音色 idsys_* 系统音色 或 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<typeof useWorkflowStore>} 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<typeof useWorkflowStore>} 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<typeof useWorkflowStore>} 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<typeof useWorkflowStore>} 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<typeof useWorkflowStore>} 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<typeof useWorkflowStore>} 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<typeof useWorkflowStore>} 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<typeof useWorkflowStore>} 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"),
`<mark class="legal-hit">${word}</mark>`,
);
}
return html;
}
/**
* AI 法务审核(对齐 zhenqianba _i / handleLegalCheck
* @param {ReturnType<typeof useWorkflowStore>} 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<typeof useWorkflowStore>} 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));
}