mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-02 02:30:16 +08:00
Compare commits
3 Commits
pr-5943-de
...
feat/trace
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eafb339281 | ||
|
|
f03dd87502 | ||
|
|
6e475074a4 |
@@ -17,6 +17,7 @@ ENV/
|
||||
.conda/
|
||||
dashboard/
|
||||
data/
|
||||
changelogs/
|
||||
tests/
|
||||
.ruff_cache/
|
||||
.astrbot
|
||||
|
||||
184
.env.example
184
.env.example
@@ -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
|
||||
|
||||
# ------------------------------------------
|
||||
# 国际化配置 / Internationalization Configuration
|
||||
# ------------------------------------------
|
||||
|
||||
# CLI 界面语言
|
||||
# CLI interface language
|
||||
# 可选值 Values: zh (中文), en (英文)
|
||||
# 默认 Default: zh (跟随系统 locale / follows system locale)
|
||||
# ASTRBOT_CLI_LANG=zh
|
||||
|
||||
# TUI 界面语言
|
||||
# TUI interface language
|
||||
# 可选值 Values: zh (中文), en (英文)
|
||||
# 默认 Default: zh
|
||||
# ASTRBOT_TUI_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 / 模板结束
|
||||
26
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
26
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
@@ -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: "感谢您填写我们的表单!"
|
||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -3,8 +3,8 @@
|
||||
|
||||
### 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 -->
|
||||
@@ -21,14 +21,7 @@
|
||||
<!--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.
|
||||
|
||||
92
.github/workflows/auto_release.yml
vendored
Normal file
92
.github/workflows/auto_release.yml
vendored
Normal file
@@ -0,0 +1,92 @@
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
name: Auto Release
|
||||
|
||||
jobs:
|
||||
build-and-publish-to-github-release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Dashboard Build
|
||||
run: |
|
||||
cd dashboard
|
||||
npm install
|
||||
npm run build
|
||||
echo "COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||
echo ${{ github.ref_name }} > dist/assets/version
|
||||
zip -r dist.zip dist
|
||||
|
||||
- name: Upload to Cloudflare R2
|
||||
env:
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
R2_BUCKET_NAME: "astrbot"
|
||||
R2_OBJECT_NAME: "astrbot-webui-latest.zip"
|
||||
VERSION_TAG: ${{ github.ref_name }}
|
||||
run: |
|
||||
echo "Installing rclone..."
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
|
||||
echo "Configuring rclone remote..."
|
||||
mkdir -p ~/.config/rclone
|
||||
cat <<EOF > ~/.config/rclone/rclone.conf
|
||||
[r2]
|
||||
type = s3
|
||||
provider = Cloudflare
|
||||
access_key_id = $R2_ACCESS_KEY_ID
|
||||
secret_access_key = $R2_SECRET_ACCESS_KEY
|
||||
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
|
||||
EOF
|
||||
|
||||
echo "Uploading dist.zip to R2 bucket: $R2_BUCKET_NAME/$R2_OBJECT_NAME"
|
||||
mv dashboard/dist.zip dashboard/$R2_OBJECT_NAME
|
||||
rclone copy dashboard/$R2_OBJECT_NAME r2:$R2_BUCKET_NAME --progress
|
||||
mv dashboard/$R2_OBJECT_NAME dashboard/astrbot-webui-${VERSION_TAG}.zip
|
||||
rclone copy dashboard/astrbot-webui-${VERSION_TAG}.zip r2:$R2_BUCKET_NAME --progress
|
||||
mv dashboard/astrbot-webui-${VERSION_TAG}.zip dashboard/dist.zip
|
||||
|
||||
- name: Fetch Changelog
|
||||
run: |
|
||||
echo "changelog=changelogs/${{github.ref_name}}.md" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
bodyFile: ${{ env.changelog }}
|
||||
artifacts: "dashboard/dist.zip"
|
||||
|
||||
build-and-publish-to-pypi:
|
||||
# 构建并发布到 PyPI
|
||||
runs-on: ubuntu-latest
|
||||
needs: build-and-publish-to-github-release
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install uv
|
||||
run: |
|
||||
python -m pip install uv
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
uv build
|
||||
|
||||
- name: Publish to PyPI
|
||||
env:
|
||||
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||
run: |
|
||||
uv publish
|
||||
43
.github/workflows/build-docs.yml
vendored
43
.github/workflows/build-docs.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest # 运行环境
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
- name: nodejs installation
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "18"
|
||||
- name: npm install
|
||||
run: npm add -D vitepress
|
||||
working-directory: './docs' # working-directory 指定 shell 命令运行目录
|
||||
- name: npm run build
|
||||
run: npm 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/
|
||||
2
.github/workflows/code-format.yml
vendored
2
.github/workflows/code-format.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
- 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
|
||||
|
||||
3
.github/workflows/coverage_test.yml
vendored
3
.github/workflows/coverage_test.yml
vendored
@@ -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@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
9
.github/workflows/dashboard_ci.yml
vendored
9
.github/workflows/dashboard_ci.yml
vendored
@@ -8,7 +8,6 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -17,7 +16,7 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24.13.0'
|
||||
node-version: 'latest'
|
||||
|
||||
- name: npm install, build
|
||||
run: |
|
||||
@@ -37,7 +36,7 @@ jobs:
|
||||
zip -r dist.zip dist
|
||||
|
||||
- name: Archive production artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: dist-without-markdown
|
||||
path: |
|
||||
@@ -46,11 +45,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"
|
||||
28
.github/workflows/docker-image.yml
vendored
28
.github/workflows/docker-image.yml
vendored
@@ -11,11 +11,11 @@ on:
|
||||
|
||||
jobs:
|
||||
build-nightly-image:
|
||||
if: github.repository == 'AstrBotDevs/AstrBot' && github.event_name == 'schedule'
|
||||
if: github.event_name == 'schedule'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
GHCR_OWNER: astrbotdevs
|
||||
GHCR_OWNER: soulter
|
||||
HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }}
|
||||
|
||||
steps:
|
||||
@@ -64,20 +64,20 @@ jobs:
|
||||
echo "build_date=$build_date" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set QEMU
|
||||
uses: docker/setup-qemu-action@v4.0.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4.0.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@v4.0.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.0.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ env.GHCR_OWNER }}
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and Push Nightly Image
|
||||
uses: docker/build-push-action@v7.0.0
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -109,11 +109,11 @@ jobs:
|
||||
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')))
|
||||
if: 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
|
||||
GHCR_OWNER: soulter
|
||||
HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }}
|
||||
|
||||
steps:
|
||||
@@ -163,27 +163,27 @@ jobs:
|
||||
cp -r dashboard/dist data/
|
||||
|
||||
- name: Set QEMU
|
||||
uses: docker/setup-qemu-action@v4.0.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4.0.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@v4.0.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.0.0
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ env.GHCR_OWNER }}
|
||||
password: ${{ secrets.GHCR_GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Release Image
|
||||
uses: docker/build-push-action@v7.0.0
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
54
.github/workflows/pr-title-check.yml
vendored
54
.github/workflows/pr-title-check.yml
vendored
@@ -1,54 +0,0 @@
|
||||
name: PR Title Check
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, edited, reopened, synchronize]
|
||||
|
||||
jobs:
|
||||
title-format:
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Validate PR title
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const title = (context.payload.pull_request.title || "").trim();
|
||||
// allow only:
|
||||
// feat: xxx
|
||||
// feat(scope): xxx
|
||||
const pattern = /^(feat)(\([a-z0-9-]+\))?:\s.+$/i;
|
||||
const isValid = pattern.test(title);
|
||||
const isSameRepo =
|
||||
context.payload.pull_request.head.repo.full_name === context.payload.repository.full_name;
|
||||
|
||||
if (!isValid) {
|
||||
if (isSameRepo) {
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
body: [
|
||||
"⚠️ PR title format check failed.",
|
||||
"Required formats:",
|
||||
"- `feat: xxx`",
|
||||
"- `feat(scope): xxx`",
|
||||
"Please update your PR title and push again."
|
||||
].join("\n")
|
||||
});
|
||||
} catch (e) {
|
||||
core.warning(`Failed to post PR title comment: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
core.warning("Fork PR: comment permission is restricted; skip posting review comment.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
core.setFailed("Invalid PR title. Expected format: feat: xxx or feat(scope): xxx.");
|
||||
}
|
||||
248
.github/workflows/release.yml
vendored
248
.github/workflows/release.yml
vendored
@@ -1,248 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: "Git ref to build (branch/tag/SHA)"
|
||||
required: false
|
||||
default: "master"
|
||||
tag:
|
||||
description: "Release tag to publish assets to (for example: v4.14.6)"
|
||||
required: false
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-dashboard:
|
||||
name: Build Dashboard
|
||||
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@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Resolve tag
|
||||
id: tag
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
elif [ -n "${{ inputs.tag }}" ]; then
|
||||
tag="${{ inputs.tag }}"
|
||||
else
|
||||
tag="$(git describe --tags --abbrev=0)"
|
||||
fi
|
||||
if [ -z "$tag" ]; then
|
||||
echo "Failed to resolve tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v5.0.0
|
||||
with:
|
||||
version: 10.28.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24.13.0'
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: dashboard/pnpm-lock.yaml
|
||||
|
||||
- name: Build dashboard dist
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm --dir dashboard install --frozen-lockfile
|
||||
pnpm --dir dashboard run build
|
||||
echo "${{ steps.tag.outputs.tag }}" > dashboard/dist/assets/version
|
||||
cd dashboard
|
||||
zip -r "AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip" dist
|
||||
|
||||
- name: Upload dashboard artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: Dashboard-${{ steps.tag.outputs.tag }}
|
||||
if-no-files-found: error
|
||||
path: dashboard/AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip
|
||||
|
||||
- name: Upload dashboard package to Cloudflare R2
|
||||
if: ${{ env.R2_ACCOUNT_ID != '' && env.R2_ACCESS_KEY_ID != '' && env.R2_SECRET_ACCESS_KEY != '' }}
|
||||
env:
|
||||
R2_BUCKET_NAME: "astrbot"
|
||||
R2_OBJECT_NAME: "astrbot-webui-latest.zip"
|
||||
VERSION_TAG: ${{ steps.tag.outputs.tag }}
|
||||
shell: bash
|
||||
run: |
|
||||
curl https://rclone.org/install.sh | sudo bash
|
||||
|
||||
mkdir -p ~/.config/rclone
|
||||
cat <<EOF > ~/.config/rclone/rclone.conf
|
||||
[r2]
|
||||
type = s3
|
||||
provider = Cloudflare
|
||||
access_key_id = $R2_ACCESS_KEY_ID
|
||||
secret_access_key = $R2_SECRET_ACCESS_KEY
|
||||
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
|
||||
EOF
|
||||
|
||||
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/${R2_OBJECT_NAME}"
|
||||
rclone copy "dashboard/${R2_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
|
||||
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/astrbot-webui-${VERSION_TAG}.zip"
|
||||
rclone copy "dashboard/astrbot-webui-${VERSION_TAG}.zip" "r2:${R2_BUCKET_NAME}" --progress
|
||||
|
||||
publish-release:
|
||||
name: Publish GitHub Release
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- build-dashboard
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Resolve tag
|
||||
id: tag
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
elif [ -n "${{ inputs.tag }}" ]; then
|
||||
tag="${{ inputs.tag }}"
|
||||
else
|
||||
tag="$(git describe --tags --abbrev=0)"
|
||||
fi
|
||||
if [ -z "$tag" ]; then
|
||||
echo "Failed to resolve tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download dashboard artifact
|
||||
uses: actions/download-artifact@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@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.ref || github.ref }}
|
||||
|
||||
- name: Resolve tag
|
||||
id: tag
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ]; then
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
elif [ -n "${{ inputs.tag }}" ]; then
|
||||
tag="${{ inputs.tag }}"
|
||||
else
|
||||
tag="$(git describe --tags --abbrev=0)"
|
||||
fi
|
||||
if [ -z "$tag" ]; then
|
||||
echo "Failed to resolve tag." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download dashboard artifact
|
||||
uses: actions/download-artifact@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
|
||||
15
.github/workflows/smoke_test.yml
vendored
15
.github/workflows/smoke_test.yml
vendored
@@ -5,9 +5,9 @@ on:
|
||||
branches:
|
||||
- master
|
||||
paths-ignore:
|
||||
- "README*.md"
|
||||
- "changelogs/**"
|
||||
- "dashboard/**"
|
||||
- 'README*.md'
|
||||
- 'changelogs/**'
|
||||
- 'dashboard/**'
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
name: Run smoke tests
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -26,8 +26,8 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install UV package manager
|
||||
run: |
|
||||
pip install uv
|
||||
@@ -40,9 +40,6 @@ jobs:
|
||||
- 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..."
|
||||
|
||||
1
.github/workflows/stale.yml
vendored
1
.github/workflows/stale.yml
vendored
@@ -18,7 +18,6 @@ on:
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
if: github.repository == 'AstrBotDevs/AstrBot'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
69
.github/workflows/sync-wiki.yml
vendored
69
.github/workflows/sync-wiki.yml
vendored
@@ -1,69 +0,0 @@
|
||||
name: sync 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@v6
|
||||
|
||||
- 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
|
||||
37
.github/workflows/unit_tests.yml
vendored
37
.github/workflows/unit_tests.yml
vendored
@@ -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@v6
|
||||
|
||||
- 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
|
||||
28
.gitignore
vendored
28
.gitignore
vendored
@@ -32,13 +32,10 @@ tests/astrbot_plugin_openai
|
||||
# Dashboard
|
||||
dashboard/node_modules/
|
||||
dashboard/dist/
|
||||
.pnpm-store/
|
||||
package-lock.json
|
||||
package.json
|
||||
yarn.lock
|
||||
|
||||
# Bundled dashboard dist (generated by hatch_build.py during pip wheel build)
|
||||
astrbot/dashboard/dist/
|
||||
|
||||
# Operating System
|
||||
**/.DS_Store
|
||||
.DS_Store
|
||||
@@ -56,25 +53,4 @@ IFLOW.md
|
||||
|
||||
# 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
|
||||
GenieData/
|
||||
@@ -6,20 +6,20 @@ ci:
|
||||
autoupdate_schedule: weekly
|
||||
autoupdate_commit_msg: ":balloon: pre-commit autoupdate"
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.7
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
types_or: [python, pyi]
|
||||
args: [--fix]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
types_or: [python, pyi]
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.21.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py312-plus]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
types_or: [ python, pyi ]
|
||||
args: [ --fix ]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
types_or: [ python, pyi ]
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.21.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py310-plus]
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.12
|
||||
3.10
|
||||
36
AGENTS.md
36
AGENTS.md
@@ -3,10 +3,8 @@
|
||||
### Core
|
||||
|
||||
```
|
||||
uv tool install -e . --force
|
||||
astrbot init
|
||||
astrbot run # start the bot
|
||||
astrbot run --backend-only # start the backend only
|
||||
uv sync
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
Exposed an API server on `http://localhost:6185` by default.
|
||||
@@ -15,8 +13,8 @@ Exposed an API server on `http://localhost:6185` by default.
|
||||
|
||||
```
|
||||
cd dashboard
|
||||
bun install # First time only.
|
||||
bun dev
|
||||
pnpm install # First time only. Use npm install -g pnpm if pnpm is not installed.
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Runs on `http://localhost:3000` by default.
|
||||
@@ -28,32 +26,8 @@ Runs on `http://localhost:3000` by default.
|
||||
3. After finishing, use `ruff format .` and `ruff check .` to format and check the code.
|
||||
4. When committing, ensure to use conventional commits messages, such as `feat: add new agent for data analysis` or `fix: resolve bug in provider manager`.
|
||||
5. Use English for all new comments.
|
||||
6. For path handling, use `pathlib.Path` instead of string paths, and use `astrbot.core.utils.path_utils` to get the AstrBot data and temp directory.
|
||||
7. Use Python 3.12+ type hinting syntax (e.g., `list[str]` over `List[str]`, `int | None` over `Optional[int]`). Avoid using `Any` and `cast()` - use proper TypedDict, dataclass, or Protocol instead. When encountering dict access issues (e.g., `msg.get("key")` where ty infers wrong type), 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)
|
||||
```
|
||||
8. When introducing new environment variables:
|
||||
- Use the `ASTRBOT_` prefix for naming (e.g., `ASTRBOT_ENABLE_FEATURE`).
|
||||
- Add the variable and description to `.env.example`.
|
||||
- Update `astrbot/cli/commands/cmd_run.py`:
|
||||
- Add to the module docstring under "Environment Variables Used in Project".
|
||||
- Add to the `keys_to_print` list in the `run` function for debug output.
|
||||
9. To check all available CLI commands and their usage recursively, run `astrbot help --all`.
|
||||
10. uv sync --group dev && uv run pytest --cov=astrbot tests/
|
||||
|
||||
## PR instructions
|
||||
|
||||
1. Title format: use conventional commit messages
|
||||
2. Use English to write PR title and descriptions.
|
||||
2. Use English to write PR title and descriptions.
|
||||
|
||||
180
CLAUDE.md
180
CLAUDE.md
@@ -1,180 +0,0 @@
|
||||
# AstrBot - Claude Code Guidelines
|
||||
|
||||
AstrBot is an open-source, all-in-one Agentic personal and group chat assistant supporting multiple IM platforms (QQ, Telegram, Discord, etc.) and LLM providers.
|
||||
|
||||
## Project Overview
|
||||
|
||||
- **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)
|
||||
|
||||
## Development Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
uv tool install -e . --force
|
||||
|
||||
# Initialize AstrBot
|
||||
astrbot init
|
||||
|
||||
# Run development
|
||||
astrbot run
|
||||
|
||||
# Backend only (no WebUI)
|
||||
astrbot run --backend-only
|
||||
|
||||
# Dashboard frontend
|
||||
cd dashboard && bun dev
|
||||
|
||||
# Run tests
|
||||
uv sync --group dev && uv run pytest --cov=astrbot tests/
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### Python
|
||||
|
||||
1. **Type hints required** - Use Python 3.12+ syntax:
|
||||
- `list[str]` not `List[str]`
|
||||
- `int | None` not `Optional[int]`
|
||||
- Avoid `Any` when possible
|
||||
|
||||
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
|
||||
|
||||
## 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
|
||||
|
||||
## Testing
|
||||
|
||||
1. Tests go in `tests/` directory
|
||||
2. Use `pytest` with `pytest-asyncio`
|
||||
3. Coverage target: `uv run pytest --cov=astrbot tests/`
|
||||
4. Test files: `test_*.py` or `*_test.py`
|
||||
|
||||
## 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
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## 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.
|
||||
@@ -46,32 +46,6 @@ 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
|
||||
|
||||
@@ -114,29 +88,3 @@ We use Ruff as our code formatter and static analysis tool. Before submitting yo
|
||||
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
|
||||
```
|
||||
|
||||
16
Dockerfile
16
Dockerfile
@@ -1,4 +1,4 @@
|
||||
FROM python:3.12-slim
|
||||
FROM python:3.11-slim
|
||||
WORKDIR /AstrBot
|
||||
|
||||
COPY . /AstrBot/
|
||||
@@ -15,17 +15,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
gnupg \
|
||||
git \
|
||||
&& 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/*
|
||||
|
||||
RUN apt-get update && apt-get install -y curl gnupg \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
RUN python -m pip install uv \
|
||||
&& 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
|
||||
&& echo "3.11" > .python-version
|
||||
RUN uv pip install -r requirements.txt --no-cache-dir --system
|
||||
RUN uv pip install socksio uv pilk --no-cache-dir --system
|
||||
|
||||
EXPOSE 6185
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
## Welcome to AstrBot
|
||||
|
||||
🌟 Thank you for using AstrBot!
|
||||
|
||||
AstrBot is an Agentic AI assistant for personal and group chats, with support for multiple IM platforms and a wide range of built-in features. We hope it brings you an efficient and enjoyable experience. ❤️
|
||||
|
||||
Important notice:
|
||||
|
||||
AstrBot is a **free and open-source software project** protected by the AGPLv3 license. You can find the full source code and related resources on our [**official website**](https://astrbot.app) and [**GitHub**](https://github.com/astrbotdevs/astrbot).
|
||||
As of now, AstrBot has **no commercial services of any kind**, and the official team **will never charge users any fees** under any name.
|
||||
|
||||
If anyone asks you to pay while using AstrBot, **you are likely being scammed**. Please request a refund immediately and report it to us by email.
|
||||
|
||||
📮 Official email: [community@astrbot.app](mailto:community@astrbot.app)
|
||||
@@ -1,14 +0,0 @@
|
||||
## 欢迎使用 AstrBot
|
||||
|
||||
🌟 感谢您使用 AstrBot!
|
||||
|
||||
AstrBot 是一款可接入多种 IM 平台的 Agentic AI 个人 / 群聊助手,内置多项强大功能,希望能为您带来高效、愉快的使用体验。❤️
|
||||
|
||||
我们想特别说明:
|
||||
|
||||
AstrBot 是受 AGPLv3 开源协议保护的**免费开源软件项目**,您可以在[**官方网站**](https://astrbot.app)、[**GitHub**](https://github.com/astrbotdevs/astrbot) 上找到 AstrBot 的全部源代码及相关资源。
|
||||
截至目前,AstrBot 项目**未开展任何形式的商业化服务**,官方**不会以任何名义向用户收取费用**。
|
||||
|
||||
如果您在使用 AstrBot 的过程中被要求付费,**表明您已经遭遇诈骗行为**。请立即向相关方申请退款,并及时通过邮件向我们反馈。
|
||||
|
||||
📮 官方邮箱:[community@astrbot.app](mailto:community@astrbot.app)
|
||||
41
Makefile
41
Makefile
@@ -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
|
||||
337
README.md
337
README.md
@@ -2,9 +2,10 @@
|
||||
|
||||
<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_en.md">English</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
||||
|
||||
@@ -19,276 +20,222 @@
|
||||
<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%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTk0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></a>
|
||||
<a href="https://zread.ai/AstrBotDevs/AstrBot" target="_blank"><img src="https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></a>
|
||||
<a href="https://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://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/">Home</a> |
|
||||
<a href="https://astrbot.app/">Docs</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://astrbot.featurebase.app/roadmap">路线图</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
|
||||
</div>
|
||||
|
||||
AstrBot is an open-source, all-in-one Agentic personal and group chat assistant that can be deployed on dozens of mainstream instant messaging platforms such as QQ, Telegram, WeCom, Lark, DingTalk, Slack, and more. It also features a built-in lightweight ChatUI similar to OpenWebUI, creating a reliable and scalable conversational AI infrastructure for individuals, developers, and teams. Whether it's a personal AI companion, smart customer service, automated assistant, or enterprise knowledge base, AstrBot enables you to quickly build AI applications within the workflow of your instant messaging platforms.
|
||||
AstrBot 是一个开源的一站式 Agent 聊天机器人平台,可接入主流即时通讯软件,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建生产可用的 AI 应用。
|
||||
|
||||

|
||||

|
||||
|
||||
## Key Features
|
||||
## 主要功能
|
||||
|
||||
1. 💯 Free & Open Source.
|
||||
2. ✨ Large Language Model (LLM) dialogue, Multimodal, Agent, MCP, Skills, Knowledge Base, Persona settings, automatic dialogue compression.
|
||||
3. 🤖 Supports integration with agent platforms such as Dify, Alibaba Bailian, Coze, etc.
|
||||
4. 🌐 Multi-platform support: QQ, WeCom, Lark, DingTalk, WeChat Official Account, Telegram, Slack, and [more](#supported-message-platforms).
|
||||
5. 📦 Plugin extension: 1000+ plugins available for one-click installation.
|
||||
6. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html): Isolated environment for safely executing any code, calling Shell commands, and reusing session-level resources.
|
||||
7. 💻 WebUI support.
|
||||
8. 🌈 Web ChatUI support: Built-in proxy sandbox, web search, etc. within ChatUI.
|
||||
9. 🌐 Internationalization (i18n) support.
|
||||
1. 💯 免费 & 开源。
|
||||
1. ✨ AI 大模型对话,多模态,Agent,MCP,Skills,知识库,人格设定,自动压缩对话。
|
||||
2. 🤖 支持接入 Dify、阿里云百炼、Coze 等智能体平台。
|
||||
2. 🌐 多平台,支持 QQ、企业微信、飞书、钉钉、微信公众号、Telegram、Slack 以及[更多](#支持的消息平台)。
|
||||
3. 📦 插件扩展,已有近 800 个插件可一键安装。
|
||||
5. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) 隔离化环境,安全地执行任何代码、调用 Shell、会话级资源复用。
|
||||
6. 💻 WebUI 支持。
|
||||
7. 🌈 Web ChatUI 支持,ChatUI 内置代理沙盒、网页搜索等。
|
||||
8. 🌐 国际化(i18n)支持。
|
||||
|
||||
<br>
|
||||
## 快速开始
|
||||
|
||||
<table align="center">
|
||||
<tr align="center">
|
||||
<th>💙 Roleplay & 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 部署(推荐 🥳)
|
||||
|
||||
## Quick Start
|
||||
推荐使用 Docker / Docker Compose 方式部署 AstrBot。
|
||||
|
||||
### One-Click Deployment
|
||||
请参阅官方文档 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) 。
|
||||
|
||||
For users who want to experience AstrBot quickly, are familiar with the command line, and can install the `uv` environment themselves, we recommend using `uv` for one-click deployment ⚡️.
|
||||
#### uv 部署
|
||||
|
||||
```bash
|
||||
uv tool install astrbot
|
||||
astrbot init # Execute this command only for the first time to initialize the environment
|
||||
astrbot run # astrbot run --backend-only starts only the backend service
|
||||
|
||||
# Install development version (more fixes and new features, but less stable; suitable for developers)
|
||||
uv tool install git+https://github.com/AstrBotDevs/AstrBot@dev
|
||||
uvx astrbot
|
||||
```
|
||||
|
||||
> Requires [uv](https://docs.astral.sh/uv/) installed.
|
||||
#### 宝塔面板部署
|
||||
|
||||
> [!NOTE]
|
||||
> For macOS users: Due to macOS security checks, the first execution of the `astrbot` command may take a longer time (about 10-20 seconds).
|
||||
AstrBot 与宝塔面板合作,已上架至宝塔面板。
|
||||
|
||||
Update `astrbot`:
|
||||
请参阅官方文档 [宝塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.html) 。
|
||||
|
||||
```bash
|
||||
uv tool upgrade astrbot
|
||||
```
|
||||
#### 1Panel 部署
|
||||
|
||||
### Docker Deployment
|
||||
AstrBot 已由 1Panel 官方上架至 1Panel 面板。
|
||||
|
||||
For users familiar with containers who prefer a more stable deployment suitable for production environments, we recommend using Docker / Docker Compose to deploy AstrBot.
|
||||
请参阅官方文档 [1Panel 部署](https://astrbot.app/deploy/astrbot/1panel.html) 。
|
||||
|
||||
Please refer to the official documentation [Deploy AstrBot with Docker](https://astrbot.app/deploy/astrbot/docker.html).
|
||||
#### 在 雨云 上部署
|
||||
|
||||
### Deploy on RainYun
|
||||
|
||||
For users who want to deploy AstrBot with one click and do not want to manage servers themselves, we recommend RainYun's one-click cloud deployment service ☁️:
|
||||
AstrBot 已由雨云官方上架至云应用平台,可一键部署。
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||
|
||||
### Desktop Client Deployment
|
||||
#### 在 Replit 上部署
|
||||
|
||||
For users who wish to use AstrBot on the desktop with ChatUI as the main interface, we recommend using the AstrBot App.
|
||||
|
||||
Go to [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop) to download and install; this method is intended for desktop use and is not recommended for server scenarios.
|
||||
|
||||
### Launcher Deployment
|
||||
|
||||
Also for desktop, users who want quick deployment and isolated environments for multiple instances can use the AstrBot Launcher.
|
||||
|
||||
Go to [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) to download and install.
|
||||
|
||||
### Deploy on Replit
|
||||
|
||||
Replit deployment is maintained by the community, suitable for online demos and lightweight trials.
|
||||
社区贡献的部署方式。
|
||||
|
||||
[](https://repl.it/github/AstrBotDevs/AstrBot)
|
||||
|
||||
### AUR
|
||||
#### Windows 一键安装器部署
|
||||
|
||||
The AUR method is for Arch Linux users who wish to install AstrBot via the system package manager.
|
||||
请参阅官方文档 [使用 Windows 一键安装器部署 AstrBot](https://astrbot.app/deploy/astrbot/windows.html) 。
|
||||
|
||||
Execute the following command in the terminal to install the `astrbot-git` package. You can start using it after installation completes.
|
||||
#### CasaOS 部署
|
||||
|
||||
社区贡献的部署方式。
|
||||
|
||||
请参阅官方文档 [CasaOS 部署](https://astrbot.app/deploy/astrbot/casaos.html) 。
|
||||
|
||||
#### 手动部署
|
||||
|
||||
首先安装 uv:
|
||||
|
||||
```bash
|
||||
yay -S astrbot-git
|
||||
pip install uv
|
||||
```
|
||||
|
||||
**More Deployment Methods**
|
||||
通过 Git Clone 安装 AstrBot:
|
||||
|
||||
If you need panel-based or highly customized deployment, you can refer to [BT Panel](https://astrbot.app/deploy/astrbot/btpanel.html) (BT Panel App Store), [1Panel](https://astrbot.app/deploy/astrbot/1panel.html) (1Panel App Store), [CasaOS](https://astrbot.app/deploy/astrbot/casaos.html) (NAS / Home Server visual deployment), and [Manual Deployment](https://astrbot.app/deploy/astrbot/cli.html) (Full custom installation based on source code and `uv`).
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
## Supported Message Platforms
|
||||
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
|
||||
|
||||
Connect AstrBot to your favorite chat platforms.
|
||||
## 支持的消息平台
|
||||
|
||||
| Platform | Maintainer |
|
||||
|---------|---------------|
|
||||
| **QQ** | Official |
|
||||
| **OneBot v11** | Official |
|
||||
| **Telegram** | Official |
|
||||
| **WeCom App & Bot** | Official |
|
||||
| **WeChat Customer Service & Official Account** | Official |
|
||||
| **Lark (Feishu)** | Official |
|
||||
| **DingTalk** | Official |
|
||||
| **Slack** | Official |
|
||||
| **Discord** | Official |
|
||||
| **LINE** | Official |
|
||||
| **Satori** | Official |
|
||||
| **Misskey** | Official |
|
||||
| **Whatsapp (Coming Soon)** | Official |
|
||||
| [**Matrix**](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Community |
|
||||
| [**KOOK**](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | Community |
|
||||
| [**VoceChat**](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Community |
|
||||
**官方维护**
|
||||
|
||||
## Supported Model Providers
|
||||
- QQ (官方平台 & OneBot)
|
||||
- Telegram
|
||||
- 企微应用 & 企微智能机器人
|
||||
- 微信客服 & 微信公众号
|
||||
- 飞书
|
||||
- 钉钉
|
||||
- Slack
|
||||
- Discord
|
||||
- Satori
|
||||
- Misskey
|
||||
- Whatsapp (将支持)
|
||||
- LINE (将支持)
|
||||
|
||||
| Provider | Type |
|
||||
|---------|---------------|
|
||||
| Custom | Any OpenAI API compatible service |
|
||||
| OpenAI | LLM |
|
||||
| Anthropic | LLM |
|
||||
| Google Gemini | LLM |
|
||||
| Moonshot AI | LLM |
|
||||
| Zhipu AI | LLM |
|
||||
| DeepSeek | LLM |
|
||||
| Ollama (Local) | LLM |
|
||||
| LM Studio (Local) | LLM |
|
||||
| [AIHubMix](https://aihubmix.com/?aff=4bfH) | LLM (API Gateway, supports all models) |
|
||||
| [Compshare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | LLM (API Gateway, supports all models) |
|
||||
| [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | LLM (API Gateway, supports all models) |
|
||||
| [PPIO](https://ppio.com/user/register?invited_by=AIOONE) | LLM (API Gateway, supports all models) |
|
||||
| [302.AI](https://share.302.ai/rr1M3l) | LLM (API Gateway, supports all models)|
|
||||
| [TokenPony](https://www.tokenpony.cn/3YPyf) | LLM (API Gateway, supports all models)|
|
||||
| ModelScope | LLM |
|
||||
| OneAPI | LLM |
|
||||
| Dify | LLMOps Platform |
|
||||
| Alibaba Bailian | LLMOps Platform |
|
||||
| Coze | LLMOps Platform |
|
||||
| OpenAI Whisper | Speech-to-Text |
|
||||
| SenseVoice | Speech-to-Text |
|
||||
| OpenAI TTS | Text-to-Speech |
|
||||
| Gemini TTS | Text-to-Speech |
|
||||
| GPT-Sovits-Inference | Text-to-Speech |
|
||||
| GPT-Sovits | Text-to-Speech |
|
||||
| FishAudio | Text-to-Speech |
|
||||
| Edge TTS | Text-to-Speech |
|
||||
| Alibaba Bailian TTS | Text-to-Speech |
|
||||
| Azure TTS | Text-to-Speech |
|
||||
| Minimax TTS | Text-to-Speech |
|
||||
| Volcano Engine TTS | Text-to-Speech |
|
||||
**社区维护**
|
||||
|
||||
## ❤️ Sponsors
|
||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
||||
|
||||
<p align="center">
|
||||
<img alt="sponsors" src="https://sponsors.astrbot.app/?v=1">
|
||||
</p>
|
||||
## 支持的模型服务
|
||||
|
||||
**大模型服务**
|
||||
|
||||
## ❤️ Contribution
|
||||
- OpenAI 及兼容服务
|
||||
- Anthropic
|
||||
- Google Gemini
|
||||
- Moonshot AI
|
||||
- 智谱 AI
|
||||
- DeepSeek
|
||||
- Ollama (本地部署)
|
||||
- LM Studio (本地部署)
|
||||
- [优云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
||||
- [302.AI](https://share.302.ai/rr1M3l)
|
||||
- [小马算力](https://www.tokenpony.cn/3YPyf)
|
||||
- [硅基流动](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot)
|
||||
- [PPIO 派欧云](https://ppio.com/user/register?invited_by=AIOONE)
|
||||
- ModelScope
|
||||
- OneAPI
|
||||
|
||||
Welcome any Issues/Pull Requests! Just submit your changes to this project :)
|
||||
**LLMOps 平台**
|
||||
|
||||
### How to Contribute
|
||||
- Dify
|
||||
- 阿里云百炼应用
|
||||
- Coze
|
||||
|
||||
You can contribute by viewing issues or helping to review PRs (Pull Requests). Any issues or PRs are welcome to promote community contribution. Of course, these are just suggestions; you can contribute in any way. For new feature additions, please discuss via Issue first.
|
||||
It is recommended to merge functional PRs into the `dev` branch, which will be merged into the main branch and released as a new version after testing.
|
||||
To reduce conflicts, we suggest:
|
||||
1. Create your working branch based on the `dev` branch, avoid working directly on the `main` branch.
|
||||
2. When submitting a PR, select the `dev` branch as the target.
|
||||
3. Regularly sync the `dev` branch to your local environment; use `git pull` frequently.
|
||||
**语音转文本服务**
|
||||
|
||||
### Development Environment
|
||||
- OpenAI Whisper
|
||||
- SenseVoice
|
||||
|
||||
AstrBot uses `ruff` for code formatting and checking.
|
||||
**文本转语音服务**
|
||||
|
||||
- OpenAI TTS
|
||||
- Gemini TTS
|
||||
- GPT-Sovits-Inference
|
||||
- GPT-Sovits
|
||||
- FishAudio
|
||||
- Edge TTS
|
||||
- 阿里云百炼 TTS
|
||||
- Azure TTS
|
||||
- Minimax TTS
|
||||
- 火山引擎 TTS
|
||||
|
||||
## ❤️ 贡献
|
||||
|
||||
欢迎任何 Issues/Pull Requests!只需要将你的更改提交到此项目 :)
|
||||
|
||||
### 如何贡献
|
||||
|
||||
你可以通过查看问题或帮助审核 PR(拉取请求)来贡献。任何问题或 PR 都欢迎参与,以促进社区贡献。当然,这些只是建议,你可以以任何方式进行贡献。对于新功能的添加,请先通过 Issue 讨论。
|
||||
|
||||
### 开发环境
|
||||
|
||||
AstrBot 使用 `ruff` 进行代码格式化和检查。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot
|
||||
git switch dev # Switch to dev branch
|
||||
pip install pre-commit # or uv tool install pre-commit
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
We recommend using `uv` for local installation and testing:
|
||||
## 🌍 社区
|
||||
|
||||
```bash
|
||||
uv tool install -e . --force
|
||||
astrbot init
|
||||
astrbot run
|
||||
```
|
||||
### QQ 群组
|
||||
|
||||
Frontend Debugging:
|
||||
- 1 群:322154837
|
||||
- 3 群:630166526
|
||||
- 5 群:822130018
|
||||
- 6 群:753075035
|
||||
- 7 群:743746109
|
||||
- 8 群:1030353265
|
||||
- 开发者群:975206796
|
||||
|
||||
```bash
|
||||
astrbot run --backend-only
|
||||
cd dashboard
|
||||
bun install # or pnpm, etc.
|
||||
bun dev
|
||||
```
|
||||
### Telegram 群组
|
||||
|
||||
### QQ Groups
|
||||
<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>
|
||||
|
||||
- Group 9: 1076659624 (New)
|
||||
- Group 10: 1078079676 (New)
|
||||
- Group 1: 322154837
|
||||
- Group 3: 630166526
|
||||
- Group 5: 822130018
|
||||
- Group 6: 753075035
|
||||
- Group 7: 743746109
|
||||
- Group 8: 1030353265
|
||||
- Developer Group (Casual): 975206796
|
||||
- Developer Group (Official): 1039761811
|
||||
### Discord 群组
|
||||
|
||||
### Discord Channel
|
||||
|
||||
- [Discord](https://discord.gg/hAVk6tgV36)
|
||||
<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=200&columns=14" />
|
||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
||||
</a>
|
||||
|
||||
In addition, the birth of this project cannot be separated from the help of the following open-source projects:
|
||||
此外,本项目的诞生离不开以下开源项目的帮助:
|
||||
|
||||
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - Great Cat Framework
|
||||
|
||||
Open Source Project Friendly Links:
|
||||
|
||||
- [NoneBot2](https://github.com/nonebot/nonebot2) - Excellent Python Asynchronous ChatBot Framework
|
||||
- [Koishi](https://github.com/koishijs/koishi) - Excellent Node.js ChatBot Framework
|
||||
- [MaiBot](https://github.com/Mai-with-u/MaiBot) - Excellent Anthropomorphic AI ChatBot
|
||||
- [nekro-agent](https://github.com/KroMiose/nekro-agent) - Excellent Agent ChatBot
|
||||
- [LangBot](https://github.com/langbot-app/LangBot) - Excellent Multi-platform AI ChatBot
|
||||
- [ChatLuna](https://github.com/ChatLunaLab/chatluna) - Excellent Multi-platform AI ChatBot Koishi Plugin
|
||||
- [Operit AI](https://github.com/AAswordman/Operit) - Excellent AI Assistant Android APP
|
||||
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 伟大的猫猫框架
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
> [!TIP]
|
||||
> If this project helps your life/work, or you are concerned about the future development of this project, please Star the project. This is our motivation to maintain this open-source project <3
|
||||
> 如果本项目对您的生活 / 工作产生了帮助,或者您关注本项目的未来发展,请给项目 Star,这是我们维护这个开源项目的动力 <3
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -296,12 +243,12 @@ Open Source Project Friendly Links:
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
</details>
|
||||
|
||||
_Companionship and capability should never be opposites. We hope to create a robot that can both understand emotions, provide companionship, and reliably complete tasks._
|
||||
<div align="center">
|
||||
|
||||
_私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
</div
|
||||
|
||||
</div>
|
||||
|
||||
255
README_en.md
Normal file
255
README_en.md
Normal file
@@ -0,0 +1,255 @@
|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
||||
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=2" alt="Featured|HelloGitHub" 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://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>
|
||||
</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.
|
||||
|
||||

|
||||
|
||||
## Key Features
|
||||
|
||||
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 nearly 800 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.
|
||||
|
||||
## Quick Start
|
||||
|
||||
#### Docker Deployment (Recommended 🥳)
|
||||
|
||||
We recommend deploying AstrBot using Docker or Docker Compose.
|
||||
|
||||
Please refer to the official documentation: [Deploy AstrBot with Docker](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot).
|
||||
|
||||
#### uv Deployment
|
||||
|
||||
```bash
|
||||
uvx astrbot
|
||||
```
|
||||
|
||||
#### BT-Panel Deployment
|
||||
|
||||
AstrBot has partnered with BT-Panel and is now available in their marketplace.
|
||||
|
||||
Please refer to the official documentation: [BT-Panel Deployment](https://astrbot.app/deploy/astrbot/btpanel.html).
|
||||
|
||||
#### 1Panel Deployment
|
||||
|
||||
AstrBot has been officially listed on the 1Panel marketplace.
|
||||
|
||||
Please refer to the official documentation: [1Panel Deployment](https://astrbot.app/deploy/astrbot/1panel.html).
|
||||
|
||||
#### Deploy on RainYun
|
||||
|
||||
AstrBot has been officially listed on RainYun's cloud application platform with one-click deployment.
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||
|
||||
#### Deploy on Replit
|
||||
|
||||
Community-contributed deployment method.
|
||||
|
||||
[](https://repl.it/github/AstrBotDevs/AstrBot)
|
||||
|
||||
#### Windows One-Click Installer
|
||||
|
||||
Please refer to the official documentation: [Deploy AstrBot with Windows One-Click Installer](https://astrbot.app/deploy/astrbot/windows.html).
|
||||
|
||||
#### CasaOS Deployment
|
||||
|
||||
Community-contributed deployment method.
|
||||
|
||||
Please refer to the official documentation: [CasaOS Deployment](https://astrbot.app/deploy/astrbot/casaos.html).
|
||||
|
||||
#### Manual Deployment
|
||||
|
||||
First, install uv:
|
||||
|
||||
```bash
|
||||
pip install uv
|
||||
```
|
||||
|
||||
Install AstrBot via Git Clone:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
Or refer to the official documentation: [Deploy AstrBot from Source](https://astrbot.app/deploy/astrbot/cli.html).
|
||||
|
||||
## Supported Messaging Platforms
|
||||
|
||||
**Officially Maintained**
|
||||
|
||||
- QQ (Official Platform & OneBot)
|
||||
- Telegram
|
||||
- WeChat Work Application & WeChat Work Intelligent Bot
|
||||
- WeChat Customer Service & WeChat Official Accounts
|
||||
- Feishu (Lark)
|
||||
- DingTalk
|
||||
- Slack
|
||||
- Discord
|
||||
- Satori
|
||||
- Misskey
|
||||
- WhatsApp (Coming Soon)
|
||||
- LINE (Coming Soon)
|
||||
|
||||
**Community Maintained**
|
||||
|
||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
||||
|
||||
## Supported Model Services
|
||||
|
||||
**LLM Services**
|
||||
|
||||
- OpenAI and Compatible Services
|
||||
- Anthropic
|
||||
- Google Gemini
|
||||
- Moonshot AI
|
||||
- Zhipu AI
|
||||
- DeepSeek
|
||||
- Ollama (Self-hosted)
|
||||
- LM Studio (Self-hosted)
|
||||
- [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
||||
- [302.AI](https://share.302.ai/rr1M3l)
|
||||
- [TokenPony](https://www.tokenpony.cn/3YPyf)
|
||||
- [SiliconFlow](https://docs.siliconflow.cn/cn/usecases/use-siliconcloud-in-astrbot)
|
||||
- [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE)
|
||||
- ModelScope
|
||||
- OneAPI
|
||||
|
||||
**LLMOps Platforms**
|
||||
|
||||
- Dify
|
||||
- Alibaba Cloud Bailian Applications
|
||||
- Coze
|
||||
|
||||
**Speech-to-Text Services**
|
||||
|
||||
- OpenAI Whisper
|
||||
- SenseVoice
|
||||
|
||||
**Text-to-Speech Services**
|
||||
|
||||
- OpenAI TTS
|
||||
- Gemini TTS
|
||||
- GPT-Sovits-Inference
|
||||
- GPT-Sovits
|
||||
- FishAudio
|
||||
- Edge TTS
|
||||
- Alibaba Cloud Bailian TTS
|
||||
- Azure TTS
|
||||
- Minimax TTS
|
||||
- Volcano Engine TTS
|
||||
|
||||
## ❤️ 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.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
|
||||
## 🌍 Community
|
||||
|
||||
### QQ Groups
|
||||
|
||||
- Group 1: 322154837
|
||||
- Group 3: 630166526
|
||||
- Group 5: 822130018
|
||||
- Group 6: 753075035
|
||||
- Group 7: 743746109
|
||||
- Group 8: 1030353265
|
||||
- Developer Group: 975206796
|
||||
|
||||
### Telegram Group
|
||||
|
||||
<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 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 ❤️
|
||||
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||
<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
|
||||
|
||||
## ⭐ 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
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://star-history.com/#astrbotdevs/astrbot&Date)
|
||||
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
<div align="center">
|
||||
|
||||
_私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
</div>
|
||||
341
README_fr.md
341
README_fr.md
@@ -1,12 +1,10 @@
|
||||

|
||||
|
||||
</p>
|
||||
|
||||
<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_zh.md">简体中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -16,276 +14,226 @@
|
||||
<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%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTk4IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%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">
|
||||
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?style=for-the-badge&color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest">
|
||||
<img src="https://img.shields.io/badge/python-3.10+-blue.svg?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>
|
||||
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20plugins&style=for-the-badge&label=Marketplace&cacheSeconds=3600">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<a href="https://astrbot.app/">Accueil</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
||||
|
||||
<a href="https://astrbot.app/">Documentation</a> |
|
||||
<a href="https://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</a>
|
||||
|
||||
</div>
|
||||
|
||||
AstrBot est un assistant de chat personnel et de groupe Agentic tout-en-un et open-source, qui peut être déployé sur des dizaines de logiciels de messagerie instantanée grand public tels que QQ, Telegram, WeCom (WeChat Entreprise), Lark (Feishu), DingTalk, Slack, etc. Il intègre également une interface de chat légère similaire à OpenWebUI, créant ainsi une infrastructure conversationnelle intelligente fiable et extensible pour les particuliers, les développeurs et les équipes. Qu'il s'agisse d'un compagnon IA personnel, d'un service client intelligent, d'un assistant automatisé ou d'une base de connaissances d'entreprise, AstrBot vous permet de construire rapidement des applications IA au sein du flux de travail de vos plateformes de messagerie instantanée.
|
||||
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.
|
||||
|
||||

|
||||
<img width="1776" height="1080" alt="image" src="https://github.com/user-attachments/assets/00782c4c-4437-4d97-aabc-605e3738da5c" />
|
||||
|
||||
## Fonctionnalités Principales
|
||||
## Fonctionnalités principales
|
||||
|
||||
1. 💯 Gratuit & Open Source.
|
||||
2. ✨ Dialogue avec de grands modèles d'IA (LLM), multimodal, Agent, MCP, Compétences (Skills), base de connaissances, définition de persona, compression automatique des dialogues.
|
||||
3. 🤖 Prend en charge l'intégration avec des plateformes d'agents comme Dify, Alibaba Bailian, Coze, etc.
|
||||
4. 🌐 Multiplateforme, prend en charge QQ, WeCom, Lark, DingTalk, Compte Officiel WeChat, Telegram, Slack et [plus encore](#plateformes-de-messagerie-prises-en-charge).
|
||||
5. 📦 Extension par plugins, plus de 1000 plugins disponibles pour une installation en un clic.
|
||||
6. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) : environnement isolé pour exécuter n'importe quel code, appeler le Shell et réutiliser les ressources au niveau de la session en toute sécurité.
|
||||
7. 💻 Support WebUI.
|
||||
8. 🌈 Support Web ChatUI, avec sandbox de proxy intégré, recherche web, etc.
|
||||
9. 🌐 Support de l'internationalisation (i18n).
|
||||
2. ✨ Conversations avec LLM IA, Multimodal, Agent, MCP, Base de connaissances, Paramètres de personnalité.
|
||||
3. 🤖 Prise en charge de l'intégration avec Dify, Alibaba Cloud Bailian, Coze et autres plateformes d'agents.
|
||||
4. 🌐 Multi-plateforme : QQ, WeChat Work, Feishu, DingTalk, Comptes officiels WeChat, Telegram, Slack, et [plus encore](#plateformes-de-messagerie-prises-en-charge).
|
||||
5. 📦 Extensions de plugins avec près de 800 plugins disponibles pour une installation en un clic.
|
||||
6. 💻 Support WebUI.
|
||||
7. 🌐 Support de l'internationalisation (i18n).
|
||||
|
||||
<br>
|
||||
## Démarrage rapide
|
||||
|
||||
<table align="center">
|
||||
<tr align="center">
|
||||
<th>💙 Jeu de rôle & Accompagnement émotionnel</th>
|
||||
<th>✨ Agent Proactif</th>
|
||||
<th>🚀 Capacités Agentic Génériques</th>
|
||||
<th>🧩 1000+ Plugins Communautaires</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éploiement Docker (Recommandé 🥳)
|
||||
|
||||
## Démarrage Rapide
|
||||
Nous recommandons de déployer AstrBot en utilisant Docker ou Docker Compose.
|
||||
|
||||
### Déploiement en un clic
|
||||
Veuillez consulter la documentation officielle : [Déployer AstrBot avec Docker](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot).
|
||||
|
||||
Pour les utilisateurs qui souhaitent essayer AstrBot rapidement, qui sont familiers avec la ligne de commande et capables d'installer l'environnement `uv` par eux-mêmes, nous recommandons la méthode de déploiement en un clic avec `uv` ⚡️.
|
||||
#### Déploiement uv
|
||||
|
||||
```bash
|
||||
uv tool install astrbot
|
||||
astrbot init # Exécutez cette commande uniquement la première fois pour initialiser l'environnement
|
||||
astrbot run # astrbot run --backend-only démarre uniquement le service backend
|
||||
|
||||
# Installer la version de développement (plus de correctifs, nouvelles fonctionnalités, mais moins stable, adapté aux développeurs)
|
||||
uv tool install git+https://github.com/AstrBotDevs/AstrBot@dev
|
||||
uvx astrbot
|
||||
```
|
||||
|
||||
> Nécessite l'installation de [uv](https://docs.astral.sh/uv/).
|
||||
#### Déploiement BT-Panel
|
||||
|
||||
> [!NOTE]
|
||||
> Pour les utilisateurs de macOS : en raison des contrôles de sécurité de macOS, la première exécution de la commande `astrbot` peut prendre un certain temps (environ 10-20 secondes).
|
||||
AstrBot s'est associé à BT-Panel et est maintenant disponible sur leur marketplace.
|
||||
|
||||
Mettre à jour `astrbot` :
|
||||
Veuillez consulter la documentation officielle : [Déploiement BT-Panel](https://astrbot.app/deploy/astrbot/btpanel.html).
|
||||
|
||||
```bash
|
||||
uv tool upgrade astrbot
|
||||
```
|
||||
#### Déploiement 1Panel
|
||||
|
||||
### Déploiement Docker
|
||||
AstrBot a été officiellement listé sur le marketplace 1Panel.
|
||||
|
||||
Pour les utilisateurs familiers avec les conteneurs et souhaitant une méthode de déploiement plus stable et adaptée aux environnements de production, nous recommandons d'utiliser Docker / Docker Compose pour déployer AstrBot.
|
||||
Veuillez consulter la documentation officielle : [Déploiement 1Panel](https://astrbot.app/deploy/astrbot/1panel.html).
|
||||
|
||||
Veuillez vous référer à la documentation officielle [Déployer AstrBot avec Docker](https://astrbot.app/deploy/astrbot/docker.html).
|
||||
#### Déployer sur RainYun
|
||||
|
||||
### Déploiement sur RainYun
|
||||
|
||||
Pour les utilisateurs souhaitant déployer AstrBot en un clic sans gérer de serveur, nous recommandons le service de déploiement cloud en un clic de RainYun ☁️ :
|
||||
AstrBot a été officiellement listé sur la plateforme d'applications cloud de RainYun avec un déploiement en un clic.
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||
|
||||
### Déploiement Client Bureau
|
||||
#### Déployer sur Replit
|
||||
|
||||
Pour les utilisateurs souhaitant utiliser AstrBot sur ordinateur de bureau et utiliser principalement ChatUI comme point d'entrée, nous recommandons l'application AstrBot App.
|
||||
|
||||
Rendez-vous sur [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop) pour télécharger et installer ; cette méthode est destinée à un usage bureautique et n'est pas recommandée pour les scénarios serveur.
|
||||
|
||||
### Déploiement Launcher
|
||||
|
||||
Également pour une utilisation sur bureau, pour les utilisateurs souhaitant un déploiement rapide et une isolation de l'environnement pour plusieurs instances, nous recommandons AstrBot Launcher.
|
||||
|
||||
Rendez-vous sur [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) pour télécharger et installer.
|
||||
|
||||
### Déploiement sur Replit
|
||||
|
||||
Le déploiement sur Replit est maintenu par la communauté et convient aux démonstrations en ligne et aux scénarios d'essai légers.
|
||||
Méthode de déploiement contribuée par la communauté.
|
||||
|
||||
[](https://repl.it/github/AstrBotDevs/AstrBot)
|
||||
|
||||
### AUR
|
||||
#### Installateur Windows en un clic
|
||||
|
||||
La méthode AUR est destinée aux utilisateurs d'Arch Linux souhaitant installer AstrBot via le gestionnaire de paquets du système.
|
||||
Veuillez consulter la documentation officielle : [Déployer AstrBot avec l'installateur Windows en un clic](https://astrbot.app/deploy/astrbot/windows.html).
|
||||
|
||||
Exécutez la commande ci-dessous dans le terminal pour installer le paquet `astrbot-git`. Une fois l'installation terminée, vous pouvez le lancer.
|
||||
#### Déploiement CasaOS
|
||||
|
||||
Méthode de déploiement contribuée par la communauté.
|
||||
|
||||
Veuillez consulter la documentation officielle : [Déploiement CasaOS](https://astrbot.app/deploy/astrbot/casaos.html).
|
||||
|
||||
#### Déploiement manuel
|
||||
|
||||
Tout d'abord, installez uv :
|
||||
|
||||
```bash
|
||||
yay -S astrbot-git
|
||||
pip install uv
|
||||
```
|
||||
|
||||
**Plus de méthodes de déploiement**
|
||||
Installez AstrBot via Git Clone :
|
||||
|
||||
Si vous avez besoin d'un déploiement via panneau de contrôle ou hautement personnalisé, vous pouvez consulter [BT Panel](https://astrbot.app/deploy/astrbot/btpanel.html) (installation via le magasin d'applications BT Panel), [1Panel](https://astrbot.app/deploy/astrbot/1panel.html) (installation via le magasin d'applications 1Panel), [CasaOS](https://astrbot.app/deploy/astrbot/casaos.html) (déploiement visuel pour NAS / serveur domestique) et [Déploiement Manuel](https://astrbot.app/deploy/astrbot/cli.html) (installation personnalisée complète basée sur le code source et `uv`).
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
## Plateformes de Messagerie Prises en Charge
|
||||
Ou consultez la documentation officielle : [Déployer AstrBot depuis les sources](https://astrbot.app/deploy/astrbot/cli.html).
|
||||
|
||||
Connectez AstrBot à vos plateformes de chat préférées.
|
||||
## Plateformes de messagerie prises en charge
|
||||
|
||||
| Plateforme | Mainteneur |
|
||||
|---------|---------------|
|
||||
| **QQ** | Officiel |
|
||||
| **OneBot v11** | Officiel |
|
||||
| **Telegram** | Officiel |
|
||||
| **WeCom (App & Smart Bot)** | Officiel |
|
||||
| **WeChat (Service Client & Compte Officiel)** | Officiel |
|
||||
| **Lark (Feishu)** | Officiel |
|
||||
| **DingTalk** | Officiel |
|
||||
| **Slack** | Officiel |
|
||||
| **Discord** | Officiel |
|
||||
| **LINE** | Officiel |
|
||||
| **Satori** | Officiel |
|
||||
| **Misskey** | Officiel |
|
||||
| **Whatsapp (Bientôt)** | Officiel |
|
||||
| [**Matrix**](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Communauté |
|
||||
| [**KOOK**](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | Communauté |
|
||||
| [**VoceChat**](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Communauté |
|
||||
**Maintenues officiellement**
|
||||
|
||||
## Fournisseurs de Modèles Pris en Charge
|
||||
- QQ (Plateforme officielle & OneBot)
|
||||
- Telegram
|
||||
- Application WeChat Work & Bot intelligent WeChat Work
|
||||
- Service client WeChat & Comptes officiels WeChat
|
||||
- Feishu (Lark)
|
||||
- DingTalk
|
||||
- Slack
|
||||
- Discord
|
||||
- Satori
|
||||
- Misskey
|
||||
- WhatsApp (Bientôt disponible)
|
||||
- LINE (Bientôt disponible)
|
||||
|
||||
| Fournisseur | Type |
|
||||
|---------|---------------|
|
||||
| Personnalisé | Tout service compatible avec l'API OpenAI |
|
||||
| OpenAI | LLM |
|
||||
| Anthropic | LLM |
|
||||
| Google Gemini | LLM |
|
||||
| Moonshot AI | LLM |
|
||||
| Zhipu AI | LLM |
|
||||
| DeepSeek | LLM |
|
||||
| Ollama (Local) | LLM |
|
||||
| LM Studio (Local) | LLM |
|
||||
| [AIHubMix](https://aihubmix.com/?aff=4bfH) | LLM (Passerelle API, supporte tous les modèles) |
|
||||
| [Uyun AI](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | LLM (Passerelle API, supporte tous les modèles) |
|
||||
| [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | LLM (Passerelle API, supporte tous les modèles) |
|
||||
| [PPIO](https://ppio.com/user/register?invited_by=AIOONE) | LLM (Passerelle API, supporte tous les modèles) |
|
||||
| [302.AI](https://share.302.ai/rr1M3l) | LLM (Passerelle API, supporte tous les modèles)|
|
||||
| [TokenPony](https://www.tokenpony.cn/3YPyf) | LLM (Passerelle API, supporte tous les modèles)|
|
||||
| ModelScope | LLM |
|
||||
| OneAPI | LLM |
|
||||
| Dify | Plateforme LLMOps |
|
||||
| Alibaba Bailian | Plateforme LLMOps |
|
||||
| Coze | Plateforme LLMOps |
|
||||
| OpenAI Whisper | Synthèse vocale (Speech-to-Text) |
|
||||
| SenseVoice | Synthèse vocale (Speech-to-Text) |
|
||||
| OpenAI TTS | Synthèse vocale (Text-to-Speech) |
|
||||
| Gemini TTS | Synthèse vocale (Text-to-Speech) |
|
||||
| GPT-Sovits-Inference | Synthèse vocale (Text-to-Speech) |
|
||||
| GPT-Sovits | Synthèse vocale (Text-to-Speech) |
|
||||
| FishAudio | Synthèse vocale (Text-to-Speech) |
|
||||
| Edge TTS | Synthèse vocale (Text-to-Speech) |
|
||||
| Alibaba Bailian TTS | Synthèse vocale (Text-to-Speech) |
|
||||
| Azure TTS | Synthèse vocale (Text-to-Speech) |
|
||||
| Minimax TTS | Synthèse vocale (Text-to-Speech) |
|
||||
| Volcengine TTS | Synthèse vocale (Text-to-Speech) |
|
||||
**Maintenues par la communauté**
|
||||
|
||||
## ❤️ Sponsors
|
||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
||||
|
||||
<p align="center">
|
||||
<img alt="sponsors" src="https://sponsors.astrbot.app/?v=1">
|
||||
</p>
|
||||
## Services de modèles pris en charge
|
||||
|
||||
**Services LLM**
|
||||
|
||||
## ❤️ Contribution
|
||||
- OpenAI et services compatibles
|
||||
- Anthropic
|
||||
- Google Gemini
|
||||
- Moonshot AI
|
||||
- Zhipu AI
|
||||
- DeepSeek
|
||||
- Ollama (Auto-hébergé)
|
||||
- LM Studio (Auto-hébergé)
|
||||
- [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
||||
- [302.AI](https://share.302.ai/rr1M3l)
|
||||
- [TokenPony](https://www.tokenpony.cn/3YPyf)
|
||||
- [SiliconFlow](https://docs.siliconflow.cn/cn/usecases/use-siliconcloud-in-astrbot)
|
||||
- [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE)
|
||||
- ModelScope
|
||||
- OneAPI
|
||||
|
||||
Les Issues et Pull Requests sont les bienvenus ! Soumettez simplement vos modifications à ce projet :)
|
||||
**Plateformes LLMOps**
|
||||
|
||||
### Comment Contribuer
|
||||
- Dify
|
||||
- Applications Alibaba Cloud Bailian
|
||||
- Coze
|
||||
|
||||
Vous pouvez contribuer en examinant les problèmes ou en aidant à réviser les PR (Pull Requests). Tout problème ou PR est le bienvenu pour promouvoir la contribution communautaire. Bien sûr, ce ne sont que des suggestions, vous pouvez contribuer de n'importe quelle manière. Pour l'ajout de nouvelles fonctionnalités, veuillez d'abord en discuter via une Issue.
|
||||
Il est recommandé de fusionner les PR fonctionnels dans la branche `dev`, qui sera fusionnée dans la branche principale et publiée en tant que nouvelle version après test des modifications.
|
||||
Pour réduire les conflits, nous suggérons :
|
||||
1. Créez votre branche de travail basée sur la branche `dev`, évitez de travailler directement sur la branche `main`.
|
||||
2. Lors de la soumission d'une PR, sélectionnez la branche `dev` comme cible.
|
||||
3. Synchronisez régulièrement la branche `dev` en local, utilisez souvent `git pull`.
|
||||
**Services de reconnaissance vocale**
|
||||
|
||||
### Environnement de Développement
|
||||
- OpenAI Whisper
|
||||
- SenseVoice
|
||||
|
||||
AstrBot utilise `ruff` pour le formatage et la vérification du code.
|
||||
**Services de synthèse vocale**
|
||||
|
||||
- OpenAI TTS
|
||||
- Gemini TTS
|
||||
- GPT-Sovits-Inference
|
||||
- GPT-Sovits
|
||||
- FishAudio
|
||||
- Edge TTS
|
||||
- Alibaba Cloud Bailian TTS
|
||||
- Azure TTS
|
||||
- Minimax TTS
|
||||
- Volcano Engine TTS
|
||||
|
||||
## ❤️ 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
|
||||
git switch dev # Basculer vers la branche de développement
|
||||
pip install pre-commit # ou uv tool install pre-commit
|
||||
pip install pre-commit
|
||||
pre-commit install
|
||||
```
|
||||
Il est recommandé d'utiliser `uv` pour l'installation locale et les tests.
|
||||
```bash
|
||||
uv tool install -e . --force
|
||||
astrbot init
|
||||
astrbot run
|
||||
```
|
||||
Débogage frontend
|
||||
```bash
|
||||
astrbot run --backend-only
|
||||
cd dashboard
|
||||
bun install # ou pnpm, etc.
|
||||
bun dev
|
||||
```
|
||||
|
||||
## 🌍 Communauté
|
||||
|
||||
### Groupes QQ
|
||||
|
||||
- Groupe 9 : 1076659624 (Nouveau)
|
||||
- Groupe 10 : 1078079676 (Nouveau)
|
||||
- Groupe 1 : 322154837
|
||||
- Groupe 3 : 630166526
|
||||
- Groupe 5 : 822130018
|
||||
- Groupe 6 : 753075035
|
||||
- Groupe 7 : 743746109
|
||||
- Groupe 8 : 1030353265
|
||||
- Groupe Développeurs (Discussion libre) : 975206796
|
||||
- Groupe Développeurs (Officiel) : 1039761811
|
||||
- Groupe développeurs : 975206796
|
||||
|
||||
### Canal Discord
|
||||
### Groupe Telegram
|
||||
|
||||
- [Discord](https://discord.gg/hAVk6tgV36)
|
||||
<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>
|
||||
|
||||
## ❤️ Remerciements Spéciaux
|
||||
### Serveur Discord
|
||||
|
||||
Un grand merci à tous les Contributeurs et développeurs de plugins pour leur contribution à AstrBot ❤️
|
||||
<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=200&columns=14" />
|
||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
||||
</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) - Le grand framework félin
|
||||
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - L'incroyable framework chat
|
||||
|
||||
Liens amicaux vers des projets open source :
|
||||
|
||||
- [NoneBot2](https://github.com/nonebot/nonebot2) - Excellent framework de ChatBot asynchrone en Python
|
||||
- [Koishi](https://github.com/koishijs/koishi) - Excellent framework de ChatBot en Node.js
|
||||
- [MaiBot](https://github.com/Mai-with-u/MaiBot) - Excellent ChatBot IA anthropomorphe
|
||||
- [nekro-agent](https://github.com/KroMiose/nekro-agent) - Excellent ChatBot Agent
|
||||
- [LangBot](https://github.com/langbot-app/LangBot) - Excellent ChatBot IA multiplateforme
|
||||
- [ChatLuna](https://github.com/ChatLunaLab/chatluna) - Excellent plugin Koishi de ChatBot IA multiplateforme
|
||||
- [Operit AI](https://github.com/AAswordman/Operit) - Excellente application Android d'assistant intelligent IA
|
||||
|
||||
## ⭐ Historique des Étoiles
|
||||
## ⭐ Historique des étoiles
|
||||
|
||||
> [!TIP]
|
||||
> Si ce projet vous a été utile dans votre vie ou votre travail, ou si vous vous intéressez à son développement futur, merci de lui donner une Étoile. C'est notre motivation pour maintenir ce projet open source <3
|
||||
> 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">
|
||||
|
||||
@@ -293,12 +241,7 @@ Liens amicaux vers des projets open source :
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
</details>
|
||||
|
||||
_La compagnie et la compétence ne devraient jamais être opposées. Nous espérons créer un robot capable à la fois de comprendre les émotions, d'offrir de la compagnie et d'accomplir des tâches de manière fiable._
|
||||
_私は、高性能ですから!_
|
||||
|
||||
_私は、高性能ですから!_ (Je suis performant !)
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
|
||||
</div>
|
||||
|
||||
355
README_ja.md
355
README_ja.md
@@ -1,12 +1,10 @@
|
||||

|
||||
|
||||
</p>
|
||||
|
||||
<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_zh.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/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>
|
||||
@@ -16,276 +14,227 @@
|
||||
<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=%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%82%A2&cacheSeconds=3600">
|
||||
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
|
||||
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?style=for-the-badge&color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest">
|
||||
<img src="https://img.shields.io/badge/python-3.10+-blue.svg?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>
|
||||
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E5%80%8B&style=for-the-badge&label=%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3&cacheSeconds=3600">
|
||||
</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>
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
||||
|
||||
<a href="https://astrbot.app/">ドキュメント</a> |
|
||||
<a href="https://blog.astrbot.app/">Blog</a> |
|
||||
<a href="https://astrbot.featurebase.app/roadmap">ロードマップ</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/issues">Issue</a>
|
||||
</div>
|
||||
|
||||
AstrBotは、オープンソースのオールインワンAgentic個人およびグループチャットアシスタントです。QQ、Telegram、WeCom(企業微信)、Lark(飛書)、DingTalk(釘釘)、Slackなど、数十種類の主要なインスタントメッセージングソフトウェアに導入できます。さらに、OpenWebUIに似た軽量のChatUIも組み込まれており、個人、開発者、チーム向けに信頼性が高く拡張可能な会話型AIインフラストラクチャを提供します。個人のAIパートナー、インテリジェントなカスタマーサービス、自動化アシスタント、または企業のナレッジベースであっても、AstrBotはインスタントメッセージングプラットフォームのワークフロー内でAIアプリケーションを迅速に構築することを可能にします。
|
||||
AstrBot は、主要なインスタントメッセージングアプリと統合できるオープンソースのオールインワン Agent チャットボットプラットフォームです。個人、開発者、チームに信頼性が高くスケーラブルな会話型 AI インフラストラクチャを提供します。パーソナル AI コンパニオン、インテリジェントカスタマーサービス、オートメーションアシスタント、エンタープライズナレッジベースなど、AstrBot を使用すると、IM プラットフォームのワークフロー内で本番環境対応の AI アプリケーションを迅速に構築できます。
|
||||
|
||||

|
||||
<img width="1776" height="1080" alt="image" src="https://github.com/user-attachments/assets/00782c4c-4437-4d97-aabc-605e3738da5c" />
|
||||
|
||||
## 主な機能
|
||||
|
||||
1. 💯 無料 & オープンソース。
|
||||
2. ✨ AI大規模モデル対話、マルチモーダル、エージェント、MCP、スキル、ナレッジベース、人格設定、対話の自動圧縮。
|
||||
3. 🤖 Dify、Alibaba Bailian(阿里雲百煉)、Cozeなどのエージェントプラットフォームとの連携をサポート。
|
||||
4. 🌐 マルチプラットフォーム対応:QQ、WeCom、Lark、DingTalk、WeChat公式アカウント、Telegram、Slack、その他[多数](#対応メッセージングプラットフォーム)。
|
||||
5. 📦 プラグイン拡張:1000以上のプラグインがワンクリックでインストール可能。
|
||||
6. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html):隔離された環境で、あらゆるコードの安全な実行、シェル呼び出し、セッションレベルのリソース再利用が可能。
|
||||
7. 💻 WebUIサポート。
|
||||
8. 🌈 Web ChatUIサポート:ChatUIにはプロキシサンドボックス、Web検索などが組み込まれています。
|
||||
9. 🌐 国際化(i18n)サポート。
|
||||
|
||||
<br>
|
||||
|
||||
<table align="center">
|
||||
<tr align="center">
|
||||
<th>💙 ロールプレイ & 感情的な付き添い</th>
|
||||
<th>✨ 能動的エージェント</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>
|
||||
2. ✨ AI 大規模言語モデル対話、マルチモーダル、Agent、MCP、ナレッジベース、ペルソナ設定。
|
||||
3. 🤖 Dify、Alibaba Cloud 百炼、Coze などの Agent プラットフォームとの統合をサポート。
|
||||
4. 🌐 マルチプラットフォーム:QQ、WeChat Work、Feishu、DingTalk、WeChat 公式アカウント、Telegram、Slack、[その他](#サポートされているメッセージプラットフォーム)。
|
||||
5. 📦 約800個のプラグインをワンクリックでインストール可能なプラグイン拡張機能。
|
||||
6. 💻 WebUI サポート。
|
||||
7. 🌐 国際化(i18n)サポート。
|
||||
|
||||
## クイックスタート
|
||||
|
||||
### ワンクリックデプロイ
|
||||
#### Docker デプロイ(推奨 🥳)
|
||||
|
||||
AstrBotをすぐに試してみたい方で、コマンドラインに慣れており、`uv`環境を自分でインストールできる方には、`uv`を使用したワンクリックデプロイをお勧めします⚡️。
|
||||
Docker / Docker Compose を使用した AstrBot のデプロイを推奨します。
|
||||
|
||||
公式ドキュメント [Docker を使用した AstrBot のデプロイ](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) をご参照ください。
|
||||
|
||||
#### uv デプロイ
|
||||
|
||||
```bash
|
||||
uv tool install astrbot
|
||||
astrbot init # 初回のみ環境初期化のために実行
|
||||
astrbot run # astrbot run --backend-only バックエンドサービスのみ起動
|
||||
|
||||
# 開発版のインストール(修正や新機能が多いですが、不安定な場合があります。開発者向け)
|
||||
uv tool install git+https://github.com/AstrBotDevs/AstrBot@dev
|
||||
uvx astrbot
|
||||
```
|
||||
|
||||
> [uv](https://docs.astral.sh/uv/)のインストールが必要です。
|
||||
#### 宝塔パネルデプロイ
|
||||
|
||||
> [!NOTE]
|
||||
> macOSユーザーの場合:macOSのセキュリティチェックにより、`astrbot`コマンドの初回実行に時間がかかる場合があります(約10〜20秒)。
|
||||
AstrBot は宝塔パネルと提携し、宝塔パネルに公開されています。
|
||||
|
||||
`astrbot`の更新:
|
||||
公式ドキュメント [宝塔パネルデプロイ](https://astrbot.app/deploy/astrbot/btpanel.html) をご参照ください。
|
||||
|
||||
```bash
|
||||
uv tool upgrade astrbot
|
||||
```
|
||||
#### 1Panel デプロイ
|
||||
|
||||
### Dockerデプロイ
|
||||
AstrBot は 1Panel 公式により 1Panel パネルに公開されています。
|
||||
|
||||
コンテナに精通しており、より安定的で本番環境に適したデプロイ方法を好むユーザーには、Docker / Docker Composeを使用したAstrBotのデプロイをお勧めします。
|
||||
公式ドキュメント [1Panel デプロイ](https://astrbot.app/deploy/astrbot/1panel.html) をご参照ください。
|
||||
|
||||
公式ドキュメントの[Dockerを使用してAstrBotをデプロイする](https://astrbot.app/deploy/astrbot/docker.html)を参照してください。
|
||||
#### 雨云でのデプロイ
|
||||
|
||||
### RainYun(雨云)でのデプロイ
|
||||
|
||||
サーバーを自分で管理せずにAstrBotをワンクリックでデプロイしたいユーザーには、RainYunのワンクリッククラウドデプロイサービスをお勧めします☁️:
|
||||
AstrBot は雨云公式によりクラウドアプリケーションプラットフォームに公開され、ワンクリックでデプロイ可能です。
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||
|
||||
### デスクトップクライアントデプロイ
|
||||
#### Replit でのデプロイ
|
||||
|
||||
デスクトップでAstrBotを使用し、主にChatUIを入り口として使用したいユーザーには、AstrBot Appをお勧めします。
|
||||
|
||||
[AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop)にアクセスしてダウンロードおよびインストールしてください。この方法はデスクトップ利用向けであり、サーバーシナリオには推奨されません。
|
||||
|
||||
### ランチャーデプロイ
|
||||
|
||||
同じくデスクトップ向けで、迅速にデプロイし、環境を分離して複数起動したいユーザーには、AstrBot Launcherをお勧めします。
|
||||
|
||||
[AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher)にアクセスしてダウンロードおよびインストールしてください。
|
||||
|
||||
### Replitでのデプロイ
|
||||
|
||||
Replitデプロイはコミュニティによって維持されており、オンラインデモや軽量な試用シナリオに適しています。
|
||||
コミュニティ貢献によるデプロイ方法。
|
||||
|
||||
[](https://repl.it/github/AstrBotDevs/AstrBot)
|
||||
|
||||
### AUR
|
||||
#### Windows ワンクリックインストーラーデプロイ
|
||||
|
||||
AUR方式はArch Linuxユーザー向けで、システムパッケージマネージャーを通じてAstrBotをインストールしたい場合に適しています。
|
||||
公式ドキュメント [Windows ワンクリックインストーラーを使用した AstrBot のデプロイ](https://astrbot.app/deploy/astrbot/windows.html) をご参照ください。
|
||||
|
||||
ターミナルで以下のコマンドを実行して`astrbot-git`パッケージをインストールすると、起動して使用できます。
|
||||
#### CasaOS デプロイ
|
||||
|
||||
コミュニティ貢献によるデプロイ方法。
|
||||
|
||||
公式ドキュメント [CasaOS デプロイ](https://astrbot.app/deploy/astrbot/casaos.html) をご参照ください。
|
||||
|
||||
#### 手動デプロイ
|
||||
|
||||
まず uv をインストールします:
|
||||
|
||||
```bash
|
||||
yay -S astrbot-git
|
||||
pip install uv
|
||||
```
|
||||
|
||||
**その他のデプロイ方法**
|
||||
Git Clone で AstrBot をインストール:
|
||||
|
||||
パネル化や高度なカスタマイズデプロイが必要な場合は、[BT Panel(宝塔パネル)](https://astrbot.app/deploy/astrbot/btpanel.html)(BT Panelアプリストアインストール)、[1Panel](https://astrbot.app/deploy/astrbot/1panel.html)(1Panelアプリストアインストール)、[CasaOS](https://astrbot.app/deploy/astrbot/casaos.html)(NAS / ホームサーバーの視覚的デプロイ)、および[手動デプロイ](https://astrbot.app/deploy/astrbot/cli.html)(ソースコードと`uv`に基づく完全なカスタムインストール)を参照してください。
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
## 対応メッセージングプラットフォーム
|
||||
または、公式ドキュメント [ソースコードから AstrBot をデプロイ](https://astrbot.app/deploy/astrbot/cli.html) をご参照ください。
|
||||
|
||||
AstrBotを普段使用しているチャットプラットフォームに接続しましょう。
|
||||
## サポートされているメッセージプラットフォーム
|
||||
|
||||
| プラットフォーム | 管理者 |
|
||||
|---------|---------------|
|
||||
| **QQ** | 公式管理 |
|
||||
| **OneBot v11** | 公式管理 |
|
||||
| **Telegram** | 公式管理 |
|
||||
| **WeComアプリ & WeComボット** | 公式管理 |
|
||||
| **WeChatカスタマーサービス & WeChat公式アカウント** | 公式管理 |
|
||||
| **Lark (飛書)** | 公式管理 |
|
||||
| **DingTalk (釘釘)** | 公式管理 |
|
||||
| **Slack** | 公式管理 |
|
||||
| **Discord** | 公式管理 |
|
||||
| **LINE** | 公式管理 |
|
||||
| **Satori** | 公式管理 |
|
||||
| **Misskey** | 公式管理 |
|
||||
| **Whatsapp (対応予定)** | 公式管理 |
|
||||
| [**Matrix**](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | コミュニティ管理 |
|
||||
| [**KOOK**](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | コミュニティ管理 |
|
||||
| [**VoceChat**](https://github.com/HikariFroya/astrbot_plugin_vocechat) | コミュニティ管理 |
|
||||
**公式メンテナンス**
|
||||
|
||||
## 対応モデルプロバイダー
|
||||
- QQ (公式プラットフォーム & OneBot)
|
||||
- Telegram
|
||||
- WeChat Work アプリケーション & WeChat Work インテリジェントボット
|
||||
- WeChat カスタマーサービス & WeChat 公式アカウント
|
||||
- Feishu (Lark)
|
||||
- DingTalk
|
||||
- Slack
|
||||
- Discord
|
||||
- Satori
|
||||
- Misskey
|
||||
- WhatsApp (近日対応予定)
|
||||
- LINE (近日対応予定)
|
||||
|
||||
| プロバイダー | タイプ |
|
||||
|---------|---------------|
|
||||
| カスタム | OpenAI API互換の任意のサービス |
|
||||
| OpenAI | LLM |
|
||||
| Anthropic | LLM |
|
||||
| Google Gemini | LLM |
|
||||
| Moonshot AI | LLM |
|
||||
| Zhipu AI (智譜AI) | LLM |
|
||||
| DeepSeek | LLM |
|
||||
| Ollama (ローカル) | LLM |
|
||||
| LM Studio (ローカル) | LLM |
|
||||
| [AIHubMix](https://aihubmix.com/?aff=4bfH) | LLM (APIゲートウェイ, 全モデル対応) |
|
||||
| [Uyun AI (優雲智算)](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | LLM (APIゲートウェイ, 全モデル対応) |
|
||||
| [SiliconFlow (硅基流動)](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ゲートウェイ, 全モデル対応)|
|
||||
| [TokenPony (小馬算力)](https://www.tokenpony.cn/3YPyf) | LLM (APIゲートウェイ, 全モデル対応)|
|
||||
| ModelScope | LLM |
|
||||
| OneAPI | LLM |
|
||||
| Dify | LLMOpsプラットフォーム |
|
||||
| Alibaba Bailian (阿里雲百煉) | LLMOpsプラットフォーム |
|
||||
| Coze | LLMOpsプラットフォーム |
|
||||
| OpenAI Whisper | 音声認識 (STT) |
|
||||
| SenseVoice | 音声認識 (STT) |
|
||||
| OpenAI TTS | 音声合成 (TTS) |
|
||||
| Gemini TTS | 音声合成 (TTS) |
|
||||
| GPT-Sovits-Inference | 音声合成 (TTS) |
|
||||
| GPT-Sovits | 音声合成 (TTS) |
|
||||
| FishAudio | 音声合成 (TTS) |
|
||||
| Edge TTS | 音声合成 (TTS) |
|
||||
| Alibaba Bailian TTS | 音声合成 (TTS) |
|
||||
| Azure TTS | 音声合成 (TTS) |
|
||||
| Minimax TTS | 音声合成 (TTS) |
|
||||
| Volcengine TTS (火山エンジン) | 音声合成 (TTS) |
|
||||
**コミュニティメンテナンス**
|
||||
|
||||
## ❤️ Sponsors
|
||||
|
||||
<p align="center">
|
||||
<img alt="sponsors" src="https://sponsors.astrbot.app/?v=1">
|
||||
</p>
|
||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
||||
|
||||
|
||||
## ❤️ 貢献
|
||||
## サポートされているモデルサービス
|
||||
|
||||
IssueやPull Requestは大歓迎です!変更をこのプロジェクトに送信してください :)
|
||||
**大規模言語モデルサービス**
|
||||
|
||||
### 貢献方法
|
||||
- OpenAI および互換サービス
|
||||
- Anthropic
|
||||
- Google Gemini
|
||||
- Moonshot AI
|
||||
- 智谱 AI
|
||||
- DeepSeek
|
||||
- Ollama (セルフホスト)
|
||||
- LM Studio (セルフホスト)
|
||||
- [優云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
||||
- [302.AI](https://share.302.ai/rr1M3l)
|
||||
- [小馬算力](https://www.tokenpony.cn/3YPyf)
|
||||
- [硅基流動](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot)
|
||||
- [PPIO 派欧云](https://ppio.com/user/register?invited_by=AIOONE)
|
||||
- ModelScope
|
||||
- OneAPI
|
||||
|
||||
問題の確認やPR(プルリクエスト)のレビューを通じて貢献できます。コミュニティの貢献を促進するために、あらゆる問題やPRへの参加を歓迎します。もちろん、これらは提案に過ぎず、どのような方法で貢献しても構いません。新機能の追加については、まずIssueで議論してください。
|
||||
機能的なPRは`dev`ブランチにマージすることをお勧めします。テスト修正後にメインブランチにマージされ、新しいバージョンとしてリリースされます。
|
||||
コンフリクトを減らすために、以下のことを推奨します:
|
||||
1. 作業ブランチは`dev`ブランチに基づいて作成し、`main`ブランチで直接作業することは避けてください。
|
||||
2. PRを送信する際は、ターゲットブランチとして`dev`ブランチを選択してください。
|
||||
3. 定期的に`dev`ブランチをローカルに同期し、`git pull`を頻繁に使用してください。
|
||||
**LLMOps プラットフォーム**
|
||||
|
||||
- Dify
|
||||
- Alibaba Cloud 百炼アプリケーション
|
||||
- Coze
|
||||
|
||||
**音声認識サービス**
|
||||
|
||||
- OpenAI Whisper
|
||||
- SenseVoice
|
||||
|
||||
**音声合成サービス**
|
||||
|
||||
- OpenAI TTS
|
||||
- Gemini TTS
|
||||
- GPT-Sovits-Inference
|
||||
- GPT-Sovits
|
||||
- FishAudio
|
||||
- Edge TTS
|
||||
- Alibaba Cloud 百炼 TTS
|
||||
- Azure TTS
|
||||
- Minimax TTS
|
||||
- Volcano Engine TTS
|
||||
|
||||
## ❤️ コントリビューション
|
||||
|
||||
Issue や Pull Request は大歓迎です!このプロジェクトに変更を送信してください :)
|
||||
|
||||
### コントリビュート方法
|
||||
|
||||
Issue を確認したり、PR(プルリクエスト)のレビューを手伝うことで貢献できます。どんな Issue や PR への参加も歓迎され、コミュニティ貢献を促進します。もちろん、これらは提案に過ぎず、どんな方法でも貢献できます。新機能の追加については、まず Issue で議論してください。
|
||||
|
||||
### 開発環境
|
||||
|
||||
AstrBotはコードのフォーマットとチェックに`ruff`を使用しています。
|
||||
AstrBot はコードのフォーマットとチェックに `ruff` を使用しています。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot
|
||||
git switch dev # 開発ブランチに切り替え
|
||||
pip install pre-commit # または uv tool install pre-commit
|
||||
pip 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グループ
|
||||
## 🌍 コミュニティ
|
||||
|
||||
- 9群: 1076659624 (新)
|
||||
- 10群: 1078079676 (新)
|
||||
- 1群:322154837
|
||||
- 3群:630166526
|
||||
- 5群:822130018
|
||||
- 6群:753075035
|
||||
- 7群:743746109
|
||||
- 8群:1030353265
|
||||
- 開発者群(雑談):975206796
|
||||
- 開発者群(公式):1039761811
|
||||
### QQ グループ
|
||||
|
||||
### Discordチャンネル
|
||||
- 1群: 322154837
|
||||
- 3群: 630166526
|
||||
- 5群: 822130018
|
||||
- 6群: 753075035
|
||||
- 開発者群: 975206796
|
||||
|
||||
- [Discord](https://discord.gg/hAVk6tgV36)
|
||||
### 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>
|
||||
|
||||
## ❤️ Special Thanks
|
||||
|
||||
AstrBotに貢献してくださったすべてのコントリビューターとプラグイン開発者に感謝します ❤️
|
||||
AstrBot への貢献をしていただいたすべてのコントリビューターとプラグイン開発者に特別な感謝を ❤️
|
||||
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" />
|
||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
||||
</a>
|
||||
|
||||
さらに、このプロジェクトの誕生は、以下のオープンソースプロジェクトの助けなしにはあり得ませんでした:
|
||||
また、このプロジェクトの誕生は以下のオープンソースプロジェクトの助けなしには実現できませんでした:
|
||||
|
||||
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 偉大な猫フレームワーク
|
||||
|
||||
オープンソースプロジェクトのフレンドリーリンク:
|
||||
|
||||
- [NoneBot2](https://github.com/nonebot/nonebot2) - 優れたPython非同期チャットボットフレームワーク
|
||||
- [Koishi](https://github.com/koishijs/koishi) - 優れたNode.jsチャットボットフレームワーク
|
||||
- [MaiBot](https://github.com/Mai-with-u/MaiBot) - 優れた擬人化AIチャットボット
|
||||
- [nekro-agent](https://github.com/KroMiose/nekro-agent) - 優れたエージェントチャットボット
|
||||
- [LangBot](https://github.com/langbot-app/LangBot) - 優れたマルチプラットフォームAIチャットボット
|
||||
- [ChatLuna](https://github.com/ChatLunaLab/chatluna) - 優れたマルチプラットフォームAIチャットボットKoishiプラグイン
|
||||
- [Operit AI](https://github.com/AAswordman/Operit) - 優れたAIインテリジェントアシスタントAndroidアプリ
|
||||
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 素晴らしい猫猫フレームワーク
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
> [!TIP]
|
||||
> もしこのプロジェクトがあなたの生活や仕事の助けになったなら、あるいはこのプロジェクトの将来の発展に関心があるなら、プロジェクトにStarを付けてください。これは私たちがこのオープンソースプロジェクトを維持するための原動力となります <3
|
||||
> このプロジェクトがあなたの生活や仕事に役立ったり、このプロジェクトの今後の発展に関心がある場合は、プロジェクトに Star をください。これがこのオープンソースプロジェクトを維持する原動力です <3
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -293,12 +242,6 @@ AstrBotに貢献してくださったすべてのコントリビューターと
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
_付き添いと能力は決して対立するものであってはなりません。私たちが創造したいのは、感情を理解し、寄り添いながらも、確実に仕事を遂行できるロボットです。_
|
||||
</details>
|
||||
|
||||
_私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
|
||||
</div>
|
||||
|
||||
335
README_ru.md
335
README_ru.md
@@ -1,12 +1,10 @@
|
||||

|
||||
|
||||
</p>
|
||||
|
||||
<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_zh.md">简体中文</a>
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -16,276 +14,226 @@
|
||||
<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=%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">
|
||||
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?style=for-the-badge&color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest">
|
||||
<img src="https://img.shields.io/badge/python-3.10+-blue.svg?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>
|
||||
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD%D0%BE%D0%B2&style=for-the-badge&label=%D0%9C%D0%B0%D0%B3%D0%B0%D0%B7%D0%B8%D0%BD&cacheSeconds=3600">
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<a href="https://astrbot.app/">Главная</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
|
||||
|
||||
<a href="https://astrbot.app/">Документация</a> |
|
||||
<a href="https://blog.astrbot.app/">Блог</a> |
|
||||
<a href="https://astrbot.featurebase.app/roadmap">Roadmap</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 — это универсальный агентский помощник для личных и групповых чатов с открытым исходным кодом. Он может быть развернут в десятках популярных мессенджеров, таких как QQ, Telegram, WeCom (Enterprise WeChat), Lark (Feishu), DingTalk, Slack и других. Кроме того, он имеет встроенный легковесный веб-интерфейс чата (ChatUI), похожий на OpenWebUI, создавая надежную и масштабируемую диалоговую интеллектуальную инфраструктуру для частных лиц, разработчиков и команд. Будь то личный AI-компаньон, интеллектуальная служба поддержки, автоматизированный помощник или корпоративная база знаний, AstrBot позволяет быстро создавать AI-приложения в рабочем процессе ваших платформ обмена мгновенными сообщениями.
|
||||
AstrBot — это универсальная платформа Agent-чатботов с открытым исходным кодом, которая интегрируется с основными приложениями для обмена мгновенными сообщениями. Она предоставляет надёжную и масштабируемую инфраструктуру разговорного ИИ для частных лиц, разработчиков и команд. Будь то персональный ИИ-компаньон, интеллектуальная служба поддержки, автоматизированный помощник или корпоративная база знаний — AstrBot позволяет быстро создавать готовые к использованию ИИ-приложения в рабочих процессах вашей платформы обмена сообщениями.
|
||||
|
||||

|
||||
<img width="1776" height="1080" alt="image" src="https://github.com/user-attachments/assets/00782c4c-4437-4d97-aabc-605e3738da5c" />
|
||||
|
||||
## Основные возможности
|
||||
|
||||
1. 💯 Бесплатно и с открытым исходным кодом.
|
||||
2. ✨ Поддержка диалога с большими языковыми моделями (LLM), мультимодальность, Агенты, MCP, Навыки (Skills), База знаний, Персонализация, автоматическое сжатие диалога.
|
||||
3. 🤖 Поддержка интеграции с платформами агентов, такими как Dify, Alibaba Bailian, Coze и др.
|
||||
4. 🌐 Мультиплатформенность: поддержка QQ, WeCom, Lark, DingTalk, WeChat Official Account, 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>✨ Проактивный Агент</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>
|
||||
2. ✨ ИИ-диалоги с LLM, мультимодальность, Agent, MCP, база знаний, настройки личности.
|
||||
3. 🤖 Поддержка интеграции с Dify, Alibaba Cloud Bailian, Coze и другими платформами агентов.
|
||||
4. 🌐 Мультиплатформенность: QQ, WeChat Work, Feishu, DingTalk, официальные аккаунты WeChat, Telegram, Slack и [другие](#поддерживаемые-платформы-обмена-сообщениями).
|
||||
5. 📦 Расширения плагинов с почти 800 плагинами, доступными для установки в один клик.
|
||||
6. 💻 Поддержка WebUI.
|
||||
7. 🌐 Поддержка интернационализации (i18n).
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### Развертывание в один клик
|
||||
#### Развёртывание Docker (Рекомендуется 🥳)
|
||||
|
||||
Для пользователей, которые хотят быстро протестировать AstrBot, знакомы с командной строкой и могут самостоятельно установить среду `uv`, мы рекомендуем метод развертывания в один клик с помощью `uv` ⚡️.
|
||||
Мы рекомендуем развёртывать AstrBot с помощью Docker или Docker Compose.
|
||||
|
||||
См. официальную документацию: [Развёртывание AstrBot с Docker](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot).
|
||||
|
||||
#### Развёртывание uv
|
||||
|
||||
```bash
|
||||
uv tool install astrbot
|
||||
astrbot init # Выполните эту команду только в первый раз для инициализации среды
|
||||
astrbot run # astrbot run --backend-only запускает только бэкенд сервис
|
||||
|
||||
# Установка версии для разработчиков (больше исправлений и новых функций, но менее стабильна; подходит для разработчиков)
|
||||
uv tool install git+https://github.com/AstrBotDevs/AstrBot@dev
|
||||
uvx astrbot
|
||||
```
|
||||
|
||||
> Требуется установленный [uv](https://docs.astral.sh/uv/).
|
||||
#### Развёртывание BT-Panel
|
||||
|
||||
> [!NOTE]
|
||||
> Для пользователей macOS: Из-за проверок безопасности macOS первый запуск команды `astrbot` может занять длительное время (около 10-20 секунд).
|
||||
AstrBot в партнёрстве с BT-Panel теперь доступен на их маркетплейсе.
|
||||
|
||||
Обновление `astrbot`:
|
||||
См. официальную документацию: [Развёртывание BT-Panel](https://astrbot.app/deploy/astrbot/btpanel.html).
|
||||
|
||||
```bash
|
||||
uv tool upgrade astrbot
|
||||
```
|
||||
#### Развёртывание 1Panel
|
||||
|
||||
### Развертывание через Docker
|
||||
AstrBot официально размещён на маркетплейсе 1Panel.
|
||||
|
||||
Для пользователей, знакомых с контейнерами и предпочитающих более стабильный метод развертывания, подходящий для производственных сред, мы рекомендуем использовать Docker / Docker Compose для развертывания AstrBot.
|
||||
См. официальную документацию: [Развёртывание 1Panel](https://astrbot.app/deploy/astrbot/1panel.html).
|
||||
|
||||
Пожалуйста, обратитесь к официальной документации [Развертывание AstrBot с помощью Docker](https://astrbot.app/deploy/astrbot/docker.html).
|
||||
#### Развёртывание на RainYun
|
||||
|
||||
### Развертывание на RainYun
|
||||
|
||||
Для пользователей, которые хотят развернуть AstrBot в один клик и не хотят самостоятельно управлять серверами, мы рекомендуем облачный сервис развертывания в один клик от RainYun ☁️:
|
||||
AstrBot официально размещён на облачной платформе приложений RainYun с развёртыванием в один клик.
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||
|
||||
### Развертывание настольного клиента
|
||||
#### Развёртывание на Replit
|
||||
|
||||
Для пользователей, желающих использовать AstrBot на рабочем столе и использовать ChatUI в качестве основного интерфейса, мы рекомендуем приложение AstrBot App.
|
||||
|
||||
Перейдите на [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop) для загрузки и установки; этот метод предназначен для использования на рабочем столе и не рекомендуется для серверных сценариев.
|
||||
|
||||
### Развертывание через лаунчер
|
||||
|
||||
Также для настольных компьютеров, для пользователей, которым требуется быстрое развертывание и изоляция среды для нескольких экземпляров, мы рекомендуем AstrBot Launcher.
|
||||
|
||||
Перейдите на [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) для загрузки и установки.
|
||||
|
||||
### Развертывание на Replit
|
||||
|
||||
Развертывание на Replit поддерживается сообществом и подходит для онлайн-демонстраций и легких тестовых сценариев.
|
||||
Метод развёртывания от сообщества.
|
||||
|
||||
[](https://repl.it/github/AstrBotDevs/AstrBot)
|
||||
|
||||
### AUR
|
||||
#### Установщик Windows в один клик
|
||||
|
||||
Метод AUR предназначен для пользователей Arch Linux, желающих установить AstrBot через системный менеджер пакетов.
|
||||
См. официальную документацию: [Развёртывание AstrBot с установщиком Windows в один клик](https://astrbot.app/deploy/astrbot/windows.html).
|
||||
|
||||
Выполните приведенную ниже команду в терминале, чтобы установить пакет `astrbot-git`. После завершения установки вы сможете запустить его.
|
||||
#### Развёртывание CasaOS
|
||||
|
||||
Метод развёртывания от сообщества.
|
||||
|
||||
См. официальную документацию: [Развёртывание CasaOS](https://astrbot.app/deploy/astrbot/casaos.html).
|
||||
|
||||
#### Ручное развёртывание
|
||||
|
||||
Сначала установите uv:
|
||||
|
||||
```bash
|
||||
yay -S astrbot-git
|
||||
pip install uv
|
||||
```
|
||||
|
||||
**Другие методы развертывания**
|
||||
Установите AstrBot через Git Clone:
|
||||
|
||||
Если вам требуется панельное управление или более кастомизированное развертывание, вы можете обратиться к [BT Panel](https://astrbot.app/deploy/astrbot/btpanel.html) (установка через магазин приложений BT Panel), [1Panel](https://astrbot.app/deploy/astrbot/1panel.html) (установка через магазин приложений 1Panel), [CasaOS](https://astrbot.app/deploy/astrbot/casaos.html) (визуальное развертывание для NAS / домашнего сервера) и [Ручное развертывание](https://astrbot.app/deploy/astrbot/cli.html) (полная пользовательская установка на основе исходного кода и `uv`).
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
## Поддерживаемые платформы сообщений
|
||||
Или см. официальную документацию: [Развёртывание AstrBot из исходного кода](https://astrbot.app/deploy/astrbot/cli.html).
|
||||
|
||||
Подключите AstrBot к вашим любимым платформам чата.
|
||||
## Поддерживаемые платформы обмена сообщениями
|
||||
|
||||
| Платформа | Поддержка |
|
||||
|---------|---------------|
|
||||
| **QQ** | Официальная |
|
||||
| **OneBot v11** | Официальная |
|
||||
| **Telegram** | Официальная |
|
||||
| **WeCom (Приложение & Смарт-бот)** | Официальная |
|
||||
| **WeChat (Служба поддержки & Официальный аккаунт)** | Официальная |
|
||||
| **Lark (Feishu)** | Официальная |
|
||||
| **DingTalk** | Официальная |
|
||||
| **Slack** | Официальная |
|
||||
| **Discord** | Официальная |
|
||||
| **LINE** | Официальная |
|
||||
| **Satori** | Официальная |
|
||||
| **Misskey** | Официальная |
|
||||
| **Whatsapp (Скоро)** | Официальная |
|
||||
| [**Matrix**](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Сообщество |
|
||||
| [**KOOK**](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | Сообщество |
|
||||
| [**VoceChat**](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Сообщество |
|
||||
**Официально поддерживаемые**
|
||||
|
||||
## Поддерживаемые провайдеры моделей
|
||||
- QQ (Официальная платформа и OneBot)
|
||||
- Telegram
|
||||
- Приложение WeChat Work и интеллектуальный бот WeChat Work
|
||||
- Служба поддержки WeChat и официальные аккаунты WeChat
|
||||
- Feishu (Lark)
|
||||
- DingTalk
|
||||
- Slack
|
||||
- Discord
|
||||
- Satori
|
||||
- Misskey
|
||||
- WhatsApp (Скоро)
|
||||
- LINE (Скоро)
|
||||
|
||||
| Провайдер | Тип |
|
||||
|---------|---------------|
|
||||
| Пользовательский | Любой сервис, совместимый с OpenAI API |
|
||||
| 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 шлюз, поддерживает все модели) |
|
||||
| [Uyun AI](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | LLM (API шлюз, поддерживает все модели) |
|
||||
| [SiliconFlow](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 шлюз, поддерживает все модели)|
|
||||
| [TokenPony](https://www.tokenpony.cn/3YPyf) | LLM (API шлюз, поддерживает все модели)|
|
||||
| ModelScope | LLM |
|
||||
| OneAPI | LLM |
|
||||
| Dify | Платформа LLMOps |
|
||||
| Alibaba Bailian | Платформа LLMOps |
|
||||
| Coze | Платформа LLMOps |
|
||||
| OpenAI Whisper | Распознавание речи (STT) |
|
||||
| SenseVoice | Распознавание речи (STT) |
|
||||
| OpenAI TTS | Синтез речи (TTS) |
|
||||
| Gemini TTS | Синтез речи (TTS) |
|
||||
| GPT-Sovits-Inference | Синтез речи (TTS) |
|
||||
| GPT-Sovits | Синтез речи (TTS) |
|
||||
| FishAudio | Синтез речи (TTS) |
|
||||
| Edge TTS | Синтез речи (TTS) |
|
||||
| Alibaba Bailian TTS | Синтез речи (TTS) |
|
||||
| Azure TTS | Синтез речи (TTS) |
|
||||
| Minimax TTS | Синтез речи (TTS) |
|
||||
| Volcengine TTS | Синтез речи (TTS) |
|
||||
**Поддерживаемые сообществом**
|
||||
|
||||
## ❤️ Sponsors
|
||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
||||
|
||||
<p align="center">
|
||||
<img alt="sponsors" src="https://sponsors.astrbot.app/?v=1">
|
||||
</p>
|
||||
## Поддерживаемые сервисы моделей
|
||||
|
||||
**Сервисы LLM**
|
||||
|
||||
- OpenAI и совместимые сервисы
|
||||
- Anthropic
|
||||
- Google Gemini
|
||||
- Moonshot AI
|
||||
- Zhipu AI
|
||||
- DeepSeek
|
||||
- Ollama (Самостоятельное размещение)
|
||||
- LM Studio (Самостоятельное размещение)
|
||||
- [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
||||
- [302.AI](https://share.302.ai/rr1M3l)
|
||||
- [TokenPony](https://www.tokenpony.cn/3YPyf)
|
||||
- [SiliconFlow](https://docs.siliconflow.cn/cn/usecases/use-siliconcloud-in-astrbot)
|
||||
- [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE)
|
||||
- ModelScope
|
||||
- OneAPI
|
||||
|
||||
**Платформы LLMOps**
|
||||
|
||||
- Dify
|
||||
- Приложения Alibaba Cloud Bailian
|
||||
- Coze
|
||||
|
||||
**Сервисы распознавания речи**
|
||||
|
||||
- OpenAI Whisper
|
||||
- SenseVoice
|
||||
|
||||
**Сервисы синтеза речи**
|
||||
|
||||
- OpenAI TTS
|
||||
- Gemini TTS
|
||||
- GPT-Sovits-Inference
|
||||
- GPT-Sovits
|
||||
- FishAudio
|
||||
- Edge TTS
|
||||
- Alibaba Cloud Bailian TTS
|
||||
- Azure TTS
|
||||
- Minimax TTS
|
||||
- Volcano Engine TTS
|
||||
|
||||
## ❤️ Вклад в проект
|
||||
|
||||
Мы приветствуем любые Issues и Pull Requests! Просто отправьте свои изменения в этот проект :)
|
||||
Issues и Pull Request всегда приветствуются! Не стесняйтесь отправлять свои изменения в этот проект :)
|
||||
|
||||
### Как внести вклад
|
||||
|
||||
Вы можете внести свой вклад, просматривая проблемы (Issues) или помогая проверять PR (Pull Requests). Любая проблема или PR приветствуются для поощрения участия сообщества. Конечно, это всего лишь предложения, вы можете внести свой вклад любым способом. Для добавления новых функций, пожалуйста, сначала обсудите это через Issue.
|
||||
Рекомендуется объединять функциональные PR в ветку `dev`, которая будет объединена с основной веткой (`main`) и выпущена как новая версия после тестирования изменений.
|
||||
Для уменьшения конфликтов мы рекомендуем:
|
||||
1. Создавайте рабочую ветку на основе ветки `dev`, избегайте работы напрямую в ветке `main`.
|
||||
2. При отправке PR выбирайте ветку `dev` в качестве целевой.
|
||||
3. Регулярно синхронизируйте ветку `dev` с локальной средой, чаще используйте `git pull`.
|
||||
Вы можете внести вклад, просматривая issues или помогая с ревью pull request. Любые issues или PR приветствуются для поощрения участия сообщества. Конечно, это лишь предложения — вы можете вносить вклад любым удобным для вас способом. Для добавления новых функций сначала обсудите это через Issue.
|
||||
|
||||
### Среда разработки
|
||||
|
||||
AstrBot использует `ruff` для форматирования и проверки кода.
|
||||
AstrBot использует `ruff` для форматирования и линтинга кода.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot
|
||||
git switch dev # Переключиться на ветку разработки
|
||||
pip install pre-commit # или uv tool install pre-commit
|
||||
pip 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
|
||||
|
||||
- Группа 9: 1076659624 (Новая)
|
||||
- Группа 10: 1078079676 (Новая)
|
||||
- Группа 1: 322154837
|
||||
- Группа 3: 630166526
|
||||
- Группа 5: 822130018
|
||||
- Группа 6: 753075035
|
||||
- Группа 7: 743746109
|
||||
- Группа 8: 1030353265
|
||||
- Группа разработчиков (Неформальное общение): 975206796
|
||||
- Группа разработчиков (Официальная): 1039761811
|
||||
- Группа разработчиков: 975206796
|
||||
|
||||
### Канал Discord
|
||||
### Группа Telegram
|
||||
|
||||
- [Discord](https://discord.gg/hAVk6tgV36)
|
||||
<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>
|
||||
|
||||
## ❤️ Особая благодарность
|
||||
|
||||
Особая благодарность всем контрибьюторам и разработчикам плагинов за их вклад в AstrBot ❤️
|
||||
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" />
|
||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
||||
</a>
|
||||
|
||||
Кроме того, рождение этого проекта было бы невозможным без помощи следующих проектов с открытым исходным кодом:
|
||||
Кроме того, рождение этого проекта было бы невозможно без помощи следующих проектов с открытым исходным кодом:
|
||||
|
||||
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - Великий кошачий фреймворк
|
||||
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - Замечательный кошачий фреймворк
|
||||
|
||||
Дружественные ссылки на проекты с открытым исходным кодом:
|
||||
|
||||
- [NoneBot2](https://github.com/nonebot/nonebot2) - Отличный асинхронный фреймворк ChatBot на Python
|
||||
- [Koishi](https://github.com/koishijs/koishi) - Отличный фреймворк ChatBot на Node.js
|
||||
- [MaiBot](https://github.com/Mai-with-u/MaiBot) - Отличный антропоморфный AI ChatBot
|
||||
- [nekro-agent](https://github.com/KroMiose/nekro-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) - Отличное Android-приложение интеллектуального AI-помощника
|
||||
|
||||
## ⭐ История звезд
|
||||
## ⭐ История звёзд
|
||||
|
||||
> [!TIP]
|
||||
> Если этот проект помог вам в жизни или работе, или если вы заинтересованы в будущем развитии этого проекта, пожалуйста, поставьте проекту звезду (Star). Это наша мотивация поддерживать этот проект с открытым исходным кодом <3
|
||||
> Если этот проект помог вам в жизни или работе, или если вас интересует его будущее развитие, пожалуйста, поставьте проекту звезду. Это движущая сила поддержки этого проекта с открытым исходным кодом <3
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -293,12 +241,7 @@ bun dev
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
</details>
|
||||
|
||||
_Компаньонство и способности никогда не должны быть противоположностями. Мы надеемся создать робота, который сможет одновременно понимать эмоции, быть компаньоном и надежно выполнять работу._
|
||||
_私は、高性能ですから!_
|
||||
|
||||
_私は、高性能ですから!_ (Я высокопроизводительный!)
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
|
||||
</div>
|
||||
|
||||
335
README_zh-TW.md
335
README_zh-TW.md
@@ -1,12 +1,10 @@
|
||||

|
||||
|
||||
</p>
|
||||
|
||||
<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.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>
|
||||
<br>
|
||||
|
||||
<div>
|
||||
<a href="https://trendshift.io/repositories/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
@@ -16,276 +14,226 @@
|
||||
<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">
|
||||
<img src="https://img.shields.io/github/v/release/AstrBotDevs/AstrBot?style=for-the-badge&color=76bad9" href="https://github.com/AstrBotDevs/AstrBot/releases/latest">
|
||||
<img src="https://img.shields.io/badge/python-3.10+-blue.svg?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>
|
||||
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E5%80%8B&style=for-the-badge&label=%E6%8F%92%E4%BB%B6%E5%B8%82%E5%A0%B4&cacheSeconds=3600">
|
||||
</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>
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">简体中文</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
||||
|
||||
<a href="https://astrbot.app/">文件</a> |
|
||||
<a href="https://blog.astrbot.app/">Blog</a> |
|
||||
<a href="https://astrbot.featurebase.app/roadmap">路線圖</a> |
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/issues">問題回報</a>
|
||||
</div>
|
||||
|
||||
AstrBot 是一個開源的一站式 Agentic 個人和群聊助手,可在 QQ、Telegram、企業微信、飛書、釘钉、Slack 等數十款主流即時通訊軟件上部署,此外還內置類似 OpenWebUI 的輕量化 ChatUI,為個人、開發者和團隊打造可靠、可擴展的對話式智能基礎設施。無論是個人 AI 夥伴、智能客服、自動化助手,還是企業知識庫,AstrBot 都能在你的即時通訊軟件平台的工作流中快速構建 AI 應用。
|
||||
AstrBot 是一個開源的一站式 Agent 聊天機器人平台,可接入主流即時通訊軟體,為個人、開發者和團隊打造可靠、可擴展的對話式智慧基礎設施。無論是個人 AI 夥伴、智慧客服、自動化助手,還是企業知識庫,AstrBot 都能在您的即時通訊軟體平台的工作流程中快速構建生產可用的 AI 應用程式。
|
||||
|
||||

|
||||
<img width="1776" height="1080" alt="image" src="https://github.com/user-attachments/assets/00782c4c-4437-4d97-aabc-605e3738da5c" />
|
||||
|
||||
## 主要功能
|
||||
|
||||
1. 💯 免費 & 開源。
|
||||
2. ✨ AI 大模型對話,多模態,Agent,MCP,Skills,知識庫,人格設定,自動壓縮對話。
|
||||
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>
|
||||
2. ✨ AI 大型模型對話,多模態,Agent,MCP,知識庫,人格設定。
|
||||
3. 🤖 支援接入 Dify、阿里雲百煉、Coze 等智慧體平台。
|
||||
4. 🌐 多平台:QQ、企業微信、飛書、釘釘、微信公眾號、Telegram、Slack 以及[更多](#支援的訊息平台)。
|
||||
5. 📦 外掛擴充,已有近 800 個外掛可一鍵安裝。
|
||||
6. 💻 WebUI 支援。
|
||||
7. 🌐 國際化(i18n)支援。
|
||||
|
||||
## 快速開始
|
||||
|
||||
### 一鍵部署
|
||||
#### Docker 部署(推薦 🥳)
|
||||
|
||||
對於想快速體驗 AstrBot、且熟悉命令行並能夠自行安裝 `uv` 環境的用戶,我們推薦使用 `uv` 一鍵部署方式 ⚡️。
|
||||
推薦使用 Docker / Docker Compose 方式部署 AstrBot。
|
||||
|
||||
請參閱官方文件 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot)。
|
||||
|
||||
#### uv 部署
|
||||
|
||||
```bash
|
||||
uv tool install astrbot
|
||||
astrbot init # 僅首次執行此命令以初始化環境
|
||||
astrbot run # astrbot run --backend-only 僅啟動後端服務
|
||||
|
||||
# 安裝開發版本(更多修復,新功能,但不夠穩定,適合開發者)
|
||||
uv tool install git+https://github.com/AstrBotDevs/AstrBot@dev
|
||||
uvx astrbot
|
||||
```
|
||||
|
||||
> 需要安裝 [uv](https://docs.astral.sh/uv/)。
|
||||
#### 寶塔面板部署
|
||||
|
||||
> [!NOTE]
|
||||
> 對於 macOS 用戶:由於 macOS 安全檢查,首次運行 `astrbot` 命令可能需要較長時間(約 10-20 秒)。
|
||||
AstrBot 與寶塔面板合作,已上架至寶塔面板。
|
||||
|
||||
更新 `astrbot`:
|
||||
請參閱官方文件 [寶塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.html)。
|
||||
|
||||
```bash
|
||||
uv tool upgrade astrbot
|
||||
```
|
||||
#### 1Panel 部署
|
||||
|
||||
### Docker 部署
|
||||
AstrBot 已由 1Panel 官方上架至 1Panel 面板。
|
||||
|
||||
對於熟悉容器、希望獲得更穩定且更適合生產環境部署方式的用戶,我們推薦使用 Docker / Docker Compose 部署 AstrBot。
|
||||
請參閱官方文件 [1Panel 部署](https://astrbot.app/deploy/astrbot/1panel.html)。
|
||||
|
||||
請參考官方文檔 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot)。
|
||||
#### 在雨雲上部署
|
||||
|
||||
### 在 雨雲 上部署
|
||||
|
||||
對於希望一鍵部署 AstrBot 且不想自行管理服務器的用戶,我們推薦使用雨雲的一鍵雲部署服務 ☁️:
|
||||
AstrBot 已由雨雲官方上架至雲端應用程式平台,可一鍵部署。
|
||||
|
||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||
|
||||
### 桌面客戶端部署
|
||||
#### 在 Replit 上部署
|
||||
|
||||
對於希望在桌面端使用 AstrBot、並以 ChatUI 為主要入口的用戶,我們推薦使用 AstrBot App。
|
||||
|
||||
前往 [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop) 下載並安裝;該方式面向桌面使用,不推薦服務器場景。
|
||||
|
||||
### 啟動器部署
|
||||
|
||||
同樣在桌面端,希望快速部署並實現環境隔離多開的用戶,我們推薦使用 AstrBot Launcher。
|
||||
|
||||
前往 [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) 下載並安裝。
|
||||
|
||||
### 在 Replit 上部署
|
||||
|
||||
Replit 部署由社區維護,適合在線演示和輕量試用場景。
|
||||
社群貢獻的部署方式。
|
||||
|
||||
[](https://repl.it/github/AstrBotDevs/AstrBot)
|
||||
|
||||
### AUR
|
||||
#### Windows 一鍵安裝器部署
|
||||
|
||||
AUR 方式面向 Arch Linux 用戶,適合希望通過系統包管理器安裝 AstrBot 的場景。
|
||||
請參閱官方文件 [使用 Windows 一鍵安裝器部署 AstrBot](https://astrbot.app/deploy/astrbot/windows.html)。
|
||||
|
||||
在終端執行下方命令安裝 `astrbot-git` 包,安裝完成後即可啟動使用。
|
||||
#### CasaOS 部署
|
||||
|
||||
社群貢獻的部署方式。
|
||||
|
||||
請參閱官方文件 [CasaOS 部署](https://astrbot.app/deploy/astrbot/casaos.html)。
|
||||
|
||||
#### 手動部署
|
||||
|
||||
首先安裝 uv:
|
||||
|
||||
```bash
|
||||
yay -S astrbot-git
|
||||
pip install uv
|
||||
```
|
||||
|
||||
**更多部署方式**
|
||||
透過 Git Clone 安裝 AstrBot:
|
||||
|
||||
若你需要面板化或更高自定義部署,可參考 [寶塔面板](https://astrbot.app/deploy/astrbot/btpanel.html)(BT Panel 應用商店安裝)、[1Panel](https://astrbot.app/deploy/astrbot/1panel.html)(1Panel 應用商店安裝)、[CasaOS](https://astrbot.app/deploy/astrbot/casaos.html)(NAS / 家庭服務器可視化部署)和 [手動部署](https://astrbot.app/deploy/astrbot/cli.html)(基於源碼與 `uv` 的完整自定義安裝)。
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
## 支持的消息平台
|
||||
或者請參閱官方文件 [透過原始碼部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html)。
|
||||
|
||||
將 AstrBot 連接到你常用的聊天平台。
|
||||
## 支援的訊息平台
|
||||
|
||||
| 平台 | 維護方 |
|
||||
|---------|---------------|
|
||||
| **QQ** | 官方維護 |
|
||||
| **OneBot v11** | 官方維護 |
|
||||
| **Telegram** | 官方維護 |
|
||||
| **企微應用 & 企微智能機器人** | 官方維護 |
|
||||
| **微信客服 & 微信公眾號** | 官方維護 |
|
||||
| **飛書** | 官方維護 |
|
||||
| **釘釘** | 官方維護 |
|
||||
| **Slack** | 官方維護 |
|
||||
| **Discord** | 官方維護 |
|
||||
| **LINE** | 官方維護 |
|
||||
| **Satori** | 官方維護 |
|
||||
| **Misskey** | 官方維護 |
|
||||
| **Whatsapp (將支持)** | 官方維護 |
|
||||
| [**Matrix**](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | 社區維護 |
|
||||
| [**KOOK**](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | 社區維護 |
|
||||
| [**VoceChat**](https://github.com/HikariFroya/astrbot_plugin_vocechat) | 社區維護 |
|
||||
**官方維護**
|
||||
|
||||
## 支持的模型提供商
|
||||
- QQ(官方平台 & OneBot)
|
||||
- Telegram
|
||||
- 企微應用 & 企微智慧機器人
|
||||
- 微信客服 & 微信公眾號
|
||||
- 飛書
|
||||
- 釘釘
|
||||
- Slack
|
||||
- Discord
|
||||
- Satori
|
||||
- Misskey
|
||||
- Whatsapp(即將支援)
|
||||
- LINE(即將支援)
|
||||
|
||||
| 提供商 | 類型 |
|
||||
|---------|---------------|
|
||||
| 自定義 | 任何 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 | 語音轉文本 |
|
||||
| OpenAI TTS | 文本轉語音 |
|
||||
| Gemini TTS | 文本轉語音 |
|
||||
| GPT-Sovits-Inference | 文本轉語音 |
|
||||
| GPT-Sovits | 文本轉語音 |
|
||||
| FishAudio | 文本轉語音 |
|
||||
| Edge TTS | 文本轉語音 |
|
||||
| 阿里雲百煉 TTS | 文本轉語音 |
|
||||
| Azure TTS | 文本轉語音 |
|
||||
| Minimax TTS | 文本轉語音 |
|
||||
| 火山引擎 TTS | 文本轉語音 |
|
||||
**社群維護**
|
||||
|
||||
## ❤️ Sponsors
|
||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
||||
|
||||
<p align="center">
|
||||
<img alt="sponsors" src="https://sponsors.astrbot.app/?v=1">
|
||||
</p>
|
||||
## 支援的模型服務
|
||||
|
||||
**大型模型服務**
|
||||
|
||||
- OpenAI 及相容服務
|
||||
- Anthropic
|
||||
- Google Gemini
|
||||
- Moonshot AI
|
||||
- 智譜 AI
|
||||
- DeepSeek
|
||||
- Ollama(本機部署)
|
||||
- LM Studio(本機部署)
|
||||
- [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
||||
- [302.AI](https://share.302.ai/rr1M3l)
|
||||
- [小馬算力](https://www.tokenpony.cn/3YPyf)
|
||||
- [矽基流動](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot)
|
||||
- [PPIO 派歐雲](https://ppio.com/user/register?invited_by=AIOONE)
|
||||
- ModelScope
|
||||
- OneAPI
|
||||
|
||||
**LLMOps 平台**
|
||||
|
||||
- Dify
|
||||
- 阿里雲百煉應用
|
||||
- Coze
|
||||
|
||||
**語音轉文字服務**
|
||||
|
||||
- OpenAI Whisper
|
||||
- SenseVoice
|
||||
|
||||
**文字轉語音服務**
|
||||
|
||||
- OpenAI TTS
|
||||
- Gemini TTS
|
||||
- GPT-Sovits-Inference
|
||||
- GPT-Sovits
|
||||
- FishAudio
|
||||
- Edge TTS
|
||||
- 阿里雲百煉 TTS
|
||||
- Azure TTS
|
||||
- Minimax TTS
|
||||
- 火山引擎 TTS
|
||||
|
||||
## ❤️ 貢獻
|
||||
|
||||
歡迎任何 Issues/Pull Requests!只需要將你的更改提交到此項目 :)
|
||||
歡迎任何 Issues/Pull Requests!只需要將您的變更提交到此專案 :)
|
||||
|
||||
### 如何貢獻
|
||||
|
||||
你可以通過查看問題或幫助審核 PR(拉取請求)來貢獻。任何問題或 PR 都歡迎參與,以促進社區貢獻。當然,這些只是建議,你可以以任何方式進行貢獻。對於新功能的添加,請先通過 Issue 討論。
|
||||
建議將功能性PR合併至dev分支,將在測試修改後合併到主分支並發布新版本。
|
||||
為了減少衝突,建議如下:
|
||||
1. 工作分支最好基於 `dev` 分支創建,避免直接在 `main` 分支上工作。
|
||||
2. 提交 PR 時,選擇 `dev` 分支作為目標分支。
|
||||
3. 定期同步 `dev` 分支到本地,多使用git pull。
|
||||
您可以透過檢視問題或協助審核 PR(拉取請求)來貢獻。任何問題或 PR 都歡迎參與,以促進社群貢獻。當然,這些只是建議,您可以以任何方式進行貢獻。對於新功能的新增,請先透過 Issue 討論。
|
||||
|
||||
### 開發環境
|
||||
|
||||
AstrBot 使用 `ruff` 進行代碼格式化和檢查。
|
||||
AstrBot 使用 `ruff` 進行程式碼格式化和檢查。
|
||||
|
||||
```bash
|
||||
git clone https://github.com/AstrBotDevs/AstrBot
|
||||
git switch dev # 切換到開發分支
|
||||
pip install pre-commit # 或者uv tool install pre-commit
|
||||
pip 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 群組
|
||||
|
||||
- 9 群: 1076659624 (新)
|
||||
- 10 群: 1078079676 (新)
|
||||
- 1 群:322154837
|
||||
- 3 群:630166526
|
||||
- 5 群:822130018
|
||||
- 6 群:753075035
|
||||
- 7 群:743746109
|
||||
- 8 群:1030353265
|
||||
- 開發者群(偏閒聊吹水):975206796
|
||||
- 開發者群(正式):1039761811
|
||||
- 開發者群:975206796
|
||||
|
||||
### Discord 頻道
|
||||
### Telegram 群組
|
||||
|
||||
- [Discord](https://discord.gg/hAVk6tgV36)
|
||||
<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>
|
||||
|
||||
## ❤️ Special Thanks
|
||||
|
||||
特別感謝所有 Contributors 和插件開發者對 AstrBot 的貢獻 ❤️
|
||||
特別感謝所有 Contributors 和外掛開發者對 AstrBot 的貢獻 ❤️
|
||||
|
||||
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" />
|
||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
||||
</a>
|
||||
|
||||
此外,本項目的誕生離不開以下開源項目的幫助:
|
||||
此外,本專案的誕生離不開以下開源專案的幫助:
|
||||
|
||||
- [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
|
||||
> 如果本專案對您的生活 / 工作產生了幫助,或者您關注本專案的未來發展,請給專案 Star,這是我們維護這個開源專案的動力 <3
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -293,12 +241,7 @@ bun dev
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
_陪伴與能力從來不應該是對立面。我們希望創造的是一個既能理解情緒、給予陪伴,也能可靠完成工作的機器人。_
|
||||
</details>
|
||||
|
||||
_私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
|
||||
</div>
|
||||
|
||||
304
README_zh.md
304
README_zh.md
@@ -1,304 +0,0 @@
|
||||

|
||||
|
||||
<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/12875" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12875" alt="Soulter%2FAstrBot | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
<a href="https://hellogithub.com/repository/AstrBotDevs/AstrBot" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=d127d50cd5e54c5382328acc3bb25483&claim_uid=ZO9by7qCXgSd6Lp&t=2" alt="Featured|HelloGitHub" 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 应用。
|
||||
|
||||

|
||||
|
||||
## 主要功能
|
||||
|
||||
1. 💯 免费 & 开源。
|
||||
2. ✨ AI 大模型对话,多模态,Agent,MCP,Skills,知识库,人格设定,自动压缩对话。
|
||||
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
|
||||
astrbot init # 仅首次执行此命令以初始化环境
|
||||
astrbot run # astrbot run --backend-only 仅启动后端服务
|
||||
|
||||
# 安装开发版本(更多修复,新功能,但不够稳定,适合开发者)
|
||||
uv tool install git+https://github.com/AstrBotDevs/AstrBot@dev
|
||||
```
|
||||
|
||||
> 需要安装 [uv](https://docs.astral.sh/uv/)。
|
||||
|
||||
> [!NOTE]
|
||||
> 对于 macOS 用户:由于 macOS 安全检查,首次运行 `astrbot` 命令可能需要较长时间(约 10-20 秒)。
|
||||
|
||||
更新 `astrbot`:
|
||||
|
||||
```bash
|
||||
uv tool upgrade astrbot
|
||||
```
|
||||
|
||||
### Docker 部署
|
||||
|
||||
对于熟悉容器、希望获得更稳定且更适合生产环境部署方式的用户,我们推荐使用 Docker / Docker Compose 部署 AstrBot。
|
||||
|
||||
请参考官方文档 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot)。
|
||||
|
||||
### 在 雨云 上部署
|
||||
|
||||
对于希望一键部署 AstrBot 且不想自行管理服务器的用户,我们推荐使用雨云的一键云部署服务 ☁️:
|
||||
|
||||
[](https://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 部署由社区维护,适合在线演示和轻量试用场景。
|
||||
|
||||
[](https://repl.it/github/AstrBotDevs/AstrBot)
|
||||
|
||||
### AUR
|
||||
|
||||
AUR 方式面向 Arch Linux 用户,适合希望通过系统包管理器安装 AstrBot 的场景。
|
||||
|
||||
在终端执行下方命令安装 `astrbot-git` 包,安装完成后即可启动使用。
|
||||
|
||||
```bash
|
||||
yay -S astrbot-git
|
||||
```
|
||||
|
||||
**更多部署方式**
|
||||
|
||||
若你需要面板化或更高自定义部署,可参考 [宝塔面板](https://astrbot.app/deploy/astrbot/btpanel.html)(BT Panel 应用商店安装)、[1Panel](https://astrbot.app/deploy/astrbot/1panel.html)(1Panel 应用商店安装)、[CasaOS](https://astrbot.app/deploy/astrbot/casaos.html)(NAS / 家庭服务器可视化部署)和 [手动部署](https://astrbot.app/deploy/astrbot/cli.html)(基于源码与 `uv` 的完整自定义安装)。
|
||||
|
||||
## 支持的消息平台
|
||||
|
||||
将 AstrBot 连接到你常用的聊天平台。
|
||||
|
||||
| 平台 | 维护方 |
|
||||
|---------|---------------|
|
||||
| **QQ** | 官方维护 |
|
||||
| **OneBot v11** | 官方维护 |
|
||||
| **Telegram** | 官方维护 |
|
||||
| **企微应用 & 企微智能机器人** | 官方维护 |
|
||||
| **微信客服 & 微信公众号** | 官方维护 |
|
||||
| **飞书** | 官方维护 |
|
||||
| **钉钉** | 官方维护 |
|
||||
| **Slack** | 官方维护 |
|
||||
| **Discord** | 官方维护 |
|
||||
| **LINE** | 官方维护 |
|
||||
| **Satori** | 官方维护 |
|
||||
| **Misskey** | 官方维护 |
|
||||
| **Whatsapp (将支持)** | 官方维护 |
|
||||
| [**Matrix**](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | 社区维护 |
|
||||
| [**KOOK**](https://github.com/wuyan1003/astrbot_plugin_kook_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 | 语音转文本 |
|
||||
| OpenAI TTS | 文本转语音 |
|
||||
| Gemini TTS | 文本转语音 |
|
||||
| GPT-Sovits-Inference | 文本转语音 |
|
||||
| GPT-Sovits | 文本转语音 |
|
||||
| FishAudio | 文本转语音 |
|
||||
| Edge TTS | 文本转语音 |
|
||||
| 阿里云百炼 TTS | 文本转语音 |
|
||||
| Azure TTS | 文本转语音 |
|
||||
| Minimax 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 群组
|
||||
|
||||
- 9 群: 1076659624 (新)
|
||||
- 10 群: 1078079676 (新)
|
||||
- 1 群:322154837
|
||||
- 3 群:630166526
|
||||
- 5 群:822130018
|
||||
- 6 群:753075035
|
||||
- 7 群:743746109
|
||||
- 8 群:1030353265
|
||||
- 开发者群(偏闲聊吹水):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=200&columns=14" />
|
||||
</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">
|
||||
|
||||
[](https://star-history.com/#astrbotdevs/astrbot&Date)
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
_陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。_
|
||||
|
||||
_私は、高性能ですから!_
|
||||
|
||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||
|
||||
</div>
|
||||
@@ -1,16 +1,3 @@
|
||||
from __future__ import annotations
|
||||
from .core.log import LogManager
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core import logger as logger
|
||||
|
||||
__all__ = ["logger"]
|
||||
|
||||
|
||||
def __getattr__(name: str) -> Any:
|
||||
if name == "logger":
|
||||
from .core import logger
|
||||
|
||||
return logger
|
||||
raise AttributeError(name)
|
||||
logger = LogManager.GetLogger(log_name="astrbot")
|
||||
|
||||
@@ -1,151 +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,
|
||||
)
|
||||
from astrbot.runtime_bootstrap import initialize_runtime_bootstrap
|
||||
|
||||
initialize_runtime_bootstrap()
|
||||
|
||||
|
||||
# 将父目录添加到 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))
|
||||
@@ -1,5 +0,0 @@
|
||||
"""
|
||||
Astbot内部实现
|
||||
外部模块请勿导入
|
||||
|
||||
"""
|
||||
@@ -1,57 +0,0 @@
|
||||
"""
|
||||
ABP (AstrBot Protocol) client - in-process star communication.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
|
||||
class BaseAstrbotAbpClient(ABC):
|
||||
"""
|
||||
ABP client: in-process star (plugin) communication.
|
||||
|
||||
Stars register themselves; client delegates calls to registered instances.
|
||||
|
||||
Subclass must implement:
|
||||
- connect() -> None
|
||||
- register_star(name, instance) -> None
|
||||
- unregister_star(name) -> None
|
||||
- call_star_tool(star, tool, args) -> Any
|
||||
- shutdown() -> None
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def connected(self) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
async def connect(self) -> None:
|
||||
"""Lightweight: just sets connected=True."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def register_star(self, star_name: str, star_instance: Any) -> None:
|
||||
"""Add star to internal registry."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def unregister_star(self, star_name: str) -> None:
|
||||
"""Remove star from registry (idempotent)."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def call_star_tool(
|
||||
self,
|
||||
star_name: str,
|
||||
tool_name: str,
|
||||
arguments: dict[str, Any],
|
||||
) -> Any:
|
||||
"""Delegate to star_instance.call_tool(tool_name, arguments)."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""Set connected=False, cancel pending requests."""
|
||||
...
|
||||
@@ -1,66 +0,0 @@
|
||||
"""
|
||||
ACP (AstrBot Communication Protocol) client.
|
||||
|
||||
Transport: TCP | Unix Socket
|
||||
Messages: JSON with Content-Length header
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
|
||||
class BaseAstrbotAcpClient(ABC):
|
||||
"""
|
||||
ACP client: connects to ACP servers via TCP or Unix socket.
|
||||
|
||||
Subclass must implement:
|
||||
- connect() -> None
|
||||
- connect_to_server(host, port) -> None
|
||||
- connect_to_unix_socket(path) -> None
|
||||
- call_tool(server, tool, args) -> Any
|
||||
- send_notification(method, params) -> None
|
||||
- shutdown() -> None
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def connected(self) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
async def connect(self) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def connect_to_server(self, host: str, port: int) -> None:
|
||||
"""Connect via TCP."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def connect_to_unix_socket(self, socket_path: str) -> None:
|
||||
"""Connect via Unix domain socket."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def call_tool(
|
||||
self,
|
||||
server_name: str,
|
||||
tool_name: str,
|
||||
arguments: dict[str, Any],
|
||||
) -> Any:
|
||||
"""Call tool on server, return result."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def send_notification(
|
||||
self,
|
||||
method: str,
|
||||
params: dict[str, Any],
|
||||
) -> None:
|
||||
"""Send one-way notification."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""Close connection, cancel pending requests."""
|
||||
...
|
||||
@@ -1,68 +0,0 @@
|
||||
"""
|
||||
ACP (AstrBot Communication Protocol) server.
|
||||
|
||||
Transport: TCP listening socket
|
||||
Messages: JSON with Content-Length header
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
|
||||
class BaseAstrbotAcpServer(ABC):
|
||||
"""
|
||||
ACP server: listens for client connections, exposes tools.
|
||||
|
||||
Subclass must implement:
|
||||
- start(host, port) -> None
|
||||
- register_tool(name, handler) -> None
|
||||
- register_notification_handler(name, handler) -> None
|
||||
- broadcast_notification(method, params) -> None
|
||||
- shutdown() -> None
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def running(self) -> bool:
|
||||
"""True if server is accepting connections."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def start(self, host: str = "127.0.0.1", port: int = 8765) -> None:
|
||||
"""Bind and listen. Block until shutdown."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def register_tool(
|
||||
self,
|
||||
name: str,
|
||||
handler: Callable[..., Any],
|
||||
) -> None:
|
||||
"""Register async tool handler (receives params dict, returns result)."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def register_notification_handler(
|
||||
self,
|
||||
name: str,
|
||||
handler: Callable[..., Any],
|
||||
) -> None:
|
||||
"""Register async notification handler (receives params dict)."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def broadcast_notification(
|
||||
self,
|
||||
method: str,
|
||||
params: dict[str, Any],
|
||||
) -> None:
|
||||
"""Send notification to all connected clients."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""Stop accepting, close all client connections."""
|
||||
...
|
||||
@@ -1,73 +0,0 @@
|
||||
"""
|
||||
AstrBot Gateway - HTTP/WebSocket API server.
|
||||
|
||||
Built on FastAPI, provides:
|
||||
- HTTP REST API (stats, inspector, config)
|
||||
- WebSocket for real-time events
|
||||
- Static file serving (dashboard)
|
||||
- Authentication (JWT/API key)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class BaseAstrbotGateway(ABC):
|
||||
"""
|
||||
Gateway: HTTP/WebSocket server built on FastAPI.
|
||||
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ FastAPI App │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ REST Endpoints WebSocket │
|
||||
│ ├─ GET /api/stats ├─ /ws (connection manager)│
|
||||
│ ├─ GET /api/inspector/* │ │
|
||||
│ ├─ GET /api/memory/* │ │
|
||||
│ └─ ... │ │
|
||||
│ │
|
||||
│ Middleware: CORS, Auth, Logging │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Orchestrator │
|
||||
│ (owns protocol clients)│
|
||||
└─────────────────────────┘
|
||||
|
||||
Routes (typical):
|
||||
GET / → Dashboard static files
|
||||
GET /api/stats → System statistics
|
||||
GET /api/inspector/stars → List registered stars
|
||||
WS /ws → WebSocket for real-time events
|
||||
|
||||
serve() Lifecycle:
|
||||
1. Create FastAPI app
|
||||
2. Register routes
|
||||
3. Start WebSocket manager
|
||||
4. Bind to host:port
|
||||
5. Run ASGI server (uvicorn/hypercorn)
|
||||
6. Block until shutdown
|
||||
7. Close all connections
|
||||
|
||||
Subclass must implement:
|
||||
- serve(): start server, block until shutdown
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
async def serve(self) -> None:
|
||||
"""
|
||||
Start gateway server - blocks until shutdown.
|
||||
|
||||
Should:
|
||||
1. Create FastAPI app with routes
|
||||
2. Configure CORS, auth middleware
|
||||
3. Start WebSocket connection manager
|
||||
4. Bind to ASTRBOT_PORT (default 6185)
|
||||
5. Run ASGI server
|
||||
6. Handle graceful shutdown on SIGTERM/SIGINT
|
||||
|
||||
Raises:
|
||||
OSError: address already in use
|
||||
"""
|
||||
...
|
||||
@@ -1,352 +0,0 @@
|
||||
"""
|
||||
AstrBot Orchestrator - core runtime lifecycle manager.
|
||||
|
||||
Architecture
|
||||
============
|
||||
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Orchestrator │
|
||||
│ (owns lifecycle of all protocol clients + stars) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌──────────────┼──────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ LSP │ │ MCP │ │ ACP │
|
||||
│ Client │ │ Client │ │ Client │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
LSP Servers MCP Servers ACP Services
|
||||
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ ABP Client │
|
||||
│ (in-process star registry) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────┐
|
||||
│ Stars │
|
||||
│(Plugins) │
|
||||
└─────────┘
|
||||
|
||||
|
||||
Lifecycle State Machine
|
||||
=======================
|
||||
|
||||
States:
|
||||
┌─────────┐
|
||||
│ INIT │───► orchestrator created, clients not initialized
|
||||
└────┬────┘
|
||||
│ start()
|
||||
▼
|
||||
┌─────────┐
|
||||
│ RUNNING │◄─── run_loop() executing
|
||||
└────┬────┘
|
||||
│ shutdown()
|
||||
▼
|
||||
┌──────────┐
|
||||
│ SHUTDOWN │─── all clients closed, ready for GC
|
||||
└──────────┘
|
||||
|
||||
Transitions:
|
||||
INIT + start() ──► RUNNING
|
||||
RUNNING + shutdown() ──► SHUTDOWN
|
||||
|
||||
For each protocol client, the orchestrator:
|
||||
1. Creates instance in __init__
|
||||
2. Calls connect() to initialize
|
||||
3. Calls protocol-specific setup (connect_to_server, etc)
|
||||
4. Manages via run_loop() heartbeat
|
||||
5. Calls shutdown() on final cleanup
|
||||
|
||||
|
||||
Star Registration Flow
|
||||
=====================
|
||||
|
||||
orchestrator.register_star("my-star", MyStar())
|
||||
│
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ ABP Client │
|
||||
│ .register_star() │
|
||||
└───────────────────┘
|
||||
│
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ Internal dict │
|
||||
│ {"my-star": obj} │
|
||||
└───────────────────┘
|
||||
|
||||
|
||||
Message Routing (conceptual)
|
||||
===========================
|
||||
|
||||
External Tool Call
|
||||
│
|
||||
▼
|
||||
┌──────────────┐ list_tools() ┌──────────────┐
|
||||
│ MCP Client │────────────────────►│ MCP Server │
|
||||
└──────────────┘◄────────────────────└──────────────┘
|
||||
│ tool result
|
||||
▼
|
||||
┌──────────────┐ call_tool() ┌──────────────┐
|
||||
│ ABP │────────────────────►│ Star │
|
||||
│ Client │◄────────────────────└──────────────┘
|
||||
└──────────────┘ tool result
|
||||
│
|
||||
▼
|
||||
Return to caller
|
||||
|
||||
|
||||
run_loop() Responsibilities
|
||||
===========================
|
||||
|
||||
while running:
|
||||
│─ check LSP server health (ping/heartbeat)
|
||||
│─ check MCP session status (reconnect if needed)
|
||||
│─ check ACP client connections
|
||||
│─ process any pending star notifications
|
||||
│─ sleep(SLEEP_INTERVAL)
|
||||
|
||||
|
||||
Shutdown Sequence
|
||||
==================
|
||||
|
||||
shutdown()
|
||||
│
|
||||
├─ set _running = False
|
||||
│
|
||||
├─ LSP.shutdown()
|
||||
│ └─ send "shutdown" request
|
||||
│ └─ terminate subprocess
|
||||
│
|
||||
├─ ACP.shutdown()
|
||||
│ └─ close TCP/Unix connections
|
||||
│
|
||||
├─ ABP.shutdown()
|
||||
│ └─ cancel pending requests
|
||||
│
|
||||
└─ MCP.cleanup()
|
||||
└─ close all sessions
|
||||
└─ cleanup subprocesses
|
||||
|
||||
|
||||
Exception Handling
|
||||
==================
|
||||
|
||||
Each protocol client should:
|
||||
- Catch connection errors
|
||||
- Attempt reconnection with exponential backoff
|
||||
- Log errors but don't crash run_loop
|
||||
- Raise on irrecoverable failures
|
||||
|
||||
The orchestrator run_loop should:
|
||||
- Catch CancelledError on shutdown
|
||||
- Catch Exception and log (don't crash)
|
||||
- Ensure cleanup runs in finally block
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from astrbot._internal.protocols.abp.client import AstrbotAbpClient
|
||||
from astrbot._internal.protocols.acp.client import AstrbotAcpClient
|
||||
from astrbot._internal.protocols.lsp.client import AstrbotLspClient
|
||||
from astrbot._internal.protocols.mcp.client import McpClient
|
||||
|
||||
|
||||
#: Default heartbeat interval for run_loop()
|
||||
DEFAULT_SLEEP_INTERVAL: float = 5.0
|
||||
|
||||
|
||||
class BaseAstrbotOrchestrator(ABC):
|
||||
"""
|
||||
Core runtime: owns lifecycle of all protocol clients and stars.
|
||||
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Protocol Clients (always present, never None after init) │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ lsp: Language Server Protocol │
|
||||
│ Purpose: code completion, diagnostics, hover, etc │
|
||||
│ Transport: stdio subprocess │
|
||||
│ │
|
||||
│ mcp: Model Context Protocol │
|
||||
│ Purpose: external tool access │
|
||||
│ Transport: stdio | SSE | HTTP │
|
||||
│ │
|
||||
│ acp: AstrBot Communication Protocol │
|
||||
│ Purpose: inter-service communication │
|
||||
│ Transport: TCP | Unix Socket │
|
||||
│ │
|
||||
│ abp: AstrBot Protocol │
|
||||
│ Purpose: in-process star (plugin) communication │
|
||||
│ Transport: direct method calls │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ Star Registry │
|
||||
├────────────────────────────────────────────────────────────┤
|
||||
│ _stars: dict[str, Any] │
|
||||
│ Stars are plugins registered by name │
|
||||
│ ABP client delegates calls to registered stars │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
|
||||
Subclass must implement:
|
||||
- __init__(): create all protocol client instances
|
||||
- run_loop(): main event loop (block until shutdown)
|
||||
- register_star(name, instance): add to registry + ABP
|
||||
- unregister_star(name): remove from registry + ABP
|
||||
- shutdown(): clean up all clients
|
||||
"""
|
||||
|
||||
#: LSP client for language intelligence
|
||||
lsp: AstrbotLspClient
|
||||
|
||||
#: MCP client for external tools
|
||||
mcp: McpClient
|
||||
|
||||
#: ACP client for inter-service communication
|
||||
acp: AstrbotAcpClient
|
||||
|
||||
#: ABP client for in-process star communication
|
||||
abp: AstrbotAbpClient
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Initialize orchestrator and all protocol clients.
|
||||
|
||||
After __init__, all clients exist but are not connected.
|
||||
Call start() or run_loop() to begin operation.
|
||||
|
||||
Example:
|
||||
class MyOrchestrator(BaseAstrbotOrchestrator):
|
||||
def __init__(self):
|
||||
self.lsp = AstrbotLspClient()
|
||||
self.mcp = McpClient()
|
||||
self.acp = AstrbotAcpClient()
|
||||
self.abp = AstrbotAbpClient()
|
||||
self._stars: dict[str, Any] = {}
|
||||
self._running = False
|
||||
"""
|
||||
self._stars: dict[str, Any] = {}
|
||||
self._running: bool = False
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
"""True if run_loop() is executing."""
|
||||
return self._running
|
||||
|
||||
@abstractmethod
|
||||
async def start(self) -> None:
|
||||
"""
|
||||
Initialize all protocol clients.
|
||||
|
||||
Called once before run_loop(). Should:
|
||||
1. Call lsp.connect()
|
||||
2. Call mcp.connect()
|
||||
3. Call acp.connect()
|
||||
4. Call abp.connect()
|
||||
5. Set _running = True
|
||||
|
||||
Raises:
|
||||
Exception: if any client fails to initialize
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def run_loop(self) -> None:
|
||||
"""
|
||||
Main event loop - blocks until shutdown.
|
||||
|
||||
Execution:
|
||||
self._running = True
|
||||
try:
|
||||
while self._running:
|
||||
await self._heartbeat()
|
||||
await anyio.sleep(DEFAULT_SLEEP_INTERVAL)
|
||||
except asyncio.CancelledError:
|
||||
pass # shutdown requested
|
||||
finally:
|
||||
self._running = False
|
||||
|
||||
_heartbeat() responsibilities:
|
||||
- Check LSP server health (optional ping)
|
||||
- Check MCP session status, reconnect if needed
|
||||
- Check ACP connections
|
||||
- Process any pending star notifications
|
||||
|
||||
Raises:
|
||||
asyncio.CancelledError: when shutdown() called
|
||||
|
||||
Note:
|
||||
Subclass defines _heartbeat() for periodic tasks.
|
||||
This method only handles the loop control.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def register_star(self, name: str, star_instance: Any) -> None:
|
||||
"""
|
||||
Register a star (plugin) with the orchestrator.
|
||||
|
||||
Args:
|
||||
name: Unique identifier for the star
|
||||
instance: Star plugin instance (must have .call_tool() method)
|
||||
|
||||
Does:
|
||||
self._stars[name] = star_instance
|
||||
self.abp.register_star(name, star_instance)
|
||||
|
||||
Raises:
|
||||
ValueError: if name already registered
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def unregister_star(self, name: str) -> None:
|
||||
"""
|
||||
Unregister a star (plugin) from the orchestrator.
|
||||
|
||||
Args:
|
||||
name: Identifier of star to remove
|
||||
|
||||
Does:
|
||||
del self._stars[name]
|
||||
self.abp.unregister_star(name)
|
||||
|
||||
Note:
|
||||
Idempotent - does nothing if name not found.
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def get_star(self, name: str) -> Any | None:
|
||||
"""Get registered star by name. Returns None if not found."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def list_stars(self) -> list[str]:
|
||||
"""Return list of registered star names."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
Graceful shutdown of orchestrator and all clients.
|
||||
|
||||
Execution order:
|
||||
1. self._running = False (stop run_loop)
|
||||
2. await lsp.shutdown()
|
||||
3. await acp.shutdown()
|
||||
4. await abp.shutdown()
|
||||
5. await mcp.cleanup()
|
||||
|
||||
Does NOT unregister stars - caller should do that first.
|
||||
|
||||
After shutdown, orchestrator is ready for garbage collection.
|
||||
"""
|
||||
...
|
||||
@@ -1,114 +0,0 @@
|
||||
"""
|
||||
LSP (Language Server Protocol) client.
|
||||
|
||||
Transport: stdio subprocess
|
||||
Messages: JSON-RPC 2.0 with Content-Length header
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class LspMessage:
|
||||
"""JSON-RPC 2.0 message."""
|
||||
|
||||
jsonrpc: str = "2.0"
|
||||
id: int | str | None = None
|
||||
method: str | None = None
|
||||
params: dict[str, Any] | None = None
|
||||
result: Any = None
|
||||
error: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class LspRequest(LspMessage):
|
||||
"""Outgoing request."""
|
||||
|
||||
def __init__(self, method: str, params: dict[str, Any] | None = None) -> None:
|
||||
self.id = id(self)
|
||||
self.method = method
|
||||
self.params = params
|
||||
|
||||
|
||||
class LspResponse(LspMessage):
|
||||
"""Incoming response."""
|
||||
|
||||
|
||||
class LspNotification(LspMessage):
|
||||
"""Incoming notification (no id)."""
|
||||
|
||||
|
||||
class BaseAstrbotLspClient(ABC):
|
||||
"""
|
||||
LSP client: connects to LSP servers via stdio subprocess.
|
||||
|
||||
Subclass must implement:
|
||||
- connect() -> None
|
||||
- connect_to_server(command, workspace_uri) -> None
|
||||
- send_request(method, params) -> dict
|
||||
- send_notification(method, params) -> None
|
||||
- shutdown() -> None
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def connected(self) -> bool:
|
||||
"""True if connected to an LSP server."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def connect(self) -> None:
|
||||
self._connected = False
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def connect_to_server(
|
||||
self,
|
||||
command: list[str],
|
||||
workspace_uri: str,
|
||||
) -> None:
|
||||
"""
|
||||
Start LSP server subprocess and complete handshake.
|
||||
|
||||
Steps:
|
||||
1. Spawn subprocess with stdin/stdout pipes
|
||||
2. Send initialize request
|
||||
3. Wait for response
|
||||
4. Send initialized notification
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def send_request(
|
||||
self,
|
||||
method: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
) -> Any:
|
||||
"""
|
||||
Send JSON-RPC request and return result.
|
||||
|
||||
Raises:
|
||||
RuntimeError: not connected
|
||||
Exception: server returned error
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def send_notification(
|
||||
self,
|
||||
method: str,
|
||||
params: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Send JSON-RPC notification (no response expected).
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def shutdown(self) -> None:
|
||||
"""Send shutdown, terminate subprocess, cleanup."""
|
||||
...
|
||||
@@ -1,95 +0,0 @@
|
||||
"""
|
||||
MCP (Model Context Protocol) client.
|
||||
|
||||
Transport: stdio | SSE | streamable_http
|
||||
Messages: JSON-RPC 2.0
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any, Literal, TypedDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
class McpServerConfig(TypedDict, total=False):
|
||||
"""MCP server configuration."""
|
||||
|
||||
# Stdio transport
|
||||
command: str
|
||||
args: list[str]
|
||||
env: dict[str, str]
|
||||
cwd: str
|
||||
|
||||
# HTTP transport
|
||||
url: str
|
||||
headers: dict[str, str]
|
||||
transport: Literal["sse", "streamable_http"]
|
||||
|
||||
|
||||
class McpToolInfo(TypedDict):
|
||||
"""MCP tool descriptor."""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
inputSchema: dict[str, Any]
|
||||
|
||||
|
||||
class BaseAstrbotMcpClient(ABC):
|
||||
"""
|
||||
MCP client: connects to MCP servers for external tools.
|
||||
|
||||
Subclass must implement:
|
||||
- connect() -> None
|
||||
- connect_to_server(config, name) -> None
|
||||
- list_tools() -> list[McpToolInfo]
|
||||
- call_tool(name, args, timeout) -> CallToolResult
|
||||
- cleanup() -> None
|
||||
"""
|
||||
|
||||
session: Any # mcp.ClientSession
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def connected(self) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
async def connect(self) -> None:
|
||||
"""Initialize client session."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def connect_to_server(
|
||||
self,
|
||||
config: McpServerConfig,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""
|
||||
Connect to MCP server.
|
||||
|
||||
Stdio: {"command": "python", "args": ["server.py"], "env": {...}}
|
||||
HTTP: {"url": "https://...", "transport": "sse"}
|
||||
"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def list_tools(self) -> list[McpToolInfo]:
|
||||
"""Call tools/list and return tools."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any],
|
||||
read_timeout_seconds: int = 60,
|
||||
) -> Any:
|
||||
"""Call tools/call with reconnection support."""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def cleanup(self) -> None:
|
||||
"""Close all server connections."""
|
||||
...
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Gateway module - FastAPI server for the dashboard backend."""
|
||||
|
||||
from .server import AstrbotGateway
|
||||
from .ws_manager import WebSocketManager
|
||||
|
||||
__all__ = ["AstrbotGateway", "WebSocketManager"]
|
||||
@@ -1,4 +0,0 @@
|
||||
"""
|
||||
依赖注入
|
||||
|
||||
"""
|
||||
@@ -1,248 +0,0 @@
|
||||
"""
|
||||
AstrBot Gateway - FastAPI server for the dashboard backend.
|
||||
|
||||
Provides REST API endpoints and WebSocket connections for the frontend dashboard.
|
||||
The gateway acts as the communication bridge between the dashboard and the orchestrator.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot._internal.abc.base_astrbot_gateway import BaseAstrbotGateway
|
||||
from astrbot._internal.abc.base_astrbot_orchestrator import BaseAstrbotOrchestrator
|
||||
from astrbot._internal.geteway.ws_manager import WebSocketManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
else:
|
||||
try:
|
||||
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
except ImportError:
|
||||
logger.warning("FastAPI not installed, gateway unavailable.")
|
||||
FastAPI = cast(Any, None)
|
||||
WebSocket = cast(Any, None)
|
||||
WebSocketDisconnect = cast(Any, None)
|
||||
CORSMiddleware = cast(Any, None)
|
||||
|
||||
log = logger
|
||||
|
||||
|
||||
class AstrbotGateway(BaseAstrbotGateway):
|
||||
"""
|
||||
FastAPI-based gateway server for AstrBot.
|
||||
|
||||
Handles:
|
||||
- REST API endpoints for configuration and stats
|
||||
- WebSocket connections for real-time communication
|
||||
- CORS middleware for dashboard access
|
||||
"""
|
||||
|
||||
def __init__(self, orchestrator: BaseAstrbotOrchestrator) -> None:
|
||||
self.orchestrator = orchestrator
|
||||
self.ws_manager = WebSocketManager()
|
||||
self._app: FastAPI | None = None
|
||||
self._host = "0.0.0.0"
|
||||
self._port = 8765
|
||||
|
||||
async def serve(self) -> None:
|
||||
"""
|
||||
Start the gateway server.
|
||||
|
||||
Creates and runs a FastAPI application with WebSocket support.
|
||||
"""
|
||||
if FastAPI is None:
|
||||
raise RuntimeError("FastAPI is not installed")
|
||||
|
||||
log.info(f"Starting AstrBot Gateway on {self._host}:{self._port}")
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
log.info("Gateway server started.")
|
||||
yield
|
||||
# Shutdown
|
||||
await self.ws_manager.broadcast({"type": "server_shutdown"})
|
||||
log.info("Gateway server stopped.")
|
||||
|
||||
self._app = FastAPI(
|
||||
title="AstrBot Gateway",
|
||||
description="Backend API for AstrBot dashboard",
|
||||
version="1.0.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# CORS middleware
|
||||
self._app.add_middleware(
|
||||
cast(Any, CORSMiddleware),
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include routers
|
||||
self._setup_routes()
|
||||
|
||||
# Run with uvicorn
|
||||
import uvicorn
|
||||
|
||||
config = uvicorn.Config(
|
||||
self._app,
|
||||
host=self._host,
|
||||
port=self._port,
|
||||
log_level="info",
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
await server.serve()
|
||||
|
||||
def _setup_routes(self) -> None:
|
||||
"""Set up API routes."""
|
||||
if self._app is None:
|
||||
return
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
# Health check
|
||||
@self._app.get("/health")
|
||||
async def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
# WebSocket endpoint
|
||||
@self._app.websocket("/ws")
|
||||
async def websocket_endpoint(ws: WebSocket):
|
||||
await self.ws_manager.connect(ws)
|
||||
try:
|
||||
while True:
|
||||
data = await ws.receive_text()
|
||||
try:
|
||||
message = json.loads(data)
|
||||
response = await self._handle_ws_message(message)
|
||||
if response:
|
||||
await ws.send_json(response)
|
||||
except json.JSONDecodeError:
|
||||
await ws.send_json({"error": "Invalid JSON"})
|
||||
except WebSocketDisconnect:
|
||||
self.ws_manager.disconnect(ws)
|
||||
|
||||
# Stats router
|
||||
stats_router = APIRouter(prefix="/api/stats", tags=["stats"])
|
||||
|
||||
@stats_router.get("/overview")
|
||||
async def get_overview():
|
||||
return await self._get_stats_overview()
|
||||
|
||||
self._app.include_router(stats_router)
|
||||
|
||||
# Inspector router
|
||||
inspector_router = APIRouter(prefix="/api/inspector", tags=["inspector"])
|
||||
|
||||
@inspector_router.get("/stars")
|
||||
async def list_stars():
|
||||
return await self._list_stars()
|
||||
|
||||
@inspector_router.get("/stars/{star_name}")
|
||||
async def get_star(star_name: str):
|
||||
return await self._get_star_detail(star_name)
|
||||
|
||||
self._app.include_router(inspector_router)
|
||||
|
||||
# Memory router
|
||||
memory_router = APIRouter(prefix="/api/memory", tags=["memory"])
|
||||
|
||||
@memory_router.get("/")
|
||||
async def get_memory():
|
||||
return await self._get_memory_info()
|
||||
|
||||
self._app.include_router(memory_router)
|
||||
|
||||
async def _handle_ws_message(
|
||||
self, message: dict[str, Any]
|
||||
) -> dict[str, Any] | None:
|
||||
"""
|
||||
Handle an incoming WebSocket message.
|
||||
|
||||
Args:
|
||||
message: Parsed JSON message from the client
|
||||
|
||||
Returns:
|
||||
Response message to send back, or None for no response
|
||||
"""
|
||||
msg_type = message.get("type")
|
||||
data = message.get("data", {})
|
||||
|
||||
if msg_type == "ping":
|
||||
return {"type": "pong", "data": {}}
|
||||
|
||||
if msg_type == "call_tool":
|
||||
return await self._handle_call_tool(data)
|
||||
|
||||
if msg_type == "get_stars":
|
||||
return {"type": "stars_list", "data": await self._list_stars()}
|
||||
|
||||
return {
|
||||
"type": "error",
|
||||
"data": {"message": f"Unknown message type: {msg_type}"},
|
||||
}
|
||||
|
||||
async def _handle_call_tool(self, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Handle a tool call request via WebSocket."""
|
||||
star_name = data.get("star")
|
||||
tool_name = data.get("tool")
|
||||
arguments = data.get("arguments", {})
|
||||
|
||||
if not star_name or not tool_name:
|
||||
return {
|
||||
"type": "tool_result",
|
||||
"data": {"error": "Missing star or tool name"},
|
||||
}
|
||||
|
||||
try:
|
||||
result = await self.orchestrator.abp.call_star_tool(
|
||||
star_name, tool_name, arguments
|
||||
)
|
||||
return {"type": "tool_result", "data": {"result": result}}
|
||||
except Exception as e:
|
||||
return {"type": "tool_result", "data": {"error": str(e)}}
|
||||
|
||||
async def _get_stats_overview(self) -> dict[str, Any]:
|
||||
"""Get overview statistics."""
|
||||
return {
|
||||
"stars_count": len(self.orchestrator.abp._stars),
|
||||
"lsp_connected": self.orchestrator.lsp._connected,
|
||||
"mcp_sessions": getattr(self.orchestrator.mcp, "session", None) is not None,
|
||||
"acp_clients": len(getattr(self.orchestrator.acp, "_clients", [])),
|
||||
}
|
||||
|
||||
async def _list_stars(self) -> list[dict[str, Any]]:
|
||||
"""List all registered stars."""
|
||||
stars = []
|
||||
for name in self.orchestrator.abp._stars:
|
||||
stars.append({"name": name, "status": "active"})
|
||||
return stars
|
||||
|
||||
async def _get_star_detail(self, star_name: str) -> dict[str, Any]:
|
||||
"""Get details of a specific star."""
|
||||
star = self.orchestrator.abp._stars.get(star_name)
|
||||
if not star:
|
||||
return {"error": f"Star '{star_name}' not found"}
|
||||
return {"name": star_name, "status": "active"}
|
||||
|
||||
async def _get_memory_info(self) -> dict[str, Any]:
|
||||
"""Get memory usage information."""
|
||||
import gc
|
||||
|
||||
gc.collect()
|
||||
return {
|
||||
"gc_objects": len(gc.get_objects()),
|
||||
"python_memory": "N/A", # Would need psutil for actual values
|
||||
}
|
||||
|
||||
def set_listen_address(self, host: str, port: int) -> None:
|
||||
"""Set the listen address for the gateway server."""
|
||||
self._host = host
|
||||
self._port = port
|
||||
@@ -1,103 +0,0 @@
|
||||
"""
|
||||
WebSocket connection manager for the AstrBot gateway.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import anyio
|
||||
|
||||
from astrbot import logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from fastapi import WebSocket
|
||||
else:
|
||||
try:
|
||||
from fastapi import WebSocket
|
||||
except ImportError:
|
||||
logger.warning("FastAPI not installed, WebSocketManager unavailable.")
|
||||
WebSocket = cast(Any, None)
|
||||
|
||||
log = logger
|
||||
|
||||
|
||||
class WebSocketManager:
|
||||
"""
|
||||
Manages all active WebSocket connections.
|
||||
|
||||
Provides connection/disconnection handling and broadcast capabilities.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._connections: set[WebSocket] = set()
|
||||
self._lock = anyio.Lock()
|
||||
|
||||
async def connect(self, websocket: WebSocket) -> None:
|
||||
"""Accept and register a new WebSocket connection."""
|
||||
await websocket.accept()
|
||||
async with self._lock:
|
||||
self._connections.add(websocket)
|
||||
log.debug(f"WebSocket connected. Total: {len(self._connections)}")
|
||||
|
||||
async def disconnect(self, websocket: WebSocket) -> None:
|
||||
"""Remove a WebSocket connection."""
|
||||
async with self._lock:
|
||||
self._connections.discard(websocket)
|
||||
log.debug(f"WebSocket disconnected. Total: {len(self._connections)}")
|
||||
|
||||
async def send_json(self, websocket: WebSocket, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Send JSON data to a specific WebSocket.
|
||||
|
||||
Args:
|
||||
websocket: Target WebSocket connection
|
||||
data: Data to send (must be JSON-serializable)
|
||||
"""
|
||||
try:
|
||||
await websocket.send_json(data)
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to send to WebSocket: {e}")
|
||||
await self.disconnect(websocket)
|
||||
|
||||
async def broadcast(self, data: dict[str, Any]) -> None:
|
||||
"""
|
||||
Broadcast JSON data to all connected WebSockets.
|
||||
|
||||
Args:
|
||||
data: Data to broadcast (must be JSON-serializable)
|
||||
"""
|
||||
async with self._lock:
|
||||
connections = list(self._connections)
|
||||
|
||||
for conn in connections:
|
||||
try:
|
||||
await conn.send_json(data)
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to broadcast to WebSocket: {e}")
|
||||
async with self._lock:
|
||||
self._connections.discard(conn)
|
||||
|
||||
async def send_to(
|
||||
self, websocket: WebSocket, message: str | dict[str, Any]
|
||||
) -> None:
|
||||
"""
|
||||
Send a message to a specific WebSocket.
|
||||
|
||||
Args:
|
||||
websocket: Target WebSocket connection
|
||||
message: Message to send (string or dict)
|
||||
"""
|
||||
try:
|
||||
if isinstance(message, str):
|
||||
await websocket.send_text(message)
|
||||
else:
|
||||
await websocket.send_json(message)
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to send to WebSocket: {e}")
|
||||
await self.disconnect(websocket)
|
||||
|
||||
@property
|
||||
def connection_count(self) -> int:
|
||||
"""Return the number of active connections."""
|
||||
return len(self._connections)
|
||||
@@ -1,5 +0,0 @@
|
||||
"""ABP module - AstrBot Protocol client implementation (built-in plugin protocol)."""
|
||||
|
||||
from .client import AstrbotAbpClient
|
||||
|
||||
__all__ = ["AstrbotAbpClient"]
|
||||
@@ -1,93 +0,0 @@
|
||||
"""
|
||||
ABP (AstrBot Protocol) client implementation.
|
||||
|
||||
ABP is the built-in plugin protocol where the orchestrator acts as client
|
||||
connecting to internal stars (plugins) embedded in the runtime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot._internal.abc.abp.base_astrbot_abp_client import BaseAstrbotAbpClient
|
||||
|
||||
log = logger
|
||||
|
||||
|
||||
class AstrbotAbpClient(BaseAstrbotAbpClient):
|
||||
"""
|
||||
ABP client for communicating with internal stars (built-in plugins).
|
||||
|
||||
The orchestrator acts as the client, sending requests to and receiving
|
||||
notifications from stars running within the same process.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._connected = False
|
||||
self._stars: dict[str, Any] = {}
|
||||
# Use a simple dict for pending requests; we avoid asyncio.Future here.
|
||||
self._pending_requests: dict[str, Any] = {}
|
||||
self._request_id = 0
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""True if connected to stars registry."""
|
||||
return self._connected
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Connect to internal stars registry."""
|
||||
log.debug("ABP client connecting to internal stars...")
|
||||
self._connected = True
|
||||
log.info("ABP client connected to internal stars registry.")
|
||||
|
||||
async def call_star_tool(
|
||||
self, star_name: str, tool_name: str, arguments: dict[str, Any]
|
||||
) -> Any:
|
||||
"""
|
||||
Call a tool on a registered star.
|
||||
|
||||
Args:
|
||||
star_name: Name of the star (plugin)
|
||||
tool_name: Name of the tool to call
|
||||
arguments: Tool arguments
|
||||
|
||||
Returns:
|
||||
Tool call result
|
||||
"""
|
||||
if not self._connected:
|
||||
raise RuntimeError("ABP client is not connected")
|
||||
|
||||
star = self._stars.get(star_name)
|
||||
if not star:
|
||||
raise ValueError(f"Star '{star_name}' not found")
|
||||
|
||||
request_id = f"{self._request_id}"
|
||||
self._request_id += 1
|
||||
|
||||
# No asyncio.Future used; store a placeholder entry for tracking if needed.
|
||||
self._pending_requests[request_id] = None
|
||||
|
||||
try:
|
||||
# Call the star's tool handler
|
||||
result = await star.call_tool(tool_name, arguments)
|
||||
return result
|
||||
finally:
|
||||
self._pending_requests.pop(request_id, None)
|
||||
|
||||
def register_star(self, star_name: str, star_instance: Any) -> None:
|
||||
"""Register a star (plugin) with the ABP client."""
|
||||
self._stars[star_name] = star_instance
|
||||
log.debug(f"Star '{star_name}' registered with ABP client.")
|
||||
|
||||
def unregister_star(self, star_name: str) -> None:
|
||||
"""Unregister a star from the ABP client."""
|
||||
self._stars.pop(star_name, None)
|
||||
log.debug(f"Star '{star_name}' unregistered from ABP client.")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the ABP client connection."""
|
||||
self._connected = False
|
||||
# Clear any pending requests (no asyncio futures used in this implementation)
|
||||
self._pending_requests.clear()
|
||||
log.info("ABP client shut down.")
|
||||
@@ -1,6 +0,0 @@
|
||||
"""ACP module - AstrBot Communication Protocol client and server implementations."""
|
||||
|
||||
from .client import AstrbotAcpClient
|
||||
from .server import AstrbotAcpServer
|
||||
|
||||
__all__ = ["AstrbotAcpClient", "AstrbotAcpServer"]
|
||||
@@ -1,220 +0,0 @@
|
||||
"""
|
||||
ACP (AstrBot Communication Protocol) client implementation.
|
||||
|
||||
ACP is a client-server protocol for inter-service communication,
|
||||
similar to MCP but designed specifically for AstrBot's architecture.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot._internal.abc.acp.base_astrbot_acp_client import BaseAstrbotAcpClient
|
||||
|
||||
log = logger
|
||||
|
||||
|
||||
class AstrbotAcpClient(BaseAstrbotAcpClient):
|
||||
"""
|
||||
ACP client for communicating with ACP servers.
|
||||
|
||||
The orchestrator acts as an ACP client, connecting to external
|
||||
ACP-compatible services.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._connected = False
|
||||
self._reader: asyncio.StreamReader | None = None
|
||||
self._writer: asyncio.StreamWriter | None = None
|
||||
self._server_url: str | None = None
|
||||
self._pending_requests: dict[str, asyncio.Future[dict[str, Any]]] = {}
|
||||
self._request_id = 0
|
||||
self._reader_task: asyncio.Task[None] | None = None
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""True if connected to an ACP server."""
|
||||
return self._connected
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""
|
||||
Connect to configured ACP servers.
|
||||
|
||||
ACP servers can be accessed via TCP (host:port) or Unix socket.
|
||||
"""
|
||||
log.debug("ACP client connecting...")
|
||||
# TODO: Load ACP server configurations
|
||||
self._connected = True
|
||||
log.info("ACP client initialized.")
|
||||
|
||||
async def connect_to_server(self, host: str, port: int) -> None:
|
||||
"""
|
||||
Connect to an ACP server via TCP.
|
||||
|
||||
Args:
|
||||
host: Server hostname or IP
|
||||
port: Server port
|
||||
"""
|
||||
self._server_url = f"{host}:{port}"
|
||||
self._reader, self._writer = await asyncio.open_connection(host, port)
|
||||
self._connected = True
|
||||
|
||||
# Start reading responses
|
||||
self._reader_task = asyncio.create_task(self._read_messages())
|
||||
|
||||
log.info(f"ACP client connected to {self._server_url}")
|
||||
|
||||
async def connect_to_unix_socket(self, socket_path: str) -> None:
|
||||
"""
|
||||
Connect to an ACP server via Unix socket.
|
||||
|
||||
Args:
|
||||
socket_path: Path to the Unix socket
|
||||
"""
|
||||
self._server_url = f"unix://{socket_path}"
|
||||
self._reader, self._writer = await asyncio.open_unix_connection(socket_path)
|
||||
self._connected = True
|
||||
|
||||
self._reader_task = asyncio.create_task(self._read_messages())
|
||||
|
||||
log.info(f"ACP client connected to {self._server_url}")
|
||||
|
||||
async def _read_messages(self) -> None:
|
||||
"""Background task to read ACP messages."""
|
||||
if not self._reader:
|
||||
return
|
||||
|
||||
buffer = b""
|
||||
while self._connected:
|
||||
try:
|
||||
data = await self._reader.read(4096)
|
||||
if not data:
|
||||
break
|
||||
buffer += data
|
||||
|
||||
while True:
|
||||
header_end = buffer.find(b"\n")
|
||||
if header_end == -1:
|
||||
break
|
||||
|
||||
try:
|
||||
header = json.loads(buffer[:header_end].decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
buffer = buffer[header_end + 1 :]
|
||||
continue
|
||||
|
||||
content_length = header.get("content-length", 0)
|
||||
if (
|
||||
content_length == 0
|
||||
or len(buffer) < header_end + 1 + content_length
|
||||
):
|
||||
break
|
||||
|
||||
content = buffer[header_end + 1 : header_end + 1 + content_length]
|
||||
buffer = buffer[header_end + 1 + content_length :]
|
||||
|
||||
message = json.loads(content.decode("utf-8"))
|
||||
|
||||
if "id" in message:
|
||||
request_id = str(message["id"])
|
||||
future = self._pending_requests.pop(request_id, None)
|
||||
if future and not future.done():
|
||||
if "error" in message:
|
||||
future.set_exception(Exception(str(message["error"])))
|
||||
else:
|
||||
future.set_result(message.get("result", {}))
|
||||
else:
|
||||
await self._handle_notification(message)
|
||||
|
||||
except Exception as e:
|
||||
if self._connected:
|
||||
log.error(f"ACP read error: {e}")
|
||||
break
|
||||
|
||||
async def _handle_notification(self, notification: dict[str, Any]) -> None:
|
||||
"""Handle incoming ACP notifications."""
|
||||
method = notification.get("method", "")
|
||||
log.debug(f"ACP notification: {method}")
|
||||
|
||||
async def call_tool(
|
||||
self, server_name: str, tool_name: str, arguments: dict[str, Any]
|
||||
) -> Any:
|
||||
"""
|
||||
Call a tool on an ACP server.
|
||||
|
||||
Args:
|
||||
server_name: Name of the ACP server
|
||||
tool_name: Name of the tool to call
|
||||
arguments: Tool arguments
|
||||
|
||||
Returns:
|
||||
Tool call result
|
||||
"""
|
||||
if not self._connected:
|
||||
raise RuntimeError("ACP client is not connected")
|
||||
|
||||
request_id = str(self._request_id)
|
||||
self._request_id += 1
|
||||
|
||||
message = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": f"{server_name}/{tool_name}",
|
||||
"params": arguments,
|
||||
}
|
||||
|
||||
future: asyncio.Future[dict[str, Any]] = asyncio.Future()
|
||||
self._pending_requests[request_id] = future
|
||||
|
||||
await self._send_message(message)
|
||||
return await future
|
||||
|
||||
async def _send_message(self, message: dict[str, Any]) -> None:
|
||||
"""Send an ACP message."""
|
||||
if not self._writer:
|
||||
raise RuntimeError("ACP client not connected")
|
||||
|
||||
content = json.dumps(message)
|
||||
header = json.dumps({"content-length": len(content)}) + "\n"
|
||||
|
||||
self._writer.write((header + content).encode())
|
||||
await self._writer.drain()
|
||||
|
||||
async def send_notification(
|
||||
self, method: str, params: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""Send a one-way notification to the server."""
|
||||
message = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params or {},
|
||||
}
|
||||
await self._send_message(message)
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the ACP client connection."""
|
||||
self._connected = False
|
||||
|
||||
if self._reader_task:
|
||||
self._reader_task.cancel()
|
||||
try:
|
||||
await self._reader_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
if self._writer:
|
||||
self._writer.close()
|
||||
try:
|
||||
await self._writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for future in self._pending_requests.values():
|
||||
if not future.done():
|
||||
future.cancel()
|
||||
self._pending_requests.clear()
|
||||
|
||||
log.info("ACP client shut down.")
|
||||
@@ -1,223 +0,0 @@
|
||||
"""
|
||||
ACP (AstrBot Communication Protocol) server implementation.
|
||||
|
||||
ACP servers listen for connections from ACP clients and provide
|
||||
services/tools to the orchestrator.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot._internal.abc.acp.base_astrbot_acp_server import BaseAstrbotAcpServer
|
||||
|
||||
log = logger
|
||||
|
||||
|
||||
class AstrbotAcpServer(BaseAstrbotAcpServer):
|
||||
"""
|
||||
ACP server for accepting connections from ACP clients.
|
||||
|
||||
ACP servers expose tools/notifications that can be called by clients.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._running = False
|
||||
self._host: str = "127.0.0.1"
|
||||
self._port: int = 8765
|
||||
self._server: asyncio.Server | None = None
|
||||
self._clients: set[tuple[asyncio.StreamReader, asyncio.StreamWriter]] = set()
|
||||
self._tool_handlers: dict[str, Callable[..., Any]] = {}
|
||||
self._notification_handlers: dict[str, Callable[..., Any]] = {}
|
||||
|
||||
def register_tool(self, name: str, handler: Callable[..., Any]) -> None:
|
||||
"""
|
||||
Register a tool handler.
|
||||
|
||||
Args:
|
||||
name: Tool name
|
||||
handler: Async callable that handles tool calls
|
||||
"""
|
||||
self._tool_handlers[name] = handler
|
||||
log.debug(f"ACP server registered tool: {name}")
|
||||
|
||||
def register_notification_handler(
|
||||
self, name: str, handler: Callable[..., Any]
|
||||
) -> None:
|
||||
"""
|
||||
Register a notification handler.
|
||||
|
||||
Args:
|
||||
name: Notification method name
|
||||
handler: Async callable that handles notifications
|
||||
"""
|
||||
self._notification_handlers[name] = handler
|
||||
log.debug(f"ACP server registered notification handler: {name}")
|
||||
|
||||
async def start(self, host: str = "127.0.0.1", port: int = 8765) -> None:
|
||||
"""
|
||||
Start the ACP server.
|
||||
|
||||
Args:
|
||||
host: Host to bind to
|
||||
port: Port to listen on
|
||||
"""
|
||||
self._host = host
|
||||
self._port = port
|
||||
self._server = await asyncio.start_server(
|
||||
self._handle_client,
|
||||
host=host,
|
||||
port=port,
|
||||
)
|
||||
self._running = True
|
||||
log.info(f"ACP server listening on {host}:{port}")
|
||||
|
||||
async def _handle_client(
|
||||
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||
) -> None:
|
||||
"""Handle an incoming ACP client connection."""
|
||||
addr = writer.get_extra_info("peername")
|
||||
log.debug(f"ACP client connected: {addr}")
|
||||
self._clients.add((reader, writer))
|
||||
|
||||
buffer = b""
|
||||
try:
|
||||
while self._running:
|
||||
try:
|
||||
data = await reader.read(4096)
|
||||
if not data:
|
||||
break
|
||||
buffer += data
|
||||
|
||||
while True:
|
||||
header_end = buffer.find(b"\n")
|
||||
if header_end == -1:
|
||||
break
|
||||
|
||||
try:
|
||||
header = json.loads(buffer[:header_end].decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
buffer = buffer[header_end + 1 :]
|
||||
continue
|
||||
|
||||
content_length = header.get("content-length", 0)
|
||||
if (
|
||||
content_length == 0
|
||||
or len(buffer) < header_end + 1 + content_length
|
||||
):
|
||||
break
|
||||
|
||||
content = buffer[
|
||||
header_end + 1 : header_end + 1 + content_length
|
||||
]
|
||||
buffer = buffer[header_end + 1 + content_length :]
|
||||
|
||||
message = json.loads(content.decode("utf-8"))
|
||||
response = await self._handle_message(message)
|
||||
|
||||
if response:
|
||||
content = json.dumps(response)
|
||||
resp_header = (
|
||||
json.dumps({"content-length": len(content)}) + "\n"
|
||||
)
|
||||
writer.write(resp_header.encode() + content.encode())
|
||||
await writer.drain()
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"ACP client error ({addr}): {e}")
|
||||
break
|
||||
|
||||
finally:
|
||||
self._clients.discard((reader, writer))
|
||||
writer.close()
|
||||
try:
|
||||
await writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
log.debug(f"ACP client disconnected: {addr}")
|
||||
|
||||
async def _handle_message(self, message: dict[str, Any]) -> dict[str, Any] | None:
|
||||
"""Handle an incoming ACP message."""
|
||||
method = message.get("method", "")
|
||||
msg_id = message.get("id")
|
||||
params = message.get("params", {})
|
||||
|
||||
# Check if it's a notification (no id) or request (has id)
|
||||
if msg_id is None:
|
||||
# Notification
|
||||
handler = self._notification_handlers.get(method)
|
||||
if handler:
|
||||
try:
|
||||
await handler(params)
|
||||
except Exception as e:
|
||||
log.error(f"ACP notification handler error ({method}): {e}")
|
||||
return None
|
||||
|
||||
# Request
|
||||
result = None
|
||||
error = None
|
||||
|
||||
handler = self._tool_handlers.get(method)
|
||||
if handler:
|
||||
try:
|
||||
result = await handler(params)
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
log.error(f"ACP tool handler error ({method}): {e}")
|
||||
else:
|
||||
error = f"Unknown method: {method}"
|
||||
|
||||
response: dict[str, Any] = {"jsonrpc": "2.0", "id": msg_id}
|
||||
if error:
|
||||
response["error"] = {"code": -32601, "message": error}
|
||||
else:
|
||||
response["result"] = result
|
||||
|
||||
return response
|
||||
|
||||
async def broadcast_notification(self, method: str, params: dict[str, Any]) -> None:
|
||||
"""
|
||||
Broadcast a notification to all connected clients.
|
||||
|
||||
Args:
|
||||
method: Notification method name
|
||||
params: Notification parameters
|
||||
"""
|
||||
message = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params,
|
||||
}
|
||||
content = json.dumps(message)
|
||||
header = json.dumps({"content-length": len(content)}) + "\n"
|
||||
data = header.encode() + content.encode()
|
||||
|
||||
for reader, writer in list(self._clients):
|
||||
try:
|
||||
writer.write(data)
|
||||
await writer.drain()
|
||||
except Exception as e:
|
||||
log.warning(f"Failed to broadcast to client: {e}")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the ACP server."""
|
||||
self._running = False
|
||||
|
||||
if self._server:
|
||||
self._server.close()
|
||||
await self._server.wait_closed()
|
||||
self._server = None
|
||||
|
||||
for reader, writer in list(self._clients):
|
||||
writer.close()
|
||||
try:
|
||||
await writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
self._clients.clear()
|
||||
|
||||
log.info("ACP server shut down.")
|
||||
@@ -1,5 +0,0 @@
|
||||
"""LSP module - Language Server Protocol client implementation."""
|
||||
|
||||
from .client import AstrbotLspClient
|
||||
|
||||
__all__ = ["AstrbotLspClient"]
|
||||
@@ -1,243 +0,0 @@
|
||||
"""
|
||||
LSP (Language Server Protocol) client implementation.
|
||||
|
||||
The orchestrator acts as an LSP client, connecting to LSP servers
|
||||
that provide language intelligence features (completions, diagnostics, etc.).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
from anyio.abc import ByteReceiveStream, ByteSendStream, Process
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot._internal.abc.lsp.base_astrbot_lsp_client import BaseAstrbotLspClient
|
||||
|
||||
log = logger
|
||||
|
||||
|
||||
class AstrbotLspClient(BaseAstrbotLspClient):
|
||||
"""
|
||||
LSP client for communicating with LSP servers.
|
||||
|
||||
Implements the Microsoft Language Server Protocol for connecting to
|
||||
external language intelligence services.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._connected = False
|
||||
self._reader: ByteReceiveStream | None = None
|
||||
self._writer: ByteSendStream | None = None
|
||||
self._server_process: Process | None = None
|
||||
self._pending_requests: dict[int, Any] = {}
|
||||
self._request_id = 0
|
||||
self._server_command: list[str] | None = None
|
||||
# anyio TaskGroup handle for background readers
|
||||
self._task_group: Any | None = None
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""True if connected to an LSP server."""
|
||||
return self._connected
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""
|
||||
Connect to configured LSP servers.
|
||||
|
||||
LSP servers are typically stdio-based subprocesses. This method
|
||||
establishes the communication channel.
|
||||
"""
|
||||
log.debug("LSP client connecting...")
|
||||
# TODO: Load LSP server configurations and start subprocesses
|
||||
# For now, mark as connected in idle mode
|
||||
self._connected = True
|
||||
log.info("LSP client initialized.")
|
||||
|
||||
async def connect_to_server(self, command: list[str], workspace_uri: str) -> None:
|
||||
"""
|
||||
Connect to an LSP server subprocess.
|
||||
|
||||
Args:
|
||||
command: Command line to start the LSP server (e.g., ["python", "lsp_server.py"])
|
||||
workspace_uri: Root URI of the workspace to serve
|
||||
"""
|
||||
log.debug(f"Starting LSP server: {' '.join(command)}")
|
||||
|
||||
self._server_process = await anyio.open_process(
|
||||
command,
|
||||
stdin=-1,
|
||||
stdout=-1,
|
||||
stderr=-1,
|
||||
)
|
||||
self._reader = self._server_process.stdout
|
||||
self._writer = self._server_process.stdin
|
||||
self._server_command = command
|
||||
self._connected = True
|
||||
|
||||
# Start reading responses in background using anyio TaskGroup
|
||||
# Create and enter a TaskGroup so the reader runs until we close it at shutdown.
|
||||
self._task_group = anyio.create_task_group()
|
||||
await self._task_group.__aenter__()
|
||||
self._task_group.start_soon(self._read_responses)
|
||||
|
||||
# Send initialize request
|
||||
await self.send_request(
|
||||
"initialize",
|
||||
{
|
||||
"processId": None,
|
||||
"rootUri": workspace_uri,
|
||||
"capabilities": {},
|
||||
},
|
||||
)
|
||||
|
||||
# Send initialized notification
|
||||
await self.send_notification("initialized", {})
|
||||
|
||||
log.info(f"LSP client connected to server: {command[0]}")
|
||||
|
||||
async def send_request(
|
||||
self, method: str, params: dict[str, Any] | None = None
|
||||
) -> Any:
|
||||
"""Send an LSP request and wait for response."""
|
||||
if not self._writer:
|
||||
raise RuntimeError("LSP client not connected")
|
||||
|
||||
request_id = self._request_id
|
||||
self._request_id += 1
|
||||
|
||||
message = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": request_id,
|
||||
"method": method,
|
||||
"params": params or {},
|
||||
}
|
||||
|
||||
# Use anyio.Event for request/response matching
|
||||
response_event: anyio.Event = anyio.Event()
|
||||
response_holder: dict[str, Any] = {}
|
||||
|
||||
async def set_response(response: dict[str, Any]) -> None:
|
||||
response_holder["response"] = response
|
||||
response_event.set()
|
||||
|
||||
self._pending_requests[request_id] = set_response
|
||||
|
||||
content = json.dumps(message)
|
||||
headers = f"Content-Length: {len(content)}\r\n\r\n"
|
||||
await self._writer.send((headers + content).encode())
|
||||
|
||||
# Wait for response with timeout
|
||||
with anyio.move_on_after(30):
|
||||
await response_event.wait()
|
||||
|
||||
if "response" in response_holder:
|
||||
return response_holder["response"]
|
||||
raise TimeoutError(f"LSP request {method} timed out")
|
||||
|
||||
async def send_notification(
|
||||
self, method: str, params: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""Send an LSP notification (no response expected)."""
|
||||
if not self._writer:
|
||||
raise RuntimeError("LSP client not connected")
|
||||
|
||||
message = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": method,
|
||||
"params": params or {},
|
||||
}
|
||||
|
||||
content = json.dumps(message)
|
||||
headers = f"Content-Length: {len(content)}\r\n\r\n"
|
||||
await self._writer.send((headers + content).encode())
|
||||
|
||||
async def _read_responses(self) -> None:
|
||||
"""Background task to read LSP responses."""
|
||||
if not self._reader:
|
||||
return
|
||||
|
||||
buffer = b""
|
||||
try:
|
||||
while self._connected:
|
||||
try:
|
||||
data = await self._reader.receive()
|
||||
if not data:
|
||||
break
|
||||
buffer += data
|
||||
|
||||
while True:
|
||||
# Parse Content-Length header
|
||||
header_end = buffer.find(b"\r\n\r\n")
|
||||
if header_end == -1:
|
||||
break
|
||||
|
||||
header = buffer[:header_end].decode("utf-8")
|
||||
content_length = 0
|
||||
for line in header.split("\r\n"):
|
||||
if line.startswith("Content-Length:"):
|
||||
content_length = int(line.split(":")[1].strip())
|
||||
|
||||
if content_length == 0:
|
||||
break
|
||||
|
||||
total_length = header_end + 4 + content_length
|
||||
if len(buffer) < total_length:
|
||||
break
|
||||
|
||||
content = buffer[header_end + 4 : total_length]
|
||||
buffer = buffer[total_length:]
|
||||
|
||||
response = json.loads(content.decode("utf-8"))
|
||||
|
||||
# Handle response vs notification
|
||||
if "id" in response:
|
||||
request_id = response["id"]
|
||||
handler = self._pending_requests.pop(request_id, None)
|
||||
if handler:
|
||||
await handler(response)
|
||||
else:
|
||||
# Notification (e.g., window/logMessage)
|
||||
await self._handle_notification(response)
|
||||
|
||||
except anyio.EndOfStream:
|
||||
break
|
||||
except anyio.get_cancelled_exc_class():
|
||||
# Task was cancelled via the TaskGroup cancel/exit during shutdown
|
||||
pass
|
||||
|
||||
async def _handle_notification(self, notification: dict[str, Any]) -> None:
|
||||
"""Handle incoming LSP notifications."""
|
||||
method = notification.get("method", "")
|
||||
log.debug(f"LSP notification: {method}")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown the LSP client."""
|
||||
self._connected = False
|
||||
|
||||
if self._task_group:
|
||||
try:
|
||||
# Exit the TaskGroup, which cancels background tasks started within it
|
||||
await self._task_group.__aexit__(None, None, None)
|
||||
except anyio.get_cancelled_exc_class():
|
||||
pass
|
||||
self._task_group = None
|
||||
|
||||
if self._server_process:
|
||||
try:
|
||||
await self.send_notification("shutdown", {})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self._server_process.terminate()
|
||||
try:
|
||||
with anyio.move_on_after(5):
|
||||
await self._server_process.wait()
|
||||
except Exception:
|
||||
self._server_process.kill()
|
||||
self._server_process = None
|
||||
|
||||
self._pending_requests.clear()
|
||||
log.info("LSP client shut down.")
|
||||
@@ -1,63 +0,0 @@
|
||||
"""MCP module - Model Context Protocol client and tool implementations.
|
||||
|
||||
This module provides MCP client functionality and MCP tool wrappers.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .client import McpClient
|
||||
from .config import (
|
||||
DEFAULT_MCP_CONFIG,
|
||||
get_mcp_config_path,
|
||||
load_mcp_config,
|
||||
save_mcp_config,
|
||||
)
|
||||
from .tool import MCPTool
|
||||
|
||||
|
||||
# Exceptions
|
||||
class MCPInitError(Exception):
|
||||
"""Base exception for MCP initialization failures."""
|
||||
|
||||
|
||||
class MCPInitTimeoutError(asyncio.TimeoutError, MCPInitError):
|
||||
"""Raised when MCP client initialization exceeds the configured timeout."""
|
||||
|
||||
|
||||
class MCPAllServicesFailedError(MCPInitError):
|
||||
"""Raised when all configured MCP services fail to initialize."""
|
||||
|
||||
|
||||
class MCPShutdownTimeoutError(asyncio.TimeoutError):
|
||||
"""Raised when MCP shutdown exceeds the configured timeout."""
|
||||
|
||||
def __init__(self, names: list[str], timeout: float) -> None:
|
||||
self.names = names
|
||||
self.timeout = timeout
|
||||
message = f"MCP 服务关闭超时({timeout:g} 秒):{', '.join(names)}"
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MCPInitSummary:
|
||||
"""Summary of MCP initialization results."""
|
||||
|
||||
total: int
|
||||
success: int
|
||||
failed: list[str]
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_MCP_CONFIG",
|
||||
"MCPAllServicesFailedError",
|
||||
"MCPInitError",
|
||||
"MCPInitSummary",
|
||||
"MCPInitTimeoutError",
|
||||
"MCPShutdownTimeoutError",
|
||||
"MCPTool",
|
||||
"McpClient",
|
||||
"get_mcp_config_path",
|
||||
"load_mcp_config",
|
||||
"save_mcp_config",
|
||||
]
|
||||
@@ -1,486 +0,0 @@
|
||||
"""MCP client implementation."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from contextlib import AsyncExitStack
|
||||
from datetime import timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
from tenacity import (
|
||||
before_sleep_log,
|
||||
retry,
|
||||
retry_if_exception_type,
|
||||
stop_after_attempt,
|
||||
wait_exponential,
|
||||
)
|
||||
|
||||
from astrbot._internal.abc.mcp.base_astrbot_mcp_client import (
|
||||
BaseAstrbotMcpClient,
|
||||
McpServerConfig,
|
||||
McpToolInfo,
|
||||
)
|
||||
from astrbot.core.utils.log_pipe import LogPipe
|
||||
|
||||
logger = logging.getLogger("astrbot")
|
||||
|
||||
|
||||
try:
|
||||
import anyio
|
||||
|
||||
import mcp
|
||||
from mcp.client.sse import sse_client
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
logger.warning(
|
||||
"Warning: Missing 'mcp' dependency, MCP services will be unavailable."
|
||||
)
|
||||
|
||||
try:
|
||||
from mcp.client.streamable_http import streamablehttp_client
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
logger.warning(
|
||||
"Warning: Missing 'mcp' dependency or MCP library version too old, Streamable HTTP connection unavailable.",
|
||||
)
|
||||
|
||||
|
||||
class TenacityLogger:
|
||||
"""Wraps a logging.Logger to satisfy tenacity's LoggerProtocol."""
|
||||
|
||||
__slots__ = ("_logger",)
|
||||
_logger: logging.Logger
|
||||
|
||||
def __init__(self, logger: logging.Logger) -> None:
|
||||
self._logger = logger
|
||||
|
||||
def log(
|
||||
self,
|
||||
level: int,
|
||||
msg: str,
|
||||
/,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
self._logger.log(level, msg, *args, **kwargs)
|
||||
|
||||
|
||||
def _prepare_config(config: dict) -> dict:
|
||||
"""Prepare configuration, handle nested format."""
|
||||
if config.get("mcpServers"):
|
||||
first_key = next(iter(config["mcpServers"]))
|
||||
config = config["mcpServers"][first_key]
|
||||
config.pop("active", None)
|
||||
return config
|
||||
|
||||
|
||||
def _prepare_stdio_env(config: dict) -> dict:
|
||||
"""Preserve Windows executable resolution for stdio subprocesses."""
|
||||
if sys.platform != "win32":
|
||||
return config
|
||||
|
||||
pathext = os.environ.get("PATHEXT")
|
||||
if not pathext:
|
||||
return config
|
||||
|
||||
prepared = config.copy()
|
||||
env = dict(prepared.get("env") or {})
|
||||
env.setdefault("PATHEXT", pathext)
|
||||
prepared["env"] = env
|
||||
return prepared
|
||||
|
||||
|
||||
async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
|
||||
"""Quick test MCP server connectivity."""
|
||||
import aiohttp
|
||||
|
||||
cfg = _prepare_config(config.copy())
|
||||
|
||||
url = cfg["url"]
|
||||
headers = cfg.get("headers", {})
|
||||
timeout = cfg.get("timeout", 10)
|
||||
|
||||
try:
|
||||
if "transport" in cfg:
|
||||
transport_type = cfg["transport"]
|
||||
elif "type" in cfg:
|
||||
transport_type = cfg["type"]
|
||||
else:
|
||||
raise Exception("MCP connection config missing transport or type field")
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
if transport_type == "streamable_http":
|
||||
test_payload = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "initialize",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {},
|
||||
"clientInfo": {"name": "test-client", "version": "1.2.3"},
|
||||
},
|
||||
}
|
||||
async with session.post(
|
||||
url,
|
||||
headers={
|
||||
**headers,
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json, text/event-stream",
|
||||
},
|
||||
json=test_payload,
|
||||
timeout=aiohttp.ClientTimeout(total=timeout),
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return True, ""
|
||||
return False, f"HTTP {response.status}: {response.reason}"
|
||||
else:
|
||||
async with session.get(
|
||||
url,
|
||||
headers={
|
||||
**headers,
|
||||
"Accept": "application/json, text/event-stream",
|
||||
},
|
||||
timeout=aiohttp.ClientTimeout(total=timeout),
|
||||
) as response:
|
||||
if response.status == 200:
|
||||
return True, ""
|
||||
return False, f"HTTP {response.status}: {response.reason}"
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
return False, f"Connection timeout: {timeout} seconds"
|
||||
except Exception as e:
|
||||
return False, f"{e!s}"
|
||||
|
||||
|
||||
class McpClient(BaseAstrbotMcpClient):
|
||||
def __init__(self) -> None:
|
||||
# Initialize session and client objects
|
||||
self.session: mcp.ClientSession | None = None
|
||||
self.exit_stack = AsyncExitStack()
|
||||
self._old_exit_stacks: list[AsyncExitStack] = [] # Track old stacks for cleanup
|
||||
|
||||
self.name: str | None = None
|
||||
self.active: bool = True
|
||||
self.tools: list[mcp.Tool] = []
|
||||
self.server_errlogs: list[str] = []
|
||||
self.running_event = anyio.Event()
|
||||
self.process_pid: int | None = None
|
||||
|
||||
# Store connection config for reconnection
|
||||
self._mcp_server_config: McpServerConfig | None = None
|
||||
self._server_name: str | None = None
|
||||
self._reconnect_lock = anyio.Lock() # Lock for thread-safe reconnection
|
||||
self._reconnecting: bool = False # For logging and debugging
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Initialize the MCP client connection.
|
||||
|
||||
Note: Actual server connections are made via connect_to_server().
|
||||
This method prepares the client for use.
|
||||
"""
|
||||
# MCP client is initialized on-demand via connect_to_server
|
||||
# This is a no-op stub to satisfy BaseAstrbotMcpClient
|
||||
logger.debug("MCP client initialized.")
|
||||
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
"""True if MCP client has an active session."""
|
||||
return self.session is not None
|
||||
|
||||
async def list_tools(self) -> list[McpToolInfo]:
|
||||
"""List all tools from connected MCP servers."""
|
||||
if not self.session:
|
||||
return []
|
||||
result = await self.list_tools_and_save()
|
||||
tools = [
|
||||
{
|
||||
"name": tool.name,
|
||||
"description": tool.description or "",
|
||||
"inputSchema": tool.inputSchema,
|
||||
}
|
||||
for tool in result.tools
|
||||
]
|
||||
return cast(list[McpToolInfo], tools)
|
||||
|
||||
async def call_tool(
|
||||
self,
|
||||
name: str,
|
||||
arguments: dict[str, Any],
|
||||
read_timeout_seconds: int = 60,
|
||||
) -> Any:
|
||||
"""Call a tool on the MCP server with reconnection support."""
|
||||
return await self.call_tool_with_reconnect(
|
||||
tool_name=name,
|
||||
arguments=arguments,
|
||||
read_timeout_seconds=timedelta(seconds=read_timeout_seconds),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_stdio_process_pid(streams_context: object) -> int | None:
|
||||
"""Best-effort extraction for stdio subprocess PID used by lease cleanup.
|
||||
|
||||
TODO(refactor): replace this async-generator frame introspection with a
|
||||
stable MCP library hook once the upstream transport exposes process PID.
|
||||
"""
|
||||
generator = getattr(streams_context, "gen", None)
|
||||
frame = getattr(generator, "ag_frame", None)
|
||||
if frame is None:
|
||||
return None
|
||||
process = frame.f_locals.get("process")
|
||||
pid = getattr(process, "pid", None)
|
||||
try:
|
||||
return int(pid) if pid is not None else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
async def connect_to_server(self, config: McpServerConfig, name: str) -> None:
|
||||
"""Connect to MCP server
|
||||
|
||||
If `url` parameter exists:
|
||||
1. When transport is specified as `streamable_http`, use Streamable HTTP connection.
|
||||
2. When transport is specified as `sse`, use SSE connection.
|
||||
3. If not specified, default to SSE connection to MCP service.
|
||||
|
||||
Args:
|
||||
config: Configuration for the MCP server. See https://modelcontextprotocol.io/quickstart/server
|
||||
|
||||
"""
|
||||
# Store config for reconnection
|
||||
self._mcp_server_config = config
|
||||
self._server_name = name
|
||||
self.process_pid = None
|
||||
|
||||
cfg = _prepare_config(dict(config))
|
||||
|
||||
def logging_callback(
|
||||
msg: str | mcp.types.LoggingMessageNotificationParams,
|
||||
) -> None:
|
||||
# Handle MCP service error logs
|
||||
if isinstance(msg, mcp.types.LoggingMessageNotificationParams):
|
||||
if msg.level in ("warning", "error", "critical", "alert", "emergency"):
|
||||
log_msg = f"[{msg.level.upper()}] {msg.data!s}"
|
||||
self.server_errlogs.append(log_msg)
|
||||
|
||||
if "url" in cfg:
|
||||
success, error_msg = await _quick_test_mcp_connection(cfg)
|
||||
if not success:
|
||||
raise Exception(error_msg)
|
||||
|
||||
if "transport" in cfg:
|
||||
transport_type = cfg["transport"]
|
||||
elif "type" in cfg:
|
||||
transport_type = cfg["type"]
|
||||
else:
|
||||
raise Exception("MCP connection config missing transport or type field")
|
||||
|
||||
if transport_type != "streamable_http":
|
||||
# SSE transport method
|
||||
self._streams_context = sse_client(
|
||||
url=cfg["url"],
|
||||
headers=cfg.get("headers", {}),
|
||||
timeout=cfg.get("timeout", 5),
|
||||
sse_read_timeout=cfg.get("sse_read_timeout", 60 * 5),
|
||||
)
|
||||
streams = await self.exit_stack.enter_async_context(
|
||||
self._streams_context,
|
||||
)
|
||||
|
||||
# Create a new client session
|
||||
read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 60))
|
||||
self.session = await self.exit_stack.enter_async_context(
|
||||
mcp.ClientSession(
|
||||
*streams,
|
||||
read_timeout_seconds=read_timeout,
|
||||
logging_callback=cast(Any, logging_callback),
|
||||
),
|
||||
)
|
||||
else:
|
||||
timeout = timedelta(seconds=cfg.get("timeout", 30))
|
||||
sse_read_timeout = timedelta(
|
||||
seconds=cfg.get("sse_read_timeout", 60 * 5),
|
||||
)
|
||||
self._streams_context = streamablehttp_client(
|
||||
url=cfg["url"],
|
||||
headers=cfg.get("headers", {}),
|
||||
timeout=timeout,
|
||||
sse_read_timeout=sse_read_timeout,
|
||||
terminate_on_close=cfg.get("terminate_on_close", True),
|
||||
)
|
||||
read_s, write_s, _ = await self.exit_stack.enter_async_context(
|
||||
self._streams_context,
|
||||
)
|
||||
|
||||
# Create a new client session
|
||||
read_timeout = timedelta(seconds=cfg.get("session_read_timeout", 60))
|
||||
self.session = await self.exit_stack.enter_async_context(
|
||||
mcp.ClientSession(
|
||||
read_stream=read_s,
|
||||
write_stream=write_s,
|
||||
read_timeout_seconds=read_timeout,
|
||||
logging_callback=logging_callback, # type: ignore
|
||||
),
|
||||
)
|
||||
|
||||
else:
|
||||
cfg = _prepare_stdio_env(cfg)
|
||||
server_params = mcp.StdioServerParameters(
|
||||
**cfg,
|
||||
)
|
||||
|
||||
def callback(msg: str | mcp.types.LoggingMessageNotificationParams) -> None:
|
||||
# Handle MCP service error logs
|
||||
if isinstance(msg, mcp.types.LoggingMessageNotificationParams):
|
||||
if msg.level in (
|
||||
"warning",
|
||||
"error",
|
||||
"critical",
|
||||
"alert",
|
||||
"emergency",
|
||||
):
|
||||
log_msg = f"[{msg.level.upper()}] {msg.data!s}"
|
||||
self.server_errlogs.append(log_msg)
|
||||
|
||||
stdio_transport = await self.exit_stack.enter_async_context(
|
||||
mcp.stdio_client(
|
||||
server_params,
|
||||
errlog=cast(
|
||||
Any,
|
||||
LogPipe(
|
||||
level=logging.INFO,
|
||||
logger=logger,
|
||||
identifier=f"MCPServer-{name}",
|
||||
callback=callback,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
self.process_pid = self._extract_stdio_process_pid(stdio_transport)
|
||||
|
||||
# Create a new client session
|
||||
self.session = await self.exit_stack.enter_async_context(
|
||||
mcp.ClientSession(*stdio_transport),
|
||||
)
|
||||
await self.session.initialize()
|
||||
|
||||
async def list_tools_and_save(self) -> mcp.ListToolsResult:
|
||||
"""List all tools from the server and save them to self.tools"""
|
||||
if not self.session:
|
||||
raise Exception("MCP Client is not initialized")
|
||||
response = await self.session.list_tools()
|
||||
self.tools = response.tools
|
||||
return response
|
||||
|
||||
async def _reconnect(self) -> None:
|
||||
"""Reconnect to the MCP server using the stored configuration.
|
||||
|
||||
Uses asyncio.Lock to ensure thread-safe reconnection in concurrent environments.
|
||||
|
||||
Raises:
|
||||
Exception: raised when reconnection fails
|
||||
"""
|
||||
async with self._reconnect_lock:
|
||||
# Check if already reconnecting (useful for logging)
|
||||
if self._reconnecting:
|
||||
logger.debug(
|
||||
f"MCP Client {self._server_name} is already reconnecting, skipping"
|
||||
)
|
||||
return
|
||||
|
||||
if not self._mcp_server_config or not self._server_name:
|
||||
raise Exception("Cannot reconnect: missing connection configuration")
|
||||
|
||||
self._reconnecting = True
|
||||
try:
|
||||
logger.info(
|
||||
f"Attempting to reconnect to MCP server {self._server_name}..."
|
||||
)
|
||||
|
||||
# Save old exit_stack for later cleanup (don't close it now to avoid cancel scope issues)
|
||||
if self.exit_stack:
|
||||
self._old_exit_stacks.append(self.exit_stack)
|
||||
|
||||
# Mark old session as invalid
|
||||
self.session = None
|
||||
|
||||
# Create new exit stack for new connection
|
||||
self.exit_stack = AsyncExitStack()
|
||||
|
||||
# Reconnect using stored config
|
||||
await self.connect_to_server(self._mcp_server_config, self._server_name)
|
||||
await self.list_tools_and_save()
|
||||
|
||||
logger.info(
|
||||
f"Successfully reconnected to MCP server {self._server_name}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to reconnect to MCP server {self._server_name}: {e}"
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
self._reconnecting = False
|
||||
|
||||
async def call_tool_with_reconnect(
|
||||
self,
|
||||
tool_name: str,
|
||||
arguments: dict,
|
||||
read_timeout_seconds: timedelta,
|
||||
) -> mcp.types.CallToolResult:
|
||||
"""Call MCP tool with automatic reconnection on failure, max 2 retries.
|
||||
|
||||
Args:
|
||||
tool_name: tool name
|
||||
arguments: tool arguments
|
||||
read_timeout_seconds: read timeout
|
||||
|
||||
Returns:
|
||||
MCP tool call result
|
||||
|
||||
Raises:
|
||||
ValueError: MCP session is not available
|
||||
anyio.ClosedResourceError: raised after reconnection failure
|
||||
"""
|
||||
|
||||
@retry(
|
||||
retry=retry_if_exception_type(anyio.ClosedResourceError),
|
||||
stop=stop_after_attempt(2),
|
||||
wait=wait_exponential(multiplier=1, min=1, max=3),
|
||||
before_sleep=before_sleep_log(TenacityLogger(logger), logging.WARNING),
|
||||
reraise=True,
|
||||
)
|
||||
async def _call_with_retry():
|
||||
if not self.session:
|
||||
raise ValueError("MCP session is not available for MCP function tools.")
|
||||
|
||||
try:
|
||||
return await self.session.call_tool(
|
||||
name=tool_name,
|
||||
arguments=arguments,
|
||||
read_timeout_seconds=read_timeout_seconds,
|
||||
)
|
||||
except anyio.ClosedResourceError:
|
||||
logger.warning(
|
||||
f"MCP tool {tool_name} call failed (ClosedResourceError), attempting to reconnect..."
|
||||
)
|
||||
# Attempt to reconnect
|
||||
await self._reconnect()
|
||||
# Reraise the exception to trigger tenacity retry
|
||||
raise
|
||||
|
||||
return await _call_with_retry()
|
||||
|
||||
async def cleanup(self) -> None:
|
||||
"""Clean up resources including old exit stacks from reconnections"""
|
||||
# Close current exit stack
|
||||
try:
|
||||
await self.exit_stack.aclose()
|
||||
except Exception as e:
|
||||
logger.debug(f"Error closing current exit stack: {e}")
|
||||
|
||||
# Don't close old exit stacks as they may be in different task contexts
|
||||
# They will be garbage collected naturally
|
||||
# Just clear the list to release references
|
||||
self._old_exit_stacks.clear()
|
||||
|
||||
# Set running_event first to unblock any waiting tasks
|
||||
self.running_event.set()
|
||||
self.process_pid = None
|
||||
@@ -1,55 +0,0 @@
|
||||
"""MCP configuration management."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
DEFAULT_MCP_CONFIG = {"mcpServers": {}}
|
||||
|
||||
|
||||
def get_mcp_config_path() -> str:
|
||||
"""Get the path to the MCP configuration file."""
|
||||
data_dir = get_astrbot_data_path()
|
||||
return os.path.join(data_dir, "mcp_server.json")
|
||||
|
||||
|
||||
def load_mcp_config() -> dict:
|
||||
"""Load MCP configuration from file.
|
||||
|
||||
Returns:
|
||||
MCP configuration dict. If file doesn't exist, returns default config.
|
||||
|
||||
"""
|
||||
config_path = get_mcp_config_path()
|
||||
if not os.path.exists(config_path):
|
||||
# Create default config if not exists
|
||||
os.makedirs(os.path.dirname(config_path), exist_ok=True)
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(DEFAULT_MCP_CONFIG, f, ensure_ascii=False, indent=4)
|
||||
return DEFAULT_MCP_CONFIG
|
||||
|
||||
try:
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return DEFAULT_MCP_CONFIG
|
||||
|
||||
|
||||
def save_mcp_config(config: dict) -> bool:
|
||||
"""Save MCP configuration to file.
|
||||
|
||||
Args:
|
||||
config: MCP configuration dict to save.
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise.
|
||||
|
||||
"""
|
||||
config_path = get_mcp_config_path()
|
||||
try:
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, ensure_ascii=False, indent=4)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
@@ -1,45 +0,0 @@
|
||||
"""MCP tool wrapper."""
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
try:
|
||||
import mcp
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
mcp = None # type: ignore
|
||||
|
||||
from astrbot._internal.tools.base import FunctionTool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from astrbot._internal.protocols.mcp.client import McpClient
|
||||
|
||||
|
||||
class MCPTool(FunctionTool):
|
||||
"""A function tool that calls an MCP service."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mcp_tool: "mcp.types.Tool",
|
||||
mcp_client: "McpClient",
|
||||
mcp_server_name: str,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
name=mcp_tool.name,
|
||||
description=mcp_tool.description or "",
|
||||
parameters=mcp_tool.inputSchema,
|
||||
)
|
||||
self.mcp_tool = mcp_tool
|
||||
self.mcp_client = mcp_client
|
||||
self.mcp_server_name = mcp_server_name
|
||||
self.source = "mcp"
|
||||
|
||||
async def call(self, **kwargs: Any) -> Any:
|
||||
"""Call the MCP tool with the given arguments."""
|
||||
# Note: For actual usage, context.tool_call_timeout is needed
|
||||
# but for simplicity we use a default timeout here
|
||||
return await self.mcp_client.call_tool_with_reconnect(
|
||||
tool_name=self.mcp_tool.name,
|
||||
arguments=kwargs,
|
||||
read_timeout_seconds=timedelta(seconds=60),
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
from astrbot._internal.runtime.__main__ import bootstrap
|
||||
|
||||
__all__ = ["bootstrap"]
|
||||
@@ -1,24 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import anyio
|
||||
|
||||
from astrbot._internal.abc.base_astrbot_gateway import BaseAstrbotGateway
|
||||
from astrbot._internal.abc.base_astrbot_orchestrator import BaseAstrbotOrchestrator
|
||||
from astrbot._internal.geteway.server import AstrbotGateway
|
||||
from astrbot._internal.runtime.orchestrator import AstrbotOrchestrator
|
||||
|
||||
|
||||
async def bootstrap():
|
||||
orchestrator: BaseAstrbotOrchestrator = AstrbotOrchestrator()
|
||||
gw: BaseAstrbotGateway = AstrbotGateway(orchestrator)
|
||||
|
||||
# anyio 的结构化并发
|
||||
async with anyio.create_task_group() as tg:
|
||||
tg.start_soon(orchestrator.lsp.connect) # 启动 LSP client
|
||||
tg.start_soon(orchestrator.mcp.connect) # 启动 MCP client
|
||||
tg.start_soon(orchestrator.acp.connect) # 启动 ACP client
|
||||
tg.start_soon(orchestrator.abp.connect) # 启动 ABP client
|
||||
await anyio.sleep(0.5)
|
||||
tg.start_soon(orchestrator.run_loop) # 启动编排器循环
|
||||
|
||||
tg.start_soon(gw.serve) # 面板后端服务
|
||||
@@ -1,164 +0,0 @@
|
||||
"""
|
||||
AstrBot Orchestrator - core runtime that coordinates all protocol clients.
|
||||
|
||||
The orchestrator manages the lifecycle of LSP, MCP, ACP, and ABP clients,
|
||||
and runs the main event loop that dispatches messages between components.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import anyio
|
||||
|
||||
from astrbot import logger
|
||||
from astrbot._internal.abc.base_astrbot_orchestrator import BaseAstrbotOrchestrator
|
||||
from astrbot._internal.protocols.abp.client import AstrbotAbpClient
|
||||
from astrbot._internal.protocols.acp.client import AstrbotAcpClient
|
||||
from astrbot._internal.protocols.lsp.client import AstrbotLspClient
|
||||
from astrbot._internal.protocols.mcp.client import McpClient
|
||||
from astrbot._internal.stars import RuntimeStatusStar
|
||||
|
||||
log = logger
|
||||
|
||||
|
||||
class AstrbotOrchestrator(BaseAstrbotOrchestrator):
|
||||
"""
|
||||
Core runtime orchestrator for AstrBot.
|
||||
|
||||
Manages:
|
||||
- LSP client: Language Server Protocol for editor integrations
|
||||
- MCP client: Model Context Protocol for external tool servers
|
||||
- ACP client: AstrBot Communication Protocol for inter-service communication
|
||||
- ABP client: AstrBot Protocol for built-in star (plugin) communication
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Initialize protocol clients (use concrete types for full method access)
|
||||
self.lsp = AstrbotLspClient()
|
||||
self.mcp = McpClient()
|
||||
self.acp = AstrbotAcpClient()
|
||||
self.abp = AstrbotAbpClient()
|
||||
|
||||
self._running = False
|
||||
self._stars: dict[str, Any] = {}
|
||||
self._message_count: int = 0
|
||||
self._last_activity_timestamp: float | None = None
|
||||
|
||||
# Auto-register RuntimeStatusStar
|
||||
self._runtime_status_star = RuntimeStatusStar()
|
||||
self._runtime_status_star.set_orchestrator(self)
|
||||
self._stars["runtime-status-star"] = self._runtime_status_star
|
||||
self.abp.register_star("runtime-status-star", self._runtime_status_star)
|
||||
|
||||
log.debug("AstrbotOrchestrator initialized.")
|
||||
|
||||
async def start(self) -> None:
|
||||
"""
|
||||
Initialize all protocol clients.
|
||||
|
||||
Calls connect() on all protocol clients to prepare them for use.
|
||||
"""
|
||||
log.info("Starting AstrbotOrchestrator...")
|
||||
|
||||
await self.lsp.connect()
|
||||
await self.mcp.connect()
|
||||
await self.acp.connect()
|
||||
await self.abp.connect()
|
||||
|
||||
self._running = True
|
||||
log.info("AstrbotOrchestrator started.")
|
||||
|
||||
async def run_loop(self) -> None:
|
||||
"""
|
||||
Main orchestrator event loop.
|
||||
|
||||
This loop runs continuously, handling:
|
||||
- Periodic health checks of protocol clients
|
||||
- Message routing between protocols
|
||||
- Star (plugin) lifecycle management
|
||||
"""
|
||||
self._running = True
|
||||
log.info("AstrbotOrchestrator run loop started.")
|
||||
|
||||
stop_event = anyio.Event()
|
||||
|
||||
def set_stop() -> None:
|
||||
stop_event.set()
|
||||
|
||||
# Store the callback for cleanup
|
||||
self._stop_callback = set_stop
|
||||
|
||||
try:
|
||||
while self._running:
|
||||
# TODO: Periodic tasks:
|
||||
# - Check LSP server health
|
||||
# - Check MCP session status
|
||||
# - Check ACP client connections
|
||||
# - Process any pending star notifications
|
||||
|
||||
# Wait for 5 seconds or until shutdown is called
|
||||
with anyio.move_on_after(5):
|
||||
await stop_event.wait()
|
||||
|
||||
except anyio.get_cancelled_exc_class():
|
||||
log.info("Orchestrator run loop cancelled.")
|
||||
finally:
|
||||
self._running = False
|
||||
self._stop_callback = None
|
||||
log.info("AstrbotOrchestrator run loop stopped.")
|
||||
|
||||
async def register_star(self, name: str, star_instance: Any) -> None:
|
||||
"""
|
||||
Register a star (plugin) with the orchestrator.
|
||||
|
||||
Args:
|
||||
name: Unique name for the star
|
||||
star_instance: Star plugin instance
|
||||
"""
|
||||
self._stars[name] = star_instance
|
||||
self.abp.register_star(name, star_instance)
|
||||
log.info(f"Star '{name}' registered.")
|
||||
|
||||
async def unregister_star(self, name: str) -> None:
|
||||
"""
|
||||
Unregister a star (plugin) from the orchestrator.
|
||||
|
||||
Args:
|
||||
name: Name of the star to unregister
|
||||
"""
|
||||
self._stars.pop(name, None)
|
||||
self.abp.unregister_star(name)
|
||||
log.info(f"Star '{name}' unregistered.")
|
||||
|
||||
async def get_star(self, name: str) -> Any | None:
|
||||
"""Get a registered star by name."""
|
||||
return self._stars.get(name)
|
||||
|
||||
async def list_stars(self) -> list[str]:
|
||||
"""List all registered star names."""
|
||||
return list(self._stars.keys())
|
||||
|
||||
def record_activity(self) -> None:
|
||||
"""Record a message activity for stats tracking."""
|
||||
self._message_count += 1
|
||||
import time
|
||||
|
||||
self._last_activity_timestamp = time.time()
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""
|
||||
Shutdown the orchestrator and all protocol clients.
|
||||
"""
|
||||
log.info("Shutting down AstrbotOrchestrator...")
|
||||
self._running = False
|
||||
|
||||
# Shutdown all protocol clients
|
||||
await self.lsp.shutdown()
|
||||
await self.acp.shutdown()
|
||||
await self.abp.shutdown()
|
||||
|
||||
# MCP cleanup
|
||||
await self.mcp.cleanup()
|
||||
|
||||
log.info("AstrbotOrchestrator shut down.")
|
||||
@@ -1,18 +0,0 @@
|
||||
import sys
|
||||
|
||||
try:
|
||||
from ._core import cli as _cli
|
||||
|
||||
def cli():
|
||||
if len(sys.argv) == 1:
|
||||
sys.argv.append("--help")
|
||||
return _cli()
|
||||
except ImportError:
|
||||
from click import echo
|
||||
|
||||
def cli():
|
||||
echo("""
|
||||
AstrBot CLI(rust) is not available.
|
||||
Developer: maturin dev
|
||||
User: uv run astrbot-rs
|
||||
""")
|
||||
@@ -1,16 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
class AstrbotOrchestrator:
|
||||
def start(self) -> None: ...
|
||||
def stop(self) -> None: ...
|
||||
def is_running(self) -> bool: ...
|
||||
def register_star(self, name: str, handler: str) -> None: ...
|
||||
def unregister_star(self, name: str) -> None: ...
|
||||
def list_stars(self) -> list[str]: ...
|
||||
def record_activity(self) -> None: ...
|
||||
def get_stats(self) -> dict[str, Any]: ...
|
||||
def set_protocol_connected(self, protocol: str, connected: bool) -> None: ...
|
||||
def get_protocol_status(self, protocol: str) -> dict[str, Any] | None: ...
|
||||
|
||||
def get_orchestrator() -> AstrbotOrchestrator: ...
|
||||
def cli() -> None: ...
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Internal skills module - re-exports from core.skills.skill_manager."""
|
||||
|
||||
from astrbot.core.skills.skill_manager import (
|
||||
SkillInfo,
|
||||
SkillManager,
|
||||
build_skills_prompt,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"SkillInfo",
|
||||
"SkillManager",
|
||||
"build_skills_prompt",
|
||||
]
|
||||
@@ -1,7 +0,0 @@
|
||||
"""
|
||||
Stars (built-in plugins) for AstrBot runtime.
|
||||
"""
|
||||
|
||||
from astrbot._internal.stars.runtime_status_star import RuntimeStatusStar
|
||||
|
||||
__all__ = ["RuntimeStatusStar"]
|
||||
@@ -1,127 +0,0 @@
|
||||
"""
|
||||
RuntimeStatusStar - ABP plugin that exposes core runtime internal state.
|
||||
|
||||
This star provides tools for querying:
|
||||
- Runtime status (running state, uptime)
|
||||
- Protocol client status (LSP, MCP, ACP, ABP)
|
||||
- Registered stars registry
|
||||
- Message counts and metrics
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeStatusStar:
|
||||
"""
|
||||
ABP star that exposes core runtime internal state as callable tools.
|
||||
|
||||
Tools provided:
|
||||
- get_runtime_status: Returns running state and uptime
|
||||
- get_protocol_status: Returns LSP, MCP, ACP, ABP status
|
||||
- get_star_registry: Returns registered star names
|
||||
- get_stats: Returns message counts and metrics
|
||||
"""
|
||||
|
||||
name: str = "runtime-status-star"
|
||||
description: str = "ABP plugin that exposes core runtime internal state"
|
||||
|
||||
_start_time: float = field(default_factory=time.time, init=False)
|
||||
_orchestrator: Any = field(default=None, init=False)
|
||||
|
||||
def set_orchestrator(self, orchestrator: Any) -> None:
|
||||
"""Set the orchestrator reference for status queries."""
|
||||
self._orchestrator = orchestrator
|
||||
|
||||
async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any:
|
||||
"""
|
||||
Handle tool calls from ABP client.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool to call
|
||||
arguments: Tool arguments
|
||||
|
||||
Returns:
|
||||
Tool result
|
||||
"""
|
||||
if tool_name == "get_runtime_status":
|
||||
return self._get_runtime_status()
|
||||
elif tool_name == "get_protocol_status":
|
||||
return await self._get_protocol_status()
|
||||
elif tool_name == "get_star_registry":
|
||||
return await self._get_star_registry()
|
||||
elif tool_name == "get_stats":
|
||||
return self._get_stats()
|
||||
else:
|
||||
raise ValueError(f"Unknown tool: {tool_name}")
|
||||
|
||||
def _get_runtime_status(self) -> dict[str, Any]:
|
||||
"""Get overall runtime state."""
|
||||
running = (
|
||||
getattr(self._orchestrator, "running", False)
|
||||
if self._orchestrator
|
||||
else False
|
||||
)
|
||||
uptime_seconds = time.time() - self._start_time
|
||||
return {
|
||||
"running": running,
|
||||
"uptime_seconds": uptime_seconds,
|
||||
}
|
||||
|
||||
async def _get_protocol_status(self) -> dict[str, Any]:
|
||||
"""Get status of each protocol client."""
|
||||
if not self._orchestrator:
|
||||
return {
|
||||
"lsp": {"connected": False, "name": "lsp-client"},
|
||||
"mcp": {"connected": False, "name": "mcp-client"},
|
||||
"acp": {"connected": False, "name": "acp-client"},
|
||||
"abp": {"connected": False, "name": "abp-client"},
|
||||
}
|
||||
|
||||
return {
|
||||
"lsp": {
|
||||
"connected": getattr(self._orchestrator.lsp, "connected", False),
|
||||
"name": "lsp-client",
|
||||
},
|
||||
"mcp": {
|
||||
"connected": getattr(self._orchestrator.mcp, "connected", False),
|
||||
"name": "mcp-client",
|
||||
},
|
||||
"acp": {
|
||||
"connected": getattr(self._orchestrator.acp, "connected", False),
|
||||
"name": "acp-client",
|
||||
},
|
||||
"abp": {
|
||||
"connected": getattr(self._orchestrator.abp, "connected", False),
|
||||
"name": "abp-client",
|
||||
},
|
||||
}
|
||||
|
||||
async def _get_star_registry(self) -> dict[str, Any]:
|
||||
"""Get list of registered stars."""
|
||||
if not self._orchestrator:
|
||||
return {"stars": []}
|
||||
|
||||
stars = await self._orchestrator.list_stars()
|
||||
return {"stars": stars}
|
||||
|
||||
def _get_stats(self) -> dict[str, Any]:
|
||||
"""Get message counts and metrics."""
|
||||
result: dict[str, Any] = {
|
||||
"uptime_seconds": time.time() - self._start_time,
|
||||
}
|
||||
if self._orchestrator:
|
||||
result["total_messages"] = getattr(self._orchestrator, "_message_count", 0)
|
||||
last_ts = getattr(self._orchestrator, "_last_activity_timestamp", None)
|
||||
if last_ts is not None:
|
||||
result["last_activity"] = datetime.fromtimestamp(
|
||||
last_ts, tz=timezone.utc
|
||||
).isoformat()
|
||||
else:
|
||||
result["last_activity"] = None
|
||||
return result
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Internal tools module for AstrBot runtime."""
|
||||
|
||||
from .base import FunctionTool, ToolSet
|
||||
|
||||
__all__ = ["FunctionTool", "ToolSet"]
|
||||
@@ -1,332 +0,0 @@
|
||||
"""Base tool classes for AstrBot internal runtime.
|
||||
|
||||
This module provides the FunctionTool base class used by MCP tools
|
||||
in the new internal architecture.
|
||||
"""
|
||||
|
||||
import copy
|
||||
from collections.abc import AsyncGenerator, Awaitable, Callable, Iterator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from pydantic import model_validator
|
||||
|
||||
ParametersType = dict[str, Any]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolSchema:
|
||||
"""A class representing the schema of a tool for function calling."""
|
||||
|
||||
name: str
|
||||
"""The name of the tool."""
|
||||
|
||||
description: str
|
||||
"""The description of the tool."""
|
||||
|
||||
parameters: ParametersType = field(default_factory=dict)
|
||||
"""The parameters of the tool, in JSON Schema format."""
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_parameters(self) -> "ToolSchema":
|
||||
"""Validate the parameters JSON schema."""
|
||||
import jsonschema
|
||||
|
||||
jsonschema.validate(
|
||||
self.parameters, jsonschema.Draft202012Validator.META_SCHEMA
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionTool(ToolSchema):
|
||||
"""A callable tool, for function calling."""
|
||||
|
||||
handler: Callable[..., Awaitable[str | None] | AsyncGenerator[Any, None]] | None = (
|
||||
None
|
||||
)
|
||||
"""a callable that implements the tool's functionality. It should be an async function."""
|
||||
|
||||
handler_module_path: str | None = None
|
||||
"""
|
||||
The module path of the handler function. This is empty when the origin is mcp.
|
||||
This field must be retained, as the handler will be wrapped in functools.partial during initialization,
|
||||
causing the handler's __module__ to be functools
|
||||
"""
|
||||
|
||||
active: bool = True
|
||||
"""
|
||||
Whether the tool is active. This field is a special field for AstrBot.
|
||||
You can ignore it when integrating with other frameworks.
|
||||
"""
|
||||
|
||||
is_background_task: bool = False
|
||||
"""
|
||||
Declare this tool as a background task. Background tasks return immediately
|
||||
with a task identifier while the real work continues asynchronously.
|
||||
"""
|
||||
|
||||
source: str = "mcp"
|
||||
"""
|
||||
Origin of this tool: 'plugin' (from star plugins), 'internal' (AstrBot built-in),
|
||||
or 'mcp' (from MCP servers). Used by WebUI for display grouping.
|
||||
"""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
|
||||
|
||||
async def call(self, **kwargs: Any) -> Any:
|
||||
"""Run the tool with the given arguments. The handler field has priority."""
|
||||
raise NotImplementedError(
|
||||
"FunctionTool.call() must be implemented by subclasses or set a handler."
|
||||
)
|
||||
|
||||
|
||||
class ToolSet:
|
||||
"""
|
||||
A collection of FunctionTools grouped under a namespace.
|
||||
|
||||
ToolSets allow organizing related tools together. The LLM sees tools
|
||||
as "namespace/tool_name" when calling.
|
||||
"""
|
||||
|
||||
def __init__(self, namespace: str, tools: list[FunctionTool] | None = None) -> None:
|
||||
self.namespace = namespace
|
||||
self._tools: dict[str, FunctionTool] = {}
|
||||
if tools:
|
||||
for tool in tools:
|
||||
self.add(tool)
|
||||
|
||||
def add(self, tool: FunctionTool) -> None:
|
||||
"""Add a tool to the set."""
|
||||
self._tools[tool.name] = tool
|
||||
|
||||
def add_tool(self, tool: FunctionTool) -> None:
|
||||
"""Add a tool to the set (alias for add())."""
|
||||
self.add(tool)
|
||||
|
||||
def remove(self, name: str) -> FunctionTool | None:
|
||||
"""Remove and return a tool by name."""
|
||||
return self._tools.pop(name, None)
|
||||
|
||||
def remove_tool(self, name: str) -> None:
|
||||
"""Remove a tool by its name."""
|
||||
self._tools.pop(name, None)
|
||||
|
||||
def get(self, name: str) -> FunctionTool | None:
|
||||
"""Get a tool by name."""
|
||||
return self._tools.get(name)
|
||||
|
||||
def get_tool(self, name: str) -> FunctionTool | None:
|
||||
"""Get a tool by name (alias for get)."""
|
||||
return self.get(name)
|
||||
|
||||
def list_tools(self) -> list[FunctionTool]:
|
||||
"""List all tools in this set."""
|
||||
return list(self._tools.values())
|
||||
|
||||
def __iter__(self) -> Iterator[FunctionTool]:
|
||||
return iter(self._tools.values())
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._tools)
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._tools)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ToolSet(namespace={self.namespace!r}, tools={self.list_tools()!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"ToolSet({self.namespace}, {len(self)} tools)"
|
||||
|
||||
def names(self) -> list[str]:
|
||||
"""Get names of all tools in this set."""
|
||||
return [tool.name for tool in self.tools]
|
||||
|
||||
def empty(self) -> bool:
|
||||
"""Check if the tool set is empty."""
|
||||
return len(self) == 0
|
||||
|
||||
def merge(self, other: "ToolSet") -> None:
|
||||
"""Merge another ToolSet into this one."""
|
||||
for tool in other.tools:
|
||||
self.add(tool)
|
||||
|
||||
def normalize(self) -> None:
|
||||
"""Sort tools by name for deterministic serialization."""
|
||||
self._tools = dict(sorted(self._tools.items(), key=lambda x: x[0]))
|
||||
|
||||
def get_light_tool_set(self) -> "ToolSet":
|
||||
"""Return a light tool set with only name/description."""
|
||||
light_tools = []
|
||||
for tool in self.tools:
|
||||
if hasattr(tool, "active") and not tool.active:
|
||||
continue
|
||||
light_tools.append(
|
||||
FunctionTool(
|
||||
name=tool.name,
|
||||
description=tool.description,
|
||||
parameters={"type": "object", "properties": {}},
|
||||
handler=None,
|
||||
)
|
||||
)
|
||||
return ToolSet("default", light_tools)
|
||||
|
||||
def get_param_only_tool_set(self) -> "ToolSet":
|
||||
"""Return a tool set with name/parameters only (no description)."""
|
||||
param_tools = []
|
||||
for tool in self.tools:
|
||||
if hasattr(tool, "active") and not tool.active:
|
||||
continue
|
||||
params = (
|
||||
copy.deepcopy(tool.parameters)
|
||||
if tool.parameters
|
||||
else {"type": "object", "properties": {}}
|
||||
)
|
||||
param_tools.append(
|
||||
FunctionTool(
|
||||
name=tool.name,
|
||||
description="",
|
||||
parameters=params,
|
||||
handler=None,
|
||||
)
|
||||
)
|
||||
return ToolSet("default", param_tools)
|
||||
|
||||
@property
|
||||
def tools(self) -> list[FunctionTool]:
|
||||
"""List all tools in this set."""
|
||||
return list(self._tools.values())
|
||||
|
||||
def openai_schema(
|
||||
self, omit_empty_parameter_field: bool = False
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Convert tools to OpenAI API function calling schema format."""
|
||||
result: list[dict[str, Any]] = []
|
||||
for tool in self._tools.values():
|
||||
func_def: dict[str, Any] = {
|
||||
"type": "function",
|
||||
"function": {"name": tool.name},
|
||||
}
|
||||
if tool.description:
|
||||
func_def["function"]["description"] = tool.description
|
||||
|
||||
if tool.parameters is not None:
|
||||
if (
|
||||
tool.parameters.get("properties")
|
||||
) or not omit_empty_parameter_field:
|
||||
func_def["function"]["parameters"] = tool.parameters
|
||||
|
||||
result.append(func_def)
|
||||
return result
|
||||
|
||||
def anthropic_schema(self) -> list[dict]:
|
||||
"""Convert tools to Anthropic API format."""
|
||||
result = []
|
||||
for tool in self.tools:
|
||||
input_schema: dict[str, Any] = {"type": "object"}
|
||||
if tool.parameters:
|
||||
input_schema["properties"] = tool.parameters.get("properties", {})
|
||||
input_schema["required"] = tool.parameters.get("required", [])
|
||||
tool_def: dict[str, Any] = {"name": tool.name, "input_schema": input_schema}
|
||||
if tool.description:
|
||||
tool_def["description"] = tool.description
|
||||
result.append(tool_def)
|
||||
return result
|
||||
|
||||
def google_schema(self) -> dict:
|
||||
"""Convert tools to Google GenAI API format."""
|
||||
|
||||
def convert_schema(schema: dict) -> dict:
|
||||
supported_types = {
|
||||
"string",
|
||||
"number",
|
||||
"integer",
|
||||
"boolean",
|
||||
"array",
|
||||
"object",
|
||||
"null",
|
||||
}
|
||||
supported_formats = {
|
||||
"string": {"enum", "date-time"},
|
||||
"integer": {"int32", "int64"},
|
||||
"number": {"float", "double"},
|
||||
}
|
||||
|
||||
if "anyOf" in schema:
|
||||
return {"anyOf": [convert_schema(s) for s in schema["anyOf"]]}
|
||||
|
||||
result = {}
|
||||
origin_type = schema.get("type")
|
||||
target_type = origin_type
|
||||
|
||||
if isinstance(origin_type, list):
|
||||
target_type = next((t for t in origin_type if t != "null"), "string")
|
||||
|
||||
if target_type in supported_types:
|
||||
result["type"] = target_type
|
||||
if "format" in schema and schema["format"] in supported_formats.get(result["type"], set()):
|
||||
result["format"] = schema["format"]
|
||||
else:
|
||||
result["type"] = "null"
|
||||
|
||||
support_fields = {
|
||||
"title",
|
||||
"description",
|
||||
"enum",
|
||||
"minimum",
|
||||
"maximum",
|
||||
"maxItems",
|
||||
"minItems",
|
||||
"nullable",
|
||||
"required",
|
||||
}
|
||||
result.update({k: schema[k] for k in support_fields if k in schema})
|
||||
|
||||
if "properties" in schema:
|
||||
properties = {}
|
||||
for key, value in schema["properties"].items():
|
||||
prop_value = convert_schema(value)
|
||||
if "default" in prop_value:
|
||||
del prop_value["default"]
|
||||
if "additionalProperties" in prop_value:
|
||||
del prop_value["additionalProperties"]
|
||||
properties[key] = prop_value
|
||||
if properties:
|
||||
result["properties"] = properties
|
||||
|
||||
if target_type == "array":
|
||||
items_schema = schema.get("items")
|
||||
if isinstance(items_schema, dict):
|
||||
result["items"] = convert_schema(items_schema)
|
||||
else:
|
||||
result["items"] = {"type": "string"}
|
||||
|
||||
return result
|
||||
|
||||
tools_list = []
|
||||
for tool in self.tools:
|
||||
d: dict[str, Any] = {"name": tool.name}
|
||||
if tool.description:
|
||||
d["description"] = tool.description
|
||||
if tool.parameters:
|
||||
d["parameters"] = convert_schema(tool.parameters)
|
||||
tools_list.append(d)
|
||||
|
||||
declarations: dict[str, Any] = {}
|
||||
if tools_list:
|
||||
declarations["function_declarations"] = tools_list
|
||||
return declarations
|
||||
|
||||
def get_func_desc_openai_style(self, omit_empty_parameter_field: bool = False):
|
||||
"""Get tools in OpenAI function calling style (deprecated)."""
|
||||
return self.openai_schema(omit_empty_parameter_field)
|
||||
|
||||
def get_func_desc_anthropic_style(self):
|
||||
"""Get tools in Anthropic style (deprecated)."""
|
||||
return self.anthropic_schema()
|
||||
|
||||
def get_func_desc_google_genai_style(self):
|
||||
"""Get tools in Google GenAI style (deprecated)."""
|
||||
return self.google_schema()
|
||||
@@ -1,48 +0,0 @@
|
||||
"""
|
||||
Builtin tools for AstrBot - re-exports from core.tools for backward compatibility.
|
||||
|
||||
This module re-exports the builtin tools (cron, send_message, kb_query) from
|
||||
the deprecated core.tools module for backward compatibility.
|
||||
|
||||
TODO: These tools should be fully migrated to _internal and core.tools
|
||||
should be removed once all consumers update their imports.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# Re-export cron tools
|
||||
from astrbot.core.tools.cron_tools import (
|
||||
CREATE_CRON_JOB_TOOL,
|
||||
DELETE_CRON_JOB_TOOL,
|
||||
LIST_CRON_JOBS_TOOL,
|
||||
CreateActiveCronTool,
|
||||
DeleteCronJobTool,
|
||||
ListCronJobsTool,
|
||||
)
|
||||
|
||||
# Re-export knowledge_base_query tool
|
||||
from astrbot.core.tools.kb_query import (
|
||||
KNOWLEDGE_BASE_QUERY_TOOL,
|
||||
KnowledgeBaseQueryTool,
|
||||
)
|
||||
|
||||
# Re-export send_message tool
|
||||
from astrbot.core.tools.send_message import (
|
||||
SEND_MESSAGE_TO_USER_TOOL,
|
||||
SendMessageToUserTool,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Cron tools
|
||||
"CREATE_CRON_JOB_TOOL",
|
||||
"DELETE_CRON_JOB_TOOL",
|
||||
"KNOWLEDGE_BASE_QUERY_TOOL",
|
||||
"LIST_CRON_JOBS_TOOL",
|
||||
"SEND_MESSAGE_TO_USER_TOOL",
|
||||
# Classes
|
||||
"CreateActiveCronTool",
|
||||
"DeleteCronJobTool",
|
||||
"KnowledgeBaseQueryTool",
|
||||
"ListCronJobsTool",
|
||||
"SendMessageToUserTool",
|
||||
]
|
||||
@@ -1,278 +0,0 @@
|
||||
"""Tools registry for AstrBot internal runtime."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
# Re-export from base
|
||||
from astrbot._internal.tools.base import FunctionTool, ToolSet
|
||||
|
||||
__all__ = [
|
||||
"DEFAULT_MCP_CONFIG",
|
||||
"ENABLE_MCP_TIMEOUT_ENV",
|
||||
"FuncCall",
|
||||
"FunctionTool",
|
||||
"FunctionToolManager",
|
||||
"MCPAllServicesFailedError",
|
||||
"MCPInitError",
|
||||
"MCPInitSummary",
|
||||
"MCPInitTimeoutError",
|
||||
"MCPShutdownTimeoutError",
|
||||
"ToolSet",
|
||||
]
|
||||
|
||||
|
||||
# MCP config constants (re-exported from protocols)
|
||||
try:
|
||||
from astrbot._internal.protocols.mcp import (
|
||||
DEFAULT_MCP_CONFIG,
|
||||
MCPAllServicesFailedError,
|
||||
MCPInitError,
|
||||
MCPInitSummary,
|
||||
MCPInitTimeoutError,
|
||||
MCPShutdownTimeoutError,
|
||||
)
|
||||
except ImportError:
|
||||
DEFAULT_MCP_CONFIG: dict[str, Any] = {}
|
||||
MCPAllServicesFailedError: type[Exception] = Exception
|
||||
MCPInitError: type[Exception] = Exception
|
||||
MCPInitSummary: type[dict] = dict
|
||||
MCPInitTimeoutError: type[TimeoutError] = TimeoutError
|
||||
MCPShutdownTimeoutError: type[TimeoutError] = TimeoutError
|
||||
|
||||
ENABLE_MCP_TIMEOUT_ENV = "ASTRBOT_MCP_TIMEOUT_ENABLED"
|
||||
MCP_INIT_TIMEOUT_ENV = "ASTRBOT_MCP_INIT_TIMEOUT"
|
||||
|
||||
|
||||
class FunctionToolManager:
|
||||
"""Central registry for all function tools."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._func_list: list[FunctionTool] = []
|
||||
|
||||
@property
|
||||
def func_list(self) -> list[FunctionTool]:
|
||||
"""Get the list of function tools."""
|
||||
return self._func_list
|
||||
|
||||
@func_list.setter
|
||||
def func_list(self, value: list[FunctionTool]) -> None:
|
||||
"""Set the list of function tools."""
|
||||
self._func_list = value
|
||||
|
||||
def add(self, tool: FunctionTool) -> None:
|
||||
"""Add a tool to the registry."""
|
||||
self._func_list.append(tool)
|
||||
|
||||
def remove(self, name: str) -> bool:
|
||||
"""Remove a tool by name. Returns True if found."""
|
||||
for i, f in enumerate(self._func_list):
|
||||
if f.name == name:
|
||||
self._func_list.pop(i)
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_func(self, name: str) -> FunctionTool | None:
|
||||
"""Get a tool by name. Returns the last active tool if multiple match."""
|
||||
last_match: FunctionTool | None = None
|
||||
for f in reversed(self._func_list):
|
||||
if f.name == name:
|
||||
if getattr(f, "active", True):
|
||||
return f
|
||||
if last_match is None:
|
||||
last_match = f
|
||||
return last_match
|
||||
|
||||
def get_full_tool_set(self) -> ToolSet:
|
||||
"""Return a ToolSet with all active tools, deduplicated by name."""
|
||||
seen: dict[str, FunctionTool] = {}
|
||||
for tool in reversed(self._func_list):
|
||||
if tool.name not in seen and getattr(tool, "active", True):
|
||||
seen[tool.name] = tool
|
||||
return ToolSet("default", list(seen.values()))
|
||||
|
||||
def register_internal_tools(self) -> None:
|
||||
"""Register built-in computer tools (shell, python, browser, neo)."""
|
||||
# Import here to avoid circular imports
|
||||
from astrbot.core.computer.computer_tool_provider import get_all_tools
|
||||
|
||||
for tool in get_all_tools():
|
||||
if self.get_func(tool.name) is None:
|
||||
self.add(tool)
|
||||
|
||||
# MCP-related stub methods for base class compatibility
|
||||
async def enable_mcp_server(
|
||||
self, name: str, config: dict[str, Any], init_timeout: int = 30
|
||||
) -> None:
|
||||
"""Enable an MCP server (stub)."""
|
||||
pass
|
||||
|
||||
async def disable_mcp_server(
|
||||
self, name: str = "", timeout: int = 10, shutdown_timeout: int = 10
|
||||
) -> None:
|
||||
"""Disable an MCP server (stub)."""
|
||||
pass
|
||||
|
||||
async def init_mcp_clients(self) -> None:
|
||||
"""Initialize MCP clients (stub)."""
|
||||
pass
|
||||
|
||||
async def test_mcp_server_connection(
|
||||
self, config: dict[str, Any]
|
||||
) -> tuple[bool, str]:
|
||||
"""Test MCP server connection (stub)."""
|
||||
return False, "Not implemented"
|
||||
|
||||
async def sync_modelscope_mcp_servers(self) -> None:
|
||||
"""Sync ModelScope MCP servers (stub)."""
|
||||
pass
|
||||
|
||||
def load_mcp_config(self) -> dict[str, Any]:
|
||||
"""Load MCP configuration (stub)."""
|
||||
return {"mcpServers": {}}
|
||||
|
||||
def save_mcp_config(self, config: dict[str, Any]) -> bool:
|
||||
"""Save MCP configuration (stub)."""
|
||||
return True
|
||||
|
||||
def activate_llm_tool(self, name: str) -> bool:
|
||||
"""Activate an LLM tool (stub)."""
|
||||
return True
|
||||
|
||||
def deactivate_llm_tool(self, name: str) -> bool:
|
||||
"""Deactivate an LLM tool (stub)."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def mcp_client_dict(self) -> dict[str, Any]:
|
||||
"""Return dict of MCP clients (stub)."""
|
||||
return {}
|
||||
|
||||
@property
|
||||
def mcp_server_runtime_view(self) -> dict[str, Any]:
|
||||
"""Return runtime view of MCP servers (stub)."""
|
||||
return {}
|
||||
|
||||
|
||||
class FuncCall(FunctionToolManager):
|
||||
"""Alias for FunctionToolManager for backward compatibility."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._mcp_server_runtime_view: dict[str, Any] = {}
|
||||
self._mcp_client_dict: dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def mcp_server_runtime_view(self) -> dict[str, Any]:
|
||||
"""Return runtime view of MCP servers."""
|
||||
return self._mcp_server_runtime_view
|
||||
|
||||
@property
|
||||
def mcp_client_dict(self) -> dict[str, Any]:
|
||||
"""Return dict of MCP clients (for backward compatibility)."""
|
||||
return self._mcp_client_dict
|
||||
|
||||
async def init_mcp_clients(self) -> None:
|
||||
"""Initialize MCP clients (stub implementation)."""
|
||||
pass
|
||||
|
||||
def add_func(
|
||||
self,
|
||||
name: str,
|
||||
func_args: list[dict[str, Any]],
|
||||
desc: str,
|
||||
handler: Any,
|
||||
) -> None:
|
||||
"""Add a function tool (deprecated, use add() instead)."""
|
||||
params: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
}
|
||||
for param in func_args:
|
||||
params["properties"][param["name"]] = {
|
||||
"type": param.get("type", "string"),
|
||||
"description": param.get("description", ""),
|
||||
}
|
||||
func = FunctionTool(
|
||||
name=name,
|
||||
parameters=params,
|
||||
description=desc,
|
||||
handler=handler,
|
||||
)
|
||||
self.add(func)
|
||||
|
||||
def remove_func(self, name: str) -> None:
|
||||
"""Remove a function tool by name (deprecated, use remove() instead)."""
|
||||
self.remove(name)
|
||||
|
||||
def get_func(self, name: str) -> FunctionTool | None:
|
||||
"""Get a function tool by name."""
|
||||
return super().get_func(name)
|
||||
|
||||
def names(self) -> list[str]:
|
||||
"""Get all tool names."""
|
||||
return [f.name for f in self.func_list]
|
||||
|
||||
def remove_tool(self, name: str) -> None:
|
||||
"""Remove a tool by its name (alias for remove)."""
|
||||
self.remove(name)
|
||||
|
||||
def get_func_desc_openai_style(
|
||||
self, omit_empty_parameter_field: bool = False
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get tools in OpenAI style (deprecated, use get_full_tool_set().openai_schema())."""
|
||||
tool_set = self.get_full_tool_set()
|
||||
return tool_set.openai_schema(omit_empty_parameter_field)
|
||||
|
||||
async def enable_mcp_server(
|
||||
self, name: str, config: dict[str, Any], init_timeout: int = 30
|
||||
) -> None:
|
||||
"""Enable an MCP server (stub implementation)."""
|
||||
pass
|
||||
|
||||
async def disable_mcp_server(
|
||||
self, name: str = "", timeout: int = 10, shutdown_timeout: int = 10
|
||||
) -> None:
|
||||
"""Disable an MCP server (stub implementation)."""
|
||||
pass
|
||||
|
||||
def load_mcp_config(self) -> dict[str, Any]:
|
||||
"""Load MCP configuration (stub implementation)."""
|
||||
return {"mcpServers": {}}
|
||||
|
||||
def save_mcp_config(self, config: dict[str, Any]) -> bool:
|
||||
"""Save MCP configuration (stub implementation)."""
|
||||
return True
|
||||
|
||||
def activate_llm_tool(self, name: str) -> bool:
|
||||
"""Activate an LLM tool (stub implementation)."""
|
||||
return True
|
||||
|
||||
def deactivate_llm_tool(self, name: str) -> bool:
|
||||
"""Deactivate an LLM tool (stub implementation)."""
|
||||
return True
|
||||
|
||||
async def test_mcp_server_connection(
|
||||
self, config: dict[str, Any]
|
||||
) -> tuple[bool, str]:
|
||||
"""Test MCP server connection (stub implementation)."""
|
||||
# Import the actual test function if available
|
||||
try:
|
||||
from astrbot._internal.protocols.mcp.client import (
|
||||
_quick_test_mcp_connection,
|
||||
)
|
||||
|
||||
success, message = await _quick_test_mcp_connection(config)
|
||||
if not success:
|
||||
raise Exception(message)
|
||||
return success, message
|
||||
except Exception as e:
|
||||
raise Exception(f"MCP connection test failed: {e!s}") from e
|
||||
|
||||
async def sync_modelscope_mcp_servers(self) -> None:
|
||||
"""Sync ModelScope MCP servers (stub implementation)."""
|
||||
pass
|
||||
|
||||
def get_full_tool_set(self) -> ToolSet:
|
||||
"""Return a ToolSet with all active tools."""
|
||||
return ToolSet("default", [t for t in self.func_list if t.active])
|
||||
@@ -1,64 +1,19 @@
|
||||
"""
|
||||
AstrBot Public API.
|
||||
|
||||
This package exposes the public interface for extending and integrating with
|
||||
AstrBot. All exports from this module are guaranteed to be stable across
|
||||
minor version updates.
|
||||
|
||||
Modules:
|
||||
tools: Tool registration and management API
|
||||
mcp: Model Context Protocol server and tool API
|
||||
skills: Skill management and conversion API
|
||||
"""
|
||||
|
||||
from astrbot import logger
|
||||
|
||||
# Tool API
|
||||
from astrbot._internal.tools.base import FunctionTool, ToolSet
|
||||
|
||||
# MCP API
|
||||
from astrbot.api.mcp import (
|
||||
MCPClient,
|
||||
MCPTool,
|
||||
get_mcp_servers,
|
||||
register_mcp_server,
|
||||
unregister_mcp_server,
|
||||
)
|
||||
|
||||
# Skills API
|
||||
from astrbot.api.skills import (
|
||||
SkillInfo,
|
||||
SkillManager,
|
||||
get_skill_manager,
|
||||
skill_to_tool,
|
||||
)
|
||||
|
||||
# Tools API (public interface)
|
||||
from astrbot.api.tools import ToolRegistry, get_registry, tool
|
||||
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.core.star.register import register_llm_tool as llm_tool
|
||||
|
||||
__all__ = [
|
||||
"AstrBotConfig",
|
||||
"BaseFunctionToolExecutor",
|
||||
"FunctionTool",
|
||||
"MCPClient",
|
||||
"MCPTool",
|
||||
"SkillInfo",
|
||||
"SkillManager",
|
||||
"ToolRegistry",
|
||||
"ToolSet",
|
||||
"agent",
|
||||
"get_mcp_servers",
|
||||
"get_registry",
|
||||
"get_skill_manager",
|
||||
"html_renderer",
|
||||
"llm_tool",
|
||||
"logger",
|
||||
"register_mcp_server",
|
||||
"skill_to_tool",
|
||||
"sp",
|
||||
"tool",
|
||||
"unregister_mcp_server",
|
||||
]
|
||||
|
||||
@@ -29,7 +29,7 @@ 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 *
|
||||
|
||||
@@ -24,9 +24,6 @@ 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,
|
||||
@@ -55,14 +52,11 @@ __all__ = [
|
||||
"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",
|
||||
"on_using_llm_tool",
|
||||
"on_llm_tool_respond",
|
||||
]
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
"""
|
||||
MCP (Model Context Protocol) Public API for AstrBot.
|
||||
|
||||
This module provides a simple, stable interface for MCP server management,
|
||||
delegating to the _internal package.
|
||||
|
||||
Example:
|
||||
from astrbot.api.mcp import get_mcp_servers, register_mcp_server
|
||||
|
||||
# List connected servers
|
||||
servers = get_mcp_servers()
|
||||
|
||||
# Register stdio MCP server
|
||||
await register_mcp_server(
|
||||
name="weather",
|
||||
command="uv",
|
||||
args=["tool", "run", "weather-mcp"],
|
||||
)
|
||||
|
||||
# Register SSE server
|
||||
await register_mcp_server(
|
||||
name="fileserver",
|
||||
url="http://localhost:8080/sse",
|
||||
transport="sse",
|
||||
)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
# Import from _internal package (the canonical source)
|
||||
# TODO: fix path - should be protocols.mcp.client
|
||||
from astrbot._internal.protocols.mcp.client import McpClient as MCPClient
|
||||
from astrbot._internal.protocols.mcp.tool import MCPTool
|
||||
|
||||
__all__ = [
|
||||
"MCPClient",
|
||||
"MCPTool",
|
||||
"get_mcp_servers",
|
||||
"register_mcp_server",
|
||||
"unregister_mcp_server",
|
||||
]
|
||||
|
||||
|
||||
def get_mcp_servers() -> dict[str, MCPClient]:
|
||||
"""Get all connected MCP servers."""
|
||||
from astrbot.core.provider.register import llm_tools as func_tool_manager
|
||||
|
||||
manager = func_tool_manager
|
||||
return dict(manager.mcp_client_dict)
|
||||
|
||||
|
||||
async def register_mcp_server(
|
||||
name: str,
|
||||
command: str | None = None,
|
||||
args: list[str] | None = None,
|
||||
url: str | None = None,
|
||||
transport: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Register and connect to an MCP server.
|
||||
|
||||
Args:
|
||||
name: Unique name for this server
|
||||
command: Command to run (for stdio transport)
|
||||
args: Command arguments
|
||||
url: URL (for SSE/Streamable HTTP transports)
|
||||
transport: "sse", "streamable_http", or None for stdio
|
||||
|
||||
Example - Stdio:
|
||||
await register_mcp_server(name="weather", command="uv",
|
||||
args=["tool", "run", "weather-mcp"])
|
||||
"""
|
||||
from astrbot.core.provider.register import llm_tools as func_tool_manager
|
||||
|
||||
manager = func_tool_manager
|
||||
|
||||
config: dict[str, Any] = {}
|
||||
if command is not None:
|
||||
config["command"] = command
|
||||
if args is not None:
|
||||
config["args"] = args
|
||||
if url is not None:
|
||||
config["url"] = url
|
||||
if transport is not None:
|
||||
config["transport"] = transport
|
||||
config.update(kwargs)
|
||||
|
||||
await manager.enable_mcp_server(name=name, config=config)
|
||||
|
||||
|
||||
async def unregister_mcp_server(name: str) -> None:
|
||||
"""Disconnect and remove an MCP server."""
|
||||
from astrbot.core.provider.register import llm_tools as func_tool_manager
|
||||
|
||||
manager = func_tool_manager
|
||||
await manager.disable_mcp_server(name=name)
|
||||
@@ -1,58 +0,0 @@
|
||||
"""
|
||||
Skills Public API for AstrBot.
|
||||
|
||||
This module provides a simple, stable interface for skill management,
|
||||
delegating to the _internal package.
|
||||
|
||||
Two skill types:
|
||||
1. Prompt-based: SKILL.md files injected into system prompt
|
||||
2. Tool-based: Skills with input_schema converted to FunctionTool
|
||||
|
||||
Example:
|
||||
from astrbot.api.skills import get_skill_manager, skill_to_tool
|
||||
|
||||
# List skills
|
||||
mgr = get_skill_manager()
|
||||
skills = mgr.list_skills()
|
||||
|
||||
# Convert tool-based skill to FunctionTool
|
||||
tool_skills = [s for s in skills if s.input_schema]
|
||||
if tool_skills:
|
||||
func_tool = skill_to_tool(tool_skills[0])
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from astrbot._internal.tools.base import FunctionTool
|
||||
|
||||
# Import from _internal package (the canonical source)
|
||||
# TODO: fix path - should be core.skills.skill_manager
|
||||
from astrbot.core.skills.skill_manager import SkillInfo, SkillManager
|
||||
|
||||
__all__ = ["SkillInfo", "SkillManager", "get_skill_manager", "skill_to_tool"]
|
||||
|
||||
|
||||
def get_skill_manager() -> SkillManager:
|
||||
"""Get the global SkillManager instance."""
|
||||
return SkillManager()
|
||||
|
||||
|
||||
def skill_to_tool(skill: SkillInfo) -> FunctionTool | None:
|
||||
"""Convert a tool-based skill (with input_schema) to a FunctionTool.
|
||||
|
||||
Args:
|
||||
skill: A SkillInfo instance with an input_schema
|
||||
|
||||
Returns:
|
||||
A FunctionTool, or None if the skill has no input_schema
|
||||
"""
|
||||
if not skill.input_schema:
|
||||
return None
|
||||
|
||||
return FunctionTool(
|
||||
name=f"skill_{skill.name}",
|
||||
description=skill.description or f"Skill: {skill.name}",
|
||||
parameters=skill.input_schema,
|
||||
handler=None,
|
||||
source="skill",
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
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"]
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
"""
|
||||
Tools Public API for AstrBot.
|
||||
|
||||
This module provides a simple, stable interface for tool registration
|
||||
and management. All implementations are delegated to the _internal package.
|
||||
|
||||
Example:
|
||||
from astrbot.api.tools import tool, get_registry
|
||||
|
||||
@tool(name="weather", description="Get weather", parameters={...})
|
||||
async def get_weather(city: str) -> str:
|
||||
return f"Weather in {city} is sunny"
|
||||
|
||||
registry = get_registry()
|
||||
tools = registry.list_tools()
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from functools import wraps
|
||||
from typing import Any
|
||||
|
||||
# Import from _internal package (the canonical source)
|
||||
from astrbot._internal.tools.base import FunctionTool, ToolSet
|
||||
from astrbot._internal.tools.registry import FunctionToolManager
|
||||
|
||||
__all__ = ["FunctionTool", "ToolRegistry", "ToolSet", "get_registry", "tool"]
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""Wrapper around FunctionToolManager for simplified tool registration.
|
||||
|
||||
This class provides a user-friendly interface for registering and
|
||||
managing tools, delegating to the internal FunctionToolManager.
|
||||
"""
|
||||
|
||||
_instance: ToolRegistry | None = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Import here to avoid circular imports
|
||||
from astrbot.core.provider.register import llm_tools as func_tool_manager
|
||||
|
||||
self._manager: FunctionToolManager = func_tool_manager
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> ToolRegistry:
|
||||
"""Get the singleton ToolRegistry instance."""
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
def register(self, tool: FunctionTool) -> None:
|
||||
"""Register a FunctionTool."""
|
||||
self._manager.func_list.append(tool)
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""Unregister a tool by name. Returns True if found and removed."""
|
||||
for i, f in enumerate(self._manager.func_list):
|
||||
if f.name == name:
|
||||
self._manager.func_list.pop(i)
|
||||
return True
|
||||
return False
|
||||
|
||||
def list_tools(self) -> list[FunctionTool]:
|
||||
"""List all registered tools."""
|
||||
return self._manager.func_list.copy()
|
||||
|
||||
def get_tool(self, name: str) -> FunctionTool | None:
|
||||
"""Get a tool by name."""
|
||||
return self._manager.get_func(name)
|
||||
|
||||
|
||||
def get_registry() -> ToolRegistry:
|
||||
"""Get the global ToolRegistry instance."""
|
||||
return ToolRegistry.get_instance()
|
||||
|
||||
|
||||
def tool(
|
||||
name: str,
|
||||
description: str,
|
||||
parameters: dict[str, Any] | None = None,
|
||||
) -> Callable[
|
||||
[Callable[..., Awaitable[str | None]]], Callable[..., Awaitable[str | None]]
|
||||
]:
|
||||
"""Decorator to register an async function as a tool.
|
||||
|
||||
Args:
|
||||
name: Tool name (used by LLM to invoke it)
|
||||
description: What the tool does
|
||||
parameters: JSON Schema for parameters (optional)
|
||||
|
||||
Example:
|
||||
@tool(name="weather", description="Get weather for a city", parameters={...})
|
||||
async def get_weather(city: str) -> str:
|
||||
return f"The weather in {city} is sunny"
|
||||
"""
|
||||
if parameters is None:
|
||||
parameters = {"type": "object", "properties": {}}
|
||||
|
||||
def decorator(
|
||||
func: Callable[..., Awaitable[str | None]],
|
||||
) -> Callable[..., Awaitable[str | None]]:
|
||||
func_tool = FunctionTool(
|
||||
name=name,
|
||||
description=description,
|
||||
parameters=parameters,
|
||||
handler=func,
|
||||
handler_module_path=getattr(func, "__module__", ""),
|
||||
source="api",
|
||||
)
|
||||
get_registry().register(func_tool)
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(*args: Any, **kwargs: Any) -> str | None:
|
||||
return await func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
@@ -17,7 +17,7 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
||||
|
||||
|
||||
class LongTermMemory:
|
||||
def __init__(self, acm: AstrBotConfigManager, context: star.Context) -> None:
|
||||
def __init__(self, acm: AstrBotConfigManager, context: star.Context):
|
||||
self.acm = acm
|
||||
self.context = context
|
||||
self.session_chats = defaultdict(list)
|
||||
@@ -76,7 +76,7 @@ class LongTermMemory:
|
||||
if not provider:
|
||||
raise Exception(f"没有找到 ID 为 {image_caption_provider_id} 的提供商")
|
||||
if not isinstance(provider, Provider):
|
||||
raise Exception(f"提供商类型错误({type(provider)}),无法获取图片描述")
|
||||
raise Exception(f"提供商类型错误({type(provider)}),无法获取图片描述")
|
||||
response = await provider.text_chat(
|
||||
prompt=image_caption_prompt,
|
||||
session_id=uuid.uuid4().hex,
|
||||
@@ -111,7 +111,7 @@ class LongTermMemory:
|
||||
|
||||
return False
|
||||
|
||||
async def handle_message(self, event: AstrMessageEvent) -> None:
|
||||
async def handle_message(self, event: AstrMessageEvent):
|
||||
"""仅支持群聊"""
|
||||
if event.get_message_type() == MessageType.GROUP_MESSAGE:
|
||||
datetime_str = datetime.datetime.now().strftime("%H:%M:%S")
|
||||
@@ -148,8 +148,8 @@ class LongTermMemory:
|
||||
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
||||
self.session_chats[event.unified_msg_origin].pop(0)
|
||||
|
||||
async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest) -> None:
|
||||
"""当触发 LLM 请求前,调用此方法修改 req"""
|
||||
async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest):
|
||||
"""当触发 LLM 请求前,调用此方法修改 req"""
|
||||
if event.unified_msg_origin not in self.session_chats:
|
||||
return
|
||||
|
||||
@@ -164,16 +164,14 @@ class LongTermMemory:
|
||||
"Please react to it. Only output your response and do not output any other information. "
|
||||
"You MUST use the SAME language as the chatroom is using."
|
||||
)
|
||||
req.contexts = [] # 清空上下文,当使用了主动回复,所有聊天记录都在一个prompt中。
|
||||
req.contexts = [] # 清空上下文,当使用了主动回复,所有聊天记录都在一个prompt中。
|
||||
else:
|
||||
req.system_prompt += (
|
||||
"You are now in a chatroom. The chat history is as follows: \n"
|
||||
)
|
||||
req.system_prompt += chats_str
|
||||
|
||||
async def after_req_llm(
|
||||
self, event: AstrMessageEvent, llm_resp: LLMResponse
|
||||
) -> None:
|
||||
async def after_req_llm(self, event: AstrMessageEvent, llm_resp: LLMResponse):
|
||||
if event.unified_msg_origin not in self.session_chats:
|
||||
return
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from astrbot.api.provider import LLMResponse, ProviderRequest
|
||||
from astrbot.core import logger
|
||||
|
||||
from .long_term_memory import LongTermMemory
|
||||
from .process_llm_request import ProcessLLMRequest
|
||||
|
||||
|
||||
class Main(star.Star):
|
||||
@@ -18,6 +19,8 @@ class Main(star.Star):
|
||||
except BaseException as e:
|
||||
logger.error(f"聊天增强 err: {e}")
|
||||
|
||||
self.proc_llm_req = ProcessLLMRequest(self.context)
|
||||
|
||||
def ltm_enabled(self, event: AstrMessageEvent):
|
||||
ltmse = self.context.get_config(umo=event.unified_msg_origin)[
|
||||
"provider_ltm_settings"
|
||||
@@ -50,7 +53,7 @@ class Main(star.Star):
|
||||
"""主动回复"""
|
||||
provider = self.context.get_using_provider(event.unified_msg_origin)
|
||||
if not provider:
|
||||
logger.error("未找到任何 LLM 提供商。请先配置。无法主动回复")
|
||||
logger.error("未找到任何 LLM 提供商。请先配置。无法主动回复")
|
||||
return
|
||||
try:
|
||||
conv = None
|
||||
@@ -60,7 +63,7 @@ class Main(star.Star):
|
||||
|
||||
if not session_curr_cid:
|
||||
logger.error(
|
||||
"当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。",
|
||||
"当前未处于对话状态,无法主动回复,请确保 平台设置->会话隔离(unique_session) 未开启,并使用 /switch 序号 切换或者 /new 创建一个会话。",
|
||||
)
|
||||
return
|
||||
|
||||
@@ -72,11 +75,12 @@ class Main(star.Star):
|
||||
prompt = event.message_str
|
||||
|
||||
if not conv:
|
||||
logger.error("未找到对话,无法主动回复")
|
||||
logger.error("未找到对话,无法主动回复")
|
||||
return
|
||||
|
||||
yield event.request_llm(
|
||||
prompt=prompt,
|
||||
func_tool_manager=self.context.get_llm_tool_manager(),
|
||||
session_id=event.session_id,
|
||||
conversation=conv,
|
||||
)
|
||||
@@ -85,10 +89,10 @@ class Main(star.Star):
|
||||
logger.error(f"主动回复失败: {e}")
|
||||
|
||||
@filter.on_llm_request()
|
||||
async def decorate_llm_req(
|
||||
self, event: AstrMessageEvent, req: ProviderRequest
|
||||
) -> None:
|
||||
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
|
||||
async def decorate_llm_req(self, event: AstrMessageEvent, req: ProviderRequest):
|
||||
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
|
||||
await self.proc_llm_req.process_llm_request(event, req)
|
||||
|
||||
if self.ltm and self.ltm_enabled(event):
|
||||
try:
|
||||
await self.ltm.on_req_llm(event, req)
|
||||
@@ -96,9 +100,7 @@ class Main(star.Star):
|
||||
logger.error(f"ltm: {e}")
|
||||
|
||||
@filter.on_llm_response()
|
||||
async def record_llm_resp_to_ltm(
|
||||
self, event: AstrMessageEvent, resp: LLMResponse
|
||||
) -> None:
|
||||
async def record_llm_resp_to_ltm(self, event: AstrMessageEvent, resp: LLMResponse):
|
||||
"""在 LLM 响应后记录对话"""
|
||||
if self.ltm and self.ltm_enabled(event):
|
||||
try:
|
||||
@@ -107,7 +109,7 @@ class Main(star.Star):
|
||||
logger.error(f"ltm: {e}")
|
||||
|
||||
@filter.after_message_sent()
|
||||
async def after_message_sent(self, event: AstrMessageEvent) -> None:
|
||||
async def after_message_sent(self, event: AstrMessageEvent):
|
||||
"""消息发送后处理"""
|
||||
if self.ltm and self.ltm_enabled(event):
|
||||
try:
|
||||
|
||||
308
astrbot/builtin_stars/astrbot/process_llm_request.py
Normal file
308
astrbot/builtin_stars/astrbot/process_llm_request.py
Normal file
@@ -0,0 +1,308 @@
|
||||
import builtins
|
||||
import copy
|
||||
import datetime
|
||||
import zoneinfo
|
||||
|
||||
from astrbot.api import logger, sp, star
|
||||
from astrbot.api.event import AstrMessageEvent
|
||||
from astrbot.api.message_components import Image, Reply
|
||||
from astrbot.api.provider import Provider, ProviderRequest
|
||||
from astrbot.core.agent.message import TextPart
|
||||
from astrbot.core.pipeline.process_stage.utils import (
|
||||
CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT,
|
||||
LOCAL_EXECUTE_SHELL_TOOL,
|
||||
LOCAL_PYTHON_TOOL,
|
||||
)
|
||||
from astrbot.core.provider.func_tool_manager import ToolSet
|
||||
from astrbot.core.skills.skill_manager import SkillManager, build_skills_prompt
|
||||
|
||||
|
||||
class ProcessLLMRequest:
|
||||
def __init__(self, context: star.Context):
|
||||
self.ctx = context
|
||||
cfg = context.get_config()
|
||||
self.timezone = cfg.get("timezone")
|
||||
if not self.timezone:
|
||||
# 系统默认时区
|
||||
self.timezone = None
|
||||
else:
|
||||
logger.info(f"Timezone set to: {self.timezone}")
|
||||
|
||||
self.skill_manager = SkillManager()
|
||||
|
||||
def _apply_local_env_tools(self, req: ProviderRequest) -> None:
|
||||
"""Add local environment tools to the provider request."""
|
||||
if req.func_tool is None:
|
||||
req.func_tool = ToolSet()
|
||||
req.func_tool.add_tool(LOCAL_EXECUTE_SHELL_TOOL)
|
||||
req.func_tool.add_tool(LOCAL_PYTHON_TOOL)
|
||||
|
||||
async def _ensure_persona(
|
||||
self,
|
||||
req: ProviderRequest,
|
||||
cfg: dict,
|
||||
umo: str,
|
||||
platform_type: str,
|
||||
event: AstrMessageEvent,
|
||||
):
|
||||
"""确保用户人格已加载"""
|
||||
if not req.conversation:
|
||||
return
|
||||
# persona inject
|
||||
|
||||
# custom rule is preferred
|
||||
persona_id = (
|
||||
await sp.get_async(
|
||||
scope="umo", scope_id=umo, key="session_service_config", default={}
|
||||
)
|
||||
).get("persona_id")
|
||||
|
||||
if not persona_id:
|
||||
persona_id = req.conversation.persona_id or cfg.get("default_personality")
|
||||
if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
|
||||
default_persona = self.ctx.persona_manager.selected_default_persona_v3
|
||||
if default_persona:
|
||||
persona_id = default_persona["name"]
|
||||
|
||||
# ChatUI special default persona
|
||||
if platform_type == "webchat":
|
||||
# non-existent persona_id to let following codes not working
|
||||
persona_id = "_chatui_default_"
|
||||
req.system_prompt += CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT
|
||||
|
||||
persona = next(
|
||||
builtins.filter(
|
||||
lambda persona: persona["name"] == persona_id,
|
||||
self.ctx.persona_manager.personas_v3,
|
||||
),
|
||||
None,
|
||||
)
|
||||
if persona:
|
||||
if prompt := persona["prompt"]:
|
||||
req.system_prompt += prompt
|
||||
if begin_dialogs := copy.deepcopy(persona["_begin_dialogs_processed"]):
|
||||
req.contexts[:0] = begin_dialogs
|
||||
|
||||
# skills select and prompt
|
||||
runtime = self.skills_cfg.get("runtime", "local")
|
||||
skills = self.skill_manager.list_skills(active_only=True, runtime=runtime)
|
||||
if runtime == "sandbox" and not self.sandbox_cfg.get("enable", False):
|
||||
logger.warning(
|
||||
"Skills runtime is set to sandbox, but sandbox mode is disabled, will skip skills prompt injection.",
|
||||
)
|
||||
req.system_prompt += "\n[Background: User added some skills, and skills runtime is set to sandbox, but sandbox mode is disabled. So skills will be unavailable.]\n"
|
||||
elif skills:
|
||||
# persona.skills == None means all skills are allowed
|
||||
if persona and persona.get("skills") is not None:
|
||||
if not persona["skills"]:
|
||||
return
|
||||
allowed = set(persona["skills"])
|
||||
skills = [skill for skill in skills if skill.name in allowed]
|
||||
if skills:
|
||||
req.system_prompt += f"\n{build_skills_prompt(skills)}\n"
|
||||
|
||||
# if user wants to use skills in non-sandbox mode, apply local env tools
|
||||
runtime = self.skills_cfg.get("runtime", "local")
|
||||
sandbox_enabled = self.sandbox_cfg.get("enable", False)
|
||||
if runtime == "local" and not sandbox_enabled:
|
||||
self._apply_local_env_tools(req)
|
||||
|
||||
# tools select
|
||||
tmgr = self.ctx.get_llm_tool_manager()
|
||||
if (persona and persona.get("tools") is None) or not persona:
|
||||
# select all
|
||||
toolset = tmgr.get_full_tool_set()
|
||||
for tool in toolset:
|
||||
if not tool.active:
|
||||
toolset.remove_tool(tool.name)
|
||||
else:
|
||||
toolset = ToolSet()
|
||||
if persona["tools"]:
|
||||
for tool_name in persona["tools"]:
|
||||
tool = tmgr.get_func(tool_name)
|
||||
if tool and tool.active:
|
||||
toolset.add_tool(tool)
|
||||
if not req.func_tool:
|
||||
req.func_tool = toolset
|
||||
else:
|
||||
req.func_tool.merge(toolset)
|
||||
event.trace.record(
|
||||
"sel_persona", persona_id=persona_id, persona_toolset=toolset.names()
|
||||
)
|
||||
logger.debug(f"Tool set for persona {persona_id}: {toolset.names()}")
|
||||
|
||||
async def _ensure_img_caption(
|
||||
self,
|
||||
req: ProviderRequest,
|
||||
cfg: dict,
|
||||
img_cap_prov_id: str,
|
||||
):
|
||||
try:
|
||||
caption = await self._request_img_caption(
|
||||
img_cap_prov_id,
|
||||
cfg,
|
||||
req.image_urls,
|
||||
)
|
||||
if caption:
|
||||
req.extra_user_content_parts.append(
|
||||
TextPart(text=f"<image_caption>{caption}</image_caption>")
|
||||
)
|
||||
req.image_urls = []
|
||||
except Exception as e:
|
||||
logger.error(f"处理图片描述失败: {e}")
|
||||
|
||||
async def _request_img_caption(
|
||||
self,
|
||||
provider_id: str,
|
||||
cfg: dict,
|
||||
image_urls: list[str],
|
||||
) -> str:
|
||||
if prov := self.ctx.get_provider_by_id(provider_id):
|
||||
if isinstance(prov, Provider):
|
||||
img_cap_prompt = cfg.get(
|
||||
"image_caption_prompt",
|
||||
"Please describe the image.",
|
||||
)
|
||||
logger.debug(f"Processing image caption with provider: {provider_id}")
|
||||
llm_resp = await prov.text_chat(
|
||||
prompt=img_cap_prompt,
|
||||
image_urls=image_urls,
|
||||
)
|
||||
return llm_resp.completion_text
|
||||
raise ValueError(
|
||||
f"Cannot get image caption because provider `{provider_id}` is not a valid Provider, it is {type(prov)}.",
|
||||
)
|
||||
raise ValueError(
|
||||
f"Cannot get image caption because provider `{provider_id}` is not exist.",
|
||||
)
|
||||
|
||||
async def process_llm_request(self, event: AstrMessageEvent, req: ProviderRequest):
|
||||
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
|
||||
cfg: dict = self.ctx.get_config(umo=event.unified_msg_origin)[
|
||||
"provider_settings"
|
||||
]
|
||||
self.skills_cfg = cfg.get("skills", {})
|
||||
self.sandbox_cfg = cfg.get("sandbox", {})
|
||||
|
||||
# prompt prefix
|
||||
if prefix := cfg.get("prompt_prefix"):
|
||||
# 支持 {{prompt}} 作为用户输入的占位符
|
||||
if "{{prompt}}" in prefix:
|
||||
req.prompt = prefix.replace("{{prompt}}", req.prompt)
|
||||
else:
|
||||
req.prompt = prefix + req.prompt
|
||||
|
||||
# 收集系统提醒信息
|
||||
system_parts = []
|
||||
|
||||
# user identifier
|
||||
if cfg.get("identifier"):
|
||||
user_id = event.message_obj.sender.user_id
|
||||
user_nickname = event.message_obj.sender.nickname
|
||||
system_parts.append(f"User ID: {user_id}, Nickname: {user_nickname}")
|
||||
|
||||
# group name identifier
|
||||
if cfg.get("group_name_display") and event.message_obj.group_id:
|
||||
if not event.message_obj.group:
|
||||
logger.error(
|
||||
f"Group name display enabled but group object is None. Group ID: {event.message_obj.group_id}"
|
||||
)
|
||||
return
|
||||
group_name = event.message_obj.group.group_name
|
||||
if group_name:
|
||||
system_parts.append(f"Group name: {group_name}")
|
||||
|
||||
# time info
|
||||
if cfg.get("datetime_system_prompt"):
|
||||
current_time = None
|
||||
if self.timezone:
|
||||
# 启用时区
|
||||
try:
|
||||
now = datetime.datetime.now(zoneinfo.ZoneInfo(self.timezone))
|
||||
current_time = now.strftime("%Y-%m-%d %H:%M (%Z)")
|
||||
except Exception as e:
|
||||
logger.error(f"时区设置错误: {e}, 使用本地时区")
|
||||
if not current_time:
|
||||
current_time = (
|
||||
datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
|
||||
)
|
||||
system_parts.append(f"Current datetime: {current_time}")
|
||||
|
||||
img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
|
||||
if req.conversation:
|
||||
# inject persona for this request
|
||||
platform_type = event.get_platform_name()
|
||||
await self._ensure_persona(
|
||||
req, cfg, event.unified_msg_origin, platform_type, event
|
||||
)
|
||||
|
||||
# image caption
|
||||
if img_cap_prov_id and req.image_urls:
|
||||
await self._ensure_img_caption(req, cfg, img_cap_prov_id)
|
||||
|
||||
# quote message processing
|
||||
# 解析引用内容
|
||||
quote = None
|
||||
for comp in event.message_obj.message:
|
||||
if isinstance(comp, Reply):
|
||||
quote = comp
|
||||
break
|
||||
if quote:
|
||||
content_parts = []
|
||||
|
||||
# 1. 处理引用的文本
|
||||
sender_info = (
|
||||
f"({quote.sender_nickname}): " if quote.sender_nickname else ""
|
||||
)
|
||||
message_str = quote.message_str or "[Empty Text]"
|
||||
content_parts.append(f"{sender_info}{message_str}")
|
||||
|
||||
# 2. 处理引用的图片 (保留原有逻辑,但改变输出目标)
|
||||
image_seg = None
|
||||
if quote.chain:
|
||||
for comp in quote.chain:
|
||||
if isinstance(comp, Image):
|
||||
image_seg = comp
|
||||
break
|
||||
|
||||
if image_seg:
|
||||
try:
|
||||
# 找到可以生成图片描述的 provider
|
||||
prov = None
|
||||
if img_cap_prov_id:
|
||||
prov = self.ctx.get_provider_by_id(img_cap_prov_id)
|
||||
if prov is None:
|
||||
prov = self.ctx.get_using_provider(event.unified_msg_origin)
|
||||
|
||||
# 调用 provider 生成图片描述
|
||||
if prov and isinstance(prov, Provider):
|
||||
llm_resp = await prov.text_chat(
|
||||
prompt="Please describe the image content.",
|
||||
image_urls=[await image_seg.convert_to_file_path()],
|
||||
)
|
||||
if llm_resp.completion_text:
|
||||
# 将图片描述作为文本添加到 content_parts
|
||||
content_parts.append(
|
||||
f"[Image Caption in quoted message]: {llm_resp.completion_text}"
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"No provider found for image captioning in quote."
|
||||
)
|
||||
except BaseException as e:
|
||||
logger.error(f"处理引用图片失败: {e}")
|
||||
|
||||
# 3. 将所有部分组合成文本并添加到 extra_user_content_parts 中
|
||||
# 确保引用内容被正确的标签包裹
|
||||
quoted_content = "\n".join(content_parts)
|
||||
# 确保所有内容都在<Quoted Message>标签内
|
||||
quoted_text = f"<Quoted Message>\n{quoted_content}\n</Quoted Message>"
|
||||
|
||||
req.extra_user_content_parts.append(TextPart(text=quoted_text))
|
||||
|
||||
# 统一包裹所有系统提醒
|
||||
if system_parts:
|
||||
system_content = (
|
||||
"<system_reminder>" + "\n".join(system_parts) + "</system_reminder>"
|
||||
)
|
||||
req.extra_user_content_parts.append(TextPart(text=system_content))
|
||||
@@ -5,60 +5,60 @@ from astrbot.core.utils.io import download_dashboard
|
||||
|
||||
|
||||
class AdminCommands:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None:
|
||||
"""授权管理员。op <admin_id>"""
|
||||
async def op(self, event: AstrMessageEvent, admin_id: str = ""):
|
||||
"""授权管理员。op <admin_id>"""
|
||||
if not admin_id:
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
"使用方法: /op <id> 授权管理员;/deop <id> 取消管理员。可通过 /sid 获取 ID。",
|
||||
"使用方法: /op <id> 授权管理员;/deop <id> 取消管理员。可通过 /sid 获取 ID。",
|
||||
),
|
||||
)
|
||||
return
|
||||
self.context.get_config()["admins_id"].append(str(admin_id))
|
||||
self.context.get_config().save_config()
|
||||
event.set_result(MessageEventResult().message("授权成功。"))
|
||||
event.set_result(MessageEventResult().message("授权成功。"))
|
||||
|
||||
async def deop(self, event: AstrMessageEvent, admin_id: str = "") -> None:
|
||||
"""取消授权管理员。deop <admin_id>"""
|
||||
async def deop(self, event: AstrMessageEvent, admin_id: str = ""):
|
||||
"""取消授权管理员。deop <admin_id>"""
|
||||
if not admin_id:
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
"使用方法: /deop <id> 取消管理员。可通过 /sid 获取 ID。",
|
||||
"使用方法: /deop <id> 取消管理员。可通过 /sid 获取 ID。",
|
||||
),
|
||||
)
|
||||
return
|
||||
try:
|
||||
self.context.get_config()["admins_id"].remove(str(admin_id))
|
||||
self.context.get_config().save_config()
|
||||
event.set_result(MessageEventResult().message("取消授权成功。"))
|
||||
event.set_result(MessageEventResult().message("取消授权成功。"))
|
||||
except ValueError:
|
||||
event.set_result(
|
||||
MessageEventResult().message("此用户 ID 不在管理员名单内。"),
|
||||
MessageEventResult().message("此用户 ID 不在管理员名单内。"),
|
||||
)
|
||||
|
||||
async def wl(self, event: AstrMessageEvent, sid: str = "") -> None:
|
||||
"""添加白名单。wl <sid>"""
|
||||
async def wl(self, event: AstrMessageEvent, sid: str = ""):
|
||||
"""添加白名单。wl <sid>"""
|
||||
if not sid:
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
"使用方法: /wl <id> 添加白名单;/dwl <id> 删除白名单。可通过 /sid 获取 ID。",
|
||||
"使用方法: /wl <id> 添加白名单;/dwl <id> 删除白名单。可通过 /sid 获取 ID。",
|
||||
),
|
||||
)
|
||||
return
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
cfg["platform_settings"]["id_whitelist"].append(str(sid))
|
||||
cfg.save_config()
|
||||
event.set_result(MessageEventResult().message("添加白名单成功。"))
|
||||
event.set_result(MessageEventResult().message("添加白名单成功。"))
|
||||
|
||||
async def dwl(self, event: AstrMessageEvent, sid: str = "") -> None:
|
||||
"""删除白名单。dwl <sid>"""
|
||||
async def dwl(self, event: AstrMessageEvent, sid: str = ""):
|
||||
"""删除白名单。dwl <sid>"""
|
||||
if not sid:
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
"使用方法: /dwl <id> 删除白名单。可通过 /sid 获取 ID。",
|
||||
"使用方法: /dwl <id> 删除白名单。可通过 /sid 获取 ID。",
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -66,12 +66,12 @@ class AdminCommands:
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
cfg["platform_settings"]["id_whitelist"].remove(str(sid))
|
||||
cfg.save_config()
|
||||
event.set_result(MessageEventResult().message("删除白名单成功。"))
|
||||
event.set_result(MessageEventResult().message("删除白名单成功。"))
|
||||
except ValueError:
|
||||
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
||||
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
||||
|
||||
async def update_dashboard(self, event: AstrMessageEvent) -> None:
|
||||
async def update_dashboard(self, event: AstrMessageEvent):
|
||||
"""更新管理面板"""
|
||||
await event.send(MessageChain().message("正在尝试更新管理面板..."))
|
||||
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||
await event.send(MessageChain().message("管理面板更新完成。"))
|
||||
await event.send(MessageChain().message("管理面板更新完成。"))
|
||||
|
||||
@@ -11,14 +11,14 @@ from .utils.rst_scene import RstScene
|
||||
|
||||
|
||||
class AlterCmdCommands(CommandParserMixin):
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def update_reset_permission(self, scene_key: str, perm_type: str) -> None:
|
||||
async def update_reset_permission(self, scene_key: str, perm_type: str):
|
||||
"""更新reset命令在特定场景下的权限设置"""
|
||||
from astrbot.api import sp
|
||||
|
||||
alter_cmd_cfg = await sp.global_get("alter_cmd", {}) or {}
|
||||
alter_cmd_cfg = await sp.global_get("alter_cmd", {})
|
||||
plugin_cfg = alter_cmd_cfg.get("astrbot", {})
|
||||
reset_cfg = plugin_cfg.get("reset", {})
|
||||
reset_cfg[scene_key] = perm_type
|
||||
@@ -26,12 +26,12 @@ class AlterCmdCommands(CommandParserMixin):
|
||||
alter_cmd_cfg["astrbot"] = plugin_cfg
|
||||
await sp.global_put("alter_cmd", alter_cmd_cfg)
|
||||
|
||||
async def alter_cmd(self, event: AstrMessageEvent) -> None:
|
||||
async def alter_cmd(self, event: AstrMessageEvent):
|
||||
token = self.parse_commands(event.message_str)
|
||||
if token.len < 3:
|
||||
await event.send(
|
||||
MessageChain().message(
|
||||
"该指令用于设置指令或指令组的权限。\n"
|
||||
"该指令用于设置指令或指令组的权限。\n"
|
||||
"格式: /alter_cmd <cmd_name> <admin/member>\n"
|
||||
"例1: /alter_cmd c1 admin 将 c1 设为管理员指令\n"
|
||||
"例2: /alter_cmd g1 c1 admin 将 g1 指令组的 c1 子指令设为管理员指令\n"
|
||||
@@ -47,7 +47,7 @@ class AlterCmdCommands(CommandParserMixin):
|
||||
if cmd_name == "reset" and cmd_type == "config":
|
||||
from astrbot.api import sp
|
||||
|
||||
alter_cmd_cfg = await sp.global_get("alter_cmd", {}) or {}
|
||||
alter_cmd_cfg = await sp.global_get("alter_cmd", {})
|
||||
plugin_ = alter_cmd_cfg.get("astrbot", {})
|
||||
reset_cfg = plugin_.get("reset", {})
|
||||
|
||||
@@ -56,11 +56,11 @@ class AlterCmdCommands(CommandParserMixin):
|
||||
private = reset_cfg.get("private", "member")
|
||||
|
||||
config_menu = f"""reset命令权限细粒度配置
|
||||
当前配置:
|
||||
当前配置:
|
||||
1. 群聊+会话隔离开: {group_unique_on}
|
||||
2. 群聊+会话隔离关: {group_unique_off}
|
||||
3. 私聊: {private}
|
||||
修改指令格式:
|
||||
修改指令格式:
|
||||
/alter_cmd reset scene <场景编号> <admin/member>
|
||||
例如: /alter_cmd reset scene 2 member"""
|
||||
await event.send(MessageChain().message(config_menu))
|
||||
@@ -82,7 +82,7 @@ class AlterCmdCommands(CommandParserMixin):
|
||||
|
||||
if perm_type not in ["admin", "member"]:
|
||||
await event.send(
|
||||
MessageChain().message("权限类型错误,只能是 admin 或 member"),
|
||||
MessageChain().message("权限类型错误,只能是 admin 或 member"),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -101,7 +101,7 @@ class AlterCmdCommands(CommandParserMixin):
|
||||
|
||||
if cmd_type not in ["admin", "member"]:
|
||||
await event.send(
|
||||
MessageChain().message("指令类型错误,可选类型有 admin, member"),
|
||||
MessageChain().message("指令类型错误,可选类型有 admin, member"),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -131,7 +131,7 @@ class AlterCmdCommands(CommandParserMixin):
|
||||
|
||||
from astrbot.api import sp
|
||||
|
||||
alter_cmd_cfg = await sp.global_get("alter_cmd", {}) or {}
|
||||
alter_cmd_cfg = await sp.global_get("alter_cmd", {})
|
||||
plugin_ = alter_cmd_cfg.get(found_plugin.name, {})
|
||||
cfg = plugin_.get(found_command.handler_name, {})
|
||||
cfg["permission"] = cmd_type
|
||||
@@ -168,6 +168,6 @@ class AlterCmdCommands(CommandParserMixin):
|
||||
cmd_group_str = "指令组" if cmd_group else "指令"
|
||||
await event.send(
|
||||
MessageChain().message(
|
||||
f"已将「{cmd_name}」{cmd_group_str} 的权限级别调整为 {cmd_type}。",
|
||||
f"已将「{cmd_name}」{cmd_group_str} 的权限级别调整为 {cmd_type}。",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2,13 +2,8 @@ import datetime
|
||||
|
||||
from astrbot.api import sp, star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
from astrbot.core.agent.runners.deerflow.constants import (
|
||||
DEERFLOW_PROVIDER_TYPE,
|
||||
DEERFLOW_THREAD_ID_KEY,
|
||||
)
|
||||
from astrbot.core.platform.astr_message_event import MessageSession
|
||||
from astrbot.core.platform.message_type import MessageType
|
||||
from astrbot.core.utils.active_event_registry import active_event_registry
|
||||
|
||||
from .utils.rst_scene import RstScene
|
||||
|
||||
@@ -16,13 +11,12 @@ 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())
|
||||
|
||||
|
||||
class ConversationCommands:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def _get_current_persona_id(self, session_id):
|
||||
@@ -39,7 +33,7 @@ class ConversationCommands:
|
||||
return None
|
||||
return conv.persona_id
|
||||
|
||||
async def reset(self, message: AstrMessageEvent) -> None:
|
||||
async def reset(self, message: AstrMessageEvent):
|
||||
"""重置 LLM 会话"""
|
||||
umo = message.unified_msg_origin
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
@@ -48,7 +42,7 @@ class ConversationCommands:
|
||||
|
||||
scene = RstScene.get_scene(is_group, is_unique_session)
|
||||
|
||||
alter_cmd_cfg = await sp.get_async("global", "global", "alter_cmd", {}) or {}
|
||||
alter_cmd_cfg = await sp.get_async("global", "global", "alter_cmd", {})
|
||||
plugin_config = alter_cmd_cfg.get("astrbot", {})
|
||||
reset_cfg = plugin_config.get("reset", {})
|
||||
|
||||
@@ -60,26 +54,25 @@ class ConversationCommands:
|
||||
if required_perm == "admin" and message.role != "admin":
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"在{scene.name}场景下,reset命令需要管理员权限,"
|
||||
f"您 (ID {message.get_sender_id()}) 不是管理员,无法执行此操作。",
|
||||
f"在{scene.name}场景下,reset命令需要管理员权限,"
|
||||
f"您 (ID {message.get_sender_id()}) 不是管理员,无法执行此操作。",
|
||||
),
|
||||
)
|
||||
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 sp.remove_async(
|
||||
scope="umo",
|
||||
scope_id=umo,
|
||||
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
|
||||
)
|
||||
message.set_result(MessageEventResult().message("重置对话成功。"))
|
||||
message.set_result(MessageEventResult().message("重置对话成功。"))
|
||||
return
|
||||
|
||||
if not self.context.get_using_provider(umo):
|
||||
message.set_result(
|
||||
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"),
|
||||
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -88,54 +81,28 @@ class ConversationCommands:
|
||||
if not cid:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"当前未处于对话状态,请 /switch 切换或者 /new 创建。",
|
||||
"当前未处于对话状态,请 /switch 切换或者 /new 创建。",
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
active_event_registry.stop_all(umo, exclude=message)
|
||||
|
||||
await self.context.conversation_manager.update_conversation(
|
||||
umo,
|
||||
cid,
|
||||
[],
|
||||
)
|
||||
|
||||
ret = "清除聊天历史成功!"
|
||||
ret = "清除聊天历史成功!"
|
||||
|
||||
message.set_extra("_clean_ltm_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"已请求停止 {stopped_count} 个运行中的任务。"
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
message.set_result(MessageEventResult().message("当前会话没有运行中的任务。"))
|
||||
|
||||
async def his(self, message: AstrMessageEvent, page: int = 1) -> None:
|
||||
async def his(self, message: AstrMessageEvent, page: int = 1):
|
||||
"""查看对话记录"""
|
||||
if not self.context.get_using_provider(message.unified_msg_origin):
|
||||
message.set_result(
|
||||
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"),
|
||||
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -166,7 +133,7 @@ class ConversationCommands:
|
||||
|
||||
history = "".join(parts)
|
||||
ret = (
|
||||
f"当前对话历史记录:"
|
||||
f"当前对话历史记录:"
|
||||
f"{history or '无历史记录'}\n\n"
|
||||
f"第 {page} 页 | 共 {total_pages} 页\n"
|
||||
f"*输入 /history 2 跳转到第 2 页"
|
||||
@@ -174,14 +141,14 @@ class ConversationCommands:
|
||||
|
||||
message.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||
|
||||
async def convs(self, message: AstrMessageEvent, page: int = 1) -> None:
|
||||
async def convs(self, message: AstrMessageEvent, page: int = 1):
|
||||
"""查看对话列表"""
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"{THIRD_PARTY_AGENT_RUNNER_STR} 对话列表功能暂不支持。",
|
||||
f"{THIRD_PARTY_AGENT_RUNNER_STR} 对话列表功能暂不支持。",
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -200,7 +167,7 @@ class ConversationCommands:
|
||||
end_idx = start_idx + size_per_page
|
||||
conversations_paged = conversations_all[start_idx:end_idx]
|
||||
|
||||
parts = ["对话列表:\n---\n"]
|
||||
parts = ["对话列表:\n---\n"]
|
||||
"""全局序号从当前页的第一个开始"""
|
||||
global_index = start_idx + 1
|
||||
|
||||
@@ -211,33 +178,16 @@ class ConversationCommands:
|
||||
_titles[conv.cid] = title
|
||||
|
||||
"""遍历分页后的对话生成列表显示"""
|
||||
provider_settings = cfg.get("provider_settings", {})
|
||||
platform_name = message.get_platform_name()
|
||||
for conv in conversations_paged:
|
||||
(
|
||||
persona_id,
|
||||
_,
|
||||
force_applied_persona_id,
|
||||
_,
|
||||
) = await self.context.persona_manager.resolve_selected_persona(
|
||||
umo=message.unified_msg_origin,
|
||||
conversation_persona_id=conv.persona_id,
|
||||
platform_name=platform_name,
|
||||
provider_settings=provider_settings,
|
||||
)
|
||||
if persona_id == "[%None]":
|
||||
persona_name = "无"
|
||||
elif persona_id:
|
||||
persona_name = persona_id
|
||||
else:
|
||||
persona_name = "无"
|
||||
|
||||
if force_applied_persona_id:
|
||||
persona_name = f"{persona_name} (自定义规则)"
|
||||
|
||||
persona_id = conv.persona_id
|
||||
if not persona_id or persona_id == "[%None]":
|
||||
persona = await self.context.persona_manager.get_default_persona_v3(
|
||||
umo=message.unified_msg_origin,
|
||||
)
|
||||
persona_id = persona["name"]
|
||||
title = _titles.get(conv.cid, "新对话")
|
||||
parts.append(
|
||||
f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_name}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n"
|
||||
f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_id}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n"
|
||||
)
|
||||
global_index += 1
|
||||
|
||||
@@ -266,21 +216,19 @@ class ConversationCommands:
|
||||
message.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||
return
|
||||
|
||||
async def new_conv(self, message: AstrMessageEvent) -> None:
|
||||
async def new_conv(self, message: AstrMessageEvent):
|
||||
"""创建新对话"""
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
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 sp.remove_async(
|
||||
scope="umo",
|
||||
scope_id=message.unified_msg_origin,
|
||||
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
|
||||
)
|
||||
message.set_result(MessageEventResult().message("已创建新对话。"))
|
||||
message.set_result(MessageEventResult().message("已创建新对话。"))
|
||||
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,
|
||||
@@ -291,10 +239,10 @@ class ConversationCommands:
|
||||
message.set_extra("_clean_ltm_session", True)
|
||||
|
||||
message.set_result(
|
||||
MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"),
|
||||
MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"),
|
||||
)
|
||||
|
||||
async def groupnew_conv(self, message: AstrMessageEvent, sid: str = "") -> None:
|
||||
async def groupnew_conv(self, message: AstrMessageEvent, sid: str = ""):
|
||||
"""创建新群聊对话"""
|
||||
if sid:
|
||||
session = str(
|
||||
@@ -313,30 +261,30 @@ class ConversationCommands:
|
||||
)
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"群聊 {session} 已切换到新对话: 新对话({cid[:4]})。",
|
||||
f"群聊 {session} 已切换到新对话: 新对话({cid[:4]})。",
|
||||
),
|
||||
)
|
||||
else:
|
||||
message.set_result(
|
||||
MessageEventResult().message("请输入群聊 ID。/groupnew 群聊ID。"),
|
||||
MessageEventResult().message("请输入群聊 ID。/groupnew 群聊ID。"),
|
||||
)
|
||||
|
||||
async def switch_conv(
|
||||
self,
|
||||
message: AstrMessageEvent,
|
||||
index: int | None = None,
|
||||
) -> None:
|
||||
):
|
||||
"""通过 /ls 前面的序号切换对话"""
|
||||
if not isinstance(index, int):
|
||||
message.set_result(
|
||||
MessageEventResult().message("类型错误,请输入数字对话序号。"),
|
||||
MessageEventResult().message("类型错误,请输入数字对话序号。"),
|
||||
)
|
||||
return
|
||||
|
||||
if index is None:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"请输入对话序号。/switch 对话序号。/ls 查看对话 /new 新建对话",
|
||||
"请输入对话序号。/switch 对话序号。/ls 查看对话 /new 新建对话",
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -345,7 +293,7 @@ class ConversationCommands:
|
||||
)
|
||||
if index > len(conversations) or index < 1:
|
||||
message.set_result(
|
||||
MessageEventResult().message("对话序号错误,请使用 /ls 查看"),
|
||||
MessageEventResult().message("对话序号错误,请使用 /ls 查看"),
|
||||
)
|
||||
else:
|
||||
conversation = conversations[index - 1]
|
||||
@@ -356,65 +304,63 @@ class ConversationCommands:
|
||||
)
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"切换到对话: {title}({conversation.cid[:4]})。",
|
||||
f"切换到对话: {title}({conversation.cid[:4]})。",
|
||||
),
|
||||
)
|
||||
|
||||
async def rename_conv(self, message: AstrMessageEvent, new_name: str = "") -> None:
|
||||
async def rename_conv(self, message: AstrMessageEvent, new_name: str = ""):
|
||||
"""重命名对话"""
|
||||
if not new_name:
|
||||
message.set_result(MessageEventResult().message("请输入新的对话名称。"))
|
||||
message.set_result(MessageEventResult().message("请输入新的对话名称。"))
|
||||
return
|
||||
await self.context.conversation_manager.update_conversation_title(
|
||||
message.unified_msg_origin,
|
||||
new_name,
|
||||
)
|
||||
message.set_result(MessageEventResult().message("重命名对话成功。"))
|
||||
message.set_result(MessageEventResult().message("重命名对话成功。"))
|
||||
|
||||
async def del_conv(self, message: AstrMessageEvent) -> None:
|
||||
async def del_conv(self, message: AstrMessageEvent):
|
||||
"""删除当前对话"""
|
||||
umo = message.unified_msg_origin
|
||||
cfg = self.context.get_config(umo=umo)
|
||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||
is_unique_session = cfg["platform_settings"]["unique_session"]
|
||||
if message.get_group_id() and not is_unique_session and message.role != "admin":
|
||||
# 群聊,没开独立会话,发送人不是管理员
|
||||
# 群聊,没开独立会话,发送人不是管理员
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"会话处于群聊,并且未开启独立会话,并且您 (ID {message.get_sender_id()}) 不是管理员,因此没有权限删除当前对话。",
|
||||
f"会话处于群聊,并且未开启独立会话,并且您 (ID {message.get_sender_id()}) 不是管理员,因此没有权限删除当前对话。",
|
||||
),
|
||||
)
|
||||
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 sp.remove_async(
|
||||
scope="umo",
|
||||
scope_id=umo,
|
||||
scope_id=message.unified_msg_origin,
|
||||
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
|
||||
)
|
||||
message.set_result(MessageEventResult().message("重置对话成功。"))
|
||||
message.set_result(MessageEventResult().message("重置对话成功。"))
|
||||
return
|
||||
|
||||
session_curr_cid = (
|
||||
await self.context.conversation_manager.get_curr_conversation_id(umo)
|
||||
await self.context.conversation_manager.get_curr_conversation_id(
|
||||
message.unified_msg_origin,
|
||||
)
|
||||
)
|
||||
|
||||
if not session_curr_cid:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"当前未处于对话状态,请 /switch 序号 切换或 /new 创建。",
|
||||
"当前未处于对话状态,请 /switch 序号 切换或 /new 创建。",
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
active_event_registry.stop_all(umo, exclude=message)
|
||||
|
||||
await self.context.conversation_manager.delete_conversation(
|
||||
umo,
|
||||
message.unified_msg_origin,
|
||||
session_curr_cid,
|
||||
)
|
||||
|
||||
ret = "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。"
|
||||
ret = "删除当前对话成功。不再处于对话状态,使用 /switch 序号 切换到其他对话或 /new 创建。"
|
||||
message.set_extra("_clean_ltm_session", True)
|
||||
message.set_result(MessageEventResult().message(ret))
|
||||
|
||||
@@ -8,7 +8,7 @@ from astrbot.core.utils.io import get_dashboard_version
|
||||
|
||||
|
||||
class HelpCommand:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def _query_astrbot_notice(self):
|
||||
@@ -24,7 +24,7 @@ class HelpCommand:
|
||||
|
||||
async def _build_reserved_command_lines(self) -> list[str]:
|
||||
"""
|
||||
使用实时指令配置生成内置指令清单,确保重命名/禁用后与实际生效状态保持一致。
|
||||
使用实时指令配置生成内置指令清单,确保重命名/禁用后与实际生效状态保持一致。
|
||||
"""
|
||||
try:
|
||||
commands = await command_management.list_commands()
|
||||
@@ -34,7 +34,7 @@ class HelpCommand:
|
||||
lines: list[str] = []
|
||||
hidden_commands = {"set", "unset", "websearch"}
|
||||
|
||||
def walk(items: list[dict], indent: int = 0) -> None:
|
||||
def walk(items: list[dict], indent: int = 0):
|
||||
for item in items:
|
||||
if not item.get("reserved") or not item.get("enabled"):
|
||||
continue
|
||||
@@ -62,7 +62,7 @@ class HelpCommand:
|
||||
walk(commands)
|
||||
return lines
|
||||
|
||||
async def help(self, event: AstrMessageEvent) -> None:
|
||||
async def help(self, event: AstrMessageEvent):
|
||||
"""查看帮助"""
|
||||
notice = ""
|
||||
try:
|
||||
|
||||
@@ -3,10 +3,10 @@ from astrbot.api.event import AstrMessageEvent, MessageChain
|
||||
|
||||
|
||||
class LLMCommands:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def llm(self, event: AstrMessageEvent) -> None:
|
||||
async def llm(self, event: AstrMessageEvent):
|
||||
"""开启/关闭 LLM"""
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
enable = cfg["provider_settings"].get("enable", True)
|
||||
@@ -17,4 +17,4 @@ class LLMCommands:
|
||||
cfg["provider_settings"]["enable"] = True
|
||||
status = "开启"
|
||||
cfg.save_config()
|
||||
await event.send(MessageChain().message(f"{status} LLM 聊天功能。"))
|
||||
await event.send(MessageChain().message(f"{status} LLM 聊天功能。"))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import builtins
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from astrbot.api import star
|
||||
from astrbot.api import sp, star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class PersonaCommands:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
def _build_tree_output(
|
||||
@@ -18,10 +18,10 @@ class PersonaCommands:
|
||||
all_personas: list["Persona"],
|
||||
depth: int = 0,
|
||||
) -> list[str]:
|
||||
"""递归构建树状输出,使用短线条表示层级"""
|
||||
"""递归构建树状输出,使用短线条表示层级"""
|
||||
lines: list[str] = []
|
||||
# 使用短线条作为缩进前缀,每层只用 "│" 加一个空格
|
||||
prefix = "│ " * depth
|
||||
# 使用短线条作为缩进前缀,每层只用 "│" 加一个空格
|
||||
prefix = "│ " * depth
|
||||
|
||||
for folder in folder_tree:
|
||||
# 输出文件夹
|
||||
@@ -31,7 +31,7 @@ class PersonaCommands:
|
||||
folder_personas = [
|
||||
p for p in all_personas if p.folder_id == folder["folder_id"]
|
||||
]
|
||||
child_prefix = "│ " * (depth + 1)
|
||||
child_prefix = "│ " * (depth + 1)
|
||||
|
||||
# 输出该文件夹下的人格
|
||||
for persona in folder_personas:
|
||||
@@ -50,8 +50,8 @@ class PersonaCommands:
|
||||
|
||||
return lines
|
||||
|
||||
async def persona(self, message: AstrMessageEvent) -> None:
|
||||
parts = message.message_str.split(" ")
|
||||
async def persona(self, message: AstrMessageEvent):
|
||||
l = message.message_str.split(" ") # noqa: E741
|
||||
umo = message.unified_msg_origin
|
||||
|
||||
curr_persona_name = "无"
|
||||
@@ -59,7 +59,12 @@ class PersonaCommands:
|
||||
default_persona = await self.context.persona_manager.get_default_persona_v3(
|
||||
umo=umo,
|
||||
)
|
||||
force_applied_persona_id = None
|
||||
|
||||
force_applied_persona_id = (
|
||||
await sp.get_async(
|
||||
scope="umo", scope_id=umo, key="session_service_config", default={}
|
||||
)
|
||||
).get("persona_id")
|
||||
|
||||
curr_cid_title = "无"
|
||||
if cid:
|
||||
@@ -71,31 +76,14 @@ class PersonaCommands:
|
||||
if conv is None:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"当前对话不存在,请先使用 /new 新建一个对话。",
|
||||
"当前对话不存在,请先使用 /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 not conv.persona_id and conv.persona_id != "[%None]":
|
||||
curr_persona_name = default_persona["name"]
|
||||
else:
|
||||
curr_persona_name = conv.persona_id
|
||||
|
||||
if force_applied_persona_id:
|
||||
curr_persona_name = f"{curr_persona_name} (自定义规则)"
|
||||
@@ -103,7 +91,7 @@ class PersonaCommands:
|
||||
curr_cid_title = conv.title if conv.title else "新对话"
|
||||
curr_cid_title += f"({cid[:4]})"
|
||||
|
||||
if len(parts) == 1:
|
||||
if len(l) == 1:
|
||||
message.set_result(
|
||||
MessageEventResult()
|
||||
.message(
|
||||
@@ -122,21 +110,21 @@ class PersonaCommands:
|
||||
)
|
||||
.use_t2i(False),
|
||||
)
|
||||
elif parts[1] == "list":
|
||||
elif l[1] == "list":
|
||||
# 获取文件夹树和所有人格
|
||||
folder_tree = await self.context.persona_manager.get_folder_tree()
|
||||
all_personas = self.context.persona_manager.personas
|
||||
|
||||
lines = ["📂 人格列表:\n"]
|
||||
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: # 如果有文件夹内容,加个空行
|
||||
if tree_lines: # 如果有文件夹内容,加个空行
|
||||
lines.append("")
|
||||
for persona in root_personas:
|
||||
lines.append(f"👤 {persona.persona_id}")
|
||||
@@ -149,11 +137,11 @@ class PersonaCommands:
|
||||
|
||||
msg = "\n".join(lines)
|
||||
message.set_result(MessageEventResult().message(msg).use_t2i(False))
|
||||
elif parts[1] == "view":
|
||||
if len(parts) == 2:
|
||||
elif l[1] == "view":
|
||||
if len(l) == 2:
|
||||
message.set_result(MessageEventResult().message("请输入人格情景名"))
|
||||
return
|
||||
ps = parts[2].strip()
|
||||
ps = l[2].strip()
|
||||
if persona := next(
|
||||
builtins.filter(
|
||||
lambda persona: persona["name"] == ps,
|
||||
@@ -161,28 +149,28 @@ class PersonaCommands:
|
||||
),
|
||||
None,
|
||||
):
|
||||
msg = f"人格{ps}的详细信息:\n"
|
||||
msg = f"人格{ps}的详细信息:\n"
|
||||
msg += f"{persona['prompt']}\n"
|
||||
else:
|
||||
msg = f"人格{ps}不存在"
|
||||
message.set_result(MessageEventResult().message(msg))
|
||||
elif parts[1] == "unset":
|
||||
elif l[1] == "unset":
|
||||
if not cid:
|
||||
message.set_result(
|
||||
MessageEventResult().message("当前没有对话,无法取消人格。"),
|
||||
MessageEventResult().message("当前没有对话,无法取消人格。"),
|
||||
)
|
||||
return
|
||||
await self.context.conversation_manager.update_conversation_persona_id(
|
||||
message.unified_msg_origin,
|
||||
"[%None]",
|
||||
)
|
||||
message.set_result(MessageEventResult().message("取消人格成功。"))
|
||||
message.set_result(MessageEventResult().message("取消人格成功。"))
|
||||
else:
|
||||
ps = "".join(parts[1:]).strip()
|
||||
ps = "".join(l[1:]).strip()
|
||||
if not cid:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"当前没有对话,请先开始对话或使用 /new 创建一个对话。",
|
||||
"当前没有对话,请先开始对话或使用 /new 创建一个对话。",
|
||||
),
|
||||
)
|
||||
return
|
||||
@@ -199,16 +187,18 @@ class PersonaCommands:
|
||||
)
|
||||
force_warn_msg = ""
|
||||
if force_applied_persona_id:
|
||||
force_warn_msg = "提醒:由于自定义规则,您现在切换的人格将不会生效。"
|
||||
force_warn_msg = (
|
||||
"提醒:由于自定义规则,您现在切换的人格将不会生效。"
|
||||
)
|
||||
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。{force_warn_msg}",
|
||||
f"设置成功。如果您正在切换到不同的人格,请注意使用 /reset 来清空上下文,防止原人格对话影响现人格。{force_warn_msg}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
"不存在该人格情景。使用 /persona list 查看所有。",
|
||||
"不存在该人格情景。使用 /persona list 查看所有。",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -4,15 +4,16 @@ from astrbot.core import DEMO_MODE, logger
|
||||
from astrbot.core.star.filter.command import CommandFilter
|
||||
from astrbot.core.star.filter.command_group import CommandGroupFilter
|
||||
from astrbot.core.star.star_handler import StarHandlerMetadata, star_handlers_registry
|
||||
from astrbot.core.star.star_manager import PluginManager
|
||||
|
||||
|
||||
class PluginCommands:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def plugin_ls(self, event: AstrMessageEvent) -> None:
|
||||
"""获取已经安装的插件列表。"""
|
||||
parts = ["已加载的插件:\n"]
|
||||
async def plugin_ls(self, event: AstrMessageEvent):
|
||||
"""获取已经安装的插件列表。"""
|
||||
parts = ["已加载的插件:\n"]
|
||||
for plugin in self.context.get_all_stars():
|
||||
line = f"- `{plugin.name}` By {plugin.author}: {plugin.desc}"
|
||||
if not plugin.activated:
|
||||
@@ -20,78 +21,72 @@ class PluginCommands:
|
||||
parts.append(line + "\n")
|
||||
|
||||
if len(parts) == 1:
|
||||
plugin_list_info = "没有加载任何插件。"
|
||||
plugin_list_info = "没有加载任何插件。"
|
||||
else:
|
||||
plugin_list_info = "".join(parts)
|
||||
|
||||
plugin_list_info += "\n使用 /plugin help <插件名> 查看插件帮助和加载的指令。\n使用 /plugin on/off <插件名> 启用或者禁用插件。"
|
||||
plugin_list_info += "\n使用 /plugin help <插件名> 查看插件帮助和加载的指令。\n使用 /plugin on/off <插件名> 启用或者禁用插件。"
|
||||
event.set_result(
|
||||
MessageEventResult().message(f"{plugin_list_info}").use_t2i(False),
|
||||
)
|
||||
|
||||
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = ""):
|
||||
"""禁用插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(MessageEventResult().message("演示模式下无法禁用插件。"))
|
||||
event.set_result(MessageEventResult().message("演示模式下无法禁用插件。"))
|
||||
return
|
||||
if not plugin_name:
|
||||
event.set_result(
|
||||
MessageEventResult().message("/plugin off <插件名> 禁用插件。"),
|
||||
MessageEventResult().message("/plugin off <插件名> 禁用插件。"),
|
||||
)
|
||||
return
|
||||
if self.context._star_manager is None:
|
||||
event.set_result(MessageEventResult().message("插件管理器未初始化。"))
|
||||
return
|
||||
await self.context._star_manager.turn_off_plugin(plugin_name)
|
||||
event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已禁用。"))
|
||||
await self.context._star_manager.turn_off_plugin(plugin_name) # type: ignore
|
||||
event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已禁用。"))
|
||||
|
||||
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = ""):
|
||||
"""启用插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(MessageEventResult().message("演示模式下无法启用插件。"))
|
||||
event.set_result(MessageEventResult().message("演示模式下无法启用插件。"))
|
||||
return
|
||||
if not plugin_name:
|
||||
event.set_result(
|
||||
MessageEventResult().message("/plugin on <插件名> 启用插件。"),
|
||||
MessageEventResult().message("/plugin on <插件名> 启用插件。"),
|
||||
)
|
||||
return
|
||||
if self.context._star_manager is None:
|
||||
event.set_result(MessageEventResult().message("插件管理器未初始化。"))
|
||||
return
|
||||
await self.context._star_manager.turn_on_plugin(plugin_name)
|
||||
event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已启用。"))
|
||||
await self.context._star_manager.turn_on_plugin(plugin_name) # type: ignore
|
||||
event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已启用。"))
|
||||
|
||||
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = "") -> None:
|
||||
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = ""):
|
||||
"""安装插件"""
|
||||
if DEMO_MODE:
|
||||
event.set_result(MessageEventResult().message("演示模式下无法安装插件。"))
|
||||
event.set_result(MessageEventResult().message("演示模式下无法安装插件。"))
|
||||
return
|
||||
if not plugin_repo:
|
||||
event.set_result(
|
||||
MessageEventResult().message("/plugin get <插件仓库地址> 安装插件"),
|
||||
)
|
||||
return
|
||||
logger.info(f"准备从 {plugin_repo} 安装插件。")
|
||||
logger.info(f"准备从 {plugin_repo} 安装插件。")
|
||||
if self.context._star_manager:
|
||||
star_mgr = self.context._star_manager
|
||||
star_mgr: PluginManager = self.context._star_manager
|
||||
try:
|
||||
await star_mgr.install_plugin(plugin_repo)
|
||||
event.set_result(MessageEventResult().message("安装插件成功。"))
|
||||
await star_mgr.install_plugin(plugin_repo) # type: ignore
|
||||
event.set_result(MessageEventResult().message("安装插件成功。"))
|
||||
except Exception as e:
|
||||
logger.error(f"安装插件失败: {e}")
|
||||
event.set_result(MessageEventResult().message(f"安装插件失败: {e}"))
|
||||
return
|
||||
|
||||
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = ""):
|
||||
"""获取插件帮助"""
|
||||
if not plugin_name:
|
||||
event.set_result(
|
||||
MessageEventResult().message("/plugin help <插件名> 查看插件信息。"),
|
||||
MessageEventResult().message("/plugin help <插件名> 查看插件信息。"),
|
||||
)
|
||||
return
|
||||
plugin = self.context.get_registered_star(plugin_name)
|
||||
if plugin is None:
|
||||
event.set_result(MessageEventResult().message("未找到此插件。"))
|
||||
event.set_result(MessageEventResult().message("未找到此插件。"))
|
||||
return
|
||||
help_msg = ""
|
||||
help_msg += f"\n\n✨ 作者: {plugin.author}\n✨ 版本: {plugin.version}"
|
||||
@@ -111,15 +106,15 @@ class PluginCommands:
|
||||
command_names.append(filter_.group_name)
|
||||
|
||||
if len(command_handlers) > 0:
|
||||
parts = ["\n\n🔧 指令列表:\n"]
|
||||
parts = ["\n\n🔧 指令列表:\n"]
|
||||
for i in range(len(command_handlers)):
|
||||
line = f"- {command_names[i]}"
|
||||
if command_handlers[i].desc:
|
||||
line += f": {command_handlers[i].desc}"
|
||||
parts.append(line + "\n")
|
||||
parts.append("\nTip: 指令的触发需要添加唤醒前缀,默认为 /。")
|
||||
parts.append("\nTip: 指令的触发需要添加唤醒前缀,默认为 /。")
|
||||
help_msg += "".join(parts)
|
||||
|
||||
ret = f"🧩 插件 {plugin_name} 帮助信息:\n" + help_msg
|
||||
ret += "更多帮助信息请查看插件仓库 README。"
|
||||
ret = f"🧩 插件 {plugin_name} 帮助信息:\n" + help_msg
|
||||
ret += "更多帮助信息请查看插件仓库 README。"
|
||||
event.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||
|
||||
@@ -1,262 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
import re
|
||||
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from astrbot.core.provider.provider import Provider
|
||||
|
||||
|
||||
MODEL_LIST_CACHE_TTL_SECONDS_DEFAULT = 30.0
|
||||
MODEL_LOOKUP_MAX_CONCURRENCY_DEFAULT = 4
|
||||
MODEL_LOOKUP_MAX_CONCURRENCY_UPPER_BOUND = 16
|
||||
MODEL_LIST_CACHE_TTL_KEY = "model_list_cache_ttl_seconds"
|
||||
MODEL_LOOKUP_MAX_CONCURRENCY_KEY = "model_lookup_max_concurrency"
|
||||
MODEL_CACHE_MAX_ENTRIES = 512
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _ModelLookupConfig:
|
||||
umo: str | None
|
||||
cache_ttl_seconds: float
|
||||
max_concurrency: int
|
||||
|
||||
|
||||
class _ModelCache:
|
||||
def __init__(self) -> None:
|
||||
self._store: dict[tuple[str, str | None], tuple[float, list[str]]] = {}
|
||||
|
||||
def get(self, provider_id: str, umo: str | None, ttl: float) -> list[str] | None:
|
||||
if ttl <= 0:
|
||||
return None
|
||||
entry = self._store.get((provider_id, umo))
|
||||
if not entry:
|
||||
return None
|
||||
timestamp, models = entry
|
||||
if time.monotonic() - timestamp > ttl:
|
||||
self._store.pop((provider_id, umo), None)
|
||||
return None
|
||||
return models
|
||||
|
||||
def set(
|
||||
self, provider_id: str, umo: str | None, models: list[str], ttl: float
|
||||
) -> None:
|
||||
if ttl <= 0:
|
||||
return
|
||||
self._store[(provider_id, umo)] = (time.monotonic(), list(models))
|
||||
self._evict_if_needed()
|
||||
|
||||
def _evict_if_needed(self) -> None:
|
||||
if len(self._store) <= MODEL_CACHE_MAX_ENTRIES:
|
||||
return
|
||||
# Drop oldest entries first when cache grows too large.
|
||||
overflow = len(self._store) - MODEL_CACHE_MAX_ENTRIES
|
||||
for key, _ in sorted(
|
||||
self._store.items(),
|
||||
key=lambda item: item[1][0],
|
||||
)[:overflow]:
|
||||
self._store.pop(key, None)
|
||||
|
||||
def invalidate(
|
||||
self, provider_id: str | None = None, *, umo: str | None = None
|
||||
) -> None:
|
||||
if provider_id is None:
|
||||
self._store.clear()
|
||||
return
|
||||
if umo is not None:
|
||||
self._store.pop((provider_id, umo), None)
|
||||
return
|
||||
stale_keys = [
|
||||
cache_key for cache_key in self._store if cache_key[0] == provider_id
|
||||
]
|
||||
for cache_key in stale_keys:
|
||||
self._store.pop(cache_key, None)
|
||||
|
||||
|
||||
class ProviderCommands:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
self._model_cache = _ModelCache()
|
||||
self._register_provider_change_hook()
|
||||
|
||||
def _register_provider_change_hook(self) -> None:
|
||||
set_change_callback = getattr(
|
||||
self.context.provider_manager,
|
||||
"set_provider_change_callback",
|
||||
None,
|
||||
)
|
||||
if callable(set_change_callback):
|
||||
set_change_callback(self._on_provider_manager_changed)
|
||||
return
|
||||
register_change_hook = getattr(
|
||||
self.context.provider_manager,
|
||||
"register_provider_change_hook",
|
||||
None,
|
||||
)
|
||||
if callable(register_change_hook):
|
||||
register_change_hook(self._on_provider_manager_changed)
|
||||
|
||||
def invalidate_provider_models_cache(
|
||||
self, provider_id: str | None = None, *, umo: str | None = None
|
||||
) -> None:
|
||||
"""Public hook for cache invalidation on external provider config changes."""
|
||||
self._model_cache.invalidate(provider_id, umo=umo)
|
||||
|
||||
def _on_provider_manager_changed(
|
||||
self,
|
||||
provider_id: str,
|
||||
provider_type: ProviderType,
|
||||
umo: str | None,
|
||||
) -> None:
|
||||
if provider_type == ProviderType.CHAT_COMPLETION:
|
||||
self.invalidate_provider_models_cache(provider_id, umo=umo)
|
||||
|
||||
def _get_provider_settings(self, umo: str | None) -> dict:
|
||||
if not umo:
|
||||
return {}
|
||||
try:
|
||||
return self.context.get_config(umo).get("provider_settings", {}) or {}
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
"读取 provider_settings 失败,使用默认值: %s",
|
||||
safe_error("", e),
|
||||
)
|
||||
return {}
|
||||
|
||||
def _get_model_cache_ttl(self, umo: str | None) -> float:
|
||||
settings = self._get_provider_settings(umo)
|
||||
raw = settings.get(
|
||||
MODEL_LIST_CACHE_TTL_KEY,
|
||||
MODEL_LIST_CACHE_TTL_SECONDS_DEFAULT,
|
||||
)
|
||||
try:
|
||||
return max(float(raw), 0.0)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
"读取 %s 失败,回退默认值 %r: %s",
|
||||
MODEL_LIST_CACHE_TTL_KEY,
|
||||
MODEL_LIST_CACHE_TTL_SECONDS_DEFAULT,
|
||||
safe_error("", e),
|
||||
)
|
||||
return MODEL_LIST_CACHE_TTL_SECONDS_DEFAULT
|
||||
|
||||
def _get_model_lookup_concurrency(self, umo: str | None) -> int:
|
||||
settings = self._get_provider_settings(umo)
|
||||
raw = settings.get(
|
||||
MODEL_LOOKUP_MAX_CONCURRENCY_KEY,
|
||||
MODEL_LOOKUP_MAX_CONCURRENCY_DEFAULT,
|
||||
)
|
||||
try:
|
||||
value = int(raw)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
"读取 %s 失败,回退默认值 %r: %s",
|
||||
MODEL_LOOKUP_MAX_CONCURRENCY_KEY,
|
||||
MODEL_LOOKUP_MAX_CONCURRENCY_DEFAULT,
|
||||
safe_error("", e),
|
||||
)
|
||||
value = MODEL_LOOKUP_MAX_CONCURRENCY_DEFAULT
|
||||
return min(max(value, 1), MODEL_LOOKUP_MAX_CONCURRENCY_UPPER_BOUND)
|
||||
|
||||
def _get_model_lookup_config(self, umo: str | None) -> _ModelLookupConfig:
|
||||
return _ModelLookupConfig(
|
||||
umo=umo,
|
||||
cache_ttl_seconds=self._get_model_cache_ttl(umo),
|
||||
max_concurrency=self._get_model_lookup_concurrency(umo),
|
||||
)
|
||||
|
||||
def _resolve_model_name(
|
||||
self,
|
||||
model_name: str,
|
||||
models: Sequence[str],
|
||||
) -> str | None:
|
||||
"""Resolve model name with precedence:
|
||||
exact > case-insensitive > provider-qualified suffix.
|
||||
"""
|
||||
requested = model_name.strip()
|
||||
if not requested:
|
||||
return None
|
||||
|
||||
requested_norm = requested.casefold()
|
||||
|
||||
# exact / case-insensitive match
|
||||
for candidate in models:
|
||||
if candidate == requested or candidate.casefold() == requested_norm:
|
||||
return candidate
|
||||
|
||||
# provider-qualified suffix match:
|
||||
# e.g. candidate `openai/gpt-4o` should match requested `gpt-4o`.
|
||||
for candidate in models:
|
||||
cand_norm = candidate.casefold()
|
||||
if cand_norm.endswith(f"/{requested_norm}") or cand_norm.endswith(
|
||||
f":{requested_norm}"
|
||||
):
|
||||
return candidate
|
||||
|
||||
return None
|
||||
|
||||
def _apply_model(
|
||||
self, prov: Provider, model_name: str, *, umo: str | None = None
|
||||
) -> str:
|
||||
prov.set_model(model_name)
|
||||
self.invalidate_provider_models_cache(prov.meta().id, umo=umo)
|
||||
return f"切换模型成功。当前提供商: [{prov.meta().id}] 当前模型: [{prov.get_model()}]"
|
||||
|
||||
async def _get_provider_models(
|
||||
self,
|
||||
provider: Provider,
|
||||
*,
|
||||
config: _ModelLookupConfig,
|
||||
use_cache: bool = True,
|
||||
) -> list[str]:
|
||||
provider_id = provider.meta().id
|
||||
ttl_seconds = config.cache_ttl_seconds
|
||||
umo = config.umo
|
||||
if use_cache:
|
||||
cached = self._model_cache.get(provider_id, umo, ttl_seconds)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
models = list(await provider.get_models())
|
||||
if use_cache:
|
||||
self._model_cache.set(provider_id, umo, models, ttl_seconds)
|
||||
return models
|
||||
|
||||
async def _get_models_or_reply_error(
|
||||
self,
|
||||
message: AstrMessageEvent,
|
||||
prov: Provider,
|
||||
config: _ModelLookupConfig,
|
||||
*,
|
||||
error_prefix: str,
|
||||
disable_t2i: bool = False,
|
||||
warning_log: str | None = None,
|
||||
) -> list[str] | None:
|
||||
try:
|
||||
return await self._get_provider_models(prov, config=config)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
if warning_log is not None:
|
||||
logger.warning(
|
||||
warning_log,
|
||||
prov.meta().id,
|
||||
safe_error("", e),
|
||||
)
|
||||
result = MessageEventResult().message(safe_error(error_prefix, e))
|
||||
if disable_t2i:
|
||||
result = result.use_t2i(False)
|
||||
message.set_result(result)
|
||||
return None
|
||||
|
||||
def _log_reachability_failure(
|
||||
self,
|
||||
@@ -264,8 +17,8 @@ class ProviderCommands:
|
||||
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",
|
||||
@@ -285,102 +38,18 @@ class ProviderCommands:
|
||||
return True, None, None
|
||||
except Exception as e:
|
||||
err_code = "TEST_FAILED"
|
||||
err_reason = safe_error("", e)
|
||||
err_reason = str(e)
|
||||
self._log_reachability_failure(
|
||||
provider, provider_capability_type, err_code, err_reason
|
||||
)
|
||||
return False, err_code, err_reason
|
||||
|
||||
async def _find_provider_for_model(
|
||||
self,
|
||||
model_name: str,
|
||||
*,
|
||||
exclude_provider_id: str | None = None,
|
||||
config: _ModelLookupConfig,
|
||||
use_cache: bool = True,
|
||||
) -> tuple[Provider | None, str | None]:
|
||||
all_providers = []
|
||||
for provider in self.context.get_all_providers():
|
||||
provider_meta = provider.meta()
|
||||
if provider_meta.provider_type != ProviderType.CHAT_COMPLETION:
|
||||
continue
|
||||
if (
|
||||
exclude_provider_id is not None
|
||||
and provider_meta.id == exclude_provider_id
|
||||
):
|
||||
continue
|
||||
all_providers.append(provider)
|
||||
if not all_providers:
|
||||
return None, None
|
||||
|
||||
semaphore = asyncio.Semaphore(config.max_concurrency)
|
||||
|
||||
async def fetch_models(
|
||||
provider: Provider,
|
||||
) -> tuple[Provider, list[str] | None, str | None]:
|
||||
async with semaphore:
|
||||
try:
|
||||
models = await self._get_provider_models(
|
||||
provider,
|
||||
config=config,
|
||||
use_cache=use_cache,
|
||||
)
|
||||
return provider, models, None
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
err = safe_error("", e)
|
||||
logger.debug(
|
||||
"跨提供商查找模型 %s 获取 %s 模型列表失败: %s",
|
||||
model_name,
|
||||
provider.meta().id,
|
||||
err,
|
||||
)
|
||||
return provider, None, err
|
||||
|
||||
results = await asyncio.gather(
|
||||
*(fetch_models(provider) for provider in all_providers)
|
||||
)
|
||||
failed_provider_errors: list[tuple[str, str]] = []
|
||||
for provider, models, err in results:
|
||||
if err is not None:
|
||||
failed_provider_errors.append((provider.meta().id, err))
|
||||
continue
|
||||
if models is None:
|
||||
continue
|
||||
|
||||
matched_model_name = self._resolve_model_name(model_name, models)
|
||||
if matched_model_name is not None:
|
||||
return provider, matched_model_name
|
||||
|
||||
if failed_provider_errors and len(failed_provider_errors) == len(all_providers):
|
||||
failed_ids = ",".join(
|
||||
provider_id for provider_id, _ in failed_provider_errors
|
||||
)
|
||||
logger.error(
|
||||
"跨提供商查找模型 %s 时,所有 %d 个提供商的 get_models() 均失败: %s。请检查配置或网络",
|
||||
model_name,
|
||||
len(all_providers),
|
||||
failed_ids,
|
||||
)
|
||||
elif failed_provider_errors:
|
||||
logger.debug(
|
||||
"跨提供商查找模型 %s 时有 %d 个提供商获取模型失败: %s",
|
||||
model_name,
|
||||
len(failed_provider_errors),
|
||||
",".join(
|
||||
f"{provider_id}({error})"
|
||||
for provider_id, error in failed_provider_errors
|
||||
),
|
||||
)
|
||||
return None, None
|
||||
|
||||
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", {})
|
||||
@@ -405,7 +74,7 @@ class ProviderCommands:
|
||||
if all_providers:
|
||||
await event.send(
|
||||
MessageEventResult().message(
|
||||
"正在进行提供商可达性测试,请稍候..."
|
||||
"正在进行提供商可达性测试,请稍候..."
|
||||
)
|
||||
)
|
||||
check_results = await asyncio.gather(
|
||||
@@ -423,15 +92,13 @@ class ProviderCommands:
|
||||
id_ = meta.id
|
||||
error_code = None
|
||||
|
||||
if isinstance(reachable, asyncio.CancelledError):
|
||||
raise reachable
|
||||
if isinstance(reachable, Exception):
|
||||
# 异常情况下兜底处理,避免单个 provider 导致列表失败
|
||||
# 异常情况下兜底处理,避免单个 provider 导致列表失败
|
||||
self._log_reachability_failure(
|
||||
p,
|
||||
None,
|
||||
reachable.__class__.__name__,
|
||||
safe_error("", reachable),
|
||||
str(reachable),
|
||||
)
|
||||
reachable_flag = False
|
||||
error_code = reachable.__class__.__name__
|
||||
@@ -501,23 +168,23 @@ class ProviderCommands:
|
||||
line += " (当前使用)"
|
||||
parts.append(line + "\n")
|
||||
|
||||
parts.append("\n使用 /provider <序号> 切换 LLM 提供商。")
|
||||
parts.append("\n使用 /provider <序号> 切换 LLM 提供商。")
|
||||
ret = "".join(parts)
|
||||
|
||||
if ttss:
|
||||
ret += "\n使用 /provider tts <序号> 切换 TTS 提供商。"
|
||||
ret += "\n使用 /provider tts <序号> 切换 TTS 提供商。"
|
||||
if stts:
|
||||
ret += "\n使用 /provider stt <序号> 切换 STT 提供商。"
|
||||
ret += "\n使用 /provider stt <序号> 切换 STT 提供商。"
|
||||
if not reachability_check_enabled:
|
||||
ret += "\n已跳过提供商可达性检测,如需检测请在配置文件中开启。"
|
||||
ret += "\n已跳过提供商可达性检测,如需检测请在配置文件中开启。"
|
||||
|
||||
event.set_result(MessageEventResult().message(ret))
|
||||
elif idx == "tts":
|
||||
if idx2 is None:
|
||||
event.set_result(MessageEventResult().message("请输入序号。"))
|
||||
event.set_result(MessageEventResult().message("请输入序号。"))
|
||||
return
|
||||
if idx2 > len(self.context.get_all_tts_providers()) or idx2 < 1:
|
||||
event.set_result(MessageEventResult().message("无效的提供商序号。"))
|
||||
event.set_result(MessageEventResult().message("无效的提供商序号。"))
|
||||
return
|
||||
provider = self.context.get_all_tts_providers()[idx2 - 1]
|
||||
id_ = provider.meta().id
|
||||
@@ -526,13 +193,13 @@ class ProviderCommands:
|
||||
provider_type=ProviderType.TEXT_TO_SPEECH,
|
||||
umo=umo,
|
||||
)
|
||||
event.set_result(MessageEventResult().message(f"成功切换到 {id_}。"))
|
||||
event.set_result(MessageEventResult().message(f"成功切换到 {id_}。"))
|
||||
elif idx == "stt":
|
||||
if idx2 is None:
|
||||
event.set_result(MessageEventResult().message("请输入序号。"))
|
||||
event.set_result(MessageEventResult().message("请输入序号。"))
|
||||
return
|
||||
if idx2 > len(self.context.get_all_stt_providers()) or idx2 < 1:
|
||||
event.set_result(MessageEventResult().message("无效的提供商序号。"))
|
||||
event.set_result(MessageEventResult().message("无效的提供商序号。"))
|
||||
return
|
||||
provider = self.context.get_all_stt_providers()[idx2 - 1]
|
||||
id_ = provider.meta().id
|
||||
@@ -541,10 +208,10 @@ class ProviderCommands:
|
||||
provider_type=ProviderType.SPEECH_TO_TEXT,
|
||||
umo=umo,
|
||||
)
|
||||
event.set_result(MessageEventResult().message(f"成功切换到 {id_}。"))
|
||||
event.set_result(MessageEventResult().message(f"成功切换到 {id_}。"))
|
||||
elif isinstance(idx, int):
|
||||
if idx > len(self.context.get_all_providers()) or idx < 1:
|
||||
event.set_result(MessageEventResult().message("无效的提供商序号。"))
|
||||
event.set_result(MessageEventResult().message("无效的提供商序号。"))
|
||||
return
|
||||
provider = self.context.get_all_providers()[idx - 1]
|
||||
id_ = provider.meta().id
|
||||
@@ -553,100 +220,36 @@ class ProviderCommands:
|
||||
provider_type=ProviderType.CHAT_COMPLETION,
|
||||
umo=umo,
|
||||
)
|
||||
event.set_result(MessageEventResult().message(f"成功切换到 {id_}。"))
|
||||
event.set_result(MessageEventResult().message(f"成功切换到 {id_}。"))
|
||||
else:
|
||||
event.set_result(MessageEventResult().message("无效的参数。"))
|
||||
|
||||
async def _switch_model_by_name(
|
||||
self, message: AstrMessageEvent, model_name: str, prov: Provider
|
||||
) -> None:
|
||||
model_name = model_name.strip()
|
||||
if not model_name:
|
||||
message.set_result(MessageEventResult().message("模型名不能为空。"))
|
||||
return
|
||||
|
||||
umo = message.unified_msg_origin
|
||||
config = self._get_model_lookup_config(umo)
|
||||
curr_provider_id = prov.meta().id
|
||||
|
||||
models = await self._get_models_or_reply_error(
|
||||
message,
|
||||
prov,
|
||||
config,
|
||||
error_prefix="获取当前提供商模型列表失败: ",
|
||||
warning_log="获取当前提供商 %s 模型列表失败,停止跨提供商查找: %s",
|
||||
)
|
||||
if models is None:
|
||||
return
|
||||
|
||||
matched_model_name = self._resolve_model_name(model_name, models)
|
||||
if matched_model_name is not None:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
self._apply_model(prov, matched_model_name, umo=umo)
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
target_prov, matched_target_model_name = await self._find_provider_for_model(
|
||||
model_name,
|
||||
exclude_provider_id=curr_provider_id,
|
||||
config=config,
|
||||
)
|
||||
|
||||
if target_prov is None or matched_target_model_name is None:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"模型 [{model_name}] 未在任何已配置的提供商中找到,或所有提供商模型列表获取失败,请检查配置或网络后重试。",
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
target_id = target_prov.meta().id
|
||||
try:
|
||||
await self.context.provider_manager.set_provider(
|
||||
provider_id=target_id,
|
||||
provider_type=ProviderType.CHAT_COMPLETION,
|
||||
umo=umo,
|
||||
)
|
||||
self._apply_model(target_prov, matched_target_model_name, umo=umo)
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"检测到模型 [{matched_target_model_name}] 属于提供商 [{target_id}],已自动切换提供商并设置模型。",
|
||||
),
|
||||
)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
safe_error("跨提供商切换并设置模型失败: ", e)
|
||||
),
|
||||
)
|
||||
event.set_result(MessageEventResult().message("无效的参数。"))
|
||||
|
||||
async def model_ls(
|
||||
self,
|
||||
message: AstrMessageEvent,
|
||||
idx_or_name: int | str | None = None,
|
||||
) -> None:
|
||||
):
|
||||
"""查看或者切换模型"""
|
||||
prov = self.context.get_using_provider(message.unified_msg_origin)
|
||||
if not prov:
|
||||
message.set_result(
|
||||
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"),
|
||||
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"),
|
||||
)
|
||||
return
|
||||
config = self._get_model_lookup_config(message.unified_msg_origin)
|
||||
# 定义正则表达式匹配 API 密钥
|
||||
api_key_pattern = re.compile(r"key=[^&'\" ]+")
|
||||
|
||||
if idx_or_name is None:
|
||||
models = await self._get_models_or_reply_error(
|
||||
message,
|
||||
prov,
|
||||
config,
|
||||
error_prefix="获取模型列表失败: ",
|
||||
disable_t2i=True,
|
||||
)
|
||||
if models is None:
|
||||
models = []
|
||||
try:
|
||||
models = await prov.get_models()
|
||||
except BaseException as e:
|
||||
err_msg = api_key_pattern.sub("key=***", str(e))
|
||||
message.set_result(
|
||||
MessageEventResult()
|
||||
.message("获取模型列表失败: " + err_msg)
|
||||
.use_t2i(False),
|
||||
)
|
||||
return
|
||||
parts = ["下面列出了此模型提供商可用模型:"]
|
||||
for i, model in enumerate(models, 1):
|
||||
@@ -655,49 +258,46 @@ class ProviderCommands:
|
||||
curr_model = prov.get_model() or "无"
|
||||
parts.append(f"\n当前模型: [{curr_model}]")
|
||||
parts.append(
|
||||
"\nTips: 使用 /model <模型名/编号> 切换模型。输入模型名时可自动跨提供商查找并切换;跨提供商也可使用 /provider 切换。"
|
||||
"\nTips: 使用 /model <模型名/编号>,即可实时更换模型。如目标模型不存在于上表,请输入模型名。"
|
||||
)
|
||||
|
||||
ret = "".join(parts)
|
||||
message.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||
elif isinstance(idx_or_name, int):
|
||||
models = await self._get_models_or_reply_error(
|
||||
message,
|
||||
prov,
|
||||
config,
|
||||
error_prefix="获取模型列表失败: ",
|
||||
)
|
||||
if models is None:
|
||||
models = []
|
||||
try:
|
||||
models = await prov.get_models()
|
||||
except BaseException as e:
|
||||
message.set_result(
|
||||
MessageEventResult().message("获取模型列表失败: " + str(e)),
|
||||
)
|
||||
return
|
||||
if idx_or_name > len(models) or idx_or_name < 1:
|
||||
message.set_result(MessageEventResult().message("模型序号错误。"))
|
||||
message.set_result(MessageEventResult().message("模型序号错误。"))
|
||||
else:
|
||||
try:
|
||||
new_model = models[idx_or_name - 1]
|
||||
prov.set_model(new_model)
|
||||
except BaseException as e:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
self._apply_model(
|
||||
prov,
|
||||
new_model,
|
||||
umo=message.unified_msg_origin,
|
||||
)
|
||||
),
|
||||
MessageEventResult().message("切换模型未知错误: " + str(e)),
|
||||
)
|
||||
except Exception as e:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
safe_error("切换模型未知错误: ", e)
|
||||
),
|
||||
)
|
||||
return
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
f"切换模型成功。当前提供商: [{prov.meta().id}] 当前模型: [{prov.get_model()}]",
|
||||
),
|
||||
)
|
||||
else:
|
||||
await self._switch_model_by_name(message, idx_or_name, prov)
|
||||
prov.set_model(idx_or_name)
|
||||
message.set_result(
|
||||
MessageEventResult().message(f"切换模型到 {prov.get_model()}。"),
|
||||
)
|
||||
|
||||
async def key(self, message: AstrMessageEvent, index: int | None = None) -> None:
|
||||
async def key(self, message: AstrMessageEvent, index: int | None = None):
|
||||
prov = self.context.get_using_provider(message.unified_msg_origin)
|
||||
if not prov:
|
||||
message.set_result(
|
||||
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"),
|
||||
MessageEventResult().message("未找到任何 LLM 提供商。请先配置。"),
|
||||
)
|
||||
return
|
||||
|
||||
@@ -710,27 +310,20 @@ class ProviderCommands:
|
||||
|
||||
parts.append(f"\n当前 Key: {curr_key[:8]}")
|
||||
parts.append("\n当前模型: " + prov.get_model())
|
||||
parts.append("\n使用 /key <idx> 切换 Key。")
|
||||
parts.append("\n使用 /key <idx> 切换 Key。")
|
||||
|
||||
ret = "".join(parts)
|
||||
message.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||
else:
|
||||
keys_data = prov.get_keys()
|
||||
if index > len(keys_data) or index < 1:
|
||||
message.set_result(MessageEventResult().message("Key 序号错误。"))
|
||||
message.set_result(MessageEventResult().message("Key 序号错误。"))
|
||||
else:
|
||||
try:
|
||||
new_key = keys_data[index - 1]
|
||||
prov.set_key(new_key)
|
||||
self.invalidate_provider_models_cache(
|
||||
prov.meta().id,
|
||||
umo=message.unified_msg_origin,
|
||||
)
|
||||
message.set_result(MessageEventResult().message("切换 Key 成功。"))
|
||||
except Exception as e:
|
||||
except BaseException as e:
|
||||
message.set_result(
|
||||
MessageEventResult().message(
|
||||
safe_error("切换 Key 未知错误: ", e)
|
||||
),
|
||||
MessageEventResult().message(f"切换 Key 未知错误: {e!s}"),
|
||||
)
|
||||
return
|
||||
message.set_result(MessageEventResult().message("切换 Key 成功。"))
|
||||
|
||||
@@ -3,34 +3,34 @@ from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
|
||||
|
||||
class SetUnsetCommands:
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def set_variable(self, event: AstrMessageEvent, key: str, value: str) -> None:
|
||||
async def set_variable(self, event: AstrMessageEvent, key: str, value: str):
|
||||
"""设置会话变量"""
|
||||
uid = event.unified_msg_origin
|
||||
session_var = await sp.session_get(uid, "session_variables", {}) or {}
|
||||
session_var = await sp.session_get(uid, "session_variables", {})
|
||||
session_var[key] = value
|
||||
await sp.session_put(uid, "session_variables", session_var)
|
||||
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
f"会话 {uid} 变量 {key} 存储成功。使用 /unset 移除。",
|
||||
f"会话 {uid} 变量 {key} 存储成功。使用 /unset 移除。",
|
||||
),
|
||||
)
|
||||
|
||||
async def unset_variable(self, event: AstrMessageEvent, key: str) -> None:
|
||||
async def unset_variable(self, event: AstrMessageEvent, key: str):
|
||||
"""移除会话变量"""
|
||||
uid = event.unified_msg_origin
|
||||
session_var = await sp.session_get(uid, "session_variables", {}) or {}
|
||||
session_var = await sp.session_get(uid, "session_variables", {})
|
||||
|
||||
if key not in session_var:
|
||||
event.set_result(
|
||||
MessageEventResult().message("没有那个变量名。格式 /unset 变量名。"),
|
||||
MessageEventResult().message("没有那个变量名。格式 /unset 变量名。"),
|
||||
)
|
||||
else:
|
||||
del session_var[key]
|
||||
await sp.session_put(uid, "session_variables", session_var)
|
||||
event.set_result(
|
||||
MessageEventResult().message(f"会话 {uid} 变量 {key} 移除成功。"),
|
||||
MessageEventResult().message(f"会话 {uid} 变量 {key} 移除成功。"),
|
||||
)
|
||||
|
||||
@@ -7,10 +7,10 @@ from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
class SIDCommand:
|
||||
"""会话ID命令类"""
|
||||
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def sid(self, event: AstrMessageEvent) -> None:
|
||||
async def sid(self, event: AstrMessageEvent):
|
||||
"""获取消息来源信息"""
|
||||
sid = event.unified_msg_origin
|
||||
user_id = str(event.get_sender_id())
|
||||
@@ -18,19 +18,19 @@ class SIDCommand:
|
||||
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"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"消息来源可用于配置机器人的配置文件路由。"
|
||||
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 加入白名单来放行整个群聊。"
|
||||
ret += f"\n\n当前处于独立会话模式, 此群 ID: 「{event.get_group_id()}」, 也可将此 ID 加入白名单来放行整个群聊。"
|
||||
|
||||
event.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||
|
||||
@@ -7,17 +7,17 @@ from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
class T2ICommand:
|
||||
"""文本转图片命令类"""
|
||||
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def t2i(self, event: AstrMessageEvent) -> None:
|
||||
async def t2i(self, event: AstrMessageEvent):
|
||||
"""开关文本转图片"""
|
||||
config = self.context.get_config(umo=event.unified_msg_origin)
|
||||
if config["t2i"]:
|
||||
config["t2i"] = False
|
||||
config.save_config()
|
||||
event.set_result(MessageEventResult().message("已关闭文本转图片模式。"))
|
||||
event.set_result(MessageEventResult().message("已关闭文本转图片模式。"))
|
||||
return
|
||||
config["t2i"] = True
|
||||
config.save_config()
|
||||
event.set_result(MessageEventResult().message("已开启文本转图片模式。"))
|
||||
event.set_result(MessageEventResult().message("已开启文本转图片模式。"))
|
||||
|
||||
@@ -8,11 +8,11 @@ from astrbot.core.star.session_llm_manager import SessionServiceManager
|
||||
class TTSCommand:
|
||||
"""文本转语音命令类"""
|
||||
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
def __init__(self, context: star.Context):
|
||||
self.context = context
|
||||
|
||||
async def tts(self, event: AstrMessageEvent) -> None:
|
||||
"""开关文本转语音(会话级别)"""
|
||||
async def tts(self, event: AstrMessageEvent):
|
||||
"""开关文本转语音(会话级别)"""
|
||||
umo = event.unified_msg_origin
|
||||
ses_tts = await SessionServiceManager.is_tts_enabled_for_session(umo)
|
||||
cfg = self.context.get_config(umo=umo)
|
||||
@@ -27,10 +27,10 @@ class TTSCommand:
|
||||
if new_status and not tts_enable:
|
||||
event.set_result(
|
||||
MessageEventResult().message(
|
||||
f"{status_text}当前会话的文本转语音。但 TTS 功能在配置中未启用,请前往 WebUI 开启。",
|
||||
f"{status_text}当前会话的文本转语音。但 TTS 功能在配置中未启用,请前往 WebUI 开启。",
|
||||
),
|
||||
)
|
||||
else:
|
||||
event.set_result(
|
||||
MessageEventResult().message(f"{status_text}当前会话的文本转语音。"),
|
||||
MessageEventResult().message(f"{status_text}当前会话的文本转语音。"),
|
||||
)
|
||||
|
||||
@@ -35,85 +35,85 @@ class Main(star.Star):
|
||||
self.sid_c = SIDCommand(self.context)
|
||||
|
||||
@filter.command("help")
|
||||
async def help(self, event: AstrMessageEvent) -> None:
|
||||
async def help(self, event: AstrMessageEvent):
|
||||
"""查看帮助"""
|
||||
await self.help_c.help(event)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("llm")
|
||||
async def llm(self, event: AstrMessageEvent) -> None:
|
||||
async def llm(self, event: AstrMessageEvent):
|
||||
"""开启/关闭 LLM"""
|
||||
await self.llm_c.llm(event)
|
||||
|
||||
@filter.command_group("plugin")
|
||||
def plugin(self) -> None:
|
||||
def plugin(self):
|
||||
"""插件管理"""
|
||||
|
||||
@plugin.command("ls")
|
||||
async def plugin_ls(self, event: AstrMessageEvent) -> None:
|
||||
"""获取已经安装的插件列表。"""
|
||||
async def plugin_ls(self, event: AstrMessageEvent):
|
||||
"""获取已经安装的插件列表。"""
|
||||
await self.plugin_c.plugin_ls(event)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@plugin.command("off")
|
||||
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = ""):
|
||||
"""禁用插件"""
|
||||
await self.plugin_c.plugin_off(event, plugin_name)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@plugin.command("on")
|
||||
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = ""):
|
||||
"""启用插件"""
|
||||
await self.plugin_c.plugin_on(event, plugin_name)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@plugin.command("get")
|
||||
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = "") -> None:
|
||||
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = ""):
|
||||
"""安装插件"""
|
||||
await self.plugin_c.plugin_get(event, plugin_repo)
|
||||
|
||||
@plugin.command("help")
|
||||
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = ""):
|
||||
"""获取插件帮助"""
|
||||
await self.plugin_c.plugin_help(event, plugin_name)
|
||||
|
||||
@filter.command("t2i")
|
||||
async def t2i(self, event: AstrMessageEvent) -> None:
|
||||
async def t2i(self, event: AstrMessageEvent):
|
||||
"""开关文本转图片"""
|
||||
await self.t2i_c.t2i(event)
|
||||
|
||||
@filter.command("tts")
|
||||
async def tts(self, event: AstrMessageEvent) -> None:
|
||||
"""开关文本转语音(会话级别)"""
|
||||
async def tts(self, event: AstrMessageEvent):
|
||||
"""开关文本转语音(会话级别)"""
|
||||
await self.tts_c.tts(event)
|
||||
|
||||
@filter.command("sid")
|
||||
async def sid(self, event: AstrMessageEvent) -> None:
|
||||
async def sid(self, event: AstrMessageEvent):
|
||||
"""获取会话 ID 和 管理员 ID"""
|
||||
await self.sid_c.sid(event)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("op")
|
||||
async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None:
|
||||
"""授权管理员。op <admin_id>"""
|
||||
async def op(self, event: AstrMessageEvent, admin_id: str = ""):
|
||||
"""授权管理员。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>"""
|
||||
async def deop(self, event: AstrMessageEvent, admin_id: str):
|
||||
"""取消授权管理员。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>"""
|
||||
async def wl(self, event: AstrMessageEvent, sid: str = ""):
|
||||
"""添加白名单。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>"""
|
||||
async def dwl(self, event: AstrMessageEvent, sid: str):
|
||||
"""删除白名单。dwl <sid>"""
|
||||
await self.admin_c.dwl(event, sid)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@@ -123,96 +123,89 @@ class Main(star.Star):
|
||||
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:
|
||||
async def reset(self, message: AstrMessageEvent):
|
||||
"""重置 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:
|
||||
async def his(self, message: AstrMessageEvent, page: int = 1):
|
||||
"""查看对话记录"""
|
||||
await self.conversation_c.his(message, page)
|
||||
|
||||
@filter.command("ls")
|
||||
async def convs(self, message: AstrMessageEvent, page: int = 1) -> None:
|
||||
async def convs(self, message: AstrMessageEvent, page: int = 1):
|
||||
"""查看对话列表"""
|
||||
await self.conversation_c.convs(message, page)
|
||||
|
||||
@filter.command("new")
|
||||
async def new_conv(self, message: AstrMessageEvent) -> None:
|
||||
async def new_conv(self, message: AstrMessageEvent):
|
||||
"""创建新对话"""
|
||||
await self.conversation_c.new_conv(message)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("groupnew")
|
||||
async def groupnew_conv(self, message: AstrMessageEvent, sid: str) -> None:
|
||||
async def groupnew_conv(self, message: AstrMessageEvent, sid: str):
|
||||
"""创建新群聊对话"""
|
||||
await self.conversation_c.groupnew_conv(message, sid)
|
||||
|
||||
@filter.command("switch")
|
||||
async def switch_conv(
|
||||
self, message: AstrMessageEvent, index: int | None = None
|
||||
) -> None:
|
||||
async def switch_conv(self, message: AstrMessageEvent, index: int | None = None):
|
||||
"""通过 /ls 前面的序号切换对话"""
|
||||
await self.conversation_c.switch_conv(message, index)
|
||||
|
||||
@filter.command("rename")
|
||||
async def rename_conv(self, message: AstrMessageEvent, new_name: str) -> None:
|
||||
async def rename_conv(self, message: AstrMessageEvent, new_name: str):
|
||||
"""重命名对话"""
|
||||
await self.conversation_c.rename_conv(message, new_name)
|
||||
|
||||
@filter.command("del")
|
||||
async def del_conv(self, message: AstrMessageEvent) -> None:
|
||||
async def del_conv(self, message: AstrMessageEvent):
|
||||
"""删除当前对话"""
|
||||
await self.conversation_c.del_conv(message)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("key")
|
||||
async def key(self, message: AstrMessageEvent, index: int | None = None) -> None:
|
||||
async def key(self, message: AstrMessageEvent, index: int | None = None):
|
||||
"""查看或者切换 Key"""
|
||||
await self.provider_c.key(message, index)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("persona")
|
||||
async def persona(self, message: AstrMessageEvent) -> None:
|
||||
async def persona(self, message: AstrMessageEvent):
|
||||
"""查看或者切换 Persona"""
|
||||
await self.persona_c.persona(message)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("dashboard_update")
|
||||
async def update_dashboard(self, event: AstrMessageEvent) -> None:
|
||||
async def update_dashboard(self, event: AstrMessageEvent):
|
||||
"""更新管理面板"""
|
||||
await self.admin_c.update_dashboard(event)
|
||||
|
||||
@filter.command("set")
|
||||
async def set_variable(self, event: AstrMessageEvent, key: str, value: str) -> None:
|
||||
async def set_variable(self, event: AstrMessageEvent, key: str, value: str):
|
||||
await self.setunset_c.set_variable(event, key, value)
|
||||
|
||||
@filter.command("unset")
|
||||
async def unset_variable(self, event: AstrMessageEvent, key: str) -> None:
|
||||
async def unset_variable(self, event: AstrMessageEvent, key: str):
|
||||
await self.setunset_c.unset_variable(event, key)
|
||||
|
||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||
@filter.command("alter_cmd", alias={"alter"})
|
||||
async def alter_cmd(self, event: AstrMessageEvent) -> None:
|
||||
async def alter_cmd(self, event: AstrMessageEvent):
|
||||
"""修改命令权限"""
|
||||
await self.alter_cmd_c.alter_cmd(event)
|
||||
|
||||
266
astrbot/builtin_stars/reminder/main.py
Normal file
266
astrbot/builtin_stars/reminder/main.py
Normal file
@@ -0,0 +1,266 @@
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
import zoneinfo
|
||||
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from astrbot.api import llm_tool, logger, star
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult, filter
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
|
||||
class Main(star.Star):
|
||||
"""使用 LLM 待办提醒。只需对 LLM 说想要提醒的事情和时间即可。比如:`之后每天这个时候都提醒我做多邻国`"""
|
||||
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
self.context = context
|
||||
self.timezone = self.context.get_config().get("timezone")
|
||||
if not self.timezone:
|
||||
self.timezone = None
|
||||
try:
|
||||
self.timezone = zoneinfo.ZoneInfo(self.timezone) if self.timezone else None
|
||||
except Exception as e:
|
||||
logger.error(f"时区设置错误: {e}, 使用本地时区")
|
||||
self.timezone = None
|
||||
self.scheduler = AsyncIOScheduler(timezone=self.timezone)
|
||||
|
||||
# set and load config
|
||||
reminder_file = os.path.join(get_astrbot_data_path(), "astrbot-reminder.json")
|
||||
if not os.path.exists(reminder_file):
|
||||
with open(reminder_file, "w", encoding="utf-8") as f:
|
||||
f.write("{}")
|
||||
with open(reminder_file, encoding="utf-8") as f:
|
||||
self.reminder_data = json.load(f)
|
||||
|
||||
self._init_scheduler()
|
||||
self.scheduler.start()
|
||||
|
||||
def _init_scheduler(self):
|
||||
"""Initialize the scheduler."""
|
||||
for group in self.reminder_data:
|
||||
for reminder in self.reminder_data[group]:
|
||||
if "id" not in reminder:
|
||||
id_ = str(uuid.uuid4())
|
||||
reminder["id"] = id_
|
||||
else:
|
||||
id_ = reminder["id"]
|
||||
|
||||
if "datetime" in reminder:
|
||||
if self.check_is_outdated(reminder):
|
||||
continue
|
||||
self.scheduler.add_job(
|
||||
self._reminder_callback,
|
||||
id=id_,
|
||||
trigger="date",
|
||||
args=[group, reminder],
|
||||
run_date=datetime.datetime.strptime(
|
||||
reminder["datetime"],
|
||||
"%Y-%m-%d %H:%M",
|
||||
),
|
||||
misfire_grace_time=60,
|
||||
)
|
||||
elif "cron" in reminder:
|
||||
trigger = CronTrigger(**self._parse_cron_expr(reminder["cron"]))
|
||||
self.scheduler.add_job(
|
||||
self._reminder_callback,
|
||||
trigger=trigger,
|
||||
id=id_,
|
||||
args=[group, reminder],
|
||||
misfire_grace_time=60,
|
||||
)
|
||||
|
||||
def check_is_outdated(self, reminder: dict):
|
||||
"""Check if the reminder is outdated."""
|
||||
if "datetime" in reminder:
|
||||
reminder_time = datetime.datetime.strptime(
|
||||
reminder["datetime"],
|
||||
"%Y-%m-%d %H:%M",
|
||||
).replace(tzinfo=self.timezone)
|
||||
return reminder_time < datetime.datetime.now(self.timezone)
|
||||
return False
|
||||
|
||||
async def _save_data(self):
|
||||
"""Save the reminder data."""
|
||||
reminder_file = os.path.join(get_astrbot_data_path(), "astrbot-reminder.json")
|
||||
with open(reminder_file, "w", encoding="utf-8") as f:
|
||||
json.dump(self.reminder_data, f, ensure_ascii=False)
|
||||
|
||||
def _parse_cron_expr(self, cron_expr: str):
|
||||
fields = cron_expr.split(" ")
|
||||
return {
|
||||
"minute": fields[0],
|
||||
"hour": fields[1],
|
||||
"day": fields[2],
|
||||
"month": fields[3],
|
||||
"day_of_week": fields[4],
|
||||
}
|
||||
|
||||
@llm_tool("reminder")
|
||||
async def reminder_tool(
|
||||
self,
|
||||
event: AstrMessageEvent,
|
||||
text: str | None = None,
|
||||
datetime_str: str | None = None,
|
||||
cron_expression: str | None = None,
|
||||
human_readable_cron: str | None = None,
|
||||
):
|
||||
"""Call this function when user is asking for setting a reminder.
|
||||
|
||||
Args:
|
||||
text(string): Must Required. The content of the reminder.
|
||||
datetime_str(string): Required when user's reminder is a single reminder. The datetime string of the reminder, Must format with %Y-%m-%d %H:%M
|
||||
cron_expression(string): Required when user's reminder is a repeated reminder. The cron expression of the reminder. Monday is 0 and Sunday is 6.
|
||||
human_readable_cron(string): Optional. The human readable cron expression of the reminder.
|
||||
|
||||
"""
|
||||
if event.get_platform_name() == "qq_official":
|
||||
yield event.plain_result("reminder 暂不支持 QQ 官方机器人。")
|
||||
return
|
||||
|
||||
if event.unified_msg_origin not in self.reminder_data:
|
||||
self.reminder_data[event.unified_msg_origin] = []
|
||||
|
||||
if not cron_expression and not datetime_str:
|
||||
raise ValueError(
|
||||
"The cron_expression and datetime_str cannot be both None.",
|
||||
)
|
||||
reminder_time = ""
|
||||
|
||||
if not text:
|
||||
text = "未命名待办事项"
|
||||
|
||||
if cron_expression:
|
||||
d = {
|
||||
"text": text,
|
||||
"cron": cron_expression,
|
||||
"cron_h": human_readable_cron,
|
||||
"id": str(uuid.uuid4()),
|
||||
}
|
||||
self.reminder_data[event.unified_msg_origin].append(d)
|
||||
trigger = CronTrigger(**self._parse_cron_expr(cron_expression))
|
||||
self.scheduler.add_job(
|
||||
self._reminder_callback,
|
||||
trigger,
|
||||
id=d["id"],
|
||||
misfire_grace_time=60,
|
||||
args=[event.unified_msg_origin, d],
|
||||
)
|
||||
if human_readable_cron:
|
||||
reminder_time = f"{human_readable_cron}(Cron: {cron_expression})"
|
||||
else:
|
||||
if datetime_str is None:
|
||||
raise ValueError("datetime_str cannot be None.")
|
||||
d = {"text": text, "datetime": datetime_str, "id": str(uuid.uuid4())}
|
||||
self.reminder_data[event.unified_msg_origin].append(d)
|
||||
datetime_scheduled = datetime.datetime.strptime(
|
||||
datetime_str,
|
||||
"%Y-%m-%d %H:%M",
|
||||
)
|
||||
self.scheduler.add_job(
|
||||
self._reminder_callback,
|
||||
"date",
|
||||
id=d["id"],
|
||||
args=[event.unified_msg_origin, d],
|
||||
run_date=datetime_scheduled,
|
||||
misfire_grace_time=60,
|
||||
)
|
||||
reminder_time = datetime_str
|
||||
await self._save_data()
|
||||
yield event.plain_result(
|
||||
"成功设置待办事项。\n内容: "
|
||||
+ text
|
||||
+ "\n时间: "
|
||||
+ reminder_time
|
||||
+ "\n\n使用 /reminder ls 查看所有待办事项。\n使用 /tool off reminder 关闭此功能。",
|
||||
)
|
||||
|
||||
@filter.command_group("reminder")
|
||||
def reminder(self):
|
||||
"""待办提醒"""
|
||||
|
||||
async def get_upcoming_reminders(self, unified_msg_origin: str):
|
||||
"""Get upcoming reminders."""
|
||||
reminders = self.reminder_data.get(unified_msg_origin, [])
|
||||
if not reminders:
|
||||
return []
|
||||
now = datetime.datetime.now(self.timezone)
|
||||
upcoming_reminders = [
|
||||
reminder
|
||||
for reminder in reminders
|
||||
if "datetime" not in reminder
|
||||
or datetime.datetime.strptime(
|
||||
reminder["datetime"],
|
||||
"%Y-%m-%d %H:%M",
|
||||
).replace(tzinfo=self.timezone)
|
||||
>= now
|
||||
]
|
||||
return upcoming_reminders
|
||||
|
||||
@reminder.command("ls")
|
||||
async def reminder_ls(self, event: AstrMessageEvent):
|
||||
"""List upcoming reminders."""
|
||||
reminders = await self.get_upcoming_reminders(event.unified_msg_origin)
|
||||
if not reminders:
|
||||
yield event.plain_result("没有正在进行的待办事项。")
|
||||
else:
|
||||
parts = ["正在进行的待办事项:\n"]
|
||||
for i, reminder in enumerate(reminders):
|
||||
time_ = reminder.get("datetime", "")
|
||||
if not time_:
|
||||
cron_expr = reminder.get("cron", "")
|
||||
time_ = reminder.get("cron_h", "") + f"(Cron: {cron_expr})"
|
||||
parts.append(f"{i + 1}. {reminder['text']} - {time_}\n")
|
||||
parts.append("\n使用 /reminder rm <id> 删除待办事项。\n")
|
||||
reminder_str = "".join(parts)
|
||||
yield event.plain_result(reminder_str)
|
||||
|
||||
@reminder.command("rm")
|
||||
async def reminder_rm(self, event: AstrMessageEvent, index: int):
|
||||
"""Remove a reminder by index."""
|
||||
reminders = await self.get_upcoming_reminders(event.unified_msg_origin)
|
||||
|
||||
if not reminders:
|
||||
yield event.plain_result("没有待办事项。")
|
||||
elif index < 1 or index > len(reminders):
|
||||
yield event.plain_result("索引越界。")
|
||||
else:
|
||||
reminder = reminders.pop(index - 1)
|
||||
job_id = reminder.get("id")
|
||||
|
||||
# self.reminder_data[event.unified_msg_origin] = reminder
|
||||
users_reminders = self.reminder_data.get(event.unified_msg_origin, [])
|
||||
for i, r in enumerate(users_reminders):
|
||||
if r.get("id") == job_id:
|
||||
users_reminders.pop(i)
|
||||
|
||||
try:
|
||||
self.scheduler.remove_job(job_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Remove job error: {e}")
|
||||
yield event.plain_result(
|
||||
f"成功移除对应的待办事项。删除定时任务失败: {e!s} 可能需要重启 AstrBot 以取消该提醒任务。",
|
||||
)
|
||||
await self._save_data()
|
||||
yield event.plain_result("成功删除待办事项:\n" + reminder["text"])
|
||||
|
||||
async def _reminder_callback(self, unified_msg_origin: str, d: dict):
|
||||
"""The callback function of the reminder."""
|
||||
logger.info(f"Reminder Activated: {d['text']}, created by {unified_msg_origin}")
|
||||
await self.context.send_message(
|
||||
unified_msg_origin,
|
||||
MessageEventResult().message(
|
||||
"待办提醒: \n\n"
|
||||
+ d["text"]
|
||||
+ "\n时间: "
|
||||
+ d.get("datetime", "")
|
||||
+ d.get("cron_h", ""),
|
||||
),
|
||||
)
|
||||
|
||||
async def terminate(self):
|
||||
self.scheduler.shutdown()
|
||||
await self._save_data()
|
||||
logger.info("Reminder plugin terminated.")
|
||||
4
astrbot/builtin_stars/reminder/metadata.yaml
Normal file
4
astrbot/builtin_stars/reminder/metadata.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: astrbot-reminder
|
||||
desc: 使用 LLM 待办提醒
|
||||
author: Soulter
|
||||
version: 0.0.1
|
||||
@@ -17,11 +17,11 @@ from astrbot.core.utils.session_waiter import (
|
||||
class Main(Star):
|
||||
"""会话控制"""
|
||||
|
||||
def __init__(self, context: Context) -> None:
|
||||
def __init__(self, context: Context):
|
||||
super().__init__(context)
|
||||
|
||||
@filter.event_message_type(filter.EventMessageType.ALL, priority=maxsize)
|
||||
async def handle_session_control_agent(self, event: AstrMessageEvent) -> None:
|
||||
async def handle_session_control_agent(self, event: AstrMessageEvent):
|
||||
"""会话控制代理"""
|
||||
for session_filter in FILTERS:
|
||||
session_id = session_filter.filter(event)
|
||||
@@ -49,7 +49,7 @@ class Main(Star):
|
||||
if p_settings.get("empty_mention_waiting_need_reply", True):
|
||||
try:
|
||||
# 尝试使用 LLM 生成更生动的回复
|
||||
# func_tools_mgr = self.context.get_llm_tool_manager()
|
||||
func_tools_mgr = self.context.get_llm_tool_manager()
|
||||
|
||||
# 获取用户当前的对话信息
|
||||
curr_cid = await self.context.conversation_manager.get_curr_conversation_id(
|
||||
@@ -72,10 +72,11 @@ class Main(Star):
|
||||
# 使用 LLM 生成回复
|
||||
yield event.request_llm(
|
||||
prompt=(
|
||||
"注意,你正在社交媒体上中与用户进行聊天,用户只是通过@来唤醒你,但并未在这条消息中输入内容,他可能会在接下来一条发送他想发送的内容。"
|
||||
"你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。"
|
||||
"请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西"
|
||||
"注意,你正在社交媒体上中与用户进行聊天,用户只是通过@来唤醒你,但并未在这条消息中输入内容,他可能会在接下来一条发送他想发送的内容。"
|
||||
"你友好地询问用户想要聊些什么或者需要什么帮助,回复要符合人设,不要太过机械化。"
|
||||
"请注意,你仅需要输出要回复用户的内容,不要输出其他任何东西"
|
||||
),
|
||||
func_tool_manager=func_tools_mgr,
|
||||
session_id=curr_cid,
|
||||
contexts=[],
|
||||
system_prompt="",
|
||||
@@ -83,14 +84,14 @@ class Main(Star):
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"LLM response failed: {e!s}")
|
||||
# LLM 回复失败,使用原始预设回复
|
||||
yield event.plain_result("想要问什么呢?😄")
|
||||
# LLM 回复失败,使用原始预设回复
|
||||
yield event.plain_result("想要问什么呢?😄")
|
||||
|
||||
@session_waiter(60)
|
||||
async def empty_mention_waiter(
|
||||
controller: SessionController,
|
||||
event: AstrMessageEvent,
|
||||
) -> None:
|
||||
):
|
||||
event.message_obj.message.insert(
|
||||
0,
|
||||
Comp.At(qq=event.get_self_id(), name=event.get_self_id()),
|
||||
@@ -106,7 +107,7 @@ class Main(Star):
|
||||
except TimeoutError as _:
|
||||
pass
|
||||
except Exception as e:
|
||||
yield event.plain_result("发生错误,请联系管理员: " + str(e))
|
||||
yield event.plain_result("发生错误,请联系管理员: " + str(e))
|
||||
finally:
|
||||
event.stop_event()
|
||||
except Exception as e:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user