1448 lines
36 KiB
JavaScript
1448 lines
36 KiB
JavaScript
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 { COVER_TEMPLATES } from "../config/coverTemplates.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}},生成吸引人的标题、相关标签,以及四个分组关键词(重点词/成语词、描述词、行动词、情感词,每组 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,
|
||
|
||
/** 编辑后视频(与 generated 同步更新,便于后续步骤区分) */
|
||
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: "default",
|
||
|
||
/** 封面素材视频(可选,覆盖口播视频抽帧) */
|
||
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) => {
|
||
const tpl = COVER_TEMPLATES.find(
|
||
(t) => t.id === (state.coverTemplateId || "default"),
|
||
);
|
||
return tpl?.label || "未选择";
|
||
},
|
||
|
||
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,
|
||
|
||
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));
|
||
|
||
}
|
||
|