Compare commits
29 Commits
refactor/a
...
feat/tauri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c090299b1 | ||
|
|
b360c8446e | ||
|
|
6d00717655 | ||
|
|
bb5f06498e | ||
|
|
aca5743ab6 | ||
|
|
6903032f7e | ||
|
|
1ce0ff87bd | ||
|
|
e39d6bae0b | ||
|
|
8028e9e9a6 | ||
|
|
817f20ea01 | ||
|
|
ad5579a2f4 | ||
|
|
81a689a79b | ||
|
|
1893dd8336 | ||
|
|
021ca8175b | ||
|
|
39d6207fe1 | ||
|
|
23ce687229 | ||
|
|
3715312fd2 | ||
|
|
8196922cac | ||
|
|
8089ad91da | ||
|
|
2930cc3fd8 | ||
|
|
0e841a8b25 | ||
|
|
67fa1611cc | ||
|
|
91136bb9f7 | ||
|
|
7c050d1adc | ||
|
|
a0690a6afc | ||
|
|
c51609b261 | ||
|
|
72148f66eb | ||
|
|
a04993a2bb | ||
|
|
74f845b06d |
@@ -1,9 +1,9 @@
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
# github acions
|
||||
# github actions
|
||||
.git
|
||||
.github/
|
||||
.*ignore
|
||||
.git/
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
# Byte-compiled / optimized / DLL files
|
||||
@@ -15,10 +15,10 @@ env/
|
||||
venv*/
|
||||
ENV/
|
||||
.conda/
|
||||
README*.md
|
||||
dashboard/
|
||||
data/
|
||||
changelogs/
|
||||
tests/
|
||||
.ruff_cache/
|
||||
.astrbot
|
||||
.astrbot
|
||||
astrbot.lock
|
||||
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)
|
||||
18
Dockerfile
@@ -12,19 +12,21 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
bash \
|
||||
ffmpeg \
|
||||
curl \
|
||||
gnupg \
|
||||
git \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN apt-get update && apt-get install -y curl gnupg && \
|
||||
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
|
||||
apt-get install -y nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN apt-get update && apt-get install -y curl gnupg \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
RUN python -m pip install uv
|
||||
RUN python -m pip install uv \
|
||||
&& echo "3.11" > .python-version
|
||||
RUN uv pip install -r requirements.txt --no-cache-dir --system
|
||||
RUN uv pip install socksio uv pilk --no-cache-dir --system
|
||||
|
||||
EXPOSE 6185
|
||||
EXPOSE 6186
|
||||
|
||||
CMD [ "python", "main.py" ]
|
||||
CMD ["python", "main.py"]
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
WORKDIR /AstrBot
|
||||
|
||||
COPY . /AstrBot/
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
build-essential \
|
||||
python3-dev \
|
||||
libffi-dev \
|
||||
libssl-dev \
|
||||
curl \
|
||||
unzip \
|
||||
ca-certificates \
|
||||
bash \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Installation of Node.js
|
||||
ENV NVM_DIR="/root/.nvm"
|
||||
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash && \
|
||||
. "$NVM_DIR/nvm.sh" && \
|
||||
nvm install 22 && \
|
||||
nvm use 22
|
||||
RUN /bin/bash -c ". \"$NVM_DIR/nvm.sh\" && node -v && npm -v"
|
||||
|
||||
RUN python -m pip install uv
|
||||
RUN uv pip install -r requirements.txt --no-cache-dir --system
|
||||
RUN uv pip install socksio uv pyffmpeg --no-cache-dir --system
|
||||
|
||||
EXPOSE 6185
|
||||
EXPOSE 6186
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
116
README.md
@@ -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>
|
||||
@@ -119,83 +119,73 @@ uv run main.py
|
||||
|
||||
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
||||
|
||||
## ⚡ 消息平台支持情况
|
||||
## 支持的消息平台
|
||||
|
||||
**官方维护**
|
||||
|
||||
| 平台 | 支持性 |
|
||||
| -------- | ------- |
|
||||
| QQ(官方平台) | ✔ |
|
||||
| QQ(OneBot) | ✔ |
|
||||
| Telegram | ✔ |
|
||||
| 企微应用 | ✔ |
|
||||
| 企微智能机器人 | ✔ |
|
||||
| 微信客服 | ✔ |
|
||||
| 微信公众号 | ✔ |
|
||||
| 飞书 | ✔ |
|
||||
| 钉钉 | ✔ |
|
||||
| Slack | ✔ |
|
||||
| Discord | ✔ |
|
||||
| Satori | ✔ |
|
||||
| Misskey | ✔ |
|
||||
| Whatsapp | 将支持 |
|
||||
| LINE | 将支持 |
|
||||
- QQ (官方平台 & OneBot)
|
||||
- Telegram
|
||||
- 企微应用 & 企微智能机器人
|
||||
- 微信客服 & 微信公众号
|
||||
- 飞书
|
||||
- 钉钉
|
||||
- Slack
|
||||
- Discord
|
||||
- Satori
|
||||
- Misskey
|
||||
- Whatsapp (将支持)
|
||||
- LINE (将支持)
|
||||
|
||||
**社区维护**
|
||||
|
||||
| 平台 | 支持性 |
|
||||
| -------- | ------- |
|
||||
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | ✔ |
|
||||
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | ✔ |
|
||||
| [Bilibili 私信](https://github.com/Hina-Chat/astrbot_plugin_bilibili_adapter) | ✔ |
|
||||
| [wxauto](https://github.com/luosheng520qaq/wxauto-repost-onebotv11) | ✔ |
|
||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
||||
- [Bilibili 私信](https://github.com/Hina-Chat/astrbot_plugin_bilibili_adapter)
|
||||
- [wxauto](https://github.com/luosheng520qaq/wxauto-repost-onebotv11)
|
||||
|
||||
## ⚡ 提供商支持情况
|
||||
## 支持的模型服务
|
||||
|
||||
**大模型服务**
|
||||
|
||||
| 名称 | 支持性 | 备注 |
|
||||
| -------- | ------- | ------- |
|
||||
| OpenAI | ✔ | 支持任何兼容 OpenAI API 的服务 |
|
||||
| Anthropic | ✔ | |
|
||||
| Google Gemini | ✔ | |
|
||||
| Moonshot AI | ✔ | |
|
||||
| 智谱 AI | ✔ | |
|
||||
| DeepSeek | ✔ | |
|
||||
| Ollama | ✔ | 本地部署 DeepSeek 等开源语言模型 |
|
||||
| LM Studio | ✔ | 本地部署 DeepSeek 等开源语言模型 |
|
||||
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | ✔ | |
|
||||
| [302.AI](https://share.302.ai/rr1M3l) | ✔ | |
|
||||
| [小马算力](https://www.tokenpony.cn/3YPyf) | ✔ | |
|
||||
| 硅基流动 | ✔ | |
|
||||
| PPIO 派欧云 | ✔ | |
|
||||
| ModelScope | ✔ | |
|
||||
| OneAPI | ✔ | |
|
||||
| Dify | ✔ | |
|
||||
| 阿里云百炼应用 | ✔ | |
|
||||
| Coze | ✔ | |
|
||||
- OpenAI 及兼容服务
|
||||
- Anthropic
|
||||
- Google Gemini
|
||||
- Moonshot AI
|
||||
- 智谱 AI
|
||||
- DeepSeek
|
||||
- Ollama (本地部署)
|
||||
- LM Studio (本地部署)
|
||||
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
||||
- [302.AI](https://share.302.ai/rr1M3l)
|
||||
- [小马算力](https://www.tokenpony.cn/3YPyf)
|
||||
- [硅基流动](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot)
|
||||
- [PPIO 派欧云](https://ppio.com/user/register?invited_by=AIOONE)
|
||||
- ModelScope
|
||||
- OneAPI
|
||||
|
||||
**LLMOps 平台**
|
||||
|
||||
- Dify
|
||||
- 阿里云百炼应用
|
||||
- Coze
|
||||
|
||||
**语音转文本服务**
|
||||
|
||||
| 名称 | 支持性 | 备注 |
|
||||
| -------- | ------- | ------- |
|
||||
| Whisper | ✔ | 支持 API、本地部署 |
|
||||
| SenseVoice | ✔ | 本地部署 |
|
||||
- OpenAI Whisper
|
||||
- SenseVoice
|
||||
|
||||
**文本转语音服务**
|
||||
|
||||
| 名称 | 支持性 | 备注 |
|
||||
| -------- | ------- | ------- |
|
||||
| OpenAI TTS | ✔ | |
|
||||
| Gemini TTS | ✔ | |
|
||||
| GSVI | ✔ | GPT-Sovits-Inference |
|
||||
| GPT-SoVITs | ✔ | GPT-Sovits |
|
||||
| FishAudio | ✔ | |
|
||||
| Edge TTS | ✔ | Edge 浏览器的免费 TTS |
|
||||
| 阿里云百炼 TTS | ✔ | |
|
||||
| Azure TTS | ✔ | |
|
||||
| Minimax TTS | ✔ | |
|
||||
| 火山引擎 TTS | ✔ | |
|
||||
- OpenAI TTS
|
||||
- Gemini TTS
|
||||
- GPT-Sovits-Inference
|
||||
- GPT-Sovits
|
||||
- FishAudio
|
||||
- Edge TTS
|
||||
- 阿里云百炼 TTS
|
||||
- Azure TTS
|
||||
- Minimax TTS
|
||||
- 火山引擎 TTS
|
||||
|
||||
## ❤️ 贡献
|
||||
|
||||
@@ -229,7 +219,7 @@ pre-commit install
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
> [!TIP]
|
||||
> [!TIP]
|
||||
> 如果本项目对您的生活 / 工作产生了帮助,或者您关注本项目的未来发展,请给项目 Star,这是我们维护这个开源项目的动力 <3
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
import click
|
||||
from filelock import FileLock, Timeout
|
||||
|
||||
@@ -48,7 +48,7 @@ def init() -> None:
|
||||
|
||||
try:
|
||||
with lock.acquire():
|
||||
anyio.run(initialize_astrbot, astrbot_root)
|
||||
asyncio.run(initialize_astrbot(astrbot_root))
|
||||
except Timeout:
|
||||
raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行")
|
||||
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from pathlib import Path
|
||||
|
||||
import anyio
|
||||
import click
|
||||
from filelock import FileLock, Timeout
|
||||
|
||||
from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root
|
||||
|
||||
|
||||
async def run_astrbot(astrbot_root: Path) -> None:
|
||||
async def run_astrbot(astrbot_root: Path):
|
||||
"""运行 AstrBot"""
|
||||
from astrbot.core import LogBroker, LogManager, db_helper, logger
|
||||
from astrbot.core.initial_loader import InitialLoader
|
||||
@@ -53,7 +53,7 @@ def run(reload: bool, port: str) -> None:
|
||||
lock_file = astrbot_root / "astrbot.lock"
|
||||
lock = FileLock(lock_file, timeout=5)
|
||||
with lock.acquire():
|
||||
anyio.run(run_astrbot, astrbot_root)
|
||||
asyncio.run(run_astrbot(astrbot_root))
|
||||
except KeyboardInterrupt:
|
||||
click.echo("AstrBot 已关闭...")
|
||||
except Timeout:
|
||||
|
||||
@@ -55,14 +55,6 @@ class FunctionTool(ToolSchema, Generic[TContext]):
|
||||
def __repr__(self):
|
||||
return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
|
||||
|
||||
def __dict__(self) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"parameters": self.parameters,
|
||||
"description": self.description,
|
||||
"active": self.active,
|
||||
}
|
||||
|
||||
async def call(
|
||||
self, context: ContextWrapper[TContext], **kwargs
|
||||
) -> str | mcp.types.CallToolResult:
|
||||
|
||||
@@ -4,7 +4,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.5.1"
|
||||
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",
|
||||
|
||||
@@ -14,8 +14,7 @@ import os
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import anyio
|
||||
from asyncio import Queue
|
||||
|
||||
from astrbot.core import LogBroker, logger, sp
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
@@ -105,9 +104,7 @@ class AstrBotCoreLifecycle:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
# 初始化事件队列
|
||||
self._event_queue_send, self.event_queue = anyio.create_memory_object_stream[
|
||||
object
|
||||
](0)
|
||||
self.event_queue = Queue()
|
||||
|
||||
# 初始化人格管理器
|
||||
self.persona_mgr = PersonaManager(self.db, self.astrbot_config_mgr)
|
||||
@@ -121,9 +118,7 @@ class AstrBotCoreLifecycle:
|
||||
)
|
||||
|
||||
# 初始化平台管理器
|
||||
self.platform_manager = PlatformManager(
|
||||
self.astrbot_config, self._event_queue_send
|
||||
)
|
||||
self.platform_manager = PlatformManager(self.astrbot_config, self.event_queue)
|
||||
|
||||
# 初始化对话管理器
|
||||
self.conversation_manager = ConversationManager(self.db)
|
||||
@@ -136,7 +131,7 @@ class AstrBotCoreLifecycle:
|
||||
|
||||
# 初始化提供给插件的上下文
|
||||
self.star_context = Context(
|
||||
self._event_queue_send,
|
||||
self.event_queue,
|
||||
self.astrbot_config,
|
||||
self.db,
|
||||
self.provider_manager,
|
||||
|
||||
@@ -271,7 +271,7 @@ class SQLiteDatabase(BaseDatabase):
|
||||
async with session.begin():
|
||||
await session.execute(
|
||||
delete(ConversationV2).where(
|
||||
col(ConversationV2.user_id) == user_id,
|
||||
col(ConversationV2.user_id) == user_id
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""事件总线, 用于处理事件的分发和处理.
|
||||
|
||||
"""事件总线, 用于处理事件的分发和处理
|
||||
事件总线是一个异步队列, 用于接收各种消息事件, 并将其发送到Scheduler调度器进行处理
|
||||
其中包含了一个无限循环的调度函数, 用于从事件队列中获取新的事件, 并创建一个新的异步任务来执行管道调度器的处理逻辑
|
||||
|
||||
@@ -11,8 +10,8 @@ class:
|
||||
2. 无限循环的调度函数, 从事件队列中获取新的事件, 打印日志并创建一个新的异步任务来执行管道调度器的处理逻辑
|
||||
"""
|
||||
|
||||
import anyio
|
||||
from anyio.streams.memory import MemoryObjectReceiveStream
|
||||
import asyncio
|
||||
from asyncio import Queue
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
@@ -26,29 +25,28 @@ class EventBus:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
event_queue: MemoryObjectReceiveStream[AstrMessageEvent],
|
||||
event_queue: Queue,
|
||||
pipeline_scheduler_mapping: dict[str, PipelineScheduler],
|
||||
astrbot_config_mgr: AstrBotConfigManager | None = None,
|
||||
) -> None:
|
||||
astrbot_config_mgr: AstrBotConfigManager = None,
|
||||
):
|
||||
self.event_queue = event_queue # 事件队列
|
||||
# abconf uuid -> scheduler
|
||||
self.pipeline_scheduler_mapping = pipeline_scheduler_mapping
|
||||
self.astrbot_config_mgr = astrbot_config_mgr
|
||||
|
||||
async def dispatch(self) -> None:
|
||||
async def dispatch(self):
|
||||
while True:
|
||||
event: AstrMessageEvent = await self.event_queue.receive()
|
||||
event: AstrMessageEvent = await self.event_queue.get()
|
||||
conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin)
|
||||
self._print_event(event, conf_info["name"])
|
||||
scheduler = self.pipeline_scheduler_mapping.get(conf_info["id"])
|
||||
anyio.create_task(scheduler.execute(event))
|
||||
asyncio.create_task(scheduler.execute(event))
|
||||
|
||||
def _print_event(self, event: AstrMessageEvent, conf_name: str) -> None:
|
||||
def _print_event(self, event: AstrMessageEvent, conf_name: str):
|
||||
"""用于记录事件信息
|
||||
|
||||
Args:
|
||||
event: 事件对象
|
||||
conf_name: 配置名称
|
||||
event (AstrMessageEvent): 事件对象
|
||||
|
||||
"""
|
||||
# 如果有发送者名称: [平台名] 发送者名称/发送者ID: 消息概要
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
import time
|
||||
import uuid
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
import anyio
|
||||
|
||||
|
||||
class FileTokenService:
|
||||
"""维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。"""
|
||||
|
||||
def __init__(self, default_timeout: float = 300) -> None:
|
||||
self.lock = anyio.Lock()
|
||||
self.staged_files: dict = {} # token: (file_path, expire_time)
|
||||
def __init__(self, default_timeout: float = 300):
|
||||
self.lock = asyncio.Lock()
|
||||
self.staged_files = {} # token: (file_path, expire_time)
|
||||
self.default_timeout = default_timeout
|
||||
|
||||
async def _cleanup_expired_tokens(self):
|
||||
|
||||
@@ -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,9 +1,8 @@
|
||||
import asyncio
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import AsyncGenerator
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import anyio
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.config.astrbot_config import RateLimitStrategy
|
||||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||||
@@ -20,11 +19,11 @@ class RateLimitStage(Stage):
|
||||
如果触发限流,将 stall 流水线,直到下一个时间窗口来临时自动唤醒。
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self):
|
||||
# 存储每个会话的请求时间队列
|
||||
self.event_timestamps: defaultdict[str, deque[datetime]] = defaultdict(deque)
|
||||
# 为每个会话设置一个锁,避免并发冲突
|
||||
self.locks: defaultdict[str, anyio.Lock] = defaultdict(anyio.Lock)
|
||||
self.locks: defaultdict[str, asyncio.Lock] = defaultdict(asyncio.Lock)
|
||||
# 限流参数
|
||||
self.rate_limit_count: int = 0
|
||||
self.rate_limit_time: timedelta = timedelta(0)
|
||||
@@ -75,7 +74,7 @@ class RateLimitStage(Stage):
|
||||
logger.info(
|
||||
f"会话 {session_id} 被限流。根据限流策略,此会话处理将被暂停 {stall_duration:.2f} 秒。",
|
||||
)
|
||||
await anyio.sleep(stall_duration)
|
||||
await asyncio.sleep(stall_duration)
|
||||
now = datetime.now()
|
||||
case RateLimitStrategy.DISCARD.value:
|
||||
logger.info(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import asyncio
|
||||
import traceback
|
||||
|
||||
from anyio.streams.memory import MemoryObjectSendStream
|
||||
from asyncio import Queue
|
||||
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
@@ -13,7 +12,7 @@ from .sources.webchat.webchat_adapter import WebChatAdapter
|
||||
|
||||
|
||||
class PlatformManager:
|
||||
def __init__(self, config: AstrBotConfig, event_queue: MemoryObjectSendStream):
|
||||
def __init__(self, config: AstrBotConfig, event_queue: Queue):
|
||||
self.platform_insts: list[Platform] = []
|
||||
"""加载的 Platform 的实例"""
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import abc
|
||||
import uuid
|
||||
from asyncio import Queue
|
||||
from collections.abc import Awaitable
|
||||
from typing import Any
|
||||
|
||||
from anyio.streams.memory import MemoryObjectSendStream
|
||||
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
from astrbot.core.utils.metrics import Metric
|
||||
|
||||
@@ -14,7 +13,7 @@ from .platform_metadata import PlatformMetadata
|
||||
|
||||
|
||||
class Platform(abc.ABC):
|
||||
def __init__(self, event_queue: MemoryObjectSendStream):
|
||||
def __init__(self, event_queue: Queue):
|
||||
super().__init__()
|
||||
# 维护了消息平台的事件队列,EventBus 会从这里取出事件并处理。
|
||||
self._event_queue = event_queue
|
||||
@@ -46,7 +45,7 @@ class Platform(abc.ABC):
|
||||
|
||||
def commit_event(self, event: AstrMessageEvent):
|
||||
"""提交一个事件到事件队列。"""
|
||||
self._event_queue.send_nowait(event)
|
||||
self._event_queue.put_nowait(event)
|
||||
|
||||
def get_client(self):
|
||||
"""获取平台的客户端对象。"""
|
||||
|
||||
@@ -107,7 +107,7 @@ class AiocqhttpAdapter(Platform):
|
||||
)
|
||||
await super().send_by_session(session, message_chain)
|
||||
|
||||
async def convert_message(self, event: Event) -> AstrBotMessage:
|
||||
async def convert_message(self, event: Event) -> AstrBotMessage | None:
|
||||
logger.debug(f"[aiocqhttp] RawMessage {event}")
|
||||
|
||||
if event["post_type"] == "message":
|
||||
@@ -222,7 +222,7 @@ class AiocqhttpAdapter(Platform):
|
||||
err = f"aiocqhttp: 无法识别的消息类型: {event.message!s},此条消息将被忽略。如果您在使用 go-cqhttp,请将其配置文件中的 message.post-format 更改为 array。"
|
||||
logger.critical(err)
|
||||
try:
|
||||
self.bot.send(event, err)
|
||||
await self.bot.send(event, err)
|
||||
except BaseException as e:
|
||||
logger.error(f"回复消息失败: {e}")
|
||||
return None
|
||||
|
||||
@@ -216,7 +216,7 @@ class DingtalkPlatformAdapter(Platform):
|
||||
client=self.client,
|
||||
)
|
||||
|
||||
self._event_queue.send_nowait(event)
|
||||
self._event_queue.put_nowait(event)
|
||||
|
||||
async def run(self):
|
||||
# await self.client_.start()
|
||||
|
||||
@@ -90,7 +90,7 @@ class DiscordPlatformAdapter(Platform):
|
||||
)
|
||||
message_obj.self_id = self.client_self_id
|
||||
message_obj.session_id = session.session_id
|
||||
message_obj.message = message_chain
|
||||
message_obj.message = message_chain.chain
|
||||
|
||||
# 创建临时事件对象来发送消息
|
||||
temp_event = DiscordPlatformEvent(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import sys
|
||||
import binascii
|
||||
from collections.abc import AsyncGenerator
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
@@ -20,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):
|
||||
@@ -48,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 所需的对象
|
||||
@@ -97,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:
|
||||
@@ -183,7 +193,7 @@ class DiscordPlatformEvent(AstrMessageEvent):
|
||||
BytesIO(img_bytes),
|
||||
filename=filename or "image.png",
|
||||
)
|
||||
except (ValueError, TypeError, base64.binascii.Error):
|
||||
except (ValueError, TypeError, binascii.Error):
|
||||
logger.debug(
|
||||
f"[Discord] 裸 Base64 解码失败,作为本地路径处理: {file_content}",
|
||||
)
|
||||
|
||||
@@ -224,7 +224,7 @@ class LarkPlatformAdapter(Platform):
|
||||
bot=self.lark_api,
|
||||
)
|
||||
|
||||
self._event_queue.send_nowait(event)
|
||||
self._event_queue.put_nowait(event)
|
||||
|
||||
async def run(self):
|
||||
# self.client.start()
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import logging
|
||||
from collections.abc import Callable
|
||||
|
||||
import anyio
|
||||
from quart import Quart, Response, request
|
||||
from slack_sdk.socket_mode.aiohttp import SocketModeClient
|
||||
from slack_sdk.socket_mode.request import SocketModeRequest
|
||||
@@ -40,7 +40,7 @@ class SlackWebhookClient:
|
||||
logging.getLogger("quart.app").setLevel(logging.WARNING)
|
||||
logging.getLogger("quart.serving").setLevel(logging.WARNING)
|
||||
|
||||
self.shutdown_event = anyio.Event()
|
||||
self.shutdown_event = asyncio.Event()
|
||||
|
||||
def _setup_routes(self):
|
||||
"""设置路由"""
|
||||
|
||||
@@ -82,7 +82,7 @@ class SlackAdapter(Platform):
|
||||
session: MessageSesion,
|
||||
message_chain: MessageChain,
|
||||
):
|
||||
blocks, text = SlackMessageEvent._parse_slack_blocks(
|
||||
blocks, text = await SlackMessageEvent._parse_slack_blocks(
|
||||
message_chain=message_chain,
|
||||
web_client=self.web_client,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -10,7 +10,7 @@ import base64
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import secrets
|
||||
import socket
|
||||
import struct
|
||||
import time
|
||||
@@ -139,6 +139,12 @@ class PKCS7Encoder:
|
||||
class Prpcrypt:
|
||||
"""提供接收和推送给企业微信消息的加解密接口"""
|
||||
|
||||
# 16位随机字符串的范围常量
|
||||
# randbelow(RANDOM_RANGE) 返回 [0, 8999999999999999](两端都包含,即包含0和8999999999999999)
|
||||
# 加上 MIN_RANDOM_VALUE 后得到 [1000000000000000, 9999999999999999](两端都包含)即16位数字
|
||||
MIN_RANDOM_VALUE = 1000000000000000 # 最小值: 1000000000000000 (16位)
|
||||
RANDOM_RANGE = 9000000000000000 # 范围大小: 确保最大值为 9999999999999999 (16位)
|
||||
|
||||
def __init__(self, key):
|
||||
# self.key = base64.b64decode(key+"=")
|
||||
self.key = key
|
||||
@@ -207,7 +213,9 @@ class Prpcrypt:
|
||||
"""随机生成16位字符串
|
||||
@return: 16位字符串
|
||||
"""
|
||||
return str(random.randint(1000000000000000, 9999999999999999)).encode()
|
||||
return str(
|
||||
secrets.randbelow(self.RANDOM_RANGE) + self.MIN_RANDOM_VALUE
|
||||
).encode()
|
||||
|
||||
|
||||
class WXBizJsonMsgCrypt:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""企业微信智能机器人 API 客户端.
|
||||
|
||||
"""企业微信智能机器人 API 客户端
|
||||
处理消息加密解密、API 调用等
|
||||
"""
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
处理企业微信智能机器人的 HTTP 回调请求
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
import quart
|
||||
|
||||
from astrbot.api import logger
|
||||
@@ -41,7 +41,7 @@ class WecomAIBotServer:
|
||||
self.app = quart.Quart(__name__)
|
||||
self._setup_routes()
|
||||
|
||||
self.shutdown_event = anyio.Event()
|
||||
self.shutdown_event = asyncio.Event()
|
||||
|
||||
def _setup_routes(self):
|
||||
"""设置 Quart 路由"""
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import hashlib
|
||||
import random
|
||||
import secrets
|
||||
import string
|
||||
from typing import Any
|
||||
|
||||
@@ -53,7 +53,7 @@ def generate_random_string(length: int = 10) -> str:
|
||||
|
||||
"""
|
||||
letters = string.ascii_letters + string.digits
|
||||
return "".join(random.choice(letters) for _ in range(length))
|
||||
return "".join(secrets.choice(letters) for _ in range(length))
|
||||
|
||||
|
||||
def calculate_image_md5(image_data: bytes) -> str:
|
||||
|
||||
@@ -7,7 +7,6 @@ from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import anyio
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot.core import sp
|
||||
@@ -99,7 +98,7 @@ class FunctionToolManager:
|
||||
self.func_list: list[FuncTool] = []
|
||||
self.mcp_client_dict: dict[str, MCPClient] = {}
|
||||
"""MCP 服务列表"""
|
||||
self.mcp_client_event: dict[str, anyio.Event] = {}
|
||||
self.mcp_client_event: dict[str, asyncio.Event] = {}
|
||||
|
||||
def empty(self) -> bool:
|
||||
return len(self.func_list) == 0
|
||||
@@ -207,7 +206,7 @@ class FunctionToolManager:
|
||||
for name in mcp_server_json_obj:
|
||||
cfg = mcp_server_json_obj[name]
|
||||
if cfg.get("active", True):
|
||||
event = anyio.Event()
|
||||
event = asyncio.Event()
|
||||
asyncio.create_task(
|
||||
self._init_mcp_client_task_wrapper(name, cfg, event),
|
||||
)
|
||||
@@ -217,7 +216,7 @@ class FunctionToolManager:
|
||||
self,
|
||||
name: str,
|
||||
cfg: dict,
|
||||
event: anyio.Event,
|
||||
event: asyncio.Event,
|
||||
ready_future: asyncio.Future | None = None,
|
||||
) -> None:
|
||||
"""初始化 MCP 客户端的包装函数,用于捕获异常"""
|
||||
@@ -308,7 +307,7 @@ class FunctionToolManager:
|
||||
self,
|
||||
name: str,
|
||||
config: dict,
|
||||
event: anyio.Event | None = None,
|
||||
event: asyncio.Event | None = None,
|
||||
ready_future: asyncio.Future | None = None,
|
||||
timeout: int = 30,
|
||||
) -> None:
|
||||
@@ -317,7 +316,7 @@ class FunctionToolManager:
|
||||
Args:
|
||||
name (str): The name of the MCP server.
|
||||
config (dict): Configuration for the MCP server.
|
||||
event (anyio.Event): Event to signal when the MCP client is ready.
|
||||
event (asyncio.Event): Event to signal when the MCP client is ready.
|
||||
ready_future (asyncio.Future): Future to signal when the MCP client is ready.
|
||||
timeout (int): Timeout for the initialization.
|
||||
|
||||
@@ -327,7 +326,7 @@ class FunctionToolManager:
|
||||
|
||||
"""
|
||||
if not event:
|
||||
event = anyio.Event()
|
||||
event = asyncio.Event()
|
||||
if not ready_future:
|
||||
ready_future = asyncio.Future()
|
||||
if name in self.mcp_client_dict:
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import secrets
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
@@ -54,7 +54,9 @@ class OTTSProvider:
|
||||
async def _generate_signature(self) -> str:
|
||||
await self._sync_time()
|
||||
timestamp = int(time.time()) + self.time_offset
|
||||
nonce = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=10))
|
||||
nonce = "".join(
|
||||
secrets.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(10)
|
||||
)
|
||||
path = re.sub(r"^https?://[^/]+", "", self.api_url) or "/"
|
||||
return f"{timestamp}-{nonce}-0-{hashlib.md5(f'{path}-{timestamp}-{nonce}-0-{self.skey}'.encode()).hexdigest()}"
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import logging
|
||||
from asyncio import Queue
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
from anyio.streams.memory import MemoryObjectSendStream
|
||||
from deprecated import deprecated
|
||||
|
||||
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
@@ -50,7 +50,7 @@ class Context:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
event_queue: MemoryObjectSendStream,
|
||||
event_queue: Queue,
|
||||
config: AstrBotConfig,
|
||||
db: BaseDatabase,
|
||||
provider_manager: ProviderManager,
|
||||
@@ -193,7 +193,7 @@ class Context:
|
||||
"""获取 AstrBot 数据库。"""
|
||||
return self._db
|
||||
|
||||
def get_event_queue(self) -> MemoryObjectSendStream:
|
||||
def get_event_queue(self) -> Queue:
|
||||
"""获取事件队列。"""
|
||||
return self._event_queue
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ class CommandGroupFilter(HandlerFilter):
|
||||
prefix + "│ ",
|
||||
event=event,
|
||||
cfg=cfg,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return "".join(parts)
|
||||
|
||||
@@ -680,11 +680,18 @@ class PluginManager:
|
||||
|
||||
return plugin_info
|
||||
|
||||
async def uninstall_plugin(self, plugin_name: str):
|
||||
async def uninstall_plugin(
|
||||
self,
|
||||
plugin_name: str,
|
||||
delete_config: bool = False,
|
||||
delete_data: bool = False,
|
||||
):
|
||||
"""卸载指定的插件。
|
||||
|
||||
Args:
|
||||
plugin_name (str): 要卸载的插件名称
|
||||
delete_config (bool): 是否删除插件配置文件,默认为 False
|
||||
delete_data (bool): 是否删除插件数据,默认为 False
|
||||
|
||||
Raises:
|
||||
Exception: 当插件不存在、是保留插件时,或删除插件文件夹失败时抛出异常
|
||||
@@ -714,6 +721,7 @@ class PluginManager:
|
||||
|
||||
await self._unbind_plugin(plugin_name, plugin.module_path)
|
||||
|
||||
# 删除插件文件夹
|
||||
try:
|
||||
remove_dir(os.path.join(ppath, root_dir_name))
|
||||
except Exception as e:
|
||||
@@ -721,6 +729,51 @@ class PluginManager:
|
||||
f"移除插件成功,但是删除插件文件夹失败: {e!s}。您可以手动删除该文件夹,位于 addons/plugins/ 下。",
|
||||
)
|
||||
|
||||
# 删除插件配置文件
|
||||
if delete_config and root_dir_name:
|
||||
config_file = os.path.join(
|
||||
self.plugin_config_path,
|
||||
f"{root_dir_name}_config.json",
|
||||
)
|
||||
if os.path.exists(config_file):
|
||||
try:
|
||||
os.remove(config_file)
|
||||
logger.info(f"已删除插件 {plugin_name} 的配置文件")
|
||||
except Exception as e:
|
||||
logger.warning(f"删除插件配置文件失败: {e!s}")
|
||||
|
||||
# 删除插件持久化数据
|
||||
# 注意:需要检查两个可能的目录名(plugin_data 和 plugins_data)
|
||||
# data/temp 目录可能被多个插件共享,不自动删除以防误删
|
||||
if delete_data and root_dir_name:
|
||||
data_base_dir = os.path.dirname(ppath) # data/
|
||||
|
||||
# 删除 data/plugin_data 下的插件持久化数据(单数形式,新版本)
|
||||
plugin_data_dir = os.path.join(
|
||||
data_base_dir, "plugin_data", root_dir_name
|
||||
)
|
||||
if os.path.exists(plugin_data_dir):
|
||||
try:
|
||||
remove_dir(plugin_data_dir)
|
||||
logger.info(
|
||||
f"已删除插件 {plugin_name} 的持久化数据 (plugin_data)"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"删除插件持久化数据失败 (plugin_data): {e!s}")
|
||||
|
||||
# 删除 data/plugins_data 下的插件持久化数据(复数形式,旧版本兼容)
|
||||
plugins_data_dir = os.path.join(
|
||||
data_base_dir, "plugins_data", root_dir_name
|
||||
)
|
||||
if os.path.exists(plugins_data_dir):
|
||||
try:
|
||||
remove_dir(plugins_data_dir)
|
||||
logger.info(
|
||||
f"已删除插件 {plugin_name} 的持久化数据 (plugins_data)"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"删除插件持久化数据失败 (plugins_data): {e!s}")
|
||||
|
||||
async def _unbind_plugin(self, plugin_name: str, plugin_module_path: str):
|
||||
"""解绑并移除一个插件。
|
||||
|
||||
|
||||
@@ -30,9 +30,7 @@ class UmopConfigRouter:
|
||||
if len(p1_ls) != 3 or len(p2_ls) != 3:
|
||||
return False # 非法格式
|
||||
|
||||
return all(
|
||||
p == "" or p == "*" or p == t for p, t in zip(p1_ls, p2_ls, strict=False)
|
||||
)
|
||||
return all(p == "" or p == "*" or p == t for p, t in zip(p1_ls, p2_ls))
|
||||
|
||||
def get_conf_id_for_umop(self, umo: str) -> str | None:
|
||||
"""根据 UMO 获取对应的配置文件 ID
|
||||
|
||||
@@ -105,16 +105,31 @@ async def download_image_by_url(
|
||||
f.write(await resp.read())
|
||||
return path
|
||||
except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
|
||||
# 关闭SSL验证
|
||||
# 关闭SSL验证(仅在证书验证失败时作为fallback)
|
||||
logger.warning(
|
||||
f"SSL certificate verification failed for {url}. "
|
||||
"Disabling SSL verification (CERT_NONE) as a fallback. "
|
||||
"This is insecure and exposes the application to man-in-the-middle attacks. "
|
||||
"Please investigate and resolve certificate issues."
|
||||
)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.set_ciphers("DEFAULT")
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if post:
|
||||
async with session.get(url, ssl=ssl_context) as resp:
|
||||
return save_temp_img(await resp.read())
|
||||
async with session.post(url, json=post_data, ssl=ssl_context) as resp:
|
||||
if not path:
|
||||
return save_temp_img(await resp.read())
|
||||
with open(path, "wb") as f:
|
||||
f.write(await resp.read())
|
||||
return path
|
||||
else:
|
||||
async with session.get(url, ssl=ssl_context) as resp:
|
||||
return save_temp_img(await resp.read())
|
||||
if not path:
|
||||
return save_temp_img(await resp.read())
|
||||
with open(path, "wb") as f:
|
||||
f.write(await resp.read())
|
||||
return path
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
@@ -157,9 +172,19 @@ async def download_file(url: str, path: str, show_progress: bool = False):
|
||||
end="",
|
||||
)
|
||||
except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError):
|
||||
# 关闭SSL验证
|
||||
# 关闭SSL验证(仅在证书验证失败时作为fallback)
|
||||
logger.warning(
|
||||
"SSL 证书验证失败,已关闭 SSL 验证(不安全,仅用于临时下载)。请检查目标服务器的证书配置。"
|
||||
)
|
||||
logger.warning(
|
||||
f"SSL certificate verification failed for {url}. "
|
||||
"Falling back to unverified connection (CERT_NONE). "
|
||||
"This is insecure and exposes the application to man-in-the-middle attacks. "
|
||||
"Please investigate certificate issues with the remote server."
|
||||
)
|
||||
ssl_context = ssl.create_default_context()
|
||||
ssl_context.set_ciphers("DEFAULT")
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, ssl=ssl_context, timeout=120) as resp:
|
||||
total_size = int(resp.headers.get("content-length", 0))
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"""会话控制"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import asyncio
|
||||
import copy
|
||||
@@ -10,13 +8,11 @@ import time
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
|
||||
import astrbot.core.message.components as Comp
|
||||
from astrbot.core.platform import AstrMessageEvent
|
||||
|
||||
USER_SESSIONS: dict[str, SessionWaiter] = {} # 存储 SessionWaiter 实例
|
||||
FILTERS: list[SessionFilter] = [] # 存储 SessionFilter 实例
|
||||
USER_SESSIONS: dict[str, "SessionWaiter"] = {} # 存储 SessionWaiter 实例
|
||||
FILTERS: list["SessionFilter"] = [] # 存储 SessionFilter 实例
|
||||
|
||||
|
||||
class SessionController:
|
||||
@@ -24,16 +20,16 @@ class SessionController:
|
||||
|
||||
def __init__(self):
|
||||
self.future = asyncio.Future()
|
||||
self.current_event: anyio.Event | None = None
|
||||
self.current_event: asyncio.Event = None
|
||||
"""当前正在等待的所用的异步事件"""
|
||||
self.ts: float | None = None
|
||||
self.ts: float = None
|
||||
"""上次保持(keep)开始时的时间"""
|
||||
self.timeout: float | int | None = None
|
||||
self.timeout: float | int = None
|
||||
"""上次保持(keep)开始时的超时时间"""
|
||||
|
||||
self.history_chains: list[list[Comp.BaseMessageComponent]] = []
|
||||
|
||||
def stop(self, error: Exception | None = None):
|
||||
def stop(self, error: Exception = None):
|
||||
"""立即结束这个会话"""
|
||||
if not self.future.done():
|
||||
if error:
|
||||
@@ -57,9 +53,7 @@ class SessionController:
|
||||
self.stop()
|
||||
return
|
||||
else:
|
||||
current_timeout = self.timeout if self.timeout is not None else 0
|
||||
current_ts = self.ts if self.ts is not None else new_ts
|
||||
left_timeout = current_timeout - (new_ts - current_ts)
|
||||
left_timeout = self.timeout - (new_ts - self.ts)
|
||||
timeout = left_timeout + timeout
|
||||
if timeout <= 0:
|
||||
self.stop()
|
||||
@@ -68,19 +62,18 @@ class SessionController:
|
||||
if self.current_event and not self.current_event.is_set():
|
||||
self.current_event.set() # 通知上一个 keep 结束
|
||||
|
||||
new_event = anyio.Event()
|
||||
new_event = asyncio.Event()
|
||||
self.ts = new_ts
|
||||
self.current_event = new_event
|
||||
self.timeout = timeout
|
||||
|
||||
anyio.create_task(self._holding(new_event, timeout)) # 开始新的 keep
|
||||
asyncio.create_task(self._holding(new_event, timeout)) # 开始新的 keep
|
||||
|
||||
async def _holding(self, event: anyio.Event, timeout_seconds: float):
|
||||
async def _holding(self, event: asyncio.Event, timeout: int):
|
||||
"""等待事件结束或超时"""
|
||||
try:
|
||||
with anyio.move_on_after(timeout_seconds):
|
||||
await event.wait()
|
||||
except TimeoutError:
|
||||
await asyncio.wait_for(event.wait(), timeout)
|
||||
except asyncio.TimeoutError:
|
||||
if not self.future.done():
|
||||
self.future.set_exception(TimeoutError("等待超时"))
|
||||
except asyncio.CancelledError:
|
||||
@@ -112,12 +105,10 @@ class SessionWaiter:
|
||||
session_filter: SessionFilter,
|
||||
session_id: str,
|
||||
record_history_chains: bool,
|
||||
) -> None:
|
||||
):
|
||||
self.session_id = session_id
|
||||
self.session_filter = session_filter
|
||||
self.handler: (
|
||||
Callable[[SessionController, AstrMessageEvent], Awaitable[Any]] | None
|
||||
) = None # 处理函数
|
||||
self.handler: Callable[[str], Awaitable[Any]] | None = None # 处理函数
|
||||
|
||||
self.session_controller = SessionController()
|
||||
self.record_history_chains = record_history_chains
|
||||
@@ -128,15 +119,15 @@ class SessionWaiter:
|
||||
|
||||
async def register_wait(
|
||||
self,
|
||||
handler: Callable[[SessionController, AstrMessageEvent], Awaitable[Any]],
|
||||
timeout_seconds: int = 30,
|
||||
handler: Callable[[str], Awaitable[Any]],
|
||||
timeout: int = 30,
|
||||
) -> Any:
|
||||
"""等待外部输入并处理"""
|
||||
self.handler = handler
|
||||
USER_SESSIONS[self.session_id] = self
|
||||
|
||||
# 开始一个会话保持事件
|
||||
self.session_controller.keep(timeout_seconds, reset_timeout=True)
|
||||
self.session_controller.keep(timeout, reset_timeout=True)
|
||||
|
||||
try:
|
||||
return await self.session_controller.future
|
||||
@@ -146,7 +137,7 @@ class SessionWaiter:
|
||||
finally:
|
||||
self._cleanup()
|
||||
|
||||
def _cleanup(self, error: Exception | None = None):
|
||||
def _cleanup(self, error: Exception = None):
|
||||
"""清理会话"""
|
||||
USER_SESSIONS.pop(self.session_id, None)
|
||||
try:
|
||||
@@ -162,10 +153,6 @@ class SessionWaiter:
|
||||
if not session or session.session_controller.future.done():
|
||||
return
|
||||
|
||||
# 此时 session 不会是 None,因为上面的检查
|
||||
if session is None:
|
||||
return
|
||||
|
||||
async with session._lock:
|
||||
if not session.session_controller.future.done():
|
||||
if session.record_history_chains:
|
||||
@@ -174,8 +161,7 @@ class SessionWaiter:
|
||||
)
|
||||
try:
|
||||
# TODO: 这里使用 create_task,跟踪 task,防止超时后这里 handler 仍然在执行
|
||||
if session.handler is not None:
|
||||
await session.handler(session.session_controller, event)
|
||||
await session.handler(session.session_controller, event)
|
||||
except Exception as e:
|
||||
session.session_controller.stop(e)
|
||||
|
||||
@@ -187,13 +173,11 @@ def session_waiter(timeout: int = 30, record_history_chains: bool = False):
|
||||
:param record_history_chain: 是否自动记录历史消息链。可以通过 controller.get_history_chains() 获取。深拷贝。
|
||||
"""
|
||||
|
||||
def decorator(
|
||||
func: Callable[[SessionController, AstrMessageEvent], Awaitable[Any]],
|
||||
):
|
||||
def decorator(func: Callable[[str], Awaitable[Any]]):
|
||||
@functools.wraps(func)
|
||||
async def wrapper(
|
||||
event: AstrMessageEvent,
|
||||
session_filter: SessionFilter | None = None,
|
||||
session_filter: SessionFilter = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
|
||||
import anyio
|
||||
import jwt
|
||||
from quart import request
|
||||
|
||||
@@ -44,7 +44,7 @@ class AuthRoute(Route):
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
await anyio.sleep(3)
|
||||
await asyncio.sleep(3)
|
||||
return Response().error("用户名或密码错误").__dict__
|
||||
|
||||
async def edit_account(self):
|
||||
|
||||
@@ -4,7 +4,6 @@ import os
|
||||
import uuid
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
import anyio
|
||||
from quart import Response as QuartResponse
|
||||
from quart import g, make_response, request
|
||||
|
||||
@@ -126,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()
|
||||
@@ -189,8 +190,8 @@ class ChatRoute(Route):
|
||||
|
||||
try:
|
||||
if not client_disconnected:
|
||||
await anyio.sleep(0.05)
|
||||
except anyio.get_cancelled_exc_class():
|
||||
await asyncio.sleep(0.05)
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"[WebChat] 用户 {username} 断开聊天长连接。")
|
||||
client_disconnected = True
|
||||
|
||||
@@ -225,6 +226,7 @@ class ChatRoute(Route):
|
||||
"audio_url": audio_url,
|
||||
"selected_provider": selected_provider,
|
||||
"selected_model": selected_model,
|
||||
"enable_streaming": enable_streaming,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -817,8 +817,7 @@ class ConfigRoute(Route):
|
||||
cached_token = self._logo_token_cache[cache_key]
|
||||
# 确保platform_default_tmpl[platform.name]存在且为字典
|
||||
if platform.name not in platform_default_tmpl or not isinstance(
|
||||
platform_default_tmpl[platform.name],
|
||||
dict,
|
||||
platform_default_tmpl[platform.name], dict
|
||||
):
|
||||
platform_default_tmpl[platform.name] = {}
|
||||
platform_default_tmpl[platform.name]["logo_token"] = cached_token
|
||||
@@ -847,8 +846,7 @@ class ConfigRoute(Route):
|
||||
|
||||
# 确保platform_default_tmpl[platform.name]存在且为字典
|
||||
if platform.name not in platform_default_tmpl or not isinstance(
|
||||
platform_default_tmpl[platform.name],
|
||||
dict,
|
||||
platform_default_tmpl[platform.name], dict
|
||||
):
|
||||
platform_default_tmpl[platform.name] = {}
|
||||
|
||||
|
||||
@@ -395,9 +395,15 @@ class PluginRoute(Route):
|
||||
|
||||
post_data = await request.json
|
||||
plugin_name = post_data["name"]
|
||||
delete_config = post_data.get("delete_config", False)
|
||||
delete_data = post_data.get("delete_data", False)
|
||||
try:
|
||||
logger.info(f"正在卸载插件 {plugin_name}")
|
||||
await self.plugin_manager.uninstall_plugin(plugin_name)
|
||||
await self.plugin_manager.uninstall_plugin(
|
||||
plugin_name,
|
||||
delete_config=delete_config,
|
||||
delete_data=delete_data,
|
||||
)
|
||||
logger.info(f"卸载插件 {plugin_name} 成功")
|
||||
return Response().ok(None, "卸载成功").__dict__
|
||||
except Exception as e:
|
||||
|
||||
@@ -296,7 +296,15 @@ class ToolsRoute(Route):
|
||||
"""获取所有注册的工具列表"""
|
||||
try:
|
||||
tools = self.tool_mgr.func_list
|
||||
tools_dict = [tool.__dict__() for tool in tools]
|
||||
tools_dict = [
|
||||
{
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": tool.parameters,
|
||||
"active": tool.active,
|
||||
}
|
||||
for tool in tools
|
||||
]
|
||||
return Response().ok(data=tools_dict).__dict__
|
||||
except Exception as e:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
134
build_nuitka.py
Normal file
@@ -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)
|
||||
8
changelogs/v4.5.2.md
Normal file
@@ -0,0 +1,8 @@
|
||||
## What's Changed
|
||||
|
||||
1. 修复:>= Python 3.12 版本下可能导致 LLM Tool 注册错误的问题。
|
||||
2. 优化:更好地适配 Class 方式注册 LLM Tool 的场景。引入 `call` 方法。
|
||||
3. 新增:`ConversationManager` 类支持 `add_message_pair` 方法,简化对话消息的添加操作。
|
||||
4. 新增:增加对 Tool Parameters 的参数验证,确保工具参数符合 JSON Schema 标准。
|
||||
5. 新增:增加 LLM Message Schema 定义,提升消息结构的规范性和一致性。
|
||||
6. 新增:支持对 WebUI 的侧边栏模块进行自定义配置(入口在侧边栏下方的设置页中)。
|
||||
5
changelogs/v4.5.3.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## What's Changed
|
||||
|
||||
> hotfix version of 4.5.2
|
||||
|
||||
1. 修复:修正 `get_tool_list` 方法中工具字典推导式的错误导致的 WebUI MCP 页面工具列表无法显示的问题。
|
||||
5
changelogs/v4.5.4.md
Normal file
@@ -0,0 +1,5 @@
|
||||
## What's Changed
|
||||
|
||||
1. 修复:Docker 镜像部分依赖问题导致某些情况下无法启动容器的问题;
|
||||
2. 优化:插件卡片样式
|
||||
3. 修复:部分情况下 Windows 一键启动部署时,更新 / 部署失败的问题;
|
||||
3
changelogs/v4.5.5.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## What's Changed
|
||||
|
||||
1. 修复:部署失败
|
||||
3
changelogs/v4.5.6.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## What's Changed
|
||||
|
||||
1. 修复:构建失败
|
||||
3
dashboard/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
dist/
|
||||
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 |