From f1fce871bb4274a81d02918841309bb0679d74b1 Mon Sep 17 00:00:00 2001 From: "fengchuanhn@gmail.com" Date: Sun, 17 May 2026 12:54:04 +0800 Subject: [PATCH] 1 --- .env.example | 8 +- src-tauri/src/app_config.rs | 180 ++++++++ src-tauri/src/auth_session.rs | 1 + src-tauri/src/commands/app_config.rs | 19 + src-tauri/src/commands/auth.rs | 12 +- src-tauri/src/commands/mod.rs | 1 + src-tauri/src/commands/nodejs.rs | 9 +- src-tauri/src/lib.rs | 4 + src-tauri/src/nodejs.rs | 248 +++++++---- src/api/adminCardKeys.js | 58 +++ src/api/adminDesktopConfigs.js | 62 +++ src/api/adminUsers.js | 57 +++ src/api/client.js | 7 + src/components/AppSidebar.vue | 29 +- src/components/admin/AdminSubNav.vue | 30 ++ src/components/workflow/VideoLinkDialog.vue | 21 +- src/config/videoPipeline.js | 69 ++++ src/main.js | 6 + src/router/index.js | 429 +++++++++++++++----- src/stores/auth.js | 9 + src/stores/workflow.js | 324 +++++++++++++-- src/styles/main.css | 120 ++++++ src/views/AdminView.vue | 12 + src/views/AgentView.vue | 14 + src/views/admin/AdminCardKeysView.vue | 369 +++++++++++++++++ src/views/admin/AdminDesktopConfigView.vue | 325 +++++++++++++++ src/views/admin/AdminOverviewView.vue | 14 + src/views/admin/AdminUsersView.vue | 364 +++++++++++++++++ 28 files changed, 2566 insertions(+), 235 deletions(-) create mode 100644 src-tauri/src/app_config.rs create mode 100644 src-tauri/src/commands/app_config.rs create mode 100644 src/api/adminCardKeys.js create mode 100644 src/api/adminDesktopConfigs.js create mode 100644 src/api/adminUsers.js create mode 100644 src/components/admin/AdminSubNav.vue create mode 100644 src/config/videoPipeline.js create mode 100644 src/views/AdminView.vue create mode 100644 src/views/AgentView.vue create mode 100644 src/views/admin/AdminCardKeysView.vue create mode 100644 src/views/admin/AdminDesktopConfigView.vue create mode 100644 src/views/admin/AdminOverviewView.vue create mode 100644 src/views/admin/AdminUsersView.vue diff --git a/.env.example b/.env.example index 98240c3..7a05834 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,9 @@ # 开发默认走 Vite 代理 /api -> http://127.0.0.1:8001 -# 生产或直连后端时可设为完整地址,例如: # VITE_API_BASE_URL=http://127.0.0.1:8001/api/v1 + +# Tauri Rust 请求后端基址(拉取 appConfig、脚本等) +# AICLIENT_API_BASE=http://127.0.0.1:8001 + +# 应用配置在 pythonbackend 的 MySQL 表 desktop_configs(name/value), +# 登录后自动同步;Node 流水线通过环境变量 AICLIENT_CFG_ 读取。 +# 常用键示例:LLM_API_KEY、LLM_API_URL、LLM_MODEL_ID、ASR_MODE、DASHSCOPE_API_KEY、OSS_CONFIG(JSON) diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs new file mode 100644 index 0000000..206cbd2 --- /dev/null +++ b/src-tauri/src/app_config.rs @@ -0,0 +1,180 @@ +//! 全局应用配置:登录后从 `GET /api/v1/appConfig` 拉取 `desktop_configs` 键值表, +//! 原样缓存;启动 Node 时写入环境变量 `AICLIENT_CFG_<规范化键名>`。 + +use std::collections::HashMap; +use std::sync::Arc; + +use serde_json::Value; +use thiserror::Error; +use tokio::sync::RwLock; + +/// Node 子进程环境变量前缀,与 `scripts/nodejs/desktop_config.js` 一致。 +pub const NODE_ENV_PREFIX: &str = "AICLIENT_CFG_"; + +#[derive(Debug, Error)] +pub enum AppConfigError { + #[error("拉取配置失败: {0}")] + Fetch(String), +} + +#[derive(Debug, Default)] +struct Inner { + entries: HashMap, + last_message: String, +} + +#[derive(Debug, Default)] +pub struct AppConfig { + inner: Arc>, +} + +pub fn api_base() -> String { + std::env::var("AICLIENT_API_BASE").unwrap_or_else(|_| "http://127.0.0.1:8001".to_string()) +} + +/// 将库表 `name` 转为 Node 环境变量名(不含前缀),如 `llm.api_key` → `LLM_API_KEY`。 +pub fn normalize_config_key(name: &str) -> String { + let mut out = String::new(); + let mut prev_underscore = false; + for c in name.trim().chars() { + if c.is_ascii_alphanumeric() { + out.push(c.to_ascii_uppercase()); + prev_underscore = false; + } else if !prev_underscore { + out.push('_'); + prev_underscore = true; + } + } + while out.starts_with('_') { + out.remove(0); + } + while out.ends_with('_') { + out.pop(); + } + out +} + +/// 完整环境变量名:`AICLIENT_CFG_LLM_API_KEY` +pub fn node_env_var_name(db_name: &str) -> String { + format!("{}{}", NODE_ENV_PREFIX, normalize_config_key(db_name)) +} + +impl AppConfig { + pub fn new() -> Self { + Self { + inner: Arc::new(RwLock::new(Inner::default())), + } + } + + pub async fn entries(&self) -> HashMap { + self.inner.read().await.entries.clone() + } + + pub async fn get(&self, name: &str) -> Option { + self.inner.read().await.entries.get(name).cloned() + } + + pub async fn last_message(&self) -> String { + self.inner.read().await.last_message.clone() + } + + pub async fn clear(&self) { + let mut g = self.inner.write().await; + g.entries.clear(); + g.last_message.clear(); + } + + /// 供 Node 子进程 `Command::env` 使用。 + pub async fn env_for_node(&self) -> HashMap { + let g = self.inner.read().await; + g.entries + .iter() + .map(|(name, value)| (node_env_var_name(name), value.clone())) + .collect() + } + + pub async fn refresh_after_auth(&self, token: &str) -> Result<(), AppConfigError> { + if token.trim().is_empty() { + self.clear().await; + return Ok(()); + } + match fetch_from_server(token).await { + Ok(map) => { + let mut g = self.inner.write().await; + g.entries = map; + g.last_message.clear(); + log::info!( + target: "app_config", + "应用配置已更新,共 {} 项", + g.entries.len() + ); + Ok(()) + } + Err(e) => { + let mut g = self.inner.write().await; + g.last_message = e.to_string(); + log::warn!(target: "app_config", "拉取应用配置失败: {}", e); + Err(e) + } + } + } +} + +async fn fetch_from_server(token: &str) -> Result, AppConfigError> { + let base = api_base().trim_end_matches('/').to_string(); + let url = format!("{base}/api/v1/appConfig"); + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + .map_err(|e| AppConfigError::Fetch(e.to_string()))?; + + let res = client + .get(&url) + .header("Authorization", format!("Bearer {}", token.trim())) + .send() + .await + .map_err(|e| AppConfigError::Fetch(e.to_string()))?; + + let status = res.status(); + let body = res + .text() + .await + .map_err(|e| AppConfigError::Fetch(e.to_string()))?; + + if status == reqwest::StatusCode::UNAUTHORIZED { + return Err(AppConfigError::Fetch("未认证或令牌已失效".into())); + } + + let v: Value = + serde_json::from_str(&body).map_err(|e| AppConfigError::Fetch(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(AppConfigError::Fetch(msg.to_string())); + } + + let data = v + .get("data") + .ok_or_else(|| AppConfigError::Fetch("响应缺少 data".into()))?; + + match data { + Value::Object(obj) => { + let mut map = HashMap::new(); + for (k, val) in obj { + let s = match val { + Value::String(s) => s.clone(), + Value::Null => String::new(), + other => other.to_string(), + }; + map.insert(k.clone(), s); + } + Ok(map) + } + _ => Err(AppConfigError::Fetch("data 应为 JSON 对象".into())), + } +} diff --git a/src-tauri/src/auth_session.rs b/src-tauri/src/auth_session.rs index c716f30..31c6c97 100644 --- a/src-tauri/src/auth_session.rs +++ b/src-tauri/src/auth_session.rs @@ -7,6 +7,7 @@ use std::sync::Mutex; pub struct AuthUser { pub id: i64, pub username: String, + pub role_id: i64, } #[derive(Debug, Clone, Default)] diff --git a/src-tauri/src/commands/app_config.rs b/src-tauri/src/commands/app_config.rs new file mode 100644 index 0000000..8e27fa6 --- /dev/null +++ b/src-tauri/src/commands/app_config.rs @@ -0,0 +1,19 @@ +use std::collections::HashMap; + +use tauri::State; + +use crate::app_config::AppConfig; + +#[tauri::command] +pub async fn get_app_config( + app_config: State<'_, AppConfig>, +) -> Result, String> { + Ok(app_config.entries().await) +} + +#[tauri::command] +pub async fn get_app_config_last_message( + app_config: State<'_, AppConfig>, +) -> Result { + Ok(app_config.last_message().await) +} diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index 0a5c577..1a48c58 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -1,12 +1,18 @@ use tauri::State; +use crate::app_config::AppConfig; use crate::auth_session::{AuthSession, AuthUser}; #[tauri::command] -pub fn sync_auth_session( +pub async fn sync_auth_session( session: State<'_, AuthSession>, + app_config: State<'_, AppConfig>, token: String, user: Option, -) { - session.set(token, user); +) -> Result<(), String> { + session.set(token.clone(), user); + if let Err(e) = app_config.refresh_after_auth(&token).await { + log::debug!(target: "auth", "refresh app config: {}", e); + } + Ok(()) } diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index cec1630..24d34c1 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub mod app_config; pub mod auth; pub mod nodejs; pub mod quickjs; diff --git a/src-tauri/src/commands/nodejs.rs b/src-tauri/src/commands/nodejs.rs index 896215a..c97e07a 100644 --- a/src-tauri/src/commands/nodejs.rs +++ b/src-tauri/src/commands/nodejs.rs @@ -26,6 +26,7 @@ use serde_json::Value; use tauri::{AppHandle, Emitter, Window}; use tokio::sync::mpsc; +use crate::app_config::AppConfig; use crate::js_runtime::PipelineEvent; use crate::nodejs; @@ -80,13 +81,15 @@ fn spawn_event_pump( pub async fn run_nodejs_script( app: AppHandle, window: Window, + app_config: tauri::State<'_, AppConfig>, script_name: String, params: Value, ) -> Result { let (tx, rx) = mpsc::unbounded_channel::(); let pump = spawn_event_pump(app, window, script_name.clone(), rx); + let config_env = app_config.env_for_node().await; - let result = nodejs::run_node_script(script_name, params, tx) + let result = nodejs::run_node_script(script_name, params, tx, config_env) .await .map_err(|e| e.to_string())?; @@ -99,13 +102,15 @@ pub async fn run_nodejs_script( pub async fn run_nodejs_script_source( app: AppHandle, window: Window, + app_config: tauri::State<'_, AppConfig>, script_source: String, params: Value, ) -> Result { let (tx, rx) = mpsc::unbounded_channel::(); let pump = spawn_event_pump(app, window, "".to_string(), rx); + let config_env = app_config.env_for_node().await; - let result = nodejs::run_node_script_source(script_source, params, tx) + let result = nodejs::run_node_script_source(script_source, params, tx, config_env) .await .map_err(|e| e.to_string())?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 82ea289..8328ca3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,4 @@ +pub mod app_config; pub mod asr; pub mod auth_session; pub mod chat; @@ -16,6 +17,7 @@ pub fn run() { tauri::Builder::default() .manage(auth_session::AuthSession::default()) + .manage(app_config::AppConfig::new()) .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { if let Some(window) = app.get_webview_window("main") { let _ = window.unminimize(); @@ -26,6 +28,8 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![ commands::auth::sync_auth_session, + commands::app_config::get_app_config, + commands::app_config::get_app_config_last_message, commands::quickjs::run_quickjs_script, commands::quickjs::run_quickjs_script_source, commands::quickjs::list_quickjs_scripts, diff --git a/src-tauri/src/nodejs.rs b/src-tauri/src/nodejs.rs index bda898a..30ac1e6 100644 --- a/src-tauri/src/nodejs.rs +++ b/src-tauri/src/nodejs.rs @@ -8,16 +8,21 @@ //! - 进度 / 日志由用户脚本通过 `globalThis.__native.emitProgress(s)` //! / `__native.log(level, msg, fields)` 写到 stderr,Rust 这边解析后 //! 转成 `PipelineEvent` 推给前端; -//! - 用户脚本的最终结果通过赋值 / `return` 的方式(约定函数名为 -//! `globalThis.__nodejsMain(params)`)返回,runner 把 JSON.stringify 后 -//! 的结果以 `__RESULT__` 这一行写到 stdout,Rust 抓回来反序列化。 +//! - 用户脚本通过 `require('./xxx.js')` 引用同目录模块;Rust 在启动前 +//! 按 require 图从服务端递归拉取并写入临时 bundle,Node 原生 CommonJS +//! `require` 加载(比 ESM `import` 更简单,与 `.cjs` runner 一致); +//! - 用户脚本的最终结果通过 `globalThis.__nodejsMain(params)` 返回,runner +//! 把 JSON.stringify 后的结果以 `__RESULT__` 写到 stdout。 //! //! 这套协议有意保持极小:跟 QuickJS 的 `globalThis.__quickjsMain` / `__native` //! 看起来完全对称,方便用户在两种运行时之间迁移脚本。 -use std::path::PathBuf; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; use std::process::Stdio; +use once_cell::sync::Lazy; +use regex::Regex; use serde_json::Value; use thiserror::Error; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; @@ -27,6 +32,15 @@ use uuid::Uuid; use crate::js_runtime::PipelineEvent; +/// 与后端 `nodejs_scripts._SAFE_NAME` 一致:仅允许扁平 `.js` 文件名。 +static RE_SAFE_SCRIPT: Lazy = + Lazy::new(|| Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9._-]*\.js$").unwrap()); + +static RE_REQUIRE: Lazy = + Lazy::new(|| Regex::new(r#"require\s*\(\s*['"]([^'"]+\.js)['"]\s*\)"#).unwrap()); + +const INLINE_ENTRY: &str = "__inline_entry__.js"; + #[derive(Debug, Error)] pub enum NodeError { #[error("node.exe 未找到(请检查 resources/nodejs/ 是否存在)")] @@ -76,91 +90,168 @@ pub fn locate_node() -> Option { Some(PathBuf::from(exe_name())) } -/// 用于设置 `cwd`:让用户脚本里的 `require('xxx')` 能解析到 -/// `resources/nodejs/node_modules/` 内置依赖。 +/// 用于 `NODE_PATH`:让用户脚本里的 `require('puppeteer')` 等解析到 +/// `resources/nodejs/node_modules/`(需在该目录执行 `npm install`)。 pub fn nodejs_dir() -> Option { locate_node().and_then(|p| p.parent().map(|x| x.to_path_buf())) } // --------------------------------------------------------------------------- -// Runner 模板 -// -// HEADER 安装 __native 桥,开始 IIFE 异步函数; -// 用户脚本原样夹在中间(通常用户脚本里会赋值 globalThis.__nodejsMain); -// FOOTER 调用 __nodejsMain(params)、捕获异常、写 __RESULT__。 +// Runner 模板(文件模式:入口脚本与 require 依赖均落盘,Node 原生 require) // --------------------------------------------------------------------------- -const RUNNER_HEADER: &str = r#" +fn build_file_runner(entry_script: &str) -> String { + format!( + r#" // ---- aiclient auto-generated nodejs runner (do not edit) ---- "use strict"; -(function () { - function emit(obj) { - try { +(function () {{ + function emit(obj) {{ + try {{ process.stderr.write("__EVT__" + JSON.stringify(obj) + "\n"); - } catch (_) {} - } - globalThis.__native = { - emitProgress(status) { - emit({ type: "progress", status: String(status) }); - }, - log(level, msg, fields) { - emit({ + }} catch (_) {{}} + }} + globalThis.__native = {{ + emitProgress(status) {{ + emit({{ type: "progress", status: String(status) }}); + }}, + log(level, msg, fields) {{ + emit({{ type: "log", level: String(level || "info"), msg: String(msg), fields: fields == null ? null : fields, - }); - }, - }; -})(); + }}); + }}, + }}; +}})(); -(async () => { - // 1) read params JSON from stdin (one-shot) +(async () => {{ let raw = ""; process.stdin.setEncoding("utf8"); for await (const chunk of process.stdin) raw += chunk; const params = raw ? JSON.parse(raw) : null; - // 2) inline user script begins ↓↓↓ -"#; + require("./{entry_script}"); -const RUNNER_FOOTER: &str = r#" - // ↑↑↑ inline user script ends - - // 3) dispatch to __nodejsMain(params) - if (typeof globalThis.__nodejsMain !== "function") { + if (typeof globalThis.__nodejsMain !== "function") {{ process.stderr.write( - "__EVT__" + JSON.stringify({ + "__EVT__" + JSON.stringify({{ type: "log", level: "error", msg: "脚本未注册 globalThis.__nodejsMain", - }) + "\n" + }}) + "\n" ); process.exit(1); - } - try { + }} + try {{ const result = await globalThis.__nodejsMain(params); process.stdout.write( "__RESULT__" + JSON.stringify(result === undefined ? null : result) + "\n" ); process.exit(0); - } catch (e) { + }} catch (e) {{ const msg = (e && (e.stack || e.message)) || String(e); process.stderr.write( - "__EVT__" + JSON.stringify({ type: "log", level: "error", msg }) + "\n" + "__EVT__" + JSON.stringify({{ type: "log", level: "error", msg }}) + "\n" ); process.exit(2); - } -})(); -"#; + }} +}})(); +"# + ) +} -fn build_runner(user_source: &str) -> String { - let mut s = String::with_capacity( - RUNNER_HEADER.len() + user_source.len() + RUNNER_FOOTER.len(), - ); - s.push_str(RUNNER_HEADER); - s.push_str(user_source); - s.push_str(RUNNER_FOOTER); - s +fn normalize_script_name(name: &str) -> Option { + let base = Path::new(name.trim()) + .file_name() + .and_then(|s| s.to_str())?; + if RE_SAFE_SCRIPT.is_match(base) { + Some(base.to_string()) + } else { + None + } +} + +fn extract_require_deps(source: &str) -> Vec { + RE_REQUIRE + .captures_iter(source) + .filter_map(|c| c.get(1).map(|m| m.as_str().to_string())) + .collect() +} + +/// 从入口脚本名出发,按 `require('*.js')` 递归拉取服务端模块。 +async fn resolve_script_bundle(entry: &str) -> Result, NodeError> { + let entry = normalize_script_name(entry) + .ok_or_else(|| NodeError::FetchScript(format!("无效的入口脚本名: {entry}")))?; + + let mut modules: HashMap = HashMap::new(); + let mut stack = vec![entry]; + + while let Some(name) = stack.pop() { + if modules.contains_key(&name) { + continue; + } + let source = fetch_node_script_source(&name).await?; + for dep in extract_require_deps(&source) { + if let Some(dep_name) = normalize_script_name(&dep) { + if !modules.contains_key(&dep_name) { + stack.push(dep_name); + } + } + } + modules.insert(name, source); + } + + Ok(modules) +} + +/// 调试源码:仅拉取其中 require 到的服务端模块,入口为内存写入的 `__inline_entry__.js`。 +async fn resolve_script_bundle_for_inline( + inline_source: &str, +) -> Result, NodeError> { + let mut modules: HashMap = HashMap::new(); + let mut stack: Vec = extract_require_deps(inline_source) + .into_iter() + .filter_map(|d| normalize_script_name(&d)) + .collect(); + + while let Some(name) = stack.pop() { + if modules.contains_key(&name) { + continue; + } + let source = fetch_node_script_source(&name).await?; + for dep in extract_require_deps(&source) { + if let Some(dep_name) = normalize_script_name(&dep) { + if !modules.contains_key(&dep_name) { + stack.push(dep_name); + } + } + } + modules.insert(name, source); + } + + modules.insert(INLINE_ENTRY.to_string(), inline_source.to_string()); + Ok(modules) +} + +async fn write_script_bundle( + modules: &HashMap, + entry: &str, +) -> Result<(PathBuf, PathBuf), NodeError> { + let tmp_dir = std::env::temp_dir().join("aiclient-node"); + tokio::fs::create_dir_all(&tmp_dir).await?; + let bundle_dir = tmp_dir.join(format!("bundle-{}", Uuid::new_v4())); + tokio::fs::create_dir_all(&bundle_dir).await?; + + for (name, source) in modules { + tokio::fs::write(bundle_dir.join(name), source).await?; + } + + let runner_path = bundle_dir.join("_runner.cjs"); + let runner_src = build_file_runner(entry); + tokio::fs::write(&runner_path, runner_src).await?; + + Ok((bundle_dir, runner_path)) } // --------------------------------------------------------------------------- @@ -247,46 +338,59 @@ pub async fn list_scripts() -> Result, NodeError> { // 公开入口 // --------------------------------------------------------------------------- -/// 按脚本名运行:去后端拉源码 → 包装 runner → 启动 node → 收集结果。 +/// 按脚本名运行:去后端拉入口及 require 依赖 → 落盘 → 启动 node → 收集结果。 pub async fn run_node_script( script_name: String, params: Value, events: UnboundedSender, + config_env: HashMap, ) -> Result { - let user_source = fetch_node_script_source(&script_name).await?; - run_inline_source(user_source, params, events).await + let entry = normalize_script_name(&script_name) + .ok_or_else(|| NodeError::FetchScript(format!("无效的脚本名称: {script_name}")))?; + let modules = resolve_script_bundle(&entry).await?; + run_script_bundle(&entry, modules, params, events, config_env).await } -/// 调试入口:直接传源码字符串,跳过后端拉取。 +/// 调试入口:直接传源码;其中 `require('*.js')` 仍从服务端拉取依赖。 pub async fn run_node_script_source( source: String, params: Value, events: UnboundedSender, + config_env: HashMap, ) -> Result { - run_inline_source(source, params, events).await + let modules = resolve_script_bundle_for_inline(&source).await?; + run_script_bundle(INLINE_ENTRY, modules, params, events, config_env).await } -async fn run_inline_source( - user_source: String, +async fn run_script_bundle( + entry: &str, + modules: HashMap, params: Value, events: UnboundedSender, + config_env: HashMap, ) -> Result { let node = locate_node().ok_or(NodeError::NotFound)?; - let cwd = nodejs_dir().unwrap_or_else(std::env::temp_dir); + let node_modules_cwd = nodejs_dir().unwrap_or_else(std::env::temp_dir); - // 写到 OS 临时目录而非 resources/,避免污染只读资源 - let tmp_dir = std::env::temp_dir().join("aiclient-node"); - tokio::fs::create_dir_all(&tmp_dir).await?; - let script_path = tmp_dir.join(format!("script-{}.cjs", Uuid::new_v4())); - tokio::fs::write(&script_path, build_runner(&user_source)).await?; + let (bundle_dir, script_path) = write_script_bundle(&modules, entry).await?; - let mut child = Command::new(&node) - .arg(&script_path) - .current_dir(&cwd) + let mut cmd = Command::new(&node); + cmd.arg(&script_path) + .current_dir(&bundle_dir) + .env( + "NODE_PATH", + node_modules_cwd + .join("node_modules") + .to_string_lossy() + .to_string(), + ) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn()?; + .stderr(Stdio::piped()); + for (key, value) in config_env { + cmd.env(key, value); + } + let mut child = cmd.spawn()?; // 把 params JSON 灌进 stdin if let Some(mut stdin) = child.stdin.take() { @@ -377,7 +481,7 @@ async fn run_inline_source( let stderr_tail = stderr_task.await.unwrap_or_default(); let result_line = stdout_task.await.unwrap_or(None); - let _ = tokio::fs::remove_file(&script_path).await; + let _ = tokio::fs::remove_dir_all(&bundle_dir).await; if !status.success() { return Err(NodeError::NonZero { diff --git a/src/api/adminCardKeys.js b/src/api/adminCardKeys.js new file mode 100644 index 0000000..b149a7a --- /dev/null +++ b/src/api/adminCardKeys.js @@ -0,0 +1,58 @@ +import { apiRequest } from "./client.js"; + +/** + * @param {string} token + * @param {RequestInit} [options] + */ +function adminRequest(token, path, options = {}) { + return apiRequest(path, { + ...options, + headers: { + Authorization: `Bearer ${token}`, + ...options.headers, + }, + }); +} + +/** + * @param {string} token + * @param {{ page?: number, pageSize?: number, status?: string }} [params] + */ +export function listAdminCardKeysApi(token, { page = 1, pageSize = 20, status = "all" } = {}) { + const query = new URLSearchParams({ + page: String(page), + page_size: String(pageSize), + status, + }); + return adminRequest(token, `/admin/card-keys?${query}`); +} + +/** + * @param {string} token + * @param {object} payload + */ +export function createAdminCardKeysApi(token, payload) { + return adminRequest(token, "/admin/card-keys", { + method: "POST", + body: JSON.stringify(payload), + }); +} + +/** + * @param {string} token + * @param {number} cardId + * @param {object} payload + */ +export function updateAdminCardKeyApi(token, cardId, payload) { + return adminRequest(token, `/admin/card-keys/${cardId}`, { + method: "PATCH", + body: JSON.stringify(payload), + }); +} + +/** @param {string} token @param {number} cardId */ +export function deleteAdminCardKeyApi(token, cardId) { + return adminRequest(token, `/admin/card-keys/${cardId}`, { + method: "DELETE", + }); +} diff --git a/src/api/adminDesktopConfigs.js b/src/api/adminDesktopConfigs.js new file mode 100644 index 0000000..9a11f02 --- /dev/null +++ b/src/api/adminDesktopConfigs.js @@ -0,0 +1,62 @@ +import { apiRequest } from "./client.js"; + +/** + * @param {string} token + * @param {RequestInit} [options] + */ +function adminRequest(token, path, options = {}) { + return apiRequest(path, { + ...options, + headers: { + Authorization: `Bearer ${token}`, + ...options.headers, + }, + }); +} + +/** + * @param {string} token + * @param {{ page?: number, pageSize?: number, keyword?: string }} [params] + */ +export function listAdminDesktopConfigsApi( + token, + { page = 1, pageSize = 20, keyword = "" } = {}, +) { + const query = new URLSearchParams({ + page: String(page), + page_size: String(pageSize), + }); + const kw = keyword?.trim(); + if (kw) query.set("keyword", kw); + return adminRequest(token, `/admin/desktop-configs?${query}`); +} + +/** + * @param {string} token + * @param {object} payload + */ +export function createAdminDesktopConfigApi(token, payload) { + return adminRequest(token, "/admin/desktop-configs", { + method: "POST", + body: JSON.stringify(payload), + }); +} + +/** + * @param {string} token + * @param {number} configId + * @param {object} payload + */ +export function updateAdminDesktopConfigApi(token, configId, payload) { + return adminRequest(token, `/admin/desktop-configs/${configId}`, { + method: "PATCH", + body: JSON.stringify(payload), + }); +} + +/** @param {string} token @param {number} configId */ +export function deleteAdminDesktopConfigApi(token, configId) { + return adminRequest(token, `/admin/desktop-configs/${configId}`, { + method: "DELETE", + }); +} diff --git a/src/api/adminUsers.js b/src/api/adminUsers.js new file mode 100644 index 0000000..e576969 --- /dev/null +++ b/src/api/adminUsers.js @@ -0,0 +1,57 @@ +import { apiRequest } from "./client.js"; + +/** + * @param {string} token + * @param {RequestInit} [options] + */ +function adminRequest(token, path, options = {}) { + return apiRequest(path, { + ...options, + headers: { + Authorization: `Bearer ${token}`, + ...options.headers, + }, + }); +} + +/** + * @param {string} token + * @param {{ page?: number, pageSize?: number }} [params] + */ +export function listAdminUsersApi(token, { page = 1, pageSize = 20 } = {}) { + const query = new URLSearchParams({ + page: String(page), + page_size: String(pageSize), + }); + return adminRequest(token, `/admin/users?${query}`); +} + +/** + * @param {string} token + * @param {object} payload + */ +export function createAdminUserApi(token, payload) { + return adminRequest(token, "/admin/users", { + method: "POST", + body: JSON.stringify(payload), + }); +} + +/** + * @param {string} token + * @param {number} userId + * @param {object} payload + */ +export function updateAdminUserApi(token, userId, payload) { + return adminRequest(token, `/admin/users/${userId}`, { + method: "PATCH", + body: JSON.stringify(payload), + }); +} + +/** @param {string} token @param {number} userId */ +export function deleteAdminUserApi(token, userId) { + return adminRequest(token, `/admin/users/${userId}`, { + method: "DELETE", + }); +} diff --git a/src/api/client.js b/src/api/client.js index f18108b..8afb1d3 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -53,6 +53,13 @@ export async function apiRequest(path, options = {}) { return { ok: false, message: "未登录或登录已过期" }; } + if (res.status === 403) { + return { + ok: false, + message: body?.detail || "无权限执行此操作", + }; + } + return { ok: false, message: body?.message || `请求失败(${res.status})`, diff --git a/src/components/AppSidebar.vue b/src/components/AppSidebar.vue index a69e79e..8864dee 100644 --- a/src/components/AppSidebar.vue +++ b/src/components/AppSidebar.vue @@ -1,8 +1,10 @@ diff --git a/src/components/workflow/VideoLinkDialog.vue b/src/components/workflow/VideoLinkDialog.vue index a4d691a..f4a6f2f 100644 --- a/src/components/workflow/VideoLinkDialog.vue +++ b/src/components/workflow/VideoLinkDialog.vue @@ -4,7 +4,12 @@ import { storeToRefs } from "pinia"; import { useWorkflowStore, executeVideoRewrite } from "../../stores/workflow.js"; const workflow = useWorkflowStore(); -const { videoLinkModalVisible, videoShareText, videoRewriting } = storeToRefs(workflow); +const { + videoLinkModalVisible, + videoShareText, + videoRewriting, + rewriteProgress, +} = storeToRefs(workflow); const feedback = ref({ severity: "", message: "" }); @@ -12,8 +17,7 @@ const tipItems = [ "已支持抖音分享文本、抖音视频链接、抖音短链接", "支持直接视频文件 URL(如 .mp4、.mov 等)", "快手、小红书、B站、视频号分享链接目前会提示暂不支持", - "使用 FUNASR 进行语音识别", - "根据设置的仿写提示词生成仿写文案", + "流程:解析链接 → 下载视频 → 提取音频 → 语音识别 → AI 仿写", ]; function onCancel() { @@ -26,6 +30,8 @@ async function onRewrite() { const result = await executeVideoRewrite(workflow); if (!result.ok) { feedback.value = { severity: "error", message: result.message }; + } else { + feedback.value = { severity: "success", message: result.message }; } } @@ -49,6 +55,15 @@ async function onRewrite() { {{ feedback.message }} + + {{ rewriteProgress }} + +