Compare commits

..

1 Commits

Author SHA1 Message Date
Soulter d1759ca2ed docs: revise description for AstrBot in README.md 2026-02-05 13:35:49 +08:00
387 changed files with 3688 additions and 26194 deletions
+14 -12
View File
@@ -1,40 +1,42 @@
name: '🎉 Feature Request / 功能建议' name: '🎉 功能建议'
title: "[Feature]" title: "[Feature]"
description: Submit a suggestion to help us improve. / 提交建议帮助我们改进。 description: 提交建议帮助我们改进。
labels: [ "enhancement" ] labels: [ "enhancement" ]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Thank you for taking the time to suggest a new feature! Please explain your idea clearly and accurately. / 感谢您抽出时间提出新功能建议,请准确解释您的想法。 感谢您抽出时间提出新功能建议,请准确解释您的想法。
- type: textarea - type: textarea
attributes: attributes:
label: Description / 描述 label: 描述
description: Please describe the feature you want to be added in detail. / 请详细描述您希望添加的功能。 description: 简短描述您的功能建议
- type: textarea - type: textarea
attributes: attributes:
label: Use Case / 使用场景 label: 使用场景
description: Please describe the use case for this feature. / 请描述这个功能的使用场景。 description: 你想要发生什么?
placeholder: >
一个清晰且具体的描述这个功能的使用场景。
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Willing to Submit PR? / 是否愿意提交PR label: 愿意提交PR吗?
description: > description: >
This is not required, but if you are willing to submit a PR to implement this feature, it would be greatly appreciated! / 这不是必的,但如果您愿意提交 PR 来实现这个功能,我们将不胜感激! 这不是必的,但我们欢迎您的贡献。
options: options:
- label: Yes, I am willing to submit a PR. / 是的,我愿意提交 PR - label: 是的, 我愿意提交PR!
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Code of Conduct label: Code of Conduct
options: options:
- label: > - label: >
I have read and agree to abide by the project's [Code of Conduct](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct). / 我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)
required: true required: true
- type: markdown - type: markdown
attributes: attributes:
value: "Thank you for filling out our form!" value: "感谢您填写我们的表单!"
+92
View File
@@ -0,0 +1,92 @@
on:
push:
tags:
- 'v*'
workflow_dispatch:
name: Auto Release
jobs:
build-and-publish-to-github-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Dashboard Build
run: |
cd dashboard
npm install
npm run build
echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
echo ${{ github.ref_name }} > dist/assets/version
zip -r dist.zip dist
- name: Upload to Cloudflare R2
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_BUCKET_NAME: "astrbot"
R2_OBJECT_NAME: "astrbot-webui-latest.zip"
VERSION_TAG: ${{ github.ref_name }}
run: |
echo "Installing rclone..."
curl https://rclone.org/install.sh | sudo bash
echo "Configuring rclone remote..."
mkdir -p ~/.config/rclone
cat <<EOF > ~/.config/rclone/rclone.conf
[r2]
type = s3
provider = Cloudflare
access_key_id = $R2_ACCESS_KEY_ID
secret_access_key = $R2_SECRET_ACCESS_KEY
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
EOF
echo "Uploading dist.zip to R2 bucket: $R2_BUCKET_NAME/$R2_OBJECT_NAME"
mv dashboard/dist.zip dashboard/$R2_OBJECT_NAME
rclone copy dashboard/$R2_OBJECT_NAME r2:$R2_BUCKET_NAME --progress
mv dashboard/$R2_OBJECT_NAME dashboard/astrbot-webui-${VERSION_TAG}.zip
rclone copy dashboard/astrbot-webui-${VERSION_TAG}.zip r2:$R2_BUCKET_NAME --progress
mv dashboard/astrbot-webui-${VERSION_TAG}.zip dashboard/dist.zip
- name: Fetch Changelog
run: |
echo "changelog=changelogs/${{github.ref_name}}.md" >> "$GITHUB_ENV"
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
bodyFile: ${{ env.changelog }}
artifacts: "dashboard/dist.zip"
build-and-publish-to-pypi:
# 构建并发布到 PyPI
runs-on: ubuntu-latest
needs: build-and-publish-to-github-release
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.10'
- name: Install uv
run: |
python -m pip install uv
- name: Build package
run: |
uv build
- name: Publish to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
uv publish
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: '3.12' python-version: '3.10'
- name: Install UV - name: Install UV
run: pip install uv run: pip install uv
+2 -2
View File
@@ -16,7 +16,7 @@ jobs:
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: '24.13.0' node-version: 'latest'
- name: npm install, build - name: npm install, build
run: | run: |
@@ -52,4 +52,4 @@ jobs:
repo: astrbot-release-harbour repo: astrbot-release-harbour
body: "Automated release from commit ${{ github.sha }}" body: "Automated release from commit ${{ github.sha }}"
token: ${{ secrets.ASTRBOT_HARBOUR_TOKEN }} token: ${{ secrets.ASTRBOT_HARBOUR_TOKEN }}
artifacts: "dashboard/dist.zip" artifacts: "dashboard/dist.zip"
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
GHCR_OWNER: astrbotdevs GHCR_OWNER: soulter
HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }} HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }}
steps: steps:
@@ -113,7 +113,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
GHCR_OWNER: astrbotdevs GHCR_OWNER: soulter
HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }} HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }}
steps: steps:
-212
View File
@@ -1,212 +0,0 @@
name: Release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
ref:
description: "Git ref to build (branch/tag/SHA)"
required: false
default: "master"
tag:
description: "Release tag to publish assets to (for example: v4.14.6)"
required: false
permissions:
contents: write
jobs:
build-dashboard:
name: Build Dashboard
runs-on: ubuntu-24.04
env:
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.ref }}
- name: Resolve tag
id: tag
shell: bash
run: |
if [ "${{ github.event_name }}" = "push" ]; then
tag="${GITHUB_REF_NAME}"
elif [ -n "${{ inputs.tag }}" ]; then
tag="${{ inputs.tag }}"
else
tag="$(git describe --tags --abbrev=0)"
fi
if [ -z "$tag" ]; then
echo "Failed to resolve tag." >&2
exit 1
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.28.2
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.13.0'
cache: "pnpm"
cache-dependency-path: dashboard/pnpm-lock.yaml
- name: Build dashboard dist
shell: bash
run: |
pnpm --dir dashboard install --frozen-lockfile
pnpm --dir dashboard run build
echo "${{ steps.tag.outputs.tag }}" > dashboard/dist/assets/version
cd dashboard
zip -r "AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip" dist
- name: Upload dashboard artifact
uses: actions/upload-artifact@v6
with:
name: Dashboard-${{ steps.tag.outputs.tag }}
if-no-files-found: error
path: dashboard/AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip
- name: Upload dashboard package to Cloudflare R2
if: ${{ env.R2_ACCOUNT_ID != '' && env.R2_ACCESS_KEY_ID != '' && env.R2_SECRET_ACCESS_KEY != '' }}
env:
R2_BUCKET_NAME: "astrbot"
R2_OBJECT_NAME: "astrbot-webui-latest.zip"
VERSION_TAG: ${{ steps.tag.outputs.tag }}
shell: bash
run: |
curl https://rclone.org/install.sh | sudo bash
mkdir -p ~/.config/rclone
cat <<EOF > ~/.config/rclone/rclone.conf
[r2]
type = s3
provider = Cloudflare
access_key_id = $R2_ACCESS_KEY_ID
secret_access_key = $R2_SECRET_ACCESS_KEY
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
EOF
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/${R2_OBJECT_NAME}"
rclone copy "dashboard/${R2_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/astrbot-webui-${VERSION_TAG}.zip"
rclone copy "dashboard/astrbot-webui-${VERSION_TAG}.zip" "r2:${R2_BUCKET_NAME}" --progress
publish-release:
name: Publish GitHub Release
runs-on: ubuntu-24.04
needs:
- build-dashboard
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.ref }}
- name: Resolve tag
id: tag
shell: bash
run: |
if [ "${{ github.event_name }}" = "push" ]; then
tag="${GITHUB_REF_NAME}"
elif [ -n "${{ inputs.tag }}" ]; then
tag="${{ inputs.tag }}"
else
tag="$(git describe --tags --abbrev=0)"
fi
if [ -z "$tag" ]; then
echo "Failed to resolve tag." >&2
exit 1
fi
echo "tag=$tag" >> "$GITHUB_OUTPUT"
- name: Download dashboard artifact
uses: actions/download-artifact@v7
with:
name: Dashboard-${{ steps.tag.outputs.tag }}
path: release-assets
- name: Resolve release notes
id: notes
shell: bash
run: |
note_file="changelogs/${{ steps.tag.outputs.tag }}.md"
if [ ! -f "$note_file" ]; then
note_file="$(mktemp)"
echo "Release ${{ steps.tag.outputs.tag }}" > "$note_file"
fi
echo "file=$note_file" >> "$GITHUB_OUTPUT"
- name: Ensure release exists
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
tag="${{ steps.tag.outputs.tag }}"
if ! gh release view "$tag" >/dev/null 2>&1; then
gh release create "$tag" --title "$tag" --notes-file "${{ steps.notes.outputs.file }}"
fi
- name: Remove stale assets from release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
tag="${{ steps.tag.outputs.tag }}"
while IFS= read -r asset; do
case "$asset" in
*.AppImage|*.dmg|*.zip|*.exe|*.blockmap)
gh release delete-asset "$tag" "$asset" -y || true
;;
esac
done < <(gh release view "$tag" --json assets --jq '.assets[].name')
- name: Upload assets to release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
tag="${{ steps.tag.outputs.tag }}"
gh release upload "$tag" release-assets/* --clobber
publish-pypi:
name: Publish PyPI
runs-on: ubuntu-24.04
needs: publish-release
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ inputs.ref || github.ref }}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Install uv
shell: bash
run: python -m pip install uv
- name: Build package
shell: bash
run: uv build
- name: Publish to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
shell: bash
run: uv publish
+2 -2
View File
@@ -32,8 +32,8 @@ tests/astrbot_plugin_openai
# Dashboard # Dashboard
dashboard/node_modules/ dashboard/node_modules/
dashboard/dist/ dashboard/dist/
.pnpm-store/
package-lock.json package-lock.json
package.json
yarn.lock yarn.lock
# Operating System # Operating System
@@ -53,4 +53,4 @@ IFLOW.md
# genie_tts data # genie_tts data
CharacterModels/ CharacterModels/
GenieData/ GenieData/
+1 -1
View File
@@ -1 +1 @@
3.12 3.10
-1
View File
@@ -26,7 +26,6 @@ Runs on `http://localhost:3000` by default.
3. After finishing, use `ruff format .` and `ruff check .` to format and check the code. 3. After finishing, use `ruff format .` and `ruff check .` to format and check the code.
4. When committing, ensure to use conventional commits messages, such as `feat: add new agent for data analysis` or `fix: resolve bug in provider manager`. 4. When committing, ensure to use conventional commits messages, such as `feat: add new agent for data analysis` or `fix: resolve bug in provider manager`.
5. Use English for all new comments. 5. Use English for all new comments.
6. For path handling, use `pathlib.Path` instead of string paths, and use `astrbot.core.utils.path_utils` to get the AstrBot data and temp directory.
## PR instructions ## PR instructions
+8 -8
View File
@@ -1,4 +1,4 @@
FROM python:3.12-slim FROM python:3.11-slim
WORKDIR /AstrBot WORKDIR /AstrBot
COPY . /AstrBot/ COPY . /AstrBot/
@@ -15,17 +15,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
gnupg \ gnupg \
git \ git \
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN apt-get update && apt-get install -y curl gnupg \
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y nodejs
RUN python -m pip install uv \ RUN python -m pip install uv \
&& echo "3.12" > .python-version \ && echo "3.11" > .python-version
&& uv lock \ RUN uv pip install -r requirements.txt --no-cache-dir --system
&& uv export --format requirements.txt --output-file requirements.txt --frozen \ RUN uv pip install socksio uv pilk --no-cache-dir --system
&& uv pip install -r requirements.txt --no-cache-dir --system \
&& uv pip install socksio uv pilk --no-cache-dir --system
EXPOSE 6185 EXPOSE 6185
-14
View File
@@ -1,14 +0,0 @@
## Welcome to AstrBot
🌟 Thank you for using AstrBot!
AstrBot is an Agentic AI assistant for personal and group chats, with support for multiple IM platforms and a wide range of built-in features. We hope it brings you an efficient and enjoyable experience. ❤️
Important notice:
AstrBot is a **free and open-source software project** protected by the AGPLv3 license. You can find the full source code and related resources on our [**official website**](https://astrbot.app) and [**GitHub**](https://github.com/astrbotdevs/astrbot).
As of now, AstrBot has **no commercial services of any kind**, and the official team **will never charge users any fees** under any name.
If anyone asks you to pay while using AstrBot, **you are likely being scammed**. Please request a refund immediately and report it to us by email.
📮 Official email: [community@astrbot.app](mailto:community@astrbot.app)
-14
View File
@@ -1,14 +0,0 @@
## 欢迎使用 AstrBot
🌟 感谢您使用 AstrBot
AstrBot 是一款可接入多种 IM 平台的 Agentic AI 个人 / 群聊助手,内置多项强大功能,希望能为您带来高效、愉快的使用体验。❤️
我们想特别说明:
AstrBot 是受 AGPLv3 开源协议保护的**免费开源软件项目**,您可以在[**官方网站**](https://astrbot.app)、[**GitHub**](https://github.com/astrbotdevs/astrbot) 上找到 AstrBot 的全部源代码及相关资源。
截至目前,AstrBot 项目**未开展任何形式的商业化服务**,官方**不会以任何名义向用户收取费用**。
如果您在使用 AstrBot 的过程中被要求付费,**表明您已经遭遇诈骗行为**。请立即向相关方申请退款,并及时通过邮件向我们反馈。
📮 官方邮箱:[community@astrbot.app](mailto:community@astrbot.app)
+18 -50
View File
@@ -2,6 +2,7 @@
<div align="center"> <div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> <a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> <a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> <a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a>
@@ -33,21 +34,21 @@
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a> <a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
</div> </div>
AstrBot 是一个开源的一站式 Agentic 个人群聊助手可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。 AstrBot 是一个易用、高性能的 AI Agentic 个人 / 群聊助手可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。
![521771166-00782c4c-4437-4d97-aabc-605e3738da5c (1)](https://github.com/user-attachments/assets/61e7b505-f7db-41aa-a75f-4ef8f079b8ba) ![521771166-00782c4c-4437-4d97-aabc-605e3738da5c (1)](https://github.com/user-attachments/assets/61e7b505-f7db-41aa-a75f-4ef8f079b8ba)
## 主要功能 ## 主要功能
1. 💯 免费 & 开源。 1. 💯 免费 & 开源。
2. ✨ AI 大模型对话,多模态,Agent,MCP,Skills,知识库,人格设定,自动压缩对话。 1. ✨ AI 大模型对话,多模态,Agent,MCP,Skills,知识库,人格设定,自动压缩对话。
3. 🤖 支持接入 Dify、阿里云百炼、Coze 等智能体平台。 2. 🤖 支持接入 Dify、阿里云百炼、Coze 等智能体平台。
4. 🌐 多平台,支持 QQ、企业微信、飞书、钉钉、微信公众号、Telegram、Slack 以及[更多](#支持的消息平台)。 2. 🌐 多平台,支持 QQ、企业微信、飞书、钉钉、微信公众号、Telegram、Slack 以及[更多](#支持的消息平台)。
5. 📦 插件扩展,已有近 800 个插件可一键安装。 3. 📦 插件扩展,已有近 800 个插件可一键安装。
6. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) 隔离化环境,安全地执行任何代码、调用 Shell、会话级资源复用。 5. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) 隔离化环境,安全地执行任何代码、调用 Shell、会话级资源复用。
7. 💻 WebUI 支持。 6. 💻 WebUI 支持。
8. 🌈 Web ChatUI 支持,ChatUI 内置代理沙盒、网页搜索等。 7. 🌈 Web ChatUI 支持,ChatUI 内置代理沙盒、网页搜索等。
9. 🌐 国际化(i18n)支持。 8. 🌐 国际化(i18n)支持。
<br> <br>
@@ -77,20 +78,9 @@ AstrBot 是一个开源的一站式 Agentic 个人和群聊助手,可在 QQ、
#### uv 部署 #### uv 部署
```bash ```bash
uv tool install astrbot uvx astrbot
astrbot
``` ```
#### 桌面应用部署(Tauri
桌面应用仓库 [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop)。
支持多系统架构,安装包直接安装,开箱即用,最适合新手和懒人的一键桌面部署方案,不推荐服务器场景。
#### 启动器一键部署(AstrBot Launcher
快速部署和多开方案,实现环境隔离,进入 [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) 仓库,在 Releases 页最新版本下找到对应的系统安装包安装即可。
#### 宝塔面板部署 #### 宝塔面板部署
AstrBot 与宝塔面板合作,已上架至宝塔面板。 AstrBot 与宝塔面板合作,已上架至宝塔面板。
@@ -142,22 +132,11 @@ uv run main.py
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。 或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
#### 系统包管理器安装
##### Arch Linux
```bash
yay -S astrbot-git
# 或者使用 paru
paru -S astrbot-git
```
## 支持的消息平台 ## 支持的消息平台
**官方维护** **官方维护**
- QQ - QQ (官方平台 & OneBot)
- OneBot v11 协议实现
- Telegram - Telegram
- 企微应用 & 企微智能机器人 - 企微应用 & 企微智能机器人
- 微信客服 & 微信公众号 - 微信客服 & 微信公众号
@@ -165,10 +144,10 @@ paru -S astrbot-git
- 钉钉 - 钉钉
- Slack - Slack
- Discord - Discord
- LINE
- Satori - Satori
- Misskey - Misskey
- Whatsapp (将支持) - Whatsapp (将支持)
- LINE (将支持)
**社区维护** **社区维护**
@@ -188,7 +167,6 @@ paru -S astrbot-git
- DeepSeek - DeepSeek
- Ollama (本地部署) - Ollama (本地部署)
- LM Studio (本地部署) - LM Studio (本地部署)
- [AIHubMix](https://aihubmix.com/?aff=4bfH)
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) - [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
- [302.AI](https://share.302.ai/rr1M3l) - [302.AI](https://share.302.ai/rr1M3l)
- [小马算力](https://www.tokenpony.cn/3YPyf) - [小马算力](https://www.tokenpony.cn/3YPyf)
@@ -264,23 +242,13 @@ pre-commit install
特别感谢所有 Contributors 和插件开发者对 AstrBot 的贡献 ❤️ 特别感谢所有 Contributors 和插件开发者对 AstrBot 的贡献 ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors"> <a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" /> <img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
</a> </a>
此外,本项目的诞生离不开以下开源项目的帮助: 此外,本项目的诞生离不开以下开源项目的帮助:
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 伟大的猫猫框架 - [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 伟大的猫猫框架
开源项目友情链接:
- [NoneBot2](https://github.com/nonebot/nonebot2) - 优秀的 Python 异步 ChatBot 框架
- [Koishi](https://github.com/koishijs/koishi) - 优秀的 Node.js ChatBot 框架
- [MaiBot](https://github.com/Mai-with-u/MaiBot) - 优秀的拟人化 AI ChatBot
- [nekro-agent](https://github.com/KroMiose/nekro-agent) - 优秀的 Agent ChatBot
- [LangBot](https://github.com/langbot-app/LangBot) - 优秀的多平台 AI ChatBot
- [ChatLuna](https://github.com/ChatLunaLab/chatluna) - 优秀的多平台 AI ChatBot Koishi 插件
- [Operit AI](https://github.com/AAswordman/Operit) - 优秀的 AI 智能助手 Android APP
## ⭐ Star History ## ⭐ Star History
> [!TIP] > [!TIP]
@@ -292,12 +260,12 @@ pre-commit install
</div> </div>
<div align="center"> </details>
_陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。_ <div align="center">
_私は、高性能ですから!_ _私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/> 陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。
</div>
+7 -48
View File
@@ -3,6 +3,7 @@
<div align="center"> <div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> <a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> <a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> <a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> <a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
@@ -51,23 +52,6 @@ AstrBot is an open-source all-in-one Agent chatbot platform that integrates with
8. 🌈 Web ChatUI Support with built-in agent sandbox and web search. 8. 🌈 Web ChatUI Support with built-in agent sandbox and web search.
9. 🌐 Internationalization (i18n) Support. 9. 🌐 Internationalization (i18n) Support.
<br>
<table align="center">
<tr align="center">
<th>💙 Role-playing & Emotional Companionship</th>
<th>✨ Proactive Agent</th>
<th>🚀 General Agentic Capabilities</th>
<th>🧩 900+ Community Plugins</th>
</tr>
<tr>
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
</tr>
</table>
## Quick Start ## Quick Start
#### Docker Deployment (Recommended 🥳) #### Docker Deployment (Recommended 🥳)
@@ -79,18 +63,7 @@ Please refer to the official documentation: [Deploy AstrBot with Docker](https:/
#### uv Deployment #### uv Deployment
```bash ```bash
uv tool install astrbot uvx astrbot
astrbot
```
#### System Package Manager Installation
##### Arch Linux
```bash
yay -S astrbot-git
# or use paru
paru -S astrbot-git
``` ```
#### BT-Panel Deployment #### BT-Panel Deployment
@@ -144,20 +117,6 @@ uv run main.py
Or refer to the official documentation: [Deploy AstrBot from Source](https://astrbot.app/deploy/astrbot/cli.html). Or refer to the official documentation: [Deploy AstrBot from Source](https://astrbot.app/deploy/astrbot/cli.html).
#### System Package Manager Installation
##### Arch Linux
```bash
yay -S astrbot-git
# or use paru
paru -S astrbot-git
```
#### Desktop (Tauri)
Desktop packaging has moved to a standalone Tauri repository: [https://github.com/AstrBotDevs/AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop).
## Supported Messaging Platforms ## Supported Messaging Platforms
**Officially Maintained** **Officially Maintained**
@@ -172,8 +131,8 @@ Desktop packaging has moved to a standalone Tauri repository: [https://github.co
- Discord - Discord
- Satori - Satori
- Misskey - Misskey
- LINE
- WhatsApp (Coming Soon) - WhatsApp (Coming Soon)
- LINE (Coming Soon)
**Community Maintained** **Community Maintained**
@@ -196,7 +155,7 @@ Desktop packaging has moved to a standalone Tauri repository: [https://github.co
- [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) - [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
- [302.AI](https://share.302.ai/rr1M3l) - [302.AI](https://share.302.ai/rr1M3l)
- [TokenPony](https://www.tokenpony.cn/3YPyf) - [TokenPony](https://www.tokenpony.cn/3YPyf)
- [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) - [SiliconFlow](https://docs.siliconflow.cn/cn/usecases/use-siliconcloud-in-astrbot)
- [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE) - [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE)
- ModelScope - ModelScope
- OneAPI - OneAPI
@@ -268,7 +227,7 @@ pre-commit install
Special thanks to all Contributors and plugin developers for their contributions to AstrBot ❤️ Special thanks to all Contributors and plugin developers for their contributions to AstrBot ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors"> <a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" /> <img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
</a> </a>
Additionally, the birth of this project would not have been possible without the help of the following open-source projects: Additionally, the birth of this project would not have been possible without the help of the following open-source projects:
@@ -286,9 +245,9 @@ Additionally, the birth of this project would not have been possible without the
</div> </div>
<div align="center"> </details>
_Companionship and capability should never be at odds. What we aim to create is a robot that can understand emotions, provide genuine companionship, and reliably accomplish tasks._ <div align="center">
_私は、高性能ですから!_ _私は、高性能ですから!_
+25 -69
View File
@@ -1,12 +1,8 @@
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) ![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
<div align="center"> </p>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> <div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
<br> <br>
@@ -18,17 +14,22 @@
<br> <br>
<div> <div>
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest"> <img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?style=for-the-badge&color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest">
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python"> <img src="https://img.shields.io/badge/python-3.10+-blue.svg?style=for-the-badge&color=76bad9" alt="python">
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot"> <a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?style=for-the-badge&color=76bad9"/></a>
<a href="https://zread.ai/AstrBotDevs/AstrBot" target="_blank"><img src="https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></a> <a href="https://qm.qq.com/cgi-bin/qm/qr?k=wtbaNx7EioxeaqS9z7RQWVXPIxg2zYr7&jump_from=webapi&authKey=vlqnv/AV2DbJEvGIcxdlNSpfxVy+8vVqijgreRdnVKOaydpc+YSw4MctmEbr0k5"><img alt="QQ_community" src="https://img.shields.io/badge/QQ群-775869627-purple?style=for-the-badge&color=76bad9"></a>
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?color=76bad9"/></a> <a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E4%B8%AA&label=%E6%8F%92%E4%BB%B6%E5%B8%82%E5%9C%BA&cacheSeconds=3600"> <img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20plugins&style=for-the-badge&label=Marketplace&cacheSeconds=3600">
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
</div> </div>
<br> <br>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
<a href="https://astrbot.app/">Documentation</a> <a href="https://astrbot.app/">Documentation</a>
<a href="https://blog.astrbot.app/">Blog</a> <a href="https://blog.astrbot.app/">Blog</a>
<a href="https://astrbot.featurebase.app/roadmap">Feuille de route</a> <a href="https://astrbot.featurebase.app/roadmap">Feuille de route</a>
@@ -42,31 +43,12 @@ AstrBot est une plateforme de chatbot Agent tout-en-un open source qui s'intègr
## Fonctionnalités principales ## Fonctionnalités principales
1. 💯 Gratuit & Open Source. 1. 💯 Gratuit & Open Source.
2.Dialogue avec de grands modèles d'IA, multimodal, Agent, MCP, Skills, Base de connaissances, Paramétrage de personnalité, compression automatique des dialogues. 2.Conversations avec LLM IA, Multimodal, Agent, MCP, Base de connaissances, Paramètres de personnalité.
3. 🤖 Prise en charge de l'accès aux plateformes d'Agents telles que Dify, Alibaba Cloud Bailian, Coze, etc. 3. 🤖 Prise en charge de l'intégration avec Dify, Alibaba Cloud Bailian, Coze et autres plateformes d'agents.
4. 🌐 Multiplateforme : supporte QQ, WeChat Enterprise, Feishu, DingTalk, Comptes officiels WeChat, Telegram, Slack et [plus encore](#plateformes-de-messagerie-prises-en-charge). 4. 🌐 Multi-plateforme : QQ, WeChat Work, Feishu, DingTalk, Comptes officiels WeChat, Telegram, Slack, et [plus encore](#plateformes-de-messagerie-prises-en-charge).
5. 📦 Extension par plugins, avec près de 800 plugins déjà disponibles pour une installation en un clic. 5. 📦 Extensions de plugins avec près de 800 plugins disponibles pour une installation en un clic.
6. 🛡️ Environnement isolé [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) : exécution sécurisée de code, appels Shell et réutilisation des ressources au niveau de la session. 6. 💻 Support WebUI.
7. 💻 Support WebUI. 7. 🌐 Support de l'internationalisation (i18n).
8. 🌈 Support Web ChatUI, avec sandbox d'agent intégrée, recherche web, etc.
9. 🌐 Support de l'internationalisation (i18n).
<br>
<table align="center">
<tr align="center">
<th>💙 Jeux de rôle & Accompagnement émotionnel</th>
<th>✨ Agent proactif</th>
<th>🚀 Capacités agentiques générales</th>
<th>🧩 900+ Plugins de communauté</th>
</tr>
<tr>
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
</tr>
</table>
## Démarrage rapide ## Démarrage rapide
@@ -79,18 +61,7 @@ Veuillez consulter la documentation officielle : [Déployer AstrBot avec Docker]
#### Déploiement uv #### Déploiement uv
```bash ```bash
uv tool install astrbot uvx astrbot
astrbot
```
#### Installation via le gestionnaire de paquets du système
##### Arch Linux
```bash
yay -S astrbot-git
# ou utiliser paru
paru -S astrbot-git
``` ```
#### Déploiement BT-Panel #### Déploiement BT-Panel
@@ -144,16 +115,6 @@ uv run main.py
Ou consultez la documentation officielle : [Déployer AstrBot depuis les sources](https://astrbot.app/deploy/astrbot/cli.html). Ou consultez la documentation officielle : [Déployer AstrBot depuis les sources](https://astrbot.app/deploy/astrbot/cli.html).
#### Установка через системный пакетный менеджер
##### Arch Linux
```bash
yay -S astrbot-git
# или используйте paru
paru -S astrbot-git
```
## Plateformes de messagerie prises en charge ## Plateformes de messagerie prises en charge
**Maintenues officiellement** **Maintenues officiellement**
@@ -168,8 +129,8 @@ paru -S astrbot-git
- Discord - Discord
- Satori - Satori
- Misskey - Misskey
- LINE
- WhatsApp (Bientôt disponible) - WhatsApp (Bientôt disponible)
- LINE (Bientôt disponible)
**Maintenues par la communauté** **Maintenues par la communauté**
@@ -192,7 +153,7 @@ paru -S astrbot-git
- [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) - [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
- [302.AI](https://share.302.ai/rr1M3l) - [302.AI](https://share.302.ai/rr1M3l)
- [TokenPony](https://www.tokenpony.cn/3YPyf) - [TokenPony](https://www.tokenpony.cn/3YPyf)
- [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) - [SiliconFlow](https://docs.siliconflow.cn/cn/usecases/use-siliconcloud-in-astrbot)
- [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE) - [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE)
- ModelScope - ModelScope
- OneAPI - OneAPI
@@ -262,7 +223,7 @@ pre-commit install
Un grand merci à tous les contributeurs et développeurs de plugins pour leurs contributions à AstrBot ❤️ Un grand merci à tous les contributeurs et développeurs de plugins pour leurs contributions à AstrBot ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors"> <a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" /> <img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
</a> </a>
De plus, la naissance de ce projet n'aurait pas été possible sans l'aide des projets open source suivants : De plus, la naissance de ce projet n'aurait pas été possible sans l'aide des projets open source suivants :
@@ -280,12 +241,7 @@ De plus, la naissance de ce projet n'aurait pas été possible sans l'aide des p
</div> </div>
<div align="center"> </details>
_La compagnie et la capacité ne devraient jamais être des opposés. Nous souhaitons créer un robot capable à la fois de comprendre les émotions, d'offrir de la présence, et d'accomplir des tâches de manière fiable._
_私は、高性能ですから!_ _私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div>
+24 -69
View File
@@ -1,12 +1,8 @@
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) ![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
<div align="center"> </p>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> <div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
<br> <br>
@@ -18,17 +14,22 @@
<br> <br>
<div> <div>
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest"> <img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?style=for-the-badge&color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest">
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python"> <img src="https://img.shields.io/badge/python-3.10+-blue.svg?style=for-the-badge&color=76bad9" alt="python">
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot"> <a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?style=for-the-badge&color=76bad9"/></a>
<a href="https://zread.ai/AstrBotDevs/AstrBot" target="_blank"><img src="https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></a> <a href="https://qm.qq.com/cgi-bin/qm/qr?k=wtbaNx7EioxeaqS9z7RQWVXPIxg2zYr7&jump_from=webapi&authKey=vlqnv/AV2DbJEvGIcxdlNSpfxVy+8vVqijgreRdnVKOaydpc+YSw4MctmEbr0k5"><img alt="QQ_community" src="https://img.shields.io/badge/QQ群-775869627-purple?style=for-the-badge&color=76bad9"></a>
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?color=76bad9"/></a> <a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E4%B8%AA&label=%E6%8F%92%E4%BB%B6%E5%B8%82%E5%9C%BA&cacheSeconds=3600"> <img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E5%80%8B&style=for-the-badge&label=%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3&cacheSeconds=3600">
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
</div> </div>
<br> <br>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
<a href="https://astrbot.app/">ドキュメント</a> <a href="https://astrbot.app/">ドキュメント</a>
<a href="https://blog.astrbot.app/">Blog</a> <a href="https://blog.astrbot.app/">Blog</a>
<a href="https://astrbot.featurebase.app/roadmap">ロードマップ</a> <a href="https://astrbot.featurebase.app/roadmap">ロードマップ</a>
@@ -42,31 +43,12 @@ AstrBot は、主要なインスタントメッセージングアプリと統合
## 主な機能 ## 主な機能
1. 💯 無料 & オープンソース。 1. 💯 無料 & オープンソース。
2. ✨ AI大規模言語モデル対話、マルチモーダル、Agent、MCP、Skills、ナレッジベース、ペルソナ設定、対話の自動圧縮 2. ✨ AI 大規模言語モデル対話、マルチモーダル、Agent、MCP、ナレッジベース、ペルソナ設定。
3. 🤖 Dify、Alibaba Cloud Bailian(百煉)、Coze などのAgentプラットフォームへの接続をサポート。 3. 🤖 Dify、Alibaba Cloud 百炼、Coze などの Agent プラットフォームとの統合をサポート。
4. 🌐 マルチプラットフォーム:QQ、企業微信(WeCom)、飛書(Lark)、釘釘(DingTalk、WeChat公式アカウント、Telegram、Slack、[その他](#サポートされているメッセージプラットフォーム)に対応 4. 🌐 マルチプラットフォーム:QQ、WeChat Work、Feishu、DingTalk、WeChat 公式アカウント、Telegram、Slack、[その他](#サポートされているメッセージプラットフォーム)。
5. 📦 プラグイン拡張:800近い既存プラグインをワンクリックでインストール可能。 5. 📦 約800個のプラグインをワンクリックでインストール可能なプラグイン拡張機能
6. 🛡️ 隔離環境[Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html):コードの安全な実行、Shell呼び出し、セッションレベルのリソース再利用 6. 💻 WebUI サポート
7. 💻 WebUI 対応 7. 🌐 国際化(i18n)サポート
8. 🌈 Web ChatUI 対応:ChatUI内にAgent Sandboxやウェブ検索などを内蔵。
9. 🌐 多言語対応(i18n)。
<br>
<table align="center">
<tr align="center">
<th>💙 ロールプレイ & 感情的な対話</th>
<th>✨ プロアクティブ・エージェント (Proactive Agent)</th>
<th>🚀 汎用 エージェント的能力</th>
<th>🧩 900+ コミュニティプラグイン</th>
</tr>
<tr>
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
</tr>
</table>
## クイックスタート ## クイックスタート
@@ -79,18 +61,7 @@ Docker / Docker Compose を使用した AstrBot のデプロイを推奨しま
#### uv デプロイ #### uv デプロイ
```bash ```bash
uv tool install astrbot uvx astrbot
astrbot
```
#### システムパッケージマネージャーでのインストール
##### Arch Linux
```bash
yay -S astrbot-git
# または paru を使用
paru -S astrbot-git
``` ```
#### 宝塔パネルデプロイ #### 宝塔パネルデプロイ
@@ -144,16 +115,6 @@ uv run main.py
または、公式ドキュメント [ソースコードから AstrBot をデプロイ](https://astrbot.app/deploy/astrbot/cli.html) をご参照ください。 または、公式ドキュメント [ソースコードから AstrBot をデプロイ](https://astrbot.app/deploy/astrbot/cli.html) をご参照ください。
#### Установка через системный пакетный менеджер
##### Arch Linux
```bash
yay -S astrbot-git
# или используйте paru
paru -S astrbot-git
```
## サポートされているメッセージプラットフォーム ## サポートされているメッセージプラットフォーム
**公式メンテナンス** **公式メンテナンス**
@@ -168,8 +129,8 @@ paru -S astrbot-git
- Discord - Discord
- Satori - Satori
- Misskey - Misskey
- LINE
- WhatsApp (近日対応予定) - WhatsApp (近日対応予定)
- LINE (近日対応予定)
**コミュニティメンテナンス** **コミュニティメンテナンス**
@@ -263,7 +224,7 @@ pre-commit install
AstrBot への貢献をしていただいたすべてのコントリビューターとプラグイン開発者に特別な感謝を ❤️ AstrBot への貢献をしていただいたすべてのコントリビューターとプラグイン開発者に特別な感謝を ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors"> <a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" /> <img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
</a> </a>
また、このプロジェクトの誕生は以下のオープンソースプロジェクトの助けなしには実現できませんでした: また、このプロジェクトの誕生は以下のオープンソースプロジェクトの助けなしには実現できませんでした:
@@ -281,12 +242,6 @@ AstrBot への貢献をしていただいたすべてのコントリビュータ
</div> </div>
<div align="center"> </details>
_共感力と能力は決して対立するものではありません。私たちが目指すのは、感情を理解し、心の支えとなるだけでなく、確実に仕事をこなせるロボットの創造です。_
_私は、高性能ですから!_ _私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div>
+26 -62
View File
@@ -1,12 +1,8 @@
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) ![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
<div align="center"> </p>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> <div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
<br> <br>
@@ -18,17 +14,22 @@
<br> <br>
<div> <div>
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest"> <img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?style=for-the-badge&color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest">
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python"> <img src="https://img.shields.io/badge/python-3.10+-blue.svg?style=for-the-badge&color=76bad9" alt="python">
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot"> <a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?style=for-the-badge&color=76bad9"/></a>
<a href="https://zread.ai/AstrBotDevs/AstrBot" target="_blank"><img src="https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></a> <a href="https://qm.qq.com/cgi-bin/qm/qr?k=wtbaNx7EioxeaqS9z7RQWVXPIxg2zYr7&jump_from=webapi&authKey=vlqnv/AV2DbJEvGIcxdlNSpfxVy+8vVqijgreRdnVKOaydpc+YSw4MctmEbr0k5"><img alt="QQ_community" src="https://img.shields.io/badge/QQ群-775869627-purple?style=for-the-badge&color=76bad9"></a>
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?color=76bad9"/></a> <a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E4%B8%AA&label=%E6%8F%92%E4%BB%B6%E5%B8%82%E5%9C%BA&cacheSeconds=3600"> <img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD%D0%BE%D0%B2&style=for-the-badge&label=%D0%9C%D0%B0%D0%B3%D0%B0%D0%B7%D0%B8%D0%BD&cacheSeconds=3600">
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
</div> </div>
<br> <br>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
<a href="https://astrbot.app/">Документация</a> <a href="https://astrbot.app/">Документация</a>
<a href="https://blog.astrbot.app/">Блог</a> <a href="https://blog.astrbot.app/">Блог</a>
<a href="https://astrbot.featurebase.app/roadmap">Дорожная карта</a> <a href="https://astrbot.featurebase.app/roadmap">Дорожная карта</a>
@@ -41,32 +42,13 @@ AstrBot — это универсальная платформа Agent-чатб
## Основные возможности ## Основные возможности
1. 💯 Бесплатно & Открытый исходный код. 1. 💯 Бесплатно и с открытым исходным кодом.
2.Диалоги с ИИ-моделями, мультимодальность, Agent, MCP, Skills, База знаний, Настройка личности, автоматическое сжатие диалогов. 2.ИИ-диалоги с LLM, мультимодальность, Agent, MCP, база знаний, настройки личности.
3. 🤖 Поддержка интеграции с платформами Agents, такими как Dify, Alibaba Cloud Bailian, Coze и др. 3. 🤖 Поддержка интеграции с Dify, Alibaba Cloud Bailian, Coze и другими платформами агентов.
4. 🌐 Мультиплатформенность: поддержка QQ, WeChat для предприятий, Feishu, DingTalk, публичных аккаунтов WeChat, Telegram, Slack и [других](#Поддерживаемые-платформы-обмена-сообщениями). 4. 🌐 Мультиплатформенность: QQ, WeChat Work, Feishu, DingTalk, официальные аккаунты WeChat, Telegram, Slack и [другие](#поддерживаемые-платформы-обмена-сообщениями).
5. 📦 Расширение плагинами: доступно почти 800 плагинов для установки в один клик. 5. 📦 Расширения плагинов с почти 800 плагинами, доступными для установки в один клик.
6. 🛡️ Изолированная среда[Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html): безопасное выполнение любого кода, вызов Shell, повторное использование ресурсов на уровне сессии. 6. 💻 Поддержка WebUI.
7. 💻 Поддержка WebUI. 7. 🌐 Поддержка интернационализации (i18n).
8. 🌈 Поддержка Web ChatUI: встроенная песочница агента, веб-поиск и др.
9. 🌐 Поддержка интернационализации (i18n).
<br>
<table align="center">
<tr align="center">
<th>💙 Ролевые игры & Эмоциональная поддержка</th>
<th>✨ Проактивный Агент(Agent)</th>
<th>🚀 Универсальные Агентные возможности</th>
<th>🧩 Универсальные Агентные (Agentic) возможности</th>
</tr>
<tr>
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
</tr>
</table>
## Быстрый старт ## Быстрый старт
@@ -79,8 +61,7 @@ AstrBot — это универсальная платформа Agent-чатб
#### Развёртывание uv #### Развёртывание uv
```bash ```bash
uv tool install astrbot uvx astrbot
astrbot
``` ```
#### Развёртывание BT-Panel #### Развёртывание BT-Panel
@@ -134,16 +115,6 @@ uv run main.py
Или см. официальную документацию: [Развёртывание AstrBot из исходного кода](https://astrbot.app/deploy/astrbot/cli.html). Или см. официальную документацию: [Развёртывание AstrBot из исходного кода](https://astrbot.app/deploy/astrbot/cli.html).
#### Установка через системный пакетный менеджер
##### Arch Linux
```bash
yay -S astrbot-git
# или используйте paru
paru -S astrbot-git
```
## Поддерживаемые платформы обмена сообщениями ## Поддерживаемые платформы обмена сообщениями
**Официально поддерживаемые** **Официально поддерживаемые**
@@ -158,9 +129,8 @@ paru -S astrbot-git
- Discord - Discord
- Satori - Satori
- Misskey - Misskey
- LINE
- WhatsApp (Скоро) - WhatsApp (Скоро)
- LINE (Скоро)
**Поддерживаемые сообществом** **Поддерживаемые сообществом**
@@ -183,7 +153,7 @@ paru -S astrbot-git
- [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) - [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
- [302.AI](https://share.302.ai/rr1M3l) - [302.AI](https://share.302.ai/rr1M3l)
- [TokenPony](https://www.tokenpony.cn/3YPyf) - [TokenPony](https://www.tokenpony.cn/3YPyf)
- [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) - [SiliconFlow](https://docs.siliconflow.cn/cn/usecases/use-siliconcloud-in-astrbot)
- [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE) - [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE)
- ModelScope - ModelScope
- OneAPI - OneAPI
@@ -253,7 +223,7 @@ pre-commit install
Особая благодарность всем контрибьюторам и разработчикам плагинов за их вклад в AstrBot ❤️ Особая благодарность всем контрибьюторам и разработчикам плагинов за их вклад в AstrBot ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors"> <a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" /> <img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
</a> </a>
Кроме того, рождение этого проекта было бы невозможно без помощи следующих проектов с открытым исходным кодом: Кроме того, рождение этого проекта было бы невозможно без помощи следующих проектов с открытым исходным кодом:
@@ -265,19 +235,13 @@ pre-commit install
> [!TIP] > [!TIP]
> Если этот проект помог вам в жизни или работе, или если вас интересует его будущее развитие, пожалуйста, поставьте проекту звезду. Это движущая сила поддержки этого проекта с открытым исходным кодом <3 > Если этот проект помог вам в жизни или работе, или если вас интересует его будущее развитие, пожалуйста, поставьте проекту звезду. Это движущая сила поддержки этого проекта с открытым исходным кодом <3
<div align="center"> <div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=astrbotdevs/astrbot&type=Date)](https://star-history.com/#astrbotdevs/astrbot&Date) [![Star History Chart](https://api.star-history.com/svg?repos=astrbotdevs/astrbot&type=Date)](https://star-history.com/#astrbotdevs/astrbot&Date)
</div> </div>
<div align="center"> </details>
_Сопровождение и способности никогда не должны быть противоположностями. Мы стремимся создать робота, который сможет как понимать эмоции, оказывать душевную поддержку, так и надёжно выполнять работу._
_私は、高性能ですから!_ _私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div>
+24 -59
View File
@@ -1,12 +1,8 @@
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9) ![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
<div align="center"> </p>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">简体中文</a> <div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
<br> <br>
@@ -18,17 +14,22 @@
<br> <br>
<div> <div>
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest"> <img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?style=for-the-badge&color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest">
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python"> <img src="https://img.shields.io/badge/python-3.10+-blue.svg?style=for-the-badge&color=76bad9" alt="python">
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot"> <a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?style=for-the-badge&color=76bad9"/></a>
<a href="https://zread.ai/AstrBotDevs/AstrBot" target="_blank"><img src="https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></a> <a href="https://qm.qq.com/cgi-bin/qm/qr?k=wtbaNx7EioxeaqS9z7RQWVXPIxg2zYr7&jump_from=webapi&authKey=vlqnv/AV2DbJEvGIcxdlNSpfxVy+8vVqijgreRdnVKOaydpc+YSw4MctmEbr0k5"><img alt="QQ_community" src="https://img.shields.io/badge/QQ群-775869627-purple?style=for-the-badge&color=76bad9"></a>
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?color=76bad9"/></a> <a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E5%80%8B&label=%E6%8F%92%E4%BB%B6%E5%B8%82%E5%A0%B4&cacheSeconds=3600"> <img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E5%80%8B&style=for-the-badge&label=%E6%8F%92%E4%BB%B6%E5%B8%82%E5%A0%B4&cacheSeconds=3600">
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
</div> </div>
<br> <br>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">简体中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
<a href="https://astrbot.app/">文件</a> <a href="https://astrbot.app/">文件</a>
<a href="https://blog.astrbot.app/">Blog</a> <a href="https://blog.astrbot.app/">Blog</a>
<a href="https://astrbot.featurebase.app/roadmap">路線圖</a> <a href="https://astrbot.featurebase.app/roadmap">路線圖</a>
@@ -42,31 +43,12 @@ AstrBot 是一個開源的一站式 Agent 聊天機器人平台,可接入主
## 主要功能 ## 主要功能
1. 💯 免費 & 開源。 1. 💯 免費 & 開源。
2. ✨ AI 大模型對話,多模態,Agent,MCP,Skills知識庫,人格設定,自動壓縮對話 2. ✨ AI 大模型對話,多模態,Agent,MCP,知識庫,人格設定。
3. 🤖 支援接入 Dify、阿里雲百煉、Coze 等智慧體 (Agent) 平台。 3. 🤖 支援接入 Dify、阿里雲百煉、Coze 等智慧體平台。
4. 🌐 多平台,支援 QQ、企業微信、飛書、釘釘、微信公眾號、Telegram、Slack 以及[更多](#支援的訊息平台)。 4. 🌐 多平台QQ、企業微信、飛書、釘釘、微信公眾號、Telegram、Slack 以及[更多](#支援的訊息平台)。
5. 📦 插件擴展,已有近 800 個插件可一鍵安裝。 5. 📦 外掛擴充,已有近 800 個外掛可一鍵安裝。
6. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) 隔離化環境,安全地執行任何代碼、調用 Shell、會話級資源複用 6. 💻 WebUI 支援
7. 💻 WebUI 支援。 7. 🌐 國際化(i18n支援。
8. 🌈 Web ChatUI 支援,ChatUI 內置代理沙盒 (Agent Sandbox)、網頁搜尋等。
9. 🌐 國際化(i18n)支援。
<br>
<table align="center">
<tr align="center">
<th>💙 角色扮演 & 情感陪伴</th>
<th>✨ 主動式 Agent</th>
<th>🚀 通用 Agentic 能力</th>
<th>🧩 900+ 社區外掛程式</th>
</tr>
<tr>
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
</tr>
</table>
## 快速開始 ## 快速開始
@@ -79,8 +61,7 @@ AstrBot 是一個開源的一站式 Agent 聊天機器人平台,可接入主
#### uv 部署 #### uv 部署
```bash ```bash
uv tool install astrbot uvx astrbot
astrbot
``` ```
#### 寶塔面板部署 #### 寶塔面板部署
@@ -134,16 +115,6 @@ uv run main.py
或者請參閱官方文件 [透過原始碼部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html)。 或者請參閱官方文件 [透過原始碼部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html)。
#### 系統套件管理員安裝
##### Arch Linux
```bash
yay -S astrbot-git
# 或者使用 paru
paru -S astrbot-git
```
## 支援的訊息平台 ## 支援的訊息平台
**官方維護** **官方維護**
@@ -158,9 +129,8 @@ paru -S astrbot-git
- Discord - Discord
- Satori - Satori
- Misskey - Misskey
- LINE
- Whatsapp(即將支援) - Whatsapp(即將支援)
- LINE(即將支援)
**社群維護** **社群維護**
@@ -253,7 +223,7 @@ pre-commit install
特別感謝所有 Contributors 和外掛開發者對 AstrBot 的貢獻 ❤️ 特別感謝所有 Contributors 和外掛開發者對 AstrBot 的貢獻 ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors"> <a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" /> <img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
</a> </a>
此外,本專案的誕生離不開以下開源專案的幫助: 此外,本專案的誕生離不開以下開源專案的幫助:
@@ -271,12 +241,7 @@ pre-commit install
</div> </div>
<div align="center"> </details>
_陪伴與能力從來不應該是對立面。我們希望創造的是一個既能理解情緒、給予陪伴,也能可靠完成工作的機器人。_
_私は、高性能ですから!_ _私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div>
-2
View File
@@ -24,7 +24,6 @@ from astrbot.core.star.register import (
register_on_llm_tool_respond as on_llm_tool_respond, register_on_llm_tool_respond as on_llm_tool_respond,
) )
from astrbot.core.star.register import register_on_platform_loaded as on_platform_loaded from astrbot.core.star.register import register_on_platform_loaded as on_platform_loaded
from astrbot.core.star.register import register_on_plugin_error as on_plugin_error
from astrbot.core.star.register import register_on_using_llm_tool as on_using_llm_tool from astrbot.core.star.register import register_on_using_llm_tool as on_using_llm_tool
from astrbot.core.star.register import ( from astrbot.core.star.register import (
register_on_waiting_llm_request as on_waiting_llm_request, register_on_waiting_llm_request as on_waiting_llm_request,
@@ -53,7 +52,6 @@ __all__ = [
"on_decorating_result", "on_decorating_result",
"on_llm_request", "on_llm_request",
"on_llm_response", "on_llm_response",
"on_plugin_error",
"on_platform_loaded", "on_platform_loaded",
"on_waiting_llm_request", "on_waiting_llm_request",
"permission_type", "permission_type",
@@ -17,7 +17,7 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
class LongTermMemory: class LongTermMemory:
def __init__(self, acm: AstrBotConfigManager, context: star.Context) -> None: def __init__(self, acm: AstrBotConfigManager, context: star.Context):
self.acm = acm self.acm = acm
self.context = context self.context = context
self.session_chats = defaultdict(list) self.session_chats = defaultdict(list)
@@ -111,7 +111,7 @@ class LongTermMemory:
return False return False
async def handle_message(self, event: AstrMessageEvent) -> None: async def handle_message(self, event: AstrMessageEvent):
"""仅支持群聊""" """仅支持群聊"""
if event.get_message_type() == MessageType.GROUP_MESSAGE: if event.get_message_type() == MessageType.GROUP_MESSAGE:
datetime_str = datetime.datetime.now().strftime("%H:%M:%S") datetime_str = datetime.datetime.now().strftime("%H:%M:%S")
@@ -148,7 +148,7 @@ class LongTermMemory:
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]: if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
self.session_chats[event.unified_msg_origin].pop(0) self.session_chats[event.unified_msg_origin].pop(0)
async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest) -> None: async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest):
"""当触发 LLM 请求前,调用此方法修改 req""" """当触发 LLM 请求前,调用此方法修改 req"""
if event.unified_msg_origin not in self.session_chats: if event.unified_msg_origin not in self.session_chats:
return return
@@ -171,9 +171,7 @@ class LongTermMemory:
) )
req.system_prompt += chats_str req.system_prompt += chats_str
async def after_req_llm( async def after_req_llm(self, event: AstrMessageEvent, llm_resp: LLMResponse):
self, event: AstrMessageEvent, llm_resp: LLMResponse
) -> None:
if event.unified_msg_origin not in self.session_chats: if event.unified_msg_origin not in self.session_chats:
return return
+3 -7
View File
@@ -85,9 +85,7 @@ class Main(star.Star):
logger.error(f"主动回复失败: {e}") logger.error(f"主动回复失败: {e}")
@filter.on_llm_request() @filter.on_llm_request()
async def decorate_llm_req( async def decorate_llm_req(self, event: AstrMessageEvent, req: ProviderRequest):
self, event: AstrMessageEvent, req: ProviderRequest
) -> None:
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt""" """在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
if self.ltm and self.ltm_enabled(event): if self.ltm and self.ltm_enabled(event):
try: try:
@@ -96,9 +94,7 @@ class Main(star.Star):
logger.error(f"ltm: {e}") logger.error(f"ltm: {e}")
@filter.on_llm_response() @filter.on_llm_response()
async def record_llm_resp_to_ltm( async def record_llm_resp_to_ltm(self, event: AstrMessageEvent, resp: LLMResponse):
self, event: AstrMessageEvent, resp: LLMResponse
) -> None:
"""在 LLM 响应后记录对话""" """在 LLM 响应后记录对话"""
if self.ltm and self.ltm_enabled(event): if self.ltm and self.ltm_enabled(event):
try: try:
@@ -107,7 +103,7 @@ class Main(star.Star):
logger.error(f"ltm: {e}") logger.error(f"ltm: {e}")
@filter.after_message_sent() @filter.after_message_sent()
async def after_message_sent(self, event: AstrMessageEvent) -> None: async def after_message_sent(self, event: AstrMessageEvent):
"""消息发送后处理""" """消息发送后处理"""
if self.ltm and self.ltm_enabled(event): if self.ltm and self.ltm_enabled(event):
try: try:
@@ -5,10 +5,10 @@ from astrbot.core.utils.io import download_dashboard
class AdminCommands: class AdminCommands:
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None: async def op(self, event: AstrMessageEvent, admin_id: str = ""):
"""授权管理员。op <admin_id>""" """授权管理员。op <admin_id>"""
if not admin_id: if not admin_id:
event.set_result( event.set_result(
@@ -21,7 +21,7 @@ class AdminCommands:
self.context.get_config().save_config() self.context.get_config().save_config()
event.set_result(MessageEventResult().message("授权成功。")) event.set_result(MessageEventResult().message("授权成功。"))
async def deop(self, event: AstrMessageEvent, admin_id: str = "") -> None: async def deop(self, event: AstrMessageEvent, admin_id: str = ""):
"""取消授权管理员。deop <admin_id>""" """取消授权管理员。deop <admin_id>"""
if not admin_id: if not admin_id:
event.set_result( event.set_result(
@@ -39,7 +39,7 @@ class AdminCommands:
MessageEventResult().message("此用户 ID 不在管理员名单内。"), MessageEventResult().message("此用户 ID 不在管理员名单内。"),
) )
async def wl(self, event: AstrMessageEvent, sid: str = "") -> None: async def wl(self, event: AstrMessageEvent, sid: str = ""):
"""添加白名单。wl <sid>""" """添加白名单。wl <sid>"""
if not sid: if not sid:
event.set_result( event.set_result(
@@ -53,7 +53,7 @@ class AdminCommands:
cfg.save_config() cfg.save_config()
event.set_result(MessageEventResult().message("添加白名单成功。")) event.set_result(MessageEventResult().message("添加白名单成功。"))
async def dwl(self, event: AstrMessageEvent, sid: str = "") -> None: async def dwl(self, event: AstrMessageEvent, sid: str = ""):
"""删除白名单。dwl <sid>""" """删除白名单。dwl <sid>"""
if not sid: if not sid:
event.set_result( event.set_result(
@@ -70,7 +70,7 @@ class AdminCommands:
except ValueError: except ValueError:
event.set_result(MessageEventResult().message("此 SID 不在白名单内。")) event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
async def update_dashboard(self, event: AstrMessageEvent) -> None: async def update_dashboard(self, event: AstrMessageEvent):
"""更新管理面板""" """更新管理面板"""
await event.send(MessageChain().message("正在尝试更新管理面板...")) await event.send(MessageChain().message("正在尝试更新管理面板..."))
await download_dashboard(version=f"v{VERSION}", latest=False) await download_dashboard(version=f"v{VERSION}", latest=False)
@@ -11,10 +11,10 @@ from .utils.rst_scene import RstScene
class AlterCmdCommands(CommandParserMixin): class AlterCmdCommands(CommandParserMixin):
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
async def update_reset_permission(self, scene_key: str, perm_type: str) -> None: async def update_reset_permission(self, scene_key: str, perm_type: str):
"""更新reset命令在特定场景下的权限设置""" """更新reset命令在特定场景下的权限设置"""
from astrbot.api import sp from astrbot.api import sp
@@ -26,7 +26,7 @@ class AlterCmdCommands(CommandParserMixin):
alter_cmd_cfg["astrbot"] = plugin_cfg alter_cmd_cfg["astrbot"] = plugin_cfg
await sp.global_put("alter_cmd", alter_cmd_cfg) await sp.global_put("alter_cmd", alter_cmd_cfg)
async def alter_cmd(self, event: AstrMessageEvent) -> None: async def alter_cmd(self, event: AstrMessageEvent):
token = self.parse_commands(event.message_str) token = self.parse_commands(event.message_str)
if token.len < 3: if token.len < 3:
await event.send( await event.send(
@@ -4,7 +4,6 @@ from astrbot.api import sp, star
from astrbot.api.event import AstrMessageEvent, MessageEventResult from astrbot.api.event import AstrMessageEvent, MessageEventResult
from astrbot.core.platform.astr_message_event import MessageSession from astrbot.core.platform.astr_message_event import MessageSession
from astrbot.core.platform.message_type import MessageType from astrbot.core.platform.message_type import MessageType
from astrbot.core.utils.active_event_registry import active_event_registry
from .utils.rst_scene import RstScene from .utils.rst_scene import RstScene
@@ -17,7 +16,7 @@ THIRD_PARTY_AGENT_RUNNER_STR = ", ".join(THIRD_PARTY_AGENT_RUNNER_KEY.keys())
class ConversationCommands: class ConversationCommands:
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
async def _get_current_persona_id(self, session_id): async def _get_current_persona_id(self, session_id):
@@ -34,7 +33,7 @@ class ConversationCommands:
return None return None
return conv.persona_id return conv.persona_id
async def reset(self, message: AstrMessageEvent) -> None: async def reset(self, message: AstrMessageEvent):
"""重置 LLM 会话""" """重置 LLM 会话"""
umo = message.unified_msg_origin umo = message.unified_msg_origin
cfg = self.context.get_config(umo=message.unified_msg_origin) cfg = self.context.get_config(umo=message.unified_msg_origin)
@@ -63,7 +62,6 @@ class ConversationCommands:
agent_runner_type = cfg["provider_settings"]["agent_runner_type"] agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY: if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
active_event_registry.stop_all(umo, exclude=message)
await sp.remove_async( await sp.remove_async(
scope="umo", scope="umo",
scope_id=umo, scope_id=umo,
@@ -88,8 +86,6 @@ class ConversationCommands:
) )
return return
active_event_registry.stop_all(umo, exclude=message)
await self.context.conversation_manager.update_conversation( await self.context.conversation_manager.update_conversation(
umo, umo,
cid, cid,
@@ -102,7 +98,7 @@ class ConversationCommands:
message.set_result(MessageEventResult().message(ret)) message.set_result(MessageEventResult().message(ret))
async def his(self, message: AstrMessageEvent, page: int = 1) -> None: async def his(self, message: AstrMessageEvent, page: int = 1):
"""查看对话记录""" """查看对话记录"""
if not self.context.get_using_provider(message.unified_msg_origin): if not self.context.get_using_provider(message.unified_msg_origin):
message.set_result( message.set_result(
@@ -145,7 +141,7 @@ class ConversationCommands:
message.set_result(MessageEventResult().message(ret).use_t2i(False)) message.set_result(MessageEventResult().message(ret).use_t2i(False))
async def convs(self, message: AstrMessageEvent, page: int = 1) -> None: async def convs(self, message: AstrMessageEvent, page: int = 1):
"""查看对话列表""" """查看对话列表"""
cfg = self.context.get_config(umo=message.unified_msg_origin) cfg = self.context.get_config(umo=message.unified_msg_origin)
agent_runner_type = cfg["provider_settings"]["agent_runner_type"] agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
@@ -220,12 +216,11 @@ class ConversationCommands:
message.set_result(MessageEventResult().message(ret).use_t2i(False)) message.set_result(MessageEventResult().message(ret).use_t2i(False))
return return
async def new_conv(self, message: AstrMessageEvent) -> None: async def new_conv(self, message: AstrMessageEvent):
"""创建新对话""" """创建新对话"""
cfg = self.context.get_config(umo=message.unified_msg_origin) cfg = self.context.get_config(umo=message.unified_msg_origin)
agent_runner_type = cfg["provider_settings"]["agent_runner_type"] agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY: if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
active_event_registry.stop_all(message.unified_msg_origin, exclude=message)
await sp.remove_async( await sp.remove_async(
scope="umo", scope="umo",
scope_id=message.unified_msg_origin, scope_id=message.unified_msg_origin,
@@ -234,7 +229,6 @@ class ConversationCommands:
message.set_result(MessageEventResult().message("已创建新对话。")) message.set_result(MessageEventResult().message("已创建新对话。"))
return return
active_event_registry.stop_all(message.unified_msg_origin, exclude=message)
cpersona = await self._get_current_persona_id(message.unified_msg_origin) cpersona = await self._get_current_persona_id(message.unified_msg_origin)
cid = await self.context.conversation_manager.new_conversation( cid = await self.context.conversation_manager.new_conversation(
message.unified_msg_origin, message.unified_msg_origin,
@@ -248,7 +242,7 @@ class ConversationCommands:
MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"), MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"),
) )
async def groupnew_conv(self, message: AstrMessageEvent, sid: str = "") -> None: async def groupnew_conv(self, message: AstrMessageEvent, sid: str = ""):
"""创建新群聊对话""" """创建新群聊对话"""
if sid: if sid:
session = str( session = str(
@@ -279,7 +273,7 @@ class ConversationCommands:
self, self,
message: AstrMessageEvent, message: AstrMessageEvent,
index: int | None = None, index: int | None = None,
) -> None: ):
"""通过 /ls 前面的序号切换对话""" """通过 /ls 前面的序号切换对话"""
if not isinstance(index, int): if not isinstance(index, int):
message.set_result( message.set_result(
@@ -314,7 +308,7 @@ class ConversationCommands:
), ),
) )
async def rename_conv(self, message: AstrMessageEvent, new_name: str = "") -> None: async def rename_conv(self, message: AstrMessageEvent, new_name: str = ""):
"""重命名对话""" """重命名对话"""
if not new_name: if not new_name:
message.set_result(MessageEventResult().message("请输入新的对话名称。")) message.set_result(MessageEventResult().message("请输入新的对话名称。"))
@@ -325,10 +319,9 @@ class ConversationCommands:
) )
message.set_result(MessageEventResult().message("重命名对话成功。")) message.set_result(MessageEventResult().message("重命名对话成功。"))
async def del_conv(self, message: AstrMessageEvent) -> None: async def del_conv(self, message: AstrMessageEvent):
"""删除当前对话""" """删除当前对话"""
umo = message.unified_msg_origin cfg = self.context.get_config(umo=message.unified_msg_origin)
cfg = self.context.get_config(umo=umo)
is_unique_session = cfg["platform_settings"]["unique_session"] is_unique_session = cfg["platform_settings"]["unique_session"]
if message.get_group_id() and not is_unique_session and message.role != "admin": if message.get_group_id() and not is_unique_session and message.role != "admin":
# 群聊,没开独立会话,发送人不是管理员 # 群聊,没开独立会话,发送人不是管理员
@@ -341,17 +334,18 @@ class ConversationCommands:
agent_runner_type = cfg["provider_settings"]["agent_runner_type"] agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY: if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
active_event_registry.stop_all(umo, exclude=message)
await sp.remove_async( await sp.remove_async(
scope="umo", scope="umo",
scope_id=umo, scope_id=message.unified_msg_origin,
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type], key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
) )
message.set_result(MessageEventResult().message("重置对话成功。")) message.set_result(MessageEventResult().message("重置对话成功。"))
return return
session_curr_cid = ( session_curr_cid = (
await self.context.conversation_manager.get_curr_conversation_id(umo) await self.context.conversation_manager.get_curr_conversation_id(
message.unified_msg_origin,
)
) )
if not session_curr_cid: if not session_curr_cid:
@@ -362,10 +356,8 @@ class ConversationCommands:
) )
return return
active_event_registry.stop_all(umo, exclude=message)
await self.context.conversation_manager.delete_conversation( await self.context.conversation_manager.delete_conversation(
umo, message.unified_msg_origin,
session_curr_cid, session_curr_cid,
) )
@@ -8,7 +8,7 @@ from astrbot.core.utils.io import get_dashboard_version
class HelpCommand: class HelpCommand:
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
async def _query_astrbot_notice(self): async def _query_astrbot_notice(self):
@@ -34,7 +34,7 @@ class HelpCommand:
lines: list[str] = [] lines: list[str] = []
hidden_commands = {"set", "unset", "websearch"} hidden_commands = {"set", "unset", "websearch"}
def walk(items: list[dict], indent: int = 0) -> None: def walk(items: list[dict], indent: int = 0):
for item in items: for item in items:
if not item.get("reserved") or not item.get("enabled"): if not item.get("reserved") or not item.get("enabled"):
continue continue
@@ -62,7 +62,7 @@ class HelpCommand:
walk(commands) walk(commands)
return lines return lines
async def help(self, event: AstrMessageEvent) -> None: async def help(self, event: AstrMessageEvent):
"""查看帮助""" """查看帮助"""
notice = "" notice = ""
try: try:
@@ -3,10 +3,10 @@ from astrbot.api.event import AstrMessageEvent, MessageChain
class LLMCommands: class LLMCommands:
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
async def llm(self, event: AstrMessageEvent) -> None: async def llm(self, event: AstrMessageEvent):
"""开启/关闭 LLM""" """开启/关闭 LLM"""
cfg = self.context.get_config(umo=event.unified_msg_origin) cfg = self.context.get_config(umo=event.unified_msg_origin)
enable = cfg["provider_settings"].get("enable", True) enable = cfg["provider_settings"].get("enable", True)
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
class PersonaCommands: class PersonaCommands:
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
def _build_tree_output( def _build_tree_output(
@@ -50,7 +50,7 @@ class PersonaCommands:
return lines return lines
async def persona(self, message: AstrMessageEvent) -> None: async def persona(self, message: AstrMessageEvent):
l = message.message_str.split(" ") # noqa: E741 l = message.message_str.split(" ") # noqa: E741
umo = message.unified_msg_origin umo = message.unified_msg_origin
@@ -8,10 +8,10 @@ from astrbot.core.star.star_manager import PluginManager
class PluginCommands: class PluginCommands:
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
async def plugin_ls(self, event: AstrMessageEvent) -> None: async def plugin_ls(self, event: AstrMessageEvent):
"""获取已经安装的插件列表。""" """获取已经安装的插件列表。"""
parts = ["已加载的插件:\n"] parts = ["已加载的插件:\n"]
for plugin in self.context.get_all_stars(): for plugin in self.context.get_all_stars():
@@ -30,7 +30,7 @@ class PluginCommands:
MessageEventResult().message(f"{plugin_list_info}").use_t2i(False), MessageEventResult().message(f"{plugin_list_info}").use_t2i(False),
) )
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None: async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = ""):
"""禁用插件""" """禁用插件"""
if DEMO_MODE: if DEMO_MODE:
event.set_result(MessageEventResult().message("演示模式下无法禁用插件。")) event.set_result(MessageEventResult().message("演示模式下无法禁用插件。"))
@@ -43,7 +43,7 @@ class PluginCommands:
await self.context._star_manager.turn_off_plugin(plugin_name) # type: ignore await self.context._star_manager.turn_off_plugin(plugin_name) # type: ignore
event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已禁用。")) event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已禁用。"))
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = "") -> None: async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = ""):
"""启用插件""" """启用插件"""
if DEMO_MODE: if DEMO_MODE:
event.set_result(MessageEventResult().message("演示模式下无法启用插件。")) event.set_result(MessageEventResult().message("演示模式下无法启用插件。"))
@@ -56,7 +56,7 @@ class PluginCommands:
await self.context._star_manager.turn_on_plugin(plugin_name) # type: ignore await self.context._star_manager.turn_on_plugin(plugin_name) # type: ignore
event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已启用。")) event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已启用。"))
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = "") -> None: async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = ""):
"""安装插件""" """安装插件"""
if DEMO_MODE: if DEMO_MODE:
event.set_result(MessageEventResult().message("演示模式下无法安装插件。")) event.set_result(MessageEventResult().message("演示模式下无法安装插件。"))
@@ -77,7 +77,7 @@ class PluginCommands:
event.set_result(MessageEventResult().message(f"安装插件失败: {e}")) event.set_result(MessageEventResult().message(f"安装插件失败: {e}"))
return return
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> None: async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = ""):
"""获取插件帮助""" """获取插件帮助"""
if not plugin_name: if not plugin_name:
event.set_result( event.set_result(
@@ -8,7 +8,7 @@ from astrbot.core.provider.entities import ProviderType
class ProviderCommands: class ProviderCommands:
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
def _log_reachability_failure( def _log_reachability_failure(
@@ -17,7 +17,7 @@ class ProviderCommands:
provider_capability_type: ProviderType | None, provider_capability_type: ProviderType | None,
err_code: str, err_code: str,
err_reason: str, err_reason: str,
) -> None: ):
"""记录不可达原因到日志。""" """记录不可达原因到日志。"""
meta = provider.meta() meta = provider.meta()
logger.warning( logger.warning(
@@ -49,7 +49,7 @@ class ProviderCommands:
event: AstrMessageEvent, event: AstrMessageEvent,
idx: str | int | None = None, idx: str | int | None = None,
idx2: int | None = None, idx2: int | None = None,
) -> None: ):
"""查看或者切换 LLM Provider""" """查看或者切换 LLM Provider"""
umo = event.unified_msg_origin umo = event.unified_msg_origin
cfg = self.context.get_config(umo).get("provider_settings", {}) cfg = self.context.get_config(umo).get("provider_settings", {})
@@ -228,7 +228,7 @@ class ProviderCommands:
self, self,
message: AstrMessageEvent, message: AstrMessageEvent,
idx_or_name: int | str | None = None, idx_or_name: int | str | None = None,
) -> None: ):
"""查看或者切换模型""" """查看或者切换模型"""
prov = self.context.get_using_provider(message.unified_msg_origin) prov = self.context.get_using_provider(message.unified_msg_origin)
if not prov: if not prov:
@@ -293,7 +293,7 @@ class ProviderCommands:
MessageEventResult().message(f"切换模型到 {prov.get_model()}"), MessageEventResult().message(f"切换模型到 {prov.get_model()}"),
) )
async def key(self, message: AstrMessageEvent, index: int | None = None) -> None: async def key(self, message: AstrMessageEvent, index: int | None = None):
prov = self.context.get_using_provider(message.unified_msg_origin) prov = self.context.get_using_provider(message.unified_msg_origin)
if not prov: if not prov:
message.set_result( message.set_result(
@@ -3,10 +3,10 @@ from astrbot.api.event import AstrMessageEvent, MessageEventResult
class SetUnsetCommands: class SetUnsetCommands:
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
async def set_variable(self, event: AstrMessageEvent, key: str, value: str) -> None: async def set_variable(self, event: AstrMessageEvent, key: str, value: str):
"""设置会话变量""" """设置会话变量"""
uid = event.unified_msg_origin uid = event.unified_msg_origin
session_var = await sp.session_get(uid, "session_variables", {}) session_var = await sp.session_get(uid, "session_variables", {})
@@ -19,7 +19,7 @@ class SetUnsetCommands:
), ),
) )
async def unset_variable(self, event: AstrMessageEvent, key: str) -> None: async def unset_variable(self, event: AstrMessageEvent, key: str):
"""移除会话变量""" """移除会话变量"""
uid = event.unified_msg_origin uid = event.unified_msg_origin
session_var = await sp.session_get(uid, "session_variables", {}) session_var = await sp.session_get(uid, "session_variables", {})
@@ -7,10 +7,10 @@ from astrbot.api.event import AstrMessageEvent, MessageEventResult
class SIDCommand: class SIDCommand:
"""会话ID命令类""" """会话ID命令类"""
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
async def sid(self, event: AstrMessageEvent) -> None: async def sid(self, event: AstrMessageEvent):
"""获取消息来源信息""" """获取消息来源信息"""
sid = event.unified_msg_origin sid = event.unified_msg_origin
user_id = str(event.get_sender_id()) user_id = str(event.get_sender_id())
@@ -7,10 +7,10 @@ from astrbot.api.event import AstrMessageEvent, MessageEventResult
class T2ICommand: class T2ICommand:
"""文本转图片命令类""" """文本转图片命令类"""
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
async def t2i(self, event: AstrMessageEvent) -> None: async def t2i(self, event: AstrMessageEvent):
"""开关文本转图片""" """开关文本转图片"""
config = self.context.get_config(umo=event.unified_msg_origin) config = self.context.get_config(umo=event.unified_msg_origin)
if config["t2i"]: if config["t2i"]:
@@ -8,10 +8,10 @@ from astrbot.core.star.session_llm_manager import SessionServiceManager
class TTSCommand: class TTSCommand:
"""文本转语音命令类""" """文本转语音命令类"""
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context):
self.context = context self.context = context
async def tts(self, event: AstrMessageEvent) -> None: async def tts(self, event: AstrMessageEvent):
"""开关文本转语音(会话级别)""" """开关文本转语音(会话级别)"""
umo = event.unified_msg_origin umo = event.unified_msg_origin
ses_tts = await SessionServiceManager.is_tts_enabled_for_session(umo) ses_tts = await SessionServiceManager.is_tts_enabled_for_session(umo)
+31 -33
View File
@@ -35,84 +35,84 @@ class Main(star.Star):
self.sid_c = SIDCommand(self.context) self.sid_c = SIDCommand(self.context)
@filter.command("help") @filter.command("help")
async def help(self, event: AstrMessageEvent) -> None: async def help(self, event: AstrMessageEvent):
"""查看帮助""" """查看帮助"""
await self.help_c.help(event) await self.help_c.help(event)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("llm") @filter.command("llm")
async def llm(self, event: AstrMessageEvent) -> None: async def llm(self, event: AstrMessageEvent):
"""开启/关闭 LLM""" """开启/关闭 LLM"""
await self.llm_c.llm(event) await self.llm_c.llm(event)
@filter.command_group("plugin") @filter.command_group("plugin")
def plugin(self) -> None: def plugin(self):
"""插件管理""" """插件管理"""
@plugin.command("ls") @plugin.command("ls")
async def plugin_ls(self, event: AstrMessageEvent) -> None: async def plugin_ls(self, event: AstrMessageEvent):
"""获取已经安装的插件列表。""" """获取已经安装的插件列表。"""
await self.plugin_c.plugin_ls(event) await self.plugin_c.plugin_ls(event)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@plugin.command("off") @plugin.command("off")
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None: async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = ""):
"""禁用插件""" """禁用插件"""
await self.plugin_c.plugin_off(event, plugin_name) await self.plugin_c.plugin_off(event, plugin_name)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@plugin.command("on") @plugin.command("on")
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = "") -> None: async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = ""):
"""启用插件""" """启用插件"""
await self.plugin_c.plugin_on(event, plugin_name) await self.plugin_c.plugin_on(event, plugin_name)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@plugin.command("get") @plugin.command("get")
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = "") -> None: async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = ""):
"""安装插件""" """安装插件"""
await self.plugin_c.plugin_get(event, plugin_repo) await self.plugin_c.plugin_get(event, plugin_repo)
@plugin.command("help") @plugin.command("help")
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> None: async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = ""):
"""获取插件帮助""" """获取插件帮助"""
await self.plugin_c.plugin_help(event, plugin_name) await self.plugin_c.plugin_help(event, plugin_name)
@filter.command("t2i") @filter.command("t2i")
async def t2i(self, event: AstrMessageEvent) -> None: async def t2i(self, event: AstrMessageEvent):
"""开关文本转图片""" """开关文本转图片"""
await self.t2i_c.t2i(event) await self.t2i_c.t2i(event)
@filter.command("tts") @filter.command("tts")
async def tts(self, event: AstrMessageEvent) -> None: async def tts(self, event: AstrMessageEvent):
"""开关文本转语音(会话级别)""" """开关文本转语音(会话级别)"""
await self.tts_c.tts(event) await self.tts_c.tts(event)
@filter.command("sid") @filter.command("sid")
async def sid(self, event: AstrMessageEvent) -> None: async def sid(self, event: AstrMessageEvent):
"""获取会话 ID 和 管理员 ID""" """获取会话 ID 和 管理员 ID"""
await self.sid_c.sid(event) await self.sid_c.sid(event)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("op") @filter.command("op")
async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None: async def op(self, event: AstrMessageEvent, admin_id: str = ""):
"""授权管理员。op <admin_id>""" """授权管理员。op <admin_id>"""
await self.admin_c.op(event, admin_id) await self.admin_c.op(event, admin_id)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("deop") @filter.command("deop")
async def deop(self, event: AstrMessageEvent, admin_id: str) -> None: async def deop(self, event: AstrMessageEvent, admin_id: str):
"""取消授权管理员。deop <admin_id>""" """取消授权管理员。deop <admin_id>"""
await self.admin_c.deop(event, admin_id) await self.admin_c.deop(event, admin_id)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("wl") @filter.command("wl")
async def wl(self, event: AstrMessageEvent, sid: str = "") -> None: async def wl(self, event: AstrMessageEvent, sid: str = ""):
"""添加白名单。wl <sid>""" """添加白名单。wl <sid>"""
await self.admin_c.wl(event, sid) await self.admin_c.wl(event, sid)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("dwl") @filter.command("dwl")
async def dwl(self, event: AstrMessageEvent, sid: str) -> None: async def dwl(self, event: AstrMessageEvent, sid: str):
"""删除白名单。dwl <sid>""" """删除白名单。dwl <sid>"""
await self.admin_c.dwl(event, sid) await self.admin_c.dwl(event, sid)
@@ -123,12 +123,12 @@ class Main(star.Star):
event: AstrMessageEvent, event: AstrMessageEvent,
idx: str | int | None = None, idx: str | int | None = None,
idx2: int | None = None, idx2: int | None = None,
) -> None: ):
"""查看或者切换 LLM Provider""" """查看或者切换 LLM Provider"""
await self.provider_c.provider(event, idx, idx2) await self.provider_c.provider(event, idx, idx2)
@filter.command("reset") @filter.command("reset")
async def reset(self, message: AstrMessageEvent) -> None: async def reset(self, message: AstrMessageEvent):
"""重置 LLM 会话""" """重置 LLM 会话"""
await self.conversation_c.reset(message) await self.conversation_c.reset(message)
@@ -138,76 +138,74 @@ class Main(star.Star):
self, self,
message: AstrMessageEvent, message: AstrMessageEvent,
idx_or_name: int | str | None = None, idx_or_name: int | str | None = None,
) -> None: ):
"""查看或者切换模型""" """查看或者切换模型"""
await self.provider_c.model_ls(message, idx_or_name) await self.provider_c.model_ls(message, idx_or_name)
@filter.command("history") @filter.command("history")
async def his(self, message: AstrMessageEvent, page: int = 1) -> None: async def his(self, message: AstrMessageEvent, page: int = 1):
"""查看对话记录""" """查看对话记录"""
await self.conversation_c.his(message, page) await self.conversation_c.his(message, page)
@filter.command("ls") @filter.command("ls")
async def convs(self, message: AstrMessageEvent, page: int = 1) -> None: async def convs(self, message: AstrMessageEvent, page: int = 1):
"""查看对话列表""" """查看对话列表"""
await self.conversation_c.convs(message, page) await self.conversation_c.convs(message, page)
@filter.command("new") @filter.command("new")
async def new_conv(self, message: AstrMessageEvent) -> None: async def new_conv(self, message: AstrMessageEvent):
"""创建新对话""" """创建新对话"""
await self.conversation_c.new_conv(message) await self.conversation_c.new_conv(message)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("groupnew") @filter.command("groupnew")
async def groupnew_conv(self, message: AstrMessageEvent, sid: str) -> None: async def groupnew_conv(self, message: AstrMessageEvent, sid: str):
"""创建新群聊对话""" """创建新群聊对话"""
await self.conversation_c.groupnew_conv(message, sid) await self.conversation_c.groupnew_conv(message, sid)
@filter.command("switch") @filter.command("switch")
async def switch_conv( async def switch_conv(self, message: AstrMessageEvent, index: int | None = None):
self, message: AstrMessageEvent, index: int | None = None
) -> None:
"""通过 /ls 前面的序号切换对话""" """通过 /ls 前面的序号切换对话"""
await self.conversation_c.switch_conv(message, index) await self.conversation_c.switch_conv(message, index)
@filter.command("rename") @filter.command("rename")
async def rename_conv(self, message: AstrMessageEvent, new_name: str) -> None: async def rename_conv(self, message: AstrMessageEvent, new_name: str):
"""重命名对话""" """重命名对话"""
await self.conversation_c.rename_conv(message, new_name) await self.conversation_c.rename_conv(message, new_name)
@filter.command("del") @filter.command("del")
async def del_conv(self, message: AstrMessageEvent) -> None: async def del_conv(self, message: AstrMessageEvent):
"""删除当前对话""" """删除当前对话"""
await self.conversation_c.del_conv(message) await self.conversation_c.del_conv(message)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("key") @filter.command("key")
async def key(self, message: AstrMessageEvent, index: int | None = None) -> None: async def key(self, message: AstrMessageEvent, index: int | None = None):
"""查看或者切换 Key""" """查看或者切换 Key"""
await self.provider_c.key(message, index) await self.provider_c.key(message, index)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("persona") @filter.command("persona")
async def persona(self, message: AstrMessageEvent) -> None: async def persona(self, message: AstrMessageEvent):
"""查看或者切换 Persona""" """查看或者切换 Persona"""
await self.persona_c.persona(message) await self.persona_c.persona(message)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("dashboard_update") @filter.command("dashboard_update")
async def update_dashboard(self, event: AstrMessageEvent) -> None: async def update_dashboard(self, event: AstrMessageEvent):
"""更新管理面板""" """更新管理面板"""
await self.admin_c.update_dashboard(event) await self.admin_c.update_dashboard(event)
@filter.command("set") @filter.command("set")
async def set_variable(self, event: AstrMessageEvent, key: str, value: str) -> None: async def set_variable(self, event: AstrMessageEvent, key: str, value: str):
await self.setunset_c.set_variable(event, key, value) await self.setunset_c.set_variable(event, key, value)
@filter.command("unset") @filter.command("unset")
async def unset_variable(self, event: AstrMessageEvent, key: str) -> None: async def unset_variable(self, event: AstrMessageEvent, key: str):
await self.setunset_c.unset_variable(event, key) await self.setunset_c.unset_variable(event, key)
@filter.permission_type(filter.PermissionType.ADMIN) @filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("alter_cmd", alias={"alter"}) @filter.command("alter_cmd", alias={"alter"})
async def alter_cmd(self, event: AstrMessageEvent) -> None: async def alter_cmd(self, event: AstrMessageEvent):
"""修改命令权限""" """修改命令权限"""
await self.alter_cmd_c.alter_cmd(event) await self.alter_cmd_c.alter_cmd(event)
@@ -17,11 +17,11 @@ from astrbot.core.utils.session_waiter import (
class Main(Star): class Main(Star):
"""会话控制""" """会话控制"""
def __init__(self, context: Context) -> None: def __init__(self, context: Context):
super().__init__(context) super().__init__(context)
@filter.event_message_type(filter.EventMessageType.ALL, priority=maxsize) @filter.event_message_type(filter.EventMessageType.ALL, priority=maxsize)
async def handle_session_control_agent(self, event: AstrMessageEvent) -> None: async def handle_session_control_agent(self, event: AstrMessageEvent):
"""会话控制代理""" """会话控制代理"""
for session_filter in FILTERS: for session_filter in FILTERS:
session_id = session_filter.filter(event) session_id = session_filter.filter(event)
@@ -90,7 +90,7 @@ class Main(Star):
async def empty_mention_waiter( async def empty_mention_waiter(
controller: SessionController, controller: SessionController,
event: AstrMessageEvent, event: AstrMessageEvent,
) -> None: ):
event.message_obj.message.insert( event.message_obj.message.insert(
0, 0,
Comp.At(qq=event.get_self_id(), name=event.get_self_id()), Comp.At(qq=event.get_self_id(), name=event.get_self_id()),
@@ -49,7 +49,7 @@ class SearchEngine:
def _set_selector(self, selector: str) -> str: def _set_selector(self, selector: str) -> str:
raise NotImplementedError raise NotImplementedError
async def _get_next_page(self, query: str) -> str: def _get_next_page(self, query: str):
raise NotImplementedError raise NotImplementedError
async def _get_html(self, url: str, data: dict | None = None) -> str: async def _get_html(self, url: str, data: dict | None = None) -> str:
+4 -182
View File
@@ -23,7 +23,6 @@ class Main(star.Star):
"fetch_url", "fetch_url",
"web_search_tavily", "web_search_tavily",
"tavily_extract_web_page", "tavily_extract_web_page",
"web_search_bocha",
] ]
def __init__(self, context: star.Context) -> None: def __init__(self, context: star.Context) -> None:
@@ -31,9 +30,6 @@ class Main(star.Star):
self.tavily_key_index = 0 self.tavily_key_index = 0
self.tavily_key_lock = asyncio.Lock() self.tavily_key_lock = asyncio.Lock()
self.bocha_key_index = 0
self.bocha_key_lock = asyncio.Lock()
# 将 str 类型的 key 迁移至 list[str],并保存 # 将 str 类型的 key 迁移至 list[str],并保存
cfg = self.context.get_config() cfg = self.context.get_config()
provider_settings = cfg.get("provider_settings") provider_settings = cfg.get("provider_settings")
@@ -49,14 +45,6 @@ class Main(star.Star):
provider_settings["websearch_tavily_key"] = [] provider_settings["websearch_tavily_key"] = []
cfg.save_config() cfg.save_config()
bocha_key = provider_settings.get("websearch_bocha_key")
if isinstance(bocha_key, str):
if bocha_key:
provider_settings["websearch_bocha_key"] = [bocha_key]
else:
provider_settings["websearch_bocha_key"] = []
cfg.save_config()
self.bing_search = Bing() self.bing_search = Bing()
self.sogo_search = Sogo() self.sogo_search = Sogo()
self.baidu_initialized = False self.baidu_initialized = False
@@ -199,7 +187,7 @@ class Main(star.Star):
return results return results
@filter.command("websearch") @filter.command("websearch")
async def websearch(self, event: AstrMessageEvent, oper: str | None = None) -> None: async def websearch(self, event: AstrMessageEvent, oper: str | None = None):
"""网页搜索指令(已废弃)""" """网页搜索指令(已废弃)"""
event.set_result( event.set_result(
MessageEventResult().message( MessageEventResult().message(
@@ -246,7 +234,7 @@ class Main(star.Star):
return ret return ret
async def ensure_baidu_ai_search_mcp(self, umo: str | None = None) -> None: async def ensure_baidu_ai_search_mcp(self, umo: str | None = None):
if self.baidu_initialized: if self.baidu_initialized:
return return
cfg = self.context.get_config(umo=umo) cfg = self.context.get_config(umo=umo)
@@ -353,7 +341,7 @@ class Main(star.Star):
} }
) )
if result.favicon: if result.favicon:
sp.temporary_cache["_ws_favicon"][result.url] = result.favicon sp.temorary_cache["_ws_favicon"][result.url] = result.favicon
# ret = "\n".join(ret_ls) # ret = "\n".join(ret_ls)
ret = json.dumps({"results": ret_ls}, ensure_ascii=False) ret = json.dumps({"results": ret_ls}, ensure_ascii=False)
return ret return ret
@@ -394,166 +382,12 @@ class Main(star.Star):
return "Error: Tavily web searcher does not return any results." return "Error: Tavily web searcher does not return any results."
return ret return ret
async def _get_bocha_key(self, cfg: AstrBotConfig) -> str:
"""并发安全的从列表中获取并轮换BoCha API密钥。"""
bocha_keys = cfg.get("provider_settings", {}).get("websearch_bocha_key", [])
if not bocha_keys:
raise ValueError("错误:BoCha API密钥未在AstrBot中配置。")
async with self.bocha_key_lock:
key = bocha_keys[self.bocha_key_index]
self.bocha_key_index = (self.bocha_key_index + 1) % len(bocha_keys)
return key
async def _web_search_bocha(
self,
cfg: AstrBotConfig,
payload: dict,
) -> list[SearchResult]:
"""使用 BoCha 搜索引擎进行搜索"""
bocha_key = await self._get_bocha_key(cfg)
url = "https://api.bochaai.com/v1/web-search"
header = {
"Authorization": f"Bearer {bocha_key}",
"Content-Type": "application/json",
}
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.post(
url,
json=payload,
headers=header,
) as response:
if response.status != 200:
reason = await response.text()
raise Exception(
f"BoCha web search failed: {reason}, status: {response.status}",
)
data = await response.json()
data = data["data"]["webPages"]["value"]
results = []
for item in data:
result = SearchResult(
title=item.get("name"),
url=item.get("url"),
snippet=item.get("snippet"),
favicon=item.get("siteIcon"),
)
results.append(result)
return results
@llm_tool("web_search_bocha")
async def search_from_bocha(
self,
event: AstrMessageEvent,
query: str,
freshness: str = "noLimit",
summary: bool = False,
include: str = "",
exclude: str = "",
count: int = 10,
) -> str:
"""
A web search tool based on Bocha Search API, used to retrieve web pages
related to the user's query.
Args:
query (string): Required. User's search query.
freshness (string): Optional. Specifies the time range of the search.
Supported values:
- "noLimit": No time limit (default, recommended).
- "oneDay": Within one day.
- "oneWeek": Within one week.
- "oneMonth": Within one month.
- "oneYear": Within one year.
- "YYYY-MM-DD..YYYY-MM-DD": Search within a specific date range.
Example: "2025-01-01..2025-04-06".
- "YYYY-MM-DD": Search on a specific date.
Example: "2025-04-06".
It is recommended to use "noLimit", as the search algorithm will
automatically optimize time relevance. Manually restricting the
time range may result in no search results.
summary (boolean): Optional. Whether to include a text summary
for each search result.
- True: Include summary.
- False: Do not include summary (default).
include (string): Optional. Specifies the domains to include in
the search. Multiple domains can be separated by "|" or ",".
A maximum of 100 domains is allowed.
Examples:
- "qq.com"
- "qq.com|m.163.com"
exclude (string): Optional. Specifies the domains to exclude from
the search. Multiple domains can be separated by "|" or ",".
A maximum of 100 domains is allowed.
Examples:
- "qq.com"
- "qq.com|m.163.com"
count (number): Optional. Number of search results to return.
- Range: 150
- Default: 10
The actual number of returned results may be less than the
specified count.
"""
logger.info(f"web_searcher - search_from_bocha: {query}")
cfg = self.context.get_config(umo=event.unified_msg_origin)
# websearch_link = cfg["provider_settings"].get("web_search_link", False)
if not cfg.get("provider_settings", {}).get("websearch_bocha_key", []):
raise ValueError("Error: BoCha API key is not configured in AstrBot.")
# build payload
payload = {
"query": query,
"count": count,
}
# freshness:时间范围
if freshness:
payload["freshness"] = freshness
# 是否返回摘要
payload["summary"] = summary
# include:限制搜索域
if include:
payload["include"] = include
# exclude:排除搜索域
if exclude:
payload["exclude"] = exclude
results = await self._web_search_bocha(cfg, payload)
if not results:
return "Error: BoCha web searcher does not return any results."
ret_ls = []
ref_uuid = str(uuid.uuid4())[:4]
for idx, result in enumerate(results, 1):
index = f"{ref_uuid}.{idx}"
ret_ls.append(
{
"title": f"{result.title}",
"url": f"{result.url}",
"snippet": f"{result.snippet}",
"index": index,
}
)
if result.favicon:
sp.temporary_cache["_ws_favicon"][result.url] = result.favicon
# ret = "\n".join(ret_ls)
ret = json.dumps({"results": ret_ls}, ensure_ascii=False)
return ret
@filter.on_llm_request(priority=-10000) @filter.on_llm_request(priority=-10000)
async def edit_web_search_tools( async def edit_web_search_tools(
self, self,
event: AstrMessageEvent, event: AstrMessageEvent,
req: ProviderRequest, req: ProviderRequest,
) -> None: ):
"""Get the session conversation for the given event.""" """Get the session conversation for the given event."""
cfg = self.context.get_config(umo=event.unified_msg_origin) cfg = self.context.get_config(umo=event.unified_msg_origin)
prov_settings = cfg.get("provider_settings", {}) prov_settings = cfg.get("provider_settings", {})
@@ -585,7 +419,6 @@ class Main(star.Star):
tool_set.remove_tool("web_search_tavily") tool_set.remove_tool("web_search_tavily")
tool_set.remove_tool("tavily_extract_web_page") tool_set.remove_tool("tavily_extract_web_page")
tool_set.remove_tool("AIsearch") tool_set.remove_tool("AIsearch")
tool_set.remove_tool("web_search_bocha")
elif provider == "tavily": elif provider == "tavily":
web_search_tavily = func_tool_mgr.get_func("web_search_tavily") web_search_tavily = func_tool_mgr.get_func("web_search_tavily")
tavily_extract_web_page = func_tool_mgr.get_func("tavily_extract_web_page") tavily_extract_web_page = func_tool_mgr.get_func("tavily_extract_web_page")
@@ -596,7 +429,6 @@ class Main(star.Star):
tool_set.remove_tool("web_search") tool_set.remove_tool("web_search")
tool_set.remove_tool("fetch_url") tool_set.remove_tool("fetch_url")
tool_set.remove_tool("AIsearch") tool_set.remove_tool("AIsearch")
tool_set.remove_tool("web_search_bocha")
elif provider == "baidu_ai_search": elif provider == "baidu_ai_search":
try: try:
await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin) await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin)
@@ -608,15 +440,5 @@ class Main(star.Star):
tool_set.remove_tool("fetch_url") tool_set.remove_tool("fetch_url")
tool_set.remove_tool("web_search_tavily") tool_set.remove_tool("web_search_tavily")
tool_set.remove_tool("tavily_extract_web_page") tool_set.remove_tool("tavily_extract_web_page")
tool_set.remove_tool("web_search_bocha")
except Exception as e: except Exception as e:
logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}") logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}")
elif provider == "bocha":
web_search_bocha = func_tool_mgr.get_func("web_search_bocha")
if web_search_bocha:
tool_set.add_tool(web_search_bocha)
tool_set.remove_tool("web_search")
tool_set.remove_tool("fetch_url")
tool_set.remove_tool("AIsearch")
tool_set.remove_tool("web_search_tavily")
tool_set.remove_tool("tavily_extract_web_page")
+1 -1
View File
@@ -1 +1 @@
__version__ = "4.18.0" __version__ = "4.14.4"
+3 -3
View File
@@ -127,7 +127,7 @@ def _get_nested_item(obj: dict[str, Any], path: str) -> Any:
@click.group(name="conf") @click.group(name="conf")
def conf() -> None: def conf():
"""配置管理命令 """配置管理命令
支持的配置项: 支持的配置项:
@@ -149,7 +149,7 @@ def conf() -> None:
@conf.command(name="set") @conf.command(name="set")
@click.argument("key") @click.argument("key")
@click.argument("value") @click.argument("value")
def set_config(key: str, value: str) -> None: def set_config(key: str, value: str):
"""设置配置项的值""" """设置配置项的值"""
if key not in CONFIG_VALIDATORS: if key not in CONFIG_VALIDATORS:
raise click.ClickException(f"不支持的配置项: {key}") raise click.ClickException(f"不支持的配置项: {key}")
@@ -178,7 +178,7 @@ def set_config(key: str, value: str) -> None:
@conf.command(name="get") @conf.command(name="get")
@click.argument("key", required=False) @click.argument("key", required=False)
def get_config(key: str | None = None) -> None: def get_config(key: str | None = None):
"""获取配置项的值,不提供key则显示所有可配置项""" """获取配置项的值,不提供key则显示所有可配置项"""
config = _load_config() config = _load_config()
+8 -8
View File
@@ -15,7 +15,7 @@ from ..utils import (
@click.group() @click.group()
def plug() -> None: def plug():
"""插件管理""" """插件管理"""
@@ -28,7 +28,7 @@ def _get_data_path() -> Path:
return (base / "data").resolve() return (base / "data").resolve()
def display_plugins(plugins, title=None, color=None) -> None: def display_plugins(plugins, title=None, color=None):
if title: if title:
click.echo(click.style(title, fg=color, bold=True)) click.echo(click.style(title, fg=color, bold=True))
@@ -45,7 +45,7 @@ def display_plugins(plugins, title=None, color=None) -> None:
@plug.command() @plug.command()
@click.argument("name") @click.argument("name")
def new(name: str) -> None: def new(name: str):
"""创建新插件""" """创建新插件"""
base_path = _get_data_path() base_path = _get_data_path()
plug_path = base_path / "plugins" / name plug_path = base_path / "plugins" / name
@@ -100,7 +100,7 @@ def new(name: str) -> None:
@plug.command() @plug.command()
@click.option("--all", "-a", is_flag=True, help="列出未安装的插件") @click.option("--all", "-a", is_flag=True, help="列出未安装的插件")
def list(all: bool) -> None: def list(all: bool):
"""列出插件""" """列出插件"""
base_path = _get_data_path() base_path = _get_data_path()
plugins = build_plug_list(base_path / "plugins") plugins = build_plug_list(base_path / "plugins")
@@ -141,7 +141,7 @@ def list(all: bool) -> None:
@plug.command() @plug.command()
@click.argument("name") @click.argument("name")
@click.option("--proxy", help="代理服务器地址") @click.option("--proxy", help="代理服务器地址")
def install(name: str, proxy: str | None) -> None: def install(name: str, proxy: str | None):
"""安装插件""" """安装插件"""
base_path = _get_data_path() base_path = _get_data_path()
plug_path = base_path / "plugins" plug_path = base_path / "plugins"
@@ -164,7 +164,7 @@ def install(name: str, proxy: str | None) -> None:
@plug.command() @plug.command()
@click.argument("name") @click.argument("name")
def remove(name: str) -> None: def remove(name: str):
"""卸载插件""" """卸载插件"""
base_path = _get_data_path() base_path = _get_data_path()
plugins = build_plug_list(base_path / "plugins") plugins = build_plug_list(base_path / "plugins")
@@ -187,7 +187,7 @@ def remove(name: str) -> None:
@plug.command() @plug.command()
@click.argument("name", required=False) @click.argument("name", required=False)
@click.option("--proxy", help="Github代理地址") @click.option("--proxy", help="Github代理地址")
def update(name: str, proxy: str | None) -> None: def update(name: str, proxy: str | None):
"""更新插件""" """更新插件"""
base_path = _get_data_path() base_path = _get_data_path()
plug_path = base_path / "plugins" plug_path = base_path / "plugins"
@@ -225,7 +225,7 @@ def update(name: str, proxy: str | None) -> None:
@plug.command() @plug.command()
@click.argument("query") @click.argument("query")
def search(query: str) -> None: def search(query: str):
"""搜索插件""" """搜索插件"""
base_path = _get_data_path() base_path = _get_data_path()
plugins = build_plug_list(base_path / "plugins") plugins = build_plug_list(base_path / "plugins")
+1 -1
View File
@@ -10,7 +10,7 @@ from filelock import FileLock, Timeout
from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root
async def run_astrbot(astrbot_root: Path) -> None: async def run_astrbot(astrbot_root: Path):
"""运行 AstrBot""" """运行 AstrBot"""
from astrbot.core import LogBroker, LogManager, db_helper, logger from astrbot.core import LogBroker, LogManager, db_helper, logger
from astrbot.core.initial_loader import InitialLoader from astrbot.core.initial_loader import InitialLoader
+1 -1
View File
@@ -19,7 +19,7 @@ class PluginStatus(str, Enum):
NOT_PUBLISHED = "未发布" NOT_PUBLISHED = "未发布"
def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None: def get_git_repo(url: str, target_path: Path, proxy: str | None = None):
"""从 Git 仓库下载代码并解压到指定路径""" """从 Git 仓库下载代码并解压到指定路径"""
temp_dir = Path(tempfile.mkdtemp()) temp_dir = Path(tempfile.mkdtemp())
try: try:
+2 -4
View File
@@ -57,9 +57,7 @@ class TruncateByTurnsCompressor:
Truncates the message list by removing older turns. Truncates the message list by removing older turns.
""" """
def __init__( def __init__(self, truncate_turns: int = 1, compression_threshold: float = 0.82):
self, truncate_turns: int = 1, compression_threshold: float = 0.82
) -> None:
"""Initialize the truncate by turns compressor. """Initialize the truncate by turns compressor.
Args: Args:
@@ -154,7 +152,7 @@ class LLMSummaryCompressor:
keep_recent: int = 4, keep_recent: int = 4,
instruction_text: str | None = None, instruction_text: str | None = None,
compression_threshold: float = 0.82, compression_threshold: float = 0.82,
) -> None: ):
"""Initialize the LLM summary compressor. """Initialize the LLM summary compressor.
Args: Args:
+1 -1
View File
@@ -13,7 +13,7 @@ class ContextManager:
def __init__( def __init__(
self, self,
config: ContextConfig, config: ContextConfig,
) -> None: ):
"""Initialize the context manager. """Initialize the context manager.
There are two strategies to handle context limit reached: There are two strategies to handle context limit reached:
+2 -3
View File
@@ -14,7 +14,8 @@ class HandoffTool(FunctionTool, Generic[TContext]):
parameters: dict | None = None, parameters: dict | None = None,
tool_description: str | None = None, tool_description: str | None = None,
**kwargs, **kwargs,
) -> None: ):
self.agent = agent
# Avoid passing duplicate `description` to the FunctionTool dataclass. # Avoid passing duplicate `description` to the FunctionTool dataclass.
# Some call sites (e.g. SubAgentOrchestrator) pass `description` via kwargs # Some call sites (e.g. SubAgentOrchestrator) pass `description` via kwargs
@@ -33,8 +34,6 @@ class HandoffTool(FunctionTool, Generic[TContext]):
# Optional provider override for this subagent. When set, the handoff # Optional provider override for this subagent. When set, the handoff
# execution will use this chat provider id instead of the global/default. # execution will use this chat provider id instead of the global/default.
self.provider_id: str | None = None self.provider_id: str | None = None
# Note: Must assign after super().__init__() to prevent parent class from overriding this attribute
self.agent = agent
def default_parameters(self) -> dict: def default_parameters(self) -> dict:
return { return {
+4 -4
View File
@@ -9,22 +9,22 @@ from .run_context import ContextWrapper, TContext
class BaseAgentRunHooks(Generic[TContext]): class BaseAgentRunHooks(Generic[TContext]):
async def on_agent_begin(self, run_context: ContextWrapper[TContext]) -> None: ... async def on_agent_begin(self, run_context: ContextWrapper[TContext]): ...
async def on_tool_start( async def on_tool_start(
self, self,
run_context: ContextWrapper[TContext], run_context: ContextWrapper[TContext],
tool: FunctionTool, tool: FunctionTool,
tool_args: dict | None, tool_args: dict | None,
) -> None: ... ): ...
async def on_tool_end( async def on_tool_end(
self, self,
run_context: ContextWrapper[TContext], run_context: ContextWrapper[TContext],
tool: FunctionTool, tool: FunctionTool,
tool_args: dict | None, tool_args: dict | None,
tool_result: mcp.types.CallToolResult | None, tool_result: mcp.types.CallToolResult | None,
) -> None: ... ): ...
async def on_agent_done( async def on_agent_done(
self, self,
run_context: ContextWrapper[TContext], run_context: ContextWrapper[TContext],
llm_response: LLMResponse, llm_response: LLMResponse,
) -> None: ... ): ...
+6 -6
View File
@@ -108,7 +108,7 @@ async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
class MCPClient: class MCPClient:
def __init__(self) -> None: def __init__(self):
# Initialize session and client objects # Initialize session and client objects
self.session: mcp.ClientSession | None = None self.session: mcp.ClientSession | None = None
self.exit_stack = AsyncExitStack() self.exit_stack = AsyncExitStack()
@@ -126,7 +126,7 @@ class MCPClient:
self._reconnect_lock = asyncio.Lock() # Lock for thread-safe reconnection self._reconnect_lock = asyncio.Lock() # Lock for thread-safe reconnection
self._reconnecting: bool = False # For logging and debugging self._reconnecting: bool = False # For logging and debugging
async def connect_to_server(self, mcp_server_config: dict, name: str) -> None: async def connect_to_server(self, mcp_server_config: dict, name: str):
"""Connect to MCP server """Connect to MCP server
If `url` parameter exists: If `url` parameter exists:
@@ -144,7 +144,7 @@ class MCPClient:
cfg = _prepare_config(mcp_server_config.copy()) cfg = _prepare_config(mcp_server_config.copy())
def logging_callback(msg: str) -> None: def logging_callback(msg: str):
# Handle MCP service error logs # Handle MCP service error logs
print(f"MCP Server {name} Error: {msg}") print(f"MCP Server {name} Error: {msg}")
self.server_errlogs.append(msg) self.server_errlogs.append(msg)
@@ -214,7 +214,7 @@ class MCPClient:
**cfg, **cfg,
) )
def callback(msg: str) -> None: def callback(msg: str):
# Handle MCP service error logs # Handle MCP service error logs
self.server_errlogs.append(msg) self.server_errlogs.append(msg)
@@ -343,7 +343,7 @@ class MCPClient:
return await _call_with_retry() return await _call_with_retry()
async def cleanup(self) -> None: async def cleanup(self):
"""Clean up resources including old exit stacks from reconnections""" """Clean up resources including old exit stacks from reconnections"""
# Close current exit stack # Close current exit stack
try: try:
@@ -365,7 +365,7 @@ class MCPTool(FunctionTool, Generic[TContext]):
def __init__( def __init__(
self, mcp_tool: mcp.Tool, mcp_client: MCPClient, mcp_server_name: str, **kwargs self, mcp_tool: mcp.Tool, mcp_client: MCPClient, mcp_server_name: str, **kwargs
) -> None: ):
super().__init__( super().__init__(
name=mcp_tool.name, name=mcp_tool.name,
description=mcp_tool.description or "", description=mcp_tool.description or "",
+1 -9
View File
@@ -3,13 +3,7 @@
from typing import Any, ClassVar, Literal, cast from typing import Any, ClassVar, Literal, cast
from pydantic import ( from pydantic import BaseModel, GetCoreSchemaHandler, model_serializer, model_validator
BaseModel,
GetCoreSchemaHandler,
PrivateAttr,
model_serializer,
model_validator,
)
from pydantic_core import core_schema from pydantic_core import core_schema
@@ -184,8 +178,6 @@ class Message(BaseModel):
tool_call_id: str | None = None tool_call_id: str | None = None
"""The ID of the tool call.""" """The ID of the tool call."""
_no_save: bool = PrivateAttr(default=False)
@model_validator(mode="after") @model_validator(mode="after")
def check_content_required(self): def check_content_required(self):
# assistant + tool_calls is not None: allow content to be None # assistant + tool_calls is not None: allow content to be None
@@ -10,7 +10,7 @@ from astrbot.core import logger
class CozeAPIClient: class CozeAPIClient:
def __init__(self, api_key: str, api_base: str = "https://api.coze.cn") -> None: def __init__(self, api_key: str, api_base: str = "https://api.coze.cn"):
self.api_key = api_key self.api_key = api_key
self.api_base = api_base self.api_base = api_base
self.session = None self.session = None
@@ -277,7 +277,7 @@ class CozeAPIClient:
logger.error(f"获取Coze消息列表失败: {e!s}") logger.error(f"获取Coze消息列表失败: {e!s}")
raise Exception(f"获取Coze消息列表失败: {e!s}") raise Exception(f"获取Coze消息列表失败: {e!s}")
async def close(self) -> None: async def close(self):
"""关闭会话""" """关闭会话"""
if self.session: if self.session:
await self.session.close() await self.session.close()
@@ -288,7 +288,7 @@ if __name__ == "__main__":
import asyncio import asyncio
import os import os
async def test_coze_api_client() -> None: async def test_coze_api_client():
api_key = os.getenv("COZE_API_KEY", "") api_key = os.getenv("COZE_API_KEY", "")
bot_id = os.getenv("COZE_BOT_ID", "") bot_id = os.getenv("COZE_BOT_ID", "")
client = CozeAPIClient(api_key=api_key) client = CozeAPIClient(api_key=api_key)
@@ -67,7 +67,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]):
if isinstance(self.timeout, str): if isinstance(self.timeout, str):
self.timeout = int(self.timeout) self.timeout = int(self.timeout)
def has_rag_options(self) -> bool: def has_rag_options(self):
"""判断是否有 RAG 选项 """判断是否有 RAG 选项
Returns: Returns:
@@ -10,7 +10,7 @@ from astrbot.core.provider.entities import (
LLMResponse, LLMResponse,
ProviderRequest, ProviderRequest,
) )
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path from astrbot.core.utils.astrbot_path import get_astrbot_data_path
from astrbot.core.utils.io import download_file from astrbot.core.utils.io import download_file
from ...hooks import BaseAgentRunHooks from ...hooks import BaseAgentRunHooks
@@ -291,8 +291,8 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
return Comp.Image(file=item["url"], url=item["url"]) return Comp.Image(file=item["url"], url=item["url"])
case "audio": case "audio":
# 仅支持 wav # 仅支持 wav
temp_dir = get_astrbot_temp_path() temp_dir = os.path.join(get_astrbot_data_path(), "temp")
path = os.path.join(temp_dir, f"dify_{item['filename']}.wav") path = os.path.join(temp_dir, f"{item['filename']}.wav")
await download_file(item["url"], path) await download_file(item["url"], path)
return Comp.Image(file=item["url"], url=item["url"]) return Comp.Image(file=item["url"], url=item["url"])
case "video": case "video":
@@ -31,7 +31,7 @@ async def _stream_sse(resp: ClientResponse) -> AsyncGenerator[dict, None]:
class DifyAPIClient: class DifyAPIClient:
def __init__(self, api_key: str, api_base: str = "https://api.dify.ai/v1") -> None: def __init__(self, api_key: str, api_base: str = "https://api.dify.ai/v1"):
self.api_key = api_key self.api_key = api_key
self.api_base = api_base self.api_base = api_base
self.session = ClientSession(trust_env=True) self.session = ClientSession(trust_env=True)
@@ -155,7 +155,7 @@ class DifyAPIClient:
raise Exception(f"Dify 文件上传失败:{resp.status}. {text}") raise Exception(f"Dify 文件上传失败:{resp.status}. {text}")
return await resp.json() # {"id": "xxx", ...} return await resp.json() # {"id": "xxx", ...}
async def close(self) -> None: async def close(self):
await self.session.close() await self.session.close()
async def get_chat_convs(self, user: str, limit: int = 20): async def get_chat_convs(self, user: str, limit: int = 20):
@@ -3,7 +3,6 @@ import sys
import time import time
import traceback import traceback
import typing as T import typing as T
from dataclasses import dataclass
from mcp.types import ( from mcp.types import (
BlobResourceContents, BlobResourceContents,
@@ -15,9 +14,8 @@ from mcp.types import (
) )
from astrbot import logger from astrbot import logger
from astrbot.core.agent.message import ImageURLPart, TextPart, ThinkPart from astrbot.core.agent.message import TextPart, ThinkPart
from astrbot.core.agent.tool import ToolSet from astrbot.core.agent.tool import ToolSet
from astrbot.core.agent.tool_image_cache import tool_image_cache
from astrbot.core.message.components import Json from astrbot.core.message.components import Json
from astrbot.core.message.message_event_result import ( from astrbot.core.message.message_event_result import (
MessageChain, MessageChain,
@@ -46,28 +44,6 @@ else:
from typing_extensions import override from typing_extensions import override
@dataclass(slots=True)
class _HandleFunctionToolsResult:
kind: T.Literal["message_chain", "tool_call_result_blocks", "cached_image"]
message_chain: MessageChain | None = None
tool_call_result_blocks: list[ToolCallMessageSegment] | None = None
cached_image: T.Any = None
@classmethod
def from_message_chain(cls, chain: MessageChain) -> "_HandleFunctionToolsResult":
return cls(kind="message_chain", message_chain=chain)
@classmethod
def from_tool_call_result_blocks(
cls, blocks: list[ToolCallMessageSegment]
) -> "_HandleFunctionToolsResult":
return cls(kind="tool_call_result_blocks", tool_call_result_blocks=blocks)
@classmethod
def from_cached_image(cls, image: T.Any) -> "_HandleFunctionToolsResult":
return cls(kind="cached_image", cached_image=image)
class ToolLoopAgentRunner(BaseAgentRunner[TContext]): class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
@override @override
async def reset( async def reset(
@@ -91,7 +67,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
custom_token_counter: TokenCounter | None = None, custom_token_counter: TokenCounter | None = None,
custom_compressor: ContextCompressor | None = None, custom_compressor: ContextCompressor | None = None,
tool_schema_mode: str | None = "full", tool_schema_mode: str | None = "full",
fallback_providers: list[Provider] | None = None,
**kwargs: T.Any, **kwargs: T.Any,
) -> None: ) -> None:
self.req = request self.req = request
@@ -121,17 +96,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
self.context_manager = ContextManager(self.context_config) self.context_manager = ContextManager(self.context_config)
self.provider = provider self.provider = provider
self.fallback_providers: list[Provider] = []
seen_provider_ids: set[str] = {str(provider.provider_config.get("id", ""))}
for fallback_provider in fallback_providers or []:
fallback_id = str(fallback_provider.provider_config.get("id", ""))
if fallback_provider is provider:
continue
if fallback_id and fallback_id in seen_provider_ids:
continue
self.fallback_providers.append(fallback_provider)
if fallback_id:
seen_provider_ids.add(fallback_id)
self.final_llm_resp = None self.final_llm_resp = None
self._state = AgentState.IDLE self._state = AgentState.IDLE
self.tool_executor = tool_executor self.tool_executor = tool_executor
@@ -161,10 +125,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
messages = [] messages = []
# append existing messages in the run context # append existing messages in the run context
for msg in request.contexts: for msg in request.contexts:
m = Message.model_validate(msg) messages.append(Message.model_validate(msg))
if isinstance(msg, dict) and msg.get("_no_save"):
m._no_save = True
messages.append(m)
if request.prompt is not None: if request.prompt is not None:
m = await request.assemble_context() m = await request.assemble_context()
messages.append(Message.model_validate(m)) messages.append(Message.model_validate(m))
@@ -178,19 +139,16 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
self.stats = AgentStats() self.stats = AgentStats()
self.stats.start_time = time.time() self.stats.start_time = time.time()
async def _iter_llm_responses( async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
self, *, include_model: bool = True
) -> T.AsyncGenerator[LLMResponse, None]:
"""Yields chunks *and* a final LLMResponse.""" """Yields chunks *and* a final LLMResponse."""
payload = { payload = {
"contexts": self.run_context.messages, # list[Message] "contexts": self.run_context.messages, # list[Message]
"func_tool": self.req.func_tool, "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, "session_id": self.req.session_id,
"extra_user_content_parts": self.req.extra_user_content_parts, # list[ContentPart] "extra_user_content_parts": self.req.extra_user_content_parts, # list[ContentPart]
} }
if include_model:
# For primary provider we keep explicit model selection if provided.
payload["model"] = self.req.model
if self.streaming: if self.streaming:
stream = self.provider.text_chat_stream(**payload) stream = self.provider.text_chat_stream(**payload)
async for resp in stream: # type: ignore async for resp in stream: # type: ignore
@@ -198,83 +156,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
else: else:
yield await self.provider.text_chat(**payload) yield await self.provider.text_chat(**payload)
async def _iter_llm_responses_with_fallback(
self,
) -> T.AsyncGenerator[LLMResponse, None]:
"""Wrap _iter_llm_responses with provider fallback handling."""
candidates = [self.provider, *self.fallback_providers]
total_candidates = len(candidates)
last_exception: Exception | None = None
last_err_response: LLMResponse | None = None
for idx, candidate in enumerate(candidates):
candidate_id = candidate.provider_config.get("id", "<unknown>")
is_last_candidate = idx == total_candidates - 1
if idx > 0:
logger.warning(
"Switched from %s to fallback chat provider: %s",
self.provider.provider_config.get("id", "<unknown>"),
candidate_id,
)
self.provider = candidate
has_stream_output = False
try:
async for resp in self._iter_llm_responses(include_model=idx == 0):
if resp.is_chunk:
has_stream_output = True
yield resp
continue
if (
resp.role == "err"
and not has_stream_output
and (not is_last_candidate)
):
last_err_response = resp
logger.warning(
"Chat Model %s returns error response, trying fallback to next provider.",
candidate_id,
)
break
yield resp
return
if has_stream_output:
return
except Exception as exc: # noqa: BLE001
last_exception = exc
logger.warning(
"Chat Model %s request error: %s",
candidate_id,
exc,
exc_info=True,
)
continue
if last_err_response:
yield last_err_response
return
if last_exception:
yield LLMResponse(
role="err",
completion_text=(
"All chat models failed: "
f"{type(last_exception).__name__}: {last_exception}"
),
)
return
yield LLMResponse(
role="err",
completion_text="All available chat models are unavailable.",
)
def _simple_print_message_role(self, tag: str = ""):
roles = []
for message in self.run_context.messages:
roles.append(message.role)
logger.debug(f"{tag} RunCtx.messages -> [{len(roles)}] {','.join(roles)}")
@override @override
async def step(self): async def step(self):
"""Process a single step of the agent. """Process a single step of the agent.
@@ -295,13 +176,11 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
# do truncate and compress # do truncate and compress
token_usage = self.req.conversation.token_usage if self.req.conversation else 0 token_usage = self.req.conversation.token_usage if self.req.conversation else 0
self._simple_print_message_role("[BefCompact]")
self.run_context.messages = await self.context_manager.process( self.run_context.messages = await self.context_manager.process(
self.run_context.messages, trusted_token_usage=token_usage self.run_context.messages, trusted_token_usage=token_usage
) )
self._simple_print_message_role("[AftCompact]")
async for llm_response in self._iter_llm_responses_with_fallback(): async for llm_response in self._iter_llm_responses():
if llm_response.is_chunk: if llm_response.is_chunk:
# update ttft # update ttft
if self.stats.time_to_first_token == 0: if self.stats.time_to_first_token == 0:
@@ -357,7 +236,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
), ),
), ),
) )
return
if not llm_resp.tools_call_name: if not llm_resp.tools_call_name:
# 如果没有工具调用,转换到完成状态 # 如果没有工具调用,转换到完成状态
@@ -376,10 +254,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
) )
if llm_resp.completion_text: if llm_resp.completion_text:
parts.append(TextPart(text=llm_resp.completion_text)) parts.append(TextPart(text=llm_resp.completion_text))
if len(parts) == 0:
logger.warning(
"LLM returned empty assistant message with no tool calls."
)
self.run_context.messages.append(Message(role="assistant", content=parts)) self.run_context.messages.append(Message(role="assistant", content=parts))
# call the on_agent_done hook # call the on_agent_done hook
@@ -408,27 +282,20 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
llm_resp, _ = await self._resolve_tool_exec(llm_resp) llm_resp, _ = await self._resolve_tool_exec(llm_resp)
tool_call_result_blocks = [] tool_call_result_blocks = []
cached_images = [] # Collect cached images for LLM visibility
async for result in self._handle_function_tools(self.req, llm_resp): async for result in self._handle_function_tools(self.req, llm_resp):
if result.kind == "tool_call_result_blocks": if isinstance(result, list):
if result.tool_call_result_blocks is not None: tool_call_result_blocks = result
tool_call_result_blocks = result.tool_call_result_blocks elif isinstance(result, MessageChain):
elif result.kind == "cached_image": if result.type is None:
if result.cached_image is not None:
# Collect cached image info
cached_images.append(result.cached_image)
elif result.kind == "message_chain":
chain = result.message_chain
if chain is None or chain.type is None:
# should not happen # should not happen
continue continue
if chain.type == "tool_direct_result": if result.type == "tool_direct_result":
ar_type = "tool_call_result" ar_type = "tool_call_result"
else: else:
ar_type = chain.type ar_type = result.type
yield AgentResponse( yield AgentResponse(
type=ar_type, type=ar_type,
data=AgentResponseData(chain=chain), data=AgentResponseData(chain=result),
) )
# 将结果添加到上下文中 # 将结果添加到上下文中
@@ -442,8 +309,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
) )
if llm_resp.completion_text: if llm_resp.completion_text:
parts.append(TextPart(text=llm_resp.completion_text)) parts.append(TextPart(text=llm_resp.completion_text))
if len(parts) == 0:
parts = None
tool_calls_result = ToolCallsResult( tool_calls_result = ToolCallsResult(
tool_calls_info=AssistantMessageSegment( tool_calls_info=AssistantMessageSegment(
tool_calls=llm_resp.to_openai_to_calls_model(), tool_calls=llm_resp.to_openai_to_calls_model(),
@@ -456,41 +321,6 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
tool_calls_result.to_openai_messages_model() tool_calls_result.to_openai_messages_model()
) )
# If there are cached images and the model supports image input,
# append a user message with images so LLM can see them
if cached_images:
modalities = self.provider.provider_config.get("modalities", [])
supports_image = "image" in modalities
if supports_image:
# Build user message with images for LLM to review
image_parts = []
for cached_img in cached_images:
img_data = tool_image_cache.get_image_base64_by_path(
cached_img.file_path, cached_img.mime_type
)
if img_data:
base64_data, mime_type = img_data
image_parts.append(
TextPart(
text=f"[Image from tool '{cached_img.tool_name}', path='{cached_img.file_path}']"
)
)
image_parts.append(
ImageURLPart(
image_url=ImageURLPart.ImageURL(
url=f"data:{mime_type};base64,{base64_data}",
id=cached_img.file_path,
)
)
)
if image_parts:
self.run_context.messages.append(
Message(role="user", content=image_parts)
)
logger.debug(
f"Appended {len(cached_images)} cached image(s) to context for LLM review"
)
self.req.append_tool_calls_result(tool_calls_result) self.req.append_tool_calls_result(tool_calls_result)
async def step_until_done( async def step_until_done(
@@ -526,7 +356,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
self, self,
req: ProviderRequest, req: ProviderRequest,
llm_response: LLMResponse, llm_response: LLMResponse,
) -> T.AsyncGenerator[_HandleFunctionToolsResult, None]: ) -> T.AsyncGenerator[MessageChain | list[ToolCallMessageSegment], None]:
"""处理函数工具调用。""" """处理函数工具调用。"""
tool_call_result_blocks: list[ToolCallMessageSegment] = [] tool_call_result_blocks: list[ToolCallMessageSegment] = []
logger.info(f"Agent 使用工具: {llm_response.tools_call_name}") logger.info(f"Agent 使用工具: {llm_response.tools_call_name}")
@@ -537,20 +367,18 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
llm_response.tools_call_args, llm_response.tools_call_args,
llm_response.tools_call_ids, llm_response.tools_call_ids,
): ):
yield _HandleFunctionToolsResult.from_message_chain( yield MessageChain(
MessageChain( type="tool_call",
type="tool_call", chain=[
chain=[ Json(
Json( data={
data={ "id": func_tool_id,
"id": func_tool_id, "name": func_tool_name,
"name": func_tool_name, "args": func_tool_args,
"args": func_tool_args, "ts": time.time(),
"ts": time.time(), }
} )
) ],
],
)
) )
try: try:
if not req.func_tool: if not req.func_tool:
@@ -636,28 +464,15 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
), ),
) )
elif isinstance(res.content[0], ImageContent): elif isinstance(res.content[0], ImageContent):
# Cache the image instead of sending directly
cached_img = tool_image_cache.save_image(
base64_data=res.content[0].data,
tool_call_id=func_tool_id,
tool_name=func_tool_name,
index=0,
mime_type=res.content[0].mimeType or "image/png",
)
tool_call_result_blocks.append( tool_call_result_blocks.append(
ToolCallMessageSegment( ToolCallMessageSegment(
role="tool", role="tool",
tool_call_id=func_tool_id, tool_call_id=func_tool_id,
content=( content="The tool has successfully returned an image and sent directly to the user. You can describe it in your next response.",
f"Image returned and cached at path='{cached_img.file_path}'. "
f"Review the image below. Use send_message_to_user to send it to the user if satisfied, "
f"with type='image' and path='{cached_img.file_path}'."
),
), ),
) )
# Yield image info for LLM visibility (will be handled in step()) yield MessageChain(type="tool_direct_result").base64_image(
yield _HandleFunctionToolsResult.from_cached_image( res.content[0].data,
cached_img
) )
elif isinstance(res.content[0], EmbeddedResource): elif isinstance(res.content[0], EmbeddedResource):
resource = res.content[0].resource resource = res.content[0].resource
@@ -674,29 +489,16 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
and resource.mimeType and resource.mimeType
and resource.mimeType.startswith("image/") and resource.mimeType.startswith("image/")
): ):
# Cache the image instead of sending directly
cached_img = tool_image_cache.save_image(
base64_data=resource.blob,
tool_call_id=func_tool_id,
tool_name=func_tool_name,
index=0,
mime_type=resource.mimeType,
)
tool_call_result_blocks.append( tool_call_result_blocks.append(
ToolCallMessageSegment( ToolCallMessageSegment(
role="tool", role="tool",
tool_call_id=func_tool_id, tool_call_id=func_tool_id,
content=( content="The tool has successfully returned an image and sent directly to the user. You can describe it in your next response.",
f"Image returned and cached at path='{cached_img.file_path}'. "
f"Review the image below. Use send_message_to_user to send it to the user if satisfied, "
f"with type='image' and path='{cached_img.file_path}'."
),
), ),
) )
# Yield image info for LLM visibility yield MessageChain(
yield _HandleFunctionToolsResult.from_cached_image( type="tool_direct_result",
cached_img ).base64_image(resource.blob)
)
else: else:
tool_call_result_blocks.append( tool_call_result_blocks.append(
ToolCallMessageSegment( ToolCallMessageSegment(
@@ -757,27 +559,23 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
# yield the last tool call result # yield the last tool call result
if tool_call_result_blocks: if tool_call_result_blocks:
last_tcr_content = str(tool_call_result_blocks[-1].content) last_tcr_content = str(tool_call_result_blocks[-1].content)
yield _HandleFunctionToolsResult.from_message_chain( yield MessageChain(
MessageChain( type="tool_call_result",
type="tool_call_result", chain=[
chain=[ Json(
Json( data={
data={ "id": func_tool_id,
"id": func_tool_id, "ts": time.time(),
"ts": time.time(), "result": last_tcr_content,
"result": last_tcr_content, }
} )
) ],
],
)
) )
logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}") logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}")
# 处理函数调用响应 # 处理函数调用响应
if tool_call_result_blocks: if tool_call_result_blocks:
yield _HandleFunctionToolsResult.from_tool_call_result_blocks( yield tool_call_result_blocks
tool_call_result_blocks
)
def _build_tool_requery_context( def _build_tool_requery_context(
self, tool_names: list[str] self, tool_names: list[str]
+12 -25
View File
@@ -64,7 +64,7 @@ class FunctionTool(ToolSchema, Generic[TContext]):
with a task identifier while the real work continues asynchronously. with a task identifier while the real work continues asynchronously.
""" """
def __repr__(self) -> str: def __repr__(self):
return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})" return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
async def call(self, context: ContextWrapper[TContext], **kwargs) -> ToolExecResult: async def call(self, context: ContextWrapper[TContext], **kwargs) -> ToolExecResult:
@@ -88,7 +88,7 @@ class ToolSet:
"""Check if the tool set is empty.""" """Check if the tool set is empty."""
return len(self.tools) == 0 return len(self.tools) == 0
def add_tool(self, tool: FunctionTool) -> None: def add_tool(self, tool: FunctionTool):
"""Add a tool to the set.""" """Add a tool to the set."""
# 检查是否已存在同名工具 # 检查是否已存在同名工具
for i, existing_tool in enumerate(self.tools): for i, existing_tool in enumerate(self.tools):
@@ -97,7 +97,7 @@ class ToolSet:
return return
self.tools.append(tool) self.tools.append(tool)
def remove_tool(self, name: str) -> None: def remove_tool(self, name: str):
"""Remove a tool by its name.""" """Remove a tool by its name."""
self.tools = [tool for tool in self.tools if tool.name != name] self.tools = [tool for tool in self.tools if tool.name != name]
@@ -156,7 +156,7 @@ class ToolSet:
func_args: list, func_args: list,
desc: str, desc: str,
handler: Callable[..., Awaitable[Any]], handler: Callable[..., Awaitable[Any]],
) -> None: ):
"""Add a function tool to the set.""" """Add a function tool to the set."""
params = { params = {
"type": "object", # hard-coded here "type": "object", # hard-coded here
@@ -176,7 +176,7 @@ class ToolSet:
self.add_tool(_func) self.add_tool(_func)
@deprecated(reason="Use remove_tool() instead", version="4.0.0") @deprecated(reason="Use remove_tool() instead", version="4.0.0")
def remove_func(self, name: str) -> None: def remove_func(self, name: str):
"""Remove a function tool by its name.""" """Remove a function tool by its name."""
self.remove_tool(name) self.remove_tool(name)
@@ -246,18 +246,8 @@ class ToolSet:
result = {} result = {}
# Avoid side effects by not modifying the original schema if "type" in schema and schema["type"] in supported_types:
origin_type = schema.get("type") result["type"] = schema["type"]
target_type = origin_type
# Compatibility fix: Gemini API expects 'type' to be a string (enum),
# but standard JSON Schema (MCP) allows lists (e.g. ["string", "null"]).
# We fallback to the first non-null type.
if isinstance(origin_type, list):
target_type = next((t for t in origin_type if t != "null"), "string")
if target_type in supported_types:
result["type"] = target_type
if "format" in schema and schema["format"] in supported_formats.get( if "format" in schema and schema["format"] in supported_formats.get(
result["type"], result["type"],
set(), set(),
@@ -285,9 +275,6 @@ class ToolSet:
prop_value = convert_schema(value) prop_value = convert_schema(value)
if "default" in prop_value: if "default" in prop_value:
del prop_value["default"] del prop_value["default"]
# see #5217
if "additionalProperties" in prop_value:
del prop_value["additionalProperties"]
properties[key] = prop_value properties[key] = prop_value
if properties: if properties:
@@ -328,22 +315,22 @@ class ToolSet:
"""获取所有工具的名称列表""" """获取所有工具的名称列表"""
return [tool.name for tool in self.tools] return [tool.name for tool in self.tools]
def merge(self, other: "ToolSet") -> None: def merge(self, other: "ToolSet"):
"""Merge another ToolSet into this one.""" """Merge another ToolSet into this one."""
for tool in other.tools: for tool in other.tools:
self.add_tool(tool) self.add_tool(tool)
def __len__(self) -> int: def __len__(self):
return len(self.tools) return len(self.tools)
def __bool__(self) -> bool: def __bool__(self):
return len(self.tools) > 0 return len(self.tools) > 0
def __iter__(self): def __iter__(self):
return iter(self.tools) return iter(self.tools)
def __repr__(self) -> str: def __repr__(self):
return f"ToolSet(tools={self.tools})" return f"ToolSet(tools={self.tools})"
def __str__(self) -> str: def __str__(self):
return f"ToolSet(tools={self.tools})" return f"ToolSet(tools={self.tools})"
-162
View File
@@ -1,162 +0,0 @@
"""Tool image cache module for storing and retrieving images returned by tools.
This module allows LLM to review images before deciding whether to send them to users.
"""
import base64
import os
import time
from dataclasses import dataclass, field
from typing import ClassVar
from astrbot import logger
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
@dataclass
class CachedImage:
"""Represents a cached image from a tool call."""
tool_call_id: str
"""The tool call ID that produced this image."""
tool_name: str
"""The name of the tool that produced this image."""
file_path: str
"""The file path where the image is stored."""
mime_type: str
"""The MIME type of the image."""
created_at: float = field(default_factory=time.time)
"""Timestamp when the image was cached."""
class ToolImageCache:
"""Manages cached images from tool calls.
Images are stored in data/temp/tool_images/ and can be retrieved by file path.
"""
_instance: ClassVar["ToolImageCache | None"] = None
CACHE_DIR_NAME: ClassVar[str] = "tool_images"
# Cache expiry time in seconds (1 hour)
CACHE_EXPIRY: ClassVar[int] = 3600
def __new__(cls) -> "ToolImageCache":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self) -> None:
if self._initialized:
return
self._initialized = True
self._cache_dir = os.path.join(get_astrbot_temp_path(), self.CACHE_DIR_NAME)
os.makedirs(self._cache_dir, exist_ok=True)
logger.debug(f"ToolImageCache initialized, cache dir: {self._cache_dir}")
def _get_file_extension(self, mime_type: str) -> str:
"""Get file extension from MIME type."""
mime_to_ext = {
"image/png": ".png",
"image/jpeg": ".jpg",
"image/jpg": ".jpg",
"image/gif": ".gif",
"image/webp": ".webp",
"image/bmp": ".bmp",
"image/svg+xml": ".svg",
}
return mime_to_ext.get(mime_type.lower(), ".png")
def save_image(
self,
base64_data: str,
tool_call_id: str,
tool_name: str,
index: int = 0,
mime_type: str = "image/png",
) -> CachedImage:
"""Save an image to cache and return the cached image info.
Args:
base64_data: Base64 encoded image data.
tool_call_id: The tool call ID that produced this image.
tool_name: The name of the tool that produced this image.
index: The index of the image (for multiple images from same tool call).
mime_type: The MIME type of the image.
Returns:
CachedImage object with file path.
"""
ext = self._get_file_extension(mime_type)
file_name = f"{tool_call_id}_{index}{ext}"
file_path = os.path.join(self._cache_dir, file_name)
# Decode and save the image
try:
image_bytes = base64.b64decode(base64_data)
with open(file_path, "wb") as f:
f.write(image_bytes)
logger.debug(f"Saved tool image to: {file_path}")
except Exception as e:
logger.error(f"Failed to save tool image: {e}")
raise
return CachedImage(
tool_call_id=tool_call_id,
tool_name=tool_name,
file_path=file_path,
mime_type=mime_type,
)
def get_image_base64_by_path(
self, file_path: str, mime_type: str = "image/png"
) -> tuple[str, str] | None:
"""Read an image file and return its base64 encoded data.
Args:
file_path: The file path of the cached image.
mime_type: The MIME type of the image.
Returns:
Tuple of (base64_data, mime_type) if found, None otherwise.
"""
if not os.path.exists(file_path):
return None
try:
with open(file_path, "rb") as f:
image_bytes = f.read()
base64_data = base64.b64encode(image_bytes).decode("utf-8")
return base64_data, mime_type
except Exception as e:
logger.error(f"Failed to read cached image {file_path}: {e}")
return None
def cleanup_expired(self) -> int:
"""Clean up expired cached images.
Returns:
Number of images cleaned up.
"""
now = time.time()
cleaned = 0
try:
for file_name in os.listdir(self._cache_dir):
file_path = os.path.join(self._cache_dir, file_name)
if os.path.isfile(file_path):
file_age = now - os.path.getmtime(file_path)
if file_age > self.CACHE_EXPIRY:
os.remove(file_path)
cleaned += 1
except Exception as e:
logger.warning(f"Error during cache cleanup: {e}")
if cleaned:
logger.info(f"Cleaned up {cleaned} expired cached images")
return cleaned
# Global singleton instance
tool_image_cache = ToolImageCache()
+4 -4
View File
@@ -12,7 +12,7 @@ from astrbot.core.star.star_handler import EventType
class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]): class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
async def on_agent_done(self, run_context, llm_response) -> None: async def on_agent_done(self, run_context, llm_response):
# 执行事件钩子 # 执行事件钩子
if llm_response and llm_response.reasoning_content: if llm_response and llm_response.reasoning_content:
# we will use this in result_decorate stage to inject reasoning content to chain # we will use this in result_decorate stage to inject reasoning content to chain
@@ -31,7 +31,7 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
run_context: ContextWrapper[AstrAgentContext], run_context: ContextWrapper[AstrAgentContext],
tool: FunctionTool[Any], tool: FunctionTool[Any],
tool_args: dict | None, tool_args: dict | None,
) -> None: ):
await call_event_hook( await call_event_hook(
run_context.context.event, run_context.context.event,
EventType.OnUsingLLMToolEvent, EventType.OnUsingLLMToolEvent,
@@ -45,7 +45,7 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
tool: FunctionTool[Any], tool: FunctionTool[Any],
tool_args: dict | None, tool_args: dict | None,
tool_result: CallToolResult | None, tool_result: CallToolResult | None,
) -> None: ):
run_context.context.event.clear_result() run_context.context.event.clear_result()
await call_event_hook( await call_event_hook(
run_context.context.event, run_context.context.event,
@@ -59,7 +59,7 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
platform_name = run_context.context.event.get_platform_name() platform_name = run_context.context.event.get_platform_name()
if ( if (
platform_name == "webchat" platform_name == "webchat"
and tool.name in ["web_search_tavily", "web_search_bocha"] and tool.name == "web_search_tavily"
and len(run_context.messages) > 0 and len(run_context.messages) > 0
and tool_result and tool_result
and len(tool_result.content) and len(tool_result.content)
+3 -3
View File
@@ -295,7 +295,7 @@ async def _run_agent_feeder(
max_step: int, max_step: int,
show_tool_use: bool, show_tool_use: bool,
show_reasoning: bool, show_reasoning: bool,
) -> None: ):
"""运行 Agent 并将文本输出分句放入队列""" """运行 Agent 并将文本输出分句放入队列"""
buffer = "" buffer = ""
try: try:
@@ -352,7 +352,7 @@ async def _safe_tts_stream_wrapper(
tts_provider: TTSProvider, tts_provider: TTSProvider,
text_queue: asyncio.Queue[str | None], text_queue: asyncio.Queue[str | None],
audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]", audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]",
) -> None: ):
"""包装原生流式 TTS 确保异常处理和队列关闭""" """包装原生流式 TTS 确保异常处理和队列关闭"""
try: try:
await tts_provider.get_audio_stream(text_queue, audio_queue) await tts_provider.get_audio_stream(text_queue, audio_queue)
@@ -366,7 +366,7 @@ async def _simulated_stream_tts(
tts_provider: TTSProvider, tts_provider: TTSProvider,
text_queue: asyncio.Queue[str | None], text_queue: asyncio.Queue[str | None],
audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]", audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]",
) -> None: ):
"""模拟流式 TTS 分句生成音频""" """模拟流式 TTS 分句生成音频"""
try: try:
while True: while True:
+2 -2
View File
@@ -57,7 +57,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
elif tool.is_background_task: elif tool.is_background_task:
task_id = uuid.uuid4().hex task_id = uuid.uuid4().hex
async def _run_in_background() -> None: async def _run_in_background():
try: try:
await cls._execute_background( await cls._execute_background(
tool=tool, tool=tool,
@@ -153,7 +153,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
run_context: ContextWrapper[AstrAgentContext], run_context: ContextWrapper[AstrAgentContext],
task_id: str, task_id: str,
**tool_args, **tool_args,
) -> None: ):
from astrbot.core.astr_main_agent import ( from astrbot.core.astr_main_agent import (
MainAgentBuildConfig, MainAgentBuildConfig,
_get_session_conv, _get_session_conv,
+31 -185
View File
@@ -52,17 +52,6 @@ from astrbot.core.tools.cron_tools import (
) )
from astrbot.core.utils.file_extract import extract_file_moonshotai from astrbot.core.utils.file_extract import extract_file_moonshotai
from astrbot.core.utils.llm_metadata import LLM_METADATAS from astrbot.core.utils.llm_metadata import LLM_METADATAS
from astrbot.core.utils.quoted_message.settings import (
SETTINGS as DEFAULT_QUOTED_MESSAGE_SETTINGS,
)
from astrbot.core.utils.quoted_message.settings import (
QuotedMessageParserSettings,
)
from astrbot.core.utils.quoted_message_parser import (
extract_quoted_message_images,
extract_quoted_message_text,
)
from astrbot.core.utils.string_utils import normalize_and_dedupe_strings
@dataclass(slots=True) @dataclass(slots=True)
@@ -119,8 +108,6 @@ class MainAgentBuildConfig:
provider_settings: dict = field(default_factory=dict) provider_settings: dict = field(default_factory=dict)
subagent_orchestrator: dict = field(default_factory=dict) subagent_orchestrator: dict = field(default_factory=dict)
timezone: str | None = None timezone: str | None = None
max_quoted_fallback_images: int = 20
"""Maximum number of images injected from quoted-message fallback extraction."""
@dataclass(slots=True) @dataclass(slots=True)
@@ -339,24 +326,6 @@ async def _ensure_persona_and_skills(
) )
tmgr = plugin_context.get_llm_tool_manager() tmgr = plugin_context.get_llm_tool_manager()
# inject toolset in the persona
if (persona and persona.get("tools") is None) or not persona:
persona_toolset = tmgr.get_full_tool_set()
for tool in list(persona_toolset):
if not tool.active:
persona_toolset.remove_tool(tool.name)
else:
persona_toolset = ToolSet()
if persona["tools"]:
for tool_name in persona["tools"]:
tool = tmgr.get_func(tool_name)
if tool and tool.active:
persona_toolset.add_tool(tool)
if not req.func_tool:
req.func_tool = persona_toolset
else:
req.func_tool.merge(persona_toolset)
# sub agents integration # sub agents integration
orch_cfg = plugin_context.get_config().get("subagent_orchestrator", {}) orch_cfg = plugin_context.get_config().get("subagent_orchestrator", {})
so = plugin_context.subagent_orchestrator so = plugin_context.subagent_orchestrator
@@ -402,19 +371,22 @@ async def _ensure_persona_and_skills(
assigned_tools.add(name) assigned_tools.add(name)
if req.func_tool is None: if req.func_tool is None:
req.func_tool = ToolSet() toolset = ToolSet()
else:
toolset = req.func_tool
# add subagent handoff tools # add subagent handoff tools
for tool in so.handoffs: for tool in so.handoffs:
req.func_tool.add_tool(tool) toolset.add_tool(tool)
# check duplicates # check duplicates
if remove_dup: if remove_dup:
handoff_names = {tool.name for tool in so.handoffs} names = toolset.names()
for tool_name in assigned_tools: for tool_name in assigned_tools:
if tool_name in handoff_names: if tool_name in names:
continue toolset.remove_tool(tool_name)
req.func_tool.remove_tool(tool_name)
req.func_tool = toolset
router_prompt = ( router_prompt = (
plugin_context.get_config() plugin_context.get_config()
@@ -423,14 +395,32 @@ async def _ensure_persona_and_skills(
).strip() ).strip()
if router_prompt: if router_prompt:
req.system_prompt += f"\n{router_prompt}\n" req.system_prompt += f"\n{router_prompt}\n"
return
# inject toolset in the persona
if (persona and persona.get("tools") is None) or not persona:
toolset = tmgr.get_full_tool_set()
for tool in list(toolset):
if not tool.active:
toolset.remove_tool(tool.name)
else:
toolset = ToolSet()
if persona["tools"]:
for tool_name in persona["tools"]:
tool = tmgr.get_func(tool_name)
if tool and tool.active:
toolset.add_tool(tool)
if not req.func_tool:
req.func_tool = toolset
else:
req.func_tool.merge(toolset)
try: try:
event.trace.record( event.trace.record(
"sel_persona", "sel_persona", persona_id=persona_id, persona_toolset=toolset.names()
persona_id=persona_id,
persona_toolset=persona_toolset.names(),
) )
except Exception: except Exception:
pass pass
logger.debug("Tool set for persona %s: %s", persona_id, toolset.names())
async def _request_img_caption( async def _request_img_caption(
@@ -483,29 +473,11 @@ async def _ensure_img_caption(
logger.error("处理图片描述失败: %s", exc) logger.error("处理图片描述失败: %s", exc)
def _append_quoted_image_attachment(req: ProviderRequest, image_path: str) -> None:
req.extra_user_content_parts.append(
TextPart(text=f"[Image Attachment in quoted message: path {image_path}]")
)
def _get_quoted_message_parser_settings(
provider_settings: dict[str, object] | None,
) -> QuotedMessageParserSettings:
if not isinstance(provider_settings, dict):
return DEFAULT_QUOTED_MESSAGE_SETTINGS
overrides = provider_settings.get("quoted_message_parser")
if not isinstance(overrides, dict):
return DEFAULT_QUOTED_MESSAGE_SETTINGS
return DEFAULT_QUOTED_MESSAGE_SETTINGS.with_overrides(overrides)
async def _process_quote_message( async def _process_quote_message(
event: AstrMessageEvent, event: AstrMessageEvent,
req: ProviderRequest, req: ProviderRequest,
img_cap_prov_id: str, img_cap_prov_id: str,
plugin_context: Context, plugin_context: Context,
quoted_message_settings: QuotedMessageParserSettings = DEFAULT_QUOTED_MESSAGE_SETTINGS,
) -> None: ) -> None:
quote = None quote = None
for comp in event.message_obj.message: for comp in event.message_obj.message:
@@ -517,15 +489,7 @@ async def _process_quote_message(
content_parts = [] content_parts = []
sender_info = f"({quote.sender_nickname}): " if quote.sender_nickname else "" sender_info = f"({quote.sender_nickname}): " if quote.sender_nickname else ""
message_str = ( message_str = quote.message_str or "[Empty Text]"
await extract_quoted_message_text(
event,
quote,
settings=quoted_message_settings,
)
or quote.message_str
or "[Empty Text]"
)
content_parts.append(f"{sender_info}{message_str}") content_parts.append(f"{sender_info}{message_str}")
image_seg = None image_seg = None
@@ -631,13 +595,11 @@ async def _decorate_llm_request(
) )
img_cap_prov_id = cfg.get("default_image_caption_provider_id") or "" img_cap_prov_id = cfg.get("default_image_caption_provider_id") or ""
quoted_message_settings = _get_quoted_message_parser_settings(cfg)
await _process_quote_message( await _process_quote_message(
event, event,
req, req,
img_cap_prov_id, img_cap_prov_id,
plugin_context, plugin_context,
quoted_message_settings,
) )
tz = config.timezone tz = config.timezone
@@ -870,41 +832,6 @@ def _get_compress_provider(
return provider return provider
def _get_fallback_chat_providers(
provider: Provider, plugin_context: Context, provider_settings: dict
) -> list[Provider]:
fallback_ids = provider_settings.get("fallback_chat_models", [])
if not isinstance(fallback_ids, list):
logger.warning(
"fallback_chat_models setting is not a list, skip fallback providers."
)
return []
provider_id = str(provider.provider_config.get("id", ""))
seen_provider_ids: set[str] = {provider_id} if provider_id else set()
fallbacks: list[Provider] = []
for fallback_id in fallback_ids:
if not isinstance(fallback_id, str) or not fallback_id:
continue
if fallback_id in seen_provider_ids:
continue
fallback_provider = plugin_context.get_provider_by_id(fallback_id)
if fallback_provider is None:
logger.warning("Fallback chat provider `%s` not found, skip.", fallback_id)
continue
if not isinstance(fallback_provider, Provider):
logger.warning(
"Fallback chat provider `%s` is invalid type: %s, skip.",
fallback_id,
type(fallback_provider),
)
continue
fallbacks.append(fallback_provider)
seen_provider_ids.add(fallback_id)
return fallbacks
async def build_main_agent( async def build_main_agent(
*, *,
event: AstrMessageEvent, event: AstrMessageEvent,
@@ -943,8 +870,6 @@ async def build_main_agent(
return None return None
req.prompt = event.message_str[len(config.provider_wake_prefix) :] req.prompt = event.message_str[len(config.provider_wake_prefix) :]
# media files attachments
for comp in event.message_obj.message: for comp in event.message_obj.message:
if isinstance(comp, Image): if isinstance(comp, Image):
image_path = await comp.convert_to_file_path() image_path = await comp.convert_to_file_path()
@@ -960,81 +885,6 @@ async def build_main_agent(
text=f"[File Attachment: name {file_name}, path {file_path}]" text=f"[File Attachment: name {file_name}, path {file_path}]"
) )
) )
# quoted message attachments
reply_comps = [
comp for comp in event.message_obj.message if isinstance(comp, Reply)
]
quoted_message_settings = _get_quoted_message_parser_settings(
config.provider_settings
)
fallback_quoted_image_count = 0
for comp in reply_comps:
has_embedded_image = False
if comp.chain:
for reply_comp in comp.chain:
if isinstance(reply_comp, Image):
has_embedded_image = True
image_path = await reply_comp.convert_to_file_path()
req.image_urls.append(image_path)
_append_quoted_image_attachment(req, image_path)
elif isinstance(reply_comp, File):
file_path = await reply_comp.get_file()
file_name = reply_comp.name or os.path.basename(file_path)
req.extra_user_content_parts.append(
TextPart(
text=(
f"[File Attachment in quoted message: "
f"name {file_name}, path {file_path}]"
)
)
)
# Fallback quoted image extraction for reply-id-only payloads, or when
# embedded reply chain only contains placeholders (e.g. [Forward Message], [Image]).
if not has_embedded_image:
try:
fallback_images = normalize_and_dedupe_strings(
await extract_quoted_message_images(
event,
comp,
settings=quoted_message_settings,
)
)
remaining_limit = max(
config.max_quoted_fallback_images
- fallback_quoted_image_count,
0,
)
if remaining_limit <= 0 and fallback_images:
logger.warning(
"Skip quoted fallback images due to limit=%d for umo=%s",
config.max_quoted_fallback_images,
event.unified_msg_origin,
)
continue
if len(fallback_images) > remaining_limit:
logger.warning(
"Truncate quoted fallback images for umo=%s, reply_id=%s from %d to %d",
event.unified_msg_origin,
getattr(comp, "id", None),
len(fallback_images),
remaining_limit,
)
fallback_images = fallback_images[:remaining_limit]
for image_ref in fallback_images:
if image_ref in req.image_urls:
continue
req.image_urls.append(image_ref)
fallback_quoted_image_count += 1
_append_quoted_image_attachment(req, image_ref)
except Exception as exc: # noqa: BLE001
logger.warning(
"Failed to resolve fallback quoted images for umo=%s, reply_id=%s: %s",
event.unified_msg_origin,
getattr(comp, "id", None),
exc,
exc_info=True,
)
conversation = await _get_session_conv(event, plugin_context) conversation = await _get_session_conv(event, plugin_context)
req.conversation = conversation req.conversation = conversation
@@ -1043,7 +893,6 @@ async def build_main_agent(
if isinstance(req.contexts, str): if isinstance(req.contexts, str):
req.contexts = json.loads(req.contexts) req.contexts = json.loads(req.contexts)
req.image_urls = normalize_and_dedupe_strings(req.image_urls)
if config.file_extract_enabled: if config.file_extract_enabled:
try: try:
@@ -1128,9 +977,6 @@ async def build_main_agent(
truncate_turns=config.dequeue_context_length, truncate_turns=config.dequeue_context_length,
enforce_max_turns=config.max_context_length, enforce_max_turns=config.max_context_length,
tool_schema_mode=config.tool_schema_mode, tool_schema_mode=config.tool_schema_mode,
fallback_providers=_get_fallback_chat_providers(
provider, plugin_context, config.provider_settings
),
) )
if apply_reset: if apply_reset:
+6 -9
View File
@@ -1,7 +1,6 @@
import base64 import base64
import json import json
import os import os
import uuid
from pydantic import Field from pydantic import Field
from pydantic.dataclasses import dataclass from pydantic.dataclasses import dataclass
@@ -241,9 +240,7 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
if "_&exists_" in json.dumps(result): if "_&exists_" in json.dumps(result):
# Download the file from sandbox # Download the file from sandbox
name = os.path.basename(path) name = os.path.basename(path)
local_path = os.path.join( local_path = os.path.join(get_astrbot_temp_path(), name)
get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}"
)
await sb.download_file(path, local_path) await sb.download_file(path, local_path)
logger.info(f"Downloaded file from sandbox: {path} -> {local_path}") logger.info(f"Downloaded file from sandbox: {path} -> {local_path}")
return local_path, True return local_path, True
@@ -355,11 +352,11 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
MessageChain(chain=components), MessageChain(chain=components),
) )
# if file_from_sandbox: if file_from_sandbox:
# try: try:
# os.remove(local_path) os.remove(local_path)
# except Exception as e: except Exception as e:
# logger.error(f"Error removing temp file {local_path}: {e}") logger.error(f"Error removing temp file {local_path}: {e}")
return f"Message sent to session {target_session}" return f"Message sent to session {target_session}"
+2 -2
View File
@@ -36,7 +36,7 @@ class AstrBotConfigManager:
default_config: AstrBotConfig, default_config: AstrBotConfig,
ucr: UmopConfigRouter, ucr: UmopConfigRouter,
sp: SharedPreferences, sp: SharedPreferences,
) -> None: ):
self.sp = sp self.sp = sp
self.ucr = ucr self.ucr = ucr
self.confs: dict[str, AstrBotConfig] = {} self.confs: dict[str, AstrBotConfig] = {}
@@ -56,7 +56,7 @@ class AstrBotConfigManager:
) )
return self.abconf_data return self.abconf_data
def _load_all_configs(self) -> None: def _load_all_configs(self):
"""Load all configurations from the shared preferences.""" """Load all configurations from the shared preferences."""
abconf_data = self._get_abconf_data() abconf_data = self._get_abconf_data()
self.abconf_data = abconf_data self.abconf_data = abconf_data
-2
View File
@@ -11,7 +11,6 @@ from astrbot.core.db.po import (
CommandConflict, CommandConflict,
ConversationV2, ConversationV2,
Persona, Persona,
PersonaFolder,
PlatformMessageHistory, PlatformMessageHistory,
PlatformSession, PlatformSession,
PlatformStat, PlatformStat,
@@ -40,7 +39,6 @@ MAIN_DB_MODELS: dict[str, type[SQLModel]] = {
"platform_stats": PlatformStat, "platform_stats": PlatformStat,
"conversations": ConversationV2, "conversations": ConversationV2,
"personas": Persona, "personas": Persona,
"persona_folders": PersonaFolder,
"preferences": Preference, "preferences": Preference,
"platform_message_history": PlatformMessageHistory, "platform_message_history": PlatformMessageHistory,
"platform_sessions": PlatformSession, "platform_sessions": PlatformSession,
+1 -1
View File
@@ -59,7 +59,7 @@ class AstrBotExporter:
main_db: BaseDatabase, main_db: BaseDatabase,
kb_manager: "KnowledgeBaseManager | None" = None, kb_manager: "KnowledgeBaseManager | None" = None,
config_path: str = CMD_CONFIG_FILE_PATH, config_path: str = CMD_CONFIG_FILE_PATH,
) -> None: ):
self.main_db = main_db self.main_db = main_db
self.kb_manager = kb_manager self.kb_manager = kb_manager
self.config_path = config_path self.config_path = config_path
+2 -2
View File
@@ -110,7 +110,7 @@ class ImportPreCheckResult:
class ImportResult: class ImportResult:
"""导入结果""" """导入结果"""
def __init__(self) -> None: def __init__(self):
self.success = True self.success = True
self.imported_tables: dict[str, int] = {} self.imported_tables: dict[str, int] = {}
self.imported_files: dict[str, int] = {} self.imported_files: dict[str, int] = {}
@@ -161,7 +161,7 @@ class AstrBotImporter:
kb_manager: "KnowledgeBaseManager | None" = None, kb_manager: "KnowledgeBaseManager | None" = None,
config_path: str = CMD_CONFIG_FILE_PATH, config_path: str = CMD_CONFIG_FILE_PATH,
kb_root_dir: str = KB_PATH, kb_root_dir: str = KB_PATH,
) -> None: ):
self.main_db = main_db self.main_db = main_db
self.kb_manager = kb_manager self.kb_manager = kb_manager
self.config_path = config_path self.config_path = config_path
+1 -1
View File
@@ -22,7 +22,7 @@ class ComputerBooter:
""" """
... ...
async def download_file(self, remote_path: str, local_path: str) -> None: async def download_file(self, remote_path: str, local_path: str):
"""Download file from the computer.""" """Download file from the computer."""
... ...
+1 -1
View File
@@ -225,7 +225,7 @@ class LocalBooter(ComputerBooter):
"LocalBooter does not support upload_file operation. Use shell instead." "LocalBooter does not support upload_file operation. Use shell instead."
) )
async def download_file(self, remote_path: str, local_path: str) -> None: async def download_file(self, remote_path: str, local_path: str):
raise NotImplementedError( raise NotImplementedError(
"LocalBooter does not support download_file operation. Use shell instead." "LocalBooter does not support download_file operation. Use shell instead."
) )
+7 -10
View File
@@ -1,5 +1,4 @@
import os import os
import uuid
from dataclasses import dataclass, field from dataclasses import dataclass, field
from astrbot.api import FunctionTool, logger from astrbot.api import FunctionTool, logger
@@ -101,7 +100,7 @@ class FileUploadTool(FunctionTool):
self, self,
context: ContextWrapper[AstrAgentContext], context: ContextWrapper[AstrAgentContext],
local_path: str, local_path: str,
) -> str | None: ):
sb = await get_booter( sb = await get_booter(
context.context.context, context.context.context,
context.context.event.unified_msg_origin, context.context.event.unified_msg_origin,
@@ -168,9 +167,7 @@ class FileDownloadTool(FunctionTool):
try: try:
name = os.path.basename(remote_path) name = os.path.basename(remote_path)
local_path = os.path.join( local_path = os.path.join(get_astrbot_temp_path(), name)
get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}"
)
# Download file from sandbox # Download file from sandbox
await sb.download_file(remote_path, local_path) await sb.download_file(remote_path, local_path)
@@ -186,12 +183,12 @@ class FileDownloadTool(FunctionTool):
logger.error(f"Error sending file message: {e}") logger.error(f"Error sending file message: {e}")
# remove # remove
# try: try:
# os.remove(local_path) os.remove(local_path)
# except Exception as e: except Exception as e:
# logger.error(f"Error removing temp file {local_path}: {e}") logger.error(f"Error removing temp file {local_path}: {e}")
return f"File downloaded successfully to {local_path} and sent to user." return f"File downloaded successfully to {local_path} and sent to user. The file has been removed from local storage."
return f"File downloaded successfully to {local_path}" return f"File downloaded successfully to {local_path}"
except Exception as e: except Exception as e:
+7 -27
View File
@@ -5,9 +5,8 @@ import mcp
from astrbot.api import FunctionTool from astrbot.api import FunctionTool
from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import ToolExecResult from astrbot.core.agent.tool import ToolExecResult
from astrbot.core.astr_agent_context import AstrAgentContext, AstrMessageEvent from astrbot.core.astr_agent_context import AstrAgentContext
from astrbot.core.computer.computer_client import get_booter, get_local_booter from astrbot.core.computer.computer_client import get_booter, get_local_booter
from astrbot.core.message.message_event_result import MessageChain
param_schema = { param_schema = {
"type": "object", "type": "object",
@@ -26,22 +25,7 @@ param_schema = {
} }
def _check_admin_permission(context: ContextWrapper[AstrAgentContext]) -> str | None: def handle_result(result: dict) -> ToolExecResult:
cfg = context.context.context.get_config(
umo=context.context.event.unified_msg_origin
)
provider_settings = cfg.get("provider_settings", {})
require_admin = provider_settings.get("computer_use_require_admin", True)
if require_admin and context.context.event.role != "admin":
return (
"error: Permission denied. Python execution is only allowed for admin users. "
"Tell user to set admins in `AstrBot WebUI -> Config -> General Config` by adding their user ID to the admins list if they need this feature."
f"User's ID is: {context.context.event.get_sender_id()}. User's ID can be found by using /sid command."
)
return None
async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult:
data = result.get("data", {}) data = result.get("data", {})
output = data.get("output", {}) output = data.get("output", {})
error = data.get("error", "") error = data.get("error", "")
@@ -60,9 +44,6 @@ async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult
type="image", data=img["image/png"], mimeType="image/png" type="image", data=img["image/png"], mimeType="image/png"
) )
) )
if event.get_platform_name() == "webchat":
await event.send(message=MessageChain().base64_image(img["image/png"]))
if text: if text:
resp.content.append(mcp.types.TextContent(type="text", text=text)) resp.content.append(mcp.types.TextContent(type="text", text=text))
@@ -81,15 +62,13 @@ class PythonTool(FunctionTool):
async def call( async def call(
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
) -> ToolExecResult: ) -> ToolExecResult:
if permission_error := _check_admin_permission(context):
return permission_error
sb = await get_booter( sb = await get_booter(
context.context.context, context.context.context,
context.context.event.unified_msg_origin, context.context.event.unified_msg_origin,
) )
try: try:
result = await sb.python.exec(code, silent=silent) result = await sb.python.exec(code, silent=silent)
return await handle_result(result, context.context.event) return handle_result(result)
except Exception as e: except Exception as e:
return f"Error executing code: {str(e)}" return f"Error executing code: {str(e)}"
@@ -104,11 +83,12 @@ class LocalPythonTool(FunctionTool):
async def call( async def call(
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
) -> ToolExecResult: ) -> ToolExecResult:
if permission_error := _check_admin_permission(context): if context.context.event.role != "admin":
return permission_error return "error: Permission denied. Local Python execution is only allowed for admin users. Tell user to set admins in AstrBot WebUI."
sb = get_local_booter() sb = get_local_booter()
try: try:
result = await sb.python.exec(code, silent=silent) result = await sb.python.exec(code, silent=silent)
return await handle_result(result, context.context.event) return handle_result(result)
except Exception as e: except Exception as e:
return f"Error executing code: {str(e)}" return f"Error executing code: {str(e)}"
+2 -17
View File
@@ -9,21 +9,6 @@ from astrbot.core.astr_agent_context import AstrAgentContext
from ..computer_client import get_booter, get_local_booter from ..computer_client import get_booter, get_local_booter
def _check_admin_permission(context: ContextWrapper[AstrAgentContext]) -> str | None:
cfg = context.context.context.get_config(
umo=context.context.event.unified_msg_origin
)
provider_settings = cfg.get("provider_settings", {})
require_admin = provider_settings.get("computer_use_require_admin", True)
if require_admin and context.context.event.role != "admin":
return (
"error: Permission denied. Shell execution is only allowed for admin users. "
"Tell user to set admins in `AstrBot WebUI -> Config -> General Config` by adding their user ID to the admins list if they need this feature."
f"User's ID is: {context.context.event.get_sender_id()}. User's ID can be found by using /sid command."
)
return None
@dataclass @dataclass
class ExecuteShellTool(FunctionTool): class ExecuteShellTool(FunctionTool):
name: str = "astrbot_execute_shell" name: str = "astrbot_execute_shell"
@@ -61,8 +46,8 @@ class ExecuteShellTool(FunctionTool):
background: bool = False, background: bool = False,
env: dict = {}, env: dict = {},
) -> ToolExecResult: ) -> ToolExecResult:
if permission_error := _check_admin_permission(context): if context.context.event.role != "admin":
return permission_error return "error: Permission denied. Shell execution is only allowed for admin users. Tell user to Set admins in AstrBot WebUI."
if self.is_local: if self.is_local:
sb = get_local_booter() sb = get_local_booter()
+5 -5
View File
@@ -33,7 +33,7 @@ class AstrBotConfig(dict):
config_path: str = ASTRBOT_CONFIG_PATH, config_path: str = ASTRBOT_CONFIG_PATH,
default_config: dict = DEFAULT_CONFIG, default_config: dict = DEFAULT_CONFIG,
schema: dict | None = None, schema: dict | None = None,
) -> None: ):
super().__init__() super().__init__()
# 调用父类的 __setattr__ 方法,防止保存配置时将此属性写入配置文件 # 调用父类的 __setattr__ 方法,防止保存配置时将此属性写入配置文件
@@ -66,7 +66,7 @@ class AstrBotConfig(dict):
"""将 Schema 转换成 Config""" """将 Schema 转换成 Config"""
conf = {} conf = {}
def _parse_schema(schema: dict, conf: dict) -> None: def _parse_schema(schema: dict, conf: dict):
for k, v in schema.items(): for k, v in schema.items():
if v["type"] not in DEFAULT_VALUE_MAP: if v["type"] not in DEFAULT_VALUE_MAP:
raise TypeError( raise TypeError(
@@ -148,7 +148,7 @@ class AstrBotConfig(dict):
return has_new return has_new
def save_config(self, replace_config: dict | None = None) -> None: def save_config(self, replace_config: dict | None = None):
"""将配置写入文件 """将配置写入文件
如果传入 replace_config则将配置替换为 replace_config 如果传入 replace_config则将配置替换为 replace_config
@@ -164,14 +164,14 @@ class AstrBotConfig(dict):
except KeyError: except KeyError:
return None return None
def __delattr__(self, key) -> None: def __delattr__(self, key):
try: try:
del self[key] del self[key]
self.save_config() self.save_config()
except KeyError: except KeyError:
raise AttributeError(f"没有找到 Key: '{key}'") raise AttributeError(f"没有找到 Key: '{key}'")
def __setattr__(self, key, value) -> None: def __setattr__(self, key, value):
self[key] = value self[key] = value
def check_exist(self) -> bool: def check_exist(self) -> bool:
+13 -237
View File
@@ -5,7 +5,7 @@ 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.18.0" VERSION = "4.14.4"
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 = [
@@ -15,7 +15,6 @@ WEBHOOK_SUPPORTED_PLATFORMS = [
"wecom_ai_bot", "wecom_ai_bot",
"slack", "slack",
"lark", "lark",
"line",
] ]
# 默认配置 # 默认配置
@@ -68,7 +67,6 @@ DEFAULT_CONFIG = {
"provider_settings": { "provider_settings": {
"enable": True, "enable": True,
"default_provider_id": "", "default_provider_id": "",
"fallback_chat_models": [],
"default_image_caption_provider_id": "", "default_image_caption_provider_id": "",
"image_caption_prompt": "Please describe the image using Chinese.", "image_caption_prompt": "Please describe the image using Chinese.",
"provider_pool": ["*"], # "*" 表示使用所有可用的提供者 "provider_pool": ["*"], # "*" 表示使用所有可用的提供者
@@ -76,7 +74,6 @@ DEFAULT_CONFIG = {
"web_search": False, "web_search": False,
"websearch_provider": "default", "websearch_provider": "default",
"websearch_tavily_key": [], "websearch_tavily_key": [],
"websearch_bocha_key": [],
"websearch_baidu_app_builder_key": "", "websearch_baidu_app_builder_key": "",
"web_search_link": False, "web_search_link": False,
"display_reasoning_text": False, "display_reasoning_text": False,
@@ -101,13 +98,6 @@ DEFAULT_CONFIG = {
"streaming_response": False, "streaming_response": False,
"show_tool_use_status": False, "show_tool_use_status": False,
"sanitize_context_by_modalities": False, "sanitize_context_by_modalities": False,
"max_quoted_fallback_images": 20,
"quoted_message_parser": {
"max_component_chain_depth": 4,
"max_forward_node_depth": 6,
"max_forward_fetch": 32,
"warn_on_action_failure": False,
},
"agent_runner_type": "local", "agent_runner_type": "local",
"dify_agent_runner_provider_id": "", "dify_agent_runner_provider_id": "",
"coze_agent_runner_provider_id": "", "coze_agent_runner_provider_id": "",
@@ -128,7 +118,6 @@ DEFAULT_CONFIG = {
"add_cron_tools": True, "add_cron_tools": True,
}, },
"computer_use_runtime": "local", "computer_use_runtime": "local",
"computer_use_require_admin": True,
"sandbox": { "sandbox": {
"booter": "shipyard", "booter": "shipyard",
"shipyard_endpoint": "", "shipyard_endpoint": "",
@@ -139,9 +128,8 @@ DEFAULT_CONFIG = {
}, },
# SubAgent orchestrator mode: # SubAgent orchestrator mode:
# - main_enable = False: disabled; main LLM mounts tools normally (persona selection). # - main_enable = False: disabled; main LLM mounts tools normally (persona selection).
# - main_enable = True: enabled; main LLM keeps its own tools and includes handoff # - main_enable = True: enabled; main LLM will include handoff tools and can optionally
# tools (transfer_to_*). remove_main_duplicate_tools can remove tools that are # remove tools that are duplicated on subagents via remove_main_duplicate_tools.
# duplicated on subagents from the main LLM toolset.
"subagent_orchestrator": { "subagent_orchestrator": {
"main_enable": False, "main_enable": False,
"remove_main_duplicate_tools": False, "remove_main_duplicate_tools": False,
@@ -188,7 +176,7 @@ DEFAULT_CONFIG = {
"t2i_use_file_service": False, "t2i_use_file_service": False,
"t2i_active_template": "base", "t2i_active_template": "base",
"http_proxy": "", "http_proxy": "",
"no_proxy": ["localhost", "127.0.0.1", "::1", "10.*", "192.168.*"], "no_proxy": ["localhost", "127.0.0.1", "::1"],
"dashboard": { "dashboard": {
"enable": True, "enable": True,
"username": "astrbot", "username": "astrbot",
@@ -197,12 +185,6 @@ DEFAULT_CONFIG = {
"host": "0.0.0.0", "host": "0.0.0.0",
"port": 6185, "port": 6185,
"disable_access_log": True, "disable_access_log": True,
"ssl": {
"enable": False,
"cert_file": "",
"key_file": "",
"ca_certs": "",
},
}, },
"platform": [], "platform": [],
"platform_specific": { "platform_specific": {
@@ -219,7 +201,6 @@ DEFAULT_CONFIG = {
"log_file_enable": False, "log_file_enable": False,
"log_file_path": "logs/astrbot.log", "log_file_path": "logs/astrbot.log",
"log_file_max_mb": 20, "log_file_max_mb": 20,
"temp_dir_max_size": 1024,
"trace_enable": False, "trace_enable": False,
"trace_log_enable": False, "trace_log_enable": False,
"trace_log_path": "logs/astrbot.trace.log", "trace_log_path": "logs/astrbot.trace.log",
@@ -337,11 +318,9 @@ CONFIG_METADATA_2 = {
"id": "wecom_ai_bot", "id": "wecom_ai_bot",
"type": "wecom_ai_bot", "type": "wecom_ai_bot",
"enable": True, "enable": True,
"wecomaibot_init_respond_text": "", "wecomaibot_init_respond_text": "💭 思考中...",
"wecomaibot_friend_message_welcome_text": "", "wecomaibot_friend_message_welcome_text": "",
"wecom_ai_bot_name": "", "wecom_ai_bot_name": "",
"msg_push_webhook_url": "",
"only_use_webhook_url_to_send": False,
"token": "", "token": "",
"encoding_aes_key": "", "encoding_aes_key": "",
"unified_webhook_mode": True, "unified_webhook_mode": True,
@@ -424,7 +403,6 @@ CONFIG_METADATA_2 = {
"slack_webhook_port": 6197, "slack_webhook_port": 6197,
"slack_webhook_path": "/astrbot-slack-webhook/callback", "slack_webhook_path": "/astrbot-slack-webhook/callback",
}, },
# LINE's config is located in line_adapter.py
"Satori": { "Satori": {
"id": "satori", "id": "satori",
"type": "satori", "type": "satori",
@@ -708,23 +686,13 @@ CONFIG_METADATA_2 = {
"wecomaibot_init_respond_text": { "wecomaibot_init_respond_text": {
"description": "企业微信智能机器人初始响应文本", "description": "企业微信智能机器人初始响应文本",
"type": "string", "type": "string",
"hint": "当机器人收到消息时,首先回复的文本内容。留空则不设置", "hint": "当机器人收到消息时,首先回复的文本内容。留空则使用默认值",
}, },
"wecomaibot_friend_message_welcome_text": { "wecomaibot_friend_message_welcome_text": {
"description": "企业微信智能机器人私聊欢迎语", "description": "企业微信智能机器人私聊欢迎语",
"type": "string", "type": "string",
"hint": "当用户当天进入智能机器人单聊会话,回复欢迎语,留空则不回复。", "hint": "当用户当天进入智能机器人单聊会话,回复欢迎语,留空则不回复。",
}, },
"msg_push_webhook_url": {
"description": "企业微信消息推送 Webhook URL",
"type": "string",
"hint": "用于 send_by_session 主动消息推送。格式示例: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx",
},
"only_use_webhook_url_to_send": {
"description": "仅使用 Webhook 发送消息",
"type": "bool",
"hint": "启用后,企业微信智能机器人的所有回复都改为通过消息推送 Webhook 发送。消息推送 Webhook 支持更多的消息类型(如图片、文件等)。",
},
"lark_bot_name": { "lark_bot_name": {
"description": "飞书机器人的名字", "description": "飞书机器人的名字",
"type": "string", "type": "string",
@@ -944,7 +912,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.openai.com/v1", "api_base": "https://api.openai.com/v1",
"timeout": 120, "timeout": 120,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"Google Gemini": { "Google Gemini": {
@@ -967,7 +934,6 @@ 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"},
"proxy": "",
}, },
"Anthropic": { "Anthropic": {
"id": "anthropic", "id": "anthropic",
@@ -978,8 +944,7 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.anthropic.com/v1", "api_base": "https://api.anthropic.com/v1",
"timeout": 120, "timeout": 120,
"proxy": "", "anth_thinking_config": {"budget": 0},
"anth_thinking_config": {"type": "", "budget": 0, "effort": ""},
}, },
"Moonshot": { "Moonshot": {
"id": "moonshot", "id": "moonshot",
@@ -990,7 +955,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"timeout": 120, "timeout": 120,
"api_base": "https://api.moonshot.cn/v1", "api_base": "https://api.moonshot.cn/v1",
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"xAI": { "xAI": {
@@ -1002,7 +966,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.x.ai/v1", "api_base": "https://api.x.ai/v1",
"timeout": 120, "timeout": 120,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
"xai_native_search": False, "xai_native_search": False,
}, },
@@ -1015,7 +978,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.deepseek.com/v1", "api_base": "https://api.deepseek.com/v1",
"timeout": 120, "timeout": 120,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"Zhipu": { "Zhipu": {
@@ -1027,43 +989,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"timeout": 120, "timeout": 120,
"api_base": "https://open.bigmodel.cn/api/paas/v4/", "api_base": "https://open.bigmodel.cn/api/paas/v4/",
"proxy": "",
"custom_headers": {},
},
"AIHubMix": {
"id": "aihubmix",
"provider": "aihubmix",
"type": "aihubmix_chat_completion",
"provider_type": "chat_completion",
"enable": True,
"key": [],
"timeout": 120,
"api_base": "https://aihubmix.com/v1",
"proxy": "",
"custom_headers": {},
},
"OpenRouter": {
"id": "openrouter",
"provider": "openrouter",
"type": "openrouter_chat_completion",
"provider_type": "chat_completion",
"enable": True,
"key": [],
"timeout": 120,
"api_base": "https://openrouter.ai/v1",
"proxy": "",
"custom_headers": {},
},
"NVIDIA": {
"id": "nvidia",
"provider": "nvidia",
"type": "openai_chat_completion",
"provider_type": "chat_completion",
"enable": True,
"key": [],
"api_base": "https://integrate.api.nvidia.com/v1",
"timeout": 120,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"Azure OpenAI": { "Azure OpenAI": {
@@ -1076,7 +1001,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "", "api_base": "",
"timeout": 120, "timeout": 120,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"Ollama": { "Ollama": {
@@ -1087,7 +1011,6 @@ CONFIG_METADATA_2 = {
"enable": True, "enable": True,
"key": ["ollama"], # ollama 的 key 默认是 ollama "key": ["ollama"], # ollama 的 key 默认是 ollama
"api_base": "http://127.0.0.1:11434/v1", "api_base": "http://127.0.0.1:11434/v1",
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"LM Studio": { "LM Studio": {
@@ -1098,7 +1021,6 @@ CONFIG_METADATA_2 = {
"enable": True, "enable": True,
"key": ["lmstudio"], "key": ["lmstudio"],
"api_base": "http://127.0.0.1:1234/v1", "api_base": "http://127.0.0.1:1234/v1",
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"Gemini_OpenAI_API": { "Gemini_OpenAI_API": {
@@ -1110,7 +1032,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://generativelanguage.googleapis.com/v1beta/openai/", "api_base": "https://generativelanguage.googleapis.com/v1beta/openai/",
"timeout": 120, "timeout": 120,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"Groq": { "Groq": {
@@ -1122,7 +1043,6 @@ 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,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"302.AI": { "302.AI": {
@@ -1134,7 +1054,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.302.ai/v1", "api_base": "https://api.302.ai/v1",
"timeout": 120, "timeout": 120,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"SiliconFlow": { "SiliconFlow": {
@@ -1146,7 +1065,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"timeout": 120, "timeout": 120,
"api_base": "https://api.siliconflow.cn/v1", "api_base": "https://api.siliconflow.cn/v1",
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"PPIO": { "PPIO": {
@@ -1158,7 +1076,6 @@ 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,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"TokenPony": { "TokenPony": {
@@ -1170,7 +1087,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.tokenpony.cn/v1", "api_base": "https://api.tokenpony.cn/v1",
"timeout": 120, "timeout": 120,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"Compshare": { "Compshare": {
@@ -1182,7 +1098,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.modelverse.cn/v1", "api_base": "https://api.modelverse.cn/v1",
"timeout": 120, "timeout": 120,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"ModelScope": { "ModelScope": {
@@ -1194,7 +1109,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"timeout": 120, "timeout": 120,
"api_base": "https://api-inference.modelscope.cn/v1", "api_base": "https://api-inference.modelscope.cn/v1",
"proxy": "",
"custom_headers": {}, "custom_headers": {},
}, },
"Dify": { "Dify": {
@@ -1210,7 +1124,6 @@ CONFIG_METADATA_2 = {
"dify_query_input_key": "astrbot_text_query", "dify_query_input_key": "astrbot_text_query",
"variables": {}, "variables": {},
"timeout": 60, "timeout": 60,
"proxy": "",
}, },
"Coze": { "Coze": {
"id": "coze", "id": "coze",
@@ -1222,7 +1135,6 @@ CONFIG_METADATA_2 = {
"bot_id": "", "bot_id": "",
"coze_api_base": "https://api.coze.cn", "coze_api_base": "https://api.coze.cn",
"timeout": 60, "timeout": 60,
"proxy": "",
# "auto_save_history": True, # "auto_save_history": True,
}, },
"阿里云百炼应用": { "阿里云百炼应用": {
@@ -1241,7 +1153,6 @@ CONFIG_METADATA_2 = {
}, },
"variables": {}, "variables": {},
"timeout": 60, "timeout": 60,
"proxy": "",
}, },
"FastGPT": { "FastGPT": {
"id": "fastgpt", "id": "fastgpt",
@@ -1252,7 +1163,6 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.fastgpt.in/api/v1", "api_base": "https://api.fastgpt.in/api/v1",
"timeout": 60, "timeout": 60,
"proxy": "",
"custom_headers": {}, "custom_headers": {},
"custom_extra_body": {}, "custom_extra_body": {},
}, },
@@ -1265,7 +1175,6 @@ CONFIG_METADATA_2 = {
"api_key": "", "api_key": "",
"api_base": "", "api_base": "",
"model": "whisper-1", "model": "whisper-1",
"proxy": "",
}, },
"Whisper(Local)": { "Whisper(Local)": {
"provider": "openai", "provider": "openai",
@@ -1295,7 +1204,6 @@ CONFIG_METADATA_2 = {
"model": "tts-1", "model": "tts-1",
"openai-tts-voice": "alloy", "openai-tts-voice": "alloy",
"timeout": "20", "timeout": "20",
"proxy": "",
}, },
"Genie TTS": { "Genie TTS": {
"id": "genie_tts", "id": "genie_tts",
@@ -1376,7 +1284,6 @@ CONFIG_METADATA_2 = {
"fishaudio-tts-character": "可莉", "fishaudio-tts-character": "可莉",
"fishaudio-tts-reference-id": "", "fishaudio-tts-reference-id": "",
"timeout": "20", "timeout": "20",
"proxy": "",
}, },
"阿里云百炼 TTS(API)": { "阿里云百炼 TTS(API)": {
"hint": "API Key 从 https://bailian.console.aliyun.com/?tab=model#/api-key 获取。模型和音色的选择文档请参考: 阿里云百炼语音合成音色名称。具体可参考 https://help.aliyun.com/zh/model-studio/speech-synthesis-and-speech-recognition", "hint": "API Key 从 https://bailian.console.aliyun.com/?tab=model#/api-key 获取。模型和音色的选择文档请参考: 阿里云百炼语音合成音色名称。具体可参考 https://help.aliyun.com/zh/model-studio/speech-synthesis-and-speech-recognition",
@@ -1403,7 +1310,6 @@ CONFIG_METADATA_2 = {
"azure_tts_volume": "100", "azure_tts_volume": "100",
"azure_tts_subscription_key": "", "azure_tts_subscription_key": "",
"azure_tts_region": "eastus", "azure_tts_region": "eastus",
"proxy": "",
}, },
"MiniMax TTS(API)": { "MiniMax TTS(API)": {
"id": "minimax_tts", "id": "minimax_tts",
@@ -1426,7 +1332,6 @@ CONFIG_METADATA_2 = {
"minimax-voice-latex": False, "minimax-voice-latex": False,
"minimax-voice-english-normalization": False, "minimax-voice-english-normalization": False,
"timeout": 20, "timeout": 20,
"proxy": "",
}, },
"火山引擎_TTS(API)": { "火山引擎_TTS(API)": {
"id": "volcengine_tts", "id": "volcengine_tts",
@@ -1441,7 +1346,6 @@ CONFIG_METADATA_2 = {
"volcengine_speed_ratio": 1.0, "volcengine_speed_ratio": 1.0,
"api_base": "https://openspeech.bytedance.com/api/v1/tts", "api_base": "https://openspeech.bytedance.com/api/v1/tts",
"timeout": 20, "timeout": 20,
"proxy": "",
}, },
"Gemini TTS": { "Gemini TTS": {
"id": "gemini_tts", "id": "gemini_tts",
@@ -1455,7 +1359,6 @@ CONFIG_METADATA_2 = {
"gemini_tts_model": "gemini-2.5-flash-preview-tts", "gemini_tts_model": "gemini-2.5-flash-preview-tts",
"gemini_tts_prefix": "", "gemini_tts_prefix": "",
"gemini_tts_voice_name": "Leda", "gemini_tts_voice_name": "Leda",
"proxy": "",
}, },
"OpenAI Embedding": { "OpenAI Embedding": {
"id": "openai_embedding", "id": "openai_embedding",
@@ -1468,7 +1371,6 @@ CONFIG_METADATA_2 = {
"embedding_model": "", "embedding_model": "",
"embedding_dimensions": 1024, "embedding_dimensions": 1024,
"timeout": 20, "timeout": 20,
"proxy": "",
}, },
"Gemini Embedding": { "Gemini Embedding": {
"id": "gemini_embedding", "id": "gemini_embedding",
@@ -1481,7 +1383,6 @@ CONFIG_METADATA_2 = {
"embedding_model": "gemini-embedding-exp-03-07", "embedding_model": "gemini-embedding-exp-03-07",
"embedding_dimensions": 768, "embedding_dimensions": 768,
"timeout": 20, "timeout": 20,
"proxy": "",
}, },
"vLLM Rerank": { "vLLM Rerank": {
"id": "vllm_rerank", "id": "vllm_rerank",
@@ -1964,25 +1865,13 @@ CONFIG_METADATA_2 = {
}, },
}, },
"anth_thinking_config": { "anth_thinking_config": {
"description": "思考配置", "description": "Thinking Config",
"type": "object", "type": "object",
"items": { "items": {
"type": {
"description": "思考类型",
"type": "string",
"options": ["", "adaptive"],
"hint": "Opus 4.6+ / Sonnet 4.6+ 推荐设为 'adaptive'。留空则使用手动 budget 模式。参见: https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking",
},
"budget": { "budget": {
"description": "思考预算", "description": "Thinking Budget",
"type": "int", "type": "int",
"hint": "手动 budget_tokens,需 >= 1024。仅在 type 为空时生效。Opus 4.6 / Sonnet 4.6 上已弃用。参见: https://platform.claude.com/docs/en/build-with-claude/extended-thinking", "hint": "Anthropic thinking.budget_tokens param. Must >= 1024. See: https://platform.claude.com/docs/en/build-with-claude/extended-thinking",
},
"effort": {
"description": "思考深度",
"type": "string",
"options": ["", "low", "medium", "high", "max"],
"hint": "type 为 'adaptive' 时控制思考深度。默认 'high''max' 仅限 Opus 4.6。参见: https://platform.claude.com/docs/en/build-with-claude/effort",
}, },
}, },
}, },
@@ -2190,11 +2079,6 @@ CONFIG_METADATA_2 = {
"description": "API Base URL", "description": "API Base URL",
"type": "string", "type": "string",
}, },
"proxy": {
"description": "代理地址",
"type": "string",
"hint": "HTTP/HTTPS 代理地址,格式如 http://127.0.0.1:7890。仅对该提供商的 API 请求生效,不影响 Docker 内网通信。",
},
"model": { "model": {
"description": "模型 ID", "description": "模型 ID",
"type": "string", "type": "string",
@@ -2263,10 +2147,6 @@ CONFIG_METADATA_2 = {
"default_provider_id": { "default_provider_id": {
"type": "string", "type": "string",
}, },
"fallback_chat_models": {
"type": "list",
"items": {"type": "string"},
},
"wake_prefix": { "wake_prefix": {
"type": "string", "type": "string",
}, },
@@ -2461,23 +2341,9 @@ CONFIG_METADATA_2 = {
"type": "string", "type": "string",
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], "options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
}, },
"dashboard.ssl.enable": {"type": "bool"},
"dashboard.ssl.cert_file": {
"type": "string",
"condition": {"dashboard.ssl.enable": True},
},
"dashboard.ssl.key_file": {
"type": "string",
"condition": {"dashboard.ssl.enable": True},
},
"dashboard.ssl.ca_certs": {
"type": "string",
"condition": {"dashboard.ssl.enable": True},
},
"log_file_enable": {"type": "bool"}, "log_file_enable": {"type": "bool"},
"log_file_path": {"type": "string", "condition": {"log_file_enable": True}}, "log_file_path": {"type": "string", "condition": {"log_file_enable": True}},
"log_file_max_mb": {"type": "int", "condition": {"log_file_enable": True}}, "log_file_max_mb": {"type": "int", "condition": {"log_file_enable": True}},
"temp_dir_max_size": {"type": "int"},
"trace_log_enable": {"type": "bool"}, "trace_log_enable": {"type": "bool"},
"trace_log_path": { "trace_log_path": {
"type": "string", "type": "string",
@@ -2577,22 +2443,15 @@ CONFIG_METADATA_3 = {
}, },
"ai": { "ai": {
"description": "模型", "description": "模型",
"hint": "当使用非内置 Agent 执行器时,默认对话模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。", "hint": "当使用非内置 Agent 执行器时,默认聊天模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。",
"type": "object", "type": "object",
"items": { "items": {
"provider_settings.default_provider_id": { "provider_settings.default_provider_id": {
"description": "默认对话模型", "description": "默认聊天模型",
"type": "string", "type": "string",
"_special": "select_provider", "_special": "select_provider",
"hint": "留空时使用第一个模型", "hint": "留空时使用第一个模型",
}, },
"provider_settings.fallback_chat_models": {
"description": "回退对话模型列表",
"type": "list",
"items": {"type": "string"},
"_special": "select_providers",
"hint": "主聊天模型请求失败时,按顺序切换到这些模型。",
},
"provider_settings.default_image_caption_provider_id": { "provider_settings.default_image_caption_provider_id": {
"description": "默认图片转述模型", "description": "默认图片转述模型",
"type": "string", "type": "string",
@@ -2704,7 +2563,7 @@ CONFIG_METADATA_3 = {
"provider_settings.websearch_provider": { "provider_settings.websearch_provider": {
"description": "网页搜索提供商", "description": "网页搜索提供商",
"type": "string", "type": "string",
"options": ["default", "tavily", "baidu_ai_search", "bocha"], "options": ["default", "tavily", "baidu_ai_search"],
"condition": { "condition": {
"provider_settings.web_search": True, "provider_settings.web_search": True,
}, },
@@ -2719,16 +2578,6 @@ CONFIG_METADATA_3 = {
"provider_settings.web_search": True, "provider_settings.web_search": True,
}, },
}, },
"provider_settings.websearch_bocha_key": {
"description": "BoCha API Key",
"type": "list",
"items": {"type": "string"},
"hint": "可添加多个 Key 进行轮询。",
"condition": {
"provider_settings.websearch_provider": "bocha",
"provider_settings.web_search": True,
},
},
"provider_settings.websearch_baidu_app_builder_key": { "provider_settings.websearch_baidu_app_builder_key": {
"description": "百度千帆智能云 APP Builder API Key", "description": "百度千帆智能云 APP Builder API Key",
"type": "string", "type": "string",
@@ -2762,11 +2611,6 @@ CONFIG_METADATA_3 = {
"labels": ["", "本地", "沙箱"], "labels": ["", "本地", "沙箱"],
"hint": "选择 Computer Use 运行环境。", "hint": "选择 Computer Use 运行环境。",
}, },
"provider_settings.computer_use_require_admin": {
"description": "需要 AstrBot 管理员权限",
"type": "bool",
"hint": "开启后,需要 AstrBot 管理员权限才能调用使用电脑能力。在平台配置->管理员中可添加管理员。使用 /sid 指令查看管理员 ID。",
},
"provider_settings.sandbox.booter": { "provider_settings.sandbox.booter": {
"description": "沙箱环境驱动器", "description": "沙箱环境驱动器",
"type": "string", "type": "string",
@@ -3002,46 +2846,6 @@ CONFIG_METADATA_3 = {
"provider_settings.agent_runner_type": "local", "provider_settings.agent_runner_type": "local",
}, },
}, },
"provider_settings.max_quoted_fallback_images": {
"description": "引用图片回退解析上限",
"type": "int",
"hint": "引用/转发消息回退解析图片时的最大注入数量,超出会截断。",
"condition": {
"provider_settings.agent_runner_type": "local",
},
},
"provider_settings.quoted_message_parser.max_component_chain_depth": {
"description": "引用解析组件链深度",
"type": "int",
"hint": "解析 Reply 组件链时允许的最大递归深度。",
"condition": {
"provider_settings.agent_runner_type": "local",
},
},
"provider_settings.quoted_message_parser.max_forward_node_depth": {
"description": "引用解析转发节点深度",
"type": "int",
"hint": "解析合并转发节点时允许的最大递归深度。",
"condition": {
"provider_settings.agent_runner_type": "local",
},
},
"provider_settings.quoted_message_parser.max_forward_fetch": {
"description": "引用解析转发拉取上限",
"type": "int",
"hint": "递归拉取 get_forward_msg 的最大次数。",
"condition": {
"provider_settings.agent_runner_type": "local",
},
},
"provider_settings.quoted_message_parser.warn_on_action_failure": {
"description": "引用解析 action 失败告警",
"type": "bool",
"hint": "开启后,get_msg/get_forward_msg 全部尝试失败时输出 warning 日志。",
"condition": {
"provider_settings.agent_runner_type": "local",
},
},
"provider_settings.max_agent_step": { "provider_settings.max_agent_step": {
"description": "工具调用轮数上限", "description": "工具调用轮数上限",
"type": "int", "type": "int",
@@ -3493,29 +3297,6 @@ CONFIG_METADATA_3_SYSTEM = {
"hint": "控制台输出日志的级别。", "hint": "控制台输出日志的级别。",
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], "options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
}, },
"dashboard.ssl.enable": {
"description": "启用 WebUI HTTPS",
"type": "bool",
"hint": "启用后,WebUI 将直接使用 HTTPS 提供服务。",
},
"dashboard.ssl.cert_file": {
"description": "SSL 证书文件路径",
"type": "string",
"hint": "证书文件路径(PEM)。支持绝对路径和相对路径(相对于当前工作目录)。",
"condition": {"dashboard.ssl.enable": True},
},
"dashboard.ssl.key_file": {
"description": "SSL 私钥文件路径",
"type": "string",
"hint": "私钥文件路径(PEM)。支持绝对路径和相对路径(相对于当前工作目录)。",
"condition": {"dashboard.ssl.enable": True},
},
"dashboard.ssl.ca_certs": {
"description": "SSL CA 证书文件路径",
"type": "string",
"hint": "可选。用于指定 CA 证书文件路径。",
"condition": {"dashboard.ssl.enable": True},
},
"log_file_enable": { "log_file_enable": {
"description": "启用文件日志", "description": "启用文件日志",
"type": "bool", "type": "bool",
@@ -3531,11 +3312,6 @@ CONFIG_METADATA_3_SYSTEM = {
"type": "int", "type": "int",
"hint": "超过大小后自动轮转,默认 20MB。", "hint": "超过大小后自动轮转,默认 20MB。",
}, },
"temp_dir_max_size": {
"description": "临时目录大小上限 (MB)",
"type": "int",
"hint": "用于限制 data/temp 目录总大小,单位为 MB。系统每 10 分钟检查一次,超限时按文件修改时间从旧到新删除,释放约 30% 当前体积。",
},
"trace_log_enable": { "trace_log_enable": {
"description": "启用 Trace 文件日志", "description": "启用 Trace 文件日志",
"type": "bool", "type": "bool",
+47 -56
View File
@@ -42,55 +42,6 @@ class ConfigMetadataI18n:
""" """
result = {} result = {}
def convert_items(
group: str, section: str, items: dict[str, Any], prefix: str = ""
) -> dict[str, Any]:
items_result: dict[str, Any] = {}
for field_key, field_data in items.items():
if not isinstance(field_data, dict):
items_result[field_key] = field_data
continue
field_name = field_key
field_path = f"{prefix}.{field_name}" if prefix else field_name
field_result = {
key: value
for key, value in field_data.items()
if key not in {"description", "hint", "labels", "name"}
}
if "description" in field_data:
field_result["description"] = (
f"{group}.{section}.{field_path}.description"
)
if "hint" in field_data:
field_result["hint"] = f"{group}.{section}.{field_path}.hint"
if "labels" in field_data:
field_result["labels"] = f"{group}.{section}.{field_path}.labels"
if "name" in field_data:
field_result["name"] = f"{group}.{section}.{field_path}.name"
if "items" in field_data and isinstance(field_data["items"], dict):
field_result["items"] = convert_items(
group, section, field_data["items"], field_path
)
if "template_schema" in field_data and isinstance(
field_data["template_schema"], dict
):
field_result["template_schema"] = convert_items(
group,
section,
field_data["template_schema"],
f"{field_path}.template_schema",
)
items_result[field_key] = field_result
return items_result
for group_key, group_data in metadata.items(): for group_key, group_data in metadata.items():
group_result = { group_result = {
"name": f"{group_key}.name", "name": f"{group_key}.name",
@@ -99,19 +50,59 @@ class ConfigMetadataI18n:
for section_key, section_data in group_data.get("metadata", {}).items(): for section_key, section_data in group_data.get("metadata", {}).items():
section_result = { section_result = {
key: value "description": f"{group_key}.{section_key}.description",
for key, value in section_data.items() "type": section_data.get("type"),
if key not in {"description", "hint", "labels", "name"}
} }
section_result["description"] = f"{group_key}.{section_key}.description"
# 复制其他属性
for key in ["items", "condition", "_special", "invisible"]:
if key in section_data:
section_result[key] = section_data[key]
# 处理 hint
if "hint" in section_data: if "hint" in section_data:
section_result["hint"] = f"{group_key}.{section_key}.hint" section_result["hint"] = f"{group_key}.{section_key}.hint"
# 处理 items 中的字段
if "items" in section_data and isinstance(section_data["items"], dict): if "items" in section_data and isinstance(section_data["items"], dict):
section_result["items"] = convert_items( items_result = {}
group_key, section_key, section_data["items"] for field_key, field_data in section_data["items"].items():
) # 处理嵌套的点号字段名(如 provider_settings.enable
field_name = field_key
field_result = {}
# 复制基本属性
for attr in [
"type",
"condition",
"_special",
"invisible",
"options",
"slider",
]:
if attr in field_data:
field_result[attr] = field_data[attr]
# 转换文本属性为国际化键
if "description" in field_data:
field_result["description"] = (
f"{group_key}.{section_key}.{field_name}.description"
)
if "hint" in field_data:
field_result["hint"] = (
f"{group_key}.{section_key}.{field_name}.hint"
)
if "labels" in field_data:
field_result["labels"] = (
f"{group_key}.{section_key}.{field_name}.labels"
)
items_result[field_key] = field_result
section_result["items"] = items_result
group_result["metadata"][section_key] = section_result group_result["metadata"][section_key] = section_result
+4 -6
View File
@@ -16,7 +16,7 @@ from astrbot.core.db.po import Conversation, ConversationV2
class ConversationManager: class ConversationManager:
"""负责管理会话与 LLM 的对话,某个会话当前正在用哪个对话。""" """负责管理会话与 LLM 的对话,某个会话当前正在用哪个对话。"""
def __init__(self, db_helper: BaseDatabase) -> None: def __init__(self, db_helper: BaseDatabase):
self.session_conversations: dict[str, str] = {} self.session_conversations: dict[str, str] = {}
self.db = db_helper self.db = db_helper
self.save_interval = 60 # 每 60 秒保存一次 self.save_interval = 60 # 每 60 秒保存一次
@@ -106,9 +106,7 @@ class ConversationManager:
await sp.session_put(unified_msg_origin, "sel_conv_id", conv.conversation_id) await sp.session_put(unified_msg_origin, "sel_conv_id", conv.conversation_id)
return conv.conversation_id return conv.conversation_id
async def switch_conversation( async def switch_conversation(self, unified_msg_origin: str, conversation_id: str):
self, unified_msg_origin: str, conversation_id: str
) -> None:
"""切换会话的对话 """切换会话的对话
Args: Args:
@@ -123,7 +121,7 @@ class ConversationManager:
self, self,
unified_msg_origin: str, unified_msg_origin: str,
conversation_id: str | None = None, conversation_id: str | None = None,
) -> None: ):
"""删除会话的对话,当 conversation_id 为 None 时删除会话当前的对话 """删除会话的对话,当 conversation_id 为 None 时删除会话当前的对话
Args: Args:
@@ -140,7 +138,7 @@ class ConversationManager:
self.session_conversations.pop(unified_msg_origin, None) self.session_conversations.pop(unified_msg_origin, None)
await sp.session_remove(unified_msg_origin, "sel_conv_id") await sp.session_remove(unified_msg_origin, "sel_conv_id")
async def delete_conversations_by_user_id(self, unified_msg_origin: str) -> None: async def delete_conversations_by_user_id(self, unified_msg_origin: str):
"""删除会话的所有对话 """删除会话的所有对话
Args: Args:
-19
View File
@@ -37,7 +37,6 @@ 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.llm_metadata import update_llm_metadata
from astrbot.core.utils.migra_helper import migra from astrbot.core.utils.migra_helper import migra
from astrbot.core.utils.temp_dir_cleaner import TempDirCleaner
from . import astrbot_config, html_renderer from . import astrbot_config, html_renderer
from .event_bus import EventBus from .event_bus import EventBus
@@ -58,7 +57,6 @@ class AstrBotCoreLifecycle:
self.subagent_orchestrator: SubAgentOrchestrator | None = None self.subagent_orchestrator: SubAgentOrchestrator | None = None
self.cron_manager: CronJobManager | None = None self.cron_manager: CronJobManager | None = None
self.temp_dir_cleaner: TempDirCleaner | None = None
# 设置代理 # 设置代理
proxy_config = self.astrbot_config.get("http_proxy", "") proxy_config = self.astrbot_config.get("http_proxy", "")
@@ -127,12 +125,6 @@ class AstrBotCoreLifecycle:
ucr=self.umop_config_router, ucr=self.umop_config_router,
sp=sp, sp=sp,
) )
self.temp_dir_cleaner = TempDirCleaner(
max_size_getter=lambda: self.astrbot_config_mgr.default_conf.get(
TempDirCleaner.CONFIG_KEY,
TempDirCleaner.DEFAULT_MAX_SIZE,
),
)
# apply migration # apply migration
try: try:
@@ -246,12 +238,6 @@ class AstrBotCoreLifecycle:
self.cron_manager.start(self.star_context), self.cron_manager.start(self.star_context),
name="cron_manager", name="cron_manager",
) )
temp_dir_cleaner_task = None
if self.temp_dir_cleaner:
temp_dir_cleaner_task = asyncio.create_task(
self.temp_dir_cleaner.run(),
name="temp_dir_cleaner",
)
# 把插件中注册的所有协程函数注册到事件总线中并执行 # 把插件中注册的所有协程函数注册到事件总线中并执行
extra_tasks = [] extra_tasks = []
@@ -261,8 +247,6 @@ class AstrBotCoreLifecycle:
tasks_ = [event_bus_task, *(extra_tasks if extra_tasks else [])] tasks_ = [event_bus_task, *(extra_tasks if extra_tasks else [])]
if cron_task: if cron_task:
tasks_.append(cron_task) tasks_.append(cron_task)
if temp_dir_cleaner_task:
tasks_.append(temp_dir_cleaner_task)
for task in tasks_: for task in tasks_:
self.curr_tasks.append( self.curr_tasks.append(
asyncio.create_task(self._task_wrapper(task), name=task.get_name()), asyncio.create_task(self._task_wrapper(task), name=task.get_name()),
@@ -314,9 +298,6 @@ class AstrBotCoreLifecycle:
async def stop(self) -> None: async def stop(self) -> None:
"""停止 AstrBot 核心生命周期管理类, 取消所有当前任务并终止各个管理器.""" """停止 AstrBot 核心生命周期管理类, 取消所有当前任务并终止各个管理器."""
if self.temp_dir_cleaner:
await self.temp_dir_cleaner.stop()
# 请求停止所有正在运行的异步任务 # 请求停止所有正在运行的异步任务
for task in self.curr_tasks: for task in self.curr_tasks:
task.cancel() task.cancel()
+3 -3
View File
@@ -24,7 +24,7 @@ class CronMessageEvent(AstrMessageEvent):
sender_name: str = "Scheduler", sender_name: str = "Scheduler",
extras: dict[str, Any] | None = None, extras: dict[str, Any] | None = None,
message_type: MessageType = MessageType.FRIEND_MESSAGE, message_type: MessageType = MessageType.FRIEND_MESSAGE,
) -> None: ):
platform_meta = PlatformMetadata( platform_meta = PlatformMetadata(
name="cron", name="cron",
description="CronJob", description="CronJob",
@@ -53,13 +53,13 @@ class CronMessageEvent(AstrMessageEvent):
if extras: if extras:
self._extras.update(extras) self._extras.update(extras)
async def send(self, message: MessageChain) -> None: async def send(self, message: MessageChain):
if message is None: if message is None:
return return
await self.context_obj.send_message(self.session, message) await self.context_obj.send_message(self.session, message)
await super().send(message) await super().send(message)
async def send_streaming(self, generator, use_fallback: bool = False) -> None: async def send_streaming(self, generator, use_fallback: bool = False):
async for chain in generator: async for chain in generator:
await self.send(chain) await self.send(chain)
+10 -10
View File
@@ -25,14 +25,14 @@ if TYPE_CHECKING:
class CronJobManager: class CronJobManager:
"""Central scheduler for BasicCronJob and ActiveAgentCronJob.""" """Central scheduler for BasicCronJob and ActiveAgentCronJob."""
def __init__(self, db: BaseDatabase) -> None: def __init__(self, db: BaseDatabase):
self.db = db self.db = db
self.scheduler = AsyncIOScheduler() self.scheduler = AsyncIOScheduler()
self._basic_handlers: dict[str, Callable[..., Any]] = {} self._basic_handlers: dict[str, Callable[..., Any]] = {}
self._lock = asyncio.Lock() self._lock = asyncio.Lock()
self._started = False self._started = False
async def start(self, ctx: "Context") -> None: async def start(self, ctx: "Context"):
self.ctx: Context = ctx # star context self.ctx: Context = ctx # star context
async with self._lock: async with self._lock:
if self._started: if self._started:
@@ -41,14 +41,14 @@ class CronJobManager:
self._started = True self._started = True
await self.sync_from_db() await self.sync_from_db()
async def shutdown(self) -> None: async def shutdown(self):
async with self._lock: async with self._lock:
if not self._started: if not self._started:
return return
self.scheduler.shutdown(wait=False) self.scheduler.shutdown(wait=False)
self._started = False self._started = False
async def sync_from_db(self) -> None: async def sync_from_db(self):
jobs = await self.db.list_cron_jobs() jobs = await self.db.list_cron_jobs()
for job in jobs: for job in jobs:
if not job.enabled or not job.persistent: if not job.enabled or not job.persistent:
@@ -136,11 +136,11 @@ class CronJobManager:
async def list_jobs(self, job_type: str | None = None) -> list[CronJob]: async def list_jobs(self, job_type: str | None = None) -> list[CronJob]:
return await self.db.list_cron_jobs(job_type) return await self.db.list_cron_jobs(job_type)
def _remove_scheduled(self, job_id: str) -> None: def _remove_scheduled(self, job_id: str):
if self.scheduler.get_job(job_id): if self.scheduler.get_job(job_id):
self.scheduler.remove_job(job_id) self.scheduler.remove_job(job_id)
def _schedule_job(self, job: CronJob) -> None: def _schedule_job(self, job: CronJob):
if not self._started: if not self._started:
self.scheduler.start() self.scheduler.start()
self._started = True self._started = True
@@ -188,7 +188,7 @@ class CronJobManager:
aps_job = self.scheduler.get_job(job_id) aps_job = self.scheduler.get_job(job_id)
return aps_job.next_run_time if aps_job else None return aps_job.next_run_time if aps_job else None
async def _run_job(self, job_id: str) -> None: async def _run_job(self, job_id: str):
job = await self.db.get_cron_job(job_id) job = await self.db.get_cron_job(job_id)
if not job or not job.enabled: if not job or not job.enabled:
return return
@@ -222,7 +222,7 @@ class CronJobManager:
# one-shot: remove after execution regardless of success # one-shot: remove after execution regardless of success
await self.delete_job(job_id) await self.delete_job(job_id)
async def _run_basic_job(self, job: CronJob) -> None: async def _run_basic_job(self, job: CronJob):
handler = self._basic_handlers.get(job.job_id) handler = self._basic_handlers.get(job.job_id)
if not handler: if not handler:
raise RuntimeError(f"Basic cron job handler not found for {job.job_id}") raise RuntimeError(f"Basic cron job handler not found for {job.job_id}")
@@ -231,7 +231,7 @@ class CronJobManager:
if asyncio.iscoroutine(result): if asyncio.iscoroutine(result):
await result await result
async def _run_active_agent_job(self, job: CronJob, start_time: datetime) -> None: async def _run_active_agent_job(self, job: CronJob, start_time: datetime):
payload = job.payload or {} payload = job.payload or {}
session_str = payload.get("session") session_str = payload.get("session")
if not session_str: if not session_str:
@@ -266,7 +266,7 @@ class CronJobManager:
message: str, message: str,
session_str: str, session_str: str,
extras: dict, extras: dict,
) -> None: ):
"""Woke the main agent to handle the cron job message.""" """Woke the main agent to handle the cron job message."""
from astrbot.core.astr_main_agent import ( from astrbot.core.astr_main_agent import (
MainAgentBuildConfig, MainAgentBuildConfig,
+1 -67
View File
@@ -8,7 +8,6 @@ from deprecated import deprecated
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from astrbot.core.db.po import ( from astrbot.core.db.po import (
ApiKey,
Attachment, Attachment,
ChatUIProject, ChatUIProject,
CommandConfig, CommandConfig,
@@ -44,7 +43,7 @@ class BaseDatabase(abc.ABC):
expire_on_commit=False, expire_on_commit=False,
) )
async def initialize(self) -> None: async def initialize(self):
"""初始化数据库连接""" """初始化数据库连接"""
@asynccontextmanager @asynccontextmanager
@@ -249,55 +248,6 @@ class BaseDatabase(abc.ABC):
""" """
... ...
@abc.abstractmethod
async def create_api_key(
self,
name: str,
key_hash: str,
key_prefix: str,
scopes: list[str] | None,
created_by: str,
expires_at: datetime.datetime | None = None,
) -> ApiKey:
"""Create a new API key record."""
...
@abc.abstractmethod
async def list_api_keys(self) -> list[ApiKey]:
"""List all API keys."""
...
@abc.abstractmethod
async def get_api_key_by_id(self, key_id: str) -> ApiKey | None:
"""Get an API key by key_id."""
...
@abc.abstractmethod
async def get_active_api_key_by_hash(self, key_hash: str) -> ApiKey | None:
"""Get an active API key by hash (not revoked, not expired)."""
...
@abc.abstractmethod
async def touch_api_key(self, key_id: str) -> None:
"""Update last_used_at of an API key."""
...
@abc.abstractmethod
async def revoke_api_key(self, key_id: str) -> bool:
"""Revoke an API key.
Returns True when the key exists and is updated.
"""
...
@abc.abstractmethod
async def delete_api_key(self, key_id: str) -> bool:
"""Delete an API key.
Returns True when the key exists and is deleted.
"""
...
@abc.abstractmethod @abc.abstractmethod
async def insert_persona( async def insert_persona(
self, self,
@@ -658,22 +608,6 @@ class BaseDatabase(abc.ABC):
""" """
... ...
@abc.abstractmethod
async def get_platform_sessions_by_creator_paginated(
self,
creator: str,
platform_id: str | None = None,
page: int = 1,
page_size: int = 20,
exclude_project_sessions: bool = False,
) -> tuple[list[dict], int]:
"""Get paginated platform sessions and total count for a creator.
Returns:
tuple[list[dict], int]: (sessions_with_project_info, total_count)
"""
...
@abc.abstractmethod @abc.abstractmethod
async def update_platform_session( async def update_platform_session(
self, self,
+5 -5
View File
@@ -43,7 +43,7 @@ def get_platform_type(
async def migration_conversation_table( async def migration_conversation_table(
db_helper: BaseDatabase, db_helper: BaseDatabase,
platform_id_map: dict[str, dict[str, str]], platform_id_map: dict[str, dict[str, str]],
) -> None: ):
db_helper_v3 = SQLiteV3DatabaseV3( db_helper_v3 = SQLiteV3DatabaseV3(
db_path=DB_PATH.replace("data_v4.db", "data_v3.db"), db_path=DB_PATH.replace("data_v4.db", "data_v3.db"),
) )
@@ -101,7 +101,7 @@ async def migration_conversation_table(
async def migration_platform_table( async def migration_platform_table(
db_helper: BaseDatabase, db_helper: BaseDatabase,
platform_id_map: dict[str, dict[str, str]], platform_id_map: dict[str, dict[str, str]],
) -> None: ):
db_helper_v3 = SQLiteV3DatabaseV3( db_helper_v3 = SQLiteV3DatabaseV3(
db_path=DB_PATH.replace("data_v4.db", "data_v3.db"), db_path=DB_PATH.replace("data_v4.db", "data_v3.db"),
) )
@@ -180,7 +180,7 @@ async def migration_platform_table(
async def migration_webchat_data( async def migration_webchat_data(
db_helper: BaseDatabase, db_helper: BaseDatabase,
platform_id_map: dict[str, dict[str, str]], platform_id_map: dict[str, dict[str, str]],
) -> None: ):
"""迁移 WebChat 的历史记录到新的 PlatformMessageHistory 表中""" """迁移 WebChat 的历史记录到新的 PlatformMessageHistory 表中"""
db_helper_v3 = SQLiteV3DatabaseV3( db_helper_v3 = SQLiteV3DatabaseV3(
db_path=DB_PATH.replace("data_v4.db", "data_v3.db"), db_path=DB_PATH.replace("data_v4.db", "data_v3.db"),
@@ -236,7 +236,7 @@ async def migration_webchat_data(
async def migration_persona_data( async def migration_persona_data(
db_helper: BaseDatabase, db_helper: BaseDatabase,
astrbot_config: AstrBotConfig, astrbot_config: AstrBotConfig,
) -> None: ):
"""迁移 Persona 数据到新的表中。 """迁移 Persona 数据到新的表中。
旧的 Persona 数据存储在 preference 新的 Persona 数据存储在 persona 表中 旧的 Persona 数据存储在 preference 新的 Persona 数据存储在 persona 表中
""" """
@@ -279,7 +279,7 @@ async def migration_persona_data(
async def migration_preferences( async def migration_preferences(
db_helper: BaseDatabase, db_helper: BaseDatabase,
platform_id_map: dict[str, dict[str, str]], platform_id_map: dict[str, dict[str, str]],
) -> None: ):
# 1. global scope migration # 1. global scope migration
keys = [ keys = [
"inactivated_llm_tools", "inactivated_llm_tools",
+1 -1
View File
@@ -3,7 +3,7 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
from astrbot.core.umop_config_router import UmopConfigRouter from astrbot.core.umop_config_router import UmopConfigRouter
async def migrate_45_to_46(acm: AstrBotConfigManager, ucr: UmopConfigRouter) -> None: async def migrate_45_to_46(acm: AstrBotConfigManager, ucr: UmopConfigRouter):
abconf_data = acm.abconf_data abconf_data = acm.abconf_data
if not isinstance(abconf_data, dict): if not isinstance(abconf_data, dict):
@@ -12,7 +12,7 @@ from astrbot.api import logger, sp
from astrbot.core.db import BaseDatabase from astrbot.core.db import BaseDatabase
async def migrate_token_usage(db_helper: BaseDatabase) -> None: async def migrate_token_usage(db_helper: BaseDatabase):
"""Add token_usage column to conversations table. """Add token_usage column to conversations table.
This migration adds a new column to track token consumption in conversations. This migration adds a new column to track token consumption in conversations.
@@ -17,7 +17,7 @@ from astrbot.core.db import BaseDatabase
from astrbot.core.db.po import ConversationV2, PlatformMessageHistory, PlatformSession from astrbot.core.db.po import ConversationV2, PlatformMessageHistory, PlatformSession
async def migrate_webchat_session(db_helper: BaseDatabase) -> None: async def migrate_webchat_session(db_helper: BaseDatabase):
"""Create PlatformSession records from platform_message_history. """Create PlatformSession records from platform_message_history.
This migration extracts all unique user_ids from platform_message_history This migration extracts all unique user_ids from platform_message_history
@@ -8,7 +8,7 @@ _VT = TypeVar("_VT")
class SharedPreferences: class SharedPreferences:
def __init__(self, path=None) -> None: def __init__(self, path=None):
if path is None: if path is None:
path = os.path.join(get_astrbot_data_path(), "shared_preferences.json") path = os.path.join(get_astrbot_data_path(), "shared_preferences.json")
self.path = path self.path = path
@@ -23,7 +23,7 @@ class SharedPreferences:
os.remove(self.path) os.remove(self.path)
return {} return {}
def _save_preferences(self) -> None: def _save_preferences(self):
with open(self.path, "w") as f: with open(self.path, "w") as f:
json.dump(self._data, f, indent=4, ensure_ascii=False) json.dump(self._data, f, indent=4, ensure_ascii=False)
f.flush() f.flush()
@@ -31,16 +31,16 @@ class SharedPreferences:
def get(self, key, default: _VT = None) -> _VT: def get(self, key, default: _VT = None) -> _VT:
return self._data.get(key, default) return self._data.get(key, default)
def put(self, key, value) -> None: def put(self, key, value):
self._data[key] = value self._data[key] = value
self._save_preferences() self._save_preferences()
def remove(self, key) -> None: def remove(self, key):
if key in self._data: if key in self._data:
del self._data[key] del self._data[key]
self._save_preferences() self._save_preferences()
def clear(self) -> None: def clear(self):
self._data.clear() self._data.clear()
self._save_preferences() self._save_preferences()
+8 -10
View File
@@ -127,7 +127,7 @@ class SQLiteDatabase:
conn.text_factory = str conn.text_factory = str
return conn return conn
def _exec_sql(self, sql: str, params: tuple | None = None) -> None: def _exec_sql(self, sql: str, params: tuple | None = None):
conn = self.conn conn = self.conn
try: try:
c = self.conn.cursor() c = self.conn.cursor()
@@ -144,7 +144,7 @@ class SQLiteDatabase:
conn.commit() conn.commit()
def insert_platform_metrics(self, metrics: dict) -> None: def insert_platform_metrics(self, metrics: dict):
for k, v in metrics.items(): for k, v in metrics.items():
self._exec_sql( self._exec_sql(
""" """
@@ -153,7 +153,7 @@ class SQLiteDatabase:
(k, v, int(time.time())), (k, v, int(time.time())),
) )
def insert_llm_metrics(self, metrics: dict) -> None: def insert_llm_metrics(self, metrics: dict):
for k, v in metrics.items(): for k, v in metrics.items():
self._exec_sql( self._exec_sql(
""" """
@@ -249,7 +249,7 @@ class SQLiteDatabase:
return Conversation(*res) return Conversation(*res)
def new_conversation(self, user_id: str, cid: str) -> None: def new_conversation(self, user_id: str, cid: str):
history = "[]" history = "[]"
updated_at = int(time.time()) updated_at = int(time.time())
created_at = updated_at created_at = updated_at
@@ -287,7 +287,7 @@ class SQLiteDatabase:
) )
return conversations return conversations
def update_conversation(self, user_id: str, cid: str, history: str) -> None: def update_conversation(self, user_id: str, cid: str, history: str):
"""更新对话,并且同时更新时间""" """更新对话,并且同时更新时间"""
updated_at = int(time.time()) updated_at = int(time.time())
self._exec_sql( self._exec_sql(
@@ -297,7 +297,7 @@ class SQLiteDatabase:
(history, updated_at, user_id, cid), (history, updated_at, user_id, cid),
) )
def update_conversation_title(self, user_id: str, cid: str, title: str) -> None: def update_conversation_title(self, user_id: str, cid: str, title: str):
self._exec_sql( self._exec_sql(
""" """
UPDATE webchat_conversation SET title = ? WHERE user_id = ? AND cid = ? UPDATE webchat_conversation SET title = ? WHERE user_id = ? AND cid = ?
@@ -305,9 +305,7 @@ class SQLiteDatabase:
(title, user_id, cid), (title, user_id, cid),
) )
def update_conversation_persona_id( def update_conversation_persona_id(self, user_id: str, cid: str, persona_id: str):
self, user_id: str, cid: str, persona_id: str
) -> None:
self._exec_sql( self._exec_sql(
""" """
UPDATE webchat_conversation SET persona_id = ? WHERE user_id = ? AND cid = ? UPDATE webchat_conversation SET persona_id = ? WHERE user_id = ? AND cid = ?
@@ -315,7 +313,7 @@ class SQLiteDatabase:
(persona_id, user_id, cid), (persona_id, user_id, cid),
) )
def delete_conversation(self, user_id: str, cid: str) -> None: def delete_conversation(self, user_id: str, cid: str):
self._exec_sql( self._exec_sql(
""" """
DELETE FROM webchat_conversation WHERE user_id = ? AND cid = ? DELETE FROM webchat_conversation WHERE user_id = ? AND cid = ?
-37
View File
@@ -288,43 +288,6 @@ class Attachment(TimestampMixin, SQLModel, table=True):
) )
class ApiKey(TimestampMixin, SQLModel, table=True):
"""API keys used by external developers to access Open APIs."""
__tablename__: str = "api_keys"
inner_id: int | None = Field(
primary_key=True,
sa_column_kwargs={"autoincrement": True},
default=None,
)
key_id: str = Field(
max_length=36,
nullable=False,
unique=True,
default_factory=lambda: str(uuid.uuid4()),
)
name: str = Field(max_length=255, nullable=False)
key_hash: str = Field(max_length=128, nullable=False, unique=True)
key_prefix: str = Field(max_length=24, nullable=False)
scopes: list | None = Field(default=None, sa_type=JSON)
created_by: str = Field(max_length=255, nullable=False)
last_used_at: datetime | None = Field(default=None)
expires_at: datetime | None = Field(default=None)
revoked_at: datetime | None = Field(default=None)
__table_args__ = (
UniqueConstraint(
"key_id",
name="uix_api_key_id",
),
UniqueConstraint(
"key_hash",
name="uix_api_key_hash",
),
)
class ChatUIProject(TimestampMixin, SQLModel, table=True): class ChatUIProject(TimestampMixin, SQLModel, table=True):
"""This class represents projects for organizing ChatUI conversations. """This class represents projects for organizing ChatUI conversations.
+49 -188
View File
@@ -10,7 +10,6 @@ from sqlmodel import col, delete, desc, func, or_, select, text, update
from astrbot.core.db import BaseDatabase from astrbot.core.db import BaseDatabase
from astrbot.core.db.po import ( from astrbot.core.db.po import (
ApiKey,
Attachment, Attachment,
ChatUIProject, ChatUIProject,
CommandConfig, CommandConfig,
@@ -306,7 +305,7 @@ class SQLiteDatabase(BaseDatabase):
await session.execute(query) await session.execute(query)
return await self.get_conversation_by_id(cid) return await self.get_conversation_by_id(cid)
async def delete_conversation(self, cid) -> None: async def delete_conversation(self, cid):
async with self.get_db() as session: async with self.get_db() as session:
session: AsyncSession session: AsyncSession
async with session.begin(): async with session.begin():
@@ -462,7 +461,7 @@ class SQLiteDatabase(BaseDatabase):
platform_id, platform_id,
user_id, user_id,
offset_sec=86400, offset_sec=86400,
) -> None: ):
"""Delete platform message history records newer than the specified offset.""" """Delete platform message history records newer than the specified offset."""
async with self.get_db() as session: async with self.get_db() as session:
session: AsyncSession session: AsyncSession
@@ -574,100 +573,6 @@ class SQLiteDatabase(BaseDatabase):
result = T.cast(CursorResult, await session.execute(query)) result = T.cast(CursorResult, await session.execute(query))
return result.rowcount return result.rowcount
async def create_api_key(
self,
name: str,
key_hash: str,
key_prefix: str,
scopes: list[str] | None,
created_by: str,
expires_at: datetime | None = None,
) -> ApiKey:
"""Create a new API key record."""
async with self.get_db() as session:
session: AsyncSession
async with session.begin():
api_key = ApiKey(
name=name,
key_hash=key_hash,
key_prefix=key_prefix,
scopes=scopes,
created_by=created_by,
expires_at=expires_at,
)
session.add(api_key)
await session.flush()
await session.refresh(api_key)
return api_key
async def list_api_keys(self) -> list[ApiKey]:
"""List all API keys."""
async with self.get_db() as session:
session: AsyncSession
result = await session.execute(
select(ApiKey).order_by(desc(ApiKey.created_at))
)
return list(result.scalars().all())
async def get_api_key_by_id(self, key_id: str) -> ApiKey | None:
"""Get an API key by key_id."""
async with self.get_db() as session:
session: AsyncSession
result = await session.execute(
select(ApiKey).where(ApiKey.key_id == key_id)
)
return result.scalar_one_or_none()
async def get_active_api_key_by_hash(self, key_hash: str) -> ApiKey | None:
"""Get an active API key by hash (not revoked, not expired)."""
async with self.get_db() as session:
session: AsyncSession
now = datetime.now(timezone.utc)
query = select(ApiKey).where(
ApiKey.key_hash == key_hash,
col(ApiKey.revoked_at).is_(None),
or_(col(ApiKey.expires_at).is_(None), ApiKey.expires_at > now),
)
result = await session.execute(query)
return result.scalar_one_or_none()
async def touch_api_key(self, key_id: str) -> None:
"""Update last_used_at of an API key."""
async with self.get_db() as session:
session: AsyncSession
async with session.begin():
await session.execute(
update(ApiKey)
.where(ApiKey.key_id == key_id)
.values(last_used_at=datetime.now(timezone.utc)),
)
async def revoke_api_key(self, key_id: str) -> bool:
"""Revoke an API key."""
async with self.get_db() as session:
session: AsyncSession
async with session.begin():
query = (
update(ApiKey)
.where(ApiKey.key_id == key_id)
.values(revoked_at=datetime.now(timezone.utc))
)
result = T.cast(CursorResult, await session.execute(query))
return result.rowcount > 0
async def delete_api_key(self, key_id: str) -> bool:
"""Delete an API key."""
async with self.get_db() as session:
session: AsyncSession
async with session.begin():
result = T.cast(
CursorResult,
await session.execute(
delete(ApiKey).where(ApiKey.key_id == key_id)
),
)
return result.rowcount > 0
async def insert_persona( async def insert_persona(
self, self,
persona_id, persona_id,
@@ -740,7 +645,7 @@ class SQLiteDatabase(BaseDatabase):
await session.execute(query) await session.execute(query)
return await self.get_persona_by_id(persona_id) return await self.get_persona_by_id(persona_id)
async def delete_persona(self, persona_id) -> None: async def delete_persona(self, persona_id):
"""Delete a persona by its ID.""" """Delete a persona by its ID."""
async with self.get_db() as session: async with self.get_db() as session:
session: AsyncSession session: AsyncSession
@@ -998,7 +903,7 @@ class SQLiteDatabase(BaseDatabase):
result = await session.execute(query) result = await session.execute(query)
return result.scalars().all() return result.scalars().all()
async def remove_preference(self, scope, scope_id, key) -> None: async def remove_preference(self, scope, scope_id, key):
"""Remove a preference by scope ID and key.""" """Remove a preference by scope ID and key."""
async with self.get_db() as session: async with self.get_db() as session:
session: AsyncSession session: AsyncSession
@@ -1012,7 +917,7 @@ class SQLiteDatabase(BaseDatabase):
) )
await session.commit() await session.commit()
async def clear_preferences(self, scope, scope_id) -> None: async def clear_preferences(self, scope, scope_id):
"""Clear all preferences for a specific scope ID.""" """Clear all preferences for a specific scope ID."""
async with self.get_db() as session: async with self.get_db() as session:
session: AsyncSession session: AsyncSession
@@ -1290,7 +1195,7 @@ class SQLiteDatabase(BaseDatabase):
result = None result = None
def runner() -> None: def runner():
nonlocal result nonlocal result
result = asyncio.run(_inner()) result = asyncio.run(_inner())
@@ -1313,7 +1218,7 @@ class SQLiteDatabase(BaseDatabase):
result = None result = None
def runner() -> None: def runner():
nonlocal result nonlocal result
result = asyncio.run(_inner()) result = asyncio.run(_inner())
@@ -1348,7 +1253,7 @@ class SQLiteDatabase(BaseDatabase):
result = None result = None
def runner() -> None: def runner():
nonlocal result nonlocal result
result = asyncio.run(_inner()) result = asyncio.run(_inner())
@@ -1412,102 +1317,58 @@ class SQLiteDatabase(BaseDatabase):
Returns a list of dicts containing session info and project info (if session belongs to a project). Returns a list of dicts containing session info and project info (if session belongs to a project).
""" """
(
sessions_with_projects,
_,
) = await self.get_platform_sessions_by_creator_paginated(
creator=creator,
platform_id=platform_id,
page=page,
page_size=page_size,
exclude_project_sessions=False,
)
return sessions_with_projects
@staticmethod
def _build_platform_sessions_query(
creator: str,
platform_id: str | None = None,
exclude_project_sessions: bool = False,
):
query = (
select(
PlatformSession,
col(ChatUIProject.project_id),
col(ChatUIProject.title).label("project_title"),
col(ChatUIProject.emoji).label("project_emoji"),
)
.outerjoin(
SessionProjectRelation,
col(PlatformSession.session_id)
== col(SessionProjectRelation.session_id),
)
.outerjoin(
ChatUIProject,
col(SessionProjectRelation.project_id) == col(ChatUIProject.project_id),
)
.where(col(PlatformSession.creator) == creator)
)
if platform_id:
query = query.where(PlatformSession.platform_id == platform_id)
if exclude_project_sessions:
query = query.where(col(ChatUIProject.project_id).is_(None))
return query
@staticmethod
def _rows_to_session_dicts(rows: list[tuple]) -> list[dict]:
sessions_with_projects = []
for row in rows:
platform_session = row[0]
project_id = row[1]
project_title = row[2]
project_emoji = row[3]
session_dict = {
"session": platform_session,
"project_id": project_id,
"project_title": project_title,
"project_emoji": project_emoji,
}
sessions_with_projects.append(session_dict)
return sessions_with_projects
async def get_platform_sessions_by_creator_paginated(
self,
creator: str,
platform_id: str | None = None,
page: int = 1,
page_size: int = 20,
exclude_project_sessions: bool = False,
) -> tuple[list[dict], int]:
"""Get paginated Platform sessions for a creator with total count."""
async with self.get_db() as session: async with self.get_db() as session:
session: AsyncSession session: AsyncSession
offset = (page - 1) * page_size offset = (page - 1) * page_size
base_query = self._build_platform_sessions_query( # LEFT JOIN with SessionProjectRelation and ChatUIProject to get project info
creator=creator, query = (
platform_id=platform_id, select(
exclude_project_sessions=exclude_project_sessions, PlatformSession,
col(ChatUIProject.project_id),
col(ChatUIProject.title).label("project_title"),
col(ChatUIProject.emoji).label("project_emoji"),
)
.outerjoin(
SessionProjectRelation,
col(PlatformSession.session_id)
== col(SessionProjectRelation.session_id),
)
.outerjoin(
ChatUIProject,
col(SessionProjectRelation.project_id)
== col(ChatUIProject.project_id),
)
.where(col(PlatformSession.creator) == creator)
) )
total_result = await session.execute( if platform_id:
select(func.count()).select_from(base_query.subquery()) query = query.where(PlatformSession.platform_id == platform_id)
)
total = int(total_result.scalar_one() or 0)
result_query = ( query = (
base_query.order_by(desc(PlatformSession.updated_at)) query.order_by(desc(PlatformSession.updated_at))
.offset(offset) .offset(offset)
.limit(page_size) .limit(page_size)
) )
result = await session.execute(result_query) result = await session.execute(query)
sessions_with_projects = self._rows_to_session_dicts(result.all()) # Convert to list of dicts with session and project info
return sessions_with_projects, total sessions_with_projects = []
for row in result.all():
platform_session = row[0]
project_id = row[1]
project_title = row[2]
project_emoji = row[3]
session_dict = {
"session": platform_session,
"project_id": project_id,
"project_title": project_title,
"project_emoji": project_emoji,
}
sessions_with_projects.append(session_dict)
return sessions_with_projects
async def update_platform_session( async def update_platform_session(
self, self,
+1 -1
View File
@@ -9,7 +9,7 @@ class Result:
class BaseVecDB: class BaseVecDB:
async def initialize(self) -> None: async def initialize(self):
"""初始化向量数据库""" """初始化向量数据库"""
@abc.abstractmethod @abc.abstractmethod
@@ -33,7 +33,7 @@ class Document(BaseDocModel, table=True):
class DocumentStorage: class DocumentStorage:
def __init__(self, db_path: str) -> None: def __init__(self, db_path: str):
self.db_path = db_path self.db_path = db_path
self.DATABASE_URL = f"sqlite+aiosqlite:///{db_path}" self.DATABASE_URL = f"sqlite+aiosqlite:///{db_path}"
self.engine: AsyncEngine | None = None self.engine: AsyncEngine | None = None
@@ -43,7 +43,7 @@ class DocumentStorage:
"sqlite_init.sql", "sqlite_init.sql",
) )
async def initialize(self) -> None: async def initialize(self):
"""Initialize the SQLite database and create the documents table if it doesn't exist.""" """Initialize the SQLite database and create the documents table if it doesn't exist."""
await self.connect() await self.connect()
async with self.engine.begin() as conn: # type: ignore async with self.engine.begin() as conn: # type: ignore
@@ -80,7 +80,7 @@ class DocumentStorage:
await conn.commit() await conn.commit()
async def connect(self) -> None: async def connect(self):
"""Connect to the SQLite database.""" """Connect to the SQLite database."""
if self.engine is None: if self.engine is None:
self.engine = create_async_engine( self.engine = create_async_engine(
@@ -211,7 +211,7 @@ class DocumentStorage:
await session.flush() # Flush to get all IDs await session.flush() # Flush to get all IDs
return [doc.id for doc in documents] # type: ignore return [doc.id for doc in documents] # type: ignore
async def delete_document_by_doc_id(self, doc_id: str) -> None: async def delete_document_by_doc_id(self, doc_id: str):
"""Delete a document by its doc_id. """Delete a document by its doc_id.
Args: Args:
@@ -249,7 +249,7 @@ class DocumentStorage:
return self._document_to_dict(document) return self._document_to_dict(document)
return None return None
async def update_document_by_doc_id(self, doc_id: str, new_text: str) -> None: async def update_document_by_doc_id(self, doc_id: str, new_text: str):
"""Update a document by its doc_id. """Update a document by its doc_id.
Args: Args:
@@ -269,7 +269,7 @@ class DocumentStorage:
document.updated_at = datetime.now() document.updated_at = datetime.now()
session.add(document) session.add(document)
async def delete_documents(self, metadata_filters: dict) -> None: async def delete_documents(self, metadata_filters: dict):
"""Delete documents by their metadata filters. """Delete documents by their metadata filters.
Args: Args:
@@ -384,7 +384,7 @@ class DocumentStorage:
"updated_at": row[5], "updated_at": row[5],
} }
async def close(self) -> None: async def close(self):
"""Close the connection to the SQLite database.""" """Close the connection to the SQLite database."""
if self.engine: if self.engine:
await self.engine.dispose() await self.engine.dispose()
@@ -10,7 +10,7 @@ import numpy as np
class EmbeddingStorage: class EmbeddingStorage:
def __init__(self, dimension: int, path: str | None = None) -> None: def __init__(self, dimension: int, path: str | None = None):
self.dimension = dimension self.dimension = dimension
self.path = path self.path = path
self.index = None self.index = None
@@ -20,7 +20,7 @@ class EmbeddingStorage:
base_index = faiss.IndexFlatL2(dimension) base_index = faiss.IndexFlatL2(dimension)
self.index = faiss.IndexIDMap(base_index) self.index = faiss.IndexIDMap(base_index)
async def insert(self, vector: np.ndarray, id: int) -> None: async def insert(self, vector: np.ndarray, id: int):
"""插入向量 """插入向量
Args: Args:
@@ -38,7 +38,7 @@ class EmbeddingStorage:
self.index.add_with_ids(vector.reshape(1, -1), np.array([id])) self.index.add_with_ids(vector.reshape(1, -1), np.array([id]))
await self.save_index() await self.save_index()
async def insert_batch(self, vectors: np.ndarray, ids: list[int]) -> None: async def insert_batch(self, vectors: np.ndarray, ids: list[int]):
"""批量插入向量 """批量插入向量
Args: Args:
@@ -71,7 +71,7 @@ class EmbeddingStorage:
distances, indices = self.index.search(vector, k) distances, indices = self.index.search(vector, k)
return distances, indices return distances, indices
async def delete(self, ids: list[int]) -> None: async def delete(self, ids: list[int]):
"""删除向量 """删除向量
Args: Args:
@@ -83,7 +83,7 @@ class EmbeddingStorage:
self.index.remove_ids(id_array) self.index.remove_ids(id_array)
await self.save_index() await self.save_index()
async def save_index(self) -> None: async def save_index(self):
"""保存索引 """保存索引
Args: Args:
+5 -5
View File
@@ -20,7 +20,7 @@ class FaissVecDB(BaseVecDB):
index_store_path: str, index_store_path: str,
embedding_provider: EmbeddingProvider, embedding_provider: EmbeddingProvider,
rerank_provider: RerankProvider | None = None, rerank_provider: RerankProvider | None = None,
) -> None: ):
self.doc_store_path = doc_store_path self.doc_store_path = doc_store_path
self.index_store_path = index_store_path self.index_store_path = index_store_path
self.embedding_provider = embedding_provider self.embedding_provider = embedding_provider
@@ -32,7 +32,7 @@ class FaissVecDB(BaseVecDB):
self.embedding_provider = embedding_provider self.embedding_provider = embedding_provider
self.rerank_provider = rerank_provider self.rerank_provider = rerank_provider
async def initialize(self) -> None: async def initialize(self):
await self.document_storage.initialize() await self.document_storage.initialize()
async def insert( async def insert(
@@ -165,7 +165,7 @@ class FaissVecDB(BaseVecDB):
return top_k_results return top_k_results
async def delete(self, doc_id: str) -> None: async def delete(self, doc_id: str):
"""删除一条文档块(chunk""" """删除一条文档块(chunk"""
# 获得对应的 int id # 获得对应的 int id
result = await self.document_storage.get_document_by_doc_id(doc_id) result = await self.document_storage.get_document_by_doc_id(doc_id)
@@ -177,7 +177,7 @@ class FaissVecDB(BaseVecDB):
await self.document_storage.delete_document_by_doc_id(doc_id) await self.document_storage.delete_document_by_doc_id(doc_id)
await self.embedding_storage.delete([int_id]) await self.embedding_storage.delete([int_id])
async def close(self) -> None: async def close(self):
await self.document_storage.close() await self.document_storage.close()
async def count_documents(self, metadata_filter: dict | None = None) -> int: async def count_documents(self, metadata_filter: dict | None = None) -> int:
@@ -192,7 +192,7 @@ class FaissVecDB(BaseVecDB):
) )
return count return count
async def delete_documents(self, metadata_filters: dict) -> None: async def delete_documents(self, metadata_filters: dict):
"""根据元数据过滤器删除文档""" """根据元数据过滤器删除文档"""
docs = await self.document_storage.get_documents( docs = await self.document_storage.get_documents(
metadata_filters=metadata_filters, metadata_filters=metadata_filters,
+3 -3
View File
@@ -28,13 +28,13 @@ class EventBus:
event_queue: Queue, event_queue: Queue,
pipeline_scheduler_mapping: dict[str, PipelineScheduler], pipeline_scheduler_mapping: dict[str, PipelineScheduler],
astrbot_config_mgr: AstrBotConfigManager, astrbot_config_mgr: AstrBotConfigManager,
) -> None: ):
self.event_queue = event_queue # 事件队列 self.event_queue = event_queue # 事件队列
# abconf uuid -> scheduler # abconf uuid -> scheduler
self.pipeline_scheduler_mapping = pipeline_scheduler_mapping self.pipeline_scheduler_mapping = pipeline_scheduler_mapping
self.astrbot_config_mgr = astrbot_config_mgr self.astrbot_config_mgr = astrbot_config_mgr
async def dispatch(self) -> None: async def dispatch(self):
while True: while True:
event: AstrMessageEvent = await self.event_queue.get() event: AstrMessageEvent = await self.event_queue.get()
conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin) conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin)
@@ -47,7 +47,7 @@ class EventBus:
continue continue
asyncio.create_task(scheduler.execute(event)) asyncio.create_task(scheduler.execute(event))
def _print_event(self, event: AstrMessageEvent, conf_name: str) -> None: def _print_event(self, event: AstrMessageEvent, conf_name: str):
"""用于记录事件信息 """用于记录事件信息
Args: Args:
+2 -2
View File
@@ -9,12 +9,12 @@ from urllib.parse import unquote, urlparse
class FileTokenService: class FileTokenService:
"""维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。""" """维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。"""
def __init__(self, default_timeout: float = 300) -> None: def __init__(self, default_timeout: float = 300):
self.lock = asyncio.Lock() self.lock = asyncio.Lock()
self.staged_files = {} # token: (file_path, expire_time) self.staged_files = {} # token: (file_path, expire_time)
self.default_timeout = default_timeout self.default_timeout = default_timeout
async def _cleanup_expired_tokens(self) -> None: async def _cleanup_expired_tokens(self):
"""清理过期的令牌""" """清理过期的令牌"""
now = time.time() now = time.time()
expired_tokens = [ expired_tokens = [
+2 -2
View File
@@ -17,13 +17,13 @@ from astrbot.dashboard.server import AstrBotDashboard
class InitialLoader: class InitialLoader:
"""AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。""" """AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。"""
def __init__(self, db: BaseDatabase, log_broker: LogBroker) -> None: def __init__(self, db: BaseDatabase, log_broker: LogBroker):
self.db = db self.db = db
self.logger = logger self.logger = logger
self.log_broker = log_broker self.log_broker = log_broker
self.webui_dir: str | None = None self.webui_dir: str | None = None
async def start(self) -> None: async def start(self):
core_lifecycle = AstrBotCoreLifecycle(self.log_broker, self.db) core_lifecycle = AstrBotCoreLifecycle(self.log_broker, self.db)
try: try:
@@ -12,7 +12,7 @@ class FixedSizeChunker(BaseChunker):
按照固定的字符数分块,并支持块之间的重叠 按照固定的字符数分块,并支持块之间的重叠
""" """
def __init__(self, chunk_size: int = 512, chunk_overlap: int = 50) -> None: def __init__(self, chunk_size: int = 512, chunk_overlap: int = 50):
"""初始化分块器 """初始化分块器
Args: Args:
@@ -11,7 +11,7 @@ class RecursiveCharacterChunker(BaseChunker):
length_function: Callable[[str], int] = len, length_function: Callable[[str], int] = len,
is_separator_regex: bool = False, is_separator_regex: bool = False,
separators: list[str] | None = None, separators: list[str] | None = None,
) -> None: ):
"""初始化递归字符文本分割器 """初始化递归字符文本分割器
Args: Args:
+3 -6
View File
@@ -13,19 +13,16 @@ from astrbot.core.knowledge_base.models import (
KBMedia, KBMedia,
KnowledgeBase, KnowledgeBase,
) )
from astrbot.core.utils.astrbot_path import get_astrbot_knowledge_base_path
class KBSQLiteDatabase: class KBSQLiteDatabase:
def __init__(self, db_path: str | None = None) -> None: def __init__(self, db_path: str = "data/knowledge_base/kb.db") -> None:
"""初始化知识库数据库 """初始化知识库数据库
Args: Args:
db_path: 数据库文件路径, 默认位于 AstrBot 数据目录下的 knowledge_base/kb.db db_path: 数据库文件路径, 默认 data/knowledge_base/kb.db
""" """
if db_path is None:
db_path = str(Path(get_astrbot_knowledge_base_path()) / "kb.db")
self.db_path = db_path self.db_path = db_path
self.DATABASE_URL = f"sqlite+aiosqlite:///{db_path}" self.DATABASE_URL = f"sqlite+aiosqlite:///{db_path}"
self.inited = False self.inited = False
@@ -256,7 +253,7 @@ class KBSQLiteDatabase:
"knowledge_base": row[1], "knowledge_base": row[1],
} }
async def delete_document_by_id(self, doc_id: str, vec_db: FaissVecDB) -> None: async def delete_document_by_id(self, doc_id: str, vec_db: FaissVecDB):
"""删除单个文档及其相关数据""" """删除单个文档及其相关数据"""
# 在知识库表中删除 # 在知识库表中删除
async with self.get_db() as session, session.begin(): async with self.get_db() as session, session.begin():
+9 -9
View File
@@ -31,7 +31,7 @@ from .prompts import TEXT_REPAIR_SYSTEM_PROMPT
class RateLimiter: class RateLimiter:
"""一个简单的速率限制器""" """一个简单的速率限制器"""
def __init__(self, max_rpm: int) -> None: def __init__(self, max_rpm: int):
self.max_per_minute = max_rpm self.max_per_minute = max_rpm
self.interval = 60.0 / max_rpm if max_rpm > 0 else 0 self.interval = 60.0 / max_rpm if max_rpm > 0 else 0
self.last_call_time = 0 self.last_call_time = 0
@@ -116,7 +116,7 @@ class KBHelper:
provider_manager: ProviderManager, provider_manager: ProviderManager,
kb_root_dir: str, kb_root_dir: str,
chunker: BaseChunker, chunker: BaseChunker,
) -> None: ):
self.kb_db = kb_db self.kb_db = kb_db
self.kb = kb self.kb = kb
self.prov_mgr = provider_manager self.prov_mgr = provider_manager
@@ -130,7 +130,7 @@ class KBHelper:
self.kb_medias_dir.mkdir(parents=True, exist_ok=True) self.kb_medias_dir.mkdir(parents=True, exist_ok=True)
self.kb_files_dir.mkdir(parents=True, exist_ok=True) self.kb_files_dir.mkdir(parents=True, exist_ok=True)
async def initialize(self) -> None: async def initialize(self):
await self._ensure_vec_db() await self._ensure_vec_db()
async def get_ep(self) -> EmbeddingProvider: async def get_ep(self) -> EmbeddingProvider:
@@ -174,7 +174,7 @@ class KBHelper:
self.vec_db = vec_db self.vec_db = vec_db
return vec_db return vec_db
async def delete_vec_db(self) -> None: async def delete_vec_db(self):
"""删除知识库的向量数据库和所有相关文件""" """删除知识库的向量数据库和所有相关文件"""
import shutil import shutil
@@ -182,7 +182,7 @@ class KBHelper:
if self.kb_dir.exists(): if self.kb_dir.exists():
shutil.rmtree(self.kb_dir) shutil.rmtree(self.kb_dir)
async def terminate(self) -> None: async def terminate(self):
if self.vec_db: if self.vec_db:
await self.vec_db.close() await self.vec_db.close()
@@ -293,7 +293,7 @@ class KBHelper:
await progress_callback("chunking", 100, 100) await progress_callback("chunking", 100, 100)
# 阶段3: 生成向量(带进度回调) # 阶段3: 生成向量(带进度回调)
async def embedding_progress_callback(current, total) -> None: async def embedding_progress_callback(current, total):
if progress_callback: if progress_callback:
await progress_callback("embedding", current, total) await progress_callback("embedding", current, total)
@@ -360,7 +360,7 @@ class KBHelper:
doc = await self.kb_db.get_document_by_id(doc_id) doc = await self.kb_db.get_document_by_id(doc_id)
return doc return doc
async def delete_document(self, doc_id: str) -> None: async def delete_document(self, doc_id: str):
"""删除单个文档及其相关数据""" """删除单个文档及其相关数据"""
await self.kb_db.delete_document_by_id( await self.kb_db.delete_document_by_id(
doc_id=doc_id, doc_id=doc_id,
@@ -372,7 +372,7 @@ class KBHelper:
) )
await self.refresh_kb() await self.refresh_kb()
async def delete_chunk(self, chunk_id: str, doc_id: str) -> None: async def delete_chunk(self, chunk_id: str, doc_id: str):
"""删除单个文本块及其相关数据""" """删除单个文本块及其相关数据"""
vec_db: FaissVecDB = self.vec_db # type: ignore vec_db: FaissVecDB = self.vec_db # type: ignore
await vec_db.delete(chunk_id) await vec_db.delete(chunk_id)
@@ -383,7 +383,7 @@ class KBHelper:
await self.refresh_kb() await self.refresh_kb()
await self.refresh_document(doc_id) await self.refresh_document(doc_id)
async def refresh_kb(self) -> None: async def refresh_kb(self):
if self.kb: if self.kb:
kb = await self.kb_db.get_kb_by_id(self.kb.kb_id) kb = await self.kb_db.get_kb_by_id(self.kb.kb_id)
if kb: if kb:
+7 -8
View File
@@ -3,7 +3,6 @@ from pathlib import Path
from astrbot.core import logger from astrbot.core import logger
from astrbot.core.provider.manager import ProviderManager from astrbot.core.provider.manager import ProviderManager
from astrbot.core.utils.astrbot_path import get_astrbot_knowledge_base_path
# from .chunking.fixed_size import FixedSizeChunker # from .chunking.fixed_size import FixedSizeChunker
from .chunking.recursive import RecursiveCharacterChunker from .chunking.recursive import RecursiveCharacterChunker
@@ -14,7 +13,7 @@ from .retrieval.manager import RetrievalManager, RetrievalResult
from .retrieval.rank_fusion import RankFusion from .retrieval.rank_fusion import RankFusion
from .retrieval.sparse_retriever import SparseRetriever from .retrieval.sparse_retriever import SparseRetriever
FILES_PATH = get_astrbot_knowledge_base_path() FILES_PATH = "data/knowledge_base"
DB_PATH = Path(FILES_PATH) / "kb.db" DB_PATH = Path(FILES_PATH) / "kb.db"
"""Knowledge Base storage root directory""" """Knowledge Base storage root directory"""
CHUNKER = RecursiveCharacterChunker() CHUNKER = RecursiveCharacterChunker()
@@ -27,14 +26,14 @@ class KnowledgeBaseManager:
def __init__( def __init__(
self, self,
provider_manager: ProviderManager, provider_manager: ProviderManager,
) -> None: ):
DB_PATH.parent.mkdir(parents=True, exist_ok=True) Path(DB_PATH).parent.mkdir(parents=True, exist_ok=True)
self.provider_manager = provider_manager self.provider_manager = provider_manager
self._session_deleted_callback_registered = False self._session_deleted_callback_registered = False
self.kb_insts: dict[str, KBHelper] = {} self.kb_insts: dict[str, KBHelper] = {}
async def initialize(self) -> None: async def initialize(self):
"""初始化知识库模块""" """初始化知识库模块"""
try: try:
logger.info("正在初始化知识库模块...") logger.info("正在初始化知识库模块...")
@@ -59,13 +58,13 @@ class KnowledgeBaseManager:
logger.error(f"知识库模块初始化失败: {e}") logger.error(f"知识库模块初始化失败: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
async def _init_kb_database(self) -> None: async def _init_kb_database(self):
self.kb_db = KBSQLiteDatabase(DB_PATH.as_posix()) self.kb_db = KBSQLiteDatabase(DB_PATH.as_posix())
await self.kb_db.initialize() await self.kb_db.initialize()
await self.kb_db.migrate_to_v1() await self.kb_db.migrate_to_v1()
logger.info(f"KnowledgeBase database initialized: {DB_PATH}") logger.info(f"KnowledgeBase database initialized: {DB_PATH}")
async def load_kbs(self) -> None: async def load_kbs(self):
"""加载所有知识库实例""" """加载所有知识库实例"""
kb_records = await self.kb_db.list_kbs() kb_records = await self.kb_db.list_kbs()
for record in kb_records: for record in kb_records:
@@ -276,7 +275,7 @@ class KnowledgeBaseManager:
return "\n".join(lines) return "\n".join(lines)
async def terminate(self) -> None: async def terminate(self):
"""终止所有知识库实例,关闭数据库连接""" """终止所有知识库实例,关闭数据库连接"""
for kb_id, kb_helper in self.kb_insts.items(): for kb_id, kb_helper in self.kb_insts.items():
try: try:
@@ -6,7 +6,7 @@ import aiohttp
class URLExtractor: class URLExtractor:
"""URL 内容提取器,封装了 Tavily API 调用和密钥管理""" """URL 内容提取器,封装了 Tavily API 调用和密钥管理"""
def __init__(self, tavily_keys: list[str]) -> None: def __init__(self, tavily_keys: list[str]):
""" """
初始化 URL 提取器 初始化 URL 提取器
@@ -44,7 +44,7 @@ class RetrievalManager:
sparse_retriever: SparseRetriever, sparse_retriever: SparseRetriever,
rank_fusion: RankFusion, rank_fusion: RankFusion,
kb_db: KBSQLiteDatabase, kb_db: KBSQLiteDatabase,
) -> None: ):
"""初始化检索管理器 """初始化检索管理器
Args: Args:
@@ -31,7 +31,7 @@ class RankFusion:
- 使用 Reciprocal Rank Fusion (RRF) 算法 - 使用 Reciprocal Rank Fusion (RRF) 算法
""" """
def __init__(self, kb_db: KBSQLiteDatabase, k: int = 60) -> None: def __init__(self, kb_db: KBSQLiteDatabase, k: int = 60):
"""初始化结果融合器 """初始化结果融合器
Args: Args:

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