Compare commits

..

1 Commits

Author SHA1 Message Date
Soulter
54340cca18 stage 2025-10-10 19:41:18 +08:00
1756 changed files with 36953 additions and 359199 deletions

View File

@@ -1,9 +1,9 @@
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# github actions
.git
# github acions
.github/
.*ignore
.git/
# User-specific stuff
.idea/
# Byte-compiled / optimized / DLL files
@@ -15,12 +15,10 @@ env/
venv*/
ENV/
.conda/
README*.md
dashboard/
!astrbot/dashboard/
!astrbot/dashboard/dist/
!astrbot/dashboard/dist/**
data/
changelogs/
tests/
.ruff_cache/
.astrbot
astrbot.lock
.astrbot

View File

@@ -1,184 +0,0 @@
# ==========================================
# AstrBot Instance Configuration: ${INSTANCE_NAME}
# AstrBot 实例配置文件:${INSTANCE_NAME}
# ==========================================
# 将此文件复制为 .env 并根据需要修改。
# Copy this file to .env and modify as needed.
# 注意:在此处设置的变量将覆盖默认配置。
# Note: Variables set here override application defaults.
# ------------------------------------------
# 实例标识 / Instance Identity
# ------------------------------------------
# 实例名称(用于日志和服务名)
# Instance name (used in logs/service names)
INSTANCE_NAME="${INSTANCE_NAME}"
# ------------------------------------------
# 核心配置 / Core Configuration
# ------------------------------------------
# AstrBot 根目录路径
# AstrBot root directory path
# 默认 Default: 当前工作目录,桌面客户端为 ~/.astrbot服务器为 /var/lib/astrbot/<instance>/
# 示例 Example: /var/lib/astrbot/mybot
ASTRBOT_ROOT="${ASTRBOT_ROOT}"
# 日志等级
# Log level
# 可选值 Values: DEBUG, INFO, WARNING, ERROR, CRITICAL
# 默认 Default: INFO
# ASTRBOT_LOG_LEVEL=INFO
# 启用插件热重载(开发时有用)
# Enable plugin hot reload (useful for development)
# 可选值 Values: 0 (禁用 disabled), 1 (启用 enabled)
# 默认 Default: 0
# ASTRBOT_RELOAD=0
# 禁用匿名使用统计
# Disable anonymous usage statistics
# 可选值 Values: 0 (启用统计 enabled), 1 (禁用统计 disabled)
# 默认 Default: 0
ASTRBOT_DISABLE_METRICS=0
# 覆盖 Python 可执行文件路径(用于本地代码执行功能)
# Override Python executable path (for local code execution)
# 示例 Example: /usr/bin/python3, /home/user/.pyenv/shims/python
# PYTHON=/usr/bin/python3
# 启用演示模式(可能限制部分功能)
# Enable demo mode (may restrict certain features)
# 可选值 Values: True, False
# 默认 Default: False
# DEMO_MODE=False
# 启用测试模式(影响日志和部分行为)
# Enable testing mode (affects logging and behavior)
# 可选值 Values: True, False
# 默认 Default: False
# TESTING=False
# 标记:是否通过桌面客户端执行(主要用于内部)
# Flag: running via desktop client (internal use)
# 可选值 Values: 0, 1
# ASTRBOT_DESKTOP_CLIENT=0
# 标记:是否通过 systemd 服务执行
# Flag: running via systemd service
# 可选值 Values: 0, 1
ASTRBOT_SYSTEMD=1
# ------------------------------------------
# 管理面板配置 / Dashboard Configuration
# ------------------------------------------
# 启用或禁用 WebUI 管理面板
# Enable or disable WebUI dashboard
# 可选值 Values: True, False
# 默认 Default: True
ASTRBOT_DASHBOARD_ENABLE=True
# 允许跨域请求的来源域名(多个用逗号分隔,允许所有则用 *
# Allowed CORS origins for WebUI dashboard (comma-separated, or * for all)
# 示例 Example: https://dash.astrbot.men
# 默认 Default: *
# ASTRBOT_CORS_ALLOW_ORIGIN="*"
# ------------------------------------------
# 国际化配置 / Internationalization Configuration
# ------------------------------------------
# CLI 界面语言
# CLI interface language
# 可选值 Values: zh (中文), en (英文)
# 默认 Default: zh (跟随系统 locale / follows system locale)
# ASTRBOT_CLI_LANG=zh
# ------------------------------------------
# 网络配置 / Network Configuration
# ------------------------------------------
# API 绑定主机
# API bind host
# 示例 Example: 0.0.0.0 (所有接口 all interfaces), 127.0.0.1 (仅本地 localhost only)
ASTRBOT_HOST="${ASTRBOT_HOST}"
# API 绑定端口
# API bind port
# 示例 Example: 3000, 6185, 8080
ASTRBOT_PORT="${ASTRBOT_PORT}"
# 是否为 API 启用 SSL/TLS
# Enable SSL/TLS for API
# 可选值 Values: true, false
# 默认 Default: false
ASTRBOT_SSL_ENABLE=false
# SSL 证书路径PEM 格式)
# SSL certificate path (PEM format)
# 示例 Example: /etc/astrbot/certs/myinstance/fullchain.pem
ASTRBOT_SSL_CERT=""
# SSL 私钥路径PEM 格式)
# SSL private key path (PEM format)
# 示例 Example: /etc/astrbot/certs/myinstance/privkey.pem
ASTRBOT_SSL_KEY=""
# SSL CA 证书链路径(可选,用于客户端验证)
# SSL CA certificates bundle (optional, for client verification)
# 示例 Example: /etc/ssl/certs/ca-certificates.crt
ASTRBOT_SSL_CA_CERTS=""
# ------------------------------------------
# 代理配置 / Proxy Configuration
# ------------------------------------------
# HTTP 代理地址
# HTTP proxy URL
# 示例 Example: http://127.0.0.1:7890, socks5://127.0.0.1:1080
# http_proxy=
# HTTPS 代理地址
# HTTPS proxy URL
# 示例 Example: http://127.0.0.1:7890, socks5://127.0.0.1:1080
# https_proxy=
# 不走代理的主机列表(逗号分隔)
# Hosts to bypass proxy (comma-separated)
# 示例 Example: localhost,127.0.0.1,192.168.0.0/16,.local
# no_proxy=localhost,127.0.0.1
# ------------------------------------------
# 第三方集成 / Third-party Integrations
# ------------------------------------------
# 阿里云 DashScope API 密钥(用于 Rerank 服务)
# Alibaba DashScope API Key (for Rerank service)
# 获取地址 Get from: https://dashscope.console.aliyun.com/
# 示例 Example: sk-xxxxxxxxxxxx
# DASHSCOPE_API_KEY=
# Coze 集成
# Coze integration
# 获取地址 Get from: https://www.coze.com/
# COZE_API_KEY=
# COZE_BOT_ID=
# 计算机控制相关的数据目录(用于截图/文件存储)
# Computer control data directory (for screenshots/file storage)
# 示例 Example: /var/lib/astrbot/bay_data
# BAY_DATA_DIR=
# ------------------------------------------
# 平台特定配置 / Platform-specific Configuration
# ------------------------------------------
# QQ 官方机器人测试模式开关
# QQ official bot test mode
# 可选值 Values: on, off
# 默认 Default: off
# TEST_MODE=off
# End of template / 模板结束

2
.envrc
View File

@@ -1,2 +0,0 @@
git pull
git status

View File

@@ -16,7 +16,7 @@ body:
请将插件信息填写到下方的 JSON 代码块中。其中 `tags`(插件标签)和 `social_link`(社交链接)选填。
不熟悉 JSON ?可以从 [此站](https://plugins.astrbot.app) 右下角提交。
不熟悉 JSON 现在可以从 [这里](https://plugins.astrbot.app/#/submit) 获取你的 JSON 啦!获取到了记得复制粘贴过来哦!
- type: textarea
id: plugin-info
@@ -26,13 +26,12 @@ body:
value: |
```json
{
"name": "插件名,请以 astrbot_plugin_ 开头",
"display_name": "用于展示的插件名,方便人类阅读",
"desc": "插件的简短介绍",
"name": "插件名",
"desc": "插件介绍",
"author": "作者名",
"repo": "插件仓库链接",
"tags": [],
"social_link": "",
"social_link": ""
}
```
validations:

View File

@@ -1,44 +1,46 @@
name: '🐛 Report Bug / 报告 Bug'
name: '🐛 报告 Bug'
title: '[Bug]'
description: Submit bug report to help us improve. / 提交报告帮助我们改进。
description: 提交报告帮助我们改进。
labels: [ 'bug' ]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to report this issue! Please describe your problem accurately. If possible, please provide a reproducible snippet (this will help resolve the issue more quickly). Please note that issues that are not detailed or have no logs will be closed immediately. Thank you for your understanding. / 感谢您抽出时间报告问题!请准确解释您的问题。如果可能,请提供一个可复现的片段(这有助于更快地解决问题)。请注意,不详细 / 没有日志的 issue 会被直接关闭,谢谢理解。
感谢您抽出时间报告问题!请准确解释您的问题。如果可能,请提供一个可复现的片段(这有助于更快地解决问题)。
- type: textarea
attributes:
label: What happened / 发生了什么
description: Description
label: 发生了什么
description: 描述你遇到的异常
placeholder: >
Please provide a clear and specific description of what this exception is. Please note that issues that are not detailed or have no logs will be closed immediately. Thank you for your understanding. / 一个清晰且具体的描述这个异常是什么。请注意,不详细 / 没有日志的 issue 会被直接关闭,谢谢理解
一个清晰且具体的描述这个异常是什么
validations:
required: true
- type: textarea
attributes:
label: Reproduce / 如何复现?
label: 如何复现?
description: >
The steps to reproduce the issue. / 复现该问题的步骤
复现该问题的步骤
placeholder: >
Example: 1. Open '...'
: 1. 打开 '...'
validations:
required: true
- type: textarea
attributes:
label: AstrBot version, deployment method (e.g., Windows Docker Desktop deployment), provider used, and messaging platform used. / AstrBot 版本、部署方式(如 Windows Docker Desktop 部署)、使用的提供商、使用的消息平台适配器
label: AstrBot 版本、部署方式(如 Windows Docker Desktop 部署)、使用的提供商、使用的消息平台适配器
description: >
请提供您的 AstrBot 版本和部署方式。
placeholder: >
Example: 4.5.7 Docker, 3.1.7 Windows Launcher
如: 3.1.8 Docker, 3.1.7 Windows启动器
validations:
required: true
- type: dropdown
attributes:
label: OS
label: 操作系统
description: |
On which operating system did you encounter this problem? / 你在哪个操作系统上遇到了这个问题?
你在哪个操作系统上遇到了这个问题?
multiple: false
options:
- 'Windows'
@@ -51,30 +53,30 @@ body:
- type: textarea
attributes:
label: Logs / 报错日志
label: 报错日志
description: >
Please provide complete Debug-level logs, such as error logs and screenshots. Don't worry if they're long! Please note that issues with insufficient details or no logs will be closed immediately. Thank you for your understanding. / 如报错日志、截图等。请提供完整的 Debug 级别的日志,不要介意它很长!请注意,不详细 / 没有日志的 issue 会被直接关闭,谢谢理解。
如报错日志、截图等。请提供完整的 Debug 级别的日志,不要介意它很长!
placeholder: >
Please provide a complete error log or screenshot. / 请提供完整的报错日志或截图。
请提供完整的报错日志或截图。
validations:
required: true
- type: checkboxes
attributes:
label: Are you willing to submit a PR? / 你愿意提交 PR 吗?
label: 你愿意提交 PR 吗?
description: >
This is not required, but we would be happy to provide guidance during the contribution process, especially if you already have a good understanding of how to implement the fix. / 这不是必需的,但我们很乐意在贡献过程中为您提供指导特别是如果你已经很好地理解了如何实现修复。
这不是必需的,但我们很乐意在贡献过程中为您提供指导特别是如果你已经很好地理解了如何实现修复。
options:
- label: Yes!
- label: 是的,我愿意提交 PR!
- type: checkboxes
attributes:
label: Code of Conduct
options:
- 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
- type: markdown
attributes:
value: "Thank you for filling out our form! / 感谢您填写我们的表单!"
value: "感谢您填写我们的表单!"

View File

@@ -1,40 +1,42 @@
name: '🎉 Feature Request / 功能建议'
name: '🎉 功能建议'
title: "[Feature]"
description: Submit a suggestion to help us improve. / 提交建议帮助我们改进。
description: 提交建议帮助我们改进。
labels: [ "enhancement" ]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to suggest a new feature! Please explain your idea clearly and accurately. / 感谢您抽出时间提出新功能建议,请准确解释您的想法。
感谢您抽出时间提出新功能建议,请准确解释您的想法。
- type: textarea
attributes:
label: Description / 描述
description: Please describe the feature you want to be added in detail. / 请详细描述您希望添加的功能。
label: 描述
description: 简短描述您的功能建议
- type: textarea
attributes:
label: Use Case / 使用场景
description: Please describe the use case for this feature. / 请描述这个功能的使用场景。
label: 使用场景
description: 你想要发生什么?
placeholder: >
一个清晰且具体的描述这个功能的使用场景。
- type: checkboxes
attributes:
label: Willing to Submit PR? / 是否愿意提交PR
label: 愿意提交PR吗?
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:
- label: Yes, I am willing to submit a PR. / 是的,我愿意提交 PR
- label: 是的, 我愿意提交PR!
- type: checkboxes
attributes:
label: Code of Conduct
options:
- 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
- type: markdown
attributes:
value: "Thank you for filling out our form!"
value: "感谢您填写我们的表单!"

View File

@@ -1,34 +1,46 @@
<!--Please describe the motivation for this change: What problem does it solve? (e.g., Fixes XX issue, adds YY feature)-->
<!--请描述此项更改的动机:它解决了什么问题?(例如:修复了 XX issue添加了 YY 功能)-->
<!-- 如果有的话,请指定此 PR 旨在解决的 ISSUE 编号。 -->
<!-- If applicable, please specify the ISSUE number this PR aims to resolve. -->
fixes #XYZ
---
### Motivation / 动机
<!--请描述此项更改的动机:它解决了什么问题?(例如:修复了 XX 错误,添加了 YY 功能)-->
<!--Please describe the motivation for this change: What problem does it solve? (e.g., Fixes XX bug, adds YY feature)-->
### Modifications / 改动点
<!--Please summarize your changes: What core files were modified? What functionality was implemented?-->
<!--请总结你的改动:哪些核心文件被修改了?实现了什么功能?-->
<!--Please summarize your changes: What core files were modified? What functionality was implemented?-->
- [x] This is NOT a breaking change. / 这不是一个破坏性变更。
<!-- If your changes is a breaking change, please uncheck the checkbox above -->
### Verification Steps / 验证步骤
<!--请为审查者 (Reviewer) 提供清晰、可复现的验证步骤例如1. 导航到... 2. 点击...)。-->
<!--Please provide clear and reproducible verification steps for the Reviewer (e.g., 1. Navigate to... 2. Click...).-->
### Screenshots or Test Results / 运行截图或测试结果
<!--Please paste screenshots, GIFs, or test logs here as evidence of executing the "Verification Steps" to prove this change is effective.-->
<!--请粘贴截图、GIF 或测试日志,作为执行“验证步骤”的证据,证明此改动有效。-->
<!--Please paste screenshots, GIFs, or test logs here as evidence of executing the "Verification Steps" to prove this change is effective.-->
### Compatibility & Breaking Changes / 兼容性与破坏性变更
<!--请说明此变更的兼容性:哪些是破坏性变更?哪些地方做了向后兼容处理?是否提供了数据迁移方法?-->
<!--Please explain the compatibility of this change: What are the breaking changes? What backward-compatible measures were taken? Are data migration paths provided?-->
- [ ] 这是一个破坏性变更 (Breaking Change)。/ This is a breaking change.
- [ ] 这不是一个破坏性变更。/ This is NOT a breaking change.
---
### Checklist / 检查清单
<!--If merged, your code will serve tens of thousands of users! Please double-check the following items before submitting.-->
<!--如果分支被合并,您的代码将服务于数万名用户!在提交前,请核查一下几点内容。-->
<!--If merged, your code will serve tens of thousands of users! Please double-check the following items before submitting.-->
- [ ] 😊 If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
/ 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。
- [ ] 👀 My changes have been well-tested, **and "Verification Steps" and "Screenshots" have been provided above**.
/ 我的更改经过了良好的测试,**并已在上方提供了“验证步骤”和“运行截图”**。
- [ ] 🤓 I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in `requirements.txt` and `pyproject.toml`.
/ 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到 `requirements.txt``pyproject.toml` 文件相应位置。
- [ ] 😮 My changes do not introduce malicious code.
/ 我的更改没有引入恶意代码。
- [ ] 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
- [ ] 👀 我的更改经过了良好的测试,**并已在上方提供了“验证步骤”和“运行截图”**。/ My changes have been well-tested, **and "Verification Steps" and "Screenshots" have been provided above**.
- [ ] 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 `requirements.txt``pyproject.toml` 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in `requirements.txt` and `pyproject.toml`.
- [ ] 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

View File

@@ -11,8 +11,6 @@ reviewers:
- Larch-C
- anka-afk
- advent259141
- Fridemn
- LIghtJUNction
# - zouyonghe
# A number of reviewers added to the pull request

View File

@@ -15,6 +15,7 @@ Always reference these instructions first and fallback to search or bash command
### Running the Application
- Run main application: `uv run main.py` -- starts in ~3 seconds
- Application creates WebUI on http://localhost:6185 (default credentials: `astrbot`/`astrbot`)
- Application loads plugins automatically from `packages/` and `data/plugins/` directories
### Dashboard Build (Vue.js/Node.js)
- **Prerequisites**: Node.js 20+ and npm 10+ required
@@ -34,7 +35,7 @@ Always reference these instructions first and fallback to search or bash command
- **ALWAYS** run `uv run ruff check .` and `uv run ruff format .` before committing changes
### Plugin Development
- Plugins load from `astrbot/builtin_stars/` (built-in) and `data/plugins/` (user-installed)
- Plugins load from `packages/` (built-in) and `data/plugins/` (user-installed)
- Plugin system supports function tools and message handlers
- Key plugins: python_interpreter, web_searcher, astrbot, reminder, session_controller

92
.github/workflows/auto_release.yml vendored Normal file
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@v5
- 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@v5
- 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

View File

@@ -1,49 +0,0 @@
name: Build and Deploy AstrBot Docs
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest # 运行环境
steps:
- name: checkout
uses: actions/checkout@v7
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
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: docs/pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
working-directory: './docs'
- name: Build docs
run: pnpm run docs:build
working-directory: './docs'
- name: scp
uses: appleboy/scp-action@v1.0.0
with:
host: ${{ secrets.HOST_NEKO }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORDNEKO }}
source: 'docs/.vitepress/dist/*'
target: '/tmp/'
- name: script
uses: appleboy/ssh-action@v1.2.5
with:
host: ${{ secrets.HOST_NEKO }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORDNEKO }}
script: |
mkdir -p /root/docker_data/caddy/caddy_data/static_site/abv4/
rm -rf /root/docker_data/caddy/caddy_data/static_site/abv4/*
mv /tmp/docs/.vitepress/dist/* /root/docker_data/caddy/caddy_data/static_site/abv4/
rm -rf /tmp/docs/

View File

@@ -12,12 +12,12 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v7
uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
python-version: '3.10'
- name: Install UV
run: pip install uv

View File

@@ -56,11 +56,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -88,6 +88,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
uses: actions/checkout@v5
with:
fetch-depth: 0
@@ -37,10 +37,9 @@ jobs:
mkdir -p data/temp
export TESTING=true
export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }}
pytest --cov=astrbot -v -o log_cli=true -o log_level=DEBUG
pytest --cov=. -v -o log_cli=true -o log_level=DEBUG
- name: Upload results to Codecov
if: github.repository == 'AstrBotDevs/AstrBot'
uses: codecov/codecov-action@v7
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}

View File

@@ -8,30 +8,16 @@ on:
jobs:
build:
if: github.repository == 'AstrBotDevs/AstrBot'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v5
- name: Setup pnpm
uses: pnpm/action-setup@v6.0.9
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: Install and Build
working-directory: dashboard
- name: npm install, build
run: |
pnpm install --frozen-lockfile
pnpm lint:check
pnpm run build
cd dashboard
npm install
npm run build
- name: Inject Commit SHA
id: get_sha
@@ -43,7 +29,7 @@ jobs:
zip -r dist.zip dist
- name: Archive production artifacts
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: dist-without-markdown
path: |
@@ -52,11 +38,11 @@ jobs:
- name: Create GitHub Release
if: github.event_name == 'push'
uses: ncipollo/release-action@v1.21.0
uses: ncipollo/release-action@v1
with:
tag: release-${{ github.sha }}
owner: AstrBotDevs
repo: astrbot-release-harbour
body: "Automated release from commit ${{ github.sha }}"
token: ${{ secrets.ASTRBOT_HARBOUR_TOKEN }}
artifacts: "dashboard/dist.zip"
artifacts: "dashboard/dist.zip"

View File

@@ -3,132 +3,18 @@ name: Docker Image CI/CD
on:
push:
tags:
- "v*"
schedule:
# Run at 00:00 UTC every day
- cron: "0 0 * * *"
- 'v*'
workflow_dispatch:
jobs:
build-nightly-image:
if: github.repository == 'AstrBotDevs/AstrBot' && github.event_name == 'schedule'
publish-docker:
runs-on: ubuntu-latest
env:
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
GHCR_OWNER: astrbotdevs
HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }}
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Pull The Codes
uses: actions/checkout@v5
with:
fetch-depth: 1
fetch-tag: true
- name: Check for new commits today
if: github.event_name == 'schedule'
id: check-commits
run: |
# Get commits from the last 24 hours
commits=$(git log --since="24 hours ago" --oneline)
if [ -z "$commits" ]; then
echo "No commits in the last 24 hours, skipping build"
echo "has_commits=false" >> $GITHUB_OUTPUT
else
echo "Found commits in the last 24 hours:"
echo "$commits"
echo "has_commits=true" >> $GITHUB_OUTPUT
fi
- name: Exit if no commits
if: github.event_name == 'schedule' && steps.check-commits.outputs.has_commits == 'false'
run: exit 0
- name: Build Dashboard
run: |
dashboard_version=$(python3 - <<'PY'
import tomllib
with open("pyproject.toml", "rb") as f:
print("v" + tomllib.load(f)["project"]["version"])
PY
)
cd dashboard
npm install
npm run build
mkdir -p dist/assets
echo "$dashboard_version" > dist/assets/version
cd ..
mkdir -p astrbot/dashboard
rm -rf astrbot/dashboard/dist
cp -r dashboard/dist astrbot/dashboard/dist
- name: Determine test image tags
id: test-meta
run: |
short_sha=$(echo "${GITHUB_SHA}" | cut -c1-12)
build_date=$(date +%Y%m%d)
echo "short_sha=$short_sha" >> $GITHUB_OUTPUT
echo "build_date=$build_date" >> $GITHUB_OUTPUT
- name: Set QEMU
uses: docker/setup-qemu-action@v4.1.0
- name: Set Docker Buildx
uses: docker/setup-buildx-action@v4.1.0
- name: Log in to DockerHub
uses: docker/login-action@v4.2.0
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Login to GitHub Container Registry
if: env.HAS_GHCR_TOKEN == 'true'
uses: docker/login-action@v4.2.0
with:
registry: ghcr.io
username: ${{ env.GHCR_OWNER }}
password: ${{ secrets.GHCR_GITHUB_TOKEN }}
- name: Build nightly image tags list
id: test-tags
run: |
TAGS="${{ env.DOCKER_HUB_USERNAME }}/astrbot:nightly-latest
${{ env.DOCKER_HUB_USERNAME }}/astrbot:nightly-${{ steps.test-meta.outputs.build_date }}-${{ steps.test-meta.outputs.short_sha }}"
if [ "${{ env.HAS_GHCR_TOKEN }}" = "true" ]; then
TAGS="$TAGS
ghcr.io/${{ env.GHCR_OWNER }}/astrbot:nightly-latest
ghcr.io/${{ env.GHCR_OWNER }}/astrbot:nightly-${{ steps.test-meta.outputs.build_date }}-${{ steps.test-meta.outputs.short_sha }}"
fi
echo "tags<<EOF" >> $GITHUB_OUTPUT
echo "$TAGS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Build and Push Nightly Image
uses: docker/build-push-action@v7.2.0
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.test-tags.outputs.tags }}
- name: Post build notifications
run: echo "Test Docker image has been built and pushed successfully"
build-release-image:
if: github.repository == 'AstrBotDevs/AstrBot' && (github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')))
runs-on: ubuntu-latest
env:
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
GHCR_OWNER: astrbotdevs
HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 1
fetch-tag: true
fetch-depth: 0 # Must be 0 so we can fetch tags
- name: Get latest tag (only on manual trigger)
id: get-latest-tag
@@ -141,22 +27,21 @@ jobs:
if: github.event_name == 'workflow_dispatch'
run: git checkout ${{ steps.get-latest-tag.outputs.latest_tag }}
- name: Compute release metadata
id: release-meta
- name: Check if version is pre-release
id: check-prerelease
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
version="${{ steps.get-latest-tag.outputs.latest_tag }}"
else
version="${GITHUB_REF#refs/tags/}"
version="${{ github.ref_name }}"
fi
if [[ "$version" == *"beta"* ]] || [[ "$version" == *"alpha"* ]]; then
echo "is_prerelease=true" >> $GITHUB_OUTPUT
echo "Version $version marked as pre-release"
echo "Version $version is a pre-release, will not push latest tag"
else
echo "is_prerelease=false" >> $GITHUB_OUTPUT
echo "Version $version marked as stable"
echo "Version $version is a stable release, will push latest tag"
fi
echo "version=$version" >> $GITHUB_OUTPUT
- name: Build Dashboard
run: |
@@ -164,43 +49,41 @@ jobs:
npm install
npm run build
mkdir -p dist/assets
echo "${{ steps.release-meta.outputs.version }}" > dist/assets/version
echo $(git rev-parse HEAD) > dist/assets/version
cd ..
mkdir -p astrbot/dashboard
rm -rf astrbot/dashboard/dist
cp -r dashboard/dist astrbot/dashboard/dist
mkdir -p data
cp -r dashboard/dist data/
- name: Set QEMU
uses: docker/setup-qemu-action@v4.1.0
uses: docker/setup-qemu-action@v3
- name: Set Docker Buildx
uses: docker/setup-buildx-action@v4.1.0
uses: docker/setup-buildx-action@v3
- name: Log in to DockerHub
uses: docker/login-action@v4.2.0
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Login to GitHub Container Registry
if: env.HAS_GHCR_TOKEN == 'true'
uses: docker/login-action@v4.2.0
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ env.GHCR_OWNER }}
username: Soulter
password: ${{ secrets.GHCR_GITHUB_TOKEN }}
- name: Build and Push Release Image
uses: docker/build-push-action@v7.2.0
- name: Build and Push Docker to DockerHub and Github GHCR
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: |
${{ steps.release-meta.outputs.is_prerelease == 'false' && format('{0}/astrbot:latest', env.DOCKER_HUB_USERNAME) || '' }}
${{ steps.release-meta.outputs.is_prerelease == 'false' && env.HAS_GHCR_TOKEN == 'true' && format('ghcr.io/{0}/astrbot:latest', env.GHCR_OWNER) || '' }}
${{ format('{0}/astrbot:{1}', env.DOCKER_HUB_USERNAME, steps.release-meta.outputs.version) }}
${{ env.HAS_GHCR_TOKEN == 'true' && format('ghcr.io/{0}/astrbot:{1}', env.GHCR_OWNER, steps.release-meta.outputs.version) || '' }}
${{ steps.check-prerelease.outputs.is_prerelease == 'false' && format('{0}/astrbot:latest', secrets.DOCKER_HUB_USERNAME) || '' }}
${{ secrets.DOCKER_HUB_USERNAME }}/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }}
${{ steps.check-prerelease.outputs.is_prerelease == 'false' && 'ghcr.io/soulter/astrbot:latest' || '' }}
ghcr.io/soulter/astrbot:${{ github.event_name == 'workflow_dispatch' && steps.get-latest-tag.outputs.latest_tag || github.ref_name }}
- name: Post build notifications
run: echo "Release Docker image has been built and pushed successfully"
run: echo "Docker image has been built and pushed successfully"

View File

@@ -1,264 +0,0 @@
name: Release AstrBot
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
if: github.repository == 'AstrBotDevs/AstrBot'
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@v7
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@v6.0.9
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
working-directory: dashboard
run: |
pnpm install --frozen-lockfile
pnpm run build
echo "${{ steps.tag.outputs.tag }}" > dist/assets/version
zip -r "AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip" dist
- name: Build core package
shell: bash
run: |
git archive \
--format=zip \
--prefix="AstrBot-${{ steps.tag.outputs.tag }}/" \
--output="AstrBot-${{ steps.tag.outputs.tag }}-core.zip" \
HEAD
- name: Upload dashboard artifact
uses: actions/upload-artifact@v7
with:
name: Dashboard-${{ steps.tag.outputs.tag }}
if-no-files-found: error
path: dashboard/AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip
- name: Upload release packages to Cloudflare R2
if: ${{ env.R2_ACCOUNT_ID != '' && env.R2_ACCESS_KEY_ID != '' && env.R2_SECRET_ACCESS_KEY != '' }}
env:
R2_BUCKET_NAME: "astrbot"
DASHBOARD_LATEST_OBJECT_NAME: "astrbot-webui-latest.zip"
CORE_LATEST_OBJECT_NAME: "astrbot-core-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/${DASHBOARD_LATEST_OBJECT_NAME}"
rclone copy "dashboard/${DASHBOARD_LATEST_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
cp "AstrBot-${VERSION_TAG}-core.zip" "${CORE_LATEST_OBJECT_NAME}"
rclone copy "${CORE_LATEST_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
cp "AstrBot-${VERSION_TAG}-core.zip" "astrbot-core-${VERSION_TAG}.zip"
rclone copy "astrbot-core-${VERSION_TAG}.zip" "r2:${R2_BUCKET_NAME}" --progress
rclone copyto "AstrBot-${VERSION_TAG}-core.zip" "r2:${R2_BUCKET_NAME}/astrbot-core/${VERSION_TAG}/source.zip" --progress
rclone copyto "AstrBot-${VERSION_TAG}-core.zip" "r2:${R2_BUCKET_NAME}/download/astrbot-core/${VERSION_TAG}/source.zip" --progress
publish-release:
name: Publish GitHub Release
if: github.repository == 'AstrBotDevs/AstrBot'
runs-on: ubuntu-24.04
needs:
- build-dashboard
steps:
- name: Checkout repository
uses: actions/checkout@v7
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@v8
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
if: github.repository == 'AstrBotDevs/AstrBot'
runs-on: ubuntu-24.04
needs:
- publish-release
steps:
- name: Checkout repository
uses: actions/checkout@v7
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@v8
with:
name: Dashboard-${{ steps.tag.outputs.tag }}
path: dashboard-artifact
- name: Unpack dashboard dist into package tree
shell: bash
run: |
mkdir -p astrbot/dashboard/dist
unzip -q "dashboard-artifact/AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip" -d dashboard-artifact/unpacked
cp -r dashboard-artifact/unpacked/dist/. astrbot/dashboard/dist/
- 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
# Dashboard assets are already in astrbot/dashboard/dist/;
# ASTRBOT_BUILD_DASHBOARD is intentionally unset so the hatch hook skips npm.
run: uv build
- name: Publish to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
shell: bash
run: uv publish

View File

@@ -1,61 +0,0 @@
name: Smoke Test
on:
push:
branches:
- master
paths-ignore:
- "README*.md"
- "changelogs/**"
- "dashboard/**"
pull_request:
workflow_dispatch:
jobs:
smoke-test:
name: Run smoke tests
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Install UV package manager
run: |
pip install uv
- name: Install dependencies
run: |
uv sync
timeout-minutes: 15
- name: Run smoke tests
run: |
uv run main.py &
# uv tool install -e . --force
# astrbot init -y
# astrbot run --backend-only &
APP_PID=$!
echo "Waiting for application to start..."
for i in {1..60}; do
if curl -f http://localhost:6185 > /dev/null 2>&1; then
echo "Application started successfully!"
kill $APP_PID
exit 0
fi
sleep 1
done
echo "Application failed to start within 30 seconds"
kill $APP_PID 2>/dev/null || true
exit 1
timeout-minutes: 2

View File

@@ -1,65 +1,27 @@
# 本工作流用于标记并关闭长期不活跃的 Issue。
# 目前仅针对带 `bug` 标签的 Issue 生效,不会处理 PR。
# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
#
# 文档: https://github.com/actions/stale
name: Mark stale bug issues
# You can adjust the behavior by modifying this file.
# For more information, see:
# https://github.com/actions/stale
name: Mark stale issues and pull requests
on:
schedule:
# 每天 UTC 08:30 执行 (北京时间 16:30)
- cron: '30 8 * * *'
workflow_dispatch:
inputs:
dry-run:
description: '仅预览, 不实际执行 (Dry run mode)'
required: false
default: true
type: boolean
- cron: '21 23 * * *'
jobs:
stale:
if: github.repository == 'AstrBotDevs/AstrBot'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
operations-per-run: 200
# 只处理带 bug 标签的 Issue
any-of-labels: 'bug'
# 不处理 PR
days-before-pr-stale: -1
days-before-pr-close: -1
# 不活跃判定与关闭策略: 先标记 stale, 再延迟关闭
days-before-issue-stale: 60
days-before-issue-close: 30
stale-issue-label: 'stale'
stale-issue-message: |
This issue has been automatically marked as **stale** because it has not had any activity.
It will be closed in a certain period of time if no further activity occurs.
If this issue is still relevant, please leave a comment.
---
该 Issue 已较长时间无活动, 已被标记为 `stale`。
如无后续活动, 将在一段时间后自动关闭。
如仍需跟进, 请回复评论。
close-issue-message: |
This issue has been automatically closed due to inactivity.
If the problem still exists, feel free to reopen or create a new issue with updated information.
---
该 Issue 因长期无活动已自动关闭。
如问题仍存在, 欢迎补充复现信息并重新打开或新建 Issue。
remove-stale-when-updated: true
debug-only: ${{ github.event_name == 'workflow_dispatch' && inputs.dry-run }}
- uses: actions/stale@v10
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'Stale issue message'
stale-pr-message: 'Stale pull request message'
stale-issue-label: 'no-issue-activity'
stale-pr-label: 'no-pr-activity'

View File

@@ -1,69 +0,0 @@
name: Sync AstrBot Docs to GitHub Wiki
on:
workflow_dispatch:
push:
branches:
- master
paths:
- '.github/workflows/sync-wiki.yml'
- 'docs/scripts/sync_docs_to_wiki.py'
- 'docs/tests/test_sync_docs_to_wiki.py'
- 'docs/zh/**'
- 'docs/en/**'
concurrency:
group: sync-wiki-${{ github.ref }}
cancel-in-progress: true
jobs:
sync:
if: github.repository == 'AstrBotDevs/AstrBot'
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Validate manual ref
if: github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/master'
run: |
echo "This workflow only publishes from refs/heads/master. Re-run it from the master branch."
exit 1
- name: Check out docs repository
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Run sync unit tests
working-directory: docs
run: python -m unittest discover -s tests -p 'test_sync_docs_to_wiki.py' -v
- name: Validate internal doc links
run: python docs/scripts/sync_docs_to_wiki.py --source-root docs --check-links-only
- name: Clone AstrBot wiki
env:
WIKI_TOKEN: ${{ secrets.ASTRBOT_WIKI_TOKEN }}
run: |
test -n "$WIKI_TOKEN"
git clone "https://x-access-token:${WIKI_TOKEN}@github.com/AstrBotDevs/AstrBot.wiki.git" wiki
- name: Generate wiki pages
run: python docs/scripts/sync_docs_to_wiki.py --source-root docs --wiki-root wiki
- name: Commit and push wiki changes
working-directory: wiki
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add .
if git diff --cached --quiet; then
echo "No wiki changes to push"
exit 0
fi
git commit -m "docs: sync wiki from AstrBot-1/docs"
git push

View File

@@ -1,37 +0,0 @@
name: Unit Tests
on:
push:
branches:
- master
paths-ignore:
- 'README*.md'
- 'changelogs/**'
- 'dashboard/**'
pull_request:
workflow_dispatch:
jobs:
unit-tests:
name: Run pytest suite
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@v7
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install uv
run: |
python -m pip install --upgrade pip
python -m pip install uv
- name: Run tests
run: |
chmod +x scripts/run_pytests_ci.sh
bash ./scripts/run_pytests_ci.sh ./tests

100
.gitignore vendored
View File

@@ -1,95 +1,35 @@
# Python related
__pycache__
.venv*
.conda/
uv.lock
.coverage
# IDE and editors
.vscode
.idea
# Logs and temporary files
botpy.log
logs/
temp
cookies.json
# Data files
.vscode
.venv*
.idea
data_v2.db
data_v3.db
data
configs/session
configs/config.yaml
**/.DS_Store
temp
cmd_config.json
# Plugins
data
cookies.json
logs/
addons/plugins
astrbot/builtin_stars/python_interpreter/workplace
tests/astrbot_plugin_openai
.coverage
# Dashboard
tests/astrbot_plugin_openai
chroma
dashboard/node_modules/
dashboard/dist/
.pnpm-store/
package-lock.json
yarn.lock
# Bundled dashboard dist (generated by hatch_build.py during pip wheel build)
astrbot/dashboard/dist/
# Operating System
**/.DS_Store
.DS_Store
# AstrBot specific
.astrbot
astrbot.lock
# Other
chroma
package-lock.json
package.json
venv/*
packages/python_interpreter/workplace
.venv/*
.conda/
.idea
pytest.ini
IFLOW.md
CLAUDE.md
.astrbot
# genie_tts data
CharacterModels/
GenieData/
.agent/
.codex/
.claude/
.opencode/
.kilocode/
.serena
.worktrees/
.astrbot_sdk_testing/
.env
dashboard/warker.js
dashboard/bun.lock
.pua/
# Rust build artifacts
rust/target/
# Build outputs
dist/
*.whl
# 拓展模块
*.so
*.dll
# MDI font subset (generated by dashboard/scripts/subset-mdi-font.mjs)
dashboard/src/assets/mdi-subset/*.woff
dashboard/src/assets/mdi-subset/*.woff2
.planning
*cache
node_modules
*pinokio*
dashboard/pnpm-lock.yaml
.obsidian
dashboard/.codex
.codex
.zed/settings.json
uv.lock

View File

@@ -7,19 +7,7 @@ ci:
autoupdate_commit_msg: ":balloon: pre-commit autoupdate"
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.7
rev: v0.11.2
hooks:
# Run the linter.
- id: ruff-check
types_or: [python, pyi]
args: [--fix]
# Run the formatter.
- id: ruff
- id: ruff-format
types_or: [python, pyi]
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
hooks:
- id: pyupgrade
args: [--py312-plus]

View File

@@ -1 +1 @@
3.12
3.10

257
AGENTS.md
View File

@@ -1,257 +0,0 @@
## Setup commands
### Core
```
uv tool install -e . --force
astrbot init
astrbot run # start the bot
astrbot run --backend-only # start the backend only
```
Exposed an API server on `http://localhost:6185` by default.
### Dashboard(WebUI)
```
cd dashboard
bun install # First time only.
bun dev
```
Runs on `http://localhost:3000` by default.
## Pre-commit setup
AstrBot uses [pre-commit](https://pre-commit.com/) hooks to automatically format and lint Python code before each commit. The hooks run `ruff check`, `ruff format`, and `pyupgrade` (see [`.pre-commit-config.yaml`](.pre-commit-config.yaml) for details).
To set it up:
```bash
pip install pre-commit
pre-commit install
```
After installation, the hooks will run automatically on `git commit`. You can also run them manually at any time:
```bash
ruff format .
ruff check .
```
> **Note:** If you use VSCode, install the `Ruff` extension for real-time formatting and linting in the editor.
## Dev environment tips
- **Main entry**: `astrbot/__main__.py` or via CLI `astrbot run`
- **CLI commands**: `astrbot/cli/commands/`
- **Core modules**: `astrbot/core/`
- **Platform adapters**: `astrbot/core/platform/sources/`
- **Star plugins**: `astrbot/builtin_stars/`
- **Dashboard**: `dashboard/` (Vue.js frontend)
## File Organization
```
astrbot/
├── __main__.py # Main entry point
├── __init__.py # Package init, exports
├── cli/ # CLI commands
│ └── commands/ # Individual command modules
├── core/ # Core functionality
│ ├── agent/ # Agent execution
│ ├── platform/ # Platform adapters
│ ├── pipeline/ # Message processing
│ ├── star/ # Plugin system
│ └── config/ # Configuration
├── builtin_stars/ # Built-in plugins
├── dashboard/ # Vue.js frontend
└── utils/ # Utilities
```
## Architecture
### Core Components
- `astrbot/core/` - Core bot functionality
- `astrbot/core/platform/` - Platform adapter system
- `astrbot/core/agent/` - Agent execution logic
- `astrbot/core/star/` - Plugin/Star handler system
- `astrbot/core/pipeline/` - Message processing pipeline
- `astrbot/cli/` - Command-line interface
### Important Utilities
```python
from astrbot.core.utils.astrbot_path import (
get_astrbot_root, # AstrBot root directory
get_astrbot_data_path, # Data directory
get_astrbot_config_path, # Config directory
get_astrbot_plugin_path, # Plugin directory
get_astrbot_temp_path, # Temp directory
get_astrbot_skills_path, # Skills directory
)
```
### Platform Adapters
Platform adapters are in `astrbot/core/platform/sources/`:
- Each adapter extends base platform classes
- Use `@register_platform_adapter` decorator
- Events flow through `commit_event()` to message queue
### Star (Plugin) System
Stars are plugins in `astrbot/builtin_stars/`:
- Extend `Star` base class
- Use decorators for command handlers: `@star.on_command`, `@star.on_message`, etc.
- Access via `context` object
## Code Style
1. **Type hints required** - Use Python 3.12+ syntax:
- `list[str]` not `List[str]`
- `int | None` not `Optional[int]`
- Avoid `Any` when possible. Use proper `TypedDict`, `dataclass`, or `Protocol` instead.
- When encountering dict access issues (e.g., `msg.get("key")` where type inference is wrong), define a `TypedDict` with `total=False` to explicitly declare allowed keys.
Good example:
```python
class MessageComponent(TypedDict, total=False):
type: str
text: str
path: str
```
Bad example (avoid):
```python
msg: Any = something
msg = cast(dict, msg)
```
2. **Path handling** - Always use `pathlib.Path`:
```python
from pathlib import Path
# Use astrbot.core.utils.path_utils for data/temp directories
from astrbot.core.utils.path_utils import get_astrbot_data_path
```
3. **Formatting** - Run before committing:
```bash
ruff format .
ruff check .
```
4. **Comments** - Use English for all comments and docstrings
5. **Imports** - Use absolute imports via `astrbot.` prefix
### Environment Variables
When adding new environment variables:
1. Use `ASTRBOT_` prefix: `ASTRBOT_ENABLE_FEATURE`
2. Add to `.env.example` with description
3. Update `astrbot/cli/commands/cmd_run.py`:
- Add to module docstring under "Environment Variables Used in Project"
- Add to `keys_to_print` list for debug output
## Testing
1. Tests go in `tests/` directory
2. Use `pytest` with `pytest-asyncio`
3. Run: `uv sync --group dev && uv run pytest --cov=astrbot tests/`
4. Test files: `test_*.py` or `*_test.py`
### Code Quality Scoring Test
The project enforces a **code quality score** via `tests/test_code_quality_typing.py`. All agents must treat this as a hard constraint when modifying code.
**Run the test:**
```bash
uv run pytest tests/test_code_quality_typing.py -v
```
**Scoring rules (target: 100/100, threshold for PASS: 80/100):**
| Pattern | Cost |
|---------|------|
| `cast(Any, ...)` | -1 pt each |
| `# type: ignore` | -0.5 pt each |
| **BAD** `# type: ignore[...]` (unresolved-import, class-alias, no-name-module, attr-defined, etc.) | **-3 pt each** |
| `bare except:` (no exception type) | -0.5 pt each |
| Duplicate code block (5+ identical lines, ≥2 occurrences) | -2 pt each |
**Why bad type: ignore is heavily penalized:**
- `# type: ignore[unresolved-import]` — hides missing module/stub issues
- `# type: ignore[class-alias]` — hides improper type alias patterns
- `# type: ignore[attr-defined]` — hides missing attribute errors
- These are **workarounds, not fixes** — they paper over real type errors
**Scoring formula:**
```
score = max(0, 100 - cast_any - type_ignore*0.5 - bad_type_ignore*3 - bare_except*0.5 - dup_blocks*2)
```
**Agent rules when modifying code:**
1. **Do not add** `# type: ignore[unresolved-import]` or `# type: ignore[class-alias]` — fix the underlying issue instead
2. **Do not use** `cast(Any, ...)` to suppress type errors — use proper type annotations
3. **Do not add** bare `except:` clauses — use `except SomeSpecificException:`
4. **Do not copy-paste** 5+ line blocks — extract to a shared helper function
5. Before committing, run the scoring test and ensure score ≥ 80
## Git Conventions
### Commit Messages
Use conventional commits:
```
feat: add new feature
fix: resolve bug
docs: update documentation
refactor: restructure code
test: add tests
chore: maintenance tasks
```
### PR Guidelines
1. Title: conventional commit format
2. Description: English
3. Target branch: `dev`
4. Keep changes focused and atomic
## Project-Specific Guidelines
1. **No report files** - Do not add `xxx_SUMMARY.md` or similar
2. **Componentization** - Maintain clean code, avoid duplication in WebUI
3. **Backward compatibility** - When deprecating, add warnings
4. **CLI help** - Run `astrbot help --all` to see all commands
5. When modifying frontend/dashboard code, use the project's custom request module `@/utils/request` for HTTP calls
6. For fetch or SSE URLs, use `resolveApiUrl('/api/your-path')` so the configured `VITE_API_BASE` and dev proxy rules are respected
7. Do not import the plain `axios` package directly in dashboard source files
## Common Tasks
### Adding a new platform adapter
1. Create adapter in `astrbot/core/platform/sources/`
2. Extend `Platform` base class
3. Use `@register_platform_adapter` decorator
4. Implement required methods: `run()`, `convert_message()`, `meta()`
### Adding a new command
1. Add to appropriate module in `cli/commands/`
2. Register with `@click.command()`
3. Update `astrbot/cli/__main__.py` to add command
### Adding a new Star handler
1. Create in `astrbot/builtin_stars/` or as plugin
2. Extend `Star` class
3. Use decorators: `@star.on_command()`, `@star.on_schedule()`, etc.
## Release versions
1. Replace current version name to specific version name.
2. Write changelog in `changelogs/`, you can refer to the full commit messages between the latest tag to the latest commit.
3. Make and push a commit into master branch with message format like: `chore: bump version to 4.25.0`
4. Create a tag and push the tag. For example: `git tag v4.25.0 && git push origin v4.25.0`

View File

@@ -1,142 +0,0 @@
# CONTRIBUTING
## 贡献指南
首先,感谢您花时间做出贡献!❤️
所有类型的贡献都受到鼓励和重视。有关不同的帮助方式和处理方式的详细信息,请参阅[目录](#目录)。在做出贡献之前,请确保阅读相关部分。这将使我们维护人员的工作变得更加容易,并为所有参与者带来顺畅的体验。社区期待您的贡献。🎉
### 目录
- [报告问题](#报告问题)
- [提交代码更改](#提交代码更改)
### 报告问题
如果您在使用 AstrBot 时遇到任何问题,请按照以下步骤报告:
1. **检查现有问题**:在提交新问题之前,请先检查 [Issues](https://github.com/AstrBotDevs/AstrBot/issues) 中是否已经存在类似的问题。
2. **创建新问题**:如果没有类似的问题,请创建一个新问题。请确保提供以下信息:
- 问题的简要描述
- 重现问题的步骤
- 预期结果和实际结果
- 相关日志或错误消息
### 提交代码更改
#### 分支命名
我们使用 `fix/` 前缀来修复错误,使用 `feat/` 前缀来添加新功能。对于 `fix/` 分支,请使用简短的描述,或者直接使用 Issue 编号。例如:`fix/1234` 或者 `fix/1234-login-typo`。对于 `feat/` 分支,请使用简短的描述,例如:`feat/add-user-profile`
#### PR 描述
- 请使用英文描述您的 PR。
- 标题请使用 `fix: `, `feat: `, `docs: `, `style: `, `refactor: `, `test: `, `chore: ` 等语义化前缀,并简要描述更改内容。如:`fix: correct login page typo`
#### 代码规范
##### Core
我们使用 Ruff 作为代码格式化和静态分析工具。在提交代码之前,请运行以下命令以确保代码符合规范:
```bash
ruff format .
ruff check .
```
如果您使用 VSCode可以安装 `Ruff` 插件。
##### PR 功能完整性验证(推荐)
如果您希望在本地做一套接近 CI 的完整验证,可使用:
```bash
make pr-test-neo
```
该命令会执行:
- `uv sync --group dev`
- `ruff format --check .``ruff check .`
- Neo 相关关键测试
- `main.py` 启动 smoke test检测 `http://localhost:6185`
需要全量验证时可使用:
```bash
make pr-test-full
```
如果只想快速重复执行(跳过依赖同步和 dashboard 构建):
```bash
make pr-test-full-fast
```
## Contributing Guide
First off, thanks for taking the time to contribute! ❤️
All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉
### Table of Contents
- [Reporting Issues](#reporting-issues)
- [Pull Requests](#pull-requests)
### Reporting Issues
If you encounter any issues while using AstrBot, please follow these steps to report them:
1. **Check Existing Issues**: Before submitting a new issue, please check if a similar issue already exists in the [Issues](https://github.com/AstrBotDevs/AstrBot/issues) section of the repository.
2. **Create a New Issue**: If no similar issue exists, please create a new issue. Make sure to provide the following information:
- A brief description of the issue
- Steps to reproduce the issue
- Expected and actual results
- Relevant logs or error messages
### Pull Requests
#### Branch Naming
We use the `fix/` prefix for bug fixes and the `feat/` prefix for new features. For `fix/` branches, please use a short description or directly use the Issue number, e.g., `fix/1234` or `fix/1234-login-typo`. For `feat/` branches, please use a short description, e.g., `feat/add-user-profile`.
#### PR Description
- Please use English to describe your PR.
- Use semantic prefixes like `fix: `, `feat: `, `docs: `, `style: `, `refactor: `, `test: `, `chore: ` in the title, followed by a brief description of the changes, e.g., `fix: correct login page typo`.
#### Code Style
##### Core
We use Ruff as our code formatter and static analysis tool. Before submitting your code, please run the following commands to ensure your code adheres to the style guidelines:
```bash
ruff format .
ruff check .
```
##### PR completeness checks (recommended)
To run a local validation flow close to CI, use:
```bash
make pr-test-neo
```
This command runs:
- `uv sync --group dev`
- `ruff format --check .` and `ruff check .`
- Neo-related critical tests
- a startup smoke test against `http://localhost:6185`
For full validation, use:
```bash
make pr-test-full
```
For faster repeated runs (skip dependency sync and dashboard build), use:
```bash
make pr-test-full-fast
```

View File

@@ -1,4 +1,4 @@
FROM python:3.12-slim
FROM python:3.11-slim
WORKDIR /AstrBot
COPY . /AstrBot/
@@ -12,23 +12,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
bash \
ffmpeg \
libavcodec-extra \
curl \
gnupg \
git \
ripgrep \
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
&& rm -rf /var/lib/apt/lists/*
RUN python -m pip install uv \
&& echo "3.12" > .python-version \
&& uv lock \
&& uv export --format requirements.txt --output-file requirements.txt --frozen \
&& uv pip install -r requirements.txt --no-cache-dir --system \
&& uv pip install socksio uv pilk --no-cache-dir --system
RUN apt-get update && apt-get install -y curl gnupg && \
curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
apt-get install -y nodejs && \
rm -rf /var/lib/apt/lists/*
RUN python -m pip install uv
RUN uv pip install -r requirements.txt --no-cache-dir --system
RUN uv pip install socksio uv pilk --no-cache-dir --system
EXPOSE 6185
EXPOSE 6186
CMD ["python", "main.py"]
CMD [ "python", "main.py" ]

35
Dockerfile_with_node Normal file
View File

@@ -0,0 +1,35 @@
FROM python:3.10-slim
WORKDIR /AstrBot
COPY . /AstrBot/
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
build-essential \
python3-dev \
libffi-dev \
libssl-dev \
curl \
unzip \
ca-certificates \
bash \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Installation of Node.js
ENV NVM_DIR="/root/.nvm"
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash && \
. "$NVM_DIR/nvm.sh" && \
nvm install 22 && \
nvm use 22
RUN /bin/bash -c ". \"$NVM_DIR/nvm.sh\" && node -v && npm -v"
RUN python -m pip install uv
RUN uv pip install -r requirements.txt --no-cache-dir --system
RUN uv pip install socksio uv pyffmpeg --no-cache-dir --system
EXPOSE 6185
EXPOSE 6186
CMD ["python", "main.py"]

244
EULA.md
View File

@@ -1,244 +0,0 @@
# 最终用户许可协议EULA
> 我们热爱开源软件,并始终致力于为所有用户提供健康、安全、可靠的使用体验。 ❤️
For English edition, please refer to the section below the Chinese version.
**最后更新:** 2026-01-12
感谢您使用 **AstrBot**
在使用本项目之前,请仔细阅读以下声明内容。
**您一旦安装、运行或使用本项目,即表示您已阅读、理解并同意本声明中的全部内容。**
## 1. 项目性质
AstrBot 是一个遵循 **GNU Affero General Public License v3AGPLv3** 协议发布的**免费开源软件项目**。
* 截至目前AstrBot 项目未开展任何形式的商业化服务AstrBot 团队也未通过本项目向用户提供任何收费服务。若您因使用 AstrBot 被要求付费,请务必提高警惕,谨防诈骗行为。
* AstrBot 的代码实现未对任何第三方系统进行逆向工程、破解、反编译或绕过安全机制等行为。AstrBot 仅使用并支持各即时通讯IM平台官方公开提供的机器人接入接口、开放平台能力或相关通信协议进行集成与通信。
## 2. 无担保声明
AstrBot 按“**现状as is**”提供,不附带任何形式的明示或暗示担保。
AstrBot 团队不对以下内容作出任何保证:
* 系统本身的安全性、可靠性或稳定性;
* 任何第三方插件的安全性、正确性或可信度;
* 任何第三方 AI 模型或外部服务 API 的可用性、质量、准确性或安全性;
* 本软件对任何特定用途的适用性。
**您使用本软件所产生的一切风险均由您自行承担。**
## 3. 第三方插件与服务
* AstrBot 支持第三方插件及外部 AI 服务接入;
* AstrBot 团队**不对任何第三方插件、扩展或服务进行审计、控制、背书或担保**
* 因使用第三方插件或服务所产生的任何风险、损失、数据泄露或法律后果,均由用户自行承担。
* 第三方插件指代的是非 AstrBot 自带的插件AstrBot 自带的插件指代的是插件实现代码已经包含在 AstrBotDevs/AstrBot 代码库中的插件。插件市场中的插件都是第三方插件。
## 4. 使用与内容限制
您同意不会将 AstrBot 用于以下行为:
* 输入、生成、传播或处理任何违法、极端、暴力、色情、仇恨、辱骂或其他有害内容;
* 从事违反您所在国家或地区法律法规,或任何适用国际法律的行为;
* 试图绕过、关闭、削弱或破坏本系统内置的安全机制或内容限制。
* 任何侵犯他人合法权益、损害他人和自己身心健康、涉及个人隐私、个人信息等敏感内容的内容。
## 5. 项目用途说明
AstrBot 是一个**工具型对话与 Agent 系统**,在**安全、健康、友善**的前提下提供有限的人性化交互能力。
项目的主要目标是:
* 提供 Agent 能力与自动化辅助;
* 帮助用户提升工作、学习和信息处理效率;
* 在合理范围内提供友好的人机交互体验。
* 辅助用户成长,提供有益于用户身心健康的内容。
## 6. 安全措施说明
AstrBot 团队**已尽合理努力在技术和策略层面设置安全与内容约束机制**,以引导系统输出健康、友善、安全的内容。
但请理解:
* 世界上任何的系统均无法保证完全无误、绝对安全或无法被滥用;
* 用户仍有责任自行合理配置、监督并正确使用本系统。
如果您要关闭 AstrBot 默认启用的“健康模式”,请在 cmd_config.json 中将 `provider_settings.llm_safety_mode` 设置为 `False`。但请注意关闭健康模式不是推荐的使用方式可能导致系统输出不安全或不适当的内容。关闭该功能所产生的任何风险与后果均由用户自行承担AstrBot 团队不对此承担任何责任。
## 7. 心理健康提示
如果您在使用本项目过程中因系统输出内容而感到心理不适、情绪困扰,
或您本身正处于心理压力较大、情绪不稳定、焦虑、抑郁等状态并因此使用本项目,
请优先考虑寻求来自专业人士的帮助,例如心理咨询师、心理医生或当地心理援助机构。
如遇紧急情况(例如存在自伤或他伤风险),请立即联系当地的紧急救助电话或专业机构。
## 8. 统计信息与隐私说明
AstrBot 可能会收集有限的匿名统计信息,用于了解系统使用情况、发现问题以及持续改进项目。
所收集的统计信息仅包括与系统运行和功能使用相关的基础技术指标,例如功能使用频率、错误信息等。
AstrBot **不会收集、上传或存储您的对话内容、消息正文、输入文本,或任何能够识别您个人身份的敏感信息**
您可以手动关闭此项功能,通过在系统环境变量中设置 `ASTRBOT_DISABLE_METRICS=1` 来禁用匿名统计信息收集。
## 9. 责任限制
在法律允许的最大范围内AstrBot 团队不对因以下原因导致的任何直接或间接损失承担责任,包括但不限于:
* 使用或无法使用本软件;
* 使用第三方插件或服务;
* 系统生成的内容或输出;
* 数据丢失、服务中断或安全事件。
## 10. 条款的接受
您一旦安装、运行、修改或使用 AstrBot即确认
* 您已阅读并理解本声明内容;
* 您同意并接受上述所有条款;
* 您对自身使用行为承担全部责任。
如您不同意本声明的任何内容,请勿使用本项目。
## 11. 许可与版权
AstrBot 的源代码、文档及相关内容受版权法及相关法律保护。
在遵守本声明及 AGPLv3 协议的前提下AstrBot 授予您一项非独占、不可转让、不可再许可的许可,用于下载、安装、运行、修改和分发本软件。
除非法律另有规定或本声明另有明确说明AstrBot 团队保留本项目的所有未明确授予的权利。
## 12. 适用法律
本声明的解释与适用应遵循您所在地或项目发布地适用的法律法规。
如本声明的任何条款被认定为无效或不可执行,其余条款仍然有效。
---
# EULA
> We love open-source software and are always committed to providing all users with a healthy, safe, and reliable experience. ❤️
**Last updated:** January 12, 2026
Thank you for using **AstrBot**.
Please read the following notice carefully before using this project.
**By installing, running, or using this project, you acknowledge that you have read, understood, and agreed to all the terms stated below.**
## 1. Nature of the Project
AstrBot is a **free and open-source software project** released under the **GNU Affero General Public License v3 (AGPLv3)**.
* AstrBot does not constitute any form of commercial service;
* The AstrBot Team does not provide any paid services through this project;
* AstrBots implementation does not involve reverse engineering, cracking, decompilation, or circumvention of security mechanisms of any third-party systems. AstrBot only uses and supports officially published bot integration interfaces, open platform capabilities, or related communication protocols provided by instant messaging (IM) platforms for integration and communication.
## 2. No Warranty
AstrBot is provided **“as is”**, without any express or implied warranties.
The AstrBot Team makes no guarantees regarding:
* The security, reliability, or stability of the system;
* The security, correctness, or trustworthiness of any third-party plugins;
* The availability, quality, accuracy, or safety of any third-party AI model APIs or external services;
* The fitness of the software for any particular purpose.
**All risks arising from the use of this software are borne solely by the user.**
## 3. Third-Party Plugins and Services
* AstrBot supports third-party plugins and external AI services;
* The AstrBot Team does **not audit, control, endorse, or guarantee** any third-party plugins, extensions, or services;
* Any risks, losses, data leaks, or legal consequences arising from the use of third-party plugins or services are solely the responsibility of the user;
* “Third-party plugins” refer to plugins that are not built into AstrBot. Built-in plugins are those whose implementation code is included in the AstrBotDevs/AstrBot repository. All plugins available in the plugin marketplace are third-party plugins.
## 4. Usage and Content Restrictions
You agree not to use AstrBot for any of the following activities:
* Inputting, generating, distributing, or processing any illegal, extremist, violent, pornographic, hateful, abusive, or otherwise harmful content;
* Engaging in activities that violate the laws or regulations of your country or region, or any applicable international laws;
* Attempting to bypass, disable, weaken, or undermine the built-in safety mechanisms or content restrictions of the system;
* Any activities that infringe upon the legitimate rights and interests of others, harm the physical or mental well-being of yourself or others, or involve personal privacy or sensitive personal information.
## 5. Intended Use
AstrBot is a **tool-oriented conversational and agent system** that provides limited human-like interaction capabilities under the principles of **safety, health, and friendliness**.
The primary goals of the project are to:
* Provide agent capabilities and automation assistance;
* Help users improve efficiency in work, study, and information processing;
* Offer a friendly humancomputer interaction experience within reasonable boundaries;
* Support user growth and provide content beneficial to users physical and mental well-being.
## 6. Safety Measures
The AstrBot Team has made **reasonable efforts** at both technical and policy levels to implement safety and content restriction mechanisms, guiding the system to produce healthy, friendly, and safe outputs.
However, please understand that:
* No system in the world can be guaranteed to be completely error-free, absolutely secure, or immune to misuse;
* Users remain responsible for properly configuring, supervising, and using the system.
If you wish to disable AstrBots default “Safety Mode,” please set `provider_settings.llm_safety_mode` to `False` in `cmd_config.json`. However, please note that disabling Safety Mode is not recommended and may lead to unsafe or inappropriate outputs. Any risks or consequences arising from disabling this feature are solely borne by the user, and the AstrBot Team assumes no responsibility.
## 7. Mental Health Notice
If you experience psychological discomfort or emotional distress due to system outputs during use,
or if you are experiencing significant psychological stress, emotional instability, anxiety, or depression and are using this project for such reasons,
please prioritize seeking help from qualified professionals, such as psychologists, psychiatrists, or local mental health support services.
In case of emergency (for example, if there is a risk of self-harm or harm to others), please immediately contact your local emergency number or professional crisis support services.
## 8. Metrics and Privacy
AstrBot may collect a limited amount of anonymous usage statistics to understand system usage, identify issues, and continuously improve the project.
Collected metrics are limited to basic technical indicators related to system operation and feature usage, such as feature usage frequency and error information.
AstrBot **does not collect, upload, or store your conversation content, message bodies, input text, or any personally identifiable or sensitive information**.
You may manually disable this feature by setting the environment variable `ASTRBOT_DISABLE_METRICS=1` to turn off anonymous metrics collection.
## 9. Limitation of Liability
To the maximum extent permitted by law, the AstrBot Team shall not be liable for any direct or indirect losses arising from, including but not limited to:
* The use or inability to use this software;
* The use of third-party plugins or services;
* Generated content or system outputs;
* Data loss, service interruptions, or security incidents.
## 10. Acceptance of Terms
By installing, running, modifying, or using AstrBot, you confirm that:
* You have read and understood this Notice;
* You agree to and accept all the terms stated above;
* You assume full responsibility for your use of the software.
If you do not agree with any part of this Notice, please do not use this project.
## 11. License and Copyright
The source code, documentation, and related materials of AstrBot are protected by copyright laws and applicable regulations.
Subject to compliance with this Notice and the AGPLv3 license, AstrBot grants you a non-exclusive, non-transferable, non-sublicensable license to download, install, run, modify, and distribute this software.
Unless otherwise required by law or expressly stated in this Notice, the AstrBot Team reserves all rights not expressly granted.
## 12. Governing Law
The interpretation and application of this Notice shall be governed by the laws and regulations applicable in your jurisdiction or the jurisdiction where the project is released.
If any provision of this Notice is held to be invalid or unenforceable, the remaining provisions shall remain in full force and effect.

View File

@@ -1,16 +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.
📊 Please read the [End User License Agreement](https://github.com/AstrBotDevs/AstrBot/blob/master/EULA.md) carefully before using this project. By installing, you agree to all its contents.
📮 Official email: [community@astrbot.app](mailto:community@astrbot.app)

View File

@@ -1,16 +0,0 @@
## 欢迎使用 AstrBot
🌟 感谢您使用 AstrBot
AstrBot 是一款可接入多种 IM 平台的 Agentic AI 个人 / 群聊助手,内置多项强大功能,希望能为您带来高效、愉快的使用体验。❤️
我们想特别说明:
AstrBot 是受 AGPLv3 开源协议保护的**免费开源软件项目**,您可以在[**官方网站**](https://astrbot.app)、[**GitHub**](https://github.com/astrbotdevs/astrbot) 上找到 AstrBot 的全部源代码及相关资源。
截至目前AstrBot 项目**未开展任何形式的商业化服务**,官方**不会以任何名义向用户收取费用**。
如果您在使用 AstrBot 的过程中被要求付费,**表明您已经遭遇诈骗行为**。请立即向相关方申请退款,并及时通过邮件向我们反馈。
📊 在使用本项目之前,请仔细阅读 [最终用户许可协议](https://github.com/AstrBotDevs/AstrBot/blob/master/EULA.md)。安装即表示您已阅读并同意其中的全部内容。
📮 官方邮箱:[community@astrbot.app](mailto:community@astrbot.app)

View File

@@ -1,16 +0,0 @@
## Добро пожаловать в AstrBot
🌟 Спасибо, что используете AstrBot!
AstrBot — это Agentic AI-ассистент для личных и групповых чатов с поддержкой множества IM-платформ и широким набором встроенных функций. Надеемся, что он сделает ваше общение эффективным и приятным. ❤️
Важное уведомление:
AstrBot — это **бесплатный проект с открытым исходным кодом**, защищённый лицензией AGPLv3. Полный исходный код и связанные ресурсы доступны на [**официальном сайте**](https://astrbot.app) и [**GitHub**](https://github.com/astrbotdevs/astrbot).
На данный момент AstrBot **не предоставляет никаких коммерческих услуг**, и официальная команда **никогда не будет взимать плату с пользователей** под каким-либо названием.
Если кто-то просит вас заплатить при использовании AstrBot, **вас, скорее всего, пытаются обмануть**. Немедленно запросите возврат средств и сообщите нам по электронной почте.
📊 Пожалуйста, внимательно прочитайте [Лицензионное соглашение](https://github.com/AstrBotDevs/AstrBot/blob/master/EULA.md) перед использованием. Устанавливая программу, вы соглашаетесь со всеми его условиями.
📮 Официальная почта: [community@astrbot.app](mailto:community@astrbot.app)

View File

@@ -1,41 +0,0 @@
.PHONY: worktree worktree-add worktree-rm pr-test-neo pr-test-full pr-test-full-fast
WORKTREE_DIR ?= ../astrbot_worktree
BRANCH ?= $(word 2,$(MAKECMDGOALS))
BASE ?= $(word 3,$(MAKECMDGOALS))
BASE ?= master
worktree:
@echo "Usage:"
@echo " make worktree-add <branch> [base-branch]"
@echo " make worktree-rm <branch>"
worktree-add:
ifeq ($(strip $(BRANCH)),)
$(error Branch name required. Usage: make worktree-add <branch> [base-branch])
endif
@mkdir -p $(WORKTREE_DIR)
git worktree add $(WORKTREE_DIR)/$(BRANCH) -b $(BRANCH) $(BASE)
worktree-rm:
ifeq ($(strip $(BRANCH)),)
$(error Branch name required. Usage: make worktree-rm <branch>)
endif
@if [ -d "$(WORKTREE_DIR)/$(BRANCH)" ]; then \
git worktree remove $(WORKTREE_DIR)/$(BRANCH); \
else \
echo "Worktree $(WORKTREE_DIR)/$(BRANCH) not found."; \
fi
pr-test-neo:
./scripts/pr_test_env.sh --profile neo
pr-test-full:
./scripts/pr_test_env.sh --profile full
pr-test-full-fast:
./scripts/pr_test_env.sh --profile full --skip-sync --no-dashboard
# Swallow extra args (branch/base) so make doesn't treat them as targets
%:
@true

373
README.md
View File

@@ -1,288 +1,211 @@
![astrbot-github-banner-v2-light-0405_副本](https://github.com/user-attachments/assets/36fb04e4-cc75-4454-bd8b-049d11aa86f9)
<img width="430" height="31" alt="image" src="https://github.com/user-attachments/assets/474c822c-fab7-41be-8c23-6dae252823ed" /><p align="center">
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
</p>
<div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh.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_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://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<br>
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/Soulter/AstrBot?style=for-the-badge&color=76bad9)](https://github.com/Soulter/AstrBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10+-blue.svg?style=for-the-badge&color=76bad9" alt="python">
<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://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://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
[![wakatime](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e.svg?style=for-the-badge&color=76bad9)](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E4%B8%AA&style=for-the-badge&label=%E6%8F%92%E4%BB%B6%E5%B8%82%E5%9C%BA&cacheSeconds=3600)
<div>
<a href="https://trendshift.io/repositories/21369" target="_blank"><img src="https://trendshift.io/api/badge/repositories/21369" alt="AstrBotDevs%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=2" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
<br>
<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/badge/python-3.10+-blue.svg" alt="python">
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
<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://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?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=%20plugins&label=Marketplace&cacheSeconds=3600">
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
</div>
<br>
<a href="https://astrbot.app/">Documentation</a>
<a href="https://github.com/Soulter/AstrBot/blob/master/README_en.md">English</a>
<a href="https://github.com/Soulter/AstrBot/blob/master/README_ja.md">日本語</a>
<a href="https://astrbot.app/">文档</a>
<a href="https://blog.astrbot.app/">Blog</a>
<a href="https://astrbot.featurebase.app/roadmap">Roadmap</a>
<a href="https://github.com/AstrBotDevs/AstrBot/issues">Issue Tracker</a>
<a href="mailto:community@astrbot.app">Email Support</a>
<a href="https://github.com/Soulter/AstrBot/issues">问题提交</a>
</div>
AstrBot is an open-source all-in-one Agent chatbot platform that integrates with mainstream instant messaging apps. It provides reliable and scalable conversational AI infrastructure for individuals, developers, and teams. Whether you're building a personal AI companion, intelligent customer service, automation assistant, or enterprise knowledge base, AstrBot enables you to quickly build production-ready AI applications within your IM platform workflows.
AstrBot 是一个开源的一站式 Agentic 聊天机器人平台及开发框架。
![screenshot_1 5x_postspark_2026-02-27_22-37-45](https://github.com/user-attachments/assets/f17cdb90-52d7-4773-be2e-ff64b566af6b)
## 主要功能
## Key Features
1. **大模型对话**。支持接入多种大模型服务。支持多模态、工具调用、MCP、原生知识库、人设等功能。
2. **多消息平台支持**。支持接入 QQ、企业微信、微信公众号、飞书、Telegram、钉钉、Discord、KOOK 等平台。支持速率限制、白名单、百度内容审核。
3. **Agent**。完善适配的 Agentic 能力。支持多轮工具调用、内置沙盒代码执行器、网页搜索等功能。
4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,社区插件生态丰富。
5. **WebUI**。可视化配置和管理机器人,功能齐全。
1. 💯 Free & Open Source.
2. ✨ AI LLM Conversations, Multimodal, Agent, MCP, Skills, Knowledge Base, Persona Settings, Auto Context Compression.
3. 🤖 Supports integration with Dify, Alibaba Cloud Bailian, Coze, and other agent platforms.
4. 🌐 Multi-Platform: QQ, WeChat Work, Feishu, DingTalk, WeChat Official Accounts, Telegram, Slack, and [more](#supported-messaging-platforms).
5. 📦 Plugin Extensions with 1000+ plugins available for one-click installation.
6. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) for isolated, safe execution of code, shell calls, and session-level resource reuse.
7. 💻 WebUI Support.
8. 🌈 Web ChatUI Support with built-in agent sandbox and web search.
9. 🌐 Internationalization (i18n) Support.
## 部署方式
<br>
#### Docker 部署
<table align="center">
<tr align="center">
<th>💙 Role-playing & Emotional Companionship</th>
<th>✨ Proactive Agent</th>
<th>🚀 General Agentic Capabilities</th>
<th>🧩 1000+ 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>
推荐使用 Docker / Docker Compose 方式部署 AstrBot。
## Quick Start
请参阅官方文档 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) 。
### One-Click Deployment
#### 宝塔面板部署
For users who want to quickly experience AstrBot, are familiar with command-line usage, and can install a `uv` environment on their own, we recommend the `uv` one-click deployment method ⚡️:
AstrBot 与宝塔面板合作,已上架至宝塔面板。
```bash
uv tool install astrbot --python 3.12
astrbot init # Only execute this command for the first time to initialize the environment
astrbot run
```
请参阅官方文档 [宝塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.html) 。
> Requires [uv](https://docs.astral.sh/uv/) to be installed.
> AstrBot requires Python 3.12 or later. The `--python 3.12` option ensures that `uv` creates the tool environment with Python 3.12.
#### 1Panel 部署
> [!NOTE]
> For macOS users: due to macOS security checks, the first run of the `astrbot` command may take longer (about 10-20s).
AstrBot 已由 1Panel 官方上架至 1Panel 面板。
Update `astrbot`:
请参阅官方文档 [1Panel 部署](https://astrbot.app/deploy/astrbot/1panel.html) 。
```bash
uv tool upgrade astrbot --python 3.12
```
#### 在 雨云 上部署
> [!WARNING]
> AstrBot deployed via `uv` **does not support upgrading through the WebUI**. To update, please run the command above from the command line.
### Docker Deployment
For users familiar with containers and looking for a more stable, production-ready deployment method, we recommend deploying AstrBot with Docker / Docker Compose.
Please refer to the official documentation: [Deploy AstrBot with Docker](https://docs.astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot).
### Deploy on RainYun
For users who want one-click deployment and do not want to manage servers themselves, we recommend RainYun's one-click cloud deployment service ☁️:
AstrBot 已由雨云官方上架至云应用平台,可一键部署。
[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
### Desktop Application Deployment
#### 在 Replit 上部署
For users who want to use AstrBot on desktop and mainly use ChatUI, we recommend AstrBot App.
社区贡献的部署方式。
Visit [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop) to download and install; this method is designed for desktop usage and is not recommended for server scenarios.
[![Run on Repl.it](https://repl.it/badge/github/Soulter/AstrBot)](https://repl.it/github/Soulter/AstrBot)
### Launcher Deployment
#### Windows 一键安装器部署
For desktop users who also want fast deployment and isolated multi-instance usage, we recommend AstrBot Launcher.
请参阅官方文档 [使用 Windows 一键安装器部署 AstrBot](https://astrbot.app/deploy/astrbot/windows.html) 。
Visit [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) to download and install.
#### CasaOS 部署
### Deploy on Replit
社区贡献的部署方式。
Replit deployment is maintained by the community and is suitable for online demos and lightweight trials.
请参阅官方文档 [CasaOS 部署](https://astrbot.app/deploy/astrbot/casaos.html) 。
[![Run on Repl.it](https://repl.it/badge/github/AstrBotDevs/AstrBot)](https://repl.it/github/AstrBotDevs/AstrBot)
#### 手动部署
### AUR
AUR deployment targets Arch Linux users who prefer installing AstrBot through the system package workflow.
Run the command below to install `astrbot-git`, then start AstrBot in your local environment.
首先安装 uv
```bash
yay -S astrbot-git
pip install uv
```
**More deployment methods**
If you need panel-based management or deeper customization, see [BT-Panel Deployment](https://docs.astrbot.app/deploy/astrbot/btpanel.html) for BT Panel app-store setup, [1Panel Deployment](https://docs.astrbot.app/deploy/astrbot/1panel.html) for 1Panel app-market deployment, [CasaOS Deployment](https://docs.astrbot.app/deploy/astrbot/casaos.html) for NAS/home-server visual deployment, and [Manual Deployment](https://docs.astrbot.app/deploy/astrbot/cli.html) for fully custom source-based installation with `uv`.
## Supported Messaging Platforms
Connect AstrBot to your favorite chat platform.
| Platform | Maintainer |
|---------|---------------|
| QQ | Official |
| OneBot v11 protocol implementation | Official |
| Telegram | Official |
| Wecom & Wecom AI Bot | Official |
| WeChat Official Accounts | Official |
| Feishu (Lark) | Official |
| DingTalk | Official |
| Slack | Official |
| Discord | Official |
| LINE | Official |
| Satori | Official |
| KOOK | Official |
| Misskey | Official |
| Mattermost | Official |
| WhatsApp (Coming Soon) | Official |
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Community |
| [Rocket.Chat](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | Community |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Community |
## Supported Model Services
| Service | Type |
|---------|---------------|
| OpenAI and Compatible Services | LLM Services |
| Anthropic | LLM Services |
| Google Gemini | LLM Services |
| Moonshot AI | LLM Services |
| Zhipu AI | LLM Services |
| DeepSeek | LLM Services |
| Ollama (Self-hosted) | LLM Services |
| LM Studio (Self-hosted) | LLM Services |
| [AIHubMix](https://aihubmix.com/?aff=4bfH) | LLM Services (API Gateway, supports all models) |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | LLM Services |
| [302.AI](https://share.302.ai/rr1M3l) | LLM Services |
| [TokenPony](https://www.tokenpony.cn/3YPyf) | LLM Services |
| [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | LLM Services |
| [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE) | LLM Services |
| ModelScope | LLM Services |
| OneAPI | LLM Services |
| Dify | LLMOps Platforms |
| Alibaba Cloud Bailian Applications | LLMOps Platforms |
| Coze | LLMOps Platforms |
| OpenAI Whisper | Speech-to-Text Services |
| SenseVoice | Speech-to-Text Services |
| Xiaomi MiMo Omni | Speech-to-Text Services |
| OpenAI TTS | Text-to-Speech Services |
| Gemini TTS | Text-to-Speech Services |
| GPT-Sovits-Inference | Text-to-Speech Services |
| GPT-Sovits | Text-to-Speech Services |
| FishAudio | Text-to-Speech Services |
| Edge TTS | Text-to-Speech Services |
| Alibaba Cloud Bailian TTS | Text-to-Speech Services |
| Azure TTS | Text-to-Speech Services |
| Minimax TTS | Text-to-Speech Services |
| Xiaomi MiMo TTS | Text-to-Speech Services |
| Volcano Engine TTS | Text-to-Speech Services |
## ❤️ Sponsors
<p align="center">
<img alt="sponsors" src="https://sponsors.astrbot.app/?v=1">
</p>
## ❤️ Contributing
Issues and Pull Requests are always welcome! Feel free to submit your changes to this project :)
### How to Contribute
You can contribute by reviewing issues or helping with pull request reviews. Any issues or PRs are welcome to encourage community participation. Of course, these are just suggestions—you can contribute in any way you like. For adding new features, please discuss through an Issue first.
### Development Environment
AstrBot uses `ruff` for code formatting and linting.
通过 Git Clone 安装 AstrBot
```bash
git clone https://github.com/AstrBotDevs/AstrBot
git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
uv run main.py
```
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
## 🌍 社区
### QQ 群组
- 1 群322154837
- 3 群630166526
- 5 群822130018
- 6 群753075035
- 开发者群975206796
- 开发者群备份295657329
### Telegram 群组
<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>
### Discord 群组
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
## ⚡ 消息平台支持情况
| 平台 | 支持性 |
| -------- | ------- |
| QQ(官方机器人接口) | ✔ |
| QQ(OneBot) | ✔ |
| Telegram | ✔ |
| 企业微信 | ✔ |
| 微信客服 | ✔ |
| 微信公众号 | ✔ |
| 飞书 | ✔ |
| 钉钉 | ✔ |
| Slack | ✔ |
| Discord | ✔ |
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | ✔ |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | ✔ |
| Satori | ✔ |
| Misskey | ✔ |
## ⚡ 提供商支持情况
| 名称 | 支持性 | 类型 | 备注 |
| -------- | ------- | ------- | ------- |
| OpenAI | ✔ | 文本生成 | 支持任何兼容 OpenAI API 的服务 |
| Anthropic | ✔ | 文本生成 | |
| Google Gemini | ✔ | 文本生成 | |
| Dify | ✔ | LLMOps | |
| 阿里云百炼应用 | ✔ | LLMOps | |
| Ollama | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
| LM Studio | ✔ | 模型加载器 | 本地部署 DeepSeek、Llama 等开源语言模型 |
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | ✔ | 模型 API 及算力服务平台 | |
| [302.AI](https://share.302.ai/rr1M3l) | ✔ | 模型 API 服务平台 | |
| 硅基流动 | ✔ | 模型 API 服务平台 | |
| PPIO 派欧云 | ✔ | 模型 API 服务平台 | |
| OneAPI | ✔ | LLM 分发系统 | |
| Whisper | ✔ | 语音转文本 | 支持 API、本地部署 |
| SenseVoice | ✔ | 语音转文本 | 本地部署 |
| OpenAI TTS API | ✔ | 文本转语音 | |
| GSVI | ✔ | 文本转语音 | GPT-Sovits-Inference |
| GPT-SoVITs | ✔ | 文本转语音 | GPT-Sovits-Inference |
| FishAudio | ✔ | 文本转语音 | GPT-Sovits 作者参与的项目 |
| Edge TTS | ✔ | 文本转语音 | Edge 浏览器的免费 TTS |
| 阿里云百炼 TTS | ✔ | 文本转语音 | |
| Azure TTS | ✔ | 文本转语音 | Microsoft Azure TTS |
## ❤️ 贡献
欢迎任何 Issues/Pull Requests只需要将你的更改提交到此项目 )
### 如何贡献
你可以通过查看问题或帮助审核 PR拉取请求来贡献。任何问题或 PR 都欢迎参与,以促进社区贡献。当然,这些只是建议,你可以以任何方式进行贡献。对于新功能的添加,请先通过 Issue 讨论。
### 开发环境
AstrBot 使用 `ruff` 进行代码格式化和检查。
```bash
git clone https://github.com/Soulter/AstrBot
pip install pre-commit
pre-commit install
```
## 🌍 Community
### QQ Groups
- Group 1: 322154837 (Full)
- Group 3: 630166526 (Full)
- Group 4: 1077826412 (Full)
- Group 5: 822130018 (Full)
- Group 6: 753075035 (Full)
- Group 7: 743746109 (Full)
- Group 8: 1030353265 (Full)
- Group 9: 1076659624 (Full)
- Group 10: 1078079676 (Full)
- Group 11: 704659519 (Full)
- Group 12: 916228568 (Full)
- Group 13: 1092185289
- Group 14: 1103419483
- Developer Group(Chit-chat): 975206796
- Developer Group(Formal): 1039761811
### Discord Server
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
## ❤️ Special Thanks
Special thanks to all Contributors and plugin developers for their contributions to AstrBot ❤️
特别感谢所有 Contributors 和插件开发者对 AstrBot 的贡献 ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=300&columns=15" />
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
</a>
Additionally, the birth of this project would not have been possible without the help of the following open-source projects:
此外,本项目的诞生离不开以下开源项目的帮助:
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - The amazing cat framework
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 伟大的猫猫框架
另外,一些同类型其他的活跃开源 Bot 项目:
- [nonebot/nonebot2](https://github.com/nonebot/nonebot2) - 扩展性极强的 Bot 框架
- [koishijs/koishi](https://github.com/koishijs/koishi) - 扩展性极强的 Bot 框架
- [MaiM-with-u/MaiBot](https://github.com/MaiM-with-u/MaiBot) - 注重拟人功能的 ChatBot
- [langbot-app/LangBot](https://github.com/langbot-app/LangBot) - 功能丰富的 Bot 平台
- [KroMiose/nekro-agent](https://github.com/KroMiose/nekro-agent) - 注重 Agent 的 ChatBot
- [zhenxun-org/zhenxun_bot](https://github.com/zhenxun-org/zhenxun_bot) - 功能完善的 ChatBot
## ⭐ Star History
> [!TIP]
> If this project has helped you in your life or work, or if you're interested in its future development, please give the project a Star. It's the driving force behind maintaining this open-source project <3
> [!TIP]
> 如果本项目对您的生活 / 工作产生了帮助,或者您关注本项目的未来发展,请给项目 Star这是我维护这个开源项目的动力 <3
<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=soulter/astrbot&type=Date)](https://star-history.com/#soulter/astrbot&Date)
</div>
<div align="center">
_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._
</details>
_私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div>

182
README_en.md Normal file
View File

@@ -0,0 +1,182 @@
<p align="center">
![6e1279651f16d7fdf4727558b72bbaf1](https://github.com/user-attachments/assets/ead4c551-fc3c-48f7-a6f7-afbfdb820512)
</p>
<div align="center">
_✨ Easy-to-use Multi-platform LLM Chatbot & Development Framework ✨_
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/Soulter/AstrBot)](https://github.com/Soulter/AstrBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg"/></a>
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=wtbaNx7EioxeaqS9z7RQWVXPIxg2zYr7&jump_from=webapi&authKey=vlqnv/AV2DbJEvGIcxdlNSpfxVy+8vVqijgreRdnVKOaydpc+YSw4MctmEbr0k5"><img alt="Static Badge" src="https://img.shields.io/badge/QQ群-630166526-purple"></a>
[![wakatime](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e.svg)](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fstats&query=v&label=7%E6%97%A5%E6%B6%88%E6%81%AF%E4%B8%8A%E8%A1%8C%E9%87%8F&cacheSeconds=3600)
[![codecov](https://codecov.io/gh/Soulter/AstrBot/graph/badge.svg?token=FF3P5967B8)](https://codecov.io/gh/Soulter/AstrBot)
<a href="https://astrbot.app/">Documentation</a>
<a href="https://github.com/Soulter/AstrBot/issues">Issue Tracking</a>
</div>
AstrBot is a loosely coupled, asynchronous chatbot and development framework that supports multi-platform deployment, featuring an easy-to-use plugin system and comprehensive Large Language Model (LLM) integration capabilities.
## ✨ Key Features
1. **LLM Conversations** - Supports various LLMs including OpenAI API, Google Gemini, Llama, Deepseek, ChatGLM, etc. Enables local model deployment via Ollama/LLMTuner. Features multi-turn dialogues, personality contexts, multimodal capabilities (image understanding), and speech-to-text (Whisper).
2. **Multi-platform Integration** - Supports QQ (OneBot), QQ Channels, WeChat (Gewechat), Feishu, and Telegram. Planned support for DingTalk, Discord, WhatsApp, and Xiaomi Smart Speakers. Includes rate limiting, whitelisting, keyword filtering, and Baidu content moderation.
3. **Agent Capabilities** - Native support for code execution, natural language TODO lists, web search. Integrates with [Dify Platform](https://dify.ai/) for easy access to Dify assistants/knowledge bases/workflows.
4. **Plugin System** - Optimized plugin mechanism with minimal development effort. Supports multiple installed plugins.
5. **Web Dashboard** - Visual configuration management, plugin controls, logging, and WebChat interface for direct LLM interaction.
6. **High Stability & Modularity** - Event bus and pipeline architecture ensures high modularization and loose coupling.
> [!TIP]
> Dashboard Demo: [https://demo.astrbot.app/](https://demo.astrbot.app/)
> Username: `astrbot`, Password: `astrbot` (LLM not configured for chat page)
## ✨ Deployment
#### Docker Deployment
See docs: [Deploy with Docker](https://astrbot.app/deploy/astrbot/docker.html#docker-deployment)
#### Windows Installer
Requires Python (>3.10). See docs: [Windows Installer Guide](https://astrbot.app/deploy/astrbot/windows.html)
#### Replit Deployment
[![Run on Repl.it](https://repl.it/badge/github/Soulter/AstrBot)](https://repl.it/github/Soulter/AstrBot)
#### CasaOS Deployment
Community-contributed method.
See docs: [CasaOS Deployment](https://astrbot.app/deploy/astrbot/casaos.html)
#### Manual Deployment
See docs: [Source Code Deployment](https://astrbot.app/deploy/astrbot/cli.html)
## ⚡ Platform Support
| Platform | Status | Details | Message Types |
| -------------------------------------------------------------- | ------ | ------------------- | ------------------- |
| QQ (Official Bot) | ✔ | Private/Group chats | Text, Images |
| QQ (OneBot) | ✔ | Private/Group chats | Text, Images, Voice |
| WeChat (Personal) | ✔ | Private/Group chats | Text, Images, Voice |
| [Telegram](https://github.com/Soulter/astrbot_plugin_telegram) | ✔ | Private/Group chats | Text, Images |
| [WeChat Work](https://github.com/Soulter/astrbot_plugin_wecom) | ✔ | Private chats | Text, Images, Voice |
| Feishu | ✔ | Group chats | Text, Images |
| WeChat Open Platform | 🚧 | Planned | - |
| Discord | 🚧 | Planned | - |
| WhatsApp | 🚧 | Planned | - |
| Xiaomi Speakers | 🚧 | Planned | - |
## Provider Support Status
| Name | Support | Type | Notes |
|---------------------------|---------|------------------------|-----------------------------------------------------------------------|
| OpenAI API | ✔ | Text Generation | Supports all OpenAI API-compatible services including DeepSeek, Google Gemini, GLM, Moonshot, Alibaba Cloud Bailian, Silicon Flow, xAI, etc. |
| Claude API | ✔ | Text Generation | |
| Google Gemini API | ✔ | Text Generation | |
| Dify | ✔ | LLMOps | |
| DashScope (Alibaba Cloud) | ✔ | LLMOps | |
| Ollama | ✔ | Model Loader | Local deployment for open-source LLMs (DeepSeek, Llama, etc.) |
| LM Studio | ✔ | Model Loader | Local deployment for open-source LLMs (DeepSeek, Llama, etc.) |
| LLMTuner | ✔ | Model Loader | Local loading of fine-tuned models (e.g. LoRA) |
| OneAPI | ✔ | LLM Distribution | |
| Whisper | ✔ | Speech-to-Text | Supports API and local deployment |
| SenseVoice | ✔ | Speech-to-Text | Local deployment |
| OpenAI TTS API | ✔ | Text-to-Speech | |
| Fishaudio | ✔ | Text-to-Speech | Project involving GPT-Sovits author |
# 🦌 Roadmap
> [!TIP]
> Suggestions welcome via Issues <3
- [ ] Ensure feature parity across all platform adapters
- [ ] Optimize plugin APIs
- [ ] Add default TTS services (e.g., GPT-Sovits)
- [ ] Enhance chat features with persistent memory
- [ ] i18n Planning
## ❤️ Contributions
All Issues/PRs welcome! Simply submit your changes to this project :)
For major features, please discuss via Issues first.
## 🌟 Support
- Star this project!
- Support via [Afdian](https://afdian.com/a/soulter)
- WeChat support: [QR Code](https://drive.soulter.top/f/pYfA/d903f4fa49a496fda3f16d2be9e023b5.png)
## ✨ Demos
> [!NOTE]
> Code executor file I/O currently tested with Napcat(QQ)/Lagrange(QQ)
<div align='center'>
<img src="https://github.com/user-attachments/assets/4ee688d9-467d-45c8-99d6-368f9a8a92d8" width="600">
_✨ Docker-based Sandboxed Code Executor (Beta) ✨_
<img src="https://github.com/user-attachments/assets/0378f407-6079-4f64-ae4c-e97ab20611d2" height=500>
_✨ Multimodal Input, Web Search, Text-to-Image ✨_
<img src="https://github.com/user-attachments/assets/8ec12797-e70f-460a-959e-48eca39ca2bb" height=100>
_✨ Natural Language TODO Lists ✨_
<img src="https://github.com/user-attachments/assets/e137a9e1-340a-4bf2-bb2b-771132780735" height=150>
<img src="https://github.com/user-attachments/assets/480f5e82-cf6a-4955-a869-0d73137aa6e1" height=150>
_✨ Plugin System Showcase ✨_
<img src="https://github.com/user-attachments/assets/592a8630-14c7-4e06-b496-9c0386e4f36c" width=600>
_✨ Web Dashboard ✨_
![webchat](https://drive.soulter.top/f/vlsA/ezgif-5-fb044b2542.gif)
_✨ Built-in Web Chat Interface ✨_
</div>
## ⭐ Star History
> [!TIP]
> If this project helps you, please give it a star <3
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=soulter/astrbot&type=Date)](https://star-history.com/#soulter/astrbot&Date)
</div>
## Disclaimer
1. Licensed under `AGPL-v3`.
2. WeChat integration uses [Gewechat](https://github.com/Devo919/Gewechat). Use at your own risk with non-critical accounts.
3. Users must comply with local laws and regulations.
<!-- ## ✨ ATRI [Beta]
Available as plugin: [astrbot_plugin_atri](https://github.com/Soulter/astrbot_plugin_atri)
1. Qwen1.5-7B-Chat Lora model fine-tuned with ATRI character data
2. Long-term memory
3. Meme understanding & responses
4. TTS integration
-->
_私は、高性能ですから!_

View File

@@ -1,277 +0,0 @@
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
<div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh.md">简体中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.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_ja.md">日本語</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
<br>
<div>
<a href="https://trendshift.io/repositories/21369" target="_blank"><img src="https://trendshift.io/api/badge/repositories/21369" alt="AstrBotDevs%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=2" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
<br>
<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/badge/python-3.10+-blue.svg" alt="python">
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
<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%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFZIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></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>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20&label=Marketplace&cacheSeconds=3600">
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
</div>
<br>
<a href="https://astrbot.app/">Documentation</a>
<a href="https://blog.astrbot.app/">Blog</a>
<a href="https://astrbot.featurebase.app/roadmap">Feuille de route</a>
<a href="https://github.com/AstrBotDevs/AstrBot/issues">Signaler un problème</a>
<a href="mailto:community@astrbot.app">Email Support</a>
</div>
AstrBot est une plateforme de chatbot Agent tout-en-un open source qui s'intègre aux principales applications de messagerie instantanée. Elle fournit une infrastructure d'IA conversationnelle fiable et évolutive pour les particuliers, les développeurs et les équipes. Que vous construisiez un compagnon IA personnel, un service client intelligent, un assistant d'automatisation ou une base de connaissances d'entreprise, AstrBot vous permet de créer rapidement des applications d'IA prêtes pour la production dans les flux de travail de votre plateforme de messagerie.
![521771166-00782c4c-4437-4d97-aabc-605e3738da5c (1)](https://github.com/user-attachments/assets/61e7b505-f7db-41aa-a75f-4ef8f079b8ba)
## Fonctionnalités principales
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.
3. 🤖 Prise en charge de l'accès aux plateformes d'Agents telles que Dify, Alibaba Cloud Bailian, Coze, etc.
4. 🌐 Multiplateforme : supporte QQ, WeChat Enterprise, Feishu, DingTalk, Comptes officiels WeChat, Telegram, Slack et [plus encore](#plateformes-de-messagerie-prises-en-charge).
5. 📦 Extension par plugins, avec plus de 1000 plugins déjà 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.
7. 💻 Support WebUI.
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>🧩 1000+ 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éploiement en un clic
Pour les utilisateurs qui veulent découvrir AstrBot rapidement, qui sont familiers avec la ligne de commande et peuvent installer eux-mêmes l'environnement `uv`, nous recommandons la méthode de déploiement en un clic avec `uv` ⚡️ :
```bash
uv tool install astrbot --python 3.12
astrbot init # Exécutez cette commande uniquement la première fois pour initialiser l'environnement
astrbot run
```
> [uv](https://docs.astral.sh/uv/) doit être installé.
> AstrBot nécessite Python 3.12 ou une version plus récente. L'option `--python 3.12` garantit que `uv` crée l'environnement tool avec Python 3.12.
> [!NOTE]
> Pour les utilisateurs macOS : en raison des vérifications de sécurité de macOS, la première exécution de la commande `astrbot` peut prendre plus de temps (environ 10-20s).
Mettre à jour `astrbot` :
```bash
uv tool upgrade astrbot --python 3.12
```
> [!WARNING]
> AstrBot déployé via `uv` **ne prend pas en charge la mise à jour via le WebUI**. Pour mettre à jour, exécutez la commande ci-dessus depuis le terminal.
### Déploiement Docker
Pour les utilisateurs familiers avec les conteneurs et qui souhaitent une méthode plus stable et adaptée à la production, nous recommandons de déployer AstrBot avec Docker / Docker Compose.
Veuillez consulter la documentation officielle [Déployer AstrBot avec Docker](https://docs.astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot).
### Déployer sur RainYun
Pour les utilisateurs qui souhaitent déployer AstrBot en un clic sans gérer le serveur eux-mêmes, nous recommandons le service de déploiement cloud en un clic de RainYun ☁️ :
[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
### Déploiement de l'application de bureau
Pour les utilisateurs qui veulent utiliser AstrBot sur desktop et passer principalement par ChatUI, nous recommandons AstrBot App.
Accédez à [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop) pour télécharger et installer l'application ; cette méthode est conçue pour un usage desktop et n'est pas recommandée pour les scénarios serveur.
### Déploiement avec le lanceur
Également sur desktop, pour les utilisateurs qui souhaitent un déploiement rapide avec isolation d'environnement et multi-instances, nous recommandons AstrBot Launcher.
Accédez à [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) pour télécharger et installer.
### Déployer sur Replit
Le déploiement sur Replit est maintenu par la communauté et convient aux démonstrations en ligne et aux essais légers.
[![Run on Repl.it](https://repl.it/badge/github/AstrBotDevs/AstrBot)](https://repl.it/github/AstrBotDevs/AstrBot)
### AUR
Le mode AUR s'adresse aux utilisateurs Arch Linux qui préfèrent installer AstrBot via le gestionnaire de paquets système.
Exécutez la commande ci-dessous pour installer `astrbot-git`, puis lancez AstrBot localement.
```bash
yay -S astrbot-git
```
**Autres méthodes de déploiement**
Si vous avez besoin d'une gestion par panneau ou d'une personnalisation plus poussée, consultez [Déploiement BT-Panel](https://docs.astrbot.app/deploy/astrbot/btpanel.html) pour une installation via BT Panel, [Déploiement 1Panel](https://docs.astrbot.app/deploy/astrbot/1panel.html) pour le marketplace 1Panel, [Déploiement CasaOS](https://docs.astrbot.app/deploy/astrbot/casaos.html) pour un déploiement visuel sur NAS/serveur domestique, et [Déploiement manuel](https://docs.astrbot.app/deploy/astrbot/cli.html) pour une installation complète depuis les sources avec `uv`.
## Plateformes de messagerie prises en charge
Connectez AstrBot à vos plateformes de chat préférées.
| Plateforme | Maintenance |
|---------|---------------|
| QQ | Officielle |
| Implémentation du protocole OneBot v11 | Officielle |
| Telegram | Officielle |
| Application WeChat Work & Bot intelligent WeChat Work | Officielle |
| Service client WeChat & Comptes officiels WeChat | Officielle |
| Feishu (Lark) | Officielle |
| DingTalk | Officielle |
| Slack | Officielle |
| Discord | Officielle |
| LINE | Officielle |
| Satori | Officielle |
| KOOK | Officielle |
| Misskey | Officielle |
| Mattermost | Officielle |
| WhatsApp (Bientôt disponible) | Officielle |
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Communauté |
| [Rocket.Chat](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | Communauté |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Communauté |
## Services de modèles pris en charge
| Service | Type |
|---------|---------------|
| OpenAI et services compatibles | Services LLM |
| Anthropic | Services LLM |
| Google Gemini | Services LLM |
| Moonshot AI | Services LLM |
| Zhipu AI | Services LLM |
| DeepSeek | Services LLM |
| Ollama (Auto-hébergé) | Services LLM |
| LM Studio (Auto-hébergé) | Services LLM |
| [AIHubMix](https://aihubmix.com/?aff=4bfH) | Services LLM (Passerelle API, prend en charge tous les modèles) |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | Services LLM |
| [302.AI](https://share.302.ai/rr1M3l) | Services LLM |
| [TokenPony](https://www.tokenpony.cn/3YPyf) | Services LLM |
| [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | Services LLM |
| [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE) | Services LLM |
| ModelScope | Services LLM |
| OneAPI | Services LLM |
| Dify | Plateformes LLMOps |
| Applications Alibaba Cloud Bailian | Plateformes LLMOps |
| Coze | Plateformes LLMOps |
| OpenAI Whisper | Services de reconnaissance vocale |
| SenseVoice | Services de reconnaissance vocale |
| Xiaomi MiMo Omni | Services de reconnaissance vocale |
| OpenAI TTS | Services de synthèse vocale |
| Gemini TTS | Services de synthèse vocale |
| GPT-Sovits-Inference | Services de synthèse vocale |
| GPT-Sovits | Services de synthèse vocale |
| FishAudio | Services de synthèse vocale |
| Edge TTS | Services de synthèse vocale |
| Alibaba Cloud Bailian TTS | Services de synthèse vocale |
| Azure TTS | Services de synthèse vocale |
| Minimax TTS | Services de synthèse vocale |
| Xiaomi MiMo TTS | Services de synthèse vocale |
| Volcano Engine TTS | Services de synthèse vocale |
## ❤️ Contribuer
Les Issues et Pull Requests sont toujours les bienvenues ! N'hésitez pas à soumettre vos modifications à ce projet :)
### Comment contribuer
Vous pouvez contribuer en examinant les issues ou en aidant à la revue des pull requests. Toutes les issues ou PRs sont les bienvenues pour encourager la participation de la communauté. Bien sûr, ce ne sont que des suggestions - vous pouvez contribuer de la manière que vous souhaitez. Pour l'ajout de nouvelles fonctionnalités, veuillez d'abord en discuter via une Issue.
### Environnement de développement
AstrBot utilise `ruff` pour le formatage et le linting du code.
```bash
git clone https://github.com/AstrBotDevs/AstrBot
pip install pre-commit
pre-commit install
```
## 🌍 Communauté
### Groupes QQ
- Groupe 12 : 916228568 (nouveau)
- Groupe 9 : 1076659624 (complet)
- Groupe 10 : 1078079676 (complet)
- Groupe 11 : 704659519 (complet)
- Groupe 1 : 322154837 (complet)
- Groupe 3 : 630166526 (complet)
- Groupe 4 : 1077826412 (complet)
- Groupe 5 : 822130018 (complet)
- Groupe 6 : 753075035 (complet)
- Groupe 7 : 743746109 (complet)
- Groupe 8 : 1030353265 (complet)
- Groupe développeurs : 975206796
- Groupe développeurs (officiel) : 1039761811
### Serveur Discord
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
## ❤️ Remerciements spéciaux
Un grand merci à tous les contributeurs et développeurs de plugins pour leurs contributions à AstrBot ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=300&columns=15" />
</a>
De plus, la naissance de ce projet n'aurait pas été possible sans l'aide des projets open source suivants :
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - L'incroyable framework chat
## ⭐ Historique des étoiles
> [!TIP]
> Si ce projet vous a aidé dans votre vie ou votre travail, ou si vous êtes intéressé par son développement futur, veuillez donner une étoile au projet. C'est la force motrice derrière la maintenance de ce projet open source <3
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=astrbotdevs/astrbot&type=Date)](https://star-history.com/#astrbotdevs/astrbot&Date)
</div>
<div align="center">
_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>

View File

@@ -1,278 +1,167 @@
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
<p align="center">
![6e1279651f16d7fdf4727558b72bbaf1](https://github.com/user-attachments/assets/ead4c551-fc3c-48f7-a6f7-afbfdb820512)
</p>
<div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh.md">简体中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.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>
_✨ 簡単に使えるマルチプラットフォーム LLM チャットボットおよび開発フレームワーク ✨_
<br>
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<div>
<a href="https://trendshift.io/repositories/21369" target="_blank"><img src="https://trendshift.io/api/badge/repositories/21369" alt="AstrBotDevs%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=2" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
<br>
<div>
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest">
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/Soulter/AstrBot)](https://github.com/Soulter/AstrBot/releases/latest)
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
<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%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFZIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0LjYxNTZDNS4zMTUwMiAxNC4zOTk5IDUuNjAxNTYgMTQuMTEzNCA1LjYwMTU2IDEzLjc1OTlWMTEuMDM5OUM1LjYwMTU2IDEwLjY4NjQgNS4zMTUwMiAxMC4zOTk5IDQuOTYxNTYgMTAuMzk5OVoiIGZpbGw9IiNmZmYiLz4KPHBhdGggZD0iTTEzLjc1ODQgMS42MDAxSDExLjAzODRDMTAuNjg1IDEuNjAwMSAxMC4zOTg0IDEuODg2NjQgMTAuMzk4NCAyLjI0MDFWNC45NjAxQzEwLjM5ODQgNS4zMTM1NiAxMC42ODUgNS42MDAxIDExLjAzODQgNS42MDAxSDEzLjc1ODRDMTQuMTExOSA1LjYwMDEgMTQuMzk4NCA1LjMxMzU2IDE0LjM5ODQgNC45NjAxVjIuMjQwMUMxNC4zOTg0IDEuODg2NjQgMTQuMTExOSAxLjYwMDEgMTMuNzU4NCAxLjYwMDFZIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDRMNCAxMlpFIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></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>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20&label=%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%83%9E%E3%83%BC%E3%82%B1%E3%83%83%E3%83%88&cacheSeconds=3600">
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg"/></a>
<img alt="Static Badge" src="https://img.shields.io/badge/QQ群-630166526-purple">
[![wakatime](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e.svg)](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
![Dynamic JSON Badge](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fstats&query=v&label=7%E6%97%A5%E6%B6%88%E6%81%AF%E4%B8%8A%E8%A1%8C%E9%87%8F&cacheSeconds=3600)
[![codecov](https://codecov.io/gh/Soulter/AstrBot/graph/badge.svg?token=FF3P5967B8)](https://codecov.io/gh/Soulter/AstrBot)
<a href="https://astrbot.app/">ドキュメントを見る</a>
<a href="https://github.com/Soulter/AstrBot/issues">問題を報告する</a>
</div>
<br>
AstrBot は、疎結合、非同期、複数のメッセージプラットフォームに対応したデプロイ、使いやすいプラグインシステム、および包括的な大規模言語モデルLLM接続機能を備えたチャットボットおよび開発フレームワークです。
<a href="https://astrbot.app/">ドキュメント</a>
<a href="https://blog.astrbot.app/">Blog</a>
<a href="https://astrbot.featurebase.app/roadmap">ロードマップ</a>
<a href="https://github.com/AstrBotDevs/AstrBot/issues">Issue</a>
<a href="mailto:community@astrbot.app">Email Support</a>
</div>
## ✨ 主な機能
AstrBot は、主要なインスタントメッセージングアプリと統合できるオープンソースのオールインワン Agent チャットボットプラットフォームです。個人、開発者、チームに信頼性が高くスケーラブルな会話型 AI インフラストラクチャを提供します。パーソナル AI コンパニオン、インテリジェントカスタマーサービス、オートメーションアシスタント、エンタープライズナレッジベースなど、AstrBot を使用すると、IM プラットフォームのワークフロー内で本番環境対応の AI アプリケーションを迅速に構築できます。
1. **大規模言語モデルの対話**。OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM など、さまざまな大規模言語モデルをサポートし、Ollama、LLMTuner を介してローカルにデプロイされた大規模モデルをサポートします。多輪対話、人格シナリオ、多モーダル機能を備え、画像理解、音声からテキストへの変換Whisperをサポートします。
2. **複数のメッセージプラットフォームの接続**。QQOneBot、QQ チャンネル、Feishu、Telegram への接続をサポートします。今後、DingTalk、Discord、WhatsApp、Xiaoai 音響をサポートする予定です。レート制限、ホワイトリスト、キーワードフィルタリング、Baidu コンテンツ監査をサポートします。
3. **エージェント**。一部のエージェント機能をネイティブにサポートし、コードエグゼキューター、自然言語タスク、ウェブ検索などを提供します。[Dify プラットフォーム](https://dify.ai/)と連携し、Dify スマートアシスタント、ナレッジベース、Dify ワークフローを簡単に接続できます。
4. **プラグインの拡張**。深く最適化されたプラグインメカニズムを備え、[プラグインの開発](https://astrbot.app/dev/plugin.html)をサポートし、機能を拡張できます。複数のプラグインのインストールをサポートします。
5. **ビジュアル管理パネル**。設定の視覚的な変更、プラグイン管理、ログの表示などをサポートし、設定の難易度を低減します。WebChat を統合し、パネル上で大規模モデルと対話できます。
6. **高い安定性と高いモジュール性**。イベントバスとパイプラインに基づくアーキテクチャ設計により、高度にモジュール化され、低結合です。
![screenshot_1 5x_postspark_2026-02-27_22-37-45](https://github.com/user-attachments/assets/f17cdb90-52d7-4773-be2e-ff64b566af6b)
> [!TIP]
> 管理パネルのオンラインデモを体験する: [https://demo.astrbot.app/](https://demo.astrbot.app/)
>
> ユーザー名: `astrbot`, パスワード: `astrbot`。LLM が設定されていないため、チャットページで大規模モデルを使用することはできません。(デモのログインパスワードを変更しないでください 😭)
## 主な機能
## ✨ 使用方法
1. 💯 無料 & オープンソース。
2. ✨ AI大規模言語モデル対話、マルチモーダル、Agent、MCP、Skills、ナレッジベース、ペルソナ設定、対話の自動圧縮。
3. 🤖 Dify、Alibaba Cloud Bailian百煉、Coze などのAgentプラットフォームへの接続をサポート。
4. 🌐 マルチプラットフォームQQ、企業微信WeCom、飛書Lark、釘釘DingTalk、WeChat公式アカウント、Telegram、Slack、[その他](#サポートされているメッセージプラットフォーム)に対応。
5. 📦 プラグイン拡張1000を超える既存プラグインをワンクリックでインストール可能。
6. 🛡️ 隔離環境[Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html)コードの安全な実行、Shell呼び出し、セッションレベルのリソース再利用。
7. 💻 WebUI 対応。
8. 🌈 Web ChatUI 対応ChatUI内にAgent Sandboxやウェブ検索などを内蔵。
9. 🌐 多言語対応i18n
#### Docker デプロイ
<br>
公式ドキュメント [Docker を使用して AstrBot をデプロイする](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) を参照してください。
<table align="center">
<tr align="center">
<th>💙 ロールプレイ & 感情的な対話</th>
<th>✨ プロアクティブ・エージェント (Proactive Agent)</th>
<th>🚀 汎用 エージェント的能力</th>
<th>🧩 1000+ コミュニティプラグイン</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>
#### Windows ワンクリックインストーラーのデプロイ
## クイックスタート
コンピュータに Python>3.10)がインストールされている必要があります。公式ドキュメント [Windows ワンクリックインストーラーを使用して AstrBot をデプロイする](https://astrbot.app/deploy/astrbot/windows.html) を参照してください。
### ワンクリックデプロイ
#### Replit デプロイ
AstrBot を素早く試したいユーザーで、コマンドラインに慣れており `uv` 環境を自分でインストールできる場合は、`uv` のワンクリックデプロイをおすすめします ⚡️:
[![Run on Repl.it](https://repl.it/badge/github/Soulter/AstrBot)](https://repl.it/github/Soulter/AstrBot)
```bash
uv tool install astrbot --python 3.12
astrbot init # 初回のみ実行して環境を初期化します
astrbot run
```
#### CasaOS デプロイ
> [uv](https://docs.astral.sh/uv/) のインストールが必要です。
> AstrBot には Python 3.12 以降が必要です。`--python 3.12` を指定すると、`uv` は Python 3.12 で tool 環境を作成します。
コミュニティが提供するデプロイ方法です。
公式ドキュメント [ソースコードを使用して AstrBot をデプロイする](https://astrbot.app/deploy/astrbot/casaos.html) を参照してください。
#### 手動デプロイ
公式ドキュメント [ソースコードを使用して AstrBot をデプロイする](https://astrbot.app/deploy/astrbot/cli.html) を参照してください。
## ⚡ メッセージプラットフォームのサポート状況
| プラットフォーム | サポート状況 | 詳細 | メッセージタイプ |
| -------- | ------- | ------- | ------ |
| QQ(公式ロボットインターフェース) | ✔ | プライベートチャット、グループチャット、QQ チャンネルプライベートチャット、グループチャット | テキスト、画像 |
| QQ(OneBot) | ✔ | プライベートチャット、グループチャット | テキスト、画像、音声 |
| WeChat(個人アカウント) | ✔ | WeChat 個人アカウントのプライベートチャット、グループチャット | テキスト、画像、音声 |
| [Telegram](https://github.com/Soulter/astrbot_plugin_telegram) | ✔ | プライベートチャット、グループチャット | テキスト、画像 |
| [WeChat(企業 WeChat)](https://github.com/Soulter/astrbot_plugin_wecom) | ✔ | プライベートチャット | テキスト、画像、音声 |
| Feishu | ✔ | グループチャット | テキスト、画像 |
| WeChat 対話オープンプラットフォーム | 🚧 | 計画中 | - |
| Discord | 🚧 | 計画中 | - |
| WhatsApp | 🚧 | 計画中 | - |
| Xiaoai 音響 | 🚧 | 計画中 | - |
# 🦌 今後のロードマップ
> [!TIP]
> Issue でさらに多くの提案を歓迎します <3
- [ ] 現在のすべてのプラットフォームアダプターの機能の一貫性を確保し、改善する
- [ ] プラグインインターフェースの最適化
- [ ] GPT-Sovits などの TTS サービスをデフォルトでサポート
- [ ] "チャット強化" 部分を完成させ、永続的な記憶をサポート
- [ ] i18n の計画
## ❤️ 貢献
Issue や Pull Request を歓迎します!このプロジェクトに変更を加えるだけです :)
新機能の追加については、まず Issue で議論してください。
## 🌟 サポート
- このプロジェクトに Star を付けてください!
- [愛発電](https://afdian.com/a/soulter)で私をサポートしてください!
- [WeChat](https://drive.soulter.top/f/pYfA/d903f4fa49a496fda3f16d2be9e023b5.png)で私をサポートしてください~
## ✨ デモ
> [!NOTE]
> macOS ユーザーの場合macOS のセキュリティチェックにより、`astrbot` コマンドの初回実行に時間がかかる場合があります(約 10〜20 秒)。
> コードエグゼキューターのファイル入力/出力は現在 Napcat(QQ)、Lagrange(QQ) でのみテストされています
`astrbot` の更新:
<div align='center'>
```bash
uv tool upgrade astrbot --python 3.12
```
<img src="https://github.com/user-attachments/assets/4ee688d9-467d-45c8-99d6-368f9a8a92d8" width="600">
> [!WARNING]
> `uv` 経由でデプロイした AstrBot は、**WebUI からのバージョンアップグレードに対応していません**。更新するには、上記のコマンドをコマンドラインで実行してください。
_✨ Docker ベースのサンドボックス化されたコードエグゼキューター(ベータテスト中)✨_
### Docker デプロイ
<img src="https://github.com/user-attachments/assets/0378f407-6079-4f64-ae4c-e97ab20611d2" height=500>
コンテナ運用に慣れており、より安定した本番向けのデプロイ方法を求めるユーザーには、Docker / Docker Compose での AstrBot デプロイをおすすめします。
_✨ 多モーダル、ウェブ検索、長文の画像変換(設定可能)✨_
公式ドキュメント [Docker を使用した AstrBot のデプロイ](https://docs.astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) をご参照ください。
<img src="https://github.com/user-attachments/assets/8ec12797-e70f-460a-959e-48eca39ca2bb" height=100>
### 雨云でのデプロイ
_✨ 自然言語タスク ✨_
AstrBot をワンクリックでデプロイしたく、サーバーを自分で管理したくないユーザーには、雨云のワンクリッククラウドデプロイサービスをおすすめします ☁️:
<img src="https://github.com/user-attachments/assets/e137a9e1-340a-4bf2-bb2b-771132780735" height=150>
<img src="https://github.com/user-attachments/assets/480f5e82-cf6a-4955-a869-0d73137aa6e1" height=150>
[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
_✨ プラグインシステム - 一部のプラグインの展示 ✨_
### デスクトップアプリのデプロイ
<img src="https://github.com/user-attachments/assets/592a8630-14c7-4e06-b496-9c0386e4f36c" width="600">
デスクトップで AstrBot を使い、主に ChatUI を入口として利用するユーザーには、AstrBot App をおすすめします。
_✨ 管理パネル ✨_
[AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop) からダウンロードしてインストールしてください。この方式はデスクトップ向けであり、サーバー用途には推奨されません。
![webchat](https://drive.soulter.top/f/vlsA/ezgif-5-fb044b2542.gif)
### ランチャーのデプロイ
_✨ 内蔵 Web Chat、オンラインでボットと対話 ✨_
同じくデスクトップで、素早くデプロイしつつ環境を分離して多重起動したいユーザーには、AstrBot Launcher をおすすめします。
[AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) からダウンロードしてインストールしてください。
### Replit でのデプロイ
Replit デプロイはコミュニティ提供の方式で、オンラインデモや軽量な試用に向いています。
[![Run on Repl.it](https://repl.it/badge/github/AstrBotDevs/AstrBot)](https://repl.it/github/AstrBotDevs/AstrBot)
### AUR
AUR 方式は Arch Linux ユーザー向けで、システムのパッケージ運用に合わせて AstrBot を導入したい場合に適しています。
次のコマンドで `astrbot-git` をインストールし、ローカル環境で AstrBot を起動してください。
```bash
yay -S astrbot-git
```
**その他のデプロイ方法**
パネル操作での導入やより高度なカスタマイズが必要な場合は、[宝塔パネルデプロイ](https://docs.astrbot.app/deploy/astrbot/btpanel.html)BT Panel 経由の導入)、[1Panel デプロイ](https://docs.astrbot.app/deploy/astrbot/1panel.html)1Panel アプリマーケット経由)、[CasaOS デプロイ](https://docs.astrbot.app/deploy/astrbot/casaos.html)NAS / ホームサーバー向け可視化導入)、[手動デプロイ](https://docs.astrbot.app/deploy/astrbot/cli.html)`uv` とソースベースのフルカスタム導入)を参照してください。
## サポートされているメッセージプラットフォーム
AstrBot をよく使うチャットプラットフォームに接続できます。
| プラットフォーム | 保守 |
|---------|---------------|
| QQ | 公式 |
| OneBot v11 プロトコル実装 | 公式 |
| Telegram | 公式 |
| WeChat Work アプリケーション & WeChat Work インテリジェントボット | 公式 |
| WeChat カスタマーサービス & WeChat 公式アカウント | 公式 |
| Feishu (Lark) | 公式 |
| DingTalk | 公式 |
| Slack | 公式 |
| Discord | 公式 |
| LINE | 公式 |
| Satori | 公式 |
| KOOK | 公式 |
| Misskey | 公式 |
| Mattermost | 公式 |
| WhatsApp (近日対応予定) | 公式 |
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | コミュニティ |
| [Rocket.Chat](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | コミュニティ |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | コミュニティ |
## サポートされているモデルサービス
| サービス | 種類 |
|---------|---------------|
| OpenAI および互換サービス | 大規模言語モデルサービス |
| Anthropic | 大規模言語モデルサービス |
| Google Gemini | 大規模言語モデルサービス |
| Moonshot AI | 大規模言語モデルサービス |
| 智谱 AI | 大規模言語モデルサービス |
| DeepSeek | 大規模言語モデルサービス |
| Ollama (セルフホスト) | 大規模言語モデルサービス |
| LM Studio (セルフホスト) | 大規模言語モデルサービス |
| [AIHubMix](https://aihubmix.com/?aff=4bfH) | 大規模言語モデルサービスAPIゲートウェイ、全モデル対応 |
| [優云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | 大規模言語モデルサービス |
| [302.AI](https://share.302.ai/rr1M3l) | 大規模言語モデルサービス |
| [小馬算力](https://www.tokenpony.cn/3YPyf) | 大規模言語モデルサービス |
| [硅基流動](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | 大規模言語モデルサービス |
| [PPIO 派欧云](https://ppio.com/user/register?invited_by=AIOONE) | 大規模言語モデルサービス |
| ModelScope | 大規模言語モデルサービス |
| OneAPI | 大規模言語モデルサービス |
| Dify | LLMOps プラットフォーム |
| Alibaba Cloud 百炼アプリケーション | LLMOps プラットフォーム |
| Coze | LLMOps プラットフォーム |
| OpenAI Whisper | 音声認識サービス |
| SenseVoice | 音声認識サービス |
| Xiaomi MiMo Omni | 音声認識サービス |
| OpenAI TTS | 音声合成サービス |
| Gemini TTS | 音声合成サービス |
| GPT-Sovits-Inference | 音声合成サービス |
| GPT-Sovits | 音声合成サービス |
| FishAudio | 音声合成サービス |
| Edge TTS | 音声合成サービス |
| Alibaba Cloud 百炼 TTS | 音声合成サービス |
| Azure TTS | 音声合成サービス |
| Minimax TTS | 音声合成サービス |
| Xiaomi MiMo TTS | 音声合成サービス |
| Volcano Engine TTS | 音声合成サービス |
## ❤️ コントリビューション
Issue や Pull Request は大歓迎です!このプロジェクトに変更を送信してください :)
### コントリビュート方法
Issue を確認したり、PR(プルリクエスト)のレビューを手伝うことで貢献できます。どんな Issue や PR への参加も歓迎され、コミュニティ貢献を促進します。もちろん、これらは提案に過ぎず、どんな方法でも貢献できます。新機能の追加については、まず Issue で議論してください。
### 開発環境
AstrBot はコードのフォーマットとチェックに `ruff` を使用しています。
```bash
git clone https://github.com/AstrBotDevs/AstrBot
pip install pre-commit
pre-commit install
```
## 🌍 コミュニティ
### QQ グループ
- 12群: 916228568 (新)
- 9群: 1076659624 (満員)
- 10群: 1078079676 (満員)
- 11群: 704659519 (満員)
- 1群: 322154837 (満員)
- 3群: 630166526 (満員)
- 4群: 1077826412 (満員)
- 5群: 822130018 (満員)
- 6群: 753075035 (満員)
- 7群: 743746109 (満員)
- 8群: 1030353265 (満員)
- 開発者群: 975206796
- 開発者群(正式): 1039761811
### Discord サーバー
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
## ❤️ Special Thanks
AstrBot への貢献をしていただいたすべてのコントリビューターとプラグイン開発者に特別な感謝を ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=300&columns=15" />
</a>
また、このプロジェクトの誕生は以下のオープンソースプロジェクトの助けなしには実現できませんでした:
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 素晴らしい猫猫フレームワーク
</div>
## ⭐ Star History
> [!TIP]
> このプロジェクトがあなたの生活や仕事に役立ったり、このプロジェクトの今後の発展に関心がある場合は、プロジェクトに Star をください。これこのオープンソースプロジェクトを維持する原動力です <3
> このプロジェクトがあなたの生活や仕事に役立った場合、またはこのプロジェクトの将来の発展に関心がある場合は、プロジェクトに Star を付けてください。これこのオープンソースプロジェクトを維持するためのモチベーションです <3
<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=soulter/astrbot&type=Date)](https://star-history.com/#soulter/astrbot&Date)
</div>
<div align="center">
## スポンサー
_共感力と能力は決して対立するものではありません。私たちが目指すのは、感情を理解し、心の支えとなるだけでなく、確実に仕事をこなせるロボットの創造です。_
[<img src="https://api.gitsponsors.com/api/badge/img?id=575865240" height="20">](https://api.gitsponsors.com/api/badge/link?p=XEpbdGxlitw/RbcwiTX93UMzNK/jgDYC8NiSzamIPMoKvG2lBFmyXhSS/b0hFoWlBBMX2L5X5CxTDsUdyvcIEHTOfnkXz47UNOZvMwyt5CzbYpq0SEzsSV1OJF1cCo90qC/ZyYKYOWedal3MhZ3ikw==)
## 免責事項
1. このプロジェクトは `AGPL-v3` オープンソースライセンスの下で保護されています。
2. このプロジェクトを使用する際は、現地の法律および規制を遵守してください。
<!-- ## ✨ ATRI [ベータテスト]
この機能はプラグインとしてロードされます。プラグインリポジトリのアドレス:[astrbot_plugin_atri](https://github.com/Soulter/astrbot_plugin_atri)
1. 《ATRI ~ My Dear Moments》の主人公 ATRI のキャラクターセリフを微調整データセットとして使用した `Qwen1.5-7B-Chat Lora` 微調整モデル。
2. 長期記憶
3. ミームの理解と返信
4. TTS
-->
_私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div>

View File

@@ -1,278 +0,0 @@
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
<div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh.md">简体中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.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_ja.md">日本語</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
<br>
<div>
<a href="https://trendshift.io/repositories/21369" target="_blank"><img src="https://trendshift.io/api/badge/repositories/21369" alt="AstrBotDevs%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=2" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
<br>
<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/badge/python-3.10+-blue.svg" alt="python">
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
<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%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFZIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjczODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></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>
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20&label=%D0%9C%D0%B0%D1%80%D0%BA%D0%B5%D1%82%D0%BF%D0%BB%D0%B5%D0%B9%D1%81&cacheSeconds=3600">
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
</div>
<br>
<a href="https://astrbot.app/">Документация</a>
<a href="https://blog.astrbot.app/">Блог</a>
<a href="https://astrbot.featurebase.app/roadmap">Дорожная карта</a>
<a href="https://github.com/AstrBotDevs/AstrBot/issues">Сообщить о проблеме</a>
<a href="mailto:community@astrbot.app">Email Support</a>
</div>
AstrBot — это универсальная платформа Agent-чатботов с открытым исходным кодом, которая интегрируется с основными приложениями для обмена мгновенными сообщениями. Она предоставляет надёжную и масштабируемую инфраструктуру разговорного ИИ для частных лиц, разработчиков и команд. Будь то персональный ИИ-компаньон, интеллектуальная служба поддержки, автоматизированный помощник или корпоративная база знаний — AstrBot позволяет быстро создавать готовые к использованию ИИ-приложения в рабочих процессах вашей платформы обмена сообщениями.
![521771166-00782c4c-4437-4d97-aabc-605e3738da5c (1)](https://github.com/user-attachments/assets/61e7b505-f7db-41aa-a75f-4ef8f079b8ba)
## Основные возможности
1. 💯 Бесплатно & Открытый исходный код.
2. ✨ Диалоги с ИИ-моделями, мультимодальность, Agent, MCP, Skills, База знаний, Настройка личности, автоматическое сжатие диалогов.
3. 🤖 Поддержка интеграции с платформами Agents, такими как Dify, Alibaba Cloud Bailian, Coze и др.
4. 🌐 Мультиплатформенность: поддержка QQ, WeChat для предприятий, Feishu, DingTalk, публичных аккаунтов WeChat, Telegram, Slack и [других](#Поддерживаемые-платформы-обмена-сообщениями).
5. 📦 Расширение плагинами: доступно более 1000 плагинов для установки в один клик.
6. 🛡️ Изолированная среда[Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html): безопасное выполнение любого кода, вызов Shell, повторное использование ресурсов на уровне сессии.
7. 💻 Поддержка WebUI.
8. 🌈 Поддержка Web ChatUI: встроенная песочница агента, веб-поиск и др.
9. 🌐 Поддержка интернационализации (i18n).
<br>
<table align="center">
<tr align="center">
<th>💙 Ролевые игры & Эмоциональная поддержка</th>
<th>✨ Проактивный Агент (Agent)</th>
<th>🚀 Универсальные возможности Агента</th>
<th>🧩 1000+ плагинов сообщества</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>
## Быстрый старт
### Развёртывание в один клик
Для пользователей, которые хотят быстро попробовать AstrBot, знакомы с командной строкой и могут самостоятельно установить окружение `uv`, мы рекомендуем использовать развёртывание в один клик через `uv` ⚡️:
```bash
uv tool install astrbot --python 3.12
astrbot init # Выполните эту команду только при первом запуске для инициализации окружения
astrbot run
```
> Требуется установленный [uv](https://docs.astral.sh/uv/).
> Для AstrBot требуется Python 3.12 или новее. Параметр `--python 3.12` гарантирует, что `uv` создаст tool-окружение с Python 3.12.
> [!NOTE]
> Для пользователей macOS: из-за проверок безопасности macOS первый запуск команды `astrbot` может занять больше времени (около 10-20 секунд).
Обновить `astrbot`:
```bash
uv tool upgrade astrbot --python 3.12
```
> [!WARNING]
> AstrBot, развёрнутый через `uv`, **не поддерживает обновление через WebUI**. Для обновления выполните указанную выше команду из командной строки.
### Развёртывание Docker
Для пользователей, знакомых с контейнерами и которым нужен более стабильный и подходящий для production способ, мы рекомендуем разворачивать AstrBot через Docker / Docker Compose.
См. официальную документацию [Развёртывание AstrBot с Docker](https://docs.astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot).
### Развёртывание на RainYun
Для пользователей, которые хотят развернуть AstrBot в один клик и не хотят самостоятельно управлять сервером, мы рекомендуем облачный сервис развёртывания в один клик от RainYun ☁️:
[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
### Развёртывание десктопного приложения
Для пользователей, которые хотят использовать AstrBot на десктопе и в основном работают через ChatUI, мы рекомендуем AstrBot App.
Перейдите в [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop), скачайте и установите приложение; этот вариант предназначен для десктопа и не рекомендуется для серверных сценариев.
### Развёртывание через лаунчер
Также на десктопе, для пользователей, которым нужен быстрый запуск и мультиинстанс с изоляцией окружений, мы рекомендуем AstrBot Launcher.
Перейдите в [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher), чтобы скачать и установить.
### Развёртывание на Replit
Развёртывание через Replit поддерживается сообществом и подходит для онлайн-демо и лёгких тестовых запусков.
[![Run on Repl.it](https://repl.it/badge/github/AstrBotDevs/AstrBot)](https://repl.it/github/AstrBotDevs/AstrBot)
### AUR
AUR-вариант предназначен для пользователей Arch Linux, которым удобна установка через системный менеджер пакетов.
Выполните команду ниже для установки `astrbot-git`, затем запустите AstrBot локально.
```bash
yay -S astrbot-git
```
**Другие способы развёртывания**
Если вам нужна панельная установка или более глубокая кастомизация, смотрите [Развёртывание BT-Panel](https://docs.astrbot.app/deploy/astrbot/btpanel.html) (установка через BT Panel), [Развёртывание 1Panel](https://docs.astrbot.app/deploy/astrbot/1panel.html) (развёртывание через маркетплейс 1Panel), [Развёртывание CasaOS](https://docs.astrbot.app/deploy/astrbot/casaos.html) (визуальный вариант для NAS и домашних серверов) и [Ручное развёртывание](https://docs.astrbot.app/deploy/astrbot/cli.html) (полностью настраиваемая установка из исходников через `uv`).
## Поддерживаемые платформы обмена сообщениями
Подключите AstrBot к вашим любимым чат-платформам.
| Платформа | Поддержка |
|---------|---------------|
| QQ | Официальная |
| Реализация протокола OneBot v11 | Официальная |
| Telegram | Официальная |
| Приложение WeChat Work и интеллектуальный бот WeChat Work | Официальная |
| Служба поддержки WeChat и официальные аккаунты WeChat | Официальная |
| Feishu (Lark) | Официальная |
| DingTalk | Официальная |
| Slack | Официальная |
| Discord | Официальная |
| LINE | Официальная |
| Satori | Официальная |
| KOOK | Официальная |
| Misskey | Официальная |
| Mattermost | Официальная |
| WhatsApp (Скоро) | Официальная |
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Сообщество |
| [Rocket.Chat](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | Сообщество |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Сообщество |
## Поддерживаемые сервисы моделей
| Сервис | Тип |
|---------|---------------|
| OpenAI и совместимые сервисы | Сервисы LLM |
| Anthropic | Сервисы LLM |
| Google Gemini | Сервисы LLM |
| Moonshot AI | Сервисы LLM |
| Zhipu AI | Сервисы LLM |
| DeepSeek | Сервисы LLM |
| Ollama (Самостоятельное размещение) | Сервисы LLM |
| LM Studio (Самостоятельное размещение) | Сервисы LLM |
| [AIHubMix](https://aihubmix.com/?aff=4bfH) | Сервисы LLM (API-шлюз, поддерживает все модели) |
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | Сервисы LLM |
| [302.AI](https://share.302.ai/rr1M3l) | Сервисы LLM |
| [TokenPony](https://www.tokenpony.cn/3YPyf) | Сервисы LLM |
| [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | Сервисы LLM |
| [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE) | Сервисы LLM |
| ModelScope | Сервисы LLM |
| OneAPI | Сервисы LLM |
| Dify | Платформы LLMOps |
| Приложения Alibaba Cloud Bailian | Платформы LLMOps |
| Coze | Платформы LLMOps |
| OpenAI Whisper | Сервисы распознавания речи |
| SenseVoice | Сервисы распознавания речи |
| Xiaomi MiMo Omni | Сервисы распознавания речи |
| OpenAI TTS | Сервисы синтеза речи |
| Gemini TTS | Сервисы синтеза речи |
| GPT-Sovits-Inference | Сервисы синтеза речи |
| GPT-Sovits | Сервисы синтеза речи |
| FishAudio | Сервисы синтеза речи |
| Edge TTS | Сервисы синтеза речи |
| Alibaba Cloud Bailian TTS | Сервисы синтеза речи |
| Azure TTS | Сервисы синтеза речи |
| Minimax TTS | Сервисы синтеза речи |
| Xiaomi MiMo TTS | Сервисы синтеза речи |
| Volcano Engine TTS | Сервисы синтеза речи |
## ❤️ Вклад в проект
Issues и Pull Request всегда приветствуются! Не стесняйтесь отправлять свои изменения в этот проект :)
### Как внести вклад
Вы можете внести вклад, просматривая issues или помогая с ревью pull request. Любые issues или PR приветствуются для поощрения участия сообщества. Конечно, это лишь предложения — вы можете вносить вклад любым удобным для вас способом. Для добавления новых функций сначала обсудите это через Issue.
### Среда разработки
AstrBot использует `ruff` для форматирования и линтинга кода.
```bash
git clone https://github.com/AstrBotDevs/AstrBot
pip install pre-commit
pre-commit install
```
## 🌍 Сообщество
### Группы QQ
- Группа 12: 916228568 (новая)
- Группа 9: 1076659624 (полная)
- Группа 10: 1078079676 (полная)
- Группа 11: 704659519 (полная)
- Группа 1: 322154837 (полная)
- Группа 3: 630166526 (полная)
- Группа 4: 1077826412 (полная)
- Группа 5: 822130018 (полная)
- Группа 6: 753075035 (полная)
- Группа 7: 743746109 (полная)
- Группа 8: 1030353265 (полная)
- Группа разработчиков: 975206796
- Группа разработчиков (официальная): 1039761811
### Сервер Discord
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
## ❤️ Особая благодарность
Особая благодарность всем контрибьюторам и разработчикам плагинов за их вклад в AstrBot ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=300&columns=15" />
</a>
Кроме того, рождение этого проекта было бы невозможно без помощи следующих проектов с открытым исходным кодом:
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - Замечательный кошачий фреймворк
## ⭐ История звёзд
> [!TIP]
> Если этот проект помог вам в жизни или работе, или если вас интересует его будущее развитие, пожалуйста, поставьте проекту звезду. Это движущая сила поддержки этого проекта с открытым исходным кодом <3
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=astrbotdevs/astrbot&type=Date)](https://star-history.com/#astrbotdevs/astrbot&Date)
</div>
<div align="center">
_Сопровождение и способности никогда не должны быть противоположностями. Мы стремимся создать робота, который сможет как понимать эмоции, оказывать душевную поддержку, так и надёжно выполнять работу._
_私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div>

View File

@@ -1,277 +0,0 @@
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
<div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh.md">简体中文</a>
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.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>
<div>
<a href="https://trendshift.io/repositories/21369" target="_blank"><img src="https://trendshift.io/api/badge/repositories/21369" alt="AstrBotDevs%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=2" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
<br>
<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/badge/python-3.10+-blue.svg" alt="python">
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
<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://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?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://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
</div>
<br>
<a href="https://astrbot.app/">文件</a>
<a href="https://blog.astrbot.app/">Blog</a>
<a href="https://astrbot.featurebase.app/roadmap">路線圖</a>
<a href="https://github.com/AstrBotDevs/AstrBot/issues">問題回報</a>
<a href="mailto:community@astrbot.app">Email</a>
</div>
AstrBot 是一個開源的一站式 Agent 聊天機器人平台,可接入主流即時通訊軟體,為個人、開發者和團隊打造可靠、可擴展的對話式智慧基礎設施。無論是個人 AI 夥伴、智慧客服、自動化助手還是企業知識庫AstrBot 都能在您的即時通訊軟體平台的工作流程中快速構建生產可用的 AI 應用程式。
![screenshot_1 5x_postspark_2026-02-27_22-37-45](https://github.com/user-attachments/assets/f17cdb90-52d7-4773-be2e-ff64b566af6b)
## 主要功能
1. 💯 免費 & 開源。
2. ✨ AI 大模型對話多模態AgentMCPSkills知識庫人格設定自動壓縮對話。
3. 🤖 支援接入 Dify、阿里雲百煉、Coze 等智慧體 (Agent) 平台。
4. 🌐 多平台,支援 QQ、企業微信、飛書、釘釘、微信公眾號、Telegram、Slack 以及[更多](#支援的訊息平台)。
5. 📦 插件擴展,已有 1000+ 個插件可一鍵安裝。
6. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) 隔離化環境,安全地執行任何代碼、調用 Shell、會話級資源複用。
7. 💻 WebUI 支援。
8. 🌈 Web ChatUI 支援ChatUI 內置代理沙盒 (Agent Sandbox)、網頁搜尋等。
9. 🌐 國際化i18n支援。
<br>
<table align="center">
<tr align="center">
<th>💙 角色扮演 & 情感陪伴</th>
<th>✨ 主動式 Agent</th>
<th>🚀 通用 Agentic 能力</th>
<th>🧩 1000+ 社區外掛程式</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>
## 快速開始
### 一鍵部署
對於想快速體驗 AstrBot、且熟悉命令列並能自行安裝 `uv` 環境的使用者,我們推薦使用 `uv` 一鍵部署方式 ⚡️。
```bash
uv tool install astrbot --python 3.12
astrbot init # 僅首次執行此命令以初始化環境
astrbot run
```
> 需要安裝 [uv](https://docs.astral.sh/uv/)。
> AstrBot 需要 Python 3.12 或更高版本。`--python 3.12` 會確保 `uv` 使用 Python 3.12 建立 tool 環境。
> [!NOTE]
> 對於 macOS 使用者:由於 macOS 安全性檢查,首次執行 `astrbot` 指令可能需要較長時間(約 10-20 秒)。
更新 `astrbot`
```bash
uv tool upgrade astrbot --python 3.12
```
> [!WARNING]
> 透過 `uv` 部署的 AstrBot **不支援在 WebUI 中進行版本升級**。如需更新,請透過命令列執行上述命令。
### Docker 部署
對於熟悉容器、希望獲得更穩定且更適合正式環境部署方式的使用者,我們推薦使用 Docker / Docker Compose 部署 AstrBot。
請參考官方文件 [使用 Docker 部署 AstrBot](https://docs.astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot)。
### 在雨雲上部署
對於希望一鍵部署 AstrBot 且不想自行管理伺服器的使用者,我們推薦使用雨雲的一鍵雲端部署服務 ☁️:
[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
### 桌面客戶端部署
對於希望在桌面端使用 AstrBot、並以 ChatUI 為主要入口的使用者,我們推薦使用 AstrBot App。
前往 [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop) 下載並安裝;此方式面向桌面使用,不建議伺服器場景。
### 啟動器部署
同樣在桌面端,對於希望快速部署並實現環境隔離多開的使用者,我們推薦使用 AstrBot Launcher。
前往 [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) 下載並安裝。
### 在 Replit 上部署
Replit 部署由社群維護,適合線上示範與輕量試用情境。
[![Run on Repl.it](https://repl.it/badge/github/AstrBotDevs/AstrBot)](https://repl.it/github/AstrBotDevs/AstrBot)
### AUR
AUR 方式面向 Arch Linux 使用者,適合希望透過系統套件管理器安裝 AstrBot 的場景。
在終端執行下方命令安裝 `astrbot-git` 套件,安裝完成後即可啟動使用。
```bash
yay -S astrbot-git
```
**更多部署方式**
若你需要面板化或更高自訂程度的部署,可參考 [寶塔面板](https://docs.astrbot.app/deploy/astrbot/btpanel.html)BT Panel 應用商店安裝)、[1Panel](https://docs.astrbot.app/deploy/astrbot/1panel.html)1Panel 應用商店安裝)、[CasaOS](https://docs.astrbot.app/deploy/astrbot/casaos.html)NAS / 家用伺服器可視化部署)與 [手動部署](https://docs.astrbot.app/deploy/astrbot/cli.html)(基於原始碼與 `uv` 的完整自訂安裝)。
## 支援的訊息平台
將 AstrBot 連接到你常用的聊天平台。
| 平台 | 維護方 |
|---------|---------------|
| QQ | 官方維護 |
| OneBot v11 協議實作 | 官方維護 |
| Telegram | 官方維護 |
| 企微應用 & 企微智慧機器人 | 官方維護 |
| 微信客服 & 微信公眾號 | 官方維護 |
| 飛書 | 官方維護 |
| 釘釘 | 官方維護 |
| Slack | 官方維護 |
| Discord | 官方維護 |
| LINE | 官方維護 |
| Satori | 官方維護 |
| KOOK | 官方維護 |
| Misskey | 官方維護 |
| Mattermost | 官方維護 |
| WhatsApp即將支援 | 官方維護 |
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | 社群維護 |
| [Rocket.Chat](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | 社群維護 |
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | 社群維護 |
## 支援的模型服務
| 服務 | 類型 |
|---------|---------------|
| OpenAI 及相容服務 | 大型模型服務 |
| Anthropic | 大型模型服務 |
| Google Gemini | 大型模型服務 |
| Moonshot AI | 大型模型服務 |
| 智譜 AI | 大型模型服務 |
| DeepSeek | 大型模型服務 |
| Ollama本機部署 | 大型模型服務 |
| LM Studio本機部署 | 大型模型服務 |
| [AIHubMix](https://aihubmix.com/?aff=4bfH) | 大型模型服務API 閘道,支援所有模型) |
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | 大型模型服務 |
| [302.AI](https://share.302.ai/rr1M3l) | 大型模型服務 |
| [小馬算力](https://www.tokenpony.cn/3YPyf) | 大型模型服務 |
| [矽基流動](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | 大型模型服務 |
| [PPIO 派歐雲](https://ppio.com/user/register?invited_by=AIOONE) | 大型模型服務 |
| ModelScope | 大型模型服務 |
| OneAPI | 大型模型服務 |
| Dify | LLMOps 平台 |
| 阿里雲百煉應用 | LLMOps 平台 |
| Coze | LLMOps 平台 |
| OpenAI Whisper | 語音轉文字服務 |
| SenseVoice | 語音轉文字服務 |
| Xiaomi MiMo Omni | 語音轉文字服務 |
| OpenAI TTS | 文字轉語音服務 |
| Gemini TTS | 文字轉語音服務 |
| GPT-Sovits-Inference | 文字轉語音服務 |
| GPT-Sovits | 文字轉語音服務 |
| FishAudio | 文字轉語音服務 |
| Edge TTS | 文字轉語音服務 |
| 阿里雲百煉 TTS | 文字轉語音服務 |
| Azure TTS | 文字轉語音服務 |
| Minimax TTS | 文字轉語音服務 |
| Xiaomi MiMo TTS | 文字轉語音服務 |
| 火山引擎 TTS | 文字轉語音服務 |
## ❤️ 貢獻
歡迎任何 Issues/Pull Requests只需要將您的變更提交到此專案 )
### 如何貢獻
您可以透過檢視問題或協助審核 PR拉取請求來貢獻。任何問題或 PR 都歡迎參與,以促進社群貢獻。當然,這些只是建議,您可以以任何方式進行貢獻。對於新功能的新增,請先透過 Issue 討論。
### 開發環境
AstrBot 使用 `ruff` 進行程式碼格式化和檢查。
```bash
git clone https://github.com/AstrBotDevs/AstrBot
pip install pre-commit
pre-commit install
```
## 🌍 社群
### QQ 群組
- 12 群916228568 (新)
- 9 群1076659624 (人滿)
- 10 群1078079676 (人滿)
- 11 群704659519 (人滿)
- 1 群322154837 (人滿)
- 3 群630166526 (人滿)
- 4 群1077826412 (人滿)
- 5 群822130018 (人滿)
- 6 群753075035 (人滿)
- 7 群743746109 (人滿)
- 8 群1030353265 (人滿)
- 開發者群闲聊吹水975206796
- 開發者群正式1039761811
### Discord 群組
<a href="https://discord.gg/hAVk6tgV36"><img alt="Discord_community" src="https://img.shields.io/badge/Discord-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
## ❤️ Special Thanks
特別感謝所有 Contributors 和外掛開發者對 AstrBot 的貢獻 ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=300&columns=15" />
</a>
此外,本專案的誕生離不開以下開源專案的幫助:
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 偉大的貓貓框架
## ⭐ Star History
> [!TIP]
> 如果本專案對您的生活 / 工作產生了幫助,或者您關注本專案的未來發展,請給專案 Star這是我們維護這個開源專案的動力 <3
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=astrbotdevs/astrbot&type=Date)](https://star-history.com/#astrbotdevs/astrbot&Date)
</div>
<div align="center">
_陪伴與能力從來不應該是對立面。我們希望創造的是一個既能理解情緒、給予陪伴,也能可靠完成工作的機器人。_
_私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div>

View File

@@ -1,319 +0,0 @@
![AstrBot-Logo-Simplified](https://github.com/user-attachments/assets/ffd99b6b-3272-4682-beaa-6fe74250f7d9)
<div align="center">
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.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_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>
<div>
<a href="https://trendshift.io/repositories/21369" target="_blank"><img src="https://trendshift.io/api/badge/repositories/21369" alt="AstrBotDevs%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=2" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</div>
<br>
<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/badge/python-3.10+-blue.svg" alt="python">
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
<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://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?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://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
</div>
<br>
<a href="https://astrbot.app/">主页</a>
<a href="https://astrbot.app/">文档</a>
<a href="https://blog.astrbot.app/">博客</a>
<a href="https://astrbot.featurebase.app/roadmap">路线图</a>
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
<a href="mailto:community@astrbot.app">Email</a>
</div>
AstrBot 是一个开源的一站式 Agentic 个人和群聊助手,可在 QQ、Telegram、企业微信、飞书、钉钉、Slack 等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手还是企业知识库AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。
![landingpage](https://github.com/user-attachments/assets/45fc5699-cddf-4e21-af35-13040706f6c0)
## 主要功能
1. 💯 免费 & 开源。
2. ✨ AI 大模型对话多模态AgentMCPSkills知识库人格设定自动压缩对话。
3. 🤖 支持接入 Dify、阿里云百炼、Coze 等智能体平台。
4. 🌐 多平台,支持 QQ、企业微信、飞书、钉钉、微信公众号、Telegram、Slack 以及[更多](#支持的消息平台)。
5. 📦 插件扩展,已有 1000+ 个插件可一键安装。
6. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) 隔离化环境,安全地执行任何代码、调用 Shell、会话级资源复用。
7. 💻 WebUI 支持。
8. 🌈 Web ChatUI 支持ChatUI 内置代理沙盒、网页搜索等。
9. 🌐 国际化i18n支持。
<br>
<table align="center">
<tr align="center">
<th>💙 角色扮演 & 情感陪伴</th>
<th>✨ 主动式 Agent</th>
<th>🚀 通用 Agentic 能力</th>
<th>🧩 1000+ 社区插件</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>
## 快速开始
### 一键部署
对于想快速体验 AstrBot、且熟悉命令行并能够自行安装 `uv` 环境的用户,我们推荐使用 `uv` 一键部署方式 ⚡️。
```bash
uv tool install astrbot --python 3.12
astrbot init # 仅首次执行此命令以初始化环境
astrbot run # astrbot run --backend-only 仅启动后端服务
# 安装开发版本(更多修复,新功能,但不够稳定,适合开发者)
uv tool install git+https://github.com/AstrBotDevs/AstrBot@dev
```
> 需要安装 [uv](https://docs.astral.sh/uv/)。
> AstrBot 需要 Python 3.12 或更高版本。`--python 3.12` 会确保 `uv` 使用 Python 3.12 创建 tool 环境。
> [!NOTE]
> 对于 macOS 用户:由于 macOS 安全检查,首次运行 `astrbot` 命令可能需要较长时间(约 10-20 秒)。
更新 `astrbot`
```bash
uv tool upgrade astrbot --python 3.12
```
> [!WARNING]
> 通过 `uv` 部署的 AstrBot **不支持在 WebUI 中进行版本升级**。如需更新,请通过命令行执行上述命令。
### Docker 部署
对于熟悉容器、希望获得更稳定且更适合生产环境部署方式的用户,我们推荐使用 Docker / Docker Compose 部署 AstrBot。
请参考官方文档 [使用 Docker 部署 AstrBot](https://docs.astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot)。
### 在 雨云 上部署
对于希望一键部署 AstrBot 且不想自行管理服务器的用户,我们推荐使用雨云的一键云部署服务 ☁️:
[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
### 桌面客户端部署
对于希望在桌面端使用 AstrBot、并以 ChatUI 为主要入口的用户,我们推荐使用 AstrBot App。
前往 [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop) 下载并安装;该方式面向桌面使用,不推荐服务器场景。
### 启动器部署
同样在桌面端,希望快速部署并实现环境隔离多开的用户,我们推荐使用 AstrBot Launcher。
前往 [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) 下载并安装。
### 在 Replit 上部署
Replit 部署由社区维护,适合在线演示和轻量试用场景。
[![Run on Repl.it](https://repl.it/badge/github/AstrBotDevs/AstrBot)](https://repl.it/github/AstrBotDevs/AstrBot)
### AUR
AUR 方式面向 Arch Linux 用户,适合希望通过系统包管理器安装 AstrBot 的场景。
在终端执行下方命令安装 `astrbot-git` 包,安装完成后即可启动使用。
```bash
yay -S astrbot-git
```
**更多部署方式**
若你需要面板化或更高自定义部署,可参考 [宝塔面板](https://docs.astrbot.app/deploy/astrbot/btpanel.html)BT Panel 应用商店安装)、[1Panel](https://docs.astrbot.app/deploy/astrbot/1panel.html)1Panel 应用商店安装)、[CasaOS](https://docs.astrbot.app/deploy/astrbot/casaos.html)NAS / 家庭服务器可视化部署)和 [手动部署](https://docs.astrbot.app/deploy/astrbot/cli.html)(基于源码与 `uv` 的完整自定义安装)。
## 支持的消息平台
将 AstrBot 连接到你常用的聊天平台。
| 平台 | 维护方 |
|---------|---------------|
| **QQ** | 官方维护 |
| **OneBot v11** | 官方维护 |
| **Telegram** | 官方维护 |
| **企微应用 & 企微智能机器人** | 官方维护 |
| **微信客服 & 微信公众号** | 官方维护 |
| **飞书** | 官方维护 |
| **钉钉** | 官方维护 |
| **Slack** | 官方维护 |
| **Discord** | 官方维护 |
| **LINE** | 官方维护 |
| **Satori** | 官方维护 |
| **KOOK** | 官方维护 |
| **Misskey** | 官方维护 |
| **Mattermost** | 官方维护 |
| **WhatsApp将支持** | 官方维护 |
| [**Matrix**](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | 社区维护 |
| [**Rocket.Chat**](https://github.com/NET-Homeless/astrbot_plugin_rocket_chat_adapter) | 社区维护 |
| [**VoceChat**](https://github.com/HikariFroya/astrbot_plugin_vocechat) | 社区维护 |
## 支持的模型提供商
| 提供商 | 类型 |
|---------|---------------|
| 自定义 | 任何 OpenAI API 兼容的服务 |
| OpenAI | LLM |
| Anthropic | LLM |
| Google Gemini | LLM |
| Moonshot AI | LLM |
| 智谱 AI | LLM |
| DeepSeek | LLM |
| Ollama (本地部署) | LLM |
| LM Studio (本地部署) | LLM |
| [AIHubMix](https://aihubmix.com/?aff=4bfH) | LLM (API 网关, 支持所有模型) |
| [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | LLM (API 网关, 支持所有模型) |
| [硅基流动](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | LLM (API 网关, 支持所有模型) |
| [PPIO 派欧云](https://ppio.com/user/register?invited_by=AIOONE) | LLM (API 网关, 支持所有模型) |
| [302.AI](https://share.302.ai/rr1M3l) | LLM (API 网关, 支持所有模型)|
| [小马算力](https://www.tokenpony.cn/3YPyf) | LLM (API 网关, 支持所有模型)|
| ModelScope | LLM |
| OneAPI | LLM |
| Dify | LLMOps 平台 |
| 阿里云百炼应用 | LLMOps 平台 |
| Coze | LLMOps 平台 |
| OpenAI Whisper | 语音转文本 |
| SenseVoice | 语音转文本 |
| Xiaomi MiMo Omni | 语音转文本 |
| OpenAI TTS | 文本转语音 |
| Gemini TTS | 文本转语音 |
| GPT-Sovits-Inference | 文本转语音 |
| GPT-Sovits | 文本转语音 |
| FishAudio | 文本转语音 |
| Edge TTS | 文本转语音 |
| 阿里云百炼 TTS | 文本转语音 |
| Azure TTS | 文本转语音 |
| Minimax TTS | 文本转语音 |
| Xiaomi MiMo TTS | 文本转语音 |
| 火山引擎 TTS | 文本转语音 |
## ❤️ Sponsors
<p align="center">
<img alt="sponsors" src="https://sponsors.astrbot.app/?v=1">
</p>
## ❤️ 贡献
欢迎任何 Issues/Pull Requests只需要将你的更改提交到此项目 :)
### 如何贡献
你可以通过查看问题或帮助审核 PR拉取请求来贡献。任何问题或 PR 都欢迎参与,以促进社区贡献。当然,这些只是建议,你可以以任何方式进行贡献。对于新功能的添加,请先通过 Issue 讨论。
建议将功能性PR合并至dev分支将在测试修改后合并到主分支并发布新版本。
为了减少冲突,建议如下:
1. 工作分支最好基于 `dev` 分支创建,避免直接在 `main` 分支上工作。
2. 提交 PR 时,选择 `dev` 分支作为目标分支。
3. 定期同步 `dev` 分支到本地多使用git pull。
### 开发环境
AstrBot 使用 `ruff` 进行代码格式化和检查。
```bash
git clone https://github.com/AstrBotDevs/AstrBot
git switch dev # 切换到开发分支
pip install pre-commit # 或者uv tool install pre-commit
pre-commit install
```
推荐使用uv本地安装进行测试
```bash
uv tool install -e . --force
astrbot init
astrbot run
```
调试前端
```bash
astrbot run --backend-only
cd dashboard
bun install # 或者pnpm 等
bun dev
```
### QQ 群组
- 1 群322154837 (人满)
- 3 群630166526 (人满)
- 4 群1077826412 (人满)
- 5 群822130018 (人满)
- 6 群753075035 (人满)
- 7 群743746109 (人满)
- 8 群1030353265 (人满)
- 9 群1076659624 (人满)
- 10 群1078079676 (人满)
- 11 群704659519 (人满)
- 12 群916228568 (人满)
- 13 群1092185289
- 14 群1103419483
- 开发者群偏闲聊吹水975206796
- 开发者群正式1039761811
### Discord 频道
- [Discord](https://discord.gg/hAVk6tgV36)
## ❤️ Special Thanks
特别感谢所有 Contributors 和插件开发者对 AstrBot 的贡献 ❤️
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=300&columns=15" />
</a>
此外,本项目的诞生离不开以下开源项目的帮助:
- [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
> [!TIP]
> 如果本项目对您的生活 / 工作产生了帮助,或者您关注本项目的未来发展,请给项目 Star这是我们维护这个开源项目的动力 <3
<div align="center">
[![Star History Chart](https://api.star-history.com/svg?repos=astrbotdevs/astrbot&type=Date)](https://star-history.com/#astrbotdevs/astrbot&Date)
</div>
<div align="center">
_陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。_
_私は、高性能ですから!_
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
</div>

View File

@@ -1,30 +1,3 @@
from __future__ import annotations
from .core.log import LogManager
from importlib.metadata import PackageNotFoundError
from importlib.metadata import version as _pkg_version
from typing import TYPE_CHECKING, Any
try:
__version__ = _pkg_version("astrbot")
except PackageNotFoundError:
__version__ = "4.26.1"
if TYPE_CHECKING:
from .core import logger as logger
__all__ = ["logger"]
def __getattr__(name: str) -> Any:
if name == "cli":
from astrbot.cli.__main__ import cli
return cli()
if name == "logger":
from .core import logger
return logger
raise AttributeError(name)
logger = LogManager.GetLogger(log_name="astrbot")

View File

@@ -1,147 +0,0 @@
import argparse
import asyncio
import mimetypes
import os
import sys
from pathlib import Path
import anyio
from astrbot.core import LogBroker, LogManager, db_helper, logger
from astrbot.core.config.default import VERSION
from astrbot.core.initial_loader import InitialLoader
from astrbot.core.utils.astrbot_path import (
get_astrbot_config_path,
get_astrbot_data_path,
get_astrbot_knowledge_base_path,
get_astrbot_plugin_path,
get_astrbot_root,
get_astrbot_site_packages_path,
get_astrbot_skills_path,
get_astrbot_temp_path,
)
from astrbot.core.utils.io import (
download_dashboard,
get_dashboard_version,
)
# 将父目录添加到 sys.path
sys.path.append(Path(__file__).parent.as_posix())
logo_tmpl = r"""
___ _______.___________..______ .______ ______ .___________.
/ \ / | || _ \ | _ \ / __ \ | |
/ ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----`
/ /_\ \ \ \ | | | / | _ < | | | | | |
/ _____ \ .----) | | | | |\ \----.| |_) | | `--' | | |
/__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__|
"""
def check_env() -> None:
# Python version check: require 3.12 or 3.13
if not (sys.version_info.major == 3 and sys.version_info.minor in (12, 13)):
sys.exit(1)
astrbot_root = get_astrbot_root()
if astrbot_root not in sys.path:
sys.path.insert(0, astrbot_root)
site_packages_path = get_astrbot_site_packages_path()
if site_packages_path not in sys.path:
sys.path.insert(0, site_packages_path)
os.makedirs(get_astrbot_config_path(), exist_ok=True)
os.makedirs(get_astrbot_plugin_path(), exist_ok=True)
os.makedirs(get_astrbot_temp_path(), exist_ok=True)
os.makedirs(get_astrbot_knowledge_base_path(), exist_ok=True)
os.makedirs(get_astrbot_skills_path(), exist_ok=True)
os.makedirs(site_packages_path, exist_ok=True)
# 针对问题 #181 的临时解决方案
mimetypes.add_type("text/javascript", ".js")
mimetypes.add_type("text/javascript", ".mjs")
mimetypes.add_type("application/json", ".json")
async def check_dashboard_files(webui_dir: str | None = None):
"""下载管理面板文件"""
# 指定webui目录
if webui_dir:
if await anyio.Path(webui_dir).exists():
logger.info(f"使用指定的 WebUI 目录: {webui_dir}")
return webui_dir
logger.warning(f"指定的 WebUI 目录 {webui_dir} 不存在,将使用默认逻辑。")
data_dist_path = os.path.join(get_astrbot_data_path(), "dist")
if await anyio.Path(data_dist_path).exists():
v = await get_dashboard_version()
if v is not None:
# 存在文件
if v == f"v{VERSION}":
logger.info("WebUI 版本已是最新。")
else:
logger.warning(
f"检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符。",
)
return data_dist_path
logger.info(
"开始下载管理面板文件...高峰期(晚上)可能导致较慢的速度。如多次下载失败,请前往 https://github.com/AstrBotDevs/AstrBot/releases/latest 下载 dist.zip,并将其中的 dist 文件夹解压至 data 目录下。",
)
try:
await download_dashboard(version=f"v{VERSION}", latest=False)
except Exception as e:
logger.warning(
f"下载指定版本(v{VERSION})的管理面板文件失败: {e},尝试下载最新版本。",
)
try:
await download_dashboard(latest=True)
except Exception as e:
logger.critical(f"下载管理面板文件失败: {e}")
return None
logger.info("管理面板下载完成。")
return data_dist_path
async def main_async(webui_dir_arg: str | None, log_broker: LogBroker) -> None:
"""主异步入口"""
# 检查仪表板文件
webui_dir = await check_dashboard_files(webui_dir_arg)
if webui_dir is None:
logger.warning(
"管理面板文件检查失败,WebUI 功能将不可用。"
"请检查网络连接或手动指定 --webui-dir 参数。",
)
db = db_helper
# 打印 logo
logger.info(logo_tmpl)
core_lifecycle = InitialLoader(db, log_broker)
core_lifecycle.webui_dir = webui_dir
await core_lifecycle.start()
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="AstrBot")
parser.add_argument(
"--webui-dir",
type=str,
help="指定 WebUI 静态文件目录路径",
default=None,
)
args = parser.parse_args()
check_env()
# 启动日志代理
log_broker = LogBroker()
LogManager.set_queue_handler(logger, log_broker)
# 只使用一次 asyncio.run()
asyncio.run(main_async(args.webui_dir, log_broker))

View File

@@ -1,19 +1,20 @@
from astrbot import logger
from astrbot.core import html_renderer, sp
from astrbot.core.agent.tool import FunctionTool, ToolSet
from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
from astrbot.core.config.astrbot_config import AstrBotConfig
from astrbot.core.star.register import register_agent as agent
from astrbot import logger
from astrbot.core import html_renderer
from astrbot.core import sp
from astrbot.core.star.register import register_llm_tool as llm_tool
from astrbot.core.star.register import register_agent as agent
from astrbot.core.agent.tool import ToolSet, FunctionTool
from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
__all__ = [
"AstrBotConfig",
"BaseFunctionToolExecutor",
"FunctionTool",
"ToolSet",
"agent",
"logger",
"html_renderer",
"llm_tool",
"logger",
"agent",
"sp",
"ToolSet",
"FunctionTool",
"BaseFunctionToolExecutor",
]

View File

@@ -1,4 +1,3 @@
# ruff: noqa: F401, F403, F811, I001
from astrbot.core.config.astrbot_config import AstrBotConfig
from astrbot import logger
from astrbot.core import html_renderer
@@ -30,15 +29,14 @@ from astrbot.core.star.filter.platform_adapter_type import (
PlatformAdapterType,
)
from astrbot.core.star.register import (
register_star as register, # 注册插件(Star)
register_star as register, # 注册插件Star
)
from astrbot.core.star import Context, Star
from astrbot.core.star.config import *
# provider
from astrbot.core.provider import Provider, ProviderMetaData
from astrbot.core.db.po import Personality
from astrbot.core.provider import Provider, Personality, ProviderMetaData
# platform
from astrbot.core.platform import (
@@ -52,4 +50,4 @@ from astrbot.core.platform import (
from astrbot.core.platform.register import register_platform_adapter
from .message_components import *
from .message_components import *

View File

@@ -1,17 +1,18 @@
from astrbot.core.message.message_event_result import (
MessageEventResult,
MessageChain,
CommandResult,
EventResultType,
MessageChain,
MessageEventResult,
ResultContentType,
)
from astrbot.core.platform import AstrMessageEvent
__all__ = [
"AstrMessageEvent",
"MessageEventResult",
"MessageChain",
"CommandResult",
"EventResultType",
"MessageChain",
"MessageEventResult",
"AstrMessageEvent",
"ResultContentType",
]

View File

@@ -1,72 +1,51 @@
from astrbot.core.star.filter.custom_filter import CustomFilter
from astrbot.core.star.filter.event_message_type import (
EventMessageType,
EventMessageTypeFilter,
)
from astrbot.core.star.filter.permission import PermissionType, PermissionTypeFilter
from astrbot.core.star.filter.platform_adapter_type import (
PlatformAdapterType,
PlatformAdapterTypeFilter,
)
from astrbot.core.star.register import register_after_message_sent as after_message_sent
from astrbot.core.star.register import register_command as command
from astrbot.core.star.register import register_command_group as command_group
from astrbot.core.star.register import register_custom_filter as custom_filter
from astrbot.core.star.register import register_event_message_type as event_message_type
from astrbot.core.star.register import register_llm_tool as llm_tool
from astrbot.core.star.register import register_on_agent_begin as on_agent_begin
from astrbot.core.star.register import register_on_agent_done as on_agent_done
from astrbot.core.star.register import register_on_astrbot_loaded as on_astrbot_loaded
from astrbot.core.star.register import (
register_on_decorating_result as on_decorating_result,
)
from astrbot.core.star.register import register_on_llm_request as on_llm_request
from astrbot.core.star.register import register_on_llm_response as on_llm_response
from astrbot.core.star.register import (
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_plugin_error as on_plugin_error
from astrbot.core.star.register import register_on_plugin_loaded as on_plugin_loaded
from astrbot.core.star.register import register_on_plugin_unloaded as on_plugin_unloaded
from astrbot.core.star.register import register_on_using_llm_tool as on_using_llm_tool
from astrbot.core.star.register import (
register_on_waiting_llm_request as on_waiting_llm_request,
)
from astrbot.core.star.register import register_permission_type as permission_type
from astrbot.core.star.register import (
register_command as command,
register_command_group as command_group,
register_event_message_type as event_message_type,
register_regex as regex,
register_platform_adapter_type as platform_adapter_type,
register_permission_type as permission_type,
register_custom_filter as custom_filter,
register_on_astrbot_loaded as on_astrbot_loaded,
register_on_platform_loaded as on_platform_loaded,
register_on_llm_request as on_llm_request,
register_on_llm_response as on_llm_response,
register_llm_tool as llm_tool,
register_on_decorating_result as on_decorating_result,
register_after_message_sent as after_message_sent,
)
from astrbot.core.star.register import register_regex as regex
from astrbot.core.star.filter.event_message_type import (
EventMessageTypeFilter,
EventMessageType,
)
from astrbot.core.star.filter.platform_adapter_type import (
PlatformAdapterTypeFilter,
PlatformAdapterType,
)
from astrbot.core.star.filter.permission import PermissionTypeFilter, PermissionType
from astrbot.core.star.filter.custom_filter import CustomFilter
__all__ = [
"CustomFilter",
"EventMessageType",
"EventMessageTypeFilter",
"PermissionType",
"PermissionTypeFilter",
"PlatformAdapterType",
"PlatformAdapterTypeFilter",
"after_message_sent",
"command",
"command_group",
"custom_filter",
"event_message_type",
"llm_tool",
"on_agent_begin",
"on_agent_done",
"on_astrbot_loaded",
"on_decorating_result",
"on_llm_request",
"on_llm_response",
"on_llm_tool_respond",
"on_platform_loaded",
"on_plugin_error",
"on_plugin_loaded",
"on_plugin_unloaded",
"on_using_llm_tool",
"on_waiting_llm_request",
"permission_type",
"platform_adapter_type",
"regex",
"platform_adapter_type",
"permission_type",
"EventMessageTypeFilter",
"EventMessageType",
"PlatformAdapterTypeFilter",
"PlatformAdapterType",
"PermissionTypeFilter",
"CustomFilter",
"custom_filter",
"PermissionType",
"on_astrbot_loaded",
"on_platform_loaded",
"on_llm_request",
"llm_tool",
"on_decorating_result",
"after_message_sent",
"on_llm_response",
]

View File

@@ -1,22 +1,23 @@
from astrbot.core.message.components import *
from astrbot.core.platform import (
AstrBotMessage,
AstrMessageEvent,
Group,
Platform,
AstrBotMessage,
MessageMember,
MessageType,
Platform,
PlatformMetadata,
Group,
)
from astrbot.core.platform.register import register_platform_adapter
from astrbot.core.message.components import *
__all__ = [
"AstrBotMessage",
"AstrMessageEvent",
"Group",
"Platform",
"AstrBotMessage",
"MessageMember",
"MessageType",
"Platform",
"PlatformMetadata",
"register_platform_adapter",
"Group",
]

View File

@@ -1,18 +1,17 @@
from astrbot.core.db.po import Personality
from astrbot.core.provider import Provider, STTProvider
from astrbot.core.provider import Provider, STTProvider, Personality
from astrbot.core.provider.entities import (
LLMResponse,
ProviderMetaData,
ProviderRequest,
ProviderType,
ProviderMetaData,
LLMResponse,
)
__all__ = [
"LLMResponse",
"Personality",
"Provider",
"ProviderMetaData",
"STTProvider",
"Personality",
"ProviderRequest",
"ProviderType",
"STTProvider",
"ProviderMetaData",
"LLMResponse",
]

View File

@@ -1,7 +1,8 @@
from astrbot.core.star import Context, Star, StarTools
from astrbot.core.star.config import *
from astrbot.core.star.register import (
register_star as register, # 注册插件(Star)
register_star as register, # 注册插件Star
)
__all__ = ["Context", "Star", "StarTools", "register"]
from astrbot.core.star import Context, Star, StarTools
from astrbot.core.star.config import *
__all__ = ["register", "Context", "Star", "StarTools"]

View File

@@ -1,7 +1,7 @@
from astrbot.core.utils.session_waiter import (
SessionController,
SessionWaiter,
SessionController,
session_waiter,
)
__all__ = ["SessionController", "SessionWaiter", "session_waiter"]
__all__ = ["SessionWaiter", "SessionController", "session_waiter"]

View File

@@ -1,453 +0,0 @@
from __future__ import annotations
import contextvars
from collections.abc import Callable, KeysView
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Generic, TypeVar, overload
from fastapi.encoders import jsonable_encoder
from fastapi.responses import FileResponse, JSONResponse
from starlette.datastructures import Headers
from starlette.datastructures import UploadFile as StarletteUploadFile
from starlette.responses import StreamingResponse
ValueT = TypeVar("ValueT")
DefaultT = TypeVar("DefaultT")
ConvertedT = TypeVar("ConvertedT")
class PluginMultiDict(Generic[ValueT]):
"""Dictionary-like request values that preserves duplicate keys."""
def __init__(self, pairs: list[tuple[str, ValueT]]) -> None:
self._pairs = pairs
@overload
def get(self, key: str) -> ValueT | None: ...
@overload
def get(self, key: str, default: DefaultT) -> ValueT | DefaultT: ...
@overload
def get(
self,
key: str,
default: DefaultT,
type: Callable[[ValueT], ConvertedT],
) -> ConvertedT | DefaultT: ...
def get(self, key: str, default: Any = None, type: Callable | None = None):
"""Return the last value for a key.
Args:
key: Value key to read.
default: Value returned when the key is missing or conversion fails.
type: Optional callable used to convert the value.
Returns:
The matching value, converted value, or default.
"""
for item_key, item_value in reversed(self._pairs):
if item_key != key:
continue
if type is None:
return item_value
try:
return type(item_value)
except (TypeError, ValueError):
return default
return default
def getlist(self, key: str) -> list[ValueT]:
"""Return all values for a key.
Args:
key: Value key to read.
Returns:
Values in request order.
"""
return [item_value for item_key, item_value in self._pairs if item_key == key]
def keys(self) -> KeysView[str]:
return dict.fromkeys(item_key for item_key, _ in self._pairs).keys()
def values(self) -> list[ValueT]:
return [self[key] for key in self.keys()]
def items(self) -> list[tuple[str, ValueT]]:
return [(key, self[key]) for key in self.keys()]
def __contains__(self, key: str) -> bool:
return any(item_key == key for item_key, _ in self._pairs)
def __getitem__(self, key: str) -> ValueT:
value = self.get(key)
if value is None and key not in self:
raise KeyError(key)
return value
def __bool__(self) -> bool:
return bool(self._pairs)
class PluginUploadFile:
"""Uploaded file wrapper exposed to plugin Web API handlers."""
def __init__(self, upload_file: StarletteUploadFile) -> None:
self._upload_file: StarletteUploadFile = upload_file
self.filename: str | None = upload_file.filename
self.content_type: str | None = upload_file.content_type
self.headers: Headers = upload_file.headers
self.content_length: int | None = self._resolve_content_length()
def _resolve_content_length(self) -> int | None:
try:
raw = self.headers.get("content-length")
return int(raw) if raw else None
except (TypeError, ValueError):
return None
async def save(self, destination: str | Path) -> None:
"""Save the uploaded file to disk.
Args:
destination: Destination file path.
"""
path = Path(destination)
try:
await self._upload_file.seek(0)
except Exception:
pass
with path.open("wb") as output:
while True:
chunk = await self._upload_file.read(1024 * 1024)
if not chunk:
break
output.write(chunk)
async def read(self, size: int = -1) -> bytes:
"""Read bytes from the uploaded file.
Args:
size: Maximum number of bytes to read. Use -1 to read all bytes.
Returns:
File bytes.
"""
return await self._upload_file.read(size)
async def write(self, data: bytes) -> None:
"""Write bytes to the uploaded file object.
Args:
data: Bytes to write.
"""
await self._upload_file.write(data)
async def seek(self, offset: int) -> None:
"""Move the uploaded file cursor.
Args:
offset: Absolute byte offset.
"""
await self._upload_file.seek(offset)
async def close(self) -> None:
"""Close the uploaded file."""
await self._upload_file.close()
def __getattr__(self, key: str) -> Any:
return getattr(self._upload_file, key)
class PluginRequest:
"""Request object exposed to plugin Web API handlers."""
def __init__(
self,
request_: Any,
*,
path_params: dict[str, Any] | None = None,
plugin_name: str | None = None,
username: str | None = None,
) -> None:
self._request: Any = request_
self.method: str = request_.method
self.path: str = request_.url.path
self.headers: Headers = request_.headers
self.cookies: dict[str, str] = request_.cookies
self.content_type: str | None = request_.headers.get("content-type")
self.client_host: str | None = request_.client.host if request_.client else None
self.path_params: dict[str, Any] = path_params or {}
self.plugin_name: str | None = plugin_name
self.username: str | None = username
self.query: PluginMultiDict[str] = PluginMultiDict[str](
list(request_.query_params.multi_items())
)
self._form_cache: PluginMultiDict[str] | None = None
self._files_cache: PluginMultiDict[PluginUploadFile] | None = None
async def body(self) -> bytes:
"""Read the raw request body.
Returns:
Raw request body bytes.
"""
return await self._request.body()
async def json(self, default: DefaultT | None = None) -> Any | DefaultT | None:
"""Read the JSON request body.
Args:
default: Value returned when the request body cannot be parsed as JSON.
Returns:
Parsed JSON data or default.
"""
try:
return await self._request.json()
except Exception:
return default
async def _load_form_parts(self) -> None:
if self._form_cache is not None and self._files_cache is not None:
return
form = await self._request.form()
form_pairs: list[tuple[str, str]] = []
file_pairs: list[tuple[str, PluginUploadFile]] = []
for key, value in form.multi_items():
if isinstance(value, StarletteUploadFile):
file_pairs.append((key, PluginUploadFile(value)))
else:
form_pairs.append((key, value))
self._form_cache = PluginMultiDict(form_pairs)
self._files_cache = PluginMultiDict(file_pairs)
async def form(self) -> PluginMultiDict[str]:
"""Read form fields from a multipart or form-urlencoded request.
Returns:
Form values without uploaded files.
"""
await self._load_form_parts()
assert self._form_cache is not None
return self._form_cache
async def files(self) -> PluginMultiDict[PluginUploadFile]:
"""Read uploaded files from a multipart request.
Returns:
Uploaded file values.
"""
await self._load_form_parts()
assert self._files_cache is not None
return self._files_cache
_request_var: contextvars.ContextVar[PluginRequest] = contextvars.ContextVar(
"astrbot_plugin_web_request"
)
class PluginRequestProxy:
"""Typed proxy for the request bound to the current plugin Web handler."""
def _get_current(self) -> PluginRequest:
try:
return _request_var.get()
except LookupError as exc:
raise RuntimeError(
"astrbot.api.web.request is only available inside a plugin Web API "
"handler."
) from exc
@property
def method(self) -> str:
return self._get_current().method
@property
def path(self) -> str:
return self._get_current().path
@property
def headers(self) -> Headers:
return self._get_current().headers
@property
def cookies(self) -> dict[str, str]:
return self._get_current().cookies
@property
def content_type(self) -> str | None:
return self._get_current().content_type
@property
def client_host(self) -> str | None:
return self._get_current().client_host
@property
def path_params(self) -> dict[str, Any]:
return self._get_current().path_params
@property
def plugin_name(self) -> str | None:
return self._get_current().plugin_name
@property
def username(self) -> str | None:
return self._get_current().username
@property
def query(self) -> PluginMultiDict[str]:
return self._get_current().query
async def body(self) -> bytes:
return await self._get_current().body()
async def json(self, default: DefaultT | None = None) -> Any | DefaultT | None:
return await self._get_current().json(default=default)
async def form(self) -> PluginMultiDict[str]:
return await self._get_current().form()
async def files(self) -> PluginMultiDict[PluginUploadFile]:
return await self._get_current().files()
def __getattr__(self, key: str) -> Any:
return getattr(self._get_current(), key)
request: PluginRequestProxy = PluginRequestProxy()
@contextmanager
def bind_request_context(request_: PluginRequest):
"""Bind a plugin Web request for the current async context.
Args:
request_: Request object exposed through the module-level request proxy.
Yields:
The bound request object.
"""
token = _request_var.set(request_)
try:
yield request_
finally:
_request_var.reset(token)
def json_response(
data: Any = None,
*,
status_code: int = 200,
headers: dict[str, str] | None = None,
) -> JSONResponse:
"""Build a JSON response for plugin Web API handlers.
Args:
data: JSON-serializable response body.
status_code: HTTP status code.
headers: Optional response headers.
Returns:
A Starlette/FastAPI JSON response.
"""
return JSONResponse(
jsonable_encoder({} if data is None else data),
status_code=status_code,
headers=headers,
)
def error_response(
message: str,
*,
status_code: int = 400,
data: Any = None,
headers: dict[str, str] | None = None,
) -> JSONResponse:
"""Build a standard error response for plugin bridge calls.
Args:
message: Public error message.
status_code: HTTP status code.
data: Optional error details that are safe to expose.
headers: Optional response headers.
Returns:
A JSON response with the AstrBot error envelope.
"""
return json_response(
{"status": "error", "message": message, "data": data},
status_code=status_code,
headers=headers,
)
def file_response(
path: str | Path,
*,
filename: str | None = None,
content_type: str | None = None,
headers: dict[str, str] | None = None,
) -> FileResponse:
"""Build a file download response for plugin Web API handlers.
Args:
path: File path to send.
filename: Optional download filename.
content_type: Optional response media type.
headers: Optional response headers.
Returns:
A Starlette/FastAPI file response.
"""
return FileResponse(
path,
filename=filename,
media_type=content_type,
headers=headers,
)
def stream_response(
content: Any,
*,
content_type: str = "text/event-stream",
status_code: int = 200,
headers: dict[str, str] | None = None,
) -> StreamingResponse:
"""Build a streaming response for plugin Web API handlers.
Args:
content: Sync or async iterable that yields response chunks.
content_type: Response media type.
status_code: HTTP status code.
headers: Optional response headers.
Returns:
A Starlette/FastAPI streaming response.
"""
return StreamingResponse(
content,
media_type=content_type,
status_code=status_code,
headers=headers,
)
__all__ = [
"PluginMultiDict",
"PluginRequest",
"PluginRequestProxy",
"PluginUploadFile",
"bind_request_context",
"error_response",
"file_response",
"json_response",
"request",
"stream_response",
]

View File

@@ -1,6 +0,0 @@
{
"metadata": {
"display_name": "AstrBot",
"desc": "AstrBot's internal plugin, providing some basic capabilities."
}
}

View File

@@ -1,6 +0,0 @@
{
"metadata": {
"display_name": "AstrBot",
"desc": "AstrBot 的内部插件,提供一些基础能力。"
}
}

View File

@@ -1,302 +0,0 @@
import asyncio
import datetime
import random
import uuid
from collections import defaultdict, deque
from astrbot import logger
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent
from astrbot.api.message_components import (
At,
AtAll,
Face,
File,
Forward,
Image,
Plain,
Record,
Reply,
Video,
)
from astrbot.api.platform import MessageType
from astrbot.api.provider import Provider, ProviderRequest
from astrbot.core.agent.message import TextPart
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
"""
Group chat context awareness.
"""
GROUP_HISTORY_HEADER = (
"<system_reminder>"
"You are in a group chat. "
"Belows are group chat context after your last reply:\n"
"--- BEGIN CONTEXT---\n"
)
GROUP_HISTORY_FOOTER = "\n--- END CONTEXT ---\n</system_reminder>"
DEFAULT_GROUP_MESSAGE_MAX_CNT = 300
class GroupChatContext:
def __init__(self, acm: AstrBotConfigManager, context: star.Context) -> None:
self.acm = acm
self.context = context
self._locks: dict[str, asyncio.Lock] = {}
self.raw_records: dict[str, deque[str]] = defaultdict(deque)
self._record_ids: dict[str, deque[str]] = defaultdict(deque)
def _get_lock(self, umo: str) -> asyncio.Lock:
lock = self._locks.get(umo)
if lock is None:
lock = asyncio.Lock()
self._locks[umo] = lock
return lock
def cfg(self, event: AstrMessageEvent):
cfg = self.context.get_config(umo=event.unified_msg_origin)
group_context_cfg = cfg["provider_ltm_settings"]
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
image_caption_provider_id = group_context_cfg.get("image_caption_provider_id")
image_caption = group_context_cfg["image_caption"] and bool(
image_caption_provider_id
)
active_reply = group_context_cfg["active_reply"]
enable_active_reply = active_reply.get("enable", False)
ar_method = active_reply["method"]
ar_possibility = active_reply["possibility_reply"]
ar_prompt = active_reply.get("prompt", "")
ar_whitelist = active_reply.get("whitelist", [])
return {
"group_message_max_cnt": _positive_int(
group_context_cfg.get(
"group_message_max_cnt",
DEFAULT_GROUP_MESSAGE_MAX_CNT,
),
DEFAULT_GROUP_MESSAGE_MAX_CNT,
),
"image_caption": image_caption,
"image_caption_prompt": image_caption_prompt,
"image_caption_provider_id": image_caption_provider_id,
"enable_active_reply": enable_active_reply,
"ar_method": ar_method,
"ar_possibility": ar_possibility,
"ar_prompt": ar_prompt,
"ar_whitelist": ar_whitelist,
}
async def get_image_caption(
self,
image_url: str,
image_caption_provider_id: str,
image_caption_prompt: str,
) -> str:
if not image_caption_provider_id:
provider = self.context.get_using_provider()
else:
provider = self.context.get_provider_by_id(image_caption_provider_id)
if not provider:
raise Exception(f"没有找到 ID 为 {image_caption_provider_id} 的提供商")
if not isinstance(provider, Provider):
raise Exception(f"提供商类型错误({type(provider)}),无法获取图片描述")
response = await provider.text_chat(
prompt=image_caption_prompt,
session_id=uuid.uuid4().hex,
image_urls=[image_url],
persist=False,
)
return response.completion_text
async def need_active_reply(self, event: AstrMessageEvent) -> bool:
cfg = self.cfg(event)
if not cfg["enable_active_reply"]:
return False
if event.get_message_type() != MessageType.GROUP_MESSAGE:
return False
if event.is_at_or_wake_command:
return False
if cfg["ar_whitelist"] and (
event.unified_msg_origin not in cfg["ar_whitelist"]
and (
event.get_group_id() and event.get_group_id() not in cfg["ar_whitelist"]
)
):
return False
match cfg["ar_method"]:
case "possibility_reply":
return random.random() < cfg["ar_possibility"]
return False
async def remove_session(self, event: AstrMessageEvent) -> int:
umo = event.unified_msg_origin
lock = self._get_lock(umo)
async with lock:
cnt = len(self.raw_records.get(umo, deque()))
self.raw_records.pop(umo, None)
self._record_ids.pop(umo, None)
self._locks.pop(umo, None)
return cnt
async def handle_message(self, event: AstrMessageEvent) -> None:
if event.get_message_type() != MessageType.GROUP_MESSAGE:
return
umo = event.unified_msg_origin
cfg = self.cfg(event)
final_message = await self._format_message(event, cfg)
async with self._get_lock(umo):
records = self.raw_records[umo]
record_ids = self._record_ids[umo]
record_id = uuid.uuid4().hex
records.append(final_message)
record_ids.append(record_id)
_trim_left(records, cfg["group_message_max_cnt"], record_ids)
event.set_extra("_group_context_record_id", record_id)
event.set_extra("_group_context_raw_idx", len(records) - 1)
logger.debug(f"group_chat_context | {umo} | {final_message}")
async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest) -> None:
umo = event.unified_msg_origin
record_id = event.get_extra("_group_context_record_id", None)
prompt_idx = event.get_extra("_group_context_raw_idx", -1)
if not isinstance(record_id, str) and (
not isinstance(prompt_idx, int) or prompt_idx < 0
):
return
async with self._get_lock(umo):
records = self.raw_records.get(umo)
if not records:
return
raw_list = list(records)
id_list = list(self._record_ids.get(umo, deque()))
if isinstance(record_id, str) and record_id in id_list:
prompt_idx = id_list.index(record_id)
if prompt_idx >= len(raw_list):
return
records_to_inject = raw_list[:prompt_idx]
remaining = raw_list[prompt_idx + 1 :]
remaining_ids = id_list[prompt_idx + 1 :] if id_list else []
records.clear()
records.extend(remaining)
if id_list:
record_ids = self._record_ids[umo]
record_ids.clear()
record_ids.extend(remaining_ids)
if records_to_inject:
req.extra_user_content_parts.append(
TextPart(text=_format_group_history_block(records_to_inject))
)
async def _format_message(self, event: AstrMessageEvent, cfg: dict) -> str:
datetime_str = datetime.datetime.now().strftime("%H:%M:%S")
parts = [f"[{event.message_obj.sender.nickname}/{datetime_str}]: "]
for comp in event.get_messages():
if isinstance(comp, Plain):
parts.append(f" {comp.text}")
elif isinstance(comp, Image):
if cfg["image_caption"]:
try:
url = comp.url if comp.url else comp.file
if not url:
raise Exception("图片 URL 为空")
caption = await self.get_image_caption(
url,
cfg["image_caption_provider_id"],
cfg["image_caption_prompt"],
)
parts.append(f" [Image: {caption}]")
except Exception as e:
logger.error(f"获取图片描述失败: {e}")
else:
parts.append(" [Image]")
elif isinstance(comp, At):
is_at_self = str(comp.qq) in (
event.get_self_id(),
"all",
)
if is_at_self:
parts.insert(1, "⚠️[DIRECTED AT YOU] ")
parts.append(f" [At: {comp.name}]")
elif isinstance(comp, Reply):
if comp.message_str:
parts.append(
f" [Quote({comp.sender_nickname}: {_truncate_reply_text(comp.message_str)})]"
)
elif comp.chain:
chain_desc = _describe_chain(comp.chain)
parts.append(f" [Quote({comp.sender_nickname}: {chain_desc})]")
else:
parts.append(" [Quote]")
return "".join(parts)
_MAX_REPLY_TEXT_LENGTH = 200
def _describe_chain(chain: list) -> str:
"""Summarize message chain content for quoted reply display."""
desc = []
for c in chain:
if isinstance(c, Plain) and getattr(c, "text", None):
desc.append(c.text)
elif isinstance(c, Image):
desc.append("[Image]")
elif isinstance(c, At):
name = getattr(c, "name", "") or getattr(c, "qq", "")
desc.append(f"[At: {name}]")
elif isinstance(c, Record):
desc.append("[Voice]")
elif isinstance(c, Video):
desc.append("[Video]")
elif isinstance(c, File):
desc.append(f"[File: {getattr(c, 'name', '') or ''}]")
elif isinstance(c, Forward):
desc.append("[Forward]")
elif isinstance(c, AtAll):
desc.append("[At: All]")
elif isinstance(c, Face):
desc.append(f"[Sticker: {getattr(c, 'id', '')}]")
elif isinstance(c, Reply):
desc.append("[Quote]")
else:
desc.append(f"[{c.__class__.__name__}]")
return "".join(desc) or "[Unknown]"
def _truncate_reply_text(text: str) -> str:
"""Truncate overly long quoted reply text."""
if len(text) <= _MAX_REPLY_TEXT_LENGTH:
return text
return text[:_MAX_REPLY_TEXT_LENGTH] + "..."
def _positive_int(value, fallback: int) -> int:
try:
parsed = int(value)
except (TypeError, ValueError):
return fallback
return parsed if parsed > 0 else fallback
def _trim_left(
records: deque[str],
max_records: int,
record_ids: deque[str] | None = None,
) -> None:
while len(records) > max_records:
records.popleft()
if record_ids:
record_ids.popleft()
def _format_group_history_block(records: list[str]) -> str:
return GROUP_HISTORY_HEADER + "\n".join(records) + GROUP_HISTORY_FOOTER

View File

@@ -1,245 +0,0 @@
import copy
import traceback
from collections.abc import Iterable
from sys import maxsize
import astrbot.api.message_components as Comp
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent, filter
from astrbot.api.message_components import Image, Plain
from astrbot.api.provider import ProviderRequest
from astrbot.core import logger
from astrbot.core.utils.session_waiter import (
FILTERS,
USER_SESSIONS,
SessionController,
SessionWaiter,
session_waiter,
)
from .group_chat_context import GroupChatContext
def _iter_message_components(event: AstrMessageEvent):
messages = getattr(getattr(event, "message_obj", None), "message", None)
if not isinstance(messages, Iterable) or isinstance(messages, (str, bytes)):
return ()
return tuple(messages)
class Main(star.Star):
def __init__(self, context: star.Context) -> None:
self.context = context
self.group_chat_context = None
try:
self.group_chat_context = GroupChatContext(
self.context.astrbot_config_mgr,
self.context,
)
except BaseException as e:
logger.error(f"group chat context init failed: {e}")
@filter.event_message_type(filter.EventMessageType.ALL, priority=maxsize)
async def handle_session_control_agent(self, event: AstrMessageEvent) -> None:
"""会话控制代理"""
for session_filter in FILTERS:
session_id = session_filter.filter(event)
if session_id in USER_SESSIONS:
await SessionWaiter.trigger(session_id, event)
event.stop_event()
@filter.event_message_type(filter.EventMessageType.ALL, priority=maxsize - 1)
async def handle_empty_mention(self, event: AstrMessageEvent):
"""处理只有一个 @ 或仅有唤醒前缀的消息,并等待用户下一条内容。"""
try:
messages = event.get_messages()
cfg = self.context.get_config(umo=event.unified_msg_origin)
p_settings = cfg["platform_settings"]
wake_prefix = cfg.get("wake_prefix", [])
if len(messages) != 1:
return
is_empty_mention = (
isinstance(messages[0], Comp.At)
and str(messages[0].qq) == str(event.get_self_id())
and p_settings.get("empty_mention_waiting", True)
)
is_wake_prefix_only = (
isinstance(messages[0], Comp.Plain)
and messages[0].text.strip() in wake_prefix
)
if not (is_empty_mention or is_wake_prefix_only):
return
if p_settings.get("empty_mention_waiting_need_reply", True):
try:
curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
event.unified_msg_origin,
)
conversation = None
if curr_cid:
conversation = (
await self.context.conversation_manager.get_conversation(
event.unified_msg_origin,
curr_cid,
)
)
else:
curr_cid = (
await self.context.conversation_manager.new_conversation(
event.unified_msg_origin,
platform_id=event.get_platform_id(),
)
)
yield event.request_llm(
prompt=(
"注意,你正在社交媒体上中与用户进行聊天,用户只是通过@来唤醒你,但并未在这条消息中输入内容,他可能会在接下来一条发送他想发送的内容。"
"你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。"
"请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西"
),
session_id=curr_cid,
contexts=[],
system_prompt="",
conversation=conversation,
)
except Exception as e:
logger.error(f"LLM response failed: {e!s}")
yield event.plain_result("想要问什么呢?😄")
@session_waiter(60)
async def empty_mention_waiter(
controller: SessionController,
event: AstrMessageEvent,
) -> None:
if not event.message_str or not event.message_str.strip():
return
event.message_obj.message.insert(
0,
Comp.At(qq=event.get_self_id(), name=event.get_self_id()),
)
new_event = copy.copy(event)
self.context.get_event_queue().put_nowait(new_event)
event.stop_event()
controller.stop()
try:
await empty_mention_waiter(event)
except TimeoutError:
pass
except Exception as e:
yield event.plain_result("发生错误,请联系管理员: " + str(e))
finally:
event.stop_event()
except Exception as e:
logger.error("handle_empty_mention error: " + str(e))
def group_context_enabled(self, event: AstrMessageEvent):
group_context_settings = self.context.get_config(umo=event.unified_msg_origin)[
"provider_ltm_settings"
]
return (
group_context_settings["group_icl_enable"]
or group_context_settings["active_reply"]["enable"]
)
@filter.platform_adapter_type(filter.PlatformAdapterType.ALL)
async def on_message(self, event: AstrMessageEvent):
"""群聊上下文感知"""
message_components = _iter_message_components(event)
has_image_or_plain = False
for comp in message_components:
if isinstance(comp, Plain) or isinstance(comp, Image):
has_image_or_plain = True
break
group_context_enabled = False
if self.group_chat_context:
try:
group_context_enabled = self.group_context_enabled(event)
except BaseException as e:
logger.error(f"group chat context: {e}")
if group_context_enabled and self.group_chat_context and has_image_or_plain:
need_active = await self.group_chat_context.need_active_reply(event)
group_icl_enable = self.context.get_config(umo=event.unified_msg_origin)[
"provider_ltm_settings"
]["group_icl_enable"]
if group_icl_enable:
# Skip recording if a command handler matched (e.g. /reset,
# /help, /new). Slash commands are bot instructions, not group
# chat context that should be injected into future LLM requests.
if not event.get_extra("handlers_parsed_params", {}):
try:
await self.group_chat_context.handle_message(event)
except BaseException as e:
logger.error(e)
if need_active:
provider = self.context.get_using_provider(event.unified_msg_origin)
if not provider:
logger.error("未找到任何 LLM 提供商。请先配置。无法主动回复")
return
try:
session_curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
event.unified_msg_origin,
)
if not session_curr_cid:
logger.error(
"当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /new 创建一个会话。",
)
return
conv = await self.context.conversation_manager.get_conversation(
event.unified_msg_origin,
session_curr_cid,
)
if not conv:
logger.error("未找到对话,无法主动回复")
return
prompt = event.message_str
image_urls = []
for comp in message_components:
if isinstance(comp, Image):
try:
image_urls.append(await comp.convert_to_file_path())
except Exception:
logger.exception("主动回复处理图片失败")
yield event.request_llm(
prompt=prompt,
session_id=event.session_id,
image_urls=image_urls,
conversation=conv,
)
except BaseException as e:
logger.error(traceback.format_exc())
logger.error(f"主动回复失败: {e}")
@filter.on_llm_request()
async def decorate_llm_req(
self, event: AstrMessageEvent, req: ProviderRequest
) -> None:
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
if self.group_chat_context and self.group_context_enabled(event):
try:
await self.group_chat_context.on_req_llm(event, req)
except BaseException as e:
logger.error(f"group chat context: {e}")
@filter.after_message_sent()
async def after_message_sent(self, event: AstrMessageEvent) -> None:
"""消息发送后处理"""
if self.group_chat_context and self.group_context_enabled(event):
try:
clean_session = event.get_extra("_clean_group_context_session", False)
if clean_session:
await self.group_chat_context.remove_session(event)
except Exception as e:
logger.error(f"group chat context: {e}")

View File

@@ -1,4 +0,0 @@
name: astrbot
desc: AstrBot's internal plugin, providing some basic capabilities.
author: AstrBot Team
version: 4.1.0

View File

@@ -1,6 +0,0 @@
{
"metadata": {
"display_name": "Built-in Commands",
"desc": "AstrBot's internal plugin, providing built-in commands such as /reset, /help, and /sid."
}
}

View File

@@ -1,6 +0,0 @@
{
"metadata": {
"display_name": "内置指令",
"desc": "AstrBot 自带插件,提供 /reset、/help、/sid 等内置指令。"
}
}

View File

@@ -1,311 +0,0 @@
from sqlalchemy import case, func, select
from sqlmodel import col
from astrbot.api import sp, star
from astrbot.api.event import AstrMessageEvent, MessageEventResult
from astrbot.core import logger
from astrbot.core.agent.runners.deerflow.constants import (
DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY,
DEERFLOW_PROVIDER_TYPE,
DEERFLOW_THREAD_ID_KEY,
)
from astrbot.core.agent.runners.deerflow.deerflow_api_client import DeerFlowAPIClient
from astrbot.core.db.po import ProviderStat
from astrbot.core.utils.active_event_registry import active_event_registry
from .utils.rst_scene import RstScene
THIRD_PARTY_AGENT_RUNNER_KEY = {
"dify": "dify_conversation_id",
"coze": "coze_conversation_id",
"dashscope": "dashscope_conversation_id",
DEERFLOW_PROVIDER_TYPE: DEERFLOW_THREAD_ID_KEY,
}
THIRD_PARTY_AGENT_RUNNER_STR = ", ".join(THIRD_PARTY_AGENT_RUNNER_KEY.keys())
async def _cleanup_deerflow_thread_if_present(
context: star.Context,
umo: str,
) -> None:
try:
thread_id = await sp.get_async(
scope="umo",
scope_id=umo,
key=DEERFLOW_THREAD_ID_KEY,
default="",
)
if not thread_id:
return
cfg = context.get_config(umo=umo)
provider_id = cfg["provider_settings"].get(
DEERFLOW_AGENT_RUNNER_PROVIDER_ID_KEY,
"",
)
if not provider_id:
return
merged_provider_config = context.provider_manager.get_provider_config_by_id(
provider_id,
merged=True,
)
if not merged_provider_config:
logger.warning(
"Failed to resolve DeerFlow provider config for remote thread cleanup: provider_id=%s",
provider_id,
)
return
client = DeerFlowAPIClient(
api_base=merged_provider_config.get(
"deerflow_api_base",
"http://127.0.0.1:2026",
),
api_key=merged_provider_config.get("deerflow_api_key", ""),
auth_header=merged_provider_config.get("deerflow_auth_header", ""),
proxy=merged_provider_config.get("proxy", ""),
)
try:
await client.delete_thread(thread_id)
finally:
try:
await client.close()
except Exception as e:
logger.warning(
"Failed to close DeerFlow API client after thread cleanup: %s",
e,
)
except Exception as e:
logger.warning(
"Failed to clean up DeerFlow thread for session %s: %s",
umo,
e,
)
async def _clear_third_party_agent_runner_state(
context: star.Context,
umo: str,
agent_runner_type: str,
) -> None:
session_key = THIRD_PARTY_AGENT_RUNNER_KEY.get(agent_runner_type)
if not session_key:
return
if agent_runner_type == DEERFLOW_PROVIDER_TYPE:
await _cleanup_deerflow_thread_if_present(context, umo)
await sp.remove_async(
scope="umo",
scope_id=umo,
key=session_key,
)
class ConversationCommands:
def __init__(self, context: star.Context) -> None:
self.context = context
async def _get_current_persona_id(self, session_id):
curr = await self.context.conversation_manager.get_curr_conversation_id(
session_id,
)
if not curr:
return None
conv = await self.context.conversation_manager.get_conversation(
session_id,
curr,
)
if not conv:
return None
return conv.persona_id
async def reset(self, message: AstrMessageEvent) -> None:
"""重置 LLM 会话"""
umo = message.unified_msg_origin
cfg = self.context.get_config(umo=message.unified_msg_origin)
is_unique_session = cfg["platform_settings"]["unique_session"]
is_group = bool(message.get_group_id())
scene = RstScene.get_scene(is_group, is_unique_session)
alter_cmd_cfg = await sp.get_async("global", "global", "alter_cmd", {})
plugin_config = alter_cmd_cfg.get("astrbot", {})
reset_cfg = plugin_config.get("reset", {})
required_perm = reset_cfg.get(
scene.key,
"admin" if is_group and not is_unique_session else "member",
)
if required_perm == "admin" and message.role != "admin":
message.set_result(
MessageEventResult().message(
f"Reset command requires admin permission in {scene.name} scenario, "
f"you (ID {message.get_sender_id()}) are not admin, cannot perform this action.",
),
)
return
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
active_event_registry.stop_all(umo, exclude=message)
await _clear_third_party_agent_runner_state(
self.context,
umo,
agent_runner_type,
)
message.set_result(
MessageEventResult().message("✅ Conversation reset successfully.")
)
return
if not self.context.get_using_provider(umo):
message.set_result(
MessageEventResult().message(
"😕 Cannot find any LLM provider. Configure one first."
),
)
return
cid = await self.context.conversation_manager.get_curr_conversation_id(umo)
if not cid:
message.set_result(
MessageEventResult().message(
"😕 You are not in a conversation. Use /new to create one.",
),
)
return
active_event_registry.stop_all(umo, exclude=message)
await self.context.conversation_manager.update_conversation(
umo,
cid,
[],
)
ret = "✅ Conversation reset successfully."
message.set_extra("_clean_group_context_session", True)
message.set_result(MessageEventResult().message(ret))
async def stop(self, message: AstrMessageEvent) -> None:
"""停止当前会话正在运行的 Agent"""
cfg = self.context.get_config(umo=message.unified_msg_origin)
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
umo = message.unified_msg_origin
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
stopped_count = active_event_registry.stop_all(umo, exclude=message)
else:
stopped_count = active_event_registry.request_agent_stop_all(
umo,
exclude=message,
)
if stopped_count > 0:
message.set_result(
MessageEventResult().message(
f"✅ Requested to stop {stopped_count} running tasks."
)
)
return
message.set_result(
MessageEventResult().message("✅ No running tasks in the current session.")
)
async def new_conv(self, message: AstrMessageEvent) -> None:
"""创建新对话"""
cfg = self.context.get_config(umo=message.unified_msg_origin)
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
active_event_registry.stop_all(message.unified_msg_origin, exclude=message)
await _clear_third_party_agent_runner_state(
self.context,
message.unified_msg_origin,
agent_runner_type,
)
message.set_result(
MessageEventResult().message("✅ New conversation created.")
)
return
active_event_registry.stop_all(message.unified_msg_origin, exclude=message)
cpersona = await self._get_current_persona_id(message.unified_msg_origin)
cid = await self.context.conversation_manager.new_conversation(
message.unified_msg_origin,
message.get_platform_id(),
persona_id=cpersona,
)
message.set_extra("_clean_group_context_session", True)
message.set_result(
MessageEventResult().message(
f"✅ Switched to new conversation: {cid[:4]}"
),
)
async def stats(self, message: AstrMessageEvent) -> None:
"""Show token usage statistics for the current conversation."""
umo = message.unified_msg_origin
cid = await self.context.conversation_manager.get_curr_conversation_id(umo)
if not cid:
message.set_result(
MessageEventResult().message(
"❌ You are not in a conversation. Use /new to create one."
),
)
return
db = self.context.get_db()
async with db.get_db() as session:
result = await session.execute(
select(
func.count(case((col(ProviderStat.id).is_not(None), 1))).label(
"record_count",
),
func.coalesce(func.sum(ProviderStat.token_input_other), 0).label(
"total_input_other",
),
func.coalesce(func.sum(ProviderStat.token_input_cached), 0).label(
"total_input_cached",
),
func.coalesce(func.sum(ProviderStat.token_output), 0).label(
"total_output",
),
).where(
col(ProviderStat.agent_type) == "internal",
col(ProviderStat.conversation_id) == cid,
)
)
stats = result.one()
if stats.record_count == 0:
message.set_result(
MessageEventResult().message(
"📊 No stats available for this conversation yet."
),
)
return
total_input_other = stats.total_input_other
total_input_cached = stats.total_input_cached
total_output = stats.total_output
total_tokens = total_input_other + total_input_cached + total_output
ret = (
f"📊 Conversation Token usage (ID: {cid[:8]}...)\n"
f"Total: {total_tokens:,}\n"
f"Input (cached): {total_input_cached:,}\n"
f"Input (other): {total_input_other:,}\n"
f"Output: {total_output:,}\n"
)
message.set_result(MessageEventResult().message(ret))

View File

@@ -1,89 +0,0 @@
import aiohttp
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent, MessageEventResult
from astrbot.core.config.default import VERSION
from astrbot.core.star import command_management
from astrbot.core.utils.io import get_dashboard_version
class HelpCommand:
def __init__(self, context: star.Context) -> None:
self.context = context
async def _query_astrbot_notice(self):
try:
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(
"https://astrbot.app/notice.json",
timeout=2,
) as resp:
return (await resp.json())["notice"]
except BaseException:
return ""
async def _build_reserved_command_lines(self) -> list[str]:
"""使用实时指令配置生成内置指令清单,确保重命名/禁用后与实际生效状态保持一致。"""
try:
commands = await command_management.list_commands()
except BaseException:
return []
lines: list[str] = []
def walk(items: list[dict], indent: int = 0) -> None:
for item in items:
if not item.get("reserved") or not item.get("enabled"):
continue
# 仅展示顶级指令或指令组
if item.get("type") == "sub_command":
continue
if item.get("parent_signature"):
continue
effective = (
item.get("effective_command")
or item.get("original_command")
or item.get("handler_name")
)
if not effective or effective in [
"set",
"unset",
"help",
"dashboard_update",
]:
continue
description = item.get("description") or ""
desc_text = f" - {description}" if description else ""
indent_prefix = " " * indent
lines.append(f"{indent_prefix}/{effective}{desc_text}")
walk(commands)
return lines
async def help(self, event: AstrMessageEvent) -> None:
"""查看帮助"""
notice = ""
try:
notice = await self._query_astrbot_notice()
except BaseException:
pass
dashboard_version = await get_dashboard_version()
command_lines = await self._build_reserved_command_lines()
commands_section = (
"\n".join(command_lines)
if command_lines
else "No enabled built-in commands."
)
msg_parts = [
f"AstrBot v{VERSION}(WebUI: {dashboard_version})",
commands_section,
]
if notice:
msg_parts.append(notice)
msg = "\n".join(msg_parts)
event.set_result(MessageEventResult().message(msg).use_t2i(False))

View File

@@ -1,48 +0,0 @@
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent, MessageEventResult
from astrbot.core.umo_alias import get_event_auto_name, normalize_umo_name
class NameCommand:
def __init__(self, context: star.Context) -> None:
self.context = context
async def name(self, event: AstrMessageEvent, alias: str) -> None:
umo = event.unified_msg_origin
auto_name = get_event_auto_name(event)
alias = normalize_umo_name(alias)
if not alias:
saved_alias = await self.context.get_db().get_umo_alias(umo)
user_alias = normalize_umo_name(
saved_alias.user_alias if saved_alias else ""
)
event.set_result(
MessageEventResult()
.message(
"\n".join(
[
"Usage: /name <name>",
f"UMO: {umo}",
f"Auto name: {auto_name or '(empty)'}",
f"Alias: {user_alias or '(empty)'}",
]
)
)
.use_t2i(False)
)
return
sender_id = str(event.get_sender_id() or "")
await self.context.get_db().upsert_umo_alias(
umo=umo,
creator_sender_id=sender_id,
auto_name=auto_name,
user_alias=alias,
)
event.set_result(
MessageEventResult()
.message(f"UMO name set to: {alias}\nUMO: {umo}")
.use_t2i(False)
)

View File

@@ -1,214 +0,0 @@
import builtins
from typing import TYPE_CHECKING
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent, MessageEventResult
if TYPE_CHECKING:
from astrbot.core.db.po import Persona
class PersonaCommands:
def __init__(self, context: star.Context) -> None:
self.context = context
def _build_tree_output(
self,
folder_tree: list[dict],
all_personas: list["Persona"],
depth: int = 0,
) -> list[str]:
"""递归构建树状输出,使用短线条表示层级"""
lines: list[str] = []
# 使用短线条作为缩进前缀,每层只用 "" 加一个空格
prefix = " " * depth
for folder in folder_tree:
# 输出文件夹
lines.append(f"{prefix}├ 📁 {folder['name']}/")
# 获取该文件夹下的人格
folder_personas = [
p for p in all_personas if p.folder_id == folder["folder_id"]
]
child_prefix = " " * (depth + 1)
# 输出该文件夹下的人格
for persona in folder_personas:
lines.append(f"{child_prefix}├ 👤 {persona.persona_id}")
# 递归处理子文件夹
children = folder.get("children", [])
if children:
lines.extend(
self._build_tree_output(
children,
all_personas,
depth + 1,
),
)
return lines
async def persona(self, message: AstrMessageEvent) -> None:
parts = message.message_str.split(" ")
umo = message.unified_msg_origin
curr_persona_name = ""
cid = await self.context.conversation_manager.get_curr_conversation_id(umo)
default_persona = await self.context.persona_manager.get_default_persona_v3(
umo=umo,
)
force_applied_persona_id = None
curr_cid_title = ""
if cid:
conv = await self.context.conversation_manager.get_conversation(
unified_msg_origin=umo,
conversation_id=cid,
create_if_not_exists=True,
)
if conv is None:
message.set_result(
MessageEventResult().message(
"当前对话不存在,请先使用 /new 新建一个对话。",
),
)
return
provider_settings = self.context.get_config(umo=umo).get(
"provider_settings",
{},
)
(
persona_id,
_,
force_applied_persona_id,
_,
) = await self.context.persona_manager.resolve_selected_persona(
umo=umo,
conversation_persona_id=conv.persona_id,
platform_name=message.get_platform_name(),
provider_settings=provider_settings,
)
if persona_id == "[%None]":
curr_persona_name = ""
elif persona_id:
curr_persona_name = persona_id
if force_applied_persona_id:
curr_persona_name = f"{curr_persona_name} (自定义规则)"
curr_cid_title = conv.title or "新对话"
curr_cid_title += f"({cid[:4]})"
if len(parts) == 1:
message.set_result(
MessageEventResult()
.message(
f"""[Persona]
- 人格情景列表: `/persona list`
- 设置人格情景: `/persona 人格`
- 人格情景详细信息: `/persona view 人格`
- 取消人格: `/persona unset`
默认人格情景: {default_persona["name"]}
当前对话 {curr_cid_title} 的人格情景: {curr_persona_name}
配置人格情景请前往管理面板-配置页
""",
)
.use_t2i(False),
)
elif parts[1] == "list":
# 获取文件夹树和所有人格
folder_tree = await self.context.persona_manager.get_folder_tree()
all_personas = self.context.persona_manager.personas
lines = ["📂 人格列表:\n"]
# 构建树状输出
tree_lines = self._build_tree_output(folder_tree, all_personas)
lines.extend(tree_lines)
# 输出根目录下的人格(没有文件夹的)
root_personas = [p for p in all_personas if p.folder_id is None]
if root_personas:
if tree_lines: # 如果有文件夹内容,加个空行
lines.append("")
for persona in root_personas:
lines.append(f"👤 {persona.persona_id}")
# 统计信息
total_count = len(all_personas)
lines.append(f"\n{total_count} 个人格")
lines.append("\n*使用 `/persona <人格名>` 设置人格")
lines.append("*使用 `/persona view <人格名>` 查看详细信息")
msg = "\n".join(lines)
message.set_result(MessageEventResult().message(msg).use_t2i(False))
elif parts[1] == "view":
if len(parts) == 2:
message.set_result(MessageEventResult().message("请输入人格情景名"))
return
ps = parts[2].strip()
if persona_info := next(
builtins.filter(
lambda persona: persona["name"] == ps,
self.context.provider_manager.personas,
),
None,
):
msg = f"人格{ps}的详细信息:\n"
msg += f"{persona_info['prompt']}\n"
else:
msg = f"人格{ps}不存在"
message.set_result(MessageEventResult().message(msg))
elif parts[1] == "unset":
if not cid:
message.set_result(
MessageEventResult().message("当前没有对话,无法取消人格。"),
)
return
await self.context.conversation_manager.update_conversation_persona_id(
message.unified_msg_origin,
"[%None]",
)
message.set_result(MessageEventResult().message("取消人格成功。"))
else:
ps = "".join(parts[1:]).strip()
if not cid:
message.set_result(
MessageEventResult().message(
"当前没有对话,请先开始对话或使用 /new 创建一个对话。",
),
)
return
if persona_info := next(
builtins.filter(
lambda persona: persona["name"] == ps,
self.context.provider_manager.personas,
),
None,
):
await self.context.conversation_manager.update_conversation_persona_id(
message.unified_msg_origin,
ps,
)
force_warn_msg = ""
if force_applied_persona_id:
force_warn_msg = "提醒:由于自定义规则,您现在切换的人格将不会生效。"
message.set_result(
MessageEventResult().message(
f"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。{force_warn_msg}",
),
)
else:
message.set_result(
MessageEventResult().message(
"不存在该人格情景。使用 /persona list 查看所有。",
),
)

View File

@@ -1,330 +0,0 @@
from __future__ import annotations
import asyncio
from astrbot import logger
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent, MessageEventResult
from astrbot.core.provider.entities import ProviderType
from astrbot.core.utils.error_redaction import safe_error
class ProviderCommands:
def __init__(self, context: star.Context) -> None:
self.context = context
def _log_reachability_failure(
self,
provider,
provider_capability_type: ProviderType | None,
err_code: str,
err_reason: str,
) -> None:
meta = provider.meta()
logger.warning(
"Provider reachability check failed: id=%s type=%s code=%s reason=%s",
meta.id,
provider_capability_type.name if provider_capability_type else "unknown",
err_code,
err_reason,
)
async def _test_provider_capability(self, provider):
meta = provider.meta()
provider_capability_type = meta.provider_type
try:
await provider.test()
return True, None, None
except Exception as e:
err_code = "TEST_FAILED"
err_reason = safe_error("", e)
self._log_reachability_failure(
provider,
provider_capability_type,
err_code,
err_reason,
)
return False, err_code, err_reason
async def _build_provider_display_data(
self,
providers,
provider_type: str,
reachability_check_enabled: bool,
) -> list[dict]:
if not providers:
return []
if reachability_check_enabled:
check_results = await asyncio.gather(
*[self._test_provider_capability(provider) for provider in providers],
return_exceptions=True,
)
else:
check_results = [None for _ in providers]
display_data = []
for provider, reachable in zip(providers, check_results, strict=False):
meta = provider.meta()
id_ = meta.id
error_code = None
if isinstance(reachable, asyncio.CancelledError):
raise reachable
if isinstance(reachable, Exception):
self._log_reachability_failure(
provider,
None,
reachable.__class__.__name__,
safe_error("", reachable),
)
reachable_flag = False
error_code = reachable.__class__.__name__
elif isinstance(reachable, tuple):
reachable_flag, error_code, _ = reachable
else:
reachable_flag = reachable
if provider_type == "llm":
info = f"{id_} ({meta.model})"
else:
info = f"{id_}"
if reachable_flag is True:
mark = ""
elif reachable_flag is False:
if error_code:
mark = f" ❌(errcode: {error_code})"
else:
mark = ""
else:
mark = ""
display_data.append(
{
"info": info,
"mark": mark,
"provider": provider,
},
)
return display_data
async def provider(
self,
event: AstrMessageEvent,
idx: str | int | None = None,
idx2: int | None = None,
) -> None:
"""查看或者切换 LLM Provider"""
umo = event.unified_msg_origin
cfg = self.context.get_config(umo).get("provider_settings", {})
reachability_check_enabled = cfg.get("reachability_check", True)
if idx is None:
parts = ["## LLM Providers\n"]
llms = list(self.context.get_all_providers())
ttss = self.context.get_all_tts_providers()
stts = self.context.get_all_stt_providers()
if reachability_check_enabled and (llms or ttss or stts):
await event.send(
MessageEventResult().message("👀 Testing provider reachability..."),
)
llm_data, tts_data, stt_data = await asyncio.gather(
self._build_provider_display_data(
llms,
"llm",
reachability_check_enabled,
),
self._build_provider_display_data(
ttss,
"tts",
reachability_check_enabled,
),
self._build_provider_display_data(
stts,
"stt",
reachability_check_enabled,
),
)
provider_using = self.context.get_using_provider(umo=umo)
for i, d in enumerate(llm_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if (
provider_using
and provider_using.meta().id == d["provider"].meta().id
):
line += " 👈"
parts.append(line + "\n")
if tts_data:
parts.append("\n## TTS Providers\n")
tts_using = self.context.get_using_tts_provider(umo=umo)
for i, d in enumerate(tts_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if tts_using and tts_using.meta().id == d["provider"].meta().id:
line += " 👈"
parts.append(line + "\n")
if stt_data:
parts.append("\n## STT Providers\n")
stt_using = self.context.get_using_stt_provider(umo=umo)
for i, d in enumerate(stt_data):
line = f"{i + 1}. {d['info']}{d['mark']}"
if stt_using and stt_using.meta().id == d["provider"].meta().id:
line += " 👈"
parts.append(line + "\n")
parts.append("\nUse /provider <idx> to switch LLM providers.")
ret = "".join(parts)
if ttss:
ret += "\nUse /provider tts <idx> to switch TTS providers."
if stts:
ret += "\nUse /provider stt <idx> to switch STT providers."
event.set_result(MessageEventResult().message(ret))
elif idx == "tts":
if idx2 is None:
event.set_result(
MessageEventResult().message("Please enter the index."),
)
return
if idx2 > len(self.context.get_all_tts_providers()) or idx2 < 1:
event.set_result(
MessageEventResult().message("❌ Invalid provider index."),
)
return
provider = self.context.get_all_tts_providers()[idx2 - 1]
id_ = provider.meta().id
await self.context.provider_manager.set_provider(
provider_id=id_,
provider_type=ProviderType.TEXT_TO_SPEECH,
umo=umo,
)
event.set_result(
MessageEventResult().message(f"✅ Successfully switched to {id_}."),
)
elif idx == "stt":
if idx2 is None:
event.set_result(
MessageEventResult().message("Please enter the index."),
)
return
if idx2 > len(self.context.get_all_stt_providers()) or idx2 < 1:
event.set_result(
MessageEventResult().message("❌ Invalid provider index."),
)
return
provider = self.context.get_all_stt_providers()[idx2 - 1]
id_ = provider.meta().id
await self.context.provider_manager.set_provider(
provider_id=id_,
provider_type=ProviderType.SPEECH_TO_TEXT,
umo=umo,
)
event.set_result(
MessageEventResult().message(f"✅ Successfully switched to {id_}."),
)
elif isinstance(idx, int):
if idx > len(self.context.get_all_providers()) or idx < 1:
event.set_result(
MessageEventResult().message("❌ Invalid provider index."),
)
return
provider = self.context.get_all_providers()[idx - 1]
id_ = provider.meta().id
await self.context.provider_manager.set_provider(
provider_id=id_,
provider_type=ProviderType.CHAT_COMPLETION,
umo=umo,
)
event.set_result(
MessageEventResult().message(f"✅ Successfully switched to {id_}."),
)
else:
event.set_result(MessageEventResult().message("❌ Invalid parameter."))
async def model_ls(
self,
event: AstrMessageEvent,
idx_or_name: int | str | None = None,
) -> None:
"""查看或者切换当前 Provider 的模型。"""
umo = event.unified_msg_origin
provider = self.context.get_using_provider(umo=umo)
if provider is None:
event.set_result(
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"),
)
return
try:
models = await provider.get_models()
except Exception as e:
event.set_result(
MessageEventResult().message(
f"获取模型列表失败: {safe_error('', e)}",
),
)
return
current_model = provider.get_model()
if idx_or_name is None:
if not models:
event.set_result(
MessageEventResult().message(
f"当前模型: {current_model}\n此提供商未返回可切换模型列表。",
),
)
return
parts = [f"当前模型: {current_model}\n\n可用模型:\n"]
for index, model_name in enumerate(models, start=1):
suffix = " 👈" if model_name == current_model else ""
parts.append(f"{index}. {model_name}{suffix}\n")
parts.append("\n使用 /model <序号> 或 /model <模型名> 切换模型。")
event.set_result(MessageEventResult().message("".join(parts)))
return
selected_model: str | None = None
if isinstance(idx_or_name, int):
if 1 <= idx_or_name <= len(models):
selected_model = models[idx_or_name - 1]
else:
text = idx_or_name.strip()
if text.isdigit():
model_index = int(text)
if 1 <= model_index <= len(models):
selected_model = models[model_index - 1]
elif text:
selected_model = text
if not selected_model:
event.set_result(MessageEventResult().message("❌ Invalid model index."))
return
provider.set_model(selected_model)
provider.provider_config["model"] = selected_model
cfg = self.context.get_config(umo)
providers_config = cfg.get("provider", [])
if isinstance(providers_config, list):
for provider_config in providers_config:
if not isinstance(provider_config, dict):
continue
if provider_config.get("id") == provider.meta().id:
provider_config["model"] = selected_model
break
cfg.save_config()
event.set_result(
MessageEventResult().message(
f"✅ Successfully switched model to {selected_model}.",
),
)

View File

@@ -1,36 +0,0 @@
"""会话ID命令"""
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent, MessageEventResult
class SIDCommand:
"""会话ID命令类"""
def __init__(self, context: star.Context) -> None:
self.context = context
async def sid(self, event: AstrMessageEvent) -> None:
"""获取消息来源信息"""
sid = event.unified_msg_origin
user_id = str(event.get_sender_id())
umo_platform = event.session.platform_id
umo_msg_type = event.session.message_type.value
umo_session_id = event.session.session_id
ret = (
f"UMO: 「{sid}」 此值可用于设置白名单。\n"
f"UID: 「{user_id}」 此值可用于设置管理员。\n"
f"消息会话来源信息:\n"
f" 机器人 ID: 「{umo_platform}\n"
f" 消息类型: 「{umo_msg_type}\n"
f" 会话 ID: 「{umo_session_id}\n"
f"消息来源可用于配置机器人的配置文件路由。"
)
if (
self.context.get_config()["platform_settings"]["unique_session"]
and event.get_group_id()
):
ret += f"\n\n当前处于独立会话模式, 此群 ID: 「{event.get_group_id()}」, 也可将此 ID 加入白名单来放行整个群聊。"
event.set_result(MessageEventResult().message(ret).use_t2i(False))

View File

@@ -1,26 +0,0 @@
from enum import Enum
class RstScene(Enum):
GROUP_UNIQUE_ON = ("group_unique_on", "群聊+会话隔离开启")
GROUP_UNIQUE_OFF = ("group_unique_off", "群聊+会话隔离关闭")
PRIVATE = ("private", "私聊")
@property
def key(self) -> str:
return self.value[0]
@property
def name(self) -> str:
return self.value[1]
@classmethod
def from_index(cls, index: int) -> "RstScene":
mapping = {1: cls.GROUP_UNIQUE_ON, 2: cls.GROUP_UNIQUE_OFF, 3: cls.PRIVATE}
return mapping[index]
@classmethod
def get_scene(cls, is_group: bool, is_unique_session: bool) -> "RstScene":
if is_group:
return cls.GROUP_UNIQUE_ON if is_unique_session else cls.GROUP_UNIQUE_OFF
return cls.PRIVATE

View File

@@ -1,195 +0,0 @@
from astrbot.api import star
from astrbot.api.event import AstrMessageEvent, filter
from astrbot.core.star.filter.command import GreedyStr
from .commands import (
AdminCommands,
AlterCmdCommands,
ConversationCommands,
HelpCommand,
LLMCommands,
NameCommand,
PluginCommands,
ProviderCommands,
SetUnsetCommands,
SIDCommand,
T2ICommand,
TTSCommand,
)
class Main(star.Star):
def __init__(self, context: star.Context) -> None:
self.context = context
self.help_c = HelpCommand(self.context)
self.llm_c = LLMCommands(self.context)
self.plugin_c = PluginCommands(self.context)
self.admin_c = AdminCommands(self.context)
self.conversation_c = ConversationCommands(self.context)
self.name_c = NameCommand(self.context)
self.provider_c = ProviderCommands(self.context)
self.setunset_c = SetUnsetCommands(self.context)
self.t2i_c = T2ICommand(self.context)
self.tts_c = TTSCommand(self.context)
self.sid_c = SIDCommand(self.context)
self.alter_cmd_c = AlterCmdCommands(self.context)
@filter.command("help")
async def help(self, event: AstrMessageEvent) -> None:
"""查看帮助"""
await self.help_c.help(event)
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("llm")
async def llm(self, event: AstrMessageEvent) -> None:
"""开启/关闭 LLM"""
await self.llm_c.llm(event)
@filter.command_group("plugin")
def plugin(self) -> None:
"""插件管理"""
@plugin.command("ls")
async def plugin_ls(self, event: AstrMessageEvent) -> None:
"""获取已经安装的插件列表。"""
await self.plugin_c.plugin_ls(event)
@filter.permission_type(filter.PermissionType.ADMIN)
@plugin.command("off")
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
"""禁用插件"""
await self.plugin_c.plugin_off(event, plugin_name)
@filter.permission_type(filter.PermissionType.ADMIN)
@plugin.command("on")
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
"""启用插件"""
await self.plugin_c.plugin_on(event, plugin_name)
@filter.permission_type(filter.PermissionType.ADMIN)
@plugin.command("get")
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = "") -> None:
"""安装插件"""
await self.plugin_c.plugin_get(event, plugin_repo)
@plugin.command("help")
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
"""获取插件帮助"""
await self.plugin_c.plugin_help(event, plugin_name)
@filter.command("t2i")
async def t2i(self, event: AstrMessageEvent) -> None:
"""开关文本转图片"""
await self.t2i_c.t2i(event)
@filter.command("tts")
async def tts(self, event: AstrMessageEvent) -> None:
"""开关文本转语音(会话级别)"""
await self.tts_c.tts(event)
@filter.command("sid")
async def sid(self, event: AstrMessageEvent) -> None:
"""获取会话 ID 和 管理员 ID"""
await self.sid_c.sid(event)
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("name")
async def name(self, event: AstrMessageEvent, alias: GreedyStr) -> None:
"""Set display name for current UMO"""
await self.name_c.name(event, alias)
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("op")
async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None:
"""授权管理员。op <admin_id>"""
await self.admin_c.op(event, admin_id)
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("deop")
async def deop(self, event: AstrMessageEvent, admin_id: str) -> None:
"""取消授权管理员。deop <admin_id>"""
await self.admin_c.deop(event, admin_id)
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("wl")
async def wl(self, event: AstrMessageEvent, sid: str = "") -> None:
"""添加白名单。wl <sid>"""
await self.admin_c.wl(event, sid)
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("dwl")
async def dwl(self, event: AstrMessageEvent, sid: str) -> None:
"""删除白名单。dwl <sid>"""
await self.admin_c.dwl(event, sid)
@filter.command("stats")
async def stats(self, message: AstrMessageEvent) -> None:
"""Show token usage statistics for the current conversation"""
await self.conversation_c.stats(message)
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("provider")
async def provider(
self,
event: AstrMessageEvent,
idx: str | int | None = None,
idx2: int | None = None,
) -> None:
"""查看或者切换 LLM Provider"""
await self.provider_c.provider(event, idx, idx2)
@filter.command("reset")
async def reset(self, message: AstrMessageEvent) -> None:
"""重置 LLM 会话"""
await self.conversation_c.reset(message)
@filter.command("stop")
async def stop(self, message: AstrMessageEvent) -> None:
"""停止当前会话中正在运行的 Agent"""
await self.conversation_c.stop(message)
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("model")
async def model_ls(
self,
message: AstrMessageEvent,
idx_or_name: int | str | None = None,
) -> None:
"""查看或者切换模型"""
await self.provider_c.model_ls(message, idx_or_name)
@filter.command("history")
async def his(self, message: AstrMessageEvent, page: int = 1) -> None:
"""查看对话记录"""
await self.conversation_c.his(message, page)
@filter.command("ls")
async def convs(self, message: AstrMessageEvent, page: int = 1) -> None:
"""查看对话列表"""
await self.conversation_c.convs(message, page)
@filter.command("new")
async def new_conv(self, message: AstrMessageEvent) -> None:
"""创建新对话"""
await self.conversation_c.new_conv(message)
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("dashboard_update")
async def update_dashboard(self, event: AstrMessageEvent) -> None:
"""更新管理面板"""
await self.admin_c.update_dashboard(event)
@filter.command("set")
async def set_variable(self, event: AstrMessageEvent, key: str, value: str) -> None:
await self.setunset_c.set_variable(event, key, value)
@filter.command("unset")
async def unset_variable(self, event: AstrMessageEvent, key: str) -> None:
await self.setunset_c.unset_variable(event, key)
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("alter_cmd", alias={"alter"})
async def alter_cmd(self, event: AstrMessageEvent) -> None:
"""修改命令权限"""
await self.alter_cmd_c.alter_cmd(event)

View File

@@ -1,4 +0,0 @@
name: builtin_commands
desc: AstrBot's internal plugin, providing all built-in commands such as /reset.
author: Soulter
version: 0.0.1

View File

@@ -1,146 +0,0 @@
import random
import urllib.parse
from collections.abc import Callable
from dataclasses import dataclass
from aiohttp import ClientSession, ClientTimeout
from bs4 import BeautifulSoup, Tag
HEADERS = {
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; rv:84.0) Gecko/20100101 Firefox/84.0",
"Accept": "*/*",
"Connection": "keep-alive",
"Accept-Language": "en-GB,en;q=0.5",
}
USER_AGENT_BING = "Mozilla/5.0 (Windows NT 6.1; rv:84.0) Gecko/20100101 Firefox/84.0"
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:89.0) Gecko/20100101 Firefox/89.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.1.2 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Version/14.1 Safari/537.36",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:89.0) Gecko/20100101 Firefox/89.0",
"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:88.0) Gecko/20100101 Firefox/88.0",
]
@dataclass
class SearchResult:
title: str
url: str
snippet: str
favicon: str | None = None
def __str__(self) -> str:
return f"{self.title} - {self.url}\n{self.snippet}"
class SearchEngine:
"""搜索引擎爬虫基类"""
def __init__(self) -> None:
self.TIMEOUT = ClientTimeout(total=10)
self.page = 1
self.headers = HEADERS
def _set_selector(self, selector: str) -> str:
raise NotImplementedError
async def _get_next_page(self, query: str) -> str:
raise NotImplementedError
async def _get_html(self, url: str, data: dict | None = None) -> str:
headers = self.headers
headers["Referer"] = url
headers["User-Agent"] = random.choice(USER_AGENTS)
if data:
async with (
ClientSession() as session,
session.post(
url,
headers=headers,
data=data,
timeout=self.TIMEOUT,
) as resp,
):
ret = await resp.text(encoding="utf-8")
return ret
else:
async with (
ClientSession() as session,
session.get(
url,
headers=headers,
timeout=self.TIMEOUT,
) as resp,
):
ret = await resp.text(encoding="utf-8")
return ret
def tidy_text(self, text: str) -> str:
"""清理文本,去除空格、换行符等"""
return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ")
def _get_url(self, tag: Tag) -> str:
return self.tidy_text(tag.get_text())
async def search(self, query: str, num_results: int) -> list[SearchResult]:
query = urllib.parse.quote(query)
try:
resp = await self._get_next_page(query)
soup = BeautifulSoup(resp, "html.parser")
links = soup.select(self._set_selector("links"))
results = []
try:
text_selector = self._set_selector("text")
except (KeyError, NotImplementedError):
# Keep backward compatibility with engines that only expose
# title/url/link selectors and do not provide snippets.
text_selector = ""
for link in links:
# Safely get the title text (select_one may return None)
title_elem = link.select_one(self._set_selector("title"))
title = ""
if title_elem is not None:
title = self.tidy_text(title_elem.get_text())
url_tag = link.select_one(self._set_selector("url"))
snippet = ""
if text_selector:
text_elem = link.select_one(text_selector)
if text_elem is not None:
snippet = self.tidy_text(text_elem.get_text())
if title and url_tag:
url = self._get_url(url_tag)
if not url:
continue
if url.startswith("//"):
url = f"https:{url}"
results.append(SearchResult(title=title, url=url, snippet=snippet))
return results[:num_results] if len(results) > num_results else results
except Exception as e:
raise e
async def _search_with_result_filter(
self,
query: str,
num_results: int,
predicate: Callable[[SearchResult], bool],
) -> list[SearchResult]:
if num_results <= 0:
return []
rough_results = await SearchEngine.search(self, query, max(num_results * 2, 10))
final_results: list[SearchResult] = []
for result in rough_results:
if not predicate(result):
continue
final_results.append(result)
if len(final_results) >= num_results:
break
return final_results

View File

@@ -1,64 +0,0 @@
from urllib.parse import unquote, urlencode, urlparse
from bs4 import Tag
from . import SearchEngine, SearchResult
class Comet(SearchEngine):
"""Best-effort search via public Perplexity/Comet page.
Note:
- This endpoint is often protected by anti-bot challenges.
- We intentionally treat failures as non-fatal and rely on fallback engines.
"""
NAME = "comet"
def __init__(self) -> None:
super().__init__()
self.base_url = "https://www.perplexity.ai"
def _set_selector(self, selector: str):
selectors = {
"url": "a[href^='http'], a[href^='//']",
"title": "main h1, main h2, main h3, h3, h2",
"text": "main article, main div[role='article'], main section, main p, p",
"links": "main article, main div[role='article'], main li, main div.result, article, div[role='article'], li, div.result",
"next": "",
}
return selectors[selector]
async def _get_next_page(self, query: str) -> str:
url = f"{self.base_url}/search?{urlencode({'q': unquote(query)})}"
return await self._get_html(url, None)
def _get_url(self, tag: Tag) -> str:
href = str(tag.get("href") or "")
if href.startswith("//"):
return f"https:{href}"
return href
@staticmethod
def _is_valid_result_url(url: str) -> bool:
lowered = (url or "").strip().lower()
if not lowered:
return False
if lowered.startswith(("#", "javascript:", "mailto:")):
return False
if not lowered.startswith(("http://", "https://")):
return False
netloc = urlparse(lowered).netloc
if not netloc:
return False
if netloc.endswith("perplexity.ai"):
return False
return True
async def search(self, query: str, num_results: int) -> list[SearchResult]:
return await self._search_with_result_filter(
query=query,
num_results=num_results,
predicate=lambda result: self._is_valid_result_url(result.url),
)

View File

@@ -1,43 +0,0 @@
import urllib.parse
from bs4 import Tag
from . import SearchEngine, SearchResult
class DuckDuckGo(SearchEngine):
NAME = "duckduckgo"
def __init__(self) -> None:
super().__init__()
self.base_url = "https://html.duckduckgo.com/html"
def _set_selector(self, selector: str):
selectors = {
"url": "a.result__a, h2 a",
"title": "a.result__a, h2",
"text": "a.result__snippet, div.result__snippet",
"links": "div.result, div.web-result",
"next": "a.result--more__btn",
}
return selectors[selector]
async def _get_next_page(self, query: str) -> str:
params = {"q": urllib.parse.unquote(query), "kl": "us-en"}
url = f"{self.base_url}/?{urllib.parse.urlencode(params)}"
return await self._get_html(url, None)
def _get_url(self, tag: Tag) -> str:
href = str(tag.get("href") or "")
if "duckduckgo.com/l/?" in href:
parsed = urllib.parse.urlparse(href)
target = urllib.parse.parse_qs(parsed.query).get("uddg", [""])[0]
return urllib.parse.unquote(target)
return href
async def search(self, query: str, num_results: int) -> list[SearchResult]:
return await self._search_with_result_filter(
query=query,
num_results=num_results,
predicate=lambda result: result.url.startswith("http"),
)

View File

@@ -1,51 +0,0 @@
import urllib.parse
from bs4 import Tag
from . import SearchEngine, SearchResult
class Google(SearchEngine):
NAME = "google"
def __init__(self) -> None:
super().__init__()
self.base_url = "https://www.google.com"
def _set_selector(self, selector: str):
selectors = {
"url": "a[href]",
"title": "h3",
"text": "div.VwiC3b, span.aCOpRe",
"links": "div#search div.g, div#search div.MjjYud",
"next": "a#pnnext",
}
return selectors[selector]
async def _get_next_page(self, query: str) -> str:
params = {
"q": urllib.parse.unquote(query),
"hl": "en",
"gl": "us",
"pws": "0",
"num": "10",
}
url = f"{self.base_url}/search?{urllib.parse.urlencode(params)}"
return await self._get_html(url, None)
def _get_url(self, tag: Tag) -> str:
href = str(tag.get("href") or "")
if href.startswith("/url?"):
parsed = urllib.parse.urlparse(href)
q = urllib.parse.parse_qs(parsed.query).get("q", [""])[0]
return urllib.parse.unquote(q)
return href
async def search(self, query: str, num_results: int) -> list[SearchResult]:
return await self._search_with_result_filter(
query=query,
num_results=num_results,
predicate=lambda result: (
result.url.startswith("http") and "google.com/search?" not in result.url
),
)

View File

@@ -1,663 +0,0 @@
import asyncio
import json
import random
import uuid
from typing import ClassVar
import aiohttp
from bs4 import BeautifulSoup
from readability import Document
from astrbot.api import AstrBotConfig, llm_tool, logger, sp, star
from astrbot.api.event import AstrMessageEvent, filter
from astrbot.api.provider import ProviderRequest
from astrbot.core.provider.func_tool_manager import FunctionToolManager
from .engines import HEADERS, USER_AGENTS, SearchResult
from .engines.bing import Bing
from .engines.comet import Comet
from .engines.duckduckgo import DuckDuckGo
from .engines.google import Google
from .engines.sogo import Sogo
from .provider_routing import (
DEFAULT_WEB_SEARCH_PROVIDER,
build_default_engine_order,
normalize_websearch_provider,
normalize_websearch_provider_for_tools,
validate_default_engine_registry,
)
class Main(star.Star):
TOOLS: ClassVar[list[str]] = [
"web_search",
"fetch_url",
"web_search_tavily",
"tavily_extract_web_page",
"web_search_bocha",
]
def __init__(self, context: star.Context) -> None:
self.context = context
self.tavily_key_index = 0
self.tavily_key_lock = asyncio.Lock()
self.bocha_key_index = 0
self.bocha_key_lock = asyncio.Lock()
# 将 str 类型的 key 迁移至 list[str],并保存
cfg = self.context.get_config()
provider_settings = cfg.get("provider_settings")
if provider_settings:
tavily_key = provider_settings.get("websearch_tavily_key")
if isinstance(tavily_key, str):
logger.info(
"检测到旧版 websearch_tavily_key (字符串格式),自动迁移为列表格式并保存。",
)
if tavily_key:
provider_settings["websearch_tavily_key"] = [tavily_key]
else:
provider_settings["websearch_tavily_key"] = []
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.google_search = Google()
self.bing_search = Bing()
self.ddg_search = DuckDuckGo()
self.comet_search = Comet()
self.sogo_search = Sogo()
self.default_search_engines = {
engine.NAME: engine
for engine in (
self.google_search,
self.bing_search,
self.ddg_search,
self.comet_search,
self.sogo_search,
)
}
validate_default_engine_registry(self.default_search_engines)
self.baidu_initialized = False
async def _tidy_text(self, text: str) -> str:
"""清理文本,去除空格、换行符等"""
return text.strip().replace("\n", " ").replace("\r", " ").replace(" ", " ")
async def _get_from_url(self, url: str) -> str:
"""获取网页内容"""
header = HEADERS
header.update({"User-Agent": random.choice(USER_AGENTS)})
async with aiohttp.ClientSession(trust_env=True) as session:
async with session.get(url, headers=header) as response:
html = await response.text(encoding="utf-8")
doc = Document(html)
ret = doc.summary(html_partial=True)
soup = BeautifulSoup(ret, "html.parser")
ret = await self._tidy_text(soup.get_text())
return ret
async def _process_search_result(
self,
result: SearchResult,
idx: int,
websearch_link: bool,
) -> str:
"""处理单个搜索结果"""
logger.info(f"web_searcher - scraping web: {result.title} - {result.url}")
try:
site_result = await self._get_from_url(result.url)
except BaseException:
site_result = ""
site_result = (
f"{site_result[:700]}..." if len(site_result) > 700 else site_result
)
header = f"{idx}. {result.title} "
if websearch_link and result.url:
header += result.url
return f"{header}\n{result.snippet}\n{site_result}\n\n"
async def _web_search_default(
self,
query,
num_results: int = 5,
preferred_provider: str = DEFAULT_WEB_SEARCH_PROVIDER,
) -> list[SearchResult]:
for engine_name in build_default_engine_order(preferred_provider):
engine = self.default_search_engines.get(engine_name)
if not engine:
continue
try:
results = await engine.search(query, num_results)
except Exception as e:
logger.error(
f"{engine_name} search error: {e}, try the next one...",
)
continue
if results:
logger.info(
f"web_searcher - provider `{engine_name}` success: {len(results)} results",
)
return results
logger.debug(f"search {engine_name} returned no results")
return []
async def _get_tavily_key(self, cfg: AstrBotConfig) -> str:
"""并发安全的从列表中获取并轮换Tavily API密钥。"""
tavily_keys = cfg.get("provider_settings", {}).get("websearch_tavily_key", [])
if not tavily_keys:
raise ValueError("错误:Tavily API密钥未在AstrBot中配置。")
async with self.tavily_key_lock:
key = tavily_keys[self.tavily_key_index]
self.tavily_key_index = (self.tavily_key_index + 1) % len(tavily_keys)
return key
async def _web_search_tavily(
self,
cfg: AstrBotConfig,
payload: dict,
) -> list[SearchResult]:
"""使用 Tavily 搜索引擎进行搜索"""
tavily_key = await self._get_tavily_key(cfg)
url = "https://api.tavily.com/search"
header = {
"Authorization": f"Bearer {tavily_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"Tavily web search failed: {reason}, status: {response.status}",
)
data = await response.json()
results = []
for item in data.get("results", []):
result = SearchResult(
title=item.get("title"),
url=item.get("url"),
snippet=item.get("content"),
favicon=item.get("favicon"),
)
results.append(result)
return results
async def _extract_tavily(self, cfg: AstrBotConfig, payload: dict) -> list[dict]:
"""使用 Tavily 提取网页内容"""
tavily_key = await self._get_tavily_key(cfg)
url = "https://api.tavily.com/extract"
header = {
"Authorization": f"Bearer {tavily_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"Tavily web search failed: {reason}, status: {response.status}",
)
data = await response.json()
results: list[dict] = data.get("results", [])
if not results:
raise ValueError(
"Error: Tavily web searcher does not return any results.",
)
return results
@llm_tool(name="web_search")
async def search_from_search_engine(
self,
event: AstrMessageEvent,
query: str,
max_results: int = 5,
) -> str:
"""搜索网络以回答用户的问题。当用户需要搜索网络以获取即时性的信息时调用此工具。
Args:
query(string): 和用户的问题最相关的搜索关键词,用于在 Google 上搜索。
max_results(number): 返回的最大搜索结果数量,默认为 5。
"""
logger.info(f"web_searcher - search_from_search_engine: {query}")
cfg = self.context.get_config(umo=event.unified_msg_origin)
websearch_link = cfg["provider_settings"].get("web_search_link", False)
preferred_provider = normalize_websearch_provider(
cfg.get("provider_settings", {}).get(
"websearch_provider",
DEFAULT_WEB_SEARCH_PROVIDER,
),
)
results = await self._web_search_default(
query,
max_results,
preferred_provider=preferred_provider,
)
if not results:
return "Error: web searcher does not return any results."
tasks = []
for idx, result in enumerate(results, 1):
task = self._process_search_result(result, idx, websearch_link)
tasks.append(task)
processed_results = await asyncio.gather(*tasks, return_exceptions=True)
ret = ""
for processed_result in processed_results:
if isinstance(processed_result, BaseException):
logger.error(f"Error processing search result: {processed_result}")
continue
ret += processed_result
if websearch_link:
ret += "\n\n针对问题,请根据上面的结果分点总结,并且在结尾处附上对应内容的参考链接(如有)。"
return ret
async def ensure_baidu_ai_search_mcp(self, umo: str | None = None) -> None:
if self.baidu_initialized:
return
cfg = self.context.get_config(umo=umo)
key = cfg.get("provider_settings", {}).get(
"websearch_baidu_app_builder_key",
"",
)
if not key:
raise ValueError(
"Error: Baidu AI Search API key is not configured in AstrBot.",
)
func_tool_mgr = self.context.get_llm_tool_manager()
await func_tool_mgr.enable_mcp_server(
"baidu_ai_search",
config={
"transport": "sse",
"url": f"http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key={key}",
"headers": {},
"timeout": 600,
},
)
self.baidu_initialized = True
logger.info("Successfully initialized Baidu AI Search MCP server.")
@llm_tool(name="fetch_url")
async def fetch_website_content(self, event: AstrMessageEvent, url: str) -> str:
"""Fetch the content of a website with the given web url
Args:
url(string): The url of the website to fetch content from
"""
resp = await self._get_from_url(url)
return resp
@llm_tool("web_search_tavily")
async def search_from_tavily(
self,
event: AstrMessageEvent,
query: str,
max_results: int = 7,
search_depth: str = "basic",
topic: str = "general",
days: int = 3,
time_range: str = "",
start_date: str = "",
end_date: str = "",
) -> str:
"""A web search tool that uses Tavily to search the web for relevant content.
Ideal for gathering current information, news, and detailed web content analysis.
Args:
query(string): Required. Search query.
max_results(number): Optional. The maximum number of results to return. Default is 7. Range is 5-20.
search_depth(string): Optional. The depth of the search, must be one of 'basic', 'advanced'. Default is "basic".
topic(string): Optional. The topic of the search, must be one of 'general', 'news'. Default is "general".
days(number): Optional. The number of days back from the current date to include in the search results. Please note that this feature is only available when using the 'news' search topic.
time_range(string): Optional. The time range back from the current date to include in the search results. This feature is available for both 'general' and 'news' search topics. Must be one of 'day', 'week', 'month', 'year'.
start_date(string): Optional. The start date for the search results in the format 'YYYY-MM-DD'.
end_date(string): Optional. The end date for the search results in the format 'YYYY-MM-DD'.
"""
logger.info(f"web_searcher - search_from_tavily: {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_tavily_key", []):
raise ValueError("Error: Tavily API key is not configured in AstrBot.")
# build payload
payload = {"query": query, "max_results": max_results, "include_favicon": True}
if search_depth not in ["basic", "advanced"]:
search_depth = "basic"
payload["search_depth"] = search_depth
if topic not in ["general", "news"]:
topic = "general"
payload["topic"] = topic
if topic == "news":
payload["days"] = days
if time_range in ["day", "week", "month", "year"]:
payload["time_range"] = time_range
if start_date:
payload["start_date"] = start_date
if end_date:
payload["end_date"] = end_date
results = await self._web_search_tavily(cfg, payload)
if not results:
return "Error: Tavily 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}",
# TODO: do not need ref for non-webchat platform adapter
"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
@llm_tool("tavily_extract_web_page")
async def tavily_extract_web_page(
self,
event: AstrMessageEvent,
url: str = "",
extract_depth: str = "basic",
) -> str:
"""Extract the content of a web page using Tavily.
Args:
url(string): Required. An URl to extract content from.
extract_depth(string): Optional. The depth of the extraction, must be one of 'basic', 'advanced'. Default is "basic".
"""
cfg = self.context.get_config(umo=event.unified_msg_origin)
if not cfg.get("provider_settings", {}).get("websearch_tavily_key", []):
raise ValueError("Error: Tavily API key is not configured in AstrBot.")
if not url:
raise ValueError("Error: url must be a non-empty string.")
if extract_depth not in ["basic", "advanced"]:
extract_depth = "basic"
payload = {
"urls": [url],
"extract_depth": extract_depth,
}
results = await self._extract_tavily(cfg, payload)
ret_ls = []
for result in results:
ret_ls.append(f"URL: {result.get('url', 'No URL')}")
ret_ls.append(f"Content: {result.get('raw_content', 'No content')}")
ret = "\n".join(ret_ls)
if not ret:
return "Error: Tavily web searcher does not return any results."
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)
async def edit_web_search_tools(
self,
event: AstrMessageEvent,
req: ProviderRequest,
) -> None:
"""Get the session conversation for the given event."""
cfg = self.context.get_config(umo=event.unified_msg_origin)
prov_settings = cfg.get("provider_settings", {})
websearch_enable = prov_settings.get("web_search", False)
raw_provider = prov_settings.get(
"websearch_provider",
DEFAULT_WEB_SEARCH_PROVIDER,
)
branch_provider, is_known_provider = normalize_websearch_provider_for_tools(
raw_provider,
)
tool_set = req.func_tool
if isinstance(tool_set, FunctionToolManager):
req.func_tool = tool_set.get_full_tool_set()
tool_set = req.func_tool
if not tool_set:
return
if not websearch_enable:
# pop tools
for tool_name in self.TOOLS:
tool_set.remove_tool(tool_name)
return
func_tool_mgr = self.context.get_llm_tool_manager()
if branch_provider == "default":
if not is_known_provider:
logger.warning(
"Unsupported websearch_provider `%s`, fallback to default search tool branch.",
raw_provider,
)
web_search_t = func_tool_mgr.get_func("web_search")
fetch_url_t = func_tool_mgr.get_func("fetch_url")
if web_search_t and web_search_t.active:
tool_set.add_tool(web_search_t)
if fetch_url_t and fetch_url_t.active:
tool_set.add_tool(fetch_url_t)
tool_set.remove_tool("web_search_tavily")
tool_set.remove_tool("tavily_extract_web_page")
tool_set.remove_tool("AIsearch")
tool_set.remove_tool("web_search_bocha")
elif branch_provider == "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")
if web_search_tavily and web_search_tavily.active:
tool_set.add_tool(web_search_tavily)
if tavily_extract_web_page and tavily_extract_web_page.active:
tool_set.add_tool(tavily_extract_web_page)
tool_set.remove_tool("web_search")
tool_set.remove_tool("fetch_url")
tool_set.remove_tool("AIsearch")
tool_set.remove_tool("web_search_bocha")
elif branch_provider == "baidu_ai_search":
try:
await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin)
aisearch_tool = func_tool_mgr.get_func("AIsearch")
if aisearch_tool and aisearch_tool.active:
tool_set.add_tool(aisearch_tool)
tool_set.remove_tool("web_search")
tool_set.remove_tool("fetch_url")
tool_set.remove_tool("web_search_tavily")
tool_set.remove_tool("tavily_extract_web_page")
tool_set.remove_tool("web_search_bocha")
except Exception as e:
logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}")
elif branch_provider == "bocha":
web_search_bocha = func_tool_mgr.get_func("web_search_bocha")
if web_search_bocha and web_search_bocha.active:
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")

View File

@@ -1,24 +0,0 @@
from __future__ import annotations
DEFAULT_WEB_SEARCH_PROVIDER = "default"
# Canonical provider ids shown in config UI options.
WEB_SEARCH_PROVIDER_OPTIONS: tuple[str, ...] = (
DEFAULT_WEB_SEARCH_PROVIDER,
"duckduckgo",
"google",
"bing",
"comet",
"sogo",
"tavily",
"baidu_ai_search",
"bocha",
)
# Provider ids that select non-default tool branches directly.
WEB_SEARCH_TOOL_BRANCH_PROVIDERS: tuple[str, ...] = (
DEFAULT_WEB_SEARCH_PROVIDER,
"tavily",
"baidu_ai_search",
"bocha",
)

View File

@@ -1,132 +0,0 @@
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from .engines.bing import Bing
from .engines.comet import Comet
from .engines.duckduckgo import DuckDuckGo
from .engines.google import Google
from .engines.sogo import Sogo
from .provider_constants import (
DEFAULT_WEB_SEARCH_PROVIDER,
WEB_SEARCH_PROVIDER_OPTIONS,
WEB_SEARCH_TOOL_BRANCH_PROVIDERS,
)
ENGINE_REGISTRY: tuple[tuple[str, type[object], bool], ...] = (
(Bing.NAME, Bing, True),
(Sogo.NAME, Sogo, True),
# Compatibility first: DDG should stay as fallback and cannot become primary.
(DuckDuckGo.NAME, DuckDuckGo, False),
(Google.NAME, Google, True),
(Comet.NAME, Comet, True),
)
DEFAULT_ENGINE_ORDER: tuple[str, ...] = tuple(name for name, _, _ in ENGINE_REGISTRY)
_ENGINE_PROVIDER_SET = {name for name, _, _ in ENGINE_REGISTRY}
_ENGINE_CAN_BE_PRIMARY = {
name: can_be_primary for name, _, can_be_primary in ENGINE_REGISTRY
}
_TOOL_BRANCH_PROVIDER_SET = set(WEB_SEARCH_TOOL_BRANCH_PROVIDERS)
_CANONICAL_PROVIDER_SET = _ENGINE_PROVIDER_SET | _TOOL_BRANCH_PROVIDER_SET
if not _CANONICAL_PROVIDER_SET.issubset(set(WEB_SEARCH_PROVIDER_OPTIONS)):
raise RuntimeError(
"web search provider options and routing providers are out of sync: "
f"canonical={sorted(_CANONICAL_PROVIDER_SET)} options={list(WEB_SEARCH_PROVIDER_OPTIONS)}",
)
_WEB_SEARCH_PROVIDER_ALIASES: dict[str, str] = {
"": DEFAULT_WEB_SEARCH_PROVIDER,
"default": DEFAULT_WEB_SEARCH_PROVIDER,
"native": DEFAULT_WEB_SEARCH_PROVIDER,
}
_WEB_SEARCH_PROVIDER_ALIASES.update({name: name for name in _CANONICAL_PROVIDER_SET})
_WEB_SEARCH_PROVIDER_ALIASES.update(
{
"duckduck_go": DuckDuckGo.NAME,
"duckduck-go": DuckDuckGo.NAME,
"ddg": DuckDuckGo.NAME,
"baidu_ai": "baidu_ai_search",
"baidu": "baidu_ai_search",
"bochaai": "bocha",
# ZeroClaw compatibility: AstrBot has no Brave provider yet, so downgrade to default.
"brave": DEFAULT_WEB_SEARCH_PROVIDER,
},
)
@dataclass(frozen=True)
class NormalizedProvider:
canonical: str
tool_branch: str
is_known: bool
def _normalize_raw_provider(provider: object) -> str:
return str(provider or "").strip().lower().replace(" ", "")
def normalize_websearch(provider: object) -> NormalizedProvider:
raw = _normalize_raw_provider(provider)
alias = _WEB_SEARCH_PROVIDER_ALIASES.get(raw, raw)
canonical = alias or DEFAULT_WEB_SEARCH_PROVIDER
is_engine = canonical in _ENGINE_PROVIDER_SET
is_tool_branch = canonical in _TOOL_BRANCH_PROVIDER_SET
is_known = is_engine or is_tool_branch
tool_branch = canonical if is_tool_branch else DEFAULT_WEB_SEARCH_PROVIDER
return NormalizedProvider(
canonical=canonical,
tool_branch=tool_branch,
is_known=is_known,
)
def normalize_websearch_provider(provider: object) -> str:
return normalize_websearch(provider).canonical
def normalize_websearch_provider_for_tools(provider: object) -> tuple[str, bool]:
normalized = normalize_websearch(provider)
return normalized.tool_branch, normalized.is_known
def resolve_tool_branch_provider(provider: object) -> str:
return normalize_websearch(provider).tool_branch
def build_default_engine_order(provider: object) -> tuple[str, ...]:
normalized = normalize_websearch(provider)
engine_name = normalized.canonical
if engine_name not in _ENGINE_PROVIDER_SET:
return DEFAULT_ENGINE_ORDER
if not _ENGINE_CAN_BE_PRIMARY.get(engine_name, False):
return DEFAULT_ENGINE_ORDER
return (
engine_name,
*tuple(name for name in DEFAULT_ENGINE_ORDER if name != engine_name),
)
def is_known_websearch_provider(provider: object) -> bool:
return normalize_websearch(provider).is_known
def validate_default_engine_registry(engines_by_name: Mapping[str, object]) -> None:
expected_names = {name for name, _, _ in ENGINE_REGISTRY}
missing = [name for name in DEFAULT_ENGINE_ORDER if name not in engines_by_name]
extra = [name for name in engines_by_name if name not in expected_names]
if not missing and not extra:
return
raise ValueError(
"default search engine registry mismatch. "
f"missing={missing}, extra={extra}, expected_order={list(DEFAULT_ENGINE_ORDER)}",
)

View File

@@ -1,3 +1 @@
from astrbot import __version__
__all__ = ["__version__"]
__version__ = "3.5.23"

View File

@@ -1,187 +1,59 @@
"""AstrBot CLI entry point"""
import os
import platform
import sys
from pathlib import Path
"""
AstrBot CLI入口
"""
import click
from click.shell_completion import get_completion_class
import sys
from . import __version__
from .commands import bk, config, init, password, plugin, run, service, uninstall
from .i18n import t
from .commands import init, run, plug, conf
logo_tmpl = r"""
___ _______.___________..______ .______ ______ .___________.
/ \ / | || _ \ | _ \ / __ \ | |
/ ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----`
/ /_\ \ \ \ | | | / | _ < | | | | | |
/ _____ \ .----) | | | | |\ \----.| |_) | | `--' | | |
/__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__|
"""
def print_version_detail() -> None:
"""Print detailed version info (same for --version and version command)"""
from astrbot.core.utils.astrbot_path import astrbot_paths
click.echo(f"AstrBot: {__version__}")
click.echo(f"Python: {sys.version.split()[0]}")
click.echo(f"System: {platform.system()} {platform.release()}")
click.echo(f"Machine: {platform.machine()}")
git_root = Path(astrbot_paths.root) / ".git"
if git_root.exists():
import subprocess
try:
git_hash = subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"],
cwd=str(astrbot_paths.root),
text=True,
).strip()
git_branch = subprocess.check_output(
["git", "rev-parse", "--abbrev-ref", "HEAD"],
cwd=str(astrbot_paths.root),
text=True,
).strip()
click.echo(f"Git Branch: {git_branch}")
click.echo(f"Git Commit: {git_hash}")
except Exception:
pass
click.echo(f"AstrBot Root: {astrbot_paths.root}")
click.echo(f"Platform: {platform.platform()}")
def version_callback(ctx: click.Context, param: click.Parameter, value: bool) -> bool:
"""Callback for --version to show detailed version and exit."""
if not value:
return value
print_version_detail()
ctx.exit()
return value
class AstrBotCLIGroup(click.Group):
COMMAND_ALIASES = {
"conf": "config",
"plug": "plugin",
}
def get_command(self, ctx: click.Context, cmd_name: str) -> click.Command | None:
command = super().get_command(ctx, cmd_name)
if command is not None:
return command
alias_target = self.COMMAND_ALIASES.get(cmd_name)
if alias_target is None:
return None
return super().get_command(ctx, alias_target)
@click.group(cls=AstrBotCLIGroup)
@click.group()
@click.version_option(__version__, prog_name="AstrBot")
def cli() -> None:
"""Astrbot
Agentic IM Chatbot infrastructure that integrates lots of IM platforms, LLMs, plugins and AI feature, and can be your openclaw alternative. ✨
"""
"""The AstrBot CLI"""
click.echo(logo_tmpl)
click.echo("Welcome to AstrBot CLI!")
click.echo(f"AstrBot CLI version: {__version__}")
@click.command()
@click.argument("command_name", required=False, type=str)
@click.option(
"--all",
"-a",
is_flag=True,
help="Show help for all commands recursively.",
)
def help(command_name: str | None, all: bool) -> None:
"""Display help information for commands
def help(command_name: str | None) -> None:
"""显示命令的帮助信息
If COMMAND_NAME is provided, display detailed help for that command.
Otherwise, display general help information.
如果提供了 COMMAND_NAME,则显示该命令的详细帮助信息。
否则,显示通用帮助信息。
"""
ctx = click.get_current_context()
if all:
def print_recursive_help(command, parent_ctx):
name = command.name
if parent_ctx is None:
name = "astrbot"
cmd_ctx = click.Context(command, info_name=name, parent=parent_ctx)
click.echo(command.get_help(cmd_ctx))
click.echo("\n" + "-" * 50 + "\n")
if isinstance(command, click.Group):
for subcommand in command.commands.values():
print_recursive_help(subcommand, cmd_ctx)
print_recursive_help(cli, None)
return
if command_name:
# Find the specified command
# 查找指定命令
command = cli.get_command(ctx, command_name)
if command:
# Display help for the specific command
parent = ctx.parent or ctx
cmd_ctx = click.Context(command, info_name=command.name, parent=parent)
click.echo(command.get_help(cmd_ctx))
# 显示特定命令的帮助信息
click.echo(command.get_help(ctx))
else:
click.echo(t("cli_unknown_command", command=command_name))
click.echo(f"Unknown command: {command_name}")
sys.exit(1)
else:
# Display general help information
# 显示通用帮助信息
click.echo(cli.get_help(ctx))
cli.add_command(init)
cli.add_command(run)
cli.add_command(help)
cli.add_command(plugin)
cli.add_command(config)
cli.add_command(uninstall)
cli.add_command(bk)
cli.add_command(password)
cli.add_command(service)
@click.command()
@click.argument("shell", required=False, type=click.Choice(["bash", "zsh", "fish"]))
def completion(shell: str | None) -> None:
"""Generate shell completion script"""
if shell is None:
shell_path = os.environ.get("SHELL", "")
if "zsh" in shell_path:
shell = "zsh"
elif "bash" in shell_path:
shell = "bash"
elif "fish" in shell_path:
shell = "fish"
else:
click.echo(
"Could not detect shell. Please specify one of: bash, zsh, fish",
err=True,
)
sys.exit(1)
comp_cls = get_completion_class(shell)
if comp_cls is None:
click.echo(f"No completion support for shell: {shell}", err=True)
sys.exit(1)
comp = comp_cls(
cli,
ctx_args={},
prog_name="astrbot",
complete_var="_ASTRBOT_COMPLETE",
)
click.echo(comp.source())
cli.add_command(completion)
@click.command(name="version")
def version_cmd() -> None:
"""Display detailed version information"""
print_version_detail()
cli.add_command(version_cmd)
cli.add_command(plug)
cli.add_command(conf)
if __name__ == "__main__":
cli()

View File

@@ -1,28 +0,0 @@
"""ASCII logo and interactive mode utilities for CLI"""
import sys
logo_tmpl = r"""
___ _______.___________..______ .______ ______ .___________.
/ \ / | || _ \ | _ \ / __ \ | |
/ ^ \ | (----`---| |----`| |_) | | |_) | | | | | `---| |----`
/ /_\ \ \ \ | | | / | _ < | | | | | |
/ _____ \ .----) | | | | |\ \----.| |_) | | `--' | | |
/__/ \__\ |_______/ |__| | _| `._____||______/ \______/ |__|
"""
def is_interactive() -> bool:
"""Check if stdout is connected to a TTY (interactive terminal)"""
try:
return sys.stdout.isatty()
except Exception:
return False
def print_logo() -> None:
"""Print ASCII logo if in interactive mode"""
import click
if is_interactive():
click.echo(logo_tmpl)

View File

@@ -1,24 +1,6 @@
from .cmd_bk import bk
from .cmd_conf import conf as config
from .cmd_init import init
from .cmd_password import password
from .cmd_plug import plug as plugin
from .cmd_run import run
from .cmd_service import service
from .cmd_uninstall import uninstall
from .cmd_plug import plug
from .cmd_conf import conf
conf = config
plug = plugin
__all__ = [
"bk",
"conf",
"config",
"init",
"password",
"plug",
"plugin",
"run",
"service",
"uninstall",
]
__all__ = ["init", "run", "plug", "conf"]

View File

@@ -1,392 +0,0 @@
import asyncio
import hashlib
import shutil
import subprocess
from pathlib import Path
import anyio
import click
from astrbot.core import db_helper
from astrbot.core.backup import AstrBotExporter, AstrBotImporter
async def _get_kb_manager():
"""Initialize and return a KnowledgeBaseManager with full dependency chain."""
from astrbot.core import astrbot_config, sp
from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
from astrbot.core.persona_mgr import PersonaManager
from astrbot.core.provider.manager import ProviderManager
from astrbot.core.umop_config_router import UmopConfigRouter
ucr = UmopConfigRouter(sp=sp)
await ucr.initialize()
acm = AstrBotConfigManager(
default_config=astrbot_config,
ucr=ucr,
sp=sp,
)
persona_mgr = PersonaManager(db_helper, acm)
await persona_mgr.initialize()
provider_manager = ProviderManager(
acm,
db_helper,
persona_mgr,
)
kb_manager = KnowledgeBaseManager(provider_manager)
await kb_manager.initialize()
return kb_manager
@click.group(name="bk")
def bk():
"""Backup management (Export/Import)"""
@bk.command(name="export")
@click.option("--output", "-o", help="Output directory", default=None)
@click.option(
"--gpg-sign",
"-S",
is_flag=True,
help="Sign backup with GPG default private key",
)
@click.option(
"--gpg-encrypt",
"-E",
help="Encrypt for GPG recipient (Asymmetric)",
metavar="RECIPIENT",
)
@click.option(
"--gpg-symmetric",
"-C",
is_flag=True,
help="Encrypt with symmetric cipher (GPG)",
)
@click.option(
"--digest",
"-d",
type=click.Choice(["md5", "sha1", "sha256", "sha512"]),
help="Generate digital digest",
)
def export_data(
output: str | None,
gpg_sign: bool,
gpg_encrypt: str | None,
gpg_symmetric: bool,
digest: str | None,
):
"""Export all AstrBot data to a backup archive.
If any GPG option (-S, -E, -C) is used, the output file will be processed by GPG
and saved with a .gpg extension.
Examples:
\b
1. Standard Export:
astrbot bk export
-> Generates a plain .zip file.
\b
2. Signed Backup (Integrity Check):
astrbot bk export -S
-> Generates a .zip.gpg file containing the backup and your signature.
-> NOT ENCRYPTED, but packaged in OpenPGP format.
-> Use 'astrbot bk import' or 'gpg --verify' to check integrity.
\b
3. Password Protected (Symmetric Encryption):
astrbot bk export -C
-> Generates an encrypted .zip.gpg file.
-> Prompts for a passphrase.
-> Only accessible with the passphrase.
\b
4. Encrypted for Recipient (Asymmetric Encryption):
astrbot bk export -E "alice@example.com"
-> Generates an encrypted .zip.gpg file for Alice.
-> Only Alice's private key can decrypt it.
\b
5. Signed and Encrypted with Digest:
astrbot bk export -S -E "bob@example.com" -d sha256
-> Signs, encrypts for Bob, and generates a SHA256 checksum file.
"""
# Handle case where -E consumes the next flag (e.g. -E -S)
if gpg_encrypt and gpg_encrypt.startswith("-"):
consumed_flag = gpg_encrypt
click.echo(
click.style(
f"Warning: Flag '{consumed_flag}' was interpreted as the recipient for -E.",
fg="yellow",
),
)
# Recover flags
if consumed_flag == "-S":
gpg_sign = True
click.echo("Recovered flag -S (Sign).")
elif consumed_flag == "-C":
gpg_symmetric = True
click.echo("Recovered flag -C (Symmetric).")
# Prompt for the actual recipient
gpg_encrypt = click.prompt("Please enter the GPG recipient (email or key ID)")
async def _run():
if gpg_sign or gpg_encrypt or gpg_symmetric:
if not shutil.which("gpg"):
raise click.ClickException(
"GPG tool not found. Please install GnuPG to use encryption/signing features.",
)
exporter = AstrBotExporter(db_helper)
async def on_progress(stage, current, total, message):
click.echo(f"[{stage}] {message}")
try:
path_str = await exporter.export_all(output, progress_callback=on_progress)
final_path = Path(path_str)
click.echo(
click.style(f"\nRaw backup exported to: {final_path}", fg="green"),
)
# GPG Operations
if gpg_sign or gpg_encrypt or gpg_symmetric:
# Construct GPG command
# output file usually ends with .gpg
gpg_output = final_path.with_name(final_path.name + ".gpg")
cmd = ["gpg", "--output", str(gpg_output), "--yes"]
if gpg_symmetric:
if gpg_encrypt:
click.echo(
click.style(
"Warning: Symmetric encryption selected, ignoring asymmetric recipient.",
fg="yellow",
),
)
cmd.append("--symmetric")
# No --batch to allow interactive passphrase entry on TTY
else:
# Asymmetric or just Sign
# Note: If encrypting, -s adds signature to the encrypted packet.
if gpg_encrypt:
cmd.extend(["--encrypt", "--recipient", gpg_encrypt])
if gpg_sign:
cmd.append("--sign")
cmd.append(str(final_path))
click.echo(f"Running GPG: {' '.join(cmd)}")
# Replace subprocess.run with asyncio.create_subprocess_exec to avoid blocking the event loop
process = await asyncio.create_subprocess_exec(*cmd)
await process.wait()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode or 1, cmd)
# Clean up original file
await anyio.Path(final_path).unlink()
final_path = gpg_output
click.echo(
click.style(f"Processed backup created: {final_path}", fg="green"),
)
# Digest Generation
if digest:
click.echo(f"Calculating {digest} digest...")
hash_func = getattr(hashlib, digest)()
# Read file in chunks
async with await anyio.open_file(final_path, "rb") as f:
while chunk := await f.read(8192):
hash_func.update(chunk)
digest_val = hash_func.hexdigest()
digest_file = final_path.with_name(final_path.name + f".{digest}")
await anyio.Path(digest_file).write_text(
f"{digest_val} *{final_path.name}\n",
encoding="utf-8",
)
click.echo(click.style(f"Digest generated: {digest_file}", fg="green"))
except subprocess.CalledProcessError as e:
click.echo(click.style(f"\nGPG process failed: {e}", fg="red"), err=True)
except Exception as e:
click.echo(click.style(f"\nExport failed: {e}", fg="red"), err=True)
asyncio.run(_run())
@bk.command(name="import")
@click.argument("backup_file")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts")
def import_data_command(backup_file: str, yes: bool):
"""Import AstrBot data from a backup archive.
Automatically handles .zip files and .gpg files (signed or encrypted).
If the file is encrypted, you will be prompted for the passphrase.
If a digest file (.sha256, .md5, etc.) exists, it will be verified automatically.
"""
backup_path = Path(backup_file)
if not backup_path.exists():
raise click.ClickException(f"Backup file not found: {backup_file}")
# 1. Verify Digest if exists
def _verify_digest(file_path: Path) -> bool:
supported_digests = ["sha256", "sha512", "md5", "sha1"]
digest_verified = True # Default true if no digest file found
for algo in supported_digests:
digest_file = file_path.with_name(f"{file_path.name}.{algo}")
if digest_file.exists():
click.echo(f"Found digest file: {digest_file.name}")
try:
# Parse digest file
content = digest_file.read_text(encoding="utf-8").strip()
# Format: "digest *filename" or "digest filename"
# We expect the hash to be the first part
if " " in content:
expected_digest = content.split()[0].lower()
else:
expected_digest = content.lower()
click.echo(f"Verifying {algo} digest...")
hash_func = getattr(hashlib, algo)()
with open(file_path, "rb") as f:
while chunk := f.read(8192):
hash_func.update(chunk)
calculated_digest = hash_func.hexdigest().lower()
if calculated_digest == expected_digest:
click.echo(
click.style("Digest verification PASSED.", fg="green"),
)
else:
click.echo(
click.style(
"Digest verification FAILED!",
fg="red",
bold=True,
),
)
click.echo(f" Expected: {expected_digest}")
click.echo(f" Actual: {calculated_digest}")
digest_verified = False
except Exception as e:
click.echo(click.style(f"Error checking digest: {e}", fg="red"))
digest_verified = False
return digest_verified
if not _verify_digest(backup_path):
if not yes:
if not click.confirm(
"Digest verification failed. Abort import?",
default=True,
abort=True,
):
pass
else:
click.echo(
click.style(
"Warning: Digest verification failed. Continuing due to --yes.",
fg="yellow",
),
)
if not yes:
click.confirm(
"This will OVERWRITE all current data (DB, Config, Plugins). Continue?",
abort=True,
default=False,
)
async def _run():
zip_path = backup_path
is_temp_file = False
# Handle GPG encrypted files
if backup_path.suffix == ".gpg":
if not shutil.which("gpg"):
raise click.ClickException(
"GPG tool not found. Cannot decrypt .gpg file.",
)
# Remove .gpg extension for output
decrypted_path = backup_path.with_suffix("")
# If it doesn't look like a zip after stripping .gpg, maybe append .zip?
# But the exporter creates .zip.gpg, so stripping .gpg gives .zip.
click.echo(f"Processing GPG file {backup_path}...")
try:
cmd = [
"gpg",
"--output",
str(decrypted_path),
"--decrypt", # This handles both decryption and signature verification/extraction
str(backup_path),
]
# Allow interactive passphrase
process = await asyncio.create_subprocess_exec(*cmd)
await process.wait()
if process.returncode != 0:
raise subprocess.CalledProcessError(process.returncode or 1, cmd)
zip_path = decrypted_path
is_temp_file = True
except subprocess.CalledProcessError:
click.echo(
click.style(
"GPG processing failed. Verify signature or decryption key.",
fg="red",
),
err=True,
)
return
kb_mgr = await _get_kb_manager()
importer = AstrBotImporter(db_helper, kb_mgr)
async def on_progress(stage, current, total, message):
click.echo(f"[{stage}] {message}")
try:
result = await importer.import_all(
str(zip_path),
progress_callback=on_progress,
)
if result.errors:
click.echo(
click.style("\nImport failed with errors:", fg="red"),
err=True,
)
for err in result.errors:
click.echo(f" - {err}", err=True)
else:
click.echo(click.style("\nImport completed successfully!", fg="green"))
if result.warnings:
click.echo(click.style("\nWarnings:", fg="yellow"))
for warn in result.warnings:
click.echo(f" - {warn}")
finally:
if is_temp_file and await anyio.Path(zip_path).exists():
await anyio.Path(zip_path).unlink()
click.echo(f"Cleaned up temporary file: {zip_path}")
asyncio.run(_run())

View File

@@ -1,95 +1,63 @@
"""Configuration CLI for AstrBot.
This module provides:
- secure hashing utilities for the dashboard password (argon2)
- validators for commonly configurable items
- click CLI group with `set`, `get`, and `password` subcommands
"""
from __future__ import annotations
import json
import zoneinfo
from collections.abc import Callable
from typing import Any
import click
from filelock import FileLock, Timeout
from astrbot.cli.i18n import t
from astrbot.core.config.default import DEFAULT_CONFIG
from astrbot.core.utils.astrbot_path import astrbot_paths
from astrbot.core.utils.auth_password import (
_is_argon2_hash,
_is_pbkdf2_hash,
hash_dashboard_password,
hash_legacy_dashboard_password,
is_legacy_dashboard_password,
validate_dashboard_password,
)
# --- Password hashing & validation utilities ---
def is_dashboard_password_hash(value: str) -> bool:
"""Heuristic: return True if `value` looks like a supported dashboard password hash."""
if not isinstance(value, str) or not value:
return False
return _is_argon2_hash(value) or _is_pbkdf2_hash(value)
# --- Validators for CLI configuration items ---
import hashlib
import zoneinfo
from typing import Any, Callable
from ..utils import get_astrbot_root, check_astrbot_root
def _validate_log_level(value: str) -> str:
value_up = value.upper()
allowed = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
if value_up not in allowed:
raise click.ClickException(t("config_log_level_invalid"))
return value_up
def _validate_dashboard_port(value: str) -> int:
try:
port = int(value)
except ValueError:
raise click.ClickException(t("config_port_must_be_number")) from None
if port < 1 or port > 65535:
raise click.ClickException(t("config_port_range_invalid"))
return port
def _validate_dashboard_username(value: str) -> str:
if value is None or value.strip() == "":
raise click.ClickException(t("config_username_empty"))
return value.strip()
def _validate_dashboard_password(value: str) -> str:
if value is None or value == "":
raise click.ClickException(t("config_password_empty"))
try:
validate_dashboard_password(value)
except ValueError as e:
raise click.ClickException(str(e)) from e
# Return the plaintext value; callers hash it before storage.
"""验证日志级别"""
value = value.upper()
if value not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
raise click.ClickException(
"日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一"
)
return value
def _validate_dashboard_port(value: str) -> int:
"""验证 Dashboard 端口"""
try:
port = int(value)
if port < 1 or port > 65535:
raise click.ClickException("端口必须在 1-65535 范围内")
return port
except ValueError:
raise click.ClickException("端口必须是数字")
def _validate_dashboard_username(value: str) -> str:
"""验证 Dashboard 用户名"""
if not value:
raise click.ClickException("用户名不能为空")
return value
def _validate_dashboard_password(value: str) -> str:
"""验证 Dashboard 密码"""
if not value:
raise click.ClickException("密码不能为空")
return hashlib.md5(value.encode()).hexdigest()
def _validate_timezone(value: str) -> str:
"""验证时区"""
try:
zoneinfo.ZoneInfo(value)
except Exception as e:
raise click.ClickException(t("config_timezone_invalid", value=value)) from e
except Exception:
raise click.ClickException(f"无效的时区: {value}请使用有效的IANA时区名称")
return value
def _validate_callback_api_base(value: str) -> str:
if not (value.startswith("http://") or value.startswith("https://")):
raise click.ClickException(t("config_callback_invalid"))
"""验证回调接口基址"""
if not value.startswith("http://") and not value.startswith("https://"):
raise click.ClickException("回调接口基址必须以 http:// 或 https:// 开头")
return value
# 可通过CLI设置的配置项配置键到验证器函数的映射
CONFIG_VALIDATORS: dict[str, Callable[[str], Any]] = {
"timezone": _validate_timezone,
"log_level": _validate_log_level,
@@ -100,22 +68,18 @@ CONFIG_VALIDATORS: dict[str, Callable[[str], Any]] = {
}
# --- Config file helpers ---
def _load_config() -> dict[str, Any]:
"""Load or initialize the CLI config file (data/cmd_config.json).
Ensures the astrbot root is valid before proceeding.
"""
root = astrbot_paths.root
if not astrbot_paths.is_root:
"""加载或初始化配置文件"""
root = get_astrbot_root()
if not check_astrbot_root(root):
raise click.ClickException(
f"{root} is not a valid AstrBot root directory. Use 'astrbot init' to initialize",
f"{root}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init"
)
config_path = astrbot_paths.data / "cmd_config.json"
config_path = root / "data" / "cmd_config.json"
if not config_path.exists():
# Write DEFAULT_CONFIG to disk if file missing
from astrbot.core.config.default import DEFAULT_CONFIG
config_path.write_text(
json.dumps(DEFAULT_CONFIG, ensure_ascii=False, indent=2),
encoding="utf-8-sig",
@@ -124,246 +88,119 @@ def _load_config() -> dict[str, Any]:
try:
return json.loads(config_path.read_text(encoding="utf-8-sig"))
except json.JSONDecodeError as e:
raise click.ClickException(f"Failed to parse config file: {e!s}") from e
raise click.ClickException(f"配置文件解析失败: {str(e)}")
def _save_config(config: dict[str, Any]) -> None:
config_path = astrbot_paths.data / "cmd_config.json"
"""保存配置文件"""
config_path = get_astrbot_root() / "data" / "cmd_config.json"
config_path.write_text(
json.dumps(config, ensure_ascii=False, indent=2),
encoding="utf-8-sig",
json.dumps(config, ensure_ascii=False, indent=2), encoding="utf-8-sig"
)
def ensure_config_file() -> dict[str, Any]:
return _load_config()
def _set_nested_item(obj: dict[str, Any], path: str, value: Any) -> None:
"""设置嵌套字典中的值"""
parts = path.split(".")
cur = obj
for part in parts[:-1]:
if part not in cur:
cur[part] = {}
elif not isinstance(cur[part], dict):
if part not in obj:
obj[part] = {}
elif not isinstance(obj[part], dict):
raise click.ClickException(
f"Config path conflict: {'.'.join(parts[: parts.index(part) + 1])} is not a dict",
f"配置路径冲突: {'.'.join(parts[: parts.index(part) + 1])} 不是字典"
)
cur = cur[part]
cur[parts[-1]] = value
obj = obj[part]
obj[parts[-1]] = value
def _get_nested_item(obj: dict[str, Any], path: str) -> Any:
"""获取嵌套字典中的值"""
parts = path.split(".")
cur = obj
for part in parts:
cur = cur[part]
return cur
obj = obj[part]
return obj
# --- CLI commands ---
@click.group(name="conf")
def conf():
"""配置管理命令
支持的配置项:
def prompt_dashboard_password(prompt: str = "Dashboard password") -> str:
# 显示密码规则提示
click.echo()
click.echo("密码规则:")
click.echo(" - 至少 12 个字符")
click.echo(" - 必须包含至少一个大写字母")
click.echo(" - 必须包含至少一个小写字母")
click.echo(" - 必须包含至少一个数字")
click.echo()
- timezone: 时区设置 (例如: Asia/Shanghai)
password = click.prompt(prompt, hide_input=True, confirmation_prompt=True, type=str)
click.echo(f"密码长度: {len(password)} 字符")
return _validate_dashboard_password(password)
- log_level: 日志级别 (DEBUG/INFO/WARNING/ERROR/CRITICAL)
- dashboard.port: Dashboard 端口
def set_dashboard_credentials(
config: dict[str, Any],
*,
username: str | None = None,
password_hash: str | None = None,
) -> None:
if username is not None:
_set_nested_item(
config,
"dashboard.username",
_validate_dashboard_username(username),
)
if password_hash is not None:
if isinstance(password_hash, str) and is_dashboard_password_hash(password_hash):
_set_nested_item(config, "dashboard.password", password_hash)
else:
if is_legacy_dashboard_password(password_hash):
raise click.ClickException(
"Storing legacy dashboard password hashes is no longer supported. "
"Please provide the plaintext password (it will be hashed securely), "
"or provide an Argon2-encoded hash string.",
)
validated = _validate_dashboard_password(password_hash)
_set_nested_item(
config,
"dashboard.pbkdf2_password",
hash_dashboard_password(validated),
)
_set_nested_item(
config,
"dashboard.password",
hash_legacy_dashboard_password(validated),
)
- dashboard.username: Dashboard 用户名
- dashboard.password: Dashboard 密码
def _set_dashboard_password(config: dict[str, Any], raw_password: str) -> None:
"""Set dashboard password hashes and clear password migration flags."""
_set_nested_item(
config,
"dashboard.pbkdf2_password",
hash_dashboard_password(raw_password),
)
_set_nested_item(
config,
"dashboard.password",
hash_legacy_dashboard_password(raw_password),
)
_set_nested_item(config, "dashboard.password_storage_upgraded", True)
_set_nested_item(config, "dashboard.password_change_required", False)
@click.group(name="config")
def conf() -> None:
"""Configuration management commands.
Supported config keys:
- timezone
- log_level
- dashboard.port
- dashboard.username
- dashboard.password
- callback_api_base
- callback_api_base: 回调接口基址
"""
pass
@conf.command(name="set")
@click.argument("key")
@click.argument("value")
def set_config(key: str, value: str) -> None:
if key not in CONFIG_VALIDATORS:
raise click.ClickException(f"Unsupported config key: {key}")
def set_config(key: str, value: str):
"""设置配置项的值"""
if key not in CONFIG_VALIDATORS.keys():
raise click.ClickException(f"不支持的配置项: {key}")
config = _load_config()
try:
# Attempt to get old value (may raise KeyError)
try:
old_value = _get_nested_item(config, key)
except Exception:
old_value = "<not set>"
try:
old_value = _get_nested_item(config, key)
validated_value = CONFIG_VALIDATORS[key](value)
if key == "dashboard.password":
_set_dashboard_password(config, validated_value)
else:
_set_nested_item(config, key, validated_value)
_set_nested_item(config, key, validated_value)
_save_config(config)
click.echo(f"Config updated: {key}")
click.echo(f" Old value: {old_value}")
click.echo(f" New value: {validated_value}")
except KeyError as e:
raise click.ClickException(f"Unknown config key: {key}") from e
except click.ClickException:
raise
click.echo(f"配置已更新: {key}")
if key == "dashboard.password":
click.echo(" 原值: ********")
click.echo(" 新值: ********")
else:
click.echo(f" 原值: {old_value}")
click.echo(f" 新值: {validated_value}")
except KeyError:
raise click.ClickException(f"未知的配置项: {key}")
except Exception as e:
raise click.UsageError(f"Failed to set config: {e!s}") from e
raise click.UsageError(f"设置配置失败: {str(e)}")
@conf.command(name="get")
@click.argument("key", required=False)
def get_config(key: str | None = None) -> None:
def get_config(key: str = None):
"""获取配置项的值不提供key则显示所有可配置项"""
config = _load_config()
if key:
if key not in CONFIG_VALIDATORS:
raise click.ClickException(f"Unsupported config key: {key}")
if key not in CONFIG_VALIDATORS.keys():
raise click.ClickException(f"不支持的配置项: {key}")
try:
value = _get_nested_item(config, key)
if key == "dashboard.password":
value = "********"
click.echo(f"{key}: {value}")
except KeyError as e:
raise click.ClickException(f"Unknown config key: {key}") from e
except KeyError:
raise click.ClickException(f"未知的配置项: {key}")
except Exception as e:
raise click.UsageError(f"Failed to get config: {e!s}") from e
raise click.UsageError(f"获取配置失败: {str(e)}")
else:
click.echo("Current config:")
for k in CONFIG_VALIDATORS:
click.echo("当前配置:")
for key in CONFIG_VALIDATORS.keys():
try:
v = (
value = (
"********"
if k == "dashboard.password"
else _get_nested_item(config, k)
if key == "dashboard.password"
else _get_nested_item(config, key)
)
click.echo(f" {k}: {v}")
click.echo(f" {key}: {value}")
except (KeyError, TypeError):
# Missing or non-dict paths are simply skipped in listing
pass
def _check_astrbot_not_running() -> None:
"""Refuse to proceed if astrbot is currently running (lock file held)."""
lock_file = astrbot_paths.root / "astrbot.lock"
if not lock_file.exists():
return
lock = FileLock(lock_file, timeout=1)
try:
lock.acquire()
except Timeout:
raise click.ClickException(
"AstrBot is currently running. "
"Please stop it first before changing the password via CLI.",
) from None
else:
lock.release()
@conf.command(name="admin")
@click.option("-u", "--username", type=str, help="Update admain username as well")
@click.option(
"-p",
"--password",
type=str,
help="Set admain password directly without interactive prompt",
)
def set_dashboard_password(username: str | None, password: str | None) -> None:
"""Interactively set dashboard password (with confirmation) or set directly with -p.
Acceptable inputs:
- Plaintext password (recommended): it will be hashed securely before storage.
- Argon2 encoded hash (advanced): stored as-is.
"""
_check_astrbot_not_running()
config = _load_config()
if password is not None:
if isinstance(password, str) and is_dashboard_password_hash(password):
password_hash = password
else:
if is_legacy_dashboard_password(password):
raise click.ClickException(
"Providing legacy dashboard password hashes is no longer supported. "
"Please supply the plaintext password (it will be hashed securely), "
"or provide an Argon2-encoded hash string.",
)
password_hash = _validate_dashboard_password(password)
else:
password_hash = prompt_dashboard_password()
set_dashboard_credentials(
config,
username=username.strip() if username is not None else None,
password_hash=password_hash,
)
_save_config(config)
if username is not None:
click.echo(f"Dashboard username updated: {username.strip()}")
click.echo("Dashboard password updated.")

View File

@@ -1,237 +1,55 @@
import asyncio
import json
import os
import re
from pathlib import Path
import click
from filelock import FileLock, Timeout
from astrbot.cli.utils import DashboardManager
from astrbot.core.config.default import DEFAULT_CONFIG
from astrbot.core.utils.astrbot_path import astrbot_paths
from .cmd_conf import ensure_config_file, set_dashboard_credentials
DASHBOARD_INITIAL_PASSWORD_ENV = "ASTRBOT_DASHBOARD_INITIAL_PASSWORD"
from ..utils import check_dashboard, get_astrbot_root
def _initialize_config_from_env(astrbot_root: Path) -> None:
if DASHBOARD_INITIAL_PASSWORD_ENV not in os.environ:
return
from astrbot.core.config.astrbot_config import AstrBotConfig
AstrBotConfig(config_path=str(astrbot_root / "data" / "cmd_config.json"))
click.echo("Initialized data/cmd_config.json with dashboard initial password.")
async def initialize_astrbot(
astrbot_root: Path,
*,
yes: bool,
backend_only: bool,
admin_username: str | None,
admin_password: str | None,
) -> None:
"""Execute AstrBot initialization logic"""
from astrbot.cli.banner import print_logo
click.echo("=" * 60)
click.echo("AstrBot 初始化向导")
click.echo("=" * 60)
print_logo()
click.echo()
async def initialize_astrbot(astrbot_root) -> None:
"""执行 AstrBot 初始化逻辑"""
dot_astrbot = astrbot_root / ".astrbot"
if not dot_astrbot.exists():
if yes or click.confirm(
f"确定要将 AstrBot 安装到以下目录吗?\n {astrbot_root}",
click.echo(f"Current Directory: {astrbot_root}")
click.echo(
"如果你确认这是 Astrbot root directory, 你需要在当前目录下创建一个 .astrbot 文件标记该目录为 AstrBot 的数据目录。"
)
if click.confirm(
f"请检查当前目录是否正确,确认正确请回车: {astrbot_root}",
default=True,
abort=True,
):
dot_astrbot.touch()
click.echo(f"[OK] 已创建: {dot_astrbot}")
click.echo(f"Created {dot_astrbot}")
paths = {
"data": astrbot_root / "data",
"config": astrbot_root / "data" / "config",
"plugins": astrbot_root / "data" / "plugins",
"temp": astrbot_root / "data" / "temp",
"skills": astrbot_root / "data" / "skills",
}
for name, path in paths.items():
path.mkdir(parents=True, exist_ok=True)
status = "Created" if not path.exists() else "Exists"
click.echo(f" [{status}] {name.title()}: {path}")
click.echo(f"{'Created' if not path.exists() else 'Directory exists'}: {path}")
_initialize_config_from_env(astrbot_root)
config_path = astrbot_root / "data" / "cmd_config.json"
if not config_path.exists():
config_path.write_text(
json.dumps(DEFAULT_CONFIG, ensure_ascii=False, indent=2),
encoding="utf-8-sig",
)
click.echo(f"[OK] 配置文件已创建: {config_path}")
ASTRBOT_ROOT = astrbot_root
env_file = ASTRBOT_ROOT / ".env"
if not env_file.exists():
tmpl_candidates = [
Path("/opt/astrbot/config.template"),
getattr(astrbot_paths, "project_root", Path.cwd()) / "config.template",
Path.cwd() / "config.template",
]
tmpl = None
for t in tmpl_candidates:
try:
if t.exists():
tmpl = t
break
except Exception:
continue
if tmpl is not None:
try:
txt = tmpl.read_text(encoding="utf-8")
instance_name = astrbot_root.name or "astrbot"
txt = re.sub("\\$\\{INSTANCE_NAME(:-[^}]*)?\\}", instance_name, txt)
port_val = (
os.environ.get("ASTRBOT_PORT") or os.environ.get("PORT") or "8000"
)
txt = re.sub("\\$\\{PORT(:-[^}]*)?\\}", str(port_val), txt)
txt = re.sub("\\$\\{ASTRBOT_ROOT(:-[^}]*)?\\}", str(ASTRBOT_ROOT), txt)
header = f"# Generated from config.template by astrbot init for instance: {instance_name}\n# This file will be auto-loaded by 'astrbot run'\n\n"
env_file.write_text(header + txt, encoding="utf-8")
env_file.chmod(420)
click.echo(f"[OK] 环境变量文件已创建: {env_file}")
except Exception as e:
click.echo(f"[警告] 无法从模板生成 .env 文件: {e!s}")
else:
click.echo("[提示] 未找到 config.template 文件,跳过 .env 生成")
if admin_password is not None:
raise click.ClickException(
"--admin-password is no longer supported during init. Run 'astrbot conf admin' after initialization.",
)
effective_admin_username = (
admin_username.strip()
if admin_username
else str(DEFAULT_CONFIG["dashboard"]["username"])
)
if admin_username:
config = ensure_config_file()
set_dashboard_credentials(
config,
username=effective_admin_username,
password_hash=None,
)
config_path.write_text(
json.dumps(config, ensure_ascii=False, indent=2),
encoding="utf-8-sig",
)
click.echo(f"[OK] Dashboard admin 用户名已设置为: {effective_admin_username}")
click.echo()
click.echo("!" * 60)
click.echo("重要提示:")
click.echo(" 1. Dashboard 密码尚未设置!首次登录前必须先设置密码")
click.echo(" 2. 设置命令: astrbot conf admin")
click.echo(" 3. 登录地址: http://localhost:6185 或 http://服务器IP:6185")
click.echo("!" * 60)
click.echo()
if not backend_only and (
yes
or click.confirm(
"是否需要集成式 WebUI个人电脑推荐服务器推荐使用后端模式",
default=True,
)
):
await DashboardManager().ensure_installed(astrbot_root)
else:
click.echo()
click.echo("[提示] 你选择了后端模式,可以使用以下方式管理 AstrBot")
click.echo(" - 使用在线 Dashboard: 在浏览器中访问远程服务器的 WebUI")
click.echo(" - 使用 CLI 命令: astrbot conf / astrbot plug 等")
click.echo()
click.echo("!" * 60)
click.echo("安全提示:")
click.echo(" HTTPS 前端只能安全连接 localhost 的 HTTP 后端")
click.echo(" 不支持远程 + HTTP 后端(不安全)")
click.echo(" 如需远程访问,请使用 HTTPS 后端或通过反向代理")
click.echo("!" * 60)
click.echo()
await check_dashboard(astrbot_root / "data")
@click.command()
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts")
@click.option("--backend-only", "-b", is_flag=True, help="Only initialize the backend")
@click.option("--backup", "-f", help="Initialize from backup file", type=str)
@click.option(
"-u",
"--admin-username",
type=str,
help="Set dashboard admin username during initialization",
)
@click.option(
"-p",
"--admin-password",
type=str,
help="Deprecated. Run `astrbot conf admin` after initialization.",
)
@click.option(
"--root",
help="ASTRBOT root directory to initialize (overrides ASTRBOT_ROOT env)",
type=str,
)
def init(
yes: bool,
backend_only: bool,
backup: str | None,
admin_username: str | None,
admin_password: str | None,
root: str | None = None,
) -> None:
"""Initialize AstrBot"""
def init() -> None:
"""初始化 AstrBot"""
click.echo("Initializing AstrBot...")
if os.environ.get("ASTRBOT_SYSTEMD") == "1":
yes = True
from astrbot.core.utils.astrbot_path import astrbot_paths
astrbot_root = Path(root) if root else astrbot_paths.root
astrbot_root = get_astrbot_root()
lock_file = astrbot_root / "astrbot.lock"
lock = FileLock(lock_file, timeout=5)
try:
with lock.acquire():
asyncio.run(
initialize_astrbot(
astrbot_root,
yes=yes,
backend_only=backend_only,
admin_username=admin_username,
admin_password=admin_password,
),
)
if backup:
from .cmd_bk import import_data_command
asyncio.run(initialize_astrbot(astrbot_root))
except Timeout:
raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行")
click.echo(f"Restoring from backup: {backup}")
click.get_current_context().invoke(
import_data_command,
backup_file=backup,
yes=True,
)
click.echo()
click.echo("=" * 60)
click.echo("初始化完成!")
click.echo("=" * 60)
click.echo()
click.echo("启动 AstrBot")
click.echo(" 完整模式(含 Dashboard: astrbot run")
click.echo(" 仅后端模式: astrbot run --backend-only")
click.echo()
click.echo("首次使用前请先设置管理员密码:")
click.echo(" astrbot conf admin")
click.echo()
except Timeout as err:
raise click.ClickException(
"Cannot acquire lock file. Please check if another instance is running",
) from err
except Exception as e:
raise click.ClickException(f"Initialization failed: {e!s}") from e
raise click.ClickException(f"初始化失败: {e!s}")

View File

@@ -1,38 +0,0 @@
import click
from .cmd_conf import (
_load_config,
_save_config,
_set_dashboard_password,
_set_nested_item,
_validate_dashboard_password,
_validate_dashboard_username,
)
@click.command(name="password")
@click.option(
"--username",
help="Optional dashboard username to set together with the new password.",
)
def password(username: str | None) -> None:
"""Change the AstrBot dashboard password."""
config = _load_config()
new_password = click.prompt(
"New dashboard password",
hide_input=True,
confirmation_prompt=True,
)
validated_password = _validate_dashboard_password(new_password)
if username is not None:
validated_username = _validate_dashboard_username(username.strip())
_set_nested_item(config, "dashboard.username", validated_username)
_set_dashboard_password(config, validated_password)
_save_config(config)
click.echo("Dashboard password updated.")
if username is not None:
click.echo(f"Dashboard username updated: {validated_username}")

View File

@@ -1,85 +1,92 @@
import re
import shutil
from pathlib import Path
import click
import shutil
from astrbot.cli.i18n import t
from astrbot.cli.utils import (
PluginStatus,
build_plug_list,
from ..utils import (
get_git_repo,
build_plug_list,
manage_plugin,
PluginStatus,
check_astrbot_root,
get_astrbot_root,
)
@click.group(name="plugin")
def plug() -> None:
"""Plugin management"""
@click.group()
def plug():
"""插件管理"""
pass
def display_plugins(plugins, title=None, color=None) -> None:
def _get_data_path() -> Path:
base = get_astrbot_root()
if not check_astrbot_root(base):
raise click.ClickException(
f"{base}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init"
)
return (base / "data").resolve()
def display_plugins(plugins, title=None, color=None):
if title:
click.echo(click.style(title, fg=color, bold=True))
click.echo(
f"{'Name':<20} {'Version':<10} {'Status':<10} {'Author':<15} {'Description':<30}",
)
click.echo(f"{'名称':<20} {'版本':<10} {'状态':<10} {'作者':<15} {'描述':<30}")
click.echo("-" * 85)
for p in plugins:
desc = p["desc"][:30] + ("..." if len(p["desc"]) > 30 else "")
click.echo(
f"{p['name']:<20} {p['version']:<10} {p['status']:<10} "
f"{p['author']:<15} {desc:<30}",
f"{p['author']:<15} {desc:<30}"
)
@plug.command()
@click.argument("name")
def new(name: str) -> None:
"""Create a new plugin"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
def new(name: str):
"""创建新插件"""
base_path = _get_data_path()
plug_path = base_path / "plugins" / name
if plug_path.exists():
raise click.ClickException(t("plugin_already_exists", name=name))
raise click.ClickException(f"插件 {name} 已存在")
author = click.prompt("Enter plugin author", type=str)
desc = click.prompt("Enter plugin description", type=str)
version = click.prompt("Enter plugin version", type=str)
author = click.prompt("请输入插件作者", type=str)
desc = click.prompt("请输入插件描述", type=str)
version = click.prompt("请输入插件版本", type=str)
if not re.match(r"^\d+\.\d+(\.\d+)?$", version.lower().lstrip("v")):
raise click.ClickException("Version must be in x.y or x.y.z format")
repo = click.prompt("Enter plugin repository URL:", type=str)
raise click.ClickException("版本号必须为 x.y x.y.z 格式")
repo = click.prompt("请输入插件仓库:", type=str)
if not repo.startswith("http"):
raise click.ClickException("Repository URL must start with http")
raise click.ClickException("仓库地址必须以 http 开头")
click.echo("Downloading plugin template...")
click.echo("下载插件模板...")
get_git_repo(
"https://github.com/Soulter/helloworld",
plug_path,
)
click.echo("Rewriting plugin metadata...")
# Rewrite metadata.yaml
click.echo("重写插件信息...")
# 重写 metadata.yaml
with open(plug_path / "metadata.yaml", "w", encoding="utf-8") as f:
f.write(
f"name: {name}\n"
f"desc: {desc}\n"
f"version: {version}\n"
f"author: {author}\n"
f"repo: {repo}\n",
f"repo: {repo}\n"
)
# Rewrite README.md
# 重写 README.md
with open(plug_path / "README.md", "w", encoding="utf-8") as f:
f.write(
f"# {name}\n\n{desc}\n\n# Support\n\n[Documentation](https://docs.astrbot.app)\n",
)
f.write(f"# {name}\n\n{desc}\n\n# 支持\n\n[帮助文档](https://astrbot.app)\n")
# Rewrite main.py
with open(plug_path / "main.py", encoding="utf-8") as f:
# 重写 main.py
with open(plug_path / "main.py", "r", encoding="utf-8") as f:
content = f.read()
new_content = content.replace(
@@ -90,59 +97,55 @@ def new(name: str) -> None:
with open(plug_path / "main.py", "w", encoding="utf-8") as f:
f.write(new_content)
click.echo(f"Plugin {name} created successfully")
click.echo(f"插件 {name} 创建成功")
@plug.command()
@click.option("--all", "-a", is_flag=True, help="List uninstalled plugins")
def list(all: bool) -> None:
"""List plugins"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
@click.option("--all", "-a", is_flag=True, help="列出未安装的插件")
def list(all: bool):
"""列出插件"""
base_path = _get_data_path()
plugins = build_plug_list(base_path / "plugins")
# Unpublished plugins
# 未发布的插件
not_published_plugins = [
p for p in plugins if p["status"] == PluginStatus.NOT_PUBLISHED
]
if not_published_plugins:
display_plugins(not_published_plugins, "Unpublished Plugins", "red")
display_plugins(not_published_plugins, "未发布的插件", "red")
# Plugins needing update
# 需要更新的插件
need_update_plugins = [
p for p in plugins if p["status"] == PluginStatus.NEED_UPDATE
]
if need_update_plugins:
display_plugins(need_update_plugins, "Plugins Needing Update", "yellow")
display_plugins(need_update_plugins, "需要更新的插件", "yellow")
# Installed plugins
# 已安装的插件
installed_plugins = [p for p in plugins if p["status"] == PluginStatus.INSTALLED]
if installed_plugins:
display_plugins(installed_plugins, "Installed Plugins", "green")
display_plugins(installed_plugins, "已安装的插件", "green")
# Uninstalled plugins
# 未安装的插件
not_installed_plugins = [
p for p in plugins if p["status"] == PluginStatus.NOT_INSTALLED
]
if not_installed_plugins and all:
display_plugins(not_installed_plugins, "Uninstalled Plugins", "blue")
display_plugins(not_installed_plugins, "未安装的插件", "blue")
if (
not any([not_published_plugins, need_update_plugins, installed_plugins])
and not all
):
click.echo("No plugins installed")
click.echo("未安装任何插件")
@plug.command()
@click.argument("name")
@click.option("--proxy", help="Proxy server address")
def install(name: str, proxy: str | None) -> None:
"""Install a plugin"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
@click.option("--proxy", help="代理服务器地址")
def install(name: str, proxy: str | None):
"""安装插件"""
base_path = _get_data_path()
plug_path = base_path / "plugins"
plugins = build_plug_list(base_path / "plugins")
@@ -156,45 +159,39 @@ def install(name: str, proxy: str | None) -> None:
)
if not plugin:
raise click.ClickException(t("plugin_not_found_or_installed", name=name))
raise click.ClickException(f"未找到可安装的插件 {name},可能是不存在或已安装")
manage_plugin(plugin, plug_path, is_update=False, proxy=proxy)
@plug.command()
@click.argument("name")
def remove(name: str) -> None:
"""Uninstall a plugin"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
def remove(name: str):
"""卸载插件"""
base_path = _get_data_path()
plugins = build_plug_list(base_path / "plugins")
plugin = next((p for p in plugins if p["name"] == name), None)
if not plugin or not plugin.get("local_path"):
raise click.ClickException(t("plugin_not_found_or_installed", name=name))
raise click.ClickException(f"插件 {name} 不存在或未安装")
plugin_path = plugin["local_path"]
click.confirm(t("plugin_uninstall_confirm", name=name), default=False, abort=True)
click.confirm(f"确定要卸载插件 {name} 吗?", default=False, abort=True)
try:
shutil.rmtree(plugin_path)
click.echo(t("plugin_uninstall_success", name=name))
click.echo(f"插件 {name} 已卸载")
except Exception as e:
raise click.ClickException(
t("plugin_uninstall_failed_ex", name=name, error=str(e)),
) from e
raise click.ClickException(f"卸载插件 {name} 失败: {e}")
@plug.command()
@click.argument("name", required=False)
@click.option("--proxy", help="GitHub proxy address")
def update(name: str, proxy: str | None) -> None:
"""Update plugins"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
@click.option("--proxy", help="Github代理地址")
def update(name: str, proxy: str | None):
"""更新插件"""
base_path = _get_data_path()
plug_path = base_path / "plugins"
plugins = build_plug_list(base_path / "plugins")
@@ -209,9 +206,7 @@ def update(name: str, proxy: str | None) -> None:
)
if not plugin:
raise click.ClickException(
f"Plugin {name} does not need updating or cannot be updated",
)
raise click.ClickException(f"插件 {name} 不需要更新或无法更新")
manage_plugin(plugin, plug_path, is_update=True, proxy=proxy)
else:
@@ -220,23 +215,21 @@ def update(name: str, proxy: str | None) -> None:
]
if not need_update_plugins:
click.echo(t("plugin_no_update_needed"))
click.echo("没有需要更新的插件")
return
click.echo(t("plugin_found_update", count=str(len(need_update_plugins))))
click.echo(f"发现 {len(need_update_plugins)} 个插件需要更新")
for plugin in need_update_plugins:
plugin_name = plugin["name"]
click.echo(t("plugin_updating", name=plugin_name))
click.echo(f"正在更新插件 {plugin_name}...")
manage_plugin(plugin, plug_path, is_update=True, proxy=proxy)
@plug.command()
@click.argument("query")
def search(query: str) -> None:
"""Search for plugins"""
from astrbot.core.utils.astrbot_path import astrbot_paths
base_path = astrbot_paths.data
def search(query: str):
"""搜索插件"""
base_path = _get_data_path()
plugins = build_plug_list(base_path / "plugins")
matched_plugins = [
@@ -248,7 +241,7 @@ def search(query: str) -> None:
]
if not matched_plugins:
click.echo(t("plugin_search_no_result", query=query))
click.echo(f"未找到匹配 '{query}' 的插件")
return
display_plugins(matched_plugins, t("plugin_search_results", query=query), "cyan")
display_plugins(matched_plugins, f"搜索结果: '{query}'", "cyan")

View File

@@ -1,104 +1,22 @@
"""AstrBot Run
Environment Variables Used in Project:
Core:
- `ASTRBOT_ROOT`: AstrBot root directory path.
- `ASTRBOT_LOG_LEVEL`: Log level (e.g. INFO, DEBUG).
- `ASTRBOT_CLI`: Flag indicating execution via CLI.
- `ASTRBOT_DESKTOP_CLIENT`: Flag indicating execution via desktop client.
- `ASTRBOT_SYSTEMD`: Flag indicating execution via systemd service.
- `ASTRBOT_RELOAD`: Enable plugin auto-reload (set to "1").
- `ASTRBOT_DISABLE_METRICS`: Disable metrics upload (set to "1").
- `TESTING`: Enable testing mode.
- `DEMO_MODE`: Enable demo mode.
- `PYTHON`: Python executable path override (for local code execution).
Dashboard / Backend:
- `ASTRBOT_DASHBOARD_ENABLE`: Enable/Disable Dashboard.
- `ASTRBOT_HOST`: Dashboard bind host.
- `ASTRBOT_PORT`: Dashboard bind port.
SSL (AstrBot-standard names):
- `ASTRBOT_SSL_ENABLE`: Enable SSL for API.
- `ASTRBOT_SSL_CERT`: SSL Certificate path for backend.
- `ASTRBOT_SSL_KEY`: SSL Key path for backend.
- `ASTRBOT_SSL_CA_CERTS`: SSL CA Certs path for backend.
Network:
- `http_proxy` / `https_proxy`: Proxy URL.
- `no_proxy`: No proxy list.
Internationalization:
- `ASTRBOT_CLI_LANG`: CLI interface language (zh/en).
Integrations:
- `DASHSCOPE_API_KEY`: Alibaba DashScope API Key (for Rerank).
- `COZE_API_KEY` / `COZE_BOT_ID`: Coze integration.
- `BAY_DATA_DIR`: Computer Use data directory.
Platform Specific:
- `TEST_MODE`: Test mode for QQOfficial.
"""
from __future__ import annotations
import asyncio
import os
import re
import sys
import traceback
from pathlib import Path
import click
from dotenv import load_dotenv
import asyncio
import traceback
from filelock import FileLock, Timeout
from astrbot.cli.utils import DashboardManager
from astrbot.runtime_bootstrap import initialize_runtime_bootstrap
# Python version check: require 3.12 or 3.13
if not (sys.version_info.major == 3 and sys.version_info.minor in (12, 13)):
sys.exit(1)
# Regular expression to find bash-like parameter expansions:
# ${VAR:-default} or ${VAR}
_PARAM_EXPAND_RE = re.compile(r"\$\{([^}:]+?)(:-([^}]*))?\}")
from ..utils import check_dashboard, check_astrbot_root, get_astrbot_root
def _expand_parameter(
match: re.Match,
env: dict[str, str],
local: dict[str, str],
) -> str:
"""Helper to expand a single ${VAR:-default} or ${VAR} occurrence.
Precedence:
1. local dict (parsed from the same file, earlier entries)
2. environment variables
3. default provided in the expansion (if any)
4. empty string
"""
var = match.group(1)
default = match.group(3) if match.group(3) is not None else ""
# Prefer 'local' parsed values first
if var in local and local[var] != "":
return local[var]
val = env.get(var, "")
if val != "":
return val
return default
async def run_astrbot(astrbot_root: Path) -> None:
"""Run AstrBot"""
from astrbot.core import LogBroker, LogManager, db_helper, logger
async def run_astrbot(astrbot_root: Path):
"""运行 AstrBot"""
from astrbot.core import logger, LogManager, LogBroker, db_helper
from astrbot.core.initial_loader import InitialLoader
if (
os.environ.get("ASTRBOT_DASHBOARD_ENABLE", os.environ.get("DASHBOARD_ENABLE"))
== "True"
):
await DashboardManager().ensure_installed(astrbot_root)
await check_dashboard(astrbot_root / "data")
log_broker = LogBroker()
LogManager.set_queue_handler(logger, log_broker)
@@ -109,317 +27,37 @@ async def run_astrbot(astrbot_root: Path) -> None:
await core_lifecycle.start()
@click.option("--reload", "-r", is_flag=True, help="Auto-reload plugins")
@click.option("--host", "-H", help="AstrBot Dashboard Host", required=False, type=str)
@click.option("--port", "-p", help="AstrBot Dashboard port", required=False, type=str)
@click.option("--root", help="AstrBot root directory", required=False, type=str)
@click.option(
"--service-config",
"-c",
help="Service configuration file path (supports ${VAR:-default} style expansion)",
required=False,
type=str,
)
@click.option(
"--backend-only",
"-b",
is_flag=True,
default=False,
help="Disable WebUI, run backend only",
)
@click.option(
"--log-level",
"-l",
help="Log level",
required=False,
type=str,
default="INFO",
)
@click.option(
"--ssl-cert",
help="SSL certificate file path for backend (preferred env name: ASTRBOT_SSL_CERT)",
required=False,
type=str,
)
@click.option(
"--ssl-key",
help="SSL private key file path for backend (preferred env name: ASTRBOT_SSL_KEY)",
required=False,
type=str,
)
@click.option(
"--ssl-ca",
help="SSL CA certificates file path for backend (preferred env name: ASTRBOT_SSL_CA_CERTS)",
required=False,
type=str,
)
@click.option("--debug", is_flag=True, help="Enable debug mode")
@click.option("--reload", "-r", is_flag=True, help="插件自动重载")
@click.option("--port", "-p", help="Astrbot Dashboard端口", required=False, type=str)
@click.command()
def run(
reload: bool,
host: str,
port: str,
root: str,
service_config: str,
backend_only: bool,
log_level: str,
ssl_cert: str,
ssl_key: str,
ssl_ca: str,
debug: bool,
) -> None:
"""Run AstrBot"""
initialize_runtime_bootstrap()
def run(reload: bool, port: str) -> None:
"""运行 AstrBot"""
try:
if debug:
log_level = "DEBUG"
# --- Step 1: Resolve service-config path (if provided). We'll treat it as a .env file later. ---
svc_path: Path | None = None
if service_config:
candidate = Path(service_config)
if not candidate.exists():
# Try to expand user and resolve
candidate = Path(os.path.expanduser(service_config))
if candidate.exists():
svc_path = candidate
# NOTE:
# Loading of common .env files (CWD/.env, packaged project .env, ASTRBOT_ROOT/.env)
# has been moved to astrbot.core.utils.astrbot_path during import-time to avoid
# early-initialization ordering issues. Those files are loaded there using
# `override=False` so they do not clobber environment variables provided by the
# systemd unit or the caller.
#
# Here we only load an explicit service-config file (if given). Service-config
# should be able to override the common .env files, but CLI-provided values must
# still win; the CLI will set/overwrite corresponding environment variables
# below after this load.
if svc_path and svc_path.exists():
# Load service-config as an env file and allow it to override previously-loaded
# .env values (those were loaded by astrbot_path). CLI variables are applied
# after this point and will take precedence.
load_dotenv(dotenv_path=str(svc_path), override=True)
# Mark CLI execution
os.environ["ASTRBOT_CLI"] = "1"
astrbot_root = get_astrbot_root()
from astrbot.core.utils.astrbot_path import astrbot_paths
# Resolve astrbot_root with the following precedence:
# 1. CLI --root parameter (local variable `root`)
# 2. ASTRBOT_ROOT environment variable (possibly from .env or parsed service config)
# 3. packaged default astrbot_paths.root
if root:
os.environ["ASTRBOT_ROOT"] = root
astrbot_root = Path(root)
elif os.environ.get("ASTRBOT_ROOT"):
astrbot_root = Path(os.environ["ASTRBOT_ROOT"])
else:
astrbot_root = astrbot_paths.root
if not astrbot_paths.is_root:
if not check_astrbot_root(astrbot_root):
raise click.ClickException(
f"{astrbot_root} is not a valid AstrBot root directory. Use 'astrbot init' to initialize",
f"{astrbot_root}不是有效的 AstrBot 根目录,如需初始化请使用 astrbot init"
)
# Ensure ASTRBOT_ROOT env var is set to the resolved root (without overriding a CLI-provided root value above)
os.environ["ASTRBOT_ROOT"] = str(astrbot_root)
sys.path.insert(0, str(astrbot_root))
# Host/Port precedence: CLI args > parsed service config/env/.env > defaults.
if port is not None:
os.environ["ASTRBOT_PORT"] = port
if host is not None:
os.environ["ASTRBOT_HOST"] = host
# CLI-provided SSL paths should set backend-standard env names.
if ssl_cert is not None:
os.environ["ASTRBOT_SSL_CERT"] = ssl_cert
if ssl_key is not None:
os.environ["ASTRBOT_SSL_KEY"] = ssl_key
if ssl_ca is not None:
os.environ["ASTRBOT_SSL_CA_CERTS"] = ssl_ca
# Dashboard enable is derived from CLI flag (--backend-only). CLI decision should win.
os.environ["ASTRBOT_DASHBOARD_ENABLE"] = str(not backend_only)
os.environ["ASTRBOT_LOG_LEVEL"] = log_level
if port:
os.environ["DASHBOARD_PORT"] = port
if reload:
click.echo("Plugin auto-reload enabled")
click.echo("启用插件自动重载")
os.environ["ASTRBOT_RELOAD"] = "1"
if debug:
keys_to_print = [
"ASTRBOT_ROOT",
"ASTRBOT_LOG_LEVEL",
"ASTRBOT_CLI",
"ASTRBOT_DESKTOP_CLIENT",
"ASTRBOT_SYSTEMD",
"ASTRBOT_RELOAD",
"ASTRBOT_DISABLE_METRICS",
"TESTING",
"DEMO_MODE",
"PYTHON",
"ASTRBOT_DASHBOARD_ENABLE",
"DASHBOARD_ENABLE",
"ASTRBOT_HOST",
"DASHBOARD_HOST",
"ASTRBOT_PORT",
"DASHBOARD_PORT",
# Dashboard SSL (legacy)
"ASTRBOT_SSL_ENABLE",
"DASHBOARD_SSL_ENABLE",
"ASTRBOT_SSL_CERT",
"DASHBOARD_SSL_CERT",
"ASTRBOT_SSL_KEY",
"DASHBOARD_SSL_KEY",
"ASTRBOT_SSL_CA_CERTS",
"DASHBOARD_SSL_CA_CERTS",
# Backend-standard SSL (preferred)
"ASTRBOT_SSL_ENABLE",
"ASTRBOT_SSL_CERT",
"ASTRBOT_SSL_KEY",
"ASTRBOT_SSL_CA_CERTS",
"http_proxy",
"https_proxy",
"no_proxy",
"DASHSCOPE_API_KEY",
"COZE_API_KEY",
"COZE_BOT_ID",
"BAY_DATA_DIR",
"TEST_MODE",
]
click.secho("\n[Debug Mode] Environment Variables:", fg="yellow", bold=True)
for key in keys_to_print:
if key in os.environ:
val = os.environ[key]
if "KEY" in key or "PASSWORD" in key or "SECRET" in key:
if len(val) > 8:
val = val[:4] + "****" + val[-4:]
else:
val = "****"
click.echo(f" {click.style(key, fg='cyan')}: {val}")
if svc_path:
click.echo(
f" {click.style('SERVICE_CONFIG', fg='cyan')}: {svc_path!s}",
)
click.echo("")
lock_file = astrbot_root / "astrbot.lock"
lock = FileLock(lock_file, timeout=5)
with lock.acquire():
async def run_with_logging() -> None:
from astrbot.core import LogBroker, LogManager, db_helper, logger
from astrbot.core.initial_loader import InitialLoader
if (
os.environ.get(
"ASTRBOT_DASHBOARD_ENABLE",
os.environ.get("DASHBOARD_ENABLE"),
)
== "True"
):
await DashboardManager().ensure_installed(astrbot_root)
log_broker = LogBroker()
LogManager.set_queue_handler(logger, log_broker)
# Register a stdout subscriber for real-time log streaming
log_queue = log_broker.register()
db = db_helper
initial_loader = InitialLoader(db, log_broker)
# Start a task to stream logs to stdout
async def stream_logs() -> None:
"""Stream logs from LogBroker to stdout."""
while True:
try:
log_entry = await asyncio.wait_for(
log_queue.get(),
timeout=0.5,
)
# Format: [LEVEL] message
level = log_entry.get("level_name", "INFO")
message = log_entry.get("message", "")
if message:
level_color = {
"DEBUG": "cyan",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "red",
}.get(level, "white")
click.secho(
f"[{level}]",
fg=level_color,
bold=False,
nl=False,
)
click.echo(f" {message}")
except TimeoutError:
continue
except asyncio.CancelledError:
break
# Start streaming task
stream_task = asyncio.create_task(stream_logs())
try:
await initial_loader.start()
finally:
stream_task.cancel()
try:
await stream_task
except asyncio.CancelledError:
pass
click.echo()
click.echo("=" * 60)
click.echo("AstrBot 启动中...")
click.echo("=" * 60)
from astrbot.cli.banner import print_logo
print_logo()
click.echo()
if backend_only:
click.echo("[模式] 仅后端模式(无本地 Dashboard")
click.echo()
click.echo("[提示] 可以通过以下方式访问 WebUI")
click.echo(" - 使用远程服务器的在线 Dashboard")
click.echo(" - 地址: http://服务器IP:6185")
click.echo()
else:
dashboard_url = f"http://{host or 'localhost'}:{port or '6185'}"
click.echo("[模式] 完整模式(含本地 Dashboard")
click.echo()
click.echo(f"[Dashboard] 请访问: {dashboard_url}")
click.echo()
click.echo("!" * 60)
click.echo("安全提示:")
click.echo(" HTTPS 前端只能安全连接 localhost 的 HTTP 后端")
click.echo(" 不支持远程 + HTTP 后端(不安全)")
click.echo("!" * 60)
click.echo()
click.echo("正在启动服务...(日志输出中)")
click.echo()
asyncio.run(run_with_logging())
asyncio.run(run_astrbot(astrbot_root))
except KeyboardInterrupt:
click.echo("AstrBot has been shut down.")
click.echo("AstrBot 已关闭...")
except Timeout:
raise click.ClickException(
"Cannot acquire lock file. Please check if another instance is running",
) from None
raise click.ClickException("无法获取锁文件,请检查是否有其他实例正在运行")
except Exception as e:
# Keep original traceback visible for diagnostics
raise click.ClickException(
f"Runtime error: {e}\n{traceback.format_exc()}",
) from e
raise click.ClickException(f"运行时出现错误: {e}\n{traceback.format_exc()}")

File diff suppressed because it is too large Load Diff

View File

@@ -1,69 +0,0 @@
import os
import shutil
from pathlib import Path
import click
from astrbot.core.utils.astrbot_path import astrbot_paths
@click.command()
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts")
@click.option(
"--keep-data",
is_flag=True,
help="Keep data directory (config, plugins, etc.)",
)
def uninstall(yes: bool, keep_data: bool) -> None:
"""Remove AstrBot files from the current root directory."""
if os.environ.get("ASTRBOT_SYSTEMD") == "1":
yes = True
dot_astrbot = astrbot_paths.root / ".astrbot"
lock_file = astrbot_paths.root / "astrbot.lock"
data_dir = astrbot_paths.data
removable_paths: list[Path] = [dot_astrbot, lock_file]
if not keep_data:
removable_paths.insert(0, data_dir)
# Check if this looks like an AstrBot root before blowing things up
if not dot_astrbot.exists() and not data_dir.exists():
click.echo("No AstrBot initialization found in current directory.")
return
if keep_data:
click.echo("Keeping data directory as requested.")
if yes or click.confirm(
f"Are you sure you want to remove AstrBot data at {astrbot_paths.root}? \n"
f"This will delete:\n"
f" - {data_dir} (Config, Plugins, Database)\n"
f" - {dot_astrbot}\n"
f" - {lock_file}",
default=False,
abort=True,
):
removed_any = False
for path in removable_paths:
if not path.exists():
continue
removed_any = True
if path.is_dir():
click.echo(f"Removing directory: {path}")
shutil.rmtree(path)
else:
click.echo(f"Removing file: {path}")
path.unlink()
if removed_any:
click.echo("AstrBot files removed successfully.")
else:
click.echo("No removable AstrBot files were found.")
# TODO: Consider adding an explicit `--service` cleanup mode instead of
# touching systemd or other service managers during normal uninstall.
# TODO: Consider adding package-manager-specific uninstall helpers once
# the CLI can reliably detect the installation source.
click.echo("uv: uv tool uninstall astrbot")
click.echo("paru/yay: paru -R astrbot")

View File

@@ -1,278 +0,0 @@
"""Internationalization support for AstrBot CLI.
This module provides i18n support with Chinese and English languages.
Language is auto-detected from environment or can be set manually.
"""
from __future__ import annotations
import os
from enum import Enum
from functools import lru_cache
class Language(Enum):
"""Supported languages."""
ZH = "zh"
EN = "en"
# Translation dictionaries
_TRANSLATIONS: dict[Language, dict[str, str]] = {
Language.ZH: {
# CLI welcome and general
"cli_welcome": "欢迎使用 AstrBot CLI!",
"cli_version": "AstrBot CLI 版本: {version}",
"cli_unknown_command": "未知命令: {command}",
"cli_help_available": "使用 astrbot help --all 查看所有命令",
# Dashboard commands
"dashboard_bundled": "Dashboard 已打包在安装包中 - 跳过下载",
"dashboard_not_installed": "Dashboard 未安装",
"dashboard_install_confirm": "是否安装 Dashboard?",
"dashboard_installing": "正在安装 Dashboard...",
"dashboard_install_success": "Dashboard 安装成功",
"dashboard_install_failed": "Dashboard 安装失败: {error}",
"dashboard_not_needed": "Dashboard 不需要安装",
"dashboard_declined": "Dashboard 安装已取消",
"dashboard_already_up_to_date": "Dashboard 已是最新版本",
"dashboard_version": "Dashboard 版本: {version}",
"dashboard_download_failed": "Dashboard 下载失败: {error}",
"dashboard_init_dir": "正在初始化 Dashboard 目录...",
"dashboard_init_success": "Dashboard 初始化成功",
# Plugin commands
"plugin_installing": "正在安装插件: {name}",
"plugin_install_success": "插件安装成功: {name}",
"plugin_install_failed": "插件安装失败: {name}",
"plugin_uninstall_confirm": "确定要卸载插件 {name} 吗?",
"plugin_uninstall_success": "插件卸载成功: {name}",
"plugin_uninstall_failed": "插件卸载失败: {name}",
"plugin_list_empty": "未安装任何插件",
"plugin_already_installed": "插件已安装: {name}",
"plugin_not_found": "插件未找到: {name}",
"plugin_already_exists": "插件已存在: {name}",
"plugin_not_found_or_installed": "插件未找到或已安装: {name}",
"plugin_uninstall_failed_ex": "插件卸载失败 {name}: {error}",
"plugin_no_update_needed": "没有需要更新的插件",
"plugin_found_update": "发现 {count} 个插件需要更新",
"plugin_updating": "正在更新插件 {name}...",
"plugin_search_no_result": "未找到匹配 '{query}' 的插件",
"plugin_search_results": "搜索结果: '{query}'",
# Config commands
"config_show": "显示配置",
"config_set_success": "配置项已更新: {key} = {value}",
"config_set_failed": "配置项更新失败: {key}",
"config_set_failed_ex": "设置配置失败: {error}",
"config_get_success": "{key} = {value}",
"config_get_not_found": "配置项未找到: {key}",
"config_reset_confirm": "确定要重置所有配置吗?",
"config_reset_success": "配置已重置",
# Config validators
"config_log_level_invalid": "日志级别必须是 DEBUG/INFO/WARNING/ERROR/CRITICAL 之一",
"config_port_must_be_number": "端口必须是数字",
"config_port_range_invalid": "端口必须在 1-65535 范围内",
"config_username_empty": "用户名不能为空",
"config_password_empty": "密码不能为空",
"config_timezone_invalid": "无效的时区: {value}。请使用有效的 IANA 时区名称",
"config_callback_invalid": "回调 API 基础路径必须以 http:// 或 https:// 开头",
"config_key_unsupported": "不支持的配置项: {key}",
"config_key_unknown": "未知的配置项: {key}",
"config_updated": "配置已更新: {key}",
# Init command
"init_creating": "正在创建配置目录...",
"init_created": "配置目录已创建: {path}",
"init_copying": "正在复制配置文件...",
"init_copied": "配置文件已复制",
"init_success": "AstrBot 初始化完成!",
"init_failed": "初始化失败: {error}",
# Run command
"run_starting": "正在启动 AstrBot...",
"run_started": "AstrBot 已启动!",
"run_backend_only": "以无界面模式启动",
"run_failed": "启动失败: {error}",
"run_stopped": "AstrBot 已停止",
# Common
"yes": "",
"no": "",
"cancel": "取消",
"confirm": "确认",
"error": "错误",
"success": "成功",
"warning": "警告",
"info": "信息",
"loading": "加载中...",
"done": "完成",
"failed": "失败",
"retry": "重试",
"exit": "退出",
"continue": "继续",
},
Language.EN: {
# CLI welcome and general
"cli_welcome": "Welcome to AstrBot CLI!",
"cli_version": "AstrBot CLI version: {version}",
"cli_unknown_command": "Unknown command: {command}",
"cli_help_available": "Use astrbot help --all to see all commands",
# Dashboard commands
"dashboard_bundled": "Dashboard is bundled with the package - skipping download",
"dashboard_not_installed": "Dashboard is not installed",
"dashboard_install_confirm": "Install Dashboard?",
"dashboard_installing": "Installing Dashboard...",
"dashboard_install_success": "Dashboard installed successfully",
"dashboard_install_failed": "Failed to install dashboard: {error}",
"dashboard_not_needed": "Dashboard not needed",
"dashboard_declined": "Dashboard installation declined.",
"dashboard_already_up_to_date": "Dashboard is already up to date",
"dashboard_version": "Dashboard version: {version}",
"dashboard_download_failed": "Failed to download dashboard: {error}",
"dashboard_init_dir": "Initializing dashboard directory...",
"dashboard_init_success": "Dashboard initialized successfully",
# Plugin commands
"plugin_installing": "Installing plugin: {name}",
"plugin_install_success": "Plugin installed successfully: {name}",
"plugin_install_failed": "Failed to install plugin: {name}",
"plugin_uninstall_confirm": "Uninstall plugin {name}?",
"plugin_uninstall_success": "Plugin uninstalled successfully: {name}",
"plugin_uninstall_failed": "Failed to uninstall plugin: {name}",
"plugin_list_empty": "No plugins installed",
"plugin_already_installed": "Plugin already installed: {name}",
"plugin_not_found": "Plugin not found: {name}",
"plugin_already_exists": "Plugin {name} already exists",
"plugin_not_found_or_installed": "Plugin {name} not found or already installed",
"plugin_uninstall_failed_ex": "Failed to uninstall plugin {name}: {error}",
"plugin_no_update_needed": "No plugins need updating",
"plugin_found_update": "Found {count} plugin(s) needing update",
"plugin_updating": "Updating plugin {name}...",
"plugin_search_no_result": "No plugins matching '{query}' found",
"plugin_search_results": "Search results: '{query}'",
# Config commands
"config_show": "Show configuration",
"config_set_success": "Configuration updated: {key} = {value}",
"config_set_failed": "Failed to update configuration: {key}",
"config_set_failed_ex": "Failed to set config: {error}",
"config_get_success": "{key} = {value}",
"config_get_not_found": "Configuration key not found: {key}",
"config_reset_confirm": "Reset all configuration?",
"config_reset_success": "Configuration reset",
# Config validators
"config_log_level_invalid": "Log level must be one of DEBUG/INFO/WARNING/ERROR/CRITICAL",
"config_port_must_be_number": "Port must be a number",
"config_port_range_invalid": "Port must be in range 1-65535",
"config_username_empty": "Username cannot be empty",
"config_password_empty": "Password cannot be empty",
"config_timezone_invalid": "Invalid timezone: {value}. Please use a valid IANA timezone name",
"config_callback_invalid": "Callback API base must start with http:// or https://",
"config_key_unsupported": "Unsupported config key: {key}",
"config_key_unknown": "Unknown config key: {key}",
"config_updated": "Config updated: {key}",
# Init command
"init_creating": "Creating config directory...",
"init_created": "Config directory created: {path}",
"init_copying": "Copying config files...",
"init_copied": "Config files copied",
"init_success": "AstrBot initialized successfully!",
"init_failed": "Initialization failed: {error}",
# Run command
"run_starting": "Starting AstrBot...",
"run_started": "AstrBot started!",
"run_backend_only": "Starting in backend-only mode",
"run_failed": "Failed to start: {error}",
"run_stopped": "AstrBot stopped",
# Common
"yes": "Yes",
"no": "No",
"cancel": "Cancel",
"confirm": "Confirm",
"error": "Error",
"success": "Success",
"warning": "Warning",
"info": "Info",
"loading": "Loading...",
"done": "Done",
"failed": "Failed",
"retry": "Retry",
"exit": "Exit",
"continue": "Continue",
},
}
@lru_cache(maxsize=1)
def get_current_language() -> Language:
"""Get the current language based on environment or default.
Detection order:
1. ASTRBOT_CLI_LANG environment variable (zh/en)
2. LANG environment variable (if contains zh/cn)
3. LC_ALL environment variable (if contains zh/cn)
4. Default to Chinese (most users are Chinese)
"""
# Check explicit override first
explicit = os.environ.get("ASTRBOT_CLI_LANG", "").lower()
if explicit in ("zh", "en"):
return Language.ZH if explicit == "zh" else Language.EN
# Check LANG/LC_ALL for Chinese
for env_var in ("LANG", "LC_ALL"):
lang = os.environ.get(env_var, "").lower()
if "zh" in lang or "cn" in lang:
return Language.ZH
# Default to Chinese for broader appeal
return Language.ZH
def set_language(lang: Language) -> None:
"""Set the current language (clears all translation caches)."""
get_current_language.cache_clear()
_t_cached.cache_clear()
# Set environment variable for persistence
os.environ["ASTRBOT_CLI_LANG"] = lang.value
@lru_cache(maxsize=128)
def _t_cached(key: str, lang: Language) -> str:
"""Cached translation lookup."""
return _TRANSLATIONS.get(lang, {}).get(key, key)
def t(translation_key: str, **kwargs: str) -> str:
"""Get translation for the given key in the current language.
Args:
translation_key: Translation key (e.g., "cli_welcome", "plugin_installing")
**kwargs: Format arguments for the translation string
Returns:
Translated string, or the key itself if not found
"""
result = _t_cached(translation_key, get_current_language())
if kwargs:
result = result.format(**kwargs)
return result
def tr(key: str, **kwargs: str) -> str:
"""Get translation (alias for t())."""
return t(key, **kwargs)
class CLITranslations:
"""Translation accessor class for CLI contexts.
Usage:
translations = CLITranslations()
print(translations.cli_welcome)
print(translations.plugin_installing(name="my_plugin"))
"""
def __getattr__(self, key: str) -> str:
return t(key)
def __call__(self, key: str, **kwargs: str) -> str:
return t(key, **kwargs)
# Convenience instance
translations = CLITranslations()

View File

@@ -1,12 +1,18 @@
from .dashboard import DashboardManager
from .plugin import PluginStatus, build_plug_list, get_git_repo, manage_plugin
from .basic import (
get_astrbot_root,
check_astrbot_root,
check_dashboard,
)
from .plugin import get_git_repo, manage_plugin, build_plug_list, PluginStatus
from .version_comparator import VersionComparator
__all__ = [
"DashboardManager",
"PluginStatus",
"VersionComparator",
"build_plug_list",
"get_astrbot_root",
"check_astrbot_root",
"check_dashboard",
"get_git_repo",
"manage_plugin",
"build_plug_list",
"VersionComparator",
"PluginStatus",
]

View File

@@ -2,12 +2,9 @@ from pathlib import Path
import click
# Static assets bundled inside the installed wheel (built by hatch_build.py).
_BUNDLED_DIST = Path(__file__).parent.parent.parent / "dashboard" / "dist"
def check_astrbot_root(path: str | Path) -> bool:
"""Check if the path is an AstrBot root directory"""
"""检查路径是否为 AstrBot 根目录"""
if not isinstance(path, Path):
path = Path(path)
if not path.exists() or not path.is_dir():
@@ -18,58 +15,54 @@ def check_astrbot_root(path: str | Path) -> bool:
def get_astrbot_root() -> Path:
"""Get the AstrBot root directory path"""
"""获取Astrbot根目录路径"""
return Path.cwd()
async def check_dashboard(astrbot_root: Path) -> None:
"""Check if the dashboard is installed"""
"""检查是否安装了dashboard"""
from astrbot.core.utils.io import get_dashboard_version, download_dashboard
from astrbot.core.config.default import VERSION
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
from .version_comparator import VersionComparator
# If the wheel ships bundled dashboard assets, no network download is needed.
if _BUNDLED_DIST.exists():
click.echo("Dashboard is bundled with the package skipping download.")
return
try:
dashboard_version = await get_dashboard_version()
match dashboard_version:
case None:
click.echo("Dashboard is not installed")
click.echo("未安装管理面板")
if click.confirm(
"Install dashboard?",
"是否安装管理面板?",
default=True,
abort=True,
):
click.echo("Installing dashboard...")
click.echo("正在安装管理面板...")
await download_dashboard(
path="data/dashboard.zip",
extract_path=str(astrbot_root),
version=f"v{VERSION}",
latest=False,
)
click.echo("Dashboard installed successfully")
click.echo("管理面板安装完成")
case str():
if VersionComparator.compare_version(VERSION, dashboard_version) <= 0:
click.echo("Dashboard is already up to date")
return
try:
version = dashboard_version.split("v")[1]
click.echo(f"Dashboard version: {version}")
await download_dashboard(
path="data/dashboard.zip",
extract_path=str(astrbot_root),
version=f"v{VERSION}",
latest=False,
)
except Exception as e:
click.echo(f"Failed to download dashboard: {e}")
click.echo("管理面板已是最新版本")
return
else:
try:
version = dashboard_version.split("v")[1]
click.echo(f"管理面板版本: {version}")
await download_dashboard(
path="data/dashboard.zip",
extract_path=str(astrbot_root),
version=f"v{VERSION}",
latest=False,
)
except Exception as e:
click.echo(f"下载管理面板失败: {e}")
return
except FileNotFoundError:
click.echo("Initializing dashboard directory...")
click.echo("初始化管理面板目录...")
try:
await download_dashboard(
path=str(astrbot_root / "dashboard.zip"),
@@ -77,7 +70,7 @@ async def check_dashboard(astrbot_root: Path) -> None:
version=f"v{VERSION}",
latest=False,
)
click.echo("Dashboard initialized successfully")
click.echo("管理面板初始化完成")
except Exception as e:
click.echo(f"Failed to download dashboard: {e}")
click.echo(f"下载管理面板失败: {e}")
return

View File

@@ -1,79 +0,0 @@
import sys
from importlib import resources
from pathlib import Path
import click
from astrbot.cli.i18n import t
from .version_comparator import VersionComparator
class DashboardManager:
_bundled_dist = resources.files("astrbot") / "dashboard" / "dist"
async def ensure_installed(self, astrbot_root: Path) -> None:
"""Ensure the dashboard assets are installed and up to date."""
from astrbot.core.config.default import VERSION
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
if self._bundled_dist.is_dir():
click.echo(t("dashboard_bundled"))
return
try:
dashboard_version = await get_dashboard_version()
match dashboard_version:
case None:
click.echo(t("dashboard_not_installed"))
# Skip interactive prompt in non-interactive environments
if not sys.stdin.isatty():
click.echo(t("dashboard_not_needed"))
return
if click.confirm(t("dashboard_install_confirm"), default=True):
click.echo(t("dashboard_installing"))
try:
await download_dashboard(
path="data/dashboard.zip",
extract_path=str(astrbot_root / "data"),
version=f"v{VERSION}",
latest=False,
)
click.echo(t("dashboard_install_success"))
except Exception as e:
click.echo(t("dashboard_install_failed", error=str(e)))
else:
click.echo(t("dashboard_declined"))
case str():
if (
VersionComparator.compare_version(VERSION, dashboard_version)
<= 0
):
click.echo(t("dashboard_already_up_to_date"))
return
try:
version = dashboard_version.split("v")[1]
click.echo(t("dashboard_version", version=version))
await download_dashboard(
path="data/dashboard.zip",
extract_path=str(astrbot_root / "data"),
version=f"v{VERSION}",
latest=False,
)
except Exception as e:
click.echo(t("dashboard_download_failed", error=str(e)))
return
except FileNotFoundError:
click.echo(t("dashboard_init_dir"))
try:
await download_dashboard(
path=str(astrbot_root / "data" / "dashboard.zip"),
extract_path=str(astrbot_root / "data"),
version=f"v{VERSION}",
latest=False,
)
click.echo(t("dashboard_init_success"))
except Exception as e:
click.echo(t("dashboard_download_failed", error=str(e)))
return

View File

@@ -1,64 +1,61 @@
import shutil
import tempfile
import httpx
import yaml
from enum import Enum
from io import BytesIO
from pathlib import Path
from typing import Any
from zipfile import ZipFile
import click
import httpx
import yaml
from .version_comparator import VersionComparator
class PluginStatus(str, Enum):
INSTALLED = "installed"
NEED_UPDATE = "needs-update"
NOT_INSTALLED = "not-installed"
NOT_PUBLISHED = "unpublished"
INSTALLED = "已安装"
NEED_UPDATE = "需更新"
NOT_INSTALLED = "未安装"
NOT_PUBLISHED = "未发布"
def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None:
"""Download code from a Git repository and extract to the specified path"""
def get_git_repo(url: str, target_path: Path, proxy: str | None = None):
"""从 Git 仓库下载代码并解压到指定路径"""
temp_dir = Path(tempfile.mkdtemp())
try:
# Parse repository info
# 解析仓库信息
repo_namespace = url.split("/")[-2:]
author = repo_namespace[0]
repo = repo_namespace[1]
# Try to get the latest release
# 尝试获取最新的 release
release_url = f"https://api.github.com/repos/{author}/{repo}/releases"
try:
with httpx.Client(
proxy=proxy or None,
follow_redirects=True,
proxy=proxy if proxy else None, follow_redirects=True
) as client:
resp = client.get(release_url)
resp.raise_for_status()
releases = resp.json()
if releases:
# Use the latest release
# 使用最新的 release
download_url = releases[0]["zipball_url"]
else:
# No release found, use default branch
click.echo(f"Downloading {author}/{repo} from default branch")
# 没有 release,使用默认分支
click.echo(f"正在从默认分支下载 {author}/{repo}")
download_url = f"https://github.com/{author}/{repo}/archive/refs/heads/master.zip"
except Exception as e:
click.echo(f"Failed to get release info: {e}. Using provided URL directly")
click.echo(f"获取 release 信息失败: {e},将直接使用提供的 URL")
download_url = url
# Apply proxy
# 应用代理
if proxy:
download_url = f"{proxy}/{download_url}"
# Download and extract
# 下载并解压
with httpx.Client(
proxy=proxy or None,
follow_redirects=True,
proxy=proxy if proxy else None, follow_redirects=True
) as client:
resp = client.get(download_url)
if (
@@ -66,7 +63,7 @@ def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None:
and "archive/refs/heads/master.zip" in download_url
):
alt_url = download_url.replace("master.zip", "main.zip")
click.echo("Branch 'master' not found, trying 'main' branch")
click.echo("master 分支不存在,尝试下载 main 分支")
resp = client.get(alt_url)
resp.raise_for_status()
else:
@@ -84,51 +81,46 @@ def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None:
shutil.rmtree(temp_dir, ignore_errors=True)
def load_yaml_metadata(plugin_dir: Path) -> dict[str, Any]:
"""Load plugin metadata from metadata.yaml file
def load_yaml_metadata(plugin_dir: Path) -> dict:
""" metadata.yaml 文件加载插件元数据
Args:
plugin_dir: Plugin directory path
plugin_dir: 插件目录路径
Returns:
dict: Dictionary containing metadata, or empty dict if loading fails
dict: 包含元数据的字典,如果读取失败则返回空字典
"""
yaml_path = plugin_dir / "metadata.yaml"
if yaml_path.exists():
try:
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
if isinstance(data, dict):
return dict[str, Any](data)
return {}
return yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
except Exception as e:
click.echo(f"Failed to read {yaml_path}: {e}", err=True)
click.echo(f"读取 {yaml_path} 失败: {e}", err=True)
return {}
def build_plug_list(plugins_dir: Path) -> list:
"""Build plugin list containing local and online plugin information
"""构建插件列表,包含本地和在线插件信息
Args:
plugins_dir (Path): Plugin directory path
plugins_dir (Path): 插件目录路径
Returns:
list: List of dicts containing plugin information
list: 包含插件信息的字典列表
"""
# Get local plugin info
# 获取本地插件信息
result = []
if plugins_dir.exists():
for plugin_name in [d.name for d in plugins_dir.glob("*") if d.is_dir()]:
plugin_dir = plugins_dir / plugin_name
# Load metadata from metadata.yaml
# metadata.yaml 加载元数据
metadata = load_yaml_metadata(plugin_dir)
if "desc" not in metadata and "description" in metadata:
metadata["desc"] = metadata["description"]
# If metadata loaded successfully, add to result list
# 如果成功加载元数据,添加到结果列表
if metadata and all(
k in metadata for k in ["name", "desc", "version", "author", "repo"]
):
@@ -141,10 +133,10 @@ def build_plug_list(plugins_dir: Path) -> list:
"repo": str(metadata.get("repo", "")),
"status": PluginStatus.INSTALLED,
"local_path": str(plugin_dir),
},
}
)
# Get online plugin list
# 获取在线插件列表
online_plugins = []
try:
with httpx.Client() as client:
@@ -161,61 +153,53 @@ def build_plug_list(plugins_dir: Path) -> list:
"repo": str(plugin_info.get("repo", "")),
"status": PluginStatus.NOT_INSTALLED,
"local_path": None,
},
}
)
except Exception as e:
click.echo(f"Failed to get online plugin list: {e}", err=True)
click.echo(f"获取在线插件列表失败: {e}", err=True)
# Compare with online plugins and update status
# 与在线插件比对,更新状态
online_plugin_names = {plugin["name"] for plugin in online_plugins}
for local_plugin in result:
if local_plugin["name"] in online_plugin_names:
# Find the corresponding online plugin
# 查找对应的在线插件
online_plugin = next(
p for p in online_plugins if p["name"] == local_plugin["name"]
)
if (
VersionComparator.compare_version(
local_plugin["version"] or "",
online_plugin["version"] or "",
local_plugin["version"], online_plugin["version"]
)
< 0
):
local_plugin["status"] = PluginStatus.NEED_UPDATE
else:
# Local plugin is not published online
# 本地插件未在线上发布
local_plugin["status"] = PluginStatus.NOT_PUBLISHED
# Add uninstalled online plugins
# 添加未安装的在线插件
for online_plugin in online_plugins:
if not any(plugin["name"] == online_plugin["name"] for plugin in result):
clean: dict[str, str] = {
k: v for k, v in online_plugin.items() if v is not None
}
result.append(clean)
result.append(online_plugin)
return result
def manage_plugin(
plugin: dict,
plugins_dir: Path,
is_update: bool = False,
proxy: str | None = None,
plugin: dict, plugins_dir: Path, is_update: bool = False, proxy: str | None = None
) -> None:
"""Install or update a plugin
"""安装或更新插件
Args:
plugin (dict): Plugin info dict
plugins_dir (Path): Plugins directory
is_update (bool, optional): Whether this is an update operation. Defaults to False
proxy (str, optional): Proxy server address
plugin (dict): 插件信息字典
plugins_dir (Path): 插件目录
is_update (bool, optional): 是否为更新操作. 默认为 False
proxy (str, optional): 代理服务器地址
"""
plugin_name = plugin["name"]
repo_url = plugin["repo"]
# If updating and local path exists, use it directly
# 如果是更新且有本地路径,直接使用本地路径
if is_update and plugin.get("local_path"):
target_path = Path(plugin["local_path"])
else:
@@ -223,35 +207,31 @@ def manage_plugin(
backup_path = Path(f"{target_path}_backup") if is_update else None
# Check if plugin exists
# 检查插件是否存在
if is_update and not target_path.exists():
raise click.ClickException(
f"Plugin {plugin_name} is not installed and cannot be updated",
)
raise click.ClickException(f"插件 {plugin_name} 未安装,无法更新")
# Backup existing plugin
if is_update and backup_path is not None and backup_path.exists():
# 备份现有插件
if is_update and backup_path.exists():
shutil.rmtree(backup_path)
if is_update and backup_path is not None:
if is_update:
shutil.copytree(target_path, backup_path)
try:
click.echo(
f"{'Updating' if is_update else 'Downloading'} plugin {plugin_name} from {repo_url}...",
f"正在从 {repo_url} {'更新' if is_update else '下载'}插件 {plugin_name}..."
)
get_git_repo(repo_url, target_path, proxy)
# Update succeeded, delete backup
if is_update and backup_path is not None and backup_path.exists():
# 更新成功,删除备份
if is_update and backup_path.exists():
shutil.rmtree(backup_path)
click.echo(
f"Plugin {plugin_name} {'updated' if is_update else 'installed'} successfully",
)
click.echo(f"插件 {plugin_name} {'更新' if is_update else '安装'}成功")
except Exception as e:
if target_path.exists():
shutil.rmtree(target_path, ignore_errors=True)
if is_update and backup_path is not None and backup_path.exists():
if is_update and backup_path.exists():
shutil.move(backup_path, target_path)
raise click.ClickException(
f"Error {'updating' if is_update else 'installing'} plugin {plugin_name}: {e}",
) from e
f"{'更新' if is_update else '安装'}插件 {plugin_name} 时出错: {e}"
)

View File

@@ -1,4 +1,6 @@
"""Copied from astrbot.core.utils.version_comparator"""
"""
拷贝自 astrbot.core.utils.version_comparator
"""
import re
@@ -6,11 +8,11 @@ import re
class VersionComparator:
@staticmethod
def compare_version(v1: str, v2: str) -> int:
"""Compare version numbers according to Semver semantics. Supports version numbers with more than 3 digits and handles pre-release tags.
"""根据 Semver 语义版本规范来比较版本号的大小。支持不仅局限于 3 个数字的版本号,并处理预发布标签。
Reference: https://semver.org/
参考: https://semver.org/lang/zh-CN/
Returns 1 if v1 > v2, -1 if v1 < v2, 0 if v1 == v2.
返回 1 表示 v1 > v2,返回 -1 表示 v1 < v2,返回 0 表示 v1 = v2
"""
v1 = v1.lower().replace("v", "")
v2 = v2.lower().replace("v", "")
@@ -24,7 +26,7 @@ class VersionComparator:
return [], None
major_minor_patch = match.group(1).split(".")
prerelease = match.group(2)
# buildmetadata = match.group(3) # Build metadata is ignored in comparison
# buildmetadata = match.group(3) # 构建元数据在比较时忽略
parts = [int(x) for x in major_minor_patch]
prerelease = VersionComparator._split_prerelease(prerelease)
return parts, prerelease
@@ -32,7 +34,7 @@ class VersionComparator:
v1_parts, v1_prerelease = split_version(v1)
v2_parts, v2_prerelease = split_version(v2)
# Compare numeric parts
# 比较数字部分
length = max(len(v1_parts), len(v2_parts))
v1_parts.extend([0] * (length - len(v1_parts)))
v2_parts.extend([0] * (length - len(v2_parts)))
@@ -40,15 +42,15 @@ class VersionComparator:
for i in range(length):
if v1_parts[i] > v2_parts[i]:
return 1
if v1_parts[i] < v2_parts[i]:
elif v1_parts[i] < v2_parts[i]:
return -1
# Compare pre-release tags
# 比较预发布标签
if v1_prerelease is None and v2_prerelease is not None:
return 1 # Version without pre-release tag is higher than one with it
if v1_prerelease is not None and v2_prerelease is None:
return -1 # Version with pre-release tag is lower than one without it
if v1_prerelease is not None and v2_prerelease is not None:
return 1 # 没有预发布标签的版本高于有预发布标签的版本
elif v1_prerelease is not None and v2_prerelease is None:
return -1 # 有预发布标签的版本低于没有预发布标签的版本
elif v1_prerelease is not None and v2_prerelease is not None:
len_pre = max(len(v1_prerelease), len(v2_prerelease))
for i in range(len_pre):
p1 = v1_prerelease[i] if i < len(v1_prerelease) else None
@@ -56,22 +58,25 @@ class VersionComparator:
if p1 is None and p2 is not None:
return -1
if p1 is not None and p2 is None:
elif p1 is not None and p2 is None:
return 1
if isinstance(p1, int) and isinstance(p2, str):
elif isinstance(p1, int) and isinstance(p2, str):
return -1
if isinstance(p1, str) and isinstance(p2, int):
elif isinstance(p1, str) and isinstance(p2, int):
return 1
if (isinstance(p1, int) and isinstance(p2, int)) or (
isinstance(p1, str) and isinstance(p2, str)
):
elif isinstance(p1, int) and isinstance(p2, int):
if p1 > p2:
return 1
if p1 < p2:
elif p1 < p2:
return -1
return 0 # Pre-release tags are identical
elif isinstance(p1, str) and isinstance(p2, str):
if p1 > p2:
return 1
elif p1 < p2:
return -1
return 0 # 预发布标签完全相同
return 0 # Both numeric parts and pre-release tags are equal
return 0 # 数字部分和预发布标签都相同
@staticmethod
def _split_prerelease(prerelease):

View File

@@ -1,63 +1,23 @@
import os
from astrbot.core.config import AstrBotConfig
from astrbot.core.config.default import DB_PATH
from astrbot.core.db.sqlite import SQLiteDatabase
from astrbot.core.file_token_service import FileTokenService
from astrbot.core.utils.pip_installer import (
DependencyConflictError as DependencyConflictError,
)
from astrbot.core.utils.pip_installer import (
PipInstaller,
)
from astrbot.core.utils.requirements_utils import (
RequirementsPrecheckFailed as RequirementsPrecheckFailed,
)
from astrbot.core.utils.requirements_utils import (
find_missing_requirements as find_missing_requirements,
)
from astrbot.core.utils.requirements_utils import (
find_missing_requirements_or_raise as find_missing_requirements_or_raise,
)
from astrbot.core.utils.shared_preferences import SharedPreferences
from .log import LogManager, LogBroker # noqa
from astrbot.core.utils.t2i.renderer import HtmlRenderer
from astrbot.core.utils.shared_preferences import SharedPreferences
from astrbot.core.utils.pip_installer import PipInstaller
from astrbot.core.db.sqlite import SQLiteDatabase
from astrbot.core.config.default import DB_PATH
from astrbot.core.config import AstrBotConfig
from astrbot.core.file_token_service import FileTokenService
from .utils.astrbot_path import get_astrbot_data_path
from .log import LogBroker, LogManager
from .utils.astrbot_path import (
get_astrbot_config_path,
get_astrbot_data_path,
get_astrbot_knowledge_base_path,
get_astrbot_plugin_path,
get_astrbot_site_packages_path,
get_astrbot_skills_path,
get_astrbot_temp_path,
)
# 初始化数据存储文件夹
os.makedirs(get_astrbot_data_path(), exist_ok=True)
# Initialize required data directories eagerly so later agent/tool flows do not
# fail on missing paths when the runtime root resolves to a fresh location.
for required_dir in (
get_astrbot_data_path(),
get_astrbot_config_path(),
get_astrbot_plugin_path(),
get_astrbot_temp_path(),
get_astrbot_knowledge_base_path(),
get_astrbot_skills_path(),
get_astrbot_site_packages_path(),
):
os.makedirs(required_dir, exist_ok=True)
DEMO_MODE = os.getenv("DEMO_MODE", "False").strip().lower() in ("true", "1", "t")
DEMO_MODE = os.getenv("DEMO_MODE", False)
astrbot_config = AstrBotConfig()
t2i_base_url = astrbot_config.get("t2i_endpoint", "https://t2i.soulter.top/text2img")
html_renderer = HtmlRenderer(t2i_base_url)
logger = LogManager.GetLogger(log_name="astrbot")
LogManager.configure_logger(
logger,
astrbot_config,
override_level=os.getenv("ASTRBOT_LOG_LEVEL"),
)
LogManager.configure_trace_logger(astrbot_config)
db_helper = SQLiteDatabase(DB_PATH)
# 简单的偏好设置存储, 这里后续应该存储到数据库中, 一些部分可以存储到配置中
sp = SharedPreferences(db_helper=db_helper)
@@ -67,17 +27,3 @@ pip_installer = PipInstaller(
astrbot_config.get("pip_install_arg", ""),
astrbot_config.get("pypi_index_url", None),
)
__all__ = [
"DEMO_MODE",
"AstrBotConfig",
"LogBroker",
"LogManager",
"astrbot_config",
"db_helper",
"file_token_service",
"html_renderer",
"logger",
"pip_installer",
"sp",
"t2i_base_url",
]

View File

@@ -1,9 +1,8 @@
from dataclasses import dataclass
from typing import Any, Generic
from .hooks import BaseAgentRunHooks
from .run_context import TContext
from .tool import FunctionTool
from typing import Generic
from .run_context import TContext
from .hooks import BaseAgentRunHooks
@dataclass
@@ -12,4 +11,3 @@ class Agent(Generic[TContext]):
instructions: str | None = None
tools: list[str | FunctionTool] | None = None
run_hooks: BaseAgentRunHooks[TContext] | None = None
begin_dialogs: list[Any] | None = None

View File

@@ -1,310 +0,0 @@
from typing import TYPE_CHECKING, Protocol, runtime_checkable
from ...provider.modalities import (
log_context_sanitize_stats,
sanitize_contexts_by_modalities,
)
from ..message import Message
from .token_counter import EstimateTokenCounter, TokenCounter
if TYPE_CHECKING:
from astrbot import logger
else:
try:
from astrbot import logger
except ImportError:
import logging
logger = logging.getLogger("astrbot")
if TYPE_CHECKING:
from astrbot.core.provider.provider import Provider
from ..context.truncator import ContextTruncator
@runtime_checkable
class ContextCompressor(Protocol):
"""
Protocol for context compressors.
Provides an interface for compressing message lists.
"""
def should_compress(
self, messages: list[Message], current_tokens: int, max_tokens: int
) -> bool:
"""Check if compression is needed.
Args:
messages: The message list to evaluate.
current_tokens: The current token count.
max_tokens: The maximum allowed tokens for the model.
Returns:
True if compression is needed, False otherwise.
"""
...
async def __call__(self, messages: list[Message]) -> list[Message]:
"""Compress the message list.
Args:
messages: The original message list.
Returns:
The compressed message list.
"""
...
class TruncateByTurnsCompressor:
"""Truncate by turns compressor implementation.
Truncates the message list by removing older turns.
"""
def __init__(
self, truncate_turns: int = 1, compression_threshold: float = 0.82
) -> None:
"""Initialize the truncate by turns compressor.
Args:
truncate_turns: The number of turns to remove when truncating (default: 1).
compression_threshold: The compression trigger threshold (default: 0.82).
"""
self.truncate_turns = truncate_turns
self.compression_threshold = compression_threshold
def should_compress(
self, messages: list[Message], current_tokens: int, max_tokens: int
) -> bool:
"""Check if compression is needed.
Args:
messages: The message list to evaluate.
current_tokens: The current token count.
max_tokens: The maximum allowed tokens.
Returns:
True if compression is needed, False otherwise.
"""
if max_tokens <= 0 or current_tokens <= 0:
return False
usage_rate = current_tokens / max_tokens
return usage_rate > self.compression_threshold
async def __call__(self, messages: list[Message]) -> list[Message]:
truncator = ContextTruncator()
truncated_messages = truncator.truncate_by_dropping_oldest_turns(
messages,
drop_turns=self.truncate_turns,
)
return truncated_messages
def _extract_system_messages(messages: list[Message]) -> list[Message]:
"""Return the leading system messages from a message list."""
result = []
for msg in messages:
if msg.role == "system":
result.append(msg)
else:
break
return result
class LLMSummaryCompressor:
"""LLM-based summary compressor.
Uses LLM to summarize old conversation history while keeping a recent token
budget as exact context.
"""
TASK_CONTINUATION_INSTRUCTION = (
"If a task appears to be in progress, end the summary with the latest "
"known result and the concrete next step to continue the task."
)
def __init__(
self,
provider: "Provider",
keep_recent_ratio: float = 0.15,
instruction_text: str | None = None,
compression_threshold: float = 0.82,
token_counter: TokenCounter | None = None,
) -> None:
"""Initialize the LLM summary compressor.
Args:
provider: The LLM provider instance.
keep_recent_ratio: Ratio of current context tokens to keep as recent
exact context. Clamped to 0-0.3.
instruction_text: Custom instruction for summary generation.
compression_threshold: The compression trigger threshold (default: 0.82).
"""
self.provider = provider
self.keep_recent_ratio = min(max(float(keep_recent_ratio), 0.0), 0.3)
self.compression_threshold = compression_threshold
self.token_counter = token_counter or EstimateTokenCounter()
self.instruction_text = instruction_text or (
"Based on our full conversation history, produce a concise summary of key takeaways and/or project progress.\n"
"The primary goal of this summary is to enable seamless continuation of the work that follows.\n"
"1. Systematically cover all core topics discussed and the final conclusion/outcome for each; clearly highlight the latest primary focus.\n"
"2. If any tools were used, summarize tool usage (total call count) and extract the most valuable insights from tool outputs.\n"
"3. If any materials (files, documents, code, references) were read during the conversation that may be helpful for subsequent work, list each one with its scope and path.\n"
"4. If there was an initial user goal, state it first and describe the current progress/status.\n"
"5. Write the summary in the user's language.\n"
)
def should_compress(
self, messages: list[Message], current_tokens: int, max_tokens: int
) -> bool:
"""Check if compression is needed.
Args:
messages: The message list to evaluate.
current_tokens: The current token count.
max_tokens: The maximum allowed tokens.
Returns:
True if compression is needed, False otherwise.
"""
if max_tokens <= 0 or current_tokens <= 0:
return False
usage_rate = current_tokens / max_tokens
return usage_rate > self.compression_threshold
def _split_recent_rounds_by_token_ratio(
self,
rounds: list[list[Message]],
total_tokens: int,
) -> tuple[list[list[Message]], list[list[Message]]]:
"""Split rounds into summarised history and exact recent context.
The token budget is computed from the current context token count and
`keep_recent_ratio`, then floored by `int(...)`. Mapping that budget to
rounds is round-granular: a positive ratio always preserves the latest
whole round, even if that round itself exceeds the budget. Earlier
rounds are added only while the accumulated recent rounds stay within
the budget. No round is split.
"""
if not rounds or self.keep_recent_ratio <= 0 or total_tokens <= 0:
return rounds, []
budget = max(1, int(total_tokens * self.keep_recent_ratio))
used = 0
recent_start = len(rounds)
for idx in range(len(rounds) - 1, -1, -1):
round_tokens = self.token_counter.count_tokens(rounds[idx])
if used > 0 and used + round_tokens > budget:
break
used += round_tokens
recent_start = idx
return rounds[:recent_start], rounds[recent_start:]
async def __call__(self, messages: list[Message]) -> list[Message]:
"""Use LLM to generate a summary of the conversation history.
Uses round-based splitting to preserve user-assistant turn boundaries.
On LLM failure, returns the original messages unchanged (caller should
fall back to truncation).
"""
from .round_utils import split_into_rounds
rounds = split_into_rounds(messages)
message_rounds = [
[seg for seg in rnd if isinstance(seg, Message)] for rnd in rounds
]
total_tokens = self.token_counter.count_tokens(messages)
old_rounds, recent_rounds = self._split_recent_rounds_by_token_ratio(
message_rounds,
total_tokens,
)
# The latest user message is the active request. Keep its whole round
# exact even when the ratio is 0 or the ratio budget would otherwise
# summarize every round.
if messages and messages[-1].role == "user" and old_rounds:
latest_old_round = old_rounds[-1]
if latest_old_round and latest_old_round[-1] is messages[-1]:
old_rounds = old_rounds[:-1]
recent_rounds = [latest_old_round, *recent_rounds]
if not old_rounds:
if recent_rounds and messages and messages[-1].role == "user":
return messages
old_rounds = message_rounds
recent_rounds = []
summary_contexts = [msg for rnd in old_rounds for msg in rnd]
if not any(msg.role != "system" for msg in summary_contexts):
if recent_rounds and messages and messages[-1].role == "user":
return messages
old_rounds = message_rounds
recent_rounds = []
summary_contexts = [msg for rnd in old_rounds for msg in rnd]
if not any(msg.role != "system" for msg in summary_contexts):
return messages
if summary_contexts[-1].role != "assistant":
summary_contexts.append(
Message(
role="assistant",
content="Acknowledged.",
)
)
summary_contexts.append(
Message(
role="user",
content=(
"Generate a summary of our previous conversation history.\n"
f"<extra_instruction>\n{self.instruction_text}\n\n"
f"{self.TASK_CONTINUATION_INSTRUCTION}</extra_instruction>\n"
"Respond ONLY with the summary content, without any additional text or formatting."
),
)
)
sanitized_summary_contexts, sanitize_stats = sanitize_contexts_by_modalities(
summary_contexts,
self.provider.provider_config.get("modalities", None),
)
log_context_sanitize_stats(sanitize_stats)
# Generate summary
try:
response = await self.provider.text_chat(
contexts=sanitized_summary_contexts,
)
summary_content = (response.completion_text or "").strip()
except Exception as e:
logger.error(f"Failed to generate summary: {e}")
return messages
if not summary_content:
logger.warning("LLM context compression returned an empty summary.")
return messages
# Build result: system messages + summary pair + recent rounds
result = _extract_system_messages(messages)
result.append(
Message(
role="user",
content=f"Our previous history conversation summary: {summary_content}",
)
)
result.append(
Message(
role="assistant",
content="Acknowledged the summary of our previous conversation history.",
)
)
# Flatten recent rounds back to message list
for rnd in recent_rounds:
for seg in rnd:
if isinstance(seg, Message):
result.append(seg)
return result

View File

@@ -1,35 +0,0 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING
from .compressor import ContextCompressor
from .token_counter import TokenCounter
if TYPE_CHECKING:
from astrbot.core.provider.provider import Provider
@dataclass
class ContextConfig:
"""Context configuration class."""
max_context_tokens: int = 0
"""Maximum number of context tokens. <= 0 means no limit."""
enforce_max_turns: int = -1 # -1 means no limit
"""Maximum number of conversation turns to keep. -1 means no limit. Executed before compression."""
truncate_turns: int = 1
"""Number of conversation turns to discard at once when truncation is triggered.
Two processes will use this value:
1. Enforce max turns truncation.
2. Truncation by turns compression strategy.
"""
llm_compress_instruction: str | None = None
"""Instruction prompt for LLM-based compression."""
llm_compress_keep_recent_ratio: float = 0.15
"""Percent of current context tokens to keep as exact recent context during LLM-based compression."""
llm_compress_provider: "Provider | None" = None
"""LLM provider used for compression tasks. If None, truncation strategy is used."""
custom_token_counter: TokenCounter | None = None
"""Custom token counting method. If None, the default method is used."""
custom_compressor: ContextCompressor | None = None
"""Custom context compression method. If None, the default method is used."""

View File

@@ -1,132 +0,0 @@
from astrbot import logger
from astrbot.core.agent.message import Message
from .compressor import LLMSummaryCompressor, TruncateByTurnsCompressor
from .config import ContextConfig
from .token_counter import EstimateTokenCounter
from .truncator import ContextTruncator
class ContextManager:
"""Context compression manager."""
def __init__(
self,
config: ContextConfig,
) -> None:
"""Initialize the context manager.
There are two strategies to handle context limit reached:
1. Truncate by turns: remove older messages by turns.
2. LLM-based compression: use LLM to summarize old messages.
Args:
config: The context configuration.
"""
self.config = config
self.token_counter = config.custom_token_counter or EstimateTokenCounter()
self.truncator = ContextTruncator()
if config.custom_compressor:
self.compressor = config.custom_compressor
elif config.llm_compress_provider:
self.compressor = LLMSummaryCompressor(
provider=config.llm_compress_provider,
keep_recent_ratio=config.llm_compress_keep_recent_ratio,
instruction_text=config.llm_compress_instruction,
token_counter=self.token_counter,
)
else:
self.compressor = TruncateByTurnsCompressor(
truncate_turns=config.truncate_turns,
)
async def process(
self,
messages: list[Message],
trusted_token_usage: int = 0,
) -> list[Message]:
"""Process the messages.
Args:
messages: The original message list.
Returns:
The processed message list.
"""
try:
result = messages
# 1. 基于轮次的截断 (Enforce max turns)
if self.config.enforce_max_turns != -1:
result = self.truncator.truncate_by_turns(
result,
keep_most_recent_turns=self.config.enforce_max_turns,
drop_turns=self.config.truncate_turns,
)
# 2. 基于 token 的压缩
if self.config.max_context_tokens > 0:
total_tokens = self.token_counter.count_tokens(
result,
trusted_token_usage,
)
if self.compressor.should_compress(
result,
total_tokens,
self.config.max_context_tokens,
):
result = await self._run_compression(result, total_tokens)
return result
except Exception as e:
logger.error(f"Error during context processing: {e}", exc_info=True)
return messages
async def _run_compression(
self,
messages: list[Message],
prev_tokens: int,
) -> list[Message]:
"""Compress/truncate the messages.
Args:
messages: The original message list.
prev_tokens: The token count before compression.
Returns:
The compressed/truncated message list.
"""
logger.debug("Compress triggered, starting compression...")
messages = await self.compressor(messages)
# double check
tokens_after_summary = self.token_counter.count_tokens(messages)
# calculate compress rate
compress_rate = (tokens_after_summary / self.config.max_context_tokens) * 100
logger.info(
f"Compress completed."
f" {prev_tokens} -> {tokens_after_summary} tokens,"
f" compression rate: {compress_rate:.2f}%.",
)
# last check
if self.compressor.should_compress(
messages,
tokens_after_summary,
self.config.max_context_tokens,
):
logger.info(
"Context still exceeds max tokens after compression, applying halving truncation...",
)
# still need compress, truncate by half
messages = self.truncator.truncate_by_halving(messages)
return messages

View File

@@ -1,72 +0,0 @@
"""Round-based utilities shared by LTM compaction and LLMSummaryCompressor."""
import json
from collections.abc import Sequence
from typing import Any
from ..message import ContentPart, Message, ToolCall
RoundSegment = dict[str, Any] | Message
def _segment_role(seg: RoundSegment) -> str:
if isinstance(seg, Message):
return seg.role
return str(seg.get("role", "?"))
def split_into_rounds(
contexts: Sequence[RoundSegment],
) -> list[list[RoundSegment]]:
"""Split a flat contexts list into logical rounds.
A round begins at a ``user`` segment and includes all subsequent
``assistant`` / ``tool`` segments until the next ``user`` segment.
"""
rounds: list[list[RoundSegment]] = []
current: list[RoundSegment] = []
for seg in contexts:
if _segment_role(seg) == "user" and current:
rounds.append(current)
current = []
current.append(seg)
if current:
rounds.append(current)
return rounds
def _content_to_text(content: Any) -> str:
if isinstance(content, list):
normalized = [
part.model_dump_for_context() if isinstance(part, ContentPart) else part
for part in content
]
return json.dumps(normalized, ensure_ascii=False)
if isinstance(content, ContentPart):
return json.dumps(content.model_dump_for_context(), ensure_ascii=False)
return str(content or "")
def _segment_content(seg: RoundSegment) -> Any:
if isinstance(seg, Message):
if seg.content is not None:
return seg.content
if seg.tool_calls:
return [
tc.model_dump() if isinstance(tc, ToolCall) else tc
for tc in seg.tool_calls
]
return ""
return seg.get("content") or seg.get("tool_calls") or ""
def rounds_to_text(rounds: list[list[RoundSegment]]) -> str:
"""Render rounds into a plain-text string for LLM summarisation."""
lines: list[str] = []
for i, rnd in enumerate(rounds, 1):
lines.append(f"--- Round {i} ---")
for seg in rnd:
role = _segment_role(seg)
content = _content_to_text(_segment_content(seg))
lines.append(f"[{role}] {content}")
return "\n".join(lines)

View File

@@ -1,88 +0,0 @@
import json
from typing import Protocol, runtime_checkable
from astrbot.core.agent.message import (
AudioURLPart,
ImageURLPart,
Message,
TextPart,
ThinkPart,
)
@runtime_checkable
class TokenCounter(Protocol):
"""Protocol for token counters.
Provides an interface for counting tokens in message lists.
"""
def count_tokens(
self,
messages: list[Message],
trusted_token_usage: int = 0,
) -> int:
"""Count the total tokens in the message list.
Args:
messages: The message list.
trusted_token_usage: The total token usage that LLM API returned.
For some cases, this value is more accurate.
But some API does not return it, so the value defaults to 0.
Returns:
The total token count.
"""
...
# 图片/音频 token 开销估算值,参考 OpenAI vision pricing:
# low-res ~85 tokens, high-res ~170 per 512px tile, 通常几百到上千。
# 这里取一个保守中位数,宁可偏高触发压缩也不要偏低导致 API 报错。
IMAGE_TOKEN_ESTIMATE = 765
AUDIO_TOKEN_ESTIMATE = 500
class EstimateTokenCounter:
"""Estimate token counter implementation.
Provides a simple estimation of token count based on character types.
Supports multimodal content: images, audio, and thinking parts
are all counted so that the context compressor can trigger in time.
"""
def count_tokens(
self,
messages: list[Message],
trusted_token_usage: int = 0,
) -> int:
if trusted_token_usage > 0:
return trusted_token_usage
total = 0
for msg in messages:
content = msg.content
if isinstance(content, str):
total += self._estimate_tokens(content)
elif isinstance(content, list):
for part in content:
if isinstance(part, TextPart):
total += self._estimate_tokens(part.text)
elif isinstance(part, ThinkPart):
total += self._estimate_tokens(part.think)
elif isinstance(part, ImageURLPart):
total += IMAGE_TOKEN_ESTIMATE
elif isinstance(part, AudioURLPart):
total += AUDIO_TOKEN_ESTIMATE
if msg.tool_calls:
for tc in msg.tool_calls:
tc_str = json.dumps(tc if isinstance(tc, dict) else tc.model_dump())
total += self._estimate_tokens(tc_str)
return total
def _estimate_tokens(self, text: str) -> int:
chinese_count = len([c for c in text if "\u4e00" <= c <= "\u9fff"])
other_count = len(text) - chinese_count
return int(chinese_count * 0.6 + other_count * 0.3)

View File

@@ -1,234 +0,0 @@
from astrbot.core.agent.message import Message
class ContextTruncator:
"""Context truncator."""
def _has_tool_calls(self, message: Message) -> bool:
"""Check if a message contains tool calls."""
return (
message.role == "assistant"
and message.tool_calls is not None
and len(message.tool_calls) > 0
)
@staticmethod
def _split_system_rest(
messages: list[Message],
) -> tuple[list[Message], list[Message]]:
"""Split messages into system messages and the rest.
Returns:
tuple: (system_messages, non_system_messages)
"""
first_non_system = 0
for i, msg in enumerate(messages):
if msg.role != "system":
first_non_system = i
break
return messages[:first_non_system], messages[first_non_system:]
@staticmethod
def _ensure_user_message(
system_messages: list[Message],
truncated: list[Message],
original_messages: list[Message],
) -> list[Message]:
"""Ensure the result always contains a `user` message immediately after
system messages, as required by some LLM APIs.
Optimization strategy:
- If `truncated` already begins with a `user` message, return it as-is.
- If a `user` message exists later in `truncated`, move that message to
be the first non-system message while preserving the relative order of
the remaining truncated messages (without mutating the original list).
- Otherwise, fall back to the first `user` message from
`original_messages`.
This reduces unnecessary duplication and ensures the required ordering.
"""
if truncated and truncated[0].role == "user":
return system_messages + truncated
# If a user message exists inside the truncated list, promote it to the front.
index_in_truncated = next(
(i for i, m in enumerate(truncated) if m.role == "user"),
None,
)
if index_in_truncated is not None:
# Build a new truncated list that places the found user message first,
# preserving the order of the other messages and avoiding in-place mutation.
user_msg = truncated[index_in_truncated]
new_truncated = [
user_msg,
*truncated[:index_in_truncated],
*truncated[index_in_truncated + 1 :],
]
return system_messages + new_truncated
# Fallback: find the first user message in the original messages.
first_user = next((m for m in original_messages if m.role == "user"), None)
if first_user is None:
# No user messages at all; return system messages + whatever was truncated.
return system_messages + truncated
return [*system_messages, first_user, *truncated]
def fix_messages(self, messages: list[Message]) -> list[Message]:
"""Fix the message list to ensure the validity of tool call and tool response pairing.
This method ensures that:
1. Each `tool` message is preceded by an `assistant` message containing `tool_calls`.
2. Each `assistant` message containing `tool_calls` is followed by corresponding `
This is a requirement of the OpenAI Chat Completions API specification (Gemini enforces this strictly).
"""
if not messages:
return messages
fixed_messages: list[Message] = []
pending_assistant: Message | None = None
pending_tools: list[Message] = []
def flush_pending_if_valid() -> None:
nonlocal pending_assistant, pending_tools
if pending_assistant is not None and pending_tools:
fixed_messages.append(pending_assistant)
fixed_messages.extend(pending_tools)
pending_assistant = None
pending_tools = []
for msg in messages:
if msg.role == "tool":
# Only record tool responses when there is a pending assistant(tool_calls)
if pending_assistant is not None:
pending_tools.append(msg)
# Isolated tool messages without a preceding assistant(tool_calls) are ignored
continue
if self._has_tool_calls(msg):
# When encountering a new assistant(tool_calls), first process the old pending chain
flush_pending_if_valid()
pending_assistant = msg
continue
# Non-tool messages that do not contain tool_calls will break the pending chain.
# Flush any pending chain first, then append the current message normally.
flush_pending_if_valid()
fixed_messages.append(msg)
# Flush the last pending chain at the end,
# ensuring that any remaining valid assistant(tool_calls) and its tools are included in the final list.
flush_pending_if_valid()
return fixed_messages
def truncate_by_turns(
self,
messages: list[Message],
keep_most_recent_turns: int,
drop_turns: int = 1,
) -> list[Message]:
"""Turn-based truncation strategy, which drops the oldest turns while keeping the most recent N turns.
A turn consists of a user message and an assistant message.
This method ensures that the truncated context list conforms to OpenAI's context format.
Args:
messages: The original list of messages in the context.
keep_most_recent_turns: The number of most recent turns to keep. If set to -1, it means keeping all turns (no truncation).
drop_turns: The number of turns to drop from the beginning.
Returns:
The truncated list of messages.
"""
if keep_most_recent_turns == -1:
return messages
system_messages, non_system_messages = self._split_system_rest(messages)
if len(non_system_messages) // 2 <= keep_most_recent_turns:
return messages
num_to_keep = keep_most_recent_turns - drop_turns + 1
if num_to_keep <= 0:
truncated_contexts = []
else:
truncated_contexts = non_system_messages[-num_to_keep * 2 :]
# Find the first user message
index = next(
(i for i, item in enumerate(truncated_contexts) if item.role == "user"),
None,
)
if index is not None and index > 0:
truncated_contexts = truncated_contexts[index:]
result = self._ensure_user_message(
system_messages,
truncated_contexts,
messages,
)
return self.fix_messages(result)
def truncate_by_dropping_oldest_turns(
self,
messages: list[Message],
drop_turns: int = 1,
) -> list[Message]:
"""Drop the oldest N turns, regardless of the number of turns to keep."""
if drop_turns <= 0:
return messages
system_messages, non_system_messages = self._split_system_rest(messages)
if len(non_system_messages) // 2 <= drop_turns:
truncated_non_system = []
else:
truncated_non_system = non_system_messages[drop_turns * 2 :]
# Find the first user message
index = next(
(i for i, item in enumerate(truncated_non_system) if item.role == "user"),
None,
)
if index is not None:
truncated_non_system = truncated_non_system[index:]
result = self._ensure_user_message(
system_messages,
truncated_non_system,
messages,
)
return self.fix_messages(result)
def truncate_by_halving(
self,
messages: list[Message],
) -> list[Message]:
"""Halve the number of messages, keeping the most recent ones."""
if len(messages) <= 2:
return messages
system_messages, non_system_messages = self._split_system_rest(messages)
messages_to_delete = len(non_system_messages) // 2
if messages_to_delete == 0:
return messages
truncated_non_system = non_system_messages[messages_to_delete:]
# Find the first user message
index = next(
(i for i, item in enumerate(truncated_non_system) if item.role == "user"),
None,
)
if index is not None:
truncated_non_system = truncated_non_system[index:]
result = self._ensure_user_message(
system_messages,
truncated_non_system,
messages,
)
return self.fix_messages(result)

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