Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7cedf0d587 | |||
| aeb21f719e | |||
| 7c1dbecea5 | |||
| 05012af627 | |||
| 17b52ab5dd | |||
| 9449ff668b | |||
| c5a2827def | |||
| 701399c00c | |||
| eaee98d4b8 | |||
| 76c66000a7 | |||
| 4b365143c0 | |||
| 6e4e5011e2 | |||
| d853bfde84 | |||
| a0e856f80f | |||
| 8c94a0010c | |||
| a44fdaaec0 | |||
| 60105c76f5 | |||
| bcf87d3ce4 | |||
| 4d7c8c8453 | |||
| a064a9115f | |||
| 6ef99e1553 | |||
| c0dbe5cf65 | |||
| 3598c51eff | |||
| b5cdb8f650 | |||
| fc5b520f9b | |||
| 904f56b32f | |||
| 2f15fd019c | |||
| 82330b8d10 | |||
| 3ee6af7027 | |||
| 6e20ebe901 | |||
| 4d6150fd6d | |||
| 544e52191b | |||
| f2c2a6da4a | |||
| dd3df425ee | |||
| 40b4a27a3d | |||
| 9d991c7468 | |||
| ad6a8b5c94 | |||
| 1b4bfcbd72 | |||
| 9d3cc593a1 | |||
| f0dee35ba9 | |||
| 4135bd84d5 | |||
| f6da614e5d | |||
| e8b54a019e | |||
| 98ce796275 | |||
| b87dcf2275 | |||
| 591a228431 | |||
| f52f375154 | |||
| 975c685a17 | |||
| 6db80d36a8 | |||
| 4651bd2807 | |||
| 94ada3793e | |||
| 4d046f8490 | |||
| 903dd0f9f7 | |||
| 1acac0cac2 | |||
| 67c33b842d | |||
| 5431c9f46e | |||
| 764b91a5f7 | |||
| c20c1b84bf | |||
| fd66a0ac00 | |||
| b2e9dab233 | |||
| 45110200ea | |||
| a70088b799 | |||
| bb45d9cb54 |
@@ -1,79 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -32,7 +32,6 @@ tests/astrbot_plugin_openai
|
|||||||
# Dashboard
|
# Dashboard
|
||||||
dashboard/node_modules/
|
dashboard/node_modules/
|
||||||
dashboard/dist/
|
dashboard/dist/
|
||||||
dashboard/src-tauri/target
|
|
||||||
package-lock.json
|
package-lock.json
|
||||||
package.json
|
package.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
@@ -49,6 +48,5 @@ astrbot.lock
|
|||||||
chroma
|
chroma
|
||||||
venv/*
|
venv/*
|
||||||
pytest.ini
|
pytest.ini
|
||||||
build/
|
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
IFLOW.md
|
IFLOW.md
|
||||||
|
|||||||
@@ -1,287 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||

|

|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "4.9.2"
|
__version__ = "4.10.2"
|
||||||
|
|||||||
@@ -76,12 +76,20 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
|
|
||||||
async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
|
async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
|
||||||
"""Yields chunks *and* a final LLMResponse."""
|
"""Yields chunks *and* a final LLMResponse."""
|
||||||
|
payload = {
|
||||||
|
"contexts": self.run_context.messages, # list[Message]
|
||||||
|
"func_tool": self.req.func_tool,
|
||||||
|
"model": self.req.model, # NOTE: in fact, this arg is None in most cases
|
||||||
|
"session_id": self.req.session_id,
|
||||||
|
"extra_user_content_parts": self.req.extra_user_content_parts, # list[ContentPart]
|
||||||
|
}
|
||||||
|
|
||||||
if self.streaming:
|
if self.streaming:
|
||||||
stream = self.provider.text_chat_stream(**self.req.__dict__)
|
stream = self.provider.text_chat_stream(**payload)
|
||||||
async for resp in stream: # type: ignore
|
async for resp in stream: # type: ignore
|
||||||
yield resp
|
yield resp
|
||||||
else:
|
else:
|
||||||
yield await self.provider.text_chat(**self.req.__dict__)
|
yield await self.provider.text_chat(**payload)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def step(self):
|
async def step(self):
|
||||||
@@ -165,7 +173,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
self.run_context.messages.append(
|
self.run_context.messages.append(
|
||||||
Message(
|
Message(
|
||||||
role="assistant",
|
role="assistant",
|
||||||
content=llm_resp.completion_text or "",
|
content=llm_resp.completion_text or "*No response*",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
@@ -230,6 +238,25 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
async for resp in self.step():
|
async for resp in self.step():
|
||||||
yield resp
|
yield resp
|
||||||
|
|
||||||
|
# 如果循环结束了但是 agent 还没有完成,说明是达到了 max_step
|
||||||
|
if not self.done():
|
||||||
|
logger.warning(
|
||||||
|
f"Agent reached max steps ({max_step}), forcing a final response."
|
||||||
|
)
|
||||||
|
# 拔掉所有工具
|
||||||
|
if self.req:
|
||||||
|
self.req.func_tool = None
|
||||||
|
# 注入提示词
|
||||||
|
self.run_context.messages.append(
|
||||||
|
Message(
|
||||||
|
role="user",
|
||||||
|
content="工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# 再执行最后一步
|
||||||
|
async for resp in self.step():
|
||||||
|
yield resp
|
||||||
|
|
||||||
async def _handle_function_tools(
|
async def _handle_function_tools(
|
||||||
self,
|
self,
|
||||||
req: ProviderRequest,
|
req: ProviderRequest,
|
||||||
@@ -376,35 +403,33 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# yield the last tool call result
|
|
||||||
if tool_call_result_blocks:
|
|
||||||
last_tcr_content = str(tool_call_result_blocks[-1].content)
|
|
||||||
yield MessageChain(
|
|
||||||
type="tool_call_result",
|
|
||||||
chain=[
|
|
||||||
Json(
|
|
||||||
data={
|
|
||||||
"id": func_tool_id,
|
|
||||||
"ts": time.time(),
|
|
||||||
"result": last_tcr_content,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
elif resp is None:
|
elif resp is None:
|
||||||
# Tool 直接请求发送消息给用户
|
# Tool 直接请求发送消息给用户
|
||||||
# 这里我们将直接结束 Agent Loop。
|
# 这里我们将直接结束 Agent Loop。
|
||||||
# 发送消息逻辑在 ToolExecutor 中处理了。
|
# 发送消息逻辑在 ToolExecutor 中处理了。
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"{func_tool_name} 没有没有返回值或者将结果直接发送给用户,此工具调用不会被记录到历史中。"
|
f"{func_tool_name} 没有没有返回值或者将结果直接发送给用户。"
|
||||||
)
|
)
|
||||||
self._transition_state(AgentState.DONE)
|
self._transition_state(AgentState.DONE)
|
||||||
self.stats.end_time = time.time()
|
self.stats.end_time = time.time()
|
||||||
|
tool_call_result_blocks.append(
|
||||||
|
ToolCallMessageSegment(
|
||||||
|
role="tool",
|
||||||
|
tool_call_id=func_tool_id,
|
||||||
|
content="*工具没有返回值或者将结果直接发送给了用户*",
|
||||||
|
),
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# 不应该出现其他类型
|
# 不应该出现其他类型
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Tool 返回了不支持的类型: {type(resp)},将忽略。",
|
f"Tool 返回了不支持的类型: {type(resp)}。",
|
||||||
|
)
|
||||||
|
tool_call_result_blocks.append(
|
||||||
|
ToolCallMessageSegment(
|
||||||
|
role="tool",
|
||||||
|
tool_call_id=func_tool_id,
|
||||||
|
content="*工具返回了不支持的类型,请告诉用户检查这个工具的定义和实现。*",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -426,6 +451,22 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# yield the last tool call result
|
||||||
|
if tool_call_result_blocks:
|
||||||
|
last_tcr_content = str(tool_call_result_blocks[-1].content)
|
||||||
|
yield MessageChain(
|
||||||
|
type="tool_call_result",
|
||||||
|
chain=[
|
||||||
|
Json(
|
||||||
|
data={
|
||||||
|
"id": func_tool_id,
|
||||||
|
"ts": time.time(),
|
||||||
|
"result": last_tcr_content,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
# 处理函数调用响应
|
# 处理函数调用响应
|
||||||
if tool_call_result_blocks:
|
if tool_call_result_blocks:
|
||||||
yield tool_call_result_blocks
|
yield tool_call_result_blocks
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import traceback
|
|||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
from astrbot.core import logger
|
from astrbot.core import logger
|
||||||
|
from astrbot.core.agent.message import Message
|
||||||
from astrbot.core.agent.runners.tool_loop_agent_runner import ToolLoopAgentRunner
|
from astrbot.core.agent.runners.tool_loop_agent_runner import ToolLoopAgentRunner
|
||||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||||
from astrbot.core.message.components import Json
|
from astrbot.core.message.components import Json
|
||||||
@@ -24,8 +25,25 @@ async def run_agent(
|
|||||||
) -> AsyncGenerator[MessageChain | None, None]:
|
) -> AsyncGenerator[MessageChain | None, None]:
|
||||||
step_idx = 0
|
step_idx = 0
|
||||||
astr_event = agent_runner.run_context.context.event
|
astr_event = agent_runner.run_context.context.event
|
||||||
while step_idx < max_step:
|
while step_idx < max_step + 1:
|
||||||
step_idx += 1
|
step_idx += 1
|
||||||
|
|
||||||
|
if step_idx == max_step + 1:
|
||||||
|
logger.warning(
|
||||||
|
f"Agent reached max steps ({max_step}), forcing a final response."
|
||||||
|
)
|
||||||
|
if not agent_runner.done():
|
||||||
|
# 拔掉所有工具
|
||||||
|
if agent_runner.req:
|
||||||
|
agent_runner.req.func_tool = None
|
||||||
|
# 注入提示词
|
||||||
|
agent_runner.run_context.messages.append(
|
||||||
|
Message(
|
||||||
|
role="user",
|
||||||
|
content="工具调用次数已达到上限,请停止使用工具,并根据已经收集到的信息,对你的任务和发现进行总结,然后直接回复用户。",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async for resp in agent_runner.step():
|
async for resp in agent_runner.step():
|
||||||
if astr_event.is_stopped():
|
if astr_event.is_stopped():
|
||||||
|
|||||||
@@ -209,12 +209,42 @@ async def call_local_llm_tool(
|
|||||||
else:
|
else:
|
||||||
raise ValueError(f"未知的方法名: {method_name}")
|
raise ValueError(f"未知的方法名: {method_name}")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"调用本地 LLM 工具时出错: {e}", exc_info=True)
|
raise Exception(f"Tool execution ValueError: {e}") from e
|
||||||
except TypeError:
|
except TypeError as e:
|
||||||
logger.error("处理函数参数不匹配,请检查 handler 的定义。", exc_info=True)
|
# 获取函数的签名(包括类型),除了第一个 event/context 参数。
|
||||||
|
try:
|
||||||
|
sig = inspect.signature(handler)
|
||||||
|
params = list(sig.parameters.values())
|
||||||
|
# 跳过第一个参数(event 或 context)
|
||||||
|
if params:
|
||||||
|
params = params[1:]
|
||||||
|
|
||||||
|
param_strs = []
|
||||||
|
for param in params:
|
||||||
|
param_str = param.name
|
||||||
|
if param.annotation != inspect.Parameter.empty:
|
||||||
|
# 获取类型注解的字符串表示
|
||||||
|
if isinstance(param.annotation, type):
|
||||||
|
type_str = param.annotation.__name__
|
||||||
|
else:
|
||||||
|
type_str = str(param.annotation)
|
||||||
|
param_str += f": {type_str}"
|
||||||
|
if param.default != inspect.Parameter.empty:
|
||||||
|
param_str += f" = {param.default!r}"
|
||||||
|
param_strs.append(param_str)
|
||||||
|
|
||||||
|
handler_param_str = (
|
||||||
|
", ".join(param_strs) if param_strs else "(no additional parameters)"
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
handler_param_str = "(unable to inspect signature)"
|
||||||
|
|
||||||
|
raise Exception(
|
||||||
|
f"Tool handler parameter mismatch, please check the handler definition. Handler parameters: {handler_param_str}"
|
||||||
|
) from e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
trace_ = traceback.format_exc()
|
trace_ = traceback.format_exc()
|
||||||
logger.error(f"调用本地 LLM 工具时出错: {e}\n{trace_}")
|
raise Exception(f"Tool execution error: {e}. Traceback: {trace_}") from e
|
||||||
|
|
||||||
if not ready_to_call:
|
if not ready_to_call:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
"""如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。"""
|
"""如需修改配置,请在 `data/cmd_config.json` 中修改或者在管理面板中可视化修改。"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
VERSION = "4.9.2"
|
VERSION = "4.10.2"
|
||||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||||
|
|
||||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||||
@@ -61,7 +62,8 @@ DEFAULT_CONFIG = {
|
|||||||
"ignore_bot_self_message": False,
|
"ignore_bot_self_message": False,
|
||||||
"ignore_at_all": False,
|
"ignore_at_all": False,
|
||||||
},
|
},
|
||||||
"provider": [],
|
"provider_sources": [], # provider sources
|
||||||
|
"provider": [], # models from provider_sources
|
||||||
"provider_settings": {
|
"provider_settings": {
|
||||||
"enable": True,
|
"enable": True,
|
||||||
"default_provider_id": "",
|
"default_provider_id": "",
|
||||||
@@ -171,6 +173,22 @@ DEFAULT_CONFIG = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ChatProviderTemplate(TypedDict):
|
||||||
|
id: str
|
||||||
|
provider_source_id: str
|
||||||
|
model: str
|
||||||
|
modalities: list
|
||||||
|
custom_extra_body: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
CHAT_PROVIDER_TEMPLATE = {
|
||||||
|
"id": "",
|
||||||
|
"provide_source_id": "",
|
||||||
|
"model": "",
|
||||||
|
"modalities": [],
|
||||||
|
"custom_extra_body": {},
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AstrBot v3 时代的配置元数据,目前仅承担以下功能:
|
AstrBot v3 时代的配置元数据,目前仅承担以下功能:
|
||||||
|
|
||||||
@@ -844,6 +862,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"metadata": {
|
"metadata": {
|
||||||
"provider": {
|
"provider": {
|
||||||
"type": "list",
|
"type": "list",
|
||||||
|
# provider sources templates
|
||||||
"config_template": {
|
"config_template": {
|
||||||
"OpenAI": {
|
"OpenAI": {
|
||||||
"id": "openai",
|
"id": "openai",
|
||||||
@@ -854,107 +873,10 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.openai.com/v1",
|
"api_base": "https://api.openai.com/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
|
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"custom_extra_body": {},
|
|
||||||
"modalities": ["text", "image", "tool_use"],
|
|
||||||
"hint": "也兼容所有与 OpenAI API 兼容的服务。",
|
|
||||||
},
|
},
|
||||||
"Azure OpenAI": {
|
"Google Gemini": {
|
||||||
"id": "azure",
|
"id": "google_gemini",
|
||||||
"provider": "azure",
|
|
||||||
"type": "openai_chat_completion",
|
|
||||||
"provider_type": "chat_completion",
|
|
||||||
"enable": True,
|
|
||||||
"api_version": "2024-05-01-preview",
|
|
||||||
"key": [],
|
|
||||||
"api_base": "",
|
|
||||||
"timeout": 120,
|
|
||||||
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
|
|
||||||
"custom_headers": {},
|
|
||||||
"custom_extra_body": {},
|
|
||||||
"modalities": ["text", "image", "tool_use"],
|
|
||||||
},
|
|
||||||
"xAI": {
|
|
||||||
"id": "xai",
|
|
||||||
"provider": "xai",
|
|
||||||
"type": "openai_chat_completion",
|
|
||||||
"provider_type": "chat_completion",
|
|
||||||
"enable": True,
|
|
||||||
"key": [],
|
|
||||||
"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"],
|
|
||||||
},
|
|
||||||
"Anthropic": {
|
|
||||||
"hint": "注意Claude系列模型的温度调节范围为0到1.0,超出可能导致报错",
|
|
||||||
"id": "claude",
|
|
||||||
"provider": "anthropic",
|
|
||||||
"type": "anthropic_chat_completion",
|
|
||||||
"provider_type": "chat_completion",
|
|
||||||
"enable": True,
|
|
||||||
"key": [],
|
|
||||||
"api_base": "https://api.anthropic.com/v1",
|
|
||||||
"timeout": 120,
|
|
||||||
"model_config": {
|
|
||||||
"model": "claude-3-5-sonnet-latest",
|
|
||||||
"max_tokens": 4096,
|
|
||||||
"temperature": 0.2,
|
|
||||||
},
|
|
||||||
"modalities": ["text", "image", "tool_use"],
|
|
||||||
},
|
|
||||||
"Ollama": {
|
|
||||||
"hint": "启用前请确保已正确安装并运行 Ollama 服务端,Ollama默认不带鉴权,无需修改key",
|
|
||||||
"id": "ollama_default",
|
|
||||||
"provider": "ollama",
|
|
||||||
"type": "openai_chat_completion",
|
|
||||||
"provider_type": "chat_completion",
|
|
||||||
"enable": True,
|
|
||||||
"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"],
|
|
||||||
},
|
|
||||||
"LM Studio": {
|
|
||||||
"id": "lm_studio",
|
|
||||||
"provider": "lm_studio",
|
|
||||||
"type": "openai_chat_completion",
|
|
||||||
"provider_type": "chat_completion",
|
|
||||||
"enable": True,
|
|
||||||
"key": ["lmstudio"],
|
|
||||||
"api_base": "http://localhost:1234/v1",
|
|
||||||
"model_config": {
|
|
||||||
"model": "llama-3.1-8b",
|
|
||||||
},
|
|
||||||
"custom_headers": {},
|
|
||||||
"custom_extra_body": {},
|
|
||||||
"modalities": ["text", "image", "tool_use"],
|
|
||||||
},
|
|
||||||
"Gemini(OpenAI兼容)": {
|
|
||||||
"id": "gemini_default",
|
|
||||||
"provider": "google",
|
|
||||||
"type": "openai_chat_completion",
|
|
||||||
"provider_type": "chat_completion",
|
|
||||||
"enable": True,
|
|
||||||
"key": [],
|
|
||||||
"api_base": "https://generativelanguage.googleapis.com/v1beta/openai/",
|
|
||||||
"timeout": 120,
|
|
||||||
"model_config": {
|
|
||||||
"model": "gemini-3-flash-preview",
|
|
||||||
"temperature": 0.4,
|
|
||||||
},
|
|
||||||
"custom_headers": {},
|
|
||||||
"custom_extra_body": {},
|
|
||||||
"modalities": ["text", "image", "tool_use"],
|
|
||||||
},
|
|
||||||
"Gemini": {
|
|
||||||
"id": "gemini_default",
|
|
||||||
"provider": "google",
|
"provider": "google",
|
||||||
"type": "googlegenai_chat_completion",
|
"type": "googlegenai_chat_completion",
|
||||||
"provider_type": "chat_completion",
|
"provider_type": "chat_completion",
|
||||||
@@ -962,10 +884,6 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://generativelanguage.googleapis.com/",
|
"api_base": "https://generativelanguage.googleapis.com/",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"model_config": {
|
|
||||||
"model": "gemini-3-flash-preview",
|
|
||||||
"temperature": 0.4,
|
|
||||||
},
|
|
||||||
"gm_resp_image_modal": False,
|
"gm_resp_image_modal": False,
|
||||||
"gm_native_search": False,
|
"gm_native_search": False,
|
||||||
"gm_native_coderunner": False,
|
"gm_native_coderunner": False,
|
||||||
@@ -977,10 +895,42 @@ CONFIG_METADATA_2 = {
|
|||||||
"dangerous_content": "BLOCK_MEDIUM_AND_ABOVE",
|
"dangerous_content": "BLOCK_MEDIUM_AND_ABOVE",
|
||||||
},
|
},
|
||||||
"gm_thinking_config": {"budget": 0, "level": "HIGH"},
|
"gm_thinking_config": {"budget": 0, "level": "HIGH"},
|
||||||
"modalities": ["text", "image", "tool_use"],
|
},
|
||||||
|
"Anthropic": {
|
||||||
|
"id": "anthropic",
|
||||||
|
"provider": "anthropic",
|
||||||
|
"type": "anthropic_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"api_base": "https://api.anthropic.com/v1",
|
||||||
|
"timeout": 120,
|
||||||
|
},
|
||||||
|
"Moonshot": {
|
||||||
|
"id": "moonshot",
|
||||||
|
"provider": "moonshot",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"timeout": 120,
|
||||||
|
"api_base": "https://api.moonshot.cn/v1",
|
||||||
|
"custom_headers": {},
|
||||||
|
},
|
||||||
|
"xAI": {
|
||||||
|
"id": "xai",
|
||||||
|
"provider": "xai",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"api_base": "https://api.x.ai/v1",
|
||||||
|
"timeout": 120,
|
||||||
|
"custom_headers": {},
|
||||||
|
"xai_native_search": False,
|
||||||
},
|
},
|
||||||
"DeepSeek": {
|
"DeepSeek": {
|
||||||
"id": "deepseek_default",
|
"id": "deepseek",
|
||||||
"provider": "deepseek",
|
"provider": "deepseek",
|
||||||
"type": "openai_chat_completion",
|
"type": "openai_chat_completion",
|
||||||
"provider_type": "chat_completion",
|
"provider_type": "chat_completion",
|
||||||
@@ -988,13 +938,75 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.deepseek.com/v1",
|
"api_base": "https://api.deepseek.com/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"model_config": {"model": "deepseek-chat", "temperature": 0.4},
|
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"custom_extra_body": {},
|
},
|
||||||
"modalities": ["text", "tool_use"],
|
"Zhipu": {
|
||||||
|
"id": "zhipu",
|
||||||
|
"provider": "zhipu",
|
||||||
|
"type": "zhipu_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"timeout": 120,
|
||||||
|
"api_base": "https://open.bigmodel.cn/api/paas/v4/",
|
||||||
|
"custom_headers": {},
|
||||||
|
},
|
||||||
|
"Azure OpenAI": {
|
||||||
|
"id": "azure_openai",
|
||||||
|
"provider": "azure",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"api_version": "2024-05-01-preview",
|
||||||
|
"key": [],
|
||||||
|
"api_base": "",
|
||||||
|
"timeout": 120,
|
||||||
|
"custom_headers": {},
|
||||||
|
},
|
||||||
|
"Ollama": {
|
||||||
|
"id": "ollama",
|
||||||
|
"provider": "ollama",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": ["ollama"], # ollama 的 key 默认是 ollama
|
||||||
|
"api_base": "http://127.0.0.1:11434/v1",
|
||||||
|
"custom_headers": {},
|
||||||
|
},
|
||||||
|
"LM Studio": {
|
||||||
|
"id": "lm_studio",
|
||||||
|
"provider": "lm_studio",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": ["lmstudio"],
|
||||||
|
"api_base": "http://127.0.0.1:1234/v1",
|
||||||
|
"custom_headers": {},
|
||||||
|
},
|
||||||
|
"ModelStack": {
|
||||||
|
"id": "modelstack",
|
||||||
|
"provider": "modelstack",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"api_base": "https://modelstack.app/v1",
|
||||||
|
"timeout": 120,
|
||||||
|
"custom_headers": {},
|
||||||
|
},
|
||||||
|
"Gemini_OpenAI_API": {
|
||||||
|
"id": "google_gemini_openai",
|
||||||
|
"provider": "google",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"api_base": "https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||||
|
"timeout": 120,
|
||||||
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"Groq": {
|
"Groq": {
|
||||||
"id": "groq_default",
|
"id": "groq",
|
||||||
"provider": "groq",
|
"provider": "groq",
|
||||||
"type": "groq_chat_completion",
|
"type": "groq_chat_completion",
|
||||||
"provider_type": "chat_completion",
|
"provider_type": "chat_completion",
|
||||||
@@ -1002,13 +1014,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.groq.com/openai/v1",
|
"api_base": "https://api.groq.com/openai/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"model_config": {
|
|
||||||
"model": "openai/gpt-oss-20b",
|
|
||||||
"temperature": 0.4,
|
|
||||||
},
|
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"custom_extra_body": {},
|
|
||||||
"modalities": ["text", "tool_use"],
|
|
||||||
},
|
},
|
||||||
"302.AI": {
|
"302.AI": {
|
||||||
"id": "302ai",
|
"id": "302ai",
|
||||||
@@ -1019,12 +1025,9 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.302.ai/v1",
|
"api_base": "https://api.302.ai/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"model_config": {"model": "gpt-4.1-mini", "temperature": 0.4},
|
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"custom_extra_body": {},
|
|
||||||
"modalities": ["text", "image", "tool_use"],
|
|
||||||
},
|
},
|
||||||
"硅基流动": {
|
"SiliconFlow": {
|
||||||
"id": "siliconflow",
|
"id": "siliconflow",
|
||||||
"provider": "siliconflow",
|
"provider": "siliconflow",
|
||||||
"type": "openai_chat_completion",
|
"type": "openai_chat_completion",
|
||||||
@@ -1033,15 +1036,9 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"api_base": "https://api.siliconflow.cn/v1",
|
"api_base": "https://api.siliconflow.cn/v1",
|
||||||
"model_config": {
|
|
||||||
"model": "deepseek-ai/DeepSeek-V3",
|
|
||||||
"temperature": 0.4,
|
|
||||||
},
|
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"custom_extra_body": {},
|
|
||||||
"modalities": ["text", "image", "tool_use"],
|
|
||||||
},
|
},
|
||||||
"PPIO派欧云": {
|
"PPIO": {
|
||||||
"id": "ppio",
|
"id": "ppio",
|
||||||
"provider": "ppio",
|
"provider": "ppio",
|
||||||
"type": "openai_chat_completion",
|
"type": "openai_chat_completion",
|
||||||
@@ -1050,14 +1047,9 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.ppinfra.com/v3/openai",
|
"api_base": "https://api.ppinfra.com/v3/openai",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"model_config": {
|
|
||||||
"model": "deepseek/deepseek-r1",
|
|
||||||
"temperature": 0.4,
|
|
||||||
},
|
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"custom_extra_body": {},
|
|
||||||
},
|
},
|
||||||
"小马算力": {
|
"TokenPony": {
|
||||||
"id": "tokenpony",
|
"id": "tokenpony",
|
||||||
"provider": "tokenpony",
|
"provider": "tokenpony",
|
||||||
"type": "openai_chat_completion",
|
"type": "openai_chat_completion",
|
||||||
@@ -1066,14 +1058,9 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.tokenpony.cn/v1",
|
"api_base": "https://api.tokenpony.cn/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"model_config": {
|
|
||||||
"model": "kimi-k2-instruct-0905",
|
|
||||||
"temperature": 0.7,
|
|
||||||
},
|
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"custom_extra_body": {},
|
|
||||||
},
|
},
|
||||||
"优云智算": {
|
"Compshare": {
|
||||||
"id": "compshare",
|
"id": "compshare",
|
||||||
"provider": "compshare",
|
"provider": "compshare",
|
||||||
"type": "openai_chat_completion",
|
"type": "openai_chat_completion",
|
||||||
@@ -1082,42 +1069,18 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.modelverse.cn/v1",
|
"api_base": "https://api.modelverse.cn/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"model_config": {
|
|
||||||
"model": "moonshotai/Kimi-K2-Instruct",
|
|
||||||
},
|
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"custom_extra_body": {},
|
|
||||||
"modalities": ["text", "image", "tool_use"],
|
|
||||||
},
|
},
|
||||||
"Kimi": {
|
"ModelScope": {
|
||||||
"id": "moonshot",
|
"id": "modelscope",
|
||||||
"provider": "moonshot",
|
"provider": "modelscope",
|
||||||
"type": "openai_chat_completion",
|
"type": "openai_chat_completion",
|
||||||
"provider_type": "chat_completion",
|
"provider_type": "chat_completion",
|
||||||
"enable": True,
|
"enable": True,
|
||||||
"key": [],
|
"key": [],
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"api_base": "https://api.moonshot.cn/v1",
|
"api_base": "https://api-inference.modelscope.cn/v1",
|
||||||
"model_config": {"model": "moonshot-v1-8k", "temperature": 0.4},
|
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"custom_extra_body": {},
|
|
||||||
"modalities": ["text", "image", "tool_use"],
|
|
||||||
},
|
|
||||||
"智谱 AI": {
|
|
||||||
"id": "zhipu_default",
|
|
||||||
"provider": "zhipu",
|
|
||||||
"type": "zhipu_chat_completion",
|
|
||||||
"provider_type": "chat_completion",
|
|
||||||
"enable": True,
|
|
||||||
"key": [],
|
|
||||||
"timeout": 120,
|
|
||||||
"api_base": "https://open.bigmodel.cn/api/paas/v4/",
|
|
||||||
"model_config": {
|
|
||||||
"model": "glm-4-flash",
|
|
||||||
},
|
|
||||||
"custom_headers": {},
|
|
||||||
"custom_extra_body": {},
|
|
||||||
"modalities": ["text", "image", "tool_use"],
|
|
||||||
},
|
},
|
||||||
"Dify": {
|
"Dify": {
|
||||||
"id": "dify_app_default",
|
"id": "dify_app_default",
|
||||||
@@ -1132,7 +1095,6 @@ CONFIG_METADATA_2 = {
|
|||||||
"dify_query_input_key": "astrbot_text_query",
|
"dify_query_input_key": "astrbot_text_query",
|
||||||
"variables": {},
|
"variables": {},
|
||||||
"timeout": 60,
|
"timeout": 60,
|
||||||
"hint": "请确保你在 AstrBot 里设置的 APP 类型和 Dify 里面创建的应用的类型一致!",
|
|
||||||
},
|
},
|
||||||
"Coze": {
|
"Coze": {
|
||||||
"id": "coze",
|
"id": "coze",
|
||||||
@@ -1163,20 +1125,6 @@ CONFIG_METADATA_2 = {
|
|||||||
"variables": {},
|
"variables": {},
|
||||||
"timeout": 60,
|
"timeout": 60,
|
||||||
},
|
},
|
||||||
"ModelScope": {
|
|
||||||
"id": "modelscope",
|
|
||||||
"provider": "modelscope",
|
|
||||||
"type": "openai_chat_completion",
|
|
||||||
"provider_type": "chat_completion",
|
|
||||||
"enable": True,
|
|
||||||
"key": [],
|
|
||||||
"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"],
|
|
||||||
},
|
|
||||||
"FastGPT": {
|
"FastGPT": {
|
||||||
"id": "fastgpt",
|
"id": "fastgpt",
|
||||||
"provider": "fastgpt",
|
"provider": "fastgpt",
|
||||||
@@ -1200,7 +1148,6 @@ CONFIG_METADATA_2 = {
|
|||||||
"model": "whisper-1",
|
"model": "whisper-1",
|
||||||
},
|
},
|
||||||
"Whisper(Local)": {
|
"Whisper(Local)": {
|
||||||
"hint": "启用前请 pip 安装 openai-whisper 库(N卡用户大约下载 2GB,主要是 torch 和 cuda,CPU 用户大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
|
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"type": "openai_whisper_selfhost",
|
"type": "openai_whisper_selfhost",
|
||||||
"provider_type": "speech_to_text",
|
"provider_type": "speech_to_text",
|
||||||
@@ -1209,7 +1156,6 @@ CONFIG_METADATA_2 = {
|
|||||||
"model": "tiny",
|
"model": "tiny",
|
||||||
},
|
},
|
||||||
"SenseVoice(Local)": {
|
"SenseVoice(Local)": {
|
||||||
"hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库(默认使用CPU,大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
|
|
||||||
"type": "sensevoice_stt_selfhost",
|
"type": "sensevoice_stt_selfhost",
|
||||||
"provider": "sensevoice",
|
"provider": "sensevoice",
|
||||||
"provider_type": "speech_to_text",
|
"provider_type": "speech_to_text",
|
||||||
@@ -1231,7 +1177,6 @@ CONFIG_METADATA_2 = {
|
|||||||
"timeout": "20",
|
"timeout": "20",
|
||||||
},
|
},
|
||||||
"Edge TTS": {
|
"Edge TTS": {
|
||||||
"hint": "提示:使用这个服务前需要安装有 ffmpeg,并且可以直接在终端调用 ffmpeg 指令。",
|
|
||||||
"id": "edge_tts",
|
"id": "edge_tts",
|
||||||
"provider": "microsoft",
|
"provider": "microsoft",
|
||||||
"type": "edge_tts",
|
"type": "edge_tts",
|
||||||
@@ -1447,6 +1392,10 @@ CONFIG_METADATA_2 = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"items": {
|
"items": {
|
||||||
|
"provider_source_id": {
|
||||||
|
"invisible": True,
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
"xai_native_search": {
|
"xai_native_search": {
|
||||||
"description": "启用原生搜索功能",
|
"description": "启用原生搜索功能",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
@@ -2015,7 +1964,6 @@ CONFIG_METADATA_2 = {
|
|||||||
"id": {
|
"id": {
|
||||||
"description": "ID",
|
"description": "ID",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"hint": "模型提供商名字。",
|
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"description": "模型提供商种类",
|
"description": "模型提供商种类",
|
||||||
@@ -2035,29 +1983,15 @@ CONFIG_METADATA_2 = {
|
|||||||
"description": "API Key",
|
"description": "API Key",
|
||||||
"type": "list",
|
"type": "list",
|
||||||
"items": {"type": "string"},
|
"items": {"type": "string"},
|
||||||
"hint": "提供商 API Key。",
|
|
||||||
},
|
},
|
||||||
"api_base": {
|
"api_base": {
|
||||||
"description": "API Base URL",
|
"description": "API Base URL",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"hint": "API Base URL 请在模型提供商处获得。如出现 404 报错,尝试在地址末尾加上 /v1",
|
|
||||||
},
|
},
|
||||||
"model_config": {
|
"model": {
|
||||||
"description": "模型配置",
|
"description": "模型 ID",
|
||||||
"type": "object",
|
"type": "string",
|
||||||
"items": {
|
"hint": "模型名称,如 gpt-4o-mini, deepseek-chat。",
|
||||||
"model": {
|
|
||||||
"description": "模型名称",
|
|
||||||
"type": "string",
|
|
||||||
"hint": "模型名称,如 gpt-4o-mini, deepseek-chat。",
|
|
||||||
},
|
|
||||||
"max_tokens": {
|
|
||||||
"description": "模型最大输出长度(tokens)",
|
|
||||||
"type": "int",
|
|
||||||
},
|
|
||||||
"temperature": {"description": "温度", "type": "float"},
|
|
||||||
"top_p": {"description": "Top P值", "type": "float"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"dify_api_key": {
|
"dify_api_key": {
|
||||||
"description": "API Key",
|
"description": "API Key",
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from astrbot.core.star.context import Context
|
|||||||
from astrbot.core.star.star_handler import EventType, star_handlers_registry, star_map
|
from astrbot.core.star.star_handler import EventType, star_handlers_registry, star_map
|
||||||
from astrbot.core.umop_config_router import UmopConfigRouter
|
from astrbot.core.umop_config_router import UmopConfigRouter
|
||||||
from astrbot.core.updator import AstrBotUpdator
|
from astrbot.core.updator import AstrBotUpdator
|
||||||
|
from astrbot.core.utils.llm_metadata import update_llm_metadata
|
||||||
from astrbot.core.utils.migra_helper import migra
|
from astrbot.core.utils.migra_helper import migra
|
||||||
|
|
||||||
from . import astrbot_config, html_renderer
|
from . import astrbot_config, html_renderer
|
||||||
@@ -185,6 +186,8 @@ class AstrBotCoreLifecycle:
|
|||||||
# 初始化关闭控制面板的事件
|
# 初始化关闭控制面板的事件
|
||||||
self.dashboard_shutdown_event = asyncio.Event()
|
self.dashboard_shutdown_event = asyncio.Event()
|
||||||
|
|
||||||
|
asyncio.create_task(update_llm_metadata())
|
||||||
|
|
||||||
def _load(self) -> None:
|
def _load(self) -> None:
|
||||||
"""加载事件总线和任务并初始化."""
|
"""加载事件总线和任务并初始化."""
|
||||||
# 创建一个异步任务来执行事件总线的 dispatch() 方法
|
# 创建一个异步任务来执行事件总线的 dispatch() 方法
|
||||||
|
|||||||
@@ -321,7 +321,12 @@ class InternalAgentSubStage(Stage):
|
|||||||
elif isinstance(req.tool_calls_result, list):
|
elif isinstance(req.tool_calls_result, list):
|
||||||
for tcr in req.tool_calls_result:
|
for tcr in req.tool_calls_result:
|
||||||
messages.extend(tcr.to_openai_messages())
|
messages.extend(tcr.to_openai_messages())
|
||||||
messages.append({"role": "assistant", "content": llm_response.completion_text})
|
messages.append(
|
||||||
|
{
|
||||||
|
"role": "assistant",
|
||||||
|
"content": llm_response.completion_text or "*No response*",
|
||||||
|
}
|
||||||
|
)
|
||||||
messages = list(filter(lambda item: "_no_save" not in item, messages))
|
messages = list(filter(lambda item: "_no_save" not in item, messages))
|
||||||
await self.conv_manager.update_conversation(
|
await self.conv_manager.update_conversation(
|
||||||
event.unified_msg_origin,
|
event.unified_msg_origin,
|
||||||
|
|||||||
@@ -385,10 +385,25 @@ class AiocqhttpAdapter(Platform):
|
|||||||
logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。")
|
logger.error(f"获取 @ 用户信息失败: {e},此消息段将被忽略。")
|
||||||
|
|
||||||
message_str += "".join(at_parts)
|
message_str += "".join(at_parts)
|
||||||
|
elif t == "markdown":
|
||||||
|
text = m["data"].get("markdown") or m["data"].get("content", "")
|
||||||
|
abm.message.append(Plain(text=text))
|
||||||
|
message_str += text
|
||||||
else:
|
else:
|
||||||
for m in m_group:
|
for m in m_group:
|
||||||
a = ComponentTypes[t](**m["data"])
|
try:
|
||||||
abm.message.append(a)
|
if t not in ComponentTypes:
|
||||||
|
logger.warning(
|
||||||
|
f"不支持的消息段类型,已忽略: {t}, data={m['data']}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
a = ComponentTypes[t](**m["data"])
|
||||||
|
abm.message.append(a)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
f"消息段解析失败: type={t}, data={m['data']}. {e}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
abm.timestamp = int(time.time())
|
abm.timestamp = int(time.time())
|
||||||
abm.message_str = message_str
|
abm.message_str = message_str
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import astrbot.core.message.components as Comp
|
|||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.core.agent.message import (
|
from astrbot.core.agent.message import (
|
||||||
AssistantMessageSegment,
|
AssistantMessageSegment,
|
||||||
|
ContentPart,
|
||||||
ToolCall,
|
ToolCall,
|
||||||
ToolCallMessageSegment,
|
ToolCallMessageSegment,
|
||||||
)
|
)
|
||||||
@@ -92,6 +93,8 @@ class ProviderRequest:
|
|||||||
"""会话 ID"""
|
"""会话 ID"""
|
||||||
image_urls: list[str] = field(default_factory=list)
|
image_urls: list[str] = field(default_factory=list)
|
||||||
"""图片 URL 列表"""
|
"""图片 URL 列表"""
|
||||||
|
extra_user_content_parts: list[ContentPart] = field(default_factory=list)
|
||||||
|
"""额外的用户消息内容部分列表,用于在用户消息后添加额外的内容块(如系统提醒、指令等)。"""
|
||||||
func_tool: ToolSet | None = None
|
func_tool: ToolSet | None = None
|
||||||
"""可用的函数工具"""
|
"""可用的函数工具"""
|
||||||
contexts: list[dict] = field(default_factory=list)
|
contexts: list[dict] = field(default_factory=list)
|
||||||
@@ -166,13 +169,23 @@ class ProviderRequest:
|
|||||||
|
|
||||||
async def assemble_context(self) -> dict:
|
async def assemble_context(self) -> dict:
|
||||||
"""将请求(prompt 和 image_urls)包装成 OpenAI 的消息格式。"""
|
"""将请求(prompt 和 image_urls)包装成 OpenAI 的消息格式。"""
|
||||||
|
# 构建内容块列表
|
||||||
|
content_blocks = []
|
||||||
|
|
||||||
|
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
||||||
|
if self.prompt and self.prompt.strip():
|
||||||
|
content_blocks.append({"type": "text", "text": self.prompt})
|
||||||
|
elif self.image_urls:
|
||||||
|
# 如果没有文本但有图片,添加占位文本
|
||||||
|
content_blocks.append({"type": "text", "text": "[图片]"})
|
||||||
|
|
||||||
|
# 2. 额外的内容块(系统提醒、指令等)
|
||||||
|
if self.extra_user_content_parts:
|
||||||
|
for part in self.extra_user_content_parts:
|
||||||
|
content_blocks.append(part.model_dump())
|
||||||
|
|
||||||
|
# 3. 图片内容
|
||||||
if self.image_urls:
|
if self.image_urls:
|
||||||
user_content = {
|
|
||||||
"role": "user",
|
|
||||||
"content": [
|
|
||||||
{"type": "text", "text": self.prompt if self.prompt else "[图片]"},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
for image_url in self.image_urls:
|
for image_url in self.image_urls:
|
||||||
if image_url.startswith("http"):
|
if image_url.startswith("http"):
|
||||||
image_path = await download_image_by_url(image_url)
|
image_path = await download_image_by_url(image_url)
|
||||||
@@ -185,11 +198,21 @@ class ProviderRequest:
|
|||||||
if not image_data:
|
if not image_data:
|
||||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||||
continue
|
continue
|
||||||
user_content["content"].append(
|
content_blocks.append(
|
||||||
{"type": "image_url", "image_url": {"url": image_data}},
|
{"type": "image_url", "image_url": {"url": image_data}},
|
||||||
)
|
)
|
||||||
return user_content
|
|
||||||
return {"role": "user", "content": self.prompt}
|
# 只有当只有一个来自 prompt 的文本块且没有额外内容块时,才降级为简单格式以保持向后兼容
|
||||||
|
if (
|
||||||
|
len(content_blocks) == 1
|
||||||
|
and content_blocks[0]["type"] == "text"
|
||||||
|
and not self.extra_user_content_parts
|
||||||
|
and not self.image_urls
|
||||||
|
):
|
||||||
|
return {"role": "user", "content": content_blocks[0]["text"]}
|
||||||
|
|
||||||
|
# 否则返回多模态格式
|
||||||
|
return {"role": "user", "content": content_blocks}
|
||||||
|
|
||||||
async def _encode_image_bs64(self, image_url: str) -> str:
|
async def _encode_image_bs64(self, image_url: str) -> str:
|
||||||
"""将图片转换为 base64"""
|
"""将图片转换为 base64"""
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import copy
|
||||||
import traceback
|
import traceback
|
||||||
from typing import Protocol, runtime_checkable
|
from typing import Protocol, runtime_checkable
|
||||||
|
|
||||||
@@ -32,10 +33,12 @@ class ProviderManager:
|
|||||||
persona_mgr: PersonaManager,
|
persona_mgr: PersonaManager,
|
||||||
):
|
):
|
||||||
self.reload_lock = asyncio.Lock()
|
self.reload_lock = asyncio.Lock()
|
||||||
|
self.resource_lock = asyncio.Lock()
|
||||||
self.persona_mgr = persona_mgr
|
self.persona_mgr = persona_mgr
|
||||||
self.acm = acm
|
self.acm = acm
|
||||||
config = acm.confs["default"]
|
config = acm.confs["default"]
|
||||||
self.providers_config: list = config["provider"]
|
self.providers_config: list = config["provider"]
|
||||||
|
self.provider_sources_config: list = config.get("provider_sources", [])
|
||||||
self.provider_settings: dict = config["provider_settings"]
|
self.provider_settings: dict = config["provider_settings"]
|
||||||
self.provider_stt_settings: dict = config.get("provider_stt_settings", {})
|
self.provider_stt_settings: dict = config.get("provider_stt_settings", {})
|
||||||
self.provider_tts_settings: dict = config.get("provider_tts_settings", {})
|
self.provider_tts_settings: dict = config.get("provider_tts_settings", {})
|
||||||
@@ -148,6 +151,7 @@ class ProviderManager:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
provider = None
|
provider = None
|
||||||
|
provider_id = None
|
||||||
if umo:
|
if umo:
|
||||||
provider_id = sp.get(
|
provider_id = sp.get(
|
||||||
f"provider_perf_{provider_type.value}",
|
f"provider_perf_{provider_type.value}",
|
||||||
@@ -185,6 +189,12 @@ class ProviderManager:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown provider type: {provider_type}")
|
raise ValueError(f"Unknown provider type: {provider_type}")
|
||||||
|
|
||||||
|
if not provider and provider_id:
|
||||||
|
logger.warning(
|
||||||
|
f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。"
|
||||||
|
)
|
||||||
|
|
||||||
return provider
|
return provider
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
@@ -251,7 +261,136 @@ class ProviderManager:
|
|||||||
# 初始化 MCP Client 连接
|
# 初始化 MCP Client 连接
|
||||||
asyncio.create_task(self.llm_tools.init_mcp_clients(), name="init_mcp_clients")
|
asyncio.create_task(self.llm_tools.init_mcp_clients(), name="init_mcp_clients")
|
||||||
|
|
||||||
|
def dynamic_import_provider(self, type: str):
|
||||||
|
"""动态导入提供商适配器模块
|
||||||
|
|
||||||
|
Args:
|
||||||
|
type (str): 提供商请求类型。
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ImportError: 如果提供商类型未知或无法导入对应模块,则抛出异常。
|
||||||
|
"""
|
||||||
|
match type:
|
||||||
|
case "openai_chat_completion":
|
||||||
|
from .sources.openai_source import (
|
||||||
|
ProviderOpenAIOfficial as ProviderOpenAIOfficial,
|
||||||
|
)
|
||||||
|
case "zhipu_chat_completion":
|
||||||
|
from .sources.zhipu_source import ProviderZhipu as ProviderZhipu
|
||||||
|
case "groq_chat_completion":
|
||||||
|
from .sources.groq_source import ProviderGroq as ProviderGroq
|
||||||
|
case "anthropic_chat_completion":
|
||||||
|
from .sources.anthropic_source import (
|
||||||
|
ProviderAnthropic as ProviderAnthropic,
|
||||||
|
)
|
||||||
|
case "googlegenai_chat_completion":
|
||||||
|
from .sources.gemini_source import (
|
||||||
|
ProviderGoogleGenAI as ProviderGoogleGenAI,
|
||||||
|
)
|
||||||
|
case "sensevoice_stt_selfhost":
|
||||||
|
from .sources.sensevoice_selfhosted_source import (
|
||||||
|
ProviderSenseVoiceSTTSelfHost as ProviderSenseVoiceSTTSelfHost,
|
||||||
|
)
|
||||||
|
case "openai_whisper_api":
|
||||||
|
from .sources.whisper_api_source import (
|
||||||
|
ProviderOpenAIWhisperAPI as ProviderOpenAIWhisperAPI,
|
||||||
|
)
|
||||||
|
case "openai_whisper_selfhost":
|
||||||
|
from .sources.whisper_selfhosted_source import (
|
||||||
|
ProviderOpenAIWhisperSelfHost as ProviderOpenAIWhisperSelfHost,
|
||||||
|
)
|
||||||
|
case "xinference_stt":
|
||||||
|
from .sources.xinference_stt_provider import (
|
||||||
|
ProviderXinferenceSTT as ProviderXinferenceSTT,
|
||||||
|
)
|
||||||
|
case "openai_tts_api":
|
||||||
|
from .sources.openai_tts_api_source import (
|
||||||
|
ProviderOpenAITTSAPI as ProviderOpenAITTSAPI,
|
||||||
|
)
|
||||||
|
case "edge_tts":
|
||||||
|
from .sources.edge_tts_source import (
|
||||||
|
ProviderEdgeTTS as ProviderEdgeTTS,
|
||||||
|
)
|
||||||
|
case "gsv_tts_selfhost":
|
||||||
|
from .sources.gsv_selfhosted_source import (
|
||||||
|
ProviderGSVTTS as ProviderGSVTTS,
|
||||||
|
)
|
||||||
|
case "gsvi_tts_api":
|
||||||
|
from .sources.gsvi_tts_source import (
|
||||||
|
ProviderGSVITTS as ProviderGSVITTS,
|
||||||
|
)
|
||||||
|
case "fishaudio_tts_api":
|
||||||
|
from .sources.fishaudio_tts_api_source import (
|
||||||
|
ProviderFishAudioTTSAPI as ProviderFishAudioTTSAPI,
|
||||||
|
)
|
||||||
|
case "dashscope_tts":
|
||||||
|
from .sources.dashscope_tts import (
|
||||||
|
ProviderDashscopeTTSAPI as ProviderDashscopeTTSAPI,
|
||||||
|
)
|
||||||
|
case "azure_tts":
|
||||||
|
from .sources.azure_tts_source import (
|
||||||
|
AzureTTSProvider as AzureTTSProvider,
|
||||||
|
)
|
||||||
|
case "minimax_tts_api":
|
||||||
|
from .sources.minimax_tts_api_source import (
|
||||||
|
ProviderMiniMaxTTSAPI as ProviderMiniMaxTTSAPI,
|
||||||
|
)
|
||||||
|
case "volcengine_tts":
|
||||||
|
from .sources.volcengine_tts import (
|
||||||
|
ProviderVolcengineTTS as ProviderVolcengineTTS,
|
||||||
|
)
|
||||||
|
case "gemini_tts":
|
||||||
|
from .sources.gemini_tts_source import (
|
||||||
|
ProviderGeminiTTSAPI as ProviderGeminiTTSAPI,
|
||||||
|
)
|
||||||
|
case "openai_embedding":
|
||||||
|
from .sources.openai_embedding_source import (
|
||||||
|
OpenAIEmbeddingProvider as OpenAIEmbeddingProvider,
|
||||||
|
)
|
||||||
|
case "gemini_embedding":
|
||||||
|
from .sources.gemini_embedding_source import (
|
||||||
|
GeminiEmbeddingProvider as GeminiEmbeddingProvider,
|
||||||
|
)
|
||||||
|
case "vllm_rerank":
|
||||||
|
from .sources.vllm_rerank_source import (
|
||||||
|
VLLMRerankProvider as VLLMRerankProvider,
|
||||||
|
)
|
||||||
|
case "xinference_rerank":
|
||||||
|
from .sources.xinference_rerank_source import (
|
||||||
|
XinferenceRerankProvider as XinferenceRerankProvider,
|
||||||
|
)
|
||||||
|
case "bailian_rerank":
|
||||||
|
from .sources.bailian_rerank_source import (
|
||||||
|
BailianRerankProvider as BailianRerankProvider,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_merged_provider_config(self, provider_config: dict) -> dict:
|
||||||
|
"""获取 provider 配置和 provider_source 配置合并后的结果
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 合并后的 provider 配置,key 为 provider id,value 为合并后的配置字典
|
||||||
|
"""
|
||||||
|
pc = copy.deepcopy(provider_config)
|
||||||
|
provider_source_id = pc.get("provider_source_id", "")
|
||||||
|
if provider_source_id:
|
||||||
|
provider_source = None
|
||||||
|
for ps in self.provider_sources_config:
|
||||||
|
if ps.get("id") == provider_source_id:
|
||||||
|
provider_source = ps
|
||||||
|
break
|
||||||
|
|
||||||
|
if provider_source:
|
||||||
|
# 合并配置,provider 的配置优先级更高
|
||||||
|
merged_config = {**provider_source, **pc}
|
||||||
|
# 保持 id 为 provider 的 id,而不是 source 的 id
|
||||||
|
merged_config["id"] = pc["id"]
|
||||||
|
pc = merged_config
|
||||||
|
return pc
|
||||||
|
|
||||||
async def load_provider(self, provider_config: dict):
|
async def load_provider(self, provider_config: dict):
|
||||||
|
# 如果 provider_source_id 存在且不为空,则从 provider_sources 中找到对应的配置并合并
|
||||||
|
provider_config = self.get_merged_provider_config(provider_config)
|
||||||
|
|
||||||
if not provider_config["enable"]:
|
if not provider_config["enable"]:
|
||||||
logger.info(f"Provider {provider_config['id']} is disabled, skipping")
|
logger.info(f"Provider {provider_config['id']} is disabled, skipping")
|
||||||
return
|
return
|
||||||
@@ -264,99 +403,7 @@ class ProviderManager:
|
|||||||
|
|
||||||
# 动态导入
|
# 动态导入
|
||||||
try:
|
try:
|
||||||
match provider_config["type"]:
|
self.dynamic_import_provider(provider_config["type"])
|
||||||
case "openai_chat_completion":
|
|
||||||
from .sources.openai_source import (
|
|
||||||
ProviderOpenAIOfficial as ProviderOpenAIOfficial,
|
|
||||||
)
|
|
||||||
case "zhipu_chat_completion":
|
|
||||||
from .sources.zhipu_source import ProviderZhipu as ProviderZhipu
|
|
||||||
case "groq_chat_completion":
|
|
||||||
from .sources.groq_source import ProviderGroq as ProviderGroq
|
|
||||||
case "anthropic_chat_completion":
|
|
||||||
from .sources.anthropic_source import (
|
|
||||||
ProviderAnthropic as ProviderAnthropic,
|
|
||||||
)
|
|
||||||
case "googlegenai_chat_completion":
|
|
||||||
from .sources.gemini_source import (
|
|
||||||
ProviderGoogleGenAI as ProviderGoogleGenAI,
|
|
||||||
)
|
|
||||||
case "sensevoice_stt_selfhost":
|
|
||||||
from .sources.sensevoice_selfhosted_source import (
|
|
||||||
ProviderSenseVoiceSTTSelfHost as ProviderSenseVoiceSTTSelfHost,
|
|
||||||
)
|
|
||||||
case "openai_whisper_api":
|
|
||||||
from .sources.whisper_api_source import (
|
|
||||||
ProviderOpenAIWhisperAPI as ProviderOpenAIWhisperAPI,
|
|
||||||
)
|
|
||||||
case "openai_whisper_selfhost":
|
|
||||||
from .sources.whisper_selfhosted_source import (
|
|
||||||
ProviderOpenAIWhisperSelfHost as ProviderOpenAIWhisperSelfHost,
|
|
||||||
)
|
|
||||||
case "xinference_stt":
|
|
||||||
from .sources.xinference_stt_provider import (
|
|
||||||
ProviderXinferenceSTT as ProviderXinferenceSTT,
|
|
||||||
)
|
|
||||||
case "openai_tts_api":
|
|
||||||
from .sources.openai_tts_api_source import (
|
|
||||||
ProviderOpenAITTSAPI as ProviderOpenAITTSAPI,
|
|
||||||
)
|
|
||||||
case "edge_tts":
|
|
||||||
from .sources.edge_tts_source import (
|
|
||||||
ProviderEdgeTTS as ProviderEdgeTTS,
|
|
||||||
)
|
|
||||||
case "gsv_tts_selfhost":
|
|
||||||
from .sources.gsv_selfhosted_source import (
|
|
||||||
ProviderGSVTTS as ProviderGSVTTS,
|
|
||||||
)
|
|
||||||
case "gsvi_tts_api":
|
|
||||||
from .sources.gsvi_tts_source import (
|
|
||||||
ProviderGSVITTS as ProviderGSVITTS,
|
|
||||||
)
|
|
||||||
case "fishaudio_tts_api":
|
|
||||||
from .sources.fishaudio_tts_api_source import (
|
|
||||||
ProviderFishAudioTTSAPI as ProviderFishAudioTTSAPI,
|
|
||||||
)
|
|
||||||
case "dashscope_tts":
|
|
||||||
from .sources.dashscope_tts import (
|
|
||||||
ProviderDashscopeTTSAPI as ProviderDashscopeTTSAPI,
|
|
||||||
)
|
|
||||||
case "azure_tts":
|
|
||||||
from .sources.azure_tts_source import (
|
|
||||||
AzureTTSProvider as AzureTTSProvider,
|
|
||||||
)
|
|
||||||
case "minimax_tts_api":
|
|
||||||
from .sources.minimax_tts_api_source import (
|
|
||||||
ProviderMiniMaxTTSAPI as ProviderMiniMaxTTSAPI,
|
|
||||||
)
|
|
||||||
case "volcengine_tts":
|
|
||||||
from .sources.volcengine_tts import (
|
|
||||||
ProviderVolcengineTTS as ProviderVolcengineTTS,
|
|
||||||
)
|
|
||||||
case "gemini_tts":
|
|
||||||
from .sources.gemini_tts_source import (
|
|
||||||
ProviderGeminiTTSAPI as ProviderGeminiTTSAPI,
|
|
||||||
)
|
|
||||||
case "openai_embedding":
|
|
||||||
from .sources.openai_embedding_source import (
|
|
||||||
OpenAIEmbeddingProvider as OpenAIEmbeddingProvider,
|
|
||||||
)
|
|
||||||
case "gemini_embedding":
|
|
||||||
from .sources.gemini_embedding_source import (
|
|
||||||
GeminiEmbeddingProvider as GeminiEmbeddingProvider,
|
|
||||||
)
|
|
||||||
case "vllm_rerank":
|
|
||||||
from .sources.vllm_rerank_source import (
|
|
||||||
VLLMRerankProvider as VLLMRerankProvider,
|
|
||||||
)
|
|
||||||
case "xinference_rerank":
|
|
||||||
from .sources.xinference_rerank_source import (
|
|
||||||
XinferenceRerankProvider as XinferenceRerankProvider,
|
|
||||||
)
|
|
||||||
case "bailian_rerank":
|
|
||||||
from .sources.bailian_rerank_source import (
|
|
||||||
BailianRerankProvider as BailianRerankProvider,
|
|
||||||
)
|
|
||||||
except (ImportError, ModuleNotFoundError) as e:
|
except (ImportError, ModuleNotFoundError) as e:
|
||||||
logger.critical(
|
logger.critical(
|
||||||
f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。",
|
f"加载 {provider_config['type']}({provider_config['id']}) 提供商适配器失败:{e}。可能是因为有未安装的依赖。",
|
||||||
@@ -499,6 +546,7 @@ class ProviderManager:
|
|||||||
|
|
||||||
# 和配置文件保持同步
|
# 和配置文件保持同步
|
||||||
self.providers_config = astrbot_config["provider"]
|
self.providers_config = astrbot_config["provider"]
|
||||||
|
self.provider_sources_config = astrbot_config.get("provider_sources", [])
|
||||||
config_ids = [provider["id"] for provider in self.providers_config]
|
config_ids = [provider["id"] for provider in self.providers_config]
|
||||||
logger.info(f"providers in user's config: {config_ids}")
|
logger.info(f"providers in user's config: {config_ids}")
|
||||||
for key in list(self.inst_map.keys()):
|
for key in list(self.inst_map.keys()):
|
||||||
@@ -570,6 +618,68 @@ class ProviderManager:
|
|||||||
)
|
)
|
||||||
del self.inst_map[provider_id]
|
del self.inst_map[provider_id]
|
||||||
|
|
||||||
|
async def delete_provider(
|
||||||
|
self, provider_id: str | None = None, provider_source_id: str | None = None
|
||||||
|
):
|
||||||
|
"""Delete provider and/or provider source from config and terminate the instances. Config will be saved after deletion."""
|
||||||
|
async with self.resource_lock:
|
||||||
|
# delete from config
|
||||||
|
target_prov_ids = []
|
||||||
|
if provider_id:
|
||||||
|
target_prov_ids.append(provider_id)
|
||||||
|
else:
|
||||||
|
for prov in self.providers_config:
|
||||||
|
if prov.get("provider_source_id") == provider_source_id:
|
||||||
|
target_prov_ids.append(prov.get("id"))
|
||||||
|
config = self.acm.default_conf
|
||||||
|
for tpid in target_prov_ids:
|
||||||
|
await self.terminate_provider(tpid)
|
||||||
|
config["provider"] = [
|
||||||
|
prov for prov in config["provider"] if prov.get("id") != tpid
|
||||||
|
]
|
||||||
|
config.save_config()
|
||||||
|
logger.info(f"Provider {target_prov_ids} 已从配置中删除。")
|
||||||
|
|
||||||
|
async def update_provider(self, origin_provider_id: str, new_config: dict):
|
||||||
|
"""Update provider config and reload the instance. Config will be saved after update."""
|
||||||
|
async with self.resource_lock:
|
||||||
|
npid = new_config.get("id", None)
|
||||||
|
if not npid:
|
||||||
|
raise ValueError("New provider config must have an 'id' field")
|
||||||
|
config = self.acm.default_conf
|
||||||
|
for provider in config["provider"]:
|
||||||
|
if (
|
||||||
|
provider.get("id", None) == npid
|
||||||
|
and provider.get("id", None) != origin_provider_id
|
||||||
|
):
|
||||||
|
raise ValueError(f"Provider ID {npid} already exists")
|
||||||
|
# update config
|
||||||
|
for idx, provider in enumerate(config["provider"]):
|
||||||
|
if provider.get("id", None) == origin_provider_id:
|
||||||
|
config["provider"][idx] = new_config
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Provider ID {origin_provider_id} not found")
|
||||||
|
config.save_config()
|
||||||
|
# reload instance
|
||||||
|
await self.reload(new_config)
|
||||||
|
|
||||||
|
async def create_provider(self, new_config: dict):
|
||||||
|
"""Add new provider config and load the instance. Config will be saved after addition."""
|
||||||
|
async with self.resource_lock:
|
||||||
|
npid = new_config.get("id", None)
|
||||||
|
if not npid:
|
||||||
|
raise ValueError("New provider config must have an 'id' field")
|
||||||
|
config = self.acm.default_conf
|
||||||
|
for provider in config["provider"]:
|
||||||
|
if provider.get("id", None) == npid:
|
||||||
|
raise ValueError(f"Provider ID {npid} already exists")
|
||||||
|
# add to config
|
||||||
|
config["provider"].append(new_config)
|
||||||
|
config.save_config()
|
||||||
|
# load instance
|
||||||
|
await self.load_provider(new_config)
|
||||||
|
|
||||||
async def terminate(self):
|
async def terminate(self):
|
||||||
for provider_inst in self.provider_insts:
|
for provider_inst in self.provider_insts:
|
||||||
if hasattr(provider_inst, "terminate"):
|
if hasattr(provider_inst, "terminate"):
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import os
|
|||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
from typing import TypeAlias, Union
|
from typing import TypeAlias, Union
|
||||||
|
|
||||||
from astrbot.core.agent.message import Message
|
from astrbot.core.agent.message import ContentPart, Message
|
||||||
from astrbot.core.agent.tool import ToolSet
|
from astrbot.core.agent.tool import ToolSet
|
||||||
from astrbot.core.provider.entities import (
|
from astrbot.core.provider.entities import (
|
||||||
LLMResponse,
|
LLMResponse,
|
||||||
@@ -103,6 +103,7 @@ class Provider(AbstractProvider):
|
|||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> LLMResponse:
|
) -> LLMResponse:
|
||||||
"""获得 LLM 的文本对话结果。会使用当前的模型进行对话。
|
"""获得 LLM 的文本对话结果。会使用当前的模型进行对话。
|
||||||
@@ -114,6 +115,7 @@ class Provider(AbstractProvider):
|
|||||||
tools: tool set
|
tools: tool set
|
||||||
contexts: 上下文,和 prompt 二选一使用
|
contexts: 上下文,和 prompt 二选一使用
|
||||||
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
|
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
|
||||||
|
extra_user_content_parts: 额外的用户内容块列表,用于在用户消息后添加额外的文本块(如系统提醒、指令等)
|
||||||
kwargs: 其他参数
|
kwargs: 其他参数
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
@@ -133,6 +135,7 @@ class Provider(AbstractProvider):
|
|||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> AsyncGenerator[LLMResponse, None]:
|
) -> AsyncGenerator[LLMResponse, None]:
|
||||||
"""获得 LLM 的流式文本对话结果。会使用当前的模型进行对话。在生成的最后会返回一次完整的结果。
|
"""获得 LLM 的流式文本对话结果。会使用当前的模型进行对话。在生成的最后会返回一次完整的结果。
|
||||||
@@ -144,6 +147,7 @@ class Provider(AbstractProvider):
|
|||||||
tools: tool set
|
tools: tool set
|
||||||
contexts: 上下文,和 prompt 二选一使用
|
contexts: 上下文,和 prompt 二选一使用
|
||||||
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
|
tool_calls_result: 回传给 LLM 的工具调用结果。参考: https://platform.openai.com/docs/guides/function-calling
|
||||||
|
extra_user_content_parts: 额外的用户内容块列表,用于在用户消息后添加额外的文本块(如系统提醒、指令等)
|
||||||
kwargs: 其他参数
|
kwargs: 其他参数
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from anthropic.types.usage import Usage
|
|||||||
|
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.api.provider import Provider
|
from astrbot.api.provider import Provider
|
||||||
|
from astrbot.core.agent.message import ContentPart
|
||||||
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
||||||
from astrbot.core.provider.func_tool_manager import ToolSet
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
||||||
from astrbot.core.utils.io import download_image_by_url
|
from astrbot.core.utils.io import download_image_by_url
|
||||||
@@ -47,7 +48,7 @@ class ProviderAnthropic(Provider):
|
|||||||
base_url=self.base_url,
|
base_url=self.base_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.set_model(provider_config["model_config"]["model"])
|
self.set_model(provider_config.get("model", "unknown"))
|
||||||
|
|
||||||
def _prepare_payload(self, messages: list[dict]):
|
def _prepare_payload(self, messages: list[dict]):
|
||||||
"""准备 Anthropic API 的请求 payload
|
"""准备 Anthropic API 的请求 payload
|
||||||
@@ -130,7 +131,11 @@ class ProviderAnthropic(Provider):
|
|||||||
if tool_list := tools.get_func_desc_anthropic_style():
|
if tool_list := tools.get_func_desc_anthropic_style():
|
||||||
payloads["tools"] = tool_list
|
payloads["tools"] = tool_list
|
||||||
|
|
||||||
completion = await self.client.messages.create(**payloads, stream=False)
|
extra_body = self.provider_config.get("custom_extra_body", {})
|
||||||
|
|
||||||
|
completion = await self.client.messages.create(
|
||||||
|
**payloads, stream=False, extra_body=extra_body
|
||||||
|
)
|
||||||
|
|
||||||
assert isinstance(completion, Message)
|
assert isinstance(completion, Message)
|
||||||
logger.debug(f"completion: {completion}")
|
logger.debug(f"completion: {completion}")
|
||||||
@@ -173,11 +178,13 @@ class ProviderAnthropic(Provider):
|
|||||||
# 用于累积最终结果
|
# 用于累积最终结果
|
||||||
final_text = ""
|
final_text = ""
|
||||||
final_tool_calls = []
|
final_tool_calls = []
|
||||||
|
|
||||||
id = None
|
id = None
|
||||||
usage = TokenUsage()
|
usage = TokenUsage()
|
||||||
|
extra_body = self.provider_config.get("custom_extra_body", {})
|
||||||
|
|
||||||
async with self.client.messages.stream(**payloads) as stream:
|
async with self.client.messages.stream(
|
||||||
|
**payloads, extra_body=extra_body
|
||||||
|
) as stream:
|
||||||
assert isinstance(stream, anthropic.AsyncMessageStream)
|
assert isinstance(stream, anthropic.AsyncMessageStream)
|
||||||
async for event in stream:
|
async for event in stream:
|
||||||
if event.type == "message_start":
|
if event.type == "message_start":
|
||||||
@@ -290,13 +297,16 @@ class ProviderAnthropic(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> LLMResponse:
|
) -> LLMResponse:
|
||||||
if contexts is None:
|
if contexts is None:
|
||||||
contexts = []
|
contexts = []
|
||||||
new_record = None
|
new_record = None
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
new_record = await self.assemble_context(prompt, image_urls)
|
new_record = await self.assemble_context(
|
||||||
|
prompt, image_urls, extra_user_content_parts
|
||||||
|
)
|
||||||
context_query = self._ensure_message_to_dicts(contexts)
|
context_query = self._ensure_message_to_dicts(contexts)
|
||||||
if new_record:
|
if new_record:
|
||||||
context_query.append(new_record)
|
context_query.append(new_record)
|
||||||
@@ -318,10 +328,9 @@ class ProviderAnthropic(Provider):
|
|||||||
|
|
||||||
system_prompt, new_messages = self._prepare_payload(context_query)
|
system_prompt, new_messages = self._prepare_payload(context_query)
|
||||||
|
|
||||||
model_config = self.provider_config.get("model_config", {})
|
model = model or self.get_model()
|
||||||
model_config["model"] = model or self.get_model()
|
|
||||||
|
|
||||||
payloads = {"messages": new_messages, **model_config}
|
payloads = {"messages": new_messages, "model": model}
|
||||||
|
|
||||||
# Anthropic has a different way of handling system prompts
|
# Anthropic has a different way of handling system prompts
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
@@ -331,7 +340,6 @@ class ProviderAnthropic(Provider):
|
|||||||
try:
|
try:
|
||||||
llm_response = await self._query(payloads, func_tool)
|
llm_response = await self._query(payloads, func_tool)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# logger.error(f"发生了错误。Provider 配置如下: {model_config}")
|
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
return llm_response
|
return llm_response
|
||||||
@@ -346,13 +354,16 @@ class ProviderAnthropic(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
if contexts is None:
|
if contexts is None:
|
||||||
contexts = []
|
contexts = []
|
||||||
new_record = None
|
new_record = None
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
new_record = await self.assemble_context(prompt, image_urls)
|
new_record = await self.assemble_context(
|
||||||
|
prompt, image_urls, extra_user_content_parts
|
||||||
|
)
|
||||||
context_query = self._ensure_message_to_dicts(contexts)
|
context_query = self._ensure_message_to_dicts(contexts)
|
||||||
if new_record:
|
if new_record:
|
||||||
context_query.append(new_record)
|
context_query.append(new_record)
|
||||||
@@ -373,10 +384,9 @@ class ProviderAnthropic(Provider):
|
|||||||
|
|
||||||
system_prompt, new_messages = self._prepare_payload(context_query)
|
system_prompt, new_messages = self._prepare_payload(context_query)
|
||||||
|
|
||||||
model_config = self.provider_config.get("model_config", {})
|
model = model or self.get_model()
|
||||||
model_config["model"] = model or self.get_model()
|
|
||||||
|
|
||||||
payloads = {"messages": new_messages, **model_config}
|
payloads = {"messages": new_messages, "model": model}
|
||||||
|
|
||||||
# Anthropic has a different way of handling system prompts
|
# Anthropic has a different way of handling system prompts
|
||||||
if system_prompt:
|
if system_prompt:
|
||||||
@@ -385,48 +395,116 @@ class ProviderAnthropic(Provider):
|
|||||||
async for llm_response in self._query_stream(payloads, func_tool):
|
async for llm_response in self._query_stream(payloads, func_tool):
|
||||||
yield llm_response
|
yield llm_response
|
||||||
|
|
||||||
async def assemble_context(self, text: str, image_urls: list[str] | None = None):
|
async def assemble_context(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
image_urls: list[str] | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
|
):
|
||||||
"""组装上下文,支持文本和图片"""
|
"""组装上下文,支持文本和图片"""
|
||||||
if not image_urls:
|
|
||||||
return {"role": "user", "content": text}
|
|
||||||
|
|
||||||
content = []
|
content = []
|
||||||
content.append({"type": "text", "text": text})
|
|
||||||
|
|
||||||
for image_url in image_urls:
|
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
||||||
if image_url.startswith("http"):
|
if text:
|
||||||
image_path = await download_image_by_url(image_url)
|
content.append({"type": "text", "text": text})
|
||||||
image_data = await self.encode_image_bs64(image_path)
|
elif image_urls:
|
||||||
elif image_url.startswith("file:///"):
|
# 如果没有文本但有图片,添加占位文本
|
||||||
image_path = image_url.replace("file:///", "")
|
content.append({"type": "text", "text": "[图片]"})
|
||||||
image_data = await self.encode_image_bs64(image_path)
|
elif extra_user_content_parts:
|
||||||
else:
|
# 如果只有额外内容块,也需要添加占位文本
|
||||||
image_data = await self.encode_image_bs64(image_url)
|
content.append({"type": "text", "text": " "})
|
||||||
|
|
||||||
if not image_data:
|
# 2. 额外的内容块(系统提醒、指令等)
|
||||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
if extra_user_content_parts:
|
||||||
continue
|
for block in extra_user_content_parts:
|
||||||
|
block_type = block.get("type")
|
||||||
|
|
||||||
# Get mime type for the image
|
if block_type == "text":
|
||||||
mime_type, _ = guess_type(image_url)
|
# 文本直接添加
|
||||||
if not mime_type:
|
content.append(block)
|
||||||
mime_type = "image/jpeg" # Default to JPEG if can't determine
|
|
||||||
|
|
||||||
content.append(
|
elif block_type == "image_url":
|
||||||
{
|
# 转换 OpenAI 格式的图片为 Anthropic 格式
|
||||||
"type": "image",
|
image_url_data = block.get("image_url", {})
|
||||||
"source": {
|
if isinstance(image_url_data, dict):
|
||||||
"type": "base64",
|
url = image_url_data.get("url", "")
|
||||||
"media_type": mime_type,
|
else:
|
||||||
"data": (
|
# 兼容直接传 URL 字符串的情况
|
||||||
image_data.split("base64,")[1]
|
url = str(image_url_data)
|
||||||
if "base64," in image_data
|
|
||||||
else image_data
|
if url and url.startswith("data:"):
|
||||||
),
|
try:
|
||||||
|
# 提取 MIME 类型和 base64 数据
|
||||||
|
mime_type = url.split(":")[1].split(";")[0]
|
||||||
|
base64_data = (
|
||||||
|
url.split("base64,")[1] if "base64," in url else url
|
||||||
|
)
|
||||||
|
content.append(
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"source": {
|
||||||
|
"type": "base64",
|
||||||
|
"media_type": mime_type,
|
||||||
|
"data": base64_data,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"转换 image_url 到 Anthropic 格式失败: {e}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"image_url 不是有效的 data URI: {url[:50]}...")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 其他类型(如 audio_url)Anthropic 不支持,记录警告
|
||||||
|
logger.debug(f"Anthropic 不支持的内容类型 '{block_type}',已忽略")
|
||||||
|
|
||||||
|
# 3. 图片内容
|
||||||
|
if image_urls:
|
||||||
|
for image_url in image_urls:
|
||||||
|
if image_url.startswith("http"):
|
||||||
|
image_path = await download_image_by_url(image_url)
|
||||||
|
image_data = await self.encode_image_bs64(image_path)
|
||||||
|
elif image_url.startswith("file:///"):
|
||||||
|
image_path = image_url.replace("file:///", "")
|
||||||
|
image_data = await self.encode_image_bs64(image_path)
|
||||||
|
else:
|
||||||
|
image_data = await self.encode_image_bs64(image_url)
|
||||||
|
|
||||||
|
if not image_data:
|
||||||
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get mime type for the image
|
||||||
|
mime_type, _ = guess_type(image_url)
|
||||||
|
if not mime_type:
|
||||||
|
mime_type = "image/jpeg" # Default to JPEG if can't determine
|
||||||
|
|
||||||
|
content.append(
|
||||||
|
{
|
||||||
|
"type": "image",
|
||||||
|
"source": {
|
||||||
|
"type": "base64",
|
||||||
|
"media_type": mime_type,
|
||||||
|
"data": (
|
||||||
|
image_data.split("base64,")[1]
|
||||||
|
if "base64," in image_data
|
||||||
|
else image_data
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
||||||
|
if (
|
||||||
|
text
|
||||||
|
and not extra_user_content_parts
|
||||||
|
and not image_urls
|
||||||
|
and len(content) == 1
|
||||||
|
and content[0]["type"] == "text"
|
||||||
|
):
|
||||||
|
return {"role": "user", "content": content[0]["text"]}
|
||||||
|
|
||||||
|
# 否则返回多模态格式
|
||||||
return {"role": "user", "content": content}
|
return {"role": "user", "content": content}
|
||||||
|
|
||||||
async def encode_image_bs64(self, image_url: str) -> str:
|
async def encode_image_bs64(self, image_url: str) -> str:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from google.genai.errors import APIError
|
|||||||
import astrbot.core.message.components as Comp
|
import astrbot.core.message.components as Comp
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.api.provider import Provider
|
from astrbot.api.provider import Provider
|
||||||
|
from astrbot.core.agent.message import ContentPart
|
||||||
from astrbot.core.message.message_event_result import MessageChain
|
from astrbot.core.message.message_event_result import MessageChain
|
||||||
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage
|
||||||
from astrbot.core.provider.func_tool_manager import ToolSet
|
from astrbot.core.provider.func_tool_manager import ToolSet
|
||||||
@@ -68,7 +69,7 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
self.api_base = self.api_base[:-1]
|
self.api_base = self.api_base[:-1]
|
||||||
|
|
||||||
self._init_client()
|
self._init_client()
|
||||||
self.set_model(provider_config["model_config"]["model"])
|
self.set_model(provider_config.get("model", "unknown"))
|
||||||
self._init_safety_settings()
|
self._init_safety_settings()
|
||||||
|
|
||||||
def _init_client(self) -> None:
|
def _init_client(self) -> None:
|
||||||
@@ -138,7 +139,7 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
modalities = ["TEXT"]
|
modalities = ["TEXT"]
|
||||||
|
|
||||||
tool_list: list[types.Tool] | None = []
|
tool_list: list[types.Tool] | None = []
|
||||||
model_name = payloads.get("model", self.get_model())
|
model_name = cast(str, payloads.get("model", self.get_model()))
|
||||||
native_coderunner = self.provider_config.get("gm_native_coderunner", False)
|
native_coderunner = self.provider_config.get("gm_native_coderunner", False)
|
||||||
native_search = self.provider_config.get("gm_native_search", False)
|
native_search = self.provider_config.get("gm_native_search", False)
|
||||||
url_context = self.provider_config.get("gm_url_context", False)
|
url_context = self.provider_config.get("gm_url_context", False)
|
||||||
@@ -199,7 +200,16 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
|
|
||||||
# oper thinking config
|
# oper thinking config
|
||||||
thinking_config = None
|
thinking_config = None
|
||||||
if model_name.startswith("gemini-2.5"):
|
if model_name in [
|
||||||
|
"gemini-2.5-pro",
|
||||||
|
"gemini-2.5-pro-preview",
|
||||||
|
"gemini-2.5-flash",
|
||||||
|
"gemini-2.5-flash-preview",
|
||||||
|
"gemini-2.5-flash-lite",
|
||||||
|
"gemini-2.5-flash-lite-preview",
|
||||||
|
"gemini-robotics-er-1.5-preview",
|
||||||
|
"gemini-live-2.5-flash-preview-native-audio-09-2025",
|
||||||
|
]:
|
||||||
# The thinkingBudget parameter, introduced with the Gemini 2.5 series
|
# The thinkingBudget parameter, introduced with the Gemini 2.5 series
|
||||||
thinking_budget = self.provider_config.get("gm_thinking_config", {}).get(
|
thinking_budget = self.provider_config.get("gm_thinking_config", {}).get(
|
||||||
"budget", 0
|
"budget", 0
|
||||||
@@ -208,7 +218,14 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
thinking_config = types.ThinkingConfig(
|
thinking_config = types.ThinkingConfig(
|
||||||
thinking_budget=thinking_budget,
|
thinking_budget=thinking_budget,
|
||||||
)
|
)
|
||||||
elif model_name.startswith("gemini-3"):
|
elif model_name in [
|
||||||
|
"gemini-3-pro",
|
||||||
|
"gemini-3-pro-preview",
|
||||||
|
"gemini-3-flash",
|
||||||
|
"gemini-3-flash-preview",
|
||||||
|
"gemini-3-flash-lite",
|
||||||
|
"gemini-3-flash-lite-preview",
|
||||||
|
]:
|
||||||
# The thinkingLevel parameter, recommended for Gemini 3 models and onwards
|
# The thinkingLevel parameter, recommended for Gemini 3 models and onwards
|
||||||
# Gemini 2.5 series models don't support thinkingLevel; use thinkingBudget instead.
|
# Gemini 2.5 series models don't support thinkingLevel; use thinkingBudget instead.
|
||||||
thinking_level = self.provider_config.get("gm_thinking_config", {}).get(
|
thinking_level = self.provider_config.get("gm_thinking_config", {}).get(
|
||||||
@@ -664,13 +681,16 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> LLMResponse:
|
) -> LLMResponse:
|
||||||
if contexts is None:
|
if contexts is None:
|
||||||
contexts = []
|
contexts = []
|
||||||
new_record = None
|
new_record = None
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
new_record = await self.assemble_context(prompt, image_urls)
|
new_record = await self.assemble_context(
|
||||||
|
prompt, image_urls, extra_user_content_parts
|
||||||
|
)
|
||||||
context_query = self._ensure_message_to_dicts(contexts)
|
context_query = self._ensure_message_to_dicts(contexts)
|
||||||
if new_record:
|
if new_record:
|
||||||
context_query.append(new_record)
|
context_query.append(new_record)
|
||||||
@@ -689,10 +709,9 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
for tcr in tool_calls_result:
|
for tcr in tool_calls_result:
|
||||||
context_query.extend(tcr.to_openai_messages())
|
context_query.extend(tcr.to_openai_messages())
|
||||||
|
|
||||||
model_config = self.provider_config.get("model_config", {})
|
model = model or self.get_model()
|
||||||
model_config["model"] = model or self.get_model()
|
|
||||||
|
|
||||||
payloads = {"messages": context_query, **model_config}
|
payloads = {"messages": context_query, "model": model}
|
||||||
|
|
||||||
retry = 10
|
retry = 10
|
||||||
keys = self.api_keys.copy()
|
keys = self.api_keys.copy()
|
||||||
@@ -717,13 +736,16 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> AsyncGenerator[LLMResponse, None]:
|
) -> AsyncGenerator[LLMResponse, None]:
|
||||||
if contexts is None:
|
if contexts is None:
|
||||||
contexts = []
|
contexts = []
|
||||||
new_record = None
|
new_record = None
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
new_record = await self.assemble_context(prompt, image_urls)
|
new_record = await self.assemble_context(
|
||||||
|
prompt, image_urls, extra_user_content_parts
|
||||||
|
)
|
||||||
context_query = self._ensure_message_to_dicts(contexts)
|
context_query = self._ensure_message_to_dicts(contexts)
|
||||||
if new_record:
|
if new_record:
|
||||||
context_query.append(new_record)
|
context_query.append(new_record)
|
||||||
@@ -742,10 +764,9 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
for tcr in tool_calls_result:
|
for tcr in tool_calls_result:
|
||||||
context_query.extend(tcr.to_openai_messages())
|
context_query.extend(tcr.to_openai_messages())
|
||||||
|
|
||||||
model_config = self.provider_config.get("model_config", {})
|
model = model or self.get_model()
|
||||||
model_config["model"] = model or self.get_model()
|
|
||||||
|
|
||||||
payloads = {"messages": context_query, **model_config}
|
payloads = {"messages": context_query, "model": model}
|
||||||
|
|
||||||
retry = 10
|
retry = 10
|
||||||
keys = self.api_keys.copy()
|
keys = self.api_keys.copy()
|
||||||
@@ -783,13 +804,33 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
self.chosen_api_key = key
|
self.chosen_api_key = key
|
||||||
self._init_client()
|
self._init_client()
|
||||||
|
|
||||||
async def assemble_context(self, text: str, image_urls: list[str] | None = None):
|
async def assemble_context(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
image_urls: list[str] | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
|
):
|
||||||
"""组装上下文。"""
|
"""组装上下文。"""
|
||||||
|
# 构建内容块列表
|
||||||
|
content_blocks = []
|
||||||
|
|
||||||
|
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
||||||
|
if text:
|
||||||
|
content_blocks.append({"type": "text", "text": text})
|
||||||
|
elif image_urls:
|
||||||
|
# 如果没有文本但有图片,添加占位文本
|
||||||
|
content_blocks.append({"type": "text", "text": "[图片]"})
|
||||||
|
elif extra_user_content_parts:
|
||||||
|
# 如果只有额外内容块,也需要添加占位文本
|
||||||
|
content_blocks.append({"type": "text", "text": " "})
|
||||||
|
|
||||||
|
# 2. 额外的内容块(系统提醒、指令等)
|
||||||
|
if extra_user_content_parts:
|
||||||
|
for part in extra_user_content_parts:
|
||||||
|
content_blocks.append(part.model_dump())
|
||||||
|
|
||||||
|
# 3. 图片内容
|
||||||
if image_urls:
|
if image_urls:
|
||||||
user_content = {
|
|
||||||
"role": "user",
|
|
||||||
"content": [{"type": "text", "text": text if text else "[图片]"}],
|
|
||||||
}
|
|
||||||
for image_url in image_urls:
|
for image_url in image_urls:
|
||||||
if image_url.startswith("http"):
|
if image_url.startswith("http"):
|
||||||
image_path = await download_image_by_url(image_url)
|
image_path = await download_image_by_url(image_url)
|
||||||
@@ -802,14 +843,25 @@ class ProviderGoogleGenAI(Provider):
|
|||||||
if not image_data:
|
if not image_data:
|
||||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||||
continue
|
continue
|
||||||
user_content["content"].append(
|
content_blocks.append(
|
||||||
{
|
{
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
"image_url": {"url": image_data},
|
"image_url": {"url": image_data},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return user_content
|
|
||||||
return {"role": "user", "content": text}
|
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
||||||
|
if (
|
||||||
|
text
|
||||||
|
and not extra_user_content_parts
|
||||||
|
and not image_urls
|
||||||
|
and len(content_blocks) == 1
|
||||||
|
and content_blocks[0]["type"] == "text"
|
||||||
|
):
|
||||||
|
return {"role": "user", "content": content_blocks[0]["text"]}
|
||||||
|
|
||||||
|
# 否则返回多模态格式
|
||||||
|
return {"role": "user", "content": content_blocks}
|
||||||
|
|
||||||
async def encode_image_bs64(self, image_url: str) -> str:
|
async def encode_image_bs64(self, image_url: str) -> str:
|
||||||
"""将图片转换为 base64"""
|
"""将图片转换为 base64"""
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from openai.types.completion_usage import CompletionUsage
|
|||||||
import astrbot.core.message.components as Comp
|
import astrbot.core.message.components as Comp
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.api.provider import Provider
|
from astrbot.api.provider import Provider
|
||||||
from astrbot.core.agent.message import Message
|
from astrbot.core.agent.message import ContentPart, Message
|
||||||
from astrbot.core.agent.tool import ToolSet
|
from astrbot.core.agent.tool import ToolSet
|
||||||
from astrbot.core.message.message_event_result import MessageChain
|
from astrbot.core.message.message_event_result import MessageChain
|
||||||
from astrbot.core.provider.entities import LLMResponse, TokenUsage, ToolCallsResult
|
from astrbot.core.provider.entities import LLMResponse, TokenUsage, ToolCallsResult
|
||||||
@@ -69,8 +69,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
self.client.chat.completions.create,
|
self.client.chat.completions.create,
|
||||||
).parameters.keys()
|
).parameters.keys()
|
||||||
|
|
||||||
model_config = provider_config.get("model_config", {})
|
model = provider_config.get("model", "unknown")
|
||||||
model = model_config.get("model", "unknown")
|
|
||||||
self.set_model(model)
|
self.set_model(model)
|
||||||
|
|
||||||
self.reasoning_key = "reasoning_content"
|
self.reasoning_key = "reasoning_content"
|
||||||
@@ -349,6 +348,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
system_prompt: str | None = None,
|
system_prompt: str | None = None,
|
||||||
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
tool_calls_result: ToolCallsResult | list[ToolCallsResult] | None = None,
|
||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> tuple:
|
) -> tuple:
|
||||||
"""准备聊天所需的有效载荷和上下文"""
|
"""准备聊天所需的有效载荷和上下文"""
|
||||||
@@ -356,7 +356,9 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
contexts = []
|
contexts = []
|
||||||
new_record = None
|
new_record = None
|
||||||
if prompt is not None:
|
if prompt is not None:
|
||||||
new_record = await self.assemble_context(prompt, image_urls)
|
new_record = await self.assemble_context(
|
||||||
|
prompt, image_urls, extra_user_content_parts
|
||||||
|
)
|
||||||
context_query = self._ensure_message_to_dicts(contexts)
|
context_query = self._ensure_message_to_dicts(contexts)
|
||||||
if new_record:
|
if new_record:
|
||||||
context_query.append(new_record)
|
context_query.append(new_record)
|
||||||
@@ -375,10 +377,9 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
for tcr in tool_calls_result:
|
for tcr in tool_calls_result:
|
||||||
context_query.extend(tcr.to_openai_messages())
|
context_query.extend(tcr.to_openai_messages())
|
||||||
|
|
||||||
model_config = self.provider_config.get("model_config", {})
|
model = model or self.get_model()
|
||||||
model_config["model"] = model or self.get_model()
|
|
||||||
|
|
||||||
payloads = {"messages": context_query, **model_config}
|
payloads = {"messages": context_query, "model": model}
|
||||||
|
|
||||||
# xAI origin search tool inject
|
# xAI origin search tool inject
|
||||||
self._maybe_inject_xai_search(payloads, **kwargs)
|
self._maybe_inject_xai_search(payloads, **kwargs)
|
||||||
@@ -478,6 +479,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> LLMResponse:
|
) -> LLMResponse:
|
||||||
payloads, context_query = await self._prepare_chat_payload(
|
payloads, context_query = await self._prepare_chat_payload(
|
||||||
@@ -487,6 +489,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
system_prompt,
|
system_prompt,
|
||||||
tool_calls_result,
|
tool_calls_result,
|
||||||
model=model,
|
model=model,
|
||||||
|
extra_user_content_parts=extra_user_content_parts,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -541,6 +544,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
system_prompt=None,
|
system_prompt=None,
|
||||||
tool_calls_result=None,
|
tool_calls_result=None,
|
||||||
model=None,
|
model=None,
|
||||||
|
extra_user_content_parts=None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> AsyncGenerator[LLMResponse, None]:
|
) -> AsyncGenerator[LLMResponse, None]:
|
||||||
"""流式对话,与服务商交互并逐步返回结果"""
|
"""流式对话,与服务商交互并逐步返回结果"""
|
||||||
@@ -551,6 +555,7 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
system_prompt,
|
system_prompt,
|
||||||
tool_calls_result,
|
tool_calls_result,
|
||||||
model=model,
|
model=model,
|
||||||
|
extra_user_content_parts=extra_user_content_parts,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -626,13 +631,29 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
self,
|
self,
|
||||||
text: str,
|
text: str,
|
||||||
image_urls: list[str] | None = None,
|
image_urls: list[str] | None = None,
|
||||||
|
extra_user_content_parts: list[ContentPart] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""组装成符合 OpenAI 格式的 role 为 user 的消息段"""
|
"""组装成符合 OpenAI 格式的 role 为 user 的消息段"""
|
||||||
|
# 构建内容块列表
|
||||||
|
content_blocks = []
|
||||||
|
|
||||||
|
# 1. 用户原始发言(OpenAI 建议:用户发言在前)
|
||||||
|
if text:
|
||||||
|
content_blocks.append({"type": "text", "text": text})
|
||||||
|
elif image_urls:
|
||||||
|
# 如果没有文本但有图片,添加占位文本
|
||||||
|
content_blocks.append({"type": "text", "text": "[图片]"})
|
||||||
|
elif extra_user_content_parts:
|
||||||
|
# 如果只有额外内容块,也需要添加占位文本
|
||||||
|
content_blocks.append({"type": "text", "text": " "})
|
||||||
|
|
||||||
|
# 2. 额外的内容块(系统提醒、指令等)
|
||||||
|
if extra_user_content_parts:
|
||||||
|
for part in extra_user_content_parts:
|
||||||
|
content_blocks.append(part.model_dump())
|
||||||
|
|
||||||
|
# 3. 图片内容
|
||||||
if image_urls:
|
if image_urls:
|
||||||
user_content = {
|
|
||||||
"role": "user",
|
|
||||||
"content": [{"type": "text", "text": text if text else "[图片]"}],
|
|
||||||
}
|
|
||||||
for image_url in image_urls:
|
for image_url in image_urls:
|
||||||
if image_url.startswith("http"):
|
if image_url.startswith("http"):
|
||||||
image_path = await download_image_by_url(image_url)
|
image_path = await download_image_by_url(image_url)
|
||||||
@@ -645,14 +666,25 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
if not image_data:
|
if not image_data:
|
||||||
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
logger.warning(f"图片 {image_url} 得到的结果为空,将忽略。")
|
||||||
continue
|
continue
|
||||||
user_content["content"].append(
|
content_blocks.append(
|
||||||
{
|
{
|
||||||
"type": "image_url",
|
"type": "image_url",
|
||||||
"image_url": {"url": image_data},
|
"image_url": {"url": image_data},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return user_content
|
|
||||||
return {"role": "user", "content": text}
|
# 如果只有主文本且没有额外内容块和图片,返回简单格式以保持向后兼容
|
||||||
|
if (
|
||||||
|
text
|
||||||
|
and not extra_user_content_parts
|
||||||
|
and not image_urls
|
||||||
|
and len(content_blocks) == 1
|
||||||
|
and content_blocks[0]["type"] == "text"
|
||||||
|
):
|
||||||
|
return {"role": "user", "content": content_blocks[0]["text"]}
|
||||||
|
|
||||||
|
# 否则返回多模态格式
|
||||||
|
return {"role": "user", "content": content_blocks}
|
||||||
|
|
||||||
async def encode_image_bs64(self, image_url: str) -> str:
|
async def encode_image_bs64(self, image_url: str) -> str:
|
||||||
"""将图片转换为 base64"""
|
"""将图片转换为 base64"""
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from collections import defaultdict
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from astrbot.core import db_helper
|
from astrbot.core import db_helper, logger
|
||||||
from astrbot.core.db.po import CommandConfig
|
from astrbot.core.db.po import CommandConfig
|
||||||
from astrbot.core.star.filter.command import CommandFilter
|
from astrbot.core.star.filter.command import CommandFilter
|
||||||
from astrbot.core.star.filter.command_group import CommandGroupFilter
|
from astrbot.core.star.filter.command_group import CommandGroupFilter
|
||||||
@@ -90,6 +90,7 @@ async def toggle_command(handler_full_name: str, enabled: bool) -> CommandDescri
|
|||||||
async def rename_command(
|
async def rename_command(
|
||||||
handler_full_name: str,
|
handler_full_name: str,
|
||||||
new_fragment: str,
|
new_fragment: str,
|
||||||
|
aliases: list[str] | None = None,
|
||||||
) -> CommandDescriptor:
|
) -> CommandDescriptor:
|
||||||
descriptor = _build_descriptor_by_full_name(handler_full_name)
|
descriptor = _build_descriptor_by_full_name(handler_full_name)
|
||||||
if not descriptor:
|
if not descriptor:
|
||||||
@@ -99,9 +100,24 @@ async def rename_command(
|
|||||||
if not new_fragment:
|
if not new_fragment:
|
||||||
raise ValueError("指令名不能为空。")
|
raise ValueError("指令名不能为空。")
|
||||||
|
|
||||||
|
# 校验主指令名
|
||||||
candidate_full = _compose_command(descriptor.parent_signature, new_fragment)
|
candidate_full = _compose_command(descriptor.parent_signature, new_fragment)
|
||||||
if _is_command_in_use(handler_full_name, candidate_full):
|
if _is_command_in_use(handler_full_name, candidate_full):
|
||||||
raise ValueError("新的指令名已被其他指令占用,请换一个名称。")
|
raise ValueError(f"指令名 '{candidate_full}' 已被其他指令占用。")
|
||||||
|
|
||||||
|
# 校验别名
|
||||||
|
if aliases:
|
||||||
|
for alias in aliases:
|
||||||
|
alias = alias.strip()
|
||||||
|
if not alias:
|
||||||
|
continue
|
||||||
|
alias_full = _compose_command(descriptor.parent_signature, alias)
|
||||||
|
if _is_command_in_use(handler_full_name, alias_full):
|
||||||
|
raise ValueError(f"别名 '{alias_full}' 已被其他指令占用。")
|
||||||
|
|
||||||
|
existing_cfg = await db_helper.get_command_config(handler_full_name)
|
||||||
|
merged_extra = dict(existing_cfg.extra_data or {}) if existing_cfg else {}
|
||||||
|
merged_extra["resolved_aliases"] = aliases or []
|
||||||
|
|
||||||
config = await db_helper.upsert_command_config(
|
config = await db_helper.upsert_command_config(
|
||||||
handler_full_name=handler_full_name,
|
handler_full_name=handler_full_name,
|
||||||
@@ -114,7 +130,7 @@ async def rename_command(
|
|||||||
conflict_key=descriptor.original_command,
|
conflict_key=descriptor.original_command,
|
||||||
resolution_strategy="manual_rename",
|
resolution_strategy="manual_rename",
|
||||||
note=None,
|
note=None,
|
||||||
extra_data=None,
|
extra_data=merged_extra,
|
||||||
auto_managed=False,
|
auto_managed=False,
|
||||||
)
|
)
|
||||||
_bind_descriptor_with_config(descriptor, config)
|
_bind_descriptor_with_config(descriptor, config)
|
||||||
@@ -192,12 +208,18 @@ def _collect_descriptors(include_sub_commands: bool) -> list[CommandDescriptor]:
|
|||||||
"""收集指令,按需包含子指令。"""
|
"""收集指令,按需包含子指令。"""
|
||||||
descriptors: list[CommandDescriptor] = []
|
descriptors: list[CommandDescriptor] = []
|
||||||
for handler in star_handlers_registry:
|
for handler in star_handlers_registry:
|
||||||
desc = _build_descriptor(handler)
|
try:
|
||||||
if not desc:
|
desc = _build_descriptor(handler)
|
||||||
|
if not desc:
|
||||||
|
continue
|
||||||
|
if not include_sub_commands and desc.is_sub_command:
|
||||||
|
continue
|
||||||
|
descriptors.append(desc)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(
|
||||||
|
f"解析指令处理函数 {handler.handler_full_name} 失败,跳过该指令。原因: {e!s}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
if not include_sub_commands and desc.is_sub_command:
|
|
||||||
continue
|
|
||||||
descriptors.append(desc)
|
|
||||||
return descriptors
|
return descriptors
|
||||||
|
|
||||||
|
|
||||||
@@ -357,14 +379,27 @@ def _apply_config_to_descriptor(
|
|||||||
new_fragment,
|
new_fragment,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
extra = config.extra_data or {}
|
||||||
|
resolved_aliases = extra.get("resolved_aliases")
|
||||||
|
if isinstance(resolved_aliases, list):
|
||||||
|
descriptor.aliases = [str(x) for x in resolved_aliases if str(x).strip()]
|
||||||
|
|
||||||
|
|
||||||
def _apply_config_to_runtime(
|
def _apply_config_to_runtime(
|
||||||
descriptor: CommandDescriptor,
|
descriptor: CommandDescriptor,
|
||||||
config: CommandConfig,
|
config: CommandConfig,
|
||||||
) -> None:
|
) -> None:
|
||||||
descriptor.handler.enabled = config.enabled
|
descriptor.handler.enabled = config.enabled
|
||||||
if descriptor.filter_ref and descriptor.current_fragment:
|
if descriptor.filter_ref:
|
||||||
_set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
|
if descriptor.current_fragment:
|
||||||
|
_set_filter_fragment(descriptor.filter_ref, descriptor.current_fragment)
|
||||||
|
extra = config.extra_data or {}
|
||||||
|
resolved_aliases = extra.get("resolved_aliases")
|
||||||
|
if isinstance(resolved_aliases, list):
|
||||||
|
_set_filter_aliases(
|
||||||
|
descriptor.filter_ref,
|
||||||
|
[str(x) for x in resolved_aliases if str(x).strip()],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _bind_configs_to_descriptors(
|
def _bind_configs_to_descriptors(
|
||||||
@@ -403,6 +438,18 @@ def _set_filter_fragment(
|
|||||||
filter_ref._cmpl_cmd_names = None
|
filter_ref._cmpl_cmd_names = None
|
||||||
|
|
||||||
|
|
||||||
|
def _set_filter_aliases(
|
||||||
|
filter_ref: CommandFilter | CommandGroupFilter,
|
||||||
|
aliases: list[str],
|
||||||
|
) -> None:
|
||||||
|
current_aliases = getattr(filter_ref, "alias", set())
|
||||||
|
if set(aliases) == current_aliases:
|
||||||
|
return
|
||||||
|
setattr(filter_ref, "alias", set(aliases))
|
||||||
|
if hasattr(filter_ref, "_cmpl_cmd_names"):
|
||||||
|
filter_ref._cmpl_cmd_names = None
|
||||||
|
|
||||||
|
|
||||||
def _is_command_in_use(
|
def _is_command_in_use(
|
||||||
target_handler_full_name: str,
|
target_handler_full_name: str,
|
||||||
candidate_full_command: str,
|
candidate_full_command: str,
|
||||||
|
|||||||
@@ -267,6 +267,10 @@ class Context:
|
|||||||
):
|
):
|
||||||
"""通过 ID 获取对应的 LLM Provider。"""
|
"""通过 ID 获取对应的 LLM Provider。"""
|
||||||
prov = self.provider_manager.inst_map.get(provider_id)
|
prov = self.provider_manager.inst_map.get(provider_id)
|
||||||
|
if provider_id and not prov:
|
||||||
|
logger.warning(
|
||||||
|
f"没有找到 ID 为 {provider_id} 的提供商,这可能是由于您修改了提供商(模型)ID 导致的。"
|
||||||
|
)
|
||||||
return prov
|
return prov
|
||||||
|
|
||||||
def get_all_providers(self) -> list[Provider]:
|
def get_all_providers(self) -> list[Provider]:
|
||||||
@@ -296,10 +300,6 @@ class Context:
|
|||||||
provider_type=ProviderType.CHAT_COMPLETION,
|
provider_type=ProviderType.CHAT_COMPLETION,
|
||||||
umo=umo,
|
umo=umo,
|
||||||
)
|
)
|
||||||
if prov is None:
|
|
||||||
raise ProviderNotFoundError(
|
|
||||||
"provider not found, please choose provider first"
|
|
||||||
)
|
|
||||||
if not isinstance(prov, Provider):
|
if not isinstance(prov, Provider):
|
||||||
raise ValueError("返回的 Provider 不是 Provider 类型")
|
raise ValueError("返回的 Provider 不是 Provider 类型")
|
||||||
return prov
|
return prov
|
||||||
|
|||||||
@@ -631,7 +631,11 @@ class PluginManager:
|
|||||||
# 清除 pip.main 导致的多余的 logging handlers
|
# 清除 pip.main 导致的多余的 logging handlers
|
||||||
for handler in logging.root.handlers[:]:
|
for handler in logging.root.handlers[:]:
|
||||||
logging.root.removeHandler(handler)
|
logging.root.removeHandler(handler)
|
||||||
await sync_command_configs()
|
try:
|
||||||
|
await sync_command_configs()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"同步指令配置失败: {e!s}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
if not fail_rec:
|
if not fail_rec:
|
||||||
return True, None
|
return True, None
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
from typing import Literal, TypedDict
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
|
from astrbot.core import logger
|
||||||
|
|
||||||
|
|
||||||
|
class LLMModalities(TypedDict):
|
||||||
|
input: list[Literal["text", "image", "audio", "video"]]
|
||||||
|
output: list[Literal["text", "image", "audio", "video"]]
|
||||||
|
|
||||||
|
|
||||||
|
class LLMLimit(TypedDict):
|
||||||
|
context: int
|
||||||
|
output: int
|
||||||
|
|
||||||
|
|
||||||
|
class LLMMetadata(TypedDict):
|
||||||
|
id: str
|
||||||
|
reasoning: bool
|
||||||
|
tool_call: bool
|
||||||
|
knowledge: str
|
||||||
|
release_date: str
|
||||||
|
modalities: LLMModalities
|
||||||
|
open_weights: bool
|
||||||
|
limit: LLMLimit
|
||||||
|
|
||||||
|
|
||||||
|
LLM_METADATAS: dict[str, LLMMetadata] = {}
|
||||||
|
|
||||||
|
|
||||||
|
async def update_llm_metadata():
|
||||||
|
url = "https://models.dev/api.json"
|
||||||
|
try:
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url) as response:
|
||||||
|
data = await response.json()
|
||||||
|
global LLM_METADATAS
|
||||||
|
models = {}
|
||||||
|
for info in data.values():
|
||||||
|
for model in info.get("models", {}).values():
|
||||||
|
model_id = model.get("id")
|
||||||
|
if not model_id:
|
||||||
|
continue
|
||||||
|
models[model_id] = LLMMetadata(
|
||||||
|
id=model_id,
|
||||||
|
reasoning=model.get("reasoning", False),
|
||||||
|
tool_call=model.get("tool_call", False),
|
||||||
|
knowledge=model.get("knowledge", "none"),
|
||||||
|
release_date=model.get("release_date", ""),
|
||||||
|
modalities=model.get(
|
||||||
|
"modalities", {"input": [], "output": []}
|
||||||
|
),
|
||||||
|
open_weights=model.get("open_weights", False),
|
||||||
|
limit=model.get("limit", {"context": 0, "output": 0}),
|
||||||
|
)
|
||||||
|
# Replace the global cache in-place so references remain valid
|
||||||
|
LLM_METADATAS.clear()
|
||||||
|
LLM_METADATAS.update(models)
|
||||||
|
logger.info(f"Successfully fetched metadata for {len(models)} LLMs.")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to fetch LLM metadata: {e}")
|
||||||
|
return
|
||||||
@@ -32,6 +32,92 @@ def _migra_agent_runner_configs(conf: AstrBotConfig, ids_map: dict) -> None:
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
def _migra_provider_to_source_structure(conf: AstrBotConfig) -> None:
|
||||||
|
"""
|
||||||
|
Migrate old provider structure to new provider-source separation.
|
||||||
|
Provider only keeps: id, provider_source_id, model, modalities, custom_extra_body
|
||||||
|
All other fields move to provider_sources.
|
||||||
|
"""
|
||||||
|
providers = conf.get("provider", [])
|
||||||
|
provider_sources = conf.get("provider_sources", [])
|
||||||
|
|
||||||
|
# Track if any migration happened
|
||||||
|
migrated = False
|
||||||
|
|
||||||
|
# Provider-only fields that should stay in provider
|
||||||
|
provider_only_fields = {
|
||||||
|
"id",
|
||||||
|
"provider_source_id",
|
||||||
|
"model",
|
||||||
|
"modalities",
|
||||||
|
"custom_extra_body",
|
||||||
|
"enable",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fields that should not go to source
|
||||||
|
source_exclude_fields = provider_only_fields | {"model_config"}
|
||||||
|
|
||||||
|
for provider in providers:
|
||||||
|
# Skip if already has provider_source_id
|
||||||
|
if provider.get("provider_source_id"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip non-chat-completion types (they don't need source separation)
|
||||||
|
provider_type = provider.get("provider_type", "")
|
||||||
|
if provider_type != "chat_completion":
|
||||||
|
# For old types without provider_type, check type field
|
||||||
|
old_type = provider.get("type", "")
|
||||||
|
if "chat_completion" not in old_type:
|
||||||
|
continue
|
||||||
|
|
||||||
|
migrated = True
|
||||||
|
logger.info(f"Migrating provider {provider.get('id')} to new structure")
|
||||||
|
|
||||||
|
# Extract source fields from provider
|
||||||
|
source_fields = {}
|
||||||
|
for key, value in list(provider.items()):
|
||||||
|
if key not in source_exclude_fields:
|
||||||
|
source_fields[key] = value
|
||||||
|
|
||||||
|
# Create new provider_source
|
||||||
|
source_id = provider.get("id", "") + "_source"
|
||||||
|
new_source = {"id": source_id, **source_fields}
|
||||||
|
|
||||||
|
# Update provider to only keep necessary fields
|
||||||
|
provider["provider_source_id"] = source_id
|
||||||
|
|
||||||
|
# Extract model from model_config if exists
|
||||||
|
if "model_config" in provider and isinstance(provider["model_config"], dict):
|
||||||
|
model_config = provider["model_config"]
|
||||||
|
provider["model"] = model_config.get("model", "")
|
||||||
|
|
||||||
|
# Put other model_config fields into custom_extra_body
|
||||||
|
extra_body_fields = {k: v for k, v in model_config.items() if k != "model"}
|
||||||
|
if extra_body_fields:
|
||||||
|
if "custom_extra_body" not in provider:
|
||||||
|
provider["custom_extra_body"] = {}
|
||||||
|
provider["custom_extra_body"].update(extra_body_fields)
|
||||||
|
|
||||||
|
# Initialize new fields if not present
|
||||||
|
if "modalities" not in provider:
|
||||||
|
provider["modalities"] = []
|
||||||
|
if "custom_extra_body" not in provider:
|
||||||
|
provider["custom_extra_body"] = {}
|
||||||
|
|
||||||
|
# Remove fields that should be in source
|
||||||
|
keys_to_remove = [k for k in provider.keys() if k not in provider_only_fields]
|
||||||
|
for key in keys_to_remove:
|
||||||
|
del provider[key]
|
||||||
|
|
||||||
|
# Add source to provider_sources
|
||||||
|
provider_sources.append(new_source)
|
||||||
|
|
||||||
|
if migrated:
|
||||||
|
conf["provider_sources"] = provider_sources
|
||||||
|
conf.save_config()
|
||||||
|
logger.info("Provider-source structure migration completed")
|
||||||
|
|
||||||
|
|
||||||
async def migra(
|
async def migra(
|
||||||
db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager
|
db, astrbot_config_mgr, umop_config_router, acm: AstrBotConfigManager
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -71,3 +157,10 @@ async def migra(
|
|||||||
|
|
||||||
for conf in acm.confs.values():
|
for conf in acm.confs.values():
|
||||||
_migra_agent_runner_configs(conf, ids_map)
|
_migra_agent_runner_configs(conf, ids_map)
|
||||||
|
|
||||||
|
# Migrate providers to new structure: extract source fields to provider_sources
|
||||||
|
try:
|
||||||
|
_migra_provider_to_source_structure(astrbot_config)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Migration for provider-source structure failed: {e!s}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|||||||
@@ -436,7 +436,7 @@ class ChatRoute(Route):
|
|||||||
accumulated_parts = []
|
accumulated_parts = []
|
||||||
accumulated_text = ""
|
accumulated_text = ""
|
||||||
accumulated_reasoning = ""
|
accumulated_reasoning = ""
|
||||||
tool_calls = {}
|
# tool_calls = {}
|
||||||
agent_stats = {}
|
agent_stats = {}
|
||||||
except BaseException as e:
|
except BaseException as e:
|
||||||
logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
|
logger.exception(f"WebChat stream unexpected error: {e}", exc_info=True)
|
||||||
|
|||||||
@@ -61,12 +61,13 @@ class CommandRoute(Route):
|
|||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
handler_full_name = data.get("handler_full_name")
|
handler_full_name = data.get("handler_full_name")
|
||||||
new_name = data.get("new_name")
|
new_name = data.get("new_name")
|
||||||
|
aliases = data.get("aliases")
|
||||||
|
|
||||||
if not handler_full_name or not new_name:
|
if not handler_full_name or not new_name:
|
||||||
return Response().error("handler_full_name 与 new_name 均为必填。").__dict__
|
return Response().error("handler_full_name 与 new_name 均为必填。").__dict__
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await rename_command_service(handler_full_name, new_name)
|
await rename_command_service(handler_full_name, new_name, aliases=aliases)
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
return Response().error(str(exc)).__dict__
|
return Response().error(str(exc)).__dict__
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Any
|
|||||||
|
|
||||||
from quart import request
|
from quart import request
|
||||||
|
|
||||||
from astrbot.core import file_token_service, logger
|
from astrbot.core import astrbot_config, file_token_service, logger
|
||||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||||
from astrbot.core.config.default import (
|
from astrbot.core.config.default import (
|
||||||
CONFIG_METADATA_2,
|
CONFIG_METADATA_2,
|
||||||
@@ -21,6 +21,7 @@ from astrbot.core.platform.register import platform_cls_map, platform_registry
|
|||||||
from astrbot.core.provider import Provider
|
from astrbot.core.provider import Provider
|
||||||
from astrbot.core.provider.register import provider_registry
|
from astrbot.core.provider.register import provider_registry
|
||||||
from astrbot.core.star.star import star_registry
|
from astrbot.core.star.star import star_registry
|
||||||
|
from astrbot.core.utils.llm_metadata import LLM_METADATAS
|
||||||
from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config
|
from astrbot.core.utils.webhook_utils import ensure_platform_webhook_config
|
||||||
|
|
||||||
from .route import Response, Route, RouteContext
|
from .route import Response, Route, RouteContext
|
||||||
@@ -179,13 +180,157 @@ class ConfigRoute(Route):
|
|||||||
"/config/provider/new": ("POST", self.post_new_provider),
|
"/config/provider/new": ("POST", self.post_new_provider),
|
||||||
"/config/provider/update": ("POST", self.post_update_provider),
|
"/config/provider/update": ("POST", self.post_update_provider),
|
||||||
"/config/provider/delete": ("POST", self.post_delete_provider),
|
"/config/provider/delete": ("POST", self.post_delete_provider),
|
||||||
|
"/config/provider/template": ("GET", self.get_provider_template),
|
||||||
"/config/provider/check_one": ("GET", self.check_one_provider_status),
|
"/config/provider/check_one": ("GET", self.check_one_provider_status),
|
||||||
"/config/provider/list": ("GET", self.get_provider_config_list),
|
"/config/provider/list": ("GET", self.get_provider_config_list),
|
||||||
"/config/provider/model_list": ("GET", self.get_provider_model_list),
|
"/config/provider/model_list": ("GET", self.get_provider_model_list),
|
||||||
"/config/provider/get_embedding_dim": ("POST", self.get_embedding_dim),
|
"/config/provider/get_embedding_dim": ("POST", self.get_embedding_dim),
|
||||||
|
"/config/provider_sources/models": (
|
||||||
|
"GET",
|
||||||
|
self.get_provider_source_models,
|
||||||
|
),
|
||||||
|
"/config/provider_sources/update": (
|
||||||
|
"POST",
|
||||||
|
self.update_provider_source,
|
||||||
|
),
|
||||||
|
"/config/provider_sources/delete": (
|
||||||
|
"POST",
|
||||||
|
self.delete_provider_source,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
self.register_routes()
|
self.register_routes()
|
||||||
|
|
||||||
|
async def delete_provider_source(self):
|
||||||
|
"""删除 provider_source,并更新关联的 providers"""
|
||||||
|
post_data = await request.json
|
||||||
|
if not post_data:
|
||||||
|
return Response().error("缺少配置数据").__dict__
|
||||||
|
|
||||||
|
provider_source_id = post_data.get("id")
|
||||||
|
if not provider_source_id:
|
||||||
|
return Response().error("缺少 provider_source_id").__dict__
|
||||||
|
|
||||||
|
provider_sources = self.config.get("provider_sources", [])
|
||||||
|
target_idx = next(
|
||||||
|
(
|
||||||
|
i
|
||||||
|
for i, ps in enumerate(provider_sources)
|
||||||
|
if ps.get("id") == provider_source_id
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
if target_idx == -1:
|
||||||
|
return Response().error("未找到对应的 provider source").__dict__
|
||||||
|
|
||||||
|
# 删除 provider_source
|
||||||
|
del provider_sources[target_idx]
|
||||||
|
|
||||||
|
# 写回配置
|
||||||
|
self.config["provider_sources"] = provider_sources
|
||||||
|
|
||||||
|
# 删除引用了该 provider_source 的 providers
|
||||||
|
await self.core_lifecycle.provider_manager.delete_provider(
|
||||||
|
provider_source_id=provider_source_id
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
save_config(self.config, self.config, is_core=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response().error(str(e)).__dict__
|
||||||
|
|
||||||
|
return Response().ok(message="删除 provider source 成功").__dict__
|
||||||
|
|
||||||
|
async def update_provider_source(self):
|
||||||
|
"""更新或新增 provider_source,并重载关联的 providers"""
|
||||||
|
post_data = await request.json
|
||||||
|
if not post_data:
|
||||||
|
return Response().error("缺少配置数据").__dict__
|
||||||
|
|
||||||
|
new_source_config = post_data.get("config") or post_data
|
||||||
|
original_id = post_data.get("original_id")
|
||||||
|
if not original_id:
|
||||||
|
return Response().error("缺少 original_id").__dict__
|
||||||
|
|
||||||
|
if not isinstance(new_source_config, dict):
|
||||||
|
return Response().error("缺少或错误的配置数据").__dict__
|
||||||
|
|
||||||
|
# 确保配置中有 id 字段
|
||||||
|
if not new_source_config.get("id"):
|
||||||
|
new_source_config["id"] = original_id
|
||||||
|
|
||||||
|
provider_sources = self.config.get("provider_sources", [])
|
||||||
|
|
||||||
|
for ps in provider_sources:
|
||||||
|
if ps.get("id") == new_source_config["id"] and ps.get("id") != original_id:
|
||||||
|
return (
|
||||||
|
Response()
|
||||||
|
.error(
|
||||||
|
f"Provider source ID '{new_source_config['id']}' exists already, please try another ID.",
|
||||||
|
)
|
||||||
|
.__dict__
|
||||||
|
)
|
||||||
|
|
||||||
|
# 查找旧的 provider_source,若不存在则追加为新配置
|
||||||
|
target_idx = next(
|
||||||
|
(i for i, ps in enumerate(provider_sources) if ps.get("id") == original_id),
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
old_id = original_id
|
||||||
|
if target_idx == -1:
|
||||||
|
provider_sources.append(new_source_config)
|
||||||
|
else:
|
||||||
|
old_id = provider_sources[target_idx].get("id")
|
||||||
|
provider_sources[target_idx] = new_source_config
|
||||||
|
|
||||||
|
# 更新引用了该 provider_source 的 providers
|
||||||
|
affected_providers = []
|
||||||
|
for provider in self.config.get("provider", []):
|
||||||
|
if provider.get("provider_source_id") == old_id:
|
||||||
|
provider["provider_source_id"] = new_source_config["id"]
|
||||||
|
affected_providers.append(provider)
|
||||||
|
|
||||||
|
# 写回配置
|
||||||
|
self.config["provider_sources"] = provider_sources
|
||||||
|
|
||||||
|
try:
|
||||||
|
save_config(self.config, self.config, is_core=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response().error(str(e)).__dict__
|
||||||
|
|
||||||
|
# 重载受影响的 providers,使新的 source 配置生效
|
||||||
|
reload_errors = []
|
||||||
|
prov_mgr = self.core_lifecycle.provider_manager
|
||||||
|
for provider in affected_providers:
|
||||||
|
try:
|
||||||
|
await prov_mgr.reload(provider)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
reload_errors.append(f"{provider.get('id')}: {e}")
|
||||||
|
|
||||||
|
if reload_errors:
|
||||||
|
return (
|
||||||
|
Response()
|
||||||
|
.error("更新成功,但部分提供商重载失败: " + ", ".join(reload_errors))
|
||||||
|
.__dict__
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response().ok(message="更新 provider source 成功").__dict__
|
||||||
|
|
||||||
|
async def get_provider_template(self):
|
||||||
|
config_schema = {
|
||||||
|
"provider": CONFIG_METADATA_2["provider_group"]["metadata"]["provider"]
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"config_schema": config_schema,
|
||||||
|
"providers": astrbot_config["provider"],
|
||||||
|
"provider_sources": astrbot_config["provider_sources"],
|
||||||
|
}
|
||||||
|
return Response().ok(data=data).__dict__
|
||||||
|
|
||||||
async def get_uc_table(self):
|
async def get_uc_table(self):
|
||||||
"""获取 UMOP 配置路由表"""
|
"""获取 UMOP 配置路由表"""
|
||||||
return Response().ok({"routing": self.ucr.umop_to_conf_id}).__dict__
|
return Response().ok({"routing": self.ucr.umop_to_conf_id}).__dict__
|
||||||
@@ -433,9 +578,25 @@ class ConfigRoute(Route):
|
|||||||
return Response().error("缺少参数 provider_type").__dict__
|
return Response().error("缺少参数 provider_type").__dict__
|
||||||
provider_type_ls = provider_type.split(",")
|
provider_type_ls = provider_type.split(",")
|
||||||
provider_list = []
|
provider_list = []
|
||||||
astrbot_config = self.core_lifecycle.astrbot_config
|
ps = self.core_lifecycle.provider_manager.providers_config
|
||||||
for provider in astrbot_config["provider"]:
|
p_source_pt = {
|
||||||
if provider.get("provider_type", None) in provider_type_ls:
|
psrc["id"]: psrc["provider_type"]
|
||||||
|
for psrc in self.core_lifecycle.provider_manager.provider_sources_config
|
||||||
|
}
|
||||||
|
for provider in ps:
|
||||||
|
ps_id = provider.get("provider_source_id", None)
|
||||||
|
if (
|
||||||
|
ps_id
|
||||||
|
and ps_id in p_source_pt
|
||||||
|
and p_source_pt[ps_id] in provider_type_ls
|
||||||
|
):
|
||||||
|
# chat
|
||||||
|
prov = self.core_lifecycle.provider_manager.get_merged_provider_config(
|
||||||
|
provider
|
||||||
|
)
|
||||||
|
provider_list.append(prov)
|
||||||
|
elif not ps_id and provider.get("provider_type", None) in provider_type_ls:
|
||||||
|
# agent runner, embedding, etc
|
||||||
provider_list.append(provider)
|
provider_list.append(provider)
|
||||||
return Response().ok(provider_list).__dict__
|
return Response().ok(provider_list).__dict__
|
||||||
|
|
||||||
@@ -458,9 +619,18 @@ class ConfigRoute(Route):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
models = await provider.get_models()
|
models = await provider.get_models()
|
||||||
|
models = models or []
|
||||||
|
|
||||||
|
metadata_map = {}
|
||||||
|
for model_id in models:
|
||||||
|
meta = LLM_METADATAS.get(model_id)
|
||||||
|
if meta:
|
||||||
|
metadata_map[model_id] = meta
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
"models": models,
|
"models": models,
|
||||||
"provider_id": provider_id,
|
"provider_id": provider_id,
|
||||||
|
"model_metadata": metadata_map,
|
||||||
}
|
}
|
||||||
return Response().ok(ret).__dict__
|
return Response().ok(ret).__dict__
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -522,6 +692,104 @@ class ConfigRoute(Route):
|
|||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return Response().error(f"获取嵌入维度失败: {e!s}").__dict__
|
return Response().error(f"获取嵌入维度失败: {e!s}").__dict__
|
||||||
|
|
||||||
|
async def get_provider_source_models(self):
|
||||||
|
"""获取指定 provider_source 支持的模型列表
|
||||||
|
|
||||||
|
本质上会临时初始化一个 Provider 实例,调用 get_models() 获取模型列表,然后销毁实例
|
||||||
|
"""
|
||||||
|
provider_source_id = request.args.get("source_id")
|
||||||
|
if not provider_source_id:
|
||||||
|
return Response().error("缺少参数 source_id").__dict__
|
||||||
|
|
||||||
|
try:
|
||||||
|
from astrbot.core.provider.register import provider_cls_map
|
||||||
|
|
||||||
|
# 从配置中查找对应的 provider_source
|
||||||
|
provider_sources = self.config.get("provider_sources", [])
|
||||||
|
provider_source = None
|
||||||
|
for ps in provider_sources:
|
||||||
|
if ps.get("id") == provider_source_id:
|
||||||
|
provider_source = ps
|
||||||
|
break
|
||||||
|
|
||||||
|
if not provider_source:
|
||||||
|
return (
|
||||||
|
Response()
|
||||||
|
.error(f"未找到 ID 为 {provider_source_id} 的 provider_source")
|
||||||
|
.__dict__
|
||||||
|
)
|
||||||
|
|
||||||
|
# 获取 provider 类型
|
||||||
|
provider_type = provider_source.get("type", None)
|
||||||
|
if not provider_type:
|
||||||
|
return Response().error("provider_source 缺少 type 字段").__dict__
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.core_lifecycle.provider_manager.dynamic_import_provider(
|
||||||
|
provider_type
|
||||||
|
)
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response().error(f"动态导入提供商适配器失败: {e!s}").__dict__
|
||||||
|
|
||||||
|
# 获取对应的 provider 类
|
||||||
|
if provider_type not in provider_cls_map:
|
||||||
|
return (
|
||||||
|
Response()
|
||||||
|
.error(f"未找到适用于 {provider_type} 的提供商适配器")
|
||||||
|
.__dict__
|
||||||
|
)
|
||||||
|
|
||||||
|
provider_metadata = provider_cls_map[provider_type]
|
||||||
|
cls_type = provider_metadata.cls_type
|
||||||
|
|
||||||
|
if not cls_type:
|
||||||
|
return Response().error(f"无法找到 {provider_type} 的类").__dict__
|
||||||
|
|
||||||
|
# 检查是否是 Provider 类型
|
||||||
|
if not issubclass(cls_type, Provider):
|
||||||
|
return (
|
||||||
|
Response()
|
||||||
|
.error(f"提供商 {provider_type} 不支持获取模型列表")
|
||||||
|
.__dict__
|
||||||
|
)
|
||||||
|
|
||||||
|
# 临时实例化 provider
|
||||||
|
inst = cls_type(provider_source, {})
|
||||||
|
|
||||||
|
# 如果有 initialize 方法,调用它
|
||||||
|
init_fn = getattr(inst, "initialize", None)
|
||||||
|
if inspect.iscoroutinefunction(init_fn):
|
||||||
|
await init_fn()
|
||||||
|
|
||||||
|
# 获取模型列表
|
||||||
|
models = await inst.get_models()
|
||||||
|
models = models or []
|
||||||
|
|
||||||
|
metadata_map = {}
|
||||||
|
for model_id in models:
|
||||||
|
meta = LLM_METADATAS.get(model_id)
|
||||||
|
if meta:
|
||||||
|
metadata_map[model_id] = meta
|
||||||
|
|
||||||
|
# 销毁实例(如果有 terminate 方法)
|
||||||
|
terminate_fn = getattr(inst, "terminate", None)
|
||||||
|
if inspect.iscoroutinefunction(terminate_fn):
|
||||||
|
await terminate_fn()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"获取到 provider_source {provider_source_id} 的模型列表: {models}",
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
Response()
|
||||||
|
.ok({"models": models, "model_metadata": metadata_map})
|
||||||
|
.__dict__
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response().error(f"获取模型列表失败: {e!s}").__dict__
|
||||||
|
|
||||||
async def get_platform_list(self):
|
async def get_platform_list(self):
|
||||||
"""获取所有平台的列表"""
|
"""获取所有平台的列表"""
|
||||||
platform_list = []
|
platform_list = []
|
||||||
@@ -533,7 +801,15 @@ class ConfigRoute(Route):
|
|||||||
data = await request.json
|
data = await request.json
|
||||||
config = data.get("config", None)
|
config = data.get("config", None)
|
||||||
conf_id = data.get("conf_id", None)
|
conf_id = data.get("conf_id", None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 不更新 provider_sources, provider, platform
|
||||||
|
# 这些配置有单独的接口进行更新
|
||||||
|
if conf_id == "default":
|
||||||
|
no_update_keys = ["provider_sources", "provider", "platform"]
|
||||||
|
for key in no_update_keys:
|
||||||
|
config[key] = self.acm.default_conf[key]
|
||||||
|
|
||||||
await self._save_astrbot_configs(config, conf_id)
|
await self._save_astrbot_configs(config, conf_id)
|
||||||
await self.core_lifecycle.reload_pipeline_scheduler(conf_id)
|
await self.core_lifecycle.reload_pipeline_scheduler(conf_id)
|
||||||
return Response().ok(None, "保存成功~").__dict__
|
return Response().ok(None, "保存成功~").__dict__
|
||||||
@@ -573,28 +849,30 @@ class ConfigRoute(Route):
|
|||||||
|
|
||||||
async def post_new_provider(self):
|
async def post_new_provider(self):
|
||||||
new_provider_config = await request.json
|
new_provider_config = await request.json
|
||||||
self.config["provider"].append(new_provider_config)
|
|
||||||
try:
|
try:
|
||||||
save_config(self.config, self.config, is_core=True)
|
await self.core_lifecycle.provider_manager.create_provider(
|
||||||
await self.core_lifecycle.provider_manager.load_provider(
|
new_provider_config
|
||||||
new_provider_config,
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Response().error(str(e)).__dict__
|
return Response().error(str(e)).__dict__
|
||||||
return Response().ok(None, "新增服务提供商配置成功~").__dict__
|
return Response().ok(None, "新增服务提供商配置成功").__dict__
|
||||||
|
|
||||||
async def post_update_platform(self):
|
async def post_update_platform(self):
|
||||||
update_platform_config = await request.json
|
update_platform_config = await request.json
|
||||||
platform_id = update_platform_config.get("id", None)
|
origin_platform_id = update_platform_config.get("id", None)
|
||||||
new_config = update_platform_config.get("config", None)
|
new_config = update_platform_config.get("config", None)
|
||||||
if not platform_id or not new_config:
|
if not origin_platform_id or not new_config:
|
||||||
return Response().error("参数错误").__dict__
|
return Response().error("参数错误").__dict__
|
||||||
|
|
||||||
|
if origin_platform_id != new_config.get("id", None):
|
||||||
|
return Response().error("机器人名称不允许修改").__dict__
|
||||||
|
|
||||||
# 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,确保有 webhook_uuid
|
# 如果是支持统一 webhook 模式的平台,且启用了统一 webhook 模式,确保有 webhook_uuid
|
||||||
ensure_platform_webhook_config(new_config)
|
ensure_platform_webhook_config(new_config)
|
||||||
|
|
||||||
for i, platform in enumerate(self.config["platform"]):
|
for i, platform in enumerate(self.config["platform"]):
|
||||||
if platform["id"] == platform_id:
|
if platform["id"] == origin_platform_id:
|
||||||
self.config["platform"][i] = new_config
|
self.config["platform"][i] = new_config
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@@ -609,21 +887,15 @@ class ConfigRoute(Route):
|
|||||||
|
|
||||||
async def post_update_provider(self):
|
async def post_update_provider(self):
|
||||||
update_provider_config = await request.json
|
update_provider_config = await request.json
|
||||||
provider_id = update_provider_config.get("id", None)
|
origin_provider_id = update_provider_config.get("id", None)
|
||||||
new_config = update_provider_config.get("config", None)
|
new_config = update_provider_config.get("config", None)
|
||||||
if not provider_id or not new_config:
|
if not origin_provider_id or not new_config:
|
||||||
return Response().error("参数错误").__dict__
|
return Response().error("参数错误").__dict__
|
||||||
|
|
||||||
for i, provider in enumerate(self.config["provider"]):
|
|
||||||
if provider["id"] == provider_id:
|
|
||||||
self.config["provider"][i] = new_config
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return Response().error("未找到对应服务提供商").__dict__
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
save_config(self.config, self.config, is_core=True)
|
await self.core_lifecycle.provider_manager.update_provider(
|
||||||
await self.core_lifecycle.provider_manager.reload(new_config)
|
origin_provider_id, new_config
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Response().error(str(e)).__dict__
|
return Response().error(str(e)).__dict__
|
||||||
return Response().ok(None, "更新成功,已经实时生效~").__dict__
|
return Response().ok(None, "更新成功,已经实时生效~").__dict__
|
||||||
@@ -646,19 +918,17 @@ class ConfigRoute(Route):
|
|||||||
|
|
||||||
async def post_delete_provider(self):
|
async def post_delete_provider(self):
|
||||||
provider_id = await request.json
|
provider_id = await request.json
|
||||||
provider_id = provider_id.get("id")
|
provider_id = provider_id.get("id", "")
|
||||||
for i, provider in enumerate(self.config["provider"]):
|
if not provider_id:
|
||||||
if provider["id"] == provider_id:
|
return Response().error("缺少参数 id").__dict__
|
||||||
del self.config["provider"][i]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
return Response().error("未找到对应服务提供商").__dict__
|
|
||||||
try:
|
try:
|
||||||
save_config(self.config, self.config, is_core=True)
|
await self.core_lifecycle.provider_manager.delete_provider(
|
||||||
await self.core_lifecycle.provider_manager.terminate_provider(provider_id)
|
provider_id=provider_id
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return Response().error(str(e)).__dict__
|
return Response().error(str(e)).__dict__
|
||||||
return Response().ok(None, "删除成功,已经实时生效~").__dict__
|
return Response().ok(None, "删除成功,已经实时生效。").__dict__
|
||||||
|
|
||||||
async def get_llm_tools(self):
|
async def get_llm_tools(self):
|
||||||
"""获取函数调用工具。包含了本地加载的以及 MCP 服务的工具"""
|
"""获取函数调用工具。包含了本地加载的以及 MCP 服务的工具"""
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from functools import cmp_to_key
|
||||||
|
|
||||||
import aiohttp
|
import aiohttp
|
||||||
import psutil
|
import psutil
|
||||||
@@ -11,7 +14,9 @@ from astrbot.core.config import VERSION
|
|||||||
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
|
||||||
from astrbot.core.db import BaseDatabase
|
from astrbot.core.db import BaseDatabase
|
||||||
from astrbot.core.db.migration.helper import check_migration_needed_v4
|
from astrbot.core.db.migration.helper import check_migration_needed_v4
|
||||||
|
from astrbot.core.utils.astrbot_path import get_astrbot_path
|
||||||
from astrbot.core.utils.io import get_dashboard_version
|
from astrbot.core.utils.io import get_dashboard_version
|
||||||
|
from astrbot.core.utils.version_comparator import VersionComparator
|
||||||
|
|
||||||
from .route import Response, Route, RouteContext
|
from .route import Response, Route, RouteContext
|
||||||
|
|
||||||
@@ -30,6 +35,8 @@ class StatRoute(Route):
|
|||||||
"/stat/start-time": ("GET", self.get_start_time),
|
"/stat/start-time": ("GET", self.get_start_time),
|
||||||
"/stat/restart-core": ("POST", self.restart_core),
|
"/stat/restart-core": ("POST", self.restart_core),
|
||||||
"/stat/test-ghproxy-connection": ("POST", self.test_ghproxy_connection),
|
"/stat/test-ghproxy-connection": ("POST", self.test_ghproxy_connection),
|
||||||
|
"/stat/changelog": ("GET", self.get_changelog),
|
||||||
|
"/stat/changelog/list": ("GET", self.list_changelog_versions),
|
||||||
}
|
}
|
||||||
self.db_helper = db_helper
|
self.db_helper = db_helper
|
||||||
self.register_routes()
|
self.register_routes()
|
||||||
@@ -183,3 +190,92 @@ class StatRoute(Route):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return Response().error(f"Error: {e!s}").__dict__
|
return Response().error(f"Error: {e!s}").__dict__
|
||||||
|
|
||||||
|
async def get_changelog(self):
|
||||||
|
"""获取指定版本的更新日志"""
|
||||||
|
try:
|
||||||
|
version = request.args.get("version")
|
||||||
|
if not version:
|
||||||
|
return Response().error("version parameter is required").__dict__
|
||||||
|
|
||||||
|
version = version.lstrip("v")
|
||||||
|
|
||||||
|
# 防止路径遍历攻击
|
||||||
|
if not re.match(r"^[a-zA-Z0-9._-]+$", version):
|
||||||
|
return Response().error("Invalid version format").__dict__
|
||||||
|
if ".." in version or "/" in version or "\\" in version:
|
||||||
|
return Response().error("Invalid version format").__dict__
|
||||||
|
|
||||||
|
filename = f"v{version}.md"
|
||||||
|
project_path = get_astrbot_path()
|
||||||
|
changelogs_dir = os.path.join(project_path, "changelogs")
|
||||||
|
changelog_path = os.path.join(changelogs_dir, filename)
|
||||||
|
|
||||||
|
# 规范化路径,防止符号链接攻击
|
||||||
|
changelog_path = os.path.realpath(changelog_path)
|
||||||
|
changelogs_dir = os.path.realpath(changelogs_dir)
|
||||||
|
|
||||||
|
# 验证最终路径在预期的 changelogs 目录内(防止路径遍历)
|
||||||
|
# 确保规范化后的路径以 changelogs_dir 开头,且是目录内的文件
|
||||||
|
changelog_path_normalized = os.path.normpath(changelog_path)
|
||||||
|
changelogs_dir_normalized = os.path.normpath(changelogs_dir)
|
||||||
|
|
||||||
|
# 检查路径是否在预期目录内(必须是目录的子文件,不能是目录本身)
|
||||||
|
expected_prefix = changelogs_dir_normalized + os.sep
|
||||||
|
if not changelog_path_normalized.startswith(expected_prefix):
|
||||||
|
logger.warning(
|
||||||
|
f"Path traversal attempt detected: {version} -> {changelog_path}",
|
||||||
|
)
|
||||||
|
return Response().error("Invalid version format").__dict__
|
||||||
|
|
||||||
|
if not os.path.exists(changelog_path):
|
||||||
|
return (
|
||||||
|
Response()
|
||||||
|
.error(f"Changelog for version {version} not found")
|
||||||
|
.__dict__
|
||||||
|
)
|
||||||
|
if not os.path.isfile(changelog_path):
|
||||||
|
return (
|
||||||
|
Response()
|
||||||
|
.error(f"Changelog for version {version} not found")
|
||||||
|
.__dict__
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(changelog_path, encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
return Response().ok({"content": content, "version": version}).__dict__
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response().error(f"Error: {e!s}").__dict__
|
||||||
|
|
||||||
|
async def list_changelog_versions(self):
|
||||||
|
"""获取所有可用的更新日志版本列表"""
|
||||||
|
try:
|
||||||
|
project_path = get_astrbot_path()
|
||||||
|
changelogs_dir = os.path.join(project_path, "changelogs")
|
||||||
|
|
||||||
|
if not os.path.exists(changelogs_dir):
|
||||||
|
return Response().ok({"versions": []}).__dict__
|
||||||
|
|
||||||
|
versions = []
|
||||||
|
for filename in os.listdir(changelogs_dir):
|
||||||
|
if filename.endswith(".md") and filename.startswith("v"):
|
||||||
|
# 提取版本号(去除 v 前缀和 .md 后缀)
|
||||||
|
version = filename[1:-3] # 去掉 "v" 和 ".md"
|
||||||
|
# 验证版本号格式
|
||||||
|
if re.match(r"^[a-zA-Z0-9._-]+$", version):
|
||||||
|
versions.append(version)
|
||||||
|
|
||||||
|
# 按版本号排序(降序,最新的在前)
|
||||||
|
# 使用项目中的 VersionComparator 进行语义化版本号排序
|
||||||
|
versions.sort(
|
||||||
|
key=cmp_to_key(
|
||||||
|
lambda v1, v2: VersionComparator.compare_version(v2, v1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return Response().ok({"versions": versions}).__dict__
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return Response().error(f"Error: {e!s}").__dict__
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
#!/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)
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
#!/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)
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
## What's Changed
|
||||||
|
|
||||||
|
> 📢 在升级前,请**完整阅读**本次更新日志。
|
||||||
|
>
|
||||||
|
> **特别提醒:**
|
||||||
|
> 1. 该版本为 alpha.1 预览版本。
|
||||||
|
> 2. 本次升级**如果再降级**,会由于提供商配置的变更,导致提供商配置错乱,需要手动删除后重新添加。
|
||||||
|
> 3. 此版本 WebUI 包体相较上一个版本增加约 **193%**,共约 **9.8 MB**,升级可能会需要一些时间。
|
||||||
|
|
||||||
|
### 重构与优化
|
||||||
|
|
||||||
|
- 重构 Provider 页面和提供商的配置结构,将 Chat Provider 配置拆分为 Provider Source(提供商源)和 Provider(代表提供商源的各个模型),引入了提供商模型自动发现、模型元数据自动发现的功能,**提供更加便捷的模型添加体验**。
|
||||||
|
- ⚠️ 将 “MCP” 页面移动到了 “插件” 页面中
|
||||||
|
- ⚠️ 将 “MCP” 页面中的工具管理移动到了 “插件” -> “管理行为” 中。
|
||||||
|
- ⚠️ 将 “QQ 个人号(OneBot v11)” 机器人适配器类型更名为 “OneBot v11”,并将其 Logo 更改为 OneBot 的 Logo。
|
||||||
|
- ⚠️ AstrBot WebChat 升级为 **AstrBot ChatUI**,入口从边栏修改为顶部(右上角)切换按钮。
|
||||||
|
- 优化引用消息的逻辑,减少对模型输入缓存的破坏。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- ‼️ 修复部分情况下,分段回复无法正常分段的问题。
|
||||||
|
- 修复处理工具返回结果的过程中,导致一些直接发送图片的工具(如生图工具)无法正确发送到用户的问题。
|
||||||
|
- 修复 WebChat 部分情况下,上一条消息文字内容增量到下一条消息的问题。
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 支持**指令管理**,设置指令别名、解决指令冲突、查看指令详情等。入口:“插件” -> “管理行为”。
|
||||||
|
- 支持 Google Gemini 3 系列引入的 [Thinking Level](https://ai.google.dev/gemini-api/docs/thinking#thinking-levels) 配置。
|
||||||
|
- 支持记录每条 LLM 消息的耗时、Token 使用量、TTFT 数据,以及每次 Agent Loop 的各种统计数据。
|
||||||
|
- AstrBot ChatUI 支持查看每条消息的 TTFT、Token 使用量数据。
|
||||||
|
- AstrBot ChatUI 支持显示每次工具调用的耗时、参数和响应。
|
||||||
|
- AstrBot ChatUI 支持渲染 Mermaid、LateX 内容,优化了 Code Block 的显示效果(使用 Monaco Editor),并减少 DOM 更新于内存占用。(Powered by [Simon-He95/markstream-vue](https://github.com/Simon-He95/markstream-vue))
|
||||||
|
- 支持查看 Changelog 历史版本更新日志。
|
||||||
|
- 🎄
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
## What's Changed
|
||||||
|
|
||||||
|
> 📢 在升级前,请**完整阅读**本次更新日志。
|
||||||
|
>
|
||||||
|
> **特别提醒:**
|
||||||
|
> 1. 该版本为 alpha.2 预览版本。
|
||||||
|
> 2. 本次升级**如果再降级**,会由于提供商配置的变更,导致提供商配置错乱,需要手动删除后重新添加。
|
||||||
|
> 3. 此版本 WebUI 包体相较上一个版本增加约 **193%**,共约 **9.8 MB**,升级可能会需要一些时间。
|
||||||
|
|
||||||
|
## alpha.1 -> alpha.2
|
||||||
|
|
||||||
|
- 修复:“对话数据”页对话轨迹详情显示异常的问题
|
||||||
|
- 优化:当 Agent 达到最大步数时的处理。在达到最大步数后,会移除所有请求中的 tools 并告知模型根据上下文进行最终总结。
|
||||||
|
- 优化:LLM tools 执行的错误处理,减少工具调用无限循环的问题。
|
||||||
|
- 优化:ChatUI 打开模型选择菜单时,会重新获取提供商配置。
|
||||||
|
- 优化:ChatUI 新建对话并发送消息后,对话列表页自动选中该对话。
|
||||||
|
|
||||||
|
## 4.10.0 变化
|
||||||
|
|
||||||
|
### 重构与优化
|
||||||
|
|
||||||
|
- 重构 Provider 页面和提供商的配置结构,将 Chat Provider 配置拆分为 Provider Source(提供商源)和 Provider(代表提供商源的各个模型),引入了提供商模型自动发现、模型元数据自动发现的功能,**提供更加便捷的模型添加体验**。
|
||||||
|
- ⚠️ 将 “MCP” 页面移动到了 “插件” 页面中
|
||||||
|
- ⚠️ 将 “MCP” 页面中的工具管理移动到了 “插件” -> “管理行为” 中。
|
||||||
|
- ⚠️ 将 “QQ 个人号(OneBot v11)” 机器人适配器类型更名为 “OneBot v11”,并将其 Logo 更改为 OneBot 的 Logo。
|
||||||
|
- ⚠️ AstrBot WebChat 升级为 **AstrBot ChatUI**,入口从边栏修改为顶部(右上角)切换按钮。
|
||||||
|
- 优化引用消息的逻辑,减少对模型输入缓存的破坏。
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- ‼️ 修复部分情况下,分段回复无法正常分段的问题。
|
||||||
|
- 修复处理工具返回结果的过程中,导致一些直接发送图片的工具(如生图工具)无法正确发送到用户的问题。
|
||||||
|
- 修复 WebChat 部分情况下,上一条消息文字内容增量到下一条消息的问题。
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 支持**指令管理**,设置指令别名、解决指令冲突、查看指令详情等。入口:“插件” -> “管理行为”。
|
||||||
|
- 支持 Google Gemini 3 系列引入的 [Thinking Level](https://ai.google.dev/gemini-api/docs/thinking#thinking-levels) 配置。
|
||||||
|
- 支持记录每条 LLM 消息的耗时、Token 使用量、TTFT 数据,以及每次 Agent Loop 的各种统计数据。
|
||||||
|
- AstrBot ChatUI 支持查看每条消息的 TTFT、Token 使用量数据。
|
||||||
|
- AstrBot ChatUI 支持显示每次工具调用的耗时、参数和响应。
|
||||||
|
- AstrBot ChatUI 支持渲染 Mermaid、LateX 内容,优化了 Code Block 的显示效果(使用 Monaco Editor),并减少 DOM 更新于内存占用。(Powered by [Simon-He95/markstream-vue](https://github.com/Simon-He95/markstream-vue))
|
||||||
|
- 支持查看 Changelog 历史版本更新日志。
|
||||||
|
- 🎄
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
## What's Changed
|
||||||
|
|
||||||
|
> 📢 在升级前,请**完整阅读**本次更新日志。
|
||||||
|
>
|
||||||
|
> **特别提醒:**
|
||||||
|
> 1. 本次升级**如果再降级**,会由于提供商配置的变更,导致提供商配置错乱,需要手动删除后重新添加。
|
||||||
|
> 2. 此版本 WebUI 包体相较上一个版本增加约 **193%**,共约 **9.8 MB**,升级可能会需要一些时间。
|
||||||
|
> 3. **升级后请务必确保 WebUI 和 AstrBot Core 版本一致**,否则会产生预期之外的情况。(判断方法:日志中出现 `WebUI 版本已是最新。` 即为一致的版本,`检测到 WebUI 版本 (xxx) 与当前 AstrBot 版本 (xxx) 不符。` 即为不一致的版本。此版本的判断方法也可通查看 WebUI 右上角是否出现 Bot / Chat 的切换按钮控件来判断是否是新版本的 WebUI)。
|
||||||
|
> 4. 如果有任何问题请提交 [Issue](https://github.com/AstrBotDevs/AstrBot/issues) 并附带 `v4.10.0` tag。
|
||||||
|
|
||||||
|
### 重构与优化
|
||||||
|
|
||||||
|
- 重构 Provider 页面和提供商的配置结构,将 Chat Provider 配置拆分为 Provider Source(提供商源)和 Provider(代表提供商源的各个模型),引入了提供商模型自动发现、模型元数据自动发现的功能,**提供更加便捷的模型添加体验**。
|
||||||
|
- ⚠️ 将 “MCP” 页面移动到了 “插件” 页面中
|
||||||
|
- ⚠️ 将 “MCP” 页面中的工具管理移动到了 “插件” -> “管理行为” 中。
|
||||||
|
- ⚠️ 将 “QQ 个人号(OneBot v11)” 机器人适配器类型更名为 “OneBot v11”,并将其 Logo 更改为 OneBot 的 Logo。
|
||||||
|
- ⚠️ AstrBot WebChat 升级为 **AstrBot ChatUI**,入口从边栏修改为顶部(右上角)切换按钮。
|
||||||
|
- 优化引用消息的逻辑,减少对模型输入缓存的破坏。
|
||||||
|
- 优化当 Agent 达到最大步数时的处理。在达到最大步数后,会移除所有请求中的 tools 并告知模型根据上下文进行最终总结。
|
||||||
|
- 优化 LLM tools 执行的错误处理,减少工具调用无限循环的问题。
|
||||||
|
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- ‼️ 修复部分情况下,分段回复无法正常分段的问题。
|
||||||
|
- 修复处理工具返回结果的过程中,导致一些直接发送图片的工具(如生图工具)无法正确发送到用户的问题。
|
||||||
|
- 修复 WebChat 部分情况下,上一条消息文字内容增量到下一条消息的问题。
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 支持**指令管理**,设置指令别名、解决指令冲突、查看指令详情等。入口:“插件” -> “管理行为”。
|
||||||
|
- 支持 Google Gemini 3 系列引入的 [Thinking Level](https://ai.google.dev/gemini-api/docs/thinking#thinking-levels) 配置。
|
||||||
|
- 支持记录每条 LLM 消息的耗时、Token 使用量、TTFT 数据,以及每次 Agent Loop 的各种统计数据。
|
||||||
|
- AstrBot ChatUI 支持查看每条消息的 TTFT、Token 使用量数据。
|
||||||
|
- AstrBot ChatUI 支持显示每次工具调用的耗时、参数和响应。
|
||||||
|
- AstrBot ChatUI 支持渲染 Mermaid、LateX 内容,优化了 Code Block 的显示效果(使用 Monaco Editor),并减少 DOM 更新于内存占用。(Powered by [Simon-He95/markstream-vue](https://github.com/Simon-He95/markstream-vue))
|
||||||
|
- 支持查看 Changelog 历史版本更新日志。
|
||||||
|
- 🎄
|
||||||
|
|
||||||
|
Merry Christmas!
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
## What's Changed
|
||||||
|
|
||||||
|
> 📢 在升级前,请**完整阅读**本次更新日志。
|
||||||
|
>
|
||||||
|
> **特别提醒:**
|
||||||
|
> 1. 本次升级**如果再降级**,会由于提供商配置的变更,导致提供商配置错乱,需要手动删除后重新添加。
|
||||||
|
> 2. 此版本 WebUI 包体相较上一个版本增加约 **193%**,共约 **9.8 MB**,升级可能会需要一些时间。
|
||||||
|
> 3. **升级后请务必确保 WebUI 和 AstrBot Core 版本一致**,否则会产生预期之外的情况。(判断方法:日志中出现 `WebUI 版本已是最新。` 即为一致的版本,`检测到 WebUI 版本 (xxx) 与当前 AstrBot 版本 (xxx) 不符。` 即为不一致的版本。此版本的判断方法也可通查看 WebUI 右上角是否出现 Bot / Chat 的切换按钮控件来判断是否是新版本的 WebUI)。
|
||||||
|
> 4. 如果有任何问题请提交 [Issue](https://github.com/AstrBotDevs/AstrBot/issues) 并附带 `v4.10.0` tag。
|
||||||
|
|
||||||
|
## 4.10.0 -> 4.10.1
|
||||||
|
|
||||||
|
- fix(core): 修复极少数情况下由于指令管理导致的 AstrBot 启动失败的问题
|
||||||
|
- fix(core): 修复当提供商源带有斜杠(“/”)时,无法删除 / 更新提供商源的问题(报错 405)
|
||||||
|
- perf(core): 优化 OneBot 适配器的消息段解析逻辑,修复部分情况下无法正确解析消息段的问题
|
||||||
|
|
||||||
|
### 重构与优化
|
||||||
|
|
||||||
|
- 重构 Provider 页面和提供商的配置结构,将 Chat Provider 配置拆分为 Provider Source(提供商源)和 Provider(代表提供商源的各个模型),引入了提供商模型自动发现、模型元数据自动发现的功能,**提供更加便捷的模型添加体验**。
|
||||||
|
- ⚠️ 将 “MCP” 页面移动到了 “插件” 页面中
|
||||||
|
- ⚠️ 将 “MCP” 页面中的工具管理移动到了 “插件” -> “管理行为” 中。
|
||||||
|
- ⚠️ 将 “QQ 个人号(OneBot v11)” 机器人适配器类型更名为 “OneBot v11”,并将其 Logo 更改为 OneBot 的 Logo。
|
||||||
|
- ⚠️ AstrBot WebChat 升级为 **AstrBot ChatUI**,入口从边栏修改为顶部(右上角)切换按钮。
|
||||||
|
- 优化引用消息的逻辑,减少对模型输入缓存的破坏。
|
||||||
|
- 优化当 Agent 达到最大步数时的处理。在达到最大步数后,会移除所有请求中的 tools 并告知模型根据上下文进行最终总结。
|
||||||
|
- 优化 LLM tools 执行的错误处理,减少工具调用无限循环的问题。
|
||||||
|
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- ‼️ 修复部分情况下,分段回复无法正常分段的问题。
|
||||||
|
- 修复处理工具返回结果的过程中,导致一些直接发送图片的工具(如生图工具)无法正确发送到用户的问题。
|
||||||
|
- 修复 WebChat 部分情况下,上一条消息文字内容增量到下一条消息的问题。
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 支持**指令管理**,设置指令别名、解决指令冲突、查看指令详情等。入口:“插件” -> “管理行为”。
|
||||||
|
- 支持 Google Gemini 3 系列引入的 [Thinking Level](https://ai.google.dev/gemini-api/docs/thinking#thinking-levels) 配置。
|
||||||
|
- 支持记录每条 LLM 消息的耗时、Token 使用量、TTFT 数据,以及每次 Agent Loop 的各种统计数据。
|
||||||
|
- AstrBot ChatUI 支持查看每条消息的 TTFT、Token 使用量数据。
|
||||||
|
- AstrBot ChatUI 支持显示每次工具调用的耗时、参数和响应。
|
||||||
|
- AstrBot ChatUI 支持渲染 Mermaid、LateX 内容,优化了 Code Block 的显示效果(使用 Monaco Editor),并减少 DOM 更新于内存占用。(Powered by [Simon-He95/markstream-vue](https://github.com/Simon-He95/markstream-vue))
|
||||||
|
- 支持查看 Changelog 历史版本更新日志。
|
||||||
|
- 🎄
|
||||||
|
|
||||||
|
Merry Christmas!
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
1. ‼️‼️ 修复了由 `psutil` 新版本导致的启动时报错的问题。
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
1. 插件指令管理支持管理别名。
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<meta name="description" content="AstrBot Dashboard" />
|
<meta name="description" content="AstrBot Dashboard" />
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Outfit&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
|
||||||
/>
|
/>
|
||||||
<title>AstrBot - 仪表盘</title>
|
<title>AstrBot - 仪表盘</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -10,30 +10,30 @@
|
|||||||
"build-prod": "vue-tsc --noEmit && vite build --base=/vue/free/",
|
"build-prod": "vue-tsc --noEmit && vite build --base=/vue/free/",
|
||||||
"preview": "vite preview --port 5050",
|
"preview": "vite preview --port 5050",
|
||||||
"typecheck": "vue-tsc --noEmit",
|
"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": {
|
"dependencies": {
|
||||||
"@guolao/vue-monaco-editor": "^1.5.4",
|
"@guolao/vue-monaco-editor": "^1.5.4",
|
||||||
"@tauri-apps/api": "^2.9.0",
|
"@mdit/plugin-katex": "^0.24.1",
|
||||||
"@tiptap/starter-kit": "2.1.7",
|
"@tiptap/starter-kit": "2.1.7",
|
||||||
"@tiptap/vue-3": "2.1.7",
|
"@tiptap/vue-3": "2.1.7",
|
||||||
"apexcharts": "3.42.0",
|
"apexcharts": "3.42.0",
|
||||||
"axios": ">=1.6.2 <1.10.0 || >1.10.0 <2.0.0",
|
"axios": ">=1.6.2 <1.10.0 || >1.10.0 <2.0.0",
|
||||||
"axios-mock-adapter": "^1.22.0",
|
"axios-mock-adapter": "^1.22.0",
|
||||||
"chance": "1.1.11",
|
"chance": "1.1.11",
|
||||||
"d3": "^7.9.0",
|
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"js-md5": "^0.8.3",
|
"js-md5": "^0.8.3",
|
||||||
|
"katex": "^0.16.27",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"marked": "^15.0.7",
|
"markstream-vue": "0.0.3-beta.7",
|
||||||
"markdown-it": "^14.1.0",
|
"mermaid": "^11.12.2",
|
||||||
"pinyin-pro": "^3.26.0",
|
|
||||||
"pinia": "2.1.6",
|
"pinia": "2.1.6",
|
||||||
|
"pinyin-pro": "^3.26.0",
|
||||||
"remixicon": "3.5.0",
|
"remixicon": "3.5.0",
|
||||||
|
"shiki": "^3.20.0",
|
||||||
|
"stream-markdown": "^0.0.11",
|
||||||
|
"stream-monaco": "^0.0.8",
|
||||||
"vee-validate": "4.11.3",
|
"vee-validate": "4.11.3",
|
||||||
"vite-plugin-vuetify": "1.0.2",
|
"vite-plugin-vuetify": "1.0.2",
|
||||||
"vue": "3.3.4",
|
"vue": "3.3.4",
|
||||||
@@ -47,9 +47,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mdi/font": "7.2.96",
|
"@mdi/font": "7.2.96",
|
||||||
"@rushstack/eslint-patch": "1.3.3",
|
"@rushstack/eslint-patch": "1.3.3",
|
||||||
"@tauri-apps/cli": "^2.9.4",
|
|
||||||
"@types/chance": "1.1.3",
|
"@types/chance": "1.1.3",
|
||||||
"@types/markdown-it": "^14.1.2",
|
|
||||||
"@types/node": "^20.5.7",
|
"@types/node": "^20.5.7",
|
||||||
"@vitejs/plugin-vue": "4.3.3",
|
"@vitejs/plugin-vue": "4.3.3",
|
||||||
"@vue/eslint-config-prettier": "8.0.0",
|
"@vue/eslint-config-prettier": "8.0.0",
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
# Tauri specific
|
|
||||||
src-tauri/target/
|
|
||||||
src-tauri/WixTools/
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
[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" ]
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
tauri_build::build()
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
|
Before Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 3.5 KiB |
|
Before Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,5 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
@@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#fff</color>
|
|
||||||
</resources>
|
|
||||||
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 602 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 8.7 KiB |
@@ -1,104 +0,0 @@
|
|||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
{
|
|
||||||
"$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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||