Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61d4f1fd4b | ||
|
|
121b68995e | ||
|
|
d11f1d8dae | ||
|
|
c0ef2b5064 | ||
|
|
2a7308363e | ||
|
|
dc0c556f96 | ||
|
|
ba2ee1c0aa | ||
|
|
1e5f243edb | ||
|
|
e5eab2af34 | ||
|
|
c10973e160 | ||
|
|
b1e4bff3ec | ||
|
|
c1202cda63 | ||
|
|
32d6cd7776 | ||
|
|
2f78d30e93 | ||
|
|
33407c9f0d | ||
|
|
98d8eaee02 | ||
|
|
10b9228060 | ||
|
|
5872f1e017 | ||
|
|
29845fcc4c | ||
|
|
cbe94b84fc | ||
|
|
94f0419ef7 | ||
|
|
49def5d883 | ||
|
|
6975525b70 | ||
|
|
90cb5a1951 | ||
|
|
b71000e2f3 | ||
|
|
02a9769b35 | ||
|
|
7640f11bfc | ||
|
|
9fa44dbcfa | ||
|
|
2cae941bae | ||
|
|
bc0784f41d | ||
|
|
c57d75e01a | ||
|
|
73edeae013 | ||
|
|
7d46314dc8 | ||
|
|
d5a53a89eb | ||
|
|
a85bc510dd | ||
|
|
2beea7d218 | ||
|
|
a93cd3dd5f | ||
|
|
db4d02c2e2 | ||
|
|
fd7811402b | ||
|
|
eb0325e627 | ||
|
|
8b4b04ec09 | ||
|
|
9f32c9280f | ||
|
|
4fcd09cfa8 |
31
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md
vendored
31
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md
vendored
@@ -1,31 +0,0 @@
|
||||
---
|
||||
name: '🥳 发布插件'
|
||||
title: "[Plugin] 插件名"
|
||||
about: 提交插件到插件市场
|
||||
labels: [ "plugin-publish" ]
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
欢迎发布插件到插件市场!
|
||||
|
||||
## 插件基本信息
|
||||
|
||||
请将插件信息填写到下方的 Json 代码块中。`tags`(插件标签)和 `social_link`(社交链接)选填。
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "插件名",
|
||||
"desc": "插件介绍",
|
||||
"author": "作者名",
|
||||
"repo": "插件仓库链接",
|
||||
"tags": [],
|
||||
"social_link": ""
|
||||
}
|
||||
```
|
||||
|
||||
## 检查
|
||||
|
||||
- [ ] 我的插件经过完整的测试
|
||||
- [ ] 我的插件不包含恶意代码
|
||||
- [ ] 我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
|
||||
56
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: 🥳 发布插件
|
||||
description: 提交插件到插件市场
|
||||
title: "[Plugin] 插件名"
|
||||
labels: ["plugin-publish"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
欢迎发布插件到插件市场!
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 插件基本信息
|
||||
|
||||
请将插件信息填写到下方的 JSON 代码块中。其中 `tags`(插件标签)和 `social_link`(社交链接)选填。
|
||||
|
||||
不熟悉 JSON ?现在可以从 [这里](https://plugins.astrbot.app/#/submit) 获取你的 JSON 啦!获取到了记得复制粘贴过来哦!
|
||||
|
||||
- type: textarea
|
||||
id: plugin-info
|
||||
attributes:
|
||||
label: 插件信息
|
||||
description: 请在下方代码块中填写您的插件信息,确保反引号包裹了JSON
|
||||
value: |
|
||||
```json
|
||||
{
|
||||
"name": "插件名",
|
||||
"desc": "插件介绍",
|
||||
"author": "作者名",
|
||||
"repo": "插件仓库链接",
|
||||
"tags": [],
|
||||
"social_link": ""
|
||||
}
|
||||
```
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## 检查
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: 插件检查清单
|
||||
description: 请确认以下所有项目
|
||||
options:
|
||||
- label: 我的插件经过完整的测试
|
||||
required: true
|
||||
- label: 我的插件不包含恶意代码
|
||||
required: true
|
||||
- label: 我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
|
||||
required: true
|
||||
63
.github/copilot-instructions.md
vendored
Normal file
63
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
# AstrBot Development Instructions
|
||||
|
||||
AstrBot is a multi-platform LLM chatbot and development framework written in Python with a Vue.js dashboard. It supports multiple messaging platforms (QQ, Telegram, Discord, etc.) and various LLM providers (OpenAI, Anthropic, Google Gemini, etc.).
|
||||
|
||||
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
|
||||
|
||||
## Working Effectively
|
||||
|
||||
### Bootstrap and Install Dependencies
|
||||
- **Python 3.10+ required** - Check `.python-version` file
|
||||
- Install UV package manager: `pip install uv`
|
||||
- Install project dependencies: `uv sync` -- takes 6-7 minutes. NEVER CANCEL. Set timeout to 10+ minutes.
|
||||
- Create required directories: `mkdir -p data/plugins data/config data/temp`
|
||||
|
||||
### Running the Application
|
||||
- Run main application: `uv run main.py` -- starts in ~3 seconds
|
||||
- Application creates WebUI on http://localhost:6185 (default credentials: `astrbot`/`astrbot`)
|
||||
- Application loads plugins automatically from `packages/` and `data/plugins/` directories
|
||||
|
||||
### Dashboard Build (Vue.js/Node.js)
|
||||
- **Prerequisites**: Node.js 20+ and npm 10+ required
|
||||
- Navigate to dashboard: `cd dashboard`
|
||||
- Install dashboard dependencies: `npm install` -- takes 2-3 minutes. NEVER CANCEL. Set timeout to 5+ minutes.
|
||||
- Build dashboard: `npm run build` -- takes 25-30 seconds. NEVER CANCEL.
|
||||
- Dashboard creates optimized production build in `dashboard/dist/`
|
||||
|
||||
### Testing
|
||||
- Do not generate test files for now.
|
||||
|
||||
### Code Quality and Linting
|
||||
- Install ruff linter: `uv add --dev ruff`
|
||||
- Check code style: `uv run ruff check .` -- takes <1 second
|
||||
- Check formatting: `uv run ruff format --check .` -- takes <1 second
|
||||
- Fix formatting: `uv run ruff format .`
|
||||
- **ALWAYS** run `uv run ruff check .` and `uv run ruff format .` before committing changes
|
||||
|
||||
### Plugin Development
|
||||
- Plugins load from `packages/` (built-in) and `data/plugins/` (user-installed)
|
||||
- Plugin system supports function tools and message handlers
|
||||
- Key plugins: python_interpreter, web_searcher, astrbot, reminder, session_controller
|
||||
|
||||
### Common Issues and Workarounds
|
||||
- **Dashboard download fails**: Known issue with "division by zero" error - application still works
|
||||
- **Import errors in tests**: Ensure `uv run` is used to run tests in proper environment
|
||||
=- **Build timeouts**: Always set appropriate timeouts (10+ minutes for uv sync, 5+ minutes for npm install)
|
||||
|
||||
## CI/CD Integration
|
||||
- GitHub Actions workflows in `.github/workflows/`
|
||||
- Docker builds supported via `Dockerfile`
|
||||
- Pre-commit hooks enforce ruff formatting and linting
|
||||
|
||||
## Docker Support
|
||||
- Primary deployment method: `docker run soulter/astrbot:latest`
|
||||
- Compose file available: `compose.yml`
|
||||
- Exposes ports: 6185 (WebUI), 6195 (WeChat), 6199 (QQ), etc.
|
||||
- Volume mount required: `./data:/AstrBot/data`
|
||||
|
||||
## Multi-language Support
|
||||
- Documentation in Chinese (README.md), English (README_en.md), Japanese (README_ja.md)
|
||||
- UI supports internationalization
|
||||
- Default language is Chinese
|
||||
|
||||
Remember: This is a production chatbot framework with real users. Always test thoroughly and ensure changes don't break existing functionality.
|
||||
4
.github/workflows/auto_release.yml
vendored
4
.github/workflows/auto_release.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Dashboard Build
|
||||
run: |
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
needs: build-and-publish-to-github-release
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
2
.github/workflows/coverage_test.yml
vendored
2
.github/workflows/coverage_test.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
14
.github/workflows/dashboard_ci.yml
vendored
14
.github/workflows/dashboard_ci.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: npm install, build
|
||||
run: |
|
||||
@@ -25,6 +25,8 @@ jobs:
|
||||
echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||
mkdir -p dashboard/dist/assets
|
||||
echo $COMMIT_SHA > dashboard/dist/assets/version
|
||||
cd dashboard
|
||||
zip -r dist.zip dist
|
||||
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -33,3 +35,13 @@ jobs:
|
||||
path: |
|
||||
dashboard/dist
|
||||
!dist/**/*.md
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: release-${{ github.sha }}
|
||||
owner: AstrBotDevs
|
||||
repo: astrbot-release-harbour
|
||||
body: "Automated release from commit ${{ github.sha }}"
|
||||
token: ${{ secrets.ASTRBOT_HARBOUR_TOKEN }}
|
||||
artifacts: "dashboard/dist.zip"
|
||||
13
.github/workflows/docker-image.yml
vendored
13
.github/workflows/docker-image.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Pull The Codes
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0 # Must be 0 so we can fetch tags
|
||||
|
||||
@@ -27,6 +27,17 @@ jobs:
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: git checkout ${{ steps.get-latest-tag.outputs.latest_tag }}
|
||||
|
||||
- name: Build Dashboard
|
||||
run: |
|
||||
cd dashboard
|
||||
npm install
|
||||
npm run build
|
||||
mkdir -p dist/assets
|
||||
echo $(git rev-parse HEAD) > dist/assets/version
|
||||
cd ..
|
||||
mkdir -p data
|
||||
cp -r dashboard/dist data/
|
||||
|
||||
- name: Set QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
|
||||
89
README.md
89
README.md
@@ -1,4 +1,4 @@
|
||||
<p align="center">
|
||||
<img width="430" height="31" alt="image" src="https://github.com/user-attachments/assets/474c822c-fab7-41be-8c23-6dae252823ed" /><p align="center">
|
||||
|
||||

|
||||
|
||||
@@ -25,59 +25,52 @@ _✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
|
||||
<a href="https://github.com/Soulter/AstrBot/issues">问题提交</a>
|
||||
</div>
|
||||
|
||||
AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用的插件系统和完善的大语言模型(LLM)接入功能的聊天机器人及开发框架。
|
||||
|
||||
|
||||
<!-- [](https://codecov.io/gh/Soulter/AstrBot)
|
||||
-->
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> 请务必修改默认密码以及保证 AstrBot 版本 >= 3.5.13。
|
||||
|
||||
## ✨ 近期更新
|
||||
|
||||
<details><summary>1. AstrBot 现已自带知识库能力</summary>
|
||||
|
||||
📚 详见[文档](https://astrbot.app/use/knowledge-base.html)
|
||||
|
||||

|
||||
|
||||
</details>
|
||||
|
||||
2. AstrBot 现已支持接入 [MCP](https://modelcontextprotocol.io/) 服务器!
|
||||
AstrBot 是一个开源的一站式 Agentic 聊天机器人平台及开发框架。
|
||||
|
||||
## ✨ 主要功能
|
||||
|
||||
> [!NOTE]
|
||||
> 🪧 我们正基于前沿科研成果,设计并实现适用于角色扮演和情感陪伴的长短期记忆模型及情绪控制模型,旨在提升对话的真实性与情感表达能力。敬请期待 `v3.6.0` 版本!
|
||||
|
||||
1. **大语言模型对话**。支持各种大语言模型,包括 OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM 等,支持接入本地部署的大模型,通过 Ollama、LLMTuner。具有多轮对话、人格情境、多模态能力,支持图片理解、语音转文字(Whisper)。
|
||||
2. **多消息平台接入**。支持接入 QQ(OneBot、QQ 官方机器人平台)、QQ 频道、企业微信、微信公众号、飞书、Telegram、钉钉、Discord、KOOK、VoceChat。支持速率限制、白名单、关键词过滤、百度内容审核。
|
||||
3. **Agent**。原生支持部分 Agent 能力,如代码执行器、自然语言待办、网页搜索。对接 [Dify 平台](https://dify.ai/),便捷接入 Dify 智能助手、知识库和 Dify 工作流。
|
||||
4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,极简开发。已支持安装多个插件。
|
||||
5. **可视化管理面板**。支持可视化修改配置、插件管理、日志查看等功能,降低配置难度。集成 WebChat,可在面板上与大模型对话。
|
||||
6. **高稳定性、高模块化**。基于事件总线和流水线的架构设计,高度模块化,低耦合。
|
||||
|
||||
> [!TIP]
|
||||
> WebUI 在线体验 Demo: [https://demo.astrbot.app/](https://demo.astrbot.app/)
|
||||
>
|
||||
> 用户名: `astrbot`, 密码: `astrbot`。
|
||||
1. **大模型对话**。支持接入多种大模型服务。支持多模态、工具调用、MCP、原生知识库、人设等功能。
|
||||
2. **多消息平台支持**。支持接入 QQ、企业微信、微信公众号、飞书、Telegram、钉钉、Discord、KOOK 等平台。支持速率限制、白名单、百度内容审核。
|
||||
3. **Agent**。完善适配的 Agentic 能力。支持多轮工具调用、内置沙盒代码执行器、网页搜索等功能。
|
||||
4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,社区插件生态丰富。
|
||||
5. **WebUI**。可视化配置和管理机器人,功能齐全。
|
||||
|
||||
## ✨ 使用方式
|
||||
|
||||
#### Docker 部署
|
||||
|
||||
推荐使用 Docker / Docker Compose 方式部署 AstrBot。
|
||||
|
||||
请参阅官方文档 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) 。
|
||||
|
||||
#### 宝塔面板部署
|
||||
|
||||
AstrBot 与宝塔面板合作,已上架至宝塔面板。
|
||||
|
||||
请参阅官方文档 [宝塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.html) 。
|
||||
|
||||
#### 1Panel 部署
|
||||
|
||||
AstrBot 已由 1Panel 官方上架至 1Panel 面板。
|
||||
|
||||
请参阅官方文档 [1Panel 部署](https://astrbot.app/deploy/astrbot/1panel.html) 。
|
||||
|
||||
#### 在 雨云 上部署
|
||||
|
||||
AstrBot 已由雨云官方上架至云应用平台,可一键部署。
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||
|
||||
#### 在 Replit 上部署
|
||||
|
||||
社区贡献的部署方式。
|
||||
|
||||
[](https://repl.it/github/Soulter/AstrBot)
|
||||
|
||||
#### Windows 一键安装器部署
|
||||
|
||||
请参阅官方文档 [使用 Windows 一键安装器部署 AstrBot](https://astrbot.app/deploy/astrbot/windows.html) 。
|
||||
|
||||
#### 宝塔面板部署
|
||||
|
||||
请参阅官方文档 [宝塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.html) 。
|
||||
|
||||
#### CasaOS 部署
|
||||
|
||||
社区贡献的部署方式。
|
||||
@@ -101,24 +94,8 @@ git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
或者,直接通过 uvx 安装 AstrBot:
|
||||
|
||||
```bash
|
||||
mkdir astrbot && cd astrbot
|
||||
uvx astrbot init
|
||||
# uvx astrbot run
|
||||
```
|
||||
|
||||
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
|
||||
|
||||
#### 在 Replit 上部署
|
||||
|
||||
[](https://repl.it/github/Soulter/AstrBot)
|
||||
|
||||
#### 在 雨云 上部署
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||
|
||||
## ⚡ 消息平台支持情况
|
||||
|
||||
| 平台 | 支持性 |
|
||||
|
||||
@@ -37,7 +37,10 @@ async def check_dashboard(astrbot_root: Path) -> None:
|
||||
):
|
||||
click.echo("正在安装管理面板...")
|
||||
await download_dashboard(
|
||||
path="data/dashboard.zip", extract_path=str(astrbot_root)
|
||||
path="data/dashboard.zip",
|
||||
extract_path=str(astrbot_root),
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
)
|
||||
click.echo("管理面板安装完成")
|
||||
|
||||
@@ -50,7 +53,10 @@ async def check_dashboard(astrbot_root: Path) -> None:
|
||||
version = dashboard_version.split("v")[1]
|
||||
click.echo(f"管理面板版本: {version}")
|
||||
await download_dashboard(
|
||||
path="data/dashboard.zip", extract_path=str(astrbot_root)
|
||||
path="data/dashboard.zip",
|
||||
extract_path=str(astrbot_root),
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
)
|
||||
except Exception as e:
|
||||
click.echo(f"下载管理面板失败: {e}")
|
||||
@@ -59,7 +65,10 @@ async def check_dashboard(astrbot_root: Path) -> None:
|
||||
click.echo("初始化管理面板目录...")
|
||||
try:
|
||||
await download_dashboard(
|
||||
path=str(astrbot_root / "dashboard.zip"), extract_path=str(astrbot_root)
|
||||
path=str(astrbot_root / "dashboard.zip"),
|
||||
extract_path=str(astrbot_root),
|
||||
version=f"v{VERSION}",
|
||||
latest=False,
|
||||
)
|
||||
click.echo("管理面板初始化完成")
|
||||
except Exception as e:
|
||||
|
||||
@@ -117,6 +117,9 @@ def build_plug_list(plugins_dir: Path) -> list:
|
||||
# 从 metadata.yaml 加载元数据
|
||||
metadata = load_yaml_metadata(plugin_dir)
|
||||
|
||||
if "desc" not in metadata and "description" in metadata:
|
||||
metadata["desc"] = metadata["description"]
|
||||
|
||||
# 如果成功加载元数据,添加到结果列表
|
||||
if metadata and all(
|
||||
k in metadata for k in ["name", "desc", "version", "author", "repo"]
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "3.5.23"
|
||||
VERSION = "3.5.27"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -65,6 +65,7 @@ DEFAULT_CONFIG = {
|
||||
"show_tool_use_status": False,
|
||||
"streaming_segmented": False,
|
||||
"separate_provider": True,
|
||||
"max_agent_step": 30,
|
||||
},
|
||||
"provider_stt_settings": {
|
||||
"enable": False,
|
||||
@@ -102,6 +103,7 @@ DEFAULT_CONFIG = {
|
||||
"t2i_endpoint": "",
|
||||
"t2i_use_file_service": False,
|
||||
"http_proxy": "",
|
||||
"no_proxy": ["localhost", "127.0.0.1", "::1"],
|
||||
"dashboard": {
|
||||
"enable": True,
|
||||
"username": "astrbot",
|
||||
@@ -597,9 +599,8 @@ CONFIG_METADATA_2 = {
|
||||
"key": [],
|
||||
"api_base": "https://api.openai.com/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "gpt-4o-mini",
|
||||
},
|
||||
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
|
||||
"hint": "也兼容所有与OpenAI API兼容的服务。",
|
||||
},
|
||||
"Azure OpenAI": {
|
||||
"id": "azure",
|
||||
@@ -611,9 +612,7 @@ CONFIG_METADATA_2 = {
|
||||
"key": [],
|
||||
"api_base": "",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "gpt-4o-mini",
|
||||
},
|
||||
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
|
||||
},
|
||||
"xAI": {
|
||||
"id": "xai",
|
||||
@@ -624,11 +623,10 @@ CONFIG_METADATA_2 = {
|
||||
"key": [],
|
||||
"api_base": "https://api.x.ai/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "grok-2-latest",
|
||||
},
|
||||
"model_config": {"model": "grok-2-latest", "temperature": 0.4},
|
||||
},
|
||||
"Anthropic": {
|
||||
"hint": "注意Claude系列模型的温度调节范围为0到1.0,超出可能导致报错",
|
||||
"id": "claude",
|
||||
"provider": "anthropic",
|
||||
"type": "anthropic_chat_completion",
|
||||
@@ -640,9 +638,11 @@ CONFIG_METADATA_2 = {
|
||||
"model_config": {
|
||||
"model": "claude-3-5-sonnet-latest",
|
||||
"max_tokens": 4096,
|
||||
"temperature": 0.2,
|
||||
},
|
||||
},
|
||||
"Ollama": {
|
||||
"hint": "启用前请确保已正确安装并运行 Ollama 服务端,Ollama默认不带鉴权,无需修改key",
|
||||
"id": "ollama_default",
|
||||
"provider": "ollama",
|
||||
"type": "openai_chat_completion",
|
||||
@@ -650,9 +650,7 @@ CONFIG_METADATA_2 = {
|
||||
"enable": True,
|
||||
"key": ["ollama"], # ollama 的 key 默认是 ollama
|
||||
"api_base": "http://localhost:11434/v1",
|
||||
"model_config": {
|
||||
"model": "llama3.1-8b",
|
||||
},
|
||||
"model_config": {"model": "llama3.1-8b", "temperature": 0.4},
|
||||
},
|
||||
"LM Studio": {
|
||||
"id": "lm_studio",
|
||||
@@ -677,6 +675,7 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "gemini-1.5-flash",
|
||||
"temperature": 0.4,
|
||||
},
|
||||
},
|
||||
"Gemini": {
|
||||
@@ -690,6 +689,7 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "gemini-2.0-flash-exp",
|
||||
"temperature": 0.4,
|
||||
},
|
||||
"gm_resp_image_modal": False,
|
||||
"gm_native_search": False,
|
||||
@@ -714,9 +714,7 @@ CONFIG_METADATA_2 = {
|
||||
"key": [],
|
||||
"api_base": "https://api.deepseek.com/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "deepseek-chat",
|
||||
},
|
||||
"model_config": {"model": "deepseek-chat", "temperature": 0.4},
|
||||
},
|
||||
"302.AI": {
|
||||
"id": "302ai",
|
||||
@@ -727,9 +725,7 @@ CONFIG_METADATA_2 = {
|
||||
"key": [],
|
||||
"api_base": "https://api.302.ai/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "gpt-4.1-mini",
|
||||
},
|
||||
"model_config": {"model": "gpt-4.1-mini", "temperature": 0.4},
|
||||
},
|
||||
"硅基流动": {
|
||||
"id": "siliconflow",
|
||||
@@ -742,6 +738,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "https://api.siliconflow.cn/v1",
|
||||
"model_config": {
|
||||
"model": "deepseek-ai/DeepSeek-V3",
|
||||
"temperature": 0.4,
|
||||
},
|
||||
},
|
||||
"PPIO派欧云": {
|
||||
@@ -755,6 +752,20 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "deepseek/deepseek-r1",
|
||||
"temperature": 0.4,
|
||||
},
|
||||
},
|
||||
"优云智算": {
|
||||
"id": "compshare",
|
||||
"provider": "compshare",
|
||||
"type": "openai_chat_completion",
|
||||
"provider_type": "chat_completion",
|
||||
"enable": True,
|
||||
"key": [],
|
||||
"api_base": "https://api.modelverse.cn/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {
|
||||
"model": "moonshotai/Kimi-K2-Instruct",
|
||||
},
|
||||
},
|
||||
"Kimi": {
|
||||
@@ -766,9 +777,7 @@ CONFIG_METADATA_2 = {
|
||||
"key": [],
|
||||
"timeout": 120,
|
||||
"api_base": "https://api.moonshot.cn/v1",
|
||||
"model_config": {
|
||||
"model": "moonshot-v1-8k",
|
||||
},
|
||||
"model_config": {"model": "moonshot-v1-8k", "temperature": 0.4},
|
||||
},
|
||||
"智谱 AI": {
|
||||
"id": "zhipu_default",
|
||||
@@ -796,6 +805,7 @@ CONFIG_METADATA_2 = {
|
||||
"dify_query_input_key": "astrbot_text_query",
|
||||
"variables": {},
|
||||
"timeout": 60,
|
||||
"hint": "请确保你在 AstrBot 里设置的 APP 类型和 Dify 里面创建的应用的类型一致!",
|
||||
},
|
||||
"阿里云百炼应用": {
|
||||
"id": "dashscope",
|
||||
@@ -814,6 +824,17 @@ CONFIG_METADATA_2 = {
|
||||
"variables": {},
|
||||
"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},
|
||||
},
|
||||
"FastGPT": {
|
||||
"id": "fastgpt",
|
||||
"provider": "fastgpt",
|
||||
@@ -835,7 +856,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "whisper-1",
|
||||
},
|
||||
"Whisper(本地加载)": {
|
||||
"whisper_hint": "(不用修改我)",
|
||||
"hint": "启用前请 pip 安装 openai-whisper 库(N卡用户大约下载 2GB,主要是 torch 和 cuda,CPU 用户大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
|
||||
"provider": "openai",
|
||||
"type": "openai_whisper_selfhost",
|
||||
"provider_type": "speech_to_text",
|
||||
@@ -844,7 +865,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "tiny",
|
||||
},
|
||||
"SenseVoice(本地加载)": {
|
||||
"sensevoice_hint": "(不用修改我)",
|
||||
"hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库(默认使用CPU,大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
|
||||
"type": "sensevoice_stt_selfhost",
|
||||
"provider": "sensevoice",
|
||||
"provider_type": "speech_to_text",
|
||||
@@ -866,7 +887,7 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": "20",
|
||||
},
|
||||
"Edge TTS": {
|
||||
"edgetts_hint": "提示:使用这个服务前需要安装有 ffmpeg,并且可以直接在终端调用 ffmpeg 指令。",
|
||||
"hint": "提示:使用这个服务前需要安装有 ffmpeg,并且可以直接在终端调用 ffmpeg 指令。",
|
||||
"id": "edge_tts",
|
||||
"provider": "microsoft",
|
||||
"type": "edge_tts",
|
||||
@@ -927,6 +948,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_key": "",
|
||||
"api_base": "https://api.fish.audio/v1",
|
||||
"fishaudio-tts-character": "可莉",
|
||||
"fishaudio-tts-reference-id": "",
|
||||
"timeout": "20",
|
||||
},
|
||||
"阿里云百炼 TTS(API)": {
|
||||
@@ -1520,6 +1542,11 @@ CONFIG_METADATA_2 = {
|
||||
"type": "string",
|
||||
"hint": "fishaudio TTS 的角色。默认为可莉。更多角色请访问:https://fish.audio/zh-CN/discovery",
|
||||
},
|
||||
"fishaudio-tts-reference-id": {
|
||||
"description": "reference_id",
|
||||
"type": "string",
|
||||
"hint": "fishaudio TTS 的参考模型ID(可选)。如果填入此字段,将直接使用模型ID而不通过角色名称查询。例如:626bb6d3f3364c9cbc3aa6a67300a664。更多模型请访问:https://fish.audio/zh-CN/discovery,进入模型详情界面后可复制模型ID",
|
||||
},
|
||||
"whisper_hint": {
|
||||
"description": "本地部署 Whisper 模型须知",
|
||||
"type": "string",
|
||||
@@ -1686,6 +1713,10 @@ CONFIG_METADATA_2 = {
|
||||
"type": "bool",
|
||||
"hint": "启用后,若平台不支持流式回复,会分段输出。目前仅支持 aiocqhttp 两个平台,不支持或无需使用流式分段输出的平台会静默忽略此选项",
|
||||
},
|
||||
"max_agent_step": {
|
||||
"description": "工具调用轮数上限",
|
||||
"type": "int",
|
||||
},
|
||||
},
|
||||
},
|
||||
"persona": {
|
||||
@@ -1862,6 +1893,12 @@ CONFIG_METADATA_2 = {
|
||||
"type": "string",
|
||||
"hint": "启用后,会以添加环境变量的方式设置代理。格式为 `http://ip:port`",
|
||||
},
|
||||
"no_proxy": {
|
||||
"description": "直连地址列表",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"hint": "在此处添加不希望通过代理访问的地址,例如内部服务地址。回车添加,可添加多个,如未设置代理请忽略此配置",
|
||||
},
|
||||
"timezone": {
|
||||
"description": "时区",
|
||||
"type": "string",
|
||||
|
||||
@@ -47,12 +47,23 @@ class AstrBotCoreLifecycle:
|
||||
self.db = db # 初始化数据库
|
||||
|
||||
# 设置代理
|
||||
if self.astrbot_config.get("http_proxy", ""):
|
||||
os.environ["https_proxy"] = self.astrbot_config["http_proxy"]
|
||||
os.environ["http_proxy"] = self.astrbot_config["http_proxy"]
|
||||
if proxy := os.environ.get("https_proxy"):
|
||||
logger.debug(f"Using proxy: {proxy}")
|
||||
os.environ["no_proxy"] = "localhost"
|
||||
proxy_config = self.astrbot_config.get("http_proxy", "")
|
||||
if proxy_config != "":
|
||||
os.environ["https_proxy"] = proxy_config
|
||||
os.environ["http_proxy"] = proxy_config
|
||||
logger.debug(f"Using proxy: {proxy_config}")
|
||||
# 设置 no_proxy
|
||||
no_proxy_list = self.astrbot_config.get("no_proxy", [])
|
||||
os.environ["no_proxy"] = ",".join(no_proxy_list)
|
||||
else:
|
||||
# 清空代理环境变量
|
||||
if "https_proxy" in os.environ:
|
||||
del os.environ["https_proxy"]
|
||||
if "http_proxy" in os.environ:
|
||||
del os.environ["http_proxy"]
|
||||
if "no_proxy" in os.environ:
|
||||
del os.environ["no_proxy"]
|
||||
logger.debug("HTTP proxy cleared")
|
||||
|
||||
async def initialize(self):
|
||||
"""
|
||||
|
||||
@@ -2,6 +2,8 @@ import asyncio
|
||||
import os
|
||||
import uuid
|
||||
import time
|
||||
from urllib.parse import urlparse, unquote
|
||||
import platform
|
||||
|
||||
|
||||
class FileTokenService:
|
||||
@@ -15,7 +17,9 @@ class FileTokenService:
|
||||
async def _cleanup_expired_tokens(self):
|
||||
"""清理过期的令牌"""
|
||||
now = time.time()
|
||||
expired_tokens = [token for token, (_, expire) in self.staged_files.items() if expire < now]
|
||||
expired_tokens = [
|
||||
token for token, (_, expire) in self.staged_files.items() if expire < now
|
||||
]
|
||||
for token in expired_tokens:
|
||||
self.staged_files.pop(token, None)
|
||||
|
||||
@@ -32,15 +36,35 @@ class FileTokenService:
|
||||
Raises:
|
||||
FileNotFoundError: 当路径不存在时抛出
|
||||
"""
|
||||
|
||||
# 处理 file:///
|
||||
try:
|
||||
parsed_uri = urlparse(file_path)
|
||||
if parsed_uri.scheme == "file":
|
||||
local_path = unquote(parsed_uri.path)
|
||||
if platform.system() == "Windows" and local_path.startswith("/"):
|
||||
local_path = local_path[1:]
|
||||
else:
|
||||
# 如果没有 file:/// 前缀,则认为是普通路径
|
||||
local_path = file_path
|
||||
except Exception:
|
||||
# 解析失败时,按原路径处理
|
||||
local_path = file_path
|
||||
|
||||
async with self.lock:
|
||||
await self._cleanup_expired_tokens()
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||
if not os.path.exists(local_path):
|
||||
raise FileNotFoundError(
|
||||
f"文件不存在: {local_path} (原始输入: {file_path})"
|
||||
)
|
||||
|
||||
file_token = str(uuid.uuid4())
|
||||
expire_time = time.time() + (timeout if timeout is not None else self.default_timeout)
|
||||
self.staged_files[file_token] = (file_path, expire_time)
|
||||
expire_time = time.time() + (
|
||||
timeout if timeout is not None else self.default_timeout
|
||||
)
|
||||
# 存储转换后的真实路径
|
||||
self.staged_files[file_token] = (local_path, expire_time)
|
||||
return file_token
|
||||
|
||||
async def handle_file(self, file_token: str) -> str:
|
||||
|
||||
@@ -335,6 +335,10 @@ class LLMRequestSubStage(Stage):
|
||||
):
|
||||
return
|
||||
|
||||
if not llm_response.completion_text and not req.tool_calls_result:
|
||||
logger.debug("LLM 响应为空,不保存记录。")
|
||||
return
|
||||
|
||||
# 历史上下文
|
||||
messages = copy.deepcopy(req.contexts)
|
||||
# 这一轮对话请求的用户输入
|
||||
|
||||
@@ -144,8 +144,6 @@ class RespondStage(Stage):
|
||||
try:
|
||||
if await self._is_empty_message_chain(result.chain):
|
||||
logger.info("消息为空,跳过发送阶段")
|
||||
event.clear_result()
|
||||
event.stop_event()
|
||||
return
|
||||
except Exception as e:
|
||||
logger.warning(f"空内容检查异常: {e}")
|
||||
|
||||
@@ -26,7 +26,7 @@ class DingtalkMessageEvent(AstrMessageEvent):
|
||||
await asyncio.get_event_loop().run_in_executor(
|
||||
None,
|
||||
client.reply_markdown,
|
||||
"AstrBot",
|
||||
segment.text,
|
||||
segment.text,
|
||||
self.message_obj.raw_message,
|
||||
)
|
||||
|
||||
@@ -3,15 +3,23 @@ import botpy.message
|
||||
import botpy.types
|
||||
import botpy.types.message
|
||||
import asyncio
|
||||
import base64
|
||||
import aiofiles
|
||||
from astrbot.core.utils.io import file_to_base64, download_image_by_url
|
||||
from astrbot.core.utils.tencent_record_helper import wav_to_tencent_silk
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
|
||||
from astrbot.api.message_components import Plain, Image
|
||||
from astrbot.api.message_components import Plain, Image, Record
|
||||
from botpy import Client
|
||||
from botpy.http import Route
|
||||
from astrbot.api import logger
|
||||
from botpy.types.message import Media
|
||||
from botpy.types import message
|
||||
from typing import Optional
|
||||
import random
|
||||
import uuid
|
||||
import os
|
||||
|
||||
|
||||
class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
@@ -36,6 +44,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
stream_payload = {"state": 1, "id": None, "index": 0, "reset": False}
|
||||
last_edit_time = 0 # 上次编辑消息的时间
|
||||
throttle_interval = 1 # 编辑消息的间隔时间 (秒)
|
||||
ret = None
|
||||
try:
|
||||
async for chain in generator:
|
||||
source = self.message_obj.raw_message
|
||||
@@ -85,9 +94,10 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
plain_text,
|
||||
image_base64,
|
||||
image_path,
|
||||
record_file_path
|
||||
) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer)
|
||||
|
||||
if not plain_text and not image_base64 and not image_path:
|
||||
if not plain_text and not image_base64 and not image_path and not record_file_path:
|
||||
return
|
||||
|
||||
payload = {
|
||||
@@ -98,6 +108,8 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
if not isinstance(source, (botpy.message.Message, botpy.message.DirectMessage)):
|
||||
payload["msg_seq"] = random.randint(1, 10000)
|
||||
|
||||
ret = None
|
||||
|
||||
match type(source):
|
||||
case botpy.message.GroupMessage:
|
||||
if image_base64:
|
||||
@@ -106,6 +118,12 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if record_file_path: # group record msg
|
||||
media = await self.upload_group_and_c2c_record(
|
||||
record_file_path, 3, group_openid=source.group_openid
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
ret = await self.bot.api.post_group_message(
|
||||
group_openid=source.group_openid, **payload
|
||||
)
|
||||
@@ -116,6 +134,12 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if record_file_path: # c2c record
|
||||
media = await self.upload_group_and_c2c_record(
|
||||
record_file_path, 3, openid = source.author.user_openid
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if stream:
|
||||
ret = await self.post_c2c_message(
|
||||
openid=source.author.user_openid,
|
||||
@@ -165,6 +189,59 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
)
|
||||
return await self.bot.api._http.request(route, json=payload)
|
||||
|
||||
async def upload_group_and_c2c_record(
|
||||
self,
|
||||
file_source: str,
|
||||
file_type: int,
|
||||
srv_send_msg: bool = False,
|
||||
**kwargs
|
||||
) -> Optional[Media]:
|
||||
"""
|
||||
上传媒体文件
|
||||
"""
|
||||
# 构建基础payload
|
||||
payload = {
|
||||
"file_type": file_type,
|
||||
"srv_send_msg": srv_send_msg
|
||||
}
|
||||
|
||||
# 处理文件数据
|
||||
if os.path.exists(file_source):
|
||||
# 读取本地文件
|
||||
async with aiofiles.open(file_source, 'rb') as f:
|
||||
file_content = await f.read()
|
||||
# use base64 encode
|
||||
payload["file_data"] = base64.b64encode(file_content).decode('utf-8')
|
||||
else:
|
||||
# 使用URL
|
||||
payload["url"] = file_source
|
||||
|
||||
# 添加接收者信息和确定路由
|
||||
if "openid" in kwargs:
|
||||
payload["openid"] = kwargs["openid"]
|
||||
route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
|
||||
elif "group_openid" in kwargs:
|
||||
payload["group_openid"] =kwargs["group_openid"]
|
||||
route = Route("POST", "/v2/groups/{group_openid}/files", group_openid=kwargs["group_openid"])
|
||||
else:
|
||||
return None
|
||||
|
||||
try:
|
||||
# 使用底层HTTP请求
|
||||
result = await self.bot.api._http.request(route, json=payload)
|
||||
|
||||
if result:
|
||||
return Media(
|
||||
file_uuid=result.get("file_uuid"),
|
||||
file_info=result.get("file_info"),
|
||||
ttl=result.get("ttl", 0),
|
||||
file_id=result.get("id", "")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"上传请求错误: {e}")
|
||||
|
||||
return None
|
||||
|
||||
async def post_c2c_message(
|
||||
self,
|
||||
openid: str,
|
||||
@@ -191,6 +268,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
plain_text = ""
|
||||
image_base64 = None # only one img supported
|
||||
image_file_path = None
|
||||
record_file_path = None
|
||||
for i in message.chain:
|
||||
if isinstance(i, Plain):
|
||||
plain_text += i.text
|
||||
@@ -206,6 +284,21 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
else:
|
||||
image_base64 = file_to_base64(i.file)
|
||||
image_base64 = image_base64.removeprefix("base64://")
|
||||
elif isinstance(i, Record):
|
||||
if i.file:
|
||||
record_wav_path = await i.convert_to_file_path() # wav 路径
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
record_tecent_silk_path = os.path.join(temp_dir, f"{uuid.uuid4()}.silk")
|
||||
try:
|
||||
duration = await wav_to_tencent_silk(record_wav_path, record_tecent_silk_path)
|
||||
if duration > 0:
|
||||
record_file_path = record_tecent_silk_path
|
||||
else:
|
||||
record_file_path = None
|
||||
logger.error("转换音频格式时出错:音频时长不大于0")
|
||||
except Exception as e:
|
||||
logger.error(f"处理语音时出错: {e}")
|
||||
record_file_path = None
|
||||
else:
|
||||
logger.debug(f"qq_official 忽略 {i.type}")
|
||||
return plain_text, image_base64, image_file_path
|
||||
return plain_text, image_base64, image_file_path, record_file_path
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import uuid
|
||||
import re
|
||||
import ormsgpack
|
||||
from pydantic import BaseModel, conint
|
||||
from httpx import AsyncClient
|
||||
@@ -24,8 +25,8 @@ class ServeTTSRequest(BaseModel):
|
||||
# 参考音频
|
||||
references: list[ServeReferenceAudio] = []
|
||||
# 参考模型 ID
|
||||
# 例如 https://fish.audio/m/7f92f8afb8ec43bf81429cc1c9199cb1/
|
||||
# 其中reference_id为 7f92f8afb8ec43bf81429cc1c9199cb1
|
||||
# 例如 https://fish.audio/m/626bb6d3f3364c9cbc3aa6a67300a664/
|
||||
# 其中reference_id为 626bb6d3f3364c9cbc3aa6a67300a664
|
||||
reference_id: str | None = None
|
||||
# 对中英文文本进行标准化,这可以提高数字的稳定性
|
||||
normalize: bool = True
|
||||
@@ -44,6 +45,7 @@ class ProviderFishAudioTTSAPI(TTSProvider):
|
||||
) -> None:
|
||||
super().__init__(provider_config, provider_settings)
|
||||
self.chosen_api_key: str = provider_config.get("api_key", "")
|
||||
self.reference_id: str = provider_config.get("fishaudio-tts-reference-id", "")
|
||||
self.character: str = provider_config.get("fishaudio-tts-character", "可莉")
|
||||
self.api_base: str = provider_config.get(
|
||||
"api_base", "https://api.fish-audio.cn/v1"
|
||||
@@ -81,11 +83,43 @@ class ProviderFishAudioTTSAPI(TTSProvider):
|
||||
return item["_id"]
|
||||
return None
|
||||
|
||||
def _validate_reference_id(self, reference_id: str) -> bool:
|
||||
"""
|
||||
验证reference_id格式是否有效
|
||||
|
||||
Args:
|
||||
reference_id: 参考模型ID
|
||||
|
||||
Returns:
|
||||
bool: ID是否有效
|
||||
"""
|
||||
if not reference_id or not reference_id.strip():
|
||||
return False
|
||||
|
||||
# FishAudio的reference_id通常是32位十六进制字符串
|
||||
# 例如: 626bb6d3f3364c9cbc3aa6a67300a664
|
||||
pattern = r'^[a-fA-F0-9]{32}$'
|
||||
return bool(re.match(pattern, reference_id.strip()))
|
||||
|
||||
async def _generate_request(self, text: str) -> dict:
|
||||
# 向前兼容逻辑:优先使用reference_id,如果没有则使用角色名称查询
|
||||
if self.reference_id and self.reference_id.strip():
|
||||
# 验证reference_id格式
|
||||
if not self._validate_reference_id(self.reference_id):
|
||||
raise ValueError(
|
||||
f"无效的FishAudio参考模型ID: '{self.reference_id}'. "
|
||||
f"请确保ID是32位十六进制字符串(例如: 626bb6d3f3364c9cbc3aa6a67300a664)。"
|
||||
f"您可以从 https://fish.audio/zh-CN/discovery 获取有效的模型ID。"
|
||||
)
|
||||
reference_id = self.reference_id.strip()
|
||||
else:
|
||||
# 回退到原来的角色名称查询逻辑
|
||||
reference_id = await self._get_reference_id_by_character(self.character)
|
||||
|
||||
return ServeTTSRequest(
|
||||
text=text,
|
||||
format="wav",
|
||||
reference_id=await self._get_reference_id_by_character(self.character),
|
||||
reference_id=reference_id,
|
||||
)
|
||||
|
||||
async def get_audio(self, text: str) -> str:
|
||||
|
||||
@@ -431,6 +431,7 @@ class ProviderGoogleGenAI(Provider):
|
||||
continue
|
||||
|
||||
llm_response = LLMResponse("assistant")
|
||||
llm_response.raw_completion = result
|
||||
llm_response.result_chain = self._process_content_parts(result, llm_response)
|
||||
return llm_response
|
||||
|
||||
@@ -470,6 +471,10 @@ class ProviderGoogleGenAI(Provider):
|
||||
raise
|
||||
continue
|
||||
|
||||
# Accumulate the complete response text for the final response
|
||||
accumulated_text = ""
|
||||
final_response = None
|
||||
|
||||
async for chunk in result:
|
||||
llm_response = LLMResponse("assistant", is_chunk=True)
|
||||
|
||||
@@ -477,27 +482,43 @@ class ProviderGoogleGenAI(Provider):
|
||||
part.function_call for part in chunk.candidates[0].content.parts
|
||||
):
|
||||
llm_response = LLMResponse("assistant", is_chunk=False)
|
||||
llm_response.raw_completion = chunk
|
||||
llm_response.result_chain = self._process_content_parts(
|
||||
chunk, llm_response
|
||||
)
|
||||
yield llm_response
|
||||
break
|
||||
return
|
||||
|
||||
if chunk.text:
|
||||
accumulated_text += chunk.text
|
||||
llm_response.result_chain = MessageChain(chain=[Comp.Plain(chunk.text)])
|
||||
yield llm_response
|
||||
|
||||
if chunk.candidates[0].finish_reason:
|
||||
llm_response = LLMResponse("assistant", is_chunk=False)
|
||||
if not chunk.candidates[0].content.parts:
|
||||
llm_response.result_chain = MessageChain(chain=[Comp.Plain(" ")])
|
||||
else:
|
||||
llm_response.result_chain = self._process_content_parts(
|
||||
chunk, llm_response
|
||||
# Process the final chunk for potential tool calls or other content
|
||||
if chunk.candidates[0].content.parts:
|
||||
final_response = LLMResponse("assistant", is_chunk=False)
|
||||
final_response.raw_completion = chunk
|
||||
final_response.result_chain = self._process_content_parts(
|
||||
chunk, final_response
|
||||
)
|
||||
yield llm_response
|
||||
break
|
||||
|
||||
# Yield final complete response with accumulated text
|
||||
if not final_response:
|
||||
final_response = LLMResponse("assistant", is_chunk=False)
|
||||
|
||||
# Set the complete accumulated text in the final response
|
||||
if accumulated_text:
|
||||
final_response.result_chain = MessageChain(
|
||||
chain=[Comp.Plain(accumulated_text)]
|
||||
)
|
||||
elif not final_response.result_chain:
|
||||
# If no text was accumulated and no final response was set, provide empty space
|
||||
final_response.result_chain = MessageChain(chain=[Comp.Plain(" ")])
|
||||
|
||||
yield final_response
|
||||
|
||||
async def text_chat(
|
||||
self,
|
||||
prompt: str,
|
||||
|
||||
@@ -99,6 +99,14 @@ class ProviderOpenAIOfficial(Provider):
|
||||
for key in to_del:
|
||||
del payloads[key]
|
||||
|
||||
model = payloads.get("model", "")
|
||||
# 针对 qwen3 模型的特殊处理:非流式调用必须设置 enable_thinking=false
|
||||
if "qwen3" in model.lower():
|
||||
extra_body["enable_thinking"] = False
|
||||
# 针对 deepseek 模型的特殊处理:deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat
|
||||
elif model == "deepseek-reasoner" and "tools" in payloads:
|
||||
del payloads["tools"]
|
||||
|
||||
completion = await self.client.chat.completions.create(
|
||||
**payloads, stream=False, extra_body=extra_body
|
||||
)
|
||||
@@ -176,7 +184,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
raise Exception("API 返回的 completion 为空。")
|
||||
choice = completion.choices[0]
|
||||
|
||||
if choice.message.content:
|
||||
if choice.message.content is not None:
|
||||
# text completion
|
||||
completion_text = str(choice.message.content).strip()
|
||||
llm_response.result_chain = MessageChain().message(completion_text)
|
||||
@@ -210,7 +218,7 @@ class ProviderOpenAIOfficial(Provider):
|
||||
"API 返回的 completion 由于内容安全过滤被拒绝(非 AstrBot)。"
|
||||
)
|
||||
|
||||
if not llm_response.completion_text and not llm_response.tools_call_args:
|
||||
if llm_response.completion_text is None and not llm_response.tools_call_args:
|
||||
logger.error(f"API 返回的 completion 无法解析:{completion}。")
|
||||
raise Exception(f"API 返回的 completion 无法解析:{completion}。")
|
||||
|
||||
|
||||
@@ -809,11 +809,11 @@ class PluginManager:
|
||||
if star_metadata.star_cls is None:
|
||||
return
|
||||
|
||||
if hasattr(star_metadata.star_cls, "__del__"):
|
||||
if '__del__' in star_metadata.star_cls_type.__dict__:
|
||||
asyncio.get_event_loop().run_in_executor(
|
||||
None, star_metadata.star_cls.__del__
|
||||
)
|
||||
elif hasattr(star_metadata.star_cls, "terminate"):
|
||||
elif 'terminate' in star_metadata.star_cls_type.__dict__:
|
||||
await star_metadata.star_cls.terminate()
|
||||
|
||||
async def turn_on_plugin(self, plugin_name: str):
|
||||
|
||||
@@ -56,9 +56,7 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
try:
|
||||
if "astrbot" in os.path.basename(sys.argv[0]): # 兼容cli
|
||||
if os.name == "nt":
|
||||
args = [
|
||||
f'"{arg}"' if " " in arg else arg for arg in sys.argv[1:]
|
||||
]
|
||||
args = [f'"{arg}"' if " " in arg else arg for arg in sys.argv[1:]]
|
||||
else:
|
||||
args = sys.argv[1:]
|
||||
os.execl(sys.executable, py, "-m", "astrbot.cli.__main__", *args)
|
||||
@@ -68,9 +66,13 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
logger.error(f"重启失败({py}, {e}),请尝试手动重启。")
|
||||
raise e
|
||||
|
||||
async def check_update(self, url: str, current_version: str) -> ReleaseInfo:
|
||||
async def check_update(
|
||||
self, url: str, current_version: str, consider_prerelease: bool = True
|
||||
) -> ReleaseInfo:
|
||||
"""检查更新"""
|
||||
return await super().check_update(self.ASTRBOT_RELEASE_API, VERSION)
|
||||
return await super().check_update(
|
||||
self.ASTRBOT_RELEASE_API, VERSION, consider_prerelease
|
||||
)
|
||||
|
||||
async def get_releases(self) -> list:
|
||||
return await self.fetch_release_info(self.ASTRBOT_RELEASE_API)
|
||||
@@ -89,7 +91,6 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
file_url = update_data[0]["zipball_url"]
|
||||
elif str(version).startswith("v"):
|
||||
# 更新到指定版本
|
||||
logger.info(f"正在更新到指定版本: {version}")
|
||||
for data in update_data:
|
||||
if data["tag_name"] == version:
|
||||
file_url = data["zipball_url"]
|
||||
@@ -98,8 +99,8 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
else:
|
||||
if len(str(version)) != 40:
|
||||
raise Exception("commit hash 长度不正确,应为 40")
|
||||
logger.info(f"正在尝试更新到指定 commit: {version}")
|
||||
file_url = "https://github.com/Soulter/AstrBot/archive/" + version + ".zip"
|
||||
file_url = f"https://github.com/Soulter/AstrBot/archive/{version}.zip"
|
||||
logger.info(f"准备更新至指定版本的 AstrBot Core: {version}")
|
||||
|
||||
if proxy:
|
||||
proxy = proxy.removesuffix("/")
|
||||
@@ -107,6 +108,7 @@ class AstrBotUpdator(RepoZipUpdator):
|
||||
|
||||
try:
|
||||
await download_file(file_url, "temp.zip")
|
||||
logger.info("下载 AstrBot Core 更新文件完成,正在执行解压...")
|
||||
self.unzip_file("temp.zip", self.MAIN_PATH)
|
||||
except BaseException as e:
|
||||
raise e
|
||||
|
||||
@@ -8,6 +8,7 @@ import base64
|
||||
import zipfile
|
||||
import uuid
|
||||
import psutil
|
||||
import logging
|
||||
|
||||
import certifi
|
||||
|
||||
@@ -16,6 +17,8 @@ from typing import Union
|
||||
from PIL import Image
|
||||
from .astrbot_path import get_astrbot_data_path
|
||||
|
||||
logger = logging.getLogger("astrbot")
|
||||
|
||||
|
||||
def on_error(func, path, exc_info):
|
||||
"""
|
||||
@@ -212,19 +215,50 @@ async def get_dashboard_version():
|
||||
return None
|
||||
|
||||
|
||||
async def download_dashboard(path: str = None, extract_path: str = "data"):
|
||||
async def download_dashboard(
|
||||
path: str | None = None,
|
||||
extract_path: str = "data",
|
||||
latest: bool = True,
|
||||
version: str | None = None,
|
||||
proxy: str | None = None,
|
||||
):
|
||||
"""下载管理面板文件"""
|
||||
if path is None:
|
||||
path = os.path.join(get_astrbot_data_path(), "dashboard.zip")
|
||||
|
||||
dashboard_release_url = "https://astrbot-registry.soulter.top/download/astrbot-dashboard/latest/dist.zip"
|
||||
try:
|
||||
await download_file(dashboard_release_url, path, show_progress=True)
|
||||
except BaseException as _:
|
||||
dashboard_release_url = (
|
||||
"https://github.com/Soulter/AstrBot/releases/latest/download/dist.zip"
|
||||
if latest or len(str(version)) != 40:
|
||||
logger.info(f"准备下载 {version} 发行版本的 AstrBot WebUI 文件")
|
||||
ver_name = "latest" if latest else version
|
||||
dashboard_release_url = f"https://astrbot-registry.soulter.top/download/astrbot-dashboard/{ver_name}/dist.zip"
|
||||
try:
|
||||
await download_file(dashboard_release_url, path, show_progress=True)
|
||||
except BaseException as _:
|
||||
if latest:
|
||||
dashboard_release_url = "https://github.com/Soulter/AstrBot/releases/latest/download/dist.zip"
|
||||
else:
|
||||
dashboard_release_url = f"https://github.com/Soulter/AstrBot/releases/download/{version}/dist.zip"
|
||||
if proxy:
|
||||
dashboard_release_url = f"{proxy}/{dashboard_release_url}"
|
||||
await download_file(dashboard_release_url, path, show_progress=True)
|
||||
else:
|
||||
logger.info(f"准备下载指定版本的 AstrBot WebUI: {version}")
|
||||
|
||||
url = (
|
||||
"https://api.github.com/repos/AstrBotDevs/astrbot-release-harbour/releases"
|
||||
)
|
||||
await download_file(dashboard_release_url, path, show_progress=True)
|
||||
print("解压管理面板文件中...")
|
||||
if proxy:
|
||||
url = f"{proxy}/{url}"
|
||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||
async with session.get(url) as resp:
|
||||
if resp.status == 200:
|
||||
releases = await resp.json()
|
||||
for release in releases:
|
||||
if version in release["tag_name"]:
|
||||
download_url = release["assets"][0]["browser_download_url"]
|
||||
await download_file(download_url, path, show_progress=True)
|
||||
else:
|
||||
logger.warning(f"未找到指定的版本的 Dashboard 构建文件: {version}")
|
||||
return
|
||||
|
||||
with zipfile.ZipFile(path, "r") as z:
|
||||
z.extractall(extract_path)
|
||||
|
||||
@@ -107,16 +107,38 @@ class RepoZipUpdator:
|
||||
"""Semver 版本比较"""
|
||||
return VersionComparator.compare_version(v1, v2)
|
||||
|
||||
async def check_update(self, url: str, current_version: str) -> ReleaseInfo | None:
|
||||
async def check_update(
|
||||
self, url: str, current_version: str, consider_prerelease: bool = True
|
||||
) -> ReleaseInfo | None:
|
||||
update_data = await self.fetch_release_info(url)
|
||||
tag_name = update_data[0]["tag_name"]
|
||||
|
||||
sel_release_data = None
|
||||
if consider_prerelease:
|
||||
tag_name = update_data[0]["tag_name"]
|
||||
sel_release_data = update_data[0]
|
||||
else:
|
||||
for data in update_data:
|
||||
# 跳过带有 alpha、beta 等预发布标签的版本
|
||||
if re.search(
|
||||
r"[\-_.]?(alpha|beta|rc|dev)[\-_.]?\d*$",
|
||||
data["tag_name"],
|
||||
re.IGNORECASE,
|
||||
):
|
||||
continue
|
||||
tag_name = data["tag_name"]
|
||||
sel_release_data = data
|
||||
break
|
||||
|
||||
if not sel_release_data or not tag_name:
|
||||
logger.error("未找到合适的发布版本")
|
||||
return None
|
||||
|
||||
if self.compare_version(current_version, tag_name) >= 0:
|
||||
return None
|
||||
return ReleaseInfo(
|
||||
version=tag_name,
|
||||
published_at=update_data[0]["published_at"],
|
||||
body=update_data[0]["body"],
|
||||
published_at=sel_release_data["published_at"],
|
||||
body=f"{tag_name}\n\n{sel_release_data['body']}",
|
||||
)
|
||||
|
||||
async def download_from_repo_url(self, target_path: str, repo_url: str, proxy=""):
|
||||
@@ -181,14 +203,13 @@ class RepoZipUpdator:
|
||||
"""
|
||||
os.makedirs(target_dir, exist_ok=True)
|
||||
update_dir = ""
|
||||
logger.info(f"解压文件: {zip_path}")
|
||||
with zipfile.ZipFile(zip_path, "r") as z:
|
||||
update_dir = z.namelist()[0]
|
||||
z.extractall(target_dir)
|
||||
logger.debug(f"解压文件完成: {zip_path}")
|
||||
|
||||
files = os.listdir(os.path.join(target_dir, update_dir))
|
||||
for f in files:
|
||||
logger.info(f"移动更新文件/目录: {f}")
|
||||
if os.path.isdir(os.path.join(target_dir, update_dir, f)):
|
||||
if os.path.exists(os.path.join(target_dir, f)):
|
||||
shutil.rmtree(os.path.join(target_dir, f), onerror=on_error)
|
||||
@@ -198,13 +219,13 @@ class RepoZipUpdator:
|
||||
shutil.move(os.path.join(target_dir, update_dir, f), target_dir)
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
logger.debug(
|
||||
f"删除临时更新文件: {zip_path} 和 {os.path.join(target_dir, update_dir)}"
|
||||
)
|
||||
shutil.rmtree(os.path.join(target_dir, update_dir), onerror=on_error)
|
||||
os.remove(zip_path)
|
||||
except BaseException:
|
||||
logger.warn(
|
||||
logger.warning(
|
||||
f"删除更新文件失败,可以手动删除 {zip_path} 和 {os.path.join(target_dir, update_dir)}"
|
||||
)
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ class UpdateRoute(Route):
|
||||
.__dict__
|
||||
)
|
||||
else:
|
||||
ret = await self.astrbot_updator.check_update(None, None)
|
||||
ret = await self.astrbot_updator.check_update(None, None, False)
|
||||
return Response(
|
||||
status="success",
|
||||
message=str(ret) if ret is not None else "已经是最新版本了。",
|
||||
@@ -48,7 +48,7 @@ class UpdateRoute(Route):
|
||||
"version": f"v{VERSION}",
|
||||
"has_new_version": ret is not None,
|
||||
"dashboard_version": dv,
|
||||
"dashboard_has_new_version": dv != f"v{VERSION}",
|
||||
"dashboard_has_new_version": dv and dv != f"v{VERSION}",
|
||||
},
|
||||
).__dict__
|
||||
except Exception as e:
|
||||
@@ -82,11 +82,10 @@ class UpdateRoute(Route):
|
||||
latest=latest, version=version, proxy=proxy
|
||||
)
|
||||
|
||||
if latest:
|
||||
try:
|
||||
await download_dashboard()
|
||||
except Exception as e:
|
||||
logger.error(f"下载管理面板文件失败: {e}。")
|
||||
try:
|
||||
await download_dashboard(latest=latest, version=version, proxy=proxy)
|
||||
except Exception as e:
|
||||
logger.error(f"下载管理面板文件失败: {e}。")
|
||||
|
||||
# pip 更新依赖
|
||||
logger.info("更新依赖中...")
|
||||
@@ -115,7 +114,7 @@ class UpdateRoute(Route):
|
||||
async def update_dashboard(self):
|
||||
try:
|
||||
try:
|
||||
await download_dashboard()
|
||||
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||
except Exception as e:
|
||||
logger.error(f"下载管理面板文件失败: {e}。")
|
||||
return Response().error(f"下载管理面板文件失败: {e}").__dict__
|
||||
|
||||
10
changelogs/v3.5.24.md
Normal file
10
changelogs/v3.5.24.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# What's Changed
|
||||
|
||||
> 新版本预告: v4.0.0 即将发布。
|
||||
|
||||
1. 新增: 添加对 ModelScope、Compshare(优云智算)的模版支持。
|
||||
2. 优化: 增加插件数据缓存,优化插件市场数据获取时的稳定性。
|
||||
|
||||
其他更新:
|
||||
|
||||
1. 现已支持在 1Panel 平台通过应用商城快捷部署 AstrBot。详见:[在 1Panel 部署 AstrBot](https://docs.astrbot.app/deploy/astrbot/1panel.html)
|
||||
13
changelogs/v3.5.25.md
Normal file
13
changelogs/v3.5.25.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# What's Changed
|
||||
|
||||
1. 修复: 修复插件可能存在的无法正常禁用的问题 ([#2352](https://github.com/Soulter/AstrBot/issues/2352))
|
||||
2. ❗修复:当返回文本为空并且存在函数调用时错误地被终止事件,导致函数调用结果未被正常返回 ([#2491](https://github.com/Soulter/AstrBot/issues/2491))
|
||||
3. 修复:修复无法清空 AstrBot 配置下的 http_proxy 代理的问题 ([#2434](https://github.com/Soulter/AstrBot/issues/2434))
|
||||
4. ❗修复:Gemini 下开启流式输出时,持久化的消息结果不完整 ([#2424](https://github.com/Soulter/AstrBot/issues/2424))
|
||||
5. 修复:注册文件时由于 file:/// 前缀,导致文件被误判为不存在的问题 ([#2325](https://github.com/Soulter/AstrBot/issues/2325))
|
||||
6. 优化: 为部分类型供应商添加默认的温度选项 ([#2321](https://github.com/Soulter/AstrBot/issues/2321))
|
||||
7. 优化: 适配 Qwen3 模型非流式输出下需要传入 enable_think 参数(否则报错) ([#2424](https://github.com/Soulter/AstrBot/issues/2424))
|
||||
8. 优化:支持配置工具调用轮数上限,默认 30
|
||||
9. 新增: 添加 WebUI 语义化预发布版本提醒和检测功能
|
||||
|
||||
> 新版本预告: v4.0.0 即将发布。
|
||||
9
changelogs/v3.5.26.md
Normal file
9
changelogs/v3.5.26.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# What's Changed
|
||||
|
||||
1. 新增:为 FishAudio TTS 添加可选的 reference_id 直接指定功能 ([#2513](https://github.com/AstrBotDevs/AstrBot/issues/2513))
|
||||
2. 新增:Gemini 添加对 LLMResponse 的 raw_completion 支持
|
||||
3. 新增:支持官方 QQ 接口发送语音 ([#2525](https://github.com/AstrBotDevs/AstrBot/issues/2525))
|
||||
4. 新增:调用 deepseek-reasoner 时自动移除 tools ([#2531](https://github.com/AstrBotDevs/AstrBot/issues/2531))
|
||||
5. 新增:添加 no_proxy 配置支持以优化代理设置 ([#2564](https://github.com/AstrBotDevs/AstrBot/issues/2564))
|
||||
6. 新增:支持升级的同时更新到指定版本的 WebUI
|
||||
7. 修复: 修复编辑会话名称窗口的圆角和左右边距问题 ([#2583](https://github.com/AstrBotDevs/AstrBot/issues/2583))
|
||||
5
changelogs/v3.5.27.md
Normal file
5
changelogs/v3.5.27.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# What's Changed
|
||||
|
||||
1. 修复:构建 docker 镜像时同时构建 webui,并放入镜像中。
|
||||
2. 修复:下载 WebUI 文件时,明确版本号,以防止 latest 不一致导致下载的 WebUI 文件版本号与实际所需不符的问题。
|
||||
3. 优化:优化版本检测,考虑预发布版本,移除 `更新到最新版本` 按钮
|
||||
@@ -1,10 +1,10 @@
|
||||
<script setup>
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import ListConfigItem from './ListConfigItem.vue'
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
metadata: {
|
||||
type: Object,
|
||||
required: true
|
||||
@@ -16,11 +16,21 @@ defineProps({
|
||||
metadataKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isEditing: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const filteredIterable = computed(() => {
|
||||
if (!props.iterable) return {}
|
||||
const { hint, ...rest } = props.iterable
|
||||
return rest
|
||||
})
|
||||
|
||||
const dialog = ref(false)
|
||||
const currentEditingKey = ref('')
|
||||
const currentEditingLanguage = ref('json')
|
||||
@@ -54,7 +64,19 @@ function saveEditedContent() {
|
||||
<v-card-text class="px-0 py-1">
|
||||
<!-- Object Type Configuration -->
|
||||
<div v-if="metadata[metadataKey]?.type === 'object' || metadata[metadataKey]?.config_template" class="object-config">
|
||||
<div v-for="(val, key, index) in iterable" :key="key" class="config-item">
|
||||
<!-- Provider-level hint -->
|
||||
<v-alert
|
||||
v-if="iterable.hint && !isEditing"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
border="start"
|
||||
density="compact"
|
||||
>
|
||||
{{ iterable.hint }}
|
||||
</v-alert>
|
||||
|
||||
<div v-for="(val, key, index) in filteredIterable" :key="key" class="config-item">
|
||||
<!-- Nested Object -->
|
||||
<div v-if="metadata[metadataKey].items[key]?.type === 'object'" class="nested-object">
|
||||
<div v-if="metadata[metadataKey].items[key] && !metadata[metadataKey].items[key]?.invisible" class="nested-container">
|
||||
|
||||
@@ -25,10 +25,16 @@
|
||||
"dev": "🧐 Development (master branch)"
|
||||
},
|
||||
"updateToLatest": "Update to Latest Version",
|
||||
"tip": "💡 TIP: Switching to an older version or a specific version will not re-download the dashboard files, which may cause some data display errors. You can find the corresponding dashboard files dist.zip at",
|
||||
"tipLink": "here",
|
||||
"tipContinue": ", extract and replace the data/dist folder. Of course, the frontend source code is in the dashboard directory, you can also build it yourself using npm install and npm build.",
|
||||
"dockerTip": "The `Update to Latest Version` button will try to update both the bot main program and the dashboard. If you are using Docker deployment, you can also re-pull the image or use",
|
||||
"preRelease": "Pre-release",
|
||||
"preReleaseWarning": {
|
||||
"title": "Pre-release Version Notice",
|
||||
"description": "Versions marked as pre-release may contain unknown issues or bugs and are not recommended for production use. If you encounter any problems, please visit ",
|
||||
"issueLink": "GitHub Issues"
|
||||
},
|
||||
"tip": "💡 TIP:",
|
||||
"tipLink": "",
|
||||
"tipContinue": "By default, the corresponding version of the WebUI files will be downloaded when switching versions. The WebUI code is located in the dashboard directory of the project, and you can use npm to build it yourself.",
|
||||
"dockerTip": "When switching versions, it will try to update both the bot main program and the dashboard. If you are using Docker deployment, you can also re-pull the image or use",
|
||||
"dockerTipLink": "watchtower",
|
||||
"dockerTipContinue": "to automatically monitor and pull.",
|
||||
"table": {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"addPlatform": "Add Platform Adapter",
|
||||
"connectTitle": "Connect {name}",
|
||||
"viewTutorial": "View Tutorial",
|
||||
"noTemplates": "No platform templates available",
|
||||
"idConflict": {
|
||||
"title": "ID Conflict Warning",
|
||||
"message": "Detected duplicate ID \"{id}\". Please use a new ID.",
|
||||
|
||||
@@ -25,10 +25,15 @@
|
||||
"dev": "🧐 开发版(master 分支)"
|
||||
},
|
||||
"updateToLatest": "更新到最新版本",
|
||||
"tip": "💡 TIP: 跳到旧版本或者切换到某个版本不会重新下载管理面板文件,这可能会造成部分数据显示错误。您可在",
|
||||
"tipLink": "此处",
|
||||
"tipContinue": "找到对应的面板文件 dist.zip,解压后替换 data/dist 文件夹即可。当然,前端源代码在 dashboard 目录下,你也可以自己使用 npm install 和 npm build 构建。",
|
||||
"dockerTip": "`更新到最新版本` 按钮会同时尝试更新机器人主程序和管理面板。如果您正在使用 Docker 部署,也可以重新拉取镜像或者使用",
|
||||
"preRelease": "预发布",
|
||||
"preReleaseWarning": {
|
||||
"title": "预发布版本提醒",
|
||||
"description": "标有预发布标签的版本可能存在未知问题或 Bug,不建议在生产环境使用。如发现问题,请提交至 ",
|
||||
"issueLink": "GitHub Issues"
|
||||
},
|
||||
"tip": "💡 TIP: ",
|
||||
"tipContinue": "默认在切换版本时会下载对应版本的 WebUI 文件。WebUI 代码位于项目的 dashboard 目录,您可使用 npm 自行构建。",
|
||||
"dockerTip": "切换版本时,会同时尝试更新机器人主程序和管理面板。如果您正在使用 Docker 部署,也可以重新拉取镜像或者使用",
|
||||
"dockerTipLink": "watchtower",
|
||||
"dockerTipContinue": "来自动监控拉取。",
|
||||
"table": {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"addPlatform": "添加平台适配器",
|
||||
"connectTitle": "接入 {name}",
|
||||
"viewTutorial": "查看接入教程",
|
||||
"noTemplates": "暂无平台模板",
|
||||
"idConflict": {
|
||||
"title": "ID 冲突警告",
|
||||
"message": "检测到 ID \"{id}\" 重复。请使用一个新的 ID。",
|
||||
|
||||
@@ -36,7 +36,7 @@ let dashboardHasNewVersion = ref(false);
|
||||
let dashboardCurrentVersion = ref('');
|
||||
let version = ref('');
|
||||
let releases = ref([]);
|
||||
let devCommits = ref([]);
|
||||
let devCommits = ref<{ sha: string; date: string; message: string }[]>([]);
|
||||
let updatingDashboardLoading = ref(false);
|
||||
let installLoading = ref(false);
|
||||
|
||||
@@ -76,6 +76,13 @@ const open = (link: string) => {
|
||||
window.open(link, '_blank');
|
||||
};
|
||||
|
||||
// 检测是否为预发布版本
|
||||
const isPreRelease = (version: string) => {
|
||||
const preReleaseKeywords = ['alpha', 'beta', 'rc', 'pre', 'preview', 'dev'];
|
||||
const lowerVersion = version.toLowerCase();
|
||||
return preReleaseKeywords.some(keyword => lowerVersion.includes(keyword));
|
||||
};
|
||||
|
||||
// 账户修改
|
||||
function accountEdit() {
|
||||
accountEditStatus.value.loading = true;
|
||||
@@ -182,23 +189,46 @@ function getReleases() {
|
||||
}
|
||||
|
||||
function getDevCommits() {
|
||||
fetch('https://api.github.com/repos/Soulter/AstrBot/commits', {
|
||||
headers: {
|
||||
'Host': 'api.github.com',
|
||||
'Referer': 'https://api.github.com'
|
||||
}
|
||||
})
|
||||
let proxy = localStorage.getItem('selectedGitHubProxy') || '';
|
||||
const originalUrl = "https://api.github.com/repos/Soulter/AstrBot/commits";
|
||||
let commits_url = originalUrl;
|
||||
if (proxy !== '') {
|
||||
proxy = proxy.endsWith('/') ? proxy : proxy + '/';
|
||||
commits_url = proxy + originalUrl;
|
||||
}
|
||||
|
||||
function fetchCommits(url: string, onError?: () => void) {
|
||||
fetch(url, {
|
||||
headers: {
|
||||
'Host': 'api.github.com',
|
||||
'Referer': 'https://api.github.com'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
devCommits.value = data.map((commit: any) => ({
|
||||
sha: commit.sha,
|
||||
date: new Date(commit.commit.author.date).toLocaleString(),
|
||||
message: commit.commit.message
|
||||
}));
|
||||
devCommits.value = Array.isArray(data)
|
||||
? data.map((commit: any) => ({
|
||||
sha: commit.sha,
|
||||
date: new Date(commit.commit.author.date).toLocaleString(),
|
||||
message: commit.commit.message
|
||||
}))
|
||||
: [];
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
if (onError) {
|
||||
onError();
|
||||
} else {
|
||||
console.log('获取开发版提交信息失败:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fetchCommits(commits_url, () => {
|
||||
if (proxy !== '' && commits_url !== originalUrl) {
|
||||
console.log('使用代理请求失败,尝试直接请求');
|
||||
fetchCommits(originalUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function switchVersion(version: string) {
|
||||
@@ -305,7 +335,7 @@ commonStore.getStartTime();
|
||||
</v-btn>
|
||||
|
||||
<!-- 更新对话框 -->
|
||||
<v-dialog v-model="updateStatusDialog" :width="$vuetify.display.smAndDown ? '100%' : '1000'" :fullscreen="$vuetify.display.xs">
|
||||
<v-dialog v-model="updateStatusDialog" :width="$vuetify.display.smAndDown ? '100%' : '1200'" :fullscreen="$vuetify.display.xs">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn size="small" @click="checkUpdate(); getReleases(); getDevCommits();" class="action-btn"
|
||||
color="var(--v-theme-surface)" variant="flat" rounded="sm" v-bind="props">
|
||||
@@ -335,8 +365,7 @@ commonStore.getStartTime();
|
||||
</div>
|
||||
|
||||
<div class="mb-4 mt-4">
|
||||
<small>{{ t('core.header.updateDialog.tip') }} <a
|
||||
href="https://github.com/Soulter/AstrBot/releases">{{ t('core.header.updateDialog.tipLink') }}</a>
|
||||
<small>{{ t('core.header.updateDialog.tip') }}
|
||||
{{ t('core.header.updateDialog.tipContinue') }}</small>
|
||||
</div>
|
||||
|
||||
@@ -348,20 +377,49 @@ commonStore.getStartTime();
|
||||
|
||||
<!-- 发行版 -->
|
||||
<v-tabs-window-item key="0" v-show="tab == 0">
|
||||
<v-btn class="mt-4 mb-4" @click="switchVersion('latest')" color="primary" style="border-radius: 10px;"
|
||||
:disabled="!hasNewVersion">
|
||||
{{ t('core.header.updateDialog.updateToLatest') }}
|
||||
</v-btn>
|
||||
<div class="mb-4">
|
||||
<small>{{ t('core.header.updateDialog.dockerTip') }} <a
|
||||
href="https://containrrr.dev/watchtower/usage-overview/">{{ t('core.header.updateDialog.dockerTipLink') }}</a> {{ t('core.header.updateDialog.dockerTipContinue') }}</small>
|
||||
</div>
|
||||
|
||||
<v-data-table :headers="releasesHeader" :items="releases" item-key="name">
|
||||
<v-alert
|
||||
v-if="releases.some(item => isPreRelease(item['tag_name']))"
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
border="start"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon>mdi-alert-circle-outline</v-icon>
|
||||
</template>
|
||||
<div class="text-body-2">
|
||||
<strong>{{ t('core.header.updateDialog.preReleaseWarning.title') }}</strong>
|
||||
<br>
|
||||
{{ t('core.header.updateDialog.preReleaseWarning.description') }}
|
||||
<a href="https://github.com/Soulter/AstrBot/issues" target="_blank" class="text-decoration-none">
|
||||
{{ t('core.header.updateDialog.preReleaseWarning.issueLink') }}
|
||||
</a>
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<v-data-table :headers="releasesHeader" :items="releases" item-key="name" :items-per-page="5">
|
||||
<template v-slot:item.tag_name="{ item }: { item: { tag_name: string } }">
|
||||
<div class="d-flex align-center">
|
||||
<span>{{ item.tag_name }}</span>
|
||||
<v-chip
|
||||
v-if="isPreRelease(item.tag_name)"
|
||||
size="x-small"
|
||||
color="warning"
|
||||
variant="tonal"
|
||||
class="ml-2"
|
||||
>
|
||||
{{ t('core.header.updateDialog.preRelease') }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:item.body="{ item }: { item: { body: string } }">
|
||||
<v-tooltip :text="item.body">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" rounded="xl" variant="tonal" color="primary" size="small">{{ t('core.header.updateDialog.table.view') }}</v-btn>
|
||||
<v-btn v-bind="props" rounded="xl" variant="tonal" color="primary" size="x-small">{{ t('core.header.updateDialog.table.view') }}</v-btn>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
|
||||
@@ -233,6 +233,7 @@
|
||||
:iterable="newSelectedProviderConfig"
|
||||
:metadata="metadata['provider_group']?.metadata"
|
||||
metadataKey="provider"
|
||||
:is-editing="updatingMode"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
||||
@@ -549,6 +550,7 @@ export default {
|
||||
'ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg',
|
||||
'google': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
|
||||
'deepseek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
|
||||
'modelscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/modelscope.svg',
|
||||
'zhipu': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
|
||||
'siliconflow': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
|
||||
'moonshot': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg',
|
||||
|
||||
@@ -480,7 +480,7 @@
|
||||
|
||||
<!-- 会话命名编辑对话框 -->
|
||||
<v-dialog v-model="nameEditDialog" max-width="500" min-height="60%">
|
||||
<v-card v-if="selectedSessionForName" rounded="12">
|
||||
<v-card v-if="selectedSessionForName">
|
||||
<v-card-title class="bg-primary text-white py-3 px-4" style="display: flex; align-items: center;">
|
||||
<v-icon color="white" class="me-2">mdi-rename-box</v-icon>
|
||||
<span>{{ tm('nameEditor.title') }}</span>
|
||||
@@ -490,8 +490,9 @@
|
||||
</v-btn>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4">
|
||||
<v-text-field
|
||||
<v-card-text>
|
||||
<div style="padding-left: 16px; padding-right: 16px;">
|
||||
<v-text-field
|
||||
v-model="newSessionName"
|
||||
:label="tm('nameEditor.customName')"
|
||||
:placeholder="tm('nameEditor.placeholder')"
|
||||
@@ -518,6 +519,7 @@
|
||||
>
|
||||
{{ tm('nameEditor.hint') }}
|
||||
</v-alert>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="px-4 pb-4">
|
||||
|
||||
6
main.py
6
main.py
@@ -44,10 +44,10 @@ async def check_dashboard_files():
|
||||
if v is not None:
|
||||
# has file
|
||||
if v == f"v{VERSION}":
|
||||
logger.info("管理面板文件已是最新。")
|
||||
logger.info("WebUI 版本已是最新。")
|
||||
else:
|
||||
logger.warning(
|
||||
"检测到管理面板有更新。可以使用 /dashboard_update 命令更新。"
|
||||
f"检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符。"
|
||||
)
|
||||
return
|
||||
|
||||
@@ -56,7 +56,7 @@ async def check_dashboard_files():
|
||||
)
|
||||
|
||||
try:
|
||||
await download_dashboard()
|
||||
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||
except Exception as e:
|
||||
logger.critical(f"下载管理面板文件失败: {e}。")
|
||||
return
|
||||
|
||||
@@ -1119,7 +1119,7 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
@filter.command("dashboard_update")
|
||||
async def update_dashboard(self, event: AstrMessageEvent):
|
||||
yield event.plain_result("正在尝试更新管理面板...")
|
||||
await download_dashboard()
|
||||
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||
yield event.plain_result("管理面板更新完成。")
|
||||
|
||||
@filter.command("set")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "3.5.23"
|
||||
version = "3.5.27"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
@@ -76,3 +76,11 @@ lint.select = [
|
||||
"Q", # flake8-quotes
|
||||
]
|
||||
target-version = "py310"
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pytest>=8.4.1",
|
||||
"pytest-asyncio>=1.1.0",
|
||||
"pytest-cov>=6.2.1",
|
||||
"ruff>=0.12.8",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user