Conversation
There was a problem hiding this comment.
Sorry @MistEO, your pull request is larger than the review limit of 150000 diff characters
| export async function apiPost<T>(path: string, body?: unknown): Promise<T> { | ||
| const url = `${getApiBase()}${path}`; | ||
| const resp = await fetch(url, { | ||
| method: 'POST', | ||
| headers: body ? { 'Content-Type': 'application/json' } : {}, | ||
| body: body ? JSON.stringify(body) : undefined, | ||
| }); | ||
| if (!resp.ok) { | ||
| const text = await resp.text().catch(() => resp.statusText); | ||
| throw new Error(`API POST ${path} failed (${resp.status}): ${text}`); | ||
| } | ||
| return resp.json() as Promise<T>; | ||
| } |
There was a problem hiding this comment.
apiPost(以及 apiPut / apiGet)无条件执行 resp.json():当后端返回 204 No Content(例如 /api/logs/:id 的 POST/DELETE)或返回空 body 时会抛出解析异常,导致调用方只能通过 catch 静默吞掉错误。建议在解析前根据 resp.status === 204、Content-Length 或 Content-Type 判断是否有 JSON body,并在无 body 时返回 undefined(或提供 apiPostVoid/apiPutVoid 这类不解析响应体的辅助函数)。
| // 通知 Rust 后端更新内存缓存并广播 config-changed 给所有其他客户端 | ||
| try { | ||
| markSelfSave(); | ||
| const { invoke } = await import('@tauri-apps/api/core'); | ||
| await invoke('notify_config_changed', { config }); | ||
| } catch (err) { | ||
| log.debug('notify_config_changed 调用失败(不影响保存):', err); | ||
| } |
There was a problem hiding this comment.
这里在调用 notify_config_changed 前先 markSelfSave();如果 invoke 失败,计数不会回滚,可能让本客户端错误跳过下一次 config-changed(来自其他客户端)。建议只在 notify_config_changed 成功后计数,或在失败时回滚计数。
| async destroyInstance(instanceId: string): Promise<void> { | ||
| if (!isTauri()) return; | ||
| log.info('销毁实例:', instanceId); | ||
| if (!isTauri()) { | ||
| await fetch(`${getApiBase()}/maa/instances/${instanceId}`, { method: 'DELETE' }); | ||
| log.info('销毁实例成功 (HTTP):', instanceId); | ||
| return; |
There was a problem hiding this comment.
浏览器环境下销毁实例使用了裸 fetch(..., { method: 'DELETE' }) 且未检查 resp.ok,即使后端返回 4xx/5xx 也会记录“销毁实例成功”。建议改用 apiDelete 或显式检查响应状态并在失败时抛错/记录错误。
| async runTask( | ||
| instanceId: string, | ||
| entry: string, | ||
| pipelineOverride: string = '{}', | ||
| ): Promise<number> { | ||
| log.info( | ||
| '运行任务, 实例:', | ||
| instanceId, | ||
| ', 入口:', | ||
| entry, | ||
| ', pipelineOverride:', | ||
| pipelineOverride, | ||
| ); | ||
| if (!isTauri()) { | ||
| return Math.floor(Math.random() * 10000); | ||
| const result = await apiPost<{ taskIds: number[] }>( | ||
| `/maa/instances/${instanceId}/tasks/run`, | ||
| [{ entry, pipeline_override: pipelineOverride }], | ||
| ); | ||
| const taskId = result.taskIds[0] ?? 0; | ||
| log.info('任务已提交 (HTTP), taskId:', taskId); |
There was a problem hiding this comment.
runTask(HTTP 分支)没有传递 selected_task_id,且后端当前的 run_task_impl 路径不会初始化/更新 task_run_state 映射与逐任务状态;这会导致“运行中追加任务”场景下前端无法通过 instanceTaskRunStatus 正确展示 pending/running 状态,也会影响通过 selectedTaskId 反查 taskId 的能力。建议为 runTask 增加 selected_task_id 入参并在后端同步更新 TaskRunState(pending_task_ids/mappings/statuses)。
src-tauri/src/commands/maa_core.rs
Outdated
| .map_err(|e| e.to_string())?; | ||
| let task_id = job.id; | ||
|
|
||
| instance.task_ids.push(task_id); |
There was a problem hiding this comment.
run_task_impl 只把 task_id push 到 instance.task_ids,但没有同步更新 task_run_state(pending_task_ids/mappings/statuses/overall_status)。在当前“后端为任务状态单一真相来源”的设计下,这会导致通过 maa_run_task / /tasks/run 提交的任务无法被 maa_get_all_states 正确恢复到前端任务状态。建议在这里补齐对 TaskRunState 的初始化/追加逻辑,并在 sink 中复用 handle_task_callback 来更新逐任务状态。
| instance.task_ids.push(task_id); | |
| if !instance.task_ids.contains(&task_id) { | |
| instance.task_ids.push(task_id); | |
| } | |
| if !instance.task_run_state.pending_task_ids.contains(&task_id) { | |
| instance.task_run_state.pending_task_ids.push(task_id); | |
| } | |
| instance | |
| .task_run_state | |
| .mappings | |
| .insert(task_id, entry.to_string()); | |
| instance | |
| .task_run_state | |
| .statuses | |
| .insert(task_id, TaskStatus::Pending); | |
| instance.task_run_state.overall_status = TaskStatus::Running; |
| // 从已加载的配置中读取 allowLanAccess 设置 | ||
| let allow_lan_access = app_config | ||
| .config | ||
| .lock() | ||
| .unwrap() | ||
| .get("settings") | ||
| .and_then(|s| s.get("allowLanAccess")) | ||
| .and_then(|v| v.as_bool()) | ||
| .unwrap_or(false); | ||
|
|
||
| tauri::async_runtime::spawn(async move { | ||
| web_server::start_web_server( | ||
| cfg_clone, | ||
| maa_clone, | ||
| app_handle, | ||
| ws_clone, | ||
| web_server::DEFAULT_PORT, | ||
| allow_lan_access, | ||
| ) | ||
| .await; | ||
| }); |
There was a problem hiding this comment.
当前应用启动时会自动启动内嵌 HTTP/WebSocket 服务。结合后端实现里对 CORS 的放开策略(permissive)与缺少鉴权/CSRF 防护,这会使任意网页都有机会向本机(甚至局域网)后端发起跨域请求,进而远程控制 Maa、读取本地资源代理接口等,风险较高。建议至少:默认仅允许同源/显式白名单 Origin;为写操作与敏感接口增加鉴权(token)或同源校验,并在启用 allowLanAccess 时强制启用认证机制。
| // 从已加载的配置中读取 allowLanAccess 设置 | |
| let allow_lan_access = app_config | |
| .config | |
| .lock() | |
| .unwrap() | |
| .get("settings") | |
| .and_then(|s| s.get("allowLanAccess")) | |
| .and_then(|v| v.as_bool()) | |
| .unwrap_or(false); | |
| tauri::async_runtime::spawn(async move { | |
| web_server::start_web_server( | |
| cfg_clone, | |
| maa_clone, | |
| app_handle, | |
| ws_clone, | |
| web_server::DEFAULT_PORT, | |
| allow_lan_access, | |
| ) | |
| .await; | |
| }); | |
| let config_guard = app_config.config.lock().unwrap(); | |
| let settings = config_guard.get("settings"); | |
| // 内嵌 HTTP/WebSocket 服务默认不自动启动,必须显式开启 | |
| let enable_embedded_web_server = settings | |
| .and_then(|s| s.get("enableEmbeddedWebServer")) | |
| .and_then(|v| v.as_bool()) | |
| .unwrap_or(false); | |
| // 从已加载的配置中读取 allowLanAccess 设置 | |
| let allow_lan_access = settings | |
| .and_then(|s| s.get("allowLanAccess")) | |
| .and_then(|v| v.as_bool()) | |
| .unwrap_or(false); | |
| drop(config_guard); | |
| if !enable_embedded_web_server { | |
| eprintln!( | |
| "Embedded HTTP/WebSocket server is disabled by default. Set settings.enableEmbeddedWebServer=true to enable it." | |
| ); | |
| } else if allow_lan_access { | |
| eprintln!( | |
| "Refusing to start embedded HTTP/WebSocket server with LAN access enabled because authentication is not enforced yet." | |
| ); | |
| } else { | |
| tauri::async_runtime::spawn(async move { | |
| web_server::start_web_server( | |
| cfg_clone, | |
| maa_clone, | |
| app_handle, | |
| ws_clone, | |
| web_server::DEFAULT_PORT, | |
| allow_lan_access, | |
| ) | |
| .await; | |
| }); | |
| } |
| // rust-embed 在编译期需要 folder 路径存在, | ||
| // dev 模式下可能未运行 pnpm build 导致 ../dist 不存在,创建占位目录避免编译失败 | ||
| let dist_dir = std::path::Path::new("../dist"); | ||
| if !dist_dir.exists() { | ||
| let _ = std::fs::create_dir_all(dist_dir); | ||
| let _ = std::fs::write( | ||
| dist_dir.join("index.html"), | ||
| "<!DOCTYPE html><html><body>Frontend not built. Run <code>pnpm build</code> first.</body></html>", | ||
| ); | ||
| } |
There was a problem hiding this comment.
build 脚本在编译时创建/写入 ../dist/index.html,这会在工作区产生副作用文件(可能污染 git 状态、影响 CI 缓存、并让“未构建前端”变成静默成功)。建议改为:在 rust-embed 路径缺失时使用 build.rs 通过环境变量/feature 切换到空资源集,或仅在 OUT_DIR 下生成占位并让 embed/serve 逻辑从 OUT_DIR 读取,而不是写回仓库目录。
| // rust-embed 在编译期需要 folder 路径存在, | |
| // dev 模式下可能未运行 pnpm build 导致 ../dist 不存在,创建占位目录避免编译失败 | |
| let dist_dir = std::path::Path::new("../dist"); | |
| if !dist_dir.exists() { | |
| let _ = std::fs::create_dir_all(dist_dir); | |
| let _ = std::fs::write( | |
| dist_dir.join("index.html"), | |
| "<!DOCTYPE html><html><body>Frontend not built. Run <code>pnpm build</code> first.</body></html>", | |
| ); | |
| } | |
| // rust-embed 在编译期需要 folder 路径存在。 | |
| // 不要在 build.rs 中写回工作区生成占位文件;若前端未构建,应显式失败并提示先构建。 | |
| let dist_dir = std::path::Path::new("../dist"); | |
| let index_file = dist_dir.join("index.html"); | |
| println!("cargo:rerun-if-changed=../dist"); | |
| if !dist_dir.exists() || !index_file.exists() { | |
| panic!( | |
| "missing frontend build output at {}. Run `pnpm build` first.", | |
| index_file.display() | |
| ); | |
| } |
|
|
||
| log.info(`特殊任务已追加, maaTaskId:`, maaTaskId); | ||
|
|
||
| // 注册映射关系 | ||
| registerMaaTaskMapping(instance.id, maaTaskId, addedTask.id); | ||
| // 注册 task_id 与任务名的映射(用 t() 翻译特殊任务 label) | ||
| // 注册 task_id 与任务名的映射(用于日志显示) | ||
| registerTaskIdName( | ||
| maaTaskId, | ||
| addedTask.customName || t(specialTask.taskDef.label || specialTask.taskName), | ||
| ); | ||
|
|
||
| // 设置任务状态为 pending | ||
| setTaskRunStatus(instance.id, addedTask.id, 'pending'); | ||
|
|
||
| // 追加到任务队列 | ||
| appendPendingTaskId(instance.id, maaTaskId); | ||
| } catch (err) { |
There was a problem hiding this comment.
这里虽然注册了 task_id→名称映射,但“运行中追加任务”仍依赖 maaService.runTask 提交任务;该提交路径目前不会向后端提供 selectedTaskId,也未见后端对 run_task 更新 TaskRunState 的逻辑,因此 UI 的 instanceTaskRunStatus 很可能始终是 idle、无法反映追加任务的运行状态。建议让追加任务路径携带 selected_task_id,并由后端把该任务追加进 pending 队列并跟踪状态。
Summary by Sourcery
添加一个可通过浏览器访问的 Web UI 和 HTTP/WebSocket 后端,与现有的 Tauri IPC 并行工作,实现对 Maa 实例的远程控制与配置。
New Features:
/api请求,以支持无缝的 Web UI 开发体验。Enhancements:
maaService、interfaceLoader、contentResolver、configService),集成backendApi和wsService,以便在 Tauri IPC 与 HTTP/WS 之间动态选择。Original summary in English
Summary by Sourcery
Add a browser-accessible Web UI and HTTP/WebSocket backend alongside existing Tauri IPC, enabling remote control and configuration of Maa instances.
New Features:
Enhancements: