11
This commit is contained in:
@@ -20,6 +20,9 @@ import {
|
||||
|
||||
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";
|
||||
|
||||
|
||||
@@ -52,12 +55,26 @@ export const useWorkflowStore = defineStore("workflow", {
|
||||
|
||||
ipBrainCollecting: false,
|
||||
|
||||
ipBrainCollectProgress: "",
|
||||
|
||||
ipBenchmarks: [],
|
||||
|
||||
/** 当前选中对标的选题(与 benchmark.topicLibrary 同步引用) */
|
||||
|
||||
ipTopics: [],
|
||||
|
||||
ipBrainSelectedBenchmarkId: null,
|
||||
|
||||
ipBrainSelectedTopicId: null,
|
||||
|
||||
ipTopicsRewriting: false,
|
||||
|
||||
ipTopicRewriteProgress: "",
|
||||
|
||||
/** 选题库标题仿写提示词(对齐 zhenqianba topicRewritePrompt) */
|
||||
topicRewritePrompt:
|
||||
"请仿写以下标题:{{content}}。要求保持原意和风格,但用不同的表达方式,更加吸引人。",
|
||||
|
||||
videoLinkModalVisible: false,
|
||||
|
||||
videoShareText: "",
|
||||
@@ -66,6 +83,13 @@ export const useWorkflowStore = defineStore("workflow", {
|
||||
|
||||
scriptGenerating: 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: "",
|
||||
@@ -200,6 +224,23 @@ export const useWorkflowStore = defineStore("workflow", {
|
||||
|
||||
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()),
|
||||
|
||||
},
|
||||
|
||||
|
||||
@@ -224,75 +265,63 @@ export const useWorkflowStore = defineStore("workflow", {
|
||||
|
||||
async startIpBrainCollect() {
|
||||
|
||||
const url = this.ipBrainProfileUrl?.trim();
|
||||
|
||||
if (!url) {
|
||||
|
||||
return { ok: false, message: "请输入账号主页或分享链接" };
|
||||
|
||||
}
|
||||
|
||||
if (this.ipBenchmarks.length >= 5) {
|
||||
|
||||
return { ok: false, message: "最多添加 5 个对标账号" };
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
this.ipBrainCollecting = true;
|
||||
|
||||
try {
|
||||
|
||||
// TODO: 对接后端采集 API / Tauri 浏览器自动化
|
||||
|
||||
await new Promise((r) => setTimeout(r, 800));
|
||||
|
||||
|
||||
|
||||
const benchmark = {
|
||||
|
||||
id: `bm_${Date.now()}`,
|
||||
|
||||
url,
|
||||
|
||||
label: truncateUrl(url),
|
||||
|
||||
};
|
||||
|
||||
this.ipBenchmarks.push(benchmark);
|
||||
|
||||
this.ipBrainSelectedBenchmarkId = benchmark.id;
|
||||
|
||||
this.ipTopics = Array.from({ length: 5 }, (_, i) => ({
|
||||
|
||||
id: `topic_${Date.now()}_${i}`,
|
||||
|
||||
title: `示例视频标题 ${i + 1}(待对接真实采集)`,
|
||||
|
||||
}));
|
||||
|
||||
this.ipBrainProfileUrl = "";
|
||||
|
||||
this.ipBrainModalVisible = false;
|
||||
|
||||
return { ok: true, message: "采集完成" };
|
||||
|
||||
} finally {
|
||||
|
||||
this.ipBrainCollecting = false;
|
||||
|
||||
}
|
||||
return executeIpBrainCollect(this);
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
selectBenchmark(id) {
|
||||
|
||||
this.ipBrainSelectedBenchmarkId = id;
|
||||
|
||||
this.ipTopics = [];
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
@@ -326,6 +355,24 @@ export const useWorkflowStore = defineStore("workflow", {
|
||||
|
||||
},
|
||||
|
||||
async runLegalCheck() {
|
||||
|
||||
return executeLegalCheck(this);
|
||||
|
||||
},
|
||||
|
||||
applyLegalFix() {
|
||||
|
||||
return applyLegalFixToScript(this);
|
||||
|
||||
},
|
||||
|
||||
closeLegalModal() {
|
||||
|
||||
this.legalModalVisible = false;
|
||||
|
||||
},
|
||||
|
||||
resetAll() {
|
||||
|
||||
this.$reset();
|
||||
@@ -440,6 +487,171 @@ export async function executeVideoRewrite(store) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}}
|
||||
@@ -526,6 +738,128 @@ export async function executeGenerateScript(store) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 高亮原文中的违禁词(对齐 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: "快手" },
|
||||
|
||||
Reference in New Issue
Block a user