Compare commits
10 Commits
v4.5.5
...
feat/tauri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c090299b1 | ||
|
|
b360c8446e | ||
|
|
6d00717655 | ||
|
|
bb5f06498e | ||
|
|
aca5743ab6 | ||
|
|
6903032f7e | ||
|
|
1ce0ff87bd | ||
|
|
e39d6bae0b | ||
|
|
8028e9e9a6 | ||
|
|
817f20ea01 |
2
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml
vendored
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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="Featured|HelloGitHub" 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="Featured|HelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.5.5"
|
||||
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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
134
build_nuitka.py
Normal 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
@@ -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)
|
||||
3
changelogs/v4.5.6.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## What's Changed
|
||||
|
||||
1. 修复:构建失败
|
||||
225
dashboard/TAURI_README.md
Normal 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)
|
||||
@@ -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
3
dashboard/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Tauri specific
|
||||
src-tauri/target/
|
||||
src-tauri/WixTools/
|
||||
4692
dashboard/src-tauri/Cargo.lock
generated
Normal file
27
dashboard/src-tauri/Cargo.toml
Normal 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" ]
|
||||
3
dashboard/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
1
dashboard/src-tauri/gen/schemas/acl-manifests.json
Normal file
1
dashboard/src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
2244
dashboard/src-tauri/gen/schemas/desktop-schema.json
Normal file
2244
dashboard/src-tauri/gen/schemas/macOS-schema.json
Normal file
BIN
dashboard/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
dashboard/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
dashboard/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
dashboard/src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 3.2 KiB |
BIN
dashboard/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
dashboard/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 8.2 KiB |
BIN
dashboard/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
dashboard/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
dashboard/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
dashboard/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
dashboard/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
dashboard/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
dashboard/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
dashboard/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -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>
|
||||
BIN
dashboard/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
BIN
dashboard/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
BIN
dashboard/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
BIN
dashboard/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
BIN
dashboard/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
BIN
dashboard/src-tauri/icons/icon.icns
Normal file
BIN
dashboard/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
dashboard/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 47 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 602 B |
BIN
dashboard/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
dashboard/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
104
dashboard/src-tauri/src/main.rs
Normal 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();
|
||||
}
|
||||
|
||||
53
dashboard/src-tauri/tauri.conf.json
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
1
dashboard/src/assets/images/icon-no-shadow.svg
Normal file
|
After Width: | Height: | Size: 58 KiB |
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"login": "Login",
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"defaultHint": "Default username and password: astrbot",
|
||||
"logo": {
|
||||
"title": "AstrBot Dashboard",
|
||||
"subtitle": "Welcome"
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
"login": "登录",
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"defaultHint": "默认账户和密码均为:astrbot",
|
||||
"logo": {
|
||||
"title": "AstrBot 仪表盘",
|
||||
"title": "AstrBot WebUI",
|
||||
"subtitle": "欢迎使用"
|
||||
},
|
||||
"theme": {
|
||||
|
||||
@@ -57,6 +57,12 @@
|
||||
"voiceRecord": "录制语音",
|
||||
"pasteImage": "粘贴图片"
|
||||
},
|
||||
"streaming": {
|
||||
"enabled": "流式响应已开启",
|
||||
"disabled": "流式响应已关闭",
|
||||
"on": "流式",
|
||||
"off": "普通"
|
||||
},
|
||||
"connection": {
|
||||
"title": "连接状态提醒",
|
||||
"message": "系统检测到聊天连接需要重新建立。",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,6 +10,8 @@ import { useCommonStore } from '@/stores/common';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { useI18n } from '@/i18n/composables';
|
||||
import { router } from '@/router';
|
||||
import { useTheme } from 'vuetify';
|
||||
import { isTauri } from '@/utils/tauri';
|
||||
|
||||
// 配置markdown-it,默认安全设置
|
||||
const md = new MarkdownIt({
|
||||
@@ -20,6 +22,7 @@ const md = new MarkdownIt({
|
||||
});
|
||||
|
||||
const customizer = useCustomizerStore();
|
||||
const theme = useTheme();
|
||||
const { t } = useI18n();
|
||||
let dialog = ref(false);
|
||||
let accountWarning = ref(false)
|
||||
@@ -276,7 +279,9 @@ function updateDashboard() {
|
||||
}
|
||||
|
||||
function toggleDarkMode() {
|
||||
customizer.SET_UI_THEME(customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark');
|
||||
const newTheme = customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark';
|
||||
customizer.SET_UI_THEME(newTheme);
|
||||
theme.global.name.value = newTheme;
|
||||
}
|
||||
|
||||
getVersion();
|
||||
|
||||
@@ -18,24 +18,38 @@ setupI18n().then(() => {
|
||||
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(createPinia());
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
app.use(print);
|
||||
app.use(VueApexCharts);
|
||||
app.use(vuetify);
|
||||
app.use(confirmPlugin);
|
||||
app.mount('#app');
|
||||
|
||||
// 挂载后同步 Vuetify 主题
|
||||
import('./stores/customizer').then(({ useCustomizerStore }) => {
|
||||
const customizer = useCustomizerStore(pinia);
|
||||
vuetify.theme.global.name.value = customizer.uiTheme;
|
||||
});
|
||||
}).catch(error => {
|
||||
console.error('❌ 新i18n系统初始化失败:', error);
|
||||
|
||||
// 即使i18n初始化失败,也要挂载应用(使用回退机制)
|
||||
const app = createApp(App);
|
||||
app.use(router);
|
||||
app.use(createPinia());
|
||||
const pinia = createPinia();
|
||||
app.use(pinia);
|
||||
app.use(print);
|
||||
app.use(VueApexCharts);
|
||||
app.use(vuetify);
|
||||
app.use(confirmPlugin);
|
||||
app.mount('#app');
|
||||
|
||||
// 挂载后同步 Vuetify 主题
|
||||
import('./stores/customizer').then(({ useCustomizerStore }) => {
|
||||
const customizer = useCustomizerStore(pinia);
|
||||
vuetify.theme.global.name.value = customizer.uiTheme;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ html {
|
||||
|
||||
.page-wrapper {
|
||||
min-height: calc(100vh - 100px);
|
||||
padding: 15px;
|
||||
padding: 8px;
|
||||
border-radius: $border-radius-root;
|
||||
background: rgb(var(--v-theme-containerBg));
|
||||
}
|
||||
|
||||
71
dashboard/src/utils/tauri.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Tauri 环境检测工具
|
||||
* 用于区分 Web 端和桌面端环境
|
||||
*/
|
||||
|
||||
/**
|
||||
* 检测是否在 Tauri 环境中运行
|
||||
* @returns {boolean} 如果在 Tauri 环境中返回 true,否则返回 false
|
||||
*/
|
||||
export function isTauri(): boolean {
|
||||
return typeof window !== 'undefined' &&
|
||||
(window as any).__TAURI_INTERNALS__ !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否在 Web 环境中运行
|
||||
* @returns {boolean} 如果在 Web 环境中返回 true,否则返回 false
|
||||
*/
|
||||
export function isWeb(): boolean {
|
||||
return !isTauri();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Tauri API(仅在 Tauri 环境中可用)
|
||||
* @returns {any} Tauri API 对象或 null
|
||||
*/
|
||||
export function getTauriAPI(): any {
|
||||
if (isTauri()) {
|
||||
// Tauri 2.0 建议使用 @tauri-apps/api 包而不是全局对象
|
||||
return (window as any).__TAURI_INTERNALS__;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台特定的 API 调用包装器
|
||||
* 在 Web 环境中使用 HTTP API,在 Tauri 环境中可以使用本地 API
|
||||
*/
|
||||
export class PlatformAPI {
|
||||
/**
|
||||
* 根据平台选择合适的 API 端点
|
||||
* @param webEndpoint Web 端 API 地址
|
||||
* @param tauriEndpoint Tauri 端 API 地址(可选,默认使用 webEndpoint)
|
||||
*/
|
||||
static getEndpoint(webEndpoint: string, tauriEndpoint?: string): string {
|
||||
if (isTauri() && tauriEndpoint) {
|
||||
return tauriEndpoint;
|
||||
}
|
||||
return webEndpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取基础 URL
|
||||
* Web 端使用相对路径,Tauri 端使用完整的后端地址
|
||||
*/
|
||||
static getBaseURL(): string {
|
||||
if (isTauri()) {
|
||||
// Tauri 环境中,需要连接到本地运行的后端服务
|
||||
return 'http://127.0.0.1:6185';
|
||||
}
|
||||
// Web 环境中使用相对路径,由 Vite 代理处理
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
isTauri,
|
||||
isWeb,
|
||||
getTauriAPI,
|
||||
PlatformAPI
|
||||
};
|
||||
@@ -10,6 +10,6 @@ import Chat from '@/components/chat/Chat.vue'
|
||||
|
||||
<style scoped>
|
||||
.chat-container {
|
||||
height: calc(100vh - 88px)
|
||||
height: calc(100vh - 60px)
|
||||
}
|
||||
</style>
|
||||
@@ -1,24 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import AuthLogin from '../authForms/AuthLogin.vue';
|
||||
import Logo from '@/components/shared/Logo.vue';
|
||||
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { useRouter } from 'vue-router';
|
||||
import {useCustomizerStore} from "@/stores/customizer";
|
||||
import { useCustomizerStore } from "@/stores/customizer";
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
import { useTheme } from 'vuetify';
|
||||
|
||||
const cardVisible = ref(false);
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const customizer = useCustomizerStore();
|
||||
const { tm: t } = useModuleI18n('features/auth');
|
||||
const theme = useTheme();
|
||||
|
||||
// 主题切换函数
|
||||
function toggleTheme() {
|
||||
customizer.SET_UI_THEME(
|
||||
customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark'
|
||||
);
|
||||
const newTheme = customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark';
|
||||
customizer.SET_UI_THEME(newTheme);
|
||||
theme.global.name.value = newTheme;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -27,7 +28,7 @@ onMounted(() => {
|
||||
router.push(authStore.returnUrl || '/');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 添加一个小延迟以获得更好的动画效果
|
||||
setTimeout(() => {
|
||||
cardVisible.value = true;
|
||||
@@ -36,139 +37,17 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="useCustomizerStore().uiTheme==='PurpleTheme'" class="login-page-container">
|
||||
<div class="login-background"></div>
|
||||
|
||||
<div class="login-container">
|
||||
<!-- 桌面端:卡片样式 -->
|
||||
<v-card
|
||||
v-if="!$vuetify.display.xs"
|
||||
variant="outlined"
|
||||
class="login-card"
|
||||
:class="{ 'card-visible': cardVisible }"
|
||||
>
|
||||
<v-card-text class="pa-10">
|
||||
<div class="logo-wrapper">
|
||||
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
|
||||
</div>
|
||||
<div class="divider-container">
|
||||
<v-divider class="custom-divider"></v-divider>
|
||||
</div>
|
||||
<AuthLogin />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 移动端:全屏样式 -->
|
||||
<div
|
||||
v-else
|
||||
class="mobile-login-container"
|
||||
:class="{ 'mobile-visible': cardVisible }"
|
||||
>
|
||||
<div class="mobile-content">
|
||||
<div class="logo-wrapper">
|
||||
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
|
||||
</div>
|
||||
<div class="divider-container">
|
||||
<v-divider class="custom-divider"></v-divider>
|
||||
</div>
|
||||
<AuthLogin />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 悬浮式圆角工具栏 -->
|
||||
<v-card
|
||||
class="floating-toolbar"
|
||||
:class="{ 'toolbar-visible': cardVisible }"
|
||||
elevation="8"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-card-text class="pa-2">
|
||||
<div class="login-page-container">
|
||||
<v-card class="login-card" elevation="1">
|
||||
<v-card-title>
|
||||
<div class="d-flex justify-space-between align-center w-100">
|
||||
<img width="80" src="@/assets/images/icon-no-shadow.svg" alt="AstrBot Logo">
|
||||
<div class="d-flex align-center gap-1">
|
||||
<LanguageSwitcher />
|
||||
<v-divider vertical class="mx-1" style="height: 24px !important; opacity: 0.7 !important; align-self: center !important; border-color: rgba(94, 53, 177, 0.4) !important;"></v-divider>
|
||||
<v-btn
|
||||
@click="toggleTheme"
|
||||
class="theme-toggle-btn"
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
>
|
||||
<v-icon
|
||||
size="18"
|
||||
:color="useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'"
|
||||
>
|
||||
mdi-weather-night
|
||||
</v-icon>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ t('theme.switchToDark') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="login-page-container-dark">
|
||||
<div class="login-background-dark"></div>
|
||||
|
||||
<div class="login-container">
|
||||
<!-- 桌面端:卡片样式 -->
|
||||
<v-card
|
||||
v-if="!$vuetify.display.xs"
|
||||
variant="outlined"
|
||||
class="login-card"
|
||||
:class="{ 'card-visible': cardVisible }"
|
||||
>
|
||||
<v-card-text class="pa-10">
|
||||
<div class="logo-wrapper">
|
||||
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
|
||||
</div>
|
||||
<div class="divider-container">
|
||||
<v-divider class="custom-divider"></v-divider>
|
||||
</div>
|
||||
<AuthLogin />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 移动端:全屏样式 -->
|
||||
<div
|
||||
v-else
|
||||
class="mobile-login-container"
|
||||
:class="{ 'mobile-visible': cardVisible }"
|
||||
>
|
||||
<div class="mobile-content">
|
||||
<div class="logo-wrapper">
|
||||
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
|
||||
</div>
|
||||
<div class="divider-container">
|
||||
<v-divider class="custom-divider"></v-divider>
|
||||
</div>
|
||||
<AuthLogin />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 悬浮式圆角工具栏 -->
|
||||
<v-card
|
||||
class="floating-toolbar"
|
||||
:class="{ 'toolbar-visible': cardVisible }"
|
||||
elevation="8"
|
||||
rounded="xl"
|
||||
>
|
||||
<v-card-text class="pa-2">
|
||||
<div class="d-flex align-center gap-1">
|
||||
<LanguageSwitcher />
|
||||
<v-divider vertical class="mx-1" style="height: 24px !important; opacity: 0.9 !important; align-self: center !important; border-color: rgba(180, 148, 246, 0.8) !important;"></v-divider>
|
||||
<v-btn
|
||||
@click="toggleTheme"
|
||||
class="theme-toggle-btn"
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
>
|
||||
<v-icon
|
||||
size="18"
|
||||
:color="useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'"
|
||||
>
|
||||
<v-divider vertical class="mx-1"
|
||||
style="height: 24px !important; opacity: 0.9 !important; align-self: center !important; border-color: rgba(180, 148, 246, 0.8) !important;"></v-divider>
|
||||
<v-btn @click="toggleTheme" class="theme-toggle-btn" icon variant="text" size="small">
|
||||
<v-icon size="18" :color="useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'">
|
||||
mdi-white-balance-sunny
|
||||
</v-icon>
|
||||
<v-tooltip activator="parent" location="top">
|
||||
@@ -176,288 +55,31 @@ onMounted(() => {
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-2" style="font-size: 26px;">{{ t('logo.title') }}</div>
|
||||
<div class="mt-2 ml-2" style="font-size: 14px; color: grey;">{{ t('logo.subtitle') }}</div>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<AuthLogin />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.login-page-container {
|
||||
background-color: rgb(var(--v-theme-containerBg));
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background:
|
||||
linear-gradient(-45deg,
|
||||
#faf9f7 0%,
|
||||
#f9f2f1 25%,
|
||||
#f1f9f9 50%,
|
||||
#f9f3f7 75%,
|
||||
#faf9f7 100%
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-page-container-dark {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background:
|
||||
linear-gradient(-45deg,
|
||||
#1e1f21 0%,
|
||||
#221e25 25%,
|
||||
#1e2225 50%,
|
||||
#221f23 75%,
|
||||
#1e1f21 100%
|
||||
);
|
||||
background-size: 400% 400%;
|
||||
animation: gradientShift 15s ease infinite;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.login-background {
|
||||
position: absolute;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
background: radial-gradient(circle, rgba(94, 53, 177, 0.02) 0%, rgba(94, 53, 177, 0.03) 70%);
|
||||
z-index: 0;
|
||||
animation: rotate 60s linear infinite;
|
||||
}
|
||||
|
||||
.login-background-dark {
|
||||
position: absolute;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
background: radial-gradient(circle, rgba(114, 46, 209, 0.03) 0%, rgba(114, 46, 209, 0.04) 70%);
|
||||
z-index: 0;
|
||||
animation: rotate 60s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.login-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.floating-toolbar {
|
||||
background: #f8f6fc !important;
|
||||
border: 1px solid rgba(94, 53, 177, 0.15) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
transition: transform 0.6s ease 0.2s, opacity 0.6s ease 0.2s, border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
min-width: auto !important;
|
||||
width: fit-content;
|
||||
|
||||
&.toolbar-visible {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(158, 126, 222, 0.99) !important;
|
||||
box-shadow: 0 12px 40px rgba(175, 145, 230, 0.741) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.login-page-container-dark .floating-toolbar {
|
||||
background: #2a2733 !important;
|
||||
border: 1px solid rgba(110, 60, 180, 0.692) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3) !important;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(160, 118, 219, 0.782) !important;
|
||||
box-shadow: 0 12px 40px rgba(99, 44, 175, 0.462) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-toggle-btn {
|
||||
transition: all 0.3s ease;
|
||||
border-radius: 50% !important;
|
||||
min-width: 32px !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
background: rgba(94, 53, 177, 0.08) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.login-page-container-dark .theme-toggle-btn:hover {
|
||||
background: rgba(114, 46, 209, 0.12) !important;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
max-width: 520px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
color: var(--v-theme-primaryText) !important;
|
||||
border-radius: 16px !important;
|
||||
border: 1px solid rgba(94, 53, 177, 0.15) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08) !important;
|
||||
background: #f8f6fc !important;
|
||||
backdrop-filter: blur(10px);
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
transition: transform 0.5s ease, opacity 0.5s ease, border-color 0.3s ease, box-shadow 0.3s ease;
|
||||
z-index: 1;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: calc(100% + 4px);
|
||||
height: calc(100% + 4px);
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 18px;
|
||||
border: 2px solid rgba(94, 53, 177, 0);
|
||||
transition: border-color 0.3s ease;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&.card-visible {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(158, 126, 222, 0.99) !important;
|
||||
box-shadow: 0 12px 40px rgba(175, 145, 230, 0.741) !important;
|
||||
transform: translateY(-2px);
|
||||
|
||||
&::before {
|
||||
border-color: rgba(156, 114, 239, 0.907);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.login-page-container-dark .login-card {
|
||||
border: 1px solid rgba(110, 60, 180, 0.692) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3) !important;
|
||||
background: #2a2733 !important;
|
||||
|
||||
&::before {
|
||||
border: 2px solid rgba(114, 46, 209, 0);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(160, 118, 219, 0.782) !important;
|
||||
box-shadow: 0 12px 40px rgba(99, 44, 175, 0.462) !important;
|
||||
transform: translateY(-2px);
|
||||
|
||||
&::before {
|
||||
border-color: rgba(114, 46, 209, 0.15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.divider-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.custom-divider {
|
||||
border-color: rgba(94, 53, 177, 0.3) !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.login-page-container-dark .custom-divider {
|
||||
border-color: rgba(180, 148, 246, 0.4) !important;
|
||||
}
|
||||
|
||||
.loginBox {
|
||||
max-width: 475px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 移动端全屏登录样式 */
|
||||
.mobile-login-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
transition: transform 0.5s ease, opacity 0.5s ease;
|
||||
z-index: 1;
|
||||
|
||||
&.mobile-visible {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-content {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
/* 移动端调整工具栏位置 */
|
||||
@media (max-width: 599px) {
|
||||
.floating-toolbar {
|
||||
position: fixed;
|
||||
bottom: 30px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
z-index: 1000;
|
||||
|
||||
&.toolbar-visible {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateX(-50%) translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.login-container {
|
||||
gap: 0;
|
||||
}
|
||||
width: 400px;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, useCssModule} from 'vue';
|
||||
import { ref, useCssModule } from 'vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { Form } from 'vee-validate';
|
||||
import md5 from 'js-md5';
|
||||
import {useCustomizerStore} from "@/stores/customizer";
|
||||
import { useModuleI18n } from '@/i18n/composables';
|
||||
|
||||
const { tm: t } = useModuleI18n('features/auth');
|
||||
@@ -17,7 +16,7 @@ const loading = ref(false);
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
async function validate(values: any, { setErrors }: any) {
|
||||
loading.value = true;
|
||||
|
||||
|
||||
// md5加密
|
||||
let password_ = password.value;
|
||||
if (password.value != '') {
|
||||
@@ -41,58 +40,25 @@ async function validate(values: any, { setErrors }: any) {
|
||||
|
||||
<template>
|
||||
<Form @submit="validate" class="mt-4 login-form" v-slot="{ errors, isSubmitting }">
|
||||
<v-text-field
|
||||
v-model="username"
|
||||
:label="t('username')"
|
||||
class="mb-6 input-field"
|
||||
required
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
variant="outlined"
|
||||
:style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000dd' : '#ffffff'}"
|
||||
prepend-inner-icon="mdi-account"
|
||||
:disabled="loading"
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="password"
|
||||
:label="t('password')"
|
||||
required
|
||||
density="comfortable"
|
||||
variant="outlined"
|
||||
:style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000dd' : '#ffffff'}"
|
||||
hide-details="auto"
|
||||
:append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'"
|
||||
:type="show1 ? 'text' : 'password'"
|
||||
@click:append="show1 = !show1"
|
||||
class="pwd-input"
|
||||
prepend-inner-icon="mdi-lock"
|
||||
:disabled="loading"
|
||||
></v-text-field>
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
:loading="isSubmitting || loading"
|
||||
block
|
||||
class="login-btn mt-8"
|
||||
variant="flat"
|
||||
size="large"
|
||||
:disabled="valid"
|
||||
type="submit"
|
||||
elevation="2"
|
||||
<v-text-field v-model="username" :label="t('username')" class="mb-6 input-field" required hide-details="auto"
|
||||
variant="outlined" prepend-inner-icon="mdi-account" :disabled="loading"></v-text-field>
|
||||
|
||||
>
|
||||
<v-text-field v-model="password" :label="t('password')" required variant="outlined" hide-details="auto"
|
||||
:append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'" :type="show1 ? 'text' : 'password'"
|
||||
@click:append="show1 = !show1" class="pwd-input" prepend-inner-icon="mdi-lock" :disabled="loading"></v-text-field>
|
||||
|
||||
<div class="mt-2">
|
||||
<small style="color: grey;">{{ t('defaultHint') }}</small>
|
||||
</div>
|
||||
|
||||
|
||||
<v-btn color="secondary" :loading="isSubmitting || loading" block class="login-btn mt-8" variant="flat" size="large"
|
||||
:disabled="valid" type="submit">
|
||||
<span class="login-btn-text">{{ t('login') }}</span>
|
||||
</v-btn>
|
||||
|
||||
|
||||
<div v-if="errors.apiError" class="mt-4 error-container">
|
||||
<v-alert
|
||||
color="error"
|
||||
variant="tonal"
|
||||
density="comfortable"
|
||||
icon="mdi-alert-circle"
|
||||
border="start"
|
||||
>
|
||||
<v-alert color="error" variant="tonal" icon="mdi-alert-circle" border="start">
|
||||
{{ errors.apiError }}
|
||||
</v-alert>
|
||||
</div>
|
||||
@@ -105,24 +71,25 @@ async function validate(values: any, { setErrors }: any) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-field, .pwd-input {
|
||||
.input-field,
|
||||
.pwd-input {
|
||||
.v-field__field {
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
.v-field__outline {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
|
||||
&:hover .v-field__outline {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
|
||||
.v-field--focused .v-field__outline {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
.v-field__prepend-inner {
|
||||
padding-right: 8px;
|
||||
opacity: 0.7;
|
||||
@@ -138,36 +105,36 @@ async function validate(values: any, { setErrors }: any) {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
opacity: 0.7;
|
||||
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.login-btn {
|
||||
margin-top: 12px;
|
||||
height: 48px;
|
||||
transition: all 0.3s ease;
|
||||
letter-spacing: 0.5px;
|
||||
border-radius: 8px !important;
|
||||
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(94, 53, 177, 0.2) !important;
|
||||
}
|
||||
|
||||
|
||||
.login-btn-text {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.hint-text {
|
||||
color: var(--v-theme-secondaryText);
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
|
||||
.error-container {
|
||||
.v-alert {
|
||||
border-left-width: 4px !important;
|
||||
|
||||
@@ -43,5 +43,8 @@ export default defineConfig({
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
// Tauri 特定配置
|
||||
clearScreen: false,
|
||||
envPrefix: ['VITE_', 'TAURI_'],
|
||||
});
|
||||
|
||||
153
pyproject.toml
@@ -1,107 +1,100 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.5.5"
|
||||
version = "4.5.6"
|
||||
description = "Easy-to-use multi-platform LLM chatbot and development framework"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
keywords = [
|
||||
"Astrbot",
|
||||
"Astrbot Module",
|
||||
"Astrbot Plugin"
|
||||
]
|
||||
keywords = ["Astrbot", "Astrbot Module", "Astrbot Plugin"]
|
||||
|
||||
dependencies = [
|
||||
"aiocqhttp>=1.4.4",
|
||||
"aiodocker>=0.24.0",
|
||||
"aiohttp>=3.11.18",
|
||||
"aiosqlite>=0.21.0",
|
||||
"anthropic>=0.51.0",
|
||||
"apscheduler>=3.11.0",
|
||||
"beautifulsoup4>=4.13.4",
|
||||
"certifi>=2025.4.26",
|
||||
"chardet~=5.1.0",
|
||||
"colorlog>=6.9.0",
|
||||
"cryptography>=44.0.3",
|
||||
"dashscope>=1.23.2",
|
||||
"defusedxml>=0.7.1",
|
||||
"deprecated>=1.2.18",
|
||||
"dingtalk-stream>=0.22.1",
|
||||
"docstring-parser>=0.16",
|
||||
"faiss-cpu==1.10.0",
|
||||
"filelock>=3.18.0",
|
||||
"google-genai>=1.14.0",
|
||||
"lark-oapi>=1.4.15",
|
||||
"lxml-html-clean>=0.4.2",
|
||||
"mcp>=1.8.0",
|
||||
"openai>=1.78.0",
|
||||
"ormsgpack>=1.9.1",
|
||||
"pillow>=11.2.1",
|
||||
"pip>=25.1.1",
|
||||
"psutil>=5.8.0",
|
||||
"py-cord>=2.6.1",
|
||||
"pydantic~=2.10.3",
|
||||
"pydub>=0.25.1",
|
||||
"pyjwt>=2.10.1",
|
||||
"python-telegram-bot>=22.0",
|
||||
"qq-botpy>=1.2.1",
|
||||
"quart>=0.20.0",
|
||||
"readability-lxml>=0.8.4.1",
|
||||
"silk-python>=0.2.6",
|
||||
"slack-sdk>=3.35.0",
|
||||
"sqlalchemy[asyncio]>=2.0.41",
|
||||
"sqlmodel>=0.0.24",
|
||||
"telegramify-markdown>=0.5.1",
|
||||
"watchfiles>=1.0.5",
|
||||
"websockets>=15.0.1",
|
||||
"wechatpy>=1.8.18",
|
||||
"audioop-lts ; python_full_version >= '3.13'",
|
||||
"click>=8.2.1",
|
||||
"pypdf>=6.1.1",
|
||||
"aiofiles>=25.1.0",
|
||||
"rank-bm25>=0.2.2",
|
||||
"jieba>=0.42.1",
|
||||
"markitdown-no-magika[docx,xls,xlsx]>=0.1.2",
|
||||
"xinference-client",
|
||||
"aiocqhttp>=1.4.4",
|
||||
"aiodocker>=0.24.0",
|
||||
"aiohttp>=3.11.18",
|
||||
"aiosqlite>=0.21.0",
|
||||
"anthropic>=0.51.0",
|
||||
"apscheduler>=3.11.0",
|
||||
"beautifulsoup4>=4.13.4",
|
||||
"certifi>=2025.4.26",
|
||||
"chardet~=5.1.0",
|
||||
"colorlog>=6.9.0",
|
||||
"cryptography>=44.0.3",
|
||||
"dashscope>=1.23.2",
|
||||
"defusedxml>=0.7.1",
|
||||
"deprecated>=1.2.18",
|
||||
"dingtalk-stream>=0.22.1",
|
||||
"docstring-parser>=0.16",
|
||||
"faiss-cpu==1.10.0",
|
||||
"filelock>=3.18.0",
|
||||
"google-genai>=1.14.0",
|
||||
"lark-oapi>=1.4.15",
|
||||
"lxml-html-clean>=0.4.2",
|
||||
"mcp>=1.8.0",
|
||||
"openai>=1.78.0",
|
||||
"ormsgpack>=1.9.1",
|
||||
"pillow>=11.2.1",
|
||||
"pip>=25.1.1",
|
||||
"psutil>=5.8.0",
|
||||
"py-cord>=2.6.1",
|
||||
"pydantic~=2.10.3",
|
||||
"pydub>=0.25.1",
|
||||
"pyjwt>=2.10.1",
|
||||
"python-telegram-bot>=22.0",
|
||||
"qq-botpy>=1.2.1",
|
||||
"quart>=0.20.0",
|
||||
"readability-lxml>=0.8.4.1",
|
||||
"silk-python>=0.2.6",
|
||||
"slack-sdk>=3.35.0",
|
||||
"sqlalchemy[asyncio]>=2.0.41",
|
||||
"sqlmodel>=0.0.24",
|
||||
"telegramify-markdown>=0.5.1",
|
||||
"watchfiles>=1.0.5",
|
||||
"websockets>=15.0.1",
|
||||
"wechatpy>=1.8.18",
|
||||
"audioop-lts ; python_full_version >= '3.13'",
|
||||
"click>=8.2.1",
|
||||
"pypdf>=6.1.1",
|
||||
"aiofiles>=25.1.0",
|
||||
"rank-bm25>=0.2.2",
|
||||
"jieba>=0.42.1",
|
||||
"markitdown-no-magika[docx,xls,xlsx]>=0.1.2",
|
||||
"xinference-client",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"commitizen>=4.9.1",
|
||||
"pytest>=8.4.1",
|
||||
"pytest-asyncio>=1.1.0",
|
||||
"pytest-cov>=6.2.1",
|
||||
"ruff>=0.12.8",
|
||||
"commitizen>=4.9.1",
|
||||
"pytest>=8.4.1",
|
||||
"pytest-asyncio>=1.1.0",
|
||||
"pytest-cov>=6.2.1",
|
||||
"ruff>=0.12.8",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
astrbot = "astrbot.cli.__main__:cli"
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
"astrbot/core/utils/t2i/local_strategy.py",
|
||||
"astrbot/api/all.py",
|
||||
]
|
||||
exclude = ["astrbot/core/utils/t2i/local_strategy.py", "astrbot/api/all.py"]
|
||||
line-length = 88
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"F", # Pyflakes
|
||||
"W", # pycodestyle warnings
|
||||
"E", # pycodestyle errors
|
||||
"F", # Pyflakes
|
||||
"W", # pycodestyle warnings
|
||||
"E", # pycodestyle errors
|
||||
"ASYNC", # flake8-async
|
||||
"C4", # flake8-comprehensions
|
||||
"Q", # flake8-quotes
|
||||
"I", # import-order
|
||||
"UP", # pyupgrade
|
||||
"C4", # flake8-comprehensions
|
||||
"Q", # flake8-quotes
|
||||
"I", # import-order
|
||||
"UP", # pyupgrade
|
||||
# "SIM", # flake8-simplify
|
||||
]
|
||||
ignore = [
|
||||
"F403",
|
||||
"F405",
|
||||
"E501",
|
||||
"ASYNC230" # TODO: handle ASYNC230 in AstrBot
|
||||
"F403",
|
||||
"F405",
|
||||
"E501",
|
||||
"ASYNC230", # TODO: handle ASYNC230 in AstrBot
|
||||
]
|
||||
|
||||
[tool.pyright]
|
||||
@@ -109,5 +102,9 @@ typeCheckingMode = "basic"
|
||||
pythonVersion = "3.10"
|
||||
reportMissingTypeStubs = false
|
||||
reportMissingImports = false
|
||||
include = ["astrbot","packages"]
|
||||
include = ["astrbot", "packages"]
|
||||
exclude = ["dashboard", "node_modules", "dist", "data", "tests"]
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||