11
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -14,8 +14,8 @@
|
|||||||
"monaco-editor": "^0.55.1",
|
"monaco-editor": "^0.55.1",
|
||||||
"monaco-editor-vue3": "^1.0.5",
|
"monaco-editor-vue3": "^1.0.5",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.3.6",
|
"primevue": "^4.3.6",
|
||||||
"vite-plugin-monaco-editor": "^1.1.0",
|
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
},
|
},
|
||||||
@@ -26,7 +26,8 @@
|
|||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"tailwindcss": "^4.1.11",
|
"tailwindcss": "^4.1.11",
|
||||||
"vite": "^6.0.3"
|
"vite": "^6.0.3",
|
||||||
|
"vite-plugin-monaco-editor": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
@@ -2246,6 +2247,11 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/primeicons": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw=="
|
||||||
|
},
|
||||||
"node_modules/primevue": {
|
"node_modules/primevue": {
|
||||||
"version": "4.5.5",
|
"version": "4.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/primevue/-/primevue-4.5.5.tgz",
|
||||||
@@ -2494,6 +2500,7 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite-plugin-monaco-editor/-/vite-plugin-monaco-editor-1.1.0.tgz",
|
||||||
"integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==",
|
"integrity": "sha512-IvtUqZotrRoVqwT0PBBDIZPNraya3BxN/bfcNfnxZ5rkJiGcNtO5eAOWWSgT7zullIAEqQwxMU83yL9J5k7gww==",
|
||||||
|
"dev": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"monaco-editor": ">=0.33.0"
|
"monaco-editor": ">=0.33.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"monaco-editor": "^0.55.1",
|
"monaco-editor": "^0.55.1",
|
||||||
"monaco-editor-vue3": "^1.0.5",
|
"monaco-editor-vue3": "^1.0.5",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
|
"primeicons": "^7.0.0",
|
||||||
"primevue": "^4.3.6",
|
"primevue": "^4.3.6",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-router": "^4.5.1"
|
"vue-router": "^4.5.1"
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ import { storeToRefs } from "pinia";
|
|||||||
import { useWorkflowStore } from "../../stores/workflow.js";
|
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||||
|
|
||||||
const workflow = useWorkflowStore();
|
const workflow = useWorkflowStore();
|
||||||
const { ipBrainModalVisible, ipBrainProfileUrl, ipBrainCollecting } =
|
const {
|
||||||
storeToRefs(workflow);
|
ipBrainModalVisible,
|
||||||
|
ipBrainProfileUrl,
|
||||||
|
ipBrainCollecting,
|
||||||
|
ipBrainCollectProgress,
|
||||||
|
} = storeToRefs(workflow);
|
||||||
|
|
||||||
const feedback = ref({ severity: "", message: "" });
|
const feedback = ref({ severity: "", message: "" });
|
||||||
|
|
||||||
@@ -13,10 +17,11 @@ const tipItems = [
|
|||||||
"输入账号主页或分享链接后点击「开始采集」按钮",
|
"输入账号主页或分享链接后点击「开始采集」按钮",
|
||||||
"系统会自动打开浏览器并抓取数据,请保持网络连接正常",
|
"系统会自动打开浏览器并抓取数据,请保持网络连接正常",
|
||||||
"无需任何手动操作,等待采集完成即可",
|
"无需任何手动操作,等待采集完成即可",
|
||||||
"采集完成后会自动将 5 个视频标题添加到选题库中",
|
"采集完成后会自动将最多 6 个视频标题添加到选题库中",
|
||||||
];
|
];
|
||||||
|
|
||||||
function onCancel() {
|
function onCancel() {
|
||||||
|
if (ipBrainCollecting.value) return;
|
||||||
feedback.value = { severity: "", message: "" };
|
feedback.value = { severity: "", message: "" };
|
||||||
workflow.closeIpBrainDialog();
|
workflow.closeIpBrainDialog();
|
||||||
}
|
}
|
||||||
@@ -24,7 +29,9 @@ function onCancel() {
|
|||||||
async function onStart() {
|
async function onStart() {
|
||||||
feedback.value = { severity: "", message: "" };
|
feedback.value = { severity: "", message: "" };
|
||||||
const result = await workflow.startIpBrainCollect();
|
const result = await workflow.startIpBrainCollect();
|
||||||
if (!result.ok) {
|
if (result.ok) {
|
||||||
|
feedback.value = { severity: "success", message: result.message };
|
||||||
|
} else {
|
||||||
feedback.value = { severity: "error", message: result.message };
|
feedback.value = { severity: "error", message: result.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,10 +45,11 @@ async function onStart() {
|
|||||||
:style="{ width: '600px' }"
|
:style="{ width: '600px' }"
|
||||||
:draggable="false"
|
:draggable="false"
|
||||||
class="ip-brain-dialog"
|
class="ip-brain-dialog"
|
||||||
|
:closable="!ipBrainCollecting"
|
||||||
>
|
>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<Message
|
<Message
|
||||||
v-if="feedback.message"
|
v-if="feedback.message && !ipBrainCollecting"
|
||||||
:severity="feedback.severity"
|
:severity="feedback.severity"
|
||||||
:closable="false"
|
:closable="false"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
@@ -49,6 +57,15 @@ async function onStart() {
|
|||||||
{{ feedback.message }}
|
{{ feedback.message }}
|
||||||
</Message>
|
</Message>
|
||||||
|
|
||||||
|
<Message
|
||||||
|
v-if="ipBrainCollecting && ipBrainCollectProgress"
|
||||||
|
severity="info"
|
||||||
|
:closable="false"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
{{ ipBrainCollectProgress }}
|
||||||
|
</Message>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="mb-2 block font-medium text-slate-200">
|
<label class="mb-2 block font-medium text-slate-200">
|
||||||
账号主页或分享链接
|
账号主页或分享链接
|
||||||
@@ -64,7 +81,7 @@ async function onStart() {
|
|||||||
/>
|
/>
|
||||||
<p class="mt-2 text-xs text-slate-500">
|
<p class="mt-2 text-xs text-slate-500">
|
||||||
当前 IP 学习支持抖音账号主页、用户分享链接和短链接,程序会自动识别并采集最新
|
当前 IP 学习支持抖音账号主页、用户分享链接和短链接,程序会自动识别并采集最新
|
||||||
5 个视频标题
|
6 个视频标题
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
150
src/components/workflow/LegalCheckDialog.vue
Normal file
150
src/components/workflow/LegalCheckDialog.vue
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<script setup>
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||||
|
|
||||||
|
const emit = defineEmits(["feedback"]);
|
||||||
|
|
||||||
|
const workflow = useWorkflowStore();
|
||||||
|
const { legalModalVisible, legalChecking, legalReport } = storeToRefs(workflow);
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
workflow.closeLegalModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onApplyFix() {
|
||||||
|
emit("feedback", workflow.applyLegalFix());
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model:visible="legalModalVisible"
|
||||||
|
modal
|
||||||
|
header="AI 法务审核报告"
|
||||||
|
:style="{ width: '600px' }"
|
||||||
|
:draggable="false"
|
||||||
|
class="legal-check-dialog"
|
||||||
|
>
|
||||||
|
<div v-if="legalChecking && !legalReport" class="py-10 text-center">
|
||||||
|
<i class="pi pi-spin pi-spinner text-4xl text-blue-400" />
|
||||||
|
<p class="mt-4 text-sm font-medium text-slate-200">AI 正在进行法务审核...</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">检查违禁词、敏感词、极限词等法律风险</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="legalReport" class="space-y-4">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 rounded-lg border p-3"
|
||||||
|
:class="
|
||||||
|
legalReport.hasViolations
|
||||||
|
? 'border-amber-700/40 bg-amber-900/20'
|
||||||
|
: 'border-emerald-700/40 bg-emerald-900/20'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="pi text-2xl"
|
||||||
|
:class="
|
||||||
|
legalReport.hasViolations
|
||||||
|
? 'pi-exclamation-triangle text-amber-400'
|
||||||
|
: 'pi-check-circle text-emerald-400'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="text-base font-semibold"
|
||||||
|
:class="legalReport.hasViolations ? 'text-amber-300' : 'text-emerald-300'"
|
||||||
|
>
|
||||||
|
{{ legalReport.hasViolations ? "发现风险" : "内容合规" }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="text-sm"
|
||||||
|
:class="legalReport.hasViolations ? 'text-amber-400/80' : 'text-emerald-400/80'"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
legalReport.hasViolations
|
||||||
|
? `共发现 ${legalReport.totalCount} 处风险`
|
||||||
|
: "文案内容符合规范,可以发布"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="legalReport.hasViolations">
|
||||||
|
<div>
|
||||||
|
<div class="mb-2 flex items-center gap-2 text-sm text-slate-400">
|
||||||
|
<i class="pi pi-file" />
|
||||||
|
<span>原文案分析</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="legal-highlight-box max-h-36 overflow-y-auto rounded-lg border border-[#2a2a3a] bg-[#0d0d1a] p-3 text-sm leading-relaxed text-slate-300"
|
||||||
|
v-html="legalReport.highlightedText"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="legalReport.fixedText">
|
||||||
|
<div class="mb-2 flex items-center gap-2 text-sm text-emerald-400">
|
||||||
|
<i class="pi pi-pencil" />
|
||||||
|
<span>优化后文案</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-emerald-800/40 bg-emerald-950/30 p-3 text-sm leading-relaxed text-slate-200 whitespace-pre-wrap"
|
||||||
|
>
|
||||||
|
{{ legalReport.fixedText }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="legalReport.analysis">
|
||||||
|
<div class="mb-2 flex items-center gap-2 text-sm text-blue-400">
|
||||||
|
<i class="pi pi-lightbulb" />
|
||||||
|
<span>AI 审核解读</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="rounded-lg border border-[#2a2a3a] bg-[#12121f] p-3 text-sm leading-relaxed text-slate-300 whitespace-pre-wrap"
|
||||||
|
>
|
||||||
|
{{ legalReport.analysis }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="legalReport.risks?.length">
|
||||||
|
<div class="mb-2 flex items-center gap-2 text-sm text-red-400">
|
||||||
|
<i class="pi pi-list" />
|
||||||
|
<span>风险详情</span>
|
||||||
|
</div>
|
||||||
|
<div class="max-h-48 space-y-2 overflow-y-auto">
|
||||||
|
<div
|
||||||
|
v-for="(risk, idx) in legalReport.risks"
|
||||||
|
:key="idx"
|
||||||
|
class="rounded p-2 text-sm hover:bg-[#1a1a2e]"
|
||||||
|
>
|
||||||
|
<span class="font-medium text-red-300">{{ risk.word }}</span>
|
||||||
|
<div class="mt-1 text-slate-400">
|
||||||
|
<span class="text-slate-600">→</span>
|
||||||
|
{{ risk.recommendation ? risk.recommendation : "建议删除" }}
|
||||||
|
</div>
|
||||||
|
<div v-if="risk.reason" class="mt-1 text-xs text-slate-500">
|
||||||
|
{{ risk.reason }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<Button label="关闭" severity="secondary" @click="onClose" />
|
||||||
|
<Button
|
||||||
|
v-if="legalReport?.hasViolations"
|
||||||
|
label="采用优化文案"
|
||||||
|
@click="onApplyFix"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.legal-highlight-box :deep(.legal-hit) {
|
||||||
|
background: rgb(254 202 202);
|
||||||
|
color: rgb(153 27 27);
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
import { storeToRefs } from "pinia";
|
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";
|
||||||
@@ -17,19 +18,51 @@ const {
|
|||||||
recognizedContent,
|
recognizedContent,
|
||||||
ipBenchmarks,
|
ipBenchmarks,
|
||||||
ipBrainSelectedBenchmarkId,
|
ipBrainSelectedBenchmarkId,
|
||||||
|
ipTopics,
|
||||||
|
ipBrainSelectedTopicId,
|
||||||
|
ipTopicsRewriting,
|
||||||
|
ipTopicRewriteProgress,
|
||||||
|
rewrittenTopics,
|
||||||
} = storeToRefs(workflow);
|
} = storeToRefs(workflow);
|
||||||
|
|
||||||
|
const topicFeedback = ref({ severity: "", message: "" });
|
||||||
|
|
||||||
function onSelectBenchmark(id) {
|
function onSelectBenchmark(id) {
|
||||||
|
topicFeedback.value = { severity: "", message: "" };
|
||||||
workflow.selectBenchmark(id);
|
workflow.selectBenchmark(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSelectRewrittenTopic(topicId) {
|
||||||
|
workflow.selectRewrittenTopic(topicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRefreshRewritten() {
|
||||||
|
topicFeedback.value = { severity: "", message: "" };
|
||||||
|
const result = await workflow.rewriteIpTopics();
|
||||||
|
if (result.ok) {
|
||||||
|
topicFeedback.value = { severity: "success", message: result.message };
|
||||||
|
} else {
|
||||||
|
topicFeedback.value = { severity: "error", message: result.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRemoveBenchmark(id, event) {
|
||||||
|
event?.stopPropagation?.();
|
||||||
|
workflow.removeBenchmark(id);
|
||||||
|
}
|
||||||
|
|
||||||
function onOpenVideoLink() {
|
function onOpenVideoLink() {
|
||||||
workflow.videoLinkModalVisible = true;
|
workflow.videoLinkModalVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onOpenIpBrain() {
|
function onOpenIpBrain() {
|
||||||
|
if (workflow.ipBenchmarkCount >= 5) return;
|
||||||
workflow.ipBrainModalVisible = true;
|
workflow.ipBrainModalVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function topicCount(benchmark) {
|
||||||
|
return benchmark?.topicLibrary?.length ?? 0;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -48,8 +81,12 @@ function onOpenIpBrain() {
|
|||||||
label="添加IP大脑"
|
label="添加IP大脑"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
size="small"
|
size="small"
|
||||||
|
:disabled="workflow.ipBenchmarkCount >= 5"
|
||||||
@click="onOpenIpBrain"
|
@click="onOpenIpBrain"
|
||||||
/>
|
/>
|
||||||
|
<p v-if="workflow.ipBenchmarkCount >= 5" class="mt-1 text-center text-xs text-amber-400">
|
||||||
|
已达上限(5/5)
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div class="dashboard-field-label">
|
<div class="dashboard-field-label">
|
||||||
@@ -59,17 +96,32 @@ function onOpenIpBrain() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="ipBenchmarks.length === 0" class="dashboard-muted py-2">暂无对标</p>
|
<p v-if="ipBenchmarks.length === 0" class="dashboard-muted py-2">暂无对标</p>
|
||||||
<div v-else class="py-1">
|
<div v-else class="space-y-1 py-1">
|
||||||
<button
|
<div
|
||||||
v-for="item in ipBenchmarks"
|
v-for="item in ipBenchmarks"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
type="button"
|
class="benchmark-row flex items-center justify-between gap-2"
|
||||||
class="benchmark-item"
|
|
||||||
:class="{ 'benchmark-item--active': ipBrainSelectedBenchmarkId === item.id }"
|
|
||||||
@click="onSelectBenchmark(item.id)"
|
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
<button
|
||||||
</button>
|
type="button"
|
||||||
|
class="benchmark-item min-w-0 flex-1"
|
||||||
|
:class="{ 'benchmark-item--active': ipBrainSelectedBenchmarkId === item.id }"
|
||||||
|
@click="onSelectBenchmark(item.id)"
|
||||||
|
>
|
||||||
|
<span class="truncate">{{ item.label }}</span>
|
||||||
|
<span class="ml-1 shrink-0 text-xs opacity-70">
|
||||||
|
({{ topicCount(item) }}/6)
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<Button
|
||||||
|
label="删除"
|
||||||
|
size="small"
|
||||||
|
severity="danger"
|
||||||
|
text
|
||||||
|
class="shrink-0"
|
||||||
|
@click="onRemoveBenchmark(item.id, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -81,16 +133,72 @@ function onOpenIpBrain() {
|
|||||||
>
|
>
|
||||||
请先选择一个对标
|
请先选择一个对标
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="workflow.ipTopics.length === 0" class="dashboard-muted py-2 text-sm">
|
<p v-else-if="ipTopics.length === 0" class="dashboard-muted py-2 text-sm">
|
||||||
采集完成后将显示视频标题
|
采集完成后将显示视频标题
|
||||||
</p>
|
</p>
|
||||||
<ul v-else class="space-y-1 py-1 text-sm text-slate-300">
|
<ul v-else class="space-y-1 py-1">
|
||||||
<li v-for="(topic, index) in workflow.ipTopics" :key="topic.id">
|
<li
|
||||||
{{ index + 1 }}. {{ topic.title }}
|
v-for="(topic, index) in ipTopics"
|
||||||
|
:key="topic.id"
|
||||||
|
class="rounded-lg border border-white/5 bg-white/5 px-2 py-2 text-sm text-slate-400"
|
||||||
|
>
|
||||||
|
{{ index + 1 }}. {{ topic.original || topic.title }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="mb-2 flex items-center justify-between gap-2">
|
||||||
|
<div class="dashboard-field-label">仿写后</div>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-refresh"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
:loading="ipTopicsRewriting"
|
||||||
|
:disabled="!ipBrainSelectedBenchmarkId || ipTopics.length === 0 || ipTopicsRewriting"
|
||||||
|
aria-label="刷新仿写标题"
|
||||||
|
@click="onRefreshRewritten"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="ipTopicRewriteProgress" class="mb-2 text-xs text-blue-400">
|
||||||
|
{{ ipTopicRewriteProgress }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-if="!ipBrainSelectedBenchmarkId"
|
||||||
|
class="dashboard-muted py-2 text-center text-sm"
|
||||||
|
>
|
||||||
|
请先选择一个对标
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-else-if="rewrittenTopics.length === 0"
|
||||||
|
class="dashboard-muted py-2 text-sm"
|
||||||
|
>
|
||||||
|
点击右侧刷新按钮,根据选题库一键生成仿写标题
|
||||||
|
</p>
|
||||||
|
<ul v-else class="space-y-1 py-1">
|
||||||
|
<li
|
||||||
|
v-for="(topic, index) in rewrittenTopics"
|
||||||
|
:key="topic.id"
|
||||||
|
class="topic-item cursor-pointer rounded-lg border px-2 py-2 text-sm transition-colors"
|
||||||
|
:class="
|
||||||
|
ipBrainSelectedTopicId === topic.id
|
||||||
|
? 'border-blue-400/50 bg-blue-500/20 text-slate-100 ring-1 ring-blue-400/40'
|
||||||
|
: 'border-white/5 bg-white/5 text-slate-300 hover:bg-white/10'
|
||||||
|
"
|
||||||
|
@click="onSelectRewrittenTopic(topic.id)"
|
||||||
|
>
|
||||||
|
{{ index + 1 }}. {{ topic.rewritten }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p
|
||||||
|
v-if="topicFeedback.message"
|
||||||
|
class="mt-2 text-sm"
|
||||||
|
:class="topicFeedback.severity === 'error' ? 'text-red-400' : 'text-emerald-400'"
|
||||||
|
>
|
||||||
|
{{ topicFeedback.message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<AddIpBrainDialog />
|
<AddIpBrainDialog />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,21 +2,39 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useWorkflowStore } from "../../stores/workflow.js";
|
import { useWorkflowStore } from "../../stores/workflow.js";
|
||||||
|
import LegalCheckDialog from "./LegalCheckDialog.vue";
|
||||||
|
|
||||||
const workflow = useWorkflowStore();
|
const workflow = useWorkflowStore();
|
||||||
const { scriptContent, editLanguage, editWordCount, scriptGenerating } =
|
const {
|
||||||
storeToRefs(workflow);
|
scriptContent,
|
||||||
|
editLanguage,
|
||||||
|
editWordCount,
|
||||||
|
scriptGenerating,
|
||||||
|
legalChecking,
|
||||||
|
} = storeToRefs(workflow);
|
||||||
|
|
||||||
const feedback = ref({ severity: "", message: "" });
|
const feedback = ref({ severity: "", message: "" });
|
||||||
|
|
||||||
|
function showFeedback(result) {
|
||||||
|
if (!result?.message) return;
|
||||||
|
feedback.value = {
|
||||||
|
severity: result.ok ? (result.warn ? "warn" : "success") : "error",
|
||||||
|
message: result.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function onGenerateScript() {
|
async function onGenerateScript() {
|
||||||
feedback.value = { severity: "", message: "" };
|
feedback.value = { severity: "", message: "" };
|
||||||
const result = await workflow.generateScript();
|
showFeedback(await workflow.generateScript());
|
||||||
if (result.ok) {
|
}
|
||||||
feedback.value = { severity: "success", message: result.message };
|
|
||||||
} else {
|
async function onLegalCheck() {
|
||||||
feedback.value = { severity: "error", message: result.message };
|
feedback.value = { severity: "", message: "" };
|
||||||
}
|
showFeedback(await workflow.runLegalCheck());
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLegalFeedback(result) {
|
||||||
|
showFeedback(result);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -53,18 +71,31 @@ async function onGenerateScript() {
|
|||||||
label="撰写文案"
|
label="撰写文案"
|
||||||
size="small"
|
size="small"
|
||||||
:loading="scriptGenerating"
|
:loading="scriptGenerating"
|
||||||
:disabled="scriptGenerating"
|
:disabled="scriptGenerating || legalChecking"
|
||||||
@click="onGenerateScript"
|
@click="onGenerateScript"
|
||||||
/>
|
/>
|
||||||
<Button label="AI法务" size="small" severity="secondary" />
|
<Button
|
||||||
|
label="AI法务"
|
||||||
|
size="small"
|
||||||
|
severity="secondary"
|
||||||
|
:loading="legalChecking"
|
||||||
|
:disabled="scriptGenerating || legalChecking"
|
||||||
|
@click="onLegalCheck"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="feedback.message"
|
v-if="feedback.message"
|
||||||
class="text-sm"
|
class="text-sm"
|
||||||
:class="feedback.severity === 'error' ? 'text-red-400' : 'text-emerald-400'"
|
:class="{
|
||||||
|
'text-red-400': feedback.severity === 'error',
|
||||||
|
'text-amber-400': feedback.severity === 'warn',
|
||||||
|
'text-emerald-400': feedback.severity === 'success',
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
{{ feedback.message }}
|
{{ feedback.message }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<LegalCheckDialog @feedback="onLegalFeedback" />
|
||||||
</DashboardCard>
|
</DashboardCard>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import DashboardCard from "./components/dashboard/DashboardCard.vue";
|
|||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import { useAuthStore } from "./stores/auth.js";
|
import { useAuthStore } from "./stores/auth.js";
|
||||||
|
import "primeicons/primeicons.css";
|
||||||
import "./styles/main.css";
|
import "./styles/main.css";
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ import {
|
|||||||
|
|
||||||
const PIPELINE_SCRIPT = "douyin_pipeline.js";
|
const PIPELINE_SCRIPT = "douyin_pipeline.js";
|
||||||
const SCRIPT_GENERATE = "script_generate.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";
|
const NODEJS_EVENT = "nodejs:event";
|
||||||
|
|
||||||
|
|
||||||
@@ -52,12 +55,26 @@ export const useWorkflowStore = defineStore("workflow", {
|
|||||||
|
|
||||||
ipBrainCollecting: false,
|
ipBrainCollecting: false,
|
||||||
|
|
||||||
|
ipBrainCollectProgress: "",
|
||||||
|
|
||||||
ipBenchmarks: [],
|
ipBenchmarks: [],
|
||||||
|
|
||||||
|
/** 当前选中对标的选题(与 benchmark.topicLibrary 同步引用) */
|
||||||
|
|
||||||
ipTopics: [],
|
ipTopics: [],
|
||||||
|
|
||||||
ipBrainSelectedBenchmarkId: null,
|
ipBrainSelectedBenchmarkId: null,
|
||||||
|
|
||||||
|
ipBrainSelectedTopicId: null,
|
||||||
|
|
||||||
|
ipTopicsRewriting: false,
|
||||||
|
|
||||||
|
ipTopicRewriteProgress: "",
|
||||||
|
|
||||||
|
/** 选题库标题仿写提示词(对齐 zhenqianba topicRewritePrompt) */
|
||||||
|
topicRewritePrompt:
|
||||||
|
"请仿写以下标题:{{content}}。要求保持原意和风格,但用不同的表达方式,更加吸引人。",
|
||||||
|
|
||||||
videoLinkModalVisible: false,
|
videoLinkModalVisible: false,
|
||||||
|
|
||||||
videoShareText: "",
|
videoShareText: "",
|
||||||
@@ -66,6 +83,13 @@ export const useWorkflowStore = defineStore("workflow", {
|
|||||||
|
|
||||||
scriptGenerating: false,
|
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) */
|
/** 流水线进度文案(来自 quickjs:event progress) */
|
||||||
|
|
||||||
rewriteProgress: "",
|
rewriteProgress: "",
|
||||||
@@ -200,6 +224,23 @@ export const useWorkflowStore = defineStore("workflow", {
|
|||||||
|
|
||||||
state.ipBenchmarks.find((b) => b.id === state.ipBrainSelectedBenchmarkId) ?? null,
|
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() {
|
async startIpBrainCollect() {
|
||||||
|
|
||||||
const url = this.ipBrainProfileUrl?.trim();
|
return executeIpBrainCollect(this);
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
selectBenchmark(id) {
|
selectBenchmark(id) {
|
||||||
|
|
||||||
this.ipBrainSelectedBenchmarkId = 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() {
|
resetAll() {
|
||||||
|
|
||||||
this.$reset();
|
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 = `请基于以下视频原文案,创作一个新的仿写文案:
|
const DEFAULT_SCRIPT_PROMPT = `请基于以下视频原文案,创作一个新的仿写文案:
|
||||||
|
|
||||||
原文案:{{content}}
|
原文案:{{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 = [
|
const UNSUPPORTED_PLATFORM_RULES = [
|
||||||
|
|
||||||
{ pattern: /kuaishou\.com|ksurl\.cn|gifshow\.com/i, name: "快手" },
|
{ pattern: /kuaishou\.com|ksurl\.cn|gifshow\.com/i, name: "快手" },
|
||||||
|
|||||||
Reference in New Issue
Block a user