Skip to content

feat: web ui#155

Draft
MistEO wants to merge 13 commits intomainfrom
feat/web
Draft

feat: web ui#155
MistEO wants to merge 13 commits intomainfrom
feat/web

Conversation

@MistEO
Copy link
Copy Markdown
Owner

@MistEO MistEO commented Apr 6, 2026

Summary by Sourcery

添加一个可通过浏览器访问的 Web UI 和 HTTP/WebSocket 后端,与现有的 Tauri IPC 并行工作,实现对 Maa 实例的远程控制与配置。

New Features:

  • 通过内嵌的 axum HTTP 服务器向外暴露 Maa 核心操作,为浏览器客户端提供 JSON API 和 WebSocket 事件。
  • 支持 MXU 的完整浏览器模式功能(设备/窗口发现、实例管理、任务、截图、回调、agent 日志),使用 REST 和 WebSocket 替代 Tauri invoke。
  • 在设置中新增局域网访问开关,用于控制 Web UI 绑定到 localhost 还是 0.0.0.0,并在调试面板中展示实际生效的 Web 服务器地址。
  • 通过后端 API 持久化并同步配置(支持多客户端变更通知),并确保在页面卸载时刷新配置快照。
  • 从内嵌资源或外部 dist 目录提供构建后的 React 前端,并在 Vite 开发服务器中代理 /api 请求,以支持无缝的 Web UI 开发体验。

Enhancements:

  • 重构 Maa 核心 Rust 命令,使 Tauri 命令与 HTTP 处理器在控制器连接、任务运行/停止、设备/窗口发现、截图等内部实现上得以复用。
  • 统一回调和 agent 输出的投递方式,使 Tauri WebView 与浏览器客户端都能通过通用的广播层接收 Maa 事件。
  • 扩展前端服务(maaServiceinterfaceLoadercontentResolverconfigService),集成 backendApiwsService,以便在 Tauri IPC 与 HTTP/WS 之间动态选择。
  • 改进窗口/图标/背景的处理逻辑,确保在桌面和浏览器环境中都能正确加载资源与元数据,包括 favicon 更新和更安全的窗口定位。
  • 调整工具链与运行时配置(tokio 特性、开发服务器 host/proxy、JSON 库目标、构建脚本),以支持内嵌 Web 服务器和基于浏览器的 UI。
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:

  • Expose Maa core operations over an embedded axum HTTP server with JSON APIs and WebSocket events for browser clients.
  • Support full-featured browser mode for MXU (device/window discovery, instance management, tasks, screenshots, callbacks, agent logs) via REST and WebSocket instead of Tauri invoke.
  • Introduce a LAN access toggle in settings that controls whether the Web UI binds to localhost or 0.0.0.0, and surface the effective web server address in the debug panel.
  • Persist and synchronize configuration through the backend API (with multi-client change notifications) and ensure config snapshots are flushed on page unload.
  • Serve the built React frontend from embedded assets or an external dist directory, and proxy /api requests in Vite dev server for seamless Web UI development.

Enhancements:

  • Refactor Maa core Rust commands to share internal implementations between Tauri commands and HTTP handlers (controller connection, task run/stop, devices/windows discovery, screenshots).
  • Unify callback and agent output delivery so both Tauri WebView and browser clients receive Maa events via a common broadcast layer.
  • Extend frontend services (maaService, interfaceLoader, contentResolver, configService) with backendApi and wsService integration to dynamically choose between Tauri IPC and HTTP/WS.
  • Improve window/icon/background handling so assets and metadata are loaded correctly in both desktop and browser environments, including favicon updates and safer window positioning.
  • Adjust tooling and runtime configuration (tokio features, dev server host/proxy, JSON lib target, build script) to support the embedded web server and browser-based UI.

Copilot AI review requested due to automatic review settings April 6, 2026 12:52
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sourcery 对开源项目是免费的——如果你觉得我们的评审有帮助,请考虑分享给他人 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English

Hey - I've found 2 issues, and left some high level feedback:

  • 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.
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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +77 to +86
try {
const config = await apiGet<MxuConfig>('/config');
if (config && config.version) {
log.info('配置加载成功(后端 HTTP API)');
return config;
}
} catch {
// API 不可用,继续尝试静态文件
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +19 to +22
if (backendPort) {
return `http://${window.location.hostname}:${backendPort}/api`;
}
return '/api';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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://(例如用于仅本地的工具),你可能还需要:

  1. 添加一个可选的配置开关或由环境驱动的协议覆盖方式。
  2. 更新任何断言旧 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:

  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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.0 and 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.

Comment on lines +934 to +938
// 浏览器环境:用 sendBeacon 发送(即使页面关闭也能发出)
const config = flushConfig();
if (!config) return;
const blob = new Blob([JSON.stringify(config)], { type: 'application/json' });
navigator.sendBeacon?.(`${getApiBase()}/config`, blob);
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

beforeunload 里用 navigator.sendBeacon 发送到 ${getApiBase()}/config 会使用 POST 方法,但后端 /api/config 目前只注册了 GET/PUT(没有 POST)。这会导致浏览器刷新/关闭时的“强制保存”实际上不会落盘。建议改为后端同时支持 POST(与 sendBeacon 对齐),或在前端改用 fetch(url, { method: 'PUT', keepalive: true, ... }) 等可在卸载阶段完成的方案。

Suggested change
// 浏览器环境:用 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 阶段无法可靠恢复,忽略发送失败即可
});

Copilot uses AI. Check for mistakes.
Comment on lines 121 to +130
if (!isTauri()) {
// 浏览器环境不支持保存文件,使用 localStorage 作为后备
// 浏览器环境:优先通过后端 HTTP API 持久化(多端一致性)
try {
markSelfSave();
await apiPut<{ ok: boolean }>('/config', config);
log.debug('配置已通过后端 API 保存');
return true;
} catch {
// API 不可用,回退到 localStorage(离线/开发预览模式)
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

浏览器环境保存配置时先 markSelfSave()apiPut,如果 apiPut 失败(catch 分支走 localStorage 回退),_pendingSelfSaves 不会回滚,后续收到来自其他客户端的 config-changed 事件可能被错误跳过。建议只在 API 保存成功后再标记,或在失败时补偿 decrement。

Copilot uses AI. Check for mistakes.
"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,
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里把 runtime.task_ids 直接放进 json! 会移动 Vec<i64> 的所有权(而 runtime 仍然在 instances 里),通常会导致编译错误或把 task_ids 清空/失效。应与 maa_get_all_states 保持一致,改为使用 runtime.task_ids.clone()

Suggested change
"task_ids": runtime.task_ids,
"task_ids": runtime.task_ids.clone(),

Copilot uses AI. Check for mistakes.
Comment on lines +197 to +198
// CORS:允许所有来源(浏览器需要跨域支持)
let app = app.layer(CorsLayer::permissive());
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CorsLayer::permissive() 会允许任意来源读写本地 API 响应(包含 /api/config/api/local-file/api/background-image 等),在启用 LAN 访问或用户访问恶意网页时可能造成配置泄露/任意文件读取(配合 PUT config 篡改路径)。建议至少将 CORS 限制为同源/白名单来源,或引入随机 token/鉴权头并在 CORS 中只放行该 header。

Suggested change
// CORS:允许所有来源(浏览器需要跨域支持)
let app = app.layer(CorsLayer::permissive());
// 默认不启用跨源访问:仅允许同源页面直接访问 API。
// 这样可避免恶意第三方网页通过浏览器跨域读取/写入本地接口。
let app = app;

Copilot uses AI. Check for mistakes.
Comment on lines 386 to 400
// 规范化路径并拼接 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)}`;
}
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadIconAsDataUrl 在浏览器环境下返回 /api/local-file?path=... 时把 basePathresolved 拼成了 fullPath。后端 local-file 端点看起来期望的是 相对于 exe 目录的相对路径(会在后端再 join exe_dir 并做路径穿越校验);传入绝对 basePath/... 很可能被拒绝,从而导致 favicon/图标加载失败。建议浏览器分支只传 resolved(相对路径),Tauri 分支再使用 basePath 拼接后的 fullPath

Copilot uses AI. Check for mistakes.
@MistEO MistEO marked this pull request as draft April 6, 2026 13:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants