Compare commits
225 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a0656483b0 | |||
| f6ac6b9007 | |||
| b8c73430fb | |||
| 3141ed52bd | |||
| 63ff234f10 | |||
| 5219ba5c4e | |||
| 84994b5d98 | |||
| 1554f71106 | |||
| 476c01469f | |||
| 10163ec78a | |||
| 98b89ebcc5 | |||
| 39b9e55434 | |||
| 3eb15089af | |||
| c5b23d12a8 | |||
| 69f2fb291a | |||
| 78660da995 | |||
| c951b14aa2 | |||
| c384439b44 | |||
| 87d2750ff8 | |||
| 6d76d55452 | |||
| d80598b9c3 | |||
| c7d318304b | |||
| bcdbc15635 | |||
| 4749159bb9 | |||
| 5530a2260a | |||
| c24de24ca4 | |||
| b54b4c79ed | |||
| c6cc7aae84 | |||
| 84cd209074 | |||
| afda44fbe3 | |||
| f5d3b93437 | |||
| 069a3628fa | |||
| c81ef2672a | |||
| a5ae27cae0 | |||
| 73faaf6577 | |||
| 29dbd085d4 | |||
| 00b011809a | |||
| 0b46ca7ff3 | |||
| 9294b44831 | |||
| 80fd51119b | |||
| 5af5ad9e36 | |||
| 7b731ebda8 | |||
| 28bfb3b8b2 | |||
| 351895ae66 | |||
| c1009adf52 | |||
| ecaec41208 | |||
| 997b51102b | |||
| c5bd074c28 | |||
| 4c09ed3c09 | |||
| a56e43d17e | |||
| e357d9de74 | |||
| 94736ff199 | |||
| aff92a48bf | |||
| d0998a9dfb | |||
| 3678688433 | |||
| 0c03177840 | |||
| 20ff719c00 | |||
| 8a8ec492d7 | |||
| 02c1443dd1 | |||
| 79301f192c | |||
| 4b2c854c42 | |||
| d02ee7be8b | |||
| dbeadb6833 | |||
| 478cc32de1 | |||
| 7b302445c2 | |||
| ae839ef6d8 | |||
| 144a53f4b3 | |||
| fa1d1e6034 | |||
| a404436f2c | |||
| bcb12a0717 | |||
| 5d0fc8ac7a | |||
| a4d37e2c20 | |||
| c599fb75ed | |||
| e7e0f84edf | |||
| e19a282c59 | |||
| fbc8667968 | |||
| cda49c3a9a | |||
| 4be1027444 | |||
| 46152d3faf | |||
| ed4cacfffb | |||
| 52d1979937 | |||
| b30cb12133 | |||
| 31d4e304fc | |||
| 9a7a594cb5 | |||
| e469178a6b | |||
| 0a517980b7 | |||
| 9c691b2266 | |||
| 3597726aad | |||
| a4a37c268d | |||
| 651a0645c5 | |||
| bf3fa3e918 | |||
| 3b2ce9f500 | |||
| 20d6ff4620 | |||
| a2b61e2ab8 | |||
| c6289d8f75 | |||
| 567390e27c | |||
| 0c0f8bf484 | |||
| ae0a9cb591 | |||
| 3f4d7255a0 | |||
| b8d2499475 | |||
| 8cb26d886f | |||
| 3ca8dd204f | |||
| 3476afce41 | |||
| 9b0e24ec49 | |||
| 92d71fffe9 | |||
| 80c22f4f72 | |||
| 6e22d266dd | |||
| 4c285fb521 | |||
| 51c3521aaa | |||
| 32112a3326 | |||
| f22221f781 | |||
| 4250d997b3 | |||
| 153d8cef6b | |||
| c9cdf47603 | |||
| 55ac878648 | |||
| 60abddada3 | |||
| bbc583cc8d | |||
| 7906030037 | |||
| 06b385697d | |||
| 059008a903 | |||
| 97c9e95211 | |||
| a4be369e43 | |||
| bdaca78750 | |||
| 6326d7e4ba | |||
| a809a09e55 | |||
| 52c4ef2d87 | |||
| 52c31fabe2 | |||
| 79e239ad97 | |||
| 8abaf1015d | |||
| 9a0c814fd4 | |||
| c64e1b42a4 | |||
| 2d23c36067 | |||
| 754144ad99 | |||
| 0faf109c2a | |||
| 7d1eff3ec4 | |||
| e295c470a5 | |||
| 935168c024 | |||
| f44961d065 | |||
| 0c7a95ccd8 | |||
| 09215bad57 | |||
| 4ff07e3c74 | |||
| 473e01aadd | |||
| cd5312ba77 | |||
| d87bfb0d5d | |||
| d2de0ea5ad | |||
| 4af064fd17 | |||
| 8ab2b515f6 | |||
| 51a1c0e375 | |||
| 30a0098b2a | |||
| e3cb9eb8af | |||
| b0de33c801 | |||
| bcdd8c463c | |||
| 336e2a2c40 | |||
| 338d8a6610 | |||
| 9d93bda3fe | |||
| a8dda20a30 | |||
| cd7755fe07 | |||
| dc995af34b | |||
| 331ada02fd | |||
| 80e1231e9a | |||
| e61b29ec6a | |||
| 16d49d568b | |||
| 776e17062c | |||
| 8fa8c14b0b | |||
| 64de474139 | |||
| d35771f97d | |||
| 7a4d20d329 | |||
| aab095347f | |||
| 1addd5b2ab | |||
| da4bb6549c | |||
| 7193454d50 | |||
| d204b92877 | |||
| 04faf26140 | |||
| 67b81c279b | |||
| 2afb08d8b2 | |||
| 06b2c7cb16 | |||
| 9c12803ddd | |||
| ce65491d55 | |||
| b67adcf481 | |||
| 1707d55c02 | |||
| 48c2d98dde | |||
| 7dd95d8a59 | |||
| af09b5cb16 | |||
| e1b71540c7 | |||
| 85e1764857 | |||
| 0553f84d6c | |||
| 3fd89808ee | |||
| 96753821b7 | |||
| eca3ede7b0 | |||
| a7e580407c | |||
| 8bd1565696 | |||
| 03e0949067 | |||
| dbe8e33c4b | |||
| 952023db30 | |||
| 4e0b5063c6 | |||
| 30d1d55e3c | |||
| 1e9026d44c | |||
| e48950d260 | |||
| 31f46045d7 | |||
| d6455d774b | |||
| 3e928b9659 | |||
| 5e5207da95 | |||
| def8b730b7 | |||
| 22a109c2ae | |||
| 6416707e35 | |||
| 4658998b85 | |||
| d233fb8b1e | |||
| df1299b192 | |||
| 15ee17724d | |||
| 437c186a66 | |||
| 3610a42ebf | |||
| bf1bde79ec | |||
| f309638192 | |||
| 6439e4e152 | |||
| 4b1395b2c9 | |||
| 1859206007 | |||
| 3b93429353 | |||
| d68ccfcc96 | |||
| 68b8a1a01c | |||
| 75ee46715a | |||
| a8cad50f27 | |||
| fc2a67188f | |||
| d69592aaa8 | |||
| f3397f6f08 | |||
| be92e4f395 |
@@ -17,7 +17,6 @@ ENV/
|
|||||||
.conda/
|
.conda/
|
||||||
dashboard/
|
dashboard/
|
||||||
data/
|
data/
|
||||||
changelogs/
|
|
||||||
tests/
|
tests/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.astrbot
|
.astrbot
|
||||||
|
|||||||
@@ -1,42 +1,40 @@
|
|||||||
|
|
||||||
name: '🎉 功能建议'
|
name: '🎉 Feature Request / 功能建议'
|
||||||
title: "[Feature]"
|
title: "[Feature]"
|
||||||
description: 提交建议帮助我们改进。
|
description: Submit a suggestion to help us improve. / 提交建议帮助我们改进。
|
||||||
labels: [ "enhancement" ]
|
labels: [ "enhancement" ]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
感谢您抽出时间提出新功能建议,请准确解释您的想法。
|
Thank you for taking the time to suggest a new feature! Please explain your idea clearly and accurately. / 感谢您抽出时间提出新功能建议,请准确解释您的想法。
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 描述
|
label: Description / 描述
|
||||||
description: 简短描述您的功能建议。
|
description: Please describe the feature you want to be added in detail. / 请详细描述您希望添加的功能。
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
attributes:
|
attributes:
|
||||||
label: 使用场景
|
label: Use Case / 使用场景
|
||||||
description: 你想要发生什么?
|
description: Please describe the use case for this feature. / 请描述这个功能的使用场景。
|
||||||
placeholder: >
|
|
||||||
一个清晰且具体的描述这个功能的使用场景。
|
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: 你愿意提交PR吗?
|
label: Willing to Submit PR? / 是否愿意提交PR?
|
||||||
description: >
|
description: >
|
||||||
这不是必须的,但我们欢迎您的贡献。
|
This is not required, but if you are willing to submit a PR to implement this feature, it would be greatly appreciated! / 这不是必需的,但如果您愿意提交 PR 来实现这个功能,我们将不胜感激!
|
||||||
options:
|
options:
|
||||||
- label: 是的, 我愿意提交PR!
|
- label: Yes, I am willing to submit a PR. / 是的,我愿意提交 PR。
|
||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Code of Conduct
|
label: Code of Conduct
|
||||||
options:
|
options:
|
||||||
- label: >
|
- label: >
|
||||||
我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
|
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). /
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: "感谢您填写我们的表单!"
|
value: "Thank you for filling out our form!"
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.12'
|
||||||
|
|
||||||
- name: Install UV
|
- name: Install UV
|
||||||
run: pip install uv
|
run: pip install uv
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
mkdir -p data/temp
|
mkdir -p data/temp
|
||||||
export TESTING=true
|
export TESTING=true
|
||||||
export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }}
|
export ZHIPU_API_KEY=${{ secrets.OPENAI_API_KEY }}
|
||||||
pytest --cov=. -v -o log_cli=true -o log_level=DEBUG
|
pytest --cov=astrbot -v -o log_cli=true -o log_level=DEBUG
|
||||||
|
|
||||||
- name: Upload results to Codecov
|
- name: Upload results to Codecov
|
||||||
uses: codecov/codecov-action@v5
|
uses: codecov/codecov-action@v5
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 'latest'
|
node-version: '24.13.0'
|
||||||
|
|
||||||
- name: npm install, build
|
- name: npm install, build
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
GHCR_OWNER: soulter
|
GHCR_OWNER: astrbotdevs
|
||||||
HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }}
|
HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
GHCR_OWNER: soulter
|
GHCR_OWNER: astrbotdevs
|
||||||
HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }}
|
HAS_GHCR_TOKEN: ${{ secrets.GHCR_GITHUB_TOKEN != '' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: "Git ref to build (branch/tag/SHA)"
|
||||||
|
required: false
|
||||||
|
default: "master"
|
||||||
|
tag:
|
||||||
|
description: "Release tag to publish assets to (for example: v4.14.6)"
|
||||||
|
required: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-dashboard:
|
||||||
|
name: Build Dashboard
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
env:
|
||||||
|
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||||
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
|
- name: Resolve tag
|
||||||
|
id: tag
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
|
tag="${GITHUB_REF_NAME}"
|
||||||
|
elif [ -n "${{ inputs.tag }}" ]; then
|
||||||
|
tag="${{ inputs.tag }}"
|
||||||
|
else
|
||||||
|
tag="$(git describe --tags --abbrev=0)"
|
||||||
|
fi
|
||||||
|
if [ -z "$tag" ]; then
|
||||||
|
echo "Failed to resolve tag." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.28.2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: '24.13.0'
|
||||||
|
cache: "pnpm"
|
||||||
|
cache-dependency-path: dashboard/pnpm-lock.yaml
|
||||||
|
|
||||||
|
- name: Build dashboard dist
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
pnpm --dir dashboard install --frozen-lockfile
|
||||||
|
pnpm --dir dashboard run build
|
||||||
|
echo "${{ steps.tag.outputs.tag }}" > dashboard/dist/assets/version
|
||||||
|
cd dashboard
|
||||||
|
zip -r "AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip" dist
|
||||||
|
|
||||||
|
- name: Upload dashboard artifact
|
||||||
|
uses: actions/upload-artifact@v6
|
||||||
|
with:
|
||||||
|
name: Dashboard-${{ steps.tag.outputs.tag }}
|
||||||
|
if-no-files-found: error
|
||||||
|
path: dashboard/AstrBot-${{ steps.tag.outputs.tag }}-dashboard.zip
|
||||||
|
|
||||||
|
- name: Upload dashboard package to Cloudflare R2
|
||||||
|
if: ${{ env.R2_ACCOUNT_ID != '' && env.R2_ACCESS_KEY_ID != '' && env.R2_SECRET_ACCESS_KEY != '' }}
|
||||||
|
env:
|
||||||
|
R2_BUCKET_NAME: "astrbot"
|
||||||
|
R2_OBJECT_NAME: "astrbot-webui-latest.zip"
|
||||||
|
VERSION_TAG: ${{ steps.tag.outputs.tag }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
curl https://rclone.org/install.sh | sudo bash
|
||||||
|
|
||||||
|
mkdir -p ~/.config/rclone
|
||||||
|
cat <<EOF > ~/.config/rclone/rclone.conf
|
||||||
|
[r2]
|
||||||
|
type = s3
|
||||||
|
provider = Cloudflare
|
||||||
|
access_key_id = $R2_ACCESS_KEY_ID
|
||||||
|
secret_access_key = $R2_SECRET_ACCESS_KEY
|
||||||
|
endpoint = https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/${R2_OBJECT_NAME}"
|
||||||
|
rclone copy "dashboard/${R2_OBJECT_NAME}" "r2:${R2_BUCKET_NAME}" --progress
|
||||||
|
cp "dashboard/AstrBot-${VERSION_TAG}-dashboard.zip" "dashboard/astrbot-webui-${VERSION_TAG}.zip"
|
||||||
|
rclone copy "dashboard/astrbot-webui-${VERSION_TAG}.zip" "r2:${R2_BUCKET_NAME}" --progress
|
||||||
|
|
||||||
|
publish-release:
|
||||||
|
name: Publish GitHub Release
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs:
|
||||||
|
- build-dashboard
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
|
- name: Resolve tag
|
||||||
|
id: tag
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "push" ]; then
|
||||||
|
tag="${GITHUB_REF_NAME}"
|
||||||
|
elif [ -n "${{ inputs.tag }}" ]; then
|
||||||
|
tag="${{ inputs.tag }}"
|
||||||
|
else
|
||||||
|
tag="$(git describe --tags --abbrev=0)"
|
||||||
|
fi
|
||||||
|
if [ -z "$tag" ]; then
|
||||||
|
echo "Failed to resolve tag." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Download dashboard artifact
|
||||||
|
uses: actions/download-artifact@v7
|
||||||
|
with:
|
||||||
|
name: Dashboard-${{ steps.tag.outputs.tag }}
|
||||||
|
path: release-assets
|
||||||
|
|
||||||
|
|
||||||
|
- name: Resolve release notes
|
||||||
|
id: notes
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
note_file="changelogs/${{ steps.tag.outputs.tag }}.md"
|
||||||
|
if [ ! -f "$note_file" ]; then
|
||||||
|
note_file="$(mktemp)"
|
||||||
|
echo "Release ${{ steps.tag.outputs.tag }}" > "$note_file"
|
||||||
|
fi
|
||||||
|
echo "file=$note_file" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Ensure release exists
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
tag="${{ steps.tag.outputs.tag }}"
|
||||||
|
if ! gh release view "$tag" >/dev/null 2>&1; then
|
||||||
|
gh release create "$tag" --title "$tag" --notes-file "${{ steps.notes.outputs.file }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Remove stale assets from release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
tag="${{ steps.tag.outputs.tag }}"
|
||||||
|
while IFS= read -r asset; do
|
||||||
|
case "$asset" in
|
||||||
|
*.AppImage|*.dmg|*.zip|*.exe|*.blockmap)
|
||||||
|
gh release delete-asset "$tag" "$asset" -y || true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done < <(gh release view "$tag" --json assets --jq '.assets[].name')
|
||||||
|
|
||||||
|
- name: Upload assets to release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
tag="${{ steps.tag.outputs.tag }}"
|
||||||
|
gh release upload "$tag" release-assets/* --clobber
|
||||||
|
|
||||||
|
publish-pypi:
|
||||||
|
name: Publish PyPI
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
needs: publish-release
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ inputs.ref || github.ref }}
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
shell: bash
|
||||||
|
run: python -m pip install uv
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
shell: bash
|
||||||
|
run: uv build
|
||||||
|
|
||||||
|
- name: Publish to PyPI
|
||||||
|
env:
|
||||||
|
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
|
||||||
|
shell: bash
|
||||||
|
run: uv publish
|
||||||
+1
-1
@@ -32,8 +32,8 @@ tests/astrbot_plugin_openai
|
|||||||
# Dashboard
|
# Dashboard
|
||||||
dashboard/node_modules/
|
dashboard/node_modules/
|
||||||
dashboard/dist/
|
dashboard/dist/
|
||||||
|
.pnpm-store/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
package.json
|
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|
||||||
# Operating System
|
# Operating System
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
3.10
|
3.12
|
||||||
@@ -26,6 +26,7 @@ Runs on `http://localhost:3000` by default.
|
|||||||
3. After finishing, use `ruff format .` and `ruff check .` to format and check the code.
|
3. After finishing, use `ruff format .` and `ruff check .` to format and check the code.
|
||||||
4. When committing, ensure to use conventional commits messages, such as `feat: add new agent for data analysis` or `fix: resolve bug in provider manager`.
|
4. When committing, ensure to use conventional commits messages, such as `feat: add new agent for data analysis` or `fix: resolve bug in provider manager`.
|
||||||
5. Use English for all new comments.
|
5. Use English for all new comments.
|
||||||
|
6. For path handling, use `pathlib.Path` instead of string paths, and use `astrbot.core.utils.path_utils` to get the AstrBot data and temp directory.
|
||||||
|
|
||||||
## PR instructions
|
## PR instructions
|
||||||
|
|
||||||
|
|||||||
+8
-8
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3.12-slim
|
||||||
WORKDIR /AstrBot
|
WORKDIR /AstrBot
|
||||||
|
|
||||||
COPY . /AstrBot/
|
COPY . /AstrBot/
|
||||||
@@ -15,17 +15,17 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
curl \
|
curl \
|
||||||
gnupg \
|
gnupg \
|
||||||
git \
|
git \
|
||||||
|
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
|
||||||
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl gnupg \
|
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - \
|
|
||||||
&& apt-get install -y nodejs
|
|
||||||
|
|
||||||
RUN python -m pip install uv \
|
RUN python -m pip install uv \
|
||||||
&& echo "3.11" > .python-version
|
&& echo "3.12" > .python-version \
|
||||||
RUN uv pip install -r requirements.txt --no-cache-dir --system
|
&& uv lock \
|
||||||
RUN uv pip install socksio uv pilk --no-cache-dir --system
|
&& 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
|
||||||
|
|
||||||
EXPOSE 6185
|
EXPOSE 6185
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## 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)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
## 欢迎使用 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)
|
||||||
@@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
<div align="center">
|
<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_en.md">English</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
<div>
|
<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://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>
|
<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>
|
||||||
@@ -22,42 +23,42 @@
|
|||||||
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
|
<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://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>
|
<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://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://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<a href="https://astrbot.app/">文档</a> |
|
<a href="https://astrbot.app/">Documentation</a> |
|
||||||
<a href="https://blog.astrbot.app/">Blog</a> |
|
<a href="https://blog.astrbot.app/">Blog</a> |
|
||||||
<a href="https://astrbot.featurebase.app/roadmap">路线图</a> |
|
<a href="https://astrbot.featurebase.app/roadmap">Roadmap</a> |
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/issues">问题提交</a>
|
<a href="https://github.com/AstrBotDevs/AstrBot/issues">Issue Tracker</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
AstrBot 是一个易用、高性能的 AI Agentic 个人 / 群聊助手。可在 QQ、Telegram、企业微信、飞书、钉钉、Slack、等数十款主流即时通讯软件上部署,此外还内置类似 OpenWebUI 的轻量化 ChatUI,为个人、开发者和团队打造可靠、可扩展的对话式智能基础设施。无论是个人 AI 伙伴、智能客服、自动化助手,还是企业知识库,AstrBot 都能在你的即时通讯软件平台的工作流中快速构建 AI 应用。
|
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. 💯 免费 & 开源。
|
1. 💯 Free & Open Source.
|
||||||
1. ✨ AI 大模型对话,多模态,Agent,MCP,Skills,知识库,人格设定,自动压缩对话。
|
2. ✨ AI LLM Conversations, Multimodal, Agent, MCP, Skills, Knowledge Base, Persona Settings, Auto Context Compression.
|
||||||
2. 🤖 支持接入 Dify、阿里云百炼、Coze 等智能体平台。
|
3. 🤖 Supports integration with Dify, Alibaba Cloud Bailian, Coze, and other agent platforms.
|
||||||
2. 🌐 多平台,支持 QQ、企业微信、飞书、钉钉、微信公众号、Telegram、Slack 以及[更多](#支持的消息平台)。
|
4. 🌐 Multi-Platform: QQ, WeChat Work, Feishu, DingTalk, WeChat Official Accounts, Telegram, Slack, and [more](#supported-messaging-platforms).
|
||||||
3. 📦 插件扩展,已有近 800 个插件可一键安装。
|
5. 📦 Plugin Extensions with 1000+ plugins available for one-click installation.
|
||||||
5. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) 隔离化环境,安全地执行任何代码、调用 Shell、会话级资源复用。
|
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.
|
||||||
6. 💻 WebUI 支持。
|
7. 💻 WebUI Support.
|
||||||
7. 🌈 Web ChatUI 支持,ChatUI 内置代理沙盒、网页搜索等。
|
8. 🌈 Web ChatUI Support with built-in agent sandbox and web search.
|
||||||
8. 🌐 国际化(i18n)支持。
|
9. 🌐 Internationalization (i18n) Support.
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<table align="center">
|
<table align="center">
|
||||||
<tr align="center">
|
<tr align="center">
|
||||||
<th>💙 角色扮演 & 情感陪伴</th>
|
<th>💙 Role-playing & Emotional Companionship</th>
|
||||||
<th>✨ 主动式 Agent</th>
|
<th>✨ Proactive Agent</th>
|
||||||
<th>🚀 通用 Agentic 能力</th>
|
<th>🚀 General Agentic Capabilities</th>
|
||||||
<th>🧩 900+ 社区插件</th>
|
<th>🧩 1000+ Community Plugins</th>
|
||||||
</tr>
|
</tr>
|
||||||
<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="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
|
||||||
@@ -67,149 +68,163 @@ AstrBot 是一个易用、高性能的 AI Agentic 个人 / 群聊助手。可在
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## 快速开始
|
## Quick Start
|
||||||
|
|
||||||
#### Docker 部署(推荐 🥳)
|
#### Docker Deployment (Recommended 🥳)
|
||||||
|
|
||||||
推荐使用 Docker / Docker Compose 方式部署 AstrBot。
|
We recommend deploying AstrBot using Docker or Docker Compose.
|
||||||
|
|
||||||
请参阅官方文档 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) 。
|
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 部署
|
#### uv Deployment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx astrbot
|
uv tool install astrbot
|
||||||
|
astrbot
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 宝塔面板部署
|
#### System Package Manager Installation
|
||||||
|
|
||||||
AstrBot 与宝塔面板合作,已上架至宝塔面板。
|
##### Arch Linux
|
||||||
|
|
||||||
请参阅官方文档 [宝塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.html) 。
|
```bash
|
||||||
|
yay -S astrbot-git
|
||||||
|
# or use paru
|
||||||
|
paru -S astrbot-git
|
||||||
|
```
|
||||||
|
|
||||||
#### 1Panel 部署
|
#### Desktop Application (Tauri)
|
||||||
|
|
||||||
AstrBot 已由 1Panel 官方上架至 1Panel 面板。
|
Desktop repository: [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop).
|
||||||
|
|
||||||
请参阅官方文档 [1Panel 部署](https://astrbot.app/deploy/astrbot/1panel.html) 。
|
Supports multiple system architectures, direct installation, out-of-the-box experience. Ideal for beginners.
|
||||||
|
|
||||||
#### 在 雨云 上部署
|
#### AstrBot Launcher
|
||||||
|
|
||||||
AstrBot 已由雨云官方上架至云应用平台,可一键部署。
|
Quick deployment and multi-instance solution. Visit the [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) repository and find the latest release for your system.
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
For Chinese users:
|
||||||
|
|
||||||
|
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)
|
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||||
|
|
||||||
#### 在 Replit 上部署
|
#### Deploy on Replit
|
||||||
|
|
||||||
社区贡献的部署方式。
|
Community-contributed deployment method.
|
||||||
|
|
||||||
[](https://repl.it/github/AstrBotDevs/AstrBot)
|
[](https://repl.it/github/AstrBotDevs/AstrBot)
|
||||||
|
|
||||||
#### Windows 一键安装器部署
|
#### Windows One-Click Installer
|
||||||
|
|
||||||
请参阅官方文档 [使用 Windows 一键安装器部署 AstrBot](https://astrbot.app/deploy/astrbot/windows.html) 。
|
Please refer to the official documentation: [Deploy AstrBot with Windows One-Click Installer](https://astrbot.app/deploy/astrbot/windows.html).
|
||||||
|
|
||||||
#### CasaOS 部署
|
#### CasaOS Deployment
|
||||||
|
|
||||||
社区贡献的部署方式。
|
Community-contributed deployment method.
|
||||||
|
|
||||||
请参阅官方文档 [CasaOS 部署](https://astrbot.app/deploy/astrbot/casaos.html) 。
|
Please refer to the official documentation: [CasaOS Deployment](https://astrbot.app/deploy/astrbot/casaos.html).
|
||||||
|
|
||||||
#### 手动部署
|
#### Manual Deployment
|
||||||
|
|
||||||
首先安装 uv:
|
First, install uv:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install uv
|
pip install uv
|
||||||
```
|
```
|
||||||
|
|
||||||
通过 Git Clone 安装 AstrBot:
|
Install AstrBot via Git Clone:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
|
git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
|
||||||
uv run main.py
|
uv run main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
|
Or refer to the official documentation: [Deploy AstrBot from Source](https://astrbot.app/deploy/astrbot/cli.html).
|
||||||
|
|
||||||
## 支持的消息平台
|
## Supported Messaging Platforms
|
||||||
|
|
||||||
**官方维护**
|
Connect AstrBot to your favorite chat platform.
|
||||||
|
|
||||||
- QQ (官方平台 & OneBot)
|
| Platform | Maintainer |
|
||||||
- Telegram
|
|---------|---------------|
|
||||||
- 企微应用 & 企微智能机器人
|
| QQ | Official |
|
||||||
- 微信客服 & 微信公众号
|
| OneBot v11 protocol implementation | Official |
|
||||||
- 飞书
|
| Telegram | Official |
|
||||||
- 钉钉
|
| WeChat Work Application & WeChat Work Intelligent Bot | Official |
|
||||||
- Slack
|
| WeChat Customer Service & WeChat Official Accounts | Official |
|
||||||
- Discord
|
| Feishu (Lark) | Official |
|
||||||
- Satori
|
| DingTalk | Official |
|
||||||
- Misskey
|
| Slack | Official |
|
||||||
- Whatsapp (将支持)
|
| Discord | Official |
|
||||||
- LINE (将支持)
|
| 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 Services
|
||||||
|
|
||||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
| Service | Type |
|
||||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
|---------|---------------|
|
||||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
| OpenAI and Compatible Services | LLM Services |
|
||||||
|
| Anthropic | LLM Services |
|
||||||
|
| Google Gemini | LLM Services |
|
||||||
|
| Moonshot AI | LLM Services |
|
||||||
|
| Zhipu AI | LLM Services |
|
||||||
|
| DeepSeek | LLM Services |
|
||||||
|
| Ollama (Self-hosted) | LLM Services |
|
||||||
|
| LM Studio (Self-hosted) | LLM Services |
|
||||||
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | LLM Services |
|
||||||
|
| [302.AI](https://share.302.ai/rr1M3l) | LLM Services |
|
||||||
|
| [TokenPony](https://www.tokenpony.cn/3YPyf) | LLM Services |
|
||||||
|
| [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | LLM Services |
|
||||||
|
| [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE) | LLM Services |
|
||||||
|
| ModelScope | LLM Services |
|
||||||
|
| OneAPI | LLM Services |
|
||||||
|
| Dify | LLMOps Platforms |
|
||||||
|
| Alibaba Cloud Bailian Applications | LLMOps Platforms |
|
||||||
|
| Coze | LLMOps Platforms |
|
||||||
|
| OpenAI Whisper | Speech-to-Text Services |
|
||||||
|
| SenseVoice | Speech-to-Text Services |
|
||||||
|
| OpenAI TTS | Text-to-Speech Services |
|
||||||
|
| Gemini TTS | Text-to-Speech Services |
|
||||||
|
| GPT-Sovits-Inference | Text-to-Speech Services |
|
||||||
|
| GPT-Sovits | Text-to-Speech Services |
|
||||||
|
| FishAudio | Text-to-Speech Services |
|
||||||
|
| Edge TTS | Text-to-Speech Services |
|
||||||
|
| Alibaba Cloud Bailian TTS | Text-to-Speech Services |
|
||||||
|
| Azure TTS | Text-to-Speech Services |
|
||||||
|
| Minimax TTS | Text-to-Speech Services |
|
||||||
|
| Volcano Engine TTS | Text-to-Speech Services |
|
||||||
|
|
||||||
## 支持的模型服务
|
## ❤️ Contributing
|
||||||
|
|
||||||
**大模型服务**
|
Issues and Pull Requests are always welcome! Feel free to submit your changes to this project :)
|
||||||
|
|
||||||
- OpenAI 及兼容服务
|
### How to Contribute
|
||||||
- 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 平台**
|
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.
|
||||||
|
|
||||||
- Dify
|
### Development Environment
|
||||||
- 阿里云百炼应用
|
|
||||||
- Coze
|
|
||||||
|
|
||||||
**语音转文本服务**
|
AstrBot uses `ruff` for code formatting and linting.
|
||||||
|
|
||||||
- OpenAI Whisper
|
|
||||||
- SenseVoice
|
|
||||||
|
|
||||||
**文本转语音服务**
|
|
||||||
|
|
||||||
- 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
|
```bash
|
||||||
git clone https://github.com/AstrBotDevs/AstrBot
|
git clone https://github.com/AstrBotDevs/AstrBot
|
||||||
@@ -217,42 +232,42 @@ pip install pre-commit
|
|||||||
pre-commit install
|
pre-commit install
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🌍 社区
|
## 🌍 Community
|
||||||
|
|
||||||
### QQ 群组
|
### QQ Groups
|
||||||
|
|
||||||
- 1 群:322154837
|
- Group 1: 322154837
|
||||||
- 3 群:630166526
|
- Group 3: 630166526
|
||||||
- 5 群:822130018
|
- Group 5: 822130018
|
||||||
- 6 群:753075035
|
- Group 6: 753075035
|
||||||
- 7 群:743746109
|
- Group 7: 743746109
|
||||||
- 8 群:1030353265
|
- Group 8: 1030353265
|
||||||
- 开发者群:975206796
|
- Developer Group: 975206796
|
||||||
|
|
||||||
### Telegram 群组
|
### 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>
|
<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 群组
|
### 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>
|
<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
|
||||||
|
|
||||||
特别感谢所有 Contributors 和插件开发者对 AstrBot 的贡献 ❤️
|
Special thanks to all Contributors and plugin developers for their contributions to AstrBot ❤️
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" />
|
||||||
</a>
|
</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) - 伟大的猫猫框架
|
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - The amazing cat framework
|
||||||
|
|
||||||
## ⭐ Star History
|
## ⭐ Star History
|
||||||
|
|
||||||
> [!TIP]
|
> [!TIP]
|
||||||
> 如果本项目对您的生活 / 工作产生了帮助,或者您关注本项目的未来发展,请给项目 Star,这是我们维护这个开源项目的动力 <3
|
> 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">
|
<div align="center">
|
||||||
|
|
||||||
@@ -260,12 +275,11 @@ pre-commit install
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
_Companionship and capability should never be at odds. What we aim to create is a robot that can understand emotions, provide genuine companionship, and reliably accomplish tasks._
|
||||||
|
|
||||||
_私は、高性能ですから!_
|
_私は、高性能ですから!_
|
||||||
|
|
||||||
陪伴与能力从来不应该是对立面。我们希望创造的是一个既能理解情绪、给予陪伴,也能可靠完成工作的机器人。
|
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|||||||
+98
-68
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> |
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh.md">中文</a> |
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
||||||
@@ -38,7 +37,7 @@
|
|||||||
|
|
||||||
AstrBot is an open-source all-in-one Agent chatbot platform that integrates with mainstream instant messaging apps. It provides reliable and scalable conversational AI infrastructure for individuals, developers, and teams. Whether you're building a personal AI companion, intelligent customer service, automation assistant, or enterprise knowledge base, AstrBot enables you to quickly build production-ready AI applications within your IM platform workflows.
|
AstrBot 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
|
## Key Features
|
||||||
|
|
||||||
@@ -46,12 +45,29 @@ AstrBot is an open-source all-in-one Agent chatbot platform that integrates with
|
|||||||
2. ✨ AI LLM Conversations, Multimodal, Agent, MCP, Skills, Knowledge Base, Persona Settings, Auto Context Compression.
|
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.
|
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).
|
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.
|
5. 📦 Plugin Extensions with 1000+ plugins available for one-click installation.
|
||||||
6. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) for isolated, safe execution of code, shell calls, and session-level resource reuse.
|
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.
|
7. 💻 WebUI Support.
|
||||||
8. 🌈 Web ChatUI Support with built-in agent sandbox and web search.
|
8. 🌈 Web ChatUI Support with built-in agent sandbox and web search.
|
||||||
9. 🌐 Internationalization (i18n) Support.
|
9. 🌐 Internationalization (i18n) Support.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr align="center">
|
||||||
|
<th>💙 Role-playing & Emotional Companionship</th>
|
||||||
|
<th>✨ Proactive Agent</th>
|
||||||
|
<th>🚀 General Agentic Capabilities</th>
|
||||||
|
<th>🧩 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>
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
#### Docker Deployment (Recommended 🥳)
|
#### Docker Deployment (Recommended 🥳)
|
||||||
@@ -63,9 +79,30 @@ Please refer to the official documentation: [Deploy AstrBot with Docker](https:/
|
|||||||
#### uv Deployment
|
#### uv Deployment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx astrbot
|
uv tool install astrbot
|
||||||
|
astrbot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### System Package Manager Installation
|
||||||
|
|
||||||
|
##### Arch Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yay -S astrbot-git
|
||||||
|
# or use paru
|
||||||
|
paru -S astrbot-git
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Desktop Application (Tauri)
|
||||||
|
|
||||||
|
Desktop repository: [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop).
|
||||||
|
|
||||||
|
Supports multiple system architectures, direct installation, out-of-the-box experience. Ideal for beginners.
|
||||||
|
|
||||||
|
#### AstrBot Launcher
|
||||||
|
|
||||||
|
Quick deployment and multi-instance solution. Visit the [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) repository and find the latest release for your system.
|
||||||
|
|
||||||
#### BT-Panel Deployment
|
#### BT-Panel Deployment
|
||||||
|
|
||||||
AstrBot has partnered with BT-Panel and is now available in their marketplace.
|
AstrBot has partnered with BT-Panel and is now available in their marketplace.
|
||||||
@@ -80,6 +117,8 @@ Please refer to the official documentation: [1Panel Deployment](https://astrbot.
|
|||||||
|
|
||||||
#### Deploy on RainYun
|
#### Deploy on RainYun
|
||||||
|
|
||||||
|
For Chinese users:
|
||||||
|
|
||||||
AstrBot has been officially listed on RainYun's cloud application platform with one-click deployment.
|
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)
|
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||||
@@ -119,70 +158,61 @@ Or refer to the official documentation: [Deploy AstrBot from Source](https://ast
|
|||||||
|
|
||||||
## Supported Messaging Platforms
|
## Supported Messaging Platforms
|
||||||
|
|
||||||
**Officially Maintained**
|
Connect AstrBot to your favorite chat platform.
|
||||||
|
|
||||||
- QQ (Official Platform & OneBot)
|
| Platform | Maintainer |
|
||||||
- Telegram
|
|---------|---------------|
|
||||||
- WeChat Work Application & WeChat Work Intelligent Bot
|
| QQ | Official |
|
||||||
- WeChat Customer Service & WeChat Official Accounts
|
| OneBot v11 protocol implementation | Official |
|
||||||
- Feishu (Lark)
|
| Telegram | Official |
|
||||||
- DingTalk
|
| WeChat Work Application & WeChat Work Intelligent Bot | Official |
|
||||||
- Slack
|
| WeChat Customer Service & WeChat Official Accounts | Official |
|
||||||
- Discord
|
| Feishu (Lark) | Official |
|
||||||
- Satori
|
| DingTalk | Official |
|
||||||
- Misskey
|
| Slack | Official |
|
||||||
- WhatsApp (Coming Soon)
|
| Discord | Official |
|
||||||
- LINE (Coming Soon)
|
| LINE | Official |
|
||||||
|
| Satori | Official |
|
||||||
**Community Maintained**
|
| Misskey | Official |
|
||||||
|
| WhatsApp (Coming Soon) | Official |
|
||||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Community |
|
||||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | Community |
|
||||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Community |
|
||||||
|
|
||||||
## Supported Model Services
|
## Supported Model Services
|
||||||
|
|
||||||
**LLM Services**
|
| Service | Type |
|
||||||
|
|---------|---------------|
|
||||||
- OpenAI and Compatible Services
|
| OpenAI and Compatible Services | LLM Services |
|
||||||
- Anthropic
|
| Anthropic | LLM Services |
|
||||||
- Google Gemini
|
| Google Gemini | LLM Services |
|
||||||
- Moonshot AI
|
| Moonshot AI | LLM Services |
|
||||||
- Zhipu AI
|
| Zhipu AI | LLM Services |
|
||||||
- DeepSeek
|
| DeepSeek | LLM Services |
|
||||||
- Ollama (Self-hosted)
|
| Ollama (Self-hosted) | LLM Services |
|
||||||
- LM Studio (Self-hosted)
|
| LM Studio (Self-hosted) | LLM Services |
|
||||||
- [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | LLM Services |
|
||||||
- [302.AI](https://share.302.ai/rr1M3l)
|
| [302.AI](https://share.302.ai/rr1M3l) | LLM Services |
|
||||||
- [TokenPony](https://www.tokenpony.cn/3YPyf)
|
| [TokenPony](https://www.tokenpony.cn/3YPyf) | LLM Services |
|
||||||
- [SiliconFlow](https://docs.siliconflow.cn/cn/usecases/use-siliconcloud-in-astrbot)
|
| [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | LLM Services |
|
||||||
- [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE)
|
| [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE) | LLM Services |
|
||||||
- ModelScope
|
| ModelScope | LLM Services |
|
||||||
- OneAPI
|
| OneAPI | LLM Services |
|
||||||
|
| Dify | LLMOps Platforms |
|
||||||
**LLMOps Platforms**
|
| Alibaba Cloud Bailian Applications | LLMOps Platforms |
|
||||||
|
| Coze | LLMOps Platforms |
|
||||||
- Dify
|
| OpenAI Whisper | Speech-to-Text Services |
|
||||||
- Alibaba Cloud Bailian Applications
|
| SenseVoice | Speech-to-Text Services |
|
||||||
- Coze
|
| OpenAI TTS | Text-to-Speech Services |
|
||||||
|
| Gemini TTS | Text-to-Speech Services |
|
||||||
**Speech-to-Text Services**
|
| GPT-Sovits-Inference | Text-to-Speech Services |
|
||||||
|
| GPT-Sovits | Text-to-Speech Services |
|
||||||
- OpenAI Whisper
|
| FishAudio | Text-to-Speech Services |
|
||||||
- SenseVoice
|
| Edge TTS | Text-to-Speech Services |
|
||||||
|
| Alibaba Cloud Bailian TTS | Text-to-Speech Services |
|
||||||
**Text-to-Speech Services**
|
| Azure TTS | Text-to-Speech Services |
|
||||||
|
| Minimax TTS | Text-to-Speech Services |
|
||||||
- OpenAI TTS
|
| Volcano Engine TTS | Text-to-Speech Services |
|
||||||
- Gemini TTS
|
|
||||||
- GPT-Sovits-Inference
|
|
||||||
- GPT-Sovits
|
|
||||||
- FishAudio
|
|
||||||
- Edge TTS
|
|
||||||
- Alibaba Cloud Bailian TTS
|
|
||||||
- Azure TTS
|
|
||||||
- Minimax TTS
|
|
||||||
- Volcano Engine TTS
|
|
||||||
|
|
||||||
## ❤️ Contributing
|
## ❤️ Contributing
|
||||||
|
|
||||||
@@ -227,7 +257,7 @@ pre-commit install
|
|||||||
Special thanks to all Contributors and plugin developers for their contributions to AstrBot ❤️
|
Special thanks to all Contributors and plugin developers for their contributions to AstrBot ❤️
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
Additionally, the birth of this project would not have been possible without the help of the following open-source projects:
|
Additionally, the birth of this project would not have been possible without the help of the following open-source projects:
|
||||||
@@ -245,10 +275,10 @@ Additionally, the birth of this project would not have been possible without the
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
_Companionship and capability should never be at odds. What we aim to create is a robot that can understand emotions, provide genuine companionship, and reliably accomplish tasks._
|
||||||
|
|
||||||
_私は、高性能ですから!_
|
_私は、高性能ですから!_
|
||||||
|
|
||||||
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||||
|
|||||||
+121
-84
@@ -1,9 +1,13 @@
|
|||||||

|

|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh.md">中文</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">English</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -14,22 +18,17 @@
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<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/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?style=for-the-badge&color=76bad9" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
||||||
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?style=for-the-badge&color=76bad9"/></a>
|
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
|
||||||
<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://zread.ai/AstrBotDevs/AstrBot" target="_blank"><img src="https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFZIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjc1ODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></a>
|
||||||
<a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?color=76bad9"/></a>
|
||||||
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20plugins&style=for-the-badge&label=Marketplace&cacheSeconds=3600">
|
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20&label=Marketplace&cacheSeconds=3600">
|
||||||
|
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
|
||||||
|
|
||||||
<a href="https://astrbot.app/">Documentation</a> |
|
<a href="https://astrbot.app/">Documentation</a> |
|
||||||
<a href="https://blog.astrbot.app/">Blog</a> |
|
<a href="https://blog.astrbot.app/">Blog</a> |
|
||||||
<a href="https://astrbot.featurebase.app/roadmap">Feuille de route</a> |
|
<a href="https://astrbot.featurebase.app/roadmap">Feuille de route</a> |
|
||||||
@@ -38,17 +37,36 @@
|
|||||||
|
|
||||||
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.
|
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.
|
1. 💯 Gratuit & Open Source.
|
||||||
2. ✨ Conversations avec LLM IA, Multimodal, Agent, MCP, Base de connaissances, Paramètres de personnalité.
|
2. ✨ Dialogue avec de grands modèles d'IA, multimodal, Agent, MCP, Skills, Base de connaissances, Paramétrage de personnalité, compression automatique des dialogues.
|
||||||
3. 🤖 Prise en charge de l'intégration avec Dify, Alibaba Cloud Bailian, Coze et autres plateformes d'agents.
|
3. 🤖 Prise en charge de l'accès aux plateformes d'Agents telles que Dify, Alibaba Cloud Bailian, Coze, etc.
|
||||||
4. 🌐 Multi-plateforme : QQ, WeChat Work, Feishu, DingTalk, Comptes officiels WeChat, Telegram, Slack, et [plus encore](#plateformes-de-messagerie-prises-en-charge).
|
4. 🌐 Multiplateforme : supporte QQ, WeChat Enterprise, 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.
|
5. 📦 Extension par plugins, avec plus de 1000 plugins déjà disponibles pour une installation en un clic.
|
||||||
6. 💻 Support WebUI.
|
6. 🛡️ Environnement isolé [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) : exécution sécurisée de code, appels Shell et réutilisation des ressources au niveau de la session.
|
||||||
7. 🌐 Support de l'internationalisation (i18n).
|
7. 💻 Support WebUI.
|
||||||
|
8. 🌈 Support Web ChatUI, avec sandbox d'agent intégrée, recherche web, etc.
|
||||||
|
9. 🌐 Support de l'internationalisation (i18n).
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr align="center">
|
||||||
|
<th>💙 Jeux de rôle & Accompagnement émotionnel</th>
|
||||||
|
<th>✨ Agent proactif</th>
|
||||||
|
<th>🚀 Capacités agentiques générales</th>
|
||||||
|
<th>🧩 1000+ Plugins de communauté</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
## Démarrage rapide
|
## Démarrage rapide
|
||||||
|
|
||||||
@@ -61,9 +79,20 @@ Veuillez consulter la documentation officielle : [Déployer AstrBot avec Docker]
|
|||||||
#### Déploiement uv
|
#### Déploiement uv
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx astrbot
|
uv tool install astrbot
|
||||||
|
astrbot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Application de bureau (Tauri)
|
||||||
|
|
||||||
|
Dépôt de l'application de bureau : [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop).
|
||||||
|
|
||||||
|
Prend en charge plusieurs architectures système, installation directe, prête à l'emploi. La solution de déploiement de bureau en un clic la plus adaptée aux débutants. Non recommandée pour les serveurs.
|
||||||
|
|
||||||
|
#### Déploiement en un clic avec le lanceur (AstrBot Launcher)
|
||||||
|
|
||||||
|
Déploiement rapide et solution multi-instances, isolation de l'environnement. Accédez au dépôt [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher), trouvez le package d'installation correspondant à votre système sous la dernière version sur la page Releases.
|
||||||
|
|
||||||
#### Déploiement BT-Panel
|
#### Déploiement BT-Panel
|
||||||
|
|
||||||
AstrBot s'est associé à BT-Panel et est maintenant disponible sur leur marketplace.
|
AstrBot s'est associé à BT-Panel et est maintenant disponible sur leur marketplace.
|
||||||
@@ -78,6 +107,8 @@ Veuillez consulter la documentation officielle : [Déploiement 1Panel](https://a
|
|||||||
|
|
||||||
#### Déployer sur RainYun
|
#### Déployer sur RainYun
|
||||||
|
|
||||||
|
For Chinese users:
|
||||||
|
|
||||||
AstrBot a été officiellement listé sur la plateforme d'applications cloud de RainYun avec un déploiement en un clic.
|
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)
|
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||||
@@ -115,72 +146,73 @@ uv run main.py
|
|||||||
|
|
||||||
Ou consultez la documentation officielle : [Déployer AstrBot depuis les sources](https://astrbot.app/deploy/astrbot/cli.html).
|
Ou consultez la documentation officielle : [Déployer AstrBot depuis les sources](https://astrbot.app/deploy/astrbot/cli.html).
|
||||||
|
|
||||||
|
#### Installation via le gestionnaire de paquets du système
|
||||||
|
|
||||||
|
##### Arch Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yay -S astrbot-git
|
||||||
|
# ou utiliser paru
|
||||||
|
paru -S astrbot-git
|
||||||
|
```
|
||||||
|
|
||||||
## Plateformes de messagerie prises en charge
|
## Plateformes de messagerie prises en charge
|
||||||
|
|
||||||
**Maintenues officiellement**
|
Connectez AstrBot à vos plateformes de chat préférées.
|
||||||
|
|
||||||
- QQ (Plateforme officielle & OneBot)
|
| Plateforme | Maintenance |
|
||||||
- Telegram
|
|---------|---------------|
|
||||||
- Application WeChat Work & Bot intelligent WeChat Work
|
| QQ | Officielle |
|
||||||
- Service client WeChat & Comptes officiels WeChat
|
| Implémentation du protocole OneBot v11 | Officielle |
|
||||||
- Feishu (Lark)
|
| Telegram | Officielle |
|
||||||
- DingTalk
|
| Application WeChat Work & Bot intelligent WeChat Work | Officielle |
|
||||||
- Slack
|
| Service client WeChat & Comptes officiels WeChat | Officielle |
|
||||||
- Discord
|
| Feishu (Lark) | Officielle |
|
||||||
- Satori
|
| DingTalk | Officielle |
|
||||||
- Misskey
|
| Slack | Officielle |
|
||||||
- WhatsApp (Bientôt disponible)
|
| Discord | Officielle |
|
||||||
- LINE (Bientôt disponible)
|
| LINE | Officielle |
|
||||||
|
| Satori | Officielle |
|
||||||
**Maintenues par la communauté**
|
| Misskey | Officielle |
|
||||||
|
| WhatsApp (Bientôt disponible) | Officielle |
|
||||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Communauté |
|
||||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | Communauté |
|
||||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Communauté |
|
||||||
|
|
||||||
## Services de modèles pris en charge
|
## Services de modèles pris en charge
|
||||||
|
|
||||||
**Services LLM**
|
| Service | Type |
|
||||||
|
|---------|---------------|
|
||||||
- OpenAI et services compatibles
|
| OpenAI et services compatibles | Services LLM |
|
||||||
- Anthropic
|
| Anthropic | Services LLM |
|
||||||
- Google Gemini
|
| Google Gemini | Services LLM |
|
||||||
- Moonshot AI
|
| Moonshot AI | Services LLM |
|
||||||
- Zhipu AI
|
| Zhipu AI | Services LLM |
|
||||||
- DeepSeek
|
| DeepSeek | Services LLM |
|
||||||
- Ollama (Auto-hébergé)
|
| Ollama (Auto-hébergé) | Services LLM |
|
||||||
- LM Studio (Auto-hébergé)
|
| LM Studio (Auto-hébergé) | Services LLM |
|
||||||
- [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | Services LLM |
|
||||||
- [302.AI](https://share.302.ai/rr1M3l)
|
| [302.AI](https://share.302.ai/rr1M3l) | Services LLM |
|
||||||
- [TokenPony](https://www.tokenpony.cn/3YPyf)
|
| [TokenPony](https://www.tokenpony.cn/3YPyf) | Services LLM |
|
||||||
- [SiliconFlow](https://docs.siliconflow.cn/cn/usecases/use-siliconcloud-in-astrbot)
|
| [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | Services LLM |
|
||||||
- [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE)
|
| [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE) | Services LLM |
|
||||||
- ModelScope
|
| ModelScope | Services LLM |
|
||||||
- OneAPI
|
| OneAPI | Services LLM |
|
||||||
|
| Dify | Plateformes LLMOps |
|
||||||
**Plateformes LLMOps**
|
| Applications Alibaba Cloud Bailian | Plateformes LLMOps |
|
||||||
|
| Coze | Plateformes LLMOps |
|
||||||
- Dify
|
| OpenAI Whisper | Services de reconnaissance vocale |
|
||||||
- Applications Alibaba Cloud Bailian
|
| SenseVoice | Services de reconnaissance vocale |
|
||||||
- Coze
|
| OpenAI TTS | Services de synthèse vocale |
|
||||||
|
| Gemini TTS | Services de synthèse vocale |
|
||||||
**Services de reconnaissance vocale**
|
| GPT-Sovits-Inference | Services de synthèse vocale |
|
||||||
|
| GPT-Sovits | Services de synthèse vocale |
|
||||||
- OpenAI Whisper
|
| FishAudio | Services de synthèse vocale |
|
||||||
- SenseVoice
|
| Edge TTS | Services de synthèse vocale |
|
||||||
|
| Alibaba Cloud Bailian TTS | Services de synthèse vocale |
|
||||||
**Services de synthèse vocale**
|
| Azure TTS | Services de synthèse vocale |
|
||||||
|
| Minimax TTS | Services de synthèse vocale |
|
||||||
- OpenAI TTS
|
| Volcano Engine TTS | Services de synthèse vocale |
|
||||||
- Gemini TTS
|
|
||||||
- GPT-Sovits-Inference
|
|
||||||
- GPT-Sovits
|
|
||||||
- FishAudio
|
|
||||||
- Edge TTS
|
|
||||||
- Alibaba Cloud Bailian TTS
|
|
||||||
- Azure TTS
|
|
||||||
- Minimax TTS
|
|
||||||
- Volcano Engine TTS
|
|
||||||
|
|
||||||
## ❤️ Contribuer
|
## ❤️ Contribuer
|
||||||
|
|
||||||
@@ -223,7 +255,7 @@ pre-commit install
|
|||||||
Un grand merci à tous les contributeurs et développeurs de plugins pour leurs contributions à AstrBot ❤️
|
Un grand merci à tous les contributeurs et développeurs de plugins pour leurs contributions à AstrBot ❤️
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
De plus, la naissance de ce projet n'aurait pas été possible sans l'aide des projets open source suivants :
|
De plus, la naissance de ce projet n'aurait pas été possible sans l'aide des projets open source suivants :
|
||||||
@@ -241,7 +273,12 @@ De plus, la naissance de ce projet n'aurait pas été possible sans l'aide des p
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</details>
|
<div align="center">
|
||||||
|
|
||||||
|
_La compagnie et la capacité ne devraient jamais être des opposés. Nous souhaitons créer un robot capable à la fois de comprendre les émotions, d'offrir de la présence, et d'accomplir des tâches de manière fiable._
|
||||||
|
|
||||||
_私は、高性能ですから!_
|
_私は、高性能ですから!_
|
||||||
|
|
||||||
|
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
+122
-84
@@ -1,9 +1,13 @@
|
|||||||

|

|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh.md">中文</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">English</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -14,22 +18,17 @@
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<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/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?style=for-the-badge&color=76bad9" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
||||||
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?style=for-the-badge&color=76bad9"/></a>
|
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
|
||||||
<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://zread.ai/AstrBotDevs/AstrBot" target="_blank"><img src="https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFZIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0LjYxNTZDNS4zMTUwMiAxNC4zOTk5IDUuNjAxNTYgMTQuMTEzNCA1LjYwMTU2IDEzLjc1OTlWMTEuMDM5OUM1LjYwMTU2IDEwLjY4NjQgNS4zMTUwMiAxMC4zOTk5IDQuOTYxNTYgMTAuMzk5OVoiIGZpbGw9IiNmZmYiLz4KPHBhdGggZD0iTTEzLjc1ODQgMS42MDAxSDExLjAzODRDMTAuNjg1IDEuNjAwMSAxMC4zOTg0IDEuODg2NjQgMTAuMzk4NCAyLjI0MDFWNC45NjAxQzEwLjM5ODQgNS4zMTM1NiAxMC42ODUgNS42MDAxIDExLjAzODQgNS42MDAxSDEzLjc1ODRDMTQuMTExOSA1LjYwMDEgMTQuMzk4NCA1LjMxMzU2IDE0LjM5ODQgNC45NjAxVjIuMjQwMUMxNC4zOTg0IDEuODg2NjQgMTQuMTExOSAxLjYwMDEgMTMuNzU4NCAxLjYwMDFZIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDRMNCAxMlpFIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></a>
|
||||||
<a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?color=76bad9"/></a>
|
||||||
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E5%80%8B&style=for-the-badge&label=%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3&cacheSeconds=3600">
|
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20&label=%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%83%9E%E3%83%BC%E3%82%B1%E3%83%83%E3%83%88&cacheSeconds=3600">
|
||||||
|
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
|
||||||
|
|
||||||
<a href="https://astrbot.app/">ドキュメント</a> |
|
<a href="https://astrbot.app/">ドキュメント</a> |
|
||||||
<a href="https://blog.astrbot.app/">Blog</a> |
|
<a href="https://blog.astrbot.app/">Blog</a> |
|
||||||
<a href="https://astrbot.featurebase.app/roadmap">ロードマップ</a> |
|
<a href="https://astrbot.featurebase.app/roadmap">ロードマップ</a> |
|
||||||
@@ -38,17 +37,36 @@
|
|||||||
|
|
||||||
AstrBot は、主要なインスタントメッセージングアプリと統合できるオープンソースのオールインワン Agent チャットボットプラットフォームです。個人、開発者、チームに信頼性が高くスケーラブルな会話型 AI インフラストラクチャを提供します。パーソナル AI コンパニオン、インテリジェントカスタマーサービス、オートメーションアシスタント、エンタープライズナレッジベースなど、AstrBot を使用すると、IM プラットフォームのワークフロー内で本番環境対応の 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. 💯 無料 & オープンソース。
|
1. 💯 無料 & オープンソース。
|
||||||
2. ✨ AI 大規模言語モデル対話、マルチモーダル、Agent、MCP、ナレッジベース、ペルソナ設定。
|
2. ✨ AI大規模言語モデル対話、マルチモーダル、Agent、MCP、Skills、ナレッジベース、ペルソナ設定、対話の自動圧縮。
|
||||||
3. 🤖 Dify、Alibaba Cloud 百炼、Coze などの Agent プラットフォームとの統合をサポート。
|
3. 🤖 Dify、Alibaba Cloud Bailian(百煉)、Coze などのAgentプラットフォームへの接続をサポート。
|
||||||
4. 🌐 マルチプラットフォーム:QQ、WeChat Work、Feishu、DingTalk、WeChat 公式アカウント、Telegram、Slack、[その他](#サポートされているメッセージプラットフォーム)。
|
4. 🌐 マルチプラットフォーム:QQ、企業微信(WeCom)、飛書(Lark)、釘釘(DingTalk)、WeChat公式アカウント、Telegram、Slack、[その他](#サポートされているメッセージプラットフォーム)に対応。
|
||||||
5. 📦 約800個のプラグインをワンクリックでインストール可能なプラグイン拡張機能。
|
5. 📦 プラグイン拡張:1000を超える既存プラグインをワンクリックでインストール可能。
|
||||||
6. 💻 WebUI サポート。
|
6. 🛡️ 隔離環境[Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html):コードの安全な実行、Shell呼び出し、セッションレベルのリソース再利用。
|
||||||
7. 🌐 国際化(i18n)サポート。
|
7. 💻 WebUI 対応。
|
||||||
|
8. 🌈 Web ChatUI 対応:ChatUI内にAgent Sandboxやウェブ検索などを内蔵。
|
||||||
|
9. 🌐 多言語対応(i18n)。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr align="center">
|
||||||
|
<th>💙 ロールプレイ & 感情的な対話</th>
|
||||||
|
<th>✨ プロアクティブ・エージェント (Proactive Agent)</th>
|
||||||
|
<th>🚀 汎用 エージェント的能力</th>
|
||||||
|
<th>🧩 1000+ コミュニティプラグイン</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
## クイックスタート
|
## クイックスタート
|
||||||
|
|
||||||
@@ -61,9 +79,20 @@ Docker / Docker Compose を使用した AstrBot のデプロイを推奨しま
|
|||||||
#### uv デプロイ
|
#### uv デプロイ
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx astrbot
|
uv tool install astrbot
|
||||||
|
astrbot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### デスクトップアプリのデプロイ(Tauri)
|
||||||
|
|
||||||
|
デスクトップアプリのリポジトリ [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop)。
|
||||||
|
|
||||||
|
マルチシステムアーキテクチャをサポートし、インストールしてすぐに使用可能。初心者や手軽さを求める人に最適なワンクリックデスクトップデプロイソリューションです。サーバー環境での使用は推奨されません。
|
||||||
|
|
||||||
|
#### ランチャーによるワンクリックデプロイ(AstrBot Launcher)
|
||||||
|
|
||||||
|
迅速なデプロイとマルチインスタンス対応、環境の隔離が可能。[AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) リポジトリにアクセスし、Releases ページから最新バージョンのシステム対応パッケージをダウンロードしてインストールしてください。
|
||||||
|
|
||||||
#### 宝塔パネルデプロイ
|
#### 宝塔パネルデプロイ
|
||||||
|
|
||||||
AstrBot は宝塔パネルと提携し、宝塔パネルに公開されています。
|
AstrBot は宝塔パネルと提携し、宝塔パネルに公開されています。
|
||||||
@@ -78,6 +107,8 @@ AstrBot は 1Panel 公式により 1Panel パネルに公開されています
|
|||||||
|
|
||||||
#### 雨云でのデプロイ
|
#### 雨云でのデプロイ
|
||||||
|
|
||||||
|
For Chinese users:
|
||||||
|
|
||||||
AstrBot は雨云公式によりクラウドアプリケーションプラットフォームに公開され、ワンクリックでデプロイ可能です。
|
AstrBot は雨云公式によりクラウドアプリケーションプラットフォームに公開され、ワンクリックでデプロイ可能です。
|
||||||
|
|
||||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||||
@@ -115,73 +146,74 @@ uv run main.py
|
|||||||
|
|
||||||
または、公式ドキュメント [ソースコードから AstrBot をデプロイ](https://astrbot.app/deploy/astrbot/cli.html) をご参照ください。
|
または、公式ドキュメント [ソースコードから AstrBot をデプロイ](https://astrbot.app/deploy/astrbot/cli.html) をご参照ください。
|
||||||
|
|
||||||
|
#### システムパッケージマネージャーでのインストール
|
||||||
|
|
||||||
|
##### Arch Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yay -S astrbot-git
|
||||||
|
# または paru を使用
|
||||||
|
paru -S astrbot-git
|
||||||
|
```
|
||||||
|
|
||||||
## サポートされているメッセージプラットフォーム
|
## サポートされているメッセージプラットフォーム
|
||||||
|
|
||||||
**公式メンテナンス**
|
AstrBot をよく使うチャットプラットフォームに接続できます。
|
||||||
|
|
||||||
- QQ (公式プラットフォーム & OneBot)
|
| プラットフォーム | 保守 |
|
||||||
- Telegram
|
|---------|---------------|
|
||||||
- WeChat Work アプリケーション & WeChat Work インテリジェントボット
|
| QQ | 公式 |
|
||||||
- WeChat カスタマーサービス & WeChat 公式アカウント
|
| OneBot v11 プロトコル実装 | 公式 |
|
||||||
- Feishu (Lark)
|
| Telegram | 公式 |
|
||||||
- DingTalk
|
| WeChat Work アプリケーション & WeChat Work インテリジェントボット | 公式 |
|
||||||
- Slack
|
| WeChat カスタマーサービス & WeChat 公式アカウント | 公式 |
|
||||||
- Discord
|
| Feishu (Lark) | 公式 |
|
||||||
- Satori
|
| DingTalk | 公式 |
|
||||||
- Misskey
|
| Slack | 公式 |
|
||||||
- WhatsApp (近日対応予定)
|
| Discord | 公式 |
|
||||||
- LINE (近日対応予定)
|
| LINE | 公式 |
|
||||||
|
| Satori | 公式 |
|
||||||
**コミュニティメンテナンス**
|
| Misskey | 公式 |
|
||||||
|
| WhatsApp (近日対応予定) | 公式 |
|
||||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | コミュニティ |
|
||||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | コミュニティ |
|
||||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | コミュニティ |
|
||||||
|
|
||||||
|
|
||||||
## サポートされているモデルサービス
|
## サポートされているモデルサービス
|
||||||
|
|
||||||
**大規模言語モデルサービス**
|
| サービス | 種類 |
|
||||||
|
|---------|---------------|
|
||||||
- OpenAI および互換サービス
|
| OpenAI および互換サービス | 大規模言語モデルサービス |
|
||||||
- Anthropic
|
| Anthropic | 大規模言語モデルサービス |
|
||||||
- Google Gemini
|
| Google Gemini | 大規模言語モデルサービス |
|
||||||
- Moonshot AI
|
| Moonshot AI | 大規模言語モデルサービス |
|
||||||
- 智谱 AI
|
| 智谱 AI | 大規模言語モデルサービス |
|
||||||
- DeepSeek
|
| DeepSeek | 大規模言語モデルサービス |
|
||||||
- Ollama (セルフホスト)
|
| Ollama (セルフホスト) | 大規模言語モデルサービス |
|
||||||
- LM Studio (セルフホスト)
|
| LM Studio (セルフホスト) | 大規模言語モデルサービス |
|
||||||
- [優云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
| [優云智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | 大規模言語モデルサービス |
|
||||||
- [302.AI](https://share.302.ai/rr1M3l)
|
| [302.AI](https://share.302.ai/rr1M3l) | 大規模言語モデルサービス |
|
||||||
- [小馬算力](https://www.tokenpony.cn/3YPyf)
|
| [小馬算力](https://www.tokenpony.cn/3YPyf) | 大規模言語モデルサービス |
|
||||||
- [硅基流動](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot)
|
| [硅基流動](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | 大規模言語モデルサービス |
|
||||||
- [PPIO 派欧云](https://ppio.com/user/register?invited_by=AIOONE)
|
| [PPIO 派欧云](https://ppio.com/user/register?invited_by=AIOONE) | 大規模言語モデルサービス |
|
||||||
- ModelScope
|
| ModelScope | 大規模言語モデルサービス |
|
||||||
- OneAPI
|
| OneAPI | 大規模言語モデルサービス |
|
||||||
|
| Dify | LLMOps プラットフォーム |
|
||||||
**LLMOps プラットフォーム**
|
| Alibaba Cloud 百炼アプリケーション | LLMOps プラットフォーム |
|
||||||
|
| Coze | LLMOps プラットフォーム |
|
||||||
- Dify
|
| OpenAI Whisper | 音声認識サービス |
|
||||||
- Alibaba Cloud 百炼アプリケーション
|
| SenseVoice | 音声認識サービス |
|
||||||
- Coze
|
| OpenAI TTS | 音声合成サービス |
|
||||||
|
| Gemini TTS | 音声合成サービス |
|
||||||
**音声認識サービス**
|
| GPT-Sovits-Inference | 音声合成サービス |
|
||||||
|
| GPT-Sovits | 音声合成サービス |
|
||||||
- OpenAI Whisper
|
| FishAudio | 音声合成サービス |
|
||||||
- SenseVoice
|
| Edge TTS | 音声合成サービス |
|
||||||
|
| Alibaba Cloud 百炼 TTS | 音声合成サービス |
|
||||||
**音声合成サービス**
|
| Azure TTS | 音声合成サービス |
|
||||||
|
| Minimax TTS | 音声合成サービス |
|
||||||
- OpenAI TTS
|
| Volcano Engine TTS | 音声合成サービス |
|
||||||
- Gemini TTS
|
|
||||||
- GPT-Sovits-Inference
|
|
||||||
- GPT-Sovits
|
|
||||||
- FishAudio
|
|
||||||
- Edge TTS
|
|
||||||
- Alibaba Cloud 百炼 TTS
|
|
||||||
- Azure TTS
|
|
||||||
- Minimax TTS
|
|
||||||
- Volcano Engine TTS
|
|
||||||
|
|
||||||
## ❤️ コントリビューション
|
## ❤️ コントリビューション
|
||||||
|
|
||||||
@@ -224,7 +256,7 @@ pre-commit install
|
|||||||
AstrBot への貢献をしていただいたすべてのコントリビューターとプラグイン開発者に特別な感謝を ❤️
|
AstrBot への貢献をしていただいたすべてのコントリビューターとプラグイン開発者に特別な感謝を ❤️
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
また、このプロジェクトの誕生は以下のオープンソースプロジェクトの助けなしには実現できませんでした:
|
また、このプロジェクトの誕生は以下のオープンソースプロジェクトの助けなしには実現できませんでした:
|
||||||
@@ -242,6 +274,12 @@ AstrBot への貢献をしていただいたすべてのコントリビュータ
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</details>
|
<div align="center">
|
||||||
|
|
||||||
|
_共感力と能力は決して対立するものではありません。私たちが目指すのは、感情を理解し、心の支えとなるだけでなく、確実に仕事をこなせるロボットの創造です。_
|
||||||
|
|
||||||
_私は、高性能ですから!_
|
_私は、高性能ですから!_
|
||||||
|
|
||||||
|
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
+123
-85
@@ -1,9 +1,13 @@
|
|||||||

|

|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh.md">中文</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">English</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -14,22 +18,17 @@
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<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/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?style=for-the-badge&color=76bad9" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
||||||
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?style=for-the-badge&color=76bad9"/></a>
|
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
|
||||||
<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://zread.ai/AstrBotDevs/AstrBot" target="_blank"><img src="https://img.shields.io/badge/Ask_Zread-_.svg?style=flat&color=00b0aa&labelColor=000000&logo=data%3Aimage%2Fsvg%2Bxml%3Bbase64%2CPHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTQuOTYxNTYgMS42MDAxSDIuMjQxNTZDMS44ODgxIDEuNjAwMSAxLjYwMTU2IDEuODg2NjQgMS42MDE1NiAyLjI0MDFWNC45NjAxQzEuNjAxNTYgNS4zMTM1NiAxLjg4ODEgNS42MDAxIDIuMjQxNTYgNS42MDAxSDQuOTYxNTZDNS4zMTUwMiA1LjYwMDEgNS42MDE1NiA1LjMxMzU2IDUuNjAxNTYgNC45NjAxVjIuMjQwMUM1LjYwMTU2IDEuODg2NjQgNS4zMTUwMiAxLjYwMDEgNC45NjE1NiAxLjYwMDFZIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00Ljk2MTU2IDEwLjM5OTlIMi4yNDE1NkMxLjg4ODEgMTAuMzk5OSAxLjYwMTU2IDEwLjY4NjQgMS42MDE1NiAxMS4wMzk5VjEzLjc1OTlDMS42MDE1NiAxNC4xMTM0IDEuODg4MSAxNC4zOTk5IDIuMjQxNTYgMTQuMzk5OUg0Ljk2MTU2QzUuMzE1MDIgMTQuMzk5OSA1LjYwMTU2IDE0LjExMzQgNS42MDE1NiAxMy43NTk5VjExLjAzOTlDNS42MDE1NiAxMC42ODY0IDUuMzE1MDIgMTAuMzk5OSA0Ljk2MTU2IDEwLjM5OTlaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik0xMy43NTg0IDEuNjAwMUgxMS4wMzg0QzEwLjY4NSAxLjYwMDEgMTAuMzk4NCAxLjg4NjY0IDEwLjM5ODQgMi4yNDAxVjQuOTYwMUMxMC4zOTg0IDUuMzEzNTYgMTAuNjg1IDUuNjAwMSAxMS4wMzg0IDUuNjAwMUgxMy43NTg0QzE0LjExMTkgNS42MDAxIDE0LjM5ODQgNS4zMTM1NiAxNC4zOTg0IDQuOTYwMVYyLjI0MDFDMTQuMzk4NCAxLjg4NjY0IDE0LjExMTkgMS42MDAxIDEzLjczODQgMS42MDAxWiIgZmlsbD0iI2ZmZiIvPgo8cGF0aCBkPSJNNCAxMkwxMiA0TDQgMTJaIiBmaWxsPSIjZmZmIi8%2BCjxwYXRoIGQ9Ik00IDEyTDEyIDQiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K&logoColor=ffffff" alt="zread"/></a>
|
||||||
<a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?color=76bad9"/></a>
|
||||||
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20%D0%BF%D0%BB%D0%B0%D0%B3%D0%B8%D0%BD%D0%BE%D0%B2&style=for-the-badge&label=%D0%9C%D0%B0%D0%B3%D0%B0%D0%B7%D0%B8%D0%BD&cacheSeconds=3600">
|
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%20&label=%D0%9C%D0%B0%D1%80%D0%BA%D0%B5%D1%82%D0%BF%D0%BB%D0%B5%D0%B9%D1%81&cacheSeconds=3600">
|
||||||
|
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">中文</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh-TW.md">繁體中文</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a>
|
|
||||||
|
|
||||||
<a href="https://astrbot.app/">Документация</a> |
|
<a href="https://astrbot.app/">Документация</a> |
|
||||||
<a href="https://blog.astrbot.app/">Блог</a> |
|
<a href="https://blog.astrbot.app/">Блог</a> |
|
||||||
<a href="https://astrbot.featurebase.app/roadmap">Дорожная карта</a> |
|
<a href="https://astrbot.featurebase.app/roadmap">Дорожная карта</a> |
|
||||||
@@ -38,17 +37,36 @@
|
|||||||
|
|
||||||
AstrBot — это универсальная платформа Agent-чатботов с открытым исходным кодом, которая интегрируется с основными приложениями для обмена мгновенными сообщениями. Она предоставляет надёжную и масштабируемую инфраструктуру разговорного ИИ для частных лиц, разработчиков и команд. Будь то персональный ИИ-компаньон, интеллектуальная служба поддержки, автоматизированный помощник или корпоративная база знаний — AstrBot позволяет быстро создавать готовые к использованию ИИ-приложения в рабочих процессах вашей платформы обмена сообщениями.
|
AstrBot — это универсальная платформа Agent-чатботов с открытым исходным кодом, которая интегрируется с основными приложениями для обмена мгновенными сообщениями. Она предоставляет надёжную и масштабируемую инфраструктуру разговорного ИИ для частных лиц, разработчиков и команд. Будь то персональный ИИ-компаньон, интеллектуальная служба поддержки, автоматизированный помощник или корпоративная база знаний — AstrBot позволяет быстро создавать готовые к использованию ИИ-приложения в рабочих процессах вашей платформы обмена сообщениями.
|
||||||
|
|
||||||
<img width="1776" height="1080" alt="image" src="https://github.com/user-attachments/assets/00782c4c-4437-4d97-aabc-605e3738da5c" />
|

|
||||||
|
|
||||||
## Основные возможности
|
## Основные возможности
|
||||||
|
|
||||||
1. 💯 Бесплатно и с открытым исходным кодом.
|
1. 💯 Бесплатно & Открытый исходный код.
|
||||||
2. ✨ ИИ-диалоги с LLM, мультимодальность, Agent, MCP, база знаний, настройки личности.
|
2. ✨ Диалоги с ИИ-моделями, мультимодальность, Agent, MCP, Skills, База знаний, Настройка личности, автоматическое сжатие диалогов.
|
||||||
3. 🤖 Поддержка интеграции с Dify, Alibaba Cloud Bailian, Coze и другими платформами агентов.
|
3. 🤖 Поддержка интеграции с платформами Agents, такими как Dify, Alibaba Cloud Bailian, Coze и др.
|
||||||
4. 🌐 Мультиплатформенность: QQ, WeChat Work, Feishu, DingTalk, официальные аккаунты WeChat, Telegram, Slack и [другие](#поддерживаемые-платформы-обмена-сообщениями).
|
4. 🌐 Мультиплатформенность: поддержка QQ, WeChat для предприятий, Feishu, DingTalk, публичных аккаунтов WeChat, Telegram, Slack и [других](#Поддерживаемые-платформы-обмена-сообщениями).
|
||||||
5. 📦 Расширения плагинов с почти 800 плагинами, доступными для установки в один клик.
|
5. 📦 Расширение плагинами: доступно более 1000 плагинов для установки в один клик.
|
||||||
6. 💻 Поддержка WebUI.
|
6. 🛡️ Изолированная среда[Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html): безопасное выполнение любого кода, вызов Shell, повторное использование ресурсов на уровне сессии.
|
||||||
7. 🌐 Поддержка интернационализации (i18n).
|
7. 💻 Поддержка WebUI.
|
||||||
|
8. 🌈 Поддержка Web ChatUI: встроенная песочница агента, веб-поиск и др.
|
||||||
|
9. 🌐 Поддержка интернационализации (i18n).
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr align="center">
|
||||||
|
<th>💙 Ролевые игры & Эмоциональная поддержка</th>
|
||||||
|
<th>✨ Проактивный Агент (Agent)</th>
|
||||||
|
<th>🚀 Универсальные возможности Агента</th>
|
||||||
|
<th>🧩 1000+ плагинов сообщества</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
## Быстрый старт
|
## Быстрый старт
|
||||||
|
|
||||||
@@ -61,9 +79,20 @@ AstrBot — это универсальная платформа Agent-чатб
|
|||||||
#### Развёртывание uv
|
#### Развёртывание uv
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx astrbot
|
uv tool install astrbot
|
||||||
|
astrbot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Десктопное приложение (Tauri)
|
||||||
|
|
||||||
|
Репозиторий десктопного приложения: [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop).
|
||||||
|
|
||||||
|
Поддерживает различные системные архитектуры, устанавливается напрямую, "из коробки", лучшее настольное решение в один клик для новичков и тех, кто ценит простоту. Не рекомендуется для серверных сценариев.
|
||||||
|
|
||||||
|
#### Установка в один клик через лаунчер (AstrBot Launcher)
|
||||||
|
|
||||||
|
Быстрое развёртывание и поддержка нескольких экземпляров, изоляция среды. Перейдите в репозиторий [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher), найдите последнюю версию на странице Releases и установите соответствующий пакет для вашей системы.
|
||||||
|
|
||||||
#### Развёртывание BT-Panel
|
#### Развёртывание BT-Panel
|
||||||
|
|
||||||
AstrBot в партнёрстве с BT-Panel теперь доступен на их маркетплейсе.
|
AstrBot в партнёрстве с BT-Panel теперь доступен на их маркетплейсе.
|
||||||
@@ -78,6 +107,8 @@ AstrBot официально размещён на маркетплейсе 1Pan
|
|||||||
|
|
||||||
#### Развёртывание на RainYun
|
#### Развёртывание на RainYun
|
||||||
|
|
||||||
|
For Chinese users:
|
||||||
|
|
||||||
AstrBot официально размещён на облачной платформе приложений RainYun с развёртыванием в один клик.
|
AstrBot официально размещён на облачной платформе приложений RainYun с развёртыванием в один клик.
|
||||||
|
|
||||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||||
@@ -115,72 +146,73 @@ uv run main.py
|
|||||||
|
|
||||||
Или см. официальную документацию: [Развёртывание AstrBot из исходного кода](https://astrbot.app/deploy/astrbot/cli.html).
|
Или см. официальную документацию: [Развёртывание AstrBot из исходного кода](https://astrbot.app/deploy/astrbot/cli.html).
|
||||||
|
|
||||||
|
#### Установка через системный пакетный менеджер
|
||||||
|
|
||||||
|
##### Arch Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yay -S astrbot-git
|
||||||
|
# или используйте paru
|
||||||
|
paru -S astrbot-git
|
||||||
|
```
|
||||||
|
|
||||||
## Поддерживаемые платформы обмена сообщениями
|
## Поддерживаемые платформы обмена сообщениями
|
||||||
|
|
||||||
**Официально поддерживаемые**
|
Подключите AstrBot к вашим любимым чат-платформам.
|
||||||
|
|
||||||
- QQ (Официальная платформа и OneBot)
|
| Платформа | Поддержка |
|
||||||
- Telegram
|
|---------|---------------|
|
||||||
- Приложение WeChat Work и интеллектуальный бот WeChat Work
|
| QQ | Официальная |
|
||||||
- Служба поддержки WeChat и официальные аккаунты WeChat
|
| Реализация протокола OneBot v11 | Официальная |
|
||||||
- Feishu (Lark)
|
| Telegram | Официальная |
|
||||||
- DingTalk
|
| Приложение WeChat Work и интеллектуальный бот WeChat Work | Официальная |
|
||||||
- Slack
|
| Служба поддержки WeChat и официальные аккаунты WeChat | Официальная |
|
||||||
- Discord
|
| Feishu (Lark) | Официальная |
|
||||||
- Satori
|
| DingTalk | Официальная |
|
||||||
- Misskey
|
| Slack | Официальная |
|
||||||
- WhatsApp (Скоро)
|
| Discord | Официальная |
|
||||||
- LINE (Скоро)
|
| LINE | Официальная |
|
||||||
|
| Satori | Официальная |
|
||||||
**Поддерживаемые сообществом**
|
| Misskey | Официальная |
|
||||||
|
| WhatsApp (Скоро) | Официальная |
|
||||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | Сообщество |
|
||||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | Сообщество |
|
||||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | Сообщество |
|
||||||
|
|
||||||
## Поддерживаемые сервисы моделей
|
## Поддерживаемые сервисы моделей
|
||||||
|
|
||||||
**Сервисы LLM**
|
| Сервис | Тип |
|
||||||
|
|---------|---------------|
|
||||||
- OpenAI и совместимые сервисы
|
| OpenAI и совместимые сервисы | Сервисы LLM |
|
||||||
- Anthropic
|
| Anthropic | Сервисы LLM |
|
||||||
- Google Gemini
|
| Google Gemini | Сервисы LLM |
|
||||||
- Moonshot AI
|
| Moonshot AI | Сервисы LLM |
|
||||||
- Zhipu AI
|
| Zhipu AI | Сервисы LLM |
|
||||||
- DeepSeek
|
| DeepSeek | Сервисы LLM |
|
||||||
- Ollama (Самостоятельное размещение)
|
| Ollama (Самостоятельное размещение) | Сервисы LLM |
|
||||||
- LM Studio (Самостоятельное размещение)
|
| LM Studio (Самостоятельное размещение) | Сервисы LLM |
|
||||||
- [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
| [CompShare](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | Сервисы LLM |
|
||||||
- [302.AI](https://share.302.ai/rr1M3l)
|
| [302.AI](https://share.302.ai/rr1M3l) | Сервисы LLM |
|
||||||
- [TokenPony](https://www.tokenpony.cn/3YPyf)
|
| [TokenPony](https://www.tokenpony.cn/3YPyf) | Сервисы LLM |
|
||||||
- [SiliconFlow](https://docs.siliconflow.cn/cn/usecases/use-siliconcloud-in-astrbot)
|
| [SiliconFlow](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | Сервисы LLM |
|
||||||
- [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE)
|
| [PPIO Cloud](https://ppio.com/user/register?invited_by=AIOONE) | Сервисы LLM |
|
||||||
- ModelScope
|
| ModelScope | Сервисы LLM |
|
||||||
- OneAPI
|
| OneAPI | Сервисы LLM |
|
||||||
|
| Dify | Платформы LLMOps |
|
||||||
**Платформы LLMOps**
|
| Приложения Alibaba Cloud Bailian | Платформы LLMOps |
|
||||||
|
| Coze | Платформы LLMOps |
|
||||||
- Dify
|
| OpenAI Whisper | Сервисы распознавания речи |
|
||||||
- Приложения Alibaba Cloud Bailian
|
| SenseVoice | Сервисы распознавания речи |
|
||||||
- Coze
|
| OpenAI TTS | Сервисы синтеза речи |
|
||||||
|
| Gemini TTS | Сервисы синтеза речи |
|
||||||
**Сервисы распознавания речи**
|
| GPT-Sovits-Inference | Сервисы синтеза речи |
|
||||||
|
| GPT-Sovits | Сервисы синтеза речи |
|
||||||
- OpenAI Whisper
|
| FishAudio | Сервисы синтеза речи |
|
||||||
- SenseVoice
|
| Edge TTS | Сервисы синтеза речи |
|
||||||
|
| Alibaba Cloud Bailian TTS | Сервисы синтеза речи |
|
||||||
**Сервисы синтеза речи**
|
| Azure TTS | Сервисы синтеза речи |
|
||||||
|
| Minimax TTS | Сервисы синтеза речи |
|
||||||
- OpenAI TTS
|
| Volcano Engine TTS | Сервисы синтеза речи |
|
||||||
- Gemini TTS
|
|
||||||
- GPT-Sovits-Inference
|
|
||||||
- GPT-Sovits
|
|
||||||
- FishAudio
|
|
||||||
- Edge TTS
|
|
||||||
- Alibaba Cloud Bailian TTS
|
|
||||||
- Azure TTS
|
|
||||||
- Minimax TTS
|
|
||||||
- Volcano Engine TTS
|
|
||||||
|
|
||||||
## ❤️ Вклад в проект
|
## ❤️ Вклад в проект
|
||||||
|
|
||||||
@@ -223,7 +255,7 @@ pre-commit install
|
|||||||
Особая благодарность всем контрибьюторам и разработчикам плагинов за их вклад в AstrBot ❤️
|
Особая благодарность всем контрибьюторам и разработчикам плагинов за их вклад в AstrBot ❤️
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
Кроме того, рождение этого проекта было бы невозможно без помощи следующих проектов с открытым исходным кодом:
|
Кроме того, рождение этого проекта было бы невозможно без помощи следующих проектов с открытым исходным кодом:
|
||||||
@@ -235,13 +267,19 @@ pre-commit install
|
|||||||
> [!TIP]
|
> [!TIP]
|
||||||
> Если этот проект помог вам в жизни или работе, или если вас интересует его будущее развитие, пожалуйста, поставьте проекту звезду. Это движущая сила поддержки этого проекта с открытым исходным кодом <3
|
> Если этот проект помог вам в жизни или работе, или если вас интересует его будущее развитие, пожалуйста, поставьте проекту звезду. Это движущая сила поддержки этого проекта с открытым исходным кодом <3
|
||||||
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://star-history.com/#astrbotdevs/astrbot&Date)
|
[](https://star-history.com/#astrbotdevs/astrbot&Date)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</details>
|
<div align="center">
|
||||||
|
|
||||||
|
_Сопровождение и способности никогда не должны быть противоположностями. Мы стремимся создать робота, который сможет как понимать эмоции, оказывать душевную поддержку, так и надёжно выполнять работу._
|
||||||
|
|
||||||
_私は、高性能ですから!_
|
_私は、高性能ですから!_
|
||||||
|
|
||||||
|
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
+121
-84
@@ -1,9 +1,13 @@
|
|||||||

|

|
||||||
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_zh.md">简体中文</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">English</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
||||||
|
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -14,22 +18,17 @@
|
|||||||
<br>
|
<br>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<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/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?style=for-the-badge&color=76bad9" alt="python">
|
<img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="python">
|
||||||
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?style=for-the-badge&color=76bad9"/></a>
|
<img src="https://deepwiki.com/badge.svg" href="https://deepwiki.com/AstrBotDevs/AstrBot">
|
||||||
<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://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://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
<a href="https://hub.docker.com/r/soulter/astrbot"><img alt="Docker pull" src="https://img.shields.io/docker/pulls/soulter/astrbot.svg?color=76bad9"/></a>
|
||||||
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E5%80%8B&style=for-the-badge&label=%E6%8F%92%E4%BB%B6%E5%B8%82%E5%A0%B4&cacheSeconds=3600">
|
<img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fapi.soulter.top%2Fastrbot%2Fplugin-num&query=%24.result&suffix=%E5%80%8B&label=%E6%8F%92%E4%BB%B6%E5%B8%82%E5%A0%B4&cacheSeconds=3600">
|
||||||
|
<img src="https://gitcode.com/Soulter/AstrBot/star/badge.svg" href="https://gitcode.com/Soulter/AstrBot">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README.md">简体中文</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_en.md">English</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ja.md">日本語</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_fr.md">Français</a> |
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/blob/master/README_ru.md">Русский</a>
|
|
||||||
|
|
||||||
<a href="https://astrbot.app/">文件</a> |
|
<a href="https://astrbot.app/">文件</a> |
|
||||||
<a href="https://blog.astrbot.app/">Blog</a> |
|
<a href="https://blog.astrbot.app/">Blog</a> |
|
||||||
<a href="https://astrbot.featurebase.app/roadmap">路線圖</a> |
|
<a href="https://astrbot.featurebase.app/roadmap">路線圖</a> |
|
||||||
@@ -38,17 +37,36 @@
|
|||||||
|
|
||||||
AstrBot 是一個開源的一站式 Agent 聊天機器人平台,可接入主流即時通訊軟體,為個人、開發者和團隊打造可靠、可擴展的對話式智慧基礎設施。無論是個人 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. 💯 免費 & 開源。
|
1. 💯 免費 & 開源。
|
||||||
2. ✨ AI 大型模型對話,多模態,Agent,MCP,知識庫,人格設定。
|
2. ✨ AI 大模型對話,多模態,Agent,MCP,Skills,知識庫,人格設定,自動壓縮對話。
|
||||||
3. 🤖 支援接入 Dify、阿里雲百煉、Coze 等智慧體平台。
|
3. 🤖 支援接入 Dify、阿里雲百煉、Coze 等智慧體 (Agent) 平台。
|
||||||
4. 🌐 多平台:QQ、企業微信、飛書、釘釘、微信公眾號、Telegram、Slack 以及[更多](#支援的訊息平台)。
|
4. 🌐 多平台,支援 QQ、企業微信、飛書、釘釘、微信公眾號、Telegram、Slack 以及[更多](#支援的訊息平台)。
|
||||||
5. 📦 外掛擴充,已有近 800 個外掛可一鍵安裝。
|
5. 📦 插件擴展,已有 1000+ 個插件可一鍵安裝。
|
||||||
6. 💻 WebUI 支援。
|
6. 🛡️ [Agent Sandbox](https://docs.astrbot.app/use/astrbot-agent-sandbox.html) 隔離化環境,安全地執行任何代碼、調用 Shell、會話級資源複用。
|
||||||
7. 🌐 國際化(i18n)支援。
|
7. 💻 WebUI 支援。
|
||||||
|
8. 🌈 Web ChatUI 支援,ChatUI 內置代理沙盒 (Agent Sandbox)、網頁搜尋等。
|
||||||
|
9. 🌐 國際化(i18n)支援。
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<table align="center">
|
||||||
|
<tr align="center">
|
||||||
|
<th>💙 角色扮演 & 情感陪伴</th>
|
||||||
|
<th>✨ 主動式 Agent</th>
|
||||||
|
<th>🚀 通用 Agentic 能力</th>
|
||||||
|
<th>🧩 1000+ 社區外掛程式</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center"><p align="center"><img width="984" height="1746" alt="99b587c5d35eea09d84f33e6cf6cfd4f" src="https://github.com/user-attachments/assets/89196061-3290-458d-b51f-afa178049f84" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="976" height="1612" alt="c449acd838c41d0915cc08a3824025b1" src="https://github.com/user-attachments/assets/f75368b4-e022-41dc-a9e0-131c3e73e32e" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="974" height="1732" alt="image" src="https://github.com/user-attachments/assets/e22a3968-87d7-4708-a7cd-e7f198c7c32e" /></p></td>
|
||||||
|
<td align="center"><p align="center"><img width="976" height="1734" alt="image" src="https://github.com/user-attachments/assets/0952b395-6b4a-432a-8a50-c294b7f89750" /></p></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
## 快速開始
|
## 快速開始
|
||||||
|
|
||||||
@@ -61,9 +79,20 @@ AstrBot 是一個開源的一站式 Agent 聊天機器人平台,可接入主
|
|||||||
#### uv 部署
|
#### uv 部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uvx astrbot
|
uv tool install astrbot
|
||||||
|
astrbot
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 桌面應用部署(Tauri)
|
||||||
|
|
||||||
|
桌面應用倉庫 [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop)。
|
||||||
|
|
||||||
|
支援多系統架構,安裝包直接安裝,開箱即用,最適合新手和懶人的一鍵桌面部署方案,不推薦伺服器場景。
|
||||||
|
|
||||||
|
#### 啟動器一鍵部署(AstrBot Launcher)
|
||||||
|
|
||||||
|
快速部署和多開方案,實現環境隔離,進入 [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) 倉庫,在 Releases 頁最新版本下找到對應的系統安裝包安裝即可。
|
||||||
|
|
||||||
#### 寶塔面板部署
|
#### 寶塔面板部署
|
||||||
|
|
||||||
AstrBot 與寶塔面板合作,已上架至寶塔面板。
|
AstrBot 與寶塔面板合作,已上架至寶塔面板。
|
||||||
@@ -78,6 +107,8 @@ AstrBot 已由 1Panel 官方上架至 1Panel 面板。
|
|||||||
|
|
||||||
#### 在雨雲上部署
|
#### 在雨雲上部署
|
||||||
|
|
||||||
|
For Chinese users:
|
||||||
|
|
||||||
AstrBot 已由雨雲官方上架至雲端應用程式平台,可一鍵部署。
|
AstrBot 已由雨雲官方上架至雲端應用程式平台,可一鍵部署。
|
||||||
|
|
||||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||||
@@ -115,72 +146,73 @@ uv run main.py
|
|||||||
|
|
||||||
或者請參閱官方文件 [透過原始碼部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html)。
|
或者請參閱官方文件 [透過原始碼部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html)。
|
||||||
|
|
||||||
|
#### 系統套件管理員安裝
|
||||||
|
|
||||||
|
##### Arch Linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yay -S astrbot-git
|
||||||
|
# 或者使用 paru
|
||||||
|
paru -S astrbot-git
|
||||||
|
```
|
||||||
|
|
||||||
## 支援的訊息平台
|
## 支援的訊息平台
|
||||||
|
|
||||||
**官方維護**
|
將 AstrBot 連接到你常用的聊天平台。
|
||||||
|
|
||||||
- QQ(官方平台 & OneBot)
|
| 平台 | 維護方 |
|
||||||
- Telegram
|
|---------|---------------|
|
||||||
- 企微應用 & 企微智慧機器人
|
| QQ | 官方維護 |
|
||||||
- 微信客服 & 微信公眾號
|
| OneBot v11 協議實作 | 官方維護 |
|
||||||
- 飛書
|
| Telegram | 官方維護 |
|
||||||
- 釘釘
|
| 企微應用 & 企微智慧機器人 | 官方維護 |
|
||||||
- Slack
|
| 微信客服 & 微信公眾號 | 官方維護 |
|
||||||
- Discord
|
| 飛書 | 官方維護 |
|
||||||
- Satori
|
| 釘釘 | 官方維護 |
|
||||||
- Misskey
|
| Slack | 官方維護 |
|
||||||
- Whatsapp(即將支援)
|
| Discord | 官方維護 |
|
||||||
- LINE(即將支援)
|
| LINE | 官方維護 |
|
||||||
|
| Satori | 官方維護 |
|
||||||
**社群維護**
|
| Misskey | 官方維護 |
|
||||||
|
| Whatsapp(即將支援) | 官方維護 |
|
||||||
- [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter)
|
| [Matrix](https://github.com/stevessr/astrbot_plugin_matrix_adapter) | 社群維護 |
|
||||||
- [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter)
|
| [KOOK](https://github.com/wuyan1003/astrbot_plugin_kook_adapter) | 社群維護 |
|
||||||
- [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat)
|
| [VoceChat](https://github.com/HikariFroya/astrbot_plugin_vocechat) | 社群維護 |
|
||||||
|
|
||||||
## 支援的模型服務
|
## 支援的模型服務
|
||||||
|
|
||||||
**大型模型服務**
|
| 服務 | 類型 |
|
||||||
|
|---------|---------------|
|
||||||
- OpenAI 及相容服務
|
| OpenAI 及相容服務 | 大型模型服務 |
|
||||||
- Anthropic
|
| Anthropic | 大型模型服務 |
|
||||||
- Google Gemini
|
| Google Gemini | 大型模型服務 |
|
||||||
- Moonshot AI
|
| Moonshot AI | 大型模型服務 |
|
||||||
- 智譜 AI
|
| 智譜 AI | 大型模型服務 |
|
||||||
- DeepSeek
|
| DeepSeek | 大型模型服務 |
|
||||||
- Ollama(本機部署)
|
| Ollama(本機部署) | 大型模型服務 |
|
||||||
- LM Studio(本機部署)
|
| LM Studio(本機部署) | 大型模型服務 |
|
||||||
- [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74)
|
| [優雲智算](https://www.compshare.cn/?ytag=GPU_YY-gh_astrbot&referral_code=FV7DcGowN4hB5UuXKgpE74) | 大型模型服務 |
|
||||||
- [302.AI](https://share.302.ai/rr1M3l)
|
| [302.AI](https://share.302.ai/rr1M3l) | 大型模型服務 |
|
||||||
- [小馬算力](https://www.tokenpony.cn/3YPyf)
|
| [小馬算力](https://www.tokenpony.cn/3YPyf) | 大型模型服務 |
|
||||||
- [矽基流動](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot)
|
| [矽基流動](https://docs.siliconflow.cn/cn/usercases/use-siliconcloud-in-astrbot) | 大型模型服務 |
|
||||||
- [PPIO 派歐雲](https://ppio.com/user/register?invited_by=AIOONE)
|
| [PPIO 派歐雲](https://ppio.com/user/register?invited_by=AIOONE) | 大型模型服務 |
|
||||||
- ModelScope
|
| ModelScope | 大型模型服務 |
|
||||||
- OneAPI
|
| OneAPI | 大型模型服務 |
|
||||||
|
| Dify | LLMOps 平台 |
|
||||||
**LLMOps 平台**
|
| 阿里雲百煉應用 | LLMOps 平台 |
|
||||||
|
| Coze | LLMOps 平台 |
|
||||||
- Dify
|
| OpenAI Whisper | 語音轉文字服務 |
|
||||||
- 阿里雲百煉應用
|
| SenseVoice | 語音轉文字服務 |
|
||||||
- Coze
|
| OpenAI TTS | 文字轉語音服務 |
|
||||||
|
| Gemini TTS | 文字轉語音服務 |
|
||||||
**語音轉文字服務**
|
| GPT-Sovits-Inference | 文字轉語音服務 |
|
||||||
|
| GPT-Sovits | 文字轉語音服務 |
|
||||||
- OpenAI Whisper
|
| FishAudio | 文字轉語音服務 |
|
||||||
- SenseVoice
|
| Edge TTS | 文字轉語音服務 |
|
||||||
|
| 阿里雲百煉 TTS | 文字轉語音服務 |
|
||||||
**文字轉語音服務**
|
| Azure TTS | 文字轉語音服務 |
|
||||||
|
| Minimax TTS | 文字轉語音服務 |
|
||||||
- OpenAI TTS
|
| 火山引擎 TTS | 文字轉語音服務 |
|
||||||
- Gemini TTS
|
|
||||||
- GPT-Sovits-Inference
|
|
||||||
- GPT-Sovits
|
|
||||||
- FishAudio
|
|
||||||
- Edge TTS
|
|
||||||
- 阿里雲百煉 TTS
|
|
||||||
- Azure TTS
|
|
||||||
- Minimax TTS
|
|
||||||
- 火山引擎 TTS
|
|
||||||
|
|
||||||
## ❤️ 貢獻
|
## ❤️ 貢獻
|
||||||
|
|
||||||
@@ -223,7 +255,7 @@ pre-commit install
|
|||||||
特別感謝所有 Contributors 和外掛開發者對 AstrBot 的貢獻 ❤️
|
特別感謝所有 Contributors 和外掛開發者對 AstrBot 的貢獻 ❤️
|
||||||
|
|
||||||
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
<a href="https://github.com/AstrBotDevs/AstrBot/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot&max=200&columns=14" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
此外,本專案的誕生離不開以下開源專案的幫助:
|
此外,本專案的誕生離不開以下開源專案的幫助:
|
||||||
@@ -241,7 +273,12 @@ pre-commit install
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</details>
|
<div align="center">
|
||||||
|
|
||||||
|
_陪伴與能力從來不應該是對立面。我們希望創造的是一個既能理解情緒、給予陪伴,也能可靠完成工作的機器人。_
|
||||||
|
|
||||||
_私は、高性能ですから!_
|
_私は、高性能ですから!_
|
||||||
|
|
||||||
|
<img src="https://files.astrbot.app/watashiwa-koseino-desukara.gif" width="100"/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|||||||
+252
@@ -0,0 +1,252 @@
|
|||||||
|

|
||||||
|
|
||||||
|
<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_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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 一键部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv tool install astrbot
|
||||||
|
astrbot
|
||||||
|
```
|
||||||
|
|
||||||
|
> 需要安装 [uv](https://docs.astral.sh/uv/)。
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
### 桌面客户端(Tauri)
|
||||||
|
|
||||||
|
桌面应用仓库 [AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop)。
|
||||||
|
|
||||||
|
支持多系统架构,安装包直接安装,开箱即用,最适合新手和懒人的一键桌面部署方案,不推荐服务器场景。
|
||||||
|
|
||||||
|
### 启动器一键部署(AstrBot Launcher)
|
||||||
|
|
||||||
|
快速部署和多开方案,实现环境隔离,进入 [AstrBot Launcher](https://github.com/Raven95676/astrbot-launcher) 仓库,在 Releases 页最新版本下找到对应的系统安装包安装即可。
|
||||||
|
|
||||||
|
### 在 Replit 上部署
|
||||||
|
|
||||||
|
社区贡献的部署方式。
|
||||||
|
|
||||||
|
[](https://repl.it/github/AstrBotDevs/AstrBot)
|
||||||
|
|
||||||
|
### AUR
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yay -S astrbot-git
|
||||||
|
```
|
||||||
|
|
||||||
|
**更多部署方式**:[宝塔面板](https://astrbot.app/deploy/astrbot/btpanel.html) | [1Panel](https://astrbot.app/deploy/astrbot/1panel.html) | [CasaOS](https://astrbot.app/deploy/astrbot/casaos.html) | [手动部署](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) | 社区维护 |
|
||||||
|
|
||||||
|
## 支持的模型提供商
|
||||||
|
|
||||||
|
| 提供商 | 类型 |
|
||||||
|
|---------|---------------|
|
||||||
|
| 自定义 | 任何 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 | 文本转语音 |
|
||||||
|
|
||||||
|
## ❤️ 贡献
|
||||||
|
|
||||||
|
欢迎任何 Issues/Pull Requests!只需要将你的更改提交到此项目 :)
|
||||||
|
|
||||||
|
### 如何贡献
|
||||||
|
|
||||||
|
你可以通过查看问题或帮助审核 PR(拉取请求)来贡献。任何问题或 PR 都欢迎参与,以促进社区贡献。当然,这些只是建议,你可以以任何方式进行贡献。对于新功能的添加,请先通过 Issue 讨论。
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
|
||||||
|
AstrBot 使用 `ruff` 进行代码格式化和检查。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/AstrBotDevs/AstrBot
|
||||||
|
pip install pre-commit
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌍 社区
|
||||||
|
|
||||||
|
### QQ 群组
|
||||||
|
|
||||||
|
- 1 群:322154837
|
||||||
|
- 3 群:630166526
|
||||||
|
- 5 群:822130018
|
||||||
|
- 6 群:753075035
|
||||||
|
- 7 群:743746109
|
||||||
|
- 8 群:1030353265
|
||||||
|
- 开发者群:975206796
|
||||||
|
|
||||||
|
### 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>
|
||||||
@@ -24,6 +24,9 @@ from astrbot.core.star.register import (
|
|||||||
register_on_llm_tool_respond as on_llm_tool_respond,
|
register_on_llm_tool_respond as on_llm_tool_respond,
|
||||||
)
|
)
|
||||||
from astrbot.core.star.register import register_on_platform_loaded as on_platform_loaded
|
from astrbot.core.star.register import register_on_platform_loaded as on_platform_loaded
|
||||||
|
from astrbot.core.star.register import register_on_plugin_error as on_plugin_error
|
||||||
|
from astrbot.core.star.register import register_on_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_using_llm_tool as on_using_llm_tool
|
||||||
from astrbot.core.star.register import (
|
from astrbot.core.star.register import (
|
||||||
register_on_waiting_llm_request as on_waiting_llm_request,
|
register_on_waiting_llm_request as on_waiting_llm_request,
|
||||||
@@ -52,6 +55,9 @@ __all__ = [
|
|||||||
"on_decorating_result",
|
"on_decorating_result",
|
||||||
"on_llm_request",
|
"on_llm_request",
|
||||||
"on_llm_response",
|
"on_llm_response",
|
||||||
|
"on_plugin_error",
|
||||||
|
"on_plugin_loaded",
|
||||||
|
"on_plugin_unloaded",
|
||||||
"on_platform_loaded",
|
"on_platform_loaded",
|
||||||
"on_waiting_llm_request",
|
"on_waiting_llm_request",
|
||||||
"permission_type",
|
"permission_type",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
|||||||
|
|
||||||
|
|
||||||
class LongTermMemory:
|
class LongTermMemory:
|
||||||
def __init__(self, acm: AstrBotConfigManager, context: star.Context):
|
def __init__(self, acm: AstrBotConfigManager, context: star.Context) -> None:
|
||||||
self.acm = acm
|
self.acm = acm
|
||||||
self.context = context
|
self.context = context
|
||||||
self.session_chats = defaultdict(list)
|
self.session_chats = defaultdict(list)
|
||||||
@@ -111,7 +111,7 @@ class LongTermMemory:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def handle_message(self, event: AstrMessageEvent):
|
async def handle_message(self, event: AstrMessageEvent) -> None:
|
||||||
"""仅支持群聊"""
|
"""仅支持群聊"""
|
||||||
if event.get_message_type() == MessageType.GROUP_MESSAGE:
|
if event.get_message_type() == MessageType.GROUP_MESSAGE:
|
||||||
datetime_str = datetime.datetime.now().strftime("%H:%M:%S")
|
datetime_str = datetime.datetime.now().strftime("%H:%M:%S")
|
||||||
@@ -148,7 +148,7 @@ class LongTermMemory:
|
|||||||
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
if len(self.session_chats[event.unified_msg_origin]) > cfg["max_cnt"]:
|
||||||
self.session_chats[event.unified_msg_origin].pop(0)
|
self.session_chats[event.unified_msg_origin].pop(0)
|
||||||
|
|
||||||
async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest):
|
async def on_req_llm(self, event: AstrMessageEvent, req: ProviderRequest) -> None:
|
||||||
"""当触发 LLM 请求前,调用此方法修改 req"""
|
"""当触发 LLM 请求前,调用此方法修改 req"""
|
||||||
if event.unified_msg_origin not in self.session_chats:
|
if event.unified_msg_origin not in self.session_chats:
|
||||||
return
|
return
|
||||||
@@ -171,7 +171,9 @@ class LongTermMemory:
|
|||||||
)
|
)
|
||||||
req.system_prompt += chats_str
|
req.system_prompt += chats_str
|
||||||
|
|
||||||
async def after_req_llm(self, event: AstrMessageEvent, llm_resp: LLMResponse):
|
async def after_req_llm(
|
||||||
|
self, event: AstrMessageEvent, llm_resp: LLMResponse
|
||||||
|
) -> None:
|
||||||
if event.unified_msg_origin not in self.session_chats:
|
if event.unified_msg_origin not in self.session_chats:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,9 @@ class Main(star.Star):
|
|||||||
logger.error(f"主动回复失败: {e}")
|
logger.error(f"主动回复失败: {e}")
|
||||||
|
|
||||||
@filter.on_llm_request()
|
@filter.on_llm_request()
|
||||||
async def decorate_llm_req(self, event: AstrMessageEvent, req: ProviderRequest):
|
async def decorate_llm_req(
|
||||||
|
self, event: AstrMessageEvent, req: ProviderRequest
|
||||||
|
) -> None:
|
||||||
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
|
"""在请求 LLM 前注入人格信息、Identifier、时间、回复内容等 System Prompt"""
|
||||||
if self.ltm and self.ltm_enabled(event):
|
if self.ltm and self.ltm_enabled(event):
|
||||||
try:
|
try:
|
||||||
@@ -94,7 +96,9 @@ class Main(star.Star):
|
|||||||
logger.error(f"ltm: {e}")
|
logger.error(f"ltm: {e}")
|
||||||
|
|
||||||
@filter.on_llm_response()
|
@filter.on_llm_response()
|
||||||
async def record_llm_resp_to_ltm(self, event: AstrMessageEvent, resp: LLMResponse):
|
async def record_llm_resp_to_ltm(
|
||||||
|
self, event: AstrMessageEvent, resp: LLMResponse
|
||||||
|
) -> None:
|
||||||
"""在 LLM 响应后记录对话"""
|
"""在 LLM 响应后记录对话"""
|
||||||
if self.ltm and self.ltm_enabled(event):
|
if self.ltm and self.ltm_enabled(event):
|
||||||
try:
|
try:
|
||||||
@@ -103,7 +107,7 @@ class Main(star.Star):
|
|||||||
logger.error(f"ltm: {e}")
|
logger.error(f"ltm: {e}")
|
||||||
|
|
||||||
@filter.after_message_sent()
|
@filter.after_message_sent()
|
||||||
async def after_message_sent(self, event: AstrMessageEvent):
|
async def after_message_sent(self, event: AstrMessageEvent) -> None:
|
||||||
"""消息发送后处理"""
|
"""消息发送后处理"""
|
||||||
if self.ltm and self.ltm_enabled(event):
|
if self.ltm and self.ltm_enabled(event):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ from astrbot.core.utils.io import download_dashboard
|
|||||||
|
|
||||||
|
|
||||||
class AdminCommands:
|
class AdminCommands:
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
async def op(self, event: AstrMessageEvent, admin_id: str = ""):
|
async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None:
|
||||||
"""授权管理员。op <admin_id>"""
|
"""授权管理员。op <admin_id>"""
|
||||||
if not admin_id:
|
if not admin_id:
|
||||||
event.set_result(
|
event.set_result(
|
||||||
@@ -21,7 +21,7 @@ class AdminCommands:
|
|||||||
self.context.get_config().save_config()
|
self.context.get_config().save_config()
|
||||||
event.set_result(MessageEventResult().message("授权成功。"))
|
event.set_result(MessageEventResult().message("授权成功。"))
|
||||||
|
|
||||||
async def deop(self, event: AstrMessageEvent, admin_id: str = ""):
|
async def deop(self, event: AstrMessageEvent, admin_id: str = "") -> None:
|
||||||
"""取消授权管理员。deop <admin_id>"""
|
"""取消授权管理员。deop <admin_id>"""
|
||||||
if not admin_id:
|
if not admin_id:
|
||||||
event.set_result(
|
event.set_result(
|
||||||
@@ -39,7 +39,7 @@ class AdminCommands:
|
|||||||
MessageEventResult().message("此用户 ID 不在管理员名单内。"),
|
MessageEventResult().message("此用户 ID 不在管理员名单内。"),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def wl(self, event: AstrMessageEvent, sid: str = ""):
|
async def wl(self, event: AstrMessageEvent, sid: str = "") -> None:
|
||||||
"""添加白名单。wl <sid>"""
|
"""添加白名单。wl <sid>"""
|
||||||
if not sid:
|
if not sid:
|
||||||
event.set_result(
|
event.set_result(
|
||||||
@@ -53,7 +53,7 @@ class AdminCommands:
|
|||||||
cfg.save_config()
|
cfg.save_config()
|
||||||
event.set_result(MessageEventResult().message("添加白名单成功。"))
|
event.set_result(MessageEventResult().message("添加白名单成功。"))
|
||||||
|
|
||||||
async def dwl(self, event: AstrMessageEvent, sid: str = ""):
|
async def dwl(self, event: AstrMessageEvent, sid: str = "") -> None:
|
||||||
"""删除白名单。dwl <sid>"""
|
"""删除白名单。dwl <sid>"""
|
||||||
if not sid:
|
if not sid:
|
||||||
event.set_result(
|
event.set_result(
|
||||||
@@ -70,7 +70,7 @@ class AdminCommands:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
event.set_result(MessageEventResult().message("此 SID 不在白名单内。"))
|
||||||
|
|
||||||
async def update_dashboard(self, event: AstrMessageEvent):
|
async def update_dashboard(self, event: AstrMessageEvent) -> None:
|
||||||
"""更新管理面板"""
|
"""更新管理面板"""
|
||||||
await event.send(MessageChain().message("正在尝试更新管理面板..."))
|
await event.send(MessageChain().message("正在尝试更新管理面板..."))
|
||||||
await download_dashboard(version=f"v{VERSION}", latest=False)
|
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ from .utils.rst_scene import RstScene
|
|||||||
|
|
||||||
|
|
||||||
class AlterCmdCommands(CommandParserMixin):
|
class AlterCmdCommands(CommandParserMixin):
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
async def update_reset_permission(self, scene_key: str, perm_type: str):
|
async def update_reset_permission(self, scene_key: str, perm_type: str) -> None:
|
||||||
"""更新reset命令在特定场景下的权限设置"""
|
"""更新reset命令在特定场景下的权限设置"""
|
||||||
from astrbot.api import sp
|
from astrbot.api import sp
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ class AlterCmdCommands(CommandParserMixin):
|
|||||||
alter_cmd_cfg["astrbot"] = plugin_cfg
|
alter_cmd_cfg["astrbot"] = plugin_cfg
|
||||||
await sp.global_put("alter_cmd", alter_cmd_cfg)
|
await sp.global_put("alter_cmd", alter_cmd_cfg)
|
||||||
|
|
||||||
async def alter_cmd(self, event: AstrMessageEvent):
|
async def alter_cmd(self, event: AstrMessageEvent) -> None:
|
||||||
token = self.parse_commands(event.message_str)
|
token = self.parse_commands(event.message_str)
|
||||||
if token.len < 3:
|
if token.len < 3:
|
||||||
await event.send(
|
await event.send(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from astrbot.api import sp, star
|
|||||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||||
from astrbot.core.platform.astr_message_event import MessageSession
|
from astrbot.core.platform.astr_message_event import MessageSession
|
||||||
from astrbot.core.platform.message_type import MessageType
|
from astrbot.core.platform.message_type import MessageType
|
||||||
|
from astrbot.core.utils.active_event_registry import active_event_registry
|
||||||
|
|
||||||
from .utils.rst_scene import RstScene
|
from .utils.rst_scene import RstScene
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ THIRD_PARTY_AGENT_RUNNER_STR = ", ".join(THIRD_PARTY_AGENT_RUNNER_KEY.keys())
|
|||||||
|
|
||||||
|
|
||||||
class ConversationCommands:
|
class ConversationCommands:
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
async def _get_current_persona_id(self, session_id):
|
async def _get_current_persona_id(self, session_id):
|
||||||
@@ -33,7 +34,7 @@ class ConversationCommands:
|
|||||||
return None
|
return None
|
||||||
return conv.persona_id
|
return conv.persona_id
|
||||||
|
|
||||||
async def reset(self, message: AstrMessageEvent):
|
async def reset(self, message: AstrMessageEvent) -> None:
|
||||||
"""重置 LLM 会话"""
|
"""重置 LLM 会话"""
|
||||||
umo = message.unified_msg_origin
|
umo = message.unified_msg_origin
|
||||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||||
@@ -62,6 +63,7 @@ class ConversationCommands:
|
|||||||
|
|
||||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||||
|
active_event_registry.stop_all(umo, exclude=message)
|
||||||
await sp.remove_async(
|
await sp.remove_async(
|
||||||
scope="umo",
|
scope="umo",
|
||||||
scope_id=umo,
|
scope_id=umo,
|
||||||
@@ -86,6 +88,8 @@ class ConversationCommands:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
active_event_registry.stop_all(umo, exclude=message)
|
||||||
|
|
||||||
await self.context.conversation_manager.update_conversation(
|
await self.context.conversation_manager.update_conversation(
|
||||||
umo,
|
umo,
|
||||||
cid,
|
cid,
|
||||||
@@ -98,7 +102,31 @@ class ConversationCommands:
|
|||||||
|
|
||||||
message.set_result(MessageEventResult().message(ret))
|
message.set_result(MessageEventResult().message(ret))
|
||||||
|
|
||||||
async def his(self, message: AstrMessageEvent, page: int = 1):
|
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:
|
||||||
"""查看对话记录"""
|
"""查看对话记录"""
|
||||||
if not self.context.get_using_provider(message.unified_msg_origin):
|
if not self.context.get_using_provider(message.unified_msg_origin):
|
||||||
message.set_result(
|
message.set_result(
|
||||||
@@ -141,7 +169,7 @@ class ConversationCommands:
|
|||||||
|
|
||||||
message.set_result(MessageEventResult().message(ret).use_t2i(False))
|
message.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||||
|
|
||||||
async def convs(self, message: AstrMessageEvent, page: int = 1):
|
async def convs(self, message: AstrMessageEvent, page: int = 1) -> None:
|
||||||
"""查看对话列表"""
|
"""查看对话列表"""
|
||||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||||
@@ -178,16 +206,33 @@ class ConversationCommands:
|
|||||||
_titles[conv.cid] = title
|
_titles[conv.cid] = title
|
||||||
|
|
||||||
"""遍历分页后的对话生成列表显示"""
|
"""遍历分页后的对话生成列表显示"""
|
||||||
|
provider_settings = cfg.get("provider_settings", {})
|
||||||
|
platform_name = message.get_platform_name()
|
||||||
for conv in conversations_paged:
|
for conv in conversations_paged:
|
||||||
persona_id = conv.persona_id
|
(
|
||||||
if not persona_id or persona_id == "[%None]":
|
persona_id,
|
||||||
persona = await self.context.persona_manager.get_default_persona_v3(
|
_,
|
||||||
umo=message.unified_msg_origin,
|
force_applied_persona_id,
|
||||||
)
|
_,
|
||||||
persona_id = persona["name"]
|
) = 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} (自定义规则)"
|
||||||
|
|
||||||
title = _titles.get(conv.cid, "新对话")
|
title = _titles.get(conv.cid, "新对话")
|
||||||
parts.append(
|
parts.append(
|
||||||
f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_id}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n"
|
f"{global_index}. {title}({conv.cid[:4]})\n 人格情景: {persona_name}\n 上次更新: {datetime.datetime.fromtimestamp(conv.updated_at).strftime('%m-%d %H:%M')}\n"
|
||||||
)
|
)
|
||||||
global_index += 1
|
global_index += 1
|
||||||
|
|
||||||
@@ -216,11 +261,12 @@ class ConversationCommands:
|
|||||||
message.set_result(MessageEventResult().message(ret).use_t2i(False))
|
message.set_result(MessageEventResult().message(ret).use_t2i(False))
|
||||||
return
|
return
|
||||||
|
|
||||||
async def new_conv(self, message: AstrMessageEvent):
|
async def new_conv(self, message: AstrMessageEvent) -> None:
|
||||||
"""创建新对话"""
|
"""创建新对话"""
|
||||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
||||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||||
|
active_event_registry.stop_all(message.unified_msg_origin, exclude=message)
|
||||||
await sp.remove_async(
|
await sp.remove_async(
|
||||||
scope="umo",
|
scope="umo",
|
||||||
scope_id=message.unified_msg_origin,
|
scope_id=message.unified_msg_origin,
|
||||||
@@ -229,6 +275,7 @@ class ConversationCommands:
|
|||||||
message.set_result(MessageEventResult().message("已创建新对话。"))
|
message.set_result(MessageEventResult().message("已创建新对话。"))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
active_event_registry.stop_all(message.unified_msg_origin, exclude=message)
|
||||||
cpersona = await self._get_current_persona_id(message.unified_msg_origin)
|
cpersona = await self._get_current_persona_id(message.unified_msg_origin)
|
||||||
cid = await self.context.conversation_manager.new_conversation(
|
cid = await self.context.conversation_manager.new_conversation(
|
||||||
message.unified_msg_origin,
|
message.unified_msg_origin,
|
||||||
@@ -242,7 +289,7 @@ class ConversationCommands:
|
|||||||
MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"),
|
MessageEventResult().message(f"切换到新对话: 新对话({cid[:4]})。"),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def groupnew_conv(self, message: AstrMessageEvent, sid: str = ""):
|
async def groupnew_conv(self, message: AstrMessageEvent, sid: str = "") -> None:
|
||||||
"""创建新群聊对话"""
|
"""创建新群聊对话"""
|
||||||
if sid:
|
if sid:
|
||||||
session = str(
|
session = str(
|
||||||
@@ -273,7 +320,7 @@ class ConversationCommands:
|
|||||||
self,
|
self,
|
||||||
message: AstrMessageEvent,
|
message: AstrMessageEvent,
|
||||||
index: int | None = None,
|
index: int | None = None,
|
||||||
):
|
) -> None:
|
||||||
"""通过 /ls 前面的序号切换对话"""
|
"""通过 /ls 前面的序号切换对话"""
|
||||||
if not isinstance(index, int):
|
if not isinstance(index, int):
|
||||||
message.set_result(
|
message.set_result(
|
||||||
@@ -308,7 +355,7 @@ class ConversationCommands:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def rename_conv(self, message: AstrMessageEvent, new_name: str = ""):
|
async def rename_conv(self, message: AstrMessageEvent, new_name: str = "") -> None:
|
||||||
"""重命名对话"""
|
"""重命名对话"""
|
||||||
if not new_name:
|
if not new_name:
|
||||||
message.set_result(MessageEventResult().message("请输入新的对话名称。"))
|
message.set_result(MessageEventResult().message("请输入新的对话名称。"))
|
||||||
@@ -319,9 +366,10 @@ class ConversationCommands:
|
|||||||
)
|
)
|
||||||
message.set_result(MessageEventResult().message("重命名对话成功。"))
|
message.set_result(MessageEventResult().message("重命名对话成功。"))
|
||||||
|
|
||||||
async def del_conv(self, message: AstrMessageEvent):
|
async def del_conv(self, message: AstrMessageEvent) -> None:
|
||||||
"""删除当前对话"""
|
"""删除当前对话"""
|
||||||
cfg = self.context.get_config(umo=message.unified_msg_origin)
|
umo = message.unified_msg_origin
|
||||||
|
cfg = self.context.get_config(umo=umo)
|
||||||
is_unique_session = cfg["platform_settings"]["unique_session"]
|
is_unique_session = cfg["platform_settings"]["unique_session"]
|
||||||
if message.get_group_id() and not is_unique_session and message.role != "admin":
|
if message.get_group_id() and not is_unique_session and message.role != "admin":
|
||||||
# 群聊,没开独立会话,发送人不是管理员
|
# 群聊,没开独立会话,发送人不是管理员
|
||||||
@@ -334,18 +382,17 @@ class ConversationCommands:
|
|||||||
|
|
||||||
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
agent_runner_type = cfg["provider_settings"]["agent_runner_type"]
|
||||||
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
if agent_runner_type in THIRD_PARTY_AGENT_RUNNER_KEY:
|
||||||
|
active_event_registry.stop_all(umo, exclude=message)
|
||||||
await sp.remove_async(
|
await sp.remove_async(
|
||||||
scope="umo",
|
scope="umo",
|
||||||
scope_id=message.unified_msg_origin,
|
scope_id=umo,
|
||||||
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
|
key=THIRD_PARTY_AGENT_RUNNER_KEY[agent_runner_type],
|
||||||
)
|
)
|
||||||
message.set_result(MessageEventResult().message("重置对话成功。"))
|
message.set_result(MessageEventResult().message("重置对话成功。"))
|
||||||
return
|
return
|
||||||
|
|
||||||
session_curr_cid = (
|
session_curr_cid = (
|
||||||
await self.context.conversation_manager.get_curr_conversation_id(
|
await self.context.conversation_manager.get_curr_conversation_id(umo)
|
||||||
message.unified_msg_origin,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not session_curr_cid:
|
if not session_curr_cid:
|
||||||
@@ -356,8 +403,10 @@ class ConversationCommands:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
active_event_registry.stop_all(umo, exclude=message)
|
||||||
|
|
||||||
await self.context.conversation_manager.delete_conversation(
|
await self.context.conversation_manager.delete_conversation(
|
||||||
message.unified_msg_origin,
|
umo,
|
||||||
session_curr_cid,
|
session_curr_cid,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from astrbot.core.utils.io import get_dashboard_version
|
|||||||
|
|
||||||
|
|
||||||
class HelpCommand:
|
class HelpCommand:
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
async def _query_astrbot_notice(self):
|
async def _query_astrbot_notice(self):
|
||||||
@@ -34,7 +34,7 @@ class HelpCommand:
|
|||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
hidden_commands = {"set", "unset", "websearch"}
|
hidden_commands = {"set", "unset", "websearch"}
|
||||||
|
|
||||||
def walk(items: list[dict], indent: int = 0):
|
def walk(items: list[dict], indent: int = 0) -> None:
|
||||||
for item in items:
|
for item in items:
|
||||||
if not item.get("reserved") or not item.get("enabled"):
|
if not item.get("reserved") or not item.get("enabled"):
|
||||||
continue
|
continue
|
||||||
@@ -62,7 +62,7 @@ class HelpCommand:
|
|||||||
walk(commands)
|
walk(commands)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
async def help(self, event: AstrMessageEvent):
|
async def help(self, event: AstrMessageEvent) -> None:
|
||||||
"""查看帮助"""
|
"""查看帮助"""
|
||||||
notice = ""
|
notice = ""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ from astrbot.api.event import AstrMessageEvent, MessageChain
|
|||||||
|
|
||||||
|
|
||||||
class LLMCommands:
|
class LLMCommands:
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
async def llm(self, event: AstrMessageEvent):
|
async def llm(self, event: AstrMessageEvent) -> None:
|
||||||
"""开启/关闭 LLM"""
|
"""开启/关闭 LLM"""
|
||||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||||
enable = cfg["provider_settings"].get("enable", True)
|
enable = cfg["provider_settings"].get("enable", True)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import builtins
|
import builtins
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from astrbot.api import sp, star
|
from astrbot.api import star
|
||||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class PersonaCommands:
|
class PersonaCommands:
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
def _build_tree_output(
|
def _build_tree_output(
|
||||||
@@ -50,7 +50,7 @@ class PersonaCommands:
|
|||||||
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
async def persona(self, message: AstrMessageEvent):
|
async def persona(self, message: AstrMessageEvent) -> None:
|
||||||
l = message.message_str.split(" ") # noqa: E741
|
l = message.message_str.split(" ") # noqa: E741
|
||||||
umo = message.unified_msg_origin
|
umo = message.unified_msg_origin
|
||||||
|
|
||||||
@@ -59,12 +59,7 @@ class PersonaCommands:
|
|||||||
default_persona = await self.context.persona_manager.get_default_persona_v3(
|
default_persona = await self.context.persona_manager.get_default_persona_v3(
|
||||||
umo=umo,
|
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 = "无"
|
curr_cid_title = "无"
|
||||||
if cid:
|
if cid:
|
||||||
@@ -80,10 +75,27 @@ class PersonaCommands:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
if not conv.persona_id and conv.persona_id != "[%None]":
|
|
||||||
curr_persona_name = default_persona["name"]
|
provider_settings = self.context.get_config(umo=umo).get(
|
||||||
else:
|
"provider_settings",
|
||||||
curr_persona_name = conv.persona_id
|
{},
|
||||||
|
)
|
||||||
|
(
|
||||||
|
persona_id,
|
||||||
|
_,
|
||||||
|
force_applied_persona_id,
|
||||||
|
_,
|
||||||
|
) = await self.context.persona_manager.resolve_selected_persona(
|
||||||
|
umo=umo,
|
||||||
|
conversation_persona_id=conv.persona_id,
|
||||||
|
platform_name=message.get_platform_name(),
|
||||||
|
provider_settings=provider_settings,
|
||||||
|
)
|
||||||
|
|
||||||
|
if persona_id == "[%None]":
|
||||||
|
curr_persona_name = "无"
|
||||||
|
elif persona_id:
|
||||||
|
curr_persona_name = persona_id
|
||||||
|
|
||||||
if force_applied_persona_id:
|
if force_applied_persona_id:
|
||||||
curr_persona_name = f"{curr_persona_name} (自定义规则)"
|
curr_persona_name = f"{curr_persona_name} (自定义规则)"
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ from astrbot.core.star.star_manager import PluginManager
|
|||||||
|
|
||||||
|
|
||||||
class PluginCommands:
|
class PluginCommands:
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
async def plugin_ls(self, event: AstrMessageEvent):
|
async def plugin_ls(self, event: AstrMessageEvent) -> None:
|
||||||
"""获取已经安装的插件列表。"""
|
"""获取已经安装的插件列表。"""
|
||||||
parts = ["已加载的插件:\n"]
|
parts = ["已加载的插件:\n"]
|
||||||
for plugin in self.context.get_all_stars():
|
for plugin in self.context.get_all_stars():
|
||||||
@@ -30,7 +30,7 @@ class PluginCommands:
|
|||||||
MessageEventResult().message(f"{plugin_list_info}").use_t2i(False),
|
MessageEventResult().message(f"{plugin_list_info}").use_t2i(False),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = ""):
|
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||||
"""禁用插件"""
|
"""禁用插件"""
|
||||||
if DEMO_MODE:
|
if DEMO_MODE:
|
||||||
event.set_result(MessageEventResult().message("演示模式下无法禁用插件。"))
|
event.set_result(MessageEventResult().message("演示模式下无法禁用插件。"))
|
||||||
@@ -43,7 +43,7 @@ class PluginCommands:
|
|||||||
await self.context._star_manager.turn_off_plugin(plugin_name) # type: ignore
|
await self.context._star_manager.turn_off_plugin(plugin_name) # type: ignore
|
||||||
event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已禁用。"))
|
event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已禁用。"))
|
||||||
|
|
||||||
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = ""):
|
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||||
"""启用插件"""
|
"""启用插件"""
|
||||||
if DEMO_MODE:
|
if DEMO_MODE:
|
||||||
event.set_result(MessageEventResult().message("演示模式下无法启用插件。"))
|
event.set_result(MessageEventResult().message("演示模式下无法启用插件。"))
|
||||||
@@ -56,7 +56,7 @@ class PluginCommands:
|
|||||||
await self.context._star_manager.turn_on_plugin(plugin_name) # type: ignore
|
await self.context._star_manager.turn_on_plugin(plugin_name) # type: ignore
|
||||||
event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已启用。"))
|
event.set_result(MessageEventResult().message(f"插件 {plugin_name} 已启用。"))
|
||||||
|
|
||||||
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = ""):
|
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = "") -> None:
|
||||||
"""安装插件"""
|
"""安装插件"""
|
||||||
if DEMO_MODE:
|
if DEMO_MODE:
|
||||||
event.set_result(MessageEventResult().message("演示模式下无法安装插件。"))
|
event.set_result(MessageEventResult().message("演示模式下无法安装插件。"))
|
||||||
@@ -77,7 +77,7 @@ class PluginCommands:
|
|||||||
event.set_result(MessageEventResult().message(f"安装插件失败: {e}"))
|
event.set_result(MessageEventResult().message(f"安装插件失败: {e}"))
|
||||||
return
|
return
|
||||||
|
|
||||||
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = ""):
|
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||||
"""获取插件帮助"""
|
"""获取插件帮助"""
|
||||||
if not plugin_name:
|
if not plugin_name:
|
||||||
event.set_result(
|
event.set_result(
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from astrbot.core.provider.entities import ProviderType
|
|||||||
|
|
||||||
|
|
||||||
class ProviderCommands:
|
class ProviderCommands:
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
def _log_reachability_failure(
|
def _log_reachability_failure(
|
||||||
@@ -17,7 +17,7 @@ class ProviderCommands:
|
|||||||
provider_capability_type: ProviderType | None,
|
provider_capability_type: ProviderType | None,
|
||||||
err_code: str,
|
err_code: str,
|
||||||
err_reason: str,
|
err_reason: str,
|
||||||
):
|
) -> None:
|
||||||
"""记录不可达原因到日志。"""
|
"""记录不可达原因到日志。"""
|
||||||
meta = provider.meta()
|
meta = provider.meta()
|
||||||
logger.warning(
|
logger.warning(
|
||||||
@@ -49,7 +49,7 @@ class ProviderCommands:
|
|||||||
event: AstrMessageEvent,
|
event: AstrMessageEvent,
|
||||||
idx: str | int | None = None,
|
idx: str | int | None = None,
|
||||||
idx2: int | None = None,
|
idx2: int | None = None,
|
||||||
):
|
) -> None:
|
||||||
"""查看或者切换 LLM Provider"""
|
"""查看或者切换 LLM Provider"""
|
||||||
umo = event.unified_msg_origin
|
umo = event.unified_msg_origin
|
||||||
cfg = self.context.get_config(umo).get("provider_settings", {})
|
cfg = self.context.get_config(umo).get("provider_settings", {})
|
||||||
@@ -228,7 +228,7 @@ class ProviderCommands:
|
|||||||
self,
|
self,
|
||||||
message: AstrMessageEvent,
|
message: AstrMessageEvent,
|
||||||
idx_or_name: int | str | None = None,
|
idx_or_name: int | str | None = None,
|
||||||
):
|
) -> None:
|
||||||
"""查看或者切换模型"""
|
"""查看或者切换模型"""
|
||||||
prov = self.context.get_using_provider(message.unified_msg_origin)
|
prov = self.context.get_using_provider(message.unified_msg_origin)
|
||||||
if not prov:
|
if not prov:
|
||||||
@@ -293,7 +293,7 @@ class ProviderCommands:
|
|||||||
MessageEventResult().message(f"切换模型到 {prov.get_model()}。"),
|
MessageEventResult().message(f"切换模型到 {prov.get_model()}。"),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def key(self, message: AstrMessageEvent, index: int | None = None):
|
async def key(self, message: AstrMessageEvent, index: int | None = None) -> None:
|
||||||
prov = self.context.get_using_provider(message.unified_msg_origin)
|
prov = self.context.get_using_provider(message.unified_msg_origin)
|
||||||
if not prov:
|
if not prov:
|
||||||
message.set_result(
|
message.set_result(
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
|||||||
|
|
||||||
|
|
||||||
class SetUnsetCommands:
|
class SetUnsetCommands:
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
async def set_variable(self, event: AstrMessageEvent, key: str, value: str):
|
async def set_variable(self, event: AstrMessageEvent, key: str, value: str) -> None:
|
||||||
"""设置会话变量"""
|
"""设置会话变量"""
|
||||||
uid = event.unified_msg_origin
|
uid = event.unified_msg_origin
|
||||||
session_var = await sp.session_get(uid, "session_variables", {})
|
session_var = await sp.session_get(uid, "session_variables", {})
|
||||||
@@ -19,7 +19,7 @@ class SetUnsetCommands:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def unset_variable(self, event: AstrMessageEvent, key: str):
|
async def unset_variable(self, event: AstrMessageEvent, key: str) -> None:
|
||||||
"""移除会话变量"""
|
"""移除会话变量"""
|
||||||
uid = event.unified_msg_origin
|
uid = event.unified_msg_origin
|
||||||
session_var = await sp.session_get(uid, "session_variables", {})
|
session_var = await sp.session_get(uid, "session_variables", {})
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
|||||||
class SIDCommand:
|
class SIDCommand:
|
||||||
"""会话ID命令类"""
|
"""会话ID命令类"""
|
||||||
|
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
async def sid(self, event: AstrMessageEvent):
|
async def sid(self, event: AstrMessageEvent) -> None:
|
||||||
"""获取消息来源信息"""
|
"""获取消息来源信息"""
|
||||||
sid = event.unified_msg_origin
|
sid = event.unified_msg_origin
|
||||||
user_id = str(event.get_sender_id())
|
user_id = str(event.get_sender_id())
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
|||||||
class T2ICommand:
|
class T2ICommand:
|
||||||
"""文本转图片命令类"""
|
"""文本转图片命令类"""
|
||||||
|
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
async def t2i(self, event: AstrMessageEvent):
|
async def t2i(self, event: AstrMessageEvent) -> None:
|
||||||
"""开关文本转图片"""
|
"""开关文本转图片"""
|
||||||
config = self.context.get_config(umo=event.unified_msg_origin)
|
config = self.context.get_config(umo=event.unified_msg_origin)
|
||||||
if config["t2i"]:
|
if config["t2i"]:
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ from astrbot.core.star.session_llm_manager import SessionServiceManager
|
|||||||
class TTSCommand:
|
class TTSCommand:
|
||||||
"""文本转语音命令类"""
|
"""文本转语音命令类"""
|
||||||
|
|
||||||
def __init__(self, context: star.Context):
|
def __init__(self, context: star.Context) -> None:
|
||||||
self.context = context
|
self.context = context
|
||||||
|
|
||||||
async def tts(self, event: AstrMessageEvent):
|
async def tts(self, event: AstrMessageEvent) -> None:
|
||||||
"""开关文本转语音(会话级别)"""
|
"""开关文本转语音(会话级别)"""
|
||||||
umo = event.unified_msg_origin
|
umo = event.unified_msg_origin
|
||||||
ses_tts = await SessionServiceManager.is_tts_enabled_for_session(umo)
|
ses_tts = await SessionServiceManager.is_tts_enabled_for_session(umo)
|
||||||
|
|||||||
@@ -35,84 +35,84 @@ class Main(star.Star):
|
|||||||
self.sid_c = SIDCommand(self.context)
|
self.sid_c = SIDCommand(self.context)
|
||||||
|
|
||||||
@filter.command("help")
|
@filter.command("help")
|
||||||
async def help(self, event: AstrMessageEvent):
|
async def help(self, event: AstrMessageEvent) -> None:
|
||||||
"""查看帮助"""
|
"""查看帮助"""
|
||||||
await self.help_c.help(event)
|
await self.help_c.help(event)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("llm")
|
@filter.command("llm")
|
||||||
async def llm(self, event: AstrMessageEvent):
|
async def llm(self, event: AstrMessageEvent) -> None:
|
||||||
"""开启/关闭 LLM"""
|
"""开启/关闭 LLM"""
|
||||||
await self.llm_c.llm(event)
|
await self.llm_c.llm(event)
|
||||||
|
|
||||||
@filter.command_group("plugin")
|
@filter.command_group("plugin")
|
||||||
def plugin(self):
|
def plugin(self) -> None:
|
||||||
"""插件管理"""
|
"""插件管理"""
|
||||||
|
|
||||||
@plugin.command("ls")
|
@plugin.command("ls")
|
||||||
async def plugin_ls(self, event: AstrMessageEvent):
|
async def plugin_ls(self, event: AstrMessageEvent) -> None:
|
||||||
"""获取已经安装的插件列表。"""
|
"""获取已经安装的插件列表。"""
|
||||||
await self.plugin_c.plugin_ls(event)
|
await self.plugin_c.plugin_ls(event)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@plugin.command("off")
|
@plugin.command("off")
|
||||||
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = ""):
|
async def plugin_off(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||||
"""禁用插件"""
|
"""禁用插件"""
|
||||||
await self.plugin_c.plugin_off(event, plugin_name)
|
await self.plugin_c.plugin_off(event, plugin_name)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@plugin.command("on")
|
@plugin.command("on")
|
||||||
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = ""):
|
async def plugin_on(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||||
"""启用插件"""
|
"""启用插件"""
|
||||||
await self.plugin_c.plugin_on(event, plugin_name)
|
await self.plugin_c.plugin_on(event, plugin_name)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@plugin.command("get")
|
@plugin.command("get")
|
||||||
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = ""):
|
async def plugin_get(self, event: AstrMessageEvent, plugin_repo: str = "") -> None:
|
||||||
"""安装插件"""
|
"""安装插件"""
|
||||||
await self.plugin_c.plugin_get(event, plugin_repo)
|
await self.plugin_c.plugin_get(event, plugin_repo)
|
||||||
|
|
||||||
@plugin.command("help")
|
@plugin.command("help")
|
||||||
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = ""):
|
async def plugin_help(self, event: AstrMessageEvent, plugin_name: str = "") -> None:
|
||||||
"""获取插件帮助"""
|
"""获取插件帮助"""
|
||||||
await self.plugin_c.plugin_help(event, plugin_name)
|
await self.plugin_c.plugin_help(event, plugin_name)
|
||||||
|
|
||||||
@filter.command("t2i")
|
@filter.command("t2i")
|
||||||
async def t2i(self, event: AstrMessageEvent):
|
async def t2i(self, event: AstrMessageEvent) -> None:
|
||||||
"""开关文本转图片"""
|
"""开关文本转图片"""
|
||||||
await self.t2i_c.t2i(event)
|
await self.t2i_c.t2i(event)
|
||||||
|
|
||||||
@filter.command("tts")
|
@filter.command("tts")
|
||||||
async def tts(self, event: AstrMessageEvent):
|
async def tts(self, event: AstrMessageEvent) -> None:
|
||||||
"""开关文本转语音(会话级别)"""
|
"""开关文本转语音(会话级别)"""
|
||||||
await self.tts_c.tts(event)
|
await self.tts_c.tts(event)
|
||||||
|
|
||||||
@filter.command("sid")
|
@filter.command("sid")
|
||||||
async def sid(self, event: AstrMessageEvent):
|
async def sid(self, event: AstrMessageEvent) -> None:
|
||||||
"""获取会话 ID 和 管理员 ID"""
|
"""获取会话 ID 和 管理员 ID"""
|
||||||
await self.sid_c.sid(event)
|
await self.sid_c.sid(event)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("op")
|
@filter.command("op")
|
||||||
async def op(self, event: AstrMessageEvent, admin_id: str = ""):
|
async def op(self, event: AstrMessageEvent, admin_id: str = "") -> None:
|
||||||
"""授权管理员。op <admin_id>"""
|
"""授权管理员。op <admin_id>"""
|
||||||
await self.admin_c.op(event, admin_id)
|
await self.admin_c.op(event, admin_id)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("deop")
|
@filter.command("deop")
|
||||||
async def deop(self, event: AstrMessageEvent, admin_id: str):
|
async def deop(self, event: AstrMessageEvent, admin_id: str) -> None:
|
||||||
"""取消授权管理员。deop <admin_id>"""
|
"""取消授权管理员。deop <admin_id>"""
|
||||||
await self.admin_c.deop(event, admin_id)
|
await self.admin_c.deop(event, admin_id)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("wl")
|
@filter.command("wl")
|
||||||
async def wl(self, event: AstrMessageEvent, sid: str = ""):
|
async def wl(self, event: AstrMessageEvent, sid: str = "") -> None:
|
||||||
"""添加白名单。wl <sid>"""
|
"""添加白名单。wl <sid>"""
|
||||||
await self.admin_c.wl(event, sid)
|
await self.admin_c.wl(event, sid)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("dwl")
|
@filter.command("dwl")
|
||||||
async def dwl(self, event: AstrMessageEvent, sid: str):
|
async def dwl(self, event: AstrMessageEvent, sid: str) -> None:
|
||||||
"""删除白名单。dwl <sid>"""
|
"""删除白名单。dwl <sid>"""
|
||||||
await self.admin_c.dwl(event, sid)
|
await self.admin_c.dwl(event, sid)
|
||||||
|
|
||||||
@@ -123,89 +123,96 @@ class Main(star.Star):
|
|||||||
event: AstrMessageEvent,
|
event: AstrMessageEvent,
|
||||||
idx: str | int | None = None,
|
idx: str | int | None = None,
|
||||||
idx2: int | None = None,
|
idx2: int | None = None,
|
||||||
):
|
) -> None:
|
||||||
"""查看或者切换 LLM Provider"""
|
"""查看或者切换 LLM Provider"""
|
||||||
await self.provider_c.provider(event, idx, idx2)
|
await self.provider_c.provider(event, idx, idx2)
|
||||||
|
|
||||||
@filter.command("reset")
|
@filter.command("reset")
|
||||||
async def reset(self, message: AstrMessageEvent):
|
async def reset(self, message: AstrMessageEvent) -> None:
|
||||||
"""重置 LLM 会话"""
|
"""重置 LLM 会话"""
|
||||||
await self.conversation_c.reset(message)
|
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.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("model")
|
@filter.command("model")
|
||||||
async def model_ls(
|
async def model_ls(
|
||||||
self,
|
self,
|
||||||
message: AstrMessageEvent,
|
message: AstrMessageEvent,
|
||||||
idx_or_name: int | str | None = None,
|
idx_or_name: int | str | None = None,
|
||||||
):
|
) -> None:
|
||||||
"""查看或者切换模型"""
|
"""查看或者切换模型"""
|
||||||
await self.provider_c.model_ls(message, idx_or_name)
|
await self.provider_c.model_ls(message, idx_or_name)
|
||||||
|
|
||||||
@filter.command("history")
|
@filter.command("history")
|
||||||
async def his(self, message: AstrMessageEvent, page: int = 1):
|
async def his(self, message: AstrMessageEvent, page: int = 1) -> None:
|
||||||
"""查看对话记录"""
|
"""查看对话记录"""
|
||||||
await self.conversation_c.his(message, page)
|
await self.conversation_c.his(message, page)
|
||||||
|
|
||||||
@filter.command("ls")
|
@filter.command("ls")
|
||||||
async def convs(self, message: AstrMessageEvent, page: int = 1):
|
async def convs(self, message: AstrMessageEvent, page: int = 1) -> None:
|
||||||
"""查看对话列表"""
|
"""查看对话列表"""
|
||||||
await self.conversation_c.convs(message, page)
|
await self.conversation_c.convs(message, page)
|
||||||
|
|
||||||
@filter.command("new")
|
@filter.command("new")
|
||||||
async def new_conv(self, message: AstrMessageEvent):
|
async def new_conv(self, message: AstrMessageEvent) -> None:
|
||||||
"""创建新对话"""
|
"""创建新对话"""
|
||||||
await self.conversation_c.new_conv(message)
|
await self.conversation_c.new_conv(message)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("groupnew")
|
@filter.command("groupnew")
|
||||||
async def groupnew_conv(self, message: AstrMessageEvent, sid: str):
|
async def groupnew_conv(self, message: AstrMessageEvent, sid: str) -> None:
|
||||||
"""创建新群聊对话"""
|
"""创建新群聊对话"""
|
||||||
await self.conversation_c.groupnew_conv(message, sid)
|
await self.conversation_c.groupnew_conv(message, sid)
|
||||||
|
|
||||||
@filter.command("switch")
|
@filter.command("switch")
|
||||||
async def switch_conv(self, message: AstrMessageEvent, index: int | None = None):
|
async def switch_conv(
|
||||||
|
self, message: AstrMessageEvent, index: int | None = None
|
||||||
|
) -> None:
|
||||||
"""通过 /ls 前面的序号切换对话"""
|
"""通过 /ls 前面的序号切换对话"""
|
||||||
await self.conversation_c.switch_conv(message, index)
|
await self.conversation_c.switch_conv(message, index)
|
||||||
|
|
||||||
@filter.command("rename")
|
@filter.command("rename")
|
||||||
async def rename_conv(self, message: AstrMessageEvent, new_name: str):
|
async def rename_conv(self, message: AstrMessageEvent, new_name: str) -> None:
|
||||||
"""重命名对话"""
|
"""重命名对话"""
|
||||||
await self.conversation_c.rename_conv(message, new_name)
|
await self.conversation_c.rename_conv(message, new_name)
|
||||||
|
|
||||||
@filter.command("del")
|
@filter.command("del")
|
||||||
async def del_conv(self, message: AstrMessageEvent):
|
async def del_conv(self, message: AstrMessageEvent) -> None:
|
||||||
"""删除当前对话"""
|
"""删除当前对话"""
|
||||||
await self.conversation_c.del_conv(message)
|
await self.conversation_c.del_conv(message)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("key")
|
@filter.command("key")
|
||||||
async def key(self, message: AstrMessageEvent, index: int | None = None):
|
async def key(self, message: AstrMessageEvent, index: int | None = None) -> None:
|
||||||
"""查看或者切换 Key"""
|
"""查看或者切换 Key"""
|
||||||
await self.provider_c.key(message, index)
|
await self.provider_c.key(message, index)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("persona")
|
@filter.command("persona")
|
||||||
async def persona(self, message: AstrMessageEvent):
|
async def persona(self, message: AstrMessageEvent) -> None:
|
||||||
"""查看或者切换 Persona"""
|
"""查看或者切换 Persona"""
|
||||||
await self.persona_c.persona(message)
|
await self.persona_c.persona(message)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("dashboard_update")
|
@filter.command("dashboard_update")
|
||||||
async def update_dashboard(self, event: AstrMessageEvent):
|
async def update_dashboard(self, event: AstrMessageEvent) -> None:
|
||||||
"""更新管理面板"""
|
"""更新管理面板"""
|
||||||
await self.admin_c.update_dashboard(event)
|
await self.admin_c.update_dashboard(event)
|
||||||
|
|
||||||
@filter.command("set")
|
@filter.command("set")
|
||||||
async def set_variable(self, event: AstrMessageEvent, key: str, value: str):
|
async def set_variable(self, event: AstrMessageEvent, key: str, value: str) -> None:
|
||||||
await self.setunset_c.set_variable(event, key, value)
|
await self.setunset_c.set_variable(event, key, value)
|
||||||
|
|
||||||
@filter.command("unset")
|
@filter.command("unset")
|
||||||
async def unset_variable(self, event: AstrMessageEvent, key: str):
|
async def unset_variable(self, event: AstrMessageEvent, key: str) -> None:
|
||||||
await self.setunset_c.unset_variable(event, key)
|
await self.setunset_c.unset_variable(event, key)
|
||||||
|
|
||||||
@filter.permission_type(filter.PermissionType.ADMIN)
|
@filter.permission_type(filter.PermissionType.ADMIN)
|
||||||
@filter.command("alter_cmd", alias={"alter"})
|
@filter.command("alter_cmd", alias={"alter"})
|
||||||
async def alter_cmd(self, event: AstrMessageEvent):
|
async def alter_cmd(self, event: AstrMessageEvent) -> None:
|
||||||
"""修改命令权限"""
|
"""修改命令权限"""
|
||||||
await self.alter_cmd_c.alter_cmd(event)
|
await self.alter_cmd_c.alter_cmd(event)
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ from astrbot.core.utils.session_waiter import (
|
|||||||
class Main(Star):
|
class Main(Star):
|
||||||
"""会话控制"""
|
"""会话控制"""
|
||||||
|
|
||||||
def __init__(self, context: Context):
|
def __init__(self, context: Context) -> None:
|
||||||
super().__init__(context)
|
super().__init__(context)
|
||||||
|
|
||||||
@filter.event_message_type(filter.EventMessageType.ALL, priority=maxsize)
|
@filter.event_message_type(filter.EventMessageType.ALL, priority=maxsize)
|
||||||
async def handle_session_control_agent(self, event: AstrMessageEvent):
|
async def handle_session_control_agent(self, event: AstrMessageEvent) -> None:
|
||||||
"""会话控制代理"""
|
"""会话控制代理"""
|
||||||
for session_filter in FILTERS:
|
for session_filter in FILTERS:
|
||||||
session_id = session_filter.filter(event)
|
session_id = session_filter.filter(event)
|
||||||
@@ -90,7 +90,7 @@ class Main(Star):
|
|||||||
async def empty_mention_waiter(
|
async def empty_mention_waiter(
|
||||||
controller: SessionController,
|
controller: SessionController,
|
||||||
event: AstrMessageEvent,
|
event: AstrMessageEvent,
|
||||||
):
|
) -> None:
|
||||||
event.message_obj.message.insert(
|
event.message_obj.message.insert(
|
||||||
0,
|
0,
|
||||||
Comp.At(qq=event.get_self_id(), name=event.get_self_id()),
|
Comp.At(qq=event.get_self_id(), name=event.get_self_id()),
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class SearchEngine:
|
|||||||
def _set_selector(self, selector: str) -> str:
|
def _set_selector(self, selector: str) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _get_next_page(self, query: str):
|
async def _get_next_page(self, query: str) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
async def _get_html(self, url: str, data: dict | None = None) -> str:
|
async def _get_html(self, url: str, data: dict | None = None) -> str:
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class Main(star.Star):
|
|||||||
"fetch_url",
|
"fetch_url",
|
||||||
"web_search_tavily",
|
"web_search_tavily",
|
||||||
"tavily_extract_web_page",
|
"tavily_extract_web_page",
|
||||||
|
"web_search_bocha",
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, context: star.Context) -> None:
|
def __init__(self, context: star.Context) -> None:
|
||||||
@@ -30,6 +31,9 @@ class Main(star.Star):
|
|||||||
self.tavily_key_index = 0
|
self.tavily_key_index = 0
|
||||||
self.tavily_key_lock = asyncio.Lock()
|
self.tavily_key_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
self.bocha_key_index = 0
|
||||||
|
self.bocha_key_lock = asyncio.Lock()
|
||||||
|
|
||||||
# 将 str 类型的 key 迁移至 list[str],并保存
|
# 将 str 类型的 key 迁移至 list[str],并保存
|
||||||
cfg = self.context.get_config()
|
cfg = self.context.get_config()
|
||||||
provider_settings = cfg.get("provider_settings")
|
provider_settings = cfg.get("provider_settings")
|
||||||
@@ -45,6 +49,14 @@ class Main(star.Star):
|
|||||||
provider_settings["websearch_tavily_key"] = []
|
provider_settings["websearch_tavily_key"] = []
|
||||||
cfg.save_config()
|
cfg.save_config()
|
||||||
|
|
||||||
|
bocha_key = provider_settings.get("websearch_bocha_key")
|
||||||
|
if isinstance(bocha_key, str):
|
||||||
|
if bocha_key:
|
||||||
|
provider_settings["websearch_bocha_key"] = [bocha_key]
|
||||||
|
else:
|
||||||
|
provider_settings["websearch_bocha_key"] = []
|
||||||
|
cfg.save_config()
|
||||||
|
|
||||||
self.bing_search = Bing()
|
self.bing_search = Bing()
|
||||||
self.sogo_search = Sogo()
|
self.sogo_search = Sogo()
|
||||||
self.baidu_initialized = False
|
self.baidu_initialized = False
|
||||||
@@ -58,7 +70,7 @@ class Main(star.Star):
|
|||||||
header = HEADERS
|
header = HEADERS
|
||||||
header.update({"User-Agent": random.choice(USER_AGENTS)})
|
header.update({"User-Agent": random.choice(USER_AGENTS)})
|
||||||
async with aiohttp.ClientSession(trust_env=True) as session:
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
async with session.get(url, headers=header, timeout=6) as response:
|
async with session.get(url, headers=header) as response:
|
||||||
html = await response.text(encoding="utf-8")
|
html = await response.text(encoding="utf-8")
|
||||||
doc = Document(html)
|
doc = Document(html)
|
||||||
ret = doc.summary(html_partial=True)
|
ret = doc.summary(html_partial=True)
|
||||||
@@ -139,7 +151,6 @@ class Main(star.Star):
|
|||||||
url,
|
url,
|
||||||
json=payload,
|
json=payload,
|
||||||
headers=header,
|
headers=header,
|
||||||
timeout=6,
|
|
||||||
) as response:
|
) as response:
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
reason = await response.text()
|
reason = await response.text()
|
||||||
@@ -171,7 +182,6 @@ class Main(star.Star):
|
|||||||
url,
|
url,
|
||||||
json=payload,
|
json=payload,
|
||||||
headers=header,
|
headers=header,
|
||||||
timeout=6,
|
|
||||||
) as response:
|
) as response:
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
reason = await response.text()
|
reason = await response.text()
|
||||||
@@ -187,7 +197,7 @@ class Main(star.Star):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
@filter.command("websearch")
|
@filter.command("websearch")
|
||||||
async def websearch(self, event: AstrMessageEvent, oper: str | None = None):
|
async def websearch(self, event: AstrMessageEvent, oper: str | None = None) -> None:
|
||||||
"""网页搜索指令(已废弃)"""
|
"""网页搜索指令(已废弃)"""
|
||||||
event.set_result(
|
event.set_result(
|
||||||
MessageEventResult().message(
|
MessageEventResult().message(
|
||||||
@@ -234,7 +244,7 @@ class Main(star.Star):
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def ensure_baidu_ai_search_mcp(self, umo: str | None = None):
|
async def ensure_baidu_ai_search_mcp(self, umo: str | None = None) -> None:
|
||||||
if self.baidu_initialized:
|
if self.baidu_initialized:
|
||||||
return
|
return
|
||||||
cfg = self.context.get_config(umo=umo)
|
cfg = self.context.get_config(umo=umo)
|
||||||
@@ -253,7 +263,7 @@ class Main(star.Star):
|
|||||||
"transport": "sse",
|
"transport": "sse",
|
||||||
"url": f"http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key={key}",
|
"url": f"http://appbuilder.baidu.com/v2/ai_search/mcp/sse?api_key={key}",
|
||||||
"headers": {},
|
"headers": {},
|
||||||
"timeout": 30,
|
"timeout": 600,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.baidu_initialized = True
|
self.baidu_initialized = True
|
||||||
@@ -341,7 +351,7 @@ class Main(star.Star):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
if result.favicon:
|
if result.favicon:
|
||||||
sp.temorary_cache["_ws_favicon"][result.url] = result.favicon
|
sp.temporary_cache["_ws_favicon"][result.url] = result.favicon
|
||||||
# ret = "\n".join(ret_ls)
|
# ret = "\n".join(ret_ls)
|
||||||
ret = json.dumps({"results": ret_ls}, ensure_ascii=False)
|
ret = json.dumps({"results": ret_ls}, ensure_ascii=False)
|
||||||
return ret
|
return ret
|
||||||
@@ -382,12 +392,166 @@ class Main(star.Star):
|
|||||||
return "Error: Tavily web searcher does not return any results."
|
return "Error: Tavily web searcher does not return any results."
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
async def _get_bocha_key(self, cfg: AstrBotConfig) -> str:
|
||||||
|
"""并发安全的从列表中获取并轮换BoCha API密钥。"""
|
||||||
|
bocha_keys = cfg.get("provider_settings", {}).get("websearch_bocha_key", [])
|
||||||
|
if not bocha_keys:
|
||||||
|
raise ValueError("错误:BoCha API密钥未在AstrBot中配置。")
|
||||||
|
|
||||||
|
async with self.bocha_key_lock:
|
||||||
|
key = bocha_keys[self.bocha_key_index]
|
||||||
|
self.bocha_key_index = (self.bocha_key_index + 1) % len(bocha_keys)
|
||||||
|
return key
|
||||||
|
|
||||||
|
async def _web_search_bocha(
|
||||||
|
self,
|
||||||
|
cfg: AstrBotConfig,
|
||||||
|
payload: dict,
|
||||||
|
) -> list[SearchResult]:
|
||||||
|
"""使用 BoCha 搜索引擎进行搜索"""
|
||||||
|
bocha_key = await self._get_bocha_key(cfg)
|
||||||
|
url = "https://api.bochaai.com/v1/web-search"
|
||||||
|
header = {
|
||||||
|
"Authorization": f"Bearer {bocha_key}",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
async with aiohttp.ClientSession(trust_env=True) as session:
|
||||||
|
async with session.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
headers=header,
|
||||||
|
) as response:
|
||||||
|
if response.status != 200:
|
||||||
|
reason = await response.text()
|
||||||
|
raise Exception(
|
||||||
|
f"BoCha web search failed: {reason}, status: {response.status}",
|
||||||
|
)
|
||||||
|
data = await response.json()
|
||||||
|
data = data["data"]["webPages"]["value"]
|
||||||
|
results = []
|
||||||
|
for item in data:
|
||||||
|
result = SearchResult(
|
||||||
|
title=item.get("name"),
|
||||||
|
url=item.get("url"),
|
||||||
|
snippet=item.get("snippet"),
|
||||||
|
favicon=item.get("siteIcon"),
|
||||||
|
)
|
||||||
|
results.append(result)
|
||||||
|
return results
|
||||||
|
|
||||||
|
@llm_tool("web_search_bocha")
|
||||||
|
async def search_from_bocha(
|
||||||
|
self,
|
||||||
|
event: AstrMessageEvent,
|
||||||
|
query: str,
|
||||||
|
freshness: str = "noLimit",
|
||||||
|
summary: bool = False,
|
||||||
|
include: str = "",
|
||||||
|
exclude: str = "",
|
||||||
|
count: int = 10,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
A web search tool based on Bocha Search API, used to retrieve web pages
|
||||||
|
related to the user's query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query (string): Required. User's search query.
|
||||||
|
|
||||||
|
freshness (string): Optional. Specifies the time range of the search.
|
||||||
|
Supported values:
|
||||||
|
- "noLimit": No time limit (default, recommended).
|
||||||
|
- "oneDay": Within one day.
|
||||||
|
- "oneWeek": Within one week.
|
||||||
|
- "oneMonth": Within one month.
|
||||||
|
- "oneYear": Within one year.
|
||||||
|
- "YYYY-MM-DD..YYYY-MM-DD": Search within a specific date range.
|
||||||
|
Example: "2025-01-01..2025-04-06".
|
||||||
|
- "YYYY-MM-DD": Search on a specific date.
|
||||||
|
Example: "2025-04-06".
|
||||||
|
It is recommended to use "noLimit", as the search algorithm will
|
||||||
|
automatically optimize time relevance. Manually restricting the
|
||||||
|
time range may result in no search results.
|
||||||
|
|
||||||
|
summary (boolean): Optional. Whether to include a text summary
|
||||||
|
for each search result.
|
||||||
|
- True: Include summary.
|
||||||
|
- False: Do not include summary (default).
|
||||||
|
|
||||||
|
include (string): Optional. Specifies the domains to include in
|
||||||
|
the search. Multiple domains can be separated by "|" or ",".
|
||||||
|
A maximum of 100 domains is allowed.
|
||||||
|
Examples:
|
||||||
|
- "qq.com"
|
||||||
|
- "qq.com|m.163.com"
|
||||||
|
|
||||||
|
exclude (string): Optional. Specifies the domains to exclude from
|
||||||
|
the search. Multiple domains can be separated by "|" or ",".
|
||||||
|
A maximum of 100 domains is allowed.
|
||||||
|
Examples:
|
||||||
|
- "qq.com"
|
||||||
|
- "qq.com|m.163.com"
|
||||||
|
|
||||||
|
count (number): Optional. Number of search results to return.
|
||||||
|
- Range: 1–50
|
||||||
|
- Default: 10
|
||||||
|
The actual number of returned results may be less than the
|
||||||
|
specified count.
|
||||||
|
"""
|
||||||
|
logger.info(f"web_searcher - search_from_bocha: {query}")
|
||||||
|
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||||
|
# websearch_link = cfg["provider_settings"].get("web_search_link", False)
|
||||||
|
if not cfg.get("provider_settings", {}).get("websearch_bocha_key", []):
|
||||||
|
raise ValueError("Error: BoCha API key is not configured in AstrBot.")
|
||||||
|
|
||||||
|
# build payload
|
||||||
|
payload = {
|
||||||
|
"query": query,
|
||||||
|
"count": count,
|
||||||
|
}
|
||||||
|
|
||||||
|
# freshness:时间范围
|
||||||
|
if freshness:
|
||||||
|
payload["freshness"] = freshness
|
||||||
|
|
||||||
|
# 是否返回摘要
|
||||||
|
payload["summary"] = summary
|
||||||
|
|
||||||
|
# include:限制搜索域
|
||||||
|
if include:
|
||||||
|
payload["include"] = include
|
||||||
|
|
||||||
|
# exclude:排除搜索域
|
||||||
|
if exclude:
|
||||||
|
payload["exclude"] = exclude
|
||||||
|
|
||||||
|
results = await self._web_search_bocha(cfg, payload)
|
||||||
|
if not results:
|
||||||
|
return "Error: BoCha web searcher does not return any results."
|
||||||
|
|
||||||
|
ret_ls = []
|
||||||
|
ref_uuid = str(uuid.uuid4())[:4]
|
||||||
|
for idx, result in enumerate(results, 1):
|
||||||
|
index = f"{ref_uuid}.{idx}"
|
||||||
|
ret_ls.append(
|
||||||
|
{
|
||||||
|
"title": f"{result.title}",
|
||||||
|
"url": f"{result.url}",
|
||||||
|
"snippet": f"{result.snippet}",
|
||||||
|
"index": index,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if result.favicon:
|
||||||
|
sp.temporary_cache["_ws_favicon"][result.url] = result.favicon
|
||||||
|
# ret = "\n".join(ret_ls)
|
||||||
|
ret = json.dumps({"results": ret_ls}, ensure_ascii=False)
|
||||||
|
return ret
|
||||||
|
|
||||||
@filter.on_llm_request(priority=-10000)
|
@filter.on_llm_request(priority=-10000)
|
||||||
async def edit_web_search_tools(
|
async def edit_web_search_tools(
|
||||||
self,
|
self,
|
||||||
event: AstrMessageEvent,
|
event: AstrMessageEvent,
|
||||||
req: ProviderRequest,
|
req: ProviderRequest,
|
||||||
):
|
) -> None:
|
||||||
"""Get the session conversation for the given event."""
|
"""Get the session conversation for the given event."""
|
||||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||||
prov_settings = cfg.get("provider_settings", {})
|
prov_settings = cfg.get("provider_settings", {})
|
||||||
@@ -419,6 +583,7 @@ class Main(star.Star):
|
|||||||
tool_set.remove_tool("web_search_tavily")
|
tool_set.remove_tool("web_search_tavily")
|
||||||
tool_set.remove_tool("tavily_extract_web_page")
|
tool_set.remove_tool("tavily_extract_web_page")
|
||||||
tool_set.remove_tool("AIsearch")
|
tool_set.remove_tool("AIsearch")
|
||||||
|
tool_set.remove_tool("web_search_bocha")
|
||||||
elif provider == "tavily":
|
elif provider == "tavily":
|
||||||
web_search_tavily = func_tool_mgr.get_func("web_search_tavily")
|
web_search_tavily = func_tool_mgr.get_func("web_search_tavily")
|
||||||
tavily_extract_web_page = func_tool_mgr.get_func("tavily_extract_web_page")
|
tavily_extract_web_page = func_tool_mgr.get_func("tavily_extract_web_page")
|
||||||
@@ -429,6 +594,7 @@ class Main(star.Star):
|
|||||||
tool_set.remove_tool("web_search")
|
tool_set.remove_tool("web_search")
|
||||||
tool_set.remove_tool("fetch_url")
|
tool_set.remove_tool("fetch_url")
|
||||||
tool_set.remove_tool("AIsearch")
|
tool_set.remove_tool("AIsearch")
|
||||||
|
tool_set.remove_tool("web_search_bocha")
|
||||||
elif provider == "baidu_ai_search":
|
elif provider == "baidu_ai_search":
|
||||||
try:
|
try:
|
||||||
await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin)
|
await self.ensure_baidu_ai_search_mcp(event.unified_msg_origin)
|
||||||
@@ -440,5 +606,15 @@ class Main(star.Star):
|
|||||||
tool_set.remove_tool("fetch_url")
|
tool_set.remove_tool("fetch_url")
|
||||||
tool_set.remove_tool("web_search_tavily")
|
tool_set.remove_tool("web_search_tavily")
|
||||||
tool_set.remove_tool("tavily_extract_web_page")
|
tool_set.remove_tool("tavily_extract_web_page")
|
||||||
|
tool_set.remove_tool("web_search_bocha")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}")
|
logger.error(f"Cannot Initialize Baidu AI Search MCP Server: {e}")
|
||||||
|
elif provider == "bocha":
|
||||||
|
web_search_bocha = func_tool_mgr.get_func("web_search_bocha")
|
||||||
|
if web_search_bocha:
|
||||||
|
tool_set.add_tool(web_search_bocha)
|
||||||
|
tool_set.remove_tool("web_search")
|
||||||
|
tool_set.remove_tool("fetch_url")
|
||||||
|
tool_set.remove_tool("AIsearch")
|
||||||
|
tool_set.remove_tool("web_search_tavily")
|
||||||
|
tool_set.remove_tool("tavily_extract_web_page")
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "4.14.4"
|
__version__ = "4.18.3"
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ def _get_nested_item(obj: dict[str, Any], path: str) -> Any:
|
|||||||
|
|
||||||
|
|
||||||
@click.group(name="conf")
|
@click.group(name="conf")
|
||||||
def conf():
|
def conf() -> None:
|
||||||
"""配置管理命令
|
"""配置管理命令
|
||||||
|
|
||||||
支持的配置项:
|
支持的配置项:
|
||||||
@@ -149,7 +149,7 @@ def conf():
|
|||||||
@conf.command(name="set")
|
@conf.command(name="set")
|
||||||
@click.argument("key")
|
@click.argument("key")
|
||||||
@click.argument("value")
|
@click.argument("value")
|
||||||
def set_config(key: str, value: str):
|
def set_config(key: str, value: str) -> None:
|
||||||
"""设置配置项的值"""
|
"""设置配置项的值"""
|
||||||
if key not in CONFIG_VALIDATORS:
|
if key not in CONFIG_VALIDATORS:
|
||||||
raise click.ClickException(f"不支持的配置项: {key}")
|
raise click.ClickException(f"不支持的配置项: {key}")
|
||||||
@@ -178,7 +178,7 @@ def set_config(key: str, value: str):
|
|||||||
|
|
||||||
@conf.command(name="get")
|
@conf.command(name="get")
|
||||||
@click.argument("key", required=False)
|
@click.argument("key", required=False)
|
||||||
def get_config(key: str | None = None):
|
def get_config(key: str | None = None) -> None:
|
||||||
"""获取配置项的值,不提供key则显示所有可配置项"""
|
"""获取配置项的值,不提供key则显示所有可配置项"""
|
||||||
config = _load_config()
|
config = _load_config()
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from ..utils import (
|
|||||||
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def plug():
|
def plug() -> None:
|
||||||
"""插件管理"""
|
"""插件管理"""
|
||||||
|
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ def _get_data_path() -> Path:
|
|||||||
return (base / "data").resolve()
|
return (base / "data").resolve()
|
||||||
|
|
||||||
|
|
||||||
def display_plugins(plugins, title=None, color=None):
|
def display_plugins(plugins, title=None, color=None) -> None:
|
||||||
if title:
|
if title:
|
||||||
click.echo(click.style(title, fg=color, bold=True))
|
click.echo(click.style(title, fg=color, bold=True))
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ def display_plugins(plugins, title=None, color=None):
|
|||||||
|
|
||||||
@plug.command()
|
@plug.command()
|
||||||
@click.argument("name")
|
@click.argument("name")
|
||||||
def new(name: str):
|
def new(name: str) -> None:
|
||||||
"""创建新插件"""
|
"""创建新插件"""
|
||||||
base_path = _get_data_path()
|
base_path = _get_data_path()
|
||||||
plug_path = base_path / "plugins" / name
|
plug_path = base_path / "plugins" / name
|
||||||
@@ -100,7 +100,7 @@ def new(name: str):
|
|||||||
|
|
||||||
@plug.command()
|
@plug.command()
|
||||||
@click.option("--all", "-a", is_flag=True, help="列出未安装的插件")
|
@click.option("--all", "-a", is_flag=True, help="列出未安装的插件")
|
||||||
def list(all: bool):
|
def list(all: bool) -> None:
|
||||||
"""列出插件"""
|
"""列出插件"""
|
||||||
base_path = _get_data_path()
|
base_path = _get_data_path()
|
||||||
plugins = build_plug_list(base_path / "plugins")
|
plugins = build_plug_list(base_path / "plugins")
|
||||||
@@ -141,7 +141,7 @@ def list(all: bool):
|
|||||||
@plug.command()
|
@plug.command()
|
||||||
@click.argument("name")
|
@click.argument("name")
|
||||||
@click.option("--proxy", help="代理服务器地址")
|
@click.option("--proxy", help="代理服务器地址")
|
||||||
def install(name: str, proxy: str | None):
|
def install(name: str, proxy: str | None) -> None:
|
||||||
"""安装插件"""
|
"""安装插件"""
|
||||||
base_path = _get_data_path()
|
base_path = _get_data_path()
|
||||||
plug_path = base_path / "plugins"
|
plug_path = base_path / "plugins"
|
||||||
@@ -164,7 +164,7 @@ def install(name: str, proxy: str | None):
|
|||||||
|
|
||||||
@plug.command()
|
@plug.command()
|
||||||
@click.argument("name")
|
@click.argument("name")
|
||||||
def remove(name: str):
|
def remove(name: str) -> None:
|
||||||
"""卸载插件"""
|
"""卸载插件"""
|
||||||
base_path = _get_data_path()
|
base_path = _get_data_path()
|
||||||
plugins = build_plug_list(base_path / "plugins")
|
plugins = build_plug_list(base_path / "plugins")
|
||||||
@@ -187,7 +187,7 @@ def remove(name: str):
|
|||||||
@plug.command()
|
@plug.command()
|
||||||
@click.argument("name", required=False)
|
@click.argument("name", required=False)
|
||||||
@click.option("--proxy", help="Github代理地址")
|
@click.option("--proxy", help="Github代理地址")
|
||||||
def update(name: str, proxy: str | None):
|
def update(name: str, proxy: str | None) -> None:
|
||||||
"""更新插件"""
|
"""更新插件"""
|
||||||
base_path = _get_data_path()
|
base_path = _get_data_path()
|
||||||
plug_path = base_path / "plugins"
|
plug_path = base_path / "plugins"
|
||||||
@@ -225,7 +225,7 @@ def update(name: str, proxy: str | None):
|
|||||||
|
|
||||||
@plug.command()
|
@plug.command()
|
||||||
@click.argument("query")
|
@click.argument("query")
|
||||||
def search(query: str):
|
def search(query: str) -> None:
|
||||||
"""搜索插件"""
|
"""搜索插件"""
|
||||||
base_path = _get_data_path()
|
base_path = _get_data_path()
|
||||||
plugins = build_plug_list(base_path / "plugins")
|
plugins = build_plug_list(base_path / "plugins")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from filelock import FileLock, Timeout
|
|||||||
from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root
|
from ..utils import check_astrbot_root, check_dashboard, get_astrbot_root
|
||||||
|
|
||||||
|
|
||||||
async def run_astrbot(astrbot_root: Path):
|
async def run_astrbot(astrbot_root: Path) -> None:
|
||||||
"""运行 AstrBot"""
|
"""运行 AstrBot"""
|
||||||
from astrbot.core import LogBroker, LogManager, db_helper, logger
|
from astrbot.core import LogBroker, LogManager, db_helper, logger
|
||||||
from astrbot.core.initial_loader import InitialLoader
|
from astrbot.core.initial_loader import InitialLoader
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class PluginStatus(str, Enum):
|
|||||||
NOT_PUBLISHED = "未发布"
|
NOT_PUBLISHED = "未发布"
|
||||||
|
|
||||||
|
|
||||||
def get_git_repo(url: str, target_path: Path, proxy: str | None = None):
|
def get_git_repo(url: str, target_path: Path, proxy: str | None = None) -> None:
|
||||||
"""从 Git 仓库下载代码并解压到指定路径"""
|
"""从 Git 仓库下载代码并解压到指定路径"""
|
||||||
temp_dir = Path(tempfile.mkdtemp())
|
temp_dir = Path(tempfile.mkdtemp())
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ class TruncateByTurnsCompressor:
|
|||||||
Truncates the message list by removing older turns.
|
Truncates the message list by removing older turns.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, truncate_turns: int = 1, compression_threshold: float = 0.82):
|
def __init__(
|
||||||
|
self, truncate_turns: int = 1, compression_threshold: float = 0.82
|
||||||
|
) -> None:
|
||||||
"""Initialize the truncate by turns compressor.
|
"""Initialize the truncate by turns compressor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -152,7 +154,7 @@ class LLMSummaryCompressor:
|
|||||||
keep_recent: int = 4,
|
keep_recent: int = 4,
|
||||||
instruction_text: str | None = None,
|
instruction_text: str | None = None,
|
||||||
compression_threshold: float = 0.82,
|
compression_threshold: float = 0.82,
|
||||||
):
|
) -> None:
|
||||||
"""Initialize the LLM summary compressor.
|
"""Initialize the LLM summary compressor.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class ContextManager:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
config: ContextConfig,
|
config: ContextConfig,
|
||||||
):
|
) -> None:
|
||||||
"""Initialize the context manager.
|
"""Initialize the context manager.
|
||||||
|
|
||||||
There are two strategies to handle context limit reached:
|
There are two strategies to handle context limit reached:
|
||||||
|
|||||||
@@ -4,19 +4,60 @@ from ..message import Message
|
|||||||
class ContextTruncator:
|
class ContextTruncator:
|
||||||
"""Context truncator."""
|
"""Context truncator."""
|
||||||
|
|
||||||
|
def _has_tool_calls(self, message: Message) -> bool:
|
||||||
|
"""Check if a message contains tool calls."""
|
||||||
|
return (
|
||||||
|
message.role == "assistant"
|
||||||
|
and message.tool_calls is not None
|
||||||
|
and len(message.tool_calls) > 0
|
||||||
|
)
|
||||||
|
|
||||||
def fix_messages(self, messages: list[Message]) -> list[Message]:
|
def fix_messages(self, messages: list[Message]) -> list[Message]:
|
||||||
fixed_messages = []
|
"""修复消息列表,确保 tool call 和 tool response 的配对关系有效。
|
||||||
for message in messages:
|
|
||||||
if message.role == "tool":
|
此方法确保:
|
||||||
# tool block 前面必须要有 user 和 assistant block
|
1. 每个 `tool` 消息前面都有一个包含 tool_calls 的 `assistant` 消息
|
||||||
if len(fixed_messages) < 2:
|
2. 每个包含 tool_calls 的 `assistant` 消息后面都有对应的 `tool` 响应
|
||||||
# 这种情况可能是上下文被截断导致的
|
|
||||||
# 我们直接将之前的上下文都清空
|
这是 OpenAI Chat Completions API 规范的要求(Gemini 对此执行严格检查)。
|
||||||
fixed_messages = []
|
"""
|
||||||
else:
|
if not messages:
|
||||||
fixed_messages.append(message)
|
return messages
|
||||||
else:
|
|
||||||
fixed_messages.append(message)
|
fixed_messages: list[Message] = []
|
||||||
|
pending_assistant: Message | None = None
|
||||||
|
pending_tools: list[Message] = []
|
||||||
|
|
||||||
|
def flush_pending_if_valid() -> None:
|
||||||
|
nonlocal pending_assistant, pending_tools
|
||||||
|
if pending_assistant is not None and pending_tools:
|
||||||
|
fixed_messages.append(pending_assistant)
|
||||||
|
fixed_messages.extend(pending_tools)
|
||||||
|
pending_assistant = None
|
||||||
|
pending_tools = []
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
if msg.role == "tool":
|
||||||
|
# 只有在有挂起的 assistant(tool_calls) 时才记录 tool 响应
|
||||||
|
if pending_assistant is not None:
|
||||||
|
pending_tools.append(msg)
|
||||||
|
# else: 孤立的 tool 消息,直接忽略
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self._has_tool_calls(msg):
|
||||||
|
# 遇到新的 assistant(tool_calls) 前,先处理旧的 pending 链
|
||||||
|
flush_pending_if_valid()
|
||||||
|
pending_assistant = msg
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 非 tool,且不含 tool_calls 的消息
|
||||||
|
# 先结束任何 pending 链,再正常追加
|
||||||
|
flush_pending_if_valid()
|
||||||
|
fixed_messages.append(msg)
|
||||||
|
|
||||||
|
# 结束时处理最后一个 pending 链
|
||||||
|
flush_pending_if_valid()
|
||||||
|
|
||||||
return fixed_messages
|
return fixed_messages
|
||||||
|
|
||||||
def truncate_by_turns(
|
def truncate_by_turns(
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ class HandoffTool(FunctionTool, Generic[TContext]):
|
|||||||
parameters: dict | None = None,
|
parameters: dict | None = None,
|
||||||
tool_description: str | None = None,
|
tool_description: str | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
) -> None:
|
||||||
self.agent = agent
|
|
||||||
|
|
||||||
# Avoid passing duplicate `description` to the FunctionTool dataclass.
|
# Avoid passing duplicate `description` to the FunctionTool dataclass.
|
||||||
# Some call sites (e.g. SubAgentOrchestrator) pass `description` via kwargs
|
# Some call sites (e.g. SubAgentOrchestrator) pass `description` via kwargs
|
||||||
@@ -34,6 +33,8 @@ class HandoffTool(FunctionTool, Generic[TContext]):
|
|||||||
# Optional provider override for this subagent. When set, the handoff
|
# Optional provider override for this subagent. When set, the handoff
|
||||||
# execution will use this chat provider id instead of the global/default.
|
# execution will use this chat provider id instead of the global/default.
|
||||||
self.provider_id: str | None = None
|
self.provider_id: str | None = None
|
||||||
|
# Note: Must assign after super().__init__() to prevent parent class from overriding this attribute
|
||||||
|
self.agent = agent
|
||||||
|
|
||||||
def default_parameters(self) -> dict:
|
def default_parameters(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -43,6 +44,19 @@ class HandoffTool(FunctionTool, Generic[TContext]):
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The input to be handed off to another agent. This should be a clear and concise request or task.",
|
"description": "The input to be handed off to another agent. This should be a clear and concise request or task.",
|
||||||
},
|
},
|
||||||
|
"image_urls": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "Optional: An array of image sources (public HTTP URLs or local file paths) used as references in multimodal tasks such as video generation.",
|
||||||
|
},
|
||||||
|
"background_task": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": (
|
||||||
|
"Defaults to false. "
|
||||||
|
"Set to true if the task may take noticeable time, involves external tools, or the user does not need to wait. "
|
||||||
|
"Use false only for quick, immediate tasks."
|
||||||
|
),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,22 +9,22 @@ from .run_context import ContextWrapper, TContext
|
|||||||
|
|
||||||
|
|
||||||
class BaseAgentRunHooks(Generic[TContext]):
|
class BaseAgentRunHooks(Generic[TContext]):
|
||||||
async def on_agent_begin(self, run_context: ContextWrapper[TContext]): ...
|
async def on_agent_begin(self, run_context: ContextWrapper[TContext]) -> None: ...
|
||||||
async def on_tool_start(
|
async def on_tool_start(
|
||||||
self,
|
self,
|
||||||
run_context: ContextWrapper[TContext],
|
run_context: ContextWrapper[TContext],
|
||||||
tool: FunctionTool,
|
tool: FunctionTool,
|
||||||
tool_args: dict | None,
|
tool_args: dict | None,
|
||||||
): ...
|
) -> None: ...
|
||||||
async def on_tool_end(
|
async def on_tool_end(
|
||||||
self,
|
self,
|
||||||
run_context: ContextWrapper[TContext],
|
run_context: ContextWrapper[TContext],
|
||||||
tool: FunctionTool,
|
tool: FunctionTool,
|
||||||
tool_args: dict | None,
|
tool_args: dict | None,
|
||||||
tool_result: mcp.types.CallToolResult | None,
|
tool_result: mcp.types.CallToolResult | None,
|
||||||
): ...
|
) -> None: ...
|
||||||
async def on_agent_done(
|
async def on_agent_done(
|
||||||
self,
|
self,
|
||||||
run_context: ContextWrapper[TContext],
|
run_context: ContextWrapper[TContext],
|
||||||
llm_response: LLMResponse,
|
llm_response: LLMResponse,
|
||||||
): ...
|
) -> None: ...
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ async def _quick_test_mcp_connection(config: dict) -> tuple[bool, str]:
|
|||||||
|
|
||||||
|
|
||||||
class MCPClient:
|
class MCPClient:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
# Initialize session and client objects
|
# Initialize session and client objects
|
||||||
self.session: mcp.ClientSession | None = None
|
self.session: mcp.ClientSession | None = None
|
||||||
self.exit_stack = AsyncExitStack()
|
self.exit_stack = AsyncExitStack()
|
||||||
@@ -126,7 +126,7 @@ class MCPClient:
|
|||||||
self._reconnect_lock = asyncio.Lock() # Lock for thread-safe reconnection
|
self._reconnect_lock = asyncio.Lock() # Lock for thread-safe reconnection
|
||||||
self._reconnecting: bool = False # For logging and debugging
|
self._reconnecting: bool = False # For logging and debugging
|
||||||
|
|
||||||
async def connect_to_server(self, mcp_server_config: dict, name: str):
|
async def connect_to_server(self, mcp_server_config: dict, name: str) -> None:
|
||||||
"""Connect to MCP server
|
"""Connect to MCP server
|
||||||
|
|
||||||
If `url` parameter exists:
|
If `url` parameter exists:
|
||||||
@@ -144,7 +144,7 @@ class MCPClient:
|
|||||||
|
|
||||||
cfg = _prepare_config(mcp_server_config.copy())
|
cfg = _prepare_config(mcp_server_config.copy())
|
||||||
|
|
||||||
def logging_callback(msg: str):
|
def logging_callback(msg: str) -> None:
|
||||||
# Handle MCP service error logs
|
# Handle MCP service error logs
|
||||||
print(f"MCP Server {name} Error: {msg}")
|
print(f"MCP Server {name} Error: {msg}")
|
||||||
self.server_errlogs.append(msg)
|
self.server_errlogs.append(msg)
|
||||||
@@ -214,7 +214,7 @@ class MCPClient:
|
|||||||
**cfg,
|
**cfg,
|
||||||
)
|
)
|
||||||
|
|
||||||
def callback(msg: str):
|
def callback(msg: str) -> None:
|
||||||
# Handle MCP service error logs
|
# Handle MCP service error logs
|
||||||
self.server_errlogs.append(msg)
|
self.server_errlogs.append(msg)
|
||||||
|
|
||||||
@@ -343,7 +343,7 @@ class MCPClient:
|
|||||||
|
|
||||||
return await _call_with_retry()
|
return await _call_with_retry()
|
||||||
|
|
||||||
async def cleanup(self):
|
async def cleanup(self) -> None:
|
||||||
"""Clean up resources including old exit stacks from reconnections"""
|
"""Clean up resources including old exit stacks from reconnections"""
|
||||||
# Close current exit stack
|
# Close current exit stack
|
||||||
try:
|
try:
|
||||||
@@ -365,7 +365,7 @@ class MCPTool(FunctionTool, Generic[TContext]):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, mcp_tool: mcp.Tool, mcp_client: MCPClient, mcp_server_name: str, **kwargs
|
self, mcp_tool: mcp.Tool, mcp_client: MCPClient, mcp_server_name: str, **kwargs
|
||||||
):
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name=mcp_tool.name,
|
name=mcp_tool.name,
|
||||||
description=mcp_tool.description or "",
|
description=mcp_tool.description or "",
|
||||||
|
|||||||
@@ -3,7 +3,13 @@
|
|||||||
|
|
||||||
from typing import Any, ClassVar, Literal, cast
|
from typing import Any, ClassVar, Literal, cast
|
||||||
|
|
||||||
from pydantic import BaseModel, GetCoreSchemaHandler, model_serializer, model_validator
|
from pydantic import (
|
||||||
|
BaseModel,
|
||||||
|
GetCoreSchemaHandler,
|
||||||
|
PrivateAttr,
|
||||||
|
model_serializer,
|
||||||
|
model_validator,
|
||||||
|
)
|
||||||
from pydantic_core import core_schema
|
from pydantic_core import core_schema
|
||||||
|
|
||||||
|
|
||||||
@@ -178,6 +184,8 @@ class Message(BaseModel):
|
|||||||
tool_call_id: str | None = None
|
tool_call_id: str | None = None
|
||||||
"""The ID of the tool call."""
|
"""The ID of the tool call."""
|
||||||
|
|
||||||
|
_no_save: bool = PrivateAttr(default=False)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_content_required(self):
|
def check_content_required(self):
|
||||||
# assistant + tool_calls is not None: allow content to be None
|
# assistant + tool_calls is not None: allow content to be None
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from astrbot.core import logger
|
|||||||
|
|
||||||
|
|
||||||
class CozeAPIClient:
|
class CozeAPIClient:
|
||||||
def __init__(self, api_key: str, api_base: str = "https://api.coze.cn"):
|
def __init__(self, api_key: str, api_base: str = "https://api.coze.cn") -> None:
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.api_base = api_base
|
self.api_base = api_base
|
||||||
self.session = None
|
self.session = None
|
||||||
@@ -277,7 +277,7 @@ class CozeAPIClient:
|
|||||||
logger.error(f"获取Coze消息列表失败: {e!s}")
|
logger.error(f"获取Coze消息列表失败: {e!s}")
|
||||||
raise Exception(f"获取Coze消息列表失败: {e!s}")
|
raise Exception(f"获取Coze消息列表失败: {e!s}")
|
||||||
|
|
||||||
async def close(self):
|
async def close(self) -> None:
|
||||||
"""关闭会话"""
|
"""关闭会话"""
|
||||||
if self.session:
|
if self.session:
|
||||||
await self.session.close()
|
await self.session.close()
|
||||||
@@ -288,7 +288,7 @@ if __name__ == "__main__":
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
|
||||||
async def test_coze_api_client():
|
async def test_coze_api_client() -> None:
|
||||||
api_key = os.getenv("COZE_API_KEY", "")
|
api_key = os.getenv("COZE_API_KEY", "")
|
||||||
bot_id = os.getenv("COZE_BOT_ID", "")
|
bot_id = os.getenv("COZE_BOT_ID", "")
|
||||||
client = CozeAPIClient(api_key=api_key)
|
client = CozeAPIClient(api_key=api_key)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class DashscopeAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
if isinstance(self.timeout, str):
|
if isinstance(self.timeout, str):
|
||||||
self.timeout = int(self.timeout)
|
self.timeout = int(self.timeout)
|
||||||
|
|
||||||
def has_rag_options(self):
|
def has_rag_options(self) -> bool:
|
||||||
"""判断是否有 RAG 选项
|
"""判断是否有 RAG 选项
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from astrbot.core.provider.entities import (
|
|||||||
LLMResponse,
|
LLMResponse,
|
||||||
ProviderRequest,
|
ProviderRequest,
|
||||||
)
|
)
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||||
from astrbot.core.utils.io import download_file
|
from astrbot.core.utils.io import download_file
|
||||||
|
|
||||||
from ...hooks import BaseAgentRunHooks
|
from ...hooks import BaseAgentRunHooks
|
||||||
@@ -291,8 +291,8 @@ class DifyAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
return Comp.Image(file=item["url"], url=item["url"])
|
return Comp.Image(file=item["url"], url=item["url"])
|
||||||
case "audio":
|
case "audio":
|
||||||
# 仅支持 wav
|
# 仅支持 wav
|
||||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
temp_dir = get_astrbot_temp_path()
|
||||||
path = os.path.join(temp_dir, f"{item['filename']}.wav")
|
path = os.path.join(temp_dir, f"dify_{item['filename']}.wav")
|
||||||
await download_file(item["url"], path)
|
await download_file(item["url"], path)
|
||||||
return Comp.Image(file=item["url"], url=item["url"])
|
return Comp.Image(file=item["url"], url=item["url"])
|
||||||
case "video":
|
case "video":
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ async def _stream_sse(resp: ClientResponse) -> AsyncGenerator[dict, None]:
|
|||||||
|
|
||||||
|
|
||||||
class DifyAPIClient:
|
class DifyAPIClient:
|
||||||
def __init__(self, api_key: str, api_base: str = "https://api.dify.ai/v1"):
|
def __init__(self, api_key: str, api_base: str = "https://api.dify.ai/v1") -> None:
|
||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.api_base = api_base
|
self.api_base = api_base
|
||||||
self.session = ClientSession(trust_env=True)
|
self.session = ClientSession(trust_env=True)
|
||||||
@@ -155,7 +155,7 @@ class DifyAPIClient:
|
|||||||
raise Exception(f"Dify 文件上传失败:{resp.status}. {text}")
|
raise Exception(f"Dify 文件上传失败:{resp.status}. {text}")
|
||||||
return await resp.json() # {"id": "xxx", ...}
|
return await resp.json() # {"id": "xxx", ...}
|
||||||
|
|
||||||
async def close(self):
|
async def close(self) -> None:
|
||||||
await self.session.close()
|
await self.session.close()
|
||||||
|
|
||||||
async def get_chat_convs(self, user: str, limit: int = 20):
|
async def get_chat_convs(self, user: str, limit: int = 20):
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import asyncio
|
||||||
import copy
|
import copy
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import typing as T
|
import typing as T
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from mcp.types import (
|
from mcp.types import (
|
||||||
BlobResourceContents,
|
BlobResourceContents,
|
||||||
@@ -14,8 +16,9 @@ from mcp.types import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from astrbot import logger
|
from astrbot import logger
|
||||||
from astrbot.core.agent.message import TextPart, ThinkPart
|
from astrbot.core.agent.message import ImageURLPart, TextPart, ThinkPart
|
||||||
from astrbot.core.agent.tool import ToolSet
|
from astrbot.core.agent.tool import ToolSet
|
||||||
|
from astrbot.core.agent.tool_image_cache import tool_image_cache
|
||||||
from astrbot.core.message.components import Json
|
from astrbot.core.message.components import Json
|
||||||
from astrbot.core.message.message_event_result import (
|
from astrbot.core.message.message_event_result import (
|
||||||
MessageChain,
|
MessageChain,
|
||||||
@@ -44,6 +47,36 @@ else:
|
|||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class _HandleFunctionToolsResult:
|
||||||
|
kind: T.Literal["message_chain", "tool_call_result_blocks", "cached_image"]
|
||||||
|
message_chain: MessageChain | None = None
|
||||||
|
tool_call_result_blocks: list[ToolCallMessageSegment] | None = None
|
||||||
|
cached_image: T.Any = None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_message_chain(cls, chain: MessageChain) -> "_HandleFunctionToolsResult":
|
||||||
|
return cls(kind="message_chain", message_chain=chain)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_tool_call_result_blocks(
|
||||||
|
cls, blocks: list[ToolCallMessageSegment]
|
||||||
|
) -> "_HandleFunctionToolsResult":
|
||||||
|
return cls(kind="tool_call_result_blocks", tool_call_result_blocks=blocks)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_cached_image(cls, image: T.Any) -> "_HandleFunctionToolsResult":
|
||||||
|
return cls(kind="cached_image", cached_image=image)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class FollowUpTicket:
|
||||||
|
seq: int
|
||||||
|
text: str
|
||||||
|
consumed: bool = False
|
||||||
|
resolved: asyncio.Event = field(default_factory=asyncio.Event)
|
||||||
|
|
||||||
|
|
||||||
class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
||||||
@override
|
@override
|
||||||
async def reset(
|
async def reset(
|
||||||
@@ -67,6 +100,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
custom_token_counter: TokenCounter | None = None,
|
custom_token_counter: TokenCounter | None = None,
|
||||||
custom_compressor: ContextCompressor | None = None,
|
custom_compressor: ContextCompressor | None = None,
|
||||||
tool_schema_mode: str | None = "full",
|
tool_schema_mode: str | None = "full",
|
||||||
|
fallback_providers: list[Provider] | None = None,
|
||||||
**kwargs: T.Any,
|
**kwargs: T.Any,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.req = request
|
self.req = request
|
||||||
@@ -96,11 +130,26 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
self.context_manager = ContextManager(self.context_config)
|
self.context_manager = ContextManager(self.context_config)
|
||||||
|
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
|
self.fallback_providers: list[Provider] = []
|
||||||
|
seen_provider_ids: set[str] = {str(provider.provider_config.get("id", ""))}
|
||||||
|
for fallback_provider in fallback_providers or []:
|
||||||
|
fallback_id = str(fallback_provider.provider_config.get("id", ""))
|
||||||
|
if fallback_provider is provider:
|
||||||
|
continue
|
||||||
|
if fallback_id and fallback_id in seen_provider_ids:
|
||||||
|
continue
|
||||||
|
self.fallback_providers.append(fallback_provider)
|
||||||
|
if fallback_id:
|
||||||
|
seen_provider_ids.add(fallback_id)
|
||||||
self.final_llm_resp = None
|
self.final_llm_resp = None
|
||||||
self._state = AgentState.IDLE
|
self._state = AgentState.IDLE
|
||||||
self.tool_executor = tool_executor
|
self.tool_executor = tool_executor
|
||||||
self.agent_hooks = agent_hooks
|
self.agent_hooks = agent_hooks
|
||||||
self.run_context = run_context
|
self.run_context = run_context
|
||||||
|
self._stop_requested = False
|
||||||
|
self._aborted = False
|
||||||
|
self._pending_follow_ups: list[FollowUpTicket] = []
|
||||||
|
self._follow_up_seq = 0
|
||||||
|
|
||||||
# These two are used for tool schema mode handling
|
# These two are used for tool schema mode handling
|
||||||
# We now have two modes:
|
# We now have two modes:
|
||||||
@@ -125,7 +174,10 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
messages = []
|
messages = []
|
||||||
# append existing messages in the run context
|
# append existing messages in the run context
|
||||||
for msg in request.contexts:
|
for msg in request.contexts:
|
||||||
messages.append(Message.model_validate(msg))
|
m = Message.model_validate(msg)
|
||||||
|
if isinstance(msg, dict) and msg.get("_no_save"):
|
||||||
|
m._no_save = True
|
||||||
|
messages.append(m)
|
||||||
if request.prompt is not None:
|
if request.prompt is not None:
|
||||||
m = await request.assemble_context()
|
m = await request.assemble_context()
|
||||||
messages.append(Message.model_validate(m))
|
messages.append(Message.model_validate(m))
|
||||||
@@ -139,16 +191,19 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
self.stats = AgentStats()
|
self.stats = AgentStats()
|
||||||
self.stats.start_time = time.time()
|
self.stats.start_time = time.time()
|
||||||
|
|
||||||
async def _iter_llm_responses(self) -> T.AsyncGenerator[LLMResponse, None]:
|
async def _iter_llm_responses(
|
||||||
|
self, *, include_model: bool = True
|
||||||
|
) -> T.AsyncGenerator[LLMResponse, None]:
|
||||||
"""Yields chunks *and* a final LLMResponse."""
|
"""Yields chunks *and* a final LLMResponse."""
|
||||||
payload = {
|
payload = {
|
||||||
"contexts": self.run_context.messages, # list[Message]
|
"contexts": self.run_context.messages, # list[Message]
|
||||||
"func_tool": self.req.func_tool,
|
"func_tool": self.req.func_tool,
|
||||||
"model": self.req.model, # NOTE: in fact, this arg is None in most cases
|
|
||||||
"session_id": self.req.session_id,
|
"session_id": self.req.session_id,
|
||||||
"extra_user_content_parts": self.req.extra_user_content_parts, # list[ContentPart]
|
"extra_user_content_parts": self.req.extra_user_content_parts, # list[ContentPart]
|
||||||
}
|
}
|
||||||
|
if include_model:
|
||||||
|
# For primary provider we keep explicit model selection if provided.
|
||||||
|
payload["model"] = self.req.model
|
||||||
if self.streaming:
|
if self.streaming:
|
||||||
stream = self.provider.text_chat_stream(**payload)
|
stream = self.provider.text_chat_stream(**payload)
|
||||||
async for resp in stream: # type: ignore
|
async for resp in stream: # type: ignore
|
||||||
@@ -156,6 +211,132 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
else:
|
else:
|
||||||
yield await self.provider.text_chat(**payload)
|
yield await self.provider.text_chat(**payload)
|
||||||
|
|
||||||
|
async def _iter_llm_responses_with_fallback(
|
||||||
|
self,
|
||||||
|
) -> T.AsyncGenerator[LLMResponse, None]:
|
||||||
|
"""Wrap _iter_llm_responses with provider fallback handling."""
|
||||||
|
candidates = [self.provider, *self.fallback_providers]
|
||||||
|
total_candidates = len(candidates)
|
||||||
|
last_exception: Exception | None = None
|
||||||
|
last_err_response: LLMResponse | None = None
|
||||||
|
|
||||||
|
for idx, candidate in enumerate(candidates):
|
||||||
|
candidate_id = candidate.provider_config.get("id", "<unknown>")
|
||||||
|
is_last_candidate = idx == total_candidates - 1
|
||||||
|
if idx > 0:
|
||||||
|
logger.warning(
|
||||||
|
"Switched from %s to fallback chat provider: %s",
|
||||||
|
self.provider.provider_config.get("id", "<unknown>"),
|
||||||
|
candidate_id,
|
||||||
|
)
|
||||||
|
self.provider = candidate
|
||||||
|
has_stream_output = False
|
||||||
|
try:
|
||||||
|
async for resp in self._iter_llm_responses(include_model=idx == 0):
|
||||||
|
if resp.is_chunk:
|
||||||
|
has_stream_output = True
|
||||||
|
yield resp
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
resp.role == "err"
|
||||||
|
and not has_stream_output
|
||||||
|
and (not is_last_candidate)
|
||||||
|
):
|
||||||
|
last_err_response = resp
|
||||||
|
logger.warning(
|
||||||
|
"Chat Model %s returns error response, trying fallback to next provider.",
|
||||||
|
candidate_id,
|
||||||
|
)
|
||||||
|
break
|
||||||
|
|
||||||
|
yield resp
|
||||||
|
return
|
||||||
|
|
||||||
|
if has_stream_output:
|
||||||
|
return
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
last_exception = exc
|
||||||
|
logger.warning(
|
||||||
|
"Chat Model %s request error: %s",
|
||||||
|
candidate_id,
|
||||||
|
exc,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if last_err_response:
|
||||||
|
yield last_err_response
|
||||||
|
return
|
||||||
|
if last_exception:
|
||||||
|
yield LLMResponse(
|
||||||
|
role="err",
|
||||||
|
completion_text=(
|
||||||
|
"All chat models failed: "
|
||||||
|
f"{type(last_exception).__name__}: {last_exception}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
yield LLMResponse(
|
||||||
|
role="err",
|
||||||
|
completion_text="All available chat models are unavailable.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def _simple_print_message_role(self, tag: str = ""):
|
||||||
|
roles = []
|
||||||
|
for message in self.run_context.messages:
|
||||||
|
roles.append(message.role)
|
||||||
|
logger.debug(f"{tag} RunCtx.messages -> [{len(roles)}] {','.join(roles)}")
|
||||||
|
|
||||||
|
def follow_up(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
message_text: str,
|
||||||
|
) -> FollowUpTicket | None:
|
||||||
|
"""Queue a follow-up message for the next tool result."""
|
||||||
|
if self.done():
|
||||||
|
return None
|
||||||
|
text = (message_text or "").strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
ticket = FollowUpTicket(seq=self._follow_up_seq, text=text)
|
||||||
|
self._follow_up_seq += 1
|
||||||
|
self._pending_follow_ups.append(ticket)
|
||||||
|
return ticket
|
||||||
|
|
||||||
|
def _resolve_unconsumed_follow_ups(self) -> None:
|
||||||
|
if not self._pending_follow_ups:
|
||||||
|
return
|
||||||
|
follow_ups = self._pending_follow_ups
|
||||||
|
self._pending_follow_ups = []
|
||||||
|
for ticket in follow_ups:
|
||||||
|
ticket.resolved.set()
|
||||||
|
|
||||||
|
def _consume_follow_up_notice(self) -> str:
|
||||||
|
if not self._pending_follow_ups:
|
||||||
|
return ""
|
||||||
|
follow_ups = self._pending_follow_ups
|
||||||
|
self._pending_follow_ups = []
|
||||||
|
for ticket in follow_ups:
|
||||||
|
ticket.consumed = True
|
||||||
|
ticket.resolved.set()
|
||||||
|
follow_up_lines = "\n".join(
|
||||||
|
f"{idx}. {ticket.text}" for idx, ticket in enumerate(follow_ups, start=1)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
"\n\n[SYSTEM NOTICE] User sent follow-up messages while tool execution "
|
||||||
|
"was in progress. Prioritize these follow-up instructions in your next "
|
||||||
|
"actions. In your very next action, briefly acknowledge to the user "
|
||||||
|
"that their follow-up message(s) were received before continuing.\n"
|
||||||
|
f"{follow_up_lines}"
|
||||||
|
)
|
||||||
|
|
||||||
|
def _merge_follow_up_notice(self, content: str) -> str:
|
||||||
|
notice = self._consume_follow_up_notice()
|
||||||
|
if not notice:
|
||||||
|
return content
|
||||||
|
return f"{content}{notice}"
|
||||||
|
|
||||||
@override
|
@override
|
||||||
async def step(self):
|
async def step(self):
|
||||||
"""Process a single step of the agent.
|
"""Process a single step of the agent.
|
||||||
@@ -176,11 +357,13 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
|
|
||||||
# do truncate and compress
|
# do truncate and compress
|
||||||
token_usage = self.req.conversation.token_usage if self.req.conversation else 0
|
token_usage = self.req.conversation.token_usage if self.req.conversation else 0
|
||||||
|
self._simple_print_message_role("[BefCompact]")
|
||||||
self.run_context.messages = await self.context_manager.process(
|
self.run_context.messages = await self.context_manager.process(
|
||||||
self.run_context.messages, trusted_token_usage=token_usage
|
self.run_context.messages, trusted_token_usage=token_usage
|
||||||
)
|
)
|
||||||
|
self._simple_print_message_role("[AftCompact]")
|
||||||
|
|
||||||
async for llm_response in self._iter_llm_responses():
|
async for llm_response in self._iter_llm_responses_with_fallback():
|
||||||
if llm_response.is_chunk:
|
if llm_response.is_chunk:
|
||||||
# update ttft
|
# update ttft
|
||||||
if self.stats.time_to_first_token == 0:
|
if self.stats.time_to_first_token == 0:
|
||||||
@@ -207,6 +390,14 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
if self._stop_requested:
|
||||||
|
llm_resp_result = LLMResponse(
|
||||||
|
role="assistant",
|
||||||
|
completion_text="[SYSTEM: User actively interrupted the response generation. Partial output before interruption is preserved.]",
|
||||||
|
reasoning_content=llm_response.reasoning_content,
|
||||||
|
reasoning_signature=llm_response.reasoning_signature,
|
||||||
|
)
|
||||||
|
break
|
||||||
continue
|
continue
|
||||||
llm_resp_result = llm_response
|
llm_resp_result = llm_response
|
||||||
|
|
||||||
@@ -218,6 +409,49 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
break # got final response
|
break # got final response
|
||||||
|
|
||||||
if not llm_resp_result:
|
if not llm_resp_result:
|
||||||
|
if self._stop_requested:
|
||||||
|
llm_resp_result = LLMResponse(role="assistant", completion_text="")
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._stop_requested:
|
||||||
|
logger.info("Agent execution was requested to stop by user.")
|
||||||
|
llm_resp = llm_resp_result
|
||||||
|
if llm_resp.role != "assistant":
|
||||||
|
llm_resp = LLMResponse(
|
||||||
|
role="assistant",
|
||||||
|
completion_text="[SYSTEM: User actively interrupted the response generation. Partial output before interruption is preserved.]",
|
||||||
|
)
|
||||||
|
self.final_llm_resp = llm_resp
|
||||||
|
self._aborted = True
|
||||||
|
self._transition_state(AgentState.DONE)
|
||||||
|
self.stats.end_time = time.time()
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if llm_resp.reasoning_content or llm_resp.reasoning_signature:
|
||||||
|
parts.append(
|
||||||
|
ThinkPart(
|
||||||
|
think=llm_resp.reasoning_content,
|
||||||
|
encrypted=llm_resp.reasoning_signature,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if llm_resp.completion_text:
|
||||||
|
parts.append(TextPart(text=llm_resp.completion_text))
|
||||||
|
if parts:
|
||||||
|
self.run_context.messages.append(
|
||||||
|
Message(role="assistant", content=parts)
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.agent_hooks.on_agent_done(self.run_context, llm_resp)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
|
||||||
|
|
||||||
|
yield AgentResponse(
|
||||||
|
type="aborted",
|
||||||
|
data=AgentResponseData(chain=MessageChain(type="aborted")),
|
||||||
|
)
|
||||||
|
self._resolve_unconsumed_follow_ups()
|
||||||
return
|
return
|
||||||
|
|
||||||
# 处理 LLM 响应
|
# 处理 LLM 响应
|
||||||
@@ -228,6 +462,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
self.final_llm_resp = llm_resp
|
self.final_llm_resp = llm_resp
|
||||||
self.stats.end_time = time.time()
|
self.stats.end_time = time.time()
|
||||||
self._transition_state(AgentState.ERROR)
|
self._transition_state(AgentState.ERROR)
|
||||||
|
self._resolve_unconsumed_follow_ups()
|
||||||
yield AgentResponse(
|
yield AgentResponse(
|
||||||
type="err",
|
type="err",
|
||||||
data=AgentResponseData(
|
data=AgentResponseData(
|
||||||
@@ -236,6 +471,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if not llm_resp.tools_call_name:
|
if not llm_resp.tools_call_name:
|
||||||
# 如果没有工具调用,转换到完成状态
|
# 如果没有工具调用,转换到完成状态
|
||||||
@@ -254,6 +490,10 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
)
|
)
|
||||||
if llm_resp.completion_text:
|
if llm_resp.completion_text:
|
||||||
parts.append(TextPart(text=llm_resp.completion_text))
|
parts.append(TextPart(text=llm_resp.completion_text))
|
||||||
|
if len(parts) == 0:
|
||||||
|
logger.warning(
|
||||||
|
"LLM returned empty assistant message with no tool calls."
|
||||||
|
)
|
||||||
self.run_context.messages.append(Message(role="assistant", content=parts))
|
self.run_context.messages.append(Message(role="assistant", content=parts))
|
||||||
|
|
||||||
# call the on_agent_done hook
|
# call the on_agent_done hook
|
||||||
@@ -261,6 +501,7 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
await self.agent_hooks.on_agent_done(self.run_context, llm_resp)
|
await self.agent_hooks.on_agent_done(self.run_context, llm_resp)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
|
logger.error(f"Error in on_agent_done hook: {e}", exc_info=True)
|
||||||
|
self._resolve_unconsumed_follow_ups()
|
||||||
|
|
||||||
# 返回 LLM 结果
|
# 返回 LLM 结果
|
||||||
if llm_resp.result_chain:
|
if llm_resp.result_chain:
|
||||||
@@ -282,20 +523,27 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
llm_resp, _ = await self._resolve_tool_exec(llm_resp)
|
llm_resp, _ = await self._resolve_tool_exec(llm_resp)
|
||||||
|
|
||||||
tool_call_result_blocks = []
|
tool_call_result_blocks = []
|
||||||
|
cached_images = [] # Collect cached images for LLM visibility
|
||||||
async for result in self._handle_function_tools(self.req, llm_resp):
|
async for result in self._handle_function_tools(self.req, llm_resp):
|
||||||
if isinstance(result, list):
|
if result.kind == "tool_call_result_blocks":
|
||||||
tool_call_result_blocks = result
|
if result.tool_call_result_blocks is not None:
|
||||||
elif isinstance(result, MessageChain):
|
tool_call_result_blocks = result.tool_call_result_blocks
|
||||||
if result.type is None:
|
elif result.kind == "cached_image":
|
||||||
|
if result.cached_image is not None:
|
||||||
|
# Collect cached image info
|
||||||
|
cached_images.append(result.cached_image)
|
||||||
|
elif result.kind == "message_chain":
|
||||||
|
chain = result.message_chain
|
||||||
|
if chain is None or chain.type is None:
|
||||||
# should not happen
|
# should not happen
|
||||||
continue
|
continue
|
||||||
if result.type == "tool_direct_result":
|
if chain.type == "tool_direct_result":
|
||||||
ar_type = "tool_call_result"
|
ar_type = "tool_call_result"
|
||||||
else:
|
else:
|
||||||
ar_type = result.type
|
ar_type = chain.type
|
||||||
yield AgentResponse(
|
yield AgentResponse(
|
||||||
type=ar_type,
|
type=ar_type,
|
||||||
data=AgentResponseData(chain=result),
|
data=AgentResponseData(chain=chain),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 将结果添加到上下文中
|
# 将结果添加到上下文中
|
||||||
@@ -309,6 +557,8 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
)
|
)
|
||||||
if llm_resp.completion_text:
|
if llm_resp.completion_text:
|
||||||
parts.append(TextPart(text=llm_resp.completion_text))
|
parts.append(TextPart(text=llm_resp.completion_text))
|
||||||
|
if len(parts) == 0:
|
||||||
|
parts = None
|
||||||
tool_calls_result = ToolCallsResult(
|
tool_calls_result = ToolCallsResult(
|
||||||
tool_calls_info=AssistantMessageSegment(
|
tool_calls_info=AssistantMessageSegment(
|
||||||
tool_calls=llm_resp.to_openai_to_calls_model(),
|
tool_calls=llm_resp.to_openai_to_calls_model(),
|
||||||
@@ -321,6 +571,41 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
tool_calls_result.to_openai_messages_model()
|
tool_calls_result.to_openai_messages_model()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If there are cached images and the model supports image input,
|
||||||
|
# append a user message with images so LLM can see them
|
||||||
|
if cached_images:
|
||||||
|
modalities = self.provider.provider_config.get("modalities", [])
|
||||||
|
supports_image = "image" in modalities
|
||||||
|
if supports_image:
|
||||||
|
# Build user message with images for LLM to review
|
||||||
|
image_parts = []
|
||||||
|
for cached_img in cached_images:
|
||||||
|
img_data = tool_image_cache.get_image_base64_by_path(
|
||||||
|
cached_img.file_path, cached_img.mime_type
|
||||||
|
)
|
||||||
|
if img_data:
|
||||||
|
base64_data, mime_type = img_data
|
||||||
|
image_parts.append(
|
||||||
|
TextPart(
|
||||||
|
text=f"[Image from tool '{cached_img.tool_name}', path='{cached_img.file_path}']"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
image_parts.append(
|
||||||
|
ImageURLPart(
|
||||||
|
image_url=ImageURLPart.ImageURL(
|
||||||
|
url=f"data:{mime_type};base64,{base64_data}",
|
||||||
|
id=cached_img.file_path,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if image_parts:
|
||||||
|
self.run_context.messages.append(
|
||||||
|
Message(role="user", content=image_parts)
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"Appended {len(cached_images)} cached image(s) to context for LLM review"
|
||||||
|
)
|
||||||
|
|
||||||
self.req.append_tool_calls_result(tool_calls_result)
|
self.req.append_tool_calls_result(tool_calls_result)
|
||||||
|
|
||||||
async def step_until_done(
|
async def step_until_done(
|
||||||
@@ -356,29 +641,40 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
self,
|
self,
|
||||||
req: ProviderRequest,
|
req: ProviderRequest,
|
||||||
llm_response: LLMResponse,
|
llm_response: LLMResponse,
|
||||||
) -> T.AsyncGenerator[MessageChain | list[ToolCallMessageSegment], None]:
|
) -> T.AsyncGenerator[_HandleFunctionToolsResult, None]:
|
||||||
"""处理函数工具调用。"""
|
"""处理函数工具调用。"""
|
||||||
tool_call_result_blocks: list[ToolCallMessageSegment] = []
|
tool_call_result_blocks: list[ToolCallMessageSegment] = []
|
||||||
logger.info(f"Agent 使用工具: {llm_response.tools_call_name}")
|
logger.info(f"Agent 使用工具: {llm_response.tools_call_name}")
|
||||||
|
|
||||||
|
def _append_tool_call_result(tool_call_id: str, content: str) -> None:
|
||||||
|
tool_call_result_blocks.append(
|
||||||
|
ToolCallMessageSegment(
|
||||||
|
role="tool",
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
content=self._merge_follow_up_notice(content),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# 执行函数调用
|
# 执行函数调用
|
||||||
for func_tool_name, func_tool_args, func_tool_id in zip(
|
for func_tool_name, func_tool_args, func_tool_id in zip(
|
||||||
llm_response.tools_call_name,
|
llm_response.tools_call_name,
|
||||||
llm_response.tools_call_args,
|
llm_response.tools_call_args,
|
||||||
llm_response.tools_call_ids,
|
llm_response.tools_call_ids,
|
||||||
):
|
):
|
||||||
yield MessageChain(
|
yield _HandleFunctionToolsResult.from_message_chain(
|
||||||
type="tool_call",
|
MessageChain(
|
||||||
chain=[
|
type="tool_call",
|
||||||
Json(
|
chain=[
|
||||||
data={
|
Json(
|
||||||
"id": func_tool_id,
|
data={
|
||||||
"name": func_tool_name,
|
"id": func_tool_id,
|
||||||
"args": func_tool_args,
|
"name": func_tool_name,
|
||||||
"ts": time.time(),
|
"args": func_tool_args,
|
||||||
}
|
"ts": time.time(),
|
||||||
)
|
}
|
||||||
],
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
if not req.func_tool:
|
if not req.func_tool:
|
||||||
@@ -398,12 +694,9 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
|
|
||||||
if not func_tool:
|
if not func_tool:
|
||||||
logger.warning(f"未找到指定的工具: {func_tool_name},将跳过。")
|
logger.warning(f"未找到指定的工具: {func_tool_name},将跳过。")
|
||||||
tool_call_result_blocks.append(
|
_append_tool_call_result(
|
||||||
ToolCallMessageSegment(
|
func_tool_id,
|
||||||
role="tool",
|
f"error: Tool {func_tool_name} not found.",
|
||||||
tool_call_id=func_tool_id,
|
|
||||||
content=f"error: Tool {func_tool_name} not found.",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -456,56 +749,67 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
res = resp
|
res = resp
|
||||||
_final_resp = resp
|
_final_resp = resp
|
||||||
if isinstance(res.content[0], TextContent):
|
if isinstance(res.content[0], TextContent):
|
||||||
tool_call_result_blocks.append(
|
_append_tool_call_result(
|
||||||
ToolCallMessageSegment(
|
func_tool_id,
|
||||||
role="tool",
|
res.content[0].text,
|
||||||
tool_call_id=func_tool_id,
|
|
||||||
content=res.content[0].text,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
elif isinstance(res.content[0], ImageContent):
|
elif isinstance(res.content[0], ImageContent):
|
||||||
tool_call_result_blocks.append(
|
# Cache the image instead of sending directly
|
||||||
ToolCallMessageSegment(
|
cached_img = tool_image_cache.save_image(
|
||||||
role="tool",
|
base64_data=res.content[0].data,
|
||||||
tool_call_id=func_tool_id,
|
tool_call_id=func_tool_id,
|
||||||
content="The tool has successfully returned an image and sent directly to the user. You can describe it in your next response.",
|
tool_name=func_tool_name,
|
||||||
|
index=0,
|
||||||
|
mime_type=res.content[0].mimeType or "image/png",
|
||||||
|
)
|
||||||
|
_append_tool_call_result(
|
||||||
|
func_tool_id,
|
||||||
|
(
|
||||||
|
f"Image returned and cached at path='{cached_img.file_path}'. "
|
||||||
|
f"Review the image below. Use send_message_to_user to send it to the user if satisfied, "
|
||||||
|
f"with type='image' and path='{cached_img.file_path}'."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
yield MessageChain(type="tool_direct_result").base64_image(
|
# Yield image info for LLM visibility (will be handled in step())
|
||||||
res.content[0].data,
|
yield _HandleFunctionToolsResult.from_cached_image(
|
||||||
|
cached_img
|
||||||
)
|
)
|
||||||
elif isinstance(res.content[0], EmbeddedResource):
|
elif isinstance(res.content[0], EmbeddedResource):
|
||||||
resource = res.content[0].resource
|
resource = res.content[0].resource
|
||||||
if isinstance(resource, TextResourceContents):
|
if isinstance(resource, TextResourceContents):
|
||||||
tool_call_result_blocks.append(
|
_append_tool_call_result(
|
||||||
ToolCallMessageSegment(
|
func_tool_id,
|
||||||
role="tool",
|
resource.text,
|
||||||
tool_call_id=func_tool_id,
|
|
||||||
content=resource.text,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
elif (
|
elif (
|
||||||
isinstance(resource, BlobResourceContents)
|
isinstance(resource, BlobResourceContents)
|
||||||
and resource.mimeType
|
and resource.mimeType
|
||||||
and resource.mimeType.startswith("image/")
|
and resource.mimeType.startswith("image/")
|
||||||
):
|
):
|
||||||
tool_call_result_blocks.append(
|
# Cache the image instead of sending directly
|
||||||
ToolCallMessageSegment(
|
cached_img = tool_image_cache.save_image(
|
||||||
role="tool",
|
base64_data=resource.blob,
|
||||||
tool_call_id=func_tool_id,
|
tool_call_id=func_tool_id,
|
||||||
content="The tool has successfully returned an image and sent directly to the user. You can describe it in your next response.",
|
tool_name=func_tool_name,
|
||||||
|
index=0,
|
||||||
|
mime_type=resource.mimeType,
|
||||||
|
)
|
||||||
|
_append_tool_call_result(
|
||||||
|
func_tool_id,
|
||||||
|
(
|
||||||
|
f"Image returned and cached at path='{cached_img.file_path}'. "
|
||||||
|
f"Review the image below. Use send_message_to_user to send it to the user if satisfied, "
|
||||||
|
f"with type='image' and path='{cached_img.file_path}'."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
yield MessageChain(
|
# Yield image info for LLM visibility
|
||||||
type="tool_direct_result",
|
yield _HandleFunctionToolsResult.from_cached_image(
|
||||||
).base64_image(resource.blob)
|
cached_img
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
tool_call_result_blocks.append(
|
_append_tool_call_result(
|
||||||
ToolCallMessageSegment(
|
func_tool_id,
|
||||||
role="tool",
|
"The tool has returned a data type that is not supported.",
|
||||||
tool_call_id=func_tool_id,
|
|
||||||
content="The tool has returned a data type that is not supported.",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
elif resp is None:
|
elif resp is None:
|
||||||
@@ -517,24 +821,18 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
)
|
)
|
||||||
self._transition_state(AgentState.DONE)
|
self._transition_state(AgentState.DONE)
|
||||||
self.stats.end_time = time.time()
|
self.stats.end_time = time.time()
|
||||||
tool_call_result_blocks.append(
|
_append_tool_call_result(
|
||||||
ToolCallMessageSegment(
|
func_tool_id,
|
||||||
role="tool",
|
"The tool has no return value, or has sent the result directly to the user.",
|
||||||
tool_call_id=func_tool_id,
|
|
||||||
content="The tool has no return value, or has sent the result directly to the user.",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# 不应该出现其他类型
|
# 不应该出现其他类型
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Tool 返回了不支持的类型: {type(resp)}。",
|
f"Tool 返回了不支持的类型: {type(resp)}。",
|
||||||
)
|
)
|
||||||
tool_call_result_blocks.append(
|
_append_tool_call_result(
|
||||||
ToolCallMessageSegment(
|
func_tool_id,
|
||||||
role="tool",
|
"*The tool has returned an unsupported type. Please tell the user to check the definition and implementation of this tool.*",
|
||||||
tool_call_id=func_tool_id,
|
|
||||||
content="*The tool has returned an unsupported type. Please tell the user to check the definition and implementation of this tool.*",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -548,34 +846,35 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
logger.error(f"Error in on_tool_end hook: {e}", exc_info=True)
|
logger.error(f"Error in on_tool_end hook: {e}", exc_info=True)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(traceback.format_exc())
|
logger.warning(traceback.format_exc())
|
||||||
tool_call_result_blocks.append(
|
_append_tool_call_result(
|
||||||
ToolCallMessageSegment(
|
func_tool_id,
|
||||||
role="tool",
|
f"error: {e!s}",
|
||||||
tool_call_id=func_tool_id,
|
|
||||||
content=f"error: {e!s}",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# yield the last tool call result
|
# yield the last tool call result
|
||||||
if tool_call_result_blocks:
|
if tool_call_result_blocks:
|
||||||
last_tcr_content = str(tool_call_result_blocks[-1].content)
|
last_tcr_content = str(tool_call_result_blocks[-1].content)
|
||||||
yield MessageChain(
|
yield _HandleFunctionToolsResult.from_message_chain(
|
||||||
type="tool_call_result",
|
MessageChain(
|
||||||
chain=[
|
type="tool_call_result",
|
||||||
Json(
|
chain=[
|
||||||
data={
|
Json(
|
||||||
"id": func_tool_id,
|
data={
|
||||||
"ts": time.time(),
|
"id": func_tool_id,
|
||||||
"result": last_tcr_content,
|
"ts": time.time(),
|
||||||
}
|
"result": last_tcr_content,
|
||||||
)
|
}
|
||||||
],
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}")
|
logger.info(f"Tool `{func_tool_name}` Result: {last_tcr_content}")
|
||||||
|
|
||||||
# 处理函数调用响应
|
# 处理函数调用响应
|
||||||
if tool_call_result_blocks:
|
if tool_call_result_blocks:
|
||||||
yield tool_call_result_blocks
|
yield _HandleFunctionToolsResult.from_tool_call_result_blocks(
|
||||||
|
tool_call_result_blocks
|
||||||
|
)
|
||||||
|
|
||||||
def _build_tool_requery_context(
|
def _build_tool_requery_context(
|
||||||
self, tool_names: list[str]
|
self, tool_names: list[str]
|
||||||
@@ -646,5 +945,11 @@ class ToolLoopAgentRunner(BaseAgentRunner[TContext]):
|
|||||||
"""检查 Agent 是否已完成工作"""
|
"""检查 Agent 是否已完成工作"""
|
||||||
return self._state in (AgentState.DONE, AgentState.ERROR)
|
return self._state in (AgentState.DONE, AgentState.ERROR)
|
||||||
|
|
||||||
|
def request_stop(self) -> None:
|
||||||
|
self._stop_requested = True
|
||||||
|
|
||||||
|
def was_aborted(self) -> bool:
|
||||||
|
return self._aborted
|
||||||
|
|
||||||
def get_final_llm_resp(self) -> LLMResponse | None:
|
def get_final_llm_resp(self) -> LLMResponse | None:
|
||||||
return self.final_llm_resp
|
return self.final_llm_resp
|
||||||
|
|||||||
+25
-12
@@ -64,7 +64,7 @@ class FunctionTool(ToolSchema, Generic[TContext]):
|
|||||||
with a task identifier while the real work continues asynchronously.
|
with a task identifier while the real work continues asynchronously.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
|
return f"FuncTool(name={self.name}, parameters={self.parameters}, description={self.description})"
|
||||||
|
|
||||||
async def call(self, context: ContextWrapper[TContext], **kwargs) -> ToolExecResult:
|
async def call(self, context: ContextWrapper[TContext], **kwargs) -> ToolExecResult:
|
||||||
@@ -88,7 +88,7 @@ class ToolSet:
|
|||||||
"""Check if the tool set is empty."""
|
"""Check if the tool set is empty."""
|
||||||
return len(self.tools) == 0
|
return len(self.tools) == 0
|
||||||
|
|
||||||
def add_tool(self, tool: FunctionTool):
|
def add_tool(self, tool: FunctionTool) -> None:
|
||||||
"""Add a tool to the set."""
|
"""Add a tool to the set."""
|
||||||
# 检查是否已存在同名工具
|
# 检查是否已存在同名工具
|
||||||
for i, existing_tool in enumerate(self.tools):
|
for i, existing_tool in enumerate(self.tools):
|
||||||
@@ -97,7 +97,7 @@ class ToolSet:
|
|||||||
return
|
return
|
||||||
self.tools.append(tool)
|
self.tools.append(tool)
|
||||||
|
|
||||||
def remove_tool(self, name: str):
|
def remove_tool(self, name: str) -> None:
|
||||||
"""Remove a tool by its name."""
|
"""Remove a tool by its name."""
|
||||||
self.tools = [tool for tool in self.tools if tool.name != name]
|
self.tools = [tool for tool in self.tools if tool.name != name]
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ class ToolSet:
|
|||||||
func_args: list,
|
func_args: list,
|
||||||
desc: str,
|
desc: str,
|
||||||
handler: Callable[..., Awaitable[Any]],
|
handler: Callable[..., Awaitable[Any]],
|
||||||
):
|
) -> None:
|
||||||
"""Add a function tool to the set."""
|
"""Add a function tool to the set."""
|
||||||
params = {
|
params = {
|
||||||
"type": "object", # hard-coded here
|
"type": "object", # hard-coded here
|
||||||
@@ -176,7 +176,7 @@ class ToolSet:
|
|||||||
self.add_tool(_func)
|
self.add_tool(_func)
|
||||||
|
|
||||||
@deprecated(reason="Use remove_tool() instead", version="4.0.0")
|
@deprecated(reason="Use remove_tool() instead", version="4.0.0")
|
||||||
def remove_func(self, name: str):
|
def remove_func(self, name: str) -> None:
|
||||||
"""Remove a function tool by its name."""
|
"""Remove a function tool by its name."""
|
||||||
self.remove_tool(name)
|
self.remove_tool(name)
|
||||||
|
|
||||||
@@ -246,8 +246,18 @@ class ToolSet:
|
|||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
if "type" in schema and schema["type"] in supported_types:
|
# Avoid side effects by not modifying the original schema
|
||||||
result["type"] = schema["type"]
|
origin_type = schema.get("type")
|
||||||
|
target_type = origin_type
|
||||||
|
|
||||||
|
# Compatibility fix: Gemini API expects 'type' to be a string (enum),
|
||||||
|
# but standard JSON Schema (MCP) allows lists (e.g. ["string", "null"]).
|
||||||
|
# We fallback to the first non-null type.
|
||||||
|
if isinstance(origin_type, list):
|
||||||
|
target_type = next((t for t in origin_type if t != "null"), "string")
|
||||||
|
|
||||||
|
if target_type in supported_types:
|
||||||
|
result["type"] = target_type
|
||||||
if "format" in schema and schema["format"] in supported_formats.get(
|
if "format" in schema and schema["format"] in supported_formats.get(
|
||||||
result["type"],
|
result["type"],
|
||||||
set(),
|
set(),
|
||||||
@@ -275,6 +285,9 @@ class ToolSet:
|
|||||||
prop_value = convert_schema(value)
|
prop_value = convert_schema(value)
|
||||||
if "default" in prop_value:
|
if "default" in prop_value:
|
||||||
del prop_value["default"]
|
del prop_value["default"]
|
||||||
|
# see #5217
|
||||||
|
if "additionalProperties" in prop_value:
|
||||||
|
del prop_value["additionalProperties"]
|
||||||
properties[key] = prop_value
|
properties[key] = prop_value
|
||||||
|
|
||||||
if properties:
|
if properties:
|
||||||
@@ -315,22 +328,22 @@ class ToolSet:
|
|||||||
"""获取所有工具的名称列表"""
|
"""获取所有工具的名称列表"""
|
||||||
return [tool.name for tool in self.tools]
|
return [tool.name for tool in self.tools]
|
||||||
|
|
||||||
def merge(self, other: "ToolSet"):
|
def merge(self, other: "ToolSet") -> None:
|
||||||
"""Merge another ToolSet into this one."""
|
"""Merge another ToolSet into this one."""
|
||||||
for tool in other.tools:
|
for tool in other.tools:
|
||||||
self.add_tool(tool)
|
self.add_tool(tool)
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self) -> int:
|
||||||
return len(self.tools)
|
return len(self.tools)
|
||||||
|
|
||||||
def __bool__(self):
|
def __bool__(self) -> bool:
|
||||||
return len(self.tools) > 0
|
return len(self.tools) > 0
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.tools)
|
return iter(self.tools)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"ToolSet(tools={self.tools})"
|
return f"ToolSet(tools={self.tools})"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self) -> str:
|
||||||
return f"ToolSet(tools={self.tools})"
|
return f"ToolSet(tools={self.tools})"
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""Tool image cache module for storing and retrieving images returned by tools.
|
||||||
|
|
||||||
|
This module allows LLM to review images before deciding whether to send them to users.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
from astrbot import logger
|
||||||
|
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CachedImage:
|
||||||
|
"""Represents a cached image from a tool call."""
|
||||||
|
|
||||||
|
tool_call_id: str
|
||||||
|
"""The tool call ID that produced this image."""
|
||||||
|
tool_name: str
|
||||||
|
"""The name of the tool that produced this image."""
|
||||||
|
file_path: str
|
||||||
|
"""The file path where the image is stored."""
|
||||||
|
mime_type: str
|
||||||
|
"""The MIME type of the image."""
|
||||||
|
created_at: float = field(default_factory=time.time)
|
||||||
|
"""Timestamp when the image was cached."""
|
||||||
|
|
||||||
|
|
||||||
|
class ToolImageCache:
|
||||||
|
"""Manages cached images from tool calls.
|
||||||
|
|
||||||
|
Images are stored in data/temp/tool_images/ and can be retrieved by file path.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instance: ClassVar["ToolImageCache | None"] = None
|
||||||
|
CACHE_DIR_NAME: ClassVar[str] = "tool_images"
|
||||||
|
# Cache expiry time in seconds (1 hour)
|
||||||
|
CACHE_EXPIRY: ClassVar[int] = 3600
|
||||||
|
|
||||||
|
def __new__(cls) -> "ToolImageCache":
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance._initialized = False
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
if self._initialized:
|
||||||
|
return
|
||||||
|
self._initialized = True
|
||||||
|
self._cache_dir = os.path.join(get_astrbot_temp_path(), self.CACHE_DIR_NAME)
|
||||||
|
os.makedirs(self._cache_dir, exist_ok=True)
|
||||||
|
logger.debug(f"ToolImageCache initialized, cache dir: {self._cache_dir}")
|
||||||
|
|
||||||
|
def _get_file_extension(self, mime_type: str) -> str:
|
||||||
|
"""Get file extension from MIME type."""
|
||||||
|
mime_to_ext = {
|
||||||
|
"image/png": ".png",
|
||||||
|
"image/jpeg": ".jpg",
|
||||||
|
"image/jpg": ".jpg",
|
||||||
|
"image/gif": ".gif",
|
||||||
|
"image/webp": ".webp",
|
||||||
|
"image/bmp": ".bmp",
|
||||||
|
"image/svg+xml": ".svg",
|
||||||
|
}
|
||||||
|
return mime_to_ext.get(mime_type.lower(), ".png")
|
||||||
|
|
||||||
|
def save_image(
|
||||||
|
self,
|
||||||
|
base64_data: str,
|
||||||
|
tool_call_id: str,
|
||||||
|
tool_name: str,
|
||||||
|
index: int = 0,
|
||||||
|
mime_type: str = "image/png",
|
||||||
|
) -> CachedImage:
|
||||||
|
"""Save an image to cache and return the cached image info.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base64_data: Base64 encoded image data.
|
||||||
|
tool_call_id: The tool call ID that produced this image.
|
||||||
|
tool_name: The name of the tool that produced this image.
|
||||||
|
index: The index of the image (for multiple images from same tool call).
|
||||||
|
mime_type: The MIME type of the image.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
CachedImage object with file path.
|
||||||
|
"""
|
||||||
|
ext = self._get_file_extension(mime_type)
|
||||||
|
file_name = f"{tool_call_id}_{index}{ext}"
|
||||||
|
file_path = os.path.join(self._cache_dir, file_name)
|
||||||
|
|
||||||
|
# Decode and save the image
|
||||||
|
try:
|
||||||
|
image_bytes = base64.b64decode(base64_data)
|
||||||
|
with open(file_path, "wb") as f:
|
||||||
|
f.write(image_bytes)
|
||||||
|
logger.debug(f"Saved tool image to: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to save tool image: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return CachedImage(
|
||||||
|
tool_call_id=tool_call_id,
|
||||||
|
tool_name=tool_name,
|
||||||
|
file_path=file_path,
|
||||||
|
mime_type=mime_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_image_base64_by_path(
|
||||||
|
self, file_path: str, mime_type: str = "image/png"
|
||||||
|
) -> tuple[str, str] | None:
|
||||||
|
"""Read an image file and return its base64 encoded data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
file_path: The file path of the cached image.
|
||||||
|
mime_type: The MIME type of the image.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (base64_data, mime_type) if found, None otherwise.
|
||||||
|
"""
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
image_bytes = f.read()
|
||||||
|
base64_data = base64.b64encode(image_bytes).decode("utf-8")
|
||||||
|
return base64_data, mime_type
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to read cached image {file_path}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def cleanup_expired(self) -> int:
|
||||||
|
"""Clean up expired cached images.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Number of images cleaned up.
|
||||||
|
"""
|
||||||
|
now = time.time()
|
||||||
|
cleaned = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
for file_name in os.listdir(self._cache_dir):
|
||||||
|
file_path = os.path.join(self._cache_dir, file_name)
|
||||||
|
if os.path.isfile(file_path):
|
||||||
|
file_age = now - os.path.getmtime(file_path)
|
||||||
|
if file_age > self.CACHE_EXPIRY:
|
||||||
|
os.remove(file_path)
|
||||||
|
cleaned += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error during cache cleanup: {e}")
|
||||||
|
|
||||||
|
if cleaned:
|
||||||
|
logger.info(f"Cleaned up {cleaned} expired cached images")
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
|
||||||
|
|
||||||
|
# Global singleton instance
|
||||||
|
tool_image_cache = ToolImageCache()
|
||||||
@@ -12,7 +12,7 @@ from astrbot.core.star.star_handler import EventType
|
|||||||
|
|
||||||
|
|
||||||
class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
||||||
async def on_agent_done(self, run_context, llm_response):
|
async def on_agent_done(self, run_context, llm_response) -> None:
|
||||||
# 执行事件钩子
|
# 执行事件钩子
|
||||||
if llm_response and llm_response.reasoning_content:
|
if llm_response and llm_response.reasoning_content:
|
||||||
# we will use this in result_decorate stage to inject reasoning content to chain
|
# we will use this in result_decorate stage to inject reasoning content to chain
|
||||||
@@ -31,7 +31,7 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
|||||||
run_context: ContextWrapper[AstrAgentContext],
|
run_context: ContextWrapper[AstrAgentContext],
|
||||||
tool: FunctionTool[Any],
|
tool: FunctionTool[Any],
|
||||||
tool_args: dict | None,
|
tool_args: dict | None,
|
||||||
):
|
) -> None:
|
||||||
await call_event_hook(
|
await call_event_hook(
|
||||||
run_context.context.event,
|
run_context.context.event,
|
||||||
EventType.OnUsingLLMToolEvent,
|
EventType.OnUsingLLMToolEvent,
|
||||||
@@ -45,7 +45,7 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
|||||||
tool: FunctionTool[Any],
|
tool: FunctionTool[Any],
|
||||||
tool_args: dict | None,
|
tool_args: dict | None,
|
||||||
tool_result: CallToolResult | None,
|
tool_result: CallToolResult | None,
|
||||||
):
|
) -> None:
|
||||||
run_context.context.event.clear_result()
|
run_context.context.event.clear_result()
|
||||||
await call_event_hook(
|
await call_event_hook(
|
||||||
run_context.context.event,
|
run_context.context.event,
|
||||||
@@ -59,7 +59,7 @@ class MainAgentHooks(BaseAgentRunHooks[AstrAgentContext]):
|
|||||||
platform_name = run_context.context.event.get_platform_name()
|
platform_name = run_context.context.event.get_platform_name()
|
||||||
if (
|
if (
|
||||||
platform_name == "webchat"
|
platform_name == "webchat"
|
||||||
and tool.name == "web_search_tavily"
|
and tool.name in ["web_search_tavily", "web_search_bocha"]
|
||||||
and len(run_context.messages) > 0
|
and len(run_context.messages) > 0
|
||||||
and tool_result
|
and tool_result
|
||||||
and len(tool_result.content)
|
and len(tool_result.content)
|
||||||
|
|||||||
@@ -20,15 +20,81 @@ from astrbot.core.provider.provider import TTSProvider
|
|||||||
AgentRunner = ToolLoopAgentRunner[AstrAgentContext]
|
AgentRunner = ToolLoopAgentRunner[AstrAgentContext]
|
||||||
|
|
||||||
|
|
||||||
|
def _should_stop_agent(astr_event) -> bool:
|
||||||
|
return astr_event.is_stopped() or bool(astr_event.get_extra("agent_stop_requested"))
|
||||||
|
|
||||||
|
|
||||||
|
def _truncate_tool_result(text: str, limit: int = 70) -> str:
|
||||||
|
if limit <= 0:
|
||||||
|
return ""
|
||||||
|
if len(text) <= limit:
|
||||||
|
return text
|
||||||
|
if limit <= 3:
|
||||||
|
return text[:limit]
|
||||||
|
return f"{text[: limit - 3]}..."
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_chain_json_data(msg_chain: MessageChain) -> dict | None:
|
||||||
|
if not msg_chain.chain:
|
||||||
|
return None
|
||||||
|
first_comp = msg_chain.chain[0]
|
||||||
|
if isinstance(first_comp, Json) and isinstance(first_comp.data, dict):
|
||||||
|
return first_comp.data
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _record_tool_call_name(
|
||||||
|
tool_info: dict | None, tool_name_by_call_id: dict[str, str]
|
||||||
|
) -> None:
|
||||||
|
if not isinstance(tool_info, dict):
|
||||||
|
return
|
||||||
|
tool_call_id = tool_info.get("id")
|
||||||
|
tool_name = tool_info.get("name")
|
||||||
|
if tool_call_id is None or tool_name is None:
|
||||||
|
return
|
||||||
|
tool_name_by_call_id[str(tool_call_id)] = str(tool_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_tool_call_status_message(tool_info: dict | None) -> str:
|
||||||
|
if tool_info:
|
||||||
|
return f"🔨 调用工具: {tool_info.get('name', 'unknown')}"
|
||||||
|
return "🔨 调用工具..."
|
||||||
|
|
||||||
|
|
||||||
|
def _build_tool_result_status_message(
|
||||||
|
msg_chain: MessageChain, tool_name_by_call_id: dict[str, str]
|
||||||
|
) -> str:
|
||||||
|
tool_name = "unknown"
|
||||||
|
tool_result = ""
|
||||||
|
|
||||||
|
result_data = _extract_chain_json_data(msg_chain)
|
||||||
|
if result_data:
|
||||||
|
tool_call_id = result_data.get("id")
|
||||||
|
if tool_call_id is not None:
|
||||||
|
tool_name = tool_name_by_call_id.pop(str(tool_call_id), "unknown")
|
||||||
|
tool_result = str(result_data.get("result", ""))
|
||||||
|
|
||||||
|
if not tool_result:
|
||||||
|
tool_result = msg_chain.get_plain_text(with_other_comps_mark=True)
|
||||||
|
tool_result = _truncate_tool_result(tool_result, 70)
|
||||||
|
|
||||||
|
status_msg = f"🔨 调用工具: {tool_name}"
|
||||||
|
if tool_result:
|
||||||
|
status_msg = f"{status_msg}\n📎 返回结果: {tool_result}"
|
||||||
|
return status_msg
|
||||||
|
|
||||||
|
|
||||||
async def run_agent(
|
async def run_agent(
|
||||||
agent_runner: AgentRunner,
|
agent_runner: AgentRunner,
|
||||||
max_step: int = 30,
|
max_step: int = 30,
|
||||||
show_tool_use: bool = True,
|
show_tool_use: bool = True,
|
||||||
|
show_tool_call_result: bool = False,
|
||||||
stream_to_general: bool = False,
|
stream_to_general: bool = False,
|
||||||
show_reasoning: bool = False,
|
show_reasoning: bool = False,
|
||||||
) -> AsyncGenerator[MessageChain | None, None]:
|
) -> AsyncGenerator[MessageChain | None, None]:
|
||||||
step_idx = 0
|
step_idx = 0
|
||||||
astr_event = agent_runner.run_context.context.event
|
astr_event = agent_runner.run_context.context.event
|
||||||
|
tool_name_by_call_id: dict[str, str] = {}
|
||||||
while step_idx < max_step + 1:
|
while step_idx < max_step + 1:
|
||||||
step_idx += 1
|
step_idx += 1
|
||||||
|
|
||||||
@@ -48,10 +114,28 @@ async def run_agent(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stop_watcher = asyncio.create_task(
|
||||||
|
_watch_agent_stop_signal(agent_runner, astr_event),
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
async for resp in agent_runner.step():
|
async for resp in agent_runner.step():
|
||||||
if astr_event.is_stopped():
|
if _should_stop_agent(astr_event):
|
||||||
|
agent_runner.request_stop()
|
||||||
|
|
||||||
|
if resp.type == "aborted":
|
||||||
|
if not stop_watcher.done():
|
||||||
|
stop_watcher.cancel()
|
||||||
|
try:
|
||||||
|
await stop_watcher
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
astr_event.set_extra("agent_user_aborted", True)
|
||||||
|
astr_event.set_extra("agent_stop_requested", False)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if _should_stop_agent(astr_event):
|
||||||
|
continue
|
||||||
|
|
||||||
if resp.type == "tool_call_result":
|
if resp.type == "tool_call_result":
|
||||||
msg_chain = resp.data["chain"]
|
msg_chain = resp.data["chain"]
|
||||||
|
|
||||||
@@ -68,6 +152,13 @@ async def run_agent(
|
|||||||
continue
|
continue
|
||||||
if astr_event.get_platform_id() == "webchat":
|
if astr_event.get_platform_id() == "webchat":
|
||||||
await astr_event.send(msg_chain)
|
await astr_event.send(msg_chain)
|
||||||
|
elif show_tool_use and show_tool_call_result:
|
||||||
|
status_msg = _build_tool_result_status_message(
|
||||||
|
msg_chain, tool_name_by_call_id
|
||||||
|
)
|
||||||
|
await astr_event.send(
|
||||||
|
MessageChain(type="tool_call").message(status_msg)
|
||||||
|
)
|
||||||
# 对于其他情况,暂时先不处理
|
# 对于其他情况,暂时先不处理
|
||||||
continue
|
continue
|
||||||
elif resp.type == "tool_call":
|
elif resp.type == "tool_call":
|
||||||
@@ -75,25 +166,22 @@ async def run_agent(
|
|||||||
# 用来标记流式响应需要分节
|
# 用来标记流式响应需要分节
|
||||||
yield MessageChain(chain=[], type="break")
|
yield MessageChain(chain=[], type="break")
|
||||||
|
|
||||||
tool_info = None
|
tool_info = _extract_chain_json_data(resp.data["chain"])
|
||||||
|
astr_event.trace.record(
|
||||||
if resp.data["chain"].chain:
|
"agent_tool_call",
|
||||||
json_comp = resp.data["chain"].chain[0]
|
tool_name=tool_info if tool_info else "unknown",
|
||||||
if isinstance(json_comp, Json):
|
)
|
||||||
tool_info = json_comp.data
|
_record_tool_call_name(tool_info, tool_name_by_call_id)
|
||||||
astr_event.trace.record(
|
|
||||||
"agent_tool_call",
|
|
||||||
tool_name=tool_info if tool_info else "unknown",
|
|
||||||
)
|
|
||||||
|
|
||||||
if astr_event.get_platform_name() == "webchat":
|
if astr_event.get_platform_name() == "webchat":
|
||||||
await astr_event.send(resp.data["chain"])
|
await astr_event.send(resp.data["chain"])
|
||||||
elif show_tool_use:
|
elif show_tool_use:
|
||||||
if tool_info:
|
if show_tool_call_result and isinstance(tool_info, dict):
|
||||||
m = f"🔨 调用工具: {tool_info.get('name', 'unknown')}"
|
# Delay tool status notification until tool_call_result.
|
||||||
else:
|
continue
|
||||||
m = "🔨 调用工具..."
|
chain = MessageChain(type="tool_call").message(
|
||||||
chain = MessageChain(type="tool_call").message(m)
|
_build_tool_call_status_message(tool_info)
|
||||||
|
)
|
||||||
await astr_event.send(chain)
|
await astr_event.send(chain)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -120,6 +208,12 @@ async def run_agent(
|
|||||||
# display the reasoning content only when configured
|
# display the reasoning content only when configured
|
||||||
continue
|
continue
|
||||||
yield resp.data["chain"] # MessageChain
|
yield resp.data["chain"] # MessageChain
|
||||||
|
if not stop_watcher.done():
|
||||||
|
stop_watcher.cancel()
|
||||||
|
try:
|
||||||
|
await stop_watcher
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
if agent_runner.done():
|
if agent_runner.done():
|
||||||
# send agent stats to webchat
|
# send agent stats to webchat
|
||||||
if astr_event.get_platform_name() == "webchat":
|
if astr_event.get_platform_name() == "webchat":
|
||||||
@@ -133,6 +227,12 @@ async def run_agent(
|
|||||||
break
|
break
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
if "stop_watcher" in locals() and not stop_watcher.done():
|
||||||
|
stop_watcher.cancel()
|
||||||
|
try:
|
||||||
|
await stop_watcher
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
err_msg = f"\n\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {e!s}\n\n请在平台日志查看和分享错误详情。\n"
|
err_msg = f"\n\nAstrBot 请求失败。\n错误类型: {type(e).__name__}\n错误信息: {e!s}\n\n请在平台日志查看和分享错误详情。\n"
|
||||||
@@ -155,11 +255,20 @@ async def run_agent(
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
async def _watch_agent_stop_signal(agent_runner: AgentRunner, astr_event) -> None:
|
||||||
|
while not agent_runner.done():
|
||||||
|
if _should_stop_agent(astr_event):
|
||||||
|
agent_runner.request_stop()
|
||||||
|
return
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
async def run_live_agent(
|
async def run_live_agent(
|
||||||
agent_runner: AgentRunner,
|
agent_runner: AgentRunner,
|
||||||
tts_provider: TTSProvider | None = None,
|
tts_provider: TTSProvider | None = None,
|
||||||
max_step: int = 30,
|
max_step: int = 30,
|
||||||
show_tool_use: bool = True,
|
show_tool_use: bool = True,
|
||||||
|
show_tool_call_result: bool = False,
|
||||||
show_reasoning: bool = False,
|
show_reasoning: bool = False,
|
||||||
) -> AsyncGenerator[MessageChain | None, None]:
|
) -> AsyncGenerator[MessageChain | None, None]:
|
||||||
"""Live Mode 的 Agent 运行器,支持流式 TTS
|
"""Live Mode 的 Agent 运行器,支持流式 TTS
|
||||||
@@ -169,6 +278,7 @@ async def run_live_agent(
|
|||||||
tts_provider: TTS Provider 实例
|
tts_provider: TTS Provider 实例
|
||||||
max_step: 最大步数
|
max_step: 最大步数
|
||||||
show_tool_use: 是否显示工具使用
|
show_tool_use: 是否显示工具使用
|
||||||
|
show_tool_call_result: 是否显示工具返回结果
|
||||||
show_reasoning: 是否显示推理过程
|
show_reasoning: 是否显示推理过程
|
||||||
|
|
||||||
Yields:
|
Yields:
|
||||||
@@ -180,6 +290,7 @@ async def run_live_agent(
|
|||||||
agent_runner,
|
agent_runner,
|
||||||
max_step=max_step,
|
max_step=max_step,
|
||||||
show_tool_use=show_tool_use,
|
show_tool_use=show_tool_use,
|
||||||
|
show_tool_call_result=show_tool_call_result,
|
||||||
stream_to_general=False,
|
stream_to_general=False,
|
||||||
show_reasoning=show_reasoning,
|
show_reasoning=show_reasoning,
|
||||||
):
|
):
|
||||||
@@ -208,7 +319,12 @@ async def run_live_agent(
|
|||||||
# 1. 启动 Agent Feeder 任务:负责运行 Agent 并将文本分句喂给 text_queue
|
# 1. 启动 Agent Feeder 任务:负责运行 Agent 并将文本分句喂给 text_queue
|
||||||
feeder_task = asyncio.create_task(
|
feeder_task = asyncio.create_task(
|
||||||
_run_agent_feeder(
|
_run_agent_feeder(
|
||||||
agent_runner, text_queue, max_step, show_tool_use, show_reasoning
|
agent_runner,
|
||||||
|
text_queue,
|
||||||
|
max_step,
|
||||||
|
show_tool_use,
|
||||||
|
show_tool_call_result,
|
||||||
|
show_reasoning,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -294,8 +410,9 @@ async def _run_agent_feeder(
|
|||||||
text_queue: asyncio.Queue,
|
text_queue: asyncio.Queue,
|
||||||
max_step: int,
|
max_step: int,
|
||||||
show_tool_use: bool,
|
show_tool_use: bool,
|
||||||
|
show_tool_call_result: bool,
|
||||||
show_reasoning: bool,
|
show_reasoning: bool,
|
||||||
):
|
) -> None:
|
||||||
"""运行 Agent 并将文本输出分句放入队列"""
|
"""运行 Agent 并将文本输出分句放入队列"""
|
||||||
buffer = ""
|
buffer = ""
|
||||||
try:
|
try:
|
||||||
@@ -303,6 +420,7 @@ async def _run_agent_feeder(
|
|||||||
agent_runner,
|
agent_runner,
|
||||||
max_step=max_step,
|
max_step=max_step,
|
||||||
show_tool_use=show_tool_use,
|
show_tool_use=show_tool_use,
|
||||||
|
show_tool_call_result=show_tool_call_result,
|
||||||
stream_to_general=False,
|
stream_to_general=False,
|
||||||
show_reasoning=show_reasoning,
|
show_reasoning=show_reasoning,
|
||||||
):
|
):
|
||||||
@@ -352,7 +470,7 @@ async def _safe_tts_stream_wrapper(
|
|||||||
tts_provider: TTSProvider,
|
tts_provider: TTSProvider,
|
||||||
text_queue: asyncio.Queue[str | None],
|
text_queue: asyncio.Queue[str | None],
|
||||||
audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]",
|
audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]",
|
||||||
):
|
) -> None:
|
||||||
"""包装原生流式 TTS 确保异常处理和队列关闭"""
|
"""包装原生流式 TTS 确保异常处理和队列关闭"""
|
||||||
try:
|
try:
|
||||||
await tts_provider.get_audio_stream(text_queue, audio_queue)
|
await tts_provider.get_audio_stream(text_queue, audio_queue)
|
||||||
@@ -366,7 +484,7 @@ async def _simulated_stream_tts(
|
|||||||
tts_provider: TTSProvider,
|
tts_provider: TTSProvider,
|
||||||
text_queue: asyncio.Queue[str | None],
|
text_queue: asyncio.Queue[str | None],
|
||||||
audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]",
|
audio_queue: "asyncio.Queue[bytes | tuple[str, bytes] | None]",
|
||||||
):
|
) -> None:
|
||||||
"""模拟流式 TTS 分句生成音频"""
|
"""模拟流式 TTS 分句生成音频"""
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ from astrbot.core.agent.tool_executor import BaseFunctionToolExecutor
|
|||||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||||
from astrbot.core.astr_main_agent_resources import (
|
from astrbot.core.astr_main_agent_resources import (
|
||||||
BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT,
|
BACKGROUND_TASK_RESULT_WOKE_SYSTEM_PROMPT,
|
||||||
|
EXECUTE_SHELL_TOOL,
|
||||||
|
FILE_DOWNLOAD_TOOL,
|
||||||
|
FILE_UPLOAD_TOOL,
|
||||||
|
LOCAL_EXECUTE_SHELL_TOOL,
|
||||||
|
LOCAL_PYTHON_TOOL,
|
||||||
|
PYTHON_TOOL,
|
||||||
SEND_MESSAGE_TO_USER_TOOL,
|
SEND_MESSAGE_TO_USER_TOOL,
|
||||||
)
|
)
|
||||||
from astrbot.core.cron.events import CronMessageEvent
|
from astrbot.core.cron.events import CronMessageEvent
|
||||||
@@ -45,6 +51,13 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if isinstance(tool, HandoffTool):
|
if isinstance(tool, HandoffTool):
|
||||||
|
is_bg = tool_args.pop("background_task", False)
|
||||||
|
if is_bg:
|
||||||
|
async for r in cls._execute_handoff_background(
|
||||||
|
tool, run_context, **tool_args
|
||||||
|
):
|
||||||
|
yield r
|
||||||
|
return
|
||||||
async for r in cls._execute_handoff(tool, run_context, **tool_args):
|
async for r in cls._execute_handoff(tool, run_context, **tool_args):
|
||||||
yield r
|
yield r
|
||||||
return
|
return
|
||||||
@@ -57,7 +70,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
elif tool.is_background_task:
|
elif tool.is_background_task:
|
||||||
task_id = uuid.uuid4().hex
|
task_id = uuid.uuid4().hex
|
||||||
|
|
||||||
async def _run_in_background():
|
async def _run_in_background() -> None:
|
||||||
try:
|
try:
|
||||||
await cls._execute_background(
|
await cls._execute_background(
|
||||||
tool=tool,
|
tool=tool,
|
||||||
@@ -84,6 +97,65 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
yield r
|
yield r
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_runtime_computer_tools(cls, runtime: str) -> dict[str, FunctionTool]:
|
||||||
|
if runtime == "sandbox":
|
||||||
|
return {
|
||||||
|
EXECUTE_SHELL_TOOL.name: EXECUTE_SHELL_TOOL,
|
||||||
|
PYTHON_TOOL.name: PYTHON_TOOL,
|
||||||
|
FILE_UPLOAD_TOOL.name: FILE_UPLOAD_TOOL,
|
||||||
|
FILE_DOWNLOAD_TOOL.name: FILE_DOWNLOAD_TOOL,
|
||||||
|
}
|
||||||
|
if runtime == "local":
|
||||||
|
return {
|
||||||
|
LOCAL_EXECUTE_SHELL_TOOL.name: LOCAL_EXECUTE_SHELL_TOOL,
|
||||||
|
LOCAL_PYTHON_TOOL.name: LOCAL_PYTHON_TOOL,
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_handoff_toolset(
|
||||||
|
cls,
|
||||||
|
run_context: ContextWrapper[AstrAgentContext],
|
||||||
|
tools: list[str | FunctionTool] | None,
|
||||||
|
) -> ToolSet | None:
|
||||||
|
ctx = run_context.context.context
|
||||||
|
event = run_context.context.event
|
||||||
|
cfg = ctx.get_config(umo=event.unified_msg_origin)
|
||||||
|
provider_settings = cfg.get("provider_settings", {})
|
||||||
|
runtime = str(provider_settings.get("computer_use_runtime", "local"))
|
||||||
|
runtime_computer_tools = cls._get_runtime_computer_tools(runtime)
|
||||||
|
|
||||||
|
# Keep persona semantics aligned with the main agent: tools=None means
|
||||||
|
# "all tools", including runtime computer-use tools.
|
||||||
|
if tools is None:
|
||||||
|
toolset = ToolSet()
|
||||||
|
for registered_tool in llm_tools.func_list:
|
||||||
|
if isinstance(registered_tool, HandoffTool):
|
||||||
|
continue
|
||||||
|
if registered_tool.active:
|
||||||
|
toolset.add_tool(registered_tool)
|
||||||
|
for runtime_tool in runtime_computer_tools.values():
|
||||||
|
toolset.add_tool(runtime_tool)
|
||||||
|
return None if toolset.empty() else toolset
|
||||||
|
|
||||||
|
if not tools:
|
||||||
|
return None
|
||||||
|
|
||||||
|
toolset = ToolSet()
|
||||||
|
for tool_name_or_obj in tools:
|
||||||
|
if isinstance(tool_name_or_obj, str):
|
||||||
|
registered_tool = llm_tools.get_func(tool_name_or_obj)
|
||||||
|
if registered_tool and registered_tool.active:
|
||||||
|
toolset.add_tool(registered_tool)
|
||||||
|
continue
|
||||||
|
runtime_tool = runtime_computer_tools.get(tool_name_or_obj)
|
||||||
|
if runtime_tool:
|
||||||
|
toolset.add_tool(runtime_tool)
|
||||||
|
elif isinstance(tool_name_or_obj, FunctionTool):
|
||||||
|
toolset.add_tool(tool_name_or_obj)
|
||||||
|
return None if toolset.empty() else toolset
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _execute_handoff(
|
async def _execute_handoff(
|
||||||
cls,
|
cls,
|
||||||
@@ -92,20 +164,10 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
**tool_args,
|
**tool_args,
|
||||||
):
|
):
|
||||||
input_ = tool_args.get("input")
|
input_ = tool_args.get("input")
|
||||||
|
image_urls = tool_args.get("image_urls")
|
||||||
|
|
||||||
# make toolset for the agent
|
# Build handoff toolset from registered tools plus runtime computer tools.
|
||||||
tools = tool.agent.tools
|
toolset = cls._build_handoff_toolset(run_context, tool.agent.tools)
|
||||||
if tools:
|
|
||||||
toolset = ToolSet()
|
|
||||||
for t in tools:
|
|
||||||
if isinstance(t, str):
|
|
||||||
_t = llm_tools.get_func(t)
|
|
||||||
if _t:
|
|
||||||
toolset.add_tool(_t)
|
|
||||||
elif isinstance(t, FunctionTool):
|
|
||||||
toolset.add_tool(t)
|
|
||||||
else:
|
|
||||||
toolset = None
|
|
||||||
|
|
||||||
ctx = run_context.context.context
|
ctx = run_context.context.context
|
||||||
event = run_context.context.event
|
event = run_context.context.event
|
||||||
@@ -136,16 +198,98 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
event=event,
|
event=event,
|
||||||
chat_provider_id=prov_id,
|
chat_provider_id=prov_id,
|
||||||
prompt=input_,
|
prompt=input_,
|
||||||
|
image_urls=image_urls,
|
||||||
system_prompt=tool.agent.instructions,
|
system_prompt=tool.agent.instructions,
|
||||||
tools=toolset,
|
tools=toolset,
|
||||||
contexts=contexts,
|
contexts=contexts,
|
||||||
max_steps=30,
|
max_steps=30,
|
||||||
run_hooks=tool.agent.run_hooks,
|
run_hooks=tool.agent.run_hooks,
|
||||||
|
stream=ctx.get_config().get("provider_settings", {}).get("stream", False),
|
||||||
)
|
)
|
||||||
yield mcp.types.CallToolResult(
|
yield mcp.types.CallToolResult(
|
||||||
content=[mcp.types.TextContent(type="text", text=llm_resp.completion_text)]
|
content=[mcp.types.TextContent(type="text", text=llm_resp.completion_text)]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _execute_handoff_background(
|
||||||
|
cls,
|
||||||
|
tool: HandoffTool,
|
||||||
|
run_context: ContextWrapper[AstrAgentContext],
|
||||||
|
**tool_args,
|
||||||
|
):
|
||||||
|
"""Execute a handoff as a background task.
|
||||||
|
|
||||||
|
Immediately yields a success response with a task_id, then runs
|
||||||
|
the subagent asynchronously. When the subagent finishes, a
|
||||||
|
``CronMessageEvent`` is created so the main LLM can inform the
|
||||||
|
user of the result – the same pattern used by
|
||||||
|
``_execute_background`` for regular background tasks.
|
||||||
|
"""
|
||||||
|
task_id = uuid.uuid4().hex
|
||||||
|
|
||||||
|
async def _run_handoff_in_background() -> None:
|
||||||
|
try:
|
||||||
|
await cls._do_handoff_background(
|
||||||
|
tool=tool,
|
||||||
|
run_context=run_context,
|
||||||
|
task_id=task_id,
|
||||||
|
**tool_args,
|
||||||
|
)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
logger.error(
|
||||||
|
f"Background handoff {task_id} ({tool.name}) failed: {e!s}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.create_task(_run_handoff_in_background())
|
||||||
|
|
||||||
|
text_content = mcp.types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=(
|
||||||
|
f"Background task dedicated to subagent '{tool.agent.name}' submitted. task_id={task_id}. "
|
||||||
|
f"The subagent '{tool.agent.name}' is working on the task on hehalf you. "
|
||||||
|
f"You will be notified when it finishes."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yield mcp.types.CallToolResult(content=[text_content])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _do_handoff_background(
|
||||||
|
cls,
|
||||||
|
tool: HandoffTool,
|
||||||
|
run_context: ContextWrapper[AstrAgentContext],
|
||||||
|
task_id: str,
|
||||||
|
**tool_args,
|
||||||
|
) -> None:
|
||||||
|
"""Run the subagent handoff and, on completion, wake the main agent."""
|
||||||
|
result_text = ""
|
||||||
|
try:
|
||||||
|
async for r in cls._execute_handoff(tool, run_context, **tool_args):
|
||||||
|
if isinstance(r, mcp.types.CallToolResult):
|
||||||
|
for content in r.content:
|
||||||
|
if isinstance(content, mcp.types.TextContent):
|
||||||
|
result_text += content.text + "\n"
|
||||||
|
except Exception as e:
|
||||||
|
result_text = (
|
||||||
|
f"error: Background task execution failed, internal error: {e!s}"
|
||||||
|
)
|
||||||
|
|
||||||
|
event = run_context.context.event
|
||||||
|
|
||||||
|
await cls._wake_main_agent_for_background_result(
|
||||||
|
run_context=run_context,
|
||||||
|
task_id=task_id,
|
||||||
|
tool_name=tool.name,
|
||||||
|
result_text=result_text,
|
||||||
|
tool_args=tool_args,
|
||||||
|
note=(
|
||||||
|
event.get_extra("background_note")
|
||||||
|
or f"Background task for subagent '{tool.agent.name}' finished."
|
||||||
|
),
|
||||||
|
summary_name=f"Dedicated to subagent `{tool.agent.name}`",
|
||||||
|
extra_result_fields={"subagent_name": tool.agent.name},
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _execute_background(
|
async def _execute_background(
|
||||||
cls,
|
cls,
|
||||||
@@ -153,13 +297,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
run_context: ContextWrapper[AstrAgentContext],
|
run_context: ContextWrapper[AstrAgentContext],
|
||||||
task_id: str,
|
task_id: str,
|
||||||
**tool_args,
|
**tool_args,
|
||||||
):
|
) -> None:
|
||||||
from astrbot.core.astr_main_agent import (
|
|
||||||
MainAgentBuildConfig,
|
|
||||||
_get_session_conv,
|
|
||||||
build_main_agent,
|
|
||||||
)
|
|
||||||
|
|
||||||
# run the tool
|
# run the tool
|
||||||
result_text = ""
|
result_text = ""
|
||||||
try:
|
try:
|
||||||
@@ -177,21 +315,53 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
f"error: Background task execution failed, internal error: {e!s}"
|
f"error: Background task execution failed, internal error: {e!s}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
event = run_context.context.event
|
||||||
|
|
||||||
|
await cls._wake_main_agent_for_background_result(
|
||||||
|
run_context=run_context,
|
||||||
|
task_id=task_id,
|
||||||
|
tool_name=tool.name,
|
||||||
|
result_text=result_text,
|
||||||
|
tool_args=tool_args,
|
||||||
|
note=(
|
||||||
|
event.get_extra("background_note")
|
||||||
|
or f"Background task {tool.name} finished."
|
||||||
|
),
|
||||||
|
summary_name=tool.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _wake_main_agent_for_background_result(
|
||||||
|
cls,
|
||||||
|
run_context: ContextWrapper[AstrAgentContext],
|
||||||
|
*,
|
||||||
|
task_id: str,
|
||||||
|
tool_name: str,
|
||||||
|
result_text: str,
|
||||||
|
tool_args: dict[str, T.Any],
|
||||||
|
note: str,
|
||||||
|
summary_name: str,
|
||||||
|
extra_result_fields: dict[str, T.Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
from astrbot.core.astr_main_agent import (
|
||||||
|
MainAgentBuildConfig,
|
||||||
|
_get_session_conv,
|
||||||
|
build_main_agent,
|
||||||
|
)
|
||||||
|
|
||||||
event = run_context.context.event
|
event = run_context.context.event
|
||||||
ctx = run_context.context.context
|
ctx = run_context.context.context
|
||||||
|
|
||||||
note = (
|
task_result = {
|
||||||
event.get_extra("background_note")
|
"task_id": task_id,
|
||||||
or f"Background task {tool.name} finished."
|
"tool_name": tool_name,
|
||||||
)
|
"result": result_text or "",
|
||||||
extras = {
|
"tool_args": tool_args,
|
||||||
"background_task_result": {
|
|
||||||
"task_id": task_id,
|
|
||||||
"tool_name": tool.name,
|
|
||||||
"result": result_text or "",
|
|
||||||
"tool_args": tool_args,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if extra_result_fields:
|
||||||
|
task_result.update(extra_result_fields)
|
||||||
|
extras = {"background_task_result": task_result}
|
||||||
|
|
||||||
session = MessageSession.from_str(event.unified_msg_origin)
|
session = MessageSession.from_str(event.unified_msg_origin)
|
||||||
cron_event = CronMessageEvent(
|
cron_event = CronMessageEvent(
|
||||||
context=ctx,
|
context=ctx,
|
||||||
@@ -201,7 +371,12 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
message_type=session.message_type,
|
message_type=session.message_type,
|
||||||
)
|
)
|
||||||
cron_event.role = event.role
|
cron_event.role = event.role
|
||||||
config = MainAgentBuildConfig(tool_call_timeout=3600)
|
config = MainAgentBuildConfig(
|
||||||
|
tool_call_timeout=3600,
|
||||||
|
streaming_response=ctx.get_config()
|
||||||
|
.get("provider_settings", {})
|
||||||
|
.get("stream", False),
|
||||||
|
)
|
||||||
|
|
||||||
req = ProviderRequest()
|
req = ProviderRequest()
|
||||||
conv = await _get_session_conv(event=cron_event, plugin_context=ctx)
|
conv = await _get_session_conv(event=cron_event, plugin_context=ctx)
|
||||||
@@ -222,8 +397,11 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
)
|
)
|
||||||
req.prompt = (
|
req.prompt = (
|
||||||
"Proceed according to your system instructions. "
|
"Proceed according to your system instructions. "
|
||||||
"Output using same language as previous conversation."
|
"Output using same language as previous conversation. "
|
||||||
" After completing your task, summarize and output your actions and results."
|
"If you need to deliver the result to the user immediately, "
|
||||||
|
"you MUST use `send_message_to_user` tool to send the message directly to the user, "
|
||||||
|
"otherwise the user will not see the result. "
|
||||||
|
"After completing your task, summarize and output your actions and results. "
|
||||||
)
|
)
|
||||||
if not req.func_tool:
|
if not req.func_tool:
|
||||||
req.func_tool = ToolSet()
|
req.func_tool = ToolSet()
|
||||||
@@ -233,7 +411,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
event=cron_event, plugin_context=ctx, config=config, req=req
|
event=cron_event, plugin_context=ctx, config=config, req=req
|
||||||
)
|
)
|
||||||
if not result:
|
if not result:
|
||||||
logger.error("Failed to build main agent for background task job.")
|
logger.error(f"Failed to build main agent for background task {tool_name}.")
|
||||||
return
|
return
|
||||||
|
|
||||||
runner = result.agent_runner
|
runner = result.agent_runner
|
||||||
@@ -243,7 +421,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
|
|||||||
llm_resp = runner.get_final_llm_resp()
|
llm_resp = runner.get_final_llm_resp()
|
||||||
task_meta = extras.get("background_task_result", {})
|
task_meta = extras.get("background_task_result", {})
|
||||||
summary_note = (
|
summary_note = (
|
||||||
f"[BackgroundTask] {task_meta.get('tool_name', tool.name)} "
|
f"[BackgroundTask] {summary_name} "
|
||||||
f"(task_id={task_meta.get('task_id', task_id)}) finished. "
|
f"(task_id={task_meta.get('task_id', task_id)}) finished. "
|
||||||
f"Result: {task_meta.get('result') or result_text or 'no content'}"
|
f"Result: {task_meta.get('result') or result_text or 'no content'}"
|
||||||
)
|
)
|
||||||
|
|||||||
+198
-67
@@ -1,7 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import builtins
|
|
||||||
import copy
|
import copy
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
@@ -10,7 +9,6 @@ import zoneinfo
|
|||||||
from collections.abc import Coroutine
|
from collections.abc import Coroutine
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from astrbot.api import sp
|
|
||||||
from astrbot.core import logger
|
from astrbot.core import logger
|
||||||
from astrbot.core.agent.handoff import HandoffTool
|
from astrbot.core.agent.handoff import HandoffTool
|
||||||
from astrbot.core.agent.mcp_client import MCPTool
|
from astrbot.core.agent.mcp_client import MCPTool
|
||||||
@@ -52,6 +50,17 @@ from astrbot.core.tools.cron_tools import (
|
|||||||
)
|
)
|
||||||
from astrbot.core.utils.file_extract import extract_file_moonshotai
|
from astrbot.core.utils.file_extract import extract_file_moonshotai
|
||||||
from astrbot.core.utils.llm_metadata import LLM_METADATAS
|
from astrbot.core.utils.llm_metadata import LLM_METADATAS
|
||||||
|
from astrbot.core.utils.quoted_message.settings import (
|
||||||
|
SETTINGS as DEFAULT_QUOTED_MESSAGE_SETTINGS,
|
||||||
|
)
|
||||||
|
from astrbot.core.utils.quoted_message.settings import (
|
||||||
|
QuotedMessageParserSettings,
|
||||||
|
)
|
||||||
|
from astrbot.core.utils.quoted_message_parser import (
|
||||||
|
extract_quoted_message_images,
|
||||||
|
extract_quoted_message_text,
|
||||||
|
)
|
||||||
|
from astrbot.core.utils.string_utils import normalize_and_dedupe_strings
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -108,6 +117,8 @@ class MainAgentBuildConfig:
|
|||||||
provider_settings: dict = field(default_factory=dict)
|
provider_settings: dict = field(default_factory=dict)
|
||||||
subagent_orchestrator: dict = field(default_factory=dict)
|
subagent_orchestrator: dict = field(default_factory=dict)
|
||||||
timezone: str | None = None
|
timezone: str | None = None
|
||||||
|
max_quoted_fallback_images: int = 20
|
||||||
|
"""Maximum number of images injected from quoted-message fallback extraction."""
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -262,47 +273,26 @@ async def _ensure_persona_and_skills(
|
|||||||
if not req.conversation:
|
if not req.conversation:
|
||||||
return
|
return
|
||||||
|
|
||||||
# get persona ID
|
(
|
||||||
|
persona_id,
|
||||||
# 1. from session service config - highest priority
|
persona,
|
||||||
persona_id = (
|
_,
|
||||||
await sp.get_async(
|
use_webchat_special_default,
|
||||||
scope="umo",
|
) = await plugin_context.persona_manager.resolve_selected_persona(
|
||||||
scope_id=event.unified_msg_origin,
|
umo=event.unified_msg_origin,
|
||||||
key="session_service_config",
|
conversation_persona_id=req.conversation.persona_id,
|
||||||
default={},
|
platform_name=event.get_platform_name(),
|
||||||
)
|
provider_settings=cfg,
|
||||||
).get("persona_id")
|
|
||||||
|
|
||||||
if not persona_id:
|
|
||||||
# 2. from conversation setting - second priority
|
|
||||||
persona_id = req.conversation.persona_id
|
|
||||||
|
|
||||||
if persona_id == "[%None]":
|
|
||||||
# explicitly set to no persona
|
|
||||||
pass
|
|
||||||
elif persona_id is None:
|
|
||||||
# 3. from config default persona setting - last priority
|
|
||||||
persona_id = cfg.get("default_personality")
|
|
||||||
|
|
||||||
persona = next(
|
|
||||||
builtins.filter(
|
|
||||||
lambda persona: persona["name"] == persona_id,
|
|
||||||
plugin_context.persona_manager.personas_v3,
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if persona:
|
if persona:
|
||||||
# Inject persona system prompt
|
# Inject persona system prompt
|
||||||
if prompt := persona["prompt"]:
|
if prompt := persona["prompt"]:
|
||||||
req.system_prompt += f"\n# Persona Instructions\n\n{prompt}\n"
|
req.system_prompt += f"\n# Persona Instructions\n\n{prompt}\n"
|
||||||
if begin_dialogs := copy.deepcopy(persona.get("_begin_dialogs_processed")):
|
if begin_dialogs := copy.deepcopy(persona.get("_begin_dialogs_processed")):
|
||||||
req.contexts[:0] = begin_dialogs
|
req.contexts[:0] = begin_dialogs
|
||||||
else:
|
elif use_webchat_special_default:
|
||||||
# special handling for webchat persona
|
req.system_prompt += CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT
|
||||||
if event.get_platform_name() == "webchat" and persona_id != "[%None]":
|
|
||||||
persona_id = "_chatui_default_"
|
|
||||||
req.system_prompt += CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT
|
|
||||||
|
|
||||||
# Inject skills prompt
|
# Inject skills prompt
|
||||||
runtime = cfg.get("computer_use_runtime", "local")
|
runtime = cfg.get("computer_use_runtime", "local")
|
||||||
@@ -326,6 +316,24 @@ async def _ensure_persona_and_skills(
|
|||||||
)
|
)
|
||||||
tmgr = plugin_context.get_llm_tool_manager()
|
tmgr = plugin_context.get_llm_tool_manager()
|
||||||
|
|
||||||
|
# inject toolset in the persona
|
||||||
|
if (persona and persona.get("tools") is None) or not persona:
|
||||||
|
persona_toolset = tmgr.get_full_tool_set()
|
||||||
|
for tool in list(persona_toolset):
|
||||||
|
if not tool.active:
|
||||||
|
persona_toolset.remove_tool(tool.name)
|
||||||
|
else:
|
||||||
|
persona_toolset = ToolSet()
|
||||||
|
if persona["tools"]:
|
||||||
|
for tool_name in persona["tools"]:
|
||||||
|
tool = tmgr.get_func(tool_name)
|
||||||
|
if tool and tool.active:
|
||||||
|
persona_toolset.add_tool(tool)
|
||||||
|
if not req.func_tool:
|
||||||
|
req.func_tool = persona_toolset
|
||||||
|
else:
|
||||||
|
req.func_tool.merge(persona_toolset)
|
||||||
|
|
||||||
# sub agents integration
|
# sub agents integration
|
||||||
orch_cfg = plugin_context.get_config().get("subagent_orchestrator", {})
|
orch_cfg = plugin_context.get_config().get("subagent_orchestrator", {})
|
||||||
so = plugin_context.subagent_orchestrator
|
so = plugin_context.subagent_orchestrator
|
||||||
@@ -371,22 +379,19 @@ async def _ensure_persona_and_skills(
|
|||||||
assigned_tools.add(name)
|
assigned_tools.add(name)
|
||||||
|
|
||||||
if req.func_tool is None:
|
if req.func_tool is None:
|
||||||
toolset = ToolSet()
|
req.func_tool = ToolSet()
|
||||||
else:
|
|
||||||
toolset = req.func_tool
|
|
||||||
|
|
||||||
# add subagent handoff tools
|
# add subagent handoff tools
|
||||||
for tool in so.handoffs:
|
for tool in so.handoffs:
|
||||||
toolset.add_tool(tool)
|
req.func_tool.add_tool(tool)
|
||||||
|
|
||||||
# check duplicates
|
# check duplicates
|
||||||
if remove_dup:
|
if remove_dup:
|
||||||
names = toolset.names()
|
handoff_names = {tool.name for tool in so.handoffs}
|
||||||
for tool_name in assigned_tools:
|
for tool_name in assigned_tools:
|
||||||
if tool_name in names:
|
if tool_name in handoff_names:
|
||||||
toolset.remove_tool(tool_name)
|
continue
|
||||||
|
req.func_tool.remove_tool(tool_name)
|
||||||
req.func_tool = toolset
|
|
||||||
|
|
||||||
router_prompt = (
|
router_prompt = (
|
||||||
plugin_context.get_config()
|
plugin_context.get_config()
|
||||||
@@ -395,32 +400,14 @@ async def _ensure_persona_and_skills(
|
|||||||
).strip()
|
).strip()
|
||||||
if router_prompt:
|
if router_prompt:
|
||||||
req.system_prompt += f"\n{router_prompt}\n"
|
req.system_prompt += f"\n{router_prompt}\n"
|
||||||
return
|
|
||||||
|
|
||||||
# inject toolset in the persona
|
|
||||||
if (persona and persona.get("tools") is None) or not persona:
|
|
||||||
toolset = tmgr.get_full_tool_set()
|
|
||||||
for tool in list(toolset):
|
|
||||||
if not tool.active:
|
|
||||||
toolset.remove_tool(tool.name)
|
|
||||||
else:
|
|
||||||
toolset = ToolSet()
|
|
||||||
if persona["tools"]:
|
|
||||||
for tool_name in persona["tools"]:
|
|
||||||
tool = tmgr.get_func(tool_name)
|
|
||||||
if tool and tool.active:
|
|
||||||
toolset.add_tool(tool)
|
|
||||||
if not req.func_tool:
|
|
||||||
req.func_tool = toolset
|
|
||||||
else:
|
|
||||||
req.func_tool.merge(toolset)
|
|
||||||
try:
|
try:
|
||||||
event.trace.record(
|
event.trace.record(
|
||||||
"sel_persona", persona_id=persona_id, persona_toolset=toolset.names()
|
"sel_persona",
|
||||||
|
persona_id=persona_id,
|
||||||
|
persona_toolset=persona_toolset.names(),
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
logger.debug("Tool set for persona %s: %s", persona_id, toolset.names())
|
|
||||||
|
|
||||||
|
|
||||||
async def _request_img_caption(
|
async def _request_img_caption(
|
||||||
@@ -473,11 +460,29 @@ async def _ensure_img_caption(
|
|||||||
logger.error("处理图片描述失败: %s", exc)
|
logger.error("处理图片描述失败: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
|
def _append_quoted_image_attachment(req: ProviderRequest, image_path: str) -> None:
|
||||||
|
req.extra_user_content_parts.append(
|
||||||
|
TextPart(text=f"[Image Attachment in quoted message: path {image_path}]")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_quoted_message_parser_settings(
|
||||||
|
provider_settings: dict[str, object] | None,
|
||||||
|
) -> QuotedMessageParserSettings:
|
||||||
|
if not isinstance(provider_settings, dict):
|
||||||
|
return DEFAULT_QUOTED_MESSAGE_SETTINGS
|
||||||
|
overrides = provider_settings.get("quoted_message_parser")
|
||||||
|
if not isinstance(overrides, dict):
|
||||||
|
return DEFAULT_QUOTED_MESSAGE_SETTINGS
|
||||||
|
return DEFAULT_QUOTED_MESSAGE_SETTINGS.with_overrides(overrides)
|
||||||
|
|
||||||
|
|
||||||
async def _process_quote_message(
|
async def _process_quote_message(
|
||||||
event: AstrMessageEvent,
|
event: AstrMessageEvent,
|
||||||
req: ProviderRequest,
|
req: ProviderRequest,
|
||||||
img_cap_prov_id: str,
|
img_cap_prov_id: str,
|
||||||
plugin_context: Context,
|
plugin_context: Context,
|
||||||
|
quoted_message_settings: QuotedMessageParserSettings = DEFAULT_QUOTED_MESSAGE_SETTINGS,
|
||||||
) -> None:
|
) -> None:
|
||||||
quote = None
|
quote = None
|
||||||
for comp in event.message_obj.message:
|
for comp in event.message_obj.message:
|
||||||
@@ -489,7 +494,15 @@ async def _process_quote_message(
|
|||||||
|
|
||||||
content_parts = []
|
content_parts = []
|
||||||
sender_info = f"({quote.sender_nickname}): " if quote.sender_nickname else ""
|
sender_info = f"({quote.sender_nickname}): " if quote.sender_nickname else ""
|
||||||
message_str = quote.message_str or "[Empty Text]"
|
message_str = (
|
||||||
|
await extract_quoted_message_text(
|
||||||
|
event,
|
||||||
|
quote,
|
||||||
|
settings=quoted_message_settings,
|
||||||
|
)
|
||||||
|
or quote.message_str
|
||||||
|
or "[Empty Text]"
|
||||||
|
)
|
||||||
content_parts.append(f"{sender_info}{message_str}")
|
content_parts.append(f"{sender_info}{message_str}")
|
||||||
|
|
||||||
image_seg = None
|
image_seg = None
|
||||||
@@ -595,11 +608,13 @@ async def _decorate_llm_request(
|
|||||||
)
|
)
|
||||||
|
|
||||||
img_cap_prov_id = cfg.get("default_image_caption_provider_id") or ""
|
img_cap_prov_id = cfg.get("default_image_caption_provider_id") or ""
|
||||||
|
quoted_message_settings = _get_quoted_message_parser_settings(cfg)
|
||||||
await _process_quote_message(
|
await _process_quote_message(
|
||||||
event,
|
event,
|
||||||
req,
|
req,
|
||||||
img_cap_prov_id,
|
img_cap_prov_id,
|
||||||
plugin_context,
|
plugin_context,
|
||||||
|
quoted_message_settings,
|
||||||
)
|
)
|
||||||
|
|
||||||
tz = config.timezone
|
tz = config.timezone
|
||||||
@@ -832,6 +847,41 @@ def _get_compress_provider(
|
|||||||
return provider
|
return provider
|
||||||
|
|
||||||
|
|
||||||
|
def _get_fallback_chat_providers(
|
||||||
|
provider: Provider, plugin_context: Context, provider_settings: dict
|
||||||
|
) -> list[Provider]:
|
||||||
|
fallback_ids = provider_settings.get("fallback_chat_models", [])
|
||||||
|
if not isinstance(fallback_ids, list):
|
||||||
|
logger.warning(
|
||||||
|
"fallback_chat_models setting is not a list, skip fallback providers."
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
provider_id = str(provider.provider_config.get("id", ""))
|
||||||
|
seen_provider_ids: set[str] = {provider_id} if provider_id else set()
|
||||||
|
fallbacks: list[Provider] = []
|
||||||
|
|
||||||
|
for fallback_id in fallback_ids:
|
||||||
|
if not isinstance(fallback_id, str) or not fallback_id:
|
||||||
|
continue
|
||||||
|
if fallback_id in seen_provider_ids:
|
||||||
|
continue
|
||||||
|
fallback_provider = plugin_context.get_provider_by_id(fallback_id)
|
||||||
|
if fallback_provider is None:
|
||||||
|
logger.warning("Fallback chat provider `%s` not found, skip.", fallback_id)
|
||||||
|
continue
|
||||||
|
if not isinstance(fallback_provider, Provider):
|
||||||
|
logger.warning(
|
||||||
|
"Fallback chat provider `%s` is invalid type: %s, skip.",
|
||||||
|
fallback_id,
|
||||||
|
type(fallback_provider),
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
fallbacks.append(fallback_provider)
|
||||||
|
seen_provider_ids.add(fallback_id)
|
||||||
|
return fallbacks
|
||||||
|
|
||||||
|
|
||||||
async def build_main_agent(
|
async def build_main_agent(
|
||||||
*,
|
*,
|
||||||
event: AstrMessageEvent,
|
event: AstrMessageEvent,
|
||||||
@@ -870,6 +920,8 @@ async def build_main_agent(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
req.prompt = event.message_str[len(config.provider_wake_prefix) :]
|
req.prompt = event.message_str[len(config.provider_wake_prefix) :]
|
||||||
|
|
||||||
|
# media files attachments
|
||||||
for comp in event.message_obj.message:
|
for comp in event.message_obj.message:
|
||||||
if isinstance(comp, Image):
|
if isinstance(comp, Image):
|
||||||
image_path = await comp.convert_to_file_path()
|
image_path = await comp.convert_to_file_path()
|
||||||
@@ -885,6 +937,81 @@ async def build_main_agent(
|
|||||||
text=f"[File Attachment: name {file_name}, path {file_path}]"
|
text=f"[File Attachment: name {file_name}, path {file_path}]"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# quoted message attachments
|
||||||
|
reply_comps = [
|
||||||
|
comp for comp in event.message_obj.message if isinstance(comp, Reply)
|
||||||
|
]
|
||||||
|
quoted_message_settings = _get_quoted_message_parser_settings(
|
||||||
|
config.provider_settings
|
||||||
|
)
|
||||||
|
fallback_quoted_image_count = 0
|
||||||
|
for comp in reply_comps:
|
||||||
|
has_embedded_image = False
|
||||||
|
if comp.chain:
|
||||||
|
for reply_comp in comp.chain:
|
||||||
|
if isinstance(reply_comp, Image):
|
||||||
|
has_embedded_image = True
|
||||||
|
image_path = await reply_comp.convert_to_file_path()
|
||||||
|
req.image_urls.append(image_path)
|
||||||
|
_append_quoted_image_attachment(req, image_path)
|
||||||
|
elif isinstance(reply_comp, File):
|
||||||
|
file_path = await reply_comp.get_file()
|
||||||
|
file_name = reply_comp.name or os.path.basename(file_path)
|
||||||
|
req.extra_user_content_parts.append(
|
||||||
|
TextPart(
|
||||||
|
text=(
|
||||||
|
f"[File Attachment in quoted message: "
|
||||||
|
f"name {file_name}, path {file_path}]"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fallback quoted image extraction for reply-id-only payloads, or when
|
||||||
|
# embedded reply chain only contains placeholders (e.g. [Forward Message], [Image]).
|
||||||
|
if not has_embedded_image:
|
||||||
|
try:
|
||||||
|
fallback_images = normalize_and_dedupe_strings(
|
||||||
|
await extract_quoted_message_images(
|
||||||
|
event,
|
||||||
|
comp,
|
||||||
|
settings=quoted_message_settings,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
remaining_limit = max(
|
||||||
|
config.max_quoted_fallback_images
|
||||||
|
- fallback_quoted_image_count,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
if remaining_limit <= 0 and fallback_images:
|
||||||
|
logger.warning(
|
||||||
|
"Skip quoted fallback images due to limit=%d for umo=%s",
|
||||||
|
config.max_quoted_fallback_images,
|
||||||
|
event.unified_msg_origin,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if len(fallback_images) > remaining_limit:
|
||||||
|
logger.warning(
|
||||||
|
"Truncate quoted fallback images for umo=%s, reply_id=%s from %d to %d",
|
||||||
|
event.unified_msg_origin,
|
||||||
|
getattr(comp, "id", None),
|
||||||
|
len(fallback_images),
|
||||||
|
remaining_limit,
|
||||||
|
)
|
||||||
|
fallback_images = fallback_images[:remaining_limit]
|
||||||
|
for image_ref in fallback_images:
|
||||||
|
if image_ref in req.image_urls:
|
||||||
|
continue
|
||||||
|
req.image_urls.append(image_ref)
|
||||||
|
fallback_quoted_image_count += 1
|
||||||
|
_append_quoted_image_attachment(req, image_ref)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
logger.warning(
|
||||||
|
"Failed to resolve fallback quoted images for umo=%s, reply_id=%s: %s",
|
||||||
|
event.unified_msg_origin,
|
||||||
|
getattr(comp, "id", None),
|
||||||
|
exc,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
conversation = await _get_session_conv(event, plugin_context)
|
conversation = await _get_session_conv(event, plugin_context)
|
||||||
req.conversation = conversation
|
req.conversation = conversation
|
||||||
@@ -893,6 +1020,7 @@ async def build_main_agent(
|
|||||||
|
|
||||||
if isinstance(req.contexts, str):
|
if isinstance(req.contexts, str):
|
||||||
req.contexts = json.loads(req.contexts)
|
req.contexts = json.loads(req.contexts)
|
||||||
|
req.image_urls = normalize_and_dedupe_strings(req.image_urls)
|
||||||
|
|
||||||
if config.file_extract_enabled:
|
if config.file_extract_enabled:
|
||||||
try:
|
try:
|
||||||
@@ -977,6 +1105,9 @@ async def build_main_agent(
|
|||||||
truncate_turns=config.dequeue_context_length,
|
truncate_turns=config.dequeue_context_length,
|
||||||
enforce_max_turns=config.max_context_length,
|
enforce_max_turns=config.max_context_length,
|
||||||
tool_schema_mode=config.tool_schema_mode,
|
tool_schema_mode=config.tool_schema_mode,
|
||||||
|
fallback_providers=_get_fallback_chat_providers(
|
||||||
|
provider, plugin_context, config.provider_settings
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
if apply_reset:
|
if apply_reset:
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
from pydantic.dataclasses import dataclass
|
from pydantic.dataclasses import dataclass
|
||||||
@@ -240,7 +241,9 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
|
|||||||
if "_&exists_" in json.dumps(result):
|
if "_&exists_" in json.dumps(result):
|
||||||
# Download the file from sandbox
|
# Download the file from sandbox
|
||||||
name = os.path.basename(path)
|
name = os.path.basename(path)
|
||||||
local_path = os.path.join(get_astrbot_temp_path(), name)
|
local_path = os.path.join(
|
||||||
|
get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}"
|
||||||
|
)
|
||||||
await sb.download_file(path, local_path)
|
await sb.download_file(path, local_path)
|
||||||
logger.info(f"Downloaded file from sandbox: {path} -> {local_path}")
|
logger.info(f"Downloaded file from sandbox: {path} -> {local_path}")
|
||||||
return local_path, True
|
return local_path, True
|
||||||
@@ -352,11 +355,11 @@ class SendMessageToUserTool(FunctionTool[AstrAgentContext]):
|
|||||||
MessageChain(chain=components),
|
MessageChain(chain=components),
|
||||||
)
|
)
|
||||||
|
|
||||||
if file_from_sandbox:
|
# if file_from_sandbox:
|
||||||
try:
|
# try:
|
||||||
os.remove(local_path)
|
# os.remove(local_path)
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
logger.error(f"Error removing temp file {local_path}: {e}")
|
# logger.error(f"Error removing temp file {local_path}: {e}")
|
||||||
|
|
||||||
return f"Message sent to session {target_session}"
|
return f"Message sent to session {target_session}"
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class AstrBotConfigManager:
|
|||||||
default_config: AstrBotConfig,
|
default_config: AstrBotConfig,
|
||||||
ucr: UmopConfigRouter,
|
ucr: UmopConfigRouter,
|
||||||
sp: SharedPreferences,
|
sp: SharedPreferences,
|
||||||
):
|
) -> None:
|
||||||
self.sp = sp
|
self.sp = sp
|
||||||
self.ucr = ucr
|
self.ucr = ucr
|
||||||
self.confs: dict[str, AstrBotConfig] = {}
|
self.confs: dict[str, AstrBotConfig] = {}
|
||||||
@@ -56,7 +56,7 @@ class AstrBotConfigManager:
|
|||||||
)
|
)
|
||||||
return self.abconf_data
|
return self.abconf_data
|
||||||
|
|
||||||
def _load_all_configs(self):
|
def _load_all_configs(self) -> None:
|
||||||
"""Load all configurations from the shared preferences."""
|
"""Load all configurations from the shared preferences."""
|
||||||
abconf_data = self._get_abconf_data()
|
abconf_data = self._get_abconf_data()
|
||||||
self.abconf_data = abconf_data
|
self.abconf_data = abconf_data
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from astrbot.core.db.po import (
|
|||||||
CommandConflict,
|
CommandConflict,
|
||||||
ConversationV2,
|
ConversationV2,
|
||||||
Persona,
|
Persona,
|
||||||
|
PersonaFolder,
|
||||||
PlatformMessageHistory,
|
PlatformMessageHistory,
|
||||||
PlatformSession,
|
PlatformSession,
|
||||||
PlatformStat,
|
PlatformStat,
|
||||||
@@ -39,6 +40,7 @@ MAIN_DB_MODELS: dict[str, type[SQLModel]] = {
|
|||||||
"platform_stats": PlatformStat,
|
"platform_stats": PlatformStat,
|
||||||
"conversations": ConversationV2,
|
"conversations": ConversationV2,
|
||||||
"personas": Persona,
|
"personas": Persona,
|
||||||
|
"persona_folders": PersonaFolder,
|
||||||
"preferences": Preference,
|
"preferences": Preference,
|
||||||
"platform_message_history": PlatformMessageHistory,
|
"platform_message_history": PlatformMessageHistory,
|
||||||
"platform_sessions": PlatformSession,
|
"platform_sessions": PlatformSession,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class AstrBotExporter:
|
|||||||
main_db: BaseDatabase,
|
main_db: BaseDatabase,
|
||||||
kb_manager: "KnowledgeBaseManager | None" = None,
|
kb_manager: "KnowledgeBaseManager | None" = None,
|
||||||
config_path: str = CMD_CONFIG_FILE_PATH,
|
config_path: str = CMD_CONFIG_FILE_PATH,
|
||||||
):
|
) -> None:
|
||||||
self.main_db = main_db
|
self.main_db = main_db
|
||||||
self.kb_manager = kb_manager
|
self.kb_manager = kb_manager
|
||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class ImportPreCheckResult:
|
|||||||
class ImportResult:
|
class ImportResult:
|
||||||
"""导入结果"""
|
"""导入结果"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.success = True
|
self.success = True
|
||||||
self.imported_tables: dict[str, int] = {}
|
self.imported_tables: dict[str, int] = {}
|
||||||
self.imported_files: dict[str, int] = {}
|
self.imported_files: dict[str, int] = {}
|
||||||
@@ -161,7 +161,7 @@ class AstrBotImporter:
|
|||||||
kb_manager: "KnowledgeBaseManager | None" = None,
|
kb_manager: "KnowledgeBaseManager | None" = None,
|
||||||
config_path: str = CMD_CONFIG_FILE_PATH,
|
config_path: str = CMD_CONFIG_FILE_PATH,
|
||||||
kb_root_dir: str = KB_PATH,
|
kb_root_dir: str = KB_PATH,
|
||||||
):
|
) -> None:
|
||||||
self.main_db = main_db
|
self.main_db = main_db
|
||||||
self.kb_manager = kb_manager
|
self.kb_manager = kb_manager
|
||||||
self.config_path = config_path
|
self.config_path = config_path
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ComputerBooter:
|
|||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
async def download_file(self, remote_path: str, local_path: str):
|
async def download_file(self, remote_path: str, local_path: str) -> None:
|
||||||
"""Download file from the computer."""
|
"""Download file from the computer."""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ class LocalBooter(ComputerBooter):
|
|||||||
"LocalBooter does not support upload_file operation. Use shell instead."
|
"LocalBooter does not support upload_file operation. Use shell instead."
|
||||||
)
|
)
|
||||||
|
|
||||||
async def download_file(self, remote_path: str, local_path: str):
|
async def download_file(self, remote_path: str, local_path: str) -> None:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
"LocalBooter does not support download_file operation. Use shell instead."
|
"LocalBooter does not support download_file operation. Use shell instead."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from astrbot.api import FunctionTool, logger
|
from astrbot.api import FunctionTool, logger
|
||||||
@@ -10,6 +11,7 @@ from astrbot.core.message.components import File
|
|||||||
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
|
||||||
|
|
||||||
from ..computer_client import get_booter
|
from ..computer_client import get_booter
|
||||||
|
from .permissions import check_admin_permission
|
||||||
|
|
||||||
# @dataclass
|
# @dataclass
|
||||||
# class CreateFileTool(FunctionTool):
|
# class CreateFileTool(FunctionTool):
|
||||||
@@ -100,7 +102,9 @@ class FileUploadTool(FunctionTool):
|
|||||||
self,
|
self,
|
||||||
context: ContextWrapper[AstrAgentContext],
|
context: ContextWrapper[AstrAgentContext],
|
||||||
local_path: str,
|
local_path: str,
|
||||||
):
|
) -> str | None:
|
||||||
|
if permission_error := check_admin_permission(context, "File upload/download"):
|
||||||
|
return permission_error
|
||||||
sb = await get_booter(
|
sb = await get_booter(
|
||||||
context.context.context,
|
context.context.context,
|
||||||
context.context.event.unified_msg_origin,
|
context.context.event.unified_msg_origin,
|
||||||
@@ -160,6 +164,8 @@ class FileDownloadTool(FunctionTool):
|
|||||||
remote_path: str,
|
remote_path: str,
|
||||||
also_send_to_user: bool = True,
|
also_send_to_user: bool = True,
|
||||||
) -> ToolExecResult:
|
) -> ToolExecResult:
|
||||||
|
if permission_error := check_admin_permission(context, "File upload/download"):
|
||||||
|
return permission_error
|
||||||
sb = await get_booter(
|
sb = await get_booter(
|
||||||
context.context.context,
|
context.context.context,
|
||||||
context.context.event.unified_msg_origin,
|
context.context.event.unified_msg_origin,
|
||||||
@@ -167,7 +173,9 @@ class FileDownloadTool(FunctionTool):
|
|||||||
try:
|
try:
|
||||||
name = os.path.basename(remote_path)
|
name = os.path.basename(remote_path)
|
||||||
|
|
||||||
local_path = os.path.join(get_astrbot_temp_path(), name)
|
local_path = os.path.join(
|
||||||
|
get_astrbot_temp_path(), f"sandbox_{uuid.uuid4().hex[:4]}_{name}"
|
||||||
|
)
|
||||||
|
|
||||||
# Download file from sandbox
|
# Download file from sandbox
|
||||||
await sb.download_file(remote_path, local_path)
|
await sb.download_file(remote_path, local_path)
|
||||||
@@ -183,12 +191,12 @@ class FileDownloadTool(FunctionTool):
|
|||||||
logger.error(f"Error sending file message: {e}")
|
logger.error(f"Error sending file message: {e}")
|
||||||
|
|
||||||
# remove
|
# remove
|
||||||
try:
|
# try:
|
||||||
os.remove(local_path)
|
# os.remove(local_path)
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
logger.error(f"Error removing temp file {local_path}: {e}")
|
# logger.error(f"Error removing temp file {local_path}: {e}")
|
||||||
|
|
||||||
return f"File downloaded successfully to {local_path} and sent to user. The file has been removed from local storage."
|
return f"File downloaded successfully to {local_path} and sent to user."
|
||||||
|
|
||||||
return f"File downloaded successfully to {local_path}"
|
return f"File downloaded successfully to {local_path}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from astrbot.core.agent.run_context import ContextWrapper
|
||||||
|
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||||
|
|
||||||
|
|
||||||
|
def check_admin_permission(
|
||||||
|
context: ContextWrapper[AstrAgentContext], operation_name: str
|
||||||
|
) -> str | None:
|
||||||
|
cfg = context.context.context.get_config(
|
||||||
|
umo=context.context.event.unified_msg_origin
|
||||||
|
)
|
||||||
|
provider_settings = cfg.get("provider_settings", {})
|
||||||
|
require_admin = provider_settings.get("computer_use_require_admin", True)
|
||||||
|
if require_admin and context.context.event.role != "admin":
|
||||||
|
return (
|
||||||
|
f"error: Permission denied. {operation_name} is only allowed for admin users. "
|
||||||
|
"Tell user to set admins in `AstrBot WebUI -> Config -> General Config` by adding their user ID to the admins list if they need this feature. "
|
||||||
|
f"User's ID is: {context.context.event.get_sender_id()}. User's ID can be found by using /sid command."
|
||||||
|
)
|
||||||
|
return None
|
||||||
@@ -5,8 +5,10 @@ import mcp
|
|||||||
from astrbot.api import FunctionTool
|
from astrbot.api import FunctionTool
|
||||||
from astrbot.core.agent.run_context import ContextWrapper
|
from astrbot.core.agent.run_context import ContextWrapper
|
||||||
from astrbot.core.agent.tool import ToolExecResult
|
from astrbot.core.agent.tool import ToolExecResult
|
||||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
from astrbot.core.astr_agent_context import AstrAgentContext, AstrMessageEvent
|
||||||
from astrbot.core.computer.computer_client import get_booter, get_local_booter
|
from astrbot.core.computer.computer_client import get_booter, get_local_booter
|
||||||
|
from astrbot.core.computer.tools.permissions import check_admin_permission
|
||||||
|
from astrbot.core.message.message_event_result import MessageChain
|
||||||
|
|
||||||
param_schema = {
|
param_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -25,7 +27,7 @@ param_schema = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def handle_result(result: dict) -> ToolExecResult:
|
async def handle_result(result: dict, event: AstrMessageEvent) -> ToolExecResult:
|
||||||
data = result.get("data", {})
|
data = result.get("data", {})
|
||||||
output = data.get("output", {})
|
output = data.get("output", {})
|
||||||
error = data.get("error", "")
|
error = data.get("error", "")
|
||||||
@@ -44,6 +46,9 @@ def handle_result(result: dict) -> ToolExecResult:
|
|||||||
type="image", data=img["image/png"], mimeType="image/png"
|
type="image", data=img["image/png"], mimeType="image/png"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if event.get_platform_name() == "webchat":
|
||||||
|
await event.send(message=MessageChain().base64_image(img["image/png"]))
|
||||||
if text:
|
if text:
|
||||||
resp.content.append(mcp.types.TextContent(type="text", text=text))
|
resp.content.append(mcp.types.TextContent(type="text", text=text))
|
||||||
|
|
||||||
@@ -62,13 +67,15 @@ class PythonTool(FunctionTool):
|
|||||||
async def call(
|
async def call(
|
||||||
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
|
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
|
||||||
) -> ToolExecResult:
|
) -> ToolExecResult:
|
||||||
|
if permission_error := check_admin_permission(context, "Python execution"):
|
||||||
|
return permission_error
|
||||||
sb = await get_booter(
|
sb = await get_booter(
|
||||||
context.context.context,
|
context.context.context,
|
||||||
context.context.event.unified_msg_origin,
|
context.context.event.unified_msg_origin,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
result = await sb.python.exec(code, silent=silent)
|
result = await sb.python.exec(code, silent=silent)
|
||||||
return handle_result(result)
|
return await handle_result(result, context.context.event)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error executing code: {str(e)}"
|
return f"Error executing code: {str(e)}"
|
||||||
|
|
||||||
@@ -83,12 +90,11 @@ class LocalPythonTool(FunctionTool):
|
|||||||
async def call(
|
async def call(
|
||||||
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
|
self, context: ContextWrapper[AstrAgentContext], code: str, silent: bool = False
|
||||||
) -> ToolExecResult:
|
) -> ToolExecResult:
|
||||||
if context.context.event.role != "admin":
|
if permission_error := check_admin_permission(context, "Python execution"):
|
||||||
return "error: Permission denied. Local Python execution is only allowed for admin users. Tell user to set admins in AstrBot WebUI."
|
return permission_error
|
||||||
|
|
||||||
sb = get_local_booter()
|
sb = get_local_booter()
|
||||||
try:
|
try:
|
||||||
result = await sb.python.exec(code, silent=silent)
|
result = await sb.python.exec(code, silent=silent)
|
||||||
return handle_result(result)
|
return await handle_result(result, context.context.event)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error executing code: {str(e)}"
|
return f"Error executing code: {str(e)}"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from astrbot.core.agent.tool import ToolExecResult
|
|||||||
from astrbot.core.astr_agent_context import AstrAgentContext
|
from astrbot.core.astr_agent_context import AstrAgentContext
|
||||||
|
|
||||||
from ..computer_client import get_booter, get_local_booter
|
from ..computer_client import get_booter, get_local_booter
|
||||||
|
from .permissions import check_admin_permission
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -46,8 +47,8 @@ class ExecuteShellTool(FunctionTool):
|
|||||||
background: bool = False,
|
background: bool = False,
|
||||||
env: dict = {},
|
env: dict = {},
|
||||||
) -> ToolExecResult:
|
) -> ToolExecResult:
|
||||||
if context.context.event.role != "admin":
|
if permission_error := check_admin_permission(context, "Shell execution"):
|
||||||
return "error: Permission denied. Shell execution is only allowed for admin users. Tell user to Set admins in AstrBot WebUI."
|
return permission_error
|
||||||
|
|
||||||
if self.is_local:
|
if self.is_local:
|
||||||
sb = get_local_booter()
|
sb = get_local_booter()
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class AstrBotConfig(dict):
|
|||||||
config_path: str = ASTRBOT_CONFIG_PATH,
|
config_path: str = ASTRBOT_CONFIG_PATH,
|
||||||
default_config: dict = DEFAULT_CONFIG,
|
default_config: dict = DEFAULT_CONFIG,
|
||||||
schema: dict | None = None,
|
schema: dict | None = None,
|
||||||
):
|
) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
# 调用父类的 __setattr__ 方法,防止保存配置时将此属性写入配置文件
|
# 调用父类的 __setattr__ 方法,防止保存配置时将此属性写入配置文件
|
||||||
@@ -52,6 +52,9 @@ class AstrBotConfig(dict):
|
|||||||
|
|
||||||
with open(config_path, encoding="utf-8-sig") as f:
|
with open(config_path, encoding="utf-8-sig") as f:
|
||||||
conf_str = f.read()
|
conf_str = f.read()
|
||||||
|
# Handle UTF-8 BOM if present
|
||||||
|
if conf_str.startswith("\ufeff"):
|
||||||
|
conf_str = conf_str[1:]
|
||||||
conf = json.loads(conf_str)
|
conf = json.loads(conf_str)
|
||||||
|
|
||||||
# 检查配置完整性,并插入
|
# 检查配置完整性,并插入
|
||||||
@@ -66,7 +69,7 @@ class AstrBotConfig(dict):
|
|||||||
"""将 Schema 转换成 Config"""
|
"""将 Schema 转换成 Config"""
|
||||||
conf = {}
|
conf = {}
|
||||||
|
|
||||||
def _parse_schema(schema: dict, conf: dict):
|
def _parse_schema(schema: dict, conf: dict) -> None:
|
||||||
for k, v in schema.items():
|
for k, v in schema.items():
|
||||||
if v["type"] not in DEFAULT_VALUE_MAP:
|
if v["type"] not in DEFAULT_VALUE_MAP:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
@@ -148,7 +151,7 @@ class AstrBotConfig(dict):
|
|||||||
|
|
||||||
return has_new
|
return has_new
|
||||||
|
|
||||||
def save_config(self, replace_config: dict | None = None):
|
def save_config(self, replace_config: dict | None = None) -> None:
|
||||||
"""将配置写入文件
|
"""将配置写入文件
|
||||||
|
|
||||||
如果传入 replace_config,则将配置替换为 replace_config
|
如果传入 replace_config,则将配置替换为 replace_config
|
||||||
@@ -164,14 +167,14 @@ class AstrBotConfig(dict):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def __delattr__(self, key):
|
def __delattr__(self, key) -> None:
|
||||||
try:
|
try:
|
||||||
del self[key]
|
del self[key]
|
||||||
self.save_config()
|
self.save_config()
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise AttributeError(f"没有找到 Key: '{key}'")
|
raise AttributeError(f"没有找到 Key: '{key}'")
|
||||||
|
|
||||||
def __setattr__(self, key, value):
|
def __setattr__(self, key, value) -> None:
|
||||||
self[key] = value
|
self[key] = value
|
||||||
|
|
||||||
def check_exist(self) -> bool:
|
def check_exist(self) -> bool:
|
||||||
|
|||||||
+260
-13
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
|
|||||||
|
|
||||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||||
|
|
||||||
VERSION = "4.14.4"
|
VERSION = "4.18.3"
|
||||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||||
|
|
||||||
WEBHOOK_SUPPORTED_PLATFORMS = [
|
WEBHOOK_SUPPORTED_PLATFORMS = [
|
||||||
@@ -15,6 +15,7 @@ WEBHOOK_SUPPORTED_PLATFORMS = [
|
|||||||
"wecom_ai_bot",
|
"wecom_ai_bot",
|
||||||
"slack",
|
"slack",
|
||||||
"lark",
|
"lark",
|
||||||
|
"line",
|
||||||
]
|
]
|
||||||
|
|
||||||
# 默认配置
|
# 默认配置
|
||||||
@@ -67,6 +68,7 @@ DEFAULT_CONFIG = {
|
|||||||
"provider_settings": {
|
"provider_settings": {
|
||||||
"enable": True,
|
"enable": True,
|
||||||
"default_provider_id": "",
|
"default_provider_id": "",
|
||||||
|
"fallback_chat_models": [],
|
||||||
"default_image_caption_provider_id": "",
|
"default_image_caption_provider_id": "",
|
||||||
"image_caption_prompt": "Please describe the image using Chinese.",
|
"image_caption_prompt": "Please describe the image using Chinese.",
|
||||||
"provider_pool": ["*"], # "*" 表示使用所有可用的提供者
|
"provider_pool": ["*"], # "*" 表示使用所有可用的提供者
|
||||||
@@ -74,6 +76,7 @@ DEFAULT_CONFIG = {
|
|||||||
"web_search": False,
|
"web_search": False,
|
||||||
"websearch_provider": "default",
|
"websearch_provider": "default",
|
||||||
"websearch_tavily_key": [],
|
"websearch_tavily_key": [],
|
||||||
|
"websearch_bocha_key": [],
|
||||||
"websearch_baidu_app_builder_key": "",
|
"websearch_baidu_app_builder_key": "",
|
||||||
"web_search_link": False,
|
"web_search_link": False,
|
||||||
"display_reasoning_text": False,
|
"display_reasoning_text": False,
|
||||||
@@ -97,7 +100,15 @@ DEFAULT_CONFIG = {
|
|||||||
"dequeue_context_length": 1,
|
"dequeue_context_length": 1,
|
||||||
"streaming_response": False,
|
"streaming_response": False,
|
||||||
"show_tool_use_status": False,
|
"show_tool_use_status": False,
|
||||||
|
"show_tool_call_result": False,
|
||||||
"sanitize_context_by_modalities": False,
|
"sanitize_context_by_modalities": False,
|
||||||
|
"max_quoted_fallback_images": 20,
|
||||||
|
"quoted_message_parser": {
|
||||||
|
"max_component_chain_depth": 4,
|
||||||
|
"max_forward_node_depth": 6,
|
||||||
|
"max_forward_fetch": 32,
|
||||||
|
"warn_on_action_failure": False,
|
||||||
|
},
|
||||||
"agent_runner_type": "local",
|
"agent_runner_type": "local",
|
||||||
"dify_agent_runner_provider_id": "",
|
"dify_agent_runner_provider_id": "",
|
||||||
"coze_agent_runner_provider_id": "",
|
"coze_agent_runner_provider_id": "",
|
||||||
@@ -118,6 +129,7 @@ DEFAULT_CONFIG = {
|
|||||||
"add_cron_tools": True,
|
"add_cron_tools": True,
|
||||||
},
|
},
|
||||||
"computer_use_runtime": "local",
|
"computer_use_runtime": "local",
|
||||||
|
"computer_use_require_admin": True,
|
||||||
"sandbox": {
|
"sandbox": {
|
||||||
"booter": "shipyard",
|
"booter": "shipyard",
|
||||||
"shipyard_endpoint": "",
|
"shipyard_endpoint": "",
|
||||||
@@ -128,8 +140,9 @@ DEFAULT_CONFIG = {
|
|||||||
},
|
},
|
||||||
# SubAgent orchestrator mode:
|
# SubAgent orchestrator mode:
|
||||||
# - main_enable = False: disabled; main LLM mounts tools normally (persona selection).
|
# - main_enable = False: disabled; main LLM mounts tools normally (persona selection).
|
||||||
# - main_enable = True: enabled; main LLM will include handoff tools and can optionally
|
# - main_enable = True: enabled; main LLM keeps its own tools and includes handoff
|
||||||
# remove tools that are duplicated on subagents via remove_main_duplicate_tools.
|
# tools (transfer_to_*). remove_main_duplicate_tools can remove tools that are
|
||||||
|
# duplicated on subagents from the main LLM toolset.
|
||||||
"subagent_orchestrator": {
|
"subagent_orchestrator": {
|
||||||
"main_enable": False,
|
"main_enable": False,
|
||||||
"remove_main_duplicate_tools": False,
|
"remove_main_duplicate_tools": False,
|
||||||
@@ -176,7 +189,7 @@ DEFAULT_CONFIG = {
|
|||||||
"t2i_use_file_service": False,
|
"t2i_use_file_service": False,
|
||||||
"t2i_active_template": "base",
|
"t2i_active_template": "base",
|
||||||
"http_proxy": "",
|
"http_proxy": "",
|
||||||
"no_proxy": ["localhost", "127.0.0.1", "::1"],
|
"no_proxy": ["localhost", "127.0.0.1", "::1", "10.*", "192.168.*"],
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"enable": True,
|
"enable": True,
|
||||||
"username": "astrbot",
|
"username": "astrbot",
|
||||||
@@ -185,6 +198,12 @@ DEFAULT_CONFIG = {
|
|||||||
"host": "0.0.0.0",
|
"host": "0.0.0.0",
|
||||||
"port": 6185,
|
"port": 6185,
|
||||||
"disable_access_log": True,
|
"disable_access_log": True,
|
||||||
|
"ssl": {
|
||||||
|
"enable": False,
|
||||||
|
"cert_file": "",
|
||||||
|
"key_file": "",
|
||||||
|
"ca_certs": "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"platform": [],
|
"platform": [],
|
||||||
"platform_specific": {
|
"platform_specific": {
|
||||||
@@ -201,6 +220,7 @@ DEFAULT_CONFIG = {
|
|||||||
"log_file_enable": False,
|
"log_file_enable": False,
|
||||||
"log_file_path": "logs/astrbot.log",
|
"log_file_path": "logs/astrbot.log",
|
||||||
"log_file_max_mb": 20,
|
"log_file_max_mb": 20,
|
||||||
|
"temp_dir_max_size": 1024,
|
||||||
"trace_enable": False,
|
"trace_enable": False,
|
||||||
"trace_log_enable": False,
|
"trace_log_enable": False,
|
||||||
"trace_log_path": "logs/astrbot.trace.log",
|
"trace_log_path": "logs/astrbot.trace.log",
|
||||||
@@ -318,9 +338,11 @@ CONFIG_METADATA_2 = {
|
|||||||
"id": "wecom_ai_bot",
|
"id": "wecom_ai_bot",
|
||||||
"type": "wecom_ai_bot",
|
"type": "wecom_ai_bot",
|
||||||
"enable": True,
|
"enable": True,
|
||||||
"wecomaibot_init_respond_text": "💭 思考中...",
|
"wecomaibot_init_respond_text": "",
|
||||||
"wecomaibot_friend_message_welcome_text": "",
|
"wecomaibot_friend_message_welcome_text": "",
|
||||||
"wecom_ai_bot_name": "",
|
"wecom_ai_bot_name": "",
|
||||||
|
"msg_push_webhook_url": "",
|
||||||
|
"only_use_webhook_url_to_send": False,
|
||||||
"token": "",
|
"token": "",
|
||||||
"encoding_aes_key": "",
|
"encoding_aes_key": "",
|
||||||
"unified_webhook_mode": True,
|
"unified_webhook_mode": True,
|
||||||
@@ -403,6 +425,15 @@ CONFIG_METADATA_2 = {
|
|||||||
"slack_webhook_port": 6197,
|
"slack_webhook_port": 6197,
|
||||||
"slack_webhook_path": "/astrbot-slack-webhook/callback",
|
"slack_webhook_path": "/astrbot-slack-webhook/callback",
|
||||||
},
|
},
|
||||||
|
"Line": {
|
||||||
|
"id": "line",
|
||||||
|
"type": "line",
|
||||||
|
"enable": False,
|
||||||
|
"channel_access_token": "",
|
||||||
|
"channel_secret": "",
|
||||||
|
"unified_webhook_mode": True,
|
||||||
|
"webhook_uuid": "",
|
||||||
|
},
|
||||||
"Satori": {
|
"Satori": {
|
||||||
"id": "satori",
|
"id": "satori",
|
||||||
"type": "satori",
|
"type": "satori",
|
||||||
@@ -686,13 +717,23 @@ CONFIG_METADATA_2 = {
|
|||||||
"wecomaibot_init_respond_text": {
|
"wecomaibot_init_respond_text": {
|
||||||
"description": "企业微信智能机器人初始响应文本",
|
"description": "企业微信智能机器人初始响应文本",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"hint": "当机器人收到消息时,首先回复的文本内容。留空则使用默认值。",
|
"hint": "当机器人收到消息时,首先回复的文本内容。留空则不设置。",
|
||||||
},
|
},
|
||||||
"wecomaibot_friend_message_welcome_text": {
|
"wecomaibot_friend_message_welcome_text": {
|
||||||
"description": "企业微信智能机器人私聊欢迎语",
|
"description": "企业微信智能机器人私聊欢迎语",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"hint": "当用户当天进入智能机器人单聊会话,回复欢迎语,留空则不回复。",
|
"hint": "当用户当天进入智能机器人单聊会话,回复欢迎语,留空则不回复。",
|
||||||
},
|
},
|
||||||
|
"msg_push_webhook_url": {
|
||||||
|
"description": "企业微信消息推送 Webhook URL",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "用于 send_by_session 主动消息推送。格式示例: https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx",
|
||||||
|
},
|
||||||
|
"only_use_webhook_url_to_send": {
|
||||||
|
"description": "仅使用 Webhook 发送消息",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用后,企业微信智能机器人的所有回复都改为通过消息推送 Webhook 发送。消息推送 Webhook 支持更多的消息类型(如图片、文件等)。",
|
||||||
|
},
|
||||||
"lark_bot_name": {
|
"lark_bot_name": {
|
||||||
"description": "飞书机器人的名字",
|
"description": "飞书机器人的名字",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -912,6 +953,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.openai.com/v1",
|
"api_base": "https://api.openai.com/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"Google Gemini": {
|
"Google Gemini": {
|
||||||
@@ -934,6 +976,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"dangerous_content": "BLOCK_MEDIUM_AND_ABOVE",
|
"dangerous_content": "BLOCK_MEDIUM_AND_ABOVE",
|
||||||
},
|
},
|
||||||
"gm_thinking_config": {"budget": 0, "level": "HIGH"},
|
"gm_thinking_config": {"budget": 0, "level": "HIGH"},
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"Anthropic": {
|
"Anthropic": {
|
||||||
"id": "anthropic",
|
"id": "anthropic",
|
||||||
@@ -944,7 +987,8 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.anthropic.com/v1",
|
"api_base": "https://api.anthropic.com/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"anth_thinking_config": {"budget": 0},
|
"proxy": "",
|
||||||
|
"anth_thinking_config": {"type": "", "budget": 0, "effort": ""},
|
||||||
},
|
},
|
||||||
"Moonshot": {
|
"Moonshot": {
|
||||||
"id": "moonshot",
|
"id": "moonshot",
|
||||||
@@ -955,6 +999,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"api_base": "https://api.moonshot.cn/v1",
|
"api_base": "https://api.moonshot.cn/v1",
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"xAI": {
|
"xAI": {
|
||||||
@@ -966,6 +1011,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.x.ai/v1",
|
"api_base": "https://api.x.ai/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"xai_native_search": False,
|
"xai_native_search": False,
|
||||||
},
|
},
|
||||||
@@ -978,6 +1024,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.deepseek.com/v1",
|
"api_base": "https://api.deepseek.com/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"Zhipu": {
|
"Zhipu": {
|
||||||
@@ -989,6 +1036,43 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"api_base": "https://open.bigmodel.cn/api/paas/v4/",
|
"api_base": "https://open.bigmodel.cn/api/paas/v4/",
|
||||||
|
"proxy": "",
|
||||||
|
"custom_headers": {},
|
||||||
|
},
|
||||||
|
"AIHubMix": {
|
||||||
|
"id": "aihubmix",
|
||||||
|
"provider": "aihubmix",
|
||||||
|
"type": "aihubmix_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"timeout": 120,
|
||||||
|
"api_base": "https://aihubmix.com/v1",
|
||||||
|
"proxy": "",
|
||||||
|
"custom_headers": {},
|
||||||
|
},
|
||||||
|
"OpenRouter": {
|
||||||
|
"id": "openrouter",
|
||||||
|
"provider": "openrouter",
|
||||||
|
"type": "openrouter_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"timeout": 120,
|
||||||
|
"api_base": "https://openrouter.ai/v1",
|
||||||
|
"proxy": "",
|
||||||
|
"custom_headers": {},
|
||||||
|
},
|
||||||
|
"NVIDIA": {
|
||||||
|
"id": "nvidia",
|
||||||
|
"provider": "nvidia",
|
||||||
|
"type": "openai_chat_completion",
|
||||||
|
"provider_type": "chat_completion",
|
||||||
|
"enable": True,
|
||||||
|
"key": [],
|
||||||
|
"api_base": "https://integrate.api.nvidia.com/v1",
|
||||||
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"Azure OpenAI": {
|
"Azure OpenAI": {
|
||||||
@@ -1001,6 +1085,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "",
|
"api_base": "",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"Ollama": {
|
"Ollama": {
|
||||||
@@ -1011,6 +1096,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"enable": True,
|
"enable": True,
|
||||||
"key": ["ollama"], # ollama 的 key 默认是 ollama
|
"key": ["ollama"], # ollama 的 key 默认是 ollama
|
||||||
"api_base": "http://127.0.0.1:11434/v1",
|
"api_base": "http://127.0.0.1:11434/v1",
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"LM Studio": {
|
"LM Studio": {
|
||||||
@@ -1021,6 +1107,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"enable": True,
|
"enable": True,
|
||||||
"key": ["lmstudio"],
|
"key": ["lmstudio"],
|
||||||
"api_base": "http://127.0.0.1:1234/v1",
|
"api_base": "http://127.0.0.1:1234/v1",
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"Gemini_OpenAI_API": {
|
"Gemini_OpenAI_API": {
|
||||||
@@ -1032,6 +1119,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://generativelanguage.googleapis.com/v1beta/openai/",
|
"api_base": "https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"Groq": {
|
"Groq": {
|
||||||
@@ -1043,6 +1131,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.groq.com/openai/v1",
|
"api_base": "https://api.groq.com/openai/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"302.AI": {
|
"302.AI": {
|
||||||
@@ -1054,6 +1143,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.302.ai/v1",
|
"api_base": "https://api.302.ai/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"SiliconFlow": {
|
"SiliconFlow": {
|
||||||
@@ -1065,6 +1155,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"api_base": "https://api.siliconflow.cn/v1",
|
"api_base": "https://api.siliconflow.cn/v1",
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"PPIO": {
|
"PPIO": {
|
||||||
@@ -1076,6 +1167,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.ppinfra.com/v3/openai",
|
"api_base": "https://api.ppinfra.com/v3/openai",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"TokenPony": {
|
"TokenPony": {
|
||||||
@@ -1087,6 +1179,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.tokenpony.cn/v1",
|
"api_base": "https://api.tokenpony.cn/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"Compshare": {
|
"Compshare": {
|
||||||
@@ -1098,6 +1191,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.modelverse.cn/v1",
|
"api_base": "https://api.modelverse.cn/v1",
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"ModelScope": {
|
"ModelScope": {
|
||||||
@@ -1109,6 +1203,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"api_base": "https://api-inference.modelscope.cn/v1",
|
"api_base": "https://api-inference.modelscope.cn/v1",
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
},
|
},
|
||||||
"Dify": {
|
"Dify": {
|
||||||
@@ -1124,6 +1219,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"dify_query_input_key": "astrbot_text_query",
|
"dify_query_input_key": "astrbot_text_query",
|
||||||
"variables": {},
|
"variables": {},
|
||||||
"timeout": 60,
|
"timeout": 60,
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"Coze": {
|
"Coze": {
|
||||||
"id": "coze",
|
"id": "coze",
|
||||||
@@ -1135,6 +1231,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"bot_id": "",
|
"bot_id": "",
|
||||||
"coze_api_base": "https://api.coze.cn",
|
"coze_api_base": "https://api.coze.cn",
|
||||||
"timeout": 60,
|
"timeout": 60,
|
||||||
|
"proxy": "",
|
||||||
# "auto_save_history": True,
|
# "auto_save_history": True,
|
||||||
},
|
},
|
||||||
"阿里云百炼应用": {
|
"阿里云百炼应用": {
|
||||||
@@ -1153,6 +1250,7 @@ CONFIG_METADATA_2 = {
|
|||||||
},
|
},
|
||||||
"variables": {},
|
"variables": {},
|
||||||
"timeout": 60,
|
"timeout": 60,
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"FastGPT": {
|
"FastGPT": {
|
||||||
"id": "fastgpt",
|
"id": "fastgpt",
|
||||||
@@ -1163,6 +1261,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"key": [],
|
"key": [],
|
||||||
"api_base": "https://api.fastgpt.in/api/v1",
|
"api_base": "https://api.fastgpt.in/api/v1",
|
||||||
"timeout": 60,
|
"timeout": 60,
|
||||||
|
"proxy": "",
|
||||||
"custom_headers": {},
|
"custom_headers": {},
|
||||||
"custom_extra_body": {},
|
"custom_extra_body": {},
|
||||||
},
|
},
|
||||||
@@ -1175,6 +1274,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"api_key": "",
|
"api_key": "",
|
||||||
"api_base": "",
|
"api_base": "",
|
||||||
"model": "whisper-1",
|
"model": "whisper-1",
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"Whisper(Local)": {
|
"Whisper(Local)": {
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
@@ -1204,6 +1304,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"model": "tts-1",
|
"model": "tts-1",
|
||||||
"openai-tts-voice": "alloy",
|
"openai-tts-voice": "alloy",
|
||||||
"timeout": "20",
|
"timeout": "20",
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"Genie TTS": {
|
"Genie TTS": {
|
||||||
"id": "genie_tts",
|
"id": "genie_tts",
|
||||||
@@ -1284,6 +1385,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"fishaudio-tts-character": "可莉",
|
"fishaudio-tts-character": "可莉",
|
||||||
"fishaudio-tts-reference-id": "",
|
"fishaudio-tts-reference-id": "",
|
||||||
"timeout": "20",
|
"timeout": "20",
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"阿里云百炼 TTS(API)": {
|
"阿里云百炼 TTS(API)": {
|
||||||
"hint": "API Key 从 https://bailian.console.aliyun.com/?tab=model#/api-key 获取。模型和音色的选择文档请参考: 阿里云百炼语音合成音色名称。具体可参考 https://help.aliyun.com/zh/model-studio/speech-synthesis-and-speech-recognition",
|
"hint": "API Key 从 https://bailian.console.aliyun.com/?tab=model#/api-key 获取。模型和音色的选择文档请参考: 阿里云百炼语音合成音色名称。具体可参考 https://help.aliyun.com/zh/model-studio/speech-synthesis-and-speech-recognition",
|
||||||
@@ -1310,6 +1412,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"azure_tts_volume": "100",
|
"azure_tts_volume": "100",
|
||||||
"azure_tts_subscription_key": "",
|
"azure_tts_subscription_key": "",
|
||||||
"azure_tts_region": "eastus",
|
"azure_tts_region": "eastus",
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"MiniMax TTS(API)": {
|
"MiniMax TTS(API)": {
|
||||||
"id": "minimax_tts",
|
"id": "minimax_tts",
|
||||||
@@ -1332,6 +1435,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"minimax-voice-latex": False,
|
"minimax-voice-latex": False,
|
||||||
"minimax-voice-english-normalization": False,
|
"minimax-voice-english-normalization": False,
|
||||||
"timeout": 20,
|
"timeout": 20,
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"火山引擎_TTS(API)": {
|
"火山引擎_TTS(API)": {
|
||||||
"id": "volcengine_tts",
|
"id": "volcengine_tts",
|
||||||
@@ -1346,6 +1450,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"volcengine_speed_ratio": 1.0,
|
"volcengine_speed_ratio": 1.0,
|
||||||
"api_base": "https://openspeech.bytedance.com/api/v1/tts",
|
"api_base": "https://openspeech.bytedance.com/api/v1/tts",
|
||||||
"timeout": 20,
|
"timeout": 20,
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"Gemini TTS": {
|
"Gemini TTS": {
|
||||||
"id": "gemini_tts",
|
"id": "gemini_tts",
|
||||||
@@ -1359,30 +1464,35 @@ CONFIG_METADATA_2 = {
|
|||||||
"gemini_tts_model": "gemini-2.5-flash-preview-tts",
|
"gemini_tts_model": "gemini-2.5-flash-preview-tts",
|
||||||
"gemini_tts_prefix": "",
|
"gemini_tts_prefix": "",
|
||||||
"gemini_tts_voice_name": "Leda",
|
"gemini_tts_voice_name": "Leda",
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"OpenAI Embedding": {
|
"OpenAI Embedding": {
|
||||||
"id": "openai_embedding",
|
"id": "openai_embedding",
|
||||||
"type": "openai_embedding",
|
"type": "openai_embedding",
|
||||||
"provider": "openai",
|
"provider": "openai",
|
||||||
"provider_type": "embedding",
|
"provider_type": "embedding",
|
||||||
|
"hint": "provider_group.provider.openai_embedding.hint",
|
||||||
"enable": True,
|
"enable": True,
|
||||||
"embedding_api_key": "",
|
"embedding_api_key": "",
|
||||||
"embedding_api_base": "",
|
"embedding_api_base": "",
|
||||||
"embedding_model": "",
|
"embedding_model": "",
|
||||||
"embedding_dimensions": 1024,
|
"embedding_dimensions": 1024,
|
||||||
"timeout": 20,
|
"timeout": 20,
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"Gemini Embedding": {
|
"Gemini Embedding": {
|
||||||
"id": "gemini_embedding",
|
"id": "gemini_embedding",
|
||||||
"type": "gemini_embedding",
|
"type": "gemini_embedding",
|
||||||
"provider": "google",
|
"provider": "google",
|
||||||
"provider_type": "embedding",
|
"provider_type": "embedding",
|
||||||
|
"hint": "provider_group.provider.gemini_embedding.hint",
|
||||||
"enable": True,
|
"enable": True,
|
||||||
"embedding_api_key": "",
|
"embedding_api_key": "",
|
||||||
"embedding_api_base": "",
|
"embedding_api_base": "",
|
||||||
"embedding_model": "gemini-embedding-exp-03-07",
|
"embedding_model": "gemini-embedding-exp-03-07",
|
||||||
"embedding_dimensions": 768,
|
"embedding_dimensions": 768,
|
||||||
"timeout": 20,
|
"timeout": 20,
|
||||||
|
"proxy": "",
|
||||||
},
|
},
|
||||||
"vLLM Rerank": {
|
"vLLM Rerank": {
|
||||||
"id": "vllm_rerank",
|
"id": "vllm_rerank",
|
||||||
@@ -1865,13 +1975,25 @@ CONFIG_METADATA_2 = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"anth_thinking_config": {
|
"anth_thinking_config": {
|
||||||
"description": "Thinking Config",
|
"description": "思考配置",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"items": {
|
"items": {
|
||||||
|
"type": {
|
||||||
|
"description": "思考类型",
|
||||||
|
"type": "string",
|
||||||
|
"options": ["", "adaptive"],
|
||||||
|
"hint": "Opus 4.6+ / Sonnet 4.6+ 推荐设为 'adaptive'。留空则使用手动 budget 模式。参见: https://platform.claude.com/docs/en/build-with-claude/adaptive-thinking",
|
||||||
|
},
|
||||||
"budget": {
|
"budget": {
|
||||||
"description": "Thinking Budget",
|
"description": "思考预算",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"hint": "Anthropic thinking.budget_tokens param. Must >= 1024. See: https://platform.claude.com/docs/en/build-with-claude/extended-thinking",
|
"hint": "手动 budget_tokens,需 >= 1024。仅在 type 为空时生效。Opus 4.6 / Sonnet 4.6 上已弃用。参见: https://platform.claude.com/docs/en/build-with-claude/extended-thinking",
|
||||||
|
},
|
||||||
|
"effort": {
|
||||||
|
"description": "思考深度",
|
||||||
|
"type": "string",
|
||||||
|
"options": ["", "low", "medium", "high", "max"],
|
||||||
|
"hint": "type 为 'adaptive' 时控制思考深度。默认 'high'。'max' 仅限 Opus 4.6。参见: https://platform.claude.com/docs/en/build-with-claude/effort",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -2079,6 +2201,11 @@ CONFIG_METADATA_2 = {
|
|||||||
"description": "API Base URL",
|
"description": "API Base URL",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"proxy": {
|
||||||
|
"description": "provider_group.provider.proxy.description",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "provider_group.provider.proxy.hint",
|
||||||
|
},
|
||||||
"model": {
|
"model": {
|
||||||
"description": "模型 ID",
|
"description": "模型 ID",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2147,6 +2274,10 @@ CONFIG_METADATA_2 = {
|
|||||||
"default_provider_id": {
|
"default_provider_id": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"fallback_chat_models": {
|
||||||
|
"type": "list",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
"wake_prefix": {
|
"wake_prefix": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
@@ -2186,6 +2317,9 @@ CONFIG_METADATA_2 = {
|
|||||||
"show_tool_use_status": {
|
"show_tool_use_status": {
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
},
|
},
|
||||||
|
"show_tool_call_result": {
|
||||||
|
"type": "bool",
|
||||||
|
},
|
||||||
"unsupported_streaming_strategy": {
|
"unsupported_streaming_strategy": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
@@ -2341,9 +2475,23 @@ CONFIG_METADATA_2 = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||||
},
|
},
|
||||||
|
"dashboard.ssl.enable": {"type": "bool"},
|
||||||
|
"dashboard.ssl.cert_file": {
|
||||||
|
"type": "string",
|
||||||
|
"condition": {"dashboard.ssl.enable": True},
|
||||||
|
},
|
||||||
|
"dashboard.ssl.key_file": {
|
||||||
|
"type": "string",
|
||||||
|
"condition": {"dashboard.ssl.enable": True},
|
||||||
|
},
|
||||||
|
"dashboard.ssl.ca_certs": {
|
||||||
|
"type": "string",
|
||||||
|
"condition": {"dashboard.ssl.enable": True},
|
||||||
|
},
|
||||||
"log_file_enable": {"type": "bool"},
|
"log_file_enable": {"type": "bool"},
|
||||||
"log_file_path": {"type": "string", "condition": {"log_file_enable": True}},
|
"log_file_path": {"type": "string", "condition": {"log_file_enable": True}},
|
||||||
"log_file_max_mb": {"type": "int", "condition": {"log_file_enable": True}},
|
"log_file_max_mb": {"type": "int", "condition": {"log_file_enable": True}},
|
||||||
|
"temp_dir_max_size": {"type": "int"},
|
||||||
"trace_log_enable": {"type": "bool"},
|
"trace_log_enable": {"type": "bool"},
|
||||||
"trace_log_path": {
|
"trace_log_path": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2443,15 +2591,22 @@ CONFIG_METADATA_3 = {
|
|||||||
},
|
},
|
||||||
"ai": {
|
"ai": {
|
||||||
"description": "模型",
|
"description": "模型",
|
||||||
"hint": "当使用非内置 Agent 执行器时,默认聊天模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。",
|
"hint": "当使用非内置 Agent 执行器时,默认对话模型和默认图片转述模型可能会无效,但某些插件会依赖此配置项来调用 AI 能力。",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"items": {
|
"items": {
|
||||||
"provider_settings.default_provider_id": {
|
"provider_settings.default_provider_id": {
|
||||||
"description": "默认聊天模型",
|
"description": "默认对话模型",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"_special": "select_provider",
|
"_special": "select_provider",
|
||||||
"hint": "留空时使用第一个模型",
|
"hint": "留空时使用第一个模型",
|
||||||
},
|
},
|
||||||
|
"provider_settings.fallback_chat_models": {
|
||||||
|
"description": "回退对话模型列表",
|
||||||
|
"type": "list",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"_special": "select_providers",
|
||||||
|
"hint": "主聊天模型请求失败时,按顺序切换到这些模型。",
|
||||||
|
},
|
||||||
"provider_settings.default_image_caption_provider_id": {
|
"provider_settings.default_image_caption_provider_id": {
|
||||||
"description": "默认图片转述模型",
|
"description": "默认图片转述模型",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2563,7 +2718,7 @@ CONFIG_METADATA_3 = {
|
|||||||
"provider_settings.websearch_provider": {
|
"provider_settings.websearch_provider": {
|
||||||
"description": "网页搜索提供商",
|
"description": "网页搜索提供商",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"options": ["default", "tavily", "baidu_ai_search"],
|
"options": ["default", "tavily", "baidu_ai_search", "bocha"],
|
||||||
"condition": {
|
"condition": {
|
||||||
"provider_settings.web_search": True,
|
"provider_settings.web_search": True,
|
||||||
},
|
},
|
||||||
@@ -2578,6 +2733,16 @@ CONFIG_METADATA_3 = {
|
|||||||
"provider_settings.web_search": True,
|
"provider_settings.web_search": True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"provider_settings.websearch_bocha_key": {
|
||||||
|
"description": "BoCha API Key",
|
||||||
|
"type": "list",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"hint": "可添加多个 Key 进行轮询。",
|
||||||
|
"condition": {
|
||||||
|
"provider_settings.websearch_provider": "bocha",
|
||||||
|
"provider_settings.web_search": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
"provider_settings.websearch_baidu_app_builder_key": {
|
"provider_settings.websearch_baidu_app_builder_key": {
|
||||||
"description": "百度千帆智能云 APP Builder API Key",
|
"description": "百度千帆智能云 APP Builder API Key",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2611,6 +2776,11 @@ CONFIG_METADATA_3 = {
|
|||||||
"labels": ["无", "本地", "沙箱"],
|
"labels": ["无", "本地", "沙箱"],
|
||||||
"hint": "选择 Computer Use 运行环境。",
|
"hint": "选择 Computer Use 运行环境。",
|
||||||
},
|
},
|
||||||
|
"provider_settings.computer_use_require_admin": {
|
||||||
|
"description": "需要 AstrBot 管理员权限",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "开启后,需要 AstrBot 管理员权限才能调用使用电脑能力。在平台配置->管理员中可添加管理员。使用 /sid 指令查看管理员 ID。",
|
||||||
|
},
|
||||||
"provider_settings.sandbox.booter": {
|
"provider_settings.sandbox.booter": {
|
||||||
"description": "沙箱环境驱动器",
|
"description": "沙箱环境驱动器",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -2838,6 +3008,15 @@ CONFIG_METADATA_3 = {
|
|||||||
"provider_settings.agent_runner_type": "local",
|
"provider_settings.agent_runner_type": "local",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"provider_settings.show_tool_call_result": {
|
||||||
|
"description": "输出函数调用返回结果",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "仅在输出函数调用状态启用时生效,展示结果前 70 个字符。",
|
||||||
|
"condition": {
|
||||||
|
"provider_settings.agent_runner_type": "local",
|
||||||
|
"provider_settings.show_tool_use_status": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
"provider_settings.sanitize_context_by_modalities": {
|
"provider_settings.sanitize_context_by_modalities": {
|
||||||
"description": "按模型能力清理历史上下文",
|
"description": "按模型能力清理历史上下文",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
@@ -2846,6 +3025,46 @@ CONFIG_METADATA_3 = {
|
|||||||
"provider_settings.agent_runner_type": "local",
|
"provider_settings.agent_runner_type": "local",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"provider_settings.max_quoted_fallback_images": {
|
||||||
|
"description": "引用图片回退解析上限",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "引用/转发消息回退解析图片时的最大注入数量,超出会截断。",
|
||||||
|
"condition": {
|
||||||
|
"provider_settings.agent_runner_type": "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provider_settings.quoted_message_parser.max_component_chain_depth": {
|
||||||
|
"description": "引用解析组件链深度",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "解析 Reply 组件链时允许的最大递归深度。",
|
||||||
|
"condition": {
|
||||||
|
"provider_settings.agent_runner_type": "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provider_settings.quoted_message_parser.max_forward_node_depth": {
|
||||||
|
"description": "引用解析转发节点深度",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "解析合并转发节点时允许的最大递归深度。",
|
||||||
|
"condition": {
|
||||||
|
"provider_settings.agent_runner_type": "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provider_settings.quoted_message_parser.max_forward_fetch": {
|
||||||
|
"description": "引用解析转发拉取上限",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "递归拉取 get_forward_msg 的最大次数。",
|
||||||
|
"condition": {
|
||||||
|
"provider_settings.agent_runner_type": "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"provider_settings.quoted_message_parser.warn_on_action_failure": {
|
||||||
|
"description": "引用解析 action 失败告警",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "开启后,get_msg/get_forward_msg 全部尝试失败时输出 warning 日志。",
|
||||||
|
"condition": {
|
||||||
|
"provider_settings.agent_runner_type": "local",
|
||||||
|
},
|
||||||
|
},
|
||||||
"provider_settings.max_agent_step": {
|
"provider_settings.max_agent_step": {
|
||||||
"description": "工具调用轮数上限",
|
"description": "工具调用轮数上限",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
@@ -3297,6 +3516,29 @@ CONFIG_METADATA_3_SYSTEM = {
|
|||||||
"hint": "控制台输出日志的级别。",
|
"hint": "控制台输出日志的级别。",
|
||||||
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
"options": ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||||
},
|
},
|
||||||
|
"dashboard.ssl.enable": {
|
||||||
|
"description": "启用 WebUI HTTPS",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用后,WebUI 将直接使用 HTTPS 提供服务。",
|
||||||
|
},
|
||||||
|
"dashboard.ssl.cert_file": {
|
||||||
|
"description": "SSL 证书文件路径",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "证书文件路径(PEM)。支持绝对路径和相对路径(相对于当前工作目录)。",
|
||||||
|
"condition": {"dashboard.ssl.enable": True},
|
||||||
|
},
|
||||||
|
"dashboard.ssl.key_file": {
|
||||||
|
"description": "SSL 私钥文件路径",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "私钥文件路径(PEM)。支持绝对路径和相对路径(相对于当前工作目录)。",
|
||||||
|
"condition": {"dashboard.ssl.enable": True},
|
||||||
|
},
|
||||||
|
"dashboard.ssl.ca_certs": {
|
||||||
|
"description": "SSL CA 证书文件路径",
|
||||||
|
"type": "string",
|
||||||
|
"hint": "可选。用于指定 CA 证书文件路径。",
|
||||||
|
"condition": {"dashboard.ssl.enable": True},
|
||||||
|
},
|
||||||
"log_file_enable": {
|
"log_file_enable": {
|
||||||
"description": "启用文件日志",
|
"description": "启用文件日志",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
@@ -3312,6 +3554,11 @@ CONFIG_METADATA_3_SYSTEM = {
|
|||||||
"type": "int",
|
"type": "int",
|
||||||
"hint": "超过大小后自动轮转,默认 20MB。",
|
"hint": "超过大小后自动轮转,默认 20MB。",
|
||||||
},
|
},
|
||||||
|
"temp_dir_max_size": {
|
||||||
|
"description": "临时目录大小上限 (MB)",
|
||||||
|
"type": "int",
|
||||||
|
"hint": "用于限制 data/temp 目录总大小,单位为 MB。系统每 10 分钟检查一次,超限时按文件修改时间从旧到新删除,释放约 30% 当前体积。",
|
||||||
|
},
|
||||||
"trace_log_enable": {
|
"trace_log_enable": {
|
||||||
"description": "启用 Trace 文件日志",
|
"description": "启用 Trace 文件日志",
|
||||||
"type": "bool",
|
"type": "bool",
|
||||||
|
|||||||
@@ -42,6 +42,55 @@ class ConfigMetadataI18n:
|
|||||||
"""
|
"""
|
||||||
result = {}
|
result = {}
|
||||||
|
|
||||||
|
def convert_items(
|
||||||
|
group: str, section: str, items: dict[str, Any], prefix: str = ""
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
items_result: dict[str, Any] = {}
|
||||||
|
|
||||||
|
for field_key, field_data in items.items():
|
||||||
|
if not isinstance(field_data, dict):
|
||||||
|
items_result[field_key] = field_data
|
||||||
|
continue
|
||||||
|
|
||||||
|
field_name = field_key
|
||||||
|
field_path = f"{prefix}.{field_name}" if prefix else field_name
|
||||||
|
|
||||||
|
field_result = {
|
||||||
|
key: value
|
||||||
|
for key, value in field_data.items()
|
||||||
|
if key not in {"description", "hint", "labels", "name"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if "description" in field_data:
|
||||||
|
field_result["description"] = (
|
||||||
|
f"{group}.{section}.{field_path}.description"
|
||||||
|
)
|
||||||
|
if "hint" in field_data:
|
||||||
|
field_result["hint"] = f"{group}.{section}.{field_path}.hint"
|
||||||
|
if "labels" in field_data:
|
||||||
|
field_result["labels"] = f"{group}.{section}.{field_path}.labels"
|
||||||
|
if "name" in field_data:
|
||||||
|
field_result["name"] = f"{group}.{section}.{field_path}.name"
|
||||||
|
|
||||||
|
if "items" in field_data and isinstance(field_data["items"], dict):
|
||||||
|
field_result["items"] = convert_items(
|
||||||
|
group, section, field_data["items"], field_path
|
||||||
|
)
|
||||||
|
|
||||||
|
if "template_schema" in field_data and isinstance(
|
||||||
|
field_data["template_schema"], dict
|
||||||
|
):
|
||||||
|
field_result["template_schema"] = convert_items(
|
||||||
|
group,
|
||||||
|
section,
|
||||||
|
field_data["template_schema"],
|
||||||
|
f"{field_path}.template_schema",
|
||||||
|
)
|
||||||
|
|
||||||
|
items_result[field_key] = field_result
|
||||||
|
|
||||||
|
return items_result
|
||||||
|
|
||||||
for group_key, group_data in metadata.items():
|
for group_key, group_data in metadata.items():
|
||||||
group_result = {
|
group_result = {
|
||||||
"name": f"{group_key}.name",
|
"name": f"{group_key}.name",
|
||||||
@@ -50,59 +99,19 @@ class ConfigMetadataI18n:
|
|||||||
|
|
||||||
for section_key, section_data in group_data.get("metadata", {}).items():
|
for section_key, section_data in group_data.get("metadata", {}).items():
|
||||||
section_result = {
|
section_result = {
|
||||||
"description": f"{group_key}.{section_key}.description",
|
key: value
|
||||||
"type": section_data.get("type"),
|
for key, value in section_data.items()
|
||||||
|
if key not in {"description", "hint", "labels", "name"}
|
||||||
}
|
}
|
||||||
|
section_result["description"] = f"{group_key}.{section_key}.description"
|
||||||
|
|
||||||
# 复制其他属性
|
|
||||||
for key in ["items", "condition", "_special", "invisible"]:
|
|
||||||
if key in section_data:
|
|
||||||
section_result[key] = section_data[key]
|
|
||||||
|
|
||||||
# 处理 hint
|
|
||||||
if "hint" in section_data:
|
if "hint" in section_data:
|
||||||
section_result["hint"] = f"{group_key}.{section_key}.hint"
|
section_result["hint"] = f"{group_key}.{section_key}.hint"
|
||||||
|
|
||||||
# 处理 items 中的字段
|
|
||||||
if "items" in section_data and isinstance(section_data["items"], dict):
|
if "items" in section_data and isinstance(section_data["items"], dict):
|
||||||
items_result = {}
|
section_result["items"] = convert_items(
|
||||||
for field_key, field_data in section_data["items"].items():
|
group_key, section_key, section_data["items"]
|
||||||
# 处理嵌套的点号字段名(如 provider_settings.enable)
|
)
|
||||||
field_name = field_key
|
|
||||||
|
|
||||||
field_result = {}
|
|
||||||
|
|
||||||
# 复制基本属性
|
|
||||||
for attr in [
|
|
||||||
"type",
|
|
||||||
"condition",
|
|
||||||
"_special",
|
|
||||||
"invisible",
|
|
||||||
"options",
|
|
||||||
"slider",
|
|
||||||
]:
|
|
||||||
if attr in field_data:
|
|
||||||
field_result[attr] = field_data[attr]
|
|
||||||
|
|
||||||
# 转换文本属性为国际化键
|
|
||||||
if "description" in field_data:
|
|
||||||
field_result["description"] = (
|
|
||||||
f"{group_key}.{section_key}.{field_name}.description"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "hint" in field_data:
|
|
||||||
field_result["hint"] = (
|
|
||||||
f"{group_key}.{section_key}.{field_name}.hint"
|
|
||||||
)
|
|
||||||
|
|
||||||
if "labels" in field_data:
|
|
||||||
field_result["labels"] = (
|
|
||||||
f"{group_key}.{section_key}.{field_name}.labels"
|
|
||||||
)
|
|
||||||
|
|
||||||
items_result[field_key] = field_result
|
|
||||||
|
|
||||||
section_result["items"] = items_result
|
|
||||||
|
|
||||||
group_result["metadata"][section_key] = section_result
|
group_result["metadata"][section_key] = section_result
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from astrbot.core.db.po import Conversation, ConversationV2
|
|||||||
class ConversationManager:
|
class ConversationManager:
|
||||||
"""负责管理会话与 LLM 的对话,某个会话当前正在用哪个对话。"""
|
"""负责管理会话与 LLM 的对话,某个会话当前正在用哪个对话。"""
|
||||||
|
|
||||||
def __init__(self, db_helper: BaseDatabase):
|
def __init__(self, db_helper: BaseDatabase) -> None:
|
||||||
self.session_conversations: dict[str, str] = {}
|
self.session_conversations: dict[str, str] = {}
|
||||||
self.db = db_helper
|
self.db = db_helper
|
||||||
self.save_interval = 60 # 每 60 秒保存一次
|
self.save_interval = 60 # 每 60 秒保存一次
|
||||||
@@ -106,7 +106,9 @@ class ConversationManager:
|
|||||||
await sp.session_put(unified_msg_origin, "sel_conv_id", conv.conversation_id)
|
await sp.session_put(unified_msg_origin, "sel_conv_id", conv.conversation_id)
|
||||||
return conv.conversation_id
|
return conv.conversation_id
|
||||||
|
|
||||||
async def switch_conversation(self, unified_msg_origin: str, conversation_id: str):
|
async def switch_conversation(
|
||||||
|
self, unified_msg_origin: str, conversation_id: str
|
||||||
|
) -> None:
|
||||||
"""切换会话的对话
|
"""切换会话的对话
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -121,7 +123,7 @@ class ConversationManager:
|
|||||||
self,
|
self,
|
||||||
unified_msg_origin: str,
|
unified_msg_origin: str,
|
||||||
conversation_id: str | None = None,
|
conversation_id: str | None = None,
|
||||||
):
|
) -> None:
|
||||||
"""删除会话的对话,当 conversation_id 为 None 时删除会话当前的对话
|
"""删除会话的对话,当 conversation_id 为 None 时删除会话当前的对话
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -138,7 +140,7 @@ class ConversationManager:
|
|||||||
self.session_conversations.pop(unified_msg_origin, None)
|
self.session_conversations.pop(unified_msg_origin, None)
|
||||||
await sp.session_remove(unified_msg_origin, "sel_conv_id")
|
await sp.session_remove(unified_msg_origin, "sel_conv_id")
|
||||||
|
|
||||||
async def delete_conversations_by_user_id(self, unified_msg_origin: str):
|
async def delete_conversations_by_user_id(self, unified_msg_origin: str) -> None:
|
||||||
"""删除会话的所有对话
|
"""删除会话的所有对话
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ from astrbot.core.umop_config_router import UmopConfigRouter
|
|||||||
from astrbot.core.updator import AstrBotUpdator
|
from astrbot.core.updator import AstrBotUpdator
|
||||||
from astrbot.core.utils.llm_metadata import update_llm_metadata
|
from astrbot.core.utils.llm_metadata import update_llm_metadata
|
||||||
from astrbot.core.utils.migra_helper import migra
|
from astrbot.core.utils.migra_helper import migra
|
||||||
|
from astrbot.core.utils.temp_dir_cleaner import TempDirCleaner
|
||||||
|
|
||||||
from . import astrbot_config, html_renderer
|
from . import astrbot_config, html_renderer
|
||||||
from .event_bus import EventBus
|
from .event_bus import EventBus
|
||||||
@@ -57,6 +58,7 @@ class AstrBotCoreLifecycle:
|
|||||||
|
|
||||||
self.subagent_orchestrator: SubAgentOrchestrator | None = None
|
self.subagent_orchestrator: SubAgentOrchestrator | None = None
|
||||||
self.cron_manager: CronJobManager | None = None
|
self.cron_manager: CronJobManager | None = None
|
||||||
|
self.temp_dir_cleaner: TempDirCleaner | None = None
|
||||||
|
|
||||||
# 设置代理
|
# 设置代理
|
||||||
proxy_config = self.astrbot_config.get("http_proxy", "")
|
proxy_config = self.astrbot_config.get("http_proxy", "")
|
||||||
@@ -125,6 +127,12 @@ class AstrBotCoreLifecycle:
|
|||||||
ucr=self.umop_config_router,
|
ucr=self.umop_config_router,
|
||||||
sp=sp,
|
sp=sp,
|
||||||
)
|
)
|
||||||
|
self.temp_dir_cleaner = TempDirCleaner(
|
||||||
|
max_size_getter=lambda: self.astrbot_config_mgr.default_conf.get(
|
||||||
|
TempDirCleaner.CONFIG_KEY,
|
||||||
|
TempDirCleaner.DEFAULT_MAX_SIZE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# apply migration
|
# apply migration
|
||||||
try:
|
try:
|
||||||
@@ -238,6 +246,12 @@ class AstrBotCoreLifecycle:
|
|||||||
self.cron_manager.start(self.star_context),
|
self.cron_manager.start(self.star_context),
|
||||||
name="cron_manager",
|
name="cron_manager",
|
||||||
)
|
)
|
||||||
|
temp_dir_cleaner_task = None
|
||||||
|
if self.temp_dir_cleaner:
|
||||||
|
temp_dir_cleaner_task = asyncio.create_task(
|
||||||
|
self.temp_dir_cleaner.run(),
|
||||||
|
name="temp_dir_cleaner",
|
||||||
|
)
|
||||||
|
|
||||||
# 把插件中注册的所有协程函数注册到事件总线中并执行
|
# 把插件中注册的所有协程函数注册到事件总线中并执行
|
||||||
extra_tasks = []
|
extra_tasks = []
|
||||||
@@ -247,6 +261,8 @@ class AstrBotCoreLifecycle:
|
|||||||
tasks_ = [event_bus_task, *(extra_tasks if extra_tasks else [])]
|
tasks_ = [event_bus_task, *(extra_tasks if extra_tasks else [])]
|
||||||
if cron_task:
|
if cron_task:
|
||||||
tasks_.append(cron_task)
|
tasks_.append(cron_task)
|
||||||
|
if temp_dir_cleaner_task:
|
||||||
|
tasks_.append(temp_dir_cleaner_task)
|
||||||
for task in tasks_:
|
for task in tasks_:
|
||||||
self.curr_tasks.append(
|
self.curr_tasks.append(
|
||||||
asyncio.create_task(self._task_wrapper(task), name=task.get_name()),
|
asyncio.create_task(self._task_wrapper(task), name=task.get_name()),
|
||||||
@@ -298,6 +314,9 @@ class AstrBotCoreLifecycle:
|
|||||||
|
|
||||||
async def stop(self) -> None:
|
async def stop(self) -> None:
|
||||||
"""停止 AstrBot 核心生命周期管理类, 取消所有当前任务并终止各个管理器."""
|
"""停止 AstrBot 核心生命周期管理类, 取消所有当前任务并终止各个管理器."""
|
||||||
|
if self.temp_dir_cleaner:
|
||||||
|
await self.temp_dir_cleaner.stop()
|
||||||
|
|
||||||
# 请求停止所有正在运行的异步任务
|
# 请求停止所有正在运行的异步任务
|
||||||
for task in self.curr_tasks:
|
for task in self.curr_tasks:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class CronMessageEvent(AstrMessageEvent):
|
|||||||
sender_name: str = "Scheduler",
|
sender_name: str = "Scheduler",
|
||||||
extras: dict[str, Any] | None = None,
|
extras: dict[str, Any] | None = None,
|
||||||
message_type: MessageType = MessageType.FRIEND_MESSAGE,
|
message_type: MessageType = MessageType.FRIEND_MESSAGE,
|
||||||
):
|
) -> None:
|
||||||
platform_meta = PlatformMetadata(
|
platform_meta = PlatformMetadata(
|
||||||
name="cron",
|
name="cron",
|
||||||
description="CronJob",
|
description="CronJob",
|
||||||
@@ -53,13 +53,13 @@ class CronMessageEvent(AstrMessageEvent):
|
|||||||
if extras:
|
if extras:
|
||||||
self._extras.update(extras)
|
self._extras.update(extras)
|
||||||
|
|
||||||
async def send(self, message: MessageChain):
|
async def send(self, message: MessageChain) -> None:
|
||||||
if message is None:
|
if message is None:
|
||||||
return
|
return
|
||||||
await self.context_obj.send_message(self.session, message)
|
await self.context_obj.send_message(self.session, message)
|
||||||
await super().send(message)
|
await super().send(message)
|
||||||
|
|
||||||
async def send_streaming(self, generator, use_fallback: bool = False):
|
async def send_streaming(self, generator, use_fallback: bool = False) -> None:
|
||||||
async for chain in generator:
|
async for chain in generator:
|
||||||
await self.send(chain)
|
await self.send(chain)
|
||||||
|
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ if TYPE_CHECKING:
|
|||||||
class CronJobManager:
|
class CronJobManager:
|
||||||
"""Central scheduler for BasicCronJob and ActiveAgentCronJob."""
|
"""Central scheduler for BasicCronJob and ActiveAgentCronJob."""
|
||||||
|
|
||||||
def __init__(self, db: BaseDatabase):
|
def __init__(self, db: BaseDatabase) -> None:
|
||||||
self.db = db
|
self.db = db
|
||||||
self.scheduler = AsyncIOScheduler()
|
self.scheduler = AsyncIOScheduler()
|
||||||
self._basic_handlers: dict[str, Callable[..., Any]] = {}
|
self._basic_handlers: dict[str, Callable[..., Any]] = {}
|
||||||
self._lock = asyncio.Lock()
|
self._lock = asyncio.Lock()
|
||||||
self._started = False
|
self._started = False
|
||||||
|
|
||||||
async def start(self, ctx: "Context"):
|
async def start(self, ctx: "Context") -> None:
|
||||||
self.ctx: Context = ctx # star context
|
self.ctx: Context = ctx # star context
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
if self._started:
|
if self._started:
|
||||||
@@ -41,14 +41,14 @@ class CronJobManager:
|
|||||||
self._started = True
|
self._started = True
|
||||||
await self.sync_from_db()
|
await self.sync_from_db()
|
||||||
|
|
||||||
async def shutdown(self):
|
async def shutdown(self) -> None:
|
||||||
async with self._lock:
|
async with self._lock:
|
||||||
if not self._started:
|
if not self._started:
|
||||||
return
|
return
|
||||||
self.scheduler.shutdown(wait=False)
|
self.scheduler.shutdown(wait=False)
|
||||||
self._started = False
|
self._started = False
|
||||||
|
|
||||||
async def sync_from_db(self):
|
async def sync_from_db(self) -> None:
|
||||||
jobs = await self.db.list_cron_jobs()
|
jobs = await self.db.list_cron_jobs()
|
||||||
for job in jobs:
|
for job in jobs:
|
||||||
if not job.enabled or not job.persistent:
|
if not job.enabled or not job.persistent:
|
||||||
@@ -136,11 +136,11 @@ class CronJobManager:
|
|||||||
async def list_jobs(self, job_type: str | None = None) -> list[CronJob]:
|
async def list_jobs(self, job_type: str | None = None) -> list[CronJob]:
|
||||||
return await self.db.list_cron_jobs(job_type)
|
return await self.db.list_cron_jobs(job_type)
|
||||||
|
|
||||||
def _remove_scheduled(self, job_id: str):
|
def _remove_scheduled(self, job_id: str) -> None:
|
||||||
if self.scheduler.get_job(job_id):
|
if self.scheduler.get_job(job_id):
|
||||||
self.scheduler.remove_job(job_id)
|
self.scheduler.remove_job(job_id)
|
||||||
|
|
||||||
def _schedule_job(self, job: CronJob):
|
def _schedule_job(self, job: CronJob) -> None:
|
||||||
if not self._started:
|
if not self._started:
|
||||||
self.scheduler.start()
|
self.scheduler.start()
|
||||||
self._started = True
|
self._started = True
|
||||||
@@ -188,7 +188,7 @@ class CronJobManager:
|
|||||||
aps_job = self.scheduler.get_job(job_id)
|
aps_job = self.scheduler.get_job(job_id)
|
||||||
return aps_job.next_run_time if aps_job else None
|
return aps_job.next_run_time if aps_job else None
|
||||||
|
|
||||||
async def _run_job(self, job_id: str):
|
async def _run_job(self, job_id: str) -> None:
|
||||||
job = await self.db.get_cron_job(job_id)
|
job = await self.db.get_cron_job(job_id)
|
||||||
if not job or not job.enabled:
|
if not job or not job.enabled:
|
||||||
return
|
return
|
||||||
@@ -222,7 +222,7 @@ class CronJobManager:
|
|||||||
# one-shot: remove after execution regardless of success
|
# one-shot: remove after execution regardless of success
|
||||||
await self.delete_job(job_id)
|
await self.delete_job(job_id)
|
||||||
|
|
||||||
async def _run_basic_job(self, job: CronJob):
|
async def _run_basic_job(self, job: CronJob) -> None:
|
||||||
handler = self._basic_handlers.get(job.job_id)
|
handler = self._basic_handlers.get(job.job_id)
|
||||||
if not handler:
|
if not handler:
|
||||||
raise RuntimeError(f"Basic cron job handler not found for {job.job_id}")
|
raise RuntimeError(f"Basic cron job handler not found for {job.job_id}")
|
||||||
@@ -231,7 +231,7 @@ class CronJobManager:
|
|||||||
if asyncio.iscoroutine(result):
|
if asyncio.iscoroutine(result):
|
||||||
await result
|
await result
|
||||||
|
|
||||||
async def _run_active_agent_job(self, job: CronJob, start_time: datetime):
|
async def _run_active_agent_job(self, job: CronJob, start_time: datetime) -> None:
|
||||||
payload = job.payload or {}
|
payload = job.payload or {}
|
||||||
session_str = payload.get("session")
|
session_str = payload.get("session")
|
||||||
if not session_str:
|
if not session_str:
|
||||||
@@ -266,7 +266,7 @@ class CronJobManager:
|
|||||||
message: str,
|
message: str,
|
||||||
session_str: str,
|
session_str: str,
|
||||||
extras: dict,
|
extras: dict,
|
||||||
):
|
) -> None:
|
||||||
"""Woke the main agent to handle the cron job message."""
|
"""Woke the main agent to handle the cron job message."""
|
||||||
from astrbot.core.astr_main_agent import (
|
from astrbot.core.astr_main_agent import (
|
||||||
MainAgentBuildConfig,
|
MainAgentBuildConfig,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from deprecated import deprecated
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
||||||
|
|
||||||
from astrbot.core.db.po import (
|
from astrbot.core.db.po import (
|
||||||
|
ApiKey,
|
||||||
Attachment,
|
Attachment,
|
||||||
ChatUIProject,
|
ChatUIProject,
|
||||||
CommandConfig,
|
CommandConfig,
|
||||||
@@ -43,7 +44,7 @@ class BaseDatabase(abc.ABC):
|
|||||||
expire_on_commit=False,
|
expire_on_commit=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self) -> None:
|
||||||
"""初始化数据库连接"""
|
"""初始化数据库连接"""
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
@@ -248,6 +249,55 @@ class BaseDatabase(abc.ABC):
|
|||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def create_api_key(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
key_hash: str,
|
||||||
|
key_prefix: str,
|
||||||
|
scopes: list[str] | None,
|
||||||
|
created_by: str,
|
||||||
|
expires_at: datetime.datetime | None = None,
|
||||||
|
) -> ApiKey:
|
||||||
|
"""Create a new API key record."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def list_api_keys(self) -> list[ApiKey]:
|
||||||
|
"""List all API keys."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def get_api_key_by_id(self, key_id: str) -> ApiKey | None:
|
||||||
|
"""Get an API key by key_id."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def get_active_api_key_by_hash(self, key_hash: str) -> ApiKey | None:
|
||||||
|
"""Get an active API key by hash (not revoked, not expired)."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def touch_api_key(self, key_id: str) -> None:
|
||||||
|
"""Update last_used_at of an API key."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def revoke_api_key(self, key_id: str) -> bool:
|
||||||
|
"""Revoke an API key.
|
||||||
|
|
||||||
|
Returns True when the key exists and is updated.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def delete_api_key(self, key_id: str) -> bool:
|
||||||
|
"""Delete an API key.
|
||||||
|
|
||||||
|
Returns True when the key exists and is deleted.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def insert_persona(
|
async def insert_persona(
|
||||||
self,
|
self,
|
||||||
@@ -608,6 +658,22 @@ class BaseDatabase(abc.ABC):
|
|||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
async def get_platform_sessions_by_creator_paginated(
|
||||||
|
self,
|
||||||
|
creator: str,
|
||||||
|
platform_id: str | None = None,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20,
|
||||||
|
exclude_project_sessions: bool = False,
|
||||||
|
) -> tuple[list[dict], int]:
|
||||||
|
"""Get paginated platform sessions and total count for a creator.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple[list[dict], int]: (sessions_with_project_info, total_count)
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
async def update_platform_session(
|
async def update_platform_session(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ def get_platform_type(
|
|||||||
async def migration_conversation_table(
|
async def migration_conversation_table(
|
||||||
db_helper: BaseDatabase,
|
db_helper: BaseDatabase,
|
||||||
platform_id_map: dict[str, dict[str, str]],
|
platform_id_map: dict[str, dict[str, str]],
|
||||||
):
|
) -> None:
|
||||||
db_helper_v3 = SQLiteV3DatabaseV3(
|
db_helper_v3 = SQLiteV3DatabaseV3(
|
||||||
db_path=DB_PATH.replace("data_v4.db", "data_v3.db"),
|
db_path=DB_PATH.replace("data_v4.db", "data_v3.db"),
|
||||||
)
|
)
|
||||||
@@ -101,7 +101,7 @@ async def migration_conversation_table(
|
|||||||
async def migration_platform_table(
|
async def migration_platform_table(
|
||||||
db_helper: BaseDatabase,
|
db_helper: BaseDatabase,
|
||||||
platform_id_map: dict[str, dict[str, str]],
|
platform_id_map: dict[str, dict[str, str]],
|
||||||
):
|
) -> None:
|
||||||
db_helper_v3 = SQLiteV3DatabaseV3(
|
db_helper_v3 = SQLiteV3DatabaseV3(
|
||||||
db_path=DB_PATH.replace("data_v4.db", "data_v3.db"),
|
db_path=DB_PATH.replace("data_v4.db", "data_v3.db"),
|
||||||
)
|
)
|
||||||
@@ -180,7 +180,7 @@ async def migration_platform_table(
|
|||||||
async def migration_webchat_data(
|
async def migration_webchat_data(
|
||||||
db_helper: BaseDatabase,
|
db_helper: BaseDatabase,
|
||||||
platform_id_map: dict[str, dict[str, str]],
|
platform_id_map: dict[str, dict[str, str]],
|
||||||
):
|
) -> None:
|
||||||
"""迁移 WebChat 的历史记录到新的 PlatformMessageHistory 表中"""
|
"""迁移 WebChat 的历史记录到新的 PlatformMessageHistory 表中"""
|
||||||
db_helper_v3 = SQLiteV3DatabaseV3(
|
db_helper_v3 = SQLiteV3DatabaseV3(
|
||||||
db_path=DB_PATH.replace("data_v4.db", "data_v3.db"),
|
db_path=DB_PATH.replace("data_v4.db", "data_v3.db"),
|
||||||
@@ -236,7 +236,7 @@ async def migration_webchat_data(
|
|||||||
async def migration_persona_data(
|
async def migration_persona_data(
|
||||||
db_helper: BaseDatabase,
|
db_helper: BaseDatabase,
|
||||||
astrbot_config: AstrBotConfig,
|
astrbot_config: AstrBotConfig,
|
||||||
):
|
) -> None:
|
||||||
"""迁移 Persona 数据到新的表中。
|
"""迁移 Persona 数据到新的表中。
|
||||||
旧的 Persona 数据存储在 preference 中,新的 Persona 数据存储在 persona 表中。
|
旧的 Persona 数据存储在 preference 中,新的 Persona 数据存储在 persona 表中。
|
||||||
"""
|
"""
|
||||||
@@ -279,7 +279,7 @@ async def migration_persona_data(
|
|||||||
async def migration_preferences(
|
async def migration_preferences(
|
||||||
db_helper: BaseDatabase,
|
db_helper: BaseDatabase,
|
||||||
platform_id_map: dict[str, dict[str, str]],
|
platform_id_map: dict[str, dict[str, str]],
|
||||||
):
|
) -> None:
|
||||||
# 1. global scope migration
|
# 1. global scope migration
|
||||||
keys = [
|
keys = [
|
||||||
"inactivated_llm_tools",
|
"inactivated_llm_tools",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from astrbot.core.astrbot_config_mgr import AstrBotConfigManager
|
|||||||
from astrbot.core.umop_config_router import UmopConfigRouter
|
from astrbot.core.umop_config_router import UmopConfigRouter
|
||||||
|
|
||||||
|
|
||||||
async def migrate_45_to_46(acm: AstrBotConfigManager, ucr: UmopConfigRouter):
|
async def migrate_45_to_46(acm: AstrBotConfigManager, ucr: UmopConfigRouter) -> None:
|
||||||
abconf_data = acm.abconf_data
|
abconf_data = acm.abconf_data
|
||||||
|
|
||||||
if not isinstance(abconf_data, dict):
|
if not isinstance(abconf_data, dict):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from astrbot.api import logger, sp
|
|||||||
from astrbot.core.db import BaseDatabase
|
from astrbot.core.db import BaseDatabase
|
||||||
|
|
||||||
|
|
||||||
async def migrate_token_usage(db_helper: BaseDatabase):
|
async def migrate_token_usage(db_helper: BaseDatabase) -> None:
|
||||||
"""Add token_usage column to conversations table.
|
"""Add token_usage column to conversations table.
|
||||||
|
|
||||||
This migration adds a new column to track token consumption in conversations.
|
This migration adds a new column to track token consumption in conversations.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from astrbot.core.db import BaseDatabase
|
|||||||
from astrbot.core.db.po import ConversationV2, PlatformMessageHistory, PlatformSession
|
from astrbot.core.db.po import ConversationV2, PlatformMessageHistory, PlatformSession
|
||||||
|
|
||||||
|
|
||||||
async def migrate_webchat_session(db_helper: BaseDatabase):
|
async def migrate_webchat_session(db_helper: BaseDatabase) -> None:
|
||||||
"""Create PlatformSession records from platform_message_history.
|
"""Create PlatformSession records from platform_message_history.
|
||||||
|
|
||||||
This migration extracts all unique user_ids from platform_message_history
|
This migration extracts all unique user_ids from platform_message_history
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ _VT = TypeVar("_VT")
|
|||||||
|
|
||||||
|
|
||||||
class SharedPreferences:
|
class SharedPreferences:
|
||||||
def __init__(self, path=None):
|
def __init__(self, path=None) -> None:
|
||||||
if path is None:
|
if path is None:
|
||||||
path = os.path.join(get_astrbot_data_path(), "shared_preferences.json")
|
path = os.path.join(get_astrbot_data_path(), "shared_preferences.json")
|
||||||
self.path = path
|
self.path = path
|
||||||
@@ -23,7 +23,7 @@ class SharedPreferences:
|
|||||||
os.remove(self.path)
|
os.remove(self.path)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _save_preferences(self):
|
def _save_preferences(self) -> None:
|
||||||
with open(self.path, "w") as f:
|
with open(self.path, "w") as f:
|
||||||
json.dump(self._data, f, indent=4, ensure_ascii=False)
|
json.dump(self._data, f, indent=4, ensure_ascii=False)
|
||||||
f.flush()
|
f.flush()
|
||||||
@@ -31,16 +31,16 @@ class SharedPreferences:
|
|||||||
def get(self, key, default: _VT = None) -> _VT:
|
def get(self, key, default: _VT = None) -> _VT:
|
||||||
return self._data.get(key, default)
|
return self._data.get(key, default)
|
||||||
|
|
||||||
def put(self, key, value):
|
def put(self, key, value) -> None:
|
||||||
self._data[key] = value
|
self._data[key] = value
|
||||||
self._save_preferences()
|
self._save_preferences()
|
||||||
|
|
||||||
def remove(self, key):
|
def remove(self, key) -> None:
|
||||||
if key in self._data:
|
if key in self._data:
|
||||||
del self._data[key]
|
del self._data[key]
|
||||||
self._save_preferences()
|
self._save_preferences()
|
||||||
|
|
||||||
def clear(self):
|
def clear(self) -> None:
|
||||||
self._data.clear()
|
self._data.clear()
|
||||||
self._save_preferences()
|
self._save_preferences()
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class SQLiteDatabase:
|
|||||||
conn.text_factory = str
|
conn.text_factory = str
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
def _exec_sql(self, sql: str, params: tuple | None = None):
|
def _exec_sql(self, sql: str, params: tuple | None = None) -> None:
|
||||||
conn = self.conn
|
conn = self.conn
|
||||||
try:
|
try:
|
||||||
c = self.conn.cursor()
|
c = self.conn.cursor()
|
||||||
@@ -144,7 +144,7 @@ class SQLiteDatabase:
|
|||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
def insert_platform_metrics(self, metrics: dict):
|
def insert_platform_metrics(self, metrics: dict) -> None:
|
||||||
for k, v in metrics.items():
|
for k, v in metrics.items():
|
||||||
self._exec_sql(
|
self._exec_sql(
|
||||||
"""
|
"""
|
||||||
@@ -153,7 +153,7 @@ class SQLiteDatabase:
|
|||||||
(k, v, int(time.time())),
|
(k, v, int(time.time())),
|
||||||
)
|
)
|
||||||
|
|
||||||
def insert_llm_metrics(self, metrics: dict):
|
def insert_llm_metrics(self, metrics: dict) -> None:
|
||||||
for k, v in metrics.items():
|
for k, v in metrics.items():
|
||||||
self._exec_sql(
|
self._exec_sql(
|
||||||
"""
|
"""
|
||||||
@@ -249,7 +249,7 @@ class SQLiteDatabase:
|
|||||||
|
|
||||||
return Conversation(*res)
|
return Conversation(*res)
|
||||||
|
|
||||||
def new_conversation(self, user_id: str, cid: str):
|
def new_conversation(self, user_id: str, cid: str) -> None:
|
||||||
history = "[]"
|
history = "[]"
|
||||||
updated_at = int(time.time())
|
updated_at = int(time.time())
|
||||||
created_at = updated_at
|
created_at = updated_at
|
||||||
@@ -287,7 +287,7 @@ class SQLiteDatabase:
|
|||||||
)
|
)
|
||||||
return conversations
|
return conversations
|
||||||
|
|
||||||
def update_conversation(self, user_id: str, cid: str, history: str):
|
def update_conversation(self, user_id: str, cid: str, history: str) -> None:
|
||||||
"""更新对话,并且同时更新时间"""
|
"""更新对话,并且同时更新时间"""
|
||||||
updated_at = int(time.time())
|
updated_at = int(time.time())
|
||||||
self._exec_sql(
|
self._exec_sql(
|
||||||
@@ -297,7 +297,7 @@ class SQLiteDatabase:
|
|||||||
(history, updated_at, user_id, cid),
|
(history, updated_at, user_id, cid),
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_conversation_title(self, user_id: str, cid: str, title: str):
|
def update_conversation_title(self, user_id: str, cid: str, title: str) -> None:
|
||||||
self._exec_sql(
|
self._exec_sql(
|
||||||
"""
|
"""
|
||||||
UPDATE webchat_conversation SET title = ? WHERE user_id = ? AND cid = ?
|
UPDATE webchat_conversation SET title = ? WHERE user_id = ? AND cid = ?
|
||||||
@@ -305,7 +305,9 @@ class SQLiteDatabase:
|
|||||||
(title, user_id, cid),
|
(title, user_id, cid),
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_conversation_persona_id(self, user_id: str, cid: str, persona_id: str):
|
def update_conversation_persona_id(
|
||||||
|
self, user_id: str, cid: str, persona_id: str
|
||||||
|
) -> None:
|
||||||
self._exec_sql(
|
self._exec_sql(
|
||||||
"""
|
"""
|
||||||
UPDATE webchat_conversation SET persona_id = ? WHERE user_id = ? AND cid = ?
|
UPDATE webchat_conversation SET persona_id = ? WHERE user_id = ? AND cid = ?
|
||||||
@@ -313,7 +315,7 @@ class SQLiteDatabase:
|
|||||||
(persona_id, user_id, cid),
|
(persona_id, user_id, cid),
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete_conversation(self, user_id: str, cid: str):
|
def delete_conversation(self, user_id: str, cid: str) -> None:
|
||||||
self._exec_sql(
|
self._exec_sql(
|
||||||
"""
|
"""
|
||||||
DELETE FROM webchat_conversation WHERE user_id = ? AND cid = ?
|
DELETE FROM webchat_conversation WHERE user_id = ? AND cid = ?
|
||||||
|
|||||||
@@ -288,6 +288,43 @@ class Attachment(TimestampMixin, SQLModel, table=True):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKey(TimestampMixin, SQLModel, table=True):
|
||||||
|
"""API keys used by external developers to access Open APIs."""
|
||||||
|
|
||||||
|
__tablename__: str = "api_keys"
|
||||||
|
|
||||||
|
inner_id: int | None = Field(
|
||||||
|
primary_key=True,
|
||||||
|
sa_column_kwargs={"autoincrement": True},
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
key_id: str = Field(
|
||||||
|
max_length=36,
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
default_factory=lambda: str(uuid.uuid4()),
|
||||||
|
)
|
||||||
|
name: str = Field(max_length=255, nullable=False)
|
||||||
|
key_hash: str = Field(max_length=128, nullable=False, unique=True)
|
||||||
|
key_prefix: str = Field(max_length=24, nullable=False)
|
||||||
|
scopes: list | None = Field(default=None, sa_type=JSON)
|
||||||
|
created_by: str = Field(max_length=255, nullable=False)
|
||||||
|
last_used_at: datetime | None = Field(default=None)
|
||||||
|
expires_at: datetime | None = Field(default=None)
|
||||||
|
revoked_at: datetime | None = Field(default=None)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint(
|
||||||
|
"key_id",
|
||||||
|
name="uix_api_key_id",
|
||||||
|
),
|
||||||
|
UniqueConstraint(
|
||||||
|
"key_hash",
|
||||||
|
name="uix_api_key_hash",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ChatUIProject(TimestampMixin, SQLModel, table=True):
|
class ChatUIProject(TimestampMixin, SQLModel, table=True):
|
||||||
"""This class represents projects for organizing ChatUI conversations.
|
"""This class represents projects for organizing ChatUI conversations.
|
||||||
|
|
||||||
|
|||||||
+189
-50
@@ -4,12 +4,13 @@ import typing as T
|
|||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from sqlalchemy import CursorResult
|
from sqlalchemy import CursorResult, Row
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlmodel import col, delete, desc, func, or_, select, text, update
|
from sqlmodel import col, delete, desc, func, or_, select, text, update
|
||||||
|
|
||||||
from astrbot.core.db import BaseDatabase
|
from astrbot.core.db import BaseDatabase
|
||||||
from astrbot.core.db.po import (
|
from astrbot.core.db.po import (
|
||||||
|
ApiKey,
|
||||||
Attachment,
|
Attachment,
|
||||||
ChatUIProject,
|
ChatUIProject,
|
||||||
CommandConfig,
|
CommandConfig,
|
||||||
@@ -305,7 +306,7 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
await session.execute(query)
|
await session.execute(query)
|
||||||
return await self.get_conversation_by_id(cid)
|
return await self.get_conversation_by_id(cid)
|
||||||
|
|
||||||
async def delete_conversation(self, cid):
|
async def delete_conversation(self, cid) -> None:
|
||||||
async with self.get_db() as session:
|
async with self.get_db() as session:
|
||||||
session: AsyncSession
|
session: AsyncSession
|
||||||
async with session.begin():
|
async with session.begin():
|
||||||
@@ -461,7 +462,7 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
platform_id,
|
platform_id,
|
||||||
user_id,
|
user_id,
|
||||||
offset_sec=86400,
|
offset_sec=86400,
|
||||||
):
|
) -> None:
|
||||||
"""Delete platform message history records newer than the specified offset."""
|
"""Delete platform message history records newer than the specified offset."""
|
||||||
async with self.get_db() as session:
|
async with self.get_db() as session:
|
||||||
session: AsyncSession
|
session: AsyncSession
|
||||||
@@ -573,6 +574,100 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
result = T.cast(CursorResult, await session.execute(query))
|
result = T.cast(CursorResult, await session.execute(query))
|
||||||
return result.rowcount
|
return result.rowcount
|
||||||
|
|
||||||
|
async def create_api_key(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
key_hash: str,
|
||||||
|
key_prefix: str,
|
||||||
|
scopes: list[str] | None,
|
||||||
|
created_by: str,
|
||||||
|
expires_at: datetime | None = None,
|
||||||
|
) -> ApiKey:
|
||||||
|
"""Create a new API key record."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
async with session.begin():
|
||||||
|
api_key = ApiKey(
|
||||||
|
name=name,
|
||||||
|
key_hash=key_hash,
|
||||||
|
key_prefix=key_prefix,
|
||||||
|
scopes=scopes,
|
||||||
|
created_by=created_by,
|
||||||
|
expires_at=expires_at,
|
||||||
|
)
|
||||||
|
session.add(api_key)
|
||||||
|
await session.flush()
|
||||||
|
await session.refresh(api_key)
|
||||||
|
return api_key
|
||||||
|
|
||||||
|
async def list_api_keys(self) -> list[ApiKey]:
|
||||||
|
"""List all API keys."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
result = await session.execute(
|
||||||
|
select(ApiKey).order_by(desc(ApiKey.created_at))
|
||||||
|
)
|
||||||
|
return list(result.scalars().all())
|
||||||
|
|
||||||
|
async def get_api_key_by_id(self, key_id: str) -> ApiKey | None:
|
||||||
|
"""Get an API key by key_id."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
result = await session.execute(
|
||||||
|
select(ApiKey).where(ApiKey.key_id == key_id)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_active_api_key_by_hash(self, key_hash: str) -> ApiKey | None:
|
||||||
|
"""Get an active API key by hash (not revoked, not expired)."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
query = select(ApiKey).where(
|
||||||
|
ApiKey.key_hash == key_hash,
|
||||||
|
col(ApiKey.revoked_at).is_(None),
|
||||||
|
or_(col(ApiKey.expires_at).is_(None), col(ApiKey.expires_at) > now),
|
||||||
|
)
|
||||||
|
result = await session.execute(query)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
async def touch_api_key(self, key_id: str) -> None:
|
||||||
|
"""Update last_used_at of an API key."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
async with session.begin():
|
||||||
|
await session.execute(
|
||||||
|
update(ApiKey)
|
||||||
|
.where(col(ApiKey.key_id) == key_id)
|
||||||
|
.values(last_used_at=datetime.now(timezone.utc)),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def revoke_api_key(self, key_id: str) -> bool:
|
||||||
|
"""Revoke an API key."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
async with session.begin():
|
||||||
|
query = (
|
||||||
|
update(ApiKey)
|
||||||
|
.where(col(ApiKey.key_id) == key_id)
|
||||||
|
.values(revoked_at=datetime.now(timezone.utc))
|
||||||
|
)
|
||||||
|
result = T.cast(CursorResult, await session.execute(query))
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
|
async def delete_api_key(self, key_id: str) -> bool:
|
||||||
|
"""Delete an API key."""
|
||||||
|
async with self.get_db() as session:
|
||||||
|
session: AsyncSession
|
||||||
|
async with session.begin():
|
||||||
|
result = T.cast(
|
||||||
|
CursorResult,
|
||||||
|
await session.execute(
|
||||||
|
delete(ApiKey).where(col(ApiKey.key_id) == key_id)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return result.rowcount > 0
|
||||||
|
|
||||||
async def insert_persona(
|
async def insert_persona(
|
||||||
self,
|
self,
|
||||||
persona_id,
|
persona_id,
|
||||||
@@ -645,7 +740,7 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
await session.execute(query)
|
await session.execute(query)
|
||||||
return await self.get_persona_by_id(persona_id)
|
return await self.get_persona_by_id(persona_id)
|
||||||
|
|
||||||
async def delete_persona(self, persona_id):
|
async def delete_persona(self, persona_id) -> None:
|
||||||
"""Delete a persona by its ID."""
|
"""Delete a persona by its ID."""
|
||||||
async with self.get_db() as session:
|
async with self.get_db() as session:
|
||||||
session: AsyncSession
|
session: AsyncSession
|
||||||
@@ -903,7 +998,7 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
result = await session.execute(query)
|
result = await session.execute(query)
|
||||||
return result.scalars().all()
|
return result.scalars().all()
|
||||||
|
|
||||||
async def remove_preference(self, scope, scope_id, key):
|
async def remove_preference(self, scope, scope_id, key) -> None:
|
||||||
"""Remove a preference by scope ID and key."""
|
"""Remove a preference by scope ID and key."""
|
||||||
async with self.get_db() as session:
|
async with self.get_db() as session:
|
||||||
session: AsyncSession
|
session: AsyncSession
|
||||||
@@ -917,7 +1012,7 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
)
|
)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
|
|
||||||
async def clear_preferences(self, scope, scope_id):
|
async def clear_preferences(self, scope, scope_id) -> None:
|
||||||
"""Clear all preferences for a specific scope ID."""
|
"""Clear all preferences for a specific scope ID."""
|
||||||
async with self.get_db() as session:
|
async with self.get_db() as session:
|
||||||
session: AsyncSession
|
session: AsyncSession
|
||||||
@@ -1195,7 +1290,7 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
|
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
def runner():
|
def runner() -> None:
|
||||||
nonlocal result
|
nonlocal result
|
||||||
result = asyncio.run(_inner())
|
result = asyncio.run(_inner())
|
||||||
|
|
||||||
@@ -1218,7 +1313,7 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
|
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
def runner():
|
def runner() -> None:
|
||||||
nonlocal result
|
nonlocal result
|
||||||
result = asyncio.run(_inner())
|
result = asyncio.run(_inner())
|
||||||
|
|
||||||
@@ -1253,7 +1348,7 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
|
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
def runner():
|
def runner() -> None:
|
||||||
nonlocal result
|
nonlocal result
|
||||||
result = asyncio.run(_inner())
|
result = asyncio.run(_inner())
|
||||||
|
|
||||||
@@ -1317,58 +1412,102 @@ class SQLiteDatabase(BaseDatabase):
|
|||||||
|
|
||||||
Returns a list of dicts containing session info and project info (if session belongs to a project).
|
Returns a list of dicts containing session info and project info (if session belongs to a project).
|
||||||
"""
|
"""
|
||||||
|
(
|
||||||
|
sessions_with_projects,
|
||||||
|
_,
|
||||||
|
) = await self.get_platform_sessions_by_creator_paginated(
|
||||||
|
creator=creator,
|
||||||
|
platform_id=platform_id,
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
exclude_project_sessions=False,
|
||||||
|
)
|
||||||
|
return sessions_with_projects
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_platform_sessions_query(
|
||||||
|
creator: str,
|
||||||
|
platform_id: str | None = None,
|
||||||
|
exclude_project_sessions: bool = False,
|
||||||
|
):
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
PlatformSession,
|
||||||
|
col(ChatUIProject.project_id),
|
||||||
|
col(ChatUIProject.title).label("project_title"),
|
||||||
|
col(ChatUIProject.emoji).label("project_emoji"),
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
SessionProjectRelation,
|
||||||
|
col(PlatformSession.session_id)
|
||||||
|
== col(SessionProjectRelation.session_id),
|
||||||
|
)
|
||||||
|
.outerjoin(
|
||||||
|
ChatUIProject,
|
||||||
|
col(SessionProjectRelation.project_id) == col(ChatUIProject.project_id),
|
||||||
|
)
|
||||||
|
.where(col(PlatformSession.creator) == creator)
|
||||||
|
)
|
||||||
|
|
||||||
|
if platform_id:
|
||||||
|
query = query.where(PlatformSession.platform_id == platform_id)
|
||||||
|
if exclude_project_sessions:
|
||||||
|
query = query.where(col(ChatUIProject.project_id).is_(None))
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _rows_to_session_dicts(rows: T.Sequence[Row[tuple]]) -> list[dict]:
|
||||||
|
sessions_with_projects = []
|
||||||
|
for row in rows:
|
||||||
|
platform_session = row[0]
|
||||||
|
project_id = row[1]
|
||||||
|
project_title = row[2]
|
||||||
|
project_emoji = row[3]
|
||||||
|
|
||||||
|
session_dict = {
|
||||||
|
"session": platform_session,
|
||||||
|
"project_id": project_id,
|
||||||
|
"project_title": project_title,
|
||||||
|
"project_emoji": project_emoji,
|
||||||
|
}
|
||||||
|
sessions_with_projects.append(session_dict)
|
||||||
|
|
||||||
|
return sessions_with_projects
|
||||||
|
|
||||||
|
async def get_platform_sessions_by_creator_paginated(
|
||||||
|
self,
|
||||||
|
creator: str,
|
||||||
|
platform_id: str | None = None,
|
||||||
|
page: int = 1,
|
||||||
|
page_size: int = 20,
|
||||||
|
exclude_project_sessions: bool = False,
|
||||||
|
) -> tuple[list[dict], int]:
|
||||||
|
"""Get paginated Platform sessions for a creator with total count."""
|
||||||
async with self.get_db() as session:
|
async with self.get_db() as session:
|
||||||
session: AsyncSession
|
session: AsyncSession
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
|
|
||||||
# LEFT JOIN with SessionProjectRelation and ChatUIProject to get project info
|
base_query = self._build_platform_sessions_query(
|
||||||
query = (
|
creator=creator,
|
||||||
select(
|
platform_id=platform_id,
|
||||||
PlatformSession,
|
exclude_project_sessions=exclude_project_sessions,
|
||||||
col(ChatUIProject.project_id),
|
|
||||||
col(ChatUIProject.title).label("project_title"),
|
|
||||||
col(ChatUIProject.emoji).label("project_emoji"),
|
|
||||||
)
|
|
||||||
.outerjoin(
|
|
||||||
SessionProjectRelation,
|
|
||||||
col(PlatformSession.session_id)
|
|
||||||
== col(SessionProjectRelation.session_id),
|
|
||||||
)
|
|
||||||
.outerjoin(
|
|
||||||
ChatUIProject,
|
|
||||||
col(SessionProjectRelation.project_id)
|
|
||||||
== col(ChatUIProject.project_id),
|
|
||||||
)
|
|
||||||
.where(col(PlatformSession.creator) == creator)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if platform_id:
|
total_result = await session.execute(
|
||||||
query = query.where(PlatformSession.platform_id == platform_id)
|
select(func.count()).select_from(base_query.subquery())
|
||||||
|
)
|
||||||
|
total = int(total_result.scalar_one() or 0)
|
||||||
|
|
||||||
query = (
|
result_query = (
|
||||||
query.order_by(desc(PlatformSession.updated_at))
|
base_query.order_by(desc(PlatformSession.updated_at))
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(page_size)
|
.limit(page_size)
|
||||||
)
|
)
|
||||||
result = await session.execute(query)
|
result = await session.execute(result_query)
|
||||||
|
|
||||||
# Convert to list of dicts with session and project info
|
sessions_with_projects = self._rows_to_session_dicts(result.all())
|
||||||
sessions_with_projects = []
|
return sessions_with_projects, total
|
||||||
for row in result.all():
|
|
||||||
platform_session = row[0]
|
|
||||||
project_id = row[1]
|
|
||||||
project_title = row[2]
|
|
||||||
project_emoji = row[3]
|
|
||||||
|
|
||||||
session_dict = {
|
|
||||||
"session": platform_session,
|
|
||||||
"project_id": project_id,
|
|
||||||
"project_title": project_title,
|
|
||||||
"project_emoji": project_emoji,
|
|
||||||
}
|
|
||||||
sessions_with_projects.append(session_dict)
|
|
||||||
|
|
||||||
return sessions_with_projects
|
|
||||||
|
|
||||||
async def update_platform_session(
|
async def update_platform_session(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class Result:
|
|||||||
|
|
||||||
|
|
||||||
class BaseVecDB:
|
class BaseVecDB:
|
||||||
async def initialize(self):
|
async def initialize(self) -> None:
|
||||||
"""初始化向量数据库"""
|
"""初始化向量数据库"""
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class Document(BaseDocModel, table=True):
|
|||||||
|
|
||||||
|
|
||||||
class DocumentStorage:
|
class DocumentStorage:
|
||||||
def __init__(self, db_path: str):
|
def __init__(self, db_path: str) -> None:
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self.DATABASE_URL = f"sqlite+aiosqlite:///{db_path}"
|
self.DATABASE_URL = f"sqlite+aiosqlite:///{db_path}"
|
||||||
self.engine: AsyncEngine | None = None
|
self.engine: AsyncEngine | None = None
|
||||||
@@ -43,7 +43,7 @@ class DocumentStorage:
|
|||||||
"sqlite_init.sql",
|
"sqlite_init.sql",
|
||||||
)
|
)
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self) -> None:
|
||||||
"""Initialize the SQLite database and create the documents table if it doesn't exist."""
|
"""Initialize the SQLite database and create the documents table if it doesn't exist."""
|
||||||
await self.connect()
|
await self.connect()
|
||||||
async with self.engine.begin() as conn: # type: ignore
|
async with self.engine.begin() as conn: # type: ignore
|
||||||
@@ -80,7 +80,7 @@ class DocumentStorage:
|
|||||||
|
|
||||||
await conn.commit()
|
await conn.commit()
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self) -> None:
|
||||||
"""Connect to the SQLite database."""
|
"""Connect to the SQLite database."""
|
||||||
if self.engine is None:
|
if self.engine is None:
|
||||||
self.engine = create_async_engine(
|
self.engine = create_async_engine(
|
||||||
@@ -211,7 +211,7 @@ class DocumentStorage:
|
|||||||
await session.flush() # Flush to get all IDs
|
await session.flush() # Flush to get all IDs
|
||||||
return [doc.id for doc in documents] # type: ignore
|
return [doc.id for doc in documents] # type: ignore
|
||||||
|
|
||||||
async def delete_document_by_doc_id(self, doc_id: str):
|
async def delete_document_by_doc_id(self, doc_id: str) -> None:
|
||||||
"""Delete a document by its doc_id.
|
"""Delete a document by its doc_id.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -249,7 +249,7 @@ class DocumentStorage:
|
|||||||
return self._document_to_dict(document)
|
return self._document_to_dict(document)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def update_document_by_doc_id(self, doc_id: str, new_text: str):
|
async def update_document_by_doc_id(self, doc_id: str, new_text: str) -> None:
|
||||||
"""Update a document by its doc_id.
|
"""Update a document by its doc_id.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -269,7 +269,7 @@ class DocumentStorage:
|
|||||||
document.updated_at = datetime.now()
|
document.updated_at = datetime.now()
|
||||||
session.add(document)
|
session.add(document)
|
||||||
|
|
||||||
async def delete_documents(self, metadata_filters: dict):
|
async def delete_documents(self, metadata_filters: dict) -> None:
|
||||||
"""Delete documents by their metadata filters.
|
"""Delete documents by their metadata filters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -384,7 +384,7 @@ class DocumentStorage:
|
|||||||
"updated_at": row[5],
|
"updated_at": row[5],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def close(self):
|
async def close(self) -> None:
|
||||||
"""Close the connection to the SQLite database."""
|
"""Close the connection to the SQLite database."""
|
||||||
if self.engine:
|
if self.engine:
|
||||||
await self.engine.dispose()
|
await self.engine.dispose()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import numpy as np
|
|||||||
|
|
||||||
|
|
||||||
class EmbeddingStorage:
|
class EmbeddingStorage:
|
||||||
def __init__(self, dimension: int, path: str | None = None):
|
def __init__(self, dimension: int, path: str | None = None) -> None:
|
||||||
self.dimension = dimension
|
self.dimension = dimension
|
||||||
self.path = path
|
self.path = path
|
||||||
self.index = None
|
self.index = None
|
||||||
@@ -20,7 +20,7 @@ class EmbeddingStorage:
|
|||||||
base_index = faiss.IndexFlatL2(dimension)
|
base_index = faiss.IndexFlatL2(dimension)
|
||||||
self.index = faiss.IndexIDMap(base_index)
|
self.index = faiss.IndexIDMap(base_index)
|
||||||
|
|
||||||
async def insert(self, vector: np.ndarray, id: int):
|
async def insert(self, vector: np.ndarray, id: int) -> None:
|
||||||
"""插入向量
|
"""插入向量
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -38,7 +38,7 @@ class EmbeddingStorage:
|
|||||||
self.index.add_with_ids(vector.reshape(1, -1), np.array([id]))
|
self.index.add_with_ids(vector.reshape(1, -1), np.array([id]))
|
||||||
await self.save_index()
|
await self.save_index()
|
||||||
|
|
||||||
async def insert_batch(self, vectors: np.ndarray, ids: list[int]):
|
async def insert_batch(self, vectors: np.ndarray, ids: list[int]) -> None:
|
||||||
"""批量插入向量
|
"""批量插入向量
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -71,7 +71,7 @@ class EmbeddingStorage:
|
|||||||
distances, indices = self.index.search(vector, k)
|
distances, indices = self.index.search(vector, k)
|
||||||
return distances, indices
|
return distances, indices
|
||||||
|
|
||||||
async def delete(self, ids: list[int]):
|
async def delete(self, ids: list[int]) -> None:
|
||||||
"""删除向量
|
"""删除向量
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -83,7 +83,7 @@ class EmbeddingStorage:
|
|||||||
self.index.remove_ids(id_array)
|
self.index.remove_ids(id_array)
|
||||||
await self.save_index()
|
await self.save_index()
|
||||||
|
|
||||||
async def save_index(self):
|
async def save_index(self) -> None:
|
||||||
"""保存索引
|
"""保存索引
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class FaissVecDB(BaseVecDB):
|
|||||||
index_store_path: str,
|
index_store_path: str,
|
||||||
embedding_provider: EmbeddingProvider,
|
embedding_provider: EmbeddingProvider,
|
||||||
rerank_provider: RerankProvider | None = None,
|
rerank_provider: RerankProvider | None = None,
|
||||||
):
|
) -> None:
|
||||||
self.doc_store_path = doc_store_path
|
self.doc_store_path = doc_store_path
|
||||||
self.index_store_path = index_store_path
|
self.index_store_path = index_store_path
|
||||||
self.embedding_provider = embedding_provider
|
self.embedding_provider = embedding_provider
|
||||||
@@ -32,7 +32,7 @@ class FaissVecDB(BaseVecDB):
|
|||||||
self.embedding_provider = embedding_provider
|
self.embedding_provider = embedding_provider
|
||||||
self.rerank_provider = rerank_provider
|
self.rerank_provider = rerank_provider
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self) -> None:
|
||||||
await self.document_storage.initialize()
|
await self.document_storage.initialize()
|
||||||
|
|
||||||
async def insert(
|
async def insert(
|
||||||
@@ -165,7 +165,7 @@ class FaissVecDB(BaseVecDB):
|
|||||||
|
|
||||||
return top_k_results
|
return top_k_results
|
||||||
|
|
||||||
async def delete(self, doc_id: str):
|
async def delete(self, doc_id: str) -> None:
|
||||||
"""删除一条文档块(chunk)"""
|
"""删除一条文档块(chunk)"""
|
||||||
# 获得对应的 int id
|
# 获得对应的 int id
|
||||||
result = await self.document_storage.get_document_by_doc_id(doc_id)
|
result = await self.document_storage.get_document_by_doc_id(doc_id)
|
||||||
@@ -177,7 +177,7 @@ class FaissVecDB(BaseVecDB):
|
|||||||
await self.document_storage.delete_document_by_doc_id(doc_id)
|
await self.document_storage.delete_document_by_doc_id(doc_id)
|
||||||
await self.embedding_storage.delete([int_id])
|
await self.embedding_storage.delete([int_id])
|
||||||
|
|
||||||
async def close(self):
|
async def close(self) -> None:
|
||||||
await self.document_storage.close()
|
await self.document_storage.close()
|
||||||
|
|
||||||
async def count_documents(self, metadata_filter: dict | None = None) -> int:
|
async def count_documents(self, metadata_filter: dict | None = None) -> int:
|
||||||
@@ -192,7 +192,7 @@ class FaissVecDB(BaseVecDB):
|
|||||||
)
|
)
|
||||||
return count
|
return count
|
||||||
|
|
||||||
async def delete_documents(self, metadata_filters: dict):
|
async def delete_documents(self, metadata_filters: dict) -> None:
|
||||||
"""根据元数据过滤器删除文档"""
|
"""根据元数据过滤器删除文档"""
|
||||||
docs = await self.document_storage.get_documents(
|
docs = await self.document_storage.get_documents(
|
||||||
metadata_filters=metadata_filters,
|
metadata_filters=metadata_filters,
|
||||||
|
|||||||
@@ -28,13 +28,13 @@ class EventBus:
|
|||||||
event_queue: Queue,
|
event_queue: Queue,
|
||||||
pipeline_scheduler_mapping: dict[str, PipelineScheduler],
|
pipeline_scheduler_mapping: dict[str, PipelineScheduler],
|
||||||
astrbot_config_mgr: AstrBotConfigManager,
|
astrbot_config_mgr: AstrBotConfigManager,
|
||||||
):
|
) -> None:
|
||||||
self.event_queue = event_queue # 事件队列
|
self.event_queue = event_queue # 事件队列
|
||||||
# abconf uuid -> scheduler
|
# abconf uuid -> scheduler
|
||||||
self.pipeline_scheduler_mapping = pipeline_scheduler_mapping
|
self.pipeline_scheduler_mapping = pipeline_scheduler_mapping
|
||||||
self.astrbot_config_mgr = astrbot_config_mgr
|
self.astrbot_config_mgr = astrbot_config_mgr
|
||||||
|
|
||||||
async def dispatch(self):
|
async def dispatch(self) -> None:
|
||||||
while True:
|
while True:
|
||||||
event: AstrMessageEvent = await self.event_queue.get()
|
event: AstrMessageEvent = await self.event_queue.get()
|
||||||
conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin)
|
conf_info = self.astrbot_config_mgr.get_conf_info(event.unified_msg_origin)
|
||||||
@@ -47,7 +47,7 @@ class EventBus:
|
|||||||
continue
|
continue
|
||||||
asyncio.create_task(scheduler.execute(event))
|
asyncio.create_task(scheduler.execute(event))
|
||||||
|
|
||||||
def _print_event(self, event: AstrMessageEvent, conf_name: str):
|
def _print_event(self, event: AstrMessageEvent, conf_name: str) -> None:
|
||||||
"""用于记录事件信息
|
"""用于记录事件信息
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ from urllib.parse import unquote, urlparse
|
|||||||
class FileTokenService:
|
class FileTokenService:
|
||||||
"""维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。"""
|
"""维护一个简单的基于令牌的文件下载服务,支持超时和懒清除。"""
|
||||||
|
|
||||||
def __init__(self, default_timeout: float = 300):
|
def __init__(self, default_timeout: float = 300) -> None:
|
||||||
self.lock = asyncio.Lock()
|
self.lock = asyncio.Lock()
|
||||||
self.staged_files = {} # token: (file_path, expire_time)
|
self.staged_files = {} # token: (file_path, expire_time)
|
||||||
self.default_timeout = default_timeout
|
self.default_timeout = default_timeout
|
||||||
|
|
||||||
async def _cleanup_expired_tokens(self):
|
async def _cleanup_expired_tokens(self) -> None:
|
||||||
"""清理过期的令牌"""
|
"""清理过期的令牌"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
expired_tokens = [
|
expired_tokens = [
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ from astrbot.dashboard.server import AstrBotDashboard
|
|||||||
class InitialLoader:
|
class InitialLoader:
|
||||||
"""AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。"""
|
"""AstrBot 启动器,负责初始化和启动核心组件和仪表板服务器。"""
|
||||||
|
|
||||||
def __init__(self, db: BaseDatabase, log_broker: LogBroker):
|
def __init__(self, db: BaseDatabase, log_broker: LogBroker) -> None:
|
||||||
self.db = db
|
self.db = db
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.log_broker = log_broker
|
self.log_broker = log_broker
|
||||||
self.webui_dir: str | None = None
|
self.webui_dir: str | None = None
|
||||||
|
|
||||||
async def start(self):
|
async def start(self) -> None:
|
||||||
core_lifecycle = AstrBotCoreLifecycle(self.log_broker, self.db)
|
core_lifecycle = AstrBotCoreLifecycle(self.log_broker, self.db)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class FixedSizeChunker(BaseChunker):
|
|||||||
按照固定的字符数分块,并支持块之间的重叠。
|
按照固定的字符数分块,并支持块之间的重叠。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, chunk_size: int = 512, chunk_overlap: int = 50):
|
def __init__(self, chunk_size: int = 512, chunk_overlap: int = 50) -> None:
|
||||||
"""初始化分块器
|
"""初始化分块器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class RecursiveCharacterChunker(BaseChunker):
|
|||||||
length_function: Callable[[str], int] = len,
|
length_function: Callable[[str], int] = len,
|
||||||
is_separator_regex: bool = False,
|
is_separator_regex: bool = False,
|
||||||
separators: list[str] | None = None,
|
separators: list[str] | None = None,
|
||||||
):
|
) -> None:
|
||||||
"""初始化递归字符文本分割器
|
"""初始化递归字符文本分割器
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -13,16 +13,19 @@ from astrbot.core.knowledge_base.models import (
|
|||||||
KBMedia,
|
KBMedia,
|
||||||
KnowledgeBase,
|
KnowledgeBase,
|
||||||
)
|
)
|
||||||
|
from astrbot.core.utils.astrbot_path import get_astrbot_knowledge_base_path
|
||||||
|
|
||||||
|
|
||||||
class KBSQLiteDatabase:
|
class KBSQLiteDatabase:
|
||||||
def __init__(self, db_path: str = "data/knowledge_base/kb.db") -> None:
|
def __init__(self, db_path: str | None = None) -> None:
|
||||||
"""初始化知识库数据库
|
"""初始化知识库数据库
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
db_path: 数据库文件路径, 默认为 data/knowledge_base/kb.db
|
db_path: 数据库文件路径, 默认位于 AstrBot 数据目录下的 knowledge_base/kb.db
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if db_path is None:
|
||||||
|
db_path = str(Path(get_astrbot_knowledge_base_path()) / "kb.db")
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
self.DATABASE_URL = f"sqlite+aiosqlite:///{db_path}"
|
self.DATABASE_URL = f"sqlite+aiosqlite:///{db_path}"
|
||||||
self.inited = False
|
self.inited = False
|
||||||
@@ -253,7 +256,47 @@ class KBSQLiteDatabase:
|
|||||||
"knowledge_base": row[1],
|
"knowledge_base": row[1],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def delete_document_by_id(self, doc_id: str, vec_db: FaissVecDB):
|
async def get_documents_with_metadata_batch(
|
||||||
|
self, doc_ids: set[str]
|
||||||
|
) -> dict[str, dict]:
|
||||||
|
"""批量获取文档及其所属知识库元数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
doc_ids: 文档 ID 集合
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: doc_id -> {"document": KBDocument, "knowledge_base": KnowledgeBase}
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not doc_ids:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
metadata_map: dict[str, dict] = {}
|
||||||
|
# SQLite 参数上限为 999,分片查询避免超限
|
||||||
|
chunk_size = 900
|
||||||
|
doc_id_list = list(doc_ids)
|
||||||
|
|
||||||
|
async with self.get_db() as session:
|
||||||
|
for i in range(0, len(doc_id_list), chunk_size):
|
||||||
|
chunk = doc_id_list[i : i + chunk_size]
|
||||||
|
stmt = (
|
||||||
|
select(KBDocument, KnowledgeBase)
|
||||||
|
.join(
|
||||||
|
KnowledgeBase,
|
||||||
|
col(KBDocument.kb_id) == col(KnowledgeBase.kb_id),
|
||||||
|
)
|
||||||
|
.where(col(KBDocument.doc_id).in_(chunk))
|
||||||
|
)
|
||||||
|
result = await session.execute(stmt)
|
||||||
|
for row in result.all():
|
||||||
|
metadata_map[row[0].doc_id] = {
|
||||||
|
"document": row[0],
|
||||||
|
"knowledge_base": row[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata_map
|
||||||
|
|
||||||
|
async def delete_document_by_id(self, doc_id: str, vec_db: FaissVecDB) -> None:
|
||||||
"""删除单个文档及其相关数据"""
|
"""删除单个文档及其相关数据"""
|
||||||
# 在知识库表中删除
|
# 在知识库表中删除
|
||||||
async with self.get_db() as session, session.begin():
|
async with self.get_db() as session, session.begin():
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user