Compare commits

...

21 Commits

Author SHA1 Message Date
Soulter
1c090299b1 feat: tauri app 2025-11-10 15:11:59 +08:00
Soulter
b360c8446e feat: add default model selection chip in provider model selector 2025-11-10 13:04:28 +08:00
Soulter
6d00717655 feat: add streaming support with toggle in chat interface and adjust layout for mobile 2025-11-09 21:57:30 +08:00
Soulter
bb5f06498e perf: refine login page 2025-11-09 20:57:45 +08:00
Dt8333
aca5743ab6 feat: 为部分适配器添加缺失的 send_streaming 方法 (#3545)
为Wechatpadpro和discord添加缺失的方法。
2025-11-09 16:00:24 +08:00
Soulter
6903032f7e fix: improve knowledge base chip display with truncation and styling (#3582)
fixes: #3546
2025-11-09 15:30:41 +08:00
nazo
1ce0ff87bd feat: supports to add custom headers for openai providers (#3581)
* feat: OPENAI系支持自定义添加请求头

* chore: add custom headers and extra body to config for zhipu

---------

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2025-11-09 15:12:52 +08:00
Soulter
e39d6bae0b fix: update JSON submission link in plugin publish template 2025-11-09 15:06:40 +08:00
Raven95676
8028e9e9a6 chore: bump version to 4.5.6 2025-11-07 16:20:19 +08:00
Raven95676
817f20ea01 fix: pyproject 2025-11-07 16:18:42 +08:00
Raven95676
ad5579a2f4 chore: bump version to 4.5.5 2025-11-07 15:52:58 +08:00
Raven95676
81a689a79b fix: typo 2025-11-07 15:41:14 +08:00
Raven95676
1893dd8336 fix: dockefile 2025-11-07 15:41:03 +08:00
Soulter
021ca8175b chore: bump version to 4.5.4 2025-11-07 14:28:51 +08:00
Soulter
39d6207fe1 chore: remove dynamic version 2025-11-07 14:26:56 +08:00
Soulter
23ce687229 chore: fix dockerfile 2025-11-07 14:23:49 +08:00
鸦羽
3715312fd2 fix: update project description to English (#3516) 2025-11-07 01:13:32 +08:00
Soulter
8196922cac docs: simplify README 2025-11-06 15:22:43 +08:00
Soulter
8089ad91da perf: improve extension market ui 2025-11-06 13:57:46 +08:00
Soulter
2930cc3fd8 chore: bump version to 4.5.3 2025-11-05 21:21:14 +08:00
Soulter
0e841a8b25 fix: correct tools dictionary comprehension in get_tool_list method 2025-11-05 21:19:10 +08:00
110 changed files with 15673 additions and 846 deletions

View File

@@ -1,6 +1,7 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# github actions
.git
.github/
.*ignore
# User-specific stuff
@@ -19,4 +20,5 @@ data/
changelogs/
tests/
.ruff_cache/
.astrbot
.astrbot
astrbot.lock

View File

@@ -16,7 +16,7 @@ body:
请将插件信息填写到下方的 JSON 代码块中。其中 `tags`(插件标签)和 `social_link`(社交链接)选填。
不熟悉 JSON ?可以从 [此](https://plugins.astrbot.app/submit) 生成 JSON ,生成后记得复制粘贴过来.
不熟悉 JSON ?可以从 [此](https://plugins.astrbot.app) 右下角提交。
- type: textarea
id: plugin-info

79
.github/workflows/build-app.yml vendored Normal file
View File

@@ -0,0 +1,79 @@
name: Build Desktop App
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies (Ubuntu)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install Python dependencies
run: |
pip install uv
uv sync
- name: Build Python backend with Nuitka
run: |
pip install nuitka
python build_nuitka.py
- name: Install Node dependencies
working-directory: ./dashboard
run: npm install
- name: Build Tauri app
working-directory: ./dashboard
run: npm run tauri:build
- name: Upload artifacts (macOS)
if: matrix.platform == 'macos-latest'
uses: actions/upload-artifact@v4
with:
name: astrbot-macos
path: dashboard/src-tauri/target/release/bundle/dmg/*.dmg
- name: Upload artifacts (Windows)
if: matrix.platform == 'windows-latest'
uses: actions/upload-artifact@v4
with:
name: astrbot-windows
path: dashboard/src-tauri/target/release/bundle/msi/*.msi
- name: Upload artifacts (Linux)
if: matrix.platform == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: astrbot-linux
path: |
dashboard/src-tauri/target/release/bundle/deb/*.deb
dashboard/src-tauri/target/release/bundle/appimage/*.AppImage

2
.gitignore vendored
View File

@@ -32,6 +32,7 @@ tests/astrbot_plugin_openai
# Dashboard
dashboard/node_modules/
dashboard/dist/
dashboard/src-tauri/target
package-lock.json
package.json
@@ -47,3 +48,4 @@ astrbot.lock
chroma
venv/*
pytest.ini
build/

287
BUILD_INSTRUCTIONS.md Normal file
View File

@@ -0,0 +1,287 @@
# AstrBot 桌面应用构建指南
本指南介绍如何使用 Nuitka 将 Python 后端打包并集成到 Tauri 桌面应用中。
## 前置要求
### 系统要求
- Python 3.10+
- Node.js 20+
- Rust (通过 rustup 安装)
- UV 包管理器
### macOS 额外要求
- Xcode Command Line Tools: `xcode-select --install`
### Linux 额外要求
```bash
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev \
libappindicator3-dev librsvg2-dev patchelf
```
### Windows 额外要求
- Visual Studio 2019+ with C++ build tools
- Windows 10 SDK
## 构建步骤
### 1. 安装 Python 依赖
```bash
pip install uv
uv sync
```
### 2. 安装 Nuitka
```bash
pip install nuitka
```
### 3. 构建 Python 后端
```bash
python build_nuitka.py
```
这会使用 Nuitka 将 `main.py` 编译为独立可执行文件,输出到 `build/nuitka/` 目录。
**注意**: Nuitka 编译过程可能需要 10-30 分钟,取决于您的系统性能。
### 4. 安装前端依赖
```bash
cd dashboard
npm install
```
### 5. 构建 Tauri 应用
```bash
npm run tauri:build
```
构建脚本会自动:
1. 运行 `build_nuitka.py` 编译 Python 后端
2. 将编译好的可执行文件复制到 `src-tauri/resources/` 目录
3. 构建 Tauri 应用并打包所有资源
### 6. 查找构建产物
构建完成后,您可以在以下位置找到安装包:
- **macOS**: `dashboard/src-tauri/target/release/bundle/dmg/AstrBot_*.dmg`
- **Windows**: `dashboard/src-tauri/target/release/bundle/msi/AstrBot_*.msi`
- **Linux**:
- `dashboard/src-tauri/target/release/bundle/deb/astrbot_*.deb`
- `dashboard/src-tauri/target/release/bundle/appimage/astrbot_*.AppImage`
## 开发模式
在开发时,您可能不想每次都完整编译 Python 后端。
### 仅开发 Tauri + Vue
```bash
cd dashboard
npm run tauri:dev
```
这会启动开发服务器,但不会自动启动 Python 后端。您需要手动运行:
```bash
uv run main.py
```
### 测试完整集成
如果您想测试 Tauri 自动启动 Python 后端的功能:
1. 先编译一次 Python 后端:
```bash
python build_nuitka.py
```
2. 手动复制到资源目录:
```bash
# macOS
cp -r build/nuitka/main.app dashboard/src-tauri/resources/astrbot-backend.app
# Windows
copy build\nuitka\main.exe dashboard\src-tauri\resources\astrbot-backend.exe
# Linux
cp build/nuitka/main.bin dashboard/src-tauri/resources/astrbot-backend
```
3. 运行开发模式:
```bash
cd dashboard
npm run tauri:dev
```
## Nuitka 构建选项说明
`build_nuitka.py` 脚本使用以下关键选项:
- `--standalone`: 创建包含所有依赖的独立目录
- `--onefile`: 将所有内容打包到单个可执行文件
- `--follow-imports`: 自动跟踪所有 Python 导入
- `--include-package`: 明确包含特定包
- `--include-data-dir`: 包含数据目录(插件、配置等)
### 自定义构建
如果您需要修改构建选项,编辑 `build_nuitka.py`:
```python
# 添加更多要包含的包
include_packages = [
"astrbot",
"your_custom_package",
# ...
]
# 添加更多数据目录
data_includes = [
"data/config",
"your_custom_data",
# ...
]
```
## 常见问题
### 1. Nuitka 编译失败
**问题**: 编译时出现 "module not found" 错误
**解决方案**: 在 `build_nuitka.py` 中添加缺失的包到 `include_packages` 列表
### 2. 运行时找不到资源文件
**问题**: 应用启动后提示找不到配置文件或插件
**解决方案**: 确保在 `build_nuitka.py` 中使用 `--include-data-dir` 包含了所有必要的数据目录
### 3. macOS 安全警告
**问题**: macOS 提示"应用来自未知开发者"
**解决方案**:
```bash
# 临时解除限制
sudo spctl --master-disable
# 或者为特定应用授权
xattr -cr /Applications/AstrBot.app
```
对于生产发布,您需要:
1. 注册 Apple Developer 账号
2. 对应用进行代码签名
3. 提交公证 (Notarization)
### 4. Windows Defender 报毒
**问题**: Windows Defender 或其他杀毒软件报毒
**解决方案**:
- 这是 Nuitka 打包程序的常见问题
- 可以使用 `--windows-company-name``--windows-product-name` 添加元数据
- 对于生产发布,需要购买代码签名证书
### 5. Linux 依赖问题
**问题**: 在某些 Linux 发行版上缺少共享库
**解决方案**: 使用 AppImage 格式,它包含所有依赖:
```bash
# 构建时会自动生成 AppImage
npm run tauri:build
```
## 优化构建大小
默认的 `--onefile` 模式会生成较大的可执行文件。如果需要减小体积:
1. 移除不需要的包
2. 使用 `--standalone` 而不是 `--onefile`
3. 排除不必要的数据文件
修改 `build_nuitka.py`:
```python
# 移除 --onefile使用 --standalone
nuitka_cmd = [
sys.executable,
"-m", "nuitka",
"--standalone", # 只使用 standalone
# "--onefile", # 注释掉 onefile
# ...
]
```
## CI/CD 集成
项目已配置 GitHub Actions 工作流 (`.github/workflows/build-app.yml`),可以自动为所有平台构建应用。
推送标签时自动触发:
```bash
git tag v4.5.7
git push origin v4.5.7
```
或手动触发:
在 GitHub Actions 页面选择 "Build Desktop App" 工作流并点击 "Run workflow"
## 发布清单
在发布新版本前:
- [ ] 更新版本号
- `pyproject.toml` - Python 项目版本
- `dashboard/package.json` - Node 项目版本
- `dashboard/src-tauri/Cargo.toml` - Rust 项目版本
- `dashboard/src-tauri/tauri.conf.json` - Tauri 配置版本
- [ ] 运行代码检查
```bash
uv run ruff check .
uv run ruff format .
```
- [ ] 本地测试构建
```bash
python build_nuitka.py
cd dashboard && npm run tauri:build
```
- [ ] 测试安装包
- 安装生成的安装包
- 验证应用启动
- 验证 Python 后端自动启动
- 测试核心功能
- [ ] 创建发布标签
```bash
git tag -a v4.5.7 -m "Release v4.5.7"
git push origin v4.5.7
```
## 技术架构
```
┌─────────────────────────────────────┐
│ Tauri Desktop App │
│ (Rust + WebView) │
│ │
│ ┌─────────────────────────────┐ │
│ │ Vue.js Dashboard │ │
│ │ (Frontend UI) │ │
│ └─────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ Python Backend │ │
│ │ (Nuitka Compiled) │ │
│ │ - AstrBot Core │ │
│ │ - Plugins │ │
│ │ - API Server │ │
│ └─────────────────────────────┘ │
│ │
│ HTTP/WebSocket │
│ localhost:6185 │
└─────────────────────────────────────┘
```
## 参考资源
- [Nuitka 文档](https://nuitka.net/doc/user-manual.html)
- [Tauri 文档](https://tauri.app/v1/guides/)
- [AstrBot 文档](https://astrbot.fun)

View File

@@ -18,15 +18,15 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
apt-get install -y --no-install-recommends nodejs && \
echo "3.11" > .python-version && \
rm -rf /var/lib/apt/lists/*
RUN apt-get update && apt-get install -y curl gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y nodejs
RUN python -m pip install --no-cache-dir uv && \
uv pip install socksio pilk --no-cache-dir --system
RUN python -m pip install uv \
&& echo "3.11" > .python-version
RUN uv pip install -r requirements.txt --no-cache-dir --system
RUN uv pip install socksio uv pilk --no-cache-dir --system
EXPOSE 6185
EXPOSE 6186
CMD ["uv", "run", "main.py"]
CMD ["python", "main.py"]

View File

@@ -1,40 +0,0 @@
FROM python:3.11-slim
WORKDIR /AstrBot
COPY . /AstrBot/
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
build-essential \
python3-dev \
libffi-dev \
libssl-dev \
curl \
unzip \
ca-certificates \
bash \
git \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
ENV NVM_DIR="/root/.nvm" \
NODE_VERSION=22
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash && \
. "$NVM_DIR/nvm.sh" && \
nvm install $NODE_VERSION && \
nvm use $NODE_VERSION && \
nvm alias default $NODE_VERSION && \
node -v && npm -v && \
echo "3.11" > .python-version
ENV PATH="$NVM_DIR/versions/node/v$(node -v | cut -d 'v' -f 2)/bin:$PATH"
RUN python -m pip install --no-cache-dir uv
# 安装项目依赖(根据指南,使用 uv sync
RUN uv sync --no-cache
EXPOSE 6185
EXPOSE 6186
CMD ["uv", "run", "main.py"]

116
README.md
View File

@@ -8,7 +8,7 @@
<div>
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=1" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=2" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
<br>
@@ -119,83 +119,73 @@ uv run main.py
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
## 消息平台支持情况
## 支持的消息平台
**官方维护**
| 平台 | 支持性 |
| -------- | ------- |
| QQ(官方平台) | ✔ |
| QQ(OneBot) | ✔ |
| Telegram | ✔ |
| 企微应用 | ✔ |
| 企微智能机器人 | ✔ |
| 微信客服 | ✔ |
| 微信公众号 | ✔ |
| 飞书 | ✔ |
| 钉钉 | ✔ |
| Slack | ✔ |
| Discord | ✔ |
| Satori | ✔ |
| Misskey | ✔ |
| Whatsapp | 将支持 |
| LINE | 将支持 |
- QQ (官方平台 & OneBot)
- Telegram
- 企微应用 & 企微智能机器人
- 微信客服 & 微信公众号
- 飞书
- 钉钉
- Slack
- Discord
- Satori
- Misskey
- Whatsapp (将支持)
- LINE (将支持)
**社区维护**
| 平台 | 支持性 |
| -------- | ------- |
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | ✔ |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | ✔ |
| [Bilibili 私信](https://github.com/Hina-Chat/astrbot_plugin_bilibili_adapter) | ✔ |
| [wxauto](https://github.com/luosheng520qaq/wxauto-repost-onebotv11) | ✔ |
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
- [Bilibili 私信](https://github.com/Hina-Chat/astrbot_plugin_bilibili_adapter)
- [wxauto](https://github.com/luosheng520qaq/wxauto-repost-onebotv11)
## ⚡ 提供商支持情况
## 支持的模型服务
**大模型服务**
| 名称 | 支持性 | 备注 |
| -------- | ------- | ------- |
| OpenAI | ✔ | 支持任何兼容 OpenAI API 的服务 |
| Anthropic | ✔ | |
| Google Gemini | ✔ | |
| Moonshot AI | ✔ | |
| 智谱 AI | ✔ | |
| DeepSeek | ✔ | |
| Ollama | ✔ | 本地部署 DeepSeek 等开源语言模型 |
| LM Studio | ✔ | 本地部署 DeepSeek 等开源语言模型 |
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | ✔ | |
| [302.AI](https://share.302.ai/rr1M3l) | ✔ | |
| [小马算力](https://www.tokenpony.cn/3YPyf) | ✔ | |
| 硅基流动 | ✔ | |
| PPIO 派欧云 | ✔ | |
| ModelScope | ✔ | |
| OneAPI | ✔ | |
| Dify | ✔ | |
| 阿里云百炼应用 | ✔ | |
| Coze | ✔ | |
- OpenAI 及兼容服务
- Anthropic
- Google Gemini
- Moonshot AI
- 智谱 AI
- DeepSeek
- Ollama (本地部署)
- LM Studio (本地部署)
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
- [302.AI](https://share.302.ai/rr1M3l)
- [小马算力](https://www.tokenpony.cn/3YPyf)
- [硅基流动](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot)
- [PPIO 派欧云](https://ppio.com/user/register?invited_by=AIOONE)
- ModelScope
- OneAPI
**LLMOps 平台**
- Dify
- 阿里云百炼应用
- Coze
**语音转文本服务**
| 名称 | 支持性 | 备注 |
| -------- | ------- | ------- |
| Whisper | ✔ | 支持 API、本地部署 |
| SenseVoice | ✔ | 本地部署 |
- OpenAI Whisper
- SenseVoice
**文本转语音服务**
| 名称 | 支持性 | 备注 |
| -------- | ------- | ------- |
| OpenAI TTS | ✔ | |
| Gemini TTS | ✔ | |
| GSVI | ✔ | GPT-Sovits-Inference |
| GPT-SoVITs | ✔ | GPT-Sovits |
| FishAudio | ✔ | |
| Edge TTS | ✔ | Edge 浏览器的免费 TTS |
| 阿里云百炼 TTS | ✔ | |
| Azure TTS | ✔ | |
| Minimax TTS | ✔ | |
| 火山引擎 TTS | ✔ | |
- OpenAI TTS
- Gemini TTS
- GPT-Sovits-Inference
- GPT-Sovits
- FishAudio
- Edge TTS
- 阿里云百炼 TTS
- Azure TTS
- Minimax TTS
- 火山引擎 TTS
## ❤️ 贡献
@@ -229,7 +219,7 @@ pre-commit install
## ⭐ Star History
> [!TIP]
> [!TIP]
> 如果本项目对您的生活 / 工作产生了帮助,或者您关注本项目的未来发展,请给项目 Star这是我们维护这个开源项目的动力 <3
<div align="center">

View File

@@ -4,7 +4,7 @@ import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.5.2"
VERSION = "4.5.6"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
# 默认配置
@@ -740,6 +740,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://api.openai.com/v1",
"timeout": 120,
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
"hint": "也兼容所有与 OpenAI API 兼容的服务。",
@@ -755,6 +756,7 @@ CONFIG_METADATA_2 = {
"api_base": "",
"timeout": 120,
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
@@ -768,6 +770,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://api.x.ai/v1",
"timeout": 120,
"model_config": {"model": "grok-2-latest", "temperature": 0.4},
"custom_headers": {},
"custom_extra_body": {},
"xai_native_search": False,
"modalities": ["text", "image", "tool_use"],
@@ -799,6 +802,7 @@ CONFIG_METADATA_2 = {
"key": ["ollama"], # ollama 的 key 默认是 ollama
"api_base": "http://localhost:11434/v1",
"model_config": {"model": "llama3.1-8b", "temperature": 0.4},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
@@ -813,6 +817,7 @@ CONFIG_METADATA_2 = {
"model_config": {
"model": "llama-3.1-8b",
},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
@@ -829,6 +834,7 @@ CONFIG_METADATA_2 = {
"model": "gemini-1.5-flash",
"temperature": 0.4,
},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
@@ -870,6 +876,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://api.deepseek.com/v1",
"timeout": 120,
"model_config": {"model": "deepseek-chat", "temperature": 0.4},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "tool_use"],
},
@@ -883,6 +890,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://api.302.ai/v1",
"timeout": 120,
"model_config": {"model": "gpt-4.1-mini", "temperature": 0.4},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
@@ -899,6 +907,7 @@ CONFIG_METADATA_2 = {
"model": "deepseek-ai/DeepSeek-V3",
"temperature": 0.4,
},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
@@ -915,6 +924,7 @@ CONFIG_METADATA_2 = {
"model": "deepseek/deepseek-r1",
"temperature": 0.4,
},
"custom_headers": {},
"custom_extra_body": {},
},
"小马算力": {
@@ -930,6 +940,7 @@ CONFIG_METADATA_2 = {
"model": "kimi-k2-instruct-0905",
"temperature": 0.7,
},
"custom_headers": {},
"custom_extra_body": {},
},
"优云智算": {
@@ -944,6 +955,7 @@ CONFIG_METADATA_2 = {
"model_config": {
"model": "moonshotai/Kimi-K2-Instruct",
},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
@@ -957,6 +969,7 @@ CONFIG_METADATA_2 = {
"timeout": 120,
"api_base": "https://api.moonshot.cn/v1",
"model_config": {"model": "moonshot-v1-8k", "temperature": 0.4},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
@@ -972,6 +985,8 @@ CONFIG_METADATA_2 = {
"model_config": {
"model": "glm-4-flash",
},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
"Dify": {
@@ -1028,6 +1043,7 @@ CONFIG_METADATA_2 = {
"timeout": 120,
"api_base": "https://api-inference.modelscope.cn/v1",
"model_config": {"model": "Qwen/Qwen3-32B", "temperature": 0.4},
"custom_headers": {},
"custom_extra_body": {},
"modalities": ["text", "image", "tool_use"],
},
@@ -1040,6 +1056,7 @@ CONFIG_METADATA_2 = {
"key": [],
"api_base": "https://api.fastgpt.in/api/v1",
"timeout": 60,
"custom_headers": {},
"custom_extra_body": {},
},
"Whisper(API)": {
@@ -1321,6 +1338,12 @@ CONFIG_METADATA_2 = {
"render_type": "checkbox",
"hint": "模型支持的模态。如所填写的模型不支持图像,请取消勾选图像。",
},
"custom_headers": {
"description": "自定义添加请求头",
"type": "dict",
"items": {},
"hint": "此处添加的键值对将被合并到 OpenAI SDK 的 default_headers 中,用于自定义 HTTP 请求头。值必须为字符串。",
},
"custom_extra_body": {
"description": "自定义请求体参数",
"type": "dict",

View File

@@ -429,6 +429,10 @@ class LLMRequestSubStage(Stage):
logger.error(f"选择的提供商类型无效({type(provider)}),跳过 LLM 请求处理。")
return
streaming_response = self.streaming_response
if (enable_streaming := event.get_extra("enable_streaming")) is not None:
streaming_response = bool(enable_streaming)
if event.get_extra("provider_request"):
req = event.get_extra("provider_request")
assert isinstance(req, ProviderRequest), (
@@ -548,7 +552,7 @@ class LLMRequestSubStage(Stage):
provider=provider,
first_provider_request=req,
curr_provider_request=req,
streaming=self.streaming_response,
streaming=streaming_response,
event=event,
)
await agent_runner.reset(
@@ -560,10 +564,10 @@ class LLMRequestSubStage(Stage):
),
tool_executor=FunctionToolExecutor(),
agent_hooks=MAIN_AGENT_HOOKS,
streaming=self.streaming_response,
streaming=streaming_response,
)
if self.streaming_response:
if streaming_response:
# 流式响应
event.set_result(
MessageEventResult()

View File

@@ -1,7 +1,7 @@
import asyncio
import base64
import binascii
import sys
from collections.abc import AsyncGenerator
from io import BytesIO
from pathlib import Path
@@ -21,11 +21,6 @@ from astrbot.api.platform import AstrBotMessage, At, PlatformMetadata
from .client import DiscordBotClient
from .components import DiscordEmbed, DiscordView
if sys.version_info >= (3, 12):
from typing import override
else:
from typing_extensions import override
# 自定义Discord视图组件兼容旧版本
class DiscordViewComponent(BaseMessageComponent):
@@ -49,7 +44,6 @@ class DiscordPlatformEvent(AstrMessageEvent):
self.client = client
self.interaction_followup_webhook = interaction_followup_webhook
@override
async def send(self, message: MessageChain):
"""发送消息到Discord平台"""
# 解析消息链为 Discord 所需的对象
@@ -98,6 +92,21 @@ class DiscordPlatformEvent(AstrMessageEvent):
await super().send(message)
async def send_streaming(
self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False
):
buffer = None
async for chain in generator:
if not buffer:
buffer = chain
else:
buffer.chain.extend(chain.chain)
if not buffer:
return None
buffer.squash_plain()
await self.send(buffer)
return await super().send_streaming(generator, use_fallback)
async def _get_channel(self) -> discord.abc.Messageable | None:
"""获取当前事件对应的频道对象"""
try:

View File

@@ -163,6 +163,9 @@ class WebChatAdapter(Platform):
_, _, payload = message.raw_message # type: ignore
message_event.set_extra("selected_provider", payload.get("selected_provider"))
message_event.set_extra("selected_model", payload.get("selected_model"))
message_event.set_extra(
"enable_streaming", payload.get("enable_streaming", True)
)
self.commit_event(message_event)

View File

@@ -1,6 +1,7 @@
import asyncio
import base64
import io
from collections.abc import AsyncGenerator
from typing import TYPE_CHECKING
import aiohttp
@@ -50,6 +51,21 @@ class WeChatPadProMessageEvent(AstrMessageEvent):
await self._send_voice(session, comp)
await super().send(message)
async def send_streaming(
self, generator: AsyncGenerator[MessageChain, None], use_fallback: bool = False
):
buffer = None
async for chain in generator:
if not buffer:
buffer = chain
else:
buffer.chain.extend(chain.chain)
if not buffer:
return None
buffer.squash_plain()
await self.send(buffer)
return await super().send_streaming(generator, use_fallback)
async def _send_image(self, session: aiohttp.ClientSession, comp: Image):
b64 = await comp.convert_to_base64()
raw = self._validate_base64(b64)

View File

@@ -43,14 +43,23 @@ class ProviderOpenAIOfficial(Provider):
self.api_keys: list = super().get_keys()
self.chosen_api_key = self.api_keys[0] if len(self.api_keys) > 0 else None
self.timeout = provider_config.get("timeout", 120)
self.custom_headers = provider_config.get("custom_headers", {})
if isinstance(self.timeout, str):
self.timeout = int(self.timeout)
if not isinstance(self.custom_headers, dict) or not self.custom_headers:
self.custom_headers = None
else:
for key in self.custom_headers:
self.custom_headers[key] = str(self.custom_headers[key])
# 适配 azure openai #332
if "api_version" in provider_config:
# 使用 azure api
self.client = AsyncAzureOpenAI(
api_key=self.chosen_api_key,
api_version=provider_config.get("api_version", None),
default_headers=self.custom_headers,
base_url=provider_config.get("api_base", ""),
timeout=self.timeout,
)
@@ -59,6 +68,7 @@ class ProviderOpenAIOfficial(Provider):
self.client = AsyncOpenAI(
api_key=self.chosen_api_key,
base_url=provider_config.get("api_base", None),
default_headers=self.custom_headers,
timeout=self.timeout,
)

View File

@@ -125,6 +125,8 @@ class ChatRoute(Route):
audio_url = post_data.get("audio_url")
selected_provider = post_data.get("selected_provider")
selected_model = post_data.get("selected_model")
enable_streaming = post_data.get("enable_streaming", True) # 默认为 True
if not message and not image_url and not audio_url:
return (
Response()
@@ -224,6 +226,7 @@ class ChatRoute(Route):
"audio_url": audio_url,
"selected_provider": selected_provider,
"selected_model": selected_model,
"enable_streaming": enable_streaming,
},
),
)

View File

@@ -296,7 +296,15 @@ class ToolsRoute(Route):
"""获取所有注册的工具列表"""
try:
tools = self.tool_mgr.func_list
tools_dict = [tool.__dict__() for tool in tools]
tools_dict = [
{
"name": tool.name,
"description": tool.description,
"parameters": tool.parameters,
"active": tool.active,
}
for tool in tools
]
return Response().ok(data=tools_dict).__dict__
except Exception as e:
logger.error(traceback.format_exc())

134
build_nuitka.py Normal file
View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""
Use Nuitka to build the AstrBot project into standalone executables
"""
import os
import platform
import subprocess
import sys
from pathlib import Path
def get_platform_info():
"""fetch the current platform information"""
system = platform.system()
machine = platform.machine()
return system, machine
def build_with_nuitka():
"""use Nuitka to build the project"""
system, machine = get_platform_info()
print(f"🚀 Starting build for {system} ({machine}) platform...")
# Output directory
output_dir = Path("build/nuitka")
output_dir.mkdir(parents=True, exist_ok=True)
# Base Nuitka command
nuitka_cmd = [
sys.executable,
"-m",
"nuitka",
"--standalone", # Create standalone directory
"--onefile", # Single file mode
"--follow-imports", # Follow all imports
"--enable-plugin=multiprocessing", # Enable multiprocessing support
"--output-dir=build/nuitka", # Output directory
"--quiet", # Reduce output verbosity
"--assume-yes-for-downloads", # Automatically download dependencies
"--jobs=4", # Use multiple CPU cores
]
# include specific packages
include_packages = [
"astrbot",
]
for pkg in include_packages:
nuitka_cmd.extend([f"--include-package={pkg}"])
# include data directories
# data_includes = [
# "data/config",
# "data/plugins",
# "data/temp",
# ]
# for data_dir in data_includes:
# if os.path.exists(data_dir):
# nuitka_cmd.extend([f"--include-data-dir={data_dir}={data_dir}"])
# include packages directory (built-in plugins)
# if os.path.exists("packages"):
# nuitka_cmd.extend(["--include-data-dir=packages=packages"])
# Platform specific settings
if system == "Darwin": # macOS
nuitka_cmd.extend(
[
"--macos-create-app-bundle", # Create .app bundle
"--macos-app-name=AstrBot",
]
)
# macOS icon (if exists)
icon_path = "dashboard/src-tauri/icons/icon.icns"
if os.path.exists(icon_path):
nuitka_cmd.extend([f"--macos-app-icon={icon_path}"])
elif system == "Windows":
nuitka_cmd.extend(
[
"--windows-console-mode=disable", # 无控制台窗口
]
)
# Windows icon (if exists)
icon_path = "dashboard/src-tauri/icons/icon.ico"
if os.path.exists(icon_path):
nuitka_cmd.extend([f"--windows-icon-from-ico={icon_path}"])
# Main file to compile
nuitka_cmd.append("main.py")
print(f"📦 Executing command: {' '.join(nuitka_cmd)}")
try:
subprocess.run(nuitka_cmd, check=True)
print("✅ Nuitka build successful!")
# Find the generated executable
if system == "Darwin":
built_file = list(output_dir.glob("*.app"))
if built_file:
print(f"Generated macOS app: {built_file[0]}")
elif system == "Windows":
built_file = list(output_dir.glob("*.exe"))
if built_file:
print(f"Generated Windows executable: {built_file[0]}")
else: # Linux
built_file = list(output_dir.glob("main.bin"))
if built_file:
print(f"Generated Linux executable: {built_file[0]}")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Nuitka build failed: {e}")
return False
if __name__ == "__main__":
print("=" * 60)
print("AstrBot Nuitka Builder")
print("=" * 60)
# 构建
if build_with_nuitka():
print("\n" + "=" * 60)
print("🎉 Build Complete!")
print("=" * 60)
else:
print("\n" + "=" * 60)
print("❌ Build Failed")
print("=" * 60)
sys.exit(1)

134
build_pyinstaller.py Normal file
View File

@@ -0,0 +1,134 @@
#!/usr/bin/env python3
"""
Use PyInstaller to build the AstrBot project into standalone executables
"""
import platform
import subprocess
import sys
from pathlib import Path
def get_platform_info():
"""fetch the current platform information"""
system = platform.system()
machine = platform.machine()
return system, machine
def build_with_pyinstaller():
"""use PyInstaller to build the project"""
system, machine = get_platform_info()
print(f"🚀 Starting build for {system} ({machine}) platform...")
# Output directory
output_dir = Path("build/pyinstaller")
output_dir.mkdir(parents=True, exist_ok=True)
# Base PyInstaller command
pyinstaller_cmd = [
sys.executable,
"-m",
"PyInstaller",
"--clean", # Clean cache before build
"--noconfirm", # Replace output directory without asking
"--onefile", # Single file mode
"--distpath=build/pyinstaller/dist", # Distribution directory
"--workpath=build/pyinstaller/build", # Work directory
"--specpath=build/pyinstaller", # Spec file directory
"--name=AstrBot", # Output executable name
]
# Platform specific settings
# if system == "Darwin": # macOS
# # macOS icon (if exists)
# icon_path = "dashboard/src-tauri/icons/icon.icns"
# if os.path.exists(icon_path):
# pyinstaller_cmd.extend([f"--icon={icon_path}"])
# # Create .app bundle
# pyinstaller_cmd.extend(["--windowed"])
# elif system == "Windows":
# # Windows icon (if exists)
# icon_path = "dashboard/src-tauri/icons/icon.ico"
# if os.path.exists(icon_path):
# pyinstaller_cmd.extend([f"--icon={icon_path}"])
# # No console window
# pyinstaller_cmd.extend(["--windowed"])
# else: # Linux
# pyinstaller_cmd.extend(["--console"])
# Main file to compile
pyinstaller_cmd.append("main.py")
print(f"📦 Executing command: {' '.join(pyinstaller_cmd)}")
try:
subprocess.run(pyinstaller_cmd, check=True)
print("✅ PyInstaller build successful!")
# Find the generated executable
dist_dir = output_dir / "dist"
if system == "Darwin":
built_file = list(dist_dir.glob("AstrBot.app"))
if not built_file:
built_file = list(dist_dir.glob("AstrBot"))
if built_file:
print(f"📱 Generated macOS app: {built_file[0]}")
elif system == "Windows":
built_file = list(dist_dir.glob("AstrBot.exe"))
if built_file:
print(f"💻 Generated Windows executable: {built_file[0]}")
else: # Linux
built_file = list(dist_dir.glob("AstrBot"))
if built_file:
print(f"🐧 Generated Linux executable: {built_file[0]}")
print(f"\n📁 Output directory: {dist_dir.absolute()}")
return True
except subprocess.CalledProcessError as e:
print(f"❌ PyInstaller build failed: {e}")
return False
except Exception as e:
print(f"❌ Unexpected error: {e}")
return False
def install_pyinstaller():
"""Install PyInstaller if not already installed"""
try:
import PyInstaller
print(f"✅ PyInstaller already installed (version {PyInstaller.__version__})")
return True
except ImportError:
print("📥 PyInstaller not found, installing...")
try:
subprocess.run(
[sys.executable, "-m", "pip", "install", "pyinstaller"], check=True
)
print("✅ PyInstaller installed successfully!")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Failed to install PyInstaller: {e}")
return False
if __name__ == "__main__":
print("=" * 60)
print("AstrBot PyInstaller Builder")
print("=" * 60)
# Check and install PyInstaller
if not install_pyinstaller():
sys.exit(1)
# Build
if build_with_pyinstaller():
print("\n" + "=" * 60)
print("🎉 Build Complete!")
print("=" * 60)
else:
print("\n" + "=" * 60)
print("❌ Build Failed")
print("=" * 60)
sys.exit(1)

5
changelogs/v4.5.3.md Normal file
View File

@@ -0,0 +1,5 @@
## What's Changed
> hotfix version of 4.5.2
1. 修复:修正 `get_tool_list` 方法中工具字典推导式的错误导致的 WebUI MCP 页面工具列表无法显示的问题。

5
changelogs/v4.5.4.md Normal file
View File

@@ -0,0 +1,5 @@
## What's Changed
1. 修复Docker 镜像部分依赖问题导致某些情况下无法启动容器的问题;
2. 优化:插件卡片样式
3. 修复:部分情况下 Windows 一键启动部署时,更新 / 部署失败的问题;

3
changelogs/v4.5.5.md Normal file
View File

@@ -0,0 +1,3 @@
## What's Changed
1. 修复:部署失败

3
changelogs/v4.5.6.md Normal file
View File

@@ -0,0 +1,3 @@
## What's Changed
1. 修复:构建失败

225
dashboard/TAURI_README.md Normal file
View File

@@ -0,0 +1,225 @@
# AstrBot Dashboard - Tauri 桌面应用
本项目现已支持通过 Tauri 构建为桌面应用,同时保持与 Web 版本的兼容性。
## 环境要求
### 系统依赖
**macOS:**
```bash
# 安装 Xcode Command Line Tools
xcode-select --install
```
**Windows:**
- 安装 [Microsoft Visual Studio C++ Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
- 安装 [WebView2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)
**Linux (Ubuntu/Debian):**
```bash
sudo apt update
sudo apt install libwebkit2gtk-4.0-dev \
build-essential \
curl \
wget \
file \
libssl-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev
```
### Rust 环境
```bash
# 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 验证安装
rustc --version
cargo --version
```
## 安装依赖
```bash
cd dashboard
npm install
```
## 开发模式
### Web 端开发(不变)
```bash
npm run dev
```
访问 http://localhost:3000
### 桌面端开发
```bash
npm run tauri:dev
```
这会同时启动:
1. Vite 开发服务器(端口 3000
2. Tauri 桌面应用窗口
热重载功能正常工作,修改代码后会自动刷新。
## 构建
### Web 端构建(不变)
```bash
npm run build
```
输出目录:`dist/`
### 桌面端构建
```bash
npm run tauri:build
```
构建产物位置:
- **macOS**: `src-tauri/target/release/bundle/dmg/`
- **Windows**: `src-tauri/target/release/bundle/msi/`
- **Linux**: `src-tauri/target/release/bundle/deb/``appimage/`
## 图标设置
### 自动生成图标
准备一个至少 512x512 像素的 PNG 图标,然后运行:
```bash
npm run tauri icon path/to/your/icon.png
```
### 手动设置图标
将以下图标放入 `src-tauri/icons/` 目录:
- `32x32.png`
- `128x128.png`
- `128x128@2x.png`
- `icon.icns` (macOS)
- `icon.ico` (Windows)
## 代码兼容性
项目已配置为同时支持 Web 和桌面端,使用相同的代码库。
### 环境检测工具
`src/utils/tauri.ts` 中提供了环境检测工具:
```typescript
import { isTauri, isWeb, PlatformAPI } from '@/utils/tauri';
// 检测运行环境
if (isTauri()) {
console.log('运行在桌面应用中');
} else {
console.log('运行在浏览器中');
}
// 获取正确的 API 端点
const baseURL = PlatformAPI.getBaseURL();
```
### API 调用注意事项
- **Web 端**: 使用 Vite 代理API 路径为 `/api/*`
- **桌面端**: 直接连接到 `http://127.0.0.1:6185`
已在 `PlatformAPI.getBaseURL()` 中处理,使用 axios 时:
```typescript
import axios from 'axios';
import { PlatformAPI } from '@/utils/tauri';
const api = axios.create({
baseURL: PlatformAPI.getBaseURL()
});
```
## 配置说明
### tauri.conf.json
主要配置项:
- `build.devPath`: 开发服务器地址http://localhost:3000
- `build.distDir`: 构建输出目录(../dist
- `tauri.allowlist`: API 权限配置
- `tauri.windows`: 窗口配置(大小、标题等)
### 安全性
默认配置已启用必要的权限:
- 文件系统访问(限定在 APPDATA 目录)
- HTTP 请求(限定到本地后端)
- 窗口控制
- 对话框(打开/保存文件)
可在 `tauri.conf.json``allowlist` 部分调整权限。
## 后端连接
桌面应用需要后端服务运行在 `http://127.0.0.1:6185`
### 启动流程
1. 启动 AstrBot 后端:
```bash
cd /path/to/AstrBot
uv run main.py
```
2. 启动桌面应用:
```bash
cd dashboard
npm run tauri:dev
```
或直接运行打包后的应用(后端需要已启动)。
## 常见问题
### Q: 桌面应用无法连接到后端?
确保:
1. AstrBot 后端正在运行(`uv run main.py`
2. 后端监听在 `127.0.0.1:6185`
3. 防火墙未阻止连接
### Q: 图标未显示?
检查 `src-tauri/icons/` 目录中是否有所需的图标文件,或使用 `npm run tauri icon` 命令生成。
### Q: 构建失败?
- 确保已安装 Rust 和系统依赖
- 运行 `cargo clean` 清理缓存后重试
- 检查 Rust 版本(需要 1.60+
### Q: Web 端功能是否受影响?
不受影响。`npm run dev` 和 `npm run build` 的行为完全不变。
## 开发建议
1. **优先使用 Web 端开发**: 更快的热重载,更好的调试体验
2. **定期测试桌面端**: 确保跨平台兼容性
3. **使用环境检测**: 针对不同平台提供最佳体验
4. **注意 API 差异**: Web 和桌面端的某些 API 可能有差异
## 更多资源
- [Tauri 官方文档](https://tauri.app/)
- [Tauri API 参考](https://tauri.app/v1/api/js/)
- [Tauri Discord 社区](https://discord.com/invite/tauri)

View File

@@ -10,10 +10,14 @@
"build-prod": "vue-tsc --noEmit && vite build --base=/vue/free/",
"preview": "vite preview --port 5050",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"tauri": "tauri",
"tauri:dev": "tauri dev",
"tauri:build": "tauri build"
},
"dependencies": {
"@guolao/vue-monaco-editor": "^1.5.4",
"@tauri-apps/api": "^2.9.0",
"@tiptap/starter-kit": "2.1.7",
"@tiptap/vue-3": "2.1.7",
"apexcharts": "3.42.0",
@@ -43,6 +47,7 @@
"devDependencies": {
"@mdi/font": "7.2.96",
"@rushstack/eslint-patch": "1.3.3",
"@tauri-apps/cli": "^2.9.4",
"@types/chance": "1.1.3",
"@types/markdown-it": "^14.1.2",
"@types/node": "^20.5.7",

4509
dashboard/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
dashboard/src-tauri/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# Tauri specific
src-tauri/target/
src-tauri/WixTools/

4692
dashboard/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
[package]
name = "astrbot-dashboard"
version = "4.5.6"
description = "AstrBot"
authors = ["AstrBot Team"]
license = "AGPL-3.0"
repository = "https://github.com/AstrBotDevs/AstrBot"
default-run = "astrbot-dashboard"
edition = "2021"
rust-version = "1.91.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "2.9.2", features = ["macos-private-api", "protocol-asset"] }
tauri-plugin-opener = "2"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@@ -0,0 +1,104 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::process::{Child, Command};
use std::sync::Mutex;
use tauri::{AppHandle, Emitter, Listener, Manager, State};
struct BackendProcess(Mutex<Option<Child>>);
fn start_backend_process(app_handle: &AppHandle) -> Option<Child> {
#[cfg(target_os = "macos")]
let backend_path = "astrbot-backend.app/Contents/MacOS/main";
#[cfg(target_os = "windows")]
let backend_path = "astrbot-backend.exe";
#[cfg(target_os = "linux")]
let backend_path = "astrbot-backend";
// 获取资源目录
let resource_dir = match app_handle
.path()
.resource_dir()
{
Ok(dir) => dir,
Err(e) => {
eprintln!("Failed to get resource directory: {}", e);
return None;
}
};
let full_backend_path = resource_dir.join(backend_path);
println!("Starting backend process at: {:?}", full_backend_path);
match Command::new(&full_backend_path).spawn() {
Ok(child) => {
println!(
"Backend process started successfully with PID: {}",
child.id()
);
Some(child)
}
Err(e) => {
eprintln!("Failed to start backend process: {}", e);
None
}
}
}
#[tauri::command]
fn restart_backend(
app_handle: AppHandle,
backend_state: State<BackendProcess>,
) -> Result<String, String> {
let mut backend = backend_state.0.lock().unwrap();
// 停止现有进程
if let Some(mut child) = backend.take() {
let _ = child.kill();
let _ = child.wait();
}
// 启动新进程
*backend = start_backend_process(&app_handle);
if backend.is_some() {
Ok("Backend restarted successfully".to_string())
} else {
Err("Failed to restart backend".to_string())
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
// 启动后端进程
let backend_process = start_backend_process(app.handle());
app.manage(BackendProcess(Mutex::new(backend_process)));
Ok(())
})
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![restart_backend])
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { .. } = event {
// 关闭窗口时清理后端进程
if let Some(backend_state) = window.app_handle().try_state::<BackendProcess>() {
let mut backend = backend_state.0.lock().unwrap();
if let Some(mut child) = backend.take() {
let _ = child.kill();
let _ = child.wait();
}
}
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn main() {
run();
}

View File

@@ -0,0 +1,53 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "AstrBot",
"version": "4.5.6",
"identifier": "com.astrbot.app",
"build": {
"beforeDevCommand": "pnpm dev",
"devUrl": "http://localhost:3000",
"beforeBuildCommand": "pnpm build",
"frontendDist": "../dist"
},
"app": {
"withGlobalTauri": true,
"macOSPrivateApi": true,
"windows": [
{
"title": "AstrBot",
"label": "main",
"url": "/",
"width": 1400,
"height": 900
}
],
"security": {
"csp": null,
"assetProtocol": {
"enable": true,
"scope": [
"$APPDATA/**"
]
}
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"resources": [
"resources/*"
]
},
"plugins": {
"fs": {
"requireLiteralLeadingDot": false
}
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -1,55 +1,70 @@
<template>
<v-card class="chat-page-card">
<v-card class="chat-page-card" elevation="0" rounded="0">
<v-card-text class="chat-page-container">
<!-- 遮罩层 (手机端) -->
<div class="mobile-overlay" v-if="isMobile && mobileMenuOpen" @click="closeMobileSidebar"></div>
<div class="chat-layout">
<div class="sidebar-panel" :class="{ 'sidebar-collapsed': sidebarCollapsed }"
:style="{ 'background-color': isDark ? sidebarCollapsed ? '#1e1e1e' : '#2d2d2d' : sidebarCollapsed ? '#ffffff' : '#f5f5f5' }"
<div class="sidebar-panel"
:class="{
'sidebar-collapsed': sidebarCollapsed && !isMobile,
'mobile-sidebar-open': isMobile && mobileMenuOpen,
'mobile-sidebar': isMobile
}"
:style="{ 'background-color': isDark ? sidebarCollapsed ? '#1e1e1e' : '#2d2d2d' : sidebarCollapsed ? '#ffffff' : '#f1f4f9' }"
@mouseenter="handleSidebarMouseEnter" @mouseleave="handleSidebarMouseLeave">
<div style="display: flex; align-items: center; justify-content: center; padding: 16px; padding-bottom: 0px;"
v-if="chatboxMode">
<img width="50" src="@/assets/images/astrbot_logo_mini.webp" alt="AstrBot Logo">
<img width="50" src="@/assets/images/icon-no-shadow.svg" alt="AstrBot Logo">
<span v-if="!sidebarCollapsed"
style="font-weight: 1000; font-size: 26px; margin-left: 8px;">AstrBot</span>
</div>
<div class="sidebar-collapse-btn-container">
<div class="sidebar-collapse-btn-container" v-if="!isMobile">
<v-btn icon class="sidebar-collapse-btn" @click="toggleSidebar" variant="text"
color="deep-purple">
<v-icon>{{ (sidebarCollapsed || (!sidebarCollapsed && sidebarHoverExpanded)) ?
'mdi-chevron-right' : 'mdi-chevron-left' }}</v-icon>
</v-btn>
</div>
<!-- 手机端关闭按钮 -->
<div class="sidebar-collapse-btn-container" v-if="isMobile">
<v-btn icon class="sidebar-collapse-btn" @click="closeMobileSidebar" variant="text"
color="deep-purple">
<v-icon>mdi-close</v-icon>
</v-btn>
</div>
<div style="padding: 16px; padding-top: 8px;">
<v-btn block variant="text" class="new-chat-btn" @click="newC" :disabled="!currCid"
v-if="!sidebarCollapsed" prepend-icon="mdi-plus"
v-if="!sidebarCollapsed || isMobile" prepend-icon="mdi-plus"
style="background-color: transparent !important; border-radius: 4px;">{{
tm('actions.newChat') }}</v-btn>
<v-btn icon="mdi-plus" rounded="lg" @click="newC" :disabled="!currCid" v-if="sidebarCollapsed"
<v-btn icon="mdi-plus" rounded="lg" @click="newC" :disabled="!currCid" v-if="sidebarCollapsed && !isMobile"
elevation="0"></v-btn>
</div>
<div v-if="!sidebarCollapsed">
<div v-if="!sidebarCollapsed || isMobile">
<v-divider class="mx-4"></v-divider>
</div>
<div style="overflow-y: auto; flex-grow: 1;" :class="{ 'fade-in': sidebarHoverExpanded }"
v-if="!sidebarCollapsed">
v-if="!sidebarCollapsed || isMobile">
<v-card v-if="conversations.length > 0" flat style="background-color: transparent;">
<v-list density="compact" nav class="conversation-list"
style="background-color: transparent;" v-model:selected="selectedConversations"
@update:selected="getConversationMessages">
<v-list-item v-for="(item, i) in conversations" :key="item.cid" :value="item.cid"
rounded="lg" class="conversation-item" active-color="secondary">
<v-list-item-title v-if="!sidebarCollapsed" class="conversation-title">{{ item.title
<v-list-item-title v-if="!sidebarCollapsed || isMobile" class="conversation-title">{{ item.title
|| tm('conversation.newConversation') }}</v-list-item-title>
<v-list-item-subtitle v-if="!sidebarCollapsed" class="timestamp">{{
<v-list-item-subtitle v-if="!sidebarCollapsed || isMobile" class="timestamp">{{
formatDate(item.updated_at)
}}</v-list-item-subtitle>
}}</v-list-item-subtitle>
<template v-if="!sidebarCollapsed" v-slot:append>
<template v-if="!sidebarCollapsed || isMobile" v-slot:append>
<div class="conversation-actions">
<v-btn icon="mdi-pencil" size="x-small" variant="text"
class="edit-title-btn"
@@ -66,7 +81,7 @@
<v-fade-transition>
<div class="no-conversations" v-if="conversations.length === 0">
<v-icon icon="mdi-message-text-outline" size="large" color="grey-lighten-1"></v-icon>
<div class="no-conversations-text" v-if="!sidebarCollapsed || sidebarHoverExpanded">
<div class="no-conversations-text" v-if="!sidebarCollapsed || sidebarHoverExpanded || isMobile">
{{ tm('conversation.noHistory') }}</div>
</div>
</v-fade-transition>
@@ -78,12 +93,17 @@
<div class="chat-content-panel">
<div class="conversation-header fade-in">
<div v-if="currCid && getCurrentConversation">
<!-- 手机端菜单按钮 -->
<v-btn icon class="mobile-menu-btn" @click="toggleMobileSidebar" v-if="isMobile" variant="text">
<v-icon>mdi-menu</v-icon>
</v-btn>
<!-- <div v-if="currCid && getCurrentConversation">
<h3
style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ getCurrentConversation.title || tm('conversation.newConversation') }}</h3>
<span style="font-size: 12px;">{{ formatDate(getCurrentConversation.updated_at) }}</span>
</div>
</div> -->
<div class="conversation-header-actions">
<!-- router 推送到 /chatbox -->
<v-tooltip :text="tm('actions.fullscreen')" v-if="!chatboxMode">
@@ -117,7 +137,6 @@
</v-tooltip>
</div>
</div>
<v-divider v-if="currCid && getCurrentConversation" class="conversation-divider"></v-divider>
<MessageList v-if="messages && messages.length > 0" :messages="messages" :isDark="isDark"
:isStreaming="isStreaming || isConvRunning" @openImagePreview="openImagePreview"
@@ -146,17 +165,30 @@
<!-- 输入区域 -->
<div class="input-area fade-in">
<div
<div class="input-container"
style="width: 85%; max-width: 900px; margin: 0 auto; border: 1px solid #e0e0e0; border-radius: 24px;">
<textarea id="input-field" v-model="prompt" @keydown="handleInputKeyDown"
:disabled="isStreaming" @click:clear="clearMessage"
placeholder="Ask AstrBot..."
:disabled="isStreaming" @click:clear="clearMessage" placeholder="Ask AstrBot..."
style="width: 100%; resize: none; outline: none; border: 1px solid var(--v-theme-border); border-radius: 12px; padding: 8px 16px; min-height: 40px; font-family: inherit; font-size: 16px; background-color: var(--v-theme-surface);"></textarea>
<div
style="display: flex; justify-content: space-between; align-items: center; padding: 0px 8px;">
<div style="display: flex; justify-content: flex-start; margin-top: 4px;">
style="display: flex; justify-content: space-between; align-items: center; padding: 0px 12px;">
<div
style="display: flex; justify-content: flex-start; margin-top: 4px; align-items: center; gap: 8px;">
<!-- 选择提供商和模型 -->
<ProviderModelSelector ref="providerModelSelector" />
<!-- 流式响应开关 -->
<v-tooltip
:text="enableStreaming ? tm('streaming.enabled') : tm('streaming.disabled')"
location="top">
<template v-slot:activator="{ props }">
<v-chip v-bind="props" @click="toggleStreaming" size="x-small"
class="streaming-toggle-chip">
<v-icon start :icon="enableStreaming ? 'mdi-flash' : 'mdi-flash-off'"
size="small"></v-icon>
{{ enableStreaming ? tm('streaming.on') : tm('streaming.off') }}
</v-chip>
</template>
</v-tooltip>
</div>
<div
style="display: flex; justify-content: flex-end; margin-top: 8px; align-items: center;">
@@ -175,7 +207,6 @@
class="send-btn" size="small" />
</div>
</div>
</div>
<!-- 附件预览区 -->
@@ -242,6 +273,7 @@ import ProviderModelSelector from '@/components/chat/ProviderModelSelector.vue';
import MessageList from '@/components/chat/MessageList.vue';
import 'highlight.js/styles/github.css';
import { useToast } from '@/utils/toast';
import { useTheme } from 'vuetify';
export default {
name: 'ChatPage',
@@ -258,10 +290,12 @@ export default {
}, setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/chat');
const theme = useTheme();
return {
t,
tm,
theme,
router,
ref
};
@@ -287,7 +321,7 @@ export default {
// Ctrl键长按相关变量
ctrlKeyDown: false,
ctrlKeyTimer: null,
ctrlKeyLongPressThreshold: 300, // 长按阈值单位毫秒
ctrlKeyLongPressThreshold: 300, // 长按阈值,单位毫秒
mediaCache: {}, // Add a cache to store media blobs
@@ -313,6 +347,13 @@ export default {
isToastedRunningInfo: false, // To avoid multiple toasts
activeSSECount: 0, // Track number of active SSE connections
// 流式响应开关
enableStreaming: true, // 默认开启流式响应
// 手机端相关变量
isMobile: false,
mobileMenuOpen: false,
}
},
@@ -391,6 +432,18 @@ export default {
this.sidebarCollapsed = true; // 默认折叠状态
}
// 从 localStorage 读取流式响应开关状态,默认为 true开启
const savedStreamingState = localStorage.getItem('enableStreaming');
if (savedStreamingState !== null) {
this.enableStreaming = JSON.parse(savedStreamingState);
} else {
this.enableStreaming = true; // 默认开启
}
// 检测是否为手机端
this.checkMobile();
window.addEventListener('resize', this.checkMobile);
// 设置输入框标签
this.inputFieldLabel = this.tm('input.chatPrompt');
this.getConversations();
@@ -413,6 +466,9 @@ export default {
beforeUnmount() {
// 移除keyup事件监听
document.removeEventListener('keyup', this.handleInputKeyUp);
// 移除resize事件监听
window.removeEventListener('resize', this.checkMobile);
// 清除悬停定时器
if (this.sidebarHoverTimer) {
@@ -427,6 +483,28 @@ export default {
const customizer = useCustomizerStore();
const newTheme = customizer.uiTheme === 'PurpleTheme' ? 'PurpleThemeDark' : 'PurpleTheme';
customizer.SET_UI_THEME(newTheme);
this.theme.global.name.value = newTheme;
},
// 检测是否为手机端
checkMobile() {
this.isMobile = window.innerWidth <= 768;
// 如果切换到桌面端,关闭手机菜单
if (!this.isMobile) {
this.mobileMenuOpen = false;
}
},
// 切换手机端菜单
toggleMobileSidebar() {
this.mobileMenuOpen = !this.mobileMenuOpen;
},
// 关闭手机端菜单
closeMobileSidebar() {
this.mobileMenuOpen = false;
},
// 切换流式响应
toggleStreaming() {
this.enableStreaming = !this.enableStreaming;
localStorage.setItem('enableStreaming', JSON.stringify(this.enableStreaming));
},
// 切换侧边栏折叠状态
toggleSidebar() {
@@ -441,7 +519,7 @@ export default {
// 侧边栏鼠标悬停处理
handleSidebarMouseEnter() {
if (!this.sidebarCollapsed) return;
if (!this.sidebarCollapsed || this.isMobile) return;
this.sidebarHovered = true;
@@ -668,6 +746,11 @@ export default {
return
}
// 手机端关闭侧边栏
if (this.isMobile) {
this.closeMobileSidebar();
}
axios.get('/api/chat/get_conversation?conversation_id=' + cid[0]).then(async response => {
this.currCid = cid[0];
// Update the selected conversation in the sidebar
@@ -748,6 +831,10 @@ export default {
this.currCid = '';
this.selectedConversations = []; // 清除选中状态
this.messages = [];
// 手机端关闭侧边栏
if (this.isMobile) {
this.closeMobileSidebar();
}
if (this.$route.path.startsWith('/chatbox')) {
this.$router.push('/chatbox');
} else {
@@ -867,7 +954,8 @@ export default {
image_url: imageNamesToSend,
audio_url: audioNameToSend ? [audioNameToSend] : [],
selected_provider: selectedProviderId,
selected_model: selectedModelName
selected_model: selectedModelName,
enable_streaming: this.enableStreaming
})
});
@@ -1101,6 +1189,17 @@ export default {
flex-direction: column;
}
/* 流式响应开关芯片样式 */
.streaming-toggle-chip {
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.streaming-toggle-chip:hover {
opacity: 0.8;
}
.welcome-title {
font-size: 28px;
margin-bottom: 16px;
@@ -1141,7 +1240,6 @@ export default {
width: 100%;
height: 100%;
max-height: 100%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05) !important;
overflow: hidden;
}
@@ -1166,7 +1264,7 @@ export default {
display: flex;
flex-direction: column;
padding: 0;
border-right: 1px solid rgba(0, 0, 0, 0.05);
border-right: 1px solid rgba(0, 0, 0, 0.04);
height: 100%;
max-height: 100%;
position: relative;
@@ -1188,6 +1286,77 @@ export default {
transition: all 0.3s ease;
}
/* 手机端菜单按钮 */
.mobile-menu-btn {
margin-right: 8px;
}
/* 手机端遮罩层 */
.mobile-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
animation: fadeIn 0.3s ease;
}
/* 手机端侧边栏 */
.mobile-sidebar {
position: fixed;
top: 0;
left: 0;
bottom: 0;
max-width: 280px !important;
min-width: 280px !important;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 1000;
}
.mobile-sidebar-open {
transform: translateX(0) !important;
}
/* 手机端样式调整 */
@media (max-width: 768px) {
.sidebar-panel:not(.mobile-sidebar) {
display: none;
}
.chat-content-panel {
width: 100%;
}
/* 手机端去掉容器padding */
.chat-page-container {
padding: 0 !important;
}
/* 手机端输入区域样式 */
.input-area {
padding: 0 !important;
}
.input-container {
width: 100% !important;
max-width: 100% !important;
margin: 0 !important;
border-radius: 0 !important;
border-left: none !important;
border-right: none !important;
border-bottom: none !important;
}
#input-field {
border-radius: 0 !important;
border-left: none !important;
border-right: none !important;
}
}
/* 侧边栏折叠按钮 */
.sidebar-collapse-btn-container {
margin: 16px;
@@ -1267,25 +1436,12 @@ export default {
white-space: nowrap;
}
.status-chips {
display: flex;
flex-wrap: nowrap;
gap: 8px;
margin-bottom: 8px;
transition: opacity 0.25s ease;
}
.status-chips .v-chip {
.v-chip {
flex: 1 1 0;
justify-content: center;
opacity: 0.7;
}
.status-chip {
font-size: 12px;
height: 24px !important;
}
.no-conversations {
display: flex;
flex-direction: column;

View File

@@ -33,7 +33,7 @@
<v-avatar class="bot-avatar" size="36">
<v-progress-circular :index="index" v-if="isStreaming && index === messages.length - 1" indeterminate size="28"
width="2"></v-progress-circular>
<span v-else-if="messages[index - 1]?.content.type !== 'bot'" class="text-h2"></span>
<v-icon v-else-if="messages[index - 1]?.content.type !== 'bot'" size="64" color="#8fb6d2">mdi-star-four-points-small</v-icon>
</v-avatar>
<div class="bot-message-content">
<div class="message-bubble bot-bubble">

View File

@@ -1,13 +1,13 @@
<template>
<div>
<!-- 选择提供商和模型按钮 -->
<v-btn class="text-none" variant="tonal" rounded="xl" size="small"
<v-chip class="text-none" variant="tonal" size="x-small"
v-if="selectedProviderId && selectedModelName" @click="openDialog">
{{ selectedProviderId }} / {{ selectedModelName }}
</v-btn>
<v-btn variant="tonal" rounded="xl" size="small" v-else @click="openDialog">
</v-chip>
<v-chip variant="tonal" rounded="xl" size="x-small" v-else @click="openDialog">
选择模型
</v-btn>
</v-chip>
<!-- 选择提供商和模型对话框 -->
<v-dialog v-model="showDialog" max-width="800" persistent>

View File

@@ -1,22 +1,25 @@
<template>
<div class="d-flex align-center justify-space-between">
<span v-if="!modelValue || (Array.isArray(modelValue) && modelValue.length === 0)"
style="color: rgb(var(--v-theme-primaryText));">
未选择
</span>
<div v-else class="d-flex flex-wrap gap-1">
<v-chip
v-for="name in modelValue"
:key="name"
size="small"
color="primary"
variant="tonal"
closable
@click:close="removeKnowledgeBase(name)">
{{ name }}
</v-chip>
<div class="d-flex align-center justify-space-between" style="gap: 8px;">
<div style="flex: 1; min-width: 0; overflow: hidden;">
<span v-if="!modelValue || (Array.isArray(modelValue) && modelValue.length === 0)"
style="color: rgb(var(--v-theme-primaryText));">
未选择
</span>
<div v-else class="d-flex flex-wrap gap-1">
<v-chip
v-for="name in modelValue"
:key="name"
size="small"
color="primary"
variant="tonal"
closable
@click:close="removeKnowledgeBase(name)"
style="max-width: 100%;">
<span class="text-truncate" style="max-width: 200px;">{{ name }}</span>
</v-chip>
</div>
</div>
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
<v-btn size="small" color="primary" variant="tonal" @click="openDialog" style="flex-shrink: 0;">
{{ buttonText }}
</v-btn>
</div>
@@ -220,4 +223,11 @@ function goToKnowledgeBasePage() {
.gap-1 {
gap: 4px;
}
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
}
</style>

View File

@@ -70,10 +70,6 @@ const formatTitle = (title: string) => {
transition: transform 0.3s ease;
}
.logo-image img:hover {
transform: scale(1.05);
}
.logo-text {
display: flex;
flex-direction: column;

View File

@@ -2,6 +2,7 @@
"login": "Login",
"username": "Username",
"password": "Password",
"defaultHint": "Default username and password: astrbot",
"logo": {
"title": "AstrBot Dashboard",
"subtitle": "Welcome"

View File

@@ -57,6 +57,12 @@
"voiceRecord": "Record Voice",
"pasteImage": "Paste Image"
},
"streaming": {
"enabled": "Streaming enabled",
"disabled": "Streaming disabled",
"on": "Stream",
"off": "Normal"
},
"connection": {
"title": "Connection Status Notice",
"message": "The system detected that the chat connection needs to be re-established.",

View File

@@ -79,6 +79,14 @@
"devDocs": "Extension Development Docs",
"submitRepo": "Submit Extension Repository"
},
"sort": {
"default": "Default",
"stars": "Stars",
"author": "Author",
"updated": "Last Updated",
"ascending": "Ascending",
"descending": "Descending"
},
"tags": {
"danger": "Danger"
},

View File

@@ -2,8 +2,9 @@
"login": "登录",
"username": "用户名",
"password": "密码",
"defaultHint": "默认账户和密码均为astrbot",
"logo": {
"title": "AstrBot 仪表盘",
"title": "AstrBot WebUI",
"subtitle": "欢迎使用"
},
"theme": {

View File

@@ -57,6 +57,12 @@
"voiceRecord": "录制语音",
"pasteImage": "粘贴图片"
},
"streaming": {
"enabled": "流式响应已开启",
"disabled": "流式响应已关闭",
"on": "流式",
"off": "普通"
},
"connection": {
"title": "连接状态提醒",
"message": "系统检测到聊天连接需要重新建立。",

View File

@@ -79,6 +79,14 @@
"devDocs": "插件开发文档",
"submitRepo": "提交插件仓库"
},
"sort": {
"default": "默认排序",
"stars": "Star数",
"author": "作者名",
"updated": "更新时间",
"ascending": "升序",
"descending": "降序"
},
"tags": {
"danger": "危险"
},

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { RouterView } from 'vue-router';
import { ref, onMounted } from 'vue';
import { RouterView, useRoute } from 'vue-router';
import { ref, onMounted, computed } from 'vue';
import axios from 'axios';
import VerticalSidebarVue from './vertical-sidebar/VerticalSidebar.vue';
import VerticalHeaderVue from './vertical-header/VerticalHeader.vue';
@@ -8,6 +8,12 @@ import MigrationDialog from '@/components/shared/MigrationDialog.vue';
import { useCustomizerStore } from '@/stores/customizer';
const customizer = useCustomizerStore();
const route = useRoute();
// 计算是否在聊天页面(非全屏模式)
const isChatPage = computed(() => {
return route.path.startsWith('/chat');
});
const migrationDialog = ref<InstanceType<typeof MigrationDialog> | null>(null);
// 检查是否需要迁移
@@ -45,7 +51,10 @@ onMounted(() => {
<VerticalHeaderVue />
<VerticalSidebarVue />
<v-main>
<v-container fluid class="page-wrapper" style="height: calc(100% - 8px)">
<v-container fluid class="page-wrapper" :style="{
height: 'calc(100% - 8px)',
padding: isChatPage ? '0' : undefined
}">
<div style="height: 100%;">
<RouterView />
</div>

Some files were not shown because too many files have changed in this diff Show More