From f2d33ee3560eb3dd93da39f153de138c88180be2 Mon Sep 17 00:00:00 2001 From: "fengchuanhn@gmail.com" Date: Sat, 16 May 2026 11:41:42 +0800 Subject: [PATCH] 11 --- src-tauri/js/douyin_pipeline.js | 381 ------------------------------ src-tauri/src/commands/quickjs.rs | 24 +- src-tauri/src/js_runtime/mod.rs | 177 +++++++++----- src/stores/workflow.js | 6 +- 4 files changed, 121 insertions(+), 467 deletions(-) delete mode 100644 src-tauri/js/douyin_pipeline.js diff --git a/src-tauri/js/douyin_pipeline.js b/src-tauri/js/douyin_pipeline.js deleted file mode 100644 index 6413281..0000000 --- a/src-tauri/js/douyin_pipeline.js +++ /dev/null @@ -1,381 +0,0 @@ -// ============================================================================ -// douyin_pipeline.js -// -// Pure-JS port of the original `VideoRewriteIntegration.processDouyinShareComplete` -// pipeline. All host capabilities (network, fs, ffmpeg, OSS, ASR, AI chat, -// log, progress) come from the `__native` global injected by the Rust side -// (see `js_runtime/native.rs`). Nothing in this file should ever depend on -// Node.js, Electron or browser globals. -// -// Public entry point exposed to Rust: -// processDouyinShare(params) -> Promise -// -// `params` shape mirrors the original Electron IPC payload: -// { -// shareText: string, -// modelConfig: { providerId, modelId, apiUrl, apiKey, -// asrMode: "online" | "local", -// aliyunApiKey, ossConfig, asrServerInfo }, -// rewriteConfig: object, -// customPrompt: string | null -// } -// ============================================================================ - -const log = (level, msg, fields) => __native.log(level, msg, fields || null); -const progress = (s) => { try { __native.emitProgress(s); } catch (_) { /* ignore */ } }; - -// --------------------------------------------------------------------------- -// DouyinContentParser (pure-JS, no host calls) -// --------------------------------------------------------------------------- -const DouyinContentParser = { - // Extract a sharable URL from a raw "share text". Handles: - // - direct https://... links - // - "复制此链接,打开Dou音搜索,直接观看视频!https://v.douyin.com/xxxx/" - // - bare v.douyin.com short links - parseContent(shareText) { - if (!shareText || typeof shareText !== "string") { - return { success: false, error: "分享文本为空" }; - } - const urlRegex = /https?:\/\/[^\s\u4e00-\u9fa5]+/g; - const matches = shareText.match(urlRegex); - if (!matches || matches.length === 0) { - return { success: false, error: "未在文本中识别到任何 URL" }; - } - // Prefer douyin/iesdouyin domains, fallback to first match. - const douyinUrl = - matches.find((u) => /douyin\.com|iesdouyin\.com/i.test(u)) || matches[0]; - return { success: true, content: { videoUrl: douyinUrl, raw: shareText } }; - }, - - // Whether `url` already points at an actual media file we can fetch directly. - isDirectVideoUrl(url) { - if (!url) return false; - return /\.(mp4|mov|m4v|webm|mkv|flv|avi)(\?|$)/i.test(url); - }, - - // Derive a short title (<= 20 chars) from a long description. - generateTitleFromDescription(desc) { - if (!desc) return "改写视频"; - const firstLine = String(desc).split(/[\r\n]/)[0].trim(); - const cleaned = firstLine.replace(/[#@].*$/g, "").trim(); - return (cleaned || "改写视频").slice(0, 20); - }, -}; - -// --------------------------------------------------------------------------- -// DouyinVideoDownloader -// Resolves the final media URL (following 302 redirects on the share -// domain) and saves the binary to a host-managed temp file. -// --------------------------------------------------------------------------- -class DouyinVideoDownloader { - async downloadDirectVideo(url) { - log("info", "downloader.directVideo", { url }); - const dest = __native.tempPath("video", "mp4"); - const res = await __native.httpRequest({ - method: "GET", - url, - headers: { "User-Agent": this._ua() }, - response_type: "file", - dest_path: dest, - timeout_ms: 120000, - }); - if (!res || res.status >= 400) { - return { success: false, error: `直链下载失败 status=${res && res.status}` }; - } - return { success: true, videoPath: dest }; - } - - async downloadDouyinVideo(shareUrl) { - log("info", "downloader.douyin", { shareUrl }); - // Step 1: follow redirect on v.douyin.com -> iesdouyin.com to get itemId - let landing; - try { - landing = await __native.httpRequest({ - method: "GET", - url: shareUrl, - headers: { "User-Agent": this._ua() }, - response_type: "text", - follow_redirects: true, - timeout_ms: 30000, - }); - } catch (e) { - return { success: false, error: `解析跳转失败: ${e.message || e}` }; - } - const finalUrl = (landing && landing.final_url) || shareUrl; - const itemId = - this._extract(/\/video\/(\d+)/, finalUrl) || - this._extract(/item_ids?=(\d+)/, finalUrl) || - this._extract(/modal_id=(\d+)/, finalUrl); - if (!itemId) { - return { - success: false, - error: "未能从分享链接中提取视频 itemId,可能是抖音改版", - }; - } - // Step 2: ask iesdouyin for the playable URL. - const apiUrl = - "https://www.iesdouyin.com/web/api/v2/aweme/iteminfo/?item_ids=" + itemId; - let info; - try { - info = await __native.httpRequest({ - method: "GET", - url: apiUrl, - headers: { "User-Agent": this._ua() }, - response_type: "json", - timeout_ms: 30000, - }); - } catch (e) { - return { success: false, error: `获取视频信息失败: ${e.message || e}` }; - } - const item = - info && info.body && info.body.item_list && info.body.item_list[0]; - const playAddr = - item && item.video && item.video.play_addr && item.video.play_addr.url_list; - if (!playAddr || !playAddr.length) { - return { success: false, error: "iesdouyin 接口未返回播放地址" }; - } - // Force HTTPS + replace `playwm` (watermarked) with `play` if present. - const mediaUrl = playAddr[0] - .replace(/^http:\/\//, "https://") - .replace("/playwm/", "/play/"); - return this.downloadDirectVideo(mediaUrl); - } - - _ua() { - return ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + - "AppleWebKit/537.36 (KHTML, like Gecko) " + - "Chrome/124.0.0.0 Safari/537.36" - ); - } - _extract(re, s) { - const m = String(s || "").match(re); - return m ? m[1] : null; - } -} - -// --------------------------------------------------------------------------- -// AI rewrite (OpenAI-compatible chat completions) -// --------------------------------------------------------------------------- -const VideoRewriteOptimizer = { - buildRewritePrompt(content, rewriteConfig, customPrompt) { - if (customPrompt && String(customPrompt).trim().length > 0) { - return String(customPrompt).replace(/\{\{\s*content\s*\}\}/g, content); - } - const cfg = rewriteConfig || {}; - const style = cfg.style || "短视频口播风格"; - const tone = cfg.tone || "自然、口语化"; - const lengthHint = cfg.length ? `控制在 ${cfg.length} 字以内` : "保持与原文相近的长度"; - return [ - "你是一名专业的短视频文案改写助手。", - `请基于下面这段视频转写文案,用【${style}】重新仿写一版。`, - `要求:语气 ${tone},${lengthHint},保留原意但避免与原文雷同;`, - "不要解释、不要加任何额外标题,直接输出仿写后的正文。", - "", - "【原文案】", - content, - ].join("\n"); - }, -}; - -async function rewriteWithAI(content, modelConfig, rewriteConfig, customPrompt) { - if (!modelConfig || !modelConfig.apiUrl || !modelConfig.apiKey) { - log("error", "rewrite.missingConfig"); - return null; - } - const prompt = VideoRewriteOptimizer.buildRewritePrompt( - content, - rewriteConfig || {}, - customPrompt - ); - const result = await __native.openaiChat({ - apiUrl: modelConfig.apiUrl, - apiKey: modelConfig.apiKey, - model: modelConfig.modelId, - messages: [ - { role: "system", content: "你是专业的短视频文案改写助手。" }, - { role: "user", content: prompt }, - ], - temperature: typeof modelConfig.temperature === "number" ? modelConfig.temperature : 0.8, - max_tokens: modelConfig.maxTokens || 1024, - }); - if (!result || !result.success) { - log("error", "rewrite.failed", { error: result && result.error }); - return null; - } - return (result.content || "").trim(); -} - -// --------------------------------------------------------------------------- -// VideoRewriteIntegration.processDouyinShareComplete -// The 1:1 port of the Electron version's pipeline. -// --------------------------------------------------------------------------- -async function processDouyinShareComplete(shareText, modelConfig, rewriteConfig, customPrompt) { - let videoPath = null; - let audioPath = null; - try { - log("info", "pipeline.start", { - inputLength: shareText && shareText.length, - modelConfig: { - providerId: modelConfig && modelConfig.providerId, - modelId: modelConfig && modelConfig.modelId, - asrMode: modelConfig && modelConfig.asrMode, - }, - }); - - // 1) parse share text -> video URL - progress("正在解析视频URL..."); - const parseResult = DouyinContentParser.parseContent(shareText); - if (!parseResult.success || !parseResult.content) { - return { success: false, error: parseResult.error || "内容解析失败" }; - } - const { videoUrl } = parseResult.content; - log("info", "pipeline.urlExtracted", { videoUrl }); - - // 2) download video - progress("正在下载视频..."); - const downloader = new DouyinVideoDownloader(); - const downloadResult = DouyinContentParser.isDirectVideoUrl(videoUrl) - ? await downloader.downloadDirectVideo(videoUrl) - : await downloader.downloadDouyinVideo(videoUrl); - if (!downloadResult.success || !downloadResult.videoPath) { - return { success: false, error: downloadResult.error || "视频下载失败" }; - } - videoPath = downloadResult.videoPath; - log("info", "pipeline.videoDownloaded", { videoPath }); - - // 3) extract audio (ffmpeg sidecar) - progress("视频下载完成,正在提取音频..."); - try { - audioPath = await __native.ffmpegExtractAudio(videoPath); - } catch (e) { - return { - success: false, - error: `音频提取失败: ${(e && e.message) || e}`, - }; - } - log("info", "pipeline.audioExtracted", { audioPath }); - - // 4) ASR - const asrMode = (modelConfig && modelConfig.asrMode) || "online"; - let recognizedText = ""; - if (asrMode === "online") { - progress("音频提取完成,正在上传到OSS..."); - const ossConfig = modelConfig.ossConfig; - if (!ossConfig || !ossConfig.bucket) { - return { success: false, error: "未配置阿里云 OSS(bucket / region / key)" }; - } - const upload = await __native.aliyunOssUpload(audioPath, ossConfig); - if (!upload || !upload.success || !upload.url) { - return { - success: false, - error: `音频上传失败: ${(upload && upload.error) || "unknown"}`, - }; - } - log("info", "pipeline.ossOk", { audioUrl: upload.url.slice(0, 100) + "..." }); - - progress("音频上传完成,正在识别文案(在线模式)..."); - const asr = await __native.aliyunAsrFiletrans(upload.url, { - apiKey: modelConfig.aliyunApiKey, - model: "qwen3-asr-flash-filetrans", - }); - if (!asr || !asr.success) { - return { - success: false, - error: `语音识别失败(在线模式): ${(asr && asr.error) || "unknown"}`, - }; - } - recognizedText = asr.text || ""; - log("info", "pipeline.asrOk", { - method: "aliyun-online", - len: recognizedText.length, - }); - } else { - progress("音频提取完成,正在识别文案(本地模式)..."); - const info = modelConfig.asrServerInfo; - if (!info) { - return { - success: false, - error: "未配置本地语音识别服务器,请在「设置」中配置", - }; - } - const asr = await __native.localAsr(audioPath, info); - if (!asr || !asr.success) { - return { - success: false, - error: `语音识别失败(本地模式): ${(asr && asr.error) || "unknown"}`, - }; - } - recognizedText = asr.text || ""; - log("info", "pipeline.asrOk", { - method: "local", - server: info.name, - len: recognizedText.length, - }); - } - - // 5) rewrite - if (!modelConfig || !modelConfig.apiUrl || !modelConfig.apiKey) { - return { success: false, error: "未配置AI模型,请先在设置中配置" }; - } - progress("文案识别完成,正在改写..."); - const rewrittenDescription = await rewriteWithAI( - recognizedText, - modelConfig, - rewriteConfig, - customPrompt - ); - if (!rewrittenDescription) { - return { success: false, error: "AI仿写失败,请检查模型配置和网络连接" }; - } - const rewrittenTitle = DouyinContentParser.generateTitleFromDescription(rewrittenDescription); - - log("info", "pipeline.success", { - originalLength: recognizedText.length, - rewrittenLength: rewrittenDescription.length, - }); - progress("完成!"); - - return { - success: true, - original: { - videoUrl, - description: recognizedText, - hashtags: [], - title: "视频音频识别结果", - }, - rewritten: { - description: rewrittenDescription, - title: rewrittenTitle, - fullContent: rewrittenDescription, - }, - }; - } catch (e) { - log("error", "pipeline.error", { message: (e && e.message) || String(e) }); - return { success: false, error: (e && e.message) || String(e) }; - } finally { - // 6) cleanup local temp files - if (videoPath) { - try { __native.deleteFile(videoPath); } catch (_) {} - } - if (audioPath) { - try { __native.deleteFile(audioPath); } catch (_) {} - } - } -} - -// --------------------------------------------------------------------------- -// Bootstrap: register into the global script table. -// The Rust runtime looks the entry up by `scriptName` at invocation time -// (see commands/quickjs.rs + js_runtime/mod.rs). -// --------------------------------------------------------------------------- -globalThis.__scripts = globalThis.__scripts || {}; -globalThis.__scripts["process_douyin_share"] = function (params) { - const p = params || {}; - return processDouyinShareComplete( - p.shareText, - p.modelConfig, - p.rewriteConfig, - p.customPrompt || null - ); -}; diff --git a/src-tauri/src/commands/quickjs.rs b/src-tauri/src/commands/quickjs.rs index f625ee7..ac8ed54 100644 --- a/src-tauri/src/commands/quickjs.rs +++ b/src-tauri/src/commands/quickjs.rs @@ -1,21 +1,15 @@ -//! Generic Tauri commands for invoking any JS script registered in the -//! embedded QuickJS runtime. Replaces the per-pipeline command so adding a -//! new flow only requires: +//! QuickJS 流水线命令:脚本由后端 `/api/v1/quickjs-scripts` 下发, +//! 运行时注入 `__native` 后 eval,并调用 `globalThis.__quickjsMain(params)`。 //! -//! 1. Drop a new `.js` file into `src-tauri/js/`. -//! 2. Add it to `js_runtime::SCRIPT_FILES`. -//! 3. Inside the JS file, register one or more entries on -//! `globalThis.__scripts[]`. +//! 前端用法示例: //! -//! Frontend usage: -//! -//! ```ts +//! ```ignore //! import { invoke } from "@tauri-apps/api/core"; //! import { listen } from "@tauri-apps/api/event"; //! //! const off = await listen("quickjs:event", (e) => console.log(e.payload)); //! const result = await invoke("run_quickjs_script", { -//! scriptName: "process_douyin_share", +//! scriptName: "douyin_pipeline.js", //! params: { shareText: "...", modelConfig: {...}, ... }, //! }); //! off(); @@ -48,11 +42,6 @@ pub enum FrontendEvent { const EVENT_NAME: &str = "quickjs:event"; -/// Run an arbitrary registered QuickJS script by name. -/// -/// `script_name` must already be registered on `globalThis.__scripts` by -/// one of the bundled `js/*.js` files. `params` is forwarded as-is to the -/// script entry function. #[tauri::command] pub async fn run_quickjs_script( app: AppHandle, @@ -62,7 +51,6 @@ pub async fn run_quickjs_script( ) -> Result { let (tx, mut rx) = mpsc::unbounded_channel::(); - // Forward script events to the frontend window. let app_for_emit = app.clone(); let win_label = window.label().to_string(); let script_for_emit = script_name.clone(); @@ -92,7 +80,7 @@ pub async fn run_quickjs_script( Ok(result) } -/// Diagnostic helper — returns the list of registered script names. +/// 列出后端 `scripts/quickjs` 目录中的 `.js` 文件名。 #[tauri::command] pub async fn list_quickjs_scripts() -> Result, String> { js_runtime::list_scripts().await.map_err(|e| e.to_string()) diff --git a/src-tauri/src/js_runtime/mod.rs b/src-tauri/src/js_runtime/mod.rs index 238e636..a98c54d 100644 --- a/src-tauri/src/js_runtime/mod.rs +++ b/src-tauri/src/js_runtime/mod.rs @@ -1,14 +1,8 @@ -//! Embedded QuickJS runtime that runs *named* JS scripts. +//! Embedded QuickJS runtime:脚本源码由 **Python 后端** 按文件名下发, +//! 桌面端通过 HTTP 拉取后在运行时 `eval`,入口为单一的 `globalThis.__quickjsMain`。 //! -//! Each script file under `src-tauri/js/*.js` is compiled into the binary -//! via `include_str!`. At startup the runtime evaluates all of them in -//! order; each file is expected to register one or more handlers into the -//! global `__scripts` table: -//! -//! globalThis.__scripts = globalThis.__scripts || {}; -//! globalThis.__scripts["my_script"] = function (params) { ... }; -//! -//! The Rust side then resolves `__scripts[name]` at invocation time. +//! 后端:`GET /api/v1/quickjs-scripts?name=<文件名.js>`(`ApiResponse`,`data.source`)。 +//! API 基址:`AICLIENT_API_BASE` 环境变量,默认 `http://127.0.0.1:8001`。 pub mod native; @@ -21,18 +15,10 @@ use tokio::sync::mpsc::UnboundedSender; use native::NativeBridge; -// --------------------------------------------------------------------------- -// Bundled script registry -// -// Add a new line here whenever you drop a new file into `src-tauri/js/`. -// The order matters only if scripts depend on each other; ours don't. -// --------------------------------------------------------------------------- -const BOOTSTRAP_JS: &str = "globalThis.__scripts = globalThis.__scripts || {};"; +/// QuickJS 侧约定的唯一入口(服务端下发的脚本末尾应赋值此全局)。 +const ENTRY_GLOBAL: &str = "__quickjsMain"; -const SCRIPT_FILES: &[(&str, &str)] = &[ - ("douyin_pipeline.js", include_str!("../../js/douyin_pipeline.js")), - // ("my_next_pipeline.js", include_str!("../../js/my_next_pipeline.js")), -]; +const RESET_ENTRY_JS: &str = "globalThis.__quickjsMain = undefined;"; #[derive(Debug, Error)] pub enum JsError { @@ -40,12 +26,61 @@ pub enum JsError { QuickJs(String), #[error("serde: {0}")] Serde(#[from] serde_json::Error), - #[error("unknown script: {0}")] - UnknownScript(String), + #[error("拉取脚本失败: {0}")] + FetchScript(String), + #[error("脚本未注册全局入口 __quickjsMain")] + MissingMainEntry, } impl From for JsError { - fn from(e: rquickjs::Error) -> Self { JsError::QuickJs(e.to_string()) } + fn from(e: rquickjs::Error) -> Self { + JsError::QuickJs(e.to_string()) + } +} + +fn api_base() -> String { + std::env::var("AICLIENT_API_BASE").unwrap_or_else(|_| "http://127.0.0.1:8001".to_string()) +} + +async fn fetch_script_source(script_name: &str) -> Result { + let base = api_base().trim_end_matches('/').to_string(); + let q = urlencoding::encode(script_name); + let url = format!("{base}/api/v1/quickjs-scripts?name={q}"); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(120)) + .build() + .map_err(|e| JsError::FetchScript(e.to_string()))?; + + let res = client + .get(&url) + .send() + .await + .map_err(|e| JsError::FetchScript(e.to_string()))?; + + let body = res + .text() + .await + .map_err(|e| JsError::FetchScript(e.to_string()))?; + + let v: Value = + serde_json::from_str(&body).map_err(|e| JsError::FetchScript(format!("响应非 JSON: {e}")))?; + + let ok = v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false); + if !ok { + let msg = v + .get("message") + .and_then(|x| x.as_str()) + .unwrap_or("未知错误"); + return Err(JsError::FetchScript(msg.to_string())); + } + + let source = v + .get("data") + .and_then(|d| d.get("source")) + .and_then(|s| s.as_str()) + .ok_or_else(|| JsError::FetchScript("响应缺少 data.source".into()))?; + + Ok(source.to_string()) } /// Progress / log message produced by a running script. @@ -59,57 +94,46 @@ pub enum PipelineEvent { }, } -/// Execute the script registered under `script_name` and return its result. +/// 从服务端拉取 `script_name` 对应源码,执行 `__quickjsMain(params)` 并返回 JSON 结果。 pub async fn run_script( script_name: String, params: Value, events: UnboundedSender, ) -> Result { + let source = fetch_script_source(&script_name).await?; + let bridge = Arc::new(NativeBridge::new(events)); let runtime = AsyncRuntime::new()?; let context = AsyncContext::full(&runtime).await?; let result_json: Value = async_with!(context => |ctx| { - // 1) install the __native bridge let native_obj = native::build_native_object(ctx.clone(), bridge.clone()) .map_err(|e| JsError::QuickJs(format!("install native: {e}")))?; ctx.globals().set("__native", native_obj) .map_err(|e| JsError::QuickJs(format!("set __native: {e}")))?; - // 2) bootstrap + evaluate every registered script file - let _: () = ctx.eval(BOOTSTRAP_JS).catch(&ctx) - .map_err(|e| JsError::QuickJs(format!("eval bootstrap: {e}")))?; - for (file_name, source) in SCRIPT_FILES { - let _: () = ctx.eval(*source).catch(&ctx) - .map_err(|e| JsError::QuickJs(format!("eval {file_name}: {e}")))?; - } + let _: () = ctx.eval(RESET_ENTRY_JS).catch(&ctx) + .map_err(|e| JsError::QuickJs(format!("reset entry: {e}")))?; + let _: () = ctx.eval(source.as_str()).catch(&ctx) + .map_err(|e| JsError::QuickJs(format!("eval {}: {e}", script_name)))?; - // 3) look up __scripts[script_name] - let scripts: rquickjs::Object = ctx.globals().get("__scripts") - .map_err(|e| JsError::QuickJs(format!("get __scripts: {e}")))?; - let entry: rquickjs::Function = scripts - .get(script_name.as_str()) - .map_err(|_| JsError::UnknownScript(script_name.clone()))?; + let main_fn: rquickjs::Function = ctx.globals().get(ENTRY_GLOBAL) + .map_err(|_| JsError::MissingMainEntry)?; - // 4) call entry(params) let params_str = serde_json::to_string(¶ms) .map_err(|e| JsError::QuickJs(format!("serialize params: {e}")))?; let params_value: rquickjs::Value = ctx.json_parse(params_str) .map_err(|e| JsError::QuickJs(format!("parse params: {e}")))?; - // The entry may be sync (returns Value) or async (returns Promise). - // Cast the return value to Value first, then promote to Promise if it - // *is* a Promise; otherwise use it as-is. - let raw: rquickjs::Value = entry.call((params_value,)) - .map_err(|e| JsError::QuickJs(format!("call entry: {e}")))?; + let raw: rquickjs::Value = main_fn.call((params_value,)) + .map_err(|e| JsError::QuickJs(format!("call __quickjsMain: {e}")))?; let value: rquickjs::Value = match Promise::from_value(raw.clone()) { Ok(p) => p.into_future().await - .map_err(|e| JsError::QuickJs(format!("await {script_name}: {e}")))?, + .map_err(|e| JsError::QuickJs(format!("await {}: {e}", script_name)))?, Err(_) => raw, }; - // 5) JS value -> serde_json::Value via JSON.stringify let json_global: rquickjs::Object = ctx.globals().get("JSON") .map_err(|e| JsError::QuickJs(format!("get JSON: {e}")))?; let stringify: rquickjs::Function = json_global.get("stringify") @@ -124,24 +148,47 @@ pub async fn run_script( Ok(result_json) } -/// Best-effort list of names currently registered in `__scripts`. -/// Useful for diagnostics / a `list_quickjs_scripts` command. +/// 当前服务端 `scripts/quickjs` 目录下的 `.js` 文件名列表。 pub async fn list_scripts() -> Result, JsError> { - let runtime = AsyncRuntime::new()?; - let context = AsyncContext::full(&runtime).await?; - let names: Vec = async_with!(context => |ctx| { - let _: () = ctx.eval(BOOTSTRAP_JS).catch(&ctx) - .map_err(|e| JsError::QuickJs(format!("bootstrap: {e}")))?; - for (file_name, src) in SCRIPT_FILES { - let _: () = ctx.eval(*src).catch(&ctx) - .map_err(|e| JsError::QuickJs(format!("eval {file_name}: {e}")))?; - } - let keys: String = ctx.eval(r#"Object.keys(globalThis.__scripts || {}).join(",")"#) - .map_err(|e| JsError::QuickJs(format!("collect keys: {e}")))?; - Ok::, JsError>( - keys.split(',').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect() - ) - }) - .await?; + let base = api_base().trim_end_matches('/').to_string(); + let url = format!("{base}/api/v1/quickjs-scripts/list"); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| JsError::FetchScript(e.to_string()))?; + + let res = client + .get(&url) + .send() + .await + .map_err(|e| JsError::FetchScript(e.to_string()))?; + + let body = res + .text() + .await + .map_err(|e| JsError::FetchScript(e.to_string()))?; + + let v: Value = + serde_json::from_str(&body).map_err(|e| JsError::FetchScript(format!("响应非 JSON: {e}")))?; + + if !v.get("ok").and_then(|x| x.as_bool()).unwrap_or(false) { + let msg = v + .get("message") + .and_then(|x| x.as_str()) + .unwrap_or("未知错误"); + return Err(JsError::FetchScript(msg.to_string())); + } + + let names = v + .get("data") + .and_then(|d| d.get("names")) + .and_then(|n| n.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|x| x.as_str().map(|s| s.to_string())) + .collect::>() + }) + .unwrap_or_default(); + Ok(names) } diff --git a/src/stores/workflow.js b/src/stores/workflow.js index cf67ad7..7237635 100644 --- a/src/stores/workflow.js +++ b/src/stores/workflow.js @@ -155,11 +155,11 @@ export const useWorkflowStore = defineStore("workflow", { }, }, }); -export async function processDouyinShare(shareText){ +export async function processDouyinShare(shareText) { const result = await invoke("run_quickjs_script", { - scriptName:"douyin_pipeline.js", + scriptName: "douyin_pipeline.js", params: { - shareText: "复制此链接,打开Dou音搜索,直接观看视频!https://v.douyin.com/xxxxx/", + shareText: shareText ?? "", modelConfig: { providerId: "openai", modelId: "gpt-4o-mini",