11
This commit is contained in:
65
src/components/workflow/HitScriptDialog.vue
Normal file
65
src/components/workflow/HitScriptDialog.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<script setup>
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||||
|
|
||||||
|
const emit = defineEmits(["feedback"]);
|
||||||
|
|
||||||
|
const workflow = useWorkflowStore();
|
||||||
|
const {
|
||||||
|
hitScriptModalVisible,
|
||||||
|
hitScriptOptions,
|
||||||
|
hitScriptSelectedIndex,
|
||||||
|
} = storeToRefs(workflow);
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
workflow.closeHitScriptModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelect(index) {
|
||||||
|
hitScriptSelectedIndex.value = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onApply() {
|
||||||
|
emit("feedback", workflow.applyHitScriptOption(hitScriptSelectedIndex.value));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="hitScriptModalVisible"
|
||||||
|
modal
|
||||||
|
header="选择爆款文案方案"
|
||||||
|
:style="{ width: '640px' }"
|
||||||
|
:draggable="false"
|
||||||
|
class="hit-script-dialog"
|
||||||
|
@hide="onClose"
|
||||||
|
>
|
||||||
|
<p class="mb-3 text-sm text-slate-400">
|
||||||
|
已生成 {{ hitScriptOptions.length }} 个方案,点击选择后填入「视频文案编辑」
|
||||||
|
</p>
|
||||||
|
<ul class="max-h-[60vh] space-y-2 overflow-y-auto">
|
||||||
|
<li
|
||||||
|
v-for="(option, index) in hitScriptOptions"
|
||||||
|
:key="index"
|
||||||
|
class="cursor-pointer rounded-lg border px-3 py-3 text-sm transition-colors"
|
||||||
|
:class="
|
||||||
|
hitScriptSelectedIndex === index
|
||||||
|
? 'border-blue-400/50 bg-blue-500/15 text-slate-100 ring-1 ring-blue-400/40'
|
||||||
|
: 'border-white/10 bg-white/5 text-slate-300 hover:bg-white/10'
|
||||||
|
"
|
||||||
|
@click="onSelect(index)"
|
||||||
|
>
|
||||||
|
<div class="mb-1 text-xs font-medium text-blue-400">方案 {{ index + 1 }}</div>
|
||||||
|
<p class="whitespace-pre-wrap leading-relaxed">{{ option }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" severity="secondary" @click="onClose" />
|
||||||
|
<Button
|
||||||
|
label="使用此文案"
|
||||||
|
:disabled="!hitScriptOptions.length"
|
||||||
|
@click="onApply"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -4,6 +4,7 @@ import { storeToRefs } from "pinia";
|
|||||||
import { useWorkflowStore } from "../../stores/workflow.js";
|
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||||
import AddIpBrainDialog from "./AddIpBrainDialog.vue";
|
import AddIpBrainDialog from "./AddIpBrainDialog.vue";
|
||||||
import VideoLinkDialog from "./VideoLinkDialog.vue";
|
import VideoLinkDialog from "./VideoLinkDialog.vue";
|
||||||
|
import HitScriptDialog from "./HitScriptDialog.vue";
|
||||||
|
|
||||||
const workflow = useWorkflowStore();
|
const workflow = useWorkflowStore();
|
||||||
const {
|
const {
|
||||||
@@ -23,9 +24,11 @@ const {
|
|||||||
ipTopicsRewriting,
|
ipTopicsRewriting,
|
||||||
ipTopicRewriteProgress,
|
ipTopicRewriteProgress,
|
||||||
rewrittenTopics,
|
rewrittenTopics,
|
||||||
|
hitScriptGenerating,
|
||||||
} = storeToRefs(workflow);
|
} = storeToRefs(workflow);
|
||||||
|
|
||||||
const topicFeedback = ref({ severity: "", message: "" });
|
const topicFeedback = ref({ severity: "", message: "" });
|
||||||
|
const hitScriptFeedback = ref({ severity: "", message: "" });
|
||||||
|
|
||||||
function onSelectBenchmark(id) {
|
function onSelectBenchmark(id) {
|
||||||
topicFeedback.value = { severity: "", message: "" };
|
topicFeedback.value = { severity: "", message: "" };
|
||||||
@@ -63,6 +66,25 @@ function onOpenIpBrain() {
|
|||||||
function topicCount(benchmark) {
|
function topicCount(benchmark) {
|
||||||
return benchmark?.topicLibrary?.length ?? 0;
|
return benchmark?.topicLibrary?.length ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onGenerateHitScript() {
|
||||||
|
hitScriptFeedback.value = { severity: "", message: "" };
|
||||||
|
const result = await workflow.generateHitScript();
|
||||||
|
if (result.message) {
|
||||||
|
hitScriptFeedback.value = {
|
||||||
|
severity: result.ok ? "success" : "error",
|
||||||
|
message: result.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHitScriptFeedback(result) {
|
||||||
|
if (!result?.message) return;
|
||||||
|
hitScriptFeedback.value = {
|
||||||
|
severity: result.ok ? "success" : "error",
|
||||||
|
message: result.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -297,8 +319,25 @@ function topicCount(benchmark) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button label="生成爆款文案" size="small" class="w-full" />
|
<Button
|
||||||
|
label="生成爆款文案"
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
:loading="hitScriptGenerating"
|
||||||
|
:disabled="hitScriptGenerating"
|
||||||
|
@click="onGenerateHitScript"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="hitScriptFeedback.message"
|
||||||
|
class="text-sm"
|
||||||
|
:class="
|
||||||
|
hitScriptFeedback.severity === 'error' ? 'text-red-400' : 'text-emerald-400'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ hitScriptFeedback.message }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<HitScriptDialog @feedback="onHitScriptFeedback" />
|
||||||
</template>
|
</template>
|
||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ export const useWorkflowStore = defineStore("workflow", {
|
|||||||
|
|
||||||
scriptGenerating: false,
|
scriptGenerating: false,
|
||||||
|
|
||||||
|
/** 爆款文案(customPrompt 模式)生成中 */
|
||||||
|
hitScriptGenerating: false,
|
||||||
|
|
||||||
|
/** 爆款文案方案列表(最多 3 条) */
|
||||||
|
hitScriptOptions: [],
|
||||||
|
|
||||||
|
hitScriptSelectedIndex: 0,
|
||||||
|
|
||||||
|
hitScriptModalVisible: false,
|
||||||
|
|
||||||
legalChecking: false,
|
legalChecking: false,
|
||||||
|
|
||||||
legalModalVisible: false,
|
legalModalVisible: false,
|
||||||
@@ -355,6 +365,24 @@ export const useWorkflowStore = defineStore("workflow", {
|
|||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async generateHitScript() {
|
||||||
|
|
||||||
|
return executeGenerateHitScript(this);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
applyHitScriptOption(index) {
|
||||||
|
|
||||||
|
return applyHitScriptOptionToEditor(this, index);
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
closeHitScriptModal() {
|
||||||
|
|
||||||
|
this.hitScriptModalVisible = false;
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
async runLegalCheck() {
|
async runLegalCheck() {
|
||||||
|
|
||||||
return executeLegalCheck(this);
|
return executeLegalCheck(this);
|
||||||
@@ -664,6 +692,146 @@ const DEFAULT_SCRIPT_PROMPT = `请基于以下视频原文案,创作一个新
|
|||||||
|
|
||||||
请直接输出仿写后的文案:`;
|
请直接输出仿写后的文案:`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析爆款文案模型返回(对齐 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)
|
* 构建撰写文案提示词(对齐 zhenqianba generateScriptFromVideoContent)
|
||||||
* @param {ReturnType<typeof useWorkflowStore>} store
|
* @param {ReturnType<typeof useWorkflowStore>} store
|
||||||
|
|||||||
Reference in New Issue
Block a user