Conversation
There was a problem hiding this comment.
Hey - 我发现了两个问题,并给出了一些整体性反馈:
- 在
configService.saveConfig中,你在apiPut成功之前就调用了markSelfSave();如果 HTTP 调用失败,仍然会消耗下一次config-changed事件,即使该事件并不是由当前客户端触发的——建议只在保存成功后再递增挂起计数器。 - WebSocket 客户端类型
WsMessage包含state-changed变体以及wsService.onStateChanged这个 API,但目前服务端并没有任何代码发出WsEvent::StateChanged,因此这些订阅永远不会触发;请考虑要么把对应的广播打通,要么移除未使用的消息类型/处理函数。
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- 在 `configService.saveConfig` 中,你在 `apiPut` 成功之前就调用了 `markSelfSave()`;如果 HTTP 调用失败,仍然会消耗下一次 `config-changed` 事件,即使该事件并不是由当前客户端触发的——建议只在保存成功后再递增挂起计数器。
- WebSocket 客户端类型 `WsMessage` 包含 `state-changed` 变体以及 `wsService.onStateChanged` 这个 API,但目前服务端并没有任何代码发出 `WsEvent::StateChanged`,因此这些订阅永远不会触发;请考虑要么把对应的广播打通,要么移除未使用的消息类型/处理函数。
## Individual Comments
### Comment 1
<location path="src/services/configService.ts" line_range="77-86" />
<code_context>
}
const loadBackgroundImage = async () => {
try {
- const { readFile } = await import('@tauri-apps/plugin-fs');
- const fileData = await readFile(backgroundImage);
</code_context>
<issue_to_address>
**issue (bug_risk):** 在 API 调用成功之前标记 self-save 可能会导致 self-save 计数器失去同步。
由于 `markSelfSave()` 在 `apiPut` 之前运行,如果请求失败(例如后端宕机),会在没有任何对应 `ConfigChanged` 事件的情况下递增 `_pendingSelfSaves`。接下来来自其他客户端的、不相关的 `config-changed` 事件可能会被错误地视为本客户端触发而被忽略。请将 `markSelfSave()` 移动到只有在 `apiPut` 成功后才执行,或者在 `catch` 块中确保失败时会对 `_pendingSelfSaves` 进行递减。
</issue_to_address>
### Comment 2
<location path="src/utils/backendApi.ts" line_range="19-22" />
<code_context>
+}
+
+export function getApiBase(): string {
+ if (backendPort) {
+ return `http://${window.location.hostname}:${backendPort}/api`;
+ }
+ return '/api';
+}
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** 对后端直连强制使用 `http://` 可能会在应用通过 HTTPS 提供服务时导致问题。
当设置了 `backendPort` 后,这里始终构造的是 `http://hostname:port/api`,在前端通过 HTTPS 提供服务时,这会引发混合内容错误并导致请求被阻止。建议像 `wsService` 选择 `ws`/`wss` 那样,从 `window.location.protocol` 派生协议,或者提供一种方式覆盖协议设置。
Suggested implementation:
```typescript
/** 后端实际端口(0 = 未设置,走相对路径/Vite proxy) */
let backendPort = 0;
/** 设置后端直连端口(从 /api/interface 的 webServerPort 获取) */
export function setBackendPort(port: number): void {
backendPort = port;
}
export function getApiBase(): string {
if (backendPort) {
const protocol = window.location.protocol || 'http:';
// window.location.protocol already includes the trailing colon, e.g. "http:" or "https:"
return `${protocol}//${window.location.hostname}:${backendPort}/api`;
}
```
```typescript
if (backendPort) {
const protocol = window.location.protocol || 'http:';
return `${protocol}//${window.location.hostname}:${backendPort}/api`;
}
return '/api';
```
如果代码库中其他部分依赖 `getApiBase` 始终返回 `http://`(例如用于仅本地的工具),你可能还需要:
1. 添加一个可选的配置开关或由环境驱动的协议覆盖方式。
2. 更新任何断言旧 `http://` 行为的测试,以兼容 `window.location.protocol`。
</issue_to_address>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English
Hey - I've found 2 issues, and left some high level feedback:
- In
configService.saveConfigyou callmarkSelfSave()before theapiPutsucceeds; if the HTTP call fails this will still consume the nextconfig-changedevent even though it wasn’t triggered by this client—consider only incrementing the pending counter after a successful save. - The WebSocket client type
WsMessageincludes astate-changedvariant andwsService.onStateChangedAPI, but there is currently no server-side code emittingWsEvent::StateChanged, so these subscriptions will never fire; either wire up the corresponding broadcasts or remove the unused message type/handler.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `configService.saveConfig` you call `markSelfSave()` before the `apiPut` succeeds; if the HTTP call fails this will still consume the next `config-changed` event even though it wasn’t triggered by this client—consider only incrementing the pending counter after a successful save.
- The WebSocket client type `WsMessage` includes a `state-changed` variant and `wsService.onStateChanged` API, but there is currently no server-side code emitting `WsEvent::StateChanged`, so these subscriptions will never fire; either wire up the corresponding broadcasts or remove the unused message type/handler.
## Individual Comments
### Comment 1
<location path="src/services/configService.ts" line_range="77-86" />
<code_context>
}
const loadBackgroundImage = async () => {
try {
- const { readFile } = await import('@tauri-apps/plugin-fs');
- const fileData = await readFile(backgroundImage);
</code_context>
<issue_to_address>
**issue (bug_risk):** Marking self-save before the API call succeeds can desync the self-save counter.
Because `markSelfSave()` runs before `apiPut`, a failed request (e.g. backend down) increments `_pendingSelfSaves` without any corresponding `ConfigChanged` event. The next unrelated `config-changed` from another client could then be misclassified as self-originated and ignored. Please either move `markSelfSave()` to run only after a successful `apiPut`, or ensure failures decrement `_pendingSelfSaves` in a `catch` block.
</issue_to_address>
### Comment 2
<location path="src/utils/backendApi.ts" line_range="19-22" />
<code_context>
+}
+
+export function getApiBase(): string {
+ if (backendPort) {
+ return `http://${window.location.hostname}:${backendPort}/api`;
+ }
+ return '/api';
+}
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Hardcoding `http://` for direct backend access may break when the app is served over HTTPS.
When `backendPort` is set, this always builds `http://hostname:port/api`, which will cause mixed-content errors and blocked requests when the frontend is served over HTTPS. Consider deriving the scheme from `window.location.protocol` (as with `wsService` choosing `ws`/`wss`), or exposing a way to override the protocol.
Suggested implementation:
```typescript
/** 后端实际端口(0 = 未设置,走相对路径/Vite proxy) */
let backendPort = 0;
/** 设置后端直连端口(从 /api/interface 的 webServerPort 获取) */
export function setBackendPort(port: number): void {
backendPort = port;
}
export function getApiBase(): string {
if (backendPort) {
const protocol = window.location.protocol || 'http:';
// window.location.protocol already includes the trailing colon, e.g. "http:" or "https:"
return `${protocol}//${window.location.hostname}:${backendPort}/api`;
}
```
```typescript
if (backendPort) {
const protocol = window.location.protocol || 'http:';
return `${protocol}//${window.location.hostname}:${backendPort}/api`;
}
return '/api';
```
If other parts of the codebase rely on `getApiBase` always being `http://` (for example, for local-only tooling), you may also want to:
1. Add an optional configuration flag or environment-driven override for the protocol.
2. Update any tests that assert the old `http://` behavior to account for `window.location.protocol`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| try { | ||
| const config = await apiGet<MxuConfig>('/config'); | ||
| if (config && config.version) { | ||
| log.info('配置加载成功(后端 HTTP API)'); | ||
| return config; | ||
| } | ||
| } catch { | ||
| // API 不可用,继续尝试静态文件 | ||
| } | ||
|
|
There was a problem hiding this comment.
issue (bug_risk): 在 API 调用成功之前标记 self-save 可能会导致 self-save 计数器失去同步。
由于 markSelfSave() 在 apiPut 之前运行,如果请求失败(例如后端宕机),会在没有任何对应 ConfigChanged 事件的情况下递增 _pendingSelfSaves。接下来来自其他客户端的、不相关的 config-changed 事件可能会被错误地视为本客户端触发而被忽略。请将 markSelfSave() 移动到只有在 apiPut 成功后才执行,或者在 catch 块中确保失败时会对 _pendingSelfSaves 进行递减。
Original comment in English
issue (bug_risk): Marking self-save before the API call succeeds can desync the self-save counter.
Because markSelfSave() runs before apiPut, a failed request (e.g. backend down) increments _pendingSelfSaves without any corresponding ConfigChanged event. The next unrelated config-changed from another client could then be misclassified as self-originated and ignored. Please either move markSelfSave() to run only after a successful apiPut, or ensure failures decrement _pendingSelfSaves in a catch block.
| if (backendPort) { | ||
| return `http://${window.location.hostname}:${backendPort}/api`; | ||
| } | ||
| return '/api'; |
There was a problem hiding this comment.
suggestion (bug_risk): 对后端直连强制使用 http:// 可能会在应用通过 HTTPS 提供服务时导致问题。
当设置了 backendPort 后,这里始终构造的是 http://hostname:port/api,在前端通过 HTTPS 提供服务时,这会引发混合内容错误并导致请求被阻止。建议像 wsService 选择 ws/wss 那样,从 window.location.protocol 派生协议,或者提供一种方式覆盖协议设置。
Suggested implementation:
/** 后端实际端口(0 = 未设置,走相对路径/Vite proxy) */
let backendPort = 0;
/** 设置后端直连端口(从 /api/interface 的 webServerPort 获取) */
export function setBackendPort(port: number): void {
backendPort = port;
}
export function getApiBase(): string {
if (backendPort) {
const protocol = window.location.protocol || 'http:';
// window.location.protocol already includes the trailing colon, e.g. "http:" or "https:"
return `${protocol}//${window.location.hostname}:${backendPort}/api`;
} if (backendPort) {
const protocol = window.location.protocol || 'http:';
return `${protocol}//${window.location.hostname}:${backendPort}/api`;
}
return '/api';如果代码库中其他部分依赖 getApiBase 始终返回 http://(例如用于仅本地的工具),你可能还需要:
- 添加一个可选的配置开关或由环境驱动的协议覆盖方式。
- 更新任何断言旧
http://行为的测试,以兼容window.location.protocol。
Original comment in English
suggestion (bug_risk): Hardcoding http:// for direct backend access may break when the app is served over HTTPS.
When backendPort is set, this always builds http://hostname:port/api, which will cause mixed-content errors and blocked requests when the frontend is served over HTTPS. Consider deriving the scheme from window.location.protocol (as with wsService choosing ws/wss), or exposing a way to override the protocol.
Suggested implementation:
/** 后端实际端口(0 = 未设置,走相对路径/Vite proxy) */
let backendPort = 0;
/** 设置后端直连端口(从 /api/interface 的 webServerPort 获取) */
export function setBackendPort(port: number): void {
backendPort = port;
}
export function getApiBase(): string {
if (backendPort) {
const protocol = window.location.protocol || 'http:';
// window.location.protocol already includes the trailing colon, e.g. "http:" or "https:"
return `${protocol}//${window.location.hostname}:${backendPort}/api`;
} if (backendPort) {
const protocol = window.location.protocol || 'http:';
return `${protocol}//${window.location.hostname}:${backendPort}/api`;
}
return '/api';If other parts of the codebase rely on getApiBase always being http:// (for example, for local-only tooling), you may also want to:
- Add an optional configuration flag or environment-driven override for the protocol.
- Update any tests that assert the old
http://behavior to account forwindow.location.protocol.
There was a problem hiding this comment.
Pull request overview
Adds a browser-accessible Web UI mode alongside the existing Tauri WebView flow by introducing an axum-based HTTP/WebSocket server in the Tauri backend and adapting the frontend to use HTTP + WS when not running in Tauri.
Changes:
- Introduce Tauri-side axum web server (
/api+/api/ws) to serve the frontend and expose backend APIs to browsers. - Add browser-mode frontend plumbing (HTTP API helpers, WS client, non-Tauri code paths in services/logging, config syncing).
- Add “Allow LAN Access” setting to bind the dev server / backend server to
0.0.0.0and surface the Web server address in settings.
Reviewed changes
Copilot reviewed 33 out of 35 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| vite.config.ts | Read allowLanAccess for dev host binding and add /api + WS proxy to backend. |
| tsconfig.json | Bump TS lib target from ES2020 to ES2021. |
| src/utils/useMaaCallbackLogger.ts | Route agent output listening via Tauri events vs browser WS based on environment. |
| src/utils/backendApi.ts | New HTTP helper module for browser-mode backend API calls and port switching. |
| src/types/config.ts | Add allowLanAccess to persisted app settings type. |
| src/stores/types.ts | Add allowLanAccess state + setter to app store typing. |
| src/stores/appStore.ts | Persist allowLanAccess; add flushConfig / flushSaveConfig helpers for unload-time saving. |
| src/services/wsService.ts | New browser WS client with reconnect + subscription APIs. |
| src/services/maaService.ts | Add browser HTTP implementations for Maa APIs; use WS for callbacks in browser mode. |
| src/services/interfaceLoader.ts | Browser-mode interface loading via /api/interface with proxy-first and port probing fallback; inject backend port. |
| src/services/contentResolver.ts | Route browser file/icon access through backend /api/local-file proxy endpoints. |
| src/services/configService.ts | Load/save config via backend HTTP API in browser mode with localStorage fallback; self-save tracking. |
| src/i18n/locales/zh-TW.ts | Add i18n strings for LAN access + restart prompt + web server address. |
| src/i18n/locales/zh-CN.ts | Add i18n strings for LAN access + restart prompt + web server address. |
| src/i18n/locales/ko-KR.ts | Add i18n strings for LAN access + restart prompt + web server address. |
| src/i18n/locales/ja-JP.ts | Add i18n strings for LAN access + restart prompt + web server address. |
| src/i18n/locales/en-US.ts | Add i18n strings for LAN access + restart prompt + web server address. |
| src/components/TitleBar.tsx | Hide custom title bar in browser mode and on macOS/Linux. |
| src/components/settings/DebugSection.tsx | Add LAN access toggle + restart prompt; show/open web server address; fetch web server port/IP in Tauri. |
| src/components/ConnectionPanel.tsx | Skip auto-reconnect when restored backend state already indicates connected. |
| src/App.tsx | Add browser-mode WS connect, config-changed sync, unload-time config flush, browser favicon + background image loading, and dynamic shortcut import. |
| src-tauri/src/ws_broadcast.rs | New broadcast infra for pushing events to WS clients. |
| src-tauri/src/web_server.rs | New axum server serving /api routes + WS + static assets (embedded or external dist). |
| src-tauri/src/lib.rs | Initialize AppConfigState + WS broadcast and start web server at app startup; expose new system commands. |
| src-tauri/src/commands/utils.rs | Broadcast Maa callbacks to WS clients in addition to Tauri events. |
| src-tauri/src/commands/system.rs | Add commands to get web server port and LAN IP. |
| src-tauri/src/commands/mod.rs | Export new app_config module and AppConfigState. |
| src-tauri/src/commands/maa_core.rs | Refactor Maa operations into shared impl functions for both Tauri commands and HTTP handlers. |
| src-tauri/src/commands/maa_agent.rs | Broadcast agent output to WS clients in addition to Tauri events. |
| src-tauri/src/commands/file_ops.rs | Export resolve_local_file_path for web server local-file endpoint. |
| src-tauri/src/commands/app_config.rs | New in-memory cache for interface/config to back HTTP APIs; JSONC parsing + import merging. |
| src-tauri/Cargo.toml | Add axum/tower-http/rust-embed and expand tokio features for web server. |
| src-tauri/Cargo.lock | Lockfile updates for new Rust dependencies. |
| src-tauri/build.rs | Ensure ../dist exists for rust-embed at compile time (dev placeholder). |
| index.html | Update page title to MXU. |
| // 浏览器环境:用 sendBeacon 发送(即使页面关闭也能发出) | ||
| const config = flushConfig(); | ||
| if (!config) return; | ||
| const blob = new Blob([JSON.stringify(config)], { type: 'application/json' }); | ||
| navigator.sendBeacon?.(`${getApiBase()}/config`, blob); |
There was a problem hiding this comment.
beforeunload 里用 navigator.sendBeacon 发送到 ${getApiBase()}/config 会使用 POST 方法,但后端 /api/config 目前只注册了 GET/PUT(没有 POST)。这会导致浏览器刷新/关闭时的“强制保存”实际上不会落盘。建议改为后端同时支持 POST(与 sendBeacon 对齐),或在前端改用 fetch(url, { method: 'PUT', keepalive: true, ... }) 等可在卸载阶段完成的方案。
| // 浏览器环境:用 sendBeacon 发送(即使页面关闭也能发出) | |
| const config = flushConfig(); | |
| if (!config) return; | |
| const blob = new Blob([JSON.stringify(config)], { type: 'application/json' }); | |
| navigator.sendBeacon?.(`${getApiBase()}/config`, blob); | |
| // 浏览器环境:使用 keepalive fetch,并与后端 /config 的 PUT 方法保持一致 | |
| const config = flushConfig(); | |
| if (!config) return; | |
| const body = JSON.stringify(config); | |
| void fetch(`${getApiBase()}/config`, { | |
| method: 'PUT', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| }, | |
| body, | |
| keepalive: true, | |
| }).catch(() => { | |
| // beforeunload 阶段无法可靠恢复,忽略发送失败即可 | |
| }); |
| if (!isTauri()) { | ||
| // 浏览器环境不支持保存文件,使用 localStorage 作为后备 | ||
| // 浏览器环境:优先通过后端 HTTP API 持久化(多端一致性) | ||
| try { | ||
| markSelfSave(); | ||
| await apiPut<{ ok: boolean }>('/config', config); | ||
| log.debug('配置已通过后端 API 保存'); | ||
| return true; | ||
| } catch { | ||
| // API 不可用,回退到 localStorage(离线/开发预览模式) | ||
| } |
There was a problem hiding this comment.
浏览器环境保存配置时先 markSelfSave() 再 apiPut,如果 apiPut 失败(catch 分支走 localStorage 回退),_pendingSelfSaves 不会回滚,后续收到来自其他客户端的 config-changed 事件可能被错误跳过。建议只在 API 保存成功后再标记,或在失败时补偿 decrement。
| "resource_loaded": runtime.resource.as_ref().is_some_and(|r| r.loaded()), | ||
| "tasker_inited": runtime.tasker.as_ref().is_some_and(|t| t.inited()), | ||
| "is_running": is_running, | ||
| "task_ids": runtime.task_ids, |
There was a problem hiding this comment.
这里把 runtime.task_ids 直接放进 json! 会移动 Vec<i64> 的所有权(而 runtime 仍然在 instances 里),通常会导致编译错误或把 task_ids 清空/失效。应与 maa_get_all_states 保持一致,改为使用 runtime.task_ids.clone()。
| "task_ids": runtime.task_ids, | |
| "task_ids": runtime.task_ids.clone(), |
| // CORS:允许所有来源(浏览器需要跨域支持) | ||
| let app = app.layer(CorsLayer::permissive()); |
There was a problem hiding this comment.
CorsLayer::permissive() 会允许任意来源读写本地 API 响应(包含 /api/config、/api/local-file、/api/background-image 等),在启用 LAN 访问或用户访问恶意网页时可能造成配置泄露/任意文件读取(配合 PUT config 篡改路径)。建议至少将 CORS 限制为同源/白名单来源,或引入随机 token/鉴权头并在 CORS 中只放行该 header。
| // CORS:允许所有来源(浏览器需要跨域支持) | |
| let app = app.layer(CorsLayer::permissive()); | |
| // 默认不启用跨源访问:仅允许同源页面直接访问 API。 | |
| // 这样可避免恶意第三方网页通过浏览器跨域读取/写入本地接口。 | |
| let app = app; |
| // 规范化路径并拼接 basePath | ||
| resolved = normalizeFilePath(resolved); | ||
| const fullPath = joinPath(basePath, resolved); | ||
|
|
||
| try { | ||
| if (isTauri()) { | ||
| // Tauri 环境:读取文件并转换为 base64 data URL | ||
| const base64 = await readLocalFileBase64(fullPath); | ||
| const ext = resolved.split('.').pop()?.toLowerCase() || 'png'; | ||
| const mimeType = getMimeType(ext); | ||
| return `data:${mimeType};base64,${base64}`; | ||
| } else { | ||
| // 浏览器环境:直接返回 HTTP 路径 | ||
| return `/${fullPath}`; | ||
| // 浏览器环境:通过后端本地文件代理 API 访问 | ||
| return `${getApiBase()}/local-file?path=${encodeURIComponent(fullPath)}`; | ||
| } |
There was a problem hiding this comment.
loadIconAsDataUrl 在浏览器环境下返回 /api/local-file?path=... 时把 basePath 与 resolved 拼成了 fullPath。后端 local-file 端点看起来期望的是 相对于 exe 目录的相对路径(会在后端再 join exe_dir 并做路径穿越校验);传入绝对 basePath/... 很可能被拒绝,从而导致 favicon/图标加载失败。建议浏览器分支只传 resolved(相对路径),Tauri 分支再使用 basePath 拼接后的 fullPath。
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: