Conversation
* 默认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>
There was a problem hiding this comment.
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>帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的评审质量。
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_addressesyou 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_appcurrently hard-codesallow_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>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 ( |
There was a problem hiding this comment.
suggestion (bug_risk): 当 input 为 Request 对象时,尤其是结合 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 类型,这样 Request、RequestInfo 和 RequestInit 才能被正确识别。
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"/> |
There was a problem hiding this comment.
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://"); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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-onlyflags - Changed default host from
0.0.0.0to::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 |
astrbot/dashboard/server.py
Outdated
| static_folder=self.data_path, | ||
| static_url_path="/", | ||
| ) | ||
| APP = self.app |
There was a problem hiding this comment.
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.
| APP = self.app |
| [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; }' |
There was a problem hiding this comment.
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.
| 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()) |
There was a problem hiding this comment.
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.
astrbot/core/utils/io.py
Outdated
| # 过滤掉 IPv6 的 link-local 地址(fe80:...) | ||
| # 用这个不如用::1 | ||
| ip = ip_address(addr.address.split("%")[0]) # 处理带 zone index 的情况 | ||
| network_ips.append(ip) |
There was a problem hiding this comment.
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.
| network_ips.append(ip) | |
| if isinstance(ip, IPv6Address) and not ip.is_link_local: | |
| network_ips.append(ip) |
|
|
||
| @staticmethod | ||
| def _unauthorized(msg: str): | ||
| r = jsonify(Response().error(msg).to_json()) |
There was a problem hiding this comment.
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.
…,可选择不下载,使用--backend-only选项时候,不再提示要下载前端
后端: - 支持通过 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>
…s/AstrBot into feat/optional-backend
这个PR完善了astrbot run子命令
#4892
原本仅支持指定端口启动
修改为支持指定端口启动,同时支持指定host
顺便把默认值改成了::,并处理了不同系统的差异
将监听的::的行为统一为了双栈监听
然后新增一个flag option --backend-only
astrbot run --backend-only可以仅启动后端,而不启动前端
进而,我发起了这个pr,让前端支持指定任意后端,而不仅仅是同源
为了实现这个功能,我新增了quart-cors依赖,允许跨站访问
给登录界面和设置界面新增了可配置项
简而言之,在保持当前的方便性的情况下,增加了灵活性
同时,服务器可以不下载额外的前端文件,而仅仅运行一个后端,从而节省了服务器资源
Summary by Sourcery
使 Web 仪表盘的主机/端口和 API 基础 URL 完全可配置,以支持前后端分离部署,同时改进跨平台的默认网络行为和健壮性。
New Features:
config.json以及登录页和设置页中的 UI 控件对其进行暴露与配置。Bug Fixes:
type字段并避免修改原始 schema,提高与 Gemini/MCP JSON Schema 的兼容性。Enhancements:
Build:
quart-cors依赖,以支持对基于 Quart 的仪表盘 API 的跨域访问。::并将 API 调用代理到后端,从而使本地开发与新的主机默认值保持一致。Deployment:
Documentation:
Chores:
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:
Bug Fixes:
typefields and avoiding mutation of the original schema.Enhancements:
Build:
::and proxy API calls to the backend, aligning local development with the new host defaults.Deployment:
Documentation:
Chores: