feat(dashboard): embed Dashboard and integrate the desktop development workflow#153
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive Vue 3 and Vite-based dashboard for AstrBot Desktop, featuring a rich chat UI, standalone chat capabilities, live voice mode, and automated build scripts for font subsetting and syntax highlighting. The review identified several critical issues that need to be addressed: a potential crash in the API error helper when handling non-object errors, a hanging promise in the confirmation dialog when dismissed externally, and broken SSE stream parsing on CRLF line endings in both the thread and standalone chat views. Additionally, loading critical libraries via CDN poses offline usability risks for the desktop app, and improvements are needed for user-facing error handling during message dispatch and WebSocket timeout cleanup.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function isLegacyFallbackError(error: unknown): boolean { | ||
| const axiosError = error as { | ||
| response?: { status?: number; data?: { message?: string } | string }; | ||
| message?: string; | ||
| }; | ||
| if (axiosError.response?.status === 404) { |
There was a problem hiding this comment.
在 isLegacyFallbackError 函数中,直接对 error 进行类型断言并访问 axiosError.response。如果 error 为 null、undefined 或非对象类型(例如手动的 throw 'error'),访问 axiosError.response 将会抛出 TypeError: Cannot read properties of null,导致错误处理流程崩溃。建议在访问属性前先进行非空及对象类型检查。
export function isLegacyFallbackError(error: unknown): boolean {\n if (!error || typeof error !== 'object') {\n return false;\n }\n const axiosError = error as {\n response?: { status?: number; data?: { message?: string } | string };\n message?: string;\n };\n if (axiosError.response?.status === 404) {| @@ -0,0 +1,47 @@ | |||
| <template> | |||
| <v-dialog v-model="isOpen" max-width="400"> | |||
There was a problem hiding this comment.
v-dialog 没有设置 persistent 属性,这意味着用户可以通过点击对话框外部或按下 ESC 键来关闭它。当发生这种情况时,v-model 会将 isOpen 设为 false,但 resolvePromise 并不会被调用,导致调用 open() 的 Promise 永远处于 pending 状态(挂起),从而阻塞后续代码的执行。建议为 v-dialog 添加 persistent 属性,或者监听 isOpen 的变化,在非按钮触发关闭时也正确 resolve Promise。
<v-dialog v-model=\"isOpen\" max-width=\"400\" persistent>
| buffer += decoder.decode(value, { stream: true }); | ||
| const chunks = buffer.split("\n\n"); | ||
| buffer = chunks.pop() || ""; | ||
| for (const chunk of chunks) { | ||
| const data = chunk | ||
| .split("\n") | ||
| .filter((line) => line.startsWith("data:")) | ||
| .map((line) => line.slice(5).trimStart()) | ||
| .join("\n"); |
There was a problem hiding this comment.
在 readSseStream 中,使用 buffer.split('\n\n') 和 chunk.split('\n') 来解析 SSE 流。如果后端服务器使用标准的 CRLF(\r\n)作为换行符,\r\n\r\n 中并不包含 \n\n,这会导致 split('\n\n') 无法正确分割数据块,使得整个流的数据都积压在 buffer 中直到连接关闭,从而破坏了流式输出(Streaming)的效果。此外,使用 \n 分割行会使每行末尾残留 \r 字符,导致 JSON.parse 失败。建议使用正则表达式 \r?\n\r?\n 和 \r?\n 进行分割,并使用 .trim() 清除首尾空白字符。
const chunks = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = chunks.pop() || \"\";\n for (const chunk of chunks) {\n const data = chunk\n .split(/\\r?\\n/)\n .filter((line) => line.startsWith(\"data:\"))\n .map((line) => line.slice(5).trim())\n .join(\"\\n\");
| import ToolCallItem from "@/components/chat/message_list_comps/ToolCallItem.vue"; | ||
| import ThemeAwareMarkdownCodeBlock from "@/components/shared/ThemeAwareMarkdownCodeBlock.vue"; | ||
| import { | ||
| attachmentName, | ||
| attachmentPresentation, | ||
| } from "@/components/chat/attachmentPresentation"; | ||
| import { useMediaHandling } from "@/composables/useMediaHandling"; | ||
| import { | ||
| displayParts as displayMessageParts, |
There was a problem hiding this comment.
与 ThreadPanel.vue 类似,这里的 readSseStream 同样存在使用 \n\n 和 \n 分割 SSE 流的问题。如果后端使用标准的 CRLF(\r\n)换行符,会导致流式输出失效,且 JSON.parse 会因残留的 \r 字符而报错。建议使用正则表达式进行分割并使用 .trim() 清理空白字符。
const chunks = buffer.split(/\\r?\\n\\r?\\n/);\n buffer = chunks.pop() || \"\";\n for (const chunk of chunks) {\n const data = chunk\n .split(/\\r?\\n/)\n .filter((line) => line.startsWith(\"data:\"))\n .map((line) => line.slice(5).trim())\n .join(\"\\n\");
| <script src="https://cdn.jsdelivr.net/npm/onnxruntime-web@1.22.0/dist/ort.wasm.min.js"></script> | ||
| <script src="https://cdn.jsdelivr.net/npm/@ricky0123/vad-web@0.0.29/dist/bundle.min.js"></script> |
| } catch (error) { | ||
| console.error("Failed to send message:", error); | ||
| } finally { |
There was a problem hiding this comment.
| // 超时处理 | ||
| setTimeout(() => { | ||
| if (ws?.readyState !== WebSocket.OPEN) { | ||
| reject(new Error('WebSocket 连接超时')); | ||
| } | ||
| }, 5000); |
There was a problem hiding this comment.
在 connectWebSocket 的超时处理中,如果连接超时(超过 5 秒),Promise 会被 reject,但此时并没有关闭正在尝试连接的 WebSocket 实例(ws)。这会导致连接在后台继续尝试,若后续连接成功,仍会触发 onopen、onmessage 等回调,可能导致非预期的音频播放或状态混乱。建议在超时触发时,显式调用 ws.close() 并将 ws 置为 null,同时在连接成功或失败时清除该定时器。
const timeoutId = setTimeout(() => {\n if (ws?.readyState !== WebSocket.OPEN) {\n if (ws) {\n ws.close();\n ws = null;\n }\n reject(new Error('WebSocket 连接超时'));\n }\n }, 5000);
|
整体方向没有问题,Dashboard 内嵌、开发启动流程和资源准备流程也已基本验证通过。补充以下 review findings,建议在合并前处理:
|
…Devs/AstrBot-desktop into refactor-embed-dashboard
Latest Review已复查当前最新 head Previous Findings上一轮提出的 4 项问题已处理:
Verification
Non-blocking ObservationDashboard 构建仍输出一条 warning: 这看起来来自 MDI 子集生成器扫描自身生成目录/名称,不影响当前构建结果或运行时 fallback 图标。建议后续让扫描器排除 当前未发现新的阻塞问题,可以继续合并流程。 |
本 PR 将 AstrBot Dashboard 从 AstrBot 主仓库迁移至 AstrBot Desktop 仓库独立维护,解决开发和构建过程中依赖外部 Dashboard 源码、开发环境启动步骤分散,以及前后端启动时序不稳定的问题。
迁移后,开发模式会自动准备 AstrBot 后端源码、启动本地 Dashboard Vite 服务和 Tauri 应用;正式构建则直接使用当前仓库中的 Dashboard,并继续使用
resources/backend作为安装包后端资源。Summary / 改动概述
dashboard/。pnpm run install:dashboard,通过 Dashboard 自身的 lockfile 安装前端依赖。pnpm run dev:dashboard和pnpm run typecheck:dashboard,分别用于启动 Dashboard Vite 服务和执行类型检查。pnpm run dev由直接执行cargo tauri dev改为通过scripts/run-tauri.mjs dev启动;命令会读取根目录.env、准备开发后端源码,再交由 Tauri 自动启动 Dashboard 和桌面程序。pnpm run build由统一入口scripts/run-tauri.mjs build执行;Tauri 的beforeBuildCommand会调用pnpm run prepare:resources,依次构建本仓库 Dashboard 并准备resources/backend。prepare:webui、prepare:backend和prepare:resources,拆分并统一 WebUI、后端及完整打包资源的准备流程。TAURI_SIGNING_PRIVATE_KEY时,pnpm run build会自动关闭 updater artifacts,仍可生成 MSI/NSIS 安装包。pnpm run dev自动启动dashboard/下的 Vite 服务。http://localhost:1420。vendor/AstrBot。.env配置远端仓库、分支或本地源码路径。resources/backend。prepare:webui构建当前仓库中的 Dashboard。resources/backend继续作为正式安装包的内置后端。TAURI_SIGNING_PRIVATE_KEY时关闭 updater artifacts,允许本地生成普通 MSI/NSIS 安装包。主要涉及
dashboard/、scripts/、src-tauri/、package.json、.env.example和Makefile。Verification / 验证方式
已完成以下本地验证:
pnpm run dev能够同步vendor/AstrBot后端源码、启动 Dashboard Vite 服务、编译并启动 Tauri,以及通过源码启动 AstrBot 后端。cargo check通过。ASTRBOT_SOURCE_DIR的情况下仍可成功,确认前端构建已与 AstrBot 源码目录解耦。Checklist / 检查清单
src-tauri/Cargo.lock, lockfiles under changed package dirs). / 如果引入或升级依赖,已同步更新相关 lock 文件(如src-tauri/Cargo.lock、对应包目录中的 lock 文件)。