1
This commit is contained in:
@@ -5,6 +5,11 @@ import { loginApi, logoutApi, registerApi } from "../api/auth.js";
|
||||
const TOKEN_KEY = "aiclient_token";
|
||||
const USER_KEY = "aiclient_current_user";
|
||||
|
||||
/** 管理员 */
|
||||
export const ROLE_ADMIN = 1;
|
||||
/** 代理 */
|
||||
export const ROLE_AGENT = 2;
|
||||
|
||||
async function syncAuthSessionToRust(token, user) {
|
||||
try {
|
||||
await invoke("sync_auth_session", {
|
||||
@@ -29,6 +34,7 @@ function applyTokenResponse(store, res, fallbackMessage) {
|
||||
store.setSession(data.access_token, {
|
||||
id: data.user.id,
|
||||
username: data.user.username,
|
||||
role_id: data.user.role_id,
|
||||
});
|
||||
|
||||
return { ok: true, message: res.message || fallbackMessage };
|
||||
@@ -49,6 +55,9 @@ export const useAuthStore = defineStore("auth", {
|
||||
|
||||
getters: {
|
||||
isAuthenticated: (state) => Boolean(state.token),
|
||||
roleId: (state) => state.currentUser?.role_id ?? null,
|
||||
isAdmin: (state) => state.currentUser?.role_id === ROLE_ADMIN,
|
||||
isAgent: (state) => state.currentUser?.role_id === ROLE_AGENT,
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
@@ -1,238 +1,486 @@
|
||||
import { defineStore, acceptHMRUpdate } from "pinia";
|
||||
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
import {
|
||||
|
||||
ensureAppConfigReady,
|
||||
|
||||
validateShareText,
|
||||
|
||||
DEFAULT_REWRITE_CONFIG,
|
||||
|
||||
} from "../config/videoPipeline.js";
|
||||
|
||||
|
||||
|
||||
const PIPELINE_SCRIPT = "douyin_pipeline.js";
|
||||
const NODEJS_EVENT = "nodejs:event";
|
||||
|
||||
|
||||
|
||||
const defaultPublishPlatforms = () => [
|
||||
|
||||
{ label: "*音", checked: false, account: null },
|
||||
|
||||
{ label: "*手", checked: false, account: null },
|
||||
|
||||
{ label: "*频号", checked: false, account: null },
|
||||
|
||||
{ label: "*红书", checked: false, account: null },
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
export const useWorkflowStore = defineStore("workflow", {
|
||||
|
||||
state: () => ({
|
||||
|
||||
// 01 IP 深度学习
|
||||
|
||||
ipMode: "ipBrain",
|
||||
|
||||
ipBrainModalVisible: false,
|
||||
|
||||
ipBrainProfileUrl: "",
|
||||
|
||||
ipBrainCollecting: false,
|
||||
|
||||
ipBenchmarks: [],
|
||||
|
||||
ipTopics: [],
|
||||
|
||||
ipBrainSelectedBenchmarkId: null,
|
||||
|
||||
videoLinkModalVisible: false,
|
||||
|
||||
videoShareText: "",
|
||||
|
||||
videoRewriting: false,
|
||||
|
||||
/** 流水线进度文案(来自 quickjs:event progress) */
|
||||
|
||||
rewriteProgress: "",
|
||||
|
||||
recognizedContent: "",
|
||||
|
||||
/** 自定义仿写提示词(可选,对应 zhenqianba rewritePrompt) */
|
||||
|
||||
rewritePrompt: "",
|
||||
|
||||
videoType: "口播文案",
|
||||
|
||||
copyType: "人设型",
|
||||
|
||||
industryPersona: "",
|
||||
|
||||
productBusiness: "",
|
||||
|
||||
sellingPrice: "",
|
||||
|
||||
otherRequirements: "",
|
||||
|
||||
targetWords: 300,
|
||||
|
||||
|
||||
|
||||
// 视频文案编辑
|
||||
|
||||
scriptContent: "",
|
||||
|
||||
editLanguage: "中文(普通话)",
|
||||
|
||||
editWordCount: 300,
|
||||
|
||||
|
||||
|
||||
// 02 音视频生成
|
||||
|
||||
voiceEmotion: "自然",
|
||||
|
||||
speechRate: 1,
|
||||
|
||||
voiceLanguage: "中文(普通话)",
|
||||
|
||||
avatarSelect: null,
|
||||
|
||||
|
||||
|
||||
// 03 视频编辑
|
||||
|
||||
pipInPicture: false,
|
||||
|
||||
autoCutBreath: false,
|
||||
|
||||
greenScreen: false,
|
||||
|
||||
|
||||
|
||||
// 04 标题标签关键词
|
||||
|
||||
titleGenerated: "",
|
||||
|
||||
tagsGenerated: "",
|
||||
|
||||
keywordTab: "0",
|
||||
|
||||
keywordsFocus: "",
|
||||
|
||||
keywordsDescribe: "",
|
||||
|
||||
keywordsAction: "",
|
||||
|
||||
keywordsEmotion: "",
|
||||
|
||||
|
||||
|
||||
// 05 字幕和音乐
|
||||
|
||||
autoSubtitle: false,
|
||||
|
||||
smartSubtitle: true,
|
||||
|
||||
bgmEnabled: false,
|
||||
|
||||
bgmVolume: 30,
|
||||
|
||||
subtitleTemplate: null,
|
||||
|
||||
|
||||
|
||||
// 07 视频发布
|
||||
|
||||
publishPlatforms: defaultPublishPlatforms(),
|
||||
|
||||
}),
|
||||
|
||||
|
||||
|
||||
getters: {
|
||||
|
||||
ipModes: () => [
|
||||
|
||||
{ label: "IP学习", value: "ipBrain" },
|
||||
|
||||
{ label: "视频学习", value: "videoWrite" },
|
||||
|
||||
{ label: "爆款文案", value: "customPrompt" },
|
||||
|
||||
],
|
||||
|
||||
videoTypes: () => ["口播文案", "剧情文案", "带货文案"],
|
||||
|
||||
copyTypes: () => ["人设型", "卖点型", "故事型"],
|
||||
|
||||
voiceEmotions: () => ["自然", "热情", "沉稳"],
|
||||
|
||||
languages: () => ["中文(普通话)", "中文(粤语)", "English"],
|
||||
|
||||
avatarOptions: () => [{ label: "请选择形象", value: null }],
|
||||
|
||||
keywordCountFocus: (state) => countLines(state.keywordsFocus),
|
||||
|
||||
keywordCountDescribe: (state) => countLines(state.keywordsDescribe),
|
||||
|
||||
keywordCountAction: (state) => countLines(state.keywordsAction),
|
||||
|
||||
keywordCountEmotion: (state) => countLines(state.keywordsEmotion),
|
||||
|
||||
ipBenchmarkCount: (state) => state.ipBenchmarks.length,
|
||||
|
||||
selectedBenchmark: (state) =>
|
||||
|
||||
state.ipBenchmarks.find((b) => b.id === state.ipBrainSelectedBenchmarkId) ?? null,
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
actions: {
|
||||
|
||||
openIpBrainDialog() {
|
||||
|
||||
this.ipBrainModalVisible = true;
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
closeIpBrainDialog() {
|
||||
|
||||
this.ipBrainModalVisible = false;
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
selectBenchmark(id) {
|
||||
|
||||
this.ipBrainSelectedBenchmarkId = id;
|
||||
|
||||
this.ipTopics = [];
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
openVideoLinkDialog() {
|
||||
|
||||
this.videoLinkModalVisible = true;
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
closeVideoLinkDialog() {
|
||||
|
||||
this.videoLinkModalVisible = false;
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
async startVideoRewrite() {
|
||||
|
||||
return executeVideoRewrite(this);
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
resetAll() {
|
||||
|
||||
this.$reset();
|
||||
|
||||
this.publishPlatforms = defaultPublishPlatforms();
|
||||
|
||||
},
|
||||
|
||||
|
||||
|
||||
clearData() {
|
||||
|
||||
this.resetAll();
|
||||
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
export async function processDouyinShare(shareText) {
|
||||
const result = await invoke("run_quickjs_script", {
|
||||
scriptName: "douyin_pipeline.js",
|
||||
params: {
|
||||
shareText: shareText ?? "",
|
||||
modelConfig: {
|
||||
providerId: "openai",
|
||||
modelId: "gpt-4o-mini",
|
||||
apiUrl: "https://api.openai.com", // 或 dashscope/openrouter 等任意兼容端点
|
||||
apiKey: "sk-...",
|
||||
asrMode: "online",
|
||||
aliyunApiKey: "sk-dashscope-...",
|
||||
ossConfig: {
|
||||
accessKeyId: "...",
|
||||
accessKeySecret: "...",
|
||||
bucket: "my-bucket",
|
||||
region: "oss-cn-hangzhou",
|
||||
},
|
||||
},
|
||||
rewriteConfig: { style: "幽默口播", tone: "活泼" },
|
||||
customPrompt: null,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 一键仿写:Node.js douyin_pipeline(对齐 zhenqianba ipAgent:processDouyinShare)
|
||||
* @param {ReturnType<typeof useWorkflowStore>} store
|
||||
*/
|
||||
export async function executeVideoRewrite(store) {
|
||||
const text = store.videoShareText?.trim();
|
||||
if (!text) {
|
||||
return { ok: false, message: "请粘贴视频分享文本或视频链接" };
|
||||
const parsed = validateShareText(store.videoShareText);
|
||||
if (!parsed.ok) {
|
||||
return { ok: false, message: parsed.message };
|
||||
}
|
||||
|
||||
const unsupported = detectUnsupportedPlatform(text);
|
||||
const unsupported = detectUnsupportedPlatform(parsed.value);
|
||||
if (unsupported) {
|
||||
return { ok: false, message: unsupported };
|
||||
}
|
||||
|
||||
store.videoRewriting = true;
|
||||
try {
|
||||
// TODO: 对接 FUNASR 语音识别与仿写 API
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
const cfgReady = await ensureAppConfigReady();
|
||||
if (!cfgReady.ok) {
|
||||
return { ok: false, message: cfgReady.message };
|
||||
}
|
||||
|
||||
store.videoRewriting = true;
|
||||
store.rewriteProgress = "正在解析和仿写...";
|
||||
|
||||
let unlisten = null;
|
||||
try {
|
||||
try {
|
||||
unlisten = await listen(NODEJS_EVENT, (e) => {
|
||||
const p = e.payload;
|
||||
if (p?.type === "progress" && typeof p.status === "string") {
|
||||
store.rewriteProgress = p.status;
|
||||
}
|
||||
});
|
||||
} 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 (!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.recognizedContent =
|
||||
"【示例识别结果】这里是 FUNASR 语音识别后的原始文案内容,待对接后端后将显示真实结果。\n\n" +
|
||||
`来源:${truncateUrl(text, 48)}`;
|
||||
store.videoShareText = "";
|
||||
store.videoLinkModalVisible = false;
|
||||
return { ok: true, message: "仿写完成" };
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
message: "仿写完成,已为您生成新文案",
|
||||
data: result,
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err || "未知错误");
|
||||
return { ok: false, message: `仿写失败: ${msg}` };
|
||||
} finally {
|
||||
if (typeof unlisten === "function") {
|
||||
unlisten();
|
||||
}
|
||||
store.videoRewriting = false;
|
||||
store.rewriteProgress = "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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));
|
||||
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user