Skip to content

可选后端,实现前后端分离#4899

Draft
LIghtJUNction wants to merge 14 commits intomasterfrom
feat/optional-backend
Draft

可选后端,实现前后端分离#4899
LIghtJUNction wants to merge 14 commits intomasterfrom
feat/optional-backend

Conversation

@LIghtJUNction
Copy link
Member

@LIghtJUNction LIghtJUNction commented Feb 5, 2026

这个PR完善了astrbot run子命令
#4892
原本仅支持指定端口启动
修改为支持指定端口启动,同时支持指定host
顺便把默认值改成了::,并处理了不同系统的差异
将监听的::的行为统一为了双栈监听
然后新增一个flag option --backend-only
astrbot run --backend-only可以仅启动后端,而不启动前端

进而,我发起了这个pr,让前端支持指定任意后端,而不仅仅是同源
为了实现这个功能,我新增了quart-cors依赖,允许跨站访问
给登录界面和设置界面新增了可配置项

简而言之,在保持当前的方便性的情况下,增加了灵活性
同时,服务器可以不下载额外的前端文件,而仅仅运行一个后端,从而节省了服务器资源

Summary by Sourcery

使 Web 仪表盘的主机/端口和 API 基础 URL 完全可配置,以支持前后端分离部署,同时改进跨平台的默认网络行为和健壮性。

New Features:

  • 允许通过 CLI 参数、环境变量和配置文件来配置仪表盘主机、端口和启用状态,包括仅后端模式(不提供 Web UI 服务)。
  • 通过 CORS 和可配置的 API 基础 URL,使 Web 仪表盘和 API 能够从任意前端来源访问,并通过 config.json 以及登录页和设置页中的 UI 控件对其进行暴露与配置。
  • 在前端中添加对不同后端预设的选择与持久化支持,并基于配置的 API 基础 URL 动态路由 REST、WebSocket 和 SSE 调用。

Bug Fixes:

  • 当 Discord 斜杠命令上下文缺少频道时,通过回退使用 guild/channel ID 来推导会话信息,防止崩溃。
  • 通过规范化基于列表的 type 字段并避免修改原始 schema,提高与 Gemini/MCP JSON Schema 的兼容性。
  • 强化百度内容安全、定时任务(cron)、知识库和平台会话处理的类型安全与默认值设置,以避免在边缘场景中的运行时错误。
  • 确保 FishAudio TTS 具有安全的默认模型字符串,并将多个平台的 webhook 主机标准化为支持 IPv6 的默认值。

Enhancements:

  • 重构 AstrBot 仪表盘服务器初始化逻辑,拆分职责(路径、应用、路由、插件),缓存插件路由,并改进 JWT 处理和未授权响应。
  • 改进仪表盘服务器的端口检查、进程检查以及多栈绑定(IPv4/IPv6),并增强启动日志,提供更清晰的访问 URL。
  • 更新前端的 Live Mode WebSocket 和控制台日志流,使其遵循配置的 API 基础 URL,而不是硬编码的 localhost 端点。
  • 整理 TypeScript/Vue 配置和主题工具,包括 tsconfig 的 lib 目标设置以及 Vuetify 主题颜色应用。

Build:

  • 添加 quart-cors 依赖,以支持对基于 Quart 的仪表盘 API 的跨域访问。
  • 调整 Vite 开发服务器配置,使其监听 :: 并将 API 调用代理到后端,从而使本地开发与新的主机默认值保持一致。

Deployment:

  • 为 AstrBot 引入系统服务定义,以便通过系统管理工具(例如 systemd)进行托管部署。

Documentation:

  • 更新 README 中的品牌文案,并添加配套的动图以更好地展示项目。

Chores:

  • 添加仪表盘 lockfile 和公共配置 stub,以记录前端依赖状态和运行时配置默认值。
Original summary in English

Summary by Sourcery

Make the web dashboard host/port and API base URL fully configurable to support separated front-end/back-end deployments, while improving default networking behavior and robustness across platforms.

New Features:

  • Allow configuring the dashboard host, port, and enablement via CLI flags, environment variables, and config, including a backend-only mode without serving the Web UI.
  • Enable the web dashboard and APIs to be accessed from arbitrary front-end origins using CORS and a configurable API base URL exposed via config.json and UI controls on the login and settings pages.
  • Add support in the front-end for selecting and persisting different backend presets, and for dynamically routing REST, WebSocket, and SSE calls through the configured API base URL.

Bug Fixes:

  • Prevent crashes when Discord slash-command contexts lack a channel by falling back to guild/channel IDs to derive session information.
  • Improve compatibility with Gemini/MCP JSON Schema by normalizing list-based type fields and avoiding mutation of the original schema.
  • Harden Baidu content safety, cron, knowledge base, and platform session handling with safer typing and defaults to avoid runtime errors in edge cases.
  • Ensure FishAudio TTS has a safe default model string and standardize various platform webhook hosts on IPv6-friendly defaults.

Enhancements:

  • Refactor the AstrBot dashboard server initialization to separate concerns (paths, app, routes, plugins), cache plugin routes, and improve JWT handling and unauthorized responses.
  • Improve port checking, process inspection, and multi-stack binding (IPv4/IPv6) for the dashboard server, and enhance startup logging with clearer access URLs.
  • Update Live Mode WebSocket and console log streaming in the front-end to respect the configured API base URL instead of hardcoded localhost endpoints.
  • Tidy up TypeScript/Vue configuration and theming utilities, including tsconfig lib targets and Vuetify theme color application.

Build:

  • Add quart-cors as a dependency to support cross-origin access to the Quart-based dashboard APIs.
  • Adjust Vite dev server configuration to listen on :: and proxy API calls to the backend, aligning local development with the new host defaults.

Deployment:

  • Introduce a system service definition for AstrBot to facilitate managed deployment (e.g., via systemd).

Documentation:

  • Refresh README branding text and add an accompanying animated image to better present the project.

Chores:

  • Add dashboard lockfile and public config stub to capture front-end dependency state and runtime configuration defaults.

LIghtJUNction and others added 2 commits February 6, 2026 02:38
* 默认host修改为::,同时新增两个环境变量DASHBOARD_HOST,DASHBOARD_ENABLE,和DASHBOARD_PORT对齐

* feat: systemd support (#4880)

* fix: pyright lint (#4874)

* feat: 将 MessageSession 的 platform_id 改为 init=False,实例化时无需传入

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

* refactor: 将 isinstance 检查改为元组、将默认模型值设为空字符串、将类型注解改为 Any 并导入

* refactor: 为 _serialize_job 增加返回类型注解 dict

* fix: 使用 cast 获取百度 AIP 的 msg 并对 psutil_addr 引入 type: ignore

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

* refactor: 引入 _AddrWithPort 协议并替换 conn.laddr 的 cast

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

* fix: 在构建 AstrBotMessage 时对 ctx.channel 可能为 None 进行兜底处理

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

---------

Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>

* fix: TypeError when MCP schema type is a list (#4867)

* Fix TypeError when MCP schema type is a list

Fixes crash in Gemini native tools with VRChat MCP.

* Refactor: avoid modifying schema in place per feedback

* Fix formatting and cleanup comments

* docs: update watashiwakoseinodesukara

Removed duplicate text and added a new image.

* 修复/跨平台一致性

* 琐事/类型标注和一些简单错误修正

* 修复/检查端口时候包含ipv6

* 修复/enable变量的赋值逻辑

---------

Co-authored-by: Dt8333 <25431943+Dt8333@users.noreply.github.com>
Co-authored-by: aider (openai/gpt-5.2) <aider@aider.chat>
Co-authored-by: boushi1111 <95118141+boushi1111@users.noreply.github.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
Copilot AI review requested due to automatic review settings February 5, 2026 18:58
@auto-assign auto-assign bot requested review from Fridemn and Raven95676 February 5, 2026 18:58
@dosubot dosubot bot added the size:XXL This PR changes 1000+ lines, ignoring generated files. label Feb 5, 2026
@dosubot dosubot bot added area:core The bug / feature is about astrbot's core, backend area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. labels Feb 5, 2026
Copy link
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 - 我发现了 2 个安全问题、3 个其他问题,并补充了一些整体性的反馈:

安全问题

  • 检测到不安全的 WebSocket。所有 WebSocket 连接都应使用加密的 WebSocket(wss)。(link)
  • 检测到不安全的 WebSocket。所有 WebSocket 连接都应使用加密的 WebSocket(wss)。(link)

总体评论

  • get_local_ip_addresses 中,你在注释里提到要过滤掉 IPv6 链路本地地址(fe80::/10),但当前实现会追加所有 IPv6 地址;建议要么落实这个过滤逻辑,要么更新注释以避免混淆,并确保只返回可路由的地址。
  • AstrBotDashboard._init_app 中的 CORS 配置目前将 allow_origin="*", allow_methods="*", allow_headers="*" 写死;建议将这些值改为可配置(比如从 dashboard 配置中读取),或者至少在非开发环境中收紧这些设置,以避免不必要的广泛跨域暴露。
给 AI Agents 的提示
请根据这次代码评审中的评论进行修改:

## 整体评论
-`get_local_ip_addresses` 中,你在注释里提到要过滤掉 IPv6 链路本地地址(fe80::/10),但当前实现会追加所有 IPv6 地址;建议要么落实这个过滤逻辑,要么更新注释以避免混淆,并确保只返回可路由的地址。
-`AstrBotDashboard._init_app` 中的 CORS 配置目前将 `allow_origin="*", allow_methods="*", allow_headers="*"` 写死;建议将这些值改为可配置(比如从 dashboard 配置中读取),或者至少在非开发环境中收紧这些设置,以避免不必要的广泛跨域暴露。

## 单条评论

### Comment 1
<location> `dashboard/src/main.ts:93` </location>
<code_context>
-// Keep fetch() calls consistent with axios by automatically attaching the JWT.
-// Some parts of the UI use fetch directly; without this, those requests will 401.
-const _origFetch = window.fetch.bind(window);
-window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
-  const token = localStorage.getItem('token');
-  if (!token) return _origFetch(input, init);
</code_context>

<issue_to_address>
**suggestion (bug_risk):**`input``Request` 对象时,尤其是结合 URL 重写逻辑,当前自定义的 fetch 封装可能会出现意外行为。

由于你从 `input.headers` 合并了 `headers`,但在调用 `_origFetch(url, { ...init, headers })` 时仍然传入原始的 `init`,因此对于 `Request` 类型的输入,请求的属性(method、body、credentials 等)可能会相对原始请求丢失或不匹配。

建议先将输入统一规范化为一个 `Request` 对象,然后仅重写 URL 和 headers,例如:

```ts
window.fetch = (input, init) => {
  const currentBaseUrl = axios.defaults.baseURL;
  let url = typeof input === "string" || input instanceof URL ? input.toString() : input.url;

  if (typeof url === "string" && url.startsWith("/api") && currentBaseUrl) {
    const cleanBase = currentBaseUrl.replace(/\/+$/, "");
    const cleanPath = url.replace(/^\/+/, "");
    url = `${cleanBase}/${cleanPath}`;
  }

  const token = localStorage.getItem("token");
  const orig = input instanceof Request ? input : new Request(url, init);
  const headers = new Headers(orig.headers);
  if (token && !headers.has("Authorization")) {
    headers.set("Authorization", `Bearer ${token}`);
  }

  const request = new Request(url, { ...orig, headers });
  return _origFetch(request);
};
```

建议的实现:

```typescript
  // 优先使用 localStorage 中的配置,其次是 config.json,最后是空字符串
  const localApiUrl = localStorage.getItem("apiBaseUrl");
  const apiBaseUrl = localApiUrl !== null ? localApiUrl : configApiUrl;

  // Keep fetch() calls consistent with axios by automatically attaching the JWT
  // and normalizing Request handling so method/body/etc. are preserved.
  const _origFetch = window.fetch.bind(window);
  window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
    const currentBaseUrl = axios.defaults.baseURL;
    let url =
      typeof input === "string" || input instanceof URL
        ? input.toString()
        : input.url;

    if (typeof url === "string" && url.startsWith("/api") && currentBaseUrl) {
      const cleanBase = currentBaseUrl.replace(/\/+$/, "");
      const cleanPath = url.replace(/^\/+/, "");
      url = `${cleanBase}/${cleanPath}`;
    }

    const token = localStorage.getItem("token");
    const orig =
      input instanceof Request ? input : new Request(url, init);
    const headers = new Headers(orig.headers);

    if (token && !headers.has("Authorization")) {
      headers.set("Authorization", `Bearer ${token}`);
    }

    const request = new Request(url, { ...(orig as RequestInit), headers });
    return _origFetch(request);
  };

  if (apiBaseUrl) {
    console.log(

```

如果 `dashboard/src/main.ts` 中尚未导入 `axios`,请在文件顶部添加合适的导入(例如 `import axios from "axios";`),以便能够使用 `axios.defaults.baseURL`。同时,请确认该文件的编译环境启用了 DOM lib 类型,这样 `Request``RequestInfo``RequestInit` 才能被正确识别。
</issue_to_address>

### Comment 2
<location> `README.md:271` </location>
<code_context>
-陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。
+_私は、高性能ですから!_

+<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>

</code_context>

<issue_to_address>
**suggestion:** 建议为该图片添加 `alt` 属性以提升可访问性。

例如:`<img src="..." width="100" alt="对机器人动图的简短描述" />`,这样屏幕阅读器就能向用户传达动画的内容。
</issue_to_address>

### Comment 3
<location> `dashboard/src/main.ts:15` </location>
<code_context>
-import { loader } from '@guolao/vue-monaco-editor'
-import axios from 'axios';
-
-// 初始化新的i18n系统,等待完成后再挂载应用
-setupI18n().then(() => {
-  console.log('🌍 新i18n系统初始化完成');
</code_context>

<issue_to_address>
**issue (complexity):** 建议将应用启动和 HTTP 初始化中共享的逻辑提取到小的辅助函数中,以减少重复,让 `initApp` 更专注于整体编排。

在不改变行为的前提下,你可以通过抽取两个小的辅助函数来显著减少复杂度和重复:

### 1. 合并 `setupI18n` 不同分支中的应用启动逻辑

`then``catch` 两个分支都在做同样的应用创建 + 插件注册 + presets 绑定,可以将这部分逻辑集中起来,使 `setupI18n` 的控制流更简洁:

```ts
async function createAndMountApp(presets: any[]) {
  const app = createApp(App);
  app.use(router);

  const pinia = createPinia();
  app.use(pinia);

  const { useApiStore } = await import("@/stores/api");
  const apiStore = useApiStore(pinia);
  apiStore.setPresets(presets);

  app.use(print);
  app.use(VueApexCharts);
  app.use(vuetify);
  app.use(confirmPlugin);

  // Inline mountApp logic here to avoid another layer of indirection,
  // or call your existing mountApp(app, pinia)
  app.mount("#app");

  import("./stores/customizer").then(({ useCustomizerStore }) => {
    const customizer = useCustomizerStore(pinia);
    vuetify.theme.global.name.value = customizer.uiTheme;
    const storedPrimary = localStorage.getItem("themePrimary");
    const storedSecondary = localStorage.getItem("themeSecondary");
    if (storedPrimary || storedSecondary) {
      const themes = vuetify.theme.themes.value;
      ["PurpleTheme", "PurpleThemeDark"].forEach((name) => {
        const theme = themes[name];
        if (!theme?.colors) return;
        if (storedPrimary) theme.colors.primary = storedPrimary;
        if (storedSecondary) theme.colors.secondary = storedSecondary;
        if (storedPrimary && theme.colors.darkprimary)
          theme.colors.darkprimary = storedPrimary;
        if (storedSecondary && theme.colors.darksecondary)
          theme.colors.darksecondary = storedSecondary;
      });
    }
  });
}
```

然后 `initApp` 中与 i18n 相关的部分就可以简化为:

```ts
// 初始化新的i18n系统,等待完成后再挂载应用
setupI18n()
  .then(async () => {
    console.log("🌍 新i18n系统初始化完成");
    await createAndMountApp(presets);
  })
  .catch(async (error) => {
    console.error("❌ 新i18n系统初始化失败:", error);
    // 即使i18n初始化失败,也要挂载应用(使用回退机制)
    await createAndMountApp(presets);
  });
```

这样可以去掉重复的应用初始化代码,避免两个分支之间的行为漂移。

### 2. 将 HTTP 初始化(axios + fetch)从 `initApp` 中剥离出来

axios 以及覆盖 `window.fetch` 的逻辑都比较密集,且与应用启动本身关系不大。将它们放到一个专门的辅助函数里,可以让 `initApp` 更易读:

```ts
function setupHttp(apiBaseUrl: string) {
  axios.defaults.baseURL = apiBaseUrl;

  axios.interceptors.request.use((config) => {
    const token = localStorage.getItem("token");
    if (token) {
      config.headers["Authorization"] = `Bearer ${token}`;
    }
    return config;
  });

  const _origFetch = window.fetch.bind(window);
  window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
    let url = input;
    const currentBaseUrl = axios.defaults.baseURL;

    if (
      typeof input === "string" &&
      input.startsWith("/api") &&
      currentBaseUrl
    ) {
      const cleanBase = currentBaseUrl.replace(/\/+$/, "");
      const cleanPath = input.replace(/^\/+/, "");
      url = `${cleanBase}/${cleanPath}`;
    }

    const token = localStorage.getItem("token");
    if (!token) return _origFetch(url, init);

    const headers = new Headers(
      init?.headers ||
        (typeof input !== "string" && "headers" in input
          ? (input as Request).headers
          : undefined),
    );
    if (!headers.has("Authorization")) {
      headers.set("Authorization", `Bearer ${token}`);
    }
    return _origFetch(url, { ...init, headers });
  };
}
```

然后在 `initApp` 中:

```ts
const apiBaseUrl = localApiUrl !== null ? localApiUrl : configApiUrl;

if (apiBaseUrl) {
  console.log(
    `API Base URL set to: ${apiBaseUrl} (Local: ${localApiUrl}, Config: ${configApiUrl})`,
  );
}

setupHttp(apiBaseUrl);

// loader.config(...)
// setupI18n().then(...).catch(...)
```

这能保持当前行为(基于配置的 `apiBaseUrl`、presets、为 axios/fetch 提供一致的 JWT 处理),同时减少重复、简化控制流,让 `main.ts` 更像是“编排入口”,而不是“HTTP 客户端 + 应用工厂”。
</issue_to_address>

### Comment 4
<location> `dashboard/src/components/chat/LiveMode.vue:343` </location>
<code_context>
        wsBase = apiBase.replace("http://", "ws://");
</code_context>

<issue_to_address>
**security (javascript.lang.security.detect-insecure-websocket):** 检测到不安全的 WebSocket。所有 WebSocket 连接都应使用加密的 WebSocket(wss)。

*来源: opengrep*
</issue_to_address>

### Comment 5
<location> `dashboard/src/components/chat/LiveMode.vue:345` </location>
<code_context>
        wsBase = "ws://" + apiBase;
</code_context>

<issue_to_address>
**security (javascript.lang.security.detect-insecure-websocket):** 检测到不安全的 WebSocket。所有 WebSocket 连接都应使用加密的 WebSocket(wss)。

*来源: opengrep*
</issue_to_address>

Sourcery 对开源项目免费使用 —— 如果你觉得这些评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审质量。
Original comment in English

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

Security issues:

  • Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections. (link)
  • Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections. (link)

General comments:

  • In get_local_ip_addresses you mention filtering out IPv6 link-local addresses (fe80::/10) but the implementation currently appends all IPv6 addresses; either implement the intended filter or adjust the comment to avoid confusion and ensure only routable addresses are returned.
  • The CORS setup in AstrBotDashboard._init_app currently hard-codes allow_origin="*", allow_methods="*", allow_headers="*"; consider making these values configurable (e.g., from dashboard config) or at least restricting them for non-development deployments to avoid unnecessarily broad cross-origin exposure.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `get_local_ip_addresses` you mention filtering out IPv6 link-local addresses (fe80::/10) but the implementation currently appends all IPv6 addresses; either implement the intended filter or adjust the comment to avoid confusion and ensure only routable addresses are returned.
- The CORS setup in `AstrBotDashboard._init_app` currently hard-codes `allow_origin="*", allow_methods="*", allow_headers="*"`; consider making these values configurable (e.g., from dashboard config) or at least restricting them for non-development deployments to avoid unnecessarily broad cross-origin exposure.

## Individual Comments

### Comment 1
<location> `dashboard/src/main.ts:93` </location>
<code_context>
-// Keep fetch() calls consistent with axios by automatically attaching the JWT.
-// Some parts of the UI use fetch directly; without this, those requests will 401.
-const _origFetch = window.fetch.bind(window);
-window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
-  const token = localStorage.getItem('token');
-  if (!token) return _origFetch(input, init);
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The custom fetch wrapper may behave unexpectedly when `input` is a `Request` object, especially combined with URL rewriting.

Because you merge `headers` from `input.headers` but still pass the original `init` into `_origFetch(url, { ...init, headers })`, a `Request` input can lose or mismatch properties (method, body, credentials, etc.) relative to the original request.

Consider normalizing to a `Request` first, then rewriting only the URL and headers, for example:

```ts
window.fetch = (input, init) => {
  const currentBaseUrl = axios.defaults.baseURL;
  let url = typeof input === "string" || input instanceof URL ? input.toString() : input.url;

  if (typeof url === "string" && url.startsWith("/api") && currentBaseUrl) {
    const cleanBase = currentBaseUrl.replace(/\/+$/, "");
    const cleanPath = url.replace(/^\/+/, "");
    url = `${cleanBase}/${cleanPath}`;
  }

  const token = localStorage.getItem("token");
  const orig = input instanceof Request ? input : new Request(url, init);
  const headers = new Headers(orig.headers);
  if (token && !headers.has("Authorization")) {
    headers.set("Authorization", `Bearer ${token}`);
  }

  const request = new Request(url, { ...orig, headers });
  return _origFetch(request);
};
```

Suggested implementation:

```typescript
  // 优先使用 localStorage 中的配置,其次是 config.json,最后是空字符串
  const localApiUrl = localStorage.getItem("apiBaseUrl");
  const apiBaseUrl = localApiUrl !== null ? localApiUrl : configApiUrl;

  // Keep fetch() calls consistent with axios by automatically attaching the JWT
  // and normalizing Request handling so method/body/etc. are preserved.
  const _origFetch = window.fetch.bind(window);
  window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
    const currentBaseUrl = axios.defaults.baseURL;
    let url =
      typeof input === "string" || input instanceof URL
        ? input.toString()
        : input.url;

    if (typeof url === "string" && url.startsWith("/api") && currentBaseUrl) {
      const cleanBase = currentBaseUrl.replace(/\/+$/, "");
      const cleanPath = url.replace(/^\/+/, "");
      url = `${cleanBase}/${cleanPath}`;
    }

    const token = localStorage.getItem("token");
    const orig =
      input instanceof Request ? input : new Request(url, init);
    const headers = new Headers(orig.headers);

    if (token && !headers.has("Authorization")) {
      headers.set("Authorization", `Bearer ${token}`);
    }

    const request = new Request(url, { ...(orig as RequestInit), headers });
    return _origFetch(request);
  };

  if (apiBaseUrl) {
    console.log(

```

If `axios` is not already imported in `dashboard/src/main.ts`, you should add an appropriate import at the top of the file (for example, `import axios from "axios";`) so that `axios.defaults.baseURL` is available. Also confirm the file is compiled with DOM lib types so that `Request`, `RequestInfo`, and `RequestInit` are recognized.
</issue_to_address>

### Comment 2
<location> `README.md:271` </location>
<code_context>
-陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。
+_私は、高性能ですから!_

+<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>

</code_context>

<issue_to_address>
**suggestion:** Consider adding an `alt` attribute to the image for better accessibility.

For example: `<img src="..." width="100" alt="Short description of the robot GIF" />` so screen readers can convey the content of the animation.
</issue_to_address>

### Comment 3
<location> `dashboard/src/main.ts:15` </location>
<code_context>
-import { loader } from '@guolao/vue-monaco-editor'
-import axios from 'axios';
-
-// 初始化新的i18n系统,等待完成后再挂载应用
-setupI18n().then(() => {
-  console.log('🌍 新i18n系统初始化完成');
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting shared app bootstrap and HTTP setup logic into small helpers to reduce duplication and keep `initApp` focused on orchestration.

You can cut a good chunk of complexity/duplication without changing behavior by extracting two small helpers:

### 1. Deduplicate app bootstrap in `setupI18n` branches

Both `then` and `catch` branches do the same app creation + plugin registration + presets wiring. You can centralize that and keep `setupI18n` control flow simple:

```ts
async function createAndMountApp(presets: any[]) {
  const app = createApp(App);
  app.use(router);

  const pinia = createPinia();
  app.use(pinia);

  const { useApiStore } = await import("@/stores/api");
  const apiStore = useApiStore(pinia);
  apiStore.setPresets(presets);

  app.use(print);
  app.use(VueApexCharts);
  app.use(vuetify);
  app.use(confirmPlugin);

  // Inline mountApp logic here to avoid another layer of indirection,
  // or call your existing mountApp(app, pinia)
  app.mount("#app");

  import("./stores/customizer").then(({ useCustomizerStore }) => {
    const customizer = useCustomizerStore(pinia);
    vuetify.theme.global.name.value = customizer.uiTheme;
    const storedPrimary = localStorage.getItem("themePrimary");
    const storedSecondary = localStorage.getItem("themeSecondary");
    if (storedPrimary || storedSecondary) {
      const themes = vuetify.theme.themes.value;
      ["PurpleTheme", "PurpleThemeDark"].forEach((name) => {
        const theme = themes[name];
        if (!theme?.colors) return;
        if (storedPrimary) theme.colors.primary = storedPrimary;
        if (storedSecondary) theme.colors.secondary = storedSecondary;
        if (storedPrimary && theme.colors.darkprimary)
          theme.colors.darkprimary = storedPrimary;
        if (storedSecondary && theme.colors.darksecondary)
          theme.colors.darksecondary = storedSecondary;
      });
    }
  });
}
```

Then `initApp`’s i18n part becomes:

```ts
// 初始化新的i18n系统,等待完成后再挂载应用
setupI18n()
  .then(async () => {
    console.log("🌍 新i18n系统初始化完成");
    await createAndMountApp(presets);
  })
  .catch(async (error) => {
    console.error("❌ 新i18n系统初始化失败:", error);
    // 即使i18n初始化失败,也要挂载应用(使用回退机制)
    await createAndMountApp(presets);
  });
```

This removes the duplicated app setup and avoids drift between branches.

### 2. Isolate HTTP setup (axios + fetch) from `initApp`

The axios + `window.fetch` override logic is dense and not directly about bootstrapping. Moving it into a focused helper keeps `initApp` readable:

```ts
function setupHttp(apiBaseUrl: string) {
  axios.defaults.baseURL = apiBaseUrl;

  axios.interceptors.request.use((config) => {
    const token = localStorage.getItem("token");
    if (token) {
      config.headers["Authorization"] = `Bearer ${token}`;
    }
    return config;
  });

  const _origFetch = window.fetch.bind(window);
  window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
    let url = input;
    const currentBaseUrl = axios.defaults.baseURL;

    if (
      typeof input === "string" &&
      input.startsWith("/api") &&
      currentBaseUrl
    ) {
      const cleanBase = currentBaseUrl.replace(/\/+$/, "");
      const cleanPath = input.replace(/^\/+/, "");
      url = `${cleanBase}/${cleanPath}`;
    }

    const token = localStorage.getItem("token");
    if (!token) return _origFetch(url, init);

    const headers = new Headers(
      init?.headers ||
        (typeof input !== "string" && "headers" in input
          ? (input as Request).headers
          : undefined),
    );
    if (!headers.has("Authorization")) {
      headers.set("Authorization", `Bearer ${token}`);
    }
    return _origFetch(url, { ...init, headers });
  };
}
```

Then in `initApp`:

```ts
const apiBaseUrl = localApiUrl !== null ? localApiUrl : configApiUrl;

if (apiBaseUrl) {
  console.log(
    `API Base URL set to: ${apiBaseUrl} (Local: ${localApiUrl}, Config: ${configApiUrl})`,
  );
}

setupHttp(apiBaseUrl);

// loader.config(...)
// setupI18n().then(...).catch(...)
```

This keeps all current behavior (config-driven `apiBaseUrl`, presets, consistent JWT handling for axios/fetch) but reduces repetition, flattens control flow, and lets `main.ts` read more as “orchestration” than “HTTP client + app factory”.
</issue_to_address>

### Comment 4
<location> `dashboard/src/components/chat/LiveMode.vue:343` </location>
<code_context>
        wsBase = apiBase.replace("http://", "ws://");
</code_context>

<issue_to_address>
**security (javascript.lang.security.detect-insecure-websocket):** Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.

*Source: opengrep*
</issue_to_address>

### Comment 5
<location> `dashboard/src/components/chat/LiveMode.vue:345` </location>
<code_context>
        wsBase = "ws://" + apiBase;
</code_context>

<issue_to_address>
**security (javascript.lang.security.detect-insecure-websocket):** Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.

*Source: opengrep*
</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.

const currentBaseUrl = axios.defaults.baseURL;

// 如果是字符串路径且以 /api 开头,并且配置了 Base URL,则拼接
if (
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk):inputRequest 对象时,尤其是结合 URL 重写逻辑,当前自定义的 fetch 封装可能会出现意外行为。

由于你从 input.headers 合并了 headers,但在调用 _origFetch(url, { ...init, headers }) 时仍然传入原始的 init,因此对于 Request 类型的输入,请求的属性(method、body、credentials 等)可能会相对原始请求丢失或不匹配。

建议先将输入统一规范化为一个 Request 对象,然后仅重写 URL 和 headers,例如:

window.fetch = (input, init) => {
  const currentBaseUrl = axios.defaults.baseURL;
  let url = typeof input === "string" || input instanceof URL ? input.toString() : input.url;

  if (typeof url === "string" && url.startsWith("/api") && currentBaseUrl) {
    const cleanBase = currentBaseUrl.replace(/\/+$/, "");
    const cleanPath = url.replace(/^\/+/, "");
    url = `${cleanBase}/${cleanPath}`;
  }

  const token = localStorage.getItem("token");
  const orig = input instanceof Request ? input : new Request(url, init);
  const headers = new Headers(orig.headers);
  if (token && !headers.has("Authorization")) {
    headers.set("Authorization", `Bearer ${token}`);
  }

  const request = new Request(url, { ...orig, headers });
  return _origFetch(request);
};

建议的实现:

  // 优先使用 localStorage 中的配置,其次是 config.json,最后是空字符串
  const localApiUrl = localStorage.getItem("apiBaseUrl");
  const apiBaseUrl = localApiUrl !== null ? localApiUrl : configApiUrl;

  // Keep fetch() calls consistent with axios by automatically attaching the JWT
  // and normalizing Request handling so method/body/etc. are preserved.
  const _origFetch = window.fetch.bind(window);
  window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
    const currentBaseUrl = axios.defaults.baseURL;
    let url =
      typeof input === "string" || input instanceof URL
        ? input.toString()
        : input.url;

    if (typeof url === "string" && url.startsWith("/api") && currentBaseUrl) {
      const cleanBase = currentBaseUrl.replace(/\/+$/, "");
      const cleanPath = url.replace(/^\/+/, "");
      url = `${cleanBase}/${cleanPath}`;
    }

    const token = localStorage.getItem("token");
    const orig =
      input instanceof Request ? input : new Request(url, init);
    const headers = new Headers(orig.headers);

    if (token && !headers.has("Authorization")) {
      headers.set("Authorization", `Bearer ${token}`);
    }

    const request = new Request(url, { ...(orig as RequestInit), headers });
    return _origFetch(request);
  };

  if (apiBaseUrl) {
    console.log(

如果 dashboard/src/main.ts 中尚未导入 axios,请在文件顶部添加合适的导入(例如 import axios from "axios";),以便能够使用 axios.defaults.baseURL。同时,请确认该文件的编译环境启用了 DOM lib 类型,这样 RequestRequestInfoRequestInit 才能被正确识别。

Original comment in English

suggestion (bug_risk): The custom fetch wrapper may behave unexpectedly when input is a Request object, especially combined with URL rewriting.

Because you merge headers from input.headers but still pass the original init into _origFetch(url, { ...init, headers }), a Request input can lose or mismatch properties (method, body, credentials, etc.) relative to the original request.

Consider normalizing to a Request first, then rewriting only the URL and headers, for example:

window.fetch = (input, init) => {
  const currentBaseUrl = axios.defaults.baseURL;
  let url = typeof input === "string" || input instanceof URL ? input.toString() : input.url;

  if (typeof url === "string" && url.startsWith("/api") && currentBaseUrl) {
    const cleanBase = currentBaseUrl.replace(/\/+$/, "");
    const cleanPath = url.replace(/^\/+/, "");
    url = `${cleanBase}/${cleanPath}`;
  }

  const token = localStorage.getItem("token");
  const orig = input instanceof Request ? input : new Request(url, init);
  const headers = new Headers(orig.headers);
  if (token && !headers.has("Authorization")) {
    headers.set("Authorization", `Bearer ${token}`);
  }

  const request = new Request(url, { ...orig, headers });
  return _origFetch(request);
};

Suggested implementation:

  // 优先使用 localStorage 中的配置,其次是 config.json,最后是空字符串
  const localApiUrl = localStorage.getItem("apiBaseUrl");
  const apiBaseUrl = localApiUrl !== null ? localApiUrl : configApiUrl;

  // Keep fetch() calls consistent with axios by automatically attaching the JWT
  // and normalizing Request handling so method/body/etc. are preserved.
  const _origFetch = window.fetch.bind(window);
  window.fetch = (input: RequestInfo | URL, init?: RequestInit) => {
    const currentBaseUrl = axios.defaults.baseURL;
    let url =
      typeof input === "string" || input instanceof URL
        ? input.toString()
        : input.url;

    if (typeof url === "string" && url.startsWith("/api") && currentBaseUrl) {
      const cleanBase = currentBaseUrl.replace(/\/+$/, "");
      const cleanPath = url.replace(/^\/+/, "");
      url = `${cleanBase}/${cleanPath}`;
    }

    const token = localStorage.getItem("token");
    const orig =
      input instanceof Request ? input : new Request(url, init);
    const headers = new Headers(orig.headers);

    if (token && !headers.has("Authorization")) {
      headers.set("Authorization", `Bearer ${token}`);
    }

    const request = new Request(url, { ...(orig as RequestInit), headers });
    return _origFetch(request);
  };

  if (apiBaseUrl) {
    console.log(

If axios is not already imported in dashboard/src/main.ts, you should add an appropriate import at the top of the file (for example, import axios from "axios";) so that axios.defaults.baseURL is available. Also confirm the file is compiled with DOM lib types so that Request, RequestInfo, and RequestInit are recognized.

陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。
_私は、高性能ですから!_

<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
Copy link
Contributor

Choose a reason for hiding this comment

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

suggestion: 建议为该图片添加 alt 属性以提升可访问性。

例如:<img src="..." width="100" alt="对机器人动图的简短描述" />,这样屏幕阅读器就能向用户传达动画的内容。

Original comment in English

suggestion: Consider adding an alt attribute to the image for better accessibility.

For example: <img src="..." width="100" alt="Short description of the robot GIF" /> so screen readers can convey the content of the animation.

if (apiBase.startsWith("https://")) {
wsBase = apiBase.replace("https://", "wss://");
} else if (apiBase.startsWith("http://")) {
wsBase = apiBase.replace("http://", "ws://");
Copy link
Contributor

Choose a reason for hiding this comment

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

security (javascript.lang.security.detect-insecure-websocket): 检测到不安全的 WebSocket。所有 WebSocket 连接都应使用加密的 WebSocket(wss)。

来源: opengrep

Original comment in English

security (javascript.lang.security.detect-insecure-websocket): Insecure WebSocket Detected. WebSocket Secure (wss) should be used for all WebSocket connections.

Source: opengrep

@LIghtJUNction LIghtJUNction marked this pull request as draft February 5, 2026 19:06
Copy link
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

This PR implements backend/frontend separation for AstrBot, allowing the dashboard to connect to a remote backend via configurable API base URLs. It enhances the astrbot run command with IPv6 support, adds CORS capability via quart-cors, and introduces a --backend-only flag.

Changes:

  • Added CORS support and configurable API base URLs for frontend/backend separation
  • Enhanced CLI with --host, --port, and --backend-only flags
  • Changed default host from 0.0.0.0 to :: for IPv6-first networking across all platform adapters
  • Added WebSocket and SSE support for custom API base URLs in the frontend
  • Improved IP address handling with IPv6 support and public IP detection

Reviewed changes

Copilot reviewed 35 out of 37 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
pyproject.toml Added quart-cors dependency for CORS support
astrbot/dashboard/server.py Added CORS middleware, improved IPv6 handling, refactored initialization
dashboard/src/main.ts Added API base URL configuration from config.json and localStorage
dashboard/src/stores/api.ts New Pinia store for managing API base URLs and presets
astrbot/cli/commands/cmd_run.py Added --host, --port, and --backend-only CLI flags
astrbot/core/utils/io.py Enhanced IP address utilities with IPv6 support
astrbot/core/config/default.py Changed default hosts from 0.0.0.0 to ::
dashboard/vite.config.ts Updated dev server to listen on :: for IPv6
Platform adapters Updated default callback_server_host to :: across multiple platforms
Various Vue files Added server configuration UI in login and settings pages
astrbot/dashboard/routes/route.py Added to_json() helper method to Response class

static_folder=self.data_path,
static_url_path="/",
)
APP = self.app
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

This line assigns to a local variable 'APP' which is then immediately overwritten by reassigning 'self.app'. The global 'APP' variable is not actually updated. If the intent is to update the global 'APP' variable for backwards compatibility, use 'global APP' before this assignment. Otherwise, this line has no effect and should be removed.

Suggested change
APP = self.app

Copilot uses AI. Check for mistakes.
[Service]
Type=simple
WorkingDirectory=%h/.local/share/astrbot
ExecStart=/usr/bin/sh -c '/usr/bin/astrbot run || { /usr/bin/astrbot init && /usr/bin/astrbot run; }'
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The ExecStart command uses shell command chaining that may hide actual errors. If 'astrbot run' fails after 'astrbot init' succeeds, the init will run again on restart, potentially causing repeated initialization issues. Consider splitting this into separate service units or using a more robust error handling approach, such as a dedicated startup script that checks for initialization state.

Copilot uses AI. Check for mistakes.
async def srv_plug_route(self, subpath: str, *args, **kwargs):
handler = self._plugin_route_map.get((f"/{subpath}", request.method))
if not handler:
return jsonify(Response().error("未找到该路由").to_json())
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Double jsonify call: to_json() already calls jsonify() internally (route.py line 62), but this code calls jsonify() again on the result. This will cause the response to be double-encoded as JSON. Either change to_json() to return just asdict(self), or remove the outer jsonify() call here.

Copilot uses AI. Check for mistakes.
# 过滤掉 IPv6 的 link-local 地址(fe80:...)
# 用这个不如用::1
ip = ip_address(addr.address.split("%")[0]) # 处理带 zone index 的情况
network_ips.append(ip)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The comment on line 232 suggests filtering out link-local addresses, but the code doesn't actually filter them out - it still adds all IPv6 addresses to the list. This could result in fe80:: addresses being displayed in the startup URLs, which are not useful for network access. Consider adding a check like 'if not ip.is_link_local:' before appending to network_ips.

Suggested change
network_ips.append(ip)
if isinstance(ip, IPv6Address) and not ip.is_link_local:
network_ips.append(ip)

Copilot uses AI. Check for mistakes.

@staticmethod
def _unauthorized(msg: str):
r = jsonify(Response().error(msg).to_json())
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

Double jsonify call: to_json() already calls jsonify() internally (route.py line 62), but this code calls jsonify() again on the result. This will cause the response to be double-encoded as JSON. Either change to_json() to return just asdict(self), or remove the outer jsonify() call here.

Copilot uses AI. Check for mistakes.
LIghtJUNction and others added 12 commits February 6, 2026 03:13
后端:
- 支持通过 DASHBOARD_ENABLE 环境变量分离 WebUI 服务
- 修复 CORS 跨域预检请求(OPTIONS) 鉴权问题
- 优化 IPv6 地址过滤逻辑
- CLI 新增 --backend-only 模式支持

前端:
- 新增 API 地址动态配置与预设管理功能 (登录页/设置页)
- 修复 WebSocket/SSE 连接地址适配非同源环境
- 修复 TypeScript 配置报错
- 完善新增功能的国际化支持
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend area:platform The bug / feature is about IM platform adapter, such as QQ, Lark, Telegram, WebChat and so on. size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant