Compare commits

...

15 Commits

Author SHA1 Message Date
advent259141 d87d586c0a feat: add dashboard routes for session rule and group management, including available resource listings. 2026-03-11 18:18:06 +08:00
advent259141 410789311a feat: Add a new session management page with custom rules, batch operations, and group management, along with corresponding API routes. 2026-03-11 17:58:09 +08:00
エイカク 6da59cfb07 fix: 插件依赖自动安装逻辑与 Dashboard 安装体验优化 (#5954)
* fix: install plugin requirements before first load

* fix: handle pip option arguments correctly

* fix: harden pip install input parsing

* refactor: simplify pip install input parsing

* fix: align plugin dependency install handling

* fix: respect configured pip index overrides

* test: parameterize plugin dependency install flows

* refactor: simplify multiline pip input parsing

* fix: install plugin dependencies before loading

* fix: protect core dependencies from downgrades and simplify package input splitting

* fix: enhance dependency conflict reporting and improve user-facing warnings

* refactor: preserve pip log indentation and fix CodeQL URL sanitization alert

* fix: explicit re-export for DependencyConflictError to satisfy ruff F401

* test: enhance index override verification in pip installer tests

* fix: correctly map pip ERROR and WARNING outputs to proper log levels

* refactor: show specific version conflicts in DependencyConflictError and revert log level mapping

* refactor: simplify install() by decoupling pip logging, failure classification and constraint file management

* refactor: further simplify pip installer and requirement parsing logic

* refactor: simplify dependency installation logic and improve circular requirement reporting

* style: organize imports in astrbot/core/__init__.py

* refactor: optimize requirement parsing efficiency and flatten pip installer API

* style: fix import sorting in astrbot/core/__init__.py

* refactor: consolidate requirement parsing, optimize core protection, and improve exception propagation

* fix: preserve valid pip requirement parsing

* fix: skip empty pip installs and preserve blank output

* chore: normalize gitignore entry style

* fix: tighten pip trust and requirement parsing

* refactor: centralize pip install parsing and failure handling

* fix: redact pip argv credentials in logs

* fix: surface plugin dependency install errors

* fix: cache core constraints and clarify requirement installs

* fix: harden pip requirement parsing for plugin installs

* fix: simplify pip installer parsing internals

* fix: tighten pip installer parsing and redaction

* refactor: simplify plugin dependency install flow

* fix: preserve core constraint conflict errors

* fix: harden pip installer fallback resolution

* refactor: split pip requirement and constraint helpers

* refactor: simplify pip installer helper flow

* refactor: streamline requirement precheck helpers

* refactor: clarify core constraint resolution

* fix: surface pip install failures explicitly

* refactor: separate pip conflict context parsing

* fix: harden core constraint resolution

* test: cover pip installer failure call sites

* refactor: remove dead requirements fallback helper

* refactor: narrow core constraint error handling

* refactor: unify requirement iteration

* refactor: share requirement name parsing

* test: align pip helper coverage

* fix: bind pip output limit at runtime

* refactor: reuse core requirement parser for tokens
2026-03-11 14:21:55 +09:00
Soulter 10ceacfbb1 chore: bump version to 4.19.5 2026-03-11 00:17:14 +08:00
ChuwuYo 66f5ccd902 fix: add file size validation to TTS provider test and MiniMax empty audio detection (#5999)
- Add audio data validation in MiniMax TTS get_audio() method to detect empty responses
- Validate generated audio file size in TTSProvider.test() to ensure valid output
- Provide detailed error messages guiding users to check group_id configuration
- Auto-cleanup test audio files after validation
- Fixes issue where 0KB audio files would pass TTS detection when group_id is not configured
2026-03-11 00:07:19 +08:00
Soulter 3379587223 feat(mcp): enhance logging and initialize MCP clients in background (#5993)
* feat(mcp): enhance logging and initialize MCP clients in background

fixes: #5777

* rf

* fix(mcp): simplify MCP client initialization in background

* fix(mcp): update error message for MCP background initialization failure
2026-03-11 00:00:48 +08:00
邹永赫 e25a1a42cf Revert "fix: clarify missing MCP stdio command errors (#5992)"
This reverts commit 0c771e4a77.
2026-03-11 00:08:06 +09:00
エイカク 0c771e4a77 fix: clarify missing MCP stdio command errors (#5992)
* fix: clarify missing MCP stdio command errors

* refactor: tighten MCP error presentation helpers

* fix: improve MCP test connection feedback

* fix: structure MCP test connection errors

* refactor: share MCP test error codes
2026-03-10 23:05:50 +09:00
camera-2018 ec21cb13d3 feat(lark): supports CardKit streaming output for feishu (#5777)
* feat(lark): 支持飞书 CardKit 流式输出

* refactor(lark): extract streaming fallback logic and deduplicate final text update

* fix(lark): 修复流式输出竞态条件及增强健壮性

- 修复 sender loop 中 delta 快照竟态: await 期间 delta 被 generator
  更新导致 last_sent 记录了未发送的值, 造成输出卡在最后一段
- send_streaming 入口增加 platform_meta 守卫, 未启用时直接回退
- _fallback_send_streaming 移除对已耗尽 generator 的 super() 调用,
  改为内联父类副作用 (Metric.upload + _has_send_oper)
- Metric.upload 统一改为 await, 确保指标上报在方法返回前完成
- 装饰器 support_streaming_message 改为 False, 与 meta() 动态配置对齐
- i18n hint 补充提示: 需在「AI 配置 → 其他配置」中开启流式输出

* chore(lark): 收口配置

* docs(lark): update streaming output instructions and client version requirements

---------

Co-authored-by: bread-ovo <2570425204@qq.com>
Co-authored-by: Soulter <905617992@qq.com>
2026-03-10 19:40:46 +08:00
Soulter 1d26b96d90 fix(workflow): update build-docs.yml to trigger on version tags instead of master branch 2026-03-10 17:16:56 +08:00
一袋米要扛幾樓 be017c87f4 fix: 前端修正切換到 chat 切換後回 welcome 的配置保存最終切換頁面 (#5792)
* 前端修正切換到chat切換後回 welcome 的配置保存最終切換頁面

* 修復 SSR 不含localStorage 環境驗證
2026-03-10 17:14:28 +08:00
lustresixx 23fffa95c8 fix(provider): support 84-char Azure TTS subscription keys (#5813)
* fix(provider): support 84-char Azure TTS subscription keys

* test(provider): add negative Azure TTS key validation cases

* chore: delete test

---------

Co-authored-by: Soulter <905617992@qq.com>
2026-03-10 17:09:13 +08:00
dependabot[bot] 5b303e2e6d chore(deps): bump the github-actions group with 7 updates (#5966)
Bumps the github-actions group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [actions/setup-node](https://github.com/actions/setup-node) | `2` | `6` |
| [actions/checkout](https://github.com/actions/checkout) | `4` | `6` |
| [actions/setup-python](https://github.com/actions/setup-python) | `5` | `6` |
| [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) | `3` | `4` |
| [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) | `3` | `4` |
| [docker/login-action](https://github.com/docker/login-action) | `3` | `4` |
| [docker/build-push-action](https://github.com/docker/build-push-action) | `6` | `7` |


Updates `actions/setup-node` from 2 to 6
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v2...v6)

Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

Updates `actions/setup-python` from 5 to 6
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

Updates `docker/setup-qemu-action` from 3 to 4
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4)

Updates `docker/setup-buildx-action` from 3 to 4
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

Updates `docker/login-action` from 3 to 4
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

Updates `docker/build-push-action` from 6 to 7
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/setup-qemu-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-10 16:56:52 +08:00
Soulter fc33b3eb68 docs: transfer AstrBotDevs/AstrBot-docs to AstrBotDevs/AstrBot (#5960)
* docs: transfer AstrBotDevs/AstrBot-docs to AstrBotDevs/AstrBot
* refactor: reorder imports and improve type hints in sync_docs_to_wiki.py and upload_doc_images_to_r2.py
* feat: add GitHub Actions workflow to sync wiki with documentation

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
Co-authored-by: anka-afk <110004162+anka-afk@users.noreply.github.com>
Co-authored-by: zouyonghe <62183434+zouyonghe@users.noreply.github.com>
Co-authored-by: shuiping233 <49360196+shuiping233@users.noreply.github.com>
Co-authored-by: LIghtJUNction <106986785+LIghtJUNction@users.noreply.github.com>
Co-authored-by: Sjshi763 <179909421+Sjshi763@users.noreply.github.com>
Co-authored-by: xiewoc <70128845+xiewoc@users.noreply.github.com>
Co-authored-by: QingFeng-awa <151742581+QingFeng-awa@users.noreply.github.com>
Co-authored-by: PaloMiku <96452465+PaloMiku@users.noreply.github.com>
Co-authored-by: shangxueink <138397030+shangxueink@users.noreply.github.com>
Co-authored-by: IGCrystal-A <244300990+IGCrystal-A@users.noreply.github.com>
Co-authored-by: RC-CHN <67079377+RC-CHN@users.noreply.github.com>
Co-authored-by: MC090610 <113341105+MC090610@users.noreply.github.com>
Co-authored-by: Waterwzy <196913419+Waterwzy@users.noreply.github.com>
Co-authored-by: Lanhuace-Wan <186303160+Lanhuace-Wan@users.noreply.github.com>
Co-authored-by: LiAlH4qwq <61769640+LiAlH4qwq@users.noreply.github.com>
Co-authored-by: HSOS6 <209910899+HSOS6@users.noreply.github.com>
Co-authored-by: th-dd <162813557+th-dd@users.noreply.github.com>
Co-authored-by: miaoxutao123 <81676466+miaoxutao123@users.noreply.github.com>
Co-authored-by: nuomicici <143102889+nuomicici@users.noreply.github.com>
Co-authored-by: nasyt233 <210103278+nasyt233@users.noreply.github.com>
Co-authored-by: jlugjb <7426462+jlugjb@users.noreply.github.com>
Co-authored-by: Raven95676 <176760093+Raven95676@users.noreply.github.com>
Co-authored-by: Futureppo <180109455+Futureppo@users.noreply.github.com>
Co-authored-by: MliKiowa <61873808+MliKiowa@users.noreply.github.com>
Co-authored-by: Fridemn <150212937+Fridemn@users.noreply.github.com>
Co-authored-by: BakaCookie520 <138355736+BakaCookie520@users.noreply.github.com>
Co-authored-by: YumeYuka <125112916+YumeYuka@users.noreply.github.com>
Co-authored-by: xming521 <32786500+xming521@users.noreply.github.com>
Co-authored-by: ywh555hhh <121592812+ywh555hhh@users.noreply.github.com>
Co-authored-by: stevessr <89645372+stevessr@users.noreply.github.com>
Co-authored-by: roeseth <41995115+roeseth@users.noreply.github.com>
Co-authored-by: ikun-1145141 <265925499+ikun-1145141@users.noreply.github.com>
Co-authored-by: evpeople <54983536+evpeople@users.noreply.github.com>
Co-authored-by: Yue-bin <60509781+Yue-bin@users.noreply.github.com>
Co-authored-by: W1ndys <109416673+W1ndys@users.noreply.github.com>
Co-authored-by: TheFurina <218887821+TheFurina@users.noreply.github.com>
Co-authored-by: Seayon <12275933+Seayon@users.noreply.github.com>
Co-authored-by: OnlyblackTea <38585636+OnlyblackTea@users.noreply.github.com>
Co-authored-by: ocetars <74854972+ocetars@users.noreply.github.com>
Co-authored-by: railgun19457 <117180744+railgun19457@users.noreply.github.com>
Co-authored-by: JunieXD <107397009+JunieXD@users.noreply.github.com>
Co-authored-by: advent259141 <197440256+advent259141@users.noreply.github.com>
Co-authored-by: Doge2077 <91442300+Doge2077@users.noreply.github.com>
Co-authored-by: Bocity <23430545+Bocity@users.noreply.github.com>
Co-authored-by: Aurora-xk <192227833+Aurora-xk@users.noreply.github.com>
2026-03-09 23:38:21 +08:00
ChuwuYo 795aec9578 feat(extension): add filtering and sorting for installed plugins in WebUI (#5923)
* feat(extension): add PluginSortControl reusable component for sorting

* i18n: add i18n keys for plugin sorting and filtering features

* feat(extension): add sorting and status filtering for installed plugins

Backend changes (plugin.py):
- Add _resolve_plugin_dir method to resolve plugin directory path
- Add _get_plugin_installed_at method to get installation time from file mtime
- Add installed_at field to plugin API response

Frontend changes (InstalledPluginsTab.vue):
- Import PluginSortControl component
- Add status filter toggle (all/enabled/disabled) using v-btn-toggle
- Integrate PluginSortControl for sorting options
- Add toolbar layout with actions and controls sections

Frontend changes (MarketPluginsTab.vue):
- Import PluginSortControl component
- Replace v-select + v-btn combination with unified PluginSortControl

Frontend changes (useExtensionPage.js):
- Add installedStatusFilter, installedSortBy, installedSortOrder refs
- Add installedSortItems and installedSortUsesOrder computed properties
- Add sortInstalledPlugins function with multi-criteria support
- Support sorting by install time, name, author, and update status
- Add status filtering in filteredPlugins computed property
- Disable default table sorting by setting sortable: false

* test: add tests for installed_at field in plugin API

- Assert all plugins have installed_at field in get_plugins response
- Assert installed_at is not null after plugin installation

* fix(extension): add explicit fallbacks for installed plugin sort comparisons

* i18n(extension): rename install time label to last modified

* fix(extension): cache installed_at parsing and validate timestamp format in tests

* test(dashboard): strengthen installed_at coverage for plugin API
2026-03-09 17:12:22 +09:00
254 changed files with 25008 additions and 496 deletions
+43
View File
@@ -0,0 +1,43 @@
name: release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest # 运行环境
steps:
- name: checkout
uses: actions/checkout@master
- name: nodejs installation
uses: actions/setup-node@v6
with:
node-version: "18"
- name: npm install
run: npm add -D vitepress
working-directory: './docs' # working-directory 指定 shell 命令运行目录
- name: npm run build
run: npm run docs:build
working-directory: './docs'
- name: scp
uses: appleboy/scp-action@master
with:
host: ${{ secrets.HOST_NEKO }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORDNEKO }}
source: 'docs/.vitepress/dist/*'
target: '/tmp/'
- name: script
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.HOST_NEKO }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORDNEKO }}
script: |
mkdir -p /root/docker_data/caddy/caddy_data/static_site/abv4/
rm -rf /root/docker_data/caddy/caddy_data/static_site/abv4/*
mv /tmp/docs/.vitepress/dist/* /root/docker_data/caddy/caddy_data/static_site/abv4/
rm -rf /tmp/docs/
+10 -10
View File
@@ -64,20 +64,20 @@ jobs:
echo "build_date=$build_date" >> $GITHUB_OUTPUT
- name: Set QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Log in to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Login to GitHub Container Registry
if: env.HAS_GHCR_TOKEN == 'true'
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ env.GHCR_OWNER }}
@@ -98,7 +98,7 @@ jobs:
echo "EOF" >> $GITHUB_OUTPUT
- name: Build and Push Nightly Image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
@@ -163,27 +163,27 @@ jobs:
cp -r dashboard/dist data/
- name: Set QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Log in to DockerHub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Login to GitHub Container Registry
if: env.HAS_GHCR_TOKEN == 'true'
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ env.GHCR_OWNER }}
password: ${{ secrets.GHCR_GITHUB_TOKEN }}
- name: Build and Push Release Image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
+68
View File
@@ -0,0 +1,68 @@
name: sync wiki
on:
workflow_dispatch:
push:
branches:
- master
paths:
- '.github/workflows/sync-wiki.yml'
- 'docs/scripts/sync_docs_to_wiki.py'
- 'docs/tests/test_sync_docs_to_wiki.py'
- 'docs/zh/**'
- 'docs/en/**'
concurrency:
group: sync-wiki-${{ github.ref }}
cancel-in-progress: true
jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Validate manual ref
if: github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/master'
run: |
echo "This workflow only publishes from refs/heads/master. Re-run it from the master branch."
exit 1
- name: Check out docs repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Run sync unit tests
working-directory: docs
run: python -m unittest discover -s tests -p 'test_sync_docs_to_wiki.py' -v
- name: Validate internal doc links
run: python docs/scripts/sync_docs_to_wiki.py --source-root docs --check-links-only
- name: Clone AstrBot wiki
env:
WIKI_TOKEN: ${{ secrets.ASTRBOT_WIKI_TOKEN }}
run: |
test -n "$WIKI_TOKEN"
git clone "https://x-access-token:${WIKI_TOKEN}@github.com/AstrBotDevs/AstrBot.wiki.git" wiki
- name: Generate wiki pages
run: python docs/scripts/sync_docs_to_wiki.py --source-root docs --wiki-root wiki
- name: Commit and push wiki changes
working-directory: wiki
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add .
if git diff --cached --quiet; then
echo "No wiki changes to push"
exit 0
fi
git commit -m "docs: sync wiki from AstrBot-1/docs"
git push
+2
View File
@@ -61,3 +61,5 @@ GenieData/
.codex/
.opencode/
.kilocode/
.worktrees/
docs/plans/
+1 -1
View File
@@ -1 +1 @@
__version__ = "4.19.4"
__version__ = "4.19.5"
+15 -1
View File
@@ -4,7 +4,21 @@ from astrbot.core.config import AstrBotConfig
from astrbot.core.config.default import DB_PATH
from astrbot.core.db.sqlite import SQLiteDatabase
from astrbot.core.file_token_service import FileTokenService
from astrbot.core.utils.pip_installer import PipInstaller
from astrbot.core.utils.pip_installer import (
DependencyConflictError as DependencyConflictError,
)
from astrbot.core.utils.pip_installer import (
PipInstaller,
)
from astrbot.core.utils.requirements_utils import (
RequirementsPrecheckFailed as RequirementsPrecheckFailed,
)
from astrbot.core.utils.requirements_utils import (
find_missing_requirements as find_missing_requirements,
)
from astrbot.core.utils.requirements_utils import (
find_missing_requirements_or_raise as find_missing_requirements_or_raise,
)
from astrbot.core.utils.shared_preferences import SharedPreferences
from astrbot.core.utils.t2i.renderer import HtmlRenderer
+19 -6
View File
@@ -144,10 +144,14 @@ class MCPClient:
cfg = _prepare_config(mcp_server_config.copy())
def logging_callback(msg: str) -> None:
def logging_callback(
msg: str | mcp.types.LoggingMessageNotificationParams,
) -> None:
# Handle MCP service error logs
print(f"MCP Server {name} Error: {msg}")
self.server_errlogs.append(msg)
if isinstance(msg, mcp.types.LoggingMessageNotificationParams):
if msg.level in ("warning", "error", "critical", "alert", "emergency"):
log_msg = f"[{msg.level.upper()}] {str(msg.data)}"
self.server_errlogs.append(log_msg)
if "url" in cfg:
success, error_msg = await _quick_test_mcp_connection(cfg)
@@ -214,15 +218,24 @@ class MCPClient:
**cfg,
)
def callback(msg: str) -> None:
def callback(msg: str | mcp.types.LoggingMessageNotificationParams) -> None:
# Handle MCP service error logs
self.server_errlogs.append(msg)
if isinstance(msg, mcp.types.LoggingMessageNotificationParams):
if msg.level in (
"warning",
"error",
"critical",
"alert",
"emergency",
):
log_msg = f"[{msg.level.upper()}] {str(msg.data)}"
self.server_errlogs.append(log_msg)
stdio_transport = await self.exit_stack.enter_async_context(
mcp.stdio_client(
server_params,
errlog=LogPipe(
level=logging.ERROR,
level=logging.INFO,
logger=logger,
identifier=f"MCPServer-{name}",
callback=callback,
+1 -1
View File
@@ -5,7 +5,7 @@ from typing import Any, TypedDict
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "4.19.4"
VERSION = "4.19.5"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
WEBHOOK_SUPPORTED_PLATFORMS = [
@@ -34,7 +34,7 @@ from .server import LarkWebhookServer
@register_platform_adapter(
"lark", "飞书机器人官方 API 适配器", support_streaming_message=False
"lark", "飞书机器人官方 API 适配器", support_streaming_message=True
)
class LarkPlatformAdapter(Platform):
def __init__(
@@ -491,7 +491,7 @@ class LarkPlatformAdapter(Platform):
name="lark",
description="飞书机器人官方 API 适配器",
id=cast(str, self.config.get("id")),
support_streaming_message=False,
support_streaming_message=True,
)
async def convert_msg(self, event: lark.im.v1.P2ImMessageReceiveV1) -> None:
@@ -1,3 +1,4 @@
import asyncio
import base64
import json
import os
@@ -5,6 +6,14 @@ import uuid
from io import BytesIO
import lark_oapi as lark
from lark_oapi.api.cardkit.v1 import (
ContentCardElementRequest,
ContentCardElementRequestBody,
CreateCardRequest,
CreateCardRequestBody,
SettingsCardRequest,
SettingsCardRequestBody,
)
from lark_oapi.api.im.v1 import (
CreateFileRequest,
CreateFileRequestBody,
@@ -28,6 +37,7 @@ from astrbot.core.utils.media_utils import (
convert_video_format,
get_media_duration,
)
from astrbot.core.utils.metrics import Metric
class LarkMessageEvent(AstrMessageEvent):
@@ -555,15 +565,257 @@ class LarkMessageEvent(AstrMessageEvent):
logger.error(f"发送飞书表情回应失败({response.code}): {response.msg}")
return
async def send_streaming(self, generator, use_fallback: bool = False):
async def _create_streaming_card(self) -> str | None:
"""创建一个开启流式更新模式的卡片实体,返回 card_id。"""
if self.bot.cardkit is None:
logger.error("[Lark] API Client cardkit 模块未初始化")
return None
card_json = {
"schema": "2.0",
"header": {
"title": {"content": "", "tag": "plain_text"},
},
"config": {
"streaming_mode": True,
"summary": {"content": ""},
"streaming_config": {
"print_frequency_ms": {"default": 50},
"print_step": {"default": 2},
"print_strategy": "fast",
},
},
"body": {
"elements": [
{
"tag": "markdown",
"content": "",
"element_id": "markdown_1",
}
]
},
}
request = (
CreateCardRequest.builder()
.request_body(
CreateCardRequestBody.builder()
.type("card_json")
.data(json.dumps(card_json, ensure_ascii=False))
.build()
)
.build()
)
try:
response = await self.bot.cardkit.v1.card.acreate(request)
except Exception as e:
logger.error(f"[Lark] 创建流式卡片实体失败: {e}")
return None
if not response.success():
logger.error(
f"[Lark] 创建流式卡片实体失败({response.code}): {response.msg}"
)
return None
if response.data is None or not response.data.card_id:
logger.error("[Lark] 创建流式卡片实体成功但未返回 card_id")
return None
card_id = response.data.card_id
logger.debug(f"[Lark] 创建流式卡片实体成功: {card_id}")
return card_id
async def _send_card_message(
self,
card_id: str,
reply_message_id: str | None = None,
receive_id: str | None = None,
receive_id_type: str | None = None,
) -> bool:
"""将卡片实体作为 interactive 消息发送。"""
content = json.dumps(
{"type": "card", "data": {"card_id": card_id}},
ensure_ascii=False,
)
return await self._send_im_message(
self.bot,
content=content,
msg_type="interactive",
reply_message_id=reply_message_id,
receive_id=receive_id,
receive_id_type=receive_id_type,
)
async def _update_streaming_text(
self,
card_id: str,
content: str,
sequence: int,
) -> bool:
"""调用 CardKit 流式更新文本接口,向 markdown_1 组件推送全量文本。"""
if self.bot.cardkit is None:
logger.error("[Lark] API Client cardkit 模块未初始化")
return False
request = (
ContentCardElementRequest.builder()
.card_id(card_id)
.element_id("markdown_1")
.request_body(
ContentCardElementRequestBody.builder()
.content(content)
.sequence(sequence)
.uuid(str(uuid.uuid4()))
.build()
)
.build()
)
try:
response = await self.bot.cardkit.v1.card_element.acontent(request)
except Exception as e:
logger.debug(f"[Lark] 流式更新文本失败 (ignored): {e}")
return False
if not response.success():
logger.debug(f"[Lark] 流式更新文本失败({response.code}): {response.msg}")
return False
return True
async def _close_streaming_mode(
self,
card_id: str,
sequence: int,
) -> None:
"""关闭卡片的流式更新模式,使其可正常转发、摘要恢复。"""
if self.bot.cardkit is None:
logger.error("[Lark] API Client cardkit 模块未初始化")
return
settings_json = json.dumps(
{"config": {"streaming_mode": False}},
ensure_ascii=False,
)
request = (
SettingsCardRequest.builder()
.card_id(card_id)
.request_body(
SettingsCardRequestBody.builder()
.settings(settings_json)
.sequence(sequence)
.uuid(str(uuid.uuid4()))
.build()
)
.build()
)
try:
response = await self.bot.cardkit.v1.card.asettings(request)
except Exception as e:
logger.error(f"[Lark] 关闭流式模式失败: {e}")
return
if not response.success():
logger.error(f"[Lark] 关闭流式模式失败({response.code}): {response.msg}")
else:
logger.debug(f"[Lark] 流式模式已关闭: {card_id}")
async def _fallback_send_streaming(self, generator, use_fallback: bool = False):
"""回退到非流式发送:缓冲全部文本后一次性发送,并保留父类副作用。"""
buffer = None
async for chain in generator:
if not buffer:
buffer = chain
else:
buffer.chain.extend(chain.chain)
if not buffer:
return None
buffer.squash_plain()
await self.send(buffer)
return await super().send_streaming(generator, use_fallback)
if buffer:
buffer.squash_plain()
await self.send(buffer)
await Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
self._has_send_oper = True
async def send_streaming(self, generator, use_fallback: bool = False):
"""使用 CardKit 流式卡片实现打字机效果。
流程:创建卡片实体 → 发送消息 → 流式更新文本 → 关闭流式模式。
使用解耦发送循环,LLM token 到达时只更新 buffer 并唤醒发送协程,
发送频率由网络 RTT 自然限流。
"""
# Step 1: 创建流式卡片实体
card_id = await self._create_streaming_card()
if not card_id:
logger.warning("[Lark] 无法创建流式卡片,回退到非流式发送")
await self._fallback_send_streaming(generator, use_fallback)
return
# Step 2: 发送卡片消息
sent = await self._send_card_message(
card_id,
reply_message_id=self.message_obj.message_id,
)
if not sent:
logger.error("[Lark] 发送流式卡片消息失败,回退到非流式发送")
await self._fallback_send_streaming(generator, use_fallback)
return
logger.info("[Lark] 流式输出: 使用 CardKit 流式卡片")
# Step 3: 解耦发送循环 (Event-driven, 参考 Telegram Draft 路径)
sequence = 0
delta = ""
last_sent = ""
done = False
text_changed = asyncio.Event()
async def _sender_loop() -> None:
"""信号驱动的文本发送循环,有新内容就发,RTT 自然限流。"""
nonlocal sequence, last_sent
while not done:
await text_changed.wait()
text_changed.clear()
snapshot = delta
if snapshot and snapshot != last_sent:
sequence += 1
ok = await self._update_streaming_text(card_id, snapshot, sequence)
if ok:
last_sent = snapshot
if delta != snapshot:
text_changed.set()
sender_task = asyncio.create_task(_sender_loop())
try:
async for chain in generator:
if not isinstance(chain, MessageChain):
continue
if chain.type == "break":
# 飞书卡片不支持分段,忽略 break
continue
for comp in chain.chain:
if isinstance(comp, Plain):
delta += comp.text
text_changed.set()
finally:
done = True
text_changed.set()
await sender_task
# Step 4: 必要时补发最终文本 + 关闭流式模式
if delta and delta != last_sent:
sequence += 1
await self._update_streaming_text(card_id, delta, sequence)
sequence += 1
await self._close_streaming_mode(card_id, sequence)
# Step 5: 内联父类 send_streaming 的副作用
await Metric.upload(msg_event_tick=1, adapter_name=self.platform_meta.name)
self._has_send_oper = True
+23 -17
View File
@@ -21,8 +21,8 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
DEFAULT_MCP_CONFIG = {"mcpServers": {}}
DEFAULT_MCP_INIT_TIMEOUT_SECONDS = 20.0
DEFAULT_ENABLE_MCP_TIMEOUT_SECONDS = 30.0
DEFAULT_MCP_INIT_TIMEOUT_SECONDS = 180.0
DEFAULT_ENABLE_MCP_TIMEOUT_SECONDS = 180.0
MCP_INIT_TIMEOUT_ENV = "ASTRBOT_MCP_INIT_TIMEOUT"
ENABLE_MCP_TIMEOUT_ENV = "ASTRBOT_MCP_ENABLE_TIMEOUT"
MAX_MCP_TIMEOUT_SECONDS = 300.0
@@ -417,9 +417,11 @@ class FunctionToolManager:
for (name, cfg, _), result in zip(active_configs, results, strict=False):
if isinstance(result, Exception):
if isinstance(result, MCPInitTimeoutError):
logger.error(f"MCP 服务 {name} 初始化超时({timeout_display}秒)")
logger.error(
f"Connected to MCP server {name} timeout ({timeout_display} seconds)"
)
else:
logger.error(f"MCP 服务 {name} 初始化失败: {result}")
logger.error(f"Failed to initialize MCP server {name}: {result}")
self._log_safe_mcp_debug_config(cfg)
failed_services.append(name)
async with self._runtime_lock:
@@ -430,16 +432,18 @@ class FunctionToolManager:
if failed_services:
logger.warning(
f"以下 MCP 服务初始化失败: {', '.join(failed_services)}"
f"请检查配置文件 mcp_server.json 和服务器可用性。"
f"The following MCP services failed to initialize: {', '.join(failed_services)}. "
f"Please check the mcp_server.json file and server availability."
)
summary = MCPInitSummary(
total=len(active_configs), success=success_count, failed=failed_services
)
logger.info(f"MCP 服务初始化完成: {summary.success}/{summary.total} 成功")
logger.info(
f"MCP services initialization completed: {summary.success}/{summary.total} successful, {len(summary.failed)} failed."
)
if summary.total > 0 and summary.success == 0:
msg = "全部 MCP 服务初始化失败,请检查 mcp_server.json 配置和服务器可用性。"
msg = "All MCP services failed to initialize, please check the mcp_server.json and server availability."
if raise_on_all_failed:
raise MCPAllServicesFailedError(msg)
logger.error(msg)
@@ -461,7 +465,7 @@ class FunctionToolManager:
async with self._runtime_lock:
if name in self._mcp_server_runtime or name in self._mcp_starting:
logger.warning(
f"MCP 服务 {name} 已在运行,忽略本次启用请求(timeout={timeout:g})。"
f"Connected to MCP server {name}, ignoring this startup request (timeout={timeout:g})."
)
self._log_safe_mcp_debug_config(cfg)
return
@@ -478,10 +482,10 @@ class FunctionToolManager:
)
except asyncio.TimeoutError as exc:
raise MCPInitTimeoutError(
f"MCP 服务 {name} 初始化超时({timeout:g} 秒)"
f"Connected to MCP server {name} timeout ({timeout:g} seconds)"
) from exc
except Exception:
logger.error(f"初始化 MCP 客户端 {name} 失败", exc_info=True)
logger.error(f"Failed to initialize MCP client {name}", exc_info=True)
raise
finally:
if mcp_client is None:
@@ -491,9 +495,9 @@ class FunctionToolManager:
async def lifecycle() -> None:
try:
await shutdown_event.wait()
logger.info(f"收到 MCP 客户端 {name} 终止信号")
logger.info(f"Received shutdown signal for MCP client {name}")
except asyncio.CancelledError:
logger.debug(f"MCP 客户端 {name} 任务被取消")
logger.debug(f"MCP client {name} task was cancelled")
raise
finally:
await self._terminate_mcp_client(name)
@@ -545,7 +549,7 @@ class FunctionToolManager:
if strict:
raise MCPShutdownTimeoutError(pending_names, timeout)
logger.warning(
"MCP 服务关闭超时(%s 秒),以下服务未完全关闭:%s",
"MCP server shutdown timeout (%s seconds), the following servers were not fully closed: %s",
f"{timeout:g}",
", ".join(pending_names),
)
@@ -568,7 +572,9 @@ class FunctionToolManager:
try:
await mcp_client.cleanup()
except Exception as cleanup_exc: # noqa: BLE001 - only log here
logger.error(f"清理 MCP 客户端资源 {name} 失败: {cleanup_exc}")
logger.error(
f"Failed to cleanup MCP client resources {name}: {cleanup_exc}"
)
async def _init_mcp_client(self, name: str, config: dict) -> MCPClient:
"""初始化单个MCP客户端"""
@@ -602,7 +608,7 @@ class FunctionToolManager:
)
self.func_list.append(func_tool)
logger.info(f"已连接 MCP 服务 {name}, Tools: {tool_names}")
logger.info(f"Connected to MCP server {name}, Tools: {tool_names}")
return mcp_client
async def _terminate_mcp_client(self, name: str) -> None:
@@ -622,7 +628,7 @@ class FunctionToolManager:
async with self._runtime_lock:
self._mcp_server_runtime.pop(name, None)
self._mcp_starting.discard(name)
logger.info(f"已关闭 MCP 服务 {name}")
logger.info(f"Disconnected from MCP server {name}")
return
# Runtime missing but stale tools may still exist after failed flows.
+18 -18
View File
@@ -79,6 +79,7 @@ class ProviderManager:
self._provider_change_hooks: list[
Callable[[str, ProviderType, str | None], None]
] = []
self._mcp_init_task: asyncio.Task | None = None
def set_provider_change_callback(
self,
@@ -330,24 +331,16 @@ class ProviderManager:
if not self.curr_tts_provider_inst and self.tts_provider_insts:
self.curr_tts_provider_inst = self.tts_provider_insts[0]
# 初始化 MCP Client 连接(等待完成以确保工具可用)
strict_mcp_init = os.getenv("ASTRBOT_MCP_INIT_STRICT", "").strip().lower() in {
"1",
"true",
"yes",
"on",
}
mcp_init_summary = await self.llm_tools.init_mcp_clients(
raise_on_all_failed=strict_mcp_init
)
if (
mcp_init_summary.total > 0
and mcp_init_summary.success == 0
and not strict_mcp_init
):
logger.warning(
"MCP 服务全部初始化失败,系统将继续启动(可设置 "
"ASTRBOT_MCP_INIT_STRICT=1 以在此场景下中止启动)。"
async def _init_mcp_clients_bg() -> None:
try:
await self.llm_tools.init_mcp_clients()
except Exception:
logger.error("MCP init background task failed", exc_info=True)
if self._mcp_init_task is None or self._mcp_init_task.done():
self._mcp_init_task = asyncio.create_task(
_init_mcp_clients_bg(),
name="provider-manager:mcp-init",
)
def dynamic_import_provider(self, type: str) -> None:
@@ -817,6 +810,13 @@ class ProviderManager:
await self.load_provider(new_config)
async def terminate(self) -> None:
if self._mcp_init_task and not self._mcp_init_task.done():
self._mcp_init_task.cancel()
try:
await self._mcp_init_task
except asyncio.CancelledError:
pass
for provider_inst in self.provider_insts:
if hasattr(provider_inst, "terminate"):
await provider_inst.terminate() # type: ignore
+18 -1
View File
@@ -281,7 +281,24 @@ class TTSProvider(AbstractProvider):
accumulated_text += text_part
async def test(self) -> None:
await self.get_audio("hi")
audio_path = await self.get_audio("hi")
# 检查生成的音频文件是否有效
if not os.path.exists(audio_path):
raise Exception("TTS test failed: audio file was not created")
file_size = os.path.getsize(audio_path)
if file_size == 0:
raise Exception(
"TTS test failed: generated audio file is empty (0 bytes). "
"Please check your TTS provider configuration, especially required parameters like group_id for MiniMax."
)
# 清理测试文件
try:
os.remove(audio_path)
except Exception:
pass
class EmbeddingProvider(AbstractProvider):
@@ -20,6 +20,7 @@ from ..register import register_provider_adapter
TEMP_DIR = Path(get_astrbot_temp_path()) / "azure_tts"
TEMP_DIR.mkdir(parents=True, exist_ok=True)
AZURE_TTS_SUBSCRIPTION_KEY_PATTERN = r"^(?:[a-zA-Z0-9]{32}|[a-zA-Z0-9]{84})$"
class OTTSProvider:
@@ -116,7 +117,7 @@ class AzureNativeProvider(TTSProvider):
"azure_tts_subscription_key",
"",
).strip()
if not re.fullmatch(r"^[a-zA-Z0-9]{32}$", self.subscription_key):
if not re.fullmatch(AZURE_TTS_SUBSCRIPTION_KEY_PATTERN, self.subscription_key):
raise ValueError("无效的Azure订阅密钥")
self.region = provider_config.get("azure_tts_region", "eastus").strip()
self.endpoint = (
@@ -235,9 +236,9 @@ class AzureTTSProvider(TTSProvider):
raise ValueError(error_msg) from e
except KeyError as e:
raise ValueError(f"配置错误: 缺少必要参数 {e}") from e
if re.fullmatch(r"^[a-zA-Z0-9]{32}$", key_value):
if re.fullmatch(AZURE_TTS_SUBSCRIPTION_KEY_PATTERN, key_value):
return AzureNativeProvider(config, self.provider_settings)
raise ValueError("订阅密钥格式无效,应为32位字母数字或other[...]格式")
raise ValueError("订阅密钥格式无效,应为32位或84位字母数字或other[...]格式")
async def get_audio(self, text: str) -> str:
if isinstance(self.provider, OTTSProvider):
@@ -154,6 +154,14 @@ class ProviderMiniMaxTTSAPI(TTSProvider):
audio_stream = self._call_tts_stream(text)
audio = await self._audio_play(audio_stream)
# 检查音频数据是否为空
if not audio or len(audio) == 0:
raise Exception(
"MiniMax TTS API returned empty audio data. "
"Please verify your configuration, especially the 'group_id' parameter. "
"You can find your group_id in Account Management -> Basic Information on the MiniMax platform."
)
# 结果保存至文件
with open(path, "wb") as file:
file.write(audio)
@@ -161,4 +169,4 @@ class ProviderMiniMaxTTSAPI(TTSProvider):
return path
except aiohttp.ClientError as e:
raise e
raise Exception(f"MiniMax TTS API request failed: {e!s}")
+97 -9
View File
@@ -14,7 +14,12 @@ import yaml
from packaging.specifiers import InvalidSpecifier, SpecifierSet
from packaging.version import InvalidVersion, Version
from astrbot.core import logger, pip_installer, sp
from astrbot.core import (
DependencyConflictError,
logger,
pip_installer,
sp,
)
from astrbot.core.agent.handoff import FunctionTool, HandoffTool
from astrbot.core.config.astrbot_config import AstrBotConfig
from astrbot.core.config.default import VERSION
@@ -27,6 +32,10 @@ from astrbot.core.utils.astrbot_path import (
)
from astrbot.core.utils.io import remove_dir
from astrbot.core.utils.metrics import Metric
from astrbot.core.utils.requirements_utils import (
RequirementsPrecheckFailed,
find_missing_requirements_or_raise,
)
from . import StarMetadata
from .command_management import sync_command_configs
@@ -48,6 +57,49 @@ class PluginVersionIncompatibleError(Exception):
"""Raised when plugin astrbot_version is incompatible with current AstrBot."""
class PluginDependencyInstallError(Exception):
"""Raised when plugin dependency installation fails."""
def __init__(
self,
*,
plugin_label: str,
requirements_path: str,
error: Exception,
) -> None:
message = f"插件 {plugin_label} 依赖安装失败: {error!s}"
super().__init__(message)
self.plugin_label = plugin_label
self.requirements_path = requirements_path
self.error = error
async def _install_requirements_with_precheck(
*,
plugin_label: str,
requirements_path: str,
) -> None:
try:
missing = find_missing_requirements_or_raise(requirements_path)
except RequirementsPrecheckFailed:
logger.info(
f"正在安装插件 {plugin_label} 的依赖库(预检查失败,回退到完整安装): "
f"{requirements_path}"
)
await pip_installer.install(requirements_path=requirements_path)
return
if not missing:
logger.info(f"插件 {plugin_label} 的依赖已满足,跳过安装。")
return
logger.info(
f"检测到插件 {plugin_label} 缺失依赖,正在按 requirements.txt 安装: "
f"{requirements_path} -> {sorted(missing)}"
)
await pip_installer.install(requirements_path=requirements_path)
class PluginManager:
def __init__(self, context: Context, config: AstrBotConfig) -> None:
from .star_tools import StarTools
@@ -198,15 +250,37 @@ class PluginManager:
to_update.append(p.root_dir_name)
for p in to_update:
plugin_path = os.path.join(plugin_dir, p)
if os.path.exists(os.path.join(plugin_path, "requirements.txt")):
pth = os.path.join(plugin_path, "requirements.txt")
logger.info(f"正在安装插件 {p} 所需的依赖库: {pth}")
try:
await pip_installer.install(requirements_path=pth)
except Exception as e:
logger.error(f"更新插件 {p} 的依赖失败。Code: {e!s}")
await self._ensure_plugin_requirements(plugin_path, p)
return True
async def _ensure_plugin_requirements(
self,
plugin_dir_path: str,
plugin_label: str,
) -> None:
requirements_path = os.path.join(plugin_dir_path, "requirements.txt")
if not os.path.exists(requirements_path):
return
try:
await _install_requirements_with_precheck(
plugin_label=plugin_label,
requirements_path=requirements_path,
)
except asyncio.CancelledError:
raise
except DependencyConflictError as e:
logger.error(f"插件 {plugin_label} 依赖冲突: {e!s}")
raise
except Exception as e:
dependency_error = PluginDependencyInstallError(
plugin_label=plugin_label,
requirements_path=requirements_path,
error=e,
)
logger.exception(str(dependency_error))
raise dependency_error from e
async def _import_plugin_with_dependency_recovery(
self,
path: str,
@@ -422,7 +496,7 @@ class PluginManager:
root_dir_name: str,
plugin_dir_path: str,
reserved: bool,
error: Exception | str,
error: BaseException | str,
error_trace: str,
) -> dict:
record: dict = {
@@ -495,6 +569,9 @@ class PluginManager:
self._cleanup_plugin_state(dir_name)
plugin_path = os.path.join(self.plugin_store_path, dir_name)
await self._ensure_plugin_requirements(plugin_path, dir_name)
success, error = await self.load(specified_dir_name=dir_name)
if success:
self.failed_plugin_dict.pop(dir_name, None)
@@ -1078,6 +1155,10 @@ class PluginManager:
# reload the plugin
dir_name = os.path.basename(plugin_path)
await self._ensure_plugin_requirements(
plugin_path,
dir_name,
)
success, error_message = await self.load(
specified_dir_name=dir_name,
ignore_version_check=ignore_version_check,
@@ -1317,6 +1398,12 @@ class PluginManager:
raise Exception("该插件是 AstrBot 保留插件,无法更新。")
await self.updator.update(plugin, proxy=proxy)
if plugin.root_dir_name:
plugin_dir_path = os.path.join(self.plugin_store_path, plugin.root_dir_name)
await self._ensure_plugin_requirements(
plugin_dir_path,
plugin_name,
)
await self.reload(plugin_name)
async def turn_off_plugin(self, plugin_name: str) -> None:
@@ -1488,6 +1575,7 @@ class PluginManager:
os.remove(zip_file_path)
except BaseException as e:
logger.warning(f"删除插件压缩包失败: {e!s}")
await self._ensure_plugin_requirements(desti_dir, dir_name)
# await self.reload()
success, error_message = await self.load(
specified_dir_name=dir_name,
+121
View File
@@ -0,0 +1,121 @@
import contextlib
import functools
import importlib.metadata as importlib_metadata
import logging
import os
from collections.abc import Iterator
from packaging.requirements import Requirement
from astrbot.core.utils.requirements_utils import (
canonicalize_distribution_name,
collect_installed_distribution_versions,
get_requirement_check_paths,
)
logger = logging.getLogger("astrbot")
def _resolve_core_dist_name(core_dist_name: str | None) -> str | None:
if core_dist_name:
try:
importlib_metadata.distribution(core_dist_name)
return core_dist_name
except importlib_metadata.PackageNotFoundError:
return None
try:
importlib_metadata.distribution("AstrBot")
return "AstrBot"
except importlib_metadata.PackageNotFoundError:
pass
if not __package__:
return None
top_pkg = __package__.split(".")[0]
for dist in importlib_metadata.distributions():
try:
top_level = dist.read_text("top_level.txt") or ""
except Exception:
continue
if top_pkg in top_level.splitlines():
if "Name" in dist.metadata:
return dist.metadata["Name"]
return None
@functools.cache
def _get_core_constraints(core_dist_name: str | None) -> tuple[str, ...]:
try:
resolved_core_dist_name = _resolve_core_dist_name(core_dist_name)
except Exception as exc:
logger.warning("解析核心分发名称失败: %s", exc)
return ()
if not resolved_core_dist_name:
return ()
try:
dist = importlib_metadata.distribution(resolved_core_dist_name)
except importlib_metadata.PackageNotFoundError:
return ()
except Exception as exc:
logger.warning("读取核心分发元数据失败 (%s): %s", resolved_core_dist_name, exc)
return ()
if not dist or not dist.requires:
return ()
installed = collect_installed_distribution_versions(get_requirement_check_paths())
if not installed:
return ()
constraints: list[str] = []
for req_str in dist.requires:
try:
req = Requirement(req_str)
if req.marker and not req.marker.evaluate():
continue
name = canonicalize_distribution_name(req.name)
if name in installed:
constraints.append(f"{name}=={installed[name]}")
except Exception:
continue
return tuple(constraints)
class CoreConstraintsProvider:
def __init__(self, core_dist_name: str | None) -> None:
self._core_dist_name = core_dist_name
@contextlib.contextmanager
def constraints_file(self) -> Iterator[str | None]:
constraints = _get_core_constraints(self._core_dist_name)
if not constraints:
yield None
return
path: str | None = None
try:
import tempfile
with tempfile.NamedTemporaryFile(
mode="w", suffix="_constraints.txt", delete=False, encoding="utf-8"
) as f:
f.write("\n".join(constraints))
path = f.name
logger.info("已启用核心依赖版本保护 (%d 个约束)", len(constraints))
except Exception as exc:
logger.warning("创建临时约束文件失败: %s", exc)
yield None
return
try:
yield path
finally:
if path and os.path.exists(path):
with contextlib.suppress(Exception):
os.remove(path)
+428 -96
View File
@@ -7,21 +7,71 @@ import io
import logging
import os
import re
import shlex
import sys
import threading
from collections import deque
from dataclasses import dataclass
from urllib.parse import urlparse
from astrbot.core.utils.astrbot_path import get_astrbot_site_packages_path
from astrbot.core.utils.core_constraints import CoreConstraintsProvider
from astrbot.core.utils.requirements_utils import (
canonicalize_distribution_name as _canonicalize_distribution_name,
)
from astrbot.core.utils.requirements_utils import (
extract_requirement_name,
extract_requirement_names,
parse_package_install_input,
)
from astrbot.core.utils.runtime_env import is_packaged_desktop_runtime
logger = logging.getLogger("astrbot")
_DISTLIB_FINDER_PATCH_ATTEMPTED = False
_SITE_PACKAGES_IMPORT_LOCK = threading.RLock()
_PIP_FAILURE_PATTERNS = {
"error_prefix": re.compile(r"^\s*error:", re.IGNORECASE),
"user_requested": re.compile(r"\bthe user requested\b", re.IGNORECASE),
"resolution_impossible": re.compile(r"\bresolutionimpossible\b", re.IGNORECASE),
"cannot_install": re.compile(r"\bcannot install\b", re.IGNORECASE),
"conflict": re.compile(r"\bconflict(?:ing|s)?\b", re.IGNORECASE),
"constraint": re.compile(r"\(constraint\)", re.IGNORECASE),
"dependency_detail": re.compile(r"\bdepends on\b", re.IGNORECASE),
}
_SENSITIVE_PIP_VALUE_KEYS = frozenset(
{"password", "passwd", "pass", "api_token", "token", "auth_token"}
)
_MAX_PIP_OUTPUT_LINES = 200
def _canonicalize_distribution_name(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).strip("-").lower()
class DependencyConflictError(Exception):
"""Raised when pip encounters a dependency conflict."""
def __init__(
self, message: str, errors: list[str], *, is_core_conflict: bool
) -> None:
super().__init__(message)
self.errors = errors
self.is_core_conflict = is_core_conflict
class PipInstallError(Exception):
"""Raised when pip install fails without a classified dependency conflict."""
def __init__(self, message: str, *, code: int) -> None:
super().__init__(message)
self.code = code
@dataclass
class PipConflictContext:
relevant_lines: list[str]
requested_lines: list[str]
dependency_detail_lines: list[str]
constraint_lines: list[str]
has_strong_conflict_signal: bool
has_contextual_conflict_signal: bool
def _get_pip_main():
@@ -41,11 +91,12 @@ def _get_pip_main():
return pip_main
def _run_pip_main_with_output(pip_main, args: list[str]) -> tuple[int, str]:
stream = io.StringIO()
with contextlib.redirect_stdout(stream), contextlib.redirect_stderr(stream):
result_code = pip_main(args)
return result_code, stream.getvalue()
def _prepend_sys_path(path: str) -> None:
normalized_target = os.path.realpath(path)
sys.path[:] = [
item for item in sys.path if os.path.realpath(item) != normalized_target
]
sys.path.insert(0, normalized_target)
def _cleanup_added_root_handlers(original_handlers: list[logging.Handler]) -> None:
@@ -59,76 +110,258 @@ def _cleanup_added_root_handlers(original_handlers: list[logging.Handler]) -> No
handler.close()
def _prepend_sys_path(path: str) -> None:
normalized_target = os.path.realpath(path)
sys.path[:] = [
item for item in sys.path if os.path.realpath(item) != normalized_target
]
sys.path.insert(0, normalized_target)
def _get_trusted_host_for_index_url(index_url: str) -> str | None:
parsed = urlparse(index_url if "://" in index_url else f"//{index_url}")
host = parsed.hostname
if host == "mirrors.aliyun.com":
return host
return None
def _module_exists_in_site_packages(module_name: str, site_packages_path: str) -> bool:
base_path = os.path.join(site_packages_path, *module_name.split("."))
package_init = os.path.join(base_path, "__init__.py")
module_file = f"{base_path}.py"
return os.path.isfile(package_init) or os.path.isfile(module_file)
def _normalize_sensitive_pip_key(raw_key: str) -> str:
return raw_key.lstrip("-").replace("-", "_").lower()
def _is_module_loaded_from_site_packages(
module_name: str,
site_packages_path: str,
) -> bool:
module = sys.modules.get(module_name)
if module is None:
try:
module = importlib.import_module(module_name)
except Exception:
return False
def _is_sensitive_pip_value_key(raw_key: str) -> bool:
return _normalize_sensitive_pip_key(raw_key) in _SENSITIVE_PIP_VALUE_KEYS
module_file = getattr(module, "__file__", None)
if not module_file:
return False
module_path = os.path.realpath(module_file)
site_packages_real = os.path.realpath(site_packages_path)
try:
return (
os.path.commonpath([module_path, site_packages_real]) == site_packages_real
def _redact_url_credentials(raw_value: str) -> str:
"""Redact URL credentials and known inline secret values for safe logging."""
parsed = urlparse(raw_value)
if parsed.netloc and "@" in parsed.netloc:
hostname = parsed.hostname or ""
port = f":{parsed.port}" if parsed.port else ""
return parsed._replace(netloc=f"<redacted>@{hostname}{port}").geturl()
if raw_value.startswith("--"):
option, separator, _ = raw_value.partition("=")
if separator and _is_sensitive_pip_value_key(option):
return f"{option}=****"
return raw_value
key, separator, _ = raw_value.partition("=")
if separator and _is_sensitive_pip_value_key(key):
return f"{key}=****"
return raw_value
def _redact_pip_args_for_logging(args: list[str]) -> list[str]:
redacted_args: list[str] = []
redact_next_value = False
for arg in args:
if redact_next_value:
redacted_args.append("****")
redact_next_value = False
continue
if arg.startswith("--") and "=" in arg:
option, value = arg.split("=", 1)
if _is_sensitive_pip_value_key(option):
redacted_args.append(f"{option}=****")
else:
redacted_args.append(f"{option}={_redact_url_credentials(value)}")
continue
if arg.startswith("-i") and arg != "-i":
redacted_args.append(f"-i{_redact_url_credentials(arg[2:])}")
continue
if _is_sensitive_pip_value_key(arg):
redacted_args.append(arg)
redact_next_value = True
continue
redacted_args.append(_redact_url_credentials(arg))
return redacted_args
def _package_specs_override_index(package_specs: list[str]) -> bool:
for index, spec in enumerate(package_specs):
if spec == "--no-index":
return True
if spec in {"-i", "--index-url"}:
if index + 1 < len(package_specs):
return True
continue
if spec.startswith("--index-url="):
return True
if spec.startswith("-i") and spec != "-i":
return True
return False
class _StreamingLogWriter(io.TextIOBase):
def __init__(self, log_func, *, max_lines: int | None = None) -> None:
self._log_func = log_func
self._lines = deque(maxlen=max_lines or _MAX_PIP_OUTPUT_LINES)
self._buffer = ""
def write(self, text: str) -> int:
if not text:
return 0
self._buffer += text.replace("\r\n", "\n").replace("\r", "\n")
while "\n" in self._buffer:
raw_line, self._buffer = self._buffer.split("\n", 1)
line = raw_line.rstrip("\r\n")
self._log_func(line)
self._lines.append(line)
return len(text)
def flush(self) -> None:
line = self._buffer.rstrip("\r\n")
if line:
self._log_func(line)
self._lines.append(line)
self._buffer = ""
@property
def lines(self) -> list[str]:
return list(self._lines)
def _run_pip_main_streaming(pip_main, args: list[str]) -> tuple[int, list[str]]:
stream = _StreamingLogWriter(logger.info, max_lines=_MAX_PIP_OUTPUT_LINES)
with (
contextlib.redirect_stdout(stream),
contextlib.redirect_stderr(stream),
):
result_code = pip_main(args)
stream.flush()
return result_code, stream.lines
def _matches_pip_failure_pattern(line: str, *pattern_names: str) -> bool:
names = pattern_names or tuple(_PIP_FAILURE_PATTERNS)
return any(_PIP_FAILURE_PATTERNS[name].search(line) for name in names)
def _normalize_conflict_detail_line(line: str) -> str:
stripped = line.strip()
if _matches_pip_failure_pattern(stripped, "user_requested"):
return re.sub(
r"^\s*The user requested\s+",
"",
stripped,
flags=re.IGNORECASE,
)
except ValueError:
return False
return stripped
def _extract_requirement_name(raw_requirement: str) -> str | None:
line = raw_requirement.split("#", 1)[0].strip()
if not line:
return None
if line.startswith(("-r", "--requirement", "-c", "--constraint")):
return None
if line.startswith("-"):
def _build_pip_conflict_context(output_lines: list[str]) -> PipConflictContext | None:
matched_indices = [
index
for index, line in enumerate(output_lines)
if _matches_pip_failure_pattern(line)
]
if matched_indices:
relevant_index_set: set[int] = set()
for index in matched_indices:
start = max(0, index - 1)
end = min(len(output_lines), index + 2)
relevant_index_set.update(range(start, end))
relevant_output_lines = [
line
for index, line in enumerate(output_lines)
if index in relevant_index_set
]
else:
relevant_output_lines = output_lines[-5:]
if not relevant_output_lines:
return None
egg_match = re.search(r"#egg=([A-Za-z0-9_.-]+)", raw_requirement)
if egg_match:
return _canonicalize_distribution_name(egg_match.group(1))
dependency_detail_lines = [
line.strip()
for line in relevant_output_lines
if _matches_pip_failure_pattern(line, "dependency_detail")
]
requested_lines = [
line.strip()
for line in relevant_output_lines
if _matches_pip_failure_pattern(line, "user_requested")
and not _matches_pip_failure_pattern(line, "constraint")
]
if not requested_lines:
requested_lines = [
line
for line in dependency_detail_lines
if not _matches_pip_failure_pattern(line, "constraint")
]
constraint_lines = [
line.strip()
for line in relevant_output_lines
if _matches_pip_failure_pattern(line, "constraint")
]
candidate = re.split(r"[<>=!~;\s\[]", line, maxsplit=1)[0].strip()
if not candidate:
has_strong_conflict_signal = any(
_matches_pip_failure_pattern(
line,
"resolution_impossible",
"cannot_install",
)
for line in relevant_output_lines
)
has_contextual_conflict_signal = any(
_matches_pip_failure_pattern(line, "conflict") for line in relevant_output_lines
) and bool(dependency_detail_lines or requested_lines or constraint_lines)
return PipConflictContext(
relevant_lines=relevant_output_lines,
requested_lines=requested_lines,
dependency_detail_lines=dependency_detail_lines,
constraint_lines=constraint_lines,
has_strong_conflict_signal=has_strong_conflict_signal,
has_contextual_conflict_signal=has_contextual_conflict_signal,
)
def _classify_pip_failure(output_lines: list[str]) -> DependencyConflictError | None:
context = _build_pip_conflict_context(output_lines)
if context is None:
return None
return _canonicalize_distribution_name(candidate)
if (
not context.has_strong_conflict_signal
and not context.has_contextual_conflict_signal
and not (context.requested_lines and context.constraint_lines)
):
return None
def _extract_requirement_names(requirements_path: str) -> set[str]:
names: set[str] = set()
try:
with open(requirements_path, encoding="utf-8") as requirements_file:
for line in requirements_file:
requirement_name = _extract_requirement_name(line)
if requirement_name:
names.add(requirement_name)
except Exception as exc:
logger.warning("读取依赖文件失败,跳过冲突检测: %s", exc)
return names
is_core_conflict = bool(context.constraint_lines)
detail = ""
if context.constraint_lines and context.requested_lines:
detail = (
" 冲突详情: "
f"{_normalize_conflict_detail_line(context.requested_lines[0])} vs "
f"{_normalize_conflict_detail_line(context.constraint_lines[0])}"
)
elif len(context.dependency_detail_lines) >= 2:
detail = (
" 冲突详情: "
f"{_normalize_conflict_detail_line(context.dependency_detail_lines[0])} vs "
f"{_normalize_conflict_detail_line(context.dependency_detail_lines[1])}"
)
if is_core_conflict:
message = (
f"检测到核心依赖版本保护冲突。{detail}插件要求的依赖版本与 AstrBot 核心不兼容,"
"为了系统稳定,已阻止该降级行为。请联系插件作者或调整 requirements.txt。"
)
else:
message = f"检测到依赖冲突。{detail}"
return DependencyConflictError(
message,
context.relevant_lines,
is_core_conflict=is_core_conflict,
)
def _extract_top_level_modules(
@@ -155,7 +388,11 @@ def _collect_candidate_modules(
by_name: dict[str, list[importlib_metadata.Distribution]] = {}
try:
for distribution in importlib_metadata.distributions(path=[site_packages_path]):
distribution_name = distribution.metadata.get("Name")
distribution_name = (
distribution.metadata["Name"]
if "Name" in distribution.metadata
else None
)
if not distribution_name:
continue
canonical_name = _canonicalize_distribution_name(distribution_name)
@@ -173,7 +410,7 @@ def _collect_candidate_modules(
for distribution in by_name.get(requirement_name, []):
for dependency_line in distribution.requires or []:
dependency_name = _extract_requirement_name(dependency_line)
dependency_name = extract_requirement_name(dependency_line)
if not dependency_name:
continue
if dependency_name in expanded_requirement_names:
@@ -230,6 +467,38 @@ def _ensure_preferred_modules(
raise RuntimeError(conflict_message)
def _module_exists_in_site_packages(module_name: str, site_packages_path: str) -> bool:
base_path = os.path.join(site_packages_path, *module_name.split("."))
package_init = os.path.join(base_path, "__init__.py")
module_file = f"{base_path}.py"
return os.path.isfile(package_init) or os.path.isfile(module_file)
def _is_module_loaded_from_site_packages(
module_name: str,
site_packages_path: str,
) -> bool:
module = sys.modules.get(module_name)
if module is None:
try:
module = importlib.import_module(module_name)
except Exception:
return False
module_file = getattr(module, "__file__", None)
if not module_file:
return False
module_path = os.path.realpath(module_file)
site_packages_real = os.path.realpath(site_packages_path)
try:
return (
os.path.commonpath([module_path, site_packages_real]) == site_packages_real
)
except ValueError:
return False
def _prefer_module_from_site_packages(
module_name: str, site_packages_path: str
) -> bool:
@@ -531,9 +800,63 @@ def _patch_distlib_finder_for_frozen_runtime() -> None:
class PipInstaller:
def __init__(self, pip_install_arg: str, pypi_index_url: str | None = None) -> None:
def __init__(
self,
pip_install_arg: str,
pypi_index_url: str | None = None,
core_dist_name: str | None = "AstrBot",
) -> None:
self.pip_install_arg = pip_install_arg
self.pypi_index_url = pypi_index_url
self.core_dist_name = core_dist_name
self._core_constraints = CoreConstraintsProvider(core_dist_name)
def _build_pip_args(
self,
package_name: str | None,
requirements_path: str | None,
mirror: str | None,
) -> tuple[list[str], set[str]]:
args: list[str] = []
requested_requirements: set[str] = set()
normalized_requirements_path = (
requirements_path.strip() if requirements_path else ""
)
if package_name and normalized_requirements_path:
raise ValueError(
"package_name and requirements_path cannot be used together"
)
if package_name:
parsed_package = parse_package_install_input(package_name)
if parsed_package.specs:
args = ["install", *parsed_package.specs]
requested_requirements = set(parsed_package.requirement_names)
elif normalized_requirements_path:
args = ["install", "-r", normalized_requirements_path]
requested_requirements = extract_requirement_names(
normalized_requirements_path
)
if not args:
return [], requested_requirements
pip_install_args = (
shlex.split(self.pip_install_arg) if self.pip_install_arg else []
)
if not _package_specs_override_index([*args[1:], *pip_install_args]):
index_url = mirror or self.pypi_index_url or "https://pypi.org/simple"
trusted_host = _get_trusted_host_for_index_url(index_url)
if trusted_host:
args.extend(["--trusted-host", trusted_host])
args.extend(["-i", index_url])
if pip_install_args:
args.extend(pip_install_args)
return args, requested_requirements
async def install(
self,
@@ -541,36 +864,37 @@ class PipInstaller:
requirements_path: str | None = None,
mirror: str | None = None,
) -> None:
args = ["install"]
requested_requirements: set[str] = set()
if package_name:
args.append(package_name)
requirement_name = _extract_requirement_name(package_name)
if requirement_name:
requested_requirements.add(requirement_name)
elif requirements_path:
args.extend(["-r", requirements_path])
requested_requirements = _extract_requirement_names(requirements_path)
index_url = mirror or self.pypi_index_url or "https://pypi.org/simple"
args.extend(["--trusted-host", "mirrors.aliyun.com", "-i", index_url])
args, requested_requirements = self._build_pip_args(
package_name, requirements_path, mirror
)
if not args:
logger.info("Pip 包管理器跳过安装:未提供有效的包名或 requirements 文件。")
return
target_site_packages = None
if is_packaged_desktop_runtime():
target_site_packages = get_astrbot_site_packages_path()
os.makedirs(target_site_packages, exist_ok=True)
_prepend_sys_path(target_site_packages)
args.extend(["--target", target_site_packages])
args.extend(["--upgrade", "--force-reinstall"])
args.extend(
[
"--target",
target_site_packages,
"--upgrade",
"--upgrade-strategy",
"only-if-needed",
]
)
if self.pip_install_arg:
args.extend(self.pip_install_arg.split())
with self._core_constraints.constraints_file() as constraints_file_path:
if constraints_file_path:
args.extend(["-c", constraints_file_path])
logger.info(f"Pip 包管理器: pip {' '.join(args)}")
result_code = await self._run_pip_in_process(args)
if result_code != 0:
raise Exception(f"安装失败,错误码:{result_code}")
logger.info(
"Pip 包管理器 argv: %s",
["pip", *_redact_pip_args_for_logging(args)],
)
await self._run_pip_with_classification(args)
if target_site_packages:
_prepend_sys_path(target_site_packages)
@@ -589,7 +913,7 @@ class PipInstaller:
if not os.path.isdir(target_site_packages):
return
requested_requirements = _extract_requirement_names(requirements_path)
requested_requirements = extract_requirement_names(requirements_path)
if not requested_requirements:
return
@@ -605,13 +929,21 @@ class PipInstaller:
_patch_distlib_finder_for_frozen_runtime()
original_handlers = list(logging.getLogger().handlers)
result_code, output = await asyncio.to_thread(
_run_pip_main_with_output, pip_main, args
)
for line in output.splitlines():
line = line.strip()
if line:
logger.info(line)
try:
result_code, output_lines = await asyncio.to_thread(
_run_pip_main_streaming, pip_main, args
)
finally:
_cleanup_added_root_handlers(original_handlers)
if result_code != 0:
conflict = _classify_pip_failure(output_lines)
if conflict:
raise conflict
_cleanup_added_root_handlers(original_handlers)
return result_code
async def _run_pip_with_classification(self, args: list[str]) -> None:
result_code = await self._run_pip_in_process(args)
if result_code != 0:
raise PipInstallError(f"安装失败,错误码:{result_code}", code=result_code)
+408
View File
@@ -0,0 +1,408 @@
import importlib.metadata as importlib_metadata
import logging
import os
import re
import shlex
import sys
from collections.abc import Iterable, Iterator
from dataclasses import dataclass
from packaging.requirements import InvalidRequirement, Requirement
from packaging.specifiers import SpecifierSet
from packaging.version import InvalidVersion, Version
from astrbot.core.utils.astrbot_path import get_astrbot_site_packages_path
from astrbot.core.utils.runtime_env import is_packaged_desktop_runtime
logger = logging.getLogger("astrbot")
class RequirementsPrecheckFailed(Exception):
"""Raised when the pre-check of requirements fails."""
pass
@dataclass(frozen=True)
class ParsedPackageInput:
specs: tuple[str, ...]
requirement_names: frozenset[str]
def canonicalize_distribution_name(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).strip("-").lower()
def strip_inline_requirement_comment(raw_input: str) -> str:
if raw_input.lstrip().startswith("#"):
return ""
return re.split(r"[ \t]+#", raw_input, maxsplit=1)[0].strip()
def _specifier_contains_version(specifier: SpecifierSet, version: str) -> bool:
try:
parsed_version = Version(version)
except InvalidVersion:
return False
return specifier.contains(parsed_version, prereleases=True)
def _looks_like_local_path_reference(token: str) -> bool:
candidate = token.strip()
if not candidate:
return False
return candidate in {".", ".."} or candidate.startswith(
("./", "../", "/", "~/", ".\\", "..\\", "\\")
)
def looks_like_direct_reference(token: str) -> bool:
candidate = token.strip()
if not candidate:
return False
return (
_looks_like_local_path_reference(candidate)
or candidate.startswith("git+")
or "://" in candidate
)
def extract_requirement_name(raw_requirement: str) -> str | None:
line = raw_requirement.split("#", 1)[0].strip()
if not line:
return None
if line.startswith(("-r", "--requirement", "-c", "--constraint")):
return None
egg_match = re.search(r"#egg=([A-Za-z0-9_.-]+)", raw_requirement)
if egg_match:
return canonicalize_distribution_name(egg_match.group(1))
if line.startswith("-"):
return None
candidate = re.split(r"[<>=!~;\s\[]", line, maxsplit=1)[0].strip()
if not candidate:
return None
return canonicalize_distribution_name(candidate)
def _parse_editable_or_direct_name(target: str) -> str | None:
name = extract_requirement_name(target)
if not name:
return None
if "#egg=" in target or not looks_like_direct_reference(target):
return name
return None
def _parse_requirement_name_and_spec(
line: str,
) -> tuple[str | None, SpecifierSet | None]:
if line.startswith(("-c", "--constraint")):
return None, None
try:
req = Requirement(line)
except InvalidRequirement:
tokens = shlex.split(line)
if not tokens:
return None, None
editable_target: str | None = None
if tokens[0] in {"-e", "--editable"} and len(tokens) > 1:
editable_target = tokens[1]
elif tokens[0].startswith("--editable="):
editable_target = tokens[0].split("=", 1)[1]
if editable_target:
name = _parse_editable_or_direct_name(editable_target)
return (name, None) if name else (None, None)
name = _parse_editable_or_direct_name(line)
return (name, None) if name else (None, None)
if req.marker and not req.marker.evaluate():
return None, None
return canonicalize_distribution_name(req.name), (req.specifier or None)
def _parse_requirement_line(
line: str,
) -> tuple[str, SpecifierSet | None] | None:
name, specifier = _parse_requirement_name_and_spec(line)
return (name, specifier) if name else None
def _extract_requirement_names_from_package_tokens(tokens: list[str]) -> frozenset[str]:
requirement_names: set[str] = set()
skip_next_for: str | None = None
for token in tokens:
if skip_next_for:
if skip_next_for == "editable":
name = _parse_editable_or_direct_name(token)
if name:
requirement_names.add(name)
skip_next_for = None
continue
if token in {"-e", "--editable"}:
skip_next_for = "editable"
continue
if token in {
"-i",
"--index-url",
"--extra-index-url",
"-f",
"--find-links",
"--trusted-host",
"-r",
"--requirement",
"-c",
"--constraint",
}:
skip_next_for = "option-value"
continue
if token.startswith(("--editable=",)):
editable_target = token.split("=", 1)[1]
name = _parse_editable_or_direct_name(editable_target)
if name:
requirement_names.add(name)
continue
if token.startswith(
(
"--index-url=",
"--extra-index-url=",
"--find-links=",
"--trusted-host=",
"--requirement=",
"--constraint=",
)
):
continue
if (
(token.startswith("-i") and token != "-i")
or (token.startswith("-f") and token != "-f")
or token == "--no-index"
):
continue
if token.startswith("-"):
continue
name, _ = _parse_requirement_name_and_spec(token)
if name:
requirement_names.add(name)
return frozenset(requirement_names)
def parse_package_install_input(raw_input: str) -> ParsedPackageInput:
specs: list[str] = []
requirement_names: set[str] = set()
normalized = raw_input.strip()
if not normalized:
return ParsedPackageInput(specs=(), requirement_names=frozenset())
for raw_line in normalized.splitlines():
line = strip_inline_requirement_comment(raw_line)
if not line:
continue
try:
Requirement(line)
except InvalidRequirement:
tokens = shlex.split(line)
if not tokens:
continue
specs.extend(tokens)
requirement_names.update(
_extract_requirement_names_from_package_tokens(tokens)
)
continue
specs.append(line)
name, _ = _parse_requirement_name_and_spec(line)
if name:
requirement_names.add(name)
return ParsedPackageInput(
specs=tuple(specs),
requirement_names=frozenset(requirement_names),
)
def _iter_requirement_lines(
requirements_path: str,
_visited: set[str] | None = None,
) -> Iterator[str]:
visited = _visited or set()
resolved_path = os.path.realpath(requirements_path)
if resolved_path in visited:
logger.warning(
"检测到循环依赖的 requirements 包含: %s,将跳过该文件", resolved_path
)
return
visited.add(resolved_path)
with open(resolved_path, encoding="utf-8") as f:
for raw_line in f:
line = strip_inline_requirement_comment(raw_line)
if not line:
continue
tokens = shlex.split(line)
if not tokens:
continue
nested: str | None = None
if tokens[0] in {"-r", "--requirement"} and len(tokens) > 1:
nested = tokens[1]
elif tokens[0].startswith("--requirement="):
nested = tokens[0].split("=", 1)[1]
if nested:
if not os.path.isabs(nested):
nested = os.path.join(os.path.dirname(resolved_path), nested)
yield from _iter_requirement_lines(nested, _visited=visited)
continue
yield line
def iter_requirements(
requirements_path: str | None = None,
lines: Iterable[str] | None = None,
) -> Iterator[tuple[str, SpecifierSet | None]]:
if lines is None:
if requirements_path is None:
raise ValueError("Either requirements_path or lines must be provided")
lines = _iter_requirement_lines(requirements_path)
for line in lines:
parsed = _parse_requirement_line(line)
if parsed is not None:
yield parsed
def extract_requirement_names(requirements_path: str) -> set[str]:
try:
return {
name for name, _ in iter_requirements(requirements_path=requirements_path)
}
except Exception as exc:
logger.warning("读取依赖文件失败,跳过冲突检测: %s", exc)
return set()
def get_requirement_check_paths() -> list[str]:
paths = list(sys.path)
if is_packaged_desktop_runtime():
target_site_packages = get_astrbot_site_packages_path()
if os.path.isdir(target_site_packages):
paths.insert(0, target_site_packages)
return paths
def _canonical_distribution_identity(distribution) -> tuple[str | None, str | None]:
distribution_name = (
distribution.metadata["Name"] if "Name" in distribution.metadata else None
)
if not distribution_name:
return None, None
return canonicalize_distribution_name(distribution_name), distribution.version
def collect_installed_distribution_versions(paths: list[str]) -> dict[str, str] | None:
installed: dict[str, str] = {}
try:
for distribution in importlib_metadata.distributions(path=paths):
distribution_name, version = _canonical_distribution_identity(distribution)
if not distribution_name or not version:
continue
installed.setdefault(distribution_name, version)
except Exception as exc:
logger.warning("读取已安装依赖失败,跳过缺失依赖预检查: %s", exc)
return None
return installed
def _load_requirement_lines_for_precheck(
requirements_path: str,
) -> tuple[bool, list[str] | None]:
try:
requirement_lines = list(_iter_requirement_lines(requirements_path))
except Exception as exc:
logger.warning(
"预检查缺失依赖失败,将回退到完整安装: %s (%s)",
requirements_path,
exc,
)
return False, None
fallback_line = next(
(
line
for line in requirement_lines
if (
(
line.startswith(("-e ", "--editable ", "--editable="))
and "#egg=" not in line
)
or (
_parse_requirement_line(line) is None
and looks_like_direct_reference(line)
)
)
),
None,
)
if fallback_line is not None:
logger.warning(
"预检查缺失依赖失败,将回退到完整安装: unresolved direct reference in %s: %s",
requirements_path,
fallback_line,
)
return False, None
return True, requirement_lines
def find_missing_requirements(requirements_path: str) -> set[str] | None:
can_precheck, requirement_lines = _load_requirement_lines_for_precheck(
requirements_path
)
if not can_precheck or requirement_lines is None:
return None
required = list(iter_requirements(lines=requirement_lines))
if not required:
return set()
installed = collect_installed_distribution_versions(get_requirement_check_paths())
if installed is None:
return None
missing: set[str] = set()
for name, specifier in required:
installed_version = installed.get(name)
if not installed_version:
missing.add(name)
continue
if specifier and not _specifier_contains_version(specifier, installed_version):
missing.add(name)
return missing
def find_missing_requirements_or_raise(requirements_path: str) -> set[str]:
missing = find_missing_requirements(requirements_path)
if missing is None:
raise RequirementsPrecheckFailed(f"预检查失败: {requirements_path}")
return missing
+31 -1
View File
@@ -5,7 +5,8 @@ import os
import ssl
import traceback
from dataclasses import dataclass
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path
import aiohttp
import certifi
@@ -352,6 +353,34 @@ class PluginRoute(Route):
logger.warning(f"获取插件 Logo 失败: {e}")
return None
def _resolve_plugin_dir(self, plugin) -> Path | None:
if not plugin.root_dir_name:
return None
base_dir = Path(
self.plugin_manager.reserved_plugin_path
if plugin.reserved
else self.plugin_manager.plugin_store_path
)
plugin_dir = base_dir / plugin.root_dir_name
if not plugin_dir.is_dir():
return None
return plugin_dir
def _get_plugin_installed_at(self, plugin) -> str | None:
plugin_dir = self._resolve_plugin_dir(plugin)
if plugin_dir is None:
return None
try:
return datetime.fromtimestamp(
plugin_dir.stat().st_mtime,
timezone.utc,
).isoformat()
except OSError as exc:
logger.warning(f"获取插件安装时间失败 {plugin.name}: {exc!s}")
return None
async def get_plugins(self):
_plugin_resp = []
plugin_name = request.args.get("name")
@@ -377,6 +406,7 @@ class PluginRoute(Route):
"logo": f"/api/file/{logo_url}" if logo_url else None,
"support_platforms": plugin.support_platforms,
"astrbot_version": plugin.astrbot_version,
"installed_at": self._get_plugin_installed_at(plugin),
}
# 检查是否为全空的幽灵插件
if not any(
+153 -1
View File
@@ -43,6 +43,7 @@ class SessionManagementRoute(Route):
"/session/group/create": ("POST", self.create_group),
"/session/group/update": ("POST", self.update_group),
"/session/group/delete": ("POST", self.delete_group),
"/session/group/update-config": ("POST", self.update_group_config),
}
self.conv_mgr = core_lifecycle.conversation_manager
self.core_lifecycle = core_lifecycle
@@ -145,9 +146,20 @@ class SessionManagementRoute(Route):
page=page, page_size=page_size, search=search
)
# 构建规则列表
# 收集属于有配置分组的 UMO,避免重复显示
grouped_umos = set()
groups = self._get_groups()
for group_data in groups.values():
if group_data.get("config"):
grouped_umos.update(group_data.get("umos", []))
# 构建规则列表(排除已被分组管理的 UMO)
rules_list = []
filtered_count = 0
for umo, rules in umo_rules.items():
if umo in grouped_umos:
filtered_count += 1
continue
rule_info = {
"umo": umo,
"rules": rules,
@@ -159,6 +171,7 @@ class SessionManagementRoute(Route):
rule_info["message_type"] = parts[1]
rule_info["session_id"] = parts[2]
rules_list.append(rule_info)
total -= filtered_count
# 获取可用的 providers 和 personas
provider_manager = self.core_lifecycle.provider_manager
@@ -240,6 +253,7 @@ class SessionManagementRoute(Route):
"available_plugins": available_plugins,
"available_kbs": available_kbs,
"available_rule_keys": AVAILABLE_SESSION_RULE_KEYS,
"group_rules": self._get_group_rules(),
}
)
.__dict__
@@ -793,6 +807,51 @@ class SessionManagementRoute(Route):
"""保存分组"""
sp.put("session_groups", groups)
def _get_group_rules(self) -> list:
"""获取有配置的分组列表,用于在规则列表中显示"""
groups = self._get_groups()
group_rules = []
for group_id, group_data in groups.items():
config = group_data.get("config", {})
if config: # 只返回有配置的分组
group_rules.append(
{
"group_id": group_id,
"name": group_data.get("name", ""),
"umo_count": len(group_data.get("umos", [])),
"config": config,
}
)
return group_rules
async def _sync_group_config_to_umos(
self, config: dict, umos: list[str]
) -> tuple[int, list[str]]:
"""将分组配置同步到指定的 UMO 列表
Returns:
(success_count, failed_umos)
"""
success_count = 0
failed_umos = []
for umo in umos:
try:
for rule_key, rule_value in config.items():
if rule_key not in AVAILABLE_SESSION_RULE_KEYS:
continue
if rule_value is None:
continue
if rule_key == "session_plugin_config":
# session_plugin_config 需要包裹 umo key
await sp.session_put(umo, rule_key, {umo: rule_value})
else:
await sp.session_put(umo, rule_key, rule_value)
success_count += 1
except Exception as e:
logger.error(f"同步配置到 {umo} 失败: {e!s}")
failed_umos.append(umo)
return success_count, failed_umos
async def list_groups(self):
"""获取所有分组列表"""
try:
@@ -806,6 +865,7 @@ class SessionManagementRoute(Route):
"name": group_data.get("name", ""),
"umos": group_data.get("umos", []),
"umo_count": len(group_data.get("umos", [])),
"config": group_data.get("config", {}),
}
)
return Response().ok({"groups": groups_list}).__dict__
@@ -875,6 +935,7 @@ class SessionManagementRoute(Route):
return Response().error(f"分组 '{group_id}' 不存在").__dict__
group = groups[group_id]
old_umos = set(group.get("umos", []))
# 更新名称
if name is not None:
@@ -883,6 +944,7 @@ class SessionManagementRoute(Route):
# 直接设置 umos 列表
if umos is not None:
group["umos"] = umos
new_umos = set(umos)
else:
# 增量更新
current_umos = set(group.get("umos", []))
@@ -891,9 +953,21 @@ class SessionManagementRoute(Route):
if remove_umos:
current_umos.difference_update(remove_umos)
group["umos"] = list(current_umos)
new_umos = current_umos
self._save_groups(groups)
# 自动同步分组配置给新加入的成员
group_config = group.get("config", {})
newly_added = new_umos - old_umos
if group_config and newly_added:
sync_count, _ = await self._sync_group_config_to_umos(
group_config, list(newly_added)
)
logger.info(
f"自动同步分组 '{group['name']}' 配置到 {sync_count} 个新成员"
)
return (
Response()
.ok(
@@ -936,3 +1010,81 @@ class SessionManagementRoute(Route):
except Exception as e:
logger.error(f"删除分组失败: {e!s}")
return Response().error(f"删除分组失败: {e!s}").__dict__
async def update_group_config(self):
"""更新分组的配置,并同步到所有成员 UMO
请求体:
{
"group_id": "分组ID",
"config": {
"session_service_config": {...},
"session_plugin_config": {...},
"kb_config": {...},
"provider_perf_chat_completion": ...,
"provider_perf_speech_to_text": ...,
"provider_perf_text_to_speech": ...
}
}
"""
try:
data = await request.get_json()
group_id = data.get("group_id")
config = data.get("config", {})
if not group_id:
return Response().error("缺少必要参数: group_id").__dict__
groups = self._get_groups()
if group_id not in groups:
return Response().error(f"分组 '{group_id}' 不存在").__dict__
group = groups[group_id]
# 保存配置到分组
group["config"] = config
self._save_groups(groups)
# 同步到所有成员 UMO
umos = group.get("umos", [])
if not config:
# 空配置 → 清除成员上的所有分组下发规则
success_count = 0
failed_umos = []
for umo in umos:
try:
for rule_key in AVAILABLE_SESSION_RULE_KEYS:
try:
await sp.session_remove(umo, rule_key)
except Exception:
pass
success_count += 1
except Exception as e:
logger.error(f"清除 {umo} 规则失败: {e!s}")
failed_umos.append(umo)
else:
success_count, failed_umos = await self._sync_group_config_to_umos(
config, umos
)
msg = f"分组 '{group['name']}' 配置已保存并同步到 {success_count}/{len(umos)} 个会话"
if failed_umos:
msg += f"{len(failed_umos)} 个失败"
return (
Response()
.ok(
{
"message": msg,
"success_count": success_count,
"failed_count": len(failed_umos),
"failed_umos": failed_umos,
}
)
.__dict__
)
except Exception as e:
logger.error(f"更新分组配置失败: {e!s}")
return Response().error(f"更新分组配置失败: {e!s}").__dict__
+43
View File
@@ -0,0 +1,43 @@
## What's Changed
### 新增
- Lark 适配器支持 CardKit 流式输出(飞书)([#5777](https://github.com/AstrBotDevs/AstrBot/pull/5777))。
- WebUI 已安装插件列表新增筛选与排序功能 ([#5923](https://github.com/AstrBotDevs/AstrBot/pull/5923))。
### 优化
- 启动时后台加载 MCP Server,不阻塞加载流程 ([#5993](https://github.com/AstrBotDevs/AstrBot/pull/5993))。
### 修复
- 部分情况下 MCP 页报错 500 导致查看不了 MCP 服务器 ([#5993](https://github.com/AstrBotDevs/AstrBot/pull/5993))。
- 修复 TTS Provider 测试:增加文件大小校验,并补充 MiniMax 空音频检测 ([#5999](https://github.com/AstrBotDevs/AstrBot/pull/5999))。
- 修复前端切换到 Chat 后又回到 Welcome 时,页面切换配置未正确持久化的问题 ([#5792](https://github.com/AstrBotDevs/AstrBot/pull/5792))。
- 修复 Azure TTS 不支持 84 位订阅密钥的问题 ([#5813](https://github.com/AstrBotDevs/AstrBot/pull/5813))。
### 文档
- 文档仓库迁移:将 `AstrBotDevs/AstrBot-docs` 内容迁移至 `AstrBotDevs/AstrBot` ([#5960](https://github.com/AstrBotDevs/AstrBot/pull/5960))。
---
## What's Changed (EN)
### New Features
- Added CardKit streaming output support for the Lark/Feishu adapter ([#5777](https://github.com/AstrBotDevs/AstrBot/pull/5777)).
- Added filtering and sorting for installed plugins in the WebUI ([#5923](https://github.com/AstrBotDevs/AstrBot/pull/5923)).
### Impprovement
- MCP Server now loads in the background during startup without blocking the loading process ([#5993](https://github.com/AstrBotDevs/AstrBot/pull/5993)).
### Bug Fixes
- Added file size validation in TTS provider tests and MiniMax empty-audio detection ([#5999](https://github.com/AstrBotDevs/AstrBot/pull/5999)).
- Fixed frontend state persistence when switching from Chat back to Welcome ([#5792](https://github.com/AstrBotDevs/AstrBot/pull/5792)).
- Fixed Azure TTS support for 84-character subscription keys ([#5813](https://github.com/AstrBotDevs/AstrBot/pull/5813)).
- Reverted the MCP stdio missing-command error wording change after the previous fix ([#5992](https://github.com/AstrBotDevs/AstrBot/pull/5992)).
### Documentation
- Migrated documentation content from `AstrBotDevs/AstrBot-docs` into `AstrBotDevs/AstrBot` ([#5960](https://github.com/AstrBotDevs/AstrBot/pull/5960)).
@@ -0,0 +1,97 @@
<script setup>
const props = defineProps({
modelValue: {
type: String,
required: true,
},
items: {
type: Array,
required: true,
},
label: {
type: String,
required: true,
},
order: {
type: String,
default: "desc",
},
ascendingLabel: {
type: String,
default: "Ascending",
},
descendingLabel: {
type: String,
default: "Descending",
},
showOrder: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue", "update:order"]);
const updateSortBy = (value) => {
emit("update:modelValue", value);
};
const toggleOrder = () => {
emit("update:order", props.order === "desc" ? "asc" : "desc");
};
</script>
<template>
<div class="plugin-sort-control">
<v-select
:model-value="modelValue"
:items="items"
density="compact"
variant="outlined"
hide-details
:label="label"
class="plugin-sort-control__select"
@update:model-value="updateSortBy"
>
<template #prepend-inner>
<v-icon size="small">mdi-sort</v-icon>
</template>
</v-select>
<v-btn
v-if="showOrder"
icon
variant="text"
density="compact"
@click="toggleOrder"
>
<v-icon>{{
order === "desc" ? "mdi-arrow-down-thin" : "mdi-arrow-up-thin"
}}</v-icon>
<v-tooltip activator="parent" location="top">
{{ order === "desc" ? descendingLabel : ascendingLabel }}
</v-tooltip>
</v-btn>
</div>
</template>
<style scoped>
.plugin-sort-control {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.plugin-sort-control__select {
min-width: 180px;
max-width: 220px;
}
.plugin-sort-control__select :deep(.v-field__input),
.plugin-sort-control__select :deep(.v-field-label),
.plugin-sort-control__select :deep(.v-select__selection-text),
.plugin-sort-control__select :deep(.v-field__prepend-inner) {
font-size: 0.875rem;
}
</style>
@@ -1521,4 +1521,4 @@
"helpMiddle": "or",
"helpSuffix": "."
}
}
}
@@ -23,6 +23,9 @@
"placeholder": "Search extensions...",
"marketPlaceholder": "Search market extensions..."
},
"filters": {
"all": "All"
},
"views": {
"card": "Card View",
"list": "List View"
@@ -122,10 +125,14 @@
"sourceSafetyWarning": "Even with the default source, plugin stability and security cannot be fully guaranteed. Please verify carefully before use."
},
"sort": {
"by": "Sort by",
"default": "Default",
"installTime": "Last Modified",
"name": "Name",
"stars": "Stars",
"author": "Author",
"updated": "Last Updated",
"updateStatus": "Update Status",
"ascending": "Ascending",
"descending": "Descending"
},
@@ -1524,4 +1524,4 @@
"helpMiddle": "或",
"helpSuffix": "。"
}
}
}
@@ -23,6 +23,9 @@
"placeholder": "搜索插件...",
"marketPlaceholder": "搜索市场插件..."
},
"filters": {
"all": "全部"
},
"views": {
"card": "卡片视图",
"list": "列表视图"
@@ -122,10 +125,14 @@
"sourceSafetyWarning": "即使是默认插件源,我们也不能完全保证插件的稳定性和安全性,使用前请谨慎核查。"
},
"sort": {
"by": "排序方式",
"default": "默认排序",
"installTime": "最后修改时间",
"name": "名称",
"stars": "Star数",
"author": "作者名",
"updated": "更新时间",
"updateStatus": "更新状态",
"ascending": "升序",
"descending": "降序"
},
@@ -27,6 +27,7 @@ const customizer = useCustomizerStore();
const theme = useTheme();
const { t } = useI18n();
const route = useRoute();
const LAST_BOT_ROUTE_KEY = 'astrbot:last_bot_route';
let dialog = ref(false);
let accountWarning = ref(false)
let updateStatusDialog = ref(false);
@@ -402,15 +403,32 @@ const viewMode = computed({
});
// viewMode bot
watch(() => customizer.viewMode, (newMode, oldMode) => {
if (newMode === 'bot' && oldMode === 'chat') {
// chat bot
if (route.path !== '/') {
router.push('/');
// bot
// route bot
watch(() => route.fullPath, (newPath) => {
if (customizer.viewMode === 'bot' && typeof window !== 'undefined') {
try {
localStorage.setItem(LAST_BOT_ROUTE_KEY, newPath);
} catch (e) {
console.error('Failed to save last bot route to localStorage:', e);
}
}
});
// viewMode
watch(() => customizer.viewMode, (newMode, oldMode) => {
if (newMode === 'bot' && oldMode === 'chat' && typeof window !== 'undefined') {
// chat bot bot
let lastBotRoute = '/';
try {
lastBotRoute = localStorage.getItem(LAST_BOT_ROUTE_KEY) || '/';
} catch (e) {
console.error('Failed to read last bot route from localStorage:', e);
}
router.push(lastBotRoute);
}
});
// Merry Christmas! 🎄
const isChristmas = computed(() => {
const today = new Date();
+273 -51
View File
@@ -1,4 +1,4 @@
<template>
<template>
<div class="session-management-page">
<v-container fluid class="pa-0">
<v-card flat>
@@ -35,7 +35,16 @@
<!-- UMO 信息 -->
<template v-slot:item.umo_info="{ item }">
<div>
<div class="d-flex align-center">
<div class="d-flex align-center" v-if="item.isGroup">
<v-chip size="x-small" color="deep-purple" variant="flat" class="mr-2">
分组
</v-chip>
<span class="font-weight-medium">{{ item.groupName }}</span>
<v-chip size="x-small" variant="outlined" class="ml-2">
{{ item.umo_count }} 个会话
</v-chip>
</div>
<div class="d-flex align-center" v-else>
<v-chip size="x-small" :color="getPlatformColor(item.platform)" class="mr-2">
{{ item.platform || 'unknown' }}
</v-chip>
@@ -282,14 +291,24 @@
{{ tm('addRule.description') }}
</v-alert>
<v-autocomplete v-model="selectedNewUmo" :items="availableUmos" :loading="loadingUmos"
<v-radio-group v-model="addRuleTargetType" inline hide-details class="mb-4">
<v-radio label="单个会话" value="session"></v-radio>
<v-radio label="分组" value="group" :disabled="groups.length === 0"></v-radio>
</v-radio-group>
<v-autocomplete v-if="addRuleTargetType === 'session'" v-model="selectedNewUmo" :items="availableUmos" :loading="loadingUmos"
:label="tm('addRule.selectUmo')" variant="outlined" clearable :no-data-text="tm('addRule.noUmos')" />
<v-select v-if="addRuleTargetType === 'group'" v-model="selectedGroup" :items="groupSelectOptions"
item-title="label" item-value="value" return-object
label="选择分组" variant="outlined" clearable
:no-data-text="'暂无分组,请先创建分组'" />
</v-card-text>
<v-card-actions class="px-4 pb-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="addRuleDialog = false">{{ tm('buttons.cancel') }}</v-btn>
<v-btn color="primary" variant="tonal" @click="createNewRule" :disabled="!selectedNewUmo">
<v-btn color="primary" variant="tonal" @click="createNewRule" :disabled="addRuleTargetType === 'session' ? !selectedNewUmo : !selectedGroup">
{{ tm('buttons.next') }}
</v-btn>
</v-card-actions>
@@ -334,12 +353,7 @@
</v-col>
</v-row>
<div class="d-flex justify-end mt-4">
<v-btn color="primary" variant="tonal" size="small" @click="saveServiceConfig" :loading="saving"
prepend-icon="mdi-content-save">
{{ tm('buttons.save') }}
</v-btn>
</div>
<!-- Provider Config Section -->
<div class="d-flex align-center mb-4 mt-4">
@@ -364,12 +378,7 @@
</v-col>
</v-row>
<div class="d-flex justify-end mt-4">
<v-btn color="primary" variant="tonal" size="small" @click="saveProviderConfig" :loading="saving"
prepend-icon="mdi-content-save">
{{ tm('buttons.save') }}
</v-btn>
</div>
<!-- Persona Config Section -->
<div class="d-flex align-center mb-4 mt-4">
@@ -389,12 +398,7 @@
</v-col>
</v-row>
<div class="d-flex justify-end mt-4">
<v-btn color="primary" variant="tonal" size="small" @click="saveServiceConfig" :loading="saving"
prepend-icon="mdi-content-save">
{{ tm('buttons.save') }}
</v-btn>
</div>
<!-- Plugin Config Section -->
<div class="d-flex align-center mb-4 mt-4">
@@ -414,12 +418,7 @@
</v-col>
</v-row>
<div class="d-flex justify-end mt-4">
<v-btn color="primary" variant="tonal" size="small" @click="savePluginConfig" :loading="saving"
prepend-icon="mdi-content-save">
{{ tm('buttons.save') }}
</v-btn>
</div>
<!-- KB Config Section -->
<div class="d-flex align-center mb-4 mt-4">
@@ -442,14 +441,17 @@
</v-col>
</v-row>
<div class="d-flex justify-end mt-4">
<v-btn color="primary" variant="tonal" size="small" @click="saveKbConfig" :loading="saving"
prepend-icon="mdi-content-save">
{{ tm('buttons.save') }}
</v-btn>
</div>
</div>
</v-card-text>
<v-card-actions class="px-6 pb-4">
<v-spacer></v-spacer>
<v-btn variant="text" @click="closeRuleEditor">{{ tm('buttons.cancel') }}</v-btn>
<v-btn color="primary" variant="tonal" @click="saveAllConfigs" :loading="saving"
prepend-icon="mdi-content-save">
{{ tm('buttons.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -567,6 +569,8 @@ export default {
addRuleDialog: false,
availableUmos: [],
selectedNewUmo: null,
addRuleTargetType: 'session',
selectedGroup: null,
//
ruleDialog: false,
@@ -729,6 +733,13 @@ export default {
return options
},
groupSelectOptions() {
return this.groups.map(g => ({
label: `${g.name} (${g.umo_count} 个会话)`,
value: g,
}))
},
groupOptions() {
return this.groups.map(g => ({
label: `${g.name} (${g.umo_count} 个会话)`,
@@ -811,7 +822,7 @@ export default {
})
if (response.data.status === 'ok') {
const data = response.data.data
this.rulesList = data.rules
this.rulesList = data.rules || []
this.totalItems = data.total
this.availablePersonas = data.available_personas
this.availableChatProviders = data.available_chat_providers
@@ -819,6 +830,20 @@ export default {
this.availableTtsProviders = data.available_tts_providers
this.availablePlugins = data.available_plugins || []
this.availableKbs = data.available_kbs || []
//
const groupRules = data.group_rules || []
for (const gr of groupRules) {
this.rulesList.unshift({
umo: `[\u5206\u7ec4] ${gr.name}`,
isGroup: true,
groupId: gr.group_id,
groupName: gr.name,
umo_count: gr.umo_count,
rules: gr.config || {},
})
}
this.totalItems += groupRules.length
} else {
this.showError(response.data.message || this.tm('messages.loadError'))
}
@@ -872,10 +897,89 @@ export default {
async openAddRuleDialog() {
this.addRuleDialog = true
this.selectedNewUmo = null
this.addRuleTargetType = 'session'
this.selectedGroup = null
await this.loadUmos()
},
async saveAllConfigs() {
if (!this.selectedUmo) return
// API
if (this.selectedUmo.isGroup) {
this.saving = true
try {
const config = {
session_service_config: { ...this.serviceConfig },
provider_perf_chat_completion: this.providerConfig.chat_completion || null,
provider_perf_speech_to_text: this.providerConfig.speech_to_text || null,
provider_perf_text_to_speech: this.providerConfig.text_to_speech || null,
session_plugin_config: { ...this.pluginConfig },
kb_config: { ...this.kbConfig },
}
//
if (!config.session_service_config.custom_name) delete config.session_service_config.custom_name
if (config.session_service_config.persona_id === null) delete config.session_service_config.persona_id
const response = await axios.post('/api/session/group/update-config', {
group_id: this.selectedUmo.groupId,
config: config
})
if (response.data.status === 'ok') {
this.showSuccess(response.data.data?.message || '分组配置已保存并同步')
await this.loadData()
} else {
this.showError(response.data.message || this.tm('messages.saveError'))
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
} finally {
this.saving = false
}
return
}
//
this.saving = true
this._batchSaving = true
try {
await this.saveServiceConfig()
await this.saveProviderConfig()
await this.savePluginConfig()
await this.saveKbConfig()
this.showSuccess(this.tm('messages.saveSuccess'))
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
} finally {
this._batchSaving = false
this.saving = false
}
},
createNewRule() {
if (this.addRuleTargetType === 'group') {
//
if (!this.selectedGroup) return
const group = this.selectedGroup.value || this.selectedGroup
if (!group.umos || group.umos.length === 0) {
this.showError('该分组没有成员会话')
return
}
//
const newItem = {
umo: `[分组] ${group.name}`,
isGroup: true,
groupId: group.id,
groupName: group.name,
groupUmos: group.umos,
rules: {},
}
this.addRuleDialog = false
this.openRuleEditor(newItem)
return
}
//
if (!this.selectedNewUmo) return
//
@@ -943,13 +1047,37 @@ export default {
async saveServiceConfig() {
if (!this.selectedUmo) return
this.saving = true
if (!this._batchSaving) this.saving = true
try {
const config = { ...this.serviceConfig }
//
if (!config.custom_name) delete config.custom_name
if (config.persona_id === null) delete config.persona_id
//
if (this.selectedUmo.isGroup) {
const umos = this.selectedUmo.groupUmos
let successCount = 0
for (const umo of umos) {
try {
await axios.post('/api/session/update-rule', {
umo: umo,
rule_key: 'session_service_config',
rule_value: config
})
successCount++
} catch (e) {
console.error(`更新 ${umo} 失败:`, e)
}
}
if (!this._batchSaving) {
this.showSuccess(`已更新 ${successCount}/${umos.length} 个会话的服务配置`)
await this.loadData()
this.saving = false
}
return
}
const response = await axios.post('/api/session/update-rule', {
umo: this.selectedUmo.umo,
rule_key: 'session_service_config',
@@ -957,7 +1085,7 @@ export default {
})
if (response.data.status === 'ok') {
this.showSuccess(this.tm('messages.saveSuccess'))
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
this.editingRules.session_service_config = config
//
@@ -980,17 +1108,45 @@ export default {
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
}
this.saving = false
if (!this._batchSaving) this.saving = false
},
async saveProviderConfig() {
if (!this.selectedUmo) return
this.saving = true
if (!this._batchSaving) this.saving = true
try {
const providerTypes = ['chat_completion', 'speech_to_text', 'text_to_speech']
//
if (this.selectedUmo.isGroup) {
const umos = this.selectedUmo.groupUmos
let successCount = 0
for (const umo of umos) {
try {
const tasks = []
for (const type of providerTypes) {
const value = this.providerConfig[type]
if (value) {
tasks.push(axios.post('/api/session/update-rule', { umo, rule_key: `provider_perf_${type}`, rule_value: value }))
}
}
if (tasks.length > 0) await Promise.all(tasks)
successCount++
} catch (e) {
console.error(`更新 ${umo} Provider 失败:`, e)
}
}
if (!this._batchSaving) {
this.showSuccess(`已更新 ${successCount}/${umos.length} 个会话的 Provider 配置`)
await this.loadData()
this.saving = false
}
return
}
const updateTasks = []
const deleteTasks = []
const providerTypes = ['chat_completion', 'speech_to_text', 'text_to_speech']
for (const type of providerTypes) {
const value = this.providerConfig[type]
@@ -1017,7 +1173,7 @@ export default {
const allTasks = [...updateTasks, ...deleteTasks]
if (allTasks.length > 0) {
await Promise.all(allTasks)
this.showSuccess(this.tm('messages.saveSuccess'))
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
//
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
@@ -1042,24 +1198,48 @@ export default {
}
}
} else {
this.showSuccess(this.tm('messages.noChanges'))
if (!this._batchSaving) this.showSuccess(this.tm('messages.noChanges'))
}
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
}
this.saving = false
if (!this._batchSaving) this.saving = false
},
async savePluginConfig() {
if (!this.selectedUmo) return
this.saving = true
if (!this._batchSaving) this.saving = true
try {
const config = {
enabled_plugins: this.pluginConfig.enabled_plugins,
disabled_plugins: this.pluginConfig.disabled_plugins,
}
//
if (this.selectedUmo.isGroup) {
const umos = this.selectedUmo.groupUmos
let successCount = 0
for (const umo of umos) {
try {
if (config.enabled_plugins.length === 0 && config.disabled_plugins.length === 0) {
await axios.post('/api/session/delete-rule', { umo, rule_key: 'session_plugin_config' })
} else {
await axios.post('/api/session/update-rule', { umo, rule_key: 'session_plugin_config', rule_value: config })
}
successCount++
} catch (e) {
console.error(`更新 ${umo} 插件配置失败:`, e)
}
}
if (!this._batchSaving) {
this.showSuccess(`已更新 ${successCount}/${umos.length} 个会话的插件配置`)
await this.loadData()
this.saving = false
}
return
}
//
if (config.enabled_plugins.length === 0 && config.disabled_plugins.length === 0) {
if (this.editingRules.session_plugin_config) {
@@ -1071,7 +1251,7 @@ export default {
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
if (item) delete item.rules.session_plugin_config
}
this.showSuccess(this.tm('messages.saveSuccess'))
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
} else {
const response = await axios.post('/api/session/update-rule', {
umo: this.selectedUmo.umo,
@@ -1080,7 +1260,7 @@ export default {
})
if (response.data.status === 'ok') {
this.showSuccess(this.tm('messages.saveSuccess'))
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
this.editingRules.session_plugin_config = config
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
@@ -1102,13 +1282,13 @@ export default {
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
}
this.saving = false
if (!this._batchSaving) this.saving = false
},
async saveKbConfig() {
if (!this.selectedUmo) return
this.saving = true
if (!this._batchSaving) this.saving = true
try {
const config = {
kb_ids: this.kbConfig.kb_ids,
@@ -1116,6 +1296,30 @@ export default {
enable_rerank: this.kbConfig.enable_rerank,
}
//
if (this.selectedUmo.isGroup) {
const umos = this.selectedUmo.groupUmos
let successCount = 0
for (const umo of umos) {
try {
if (config.kb_ids.length === 0) {
await axios.post('/api/session/delete-rule', { umo, rule_key: 'kb_config' })
} else {
await axios.post('/api/session/update-rule', { umo, rule_key: 'kb_config', rule_value: config })
}
successCount++
} catch (e) {
console.error(`更新 ${umo} 知识库配置失败:`, e)
}
}
if (!this._batchSaving) {
this.showSuccess(`已更新 ${successCount}/${umos.length} 个会话的知识库配置`)
await this.loadData()
this.saving = false
}
return
}
// kb_ids
if (config.kb_ids.length === 0) {
if (this.editingRules.kb_config) {
@@ -1127,7 +1331,7 @@ export default {
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
if (item) delete item.rules.kb_config
}
this.showSuccess(this.tm('messages.saveSuccess'))
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
} else {
const response = await axios.post('/api/session/update-rule', {
umo: this.selectedUmo.umo,
@@ -1136,7 +1340,7 @@ export default {
})
if (response.data.status === 'ok') {
this.showSuccess(this.tm('messages.saveSuccess'))
if (!this._batchSaving) this.showSuccess(this.tm('messages.saveSuccess'))
this.editingRules.kb_config = config
let item = this.rulesList.find(u => u.umo === this.selectedUmo.umo)
@@ -1158,7 +1362,7 @@ export default {
} catch (error) {
this.showError(error.response?.data?.message || this.tm('messages.saveError'))
}
this.saving = false
if (!this._batchSaving) this.saving = false
},
confirmDeleteRules(item) {
@@ -1171,6 +1375,24 @@ export default {
this.deleting = true
try {
//
if (this.deleteTarget.isGroup) {
const response = await axios.post('/api/session/group/update-config', {
group_id: this.deleteTarget.groupId,
config: {}
})
if (response.data.status === 'ok') {
this.showSuccess('分组配置已清除')
this.deleteDialog = false
this.deleteTarget = null
await this.loadData()
} else {
this.showError(response.data.message || this.tm('messages.deleteError'))
}
this.deleting = false
return
}
const response = await axios.post('/api/session/delete-rule', {
umo: this.deleteTarget.umo
})
@@ -1,4 +1,5 @@
<script setup>
import PluginSortControl from "@/components/extension/PluginSortControl.vue";
import ExtensionCard from "@/components/shared/ExtensionCard.vue";
import StyledMenu from "@/components/shared/StyledMenu.vue";
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
@@ -48,6 +49,9 @@ const {
getInitialListViewMode,
isListView,
pluginSearch,
installedStatusFilter,
installedSortBy,
installedSortOrder,
loading_,
currentPage,
dangerConfirmDialog,
@@ -82,6 +86,8 @@ const {
toPinyinText,
toInitials,
plugin_handler_info_headers,
installedSortItems,
installedSortUsesOrder,
pluginHeaders,
filteredExtensions,
filteredPlugins,
@@ -185,30 +191,64 @@ const {
</div>
<v-row class="mb-4">
<v-col cols="12" class="d-flex align-center flex-wrap ga-2">
<v-btn variant="tonal" @click="toggleShowReserved">
<v-icon>{{
showReserved ? "mdi-eye-off" : "mdi-eye"
}}</v-icon>
{{
showReserved
? tm("buttons.hideSystemPlugins")
: tm("buttons.showSystemPlugins")
}}
</v-btn>
<v-col cols="12">
<div class="installed-toolbar">
<div class="installed-toolbar__actions">
<v-btn variant="tonal" @click="toggleShowReserved">
<v-icon>{{
showReserved ? "mdi-eye-off" : "mdi-eye"
}}</v-icon>
{{
showReserved
? tm("buttons.hideSystemPlugins")
: tm("buttons.showSystemPlugins")
}}
</v-btn>
<v-btn
class="ml-2"
color="warning"
variant="tonal"
:disabled="updatableExtensions.length === 0"
:loading="updatingAll"
@click="showUpdateAllConfirm"
>
<v-icon>mdi-update</v-icon>
{{ tm("buttons.updateAll") }}
</v-btn>
<v-btn
color="warning"
variant="tonal"
:disabled="updatableExtensions.length === 0"
:loading="updatingAll"
@click="showUpdateAllConfirm"
>
<v-icon>mdi-update</v-icon>
{{ tm("buttons.updateAll") }}
</v-btn>
</div>
<div class="installed-toolbar__controls">
<v-btn-toggle
v-model="installedStatusFilter"
mandatory
divided
density="compact"
color="primary"
class="installed-status-toggle"
>
<v-btn value="all" prepend-icon="mdi-filter-variant">
{{ tm("filters.all") }}
</v-btn>
<v-btn value="enabled" prepend-icon="mdi-play-circle-outline">
{{ tm("status.enabled") }}
</v-btn>
<v-btn value="disabled" prepend-icon="mdi-pause-circle-outline">
{{ tm("status.disabled") }}
</v-btn>
</v-btn-toggle>
<PluginSortControl
v-model="installedSortBy"
:items="installedSortItems"
:label="tm('sort.by')"
:order="installedSortOrder"
:ascending-label="tm('sort.ascending')"
:descending-label="tm('sort.descending')"
:show-order="installedSortUsesOrder"
@update:order="installedSortOrder = $event"
/>
</div>
</div>
</v-col>
</v-row>
@@ -654,6 +694,32 @@ const {
</template>
<style scoped>
.installed-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.installed-toolbar__actions,
.installed-toolbar__controls {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.installed-toolbar__controls {
margin-left: auto;
justify-content: flex-end;
}
.installed-status-toggle :deep(.v-btn) {
min-height: 34px;
text-transform: none;
}
.view-mode-toggle :deep(.v-btn) {
min-width: 30px;
height: 28px;
@@ -684,6 +750,14 @@ const {
}
}
@media (max-width: 960px) {
.installed-toolbar__controls {
margin-left: 0;
width: 100%;
justify-content: flex-start;
}
}
.fab-button {
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
@@ -1,5 +1,6 @@
<script setup>
import MarketPluginCard from "@/components/extension/MarketPluginCard.vue";
import PluginSortControl from "@/components/extension/PluginSortControl.vue";
import defaultPluginIcon from "@/assets/images/plugin_icon.png";
import { computed } from "vue";
@@ -157,6 +158,13 @@ const currentSourceName = computed(() => {
const matched = customSources.value.find((s) => s.url === selectedSource.value);
return matched?.name || tm("market.defaultSource");
});
const marketSortItems = computed(() => [
{ title: tm("sort.default"), value: "default" },
{ title: tm("sort.stars"), value: "stars" },
{ title: tm("sort.author"), value: "author" },
{ title: tm("sort.updated"), value: "updated" },
]);
</script>
<template>
@@ -327,44 +335,16 @@ const currentSourceName = computed(() => {
class="d-flex align-center"
style="gap: 8px; flex-wrap: wrap"
>
<v-select
<PluginSortControl
v-model="sortBy"
:items="[
{ title: tm('sort.default'), value: 'default' },
{ title: tm('sort.stars'), value: 'stars' },
{ title: tm('sort.author'), value: 'author' },
{ title: tm('sort.updated'), value: 'updated' },
]"
density="compact"
variant="outlined"
hide-details
style="max-width: 150px"
>
<template v-slot:prepend-inner>
<v-icon size="small">mdi-sort</v-icon>
</template>
</v-select>
<v-btn
icon
v-if="sortBy !== 'default'"
@click="sortOrder = sortOrder === 'desc' ? 'asc' : 'desc'"
variant="text"
density="compact"
>
<v-icon>{{
sortOrder === "desc"
? "mdi-sort-descending"
: "mdi-sort-ascending"
}}</v-icon>
<v-tooltip activator="parent" location="top">
{{
sortOrder === "desc"
? tm("sort.descending")
: tm("sort.ascending")
}}
</v-tooltip>
</v-btn>
:items="marketSortItems"
:label="tm('sort.by')"
:order="sortOrder"
:ascending-label="tm('sort.ascending')"
:descending-label="tm('sort.descending')"
:show-order="sortBy !== 'default'"
@update:order="sortOrder = $event"
/>
</div>
</div>
+125 -14
View File
@@ -186,6 +186,9 @@ export const useExtensionPage = () => {
};
const isListView = ref(getInitialListViewMode());
const pluginSearch = ref("");
const installedStatusFilter = ref("all");
const installedSortBy = ref("default");
const installedSortOrder = ref("desc");
const loading_ = ref(false);
// 分页相关
@@ -253,6 +256,18 @@ export const useExtensionPage = () => {
{ title: tm("table.headers.specificType"), key: "type" },
{ title: tm("table.headers.trigger"), key: "cmd" },
]);
const installedSortItems = computed(() => [
{ title: tm("sort.default"), value: "default" },
{ title: tm("sort.installTime"), value: "install_time" },
{ title: tm("sort.name"), value: "name" },
{ title: tm("sort.author"), value: "author" },
{ title: tm("sort.updateStatus"), value: "update_status" },
]);
const installedSortUsesOrder = computed(
() => installedSortBy.value !== "default",
);
// 插件表格的表头定义
const showAuthorColumn = computed(() => width.value >= 1280);
@@ -261,16 +276,19 @@ export const useExtensionPage = () => {
{
title: tm("table.headers.name"),
key: "name",
sortable: false,
width: showAuthorColumn.value ? "24%" : "26%",
},
{
title: tm("table.headers.description"),
key: "desc",
sortable: false,
width: showAuthorColumn.value ? "32%" : "36%",
},
{
title: tm("table.headers.version"),
key: "version",
sortable: false,
width: showAuthorColumn.value ? "12%" : "14%",
},
];
@@ -279,6 +297,7 @@ export const useExtensionPage = () => {
headers.push({
title: tm("table.headers.author"),
key: "author",
sortable: false,
width: "10%",
});
}
@@ -301,33 +320,120 @@ export const useExtensionPage = () => {
}
return data;
});
const sortPluginsByName = (plugins) => {
const compareInstalledPluginNames = (left, right) =>
normalizeStr(left?.name ?? "").localeCompare(
normalizeStr(right?.name ?? ""),
undefined,
{
sensitivity: "base",
},
);
const compareInstalledPluginAuthors = (left, right) =>
normalizeStr(left?.author ?? "").localeCompare(
normalizeStr(right?.author ?? ""),
undefined,
{ sensitivity: "base" },
);
const getInstalledAtTimestamp = (plugin) => {
const parsed = Date.parse(plugin?.installed_at ?? "");
return Number.isFinite(parsed) ? parsed : null;
};
const sortInstalledPlugins = (plugins) => {
return plugins
.map((plugin, index) => ({ plugin, index }))
.sort((a, b) => {
const nameA = String(a.plugin?.name ?? "");
const nameB = String(b.plugin?.name ?? "");
const nameCompare = nameA.localeCompare(nameB, undefined, {
sensitivity: "base",
});
if (nameCompare !== 0) {
return nameCompare;
.map((plugin, index) => ({
plugin,
index,
installedAtTimestamp: getInstalledAtTimestamp(plugin),
}))
.sort((left, right) => {
const fallbackNameCompare = compareInstalledPluginNames(
left.plugin,
right.plugin,
);
const fallbackResult =
fallbackNameCompare !== 0 ? fallbackNameCompare : left.index - right.index;
if (installedSortBy.value === "install_time") {
const leftTimestamp = left.installedAtTimestamp;
const rightTimestamp = right.installedAtTimestamp;
if (leftTimestamp == null && rightTimestamp == null) {
return fallbackResult;
}
if (leftTimestamp == null) {
return 1;
}
if (rightTimestamp == null) {
return -1;
}
const timeDiff =
installedSortOrder.value === "desc"
? rightTimestamp - leftTimestamp
: leftTimestamp - rightTimestamp;
return timeDiff !== 0 ? timeDiff : fallbackResult;
}
return a.index - b.index;
if (installedSortBy.value === "name") {
const nameCompare = compareInstalledPluginNames(left.plugin, right.plugin);
if (nameCompare !== 0) {
return installedSortOrder.value === "desc"
? -nameCompare
: nameCompare;
}
return left.index - right.index;
}
if (installedSortBy.value === "author") {
const authorCompare = compareInstalledPluginAuthors(
left.plugin,
right.plugin,
);
if (authorCompare !== 0) {
return installedSortOrder.value === "desc"
? -authorCompare
: authorCompare;
}
return fallbackResult;
}
if (installedSortBy.value === "update_status") {
const leftHasUpdate = left.plugin?.has_update ? 1 : 0;
const rightHasUpdate = right.plugin?.has_update ? 1 : 0;
const updateDiff =
installedSortOrder.value === "desc"
? rightHasUpdate - leftHasUpdate
: leftHasUpdate - rightHasUpdate;
return updateDiff !== 0 ? updateDiff : fallbackResult;
}
return fallbackResult;
})
.map((item) => item.plugin);
};
// 通过搜索过滤插件
const filteredPlugins = computed(() => {
const plugins = filteredExtensions.value;
const plugins = filteredExtensions.value.filter((plugin) => {
if (installedStatusFilter.value === "enabled") {
return !!plugin.activated;
}
if (installedStatusFilter.value === "disabled") {
return !plugin.activated;
}
return true;
});
const query = buildSearchQuery(pluginSearch.value);
const filtered = query
? plugins.filter((plugin) => matchesPluginSearch(plugin, query))
: plugins;
return sortPluginsByName([...filtered]);
return sortInstalledPlugins(filtered);
});
// 过滤后的插件市场数据(带搜索)
@@ -1481,6 +1587,9 @@ export const useExtensionPage = () => {
getInitialListViewMode,
isListView,
pluginSearch,
installedStatusFilter,
installedSortBy,
installedSortOrder,
loading_,
currentPage,
dangerConfirmDialog,
@@ -1516,6 +1625,8 @@ export const useExtensionPage = () => {
toPinyinText,
toInitials,
plugin_handler_info_headers,
installedSortItems,
installedSortUsesOrder,
pluginHeaders,
filteredExtensions,
filteredPlugins,
+6
View File
@@ -0,0 +1,6 @@
__pycache__/
venv/
.DS_Store
node_modules/
.vitepress/cache
*dist
+530
View File
@@ -0,0 +1,530 @@
import { defineConfig } from "vitepress";
import { head } from "./config/head";
// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "AstrBot",
description: "AstrBot",
head: head,
rewrites: {
'zh/:rest*': ':rest*'
},
sitemap: {
hostname: "https://docs.astrbot.app",
},
lastUpdated: true,
ignoreDeadLinks: true,
locales: {
root: {
label: "简体中文",
lang: "zh-Hans",
themeConfig: {
nav: [
{ text: "主页", link: "https://astrbot.app" },
{ text: "博客", link: "https://blog.astrbot.app" },
{ text: "路线图", link: "https://astrbot.featurebase.app/roadmap" },
{ text: "HTTP API", link: "https://docs.astrbot.app/scalar.html" },
],
sidebar: [
{
text: "简介",
items: [
{ text: "关于 AstrBot", link: "/what-is-astrbot" },
{ text: "社区", link: "/community" },
{ text: "常见问题", link: "/faq" },
],
},
{
text: "部署",
base: "/deploy",
collapsed: false,
items: [
{ text: "包管理器部署", link: "/astrbot/package" },
{ text: "雨云一键云部署", link: "/astrbot/rainyun" },
{ text: "桌面客户端部署", link: "/astrbot/desktop" },
{ text: "启动器一键部署", link: "/astrbot/launcher" },
{ text: "Docker 部署", link: "/astrbot/docker" },
{ text: "Kubernetes 部署", link: "/astrbot/kubernetes" },
{ text: "宝塔面板部署", link: "/astrbot/btpanel" },
{ text: "1Panel 部署", link: "/astrbot/1panel" },
{ text: "手动部署", link: "/astrbot/cli" },
{
text: "其他部署方式",
link: "/astrbot/other-deployments",
collapsed: true,
items: [
{ text: "CasaOS 部署", link: "/astrbot/casaos" },
{ text: "优云智算 GPU 部署", link: "/astrbot/compshare" },
{ text: "社区提供的部署方式", link: "/astrbot/community-deployment" },
],
},
{
text: "支持我们",
link: "/when-deployed",
},
],
},
{
text: "接入消息平台",
base: "/platform",
items: [
{
text: "快速接入指南",
link: "/start",
},
{
text: "QQ 官方机器人",
link: "/qqofficial",
collapsed: true,
items: [
{ text: "Websockets 方式(推荐)", link: "/qqofficial/websockets" },
{ text: "Webhook 方式", link: "/qqofficial/webhook" },
],
},
{
text: "OneBot v11",
base: "/platform/aiocqhttp",
collapsed: true,
items: [
{ text: "NapCat", link: "/napcat" },
{ text: "Lagrange", link: "/lagrange" },
{ text: "其他端", link: "/others" },
],
},
{ text: "企微应用", link: "/wecom" },
{ text: "企微智能机器人", link: "/wecom_ai_bot" },
{ text: "微信公众号", link: "/weixin-official-account" },
{ text: "飞书", link: "/lark" },
{ text: "钉钉", link: "/dingtalk" },
{ text: "Telegram", link: "/telegram" },
{ text: "LINE", link: "/line" },
{ text: "Slack", link: "/slack" },
{ text: "Misskey", link: "/misskey" },
{ text: "Discord", link: "/discord" },
{ text: "KOOK", link: "/kook" },
{
text: "Satori",
base: "/platform/satori",
collapsed: true,
items: [
{ text: "使用 LLOneBot", link: "/llonebot" },
{ text: "使用 server-satori", link: "/server-satori" },
],
},
{
text: "社区提供",
collapsed: false,
items: [
{ text: "Matrix", link: "/matrix" },
{ text: "VoceChat", link: "/vocechat" },
],
},
],
},
{
text: "接入 AI",
base: "/providers",
items: [
{
text: "✨ 接入模型服务",
link: "/start",
collapsed: true,
items: [
{ text: "NewAPI", link: "/newapi" },
{ text: "AIHubMix", link: "/aihubmix" },
{ text: "PPIO 派欧云", link: "/ppio" },
{ text: "硅基流动", link: "/siliconflow" },
{ text: "小马算力", link: "/tokenpony" },
{ text: "302.AI", link: "/302ai" },
{ text: "Ollama", link: "/provider-ollama" },
{ text: "LMStudio", link: "/provider-lmstudio" },
]
},
{
text: "⚙️ Agent 执行器",
link: "/agent-runners",
collapsed: false,
items: [
{ text: "内置 Agent 执行器", link: "/agent-runners/astrbot-agent-runner" },
{ text: "Dify", link: "/agent-runners/dify" },
{ text: "扣子 Coze", link: "/agent-runners/coze" },
{ text: "阿里云百炼应用", link: "/agent-runners/dashscope" },
{ text: "DeerFlow", link: "/agent-runners/deerflow" },
]
},
],
},
{
text: "使用",
base: "/use",
items: [
{ text: "WebUI", link: "/webui" },
{ text: "插件", link: "/plugin" },
{ text: "内置指令", link: "/command" },
{ text: "工具使用 Tools", link: "/function-calling" },
{ text: "技能 Skills", link: "/skills" },
{ text: "SubAgent 编排", link: "/subagent" },
{ text: "主动型 Agent 能力", link: "/proactive-agent" },
{ text: "MCP", link: "/mcp" },
{ text: "网页搜索", link: "/websearch" },
{ text: "知识库", link: "/knowledge-base" },
{ text: "自定义规则", link: "/custom-rules" },
{ text: "Agent 执行器", link: "/agent-runner" },
{ text: "统一 Webhook 模式", link: "/unified-webhook" },
{ text: "自动上下文压缩", link: "/context-compress" },
{ text: "Agent 沙箱环境", link: "/astrbot-agent-sandbox" },
],
},
{
text: "开发",
base: "/dev",
collapsed: true,
items: [
{
text: "插件开发",
base: "/dev/star",
collapsed: true,
items: [
{ text: "🌠 从这里开始", link: "/plugin-new" },
{ text: "最小实例", link: "/guides/simple" },
{ text: "接收消息事件", link: "/guides/listen-message-event" },
{ text: "发送消息", link: "/guides/send-message" },
{ text: "插件配置", link: "/guides/plugin-config" },
{ text: "调用 AI", link: "/guides/ai" },
{ text: "存储", link: "/guides/storage" },
{ text: "文转图", link: "/guides/html-to-pic" },
{ text: "会话控制器", link: "/guides/session-control" },
{ text: "杂项", link: "/guides/other" },
{ text: "发布插件", link: "/plugin-publish" },
{ text: "插件指南(旧)", link: "/plugin" },
],
},
{
text: "接入平台适配器",
link: "/plugin-platform-adapter",
},
{
text: "AstrBot HTTP API",
link: "/openapi",
},
{
text: "AstrBot 配置文件",
link: "/astrbot-config",
},
],
},
{
text: "其他",
base: "/others",
collapsed: true,
items: [
{ text: "自部署文转图", link: "/self-host-t2i" },
{ text: "插件下载不了?试试自建 GitHub 加速服务", link: "/github-proxy" },
],
},
{
text: "开源之夏",
base: "/ospp",
collapsed: true,
items: [{ text: "OSPP 2025", link: "/2025" }],
},
],
outline: {
level: 'deep',
label: '目录',
},
darkModeSwitchLabel: '切换日光/暗黑模式',
sidebarMenuLabel: '文章',
returnToTopLabel: '返回顶部',
docFooter: {
prev: '上一篇',
next: '下一篇'
},
editLink: {
pattern: 'https://github.com/AstrBotdevs/AstrBot-docs/edit/v4/:path',
text: '发现文档有问题?在 GitHub 上编辑此页',
},
logo: '/logo_prod.png',
socialLinks: [
{ icon: "github", link: "https://github.com/AstrBotDevs/AstrBot" },
],
footer: {
message: 'Deployed on&nbsp' +
'<a href="https://www.rainyun.com/NjY3OTQ5_" class="deployment-link" style="display: inline-flex; align-items: center;">' +
'<img src="https://www.rainyun.com/img/logo.d193755d.png" width="50" alt="Rainyun Logo">' +
'</a>',
}
}
},
en: {
label: "English",
lang: "en-US",
themeConfig: {
nav: [
{ text: "Home", link: "https://astrbot.app" },
{ text: "Blog", link: "https://blog.astrbot.app" },
{ text: "Roadmap", link: "https://astrbot.featurebase.app/roadmap" },
{ text: "HTTP API", link: "https://docs.astrbot.app/scalar.html" },
],
sidebar: [
{
text: "Introduction",
items: [
{ text: "What is AstrBot", link: "/en/what-is-astrbot" },
{ text: "Community", link: "/en/community" },
{ text: "FAQ", link: "/en/faq" },
],
},
{
text: "Deployment",
base: "/en/deploy",
collapsed: false,
items: [
{ text: "Package Manager", link: "/astrbot/package" },
{ text: "One-click Launcher", link: "/astrbot/launcher" },
{ text: "Docker", link: "/astrbot/docker" },
{ text: "Kubernetes", link: "/astrbot/kubernetes" },
{ text: "BT Panel", link: "/astrbot/btpanel" },
{ text: "1Panel", link: "/astrbot/1panel" },
{ text: "Manual", link: "/astrbot/cli" },
{
text: "Other Deployments",
link: "/astrbot/other-deployments",
collapsed: true,
items: [
{ text: "CasaOS", link: "/astrbot/casaos" },
{ text: "Compshare GPU", link: "/astrbot/compshare" },
{ text: "Community-provided Deployment", link: "/astrbot/community-deployment" },
],
},
{
text: "Support Us",
link: "/when-deployed",
},
],
},
{
text: "Messaging Platforms",
base: "/en/platform",
collapsed: false,
items: [
{
text: "Quick Start",
link: "/start",
},
{
text: "QQ Official Bot",
link: "/qqofficial",
collapsed: true,
items: [
{ text: "Websockets", link: "/qqofficial/websockets" },
{ text: "Webhook", link: "/qqofficial/webhook" },
],
},
{
text: "OneBot v11",
base: "/en/platform/aiocqhttp",
collapsed: true,
items: [
{ text: "NapCat", link: "/napcat" },
{ text: "Lagrange", link: "/lagrange" },
{ text: "Other Clients", link: "/others" },
],
},
{ text: "WeCom Application", link: "/wecom" },
{ text: "WeCom AI Bot", link: "/wecom_ai_bot" },
{ text: "WeChat Official Account", link: "/weixin-official-account" },
{ text: "Lark", link: "/lark" },
{ text: "DingTalk", link: "/dingtalk" },
{ text: "Telegram", link: "/telegram" },
{ text: "LINE", link: "/line" },
{ text: "Slack", link: "/slack" },
{ text: "Misskey", link: "/misskey" },
{ text: "Discord", link: "/discord" },
{
text: "Satori",
base: "/en/platform/satori",
collapsed: true,
items: [
{ text: "Using LLOneBot", link: "/llonebot" },
{ text: "Using server-satori", link: "/server-satori" },
],
},
{
text: "Community-provided",
collapsed: false,
items: [
{ text: "Matrix", link: "/matrix" },
{ text: "KOOK", link: "/kook" },
{ text: "VoceChat", link: "/vocechat" },
],
},
],
},
{
text: "AI Integration",
base: "/en/providers",
collapsed: false,
items: [
{
text: "✨ Model Providers",
link: "/start",
collapsed: true,
items: [
{ text: "NewAPI", link: "/newapi" },
{ text: "AIHubMix", link: "/aihubmix" },
{ text: "PPIO Cloud", link: "/ppio" },
{ text: "SiliconFlow", link: "/siliconflow" },
{ text: "TokenPony", link: "/tokenpony" },
{ text: "302.AI", link: "/302ai" },
{ text: "Ollama", link: "/provider-ollama" },
{ text: "LMStudio", link: "/provider-lmstudio" },
],
},
{
text: "⚙️ Agent Runners",
link: "/agent-runners",
collapsed: false,
items: [
{ text: "Built-in Agent Runner", link: "/agent-runners/astrbot-agent-runner" },
{ text: "Dify", link: "/agent-runners/dify" },
{ text: "Coze", link: "/agent-runners/coze" },
{ text: "Alibaba Bailian", link: "/agent-runners/dashscope" },
{ text: "DeerFlow", link: "/agent-runners/deerflow" },
],
},
],
},
{
text: "Usage",
base: "/en/use",
collapsed: true,
items: [
{ text: "WebUI", link: "/webui" },
{ text: "Plugins", link: "/plugin" },
{ text: "Built-in Commands", link: "/command" },
{ text: "Tool Use", link: "/function-calling" },
{ text: "Anthropic Skills", link: "/skills" },
{ text: "SubAgent Orchestration", link: "/subagent" },
{ text: "Proactive Tasks", link: "/proactive-agent" },
{ text: "MCP", link: "/mcp" },
{ text: "Web Search", link: "/websearch" },
{ text: "Knowledge Base", link: "/knowledge-base" },
{ text: "Custom Rules", link: "/custom-rules" },
{ text: "Agent Runner", link: "/agent-runner" },
{ text: "Unified Webhook Mode", link: "/unified-webhook" },
{ text: "Auto Context Compression", link: "/context-compress" },
{ text: "Agent Sandbox", link: "/astrbot-agent-sandbox" },
],
},
{
text: "Development",
base: "/en/dev",
collapsed: true,
items: [
{
text: "Plugin Development",
base: "/en/dev/star",
collapsed: true,
items: [
{ text: "🌠 Getting Started", link: "/plugin-new" },
{ text: "Minimal Example", link: "/guides/simple" },
{ text: "Listen to Message Events", link: "/guides/listen-message-event" },
{ text: "Send Messages", link: "/guides/send-message" },
{ text: "Plugin Configuration", link: "/guides/plugin-config" },
{ text: "AI", link: "/guides/ai" },
{ text: "Storage", link: "/guides/storage" },
{ text: "HTML to Image", link: "/guides/html-to-pic" },
{ text: "Session Control", link: "/guides/session-control" },
{ text: "Publish Plugin", link: "/plugin-publish" },
],
},
{
text: "Platform Adapter Integration",
link: "/plugin-platform-adapter",
},
{
text: "AstrBot HTTP API",
link: "/openapi",
},
{
text: "AstrBot Configuration File",
link: "/astrbot-config",
},
],
},
{
text: "Others",
base: "/en/others",
collapsed: true,
items: [
{ text: "Self-hosted HTML to Image", link: "/self-host-t2i" },
],
},
{
text: "Open Source Summer",
base: "/en/ospp",
collapsed: true,
items: [{ text: "OSPP 2025", link: "/2025" }],
},
],
outline: {
level: 'deep',
label: 'On this page',
},
darkModeSwitchLabel: 'Toggle dark mode',
sidebarMenuLabel: 'Menu',
returnToTopLabel: 'Return to top',
docFooter: {
prev: 'Previous',
next: 'Next'
},
editLink: {
pattern: 'https://github.com/AstrBotdevs/AstrBot-docs/edit/v4/:path',
text: 'Edit this page on GitHub',
},
logo: '/logo_prod.png',
socialLinks: [
{ icon: "github", link: "https://github.com/AstrBotDevs/AstrBot" },
],
footer: {
message: 'Deployed on&nbsp' +
'<a href="https://www.rainyun.com/NjY3OTQ5_" class="deployment-link" style="display: inline-flex; align-items: center;">' +
'<img src="https://www.rainyun.com/img/logo.d193755d.png" width="50" alt="Rainyun Logo">' +
'</a>',
}
}
},
},
themeConfig: {
search: {
provider: "local",
options: {
locales: {
root: {
translations: {
button: {
buttonText: "搜索文档",
buttonAriaLabel: "搜索文档",
},
modal: {
noResultsText: "无法找到相关结果",
resetButtonTitle: "清除查询条件",
footer: {
selectText: "选择",
navigateText: "切换",
closeText: "关闭",
},
},
},
},
},
},
},
}
});
+47
View File
@@ -0,0 +1,47 @@
import type { HeadConfig } from "vitepress";
export const head: HeadConfig[] = [
// --- Google Fonts ---
["link", { rel: "preconnect", href: "https://fonts.googleapis.cn", crossorigin: "" }],
["link", { rel: "dns-prefetch", href: "https://fonts.googleapis.cn" }],
["link", { rel: "preconnect", href: "https://fonts.gstatic.cn", crossorigin: "" }],
["link", { rel: "dns-prefetch", href: "https://fonts.gstatic.cn" }],
["link", { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap" }],
// --- 基础和SEO元数据 ---
["link", { rel: "icon", href: "/logo.png" }],
["meta", { name: "description", content: "AstrBot" }],
[
"meta",
{ name: "viewport", content: "width=device-width, initial-scale=1.0" },
],
/* // --- Open Graph (OG) () ---
["meta", { property: "og:type", content: "website" }],
["meta", { property: "og:locale", content: "zh_CN" }],
["meta", { property: "og:title", content: "AstrBot" }],
["meta", { property: "og:description", content: "AstrBot" }],
["meta", { property: "og:url", content: "https://docs.astrbot.app" }],
["meta", { property: "og:site_name", content: "AstrBot" }],
[
"meta",
{
property: "og:image",
content: "/",
},
],
[
"meta",
{ property: "og:image:alt", content: "AstrBot" },
],
["meta", { property: "og:image:width", content: "1200" }],
["meta", { property: "og:image:height", content: "630" }],
["meta", { property: "og:image:type", content: "image/png" }],
// --- Twitter Card 元数据 ---
["meta", { name: "twitter:card", content: "summary_large_image" }],
["meta", { name: "twitter:site", content: "@AstrBot" }],*/
// --- Umami Analytics ---
["script", { defer: "", src: "https://cloud.umami.is/script.js", "data-website-id": "9c3f777e-9f4a-4b79-a5c3-ff94f5dca8f9" }],
];
@@ -0,0 +1,194 @@
<script setup lang="ts">
import { ref, computed, onMounted } from "vue"
const props = defineProps({
shareText: {
type: String,
default: "分享链接",
},
copiedText: {
type: String,
default: "已复制!",
},
includeQuery: {
type: Boolean,
default: false,
},
includeHash: {
type: Boolean,
default: false,
},
copiedTimeout: {
type: Number,
default: 2000,
},
})
defineOptions({ name: "ArticleShare" })
const copied = ref(false)
const isClient =
typeof window !== "undefined" && typeof document !== "undefined"
const shareLink = computed(() => {
if (!isClient) return ""
const { origin, pathname, search, hash } = window.location
const finalSearch = props.includeQuery ? search : ""
const finalHash = props.includeHash ? hash : ""
return `${origin}${pathname}${finalSearch}${finalHash}`
})
async function copyToClipboard() {
if (copied.value || !isClient) return
try {
if (navigator.clipboard) {
await navigator.clipboard.writeText(shareLink.value)
} else {
const input = document.createElement("input")
input.setAttribute("readonly", "readonly")
input.setAttribute("value", shareLink.value)
document.body.appendChild(input)
input.select()
document.execCommand("copy")
document.body.removeChild(input)
}
copied.value = true
setTimeout(() => {
copied.value = false
}, props.copiedTimeout)
} catch (error) {
console.error("复制链接失败:", error)
}
}
const shareIconSvg = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path>
<polyline points="16 6 12 2 8 6"></polyline>
<line x1="12" y1="2" x2="12" y2="15"></line>
</svg>
`
const copiedIconSvg = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 6 9 17l-5-5"></path>
</svg>
`
// onMounted(() => {
// const script = document.createElement('script')
// script.src = 'https://cdn.wwads.cn/js/makemoney.js'
// script.async = true
// document.head.appendChild(script)
// })
</script>
<template>
<div style="display: flex; justify-content: center; align-items: center; flex-direction: column;">
<div class="article-share">
<button :class="['article-share__button', { copied: copied }]"
:aria-label="copied ? props.copiedText : props.shareText" aria-live="polite" @click="copyToClipboard">
<div v-if="!copied" class="content-wrapper">
<span class="icon" v-html="shareIconSvg"></span>
{{ props.shareText }}
</div>
<div v-else class="content-wrapper">
<span class="icon" v-html="copiedIconSvg"></span>
{{ props.copiedText }}
</div>
</button>
</div>
<!-- <div class="wwads-cn wwads-vertical sponsors" data-id="380" style="max-width:180px"></div> -->
</div>
</template>
<style scoped>
.article-share {
padding: 14px 0;
width: 100%;
}
.article-share__button {
display: flex;
justify-content: center;
align-items: center;
font-weight: 500;
font-size: 14px;
position: relative;
z-index: 1;
transition: all 0.4s var(--ease-out-cubic, cubic-bezier(0.33, 1, 0.68, 1));
cursor: pointer;
border: 1px solid transparent;
border-radius: 14px;
padding: 7px 14px;
width: 100%;
overflow: hidden;
color: var(--vp-c-text-1, #333);
background-color: var(--vp-c-bg-alt, #f6f6f7);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.02);
will-change: transform, box-shadow;
}
.article-share__button::before {
content: "";
position: absolute;
top: 0;
left: -100%;
z-index: -1;
transition: left 0.6s ease;
background-color: var(--vp-c-brand-soft, #ddf4ff);
width: 100%;
height: 100%;
}
.article-share__button:hover {
transform: translateY(-1px);
border-color: var(--vp-c-brand-soft, #ddf4ff);
background-color: var(--vp-c-brand-soft, #ddf4ff);
}
.article-share__button:active {
transform: scale(0.9);
}
.article-share__button.copied {
color: var(--vp-c-brand-1, #007acc);
/* 增加了备用颜色 */
background-color: var(--vp-c-brand-soft, #ddf4ff);
}
.article-share__button.copied::before {
left: 0;
background-color: var(--vp-c-brand-soft, #ddf4ff);
}
.content-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
.icon {
display: inline-flex;
align-items: center;
margin-right: 6px;
}
.sponsors {
max-width: 100%;
margin: 0 !important;
background-color: transparent !important;
}
.sponsors .wwads-text {
color: var(--vp-c-text-1) !important;
transition-property: color;
transition-duration: 500ms;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}
</style>
@@ -0,0 +1,7 @@
<template>
<div style="display: flex; justify-content: center; align-items: center; margin-top: 16px; gap: 12px;">
<span style="font-size: 13px; color: #666; font-style: italic;;">Deployed on</span>
<a href="https://www.rainyun.com/NjY3OTQ1_"><img src="https://www.rainyun.com/img/logo.d193755d.png" width="50" alt="Rainyun Logo"></a>
</div>
</template>
+131
View File
@@ -0,0 +1,131 @@
<script setup>
import { useRoute } from 'vitepress'
import { computed, provide, useSlots, watch } from 'vue'
import VPBackdrop from 'vitepress/dist/client/theme-default/components/VPBackdrop.vue'
import VPContent from 'vitepress/dist/client/theme-default/components/VPContent.vue'
import VPFooter from 'vitepress/dist/client/theme-default/components/VPFooter.vue'
import VPLocalNav from 'vitepress/dist/client/theme-default/components/VPLocalNav.vue'
import VPNav from 'vitepress/dist/client/theme-default/components/VPNav.vue'
import VPSidebar from 'vitepress/dist/client/theme-default/components/VPSidebar.vue'
import VPSkipLink from 'vitepress/dist/client/theme-default/components/VPSkipLink.vue'
import { useData } from 'vitepress/dist/client/theme-default/composables/data'
import { useCloseSidebarOnEscape, useSidebar } from 'vitepress/dist/client/theme-default/composables/sidebar'
import SectionTabs from './SectionTabs.vue'
const {
isOpen: isSidebarOpen,
open: openSidebar,
close: closeSidebar
} = useSidebar()
const route = useRoute()
watch(() => route.path, closeSidebar)
useCloseSidebarOnEscape(isSidebarOpen, closeSidebar)
const { frontmatter } = useData()
const sidebarScopeClass = computed(() => {
const path = route.path
const normalizedPath = path
.replace(/\.html$/, '')
.replace(/\/$/, '') || '/'
if (
normalizedPath === '/what-is-astrbot' || normalizedPath === '/community' || normalizedPath === '/faq'
|| path.startsWith('/deploy/') || path.startsWith('/others/') || path.startsWith('/ospp/')
|| normalizedPath === '/en/what-is-astrbot' || normalizedPath === '/en/community' || normalizedPath === '/en/faq'
|| path.startsWith('/en/deploy/') || path.startsWith('/en/others/') || path.startsWith('/en/ospp/')
)
return 'sidebar-scope-intro-deploy'
if (path.startsWith('/platform/') || path.startsWith('/en/platform/'))
return 'sidebar-scope-platform'
if (path.startsWith('/providers/') || path.startsWith('/en/providers/'))
return 'sidebar-scope-providers'
if (path.startsWith('/use/') || path.startsWith('/en/use/'))
return 'sidebar-scope-use'
if (path.startsWith('/dev/') || path.startsWith('/en/dev/'))
return 'sidebar-scope-dev'
return ''
})
const slots = useSlots()
const heroImageSlotExists = computed(() => !!slots['home-hero-image'])
provide('hero-image-slot-exists', heroImageSlotExists)
</script>
<template>
<div
v-if="frontmatter.layout !== false"
class="Layout"
:class="[frontmatter.pageClass, sidebarScopeClass]"
>
<slot name="layout-top" />
<VPSkipLink />
<VPBackdrop class="backdrop" :show="isSidebarOpen" @click="closeSidebar" />
<VPNav>
<template #nav-bar-title-before><slot name="nav-bar-title-before" /></template>
<template #nav-bar-title-after><slot name="nav-bar-title-after" /></template>
<template #nav-bar-content-before><slot name="nav-bar-content-before" /></template>
<template #nav-bar-content-after><slot name="nav-bar-content-after" /></template>
<template #nav-screen-content-before><slot name="nav-screen-content-before" /></template>
<template #nav-screen-content-after><slot name="nav-screen-content-after" /></template>
</VPNav>
<SectionTabs />
<VPLocalNav :open="isSidebarOpen" @open-menu="openSidebar" />
<VPSidebar :open="isSidebarOpen">
<template #sidebar-nav-before><slot name="sidebar-nav-before" /></template>
<template #sidebar-nav-after><slot name="sidebar-nav-after" /></template>
</VPSidebar>
<VPContent>
<template #page-top><slot name="page-top" /></template>
<template #page-bottom><slot name="page-bottom" /></template>
<template #not-found><slot name="not-found" /></template>
<template #home-hero-before><slot name="home-hero-before" /></template>
<template #home-hero-info-before><slot name="home-hero-info-before" /></template>
<template #home-hero-info><slot name="home-hero-info" /></template>
<template #home-hero-info-after><slot name="home-hero-info-after" /></template>
<template #home-hero-actions-after><slot name="home-hero-actions-after" /></template>
<template #home-hero-image><slot name="home-hero-image" /></template>
<template #home-hero-after><slot name="home-hero-after" /></template>
<template #home-features-before><slot name="home-features-before" /></template>
<template #home-features-after><slot name="home-features-after" /></template>
<template #doc-footer-before><slot name="doc-footer-before" /></template>
<template #doc-before><slot name="doc-before" /></template>
<template #doc-after><slot name="doc-after" /></template>
<template #doc-top><slot name="doc-top" /></template>
<template #doc-bottom><slot name="doc-bottom" /></template>
<template #aside-top><slot name="aside-top" /></template>
<template #aside-bottom><slot name="aside-bottom" /></template>
<template #aside-outline-before><slot name="aside-outline-before" /></template>
<template #aside-outline-after><slot name="aside-outline-after" /></template>
<template #aside-ads-before><slot name="aside-ads-before" /></template>
<template #aside-ads-after><slot name="aside-ads-after" /></template>
</VPContent>
<VPFooter />
<slot name="layout-bottom" />
</div>
<Content v-else />
</template>
<style scoped>
.Layout {
display: flex;
flex-direction: column;
min-height: 100vh;
}
</style>
@@ -0,0 +1,73 @@
<script setup>
import { useRouter } from 'vitepress'
const router = useRouter()
const goHome = () => {
router.go('/')
}
</script>
<template>
<div class="NotFound">
<img src="/404-seio.png" alt="404 Not Found" class="not-found-image" />
<h1 class="not-found-title">😢 你来到了未知的领域页面不存在</h1>
<p class="not-found-desc">请点击左上角 Logo 返回首页或点击下方按钮</p>
<button @click="goHome" class="not-found-button">返回首页</button>
</div>
</template>
<style scoped>
.NotFound {
padding: 4rem 2rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
}
.not-found-image {
max-width: 400px;
width: 100%;
margin-bottom: 2rem;
}
.not-found-title {
font-size: 1.5rem;
margin-bottom: 1rem;
color: var(--vp-c-text-1);
}
.not-found-desc {
font-size: 1rem;
margin-bottom: 2rem;
color: var(--vp-c-text-2);
}
.not-found-button {
padding: 0.75rem 1.5rem;
font-size: 1rem;
color: #fff;
background-color: var(--vp-c-brand-1);
border: none;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s;
}
.not-found-button:hover {
background-color: var(--vp-c-brand-2);
}
@media (max-width: 768px) {
.not-found-image {
max-width: 300px;
}
.not-found-title {
font-size: 1.25rem;
}
}
</style>
@@ -0,0 +1,121 @@
<script setup>
import { computed } from 'vue'
import { useData, useRoute } from 'vitepress'
const route = useRoute()
const { frontmatter } = useData()
const isEnglish = computed(() => route.path.startsWith('/en/'))
const zhTabs = [
{
text: '简介和部署',
link: '/what-is-astrbot',
matchers: ['/what-is-astrbot', '/community', '/faq', '/deploy/', '/others/', '/ospp/']
},
{ text: '接入消息平台', link: '/platform/start', matchers: ['/platform/'] },
{ text: '接入 AI', link: '/providers/start', matchers: ['/providers/'] },
{ text: '使用', link: '/use/webui', matchers: ['/use/'] },
{ text: '开发', link: '/dev/star/plugin-new', matchers: ['/dev/'] }
]
const enTabs = [
{
text: 'Intro & Deploy',
link: '/en/what-is-astrbot',
matchers: ['/en/what-is-astrbot', '/en/community', '/en/faq', '/en/deploy/', '/en/others/', '/en/ospp/']
},
{ text: 'Messaging Platforms', link: '/en/platform/start', matchers: ['/en/platform/'] },
{ text: 'AI Integration', link: '/en/providers/start', matchers: ['/en/providers/'] },
{ text: 'Usage', link: '/en/use/webui', matchers: ['/en/use/'] },
{ text: 'Development', link: '/en/dev/star/plugin-new', matchers: ['/en/dev/'] }
]
const tabs = computed(() => (isEnglish.value ? enTabs : zhTabs))
const isHome = computed(() => route.path === '/' || route.path === '/en/')
const shouldShow = computed(() => frontmatter.value.layout !== false && frontmatter.value.layout !== 'home' && !isHome.value)
function isActive(tab) {
return tab.matchers.some(prefix => route.path.startsWith(prefix))
}
</script>
<template>
<template v-if="shouldShow">
<div class="VPSectionTabsPlaceholder" aria-hidden="true" />
<div class="VPSectionTabs">
<div class="container">
<a
v-for="tab in tabs"
:key="tab.link"
class="tab"
:class="{ active: isActive(tab) }"
:href="tab.link"
>
{{ tab.text }}
</a>
</div>
</div>
</template>
</template>
<style scoped>
.VPSectionTabs {
display: none;
}
.VPSectionTabsPlaceholder {
display: none;
}
@media (min-width: 1280px) {
.VPSectionTabsPlaceholder {
display: block;
height: var(--vp-section-tabs-height, 44px);
}
.VPSectionTabs {
display: block;
position: fixed;
left: 0;
right: 0;
top: calc(var(--vp-layout-top-height, 0px) + var(--vp-nav-height));
z-index: 26;
border-bottom: 1px solid var(--vp-c-gutter);
background-color: var(--vp-nav-bg-color);
}
.container {
margin: 0 auto;
max-width: var(--vp-layout-max-width);
display: flex;
align-items: flex-end;
gap: 10px;
box-sizing: border-box;
height: var(--vp-section-tabs-height, 44px);
padding: 0 32px 8px;
}
.tab {
border-radius: 999px;
padding: 6px 12px;
font-size: 13px;
line-height: 20px;
color: var(--vp-c-text-2);
white-space: nowrap;
transition: color 0.2s ease, background-color 0.2s ease;
}
.tab:hover {
color: var(--vp-c-text-1);
background-color: var(--vp-c-default-soft);
}
.tab.active {
color: var(--vp-c-brand-1);
background-color: var(--vp-c-brand-soft);
}
}
</style>
+21
View File
@@ -0,0 +1,21 @@
// https://vitepress.dev/guide/custom-theme
import { h } from 'vue'
import DefaultTheme from 'vitepress/theme'
import './styles/style.css'
import './styles/custom-block.css'
import './styles/font.css'
import Layout from './components/Layout.vue'
import ArticleShare from './components/ArticleShare.vue'
import NotFound from './components/NotFound.vue'
/** @type {import('vitepress').Theme} */
export default {
extends: DefaultTheme,
Layout() {
return h(Layout, null, {
// https://vitepress.dev/guide/extending-default-theme#layout-slots
'aside-outline-after': () => h(ArticleShare),
'not-found': () => h(NotFound)
})
}
}
@@ -0,0 +1,185 @@
/* .vitepress/theme/style/custom-block.css */
/* 深浅色卡 */
:root {
--custom-block-info-left: #cccccc;
--custom-block-info-bg: #fafafa;
--custom-block-tip-left: #009400;
--custom-block-tip-bg: #b6dcc7;
--custom-block-warning-left: #e6a700;
--custom-block-warning-bg: #ffe69d;
--custom-block-danger-left: #e13238;
--custom-block-danger-bg: #ffebec;
--custom-block-note-left: #4cb3d4;
--custom-block-note-bg: #d6eff7;
--custom-block-important-left: #a371f7;
--custom-block-important-bg: #f4eefe;
--custom-block-caution-left: #e0575b;
--custom-block-caution-bg: #fde4e8;
}
.dark {
--custom-block-info-left: #cccccc;
--custom-block-info-bg: #474748;
--custom-block-tip-left: #009400;
--custom-block-tip-bg: #003100;
--custom-block-warning-left: #e6a700;
--custom-block-warning-bg: #4d3800;
--custom-block-danger-left: #e13238;
--custom-block-danger-bg: #4b1113;
--custom-block-note-left: #4cb3d4;
--custom-block-note-bg: #193c47;
--custom-block-important-left: #a371f7;
--custom-block-important-bg: #230555;
--custom-block-caution-left: #e0575b;
--custom-block-caution-bg: #391c22;
}
/* 标题字体大小 */
.custom-block-title {
font-size: 16px;
}
/* info容器:背景色、左侧 */
.custom-block.info {
background-color: var(--custom-block-info-bg);
}
/* info容器:svg图 */
.custom-block.info [class*="custom-block-title"]::before {
content: '';
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z' fill='%23ccc'/%3E%3C/svg%3E");
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
position: relative;
margin-right: 4px;
left: -5px;
top: -1px;
}
/* 提示容器:边框色、背景色、左侧 */
.custom-block.tip {
background-color: var(--custom-block-tip-bg);
}
/* 提示容器:svg图 */
.custom-block.tip [class*="custom-block-title"]::before {
content: '';
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23009400' d='M7.941 18c-.297-1.273-1.637-2.314-2.187-3a8 8 0 1 1 12.49.002c-.55.685-1.888 1.726-2.185 2.998H7.94zM16 20v1a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-1h8zm-3-9.995V6l-4.5 6.005H11v4l4.5-6H13z'/%3E%3C/svg%3E");
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
position: relative;
margin-right: 4px;
left: -5px;
top: -2px;
}
/* 警告容器:背景色、左侧 */
.custom-block.warning {
background-color: var(--custom-block-warning-bg);
}
/* 警告容器:svg图 */
.custom-block.warning [class*="custom-block-title"]::before {
content: '';
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M576.286 752.57v-95.425q0-7.031-4.771-11.802t-11.3-4.772h-96.43q-6.528 0-11.3 4.772t-4.77 11.802v95.424q0 7.031 4.77 11.803t11.3 4.77h96.43q6.528 0 11.3-4.77t4.77-11.803zm-1.005-187.836 9.04-230.524q0-6.027-5.022-9.543-6.529-5.524-12.053-5.524H456.754q-5.524 0-12.053 5.524-5.022 3.516-5.022 10.547l8.538 229.52q0 5.023 5.022 8.287t12.053 3.265h92.913q7.032 0 11.803-3.265t5.273-8.287zM568.25 95.65l385.714 707.142q17.578 31.641-1.004 63.282-8.538 14.564-23.354 23.102t-31.892 8.538H126.286q-17.076 0-31.892-8.538T71.04 866.074q-18.582-31.641-1.004-63.282L455.75 95.65q8.538-15.57 23.605-24.61T512 62t32.645 9.04 23.605 24.61z' fill='%23e6a700'/%3E%3C/svg%3E");
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
position: relative;
margin-right: 4px;
left: -5px;
}
/* 危险容器:背景色、左侧 */
.custom-block.danger {
background-color: var(--custom-block-danger-bg);
}
/* 危险容器:svg图 */
.custom-block.danger [class*="custom-block-title"]::before {
content: '';
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2c5.523 0 10 4.477 10 10v3.764a2 2 0 0 1-1.106 1.789L18 19v1a3 3 0 0 1-2.824 2.995L14.95 23a2.5 2.5 0 0 0 .044-.33L15 22.5V22a2 2 0 0 0-1.85-1.995L13 20h-2a2 2 0 0 0-1.995 1.85L9 22v.5c0 .171.017.339.05.5H9a3 3 0 0 1-3-3v-1l-2.894-1.447A2 2 0 0 1 2 15.763V12C2 6.477 6.477 2 12 2zm-4 9a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z' fill='%23e13238'/%3E%3C/svg%3E");
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
position: relative;
margin-right: 4px;
left: -5px;
top: -1px;
}
/* 提醒容器:背景色、左侧 */
.custom-block.note {
background-color: var(--custom-block-note-bg);
}
/* 提醒容器:svg图 */
.custom-block.note [class*="custom-block-title"]::before {
content: '';
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z' fill='%234cb3d4'/%3E%3C/svg%3E");
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
position: relative;
margin-right: 4px;
left: -5px;
top: -1px;
}
/* 重要容器:背景色、左侧 */
.custom-block.important {
background-color: var(--custom-block-important-bg);
}
/* 重要容器:svg图 */
.custom-block.important [class*="custom-block-title"]::before {
content: '';
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 981.333a84.992 84.992 0 0 1-84.907-84.906h169.814A84.992 84.992 0 0 1 512 981.333zm384-128H128v-42.666l85.333-85.334v-256A298.325 298.325 0 0 1 448 177.92V128a64 64 0 0 1 128 0v49.92a298.325 298.325 0 0 1 234.667 291.413v256L896 810.667v42.666zm-426.667-256v85.334h85.334v-85.334h-85.334zm0-256V512h85.334V341.333h-85.334z' fill='%23a371f7'/%3E%3C/svg%3E");
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
position: relative;
margin-right: 4px;
left: -5px;
top: -1px;
}
/* 注意容器:背景色、左侧 */
.custom-block.caution {
background-color: var(--custom-block-caution-bg);
}
/* 注意容器:svg图 */
.custom-block.caution [class*="custom-block-title"]::before {
content: '';
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2c5.523 0 10 4.477 10 10v3.764a2 2 0 0 1-1.106 1.789L18 19v1a3 3 0 0 1-2.824 2.995L14.95 23a2.5 2.5 0 0 0 .044-.33L15 22.5V22a2 2 0 0 0-1.85-1.995L13 20h-2a2 2 0 0 0-1.995 1.85L9 22v.5c0 .171.017.339.05.5H9a3 3 0 0 1-3-3v-1l-2.894-1.447A2 2 0 0 1 2 15.763V12C2 6.477 6.477 2 12 2zm-4 9a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z' fill='%23e13238'/%3E%3C/svg%3E");
width: 20px;
height: 20px;
display: inline-block;
vertical-align: middle;
position: relative;
margin-right: 4px;
left: -5px;
top: -1px;
}
+5
View File
@@ -0,0 +1,5 @@
/* Keep only the top-left navbar title in Outfit; use VitePress defaults elsewhere. */
.VPNavBarTitle .title,
.VPNavBarTitle .title .text {
font-family: "Outfit", sans-serif !important;
}
+358
View File
@@ -0,0 +1,358 @@
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
*
* Each colors have exact same color scale system with 3 levels of solid
* colors with different brightness, and 1 soft color.
*
* - `XXX-1`: The most solid color used mainly for colored text. It must
* satisfy the contrast ratio against when used on top of `XXX-soft`.
*
* - `XXX-2`: The color used mainly for hover state of the button.
*
* - `XXX-3`: The color for solid background, such as bg color of the button.
* It must satisfy the contrast ratio with pure white (#ffffff) text on
* top of it.
*
* - `XXX-soft`: The color used for subtle background such as custom container
* or badges. It must satisfy the contrast ratio when putting `XXX-1` colors
* on top of it.
*
* The soft color must be semi transparent alpha channel. This is crucial
* because it allows adding multiple "soft" colors on top of each other
* to create a accent, such as when having inline code block inside
* custom containers.
*
* - `default`: The color used purely for subtle indication without any
* special meanings attached to it such as bg color for menu hover state.
*
* - `brand`: Used for primary brand colors, such as link text, button with
* brand theme, etc.
*
* - `tip`: Used to indicate useful information. The default theme uses the
* brand color for this by default.
*
* - `warning`: Used to indicate warning to the users. Used in custom
* container, badges, etc.
*
* - `danger`: Used to show error, or dangerous message to the users. Used
* in custom container, badges, etc.
* -------------------------------------------------------------------------- */
:root {
--vp-c-default-1: var(--vp-c-gray-1);
--vp-c-default-2: var(--vp-c-gray-2);
--vp-c-default-3: var(--vp-c-gray-3);
--vp-c-default-soft: var(--vp-c-gray-soft);
--vp-c-brand-1: var(--vp-c-indigo-1);
--vp-c-brand-2: var(--vp-c-indigo-2);
--vp-c-brand-3: var(--vp-c-indigo-3);
--vp-c-brand-soft: var(--vp-c-indigo-soft);
--vp-c-tip-1: var(--vp-c-brand-1);
--vp-c-tip-2: var(--vp-c-brand-2);
--vp-c-tip-3: var(--vp-c-brand-3);
--vp-c-tip-soft: var(--vp-c-brand-soft);
--vp-c-warning-1: var(--vp-c-yellow-1);
--vp-c-warning-2: var(--vp-c-yellow-2);
--vp-c-warning-3: var(--vp-c-yellow-3);
--vp-c-warning-soft: var(--vp-c-yellow-soft);
--vp-c-danger-1: var(--vp-c-red-1);
--vp-c-danger-2: var(--vp-c-red-2);
--vp-c-danger-3: var(--vp-c-red-3);
--vp-c-danger-soft: var(--vp-c-red-soft);
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-border: transparent;
--vp-button-brand-text: var(--vp-c-white);
--vp-button-brand-bg: var(--vp-c-brand-3);
--vp-button-brand-hover-border: transparent;
--vp-button-brand-hover-text: var(--vp-c-white);
--vp-button-brand-hover-bg: var(--vp-c-brand-2);
--vp-button-brand-active-border: transparent;
--vp-button-brand-active-text: var(--vp-c-white);
--vp-button-brand-active-bg: var(--vp-c-brand-1);
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent;
--vp-home-hero-name-background: -webkit-linear-gradient(
120deg,
#bd34fe 30%,
#41d1ff
);
--vp-home-hero-image-background-image: linear-gradient(
-45deg,
#bd34fe 50%,
#47caff 50%
);
--vp-home-hero-image-filter: blur(44px);
}
@media (min-width: 640px) {
:root {
--vp-home-hero-image-filter: blur(56px);
}
}
@media (min-width: 960px) {
:root {
--vp-home-hero-image-filter: blur(68px);
}
}
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
:root {
--vp-custom-block-tip-border: transparent;
--vp-custom-block-tip-text: var(--vp-c-text-1);
--vp-custom-block-tip-bg: var(--vp-c-brand-soft);
--vp-custom-block-tip-code-bg: var(--vp-c-brand-soft);
}
/**
* Component: Sidebar
* -------------------------------------------------------------------------- */
:root {
--vp-sidebar-bg-color: transparent;
--vp-section-tabs-height: 44px;
}
@media (max-width: 959px) {
:root {
--vp-sidebar-bg-color: var(--vp-c-bg-alt);
}
.VPSidebar {
background-color: var(--vp-c-bg-alt) !important;
}
}
.VPSidebarItem.is-link > .item > .link {
margin: 2px 0;
border-radius: 8px;
padding: 0 10px;
transition: none;
}
.VPSidebarItem,
.VPSidebarItem > .item,
.VPSidebarItem > .item > .link {
border-bottom: none !important;
}
.VPSidebar .group + .group {
border-top: none !important;
}
.VPSidebar {
scrollbar-width: thin;
scrollbar-color: var(--vp-c-divider) transparent;
}
.VPSidebar::-webkit-scrollbar {
width: 10px;
}
.VPSidebar::-webkit-scrollbar-track {
background: transparent;
}
.VPSidebar::-webkit-scrollbar-thumb {
border: 2px solid transparent;
border-radius: 999px;
background-clip: padding-box;
background-color: var(--vp-c-divider);
}
.VPSidebar::-webkit-scrollbar-thumb:hover {
background-color: var(--vp-c-text-3);
}
.VPSidebarItem.is-link > .item > .link:hover {
background-color: var(--vp-c-default-soft);
}
.VPSidebarItem.is-link.is-active > .item > .link {
background-color: var(--vp-c-brand-soft);
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand-1) !important;
}
/**
* Component: Nav
* -------------------------------------------------------------------------- */
.VPNavBarTitle .logo {
width: 40px;
height: 40px;
}
.VPNavBarTitle .title > span {
font-size: 26px;
color: var(--vp-c-text-1);
}
@media (min-width: 960px) {
.VPNavBar.has-sidebar .wrapper {
padding: 0 32px !important;
background-color: var(--vp-nav-bg-color) !important;
}
.VPNavBar.has-sidebar .container {
max-width: calc(var(--vp-layout-max-width) - 64px) !important;
justify-content: flex-start !important;
gap: 24px !important;
background-color: var(--vp-nav-bg-color) !important;
}
.VPNavBar.has-sidebar .container > .title {
position: relative !important;
z-index: 3 !important;
padding: 0 !important;
width: auto !important;
max-width: none !important;
background-color: var(--vp-nav-bg-color) !important;
}
.VPNavBar.has-sidebar .content {
padding-left: 0 !important;
padding-right: 0 !important;
}
.VPNavBar.has-sidebar .content-body {
justify-content: flex-start !important;
}
.VPNavBar.has-sidebar .menu {
margin-right: auto !important;
}
.VPNavBar.has-sidebar .divider {
padding-left: 0 !important;
}
.VPNavBar.has-sidebar .VPNavBarTitle .title {
border-bottom: none !important;
background-color: var(--vp-nav-bg-color);
}
}
@media (min-width: 1440px) {
.VPNavBar.has-sidebar .container > .title {
padding-left: 0 !important;
width: auto !important;
}
.VPNavBar.has-sidebar .content {
padding-right: 0 !important;
padding-left: 0 !important;
}
.VPNavBar.has-sidebar .divider {
padding-left: 0 !important;
}
}
/**
* Component: Local Nav
* -------------------------------------------------------------------------- */
@media (min-width: 960px) {
.VPLocalNav.has-sidebar {
border-bottom: none !important;
}
.VPLocalNav.has-sidebar::after {
content: "";
position: absolute;
left: var(--vp-sidebar-width);
right: 0;
bottom: 0;
height: 1px;
background-color: var(--vp-c-gutter);
}
}
@media (min-width: 1440px) {
.VPLocalNav.has-sidebar::after {
left: calc((100vw - var(--vp-layout-max-width)) / 2 + var(--vp-sidebar-width));
}
}
.VPDocAsideOutline.has-outline .content {
border-left: none !important;
}
@media (min-width: 1280px) {
.VPNavBar.has-sidebar .divider {
display: none !important;
}
.VPSidebar {
padding-top: calc(var(--vp-nav-height) + var(--vp-section-tabs-height)) !important;
}
.Layout.sidebar-scope-intro-deploy .VPSidebar .group,
.Layout.sidebar-scope-platform .VPSidebar .group,
.Layout.sidebar-scope-providers .VPSidebar .group,
.Layout.sidebar-scope-use .VPSidebar .group,
.Layout.sidebar-scope-dev .VPSidebar .group {
display: none;
}
.Layout.sidebar-scope-intro-deploy .VPSidebar .group:nth-of-type(1),
.Layout.sidebar-scope-intro-deploy .VPSidebar .group:nth-of-type(2),
.Layout.sidebar-scope-intro-deploy .VPSidebar .group:nth-of-type(7),
.Layout.sidebar-scope-intro-deploy .VPSidebar .group:nth-of-type(8),
.Layout.sidebar-scope-platform .VPSidebar .group:nth-of-type(3),
.Layout.sidebar-scope-providers .VPSidebar .group:nth-of-type(4),
.Layout.sidebar-scope-use .VPSidebar .group:nth-of-type(5),
.Layout.sidebar-scope-dev .VPSidebar .group:nth-of-type(6) {
display: block;
}
}
.VPHomeHero:not(.has-image) .container {
text-align: center;
}
.VPHomeHero:not(.has-image) .heading {
align-items: center;
}
.VPHomeHero:not(.has-image) .name,
.VPHomeHero:not(.has-image) .text,
.VPHomeHero:not(.has-image) .tagline {
margin: 0 auto;
}
.VPHomeHero:not(.has-image) .actions {
justify-content: center;
}
+10
View File
@@ -0,0 +1,10 @@
# AstrBot
_✨ 易上手的多平台 LLM 聊天机器人及开发框架(的官方文档) ✨_
[查看文档](https://docs.astrbot.app/) [问题提交](https://github.com/AstrBotDevs/AstrBot/issues)
[AstrBot](https://github.com/AstrBotDevs/AstrBot) 是一个松耦合、异步、支持多消息平台部署、具有易用的插件系统和完善的大语言模型(LLM)接入功能的聊天机器人及开发框架。
![image](https://github.com/user-attachments/assets/48f72a71-9456-4166-bbd2-f2a6c8cd740f)
+34
View File
@@ -0,0 +1,34 @@
# Community
## Community Channels
This documentation may not cover all features comprehensively. If you have any questions or suggestions regarding AstrBot or this documentation, please feel free to reach out to us through the community channels below.
### Discord
<https://discord.gg/PxgzhmxJ>
### GitHub
Welcome to submit Issues or Pull Requests:
- [AstrBotDevs/AstrBot](https://github.com/AstrBotDevs/AstrBot)
- [AstrBotDevs/AstrBot-Docs](https://github.com/AstrBotDevs/AstrBot-docs)
### Tencent QQ Groups
> - All groups are available to join. If you find that the group size is below the limit, please feel free to join.
- Group 1: 322154837 (2000-member group)
- Group 3: 630166526 (2000-member group)
- Group 4: 1077826412 (1000-member group)
- Group 5: 822130018 (2000-member group)
- Group 6: 753075035 (2000-member group)
- Group 7: 743746109 (500-member group)
- Group 8: 1030353265 (500-member group)
- **AstrBot Core Development Group: 975206796** (AstrBot development members are usually active here. Welcome to anyone interested in programming/AI technology~)
## Become an AstrBot Organization Member
We welcome you to join us!
+12
View File
@@ -0,0 +1,12 @@
# 配置自定义的模型参数
请手动修改位于 `data/cmd_config.json` 下的配置文件。
找到 `provider`,并找到你想要修改的提供商的模型配置:
![alt text](https://files.astrbot.app/docs/source/images/model-config/image-2.png)
然后在 `model_config` 中添加新的参数即可。
具体的参数请参看对应的提供商的文档。
+27
View File
@@ -0,0 +1,27 @@
# Deploy AstrBot on 1Panel
[1Panel](https://1panel.cn/) is an open-source next-generation Linux server operation and management panel.
AstrBot has been published to the [1Panel App Store](https://apps.fit2cloud.com/1panel) by the 1Panel team, allowing users to quickly deploy and use it directly through 1Panel.
## Install 1Panel
If you haven't installed 1Panel yet, please refer to the [1Panel official website](https://1panel.cn/) for one-click installation.
> International users can refer to the [1Panel official site](https://github.com/1Panel-dev/1Panel) for tutorials.
## Install AstrBot
Open the 1Panel panel, go to the 1Panel App Store, and search for `AstrBot`, as shown below.
![image](https://files.astrbot.app/docs/source/images/1panel/image.png)
Click `Install` and wait for the installation to complete.
After successful installation, open the corresponding AstrBot port (default is 6185) in the 1Panel System-Firewall page.
If you are using cloud servers from providers like AWS, Alibaba Cloud, Tencent Cloud, etc., make sure their security groups also allow port 6185.
## Access AstrBot
Visit `http://IP:6185` to access the AstrBot dashboard.
+48
View File
@@ -0,0 +1,48 @@
# Deploy AstrBot on BT Panel
[BT Panel](https://www.bt.cn/new/index.html) is a secure, efficient, and production-ready Linux/Windows server operation panel.
AstrBot has been published to BT Panel's Docker App Store, supporting one-click installation.
## Install BT Panel
If you haven't installed BT Panel yet, please refer to [Install BT Products](https://www.bt.cn/new/download.html) for one-click installation.
## Set Acceleration URL (For Users in Mainland China)
After entering the BT Panel page, click `Docker` on the left sidebar, click Settings, and modify the `Acceleration URL`.
![alt text](https://files.astrbot.app/docs/source/images/btpanel/image-1.png)
## Install AstrBot
Go to Docker's App Store and search for `AstrBot`, as shown below.
![image](https://files.astrbot.app/docs/source/images/btpanel/image.png)
Click Install and wait for the installation to complete.
After successful installation, click `Security` on the left sidebar and open the corresponding AstrBot port (default is 6185).
If you are using cloud servers from providers like AWS, Alibaba Cloud, Tencent Cloud, etc., make sure their security groups also allow the corresponding port.
## Access AstrBot
Visit `http://IP:6185` to access the AstrBot dashboard.
> [!TIP]
> By default, the above method only opens port 6185. If you need to deploy messaging platforms, you need to additionally open the corresponding ports. Click `Container` in the top bar, find the AstrBot container, click `Manage`, click `Edit Container`, and add the corresponding ports.
>
> ![image](https://files.astrbot.app/docs/source/images/btpanel/image-2.png)
>
> For specific messaging platform port mappings, refer to the table below:
>
>| Port | Description | Type
>| -------- | ------- | ------- |
>| 6185 | AstrBot WebUI `default` port | Required |
>| 6195 | WeCom `default` port | Optional |
>| 6199 | QQ Personal Account(aiocqhttp) `default` port | Optional |
>| 6196 | QQ Official API(Webhook) `default` port | Optional |
>
> Platforms not listed do not require additional port opening.
+39
View File
@@ -0,0 +1,39 @@
# Deploy AstrBot on CasaOS
## Install CasaOS
```bash
curl -fsSL https://get.casaos.io | sudo bash
```
## Add CasaOS-AppStore-Play App Store Source
![image](https://files.astrbot.app/docs/source/images/casaos/image.png)
Click `More Apps`, then enter:
```txt
https://play.cuse.eu.org/Cp0204-AppStore-Play.zip
```
And add it, wait for the addition to complete.
If your network environment is in mainland China, please search for and add `dkTurbo` first, otherwise you may not be able to pull the AstrBot image.
![image](https://files.astrbot.app/docs/source/images/casaos/image-1.png)
Enter `Astrbot` to find AstrBot.
![image](https://files.astrbot.app/docs/source/images/casaos/image-2.png)
Click the icon (not the install button), then hover over the `Install` button and click Custom Install.
![image](https://files.astrbot.app/docs/source/images/casaos/image-3.png)
In the Network section, select `host`.
![image](https://files.astrbot.app/docs/source/images/casaos/image-4.png)
Then click `Install` to start the installation.
After installation is complete, the AstrBot APP will appear on the main interface. Click it to open the dashboard.
+92
View File
@@ -0,0 +1,92 @@
# Deploy AstrBot from Source Code
> [!WARNING]
> You are deploying this project directly from source code. This tutorial requires you to have some technical background.
>
> This tutorial assumes Python is already installed on your device with version `>=3.10`
## Download/Clone Repository
If you have `git` installed on your computer, you can download the source code with the following command:
```bash
git clone https://github.com/AstrBotDevs/AstrBot.git
# The above code will pull the latest commit of the source code, if you need to pull the latest stable release version of the source code, you can use the following command:
# git clone --depth=1 --branch $(git ls-remote --tags --sort='-v:refname' https://github.com/AstrBotDevs/AstrBot.git | head -n1 | awk -F/ '{print $3}') https://github.com/AstrBotDevs/AstrBot.git
cd AstrBot
```
If you don't have `git` installed, please download and install it first.
Alternatively, download the source code directly from GitHub and extract it:
![image](https://files.astrbot.app/docs/source/images/cli/image.png)
## Install Dependencies and Run
::: details 【🥳Recommended】Use `uv` to Manage Dependencies
> If `uv` is not installed, please refer to [Installing uv](https://docs.astral.sh/uv/getting-started/installation/) for installation.
2. Execute in terminal (in the AstrBot directory)
```bash
uv sync
uv run main.py
```
If you have installed some plugins, it is recommended to add the `--no-sync` parameter for subsequent startups to avoid reinstalling plugin dependencies. We are working on solving this issue, so stay tuned.
```bash
uv run --no-sync main.py
```
:::
::: details Install Dependencies with Python Built-in venv
In the AstrBot source code directory, run the following command in the terminal:
> If on Windows and you downloaded and extracted the source code directly, please open the extracted folder and enter in the address bar:
> ![image](https://files.astrbot.app/docs/source/images/cli/image-1.png)
```bash
python3 -m venv ./venv
```
> It might be `python` instead of `python3`
The above steps will create and activate a virtual environment (to avoid disrupting your local Python environment).
Next, install the dependencies with the following command, which may take some time:
Execute on Mac/Linux/WSL:
```bash
source venv/bin/activate
python -m pip install -r requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
python main.py
```
Execute on Windows:
```bash
venv\Scripts\activate
python -m pip install -r requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
python main.py
```
:::
## 🎉 All Done!
If everything goes well, you will see logs printed by AstrBot.
If there are no errors, you will see a log message similar to `🌈 Dashboard started, accessible at` with several links. Open one of the links to access the AstrBot dashboard. The link is `http://localhost:6185`.
> [!TIP]
> If you are deploying AstrBot on a server, you need to replace `localhost` with your server's IP address.
>
> The default username and password are `astrbot` and `astrbot`.
Next, you need to deploy any messaging platform to use AstrBot on that platform.
@@ -0,0 +1,52 @@
# Community-Provided Deployment Methods
> [!WARNING]
> AstrBot official does not guarantee the security and stability of these deployment methods.
## Linux One-Click Deployment Script
Use `curl` to download the script and execute it using `bash`:
```bash
bash <(curl -sSL https://raw.githubusercontent.com/zhende1113/Antlia/refs/heads/main/Script/AstrBot/Antlia.sh)
```
If your system does not have `curl`, you can use `wget`:
```bash
wget -qO- https://raw.githubusercontent.com/zhende1113/Antlia/refs/heads/main/Script/AstrBot/Antlia.sh | bash
```
Repository Address: [zhende1113/Antlia](https://github.com/zhende1113/Antlia/)
## Linux One-Click Deployment Script (Based on Docker)
Supports AstrBot / NapCat.
> [!TIP]
> Use `sudo` for elevated permissions if you have insufficient privileges.
### Using `curl`
```bash
curl -sSL https://raw.githubusercontent.com/railgun19457/AstrbotScript/main/AstrbotScript.sh -o AstrbotScript.sh
chmod +x AstrbotScript.sh
sudo ./AstrbotScript.sh
```
### Using `wget`
```bash
wget -qO AstrbotScript.sh https://raw.githubusercontent.com/railgun19457/AstrbotScript/main/AstrbotScript.sh
chmod +x AstrbotScript.sh
sudo ./AstrbotScript.sh
```
> [!note]
> `sudo ./AstrbotScript.sh --no-color (Optional: disable color output)`
__Repository Address: [railgun19457/AstrbotScript](https://github.com/railgun19457/AstrbotScript)__
## AstrBot Android Deployment
Refer to [zz6zz666/AstrBot-Android-App](https://github.com/zz6zz666/AstrBot-Android-App)
+92
View File
@@ -0,0 +1,92 @@
# Deploy via Compshare
Compshare is UCloud's GPU compute rental and LLM API platform, offering compute resources for AI, deep learning, and scientific workloads.
AstrBot provides an Ollama + AstrBot one-click self-deployment image on Compshare, and also supports Compshare model APIs.
## Use the Ollama + AstrBot One-Click Image
> Default image spec: RTX 3090 24GB + Intel 16-core + 64GB RAM + 200GB system disk. Billing is pay-as-you-go, so please monitor your balance.
1. Register a Compshare account via [this link](https://passport.compshare.cn/register?referral_code=FV7DcGowN4hB5UuXKgpE74).
2. Open the [AstrBot image page](https://www.compshare.cn/images/0oX7xoGrzfre) and create an instance.
3. After deployment, open `JupyterLab` from the [console](https://console.compshare.cn/light-gpu/console/resources).
4. In JupyterLab, create a new terminal and run:
```bash
cd
./astrbot_booter.sh
```
If startup succeeds, you should see output similar to:
```txt
(py312) root@f8396035c96d:/workspace# cd
./astrbot_booter.sh
Starting AstrBot...
Starting ollama...
Both services started in the background.
```
After startup, open `http://<instance-public-ip>:6185` in your browser to access the AstrBot dashboard.
You can find the public IP in Console -> Basic Network (Public).
> It may take around 30 seconds before the page becomes reachable.
![WebUI](https://www-s.ucloud.cn/2025/07/7e9fc6edc1dfa916abc069f4cecc24cf_1753940381771.png)
Login with username `astrbot` and password `astrbot`.
After logging in, you can reset your password and continue setup.
The instance imports `Ollama-DeepSeek-R1-32B` by default.
## Use Other Models
### Pull Models with Ollama
The image includes Ollama. You can pull any model and host it locally on the instance.
1. Choose a model from [Ollama Search](https://ollama.com/search).
2. Connect to the instance terminal via SSH (from Compshare Console -> Instance List -> Console Command and Password).
3. Run `ollama pull <model-name>` and wait for completion.
4. In AstrBot Dashboard -> Providers, edit `ollama_deepseek-r1`, update the model name, and save.
![image](https://files.astrbot.app/docs/source/images/compshare/image-1.png)
### Use Compshare Model API
AstrBot supports direct access to model APIs provided by Compshare.
1. Find the model you want at [Compshare Model Center](https://console.compshare.cn/light-gpu/model-center).
2. In AstrBot Dashboard -> Providers, click `+ Add Provider`, then choose Compshare.
If Compshare is not listed, choose OpenAI-compatible access and set API Base URL to `https://api.modelverse.cn/v1`.
Enter the model name in model configuration and save.
### Test
In AstrBot Dashboard, click `Chat` and run `/provider` to view and switch your active provider.
Then send a normal message to test whether the model works.
![image](https://files.astrbot.app/docs/source/images/compshare/image-2.png)
## Connect to Messaging Platforms
You can follow the latest platform integration guides in the [AstrBot Documentation](https://docs.astrbot.app/en/what-is-astrbot.html).
Open the docs and check the left sidebar under Messaging Platforms.
- Lark: [Connect to Lark](https://docs.astrbot.app/en/platform/lark.html)
- LINE: [Connect to LINE](https://docs.astrbot.app/en/platform/line.html)
- DingTalk: [Connect to DingTalk](https://docs.astrbot.app/en/platform/dingtalk.html)
- WeCom: [Connect to WeCom](https://docs.astrbot.app/en/platform/wecom.html)
- WeChat Official Account: [Connect to WeChat Official Account](https://docs.astrbot.app/en/platform/weixin-official-account.html)
- QQ Official Bot: [Connect to QQ Official API](https://docs.astrbot.app/en/platform/qqofficial/webhook.html)
- KOOK: [Connect to KOOK](https://docs.astrbot.app/en/platform/kook.html)
- Slack: [Connect to Slack](https://docs.astrbot.app/en/platform/slack.html)
- Discord: [Connect to Discord](https://docs.astrbot.app/en/platform/discord.html)
- More methods: [AstrBot Documentation](https://docs.astrbot.app/en/what-is-astrbot.html)
## More Features
For more capabilities, see the [AstrBot Documentation](https://docs.astrbot.app/en/what-is-astrbot.html).
+91
View File
@@ -0,0 +1,91 @@
# Deploy AstrBot with Docker
> [!WARNING]
> Docker provides a convenient way to deploy AstrBot on Windows, Mac, and Linux.
>
> This tutorial assumes you have Docker installed in your environment. If not, please refer to the [Docker official documentation](https://docs.docker.com/get-docker/) for installation.
## Deploy with Docker Compose
::: details Deploy AstrBot Only (General Method)
First, clone the AstrBot repository to your local machine:
```bash
git clone https://github.com/AstrBotDevs/AstrBot
cd AstrBot
```
Then, run Compose:
```bash
sudo docker compose up -d
```
> [!TIP]
> If your network environment is in mainland China, the above command will not pull properly. You may need to modify the compose.yml file and replace `image: soulter/astrbot:latest` with `image: m.daocloud.io/docker.io/soulter/astrbot:latest`.
:::
::: details Deploy with Agent Sandbox Environment
Supports native Python code execution, Shell code execution, and other features.
Deployment method:
```bash
git clone https://github.com/AstrBotDevs/AstrBot
cd AstrBot
# Modify the environment variable configuration in the compose-with-shipyard.yml file, such as Shipyard's access token, etc.
docker compose -f compose-with-shipyard.yml up -d
docker pull soulter/shipyard-ship:latest
```
For configuration and usage details, see the [Agent Sandbox Environment](/en/use/astrbot-agent-sandbox.md) documentation.
:::
## Deploy with Docker
```bash
mkdir astrbot
cd astrbot
sudo docker run -itd -p 6185:6185 -p 6199:6199 -v $PWD/data:/AstrBot/data -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro --name astrbot soulter/astrbot:latest
```
> [!TIP]
> If your network environment is in mainland China, the above command will not pull properly. Please use the following command to pull the image:
>
> ```bash
> sudo docker run -itd -p 6185:6185 -p 6199:6199 -v $PWD/data:/AstrBot/data -v /etc/localtime:/etc/localtime:ro -v /etc/timezone:/etc/timezone:ro --name astrbot m.daocloud.io/docker.io/soulter/astrbot:latest
> ```
>
> (Thanks to DaoCloud ❤️)
> No need to add sudo on Windows, same below
> Sync Host Time on Windows (requires WSL2)
```
-v \\wsl.localhost\(your-wsl-os)\etc\timezone:/etc/timezone:ro
-v \\wsl.localhost\(your-wsl-os)\etc\localtime:/etc/localtime:ro
```
View AstrBot logs with the following command:
```bash
sudo docker logs -f astrbot
```
## 🎉 All Done
If everything goes well, you will see logs printed by AstrBot.
If there are no errors, you will see a log message similar to `🌈 Dashboard started, accessible at` with several links. Open one of the links to access the AstrBot dashboard.
> [!TIP]
> Since Docker isolates the network environment, you cannot use `localhost` to access the dashboard.
>
> The default username and password are `astrbot` and `astrbot`.
>
> If deployed on a cloud server, you need to open ports `6180-6200` and `11451` in the cloud provider's console.
Next, you need to deploy any messaging platform to use AstrBot on that platform.
+197
View File
@@ -0,0 +1,197 @@
# Deploy AstrBot with Kubernetes
> [!WARNING]
> You can deploy AstrBot in a high-availability setup using Kubernetes (K8s), allowing it to automatically recover from failures.
>
> Due to the current use of an SQLite database, this deployment does not support horizontal scaling with multiple replicas. Additionally, if using the Sidecar mode, pay special attention to the persistence of NapCat's login state.
>
> The following tutorial assumes that you have `kubectl` installed and configured, and that you can connect to your K8s cluster.
## Prerequisites
Before you begin, make sure your Kubernetes cluster meets the following conditions:
1. **Default StorageClass**: Used to dynamically create `PersistentVolumeClaim` (PVC). You can check this with `kubectl get sc`. If you don't have one, you need to manually create a `PersistentVolume` (PV) or install a corresponding storage plugin (e.g., `nfs-client-provisioner`).
2. **Network Access**: Ensure that your cluster nodes can pull images from `docker.io` or your specified image repository.
## Deployment Methods
We offer two deployment options:
* **Integrated Deployment (Sidecar Mode)**: Deploy AstrBot and NapCat in the same Pod. Recommended for personal QQ accounts.
* **Standalone Deployment**: Deploy only AstrBot. Suitable for other platforms or if you want to manage NapCat independently.
---
### Method 1: Deploy with NapCatQQ (Sidecar)
This method is located in the `k8s/astrbot_with_napcat` directory.
#### 1. Deploy
```bash
# 1. Create namespace
kubectl apply -f k8s/astrbot_with_napcat/00-namespace.yaml
# 2. Create Persistent Volume Claim
# Note: astrbot-data-shared-pvc requires ReadWriteMany (RWX) access mode.
# If your cluster does not support RWX, you need to configure shared storage such as NFS and modify the storageClassName in 01-pvc.yaml.
kubectl apply -f k8s/astrbot_with_napcat/01-pvc.yaml
# 3. Deploy the application
kubectl apply -f k8s/astrbot_with_napcat/02-deployment.yaml
```
#### 2. Expose Service (Choose one)
* **Option A: NodePort**
```bash
kubectl apply -f k8s/astrbot_with_napcat/03-service-nodeport.yaml
```
The service will be exposed via the node IP and a port automatically assigned by Kubernetes. You can find the port with the following command:
```bash
kubectl get svc -n astrbot-ns
```
In the output, find the `PORT(S)` column for `astrbot-webui-svc` and `napcat-web-svc`. The format is `<internal-port>:<NodePort>/TCP`. For example, if you see `8080:30185/TCP`, the access address is `http://<NodeIP>:30185`.
* **Option B: LoadBalancer**
If your cluster supports `LoadBalancer` type services (usually provided in K8s services from cloud providers), you can use this method.
```bash
kubectl apply -f k8s/astrbot_with_napcat/04-service-loadbalancer.yaml
```
After execution, check the assigned external IP (EXTERNAL-IP) with `kubectl get svc -n astrbot-ns`.
#### 3. Configure Connection
Since AstrBot and NapCat are in the same Pod, they can communicate directly via `localhost`.
1. **Add a message platform in AstrBot:**
* Go to the AstrBot WebUI, select `Settings` -> `Message Platform` -> `Add`.
* **Select Message Platform Category**: `aiocqhttp`
* **Bot Name**: `napcat` (or custom)
* **Reverse Websocket Host**: `0.0.0.0`
* **Reverse Websocket Port**: `6199`
* Save the configuration.
2. **Configure Websocket Client in NapCat:**
* Go to the NapCat WebUI, select `Settings` -> `Reverse WS` -> `Add`.
* **Enable**: On
* **URL**: `ws://localhost:6199/ws`
* **Message Format**: `Array`
* Save the configuration.
---
### Method 2: Deploy AstrBot Only (General Purpose)
This method is located in the `k8s/astrbot` directory.
#### 1. Deploy
```bash
# 1. Create namespace
kubectl apply -f k8s/astrbot/00-namespace.yaml
# 2. Create Persistent Volume Claim
kubectl apply -f k8s/astrbot/01-pvc.yaml
# 3. Deploy the application
kubectl apply -f k8s/astrbot/02-deployment.yaml
```
#### 2. Expose Service (Choose one)
* **Option A: NodePort**
```bash
kubectl apply -f k8s/astrbot/03-service-nodeport.yaml
```
The service will be exposed via the node IP and a port automatically assigned by Kubernetes. You can find the port with the following command:
```bash
kubectl get svc -n astrbot-standalone-ns
```
In the output, find the `PORT(S)` column for `astrbot-webui-svc`. The format is `<internal-port>:<NodePort>/TCP`. For example, if you see `8080:30185/TCP`, the access address is `http://<NodeIP>:30185`.
* **Option B: LoadBalancer**
```bash
kubectl apply -f k8s/astrbot/04-service-loadbalancer.yaml
```
After execution, check the assigned external IP (EXTERNAL-IP) with `kubectl get svc -n astrbot-standalone-ns`.
---
## Advanced Configuration
### Image Mirror (for users in mainland China)
If you have difficulty pulling the `soulter/astrbot:latest` or `mlikiowa/napcat-docker:latest` images, you can manually edit the corresponding `02-deployment.yaml` file and replace the `image` field with a domestic mirror address, for example:
```yaml
# Example:
# image: soulter/astrbot:latest
# Replace with:
image: m.daocloud.io/docker.io/soulter/astrbot:latest
```
### Enable Docker Sandbox Code Executor
If you need to use the sandbox code executor, you need to mount the Docker socket file into the Pod.
Edit the `02-deployment.yaml` file and add `volumes` and `volumeMounts` under `spec.template.spec`:
1. **Add the following to the `volumeMounts` list of the `astrbot` container:**
```yaml
- name: docker-sock
mountPath: /var/run/docker.sock
```
2. **Add the following to the `spec.template.spec.volumes` list:**
```yaml
- name: docker-sock
hostPath:
path: /var/run/docker.sock
type: Socket
```
> [!WARNING]
> Mounting the Docker socket into a Pod poses a security risk. Please ensure you understand the implications.
## View Logs
* **Sidecar Deployment Mode:**
```bash
# View AstrBot logs
kubectl logs -f -n astrbot-ns deployment/astrbot-stack -c astrbot
# View NapCat logs
kubectl logs -f -n astrbot-ns deployment/astrbot-stack -c napcat
```
* **Standalone Deployment Mode:**
```bash
kubectl logs -f -n astrbot-standalone-ns deployment/astrbot-standalone
```
## 🎉 All Done!
After deploying and exposing the service, you can access the AstrBot admin panel through the corresponding IP and port.
> The default username and password are `astrbot` and `astrbot`.
+98
View File
@@ -0,0 +1,98 @@
# Deploy AstrBot with AstrBot Launcher
## Recommended Method 1: AstrBot One-Click Launcher
AstrBot One-Click Launcher supports Windows, macOS, and Linux.
0. Open [AstrBotDevs/astrbot-launcher](https://github.com/AstrBotDevs/astrbot-launcher)
1. **Optional but recommended**: give this project a [**Star ⭐**](https://github.com/AstrBotDevs/astrbot-launcher). Your support helps maintainers keep improving it.
2. Find **Releases** on the right, open the latest release, then download the installer for your system from **Assets**.
For example:
- Windows x86 users: `AstrBot.Launcher_0.2.1_x64-setup.exe`
- Windows on Arm users: `AstrBot.Launcher_0.2.1_arm64-setup.exe`
- macOS Apple Silicon users: `AstrBot.Launcher_0.2.1_aarch64.dmg`
For macOS users, if you see "damaged and can't be opened", it is caused by macOS security restrictions on unsigned apps. Fix it with:
1. Open Terminal.
2. Run:
`xattr -dr com.apple.quarantine /Applications/AstrBot\ Launcher.app`
3. Reopen AstrBot Launcher.
## Method 2: Legacy Windows Installer
We still recommend the One-Click Launcher above because it is simpler, more automated, and better for most users.
The legacy installer is a `PowerShell` script, very small (<20KB). It requires `PowerShell` (usually built in on `Windows 10` and newer).
> [!WARNING]
> `Python 3.10` or later must be installed, and environment variables must be configured.
> [!TIP]
> If deployment fails, try Docker deployment or manual deployment instead.
## Download the Legacy Installer
Open <https://github.com/AstrBotDevs/AstrBotLauncher/releases/latest>
Download `Source code (zip)` and extract it.
## Run the Legacy Installer
> The video may be outdated. Follow the steps here.
After extraction, open the folder.
Type `PowerShell` in the address bar and press Enter:
![image](https://files.astrbot.app/docs/source/images/windows/image-4.png)
Drag `launcher_astrbot_en.bat` into the PowerShell window and press Enter.
> [!WARNING]
> - The script is safe. If you see `Windows protected your PC`, click `More info` and then `Run anyway`.
> - By default, it uses `python`. If you want to specify another interpreter path/command, edit `launcher_astrbot_en.bat`, find `set PYTHON_CMD=python`, and replace `python` with your own command/path.
If Python is not detected, the script exits with a prompt.
The script checks whether an `AstrBot` folder exists. If not, it downloads the latest AstrBot source from [GitHub](https://github.com/AstrBotDevs/AstrBot/releases/latest), installs dependencies, and runs it automatically.
## Done
If everything works, you will see AstrBot logs.
Without errors, you should see a log like `🌈 Management panel started, accessible at` with several URLs. Open one URL to access AstrBot WebUI.
> [!TIP]
> Default username and password: `astrbot` / `astrbot`.
>
> If WebUI returns 404:
> Download `dist.zip` from [release](https://github.com/AstrBotDevs/AstrBot/releases), extract it into `AstrBot/data`, then restart the computer if needed.
Then deploy at least one messaging platform adapter to start using AstrBot in IM apps.
## Error: Python is not installed
If you still get this error after installing Python and restarting, your PATH is likely incorrect.
**Method 1**
Search for Python in Windows and open its file location:
![image](https://files.astrbot.app/docs/source/images/windows/image.png)
Right-click the shortcut below and open file location:
![alt text](https://files.astrbot.app/docs/source/images/windows/image-1.png)
Copy the file path:
![image](https://files.astrbot.app/docs/source/images/windows/image-2.png)
Edit `launcher_astrbot_en.bat` in Notepad, find `set PYTHON_CMD=python`, and replace `python` with your interpreter command/path. Keep quotes if your path contains spaces.
**Method 2**
Reinstall Python, check `Add Python to PATH` during installation, then restart your computer.
@@ -0,0 +1,5 @@
# Other Deployments
- [CasaOS Deployment](./casaos.md)
- [Compshare GPU Deployment](./compshare.md)
- [Community Deployments](./community-deployment.md)
+17
View File
@@ -0,0 +1,17 @@
# Package Manager Deployment (uv)
Use `uv` to install and run AstrBot quickly.
## Before You Start
If `uv` is not installed, install it first by following the official guide:
<https://docs.astral.sh/uv/>
`uv` supports Linux, Windows, and macOS.
## Install and Start
```bash
uv tool install astrbot
astrbot
```
+41
View File
@@ -0,0 +1,41 @@
# Installation via System Package Manager
> [!WARNING]
> Currently, only the AUR version is provided.
> If you are a Windows/macOS user, it is recommended to install via `uv`.
> If you are a Linux user, it is highly recommended to install via a package manager.
# Preparation
## What is AUR?
AUR (Arch User Repository) allows users to install software from community-maintained software repositories. AUR packages are typically maintained by community members rather than official maintainers.
Common AUR helpers include `yay` and `paru`.
The following tutorial uses `paru` as an example; `yay` works similarly, just replace `paru` with `yay`.
# Installation Process
## AUR
```bash
paru -S astrbot-git
# Note:
# The review step will begin; press 'q' to exit review and continue installation.
# After installation, the data directory is fixed at: ~/.local/share/astrbot
```
# Starting
>[!TIP]
> You can directly use `astrbot init` (for the first run) to initialize.
> Use `astrbot run` to run the bot.
> However, it is highly recommended to use `systemctl` for starting, as it provides features like automatic restart and log rotation.
```bash
systemctl --user start astrbot.service
```
# Auto-start on Boot
```bash
# For security reasons, it is designed to run as a user.
systemctl --user enable astrbot.service
# If you need to start it immediately, add --now
# systemctl --user enable --now astrbot.service
```
+16
View File
@@ -0,0 +1,16 @@
# Preface
After successful deployment... of course, don't forget to give [AstrBot](https://github.com/AstrBotDevs/AstrBot) a Star!
AstrBot Main Repository: [![wakatime](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e.svg)](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
AstrBot Dashboard: [![wakatime](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018c440f-c177-45f8-8224-292cdf5926f3.svg)](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018c440f-c177-45f8-8224-292cdf5926f3)
AstrBot Documentation: [![wakatime](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018c9619-e195-4b94-bd7b-2ca61679145b.svg)](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018c9619-e195-4b94-bd7b-2ca61679145b)
❤️ Contributions to this project are warmly welcomed, including Issues and Pull Requests.
## Next...
If you're reading this, it means you have successfully deployed the messaging platform and sent/received your first command. Next, you can configure large language models or add plugins. Please refer to the `Configuration - Integrating LLM Services` section.
+557
View File
@@ -0,0 +1,557 @@
---
outline: deep
---
# AstrBot Configuration File
## data/cmd_config.json
AstrBot's configuration file is a JSON format file. AstrBot reads this file at startup and initializes based on the settings within. Its path is `data/cmd_config.json`.
> Since AstrBot v4.0.0, we introduced the concept of [multiple configuration files](https://blog.astrbot.app/posts/what-is-changed-in-4.0.0/#%E5%A4%9A%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6). `data/cmd_config.json` serves as the default configuration `default`. Other configuration files you create in the WebUI are stored in the `data/config/` directory, starting with `abconf_`.
The default AstrBot configuration is as follows:
```jsonc
{
"config_version": 2,
"platform_settings": {
"unique_session": False,
"rate_limit": {
"time": 60,
"count": 30,
"strategy": "stall", # stall, discard
},
"reply_prefix": "",
"forward_threshold": 1500,
"enable_id_white_list": True,
"id_whitelist": [],
"id_whitelist_log": True,
"wl_ignore_admin_on_group": True,
"wl_ignore_admin_on_friend": True,
"reply_with_mention": False,
"reply_with_quote": False,
"path_mapping": [],
"segmented_reply": {
"enable": False,
"only_llm_result": True,
"interval_method": "random",
"interval": "1.5,3.5",
"log_base": 2.6,
"words_count_threshold": 150,
"regex": ".*?[。?!~…]+|.+$",
"content_cleanup_rule": "",
},
"no_permission_reply": True,
"empty_mention_waiting": True,
"empty_mention_waiting_need_reply": True,
"friend_message_needs_wake_prefix": False,
"ignore_bot_self_message": False,
"ignore_at_all": False,
},
"provider": [],
"provider_settings": {
"enable": True,
"default_provider_id": "",
"default_image_caption_provider_id": "",
"image_caption_prompt": "Please describe the image using Chinese.",
"provider_pool": ["*"], # "*" means use all available providers
"wake_prefix": "",
"web_search": False,
"websearch_provider": "default",
"websearch_tavily_key": [],
"web_search_link": False,
"display_reasoning_text": False,
"identifier": False,
"group_name_display": False,
"datetime_system_prompt": True,
"default_personality": "default",
"persona_pool": ["*"],
"prompt_prefix": "{{prompt}}",
"max_context_length": -1,
"dequeue_context_length": 1,
"streaming_response": False,
"show_tool_use_status": False,
"streaming_segmented": False,
"max_agent_step": 30,
"tool_call_timeout": 60,
},
"provider_stt_settings": {
"enable": False,
"provider_id": "",
},
"provider_tts_settings": {
"enable": False,
"provider_id": "",
"dual_output": False,
"use_file_service": False,
},
"provider_ltm_settings": {
"group_icl_enable": False,
"group_message_max_cnt": 300,
"image_caption": False,
"active_reply": {
"enable": False,
"method": "possibility_reply",
"possibility_reply": 0.1,
"whitelist": [],
},
},
"content_safety": {
"also_use_in_response": False,
"internal_keywords": {"enable": True, "extra_keywords": []},
"baidu_aip": {"enable": False, "app_id": "", "api_key": "", "secret_key": ""},
},
"admins_id": ["astrbot"],
"t2i": False,
"t2i_word_threshold": 150,
"t2i_strategy": "remote",
"t2i_endpoint": "",
"t2i_use_file_service": False,
"t2i_active_template": "base",
"http_proxy": "",
"no_proxy": ["localhost", "127.0.0.1", "::1"],
"dashboard": {
"enable": True,
"username": "astrbot",
"password": "77b90590a8945a7d36c963981a307dc9",
"jwt_secret": "",
"host": "0.0.0.0",
"port": 6185,
},
"platform": [],
"platform_specific": {
# Platform-specific settings: categorized by platform, then by feature group
"lark": {
"pre_ack_emoji": {"enable": False, "emojis": ["Typing"]},
},
"telegram": {
"pre_ack_emoji": {"enable": False, "emojis": ["✍️"]},
},
},
"wake_prefix": ["/"],
"log_level": "INFO",
"trace_enable": False,
"pip_install_arg": "",
"pypi_index_url": "https://mirrors.aliyun.com/pypi/simple/",
"persona": [], # deprecated
"timezone": "Asia/Shanghai",
"callback_api_base": "",
"default_kb_collection": "", # Default knowledge base name
"plugin_set": ["*"], # "*" means use all available plugins, empty list means none
}
```
## Field Details
### `config_version`
Configuration version, do not modify.
### `platform_settings`
General settings for message platform adapters.
#### `platform_settings.unique_session`
Whether to enable session isolation. Default is `false`. When enabled, each person's conversation context in groups or channels is independent.
#### `platform_settings.rate_limit`
Strategy when message rate exceeds limits. `time` is the window, `count` is the number of messages, and `strategy` is the limit strategy. `stall` means wait, `discard` means drop.
#### `platform_settings.reply_prefix`
Fixed prefix string when replying to messages. Default is empty.
#### `platform_settings.forward_threshold`
> Currently only applicable to the QQ platform adapter.
Message forwarding threshold. When the reply content exceeds a certain number of characters, the bot will fold the message into a QQ group "forwarded message" to prevent spamming.
#### `platform_settings.enable_id_white_list`
Whether to enable the ID whitelist. Default is `true`. When enabled, only messages from IDs in the whitelist will be processed.
#### `platform_settings.id_whitelist`
ID whitelist. If filled, only message events from the specified IDs will be processed. Empty means the whitelist filter is not enabled. You can use the `/sid` command to get the session ID on a platform.
Session IDs can also be found in AstrBot logs; when a message fails the whitelist, an INFO level log is output, e.g., `aiocqhttp:GroupMessage:547540978`.
#### `platform_settings.id_whitelist_log`
Whether to print logs for messages that fail the ID whitelist. Default is `true`.
#### `platform_settings.wl_ignore_admin_on_group` & `platform_settings.wl_ignore_admin_on_friend`
- `wl_ignore_admin_on_group`: Whether group messages from admins bypass the ID whitelist. Default is `true`.
- `wl_ignore_admin_on_friend`: Whether private messages from admins bypass the ID whitelist. Default is `true`.
#### `platform_settings.reply_with_mention`
Whether to @ mention the user when replying. Default is `false`.
#### `platform_settings.reply_with_quote`
Whether to quote the user's message when replying. Default is `false`.
#### `platform_settings.path_mapping`
*This configuration item has been deprecated since v4.0.0.*
List of path mappings. Used to replace file paths in messages. Each mapping item contains `from` and `to` fields, indicating that `from` in the message path is replaced with `to`.
#### `platform_settings.segmented_reply`
Segmented reply settings.
- `enable`: Whether to enable segmented replies. Default is `false`.
- `only_llm_result`: Whether to only segment replies generated by the LLM. Default is `true`.
- `interval_method`: Method for segmentation intervals. Options are `random` and `log`. Default is `random`.
- `interval`: Interval time for segmentation. For `random`, fill in two comma-separated numbers representing min and max intervals (seconds). For `log`, fill in one number representing the log base. Default is `"1.5,3.5"`.
- `log_base`: Log base, only applicable when `interval_method` is `log`. Default is `2.6`.
- `words_count_threshold`: Character limit for segmented replies. Only messages shorter than this value will be segmented; longer messages will be sent directly (unsegmented). Default is `150`.
- `regex`: Used to split a message. By default, it splits based on punctuation like periods and question marks. `re.findall(r'<regex>', text)`. Default is `".*?[。?!~…]+|.+$"`.
- `content_cleanup_rule`: Removes specified content from segments. Supports regex. For example, `[。?!]` will remove all periods, question marks, and exclamation points. `re.sub(r'<regex>', '', text)`.
#### `platform_settings.no_permission_reply`
Whether to reply with a "no permission" prompt when a user lacks authority. Default is `true`.
#### `platform_settings.empty_mention_waiting`
Whether to enable the empty @ waiting mechanism. Default is `true`. When enabled, if a user sends a message containing only an @ mention of the bot, the bot waits for the user to send the next message within 60 seconds and merges the two for processing. This is particularly useful on platforms that don't support sending @ and voice/images simultaneously.
#### `platform_settings.empty_mention_waiting_need_reply`
In the above item (`empty_mention_waiting`), if waiting is triggered, enabling this will make the bot immediately generate an LLM reply. Otherwise, it just waits without replying. Default is `true`.
#### `platform_settings.friend_message_needs_wake_prefix`
Whether private messages on platforms require a wake prefix. Default is `false`. When enabled, users must use a wake prefix to trigger a bot response in private chats.
#### `platform_settings.ignore_bot_self_message`
Whether to ignore messages sent by the bot itself. Default is `false`. When enabled, the bot won't process its own messages, preventing infinite loops on some platforms.
#### `platform_settings.ignore_at_all`
Whether to ignore @all messages. Default is `false`. When enabled, the bot won't respond to messages containing @all.
### `provider`
> This item only takes effect in `data/cmd_config.json`; AstrBot does not read this from configuration files in the `data/config/` directory.
List of configured model service provider settings.
### `provider_settings`
General settings for LLM providers.
#### `provider_settings.enable`
Whether to enable LLM chat. Default is `true`.
#### `provider_settings.default_provider_id`
Default conversation model provider ID. Must be a provider ID already configured in the `provider` list. If empty, the first provider in the list is used.
#### `provider_settings.default_image_caption_provider_id`
Default image captioning model provider ID. Must be a provider ID already configured in the `provider` list. If empty, image captioning is disabled.
This means when a user sends an image, AstrBot uses this provider to generate a text description, which is then used as part of the conversation context. This is useful when the conversation model doesn't support multimodal input.
#### `provider_settings.image_caption_prompt`
Prompt template for image captioning. Default is `"Please describe the image using Chinese."`.
#### `provider_settings.provider_pool`
*This configuration item is not yet in actual use.*
#### `provider_settings.wake_prefix`
Extra trigger condition for LLM chat. For example, if `chat` is filled, messages must start with `/chat` to trigger LLM chat, where `/` is the bot's wake prefix. This is a measure to prevent abuse.
#### `provider_settings.web_search`
Whether to enable AstrBot's built-in web search capability. Default is `false`. When enabled, the LLM may automatically search the web and answer based on the content.
#### `provider_settings.websearch_provider`
Web search provider type. Default is `default`. Currently supports `default` and `tavily`.
- `default`: Works best when Google is accessible. If Google fails, it tries Bing and Sogou in order.
- `tavily`: Uses the Tavily search engine.
#### `provider_settings.websearch_tavily_key`
API Key list for the Tavily search engine. Required when using `tavily` as the web search provider.
#### `provider_settings.web_search_link`
Whether to prompt the model to include links to search results in the reply. Default is `false`.
#### `provider_settings.display_reasoning_text`
Whether to display the model's reasoning process in the reply. Default is `false`.
#### `provider_settings.identifier`
Whether to prepend the group member's name to the prompt so the model better understands the group chat state. Default is `false`. Enabling this slightly increases token usage.
#### `provider_settings.group_name_display`
Whether to let the model know the name of the group it's in. Default is `false`. This currently only takes effect in the QQ platform adapter.
#### `provider_settings.datetime_system_prompt`
Whether to include the current machine date and time in the system prompt. Default is `true`.
#### `provider_settings.default_personality`
ID of the default personality to use. Configure personalities in the WebUI.
#### `provider_settings.persona_pool`
*This configuration item is not yet in actual use.*
#### `provider_settings.prompt_prefix`
User prompt. You can use `{{prompt}}` as a placeholder for user input. If no placeholder is provided, it's prepended to the user input.
#### `provider_settings.max_context_length`
When the conversation context exceeds this number, the oldest parts are discarded. One round of chat counts as 1. -1 means no limit.
#### `provider_settings.dequeue_context_length`
The number of conversation rounds to discard each time the `max_context_length` limit is triggered.
#### `provider_settings.streaming_response`
Whether to enable streaming responses. Default is `false`. When enabled, the model's reply is sent to the user in real-time with a typewriter effect. This only takes effect on WebChat, Telegram, and Lark platforms.
#### `provider_settings.show_tool_use_status`
Whether to show tool usage status. Default is `false`. When enabled, the model displays the tool name and input parameters when using a tool.
#### `provider_settings.streaming_segmented`
Whether platforms that don't support streaming responses should fall back to segmented replies. Default is `false`. This means if streaming is enabled but the platform doesn't support it, segmented multiple replies are used instead.
#### `provider_settings.max_agent_step`
Limit on the maximum number of Agent steps. Default is `30`. Each tool call by the model counts as one step.
#### `provider_settings.tool_call_timeout`
Added in `v4.3.5`
Maximum timeout for tool calls (seconds), default is `60` seconds.
#### `provider_stt_settings`
General settings for Speech-to-Text (STT) providers.
#### `provider_stt_settings.enable`
Whether to enable STT services. Default is `false`.
#### `provider_stt_settings.provider_id`
STT provider ID. Must be an STT provider ID already configured in the `provider` list.
#### `provider_tts_settings`
General settings for Text-to-Speech (TTS) providers.
#### `provider_tts_settings.enable`
Whether to enable TTS services. Default is `false`.
#### `provider_tts_settings.provider_id`
TTS provider ID. Must be a TTS provider ID already configured in the `provider` list.
#### `provider_tts_settings.dual_output`
Whether to enable dual output. Default is `false`. When enabled, the bot sends both text and voice messages.
#### `provider_tts_settings.use_file_service`
Whether to enable the file service. Default is `false`. When enabled, the bot provides the output voice file as an external HTTP link to the message platform. This depends on the `callback_api_base` configuration.
#### `provider_ltm_settings`
General settings for group chat context awareness providers.
#### `provider_ltm_settings.group_icl_enable`
Whether to enable group chat context awareness. Default is `false`. When enabled, the bot records group chat conversations to better understand context.
The context content is placed in the conversation's system prompt.
#### `provider_ltm_settings.group_message_max_cnt`
Maximum number of group chat messages to record. Default is `100`. Messages exceeding this count are discarded.
#### `provider_ltm_settings.image_caption`
Whether to record images in group chats and automatically generate text descriptions using an image captioning model. Default is `false`. This depends on the `provider_settings.default_image_caption_provider_id` configuration. Use with caution as it can significantly increase API calls and token usage.
#### `provider_ltm_settings.active_reply`
- `enable`: Whether to enable active replies. Default is `false`.
- `method`: Method for active replies. Option is `possibility_reply`.
- `possibility_reply`: Probability of an active reply. Default is `0.1`. Only applicable when `method` is `possibility_reply`.
- `whitelist`: ID whitelist for active replies. Only IDs in this list will trigger active replies. Empty means no whitelist filter. You can use the `/sid` command to get the session ID on a platform.
### `content_safety`
Content safety settings.
#### `content_safety.also_use_in_response`
Whether to also perform content safety checks on LLM replies. Default is `false`. When enabled, bot-generated replies also undergo safety checks to prevent inappropriate content.
#### `content_safety.internal_keywords`
Internal keyword detection settings.
- `enable`: Whether to enable internal keyword detection. Default is `true`.
- `extra_keywords`: List of extra keywords, supports regex. Default is empty.
#### `content_safety.baidu_aip`
Baidu AI content moderation settings.
- `enable`: Whether to enable Baidu AI content moderation. Default is `false`.
- `app_id`: App ID for Baidu AI content moderation.
- `api_key`: API Key for Baidu AI content moderation.
- `secret_key`: Secret Key for Baidu AI content moderation.
> [!TIP]
> To enable Baidu AI content moderation, please `pip install baidu-aip` first.
### `admins_id`
List of administrator IDs. Additionally, you can use `/op` and `/deop` commands to add or remove admins.
### `t2i`
Whether to enable Text-to-Image (T2I) functionality. Default is `false`. When enabled, if a user's message exceeds a certain character count, the bot renders the message as an image to improve readability and prevent spamming. Supports Markdown rendering.
### `t2i_word_threshold`
Character threshold for T2I. Default is `150`. When a message exceeds this count, the bot renders it as an image.
### `t2i_strategy`
Rendering strategy for T2I. Options are `local` and `remote`. Default is `remote`.
- `local`: Uses AstrBot's local T2I service for rendering. Lower quality but doesn't depend on external services.
- `remote`: Uses a remote T2I service for rendering. Uses the official AstrBot service by default, which offers better quality.
### `t2i_endpoint`
AstrBot API address. Used for rendering Markdown images. Effective when `t2i_strategy` is `remote`. Default is empty, meaning the official AstrBot service is used.
### `t2i_use_file_service`
Whether to enable the file service. Default is `false`. When enabled, the bot provides the rendered image as an external HTTP link to the message platform. This depends on the `callback_api_base` configuration.
### `http_proxy`
HTTP proxy. E.g., `http://localhost:7890`.
### `no_proxy`
List of addresses that bypass the proxy. E.g., `["localhost", "127.0.0.1"]`.
### `dashboard`
AstrBot WebUI configuration.
Please do not change the `password` value arbitrarily. It is an `md5` encoded password. Change the password in the control panel.
- `enable`: Whether to enable the AstrBot WebUI. Default is `true`.
- `username`: Username for the AstrBot WebUI. Default is `astrbot`.
- `password`: Password for the AstrBot WebUI. Default is the `md5` encoded value of `astrbot`. Do not modify directly unless you know what you are doing.
- `jwt_secret`: JWT secret key. AstrBot generates this randomly at initialization. Do not modify unless you know what you are doing.
- `host`: Address the AstrBot WebUI listens on. Default is `0.0.0.0`.
- `port`: Port the AstrBot WebUI listens on. Default is `6185`.
### `platform`
> This item only takes effect in `data/cmd_config.json`; AstrBot does not read this from configuration files in the `data/config/` directory.
List of configured AstrBot message platform adapter settings.
### `platform_specific`
Platform-specific settings. Categorized by platform, then by feature group.
#### `platform_specific.<platform>.pre_ack_emoji`
When enabled, AstrBot sends a pre-reply emoji before requesting the LLM to inform the user that the request is being processed. This currently only takes effect in the Lark and Telegram platform adapters.
##### lark
- `enable`: Whether to enable pre-reply emojis for Lark messages. Default is `false`.
- `emojis`: List of pre-reply emojis. Default is `["Typing"]`. Refer to [Emoji Documentation](https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce) for emoji names.
##### telegram
- `enable`: Whether to enable pre-reply emojis for Telegram messages. Default is `false`.
- `emojis`: List of pre-reply emojis. Default is `["✍️"]`. Telegram only supports a fixed set of reactions; refer to [reactions.txt](https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9).
### `wake_prefix`
Wake prefix. Default is `/`. When a message starts with `/`, AstrBot is awakened.
> [!TIP]
> If the awakened session is not in the ID whitelist, AstrBot will not respond.
### `log_level`
Log level. Default is `INFO`. Can be set to `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`.
### `trace_enable`
Whether to enable trace recording. Default is `false`. When enabled, AstrBot records execution traces, which can be viewed on the Trace page of the admin panel.
### `pip_install_arg`
Arguments for `pip install`. E.g., `-i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple`.
### `pypi_index_url`
PyPI index URL. Default is `https://mirrors.aliyun.com/pypi/simple/`.
### `persona`
*This configuration item has been deprecated since v4.0.0. Please use the WebUI to configure personalities.*
List of configured personalities. Each personality contains `id`, `name`, `description`, and `system_prompt` fields.
### `timezone`
Timezone setting. Please fill in an IANA timezone name, such as Asia/Shanghai. If empty, the system default timezone is used. See all timezones at: [IANA Time Zone Database](https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab).
### `callback_api_base`
Base address for the AstrBot API. Used for file services, plugin callbacks, etc. E.g., `http://example.com:6185`. Default is empty, meaning file services and plugin callbacks are disabled.
### `default_kb_collection`
Default knowledge base name. Used for RAG. If empty, no knowledge base is used.
### `plugin_set`
List of enabled plugins. `*` means all available plugins are enabled. Default is `["*"]`.
+51
View File
@@ -0,0 +1,51 @@
---
outline: deep
---
# AstrBot HTTP API
Starting from v4.18.0, AstrBot provides API Key based HTTP APIs for programmatic access.
## Quick Start
1. Create an API key in WebUI - Settings.
2. Include the API key in request headers:
```http
Authorization: Bearer abk_xxx
```
Also supported:
```http
X-API-Key: abk_xxx
```
3. For chat endpoints, `username` is required:
- `POST /api/v1/chat`: request body must include `username`
- `GET /api/v1/chat/sessions`: query params must include `username`
## Common Endpoints
- `POST /api/v1/chat`: send chat message (SSE stream, server generates UUID when `session_id` is omitted)
- `GET /api/v1/chat/sessions`: list sessions for a specific `username` with pagination
- `GET /api/v1/configs`: list available config files
- `POST /api/v1/file`: upload attachment
- `POST /api/v1/im/message`: proactive message via UMO
- `GET /api/v1/im/bots`: list bot/platform IDs
## Example
```bash
curl -N 'http://localhost:6185/api/v1/chat' \
-H 'Authorization: Bearer abk_xxx' \
-H 'Content-Type: application/json' \
-d '{"message":"Hello","username":"alice"}'
```
## Full API Reference
Use the interactive docs:
- https://docs.astrbot.app/scalar.html
+185
View File
@@ -0,0 +1,185 @@
---
outline: deep
---
# 开发一个平台适配器
AstrBot 支持以插件的形式接入平台适配器,你可以自行接入 AstrBot 没有的平台。如飞书、钉钉甚至是哔哩哔哩私信、Minecraft。
我们以一个平台 `FakePlatform` 为例展开讲解。
首先,在插件目录下新增 `fake_platform_adapter.py``fake_platform_event.py` 文件。前者主要是平台适配器的实现,后者是平台事件的定义。
## 平台适配器
假设 FakePlatform 的客户端 SDK 是这样:
```py
import asyncio
class FakeClient():
'''模拟一个消息平台,这里 5 秒钟下发一个消息'''
def __init__(self, token: str, username: str):
self.token = token
self.username = username
# ...
async def start_polling(self):
while True:
await asyncio.sleep(5)
await getattr(self, 'on_message_received')({
'bot_id': '123',
'content': '新消息',
'username': 'zhangsan',
'userid': '123',
'message_id': 'asdhoashd',
'group_id': 'group123',
})
async def send_text(self, to: str, message: str):
print('发了消息:', to, message)
async def send_image(self, to: str, image_path: str):
print('发了消息:', to, image_path)
```
我们创建 `fake_platform_adapter.py`
```py
import asyncio
from astrbot.api.platform import Platform, AstrBotMessage, MessageMember, PlatformMetadata, MessageType
from astrbot.api.event import MessageChain
from astrbot.api.message_components import Plain, Image, Record # 消息链中的组件,可以根据需要导入
from astrbot.core.platform.astr_message_event import MessageSesion
from astrbot.api.platform import register_platform_adapter
from astrbot import logger
from .client import FakeClient
from .fake_platform_event import FakePlatformEvent
# 注册平台适配器。第一个参数为平台名,第二个为描述。第三个为默认配置。
@register_platform_adapter("fake", "fake 适配器", default_config_tmpl={
"token": "your_token",
"username": "bot_username"
})
class FakePlatformAdapter(Platform):
def __init__(self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue) -> None:
super().__init__(event_queue)
self.config = platform_config # 上面的默认配置,用户填写后会传到这里
self.settings = platform_settings # platform_settings 平台设置。
async def send_by_session(self, session: MessageSesion, message_chain: MessageChain):
# 必须实现
await super().send_by_session(session, message_chain)
def meta(self) -> PlatformMetadata:
# 必须实现,直接像下面一样返回即可。
return PlatformMetadata(
"fake",
"fake 适配器",
)
async def run(self):
# 必须实现,这里是主要逻辑。
# FakeClient 是我们自己定义的,这里只是示例。这个是其回调函数
async def on_received(data):
logger.info(data)
abm = await self.convert_message(data=data) # 转换成 AstrBotMessage
await self.handle_msg(abm)
# 初始化 FakeClient
self.client = FakeClient(self.config['token'], self.config['username'])
self.client.on_message_received = on_received
await self.client.start_polling() # 持续监听消息,这是个堵塞方法。
async def convert_message(self, data: dict) -> AstrBotMessage:
# 将平台消息转换成 AstrBotMessage
# 这里就体现了适配程度,不同平台的消息结构不一样,这里需要根据实际情况进行转换。
abm = AstrBotMessage()
abm.type = MessageType.GROUP_MESSAGE # 还有 friend_message,对应私聊。具体平台具体分析。重要!
abm.group_id = data['group_id'] # 如果是私聊,这里可以不填
abm.message_str = data['content'] # 纯文本消息。重要!
abm.sender = MessageMember(user_id=data['userid'], nickname=data['username']) # 发送者。重要!
abm.message = [Plain(text=data['content'])] # 消息链。如果有其他类型的消息,直接 append 即可。重要!
abm.raw_message = data # 原始消息。
abm.self_id = data['bot_id']
abm.session_id = data['userid'] # 会话 ID。重要!
abm.message_id = data['message_id'] # 消息 ID。
return abm
async def handle_msg(self, message: AstrBotMessage):
# 处理消息
message_event = FakePlatformEvent(
message_str=message.message_str,
message_obj=message,
platform_meta=self.meta(),
session_id=message.session_id,
client=self.client
)
self.commit_event(message_event) # 提交事件到事件队列。不要忘记!
```
`fake_platform_event.py`
```py
from astrbot.api.event import AstrMessageEvent, MessageChain
from astrbot.api.platform import AstrBotMessage, PlatformMetadata
from astrbot.api.message_components import Plain, Image
from .client import FakeClient
from astrbot.core.utils.io import download_image_by_url
class FakePlatformEvent(AstrMessageEvent):
def __init__(self, message_str: str, message_obj: AstrBotMessage, platform_meta: PlatformMetadata, session_id: str, client: FakeClient):
super().__init__(message_str, message_obj, platform_meta, session_id)
self.client = client
async def send(self, message: MessageChain):
for i in message.chain: # 遍历消息链
if isinstance(i, Plain): # 如果是文字类型的
await self.client.send_text(to=self.get_sender_id(), message=i.text)
elif isinstance(i, Image): # 如果是图片类型的
img_url = i.file
img_path = ""
# 下面的三个条件可以直接参考一下。
if img_url.startswith("file:///"):
img_path = img_url[8:]
elif i.file and i.file.startswith("http"):
img_path = await download_image_by_url(i.file)
else:
img_path = img_url
# 请善于 Debug
await self.client.send_image(to=self.get_sender_id(), image_path=img_path)
await super().send(message) # 需要最后加上这一段,执行父类的 send 方法。
```
最后,main.py 只需这样,在初始化的时候导入 fake_platform_adapter 模块。装饰器会自动注册。
```py
from astrbot.api.star import Context, Star
class MyPlugin(Star):
def __init__(self, context: Context):
from .fake_platform_adapter import FakePlatformAdapter # noqa
```
搞好后,运行 AstrBot
![image](https://files.astrbot.app/docs/source/images/plugin-platform-adapter/QQ_1738155926221.png)
这里出现了我们创建的 fake。
![image](https://files.astrbot.app/docs/source/images/plugin-platform-adapter/QQ_1738155982211.png)
启动后,可以看到正常工作:
![image](https://files.astrbot.app/docs/source/images/plugin-platform-adapter/QQ_1738156166893.png)
有任何疑问欢迎加群询问~
+1
View File
@@ -0,0 +1 @@
This page has moved to [AstrBot Plugin Development Guide](/en/dev/star/plugin-new).
+489
View File
@@ -0,0 +1,489 @@
# AI
AstrBot provides built-in support for multiple Large Language Model (LLM) providers and offers a unified interface, making it convenient for plugin developers to access various LLM services.
You can use the LLM / Agent interfaces provided by AstrBot to implement your own intelligent agents.
Starting from version `v4.5.7`, we've made significant improvements to the way LLM providers are invoked. We recommend using the new approach, which is more concise and supports additional features. The legacy invocation method remains documented in the previous Chinese-only guide.
## Getting the Chat Model ID for the Current Session
> [!TIP]
> Added in v4.5.7
```py
umo = event.unified_msg_origin
provider_id = await self.context.get_current_chat_provider_id(umo=umo)
```
## Invoking Large Language Models
> [!TIP]
> Added in v4.5.7
```py
llm_resp = await self.context.llm_generate(
chat_provider_id=provider_id, # Chat model ID
prompt="Hello, world!",
)
# print(llm_resp.completion_text) # Get the returned text
```
## Defining Tools
Tools enable large language models to invoke external capabilities.
```py
from pydantic import Field
from pydantic.dataclasses import dataclass
from astrbot.core.agent.run_context import ContextWrapper
from astrbot.core.agent.tool import FunctionTool, ToolExecResult
from astrbot.core.astr_agent_context import AstrAgentContext
@dataclass
class BilibiliTool(FunctionTool[AstrAgentContext]):
name: str = "bilibili_videos" # Tool name
description: str = "A tool to fetch Bilibili videos." # Tool description
parameters: dict = Field(
default_factory=lambda: {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"description": "Keywords to search for Bilibili videos.",
},
},
"required": ["keywords"],
}
)
async def call(
self, context: ContextWrapper[AstrAgentContext], **kwargs
) -> ToolExecResult:
return "1. Video Title: How to Use AstrBot\nVideo Link: xxxxxx"
```
## Invoking Agents
> [!TIP]
> Added in v4.5.7
An Agent can be defined as a combination of system_prompt + tools + llm, enabling more sophisticated intelligent behavior.
After defining the Tool above, you can invoke an Agent as follows:
```py
llm_resp = await self.context.tool_loop_agent(
event=event,
chat_provider_id=prov_id,
prompt="Search for videos related to AstrBot on Bilibili.",
tools=ToolSet([BilibiliTool()]),
max_steps=30, # Maximum agent execution steps
tool_call_timeout=60, # Tool invocation timeout
)
# print(llm_resp.completion_text) # Get the returned text
```
`tool_loop_agent()` method automatically handles the loop of tool invocations and LLM requests until the model stops calling tools or the maximum number of steps is reached.
## Multi-Agent
> [!TIP]
> Added in v4.5.7
Multi-Agent systems decompose complex applications into multiple specialized agents that collaborate to solve problems. Unlike relying on a single agent to handle every step, multi-agent architectures allow smaller, more focused agents to be composed into coordinated workflows. We implement multi-agent systems using the `agent-as-tool` pattern.
In the example below, we define a Main Agent responsible for delegating tasks to different Sub-Agents based on user queries. Each Sub-Agent focuses on specific tasks, such as retrieving weather information.
![multi-agent-example-1](https://files.astrbot.app/docs/en/dev/star/guides/multi-agent-example-1.svg)
Define Tools:
```py
@dataclass
class AssignAgentTool(FunctionTool[AstrAgentContext]):
"""Main agent uses this tool to decide which sub-agent to delegate a task to."""
name: str = "assign_agent"
description: str = "Assign an agent to a task based on the given query"
parameters: dict = field(
default_factory=lambda: {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to call the sub-agent with.",
},
},
"required": ["query"],
}
)
async def call(
self, context: ContextWrapper[AstrAgentContext], **kwargs
) -> str | CallToolResult:
# Here you would implement the actual agent assignment logic.
# For demonstration purposes, we'll return a dummy response.
return "Based on the query, you should assign agent 1."
@dataclass
class WeatherTool(FunctionTool[AstrAgentContext]):
"""In this example, sub agent 1 uses this tool to get weather information."""
name: str = "weather"
description: str = "Get weather information for a location"
parameters: dict = field(
default_factory=lambda: {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city to get weather information for.",
},
},
"required": ["city"],
}
)
async def call(
self, context: ContextWrapper[AstrAgentContext], **kwargs
) -> str | CallToolResult:
city = kwargs["city"]
# Here you would implement the actual weather fetching logic.
# For demonstration purposes, we'll return a dummy response.
return f"The current weather in {city} is sunny with a temperature of 25°C."
@dataclass
class SubAgent1(FunctionTool[AstrAgentContext]):
"""Define a sub-agent as a function tool."""
name: str = "subagent1_name"
description: str = "subagent1_description"
parameters: dict = field(
default_factory=lambda: {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to call the sub-agent with.",
},
},
"required": ["query"],
}
)
async def call(
self, context: ContextWrapper[AstrAgentContext], **kwargs
) -> str | CallToolResult:
ctx = context.context.context
event = context.context.event
logger.info(f"the llm context messages: {context.messages}")
llm_resp = await ctx.tool_loop_agent(
event=event,
chat_provider_id=await ctx.get_current_chat_provider_id(
event.unified_msg_origin
),
prompt=kwargs["query"],
tools=ToolSet([WeatherTool()]),
max_steps=30,
)
return llm_resp.completion_text
@dataclass
class SubAgent2(FunctionTool[AstrAgentContext]):
"""Define a sub-agent as a function tool."""
name: str = "subagent2_name"
description: str = "subagent2_description"
parameters: dict = field(
default_factory=lambda: {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The query to call the sub-agent with.",
},
},
"required": ["query"],
}
)
async def call(
self, context: ContextWrapper[AstrAgentContext], **kwargs
) -> str | CallToolResult:
return "I am useless :(, you shouldn't call me :("
```
Then, similarly, invoke the Agent using the `tool_loop_agent()` method:
```py
@filter.command("test")
async def test(self, event: AstrMessageEvent):
umo = event.unified_msg_origin
prov_id = await self.context.get_current_chat_provider_id(umo)
llm_resp = await self.context.tool_loop_agent(
event=event,
chat_provider_id=prov_id,
prompt="Test calling sub-agent for Beijing's weather information.",
system_prompt=(
"You are the main agent. Your task is to delegate tasks to sub-agents based on user queries."
"Before delegating, use the 'assign_agent' tool to determine which sub-agent is best suited for the task."
),
tools=ToolSet([SubAgent1(), SubAgent2(), AssignAgentTool()]),
max_steps=30,
)
yield event.plain_result(llm_resp.completion_text)
```
## Conversation Manager
### Getting the Current LLM Conversation History for a Session
```py
from astrbot.core.conversation_mgr import Conversation
uid = event.unified_msg_origin
conv_mgr = self.context.conversation_manager
curr_cid = await conv_mgr.get_curr_conversation_id(uid)
conversation = await conv_mgr.get_conversation(uid, curr_cid) # Conversation
```
::: details Conversation 类型定义
```py
@dataclass
class Conversation:
"""The conversation entity representing a chat session."""
platform_id: str
"""The platform ID in AstrBot"""
user_id: str
"""The user ID associated with the conversation."""
cid: str
"""The conversation ID, in UUID format."""
history: str = ""
"""The conversation history as a string."""
title: str | None = ""
"""The title of the conversation. For now, it's only used in WebChat."""
persona_id: str | None = ""
"""The persona ID associated with the conversation."""
created_at: int = 0
"""The timestamp when the conversation was created."""
updated_at: int = 0
"""The timestamp when the conversation was last updated."""
```
:::
### Main Methods
#### `new_conversation`
- **Usage**
Create a new conversation in the current session and automatically switch to it.
- **Arguments**
- `unified_msg_origin: str` In the format `platform_name:message_type:session_id`
- `platform_id: str | None` Platform identifier, defaults to parsing from `unified_msg_origin`
- `content: list[dict] | None` Initial message history
- `title: str | None` Conversation title
- `persona_id: str | None` Associated persona ID
- **Returns**
`str` Newly generated UUID conversation ID
#### `switch_conversation`
- **Usage**
Switch the session to a specified conversation.
- **Arguments**
- `unified_msg_origin: str`
- `conversation_id: str`
- **Returns**
`None`
#### `delete_conversation`
- **Usage**
Delete a conversation from the session; if `conversation_id` is `None`, deletes the current conversation.
- **Arguments**
- `unified_msg_origin: str`
- `conversation_id: str | None`
- **Returns**
`None`
#### `get_curr_conversation_id`
- **Usage**
Get the conversation ID currently in use by the session.
- **Arguments**
- `unified_msg_origin: str`
- **Returns**
`str | None` Current conversation ID, returns `None` if it doesn't exist
#### `get_conversation`
- **Usage**
Get the complete object for a specified conversation; automatically creates it if it doesn't exist and `create_if_not_exists=True`.
- **Arguments**
- `unified_msg_origin: str`
- `conversation_id: str`
- `create_if_not_exists: bool = False`
- **Returns**
`Conversation | None`
#### `get_conversations`
- **Usage**
Retrieve the complete list of conversations for a user or platform.
- **Arguments**
- `unified_msg_origin: str | None` When `None`, does not filter by user
- `platform_id: str | None`
- **Returns**
`List[Conversation]`
#### `update_conversation`
- **Usage**
Update the title, history, or persona_id of a conversation.
- **Arguments**
- `unified_msg_origin: str`
- `conversation_id: str | None` Uses the current conversation when `None`
- `history: list[dict] | None`
- `title: str | None`
- `persona_id: str | None`
- **Returns**
`None`
## Persona Manager
`PersonaManager` is responsible for unified loading, caching, and providing CRUD interfaces for all Personas, while maintaining compatibility with the legacy persona format (v3) from before AstrBot 4.x.
During initialization, it automatically reads all personas from the database and generates v3-compatible data for seamless use with legacy code.
```py
persona_mgr = self.context.persona_manager
```
### Main Methods
#### `get_persona`
- **Usage**
Get persona data by persona ID.
- **Arguments**
- `persona_id: str` Persona ID
- **Returns**
`Persona` Persona data, returns None if it doesn't exist
- **Raises**
`ValueError` Raised when it doesn't exist
#### `get_all_personas`
- **Usage**
Retrieve all personas from the database at once.
- **Returns**
`list[Persona]` Persona list, may be empty
#### `create_persona`
- **Usage**
Create a new persona and immediately write it to the database; automatically refreshes the local cache upon success.
- **Arguments**
- `persona_id: str` New persona ID (unique)
- `system_prompt: str` System prompt
- `begin_dialogs: list[str]` Optional, opening dialogs (even number of entries, alternating user/assistant)
- `tools: list[str]` Optional, list of allowed tools; `None`=all tools, `[]`=disable all
- **Returns**
`Persona` Newly created persona object
- **Raises**
`ValueError` If `persona_id` already exists
#### `update_persona`
- **Usage**
Update any fields of an existing persona and synchronize to database and cache.
- **Arguments**
- `persona_id: str` Persona ID to update
- `system_prompt: str` Optional, new system prompt
- `begin_dialogs: list[str]` Optional, new opening dialogs
- `tools: list[str]` Optional, new tool list; semantics same as `create_persona`
- **Returns**
`Persona` Updated persona object
- **Raises**
`ValueError` If `persona_id` doesn't exist
#### `delete_persona`
- **Usage**
Delete the specified persona and clean up both database and cache.
- **Arguments**
- `persona_id: str` Persona ID to delete
- **Raises**
`ValueError` If `persona_id` doesn't exist
#### `get_default_persona_v3`
- **Usage**
Get the default persona (v3 format) to use based on the current session configuration.
Falls back to `DEFAULT_PERSONALITY` if configuration doesn't specify one or the specified persona doesn't exist.
- **Arguments**
- `umo: str | MessageSession | None` Session identifier, used to read user-level configuration
- **Returns**
`Personality` Default persona object in v3 format
::: details Persona / Personality 类型定义
```py
class Persona(SQLModel, table=True):
"""Persona is a set of instructions for LLMs to follow.
It can be used to customize the behavior of LLMs.
"""
__tablename__ = "personas"
id: int = Field(primary_key=True, sa_column_kwargs={"autoincrement": True})
persona_id: str = Field(max_length=255, nullable=False)
system_prompt: str = Field(sa_type=Text, nullable=False)
begin_dialogs: Optional[list] = Field(default=None, sa_type=JSON)
"""a list of strings, each representing a dialog to start with"""
tools: Optional[list] = Field(default=None, sa_type=JSON)
"""None means use ALL tools for default, empty list means no tools, otherwise a list of tool names."""
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
updated_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
sa_column_kwargs={"onupdate": datetime.now(timezone.utc)},
)
__table_args__ = (
UniqueConstraint(
"persona_id",
name="uix_persona_id",
),
)
class Personality(TypedDict):
"""LLM Persona class.
Starting from v4.0.0 and later, it's recommended to use the Persona class above. Additionally, the mood_imitation_dialogs field has been deprecated.
"""
prompt: str
name: str
begin_dialogs: list[str]
mood_imitation_dialogs: list[str]
"""Mood imitation dialog preset. Deprecated since v4.0.0 and later."""
tools: list[str] | None
"""Tool list. None means use all tools, empty list means don't use any tools"""
```
:::
+48
View File
@@ -0,0 +1,48 @@
# 开发环境准备
## 获取插件模板
1. 打开 AstrBot 插件模板: [helloworld](https://github.com/Soulter/helloworld)
2. 点击右上角的 `Use this template`
3. 然后点击 `Create new repository`
4. 在 `Repository name` 处填写您的插件名。插件名格式:
- 推荐以 `astrbot_plugin_` 开头;
- 不能包含空格;
- 保持全部字母小写;
- 尽量简短。
5. 点击右下角的 `Create repository`
![New repo](https://files.astrbot.app/docs/source/images/plugin/image.png)
## Clone 插件和 AstrBot 项目
Clone AstrBot 项目本体和刚刚创建的插件仓库到本地。
```bash
git clone https://github.com/AstrBotDevs/AstrBot
mkdir -p AstrBot/data/plugins
cd AstrBot/data/plugins
git clone 插件仓库地址
```
然后,使用 `VSCode` 打开 `AstrBot` 项目。找到 `data/plugins/<你的插件名字>` 目录。
更新 `metadata.yaml` 文件,填写插件的元数据信息。
> [!NOTE]
> AstrBot 插件市场的信息展示依赖于 `metadata.yaml` 文件。
## 调试插件
AstrBot 采用在运行时注入插件的机制。因此,在调试插件时,需要启动 AstrBot 本体。
您可以使用 AstrBot 的热重载功能简化开发流程。
插件的代码修改后,可以在 AstrBot WebUI 的插件管理处找到自己的插件,点击右上角 `...` 按钮,选择 `重载插件`
## 插件依赖管理
目前 AstrBot 对插件的依赖管理使用 `pip` 自带的 `requirements.txt` 文件。如果你的插件需要依赖第三方库,请务必在插件目录下创建 `requirements.txt` 文件并写入所使用的依赖库,以防止用户在安装你的插件时出现依赖未找到(Module Not Found)的问题。
> `requirements.txt` 的完整格式可以参考 [pip 官方文档](https://pip.pypa.io/en/stable/reference/requirements-file-format/)。
+66
View File
@@ -0,0 +1,66 @@
# Text to Image
> [!TIP]
> For easier development, you can use the [AstrBot Text2Image Playground](https://t2i-playground.astrbot.app/) for online visual editing and testing of HTML templates.
## Basic Usage
AstrBot supports rendering text into images.
```python
@filter.command("image") # Register an /image command that accepts a text parameter.
async def on_aiocqhttp(self, event: AstrMessageEvent, text: str):
url = await self.text_to_image(text) # text_to_image() is a method of the Star class.
# path = await self.text_to_image(text, return_url = False) # If you want to save the image locally
yield event.image_result(url)
```
![image](https://files.astrbot.app/docs/source/images/plugin/image-3.png)
## Customization (HTML-Based)
If you find the default rendered images insufficiently aesthetic, you can use custom HTML templates to render images.
AstrBot supports rendering text-to-image templates using `HTML + Jinja2`.
```py{7}
# Custom Jinja2 template with CSS support
TMPL = '''
<div style="font-size: 32px;">
<h1 style="color: black">Todo List</h1>
<ul>
{% for item in items %}
<li>{{ item }}</li>
{% endfor %}
</div>
'''
@filter.command("todo")
async def custom_t2i_tmpl(self, event: AstrMessageEvent):
options = {} # Optionally pass rendering options.
url = await self.html_render(TMPL, {"items": ["Eat", "Sleep", "Play Genshin"]}, options=options) # The second parameter is the data for Jinja2 rendering
yield event.image_result(url)
```
The result:
![image](https://files.astrbot.app/docs/source/images/plugin/fcc2dcb472a91b12899f617477adc5c7.png)
This is just a simple example. Thanks to the powerful capabilities of HTML and DOM renderers, you can create more complex and visually appealing designs. Additionally, Jinja2 supports syntax for loops, conditionals, and more to accommodate data structures like lists and dictionaries. You can learn more about Jinja2 online.
**Image Rendering Options (options)**:
Please refer to Playwright's [screenshot](https://playwright.dev/python/docs/api/class-page#page-screenshot) API.
- `timeout` (float, optional): Screenshot timeout duration.
- `type` (Literal["jpeg", "png"], optional): Screenshot image type.
- `quality` (int, optional): Screenshot quality, only applicable to JPEG format images.
- `omit_background` (bool, optional): Whether to hide the default white background, allowing transparent screenshots. Only applicable to PNG format.
- `full_page` (bool, optional): Whether to capture the entire page rather than just the viewport size. Defaults to True.
- `clip` (dict, optional): The region to crop after taking the screenshot. Refer to Playwright's screenshot API.
- `animations`: (Literal["allow", "disabled"], optional): Whether to allow CSS animations to play.
- `caret`: (Literal["hide", "initial"], optional): When set to hide, the text cursor will be hidden during the screenshot. Defaults to hide.
- `scale`: (Literal["css", "device"], optional): Page scaling setting. When set to css, device resolution maps one-to-one with CSS pixels, which may result in smaller screenshots on high-DPI screens. When set to device, scaling is based on the device's screen scaling settings or the device_scale_factor parameter in the current Playwright Page/Context.
@@ -0,0 +1,348 @@
# Handling Message Events
Event listeners can receive message content delivered by the platform and implement features such as commands, command groups, and event listening.
Event listener decorators are located in `astrbot.api.event.filter` and must be imported first. Please make sure to import it, otherwise it will conflict with Python's built-in `filter` higher-order function.
```py
from astrbot.api.event import filter, AstrMessageEvent
```
## Messages and Events
AstrBot receives messages delivered by messaging platforms and encapsulates them as `AstrMessageEvent` objects, which are then passed to plugins for processing.
![message-event](https://files.astrbot.app/docs/en/dev/star/guides/message-event.svg)
### Message Events
`AstrMessageEvent` is AstrBot's message event object, which stores information about the message sender, message content, etc.
### Message Object
`AstrBotMessage` is AstrBot's message object, which stores the specific content of messages delivered by the messaging platform. The `AstrMessageEvent` object contains a `message_obj` attribute to retrieve this message object.
```py{11}
class AstrBotMessage:
'''AstrBot's message object'''
type: MessageType # Message type
self_id: str # Bot's identification ID
session_id: str # Session ID. Depends on the unique_session setting.
message_id: str # Message ID
group_id: str = "" # Group ID, empty if it's a private chat
sender: MessageMember # Sender
message: List[BaseMessageComponent] # Message chain. For example: [Plain("Hello"), At(qq=123456)]
message_str: str # The most straightforward plain text message string, concatenating Plain messages (text messages) from the message chain
raw_message: object
timestamp: int # Message timestamp
```
Here, `raw_message` is the **raw message object** from the messaging platform adapter.
### Message Chain
![message-chain](https://files.astrbot.app/docs/en/dev/star/guides/message-chain.svg)
A `message chain` describes the structure of a message. It's an ordered list where each element is called a `message segment`.
Common message segment types include:
- `Plain`: Text message segment
- `At`: Mention message segment
- `Image`: Image message segment
- `Record`: Audio message segment
- `Video`: Video message segment
- `File`: File message segment
Most messaging platforms support the above message segment types.
Additionally, the OneBot v11 platform (QQ personal accounts, etc.) also supports the following common message segment types:
- `Face`: Emoji message segment
- `Node`: A node in a forward message
- `Nodes`: Multiple nodes in a forward message
- `Poke`: Poke message segment
In AstrBot, message chains are represented as lists of type `List[BaseMessageComponent]`.
## Commands
![message-event-simple-command](https://files.astrbot.app/docs/en/dev/star/guides/message-event-simple-command.svg)
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.api.star import Context, Star
class MyPlugin(Star):
def __init__(self, context: Context):
super().__init__(context)
@filter.command("helloworld") # from astrbot.api.event.filter import command
async def helloworld(self, event: AstrMessageEvent):
'''This is a hello world command'''
user_name = event.get_sender_name()
message_str = event.message_str # Get the plain text content of the message
yield event.plain_result(f"Hello, {user_name}!")
```
> [!TIP]
> Commands cannot contain spaces, otherwise AstrBot will parse them as a second parameter. You can use the command group feature below, or use a listener to parse the message content yourself.
## Commands with Parameters
![command-with-param](https://files.astrbot.app/docs/en/dev/star/guides/command-with-param.svg)
AstrBot will automatically parse command parameters for you.
```python
@filter.command("add")
def add(self, event: AstrMessageEvent, a: int, b: int):
# /add 1 2 -> Result is: 3
yield event.plain_result(f"Wow! The answer is {a + b}!")
```
## Command Groups
Command groups help you organize commands.
```python
@filter.command_group("math")
def math(self):
pass
@math.command("add")
async def add(self, event: AstrMessageEvent, a: int, b: int):
# /math add 1 2 -> Result is: 3
yield event.plain_result(f"Result is: {a + b}")
@math.command("sub")
async def sub(self, event: AstrMessageEvent, a: int, b: int):
# /math sub 1 2 -> Result is: -1
yield event.plain_result(f"Result is: {a - b}")
```
The command group function doesn't need to implement any logic; just use `pass` directly or add comments within the function. Subcommands of the command group are registered using `command_group_name.command`.
When a user doesn't input a subcommand, an error will be reported and the tree structure of the command group will be rendered.
![image](https://files.astrbot.app/docs/source/images/plugin/image-1.png)
![image](https://files.astrbot.app/docs/source/images/plugin/898a169ae7ed0478f41c0a7d14cb4d64.png)
![image](https://files.astrbot.app/docs/source/images/plugin/image-2.png)
Theoretically, command groups can be nested infinitely!
```py
'''
math
├── calc
│ ├── add (a(int),b(int),)
│ ├── sub (a(int),b(int),)
│ ├── help (command with no parameters)
'''
@filter.command_group("math")
def math():
pass
@math.group("calc") # Note: this is group, not command_group
def calc():
pass
@calc.command("add")
async def add(self, event: AstrMessageEvent, a: int, b: int):
yield event.plain_result(f"Result is: {a + b}")
@calc.command("sub")
async def sub(self, event: AstrMessageEvent, a: int, b: int):
yield event.plain_result(f"Result is: {a - b}")
@calc.command("help")
def calc_help(self, event: AstrMessageEvent):
# /math calc help
yield event.plain_result("This is a calculator plugin with add and sub commands.")
```
## Command Aliases
> Available after v3.4.28
You can add different aliases for commands or command groups:
```python
@filter.command("help", alias={'帮助', 'helpme'})
def help(self, event: AstrMessageEvent):
yield event.plain_result("This is a calculator plugin with add and sub commands.")
```
### Event Type Filtering
#### Receive All
This will receive all events.
```python
@filter.event_message_type(filter.EventMessageType.ALL)
async def on_all_message(self, event: AstrMessageEvent):
yield event.plain_result("Received a message.")
```
#### Group Chat and Private Chat
```python
@filter.event_message_type(filter.EventMessageType.PRIVATE_MESSAGE)
async def on_private_message(self, event: AstrMessageEvent):
message_str = event.message_str # Get the plain text content of the message
yield event.plain_result("Received a private message.")
```
`EventMessageType` is an `Enum` type that contains all event types. Current event types are `PRIVATE_MESSAGE` and `GROUP_MESSAGE`.
#### Messaging Platform
```python
@filter.platform_adapter_type(filter.PlatformAdapterType.AIOCQHTTP | filter.PlatformAdapterType.QQOFFICIAL)
async def on_aiocqhttp(self, event: AstrMessageEvent):
'''Only receive messages from AIOCQHTTP and QQOFFICIAL'''
yield event.plain_result("Received a message")
```
In the current version, `PlatformAdapterType` includes `AIOCQHTTP`, `QQOFFICIAL`, `GEWECHAT`, and `ALL`.
#### Admin Commands
```python
@filter.permission_type(filter.PermissionType.ADMIN)
@filter.command("test")
async def test(self, event: AstrMessageEvent):
pass
```
Only admins can use the `test` command.
### Multiple Filters
Multiple filters can be used simultaneously by adding multiple decorators to a function. Filters use `AND` logic, meaning the function will only execute if all filters pass.
```python
@filter.command("helloworld")
@filter.event_message_type(filter.EventMessageType.PRIVATE_MESSAGE)
async def helloworld(self, event: AstrMessageEvent):
yield event.plain_result("Hello!")
```
### Event Hooks
> [!TIP]
> Event hooks do not support being used together with @filter.command, @filter.command_group, @filter.event_message_type, @filter.platform_adapter_type, or @filter.permission_type.
#### On Bot Initialization Complete
> Available after v3.4.34
```python
from astrbot.api.event import filter, AstrMessageEvent
@filter.on_astrbot_loaded()
async def on_astrbot_loaded(self):
print("AstrBot initialization complete")
```
#### On LLM Request
In AstrBot's default execution flow, the `on_llm_request` hook is triggered before calling the LLM.
You can obtain the `ProviderRequest` object and modify it.
The ProviderRequest object contains all information about the LLM request, including the request text, system prompt, etc.
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.api.provider import ProviderRequest
@filter.on_llm_request()
async def my_custom_hook_1(self, event: AstrMessageEvent, req: ProviderRequest): # Note there are three parameters
print(req) # Print the request text
req.system_prompt += "Custom system_prompt"
```
> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly.
#### On LLM Response Complete
After the LLM request completes, the `on_llm_response` hook is triggered.
You can obtain the `ProviderResponse` object and modify it.
```python
from astrbot.api.event import filter, AstrMessageEvent
from astrbot.api.provider import LLMResponse
@filter.on_llm_response()
async def on_llm_resp(self, event: AstrMessageEvent, resp: LLMResponse): # Note there are three parameters
print(resp)
```
> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly.
#### Before Sending Message
Before sending a message, the `on_decorating_result` hook is triggered.
You can implement some message decoration here, such as converting to voice, converting to image, adding prefixes, etc.
```python
from astrbot.api.event import filter, AstrMessageEvent
@filter.on_decorating_result()
async def on_decorating_result(self, event: AstrMessageEvent):
result = event.get_result()
chain = result.chain
print(chain) # Print the message chain
chain.append(Plain("!")) # Add an exclamation mark at the end of the message chain
```
> You cannot use yield to send messages here. This hook is only for decorating event.get_result().chain. If you need to send, please use the `event.send()` method directly.
#### After Message Sent
After a message is sent to the messaging platform, the `after_message_sent` hook is triggered.
```python
from astrbot.api.event import filter, AstrMessageEvent
@filter.after_message_sent()
async def after_message_sent(self, event: AstrMessageEvent):
pass
```
> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly.
### Priority
Commands, event listeners, and event hooks can have priority set to execute before other commands, listeners, or hooks. The default priority is `0`.
```python
@filter.command("helloworld", priority=1)
async def helloworld(self, event: AstrMessageEvent):
yield event.plain_result("Hello!")
```
## Controlling Event Propagation
```python{6}
@filter.command("check_ok")
async def check_ok(self, event: AstrMessageEvent):
ok = self.check() # Your own logic
if not ok:
yield event.plain_result("Check failed")
event.stop_event() # Stop event propagation
```
When event propagation is stopped, all subsequent steps will not be executed.
Assuming there's a plugin A, after A terminates event propagation, all subsequent operations will not be executed, such as executing other plugins' handlers or requesting the LLM.
+211
View File
@@ -0,0 +1,211 @@
# Plugin Configuration
As plugin functionality grows, you may need to define configurations to allow users to customize plugin behavior.
AstrBot provides "powerful" configuration parsing and visualization features. Users can configure plugins directly in the management panel without modifying code.
## Configuration Definition
To register configurations, first add a `_conf_schema.json` JSON file in your plugin directory.
The file content is a `Schema` that represents the configuration. The Schema is in JSON format, for example:
```json
{
"token": {
"description": "Bot Token",
"type": "string",
},
"sub_config": {
"description": "Test nested configuration",
"type": "object",
"hint": "xxxx",
"items": {
"name": {
"description": "testsub",
"type": "string",
"hint": "xxxx"
},
"id": {
"description": "testsub",
"type": "int",
"hint": "xxxx"
},
"time": {
"description": "testsub",
"type": "int",
"hint": "xxxx",
"default": 123
}
}
}
}
```
- `type`: **Required**. The type of the configuration. Supports `string`, `text`, `int`, `float`, `bool`, `object`, `list`, `dict`, `template_list`, `file`. When the type is `text`, it will be visualized as a larger resizable textarea component to accommodate large text.
- `description`: Optional. Description of the configuration. A one-sentence description of the configuration's behavior is recommended.
- `hint`: Optional. Hint information for the configuration, displayed in the question mark button on the right in the image above, shown when hovering over it.
- `obvious_hint`: Optional. Whether the configuration hint should be prominently displayed, like `token` in the image above.
- `default`: Optional. The default value of the configuration. If the user hasn't configured it, the default value will be used. Default values: int is 0, float is 0.0, bool is False, string is "", object is {}, list is [].
- `items`: Optional. If the configuration type is `object`, the `items` field needs to be added. The content of `items` is the sub-Schema of this configuration item. Theoretically, it can be nested infinitely, but excessive nesting is not recommended.
- `invisible`: Optional. Whether the configuration is hidden. Default is `false`. If set to `true`, it will not be displayed in the management panel.
- `options`: Optional. A list, such as `"options": ["chat", "agent", "workflow"]`. Provides dropdown list options.
- `editor_mode`: Optional. Whether to enable code editor mode. Requires AstrBot >= `v3.5.10`. Versions below this won't report errors but won't take effect. Default is false.
- `editor_language`: Optional. The code language for the code editor, defaults to `json`.
- `editor_theme`: Optional. The theme for the code editor. Options are `vs-light` (default) and `vs-dark`.
- `_special`: Optional. Used to call AstrBot's visualization features for provider selection, persona selection, knowledge base selection, etc. See details below.
When the code editor is enabled, it looks like this:
![editor_mode](https://files.astrbot.app/docs/source/images/plugin/image-6.png)
![editor_mode_fullscreen](https://files.astrbot.app/docs/source/images/plugin/image-7.png)
The **_special** field is only available after v4.0.0. Currently supports `select_provider`, `select_provider_tts`, `select_provider_stt`, `select_persona`, allowing users to quickly select model providers, personas, and other data already configured in the WebUI. Results are all strings. Using select_provider as an example, it will present the following effect:
![image](https://files.astrbot.app/docs/source/images/plugin/image-select-provider.png)
### `file` type schema
Introduced in v4.13.0, this allows plugins to define file-upload configuration items to guide users to upload files required by the plugin.
```json
{
"demo_files": {
"type": "file",
"description": "Uploaded files for demo",
"default": [],
"file_types": ["pdf", "docx"]
}
}
```
### `dict` type schema
Used to visualize editing a Python `dict` type configuration. For example, AstrBot Core's custom extra body parameter configuration:
```py
"custom_extra_body": {
"description": "Custom request body parameters",
"type": "dict",
"items": {},
"hint": "Used to add extra parameters to requests, such as temperature, top_p, max_tokens, etc.",
"template_schema": {
"temperature": {
"name": "Temperature",
"description": "Temperature parameter",
"hint": "Controls randomness of output, typically 0-2. Higher is more random.",
"type": "float",
"default": 0.6,
"slider": {"min": 0, "max": 2, "step": 0.1},
},
"top_p": {
"name": "Top-p",
"description": "Top-p sampling",
"hint": "Nucleus sampling parameter, typically 0-1. Controls probability mass considered.",
"type": "float",
"default": 1.0,
"slider": {"min": 0, "max": 1, "step": 0.01},
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum tokens",
"hint": "Maximum number of tokens to generate.",
"type": "int",
"default": 8192,
},
},
}
```
### `template_list` type schema
> [!NOTE]
> Introduced in v4.10.4. For more details see: [#4208](https://github.com/AstrBotDevs/AstrBot/pull/4208)
Plugin developers can add a template-style configuration to `_conf_schema` in the following format (somewhat similar to nested configs):
```json
"field_id": {
"type": "template_list",
"description": "Template List Field",
"templates": {
"template_1": {
"name": "Template One",
"hint":"hint",
"items": {
"attr_a": {
"description": "Attribute A",
"type": "int",
"default": 10
},
"attr_b": {
"description": "Attribute B",
"hint": "This is a boolean attribute",
"type": "bool",
"default": true
}
}
},
"template_2": {
"name": "Template Two",
"hint":"hint",
"items": {
"attr_c": {
"description": "Attribute A",
"type": "int",
"default": 10
},
"attr_d": {
"description": "Attribute B",
"hint": "This is a boolean attribute",
"type": "bool",
"default": true
}
}
}
}
}
```
Saved config example:
```json
"field_id": [
{
"__template_key": "template_1",
"attr_a": 10,
"attr_b": true
},
{
"__template_key": "template_2",
"attr_c": 10,
"attr_d": true
}
]
```
<img width="1000" alt="image" src="https://github.com/user-attachments/assets/74876d30-11a4-491b-a7a0-8ebe8d603782" />
## Using Configuration in Plugins
When loading plugins, AstrBot will check if there's a `_conf_schema.json` file in the plugin directory. If it exists, it will automatically parse the configuration and save it under `data/config/<plugin_name>_config.json` (a configuration file entity created according to the Schema), and pass it to `__init__()` when instantiating the plugin class.
```py
from astrbot.api import AstrBotConfig
class ConfigPlugin(Star):
def __init__(self, context: Context, config: AstrBotConfig): # AstrBotConfig inherits from Dict and has all dictionary methods
super().__init__(context)
self.config = config
print(self.config)
# Supports direct configuration saving
# self.config.save_config() # Save configuration
```
## Configuration Updates
When you update the Schema across different versions, AstrBot will recursively inspect the configuration items in the Schema, automatically adding default values for missing items and removing those that no longer exist.
+131
View File
@@ -0,0 +1,131 @@
# Sending Messages
## Passive Messages
Passive messages refer to the bot responding to messages reactively.
```python
@filter.command("helloworld")
async def helloworld(self, event: AstrMessageEvent):
yield event.plain_result("Hello!")
yield event.plain_result("你好!")
yield event.image_result("path/to/image.jpg") # Send an image
yield event.image_result("https://example.com/image.jpg") # Send an image from URL, must start with http or https
```
## Active Messages
Active messages refer to the bot proactively pushing messages. Some platforms may not support active message sending.
For scheduled tasks or when you don't want to send messages immediately, you can use `event.unified_msg_origin` to get a string and store it, then use `self.context.send_message(unified_msg_origin, chains)` to send messages when needed.
```python
from astrbot.api.event import MessageChain
@filter.command("helloworld")
async def helloworld(self, event: AstrMessageEvent):
umo = event.unified_msg_origin
message_chain = MessageChain().message("Hello!").file_image("path/to/image.jpg")
await self.context.send_message(event.unified_msg_origin, message_chain)
```
With this feature, you can store the `unified_msg_origin` and send messages when needed.
> [!TIP]
> About unified_msg_origin.
> `unified_msg_origin` is a string that records the unique ID of a session. AstrBot uses it to identify which messaging platform and which session it belongs to. This allows messages to be sent to the correct session when using `send_message`. For more about MessageChain, see the next section.
## Rich Media Messages
AstrBot supports sending rich media messages such as images, audio, videos, etc. Use `MessageChain` to construct messages.
```python
import astrbot.api.message_components as Comp
@filter.command("helloworld")
async def helloworld(self, event: AstrMessageEvent):
chain = [
Comp.At(qq=event.get_sender_id()), # Mention the message sender
Comp.Plain("Check out this image:"),
Comp.Image.fromURL("https://example.com/image.jpg"), # Send image from URL
Comp.Image.fromFileSystem("path/to/image.jpg"), # Send image from local file system
Comp.Plain("This is an image.")
]
yield event.chain_result(chain)
```
The above constructs a `message chain`, which will ultimately send a message containing both images and text while preserving the order.
> [!TIP]
> In the aiocqhttp message adapter, for messages of type `plain`, the `strip()` method is used during sending to remove spaces and line breaks. You can add zero-width spaces `\u200b` before and after the message to resolve this issue.
Similarly,
**File**
```py
Comp.File(file="path/to/file.txt", name="file.txt") # Not supported by some platforms
```
**Audio Record**
```py
path = "path/to/record.wav" # Currently only accepts wav format, please convert other formats yourself
Comp.Record(file=path, url=path)
```
**Video**
```py
path = "path/to/video.mp4"
Comp.Video.fromFileSystem(path=path)
Comp.Video.fromURL(url="https://example.com/video.mp4")
```
## Sending Video Messages
```python
from astrbot.api.event import filter, AstrMessageEvent
@filter.command("test")
async def test(self, event: AstrMessageEvent):
from astrbot.api.message_components import Video
# fromFileSystem requires the user's protocol client and bot to be on the same system.
music = Video.fromFileSystem(
path="test.mp4"
)
# More universal approach
music = Video.fromURL(
url="https://example.com/video.mp4"
)
yield event.chain_result([music])
```
![Sending video messages](https://files.astrbot.app/docs/source/images/plugin/db93a2bb-671c-4332-b8ba-9a91c35623c2.png)
## Sending Group Forward Messages
> Most platforms do not support this message type. Current support: OneBot v11
You can send group forward messages as follows.
```py
from astrbot.api.event import filter, AstrMessageEvent
@filter.command("test")
async def test(self, event: AstrMessageEvent):
from astrbot.api.message_components import Node, Plain, Image
node = Node(
uin=905617992,
name="Soulter",
content=[
Plain("hi"),
Image.fromFileSystem("test.jpg")
]
)
yield event.chain_result([node])
```
![Sending group forward messages](https://files.astrbot.app/docs/source/images/plugin/image-4.png)
+113
View File
@@ -0,0 +1,113 @@
# Session Control
> v3.4.36 and above
Why do we need session control? Consider a Chinese idiom chain game plugin where a user or group needs to have multiple conversations with the bot rather than a one-time command. This is when session control becomes necessary.
```txt
User: /idiom-chain
Bot: Please send an idiom
User: One horse takes the lead (一马当先)
Bot: Foresight (先见之明)
User: Keen observation (明察秋毫)
...
```
AstrBot provides out-of-the-box session control functionality:
Import:
```py
import astrbot.api.message_components as Comp
from astrbot.core.utils.session_waiter import (
session_waiter,
SessionController,
)
```
Code within the handler can be written as follows:
```python
from astrbot.api.event import filter, AstrMessageEvent
@filter.command("idiom-chain")
async def handle_empty_mention(self, event: AstrMessageEvent):
"""Idiom chain game implementation"""
try:
yield event.plain_result("Please send an idiom~")
# How to use the session controller
@session_waiter(timeout=60, record_history_chains=False) # Register a session controller with a 60-second timeout, without recording message history
async def empty_mention_waiter(controller: SessionController, event: AstrMessageEvent):
idiom = event.message_str # The idiom sent by the user, e.g., "one horse takes the lead"
if idiom == "exit": # If the user wants to exit the idiom chain game by typing "exit"
await event.send(event.plain_result("Exited the idiom chain game~"))
controller.stop() # Stop the session controller, which will end immediately.
return
if len(idiom) != 4: # If the user's input is not a 4-character idiom
await event.send(event.plain_result("The idiom must be four characters~")) # Send a reply, cannot use yield
return
# Exit the current method without executing subsequent logic, but the session is not interrupted; subsequent user input will still enter the current session
# ...
message_result = event.make_result()
message_result.chain = [Comp.Plain("Foresight")] # import astrbot.api.message_components as Comp
await event.send(message_result) # Send a reply, cannot use yield
controller.keep(timeout=60, reset_timeout=True) # Reset timeout to 60s. If not reset, it will continue the previous timeout countdown.
# controller.stop() # Stop the session controller, which will end immediately.
# If history chains are recorded, you can retrieve them via controller.get_history_chains()
try:
await empty_mention_waiter(event)
except TimeoutError as _: # When timeout occurs, the session controller will raise TimeoutError
yield event.plain_result("You timed out!")
except Exception as e:
yield event.plain_result("An error occurred, please contact the administrator: " + str(e))
finally:
event.stop_event()
except Exception as e:
logger.error("handle_empty_mention error: " + str(e))
```
Once the session controller is activated, messages subsequently sent by that sender will first be processed by the `empty_mention_waiter` function you defined above, until the session controller is stopped or times out.
## SessionController
Used by developers to control whether a session should end, and to retrieve message history chains.
- keep(): Keep this session alive
- timeout (float): Required. Session timeout duration.
- reset_timeout (bool): When set to True, it resets the timeout; timeout must be > 0, if <= 0 the session ends immediately. When set to False, it maintains the original timeout; new timeout = remaining timeout + timeout (can be < 0)
- stop(): End this session
- get_history_chains() -> List[List[Comp.BaseMessageComponent]]: Retrieve message history chains
## Custom Session ID Filter
By default, the AstrBot session controller uses `sender_id` (the sender's ID) as the identifier for distinguishing different sessions. If you want to treat an entire group as one session, you need to customize the session ID filter.
```py
import astrbot.api.message_components as Comp
from astrbot.core.utils.session_waiter import (
session_waiter,
SessionFilter,
SessionController,
)
# Using the handler from above
# ...
class CustomFilter(SessionFilter):
def filter(self, event: AstrMessageEvent) -> str:
return event.get_group_id() if event.get_group_id() else event.unified_msg_origin
await empty_mention_waiter(event, session_filter=CustomFilter()) # Pass in session_filter here
# ...
```
After this setup, when a user in a group sends a message, the session controller will treat the entire group as one session, and messages from other users in the group will also be considered part of the same session.
You can even use this feature to enable team-based activities within groups!
+58
View File
@@ -0,0 +1,58 @@
# Minimal Example
The `main.py` file in the plugin template is a minimal plugin instance.
```python
from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult
from astrbot.api.star import Context, Star
from astrbot.api import logger # Use the logger interface provided by AstrBot
class MyPlugin(Star):
def __init__(self, context: Context):
super().__init__(context)
# Decorator to register a command. The command name is "helloworld". Once registered, sending `/helloworld` will trigger this command and respond with `Hello, {user_name}!`
@filter.command("helloworld")
async def helloworld(self, event: AstrMessageEvent):
'''This is a hello world command''' # This is the handler's description, which will be parsed to help users understand the plugin's functionality. Highly recommended to provide.
user_name = event.get_sender_name()
message_str = event.message_str # Get the plain text content of the message
logger.info("Hello world command triggered!")
yield event.plain_result(f"Hello, {user_name}!") # Send a plain text message
async def terminate(self):
'''Optionally implement the terminate function, which will be called when the plugin is uninstalled/disabled.'''
```
Explanation:
- Plugins must inherit from the `Star` class.
- The `Context` class is used for plugin interaction with AstrBot Core, allowing you to call various APIs provided by AstrBot Core.
- Specific handler functions are defined within the plugin class, such as the `helloworld` function here.
- `AstrMessageEvent` is AstrBot's message event object, which stores information about the message sender, message content, etc.
- `AstrBotMessage` is AstrBot's message object, which stores the specific content of messages delivered by the messaging platform. It can be accessed via `event.message_obj`.
> [!TIP]
>
> Handlers must be registered within the plugin class, with the first two parameters being `self` and `event`. If the file becomes too long, you can write services externally and call them from the handler.
>
> The file containing the plugin class must be named `main.py`.
All handler functions must be written within the plugin class. To keep content concise, in subsequent sections, we may omit the plugin class definition.
```
解释如下:
- 插件需要继承 `Star` 类。
- `Context` 类用于插件与 AstrBot Core 交互,可以由此调用 AstrBot Core 提供的各种 API。
- 具体的处理函数 `Handler` 在插件类中定义,如这里的 `helloworld` 函数。
- `AstrMessageEvent` 是 AstrBot 的消息事件对象,存储了消息发送者、消息内容等信息。
- `AstrBotMessage` 是 AstrBot 的消息对象,存储了消息平台下发的消息的具体内容。可以通过 `event.message_obj` 获取。
> [!TIP]
>
> `Handler` 一定需要在插件类中注册,前两个参数必须为 `self``event`。如果文件行数过长,可以将服务写在外部,然后在 `Handler` 中调用。
>
> 插件类所在的文件名需要命名为 `main.py`
所有的处理函数都需写在插件类中。为了精简内容,在之后的章节中,我们可能会忽略插件类的定义。
+31
View File
@@ -0,0 +1,31 @@
# Plugin Storage
## Simple KV Storage
> [!TIP]
> Requires AstrBot version >= 4.9.2.
Plugins can use AstrBot's simple key-value store to persist configuration or temporary data. The storage is scoped per plugin, so each plugin has its own isolated space.
```py
class Main(star.Star):
@filter.command("hello")
async def hello(self, event: AstrMessageEvent):
"""Aloha!"""
await self.put_kv_data("greeted", True)
greeted = await self.get_kv_data("greeted", False)
await self.delete_kv_data("greeted")
```
## Large File Storage Convention
To keep large file handling consistent, store large files under `data/plugin_data/{plugin_name}/`.
You can fetch the plugin data directory with:
```py
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
plugin_data_path = get_astrbot_data_path() / "plugin_data" / self.name # self.name is the plugin name; available in v4.9.2 and above. For lower versions, specify the plugin name yourself.
```
+128
View File
@@ -0,0 +1,128 @@
---
outline: deep
---
# AstrBot Plugin Development Guide 🌠
Welcome to the AstrBot Plugin Development Guide! This section will guide you through developing AstrBot plugins. Before we begin, we hope you have the following foundational knowledge:
1. Some experience with Python programming.
2. Some experience with Git and GitHub.
## Environment Setup
### Obtain the Plugin Template
1. Open the AstrBot plugin template: [helloworld](https://github.com/Soulter/helloworld)
2. Click `Use this template` in the upper right corner
3. Then click `Create new repository`.
4. Fill in your plugin name in the `Repository name` field. Plugin naming conventions:
- Recommended to start with `astrbot_plugin_`;
- Must not contain spaces;
- Keep all letters lowercase;
- Keep it concise.
5. Click `Create repository` in the lower right corner.
### Clone the Project Locally
Clone both the AstrBot main project and the plugin repository you just created to your local machine.
```bash
git clone https://github.com/AstrBotDevs/AstrBot
mkdir -p AstrBot/data/plugins
cd AstrBot/data/plugins
git clone <your-plugin-repository-url>
```
Then, use `VSCode` to open the `AstrBot` project. Navigate to the `data/plugins/<your-plugin-name>` directory.
Update the `metadata.yaml` file with your plugin's metadata information.
> [!WARNING]
> Please make sure to modify this file, as AstrBot relies on the `metadata.yaml` file to recognize plugin metadata.
### Set Plugin Logo (Optional)
You can add a `logo.png` file in the plugin directory as the plugin's logo. Please maintain an aspect ratio of 1:1, with a recommended size of 256x256.
![Plugin logo example](https://files.astrbot.app/docs/source/images/plugin/plugin_logo.png)
### Plugin Display Name (Optional)
You can modify (or add) the `display_name` field in the `metadata.yaml` file to serve as the plugin's display name in scenarios like the plugin marketplace, making it easier for users to read.
### Declare Supported Platforms (Optional)
You can add a `support_platforms` field (`list[str]`) to `metadata.yaml` to declare which platform adapters your plugin supports. The WebUI plugin page will display this field.
```yaml
support_platforms:
- telegram
- discord
```
The values in `support_platforms` must be keys from `ADAPTER_NAME_2_TYPE`. Currently supported:
- `aiocqhttp`
- `qq_official`
- `telegram`
- `wecom`
- `lark`
- `dingtalk`
- `discord`
- `slack`
- `kook`
- `vocechat`
- `weixin_official_account`
- `satori`
- `misskey`
- `line`
### Declare AstrBot Version Range (Optional)
You can add an `astrbot_version` field in `metadata.yaml` to declare the required AstrBot version range for your plugin. The format follows dependency specifiers in `pyproject.toml` (PEP 440), and must not include a `v` prefix.
```yaml
astrbot_version: ">=4.16,<5"
```
Examples:
- `>=4.17.0`
- `>=4.16,<5`
- `~=4.17`
If you only want to declare a minimum version, use:
- `>=4.17.0`
If the current AstrBot version does not satisfy this range, the plugin will be blocked from loading with a compatibility error.
In the WebUI installation flow, you can choose to "Ignore Warning and Install" to bypass this check.
### Debugging Plugins
AstrBot uses a runtime plugin injection mechanism. Therefore, when debugging plugins, you need to start the AstrBot main application.
You can use AstrBot's hot reload feature to streamline the development process.
After modifying the plugin code, you can find your plugin in the AstrBot WebUI's plugin management section, click the `...` button in the upper right corner, and select `Reload Plugin`.
If the plugin fails to load due to code errors or other reasons, you can also click **"Try one-click reload fix"** in the error prompt on the admin panel to reload it.
### Plugin Dependency Management
Currently, AstrBot manages plugin dependencies using pip's built-in `requirements.txt` file. If your plugin requires third-party libraries, please be sure to create a `requirements.txt` file in the plugin directory and list the dependencies used, to prevent Module Not Found errors when users install your plugin.
> For the complete format of `requirements.txt`, please refer to the [pip official documentation](https://pip.pypa.io/en/stable/reference/requirements-file-format/).
## Development Principles
Thank you for contributing to the AstrBot ecosystem. Please follow these principles when developing plugins, which are also good programming practices:
- Features must be tested.
- Include comprehensive comments.
- Store persistent data in the `data` directory, not in the plugin's own directory, to prevent data loss when updating/reinstalling the plugin.
- Implement robust error handling mechanisms; don't let a single error crash the plugin.
- Before committing, please use the [ruff](https://docs.astral.sh/ruff/) tool to format your code.
- Do not use the `requests` library for network requests; use asynchronous network request libraries such as `aiohttp` or `httpx`.
- If you're extending functionality for an existing plugin, please prioritize submitting a PR to that plugin rather than creating a separate one (unless the original plugin author has stopped maintaining it).
+9
View File
@@ -0,0 +1,9 @@
# Publishing Plugins to the Plugin Marketplace
After completing your plugin development, you can choose to publish it to the AstrBot Plugin Marketplace, allowing more users to benefit from your work.
AstrBot uses GitHub to host plugins, so you'll need to push your plugin code to the GitHub plugin repository you created earlier.
You can submit your plugin by visiting the [AstrBot Plugin Marketplace](https://plugins.astrbot.app). Once on the website, click the `+` button in the bottom-right corner, fill in the basic information, author details, repository information, and other required fields. Then click the `Submit to GITHUB` button. You will be redirected to the AstrBot repository's Issue submission page. Please verify that all information is correct, then click the `Create` button to complete the plugin publication process.
![fill out the form](https://files.astrbot.app/docs/source/images/plugin-publish/image.png)
+79
View File
@@ -0,0 +1,79 @@
# FAQ
## Dashboard Related
### Encountering 404 Error When Opening the Dashboard
Download `dist.zip` from the [release](https://github.com/AstrBotDevs/AstrBot/releases) page, extract it, and move it to `AstrBot/data`. If it still doesn't work, try restarting your computer (based on community feedback).
### Forgot Dashboard Password
If you forgot your AstrBot dashboard password, you can modify the `"dashboard"` field in the `AstrBot/data/cmd_config.json` configuration file, where `"username"` is your username and `"password"` is your password encrypted with MD5.
To modify your account credentials, follow these steps:
1. Modify the `"username"` field, keeping the `""` quotation marks. If you don't want to change the username, skip this step
2. Visit the website: [Online MD5 Generator](https://www.metools.info/code/c26.html)
3. Enter your new password in the input text box
4. Select MD5 encryption (32-bit), make sure to choose the 32-bit option
5. Paste the converted string into the configuration file, keeping the `""` quotation marks
## Bot Core Related
### How to Let AstrBot Control My Mac / Windows / Linux Computer?
1. In AstrBot WebUI's `Config -> General Config`, find `Use Computer Capabilities`, and select `local` for the runtime environment.
2. In `Config -> Other Config`, find `Admin ID List`, and add your user ID (you can get it through the `/sid` command).
> [!TIP]
> For security reasons, when runtime environment is set to `local`, AstrBot only allows AstrBot administrators to use computer capabilities by default.
> You can select `sandbox` for the runtime environment, which allows all users to use computer capabilities (in an isolated sandbox). For more details, see [AstrBot Sandbox Environment](/en/use/astrbot-agent-sandbox.md)
### Bot Cannot Chat in Group Conversations
1. In group chats, to prevent message flooding, the bot will not respond to every monitored message. Please try mentioning (@) the bot or using a wake word to chat, such as the default `/`, for example: `/hello`.
### No Permission to Execute Admin Commands
1. `/reset, /persona, /dashboard_update, /op, /deop, /wl, /dewl` are the default admin commands. You can use the `/sid` command to get a user's ID, then add it to the admin ID list in Settings -> Other Settings.
### Chinese Characters Garbled When Locally Rendering Markdown Images (t2i)
You can customize the font. See details -> [#957](https://github.com/AstrBotDevs/AstrBot/issues/957#issuecomment-2749981802)
Recommended font: [Maple Mono](https://github.com/subframe7536/maple-font).
### Cannot Parse API Returned Completion & LLM Returns `<empty content>`
This is because the provider's API returned empty text. Try the following steps:
1. Check if the API key is still valid
2. Check if the API call limit or quota has been reached
3. Check network connection
4. Try reset
5. Lower the maximum conversation count setting
6. Switch to another model from the same provider / a different provider
## Plugin Related
### Cannot Install Plugin
1. Plugins are installed via GitHub. Access to GitHub from mainland China can indeed be unstable. You can use a proxy, then go to Other Settings -> HTTP Proxy to configure it. Alternatively, download the plugin archive directly and upload it.
### Error `No module named 'xxx'` After Installing Plugin
![image](https://files.astrbot.app/docs/source/images/faq/image.png)
This is because the plugin's dependencies were not installed properly. Normally, AstrBot automatically installs plugin dependencies after installing the plugin, but installation may fail in the following situations:
1. Network issues preventing dependency downloads
2. Plugin author did not include a `requirements.txt` file
3. Python version incompatibility
Solution:
Based on the error message, refer to the plugin's README to manually install dependencies. You can install dependencies in the AstrBot WebUI under `Console` -> `Install Pip Package`.
![image](https://files.astrbot.app/docs/source/images/faq/image-1.png)
If you find that the plugin author did not include a `requirements.txt` file, please submit an issue in the plugin repository to remind the author to add it.
+31
View File
@@ -0,0 +1,31 @@
---
# https://vitepress.dev/reference/default-theme-home-page
layout: home
hero:
name: >-
<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; margin-bottom: 16px;" width="250" height="55"/></a>
text: "Agentic AI assistant for personal and group chats"
tagline: Connect any IM / 1000+ plugins / General Agent Orchestration
actions:
- theme: brand
text: Quick Start
link: /en/what-is-astrbot
- theme: alt
text: GitHub Repository
link: https://github.com/AstrBotDevs/AstrBot
features:
- icon: ✨
title: Multi-Platform Support
details: Seamlessly supports multiple messaging platforms including QQ, WeCom, Telegram, Discord, and more with multi-instance deployment.
- icon: 😌
title: User-Friendly
details: Easy deployment via Docker or Windows one-click installer with no complex configuration required. Features a highly visual management dashboard.
- icon: 🧩
title: Highly Extensible
details: Built on event bus and pipeline architecture with full modularity. All features can be enabled or disabled, with comprehensive plugin development support.
- icon: 🌟
title: Large Language Models
details: Compatible with multiple model providers including OpenAI, Anthropic, Google, Ollama, Deepseek, and more, supporting diverse LLM integrations.
---
+31
View File
@@ -0,0 +1,31 @@
# 开源之夏 2025
**开源之夏**是由中国科学院软件研究所“开源软件供应链点亮计划”发起并长期支持的一项暑期开源活动,旨在鼓励在校学生积极参与开源软件的开发维护,培养和发掘更多优秀的开发者,促进优秀开源软件社区的蓬勃发展,助力开源软件供应链建设。具体活动信息请参考 [开源之夏官网](https://summer-ospp.ac.cn/)。
AstrBot 社区有幸作为开源社区参与了本次活动,下面列出了目前我们已经发布的项目,欢迎感兴趣的同学们参与。
## 插件数据存储逻辑优化
目前,AstrBot 插件系统在数据存储方面缺乏一致的架构。部分插件使用 SharedPreference 存储机制和 JSON 格式进行数据持久化。这种多样化的存储方式导致了存储逻辑的不统一,既影响了数据的安全性,也增加了插件间的兼容性问题。此外,缺乏标准化的接口使得插件的数据存储和访问方式各异,给系统的维护和扩展带来挑战。本项目旨在重构当前存储方案,引入更安全且高效的数据存储机制,并设计一个统一的插件数据接口模型,规范插件的数据存储与访问,提升系统的安全性、可扩展性和可维护性,为未来插件的开发与管理提供坚实基础。
**项目链接**[插件数据存储逻辑优化](https://summer-ospp.ac.cn/org/prodetail/253550342?lang=zh&list=pro)
**难度**:进阶
**导师**[Soulter](https://github.com/Soulter)
**期望完成时间**210 小时
**项目产出要求**
1. 设计并实现统一且高效的插件数据存储接口模型,规范插件的数据存储;
2. 重构当前 SharedPreference 的存储逻辑,采用更安全的存储方式;
3. 补充相关技术文档。
**项目技术要求**
1. 熟悉 Python、Javascript 语言及 asyncio 异步编程技术;
2. 熟悉 SQLite 等关系型数据库相关开发;
3. 熟悉 AstrBot 框架及插件开发。
**成果仓库**[https://github.com/AstrBotDevs/AstrBot](https://github.com/AstrBotDevs/AstrBot)
+28
View File
@@ -0,0 +1,28 @@
# Self-host the Text-to-Image Service
AstrBot uses [AstrBotDevs/astrbot-t2i-service](https://github.com/AstrBotDevs/astrbot-t2i-service) as the default text-to-image service. The default service endpoints are:
```plain
https://t2i.soulter.top/text2img
https://t2i.rcfortress.site/text2img
```
This interface can ensure normal response for most of the time. However, due to the deployment of servers in New York, the response speed may be slower in some areas.
> [!TIP]
> If you'd like to support us to help pay for server costs, please consider supporting us on [Afdian](https://afdian.com/a/astrbot_team).
You can choose to self-host the text-to-image service to improve response speed.
```bash
docker run -itd -p 8999:8999 soulter/astrbot-t2i-service:latest
```
After deployment, go to AstrBot Dashboard -> Config -> System, and change `Text-to-Image Service API Endpoint` to the URL you deployed (as shown below).
> If you deployed AstrBot using the Docker tutorial in this documentation, the URL should be `http://<t2i-service-container-name>:8999`.
> If you deployed on the same machine as AstrBot, the URL should be `http://localhost:8999`.
<img width="589" height="255" alt="image" src="https://github.com/user-attachments/assets/5ef09db2-1a33-440c-9986-c7b544325e34" />
+55
View File
@@ -0,0 +1,55 @@
# Connect to Lagrange
> [!TIP]
> - Please control message frequency responsibly. Sending messages too frequently may trigger risk control.
> - This project must not be used for illegal purposes.
> - For the latest deployment steps, always refer to the official [Lagrange Docs](https://lagrangedev.github.io/Lagrange.Doc/Lagrange.OneBot/Config/#%E4%B8%8B%E8%BD%BD%E5%AE%89%E8%A3%85).
## Download
Download the latest `Lagrange.OneBot` from [GitHub Releases](https://github.com/LagrangeDev/Lagrange.Core/releases).
- Windows: `Lagrange.OneBot_win-x64_xxxx`
- Linux x86_64: `Lagrange.OneBot_linux-x64_xxx`
- Linux ARM64: `Lagrange.OneBot_linux-arm64_xxx`
- macOS Apple Silicon: `Lagrange.OneBot_osx-arm64_xxx`
- macOS Intel: `Lagrange.OneBot_osx-x64_xxx`
## Deploy
Follow the official docs:
- Run guide: <https://lagrangedev.github.io/Lagrange.Doc/Lagrange.OneBot/Config/#%E8%BF%90%E8%A1%8C>
- Config file guide: <https://lagrangedev.github.io/Lagrange.Doc/Lagrange.OneBot/Config/#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6>
In your config file, add this under `Implementations`:
```json
{
"Type": "ReverseWebSocket",
"Host": "127.0.0.1",
"Port": 6199,
"Suffix": "/ws",
"ReconnectInterval": 5000,
"HeartBeatInterval": 5000,
"AccessToken": ""
}
```
Make sure `Suffix` is exactly `/ws`.
## Connect to AstrBot
### Configure `aiocqhttp` Adapter
1. Open AstrBot Dashboard.
2. Click `Bots` in the left sidebar.
3. Click `+ Create Bot`.
4. Select `aiocqhttp (OneBot v11)`.
Fill in:
- ID (`id`): any unique identifier.
- Enable (`enable`): checked.
- Reverse WebSocket host: your machine IP (usually `0.0.0.0`).
- Reverse WebSocket port: an available port, for example `6199`.
+141
View File
@@ -0,0 +1,141 @@
# Using NapCat
> [!TIP]
>
> - Please control usage frequency appropriately. Sending messages too frequently may be identified as abnormal behavior, increasing the risk of triggering risk control mechanisms.
> - This project is strictly prohibited from being used for any purpose that violates laws and regulations. If you intend to use AstrBot for illegal industries or activities, we **explicitly oppose and refuse** your use of this project.
> - AstrBot connects to the OneBot v11 protocol through the `aiocqhttp` adapter. OneBot v11 protocol is an open communication protocol and does not represent any specific software or service.
NapCat's GitHub Repository: [NapCat](https://github.com/NapNeko/NapCatQQ)
NapCat's Documentation: [NapCat Documentation](https://napcat.napneko.icu/)
NapCat provides multiple deployment methods, including Docker, Windows one-click installation packages, and more.
## Deploy via One-Click Script
This deployment method is recommended.
### Windows
Refer to this article: [NapCat.Shell - Windows Manual Start Tutorial](https://napneko.github.io/guide/boot/Shell#napcat-shell-win%E6%89%8B%E5%8A%A8%E5%90%AF%E5%8A%A8%E6%95%99%E7%A8%8B)
### Linux
Refer to this article: [NapCat.Installer - Linux One-Click Script (Supports Ubuntu 20+/Debian 10+/Centos9)](https://napneko.github.io/guide/boot/Shell#napcat-installer-linux%E4%B8%80%E9%94%AE%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC-%E6%94%AF%E6%8C%81ubuntu-20-debian-10-centos9)
> [!TIP]
> **Where to open Napcat WebUI**:
> The WebUI link will be displayed in napcat's logs.
>
> If napcat is deployed via Linux command line one-click deployment: `docker log <account>`.
>
> For Docker-deployed NapCat: `docker logs napcat`.
## Deploy via Docker Compose
> [!TIP]
> If deploying with Docker Compose, no configuration is needed on the NapCat side. Just log in via NapCat WebUI (running on port 6099) or `docker logs napcat`, enable the aiocqhttp adapter on the AstrBot side to connect, and you can directly implement normal receiving and sending of `voice data` and `file data`.
1. Download or copy the content of [astrbot.yml](https://github.com/NapNeko/NapCat-Docker/blob/main/compose/astrbot.yml)
2. Rename the downloaded file to `astrbot.yml`
3. Modify `astrbot.yml`, change `#- "6199:6199` to `- "6199:6199"`, remove the flag of "#"
4. Execute in the directory where the `astrbot.yml` file is located:
```bash
NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose -f ./astrbot.yml up -d
```
## Deploy via Docker
> [!TIP]
> If deploying with Docker, you will not be able to properly receive `voice data` and `file data`. This means voice-to-text and sandbox file input functions will not be available. You can receive text messages, image messages, and other types of messages.
This tutorial assumes you have Docker installed.
Execute the following command in the terminal for one-click deployment.
```bash
docker run -d \
-e NAPCAT_GID=$(id -g) \
-e NAPCAT_UID=$(id -u) \
-p 3000:3000 \
-p 3001:3001 \
-p 6099:6099 \
--name napcat \
--restart=always \
mlikiowa/napcat-docker:latest
```
After successful execution, you need to check the logs to get the login QR code and the management panel URL.
```bash
docker logs napcat
```
Please copy the management panel URL and open it in your browser.
Then use the account you want to log in with to scan the QR code that appears.
If there are no issues during the login stage, deployment is successful.
## Connect to AstrBot
## Configure aiocqhttp in AstrBot
1. Enter AstrBot's management panel
2. Click `Bots` in the left sidebar
3. Then in the interface on the right, click `+ Create Bot`
4. Select `OneBot v11`
Fill in the configuration items that appear:
- ID(id): Fill in arbitrarily, only used to distinguish different messaging platform instances.
- Enable: Check this.
- Reverse WebSocket Host Address: Please fill in your machine's IP address, generally fill in `0.0.0.0` directly
- Reverse WebSocket Port: Fill in a port, default is `6199`.
- Reverse Websocket Token: Only needs to be filled when a token is configured in NapCat's network settings.
Example image: (At the fastest, just check Enable, then save)
<img width="818" height="799" alt="xinjianya" src="https://github.com/user-attachments/assets/813ac338-2fd7-4add-bde4-8b0f6d0bda95" />
Click `Save`.
### Configure Administrator
After filling in, go to the `Configuration File` page, click the `Platform Configuration` tab, find `Administrator ID`, and fill in your account number (not the bot's account number).
Remember to click `Save` in the lower right corner, AstrBot will restart and apply the configuration.
### Add WebSocket Client in NapCat
Switch back to NapCat's management panel, click `Network Configuration->New->WebSockets Client`.
<img width="649" height="751" alt="jiaochenXJY" src="https://github.com/user-attachments/assets/5044f96a-a81f-407a-a3b1-0c518499eda4" />
In the newly opened window:
- Check `Enable`.
- Fill in `URL` with `ws://HostIP:Port/ws`. For example, `ws://localhost:6199/ws` or `ws://127.0.0.1:6199/ws`.
> [!IMPORTANT]
> 1. If deploying with Docker and both AstrBot and NapCat containers are connected to the same network, use `ws://astrbot:6199/ws` (refer to the Docker script in this documentation).
> 2. Due to Docker network isolation, when not on the same network, please use the internal network IP address or public network IP address ***(unsafe)*** to connect, i.e., `ws://(internal/public IP):6199/ws`.
- Message Format: `Array`
- Heartbeat Interval: `5000`
- Reconnection Interval: `5000`
> [!WARNING]
>
> 1. Remember to add `/ws` at the end!
> 2. The IP here cannot be `0.0.0.0`
Click `Save`.
Go to AstrBot WebUI `Console`, if you see the blue log ` aiocqhttp(OneBot v11) adapter connected.`, it means the connection is successful. If not, and after several seconds ` aiocqhttp adapter has been closed` appears, it indicates connection timeout (failed), please check if the configuration is correct.
## 🎉 All Done
At this point, your AstrBot and NapCat should be successfully connected! Use `private message` to send `/help` to the bot to check if the connection is successful.
+1
View File
@@ -0,0 +1 @@
AstrBot can connect to any bot protocol client that supports OneBot v11 reverse WebSocket (AstrBot acts as the server side).
+65
View File
@@ -0,0 +1,65 @@
# Connect to DingTalk
## Supported Basic Message Types
> Version v4.15.0.
| Message Type | Receive | Send | Notes |
| --- | --- | --- | --- |
| Text | Yes | Yes | |
| Image | Yes | Yes | |
| Voice | No | Yes | |
| Video | No | Yes | |
| File | No | Yes | |
Proactive message push: Supported.
## Create and Configure the App
Go to the [DingTalk Open Platform](https://open-dev.dingtalk.com/fe/app), then create an app:
![image](https://files.astrbot.app/docs/source/images/dingtalk/image-4.png)
After creation, add app capability and choose Bot:
![image](https://files.astrbot.app/docs/source/images/dingtalk/image-5.png)
Open Bot settings and fill in bot information:
![image](https://files.astrbot.app/docs/source/images/dingtalk/image-7.png)
After confirming all settings, click Publish.
Go to Credentials & Basic Information, then copy `ClientID` and `ClientSecret`.
## Connect in AstrBot
Open AstrBot Dashboard -> `Bots` -> `+ Create Bot`, then create a DingTalk adapter.
Fill in `ClientID` and `ClientSecret`, then click Save. AstrBot will request authorization from DingTalk Open Platform automatically.
Back in DingTalk Open Platform, open Event Subscriptions, select `Stream mode push`, and click Save. If successful, you will see a connected status.
![image](https://files.astrbot.app/docs/source/images/dingtalk/image-8.png)
Save the configuration.
## Publish a Version
In the left sidebar, open Version Management and Release, then create a new version.
Fill in version number, description, and visibility scope (all employees or as needed), then save and publish.
![alt text](https://files.astrbot.app/docs/source/images/dingtalk/image-11.png)
Open a DingTalk group chat and click the top-right settings:
![image](https://files.astrbot.app/docs/source/images/dingtalk/image-12.png)
Scroll down to Add Bot, select the bot you just created, and add it:
![image](https://files.astrbot.app/docs/source/images/dingtalk/image-9.png)
## Done
In a group chat, mention the bot and send `/help`. If the bot replies, the integration is successful.
+74
View File
@@ -0,0 +1,74 @@
# Connecting to Discord
## Create AstrBot Discord Platform Adapter
Navigate to the messaging platform, click to add a new adapter, find Discord and click to enter the Discord configuration page.
![Click to create bot, select discord type](https://files.astrbot.app/docs/source/images/discord/image.png)
![Options from top to bottom: 1. Bot name 2. Enable 3. Bot token 4. Discord proxy address 5. Auto-register plugin commands as Discord slash commands 6. discord_guild_id_for_debug 7. Discord activity name](https://files.astrbot.app/docs/source/images/discord/image-3.png)
> For this tutorial, you only need to configure items 1, 2, 3, and 5
- Bot Name: Customize this to easily distinguish between different adapters
- Enable: Check to enable this adapter
- Bot Token: Token obtained after creating an App in Discord (see below)
- Discord Proxy Address: If you need to use a proxy to access Discord, you can enter the proxy address here (optional)
- Auto-register Plugin Commands as Discord Slash Commands: When checked, AstrBot will automatically register commands from installed plugins as Discord slash commands for user convenience.
## Create an App in Discord
1. Go to [Discord Developer Portal](https://discord.com/developers/applications), click the blue button in the top right corner, enter an application name, and create the application.
![Create bot (enter name)](https://files.astrbot.app/docs/source/images/discord/image-1.png)
2. Click on Bot in the left sidebar, click the Reset Token button. After the token is created, click the Copy button and paste the token into the Discord Bot Token field in the configuration.
![Token options](https://files.astrbot.app/docs/source/images/discord/image-4.png)
3. Scroll down and enable all three of these options:
![Presence Intent, Server Members Intent, Message Content Intent screenshot](https://files.astrbot.app/docs/source/images/discord/image-2.png)
- Presence Intent: Allows the bot to access user online status
- Server Members Intent: Allows the bot to access server member information
- Message Content Intent: Allows the bot to read message content
4. Click OAuth2 in the left sidebar, and in the OAuth2 URL Generator, select `Bot`
Like this:
![OAuth2 URL Generator](https://files.astrbot.app/docs/source/images/discord/image-6.png)
Then in the Bot Permissions section that appears below, select the allowed permissions. Generally, it's recommended to add the following permissions:
- Send Messages
- Create Public Threads
- Create Private Threads
- Send TTS Messages
- Manage Messages
- Manage Threads
- Embed Links
- Attach Files
- Read Message History
- Add Reactions
If you find this tedious, you can directly use administrator permissions, but it's still recommended to use the permissions configured above (or the permissions you specifically need) in your production environment.
> Remember, the higher the permissions, the greater the risk.
5. Copy the Generated URL that appears below. Open this URL to add the bot to your desired server.
![Generated URL location](https://files.astrbot.app/docs/source/images/discord/image-5.png)
6. Enter your Discord server, your bot should now show as online
![Bot online](https://files.astrbot.app/docs/source/images/discord/image-7.png)
@ mention the bot you just created (or don't mention it), type `/help`. If it responds successfully, the test is successful.
## Pre-acknowledgment Emoji
Discord supports the pre-acknowledgment emoji feature. When enabled, the bot will add an emoji reaction when processing a message, letting users know the bot is working on their request.
In the admin panel's "Configuration" page, find `Platform Specific -> Discord -> Pre-acknowledgment Emoji`:
- **Enable Pre-acknowledgment Emoji**: When enabled, the bot will automatically add an emoji reaction upon receiving a message
- **Emoji List**: Enter Unicode emoji symbols, e.g., 👍, 🤔, ⏳. You can add multiple emojis, and the bot will randomly select one to use
# Troubleshooting
- If you're stuck at the final step and the bot is not online, please ensure your server can directly connect to Discord
If you have any questions, please [submit an Issue](https://github.com/AstrBotDevs/AstrBot/issues).
+46
View File
@@ -0,0 +1,46 @@
# Connect to KOOK
## Supported Message Types
> Version v4.19.2
| Message Type | Receive | Send | Remarks |
| ------------ | ------- | ---- | -------------------------------------------------- |
| Text | Yes | Yes | Supports official [kmarkdown] syntax |
| Image | Yes | Yes | Supports external links; `jpeg`, `gif`, `png` only |
| Audio | Yes | Yes | Supports external links |
| Video | Yes | Yes | Supports external links; `mp4`, `mov` only |
| File | Yes | Yes | Supports external links |
| Card (JSON) | Yes | Yes | See [Kook Docs - Card Messages] |
Proactive message push: Supported
Message receiving mode: WebSocket
## Create a Bot on Kook
1. Go to the [Kook Developer Center] and follow these steps:
2. Log in and complete identity verification.
3. Click "Create Application" and customize your Bot's nickname.
4. Enter the application dashboard, select the **Bot** module, and enable **WebSocket connection mode**. Make sure to save the generated **Token**, as you will need it for the subsequent AstrBot configuration.
5. Under the "Bot" page in the left sidebar, click "Invite Link" and set the role permissions (full permissions are recommended to ensure all features work).
6. Copy the invite link, open it in your browser, and add the bot to your desired server.
![image](https://files.astrbot.app/docs/source/images/kook/image-1.png)
## Configure in AstrBot
1. Access the AstrBot management panel.
2. Click **Bots** in the left sidebar.
3. Click `+ Create Bot` on the right side of the interface.
4. Select the `kook` adapter.
5. Fill in the configuration fields:
- ID (id): Any name to identify this specific instance.
- Enable (enable): Check the box.
- Bot Token: Paste the Token generated from the [Kook Developer Center].
6. Click `Save` after filling in the details.
7. Finally, in a Kook server channel (create one first if you haven't), @ the bot and type `/sid`. If the bot responds, the configuration is successful.
[Kook Developer Center]: https://developer.kookapp.cn/app
[kmarkdown]: https://developer.kookapp.cn/doc/kmarkdown
[Kook Docs - Card Messages]: https://developer.kookapp.cn/doc/cardmessage
+121
View File
@@ -0,0 +1,121 @@
# Connecting to Lark
## Supported Message Types
> Version v4.15.0.
| Message Type | Receive Support | Send Support | Notes |
| --- | --- | --- | --- |
| Text | Yes | Yes | |
| Image | Yes | Yes | |
| Voice | No | Yes | |
| Video | No | Yes | |
| File | No | Yes | |
Proactive message push: Supported.
Streaming output: Supported. You must enable the `Create and update cards (cardkit:card:write)` permission for your app in the Lark Developer Console.
The Lark client version must be >= 7.20. Lower versions only display the title and an upgrade prompt.
## Creating a Bot
Navigate to the [Developer Console](https://open.feishu.cn/app) and create a custom enterprise application.
![Create Custom Enterprise Application](https://files.astrbot.app/docs/source/images/lark/image.png)
Add the Bot capability to your application.
![Add Bot Capability](https://files.astrbot.app/docs/source/images/lark/image-1.png)
Click on "Credentials & Basic Info" to obtain your app_id and app_secret.
![Get app_id and app_secret](https://files.astrbot.app/docs/source/images/lark/image-4.png)
## Configuring AstrBot
1. Access the AstrBot management panel
2. Click on `Bots` in the left sidebar
3. In the right panel, click `+ Create Bot`
4. Select `lark`
Fill in the configuration fields as follows:
- ID: Choose any identifier to distinguish between different messaging platform instances
- Enable: Check this option
- app_id: The app_id you obtained earlier
- app_secret: The app_secret you obtained earlier
- Bot name: Your Lark bot's name
For the domain field, if you're using Lark China, keep the default value. If you're using Lark International, set it to `https://open.larksuite.com`. If you're using a self-hosted enterprise Lark instance, enter your Lark instance's domain.
For the subscription method, `socket` uses a long connection subscription approach, while `webhook` sends events to your developer server and requires a public server. Generally, `socket` is recommended. However, if you're using Lark International or a self-hosted Lark instance, choose `webhook`. The subsequent configuration steps will differ accordingly.
If you selected the `webhook` method, navigate to the Lark Developer Console, click on "Events & Callbacks," then "Encryption Policy," and fill in the Encrypt Key. While not mandatory, AstrBot takes your data security seriously, so we strongly recommend setting this up. After filling it in, copy the `Encrypt Key` and `Verification Token` to the corresponding `encrypt_key` and `verification_token` fields in AstrBot's configuration.
Click `Save`.
## Setting up Callbacks and Permissions
The following steps vary depending on the subscription method you selected above. Please proceed to the corresponding section based on your choice.
### `socket` Long Connection Method
Next, click on "Events & Callbacks," select "Receive events using long connection," and click Save. **If the previous step didn't start successfully, you won't be able to save here.**
![Configure Events & Callbacks](https://files.astrbot.app/docs/source/images/lark/image-6.png)
### `webhook` Send Events to Developer Server Method
> [!TIP]
> To make better use of this method, please refer to [Unified Webhook Mode](/en/use/unified-webhook.md#how-to-use-unified-webhook-mode) for the necessary configuration.
After clicking `Save`, the bot card will display "View Webhook URL." Click to view and copy the callback URL.
![](https://files.astrbot.app/docs/source/images/lark/webhook.png)
Next, return to Lark's Events & Callbacks page, click "Event Configuration," select "Send events to developer server," enter the callback URL you just copied as the "Request URL," and click Save. If everything is correct, no errors will appear.
### Setting up Events
After completing the event configuration in the previous step, click "Add Event," navigate to "Messages & Groups," scroll down to find `Receive Message`, and add it.
![Add Event](https://files.astrbot.app/docs/source/images/lark/image-7.png)
Click to enable the following permissions.
![Enable Permissions](https://files.astrbot.app/docs/source/images/lark/image-8.png)
Then click the `Save` button at the top.
Next, click on "Permission Management," click "Enable Permissions," and enter `im:message:send,im:message,im:message:send_as_bot`. Add the filtered permissions.
Enter `im:resource:upload,im:resource` again to enable image upload permissions.
If you want to use streaming output, additionally enable `Create and update cards (cardkit:card:write)`.
The final set of permissions should look like this:
![Final Permissions](https://files.astrbot.app/docs/source/images/lark/image-11.png)
## Creating a Version
Create a new version.
![Create Version](https://files.astrbot.app/docs/source/images/lark/image-2.png)
Fill in the version number, update notes, and visibility scope, then click Save and confirm the release.
## Adding the Bot to a Group
Open the Lark app (the web version doesn't support adding bots), enter a group chat, click the button in the upper right corner → Group Bots → Add Bot.
Search for the bot you just created. For example, if you created the `AstrBot` bot as shown in this tutorial:
![Add Bot](https://files.astrbot.app/docs/source/images/lark/image-9.png)
## 🎉 All Done!
Send a `/help` command in the group, and the bot will respond.
![Success](https://files.astrbot.app/docs/source/images/lark/image-13.png)
+79
View File
@@ -0,0 +1,79 @@
# Connecting to LINE
## Supported Message Types
> Version v4.17.0.
| Message Type | Receive Support | Send Support | Notes |
| --- | --- | --- | --- |
| Text | Yes | Yes | |
| Image | Yes | Yes | |
| Voice | Yes | Yes | |
| Video | Yes | Yes | |
| File | Yes | Yes | |
| Sticker | Yes | No | |
Proactive message push: Supported.
## Create a LINE Messaging API Channel
1. Open the [LINE Developers Console](https://developers.line.biz/console/)
2. Create or select a Provider
3. Create a `Messaging API` channel (not a `LINE Login` channel)
4. Complete bot initialization on the `Messaging API` page
## Get Credentials
You need the following values:
- `channel_secret`
- `channel_access_token`
How to get them:
1. Open your channel settings page
2. Get `Channel secret` from `Basic settings`
3. Issue a `Channel access token` on the `Messaging API` page
![](https://files.astrbot.app/docs/source/images/line/7ecee0a9102f191245330f8408eb0493.png)
## Configure AstrBot
1. Open the AstrBot admin panel
2. Click `Bots` in the left sidebar
3. Click `+ Create Bot`
4. Select `line`
Fill in these fields:
- `ID`: Custom identifier to distinguish instances
- `Enable`: Checked
- `LINE Channel Access Token`: your `channel_access_token`
- `LINE Channel Secret`: your `channel_secret`
- `LINE Bot User ID`: optional; if empty, AstrBot uses webhook `destination`
Click Save.
## Configure Callback URL (Unified Webhook)
The LINE adapter supports **unified webhook mode only**.
After saving, click `View Webhook URL` on the bot card and copy the URL.
Then in LINE Developers Console:
1. Open `Messaging API`
2. Paste the URL into `Webhook settings` -> `Webhook URL`
3. Click `Verify`
4. Enable `Use webhook`
> [!TIP]
> If AstrBot is not publicly reachable, set up a public domain and reverse proxy first so LINE can access your webhook URL.
## Test
1. Add your Official Account as a friend in LINE
2. Send a message to the bot (for example, `hi`)
3. If the bot replies, setup is successful
If you want to use it in a group, invite the Official Account to the group first.
+20
View File
@@ -0,0 +1,20 @@
# Connecting to Matrix
> [!TIP]
> This platform adapter is maintained by the community ([stevessr](https://github.com/stevessr)). If you find it helpful, please support the developer by giving the repository a Star. ❤️
## Installing the astrbot_plugin_matrix_adapter Plugin
Go to the AstrBot WebUI plugin marketplace, search for `astrbot_plugin_matrix_adapter`, and click Install.
After installation, navigate to Messaging Platforms → Add Adapter → Select Matrix (if the option is missing, try restarting AstrBot or check the plugin installation status).
Click `Enable` in the configuration dialog that appears.
## Configuration
Please refer to the repository's [README.md](https://github.com/stevessr/astrbot_plugin_matrix_adapter?tab=readme-ov-file#astrbot-matrix-adapter-%E6%8F%92%E4%BB%B6) for configuration instructions.
## Issue Reporting
If you have any questions, please submit an issue to the [plugin repository](https://github.com/stevessr/astrbot_plugin_matrix_adapter/issues).
+113
View File
@@ -0,0 +1,113 @@
# Connecting to Misskey Platform
> [!WARNING]
>
> 1. We recommend that before deploying a bot on a Misskey instance you don't manage, you should review the instance rules or seek approval from the instance administration or moderation team, and enable the `Bot` identifier for the bot account after deployment.
> 2. This project is strictly prohibited from being used for any illegal purposes. If you intend to use AstrBot for illegal industries or activities, we explicitly oppose and refuse your use of this project.
## Create AstrBot Misskey Platform Adapter
Navigate to the messaging platform, click to add a new adapter, find Misskey and click to enter the Misskey configuration page.
![Create Misskey Platform Adapter](https://files.astrbot.app/docs/source/images/misskey/create.png)
## Configure Platform Adapter Settings
On the AstrBot Misskey platform adapter configuration page, we need to fill in the Misskey connection information and configure some adapter behaviors.
::: tip Note
Don't forget to click `Enable` before saving to activate the Misskey platform adapter!
:::
How to obtain the Misskey connection information is described below.
![Misskey Platform Adapter Configuration](https://files.astrbot.app/docs/source/images/misskey/config.png)
## Misskey Instance URL
This is the frontend address of the Misskey instance where your bot account is located, in standard domain format. For example, `https://misskey.example`.
## Obtain Bot Account Access Token
1. First, open the Misskey Web frontend page, find and open the `Settings > Connected Services` page in the frontend sidebar.
![Open Misskey Connected Services Page](https://files.astrbot.app/docs/source/images/misskey/pat-1.png)
2. Click "Generate Access Token" to generate an account access token.
![Generate Misskey Account Token](https://files.astrbot.app/docs/source/images/misskey/pat-2.png)
3. On the access token configuration page that appears, give the token a name, such as `AstrBot`.
4. Then we need to configure the relevant permissions for the token to allow the bot to interact with the Misskey instance.
::: tip Note
If third-party AstrBot plugins you use require additional permissions, please refer to their documentation to add the corresponding permissions. If you fully trust the bot's deployment environment, you can temporarily enable all permissions to simplify debugging, but we still recommend limiting the bot's permissions in production environments.
:::
![Configure Access Token Permissions](https://files.astrbot.app/docs/source/images/misskey/pat-3.png)
**Permissions Required by Default**
| Permission Name | Description | Purpose |
|---|---:|---|
| Read account information | View basic account information | Obtain bot's own user information and account ID |
| Compose or delete posts | Create, edit, and delete note content | Send message replies and publish content |
| Compose or delete messages | Create, edit, and delete direct messages | Handle direct message conversations |
| View notifications | Receive system notifications and reminders | Obtain mention, reply, and other notification information |
| View messages | Read direct messages and chat history | Receive and process user direct messages |
| View reactions | View replies and reactions to posts | Handle user responses to bot messages |
5. After completing the permission configuration, click "Done" to view the account access token. Copy the obtained token and paste it into the Access Token input box on the AstrBot configuration page.
![View Account Token](https://files.astrbot.app/docs/source/images/misskey/pat-4.png)
## Default Post Visibility
Modify the default visibility when the bot posts
| Name | Description |
|---|---|
| public | Anyone can see the bot's posts |
| home | Publish bot posts to the instance home timeline |
| followers | Only users who follow the bot account can see bot posts in the home timeline |
## Local Only (Do Not Federate)
When enabled, all posts sent by the bot will not participate in Fediverse federation. This is very suitable for scenarios where you only want to use and distribute the bot's posts within your own instance.
## Enable Chat Message Response
::: tip Note
Misskey's "Chat" component feature is not supported by all Misskey Fork versions! It cannot federate across instances.
Misskey added "Chat" component support in `v2025.4.0` and later versions, and it is only supported by its web frontend, not well-supported by third-party apps.
:::
Enabled by default. When enabled, the bot will respond to private chat messages sent by users in Misskey chat.
## History Records
Conversation history for individual users in chat and posts will be recorded in the AstrBot WebUI console "Conversation History" with the ID `chat:UserID`, while traditional posts will be recorded with the ID `note:UserID`.
::: tip Where is the Misskey user's UserID?
It can be found on the user's personal page in the `Raw` section. UserID is the unique key identifier for Misskey users within a single instance.
:::
![UserID](https://files.astrbot.app/docs/source/images/misskey/userid.png)
## Test the Connection
After completing the configuration and enabling it, go to Misskey to create a new post and mention the bot (@mention) to test. If the bot account successfully triggers a reply, the configuration is successful.
![Demo Example](https://files.astrbot.app/docs/source/images/misskey/demo.png)
## Additional Notes
We recommend enabling the Misskey `Bot` identifier for bot accounts to respect the relevant regulations and rate limits of various Misskey instances, which can also effectively help Misskey instance administrators manage and identify bot usage.
**How to Enable**
Enable "This is a bot account" in the advanced settings of the bot account's profile page.
![This is a bot account](https://files.astrbot.app/docs/source/images/misskey/botset.png)
+8
View File
@@ -0,0 +1,8 @@
# Connect QQ Official Bot
QQ Official Bot is Tencent's official bot platform. It lets you connect bots to QQ group chats and private chats through official APIs.
Currently, the main integration method is Webhook.
- [Webhook Method](/en/platform/qqofficial/webhook)
- [Websockets Method](/en/platform/qqofficial/websockets)
+93
View File
@@ -0,0 +1,93 @@
# Connect QQ via QQ Official Bot (Webhook)
> [!WARNING]
> 1. QQ Official Bot currently requires an IP whitelist.
> 2. It supports group chat, private chat, channel chat, and channel private chat.
> 3. You need a server with a public IP and a domain.
## Supported Basic Message Types
> Version v4.15.0.
| Message Type | Receive | Send | Notes |
| --- | --- | --- | --- |
| Text | Yes | Yes | |
| Image | Yes | Yes | |
| Voice | No | No | |
| Video | No | No | |
| File | No | No | |
Proactive message push: Not supported.
## Apply for a Bot
Open [QQ Official Bot](https://q.qq.com) and sign in.
Create a bot, fill in name/description/avatar, then submit for review. After security verification passes, creation is complete.
Open the created bot to enter its management page:
![image](https://files.astrbot.app/docs/source/images/qqofficial/image.png)
## Allow Bot in Channel / Group / Private Chat
Open `Sandbox Configuration` to set a sandbox channel / QQ group / QQ private chat (up to 20 members).
Then configure QQ groups, private chat QQ accounts, and QQ channels as needed.
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-1.png)
## Get `appid` and `secret`
After adding the bot where you need it, open `Development -> Development Settings`, then copy `appid` and `secret`.
## Add IP Whitelist
Open `Development -> Development Settings`, find IP whitelist, and add your server IP.
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-3.png)
## Configure in AstrBot
1. Open AstrBot Dashboard.
2. Click `Bots` in the left sidebar.
3. Click `+ Create Bot`.
4. Select `qq_official_webhook`.
Fill in:
- ID (`id`): any unique identifier.
- Enable (`enable`): checked.
- `appid`: from QQ Official Bot platform.
- `secret`: from QQ Official Bot platform.
Click `Save`.
## Configure Callback URL
In `Development -> Callback Configuration`, configure callback URL.
Set request URL to `<your-domain>/astrbot-qo-webhook/callback`.
Your domain should reverse-proxy traffic to AstrBot port `6196` using `Caddy`, `Nginx`, or `Apache`.
Then add callback events and select all four event categories (private, group, channel, etc.).
![image](https://files.astrbot.app/docs/source/images/webhook/image.png)
After entering values, move focus out of the input box to trigger validation. If validation passes, the confirm button on the right becomes clickable.
Then restart AstrBot.
## Done
AstrBot should now be connected. If messages do not respond immediately, wait 1-2 minutes, restart AstrBot, and test again.
## Appendix: Reverse Proxy Setup
If you are new to reverse proxy, Caddy is recommended:
1. Install Caddy: <https://caddy2.dengxiaolong.com/docs/install>
2. Configure reverse proxy: <https://caddy2.dengxiaolong.com/docs/quick-starts/reverse-proxy>
Caddy can automatically apply TLS certificates for Webhook access.
+87
View File
@@ -0,0 +1,87 @@
# Connect QQ via QQ Official Bot (Websockets)
## Supported Basic Message Types
> Version v4.15.0.
| Message Type | Receive | Send | Notes |
| --- | --- | --- | --- |
| Text | Yes | Yes | |
| Image | Yes | Yes | |
| Voice | No | No | |
| Video | No | No | |
| File | No | No | |
Proactive message push: Not supported.
## Quick Deployment Steps
> Updated: `2026/03/06`. This method only supports `private chat`.
1. Open [QQ Open Platform](https://q.qq.com/qqbot/openclaw/). Register an account if you don't have one.
2. Click the `Create Bot` button on the right.
3. Obtain your `AppID` and `AppSecret`.
4. In AstrBot WebUI, click `Bots` in the left sidebar, then click `+ Create Bot`, select `QQ Official Bot (WebSocket)`, paste the `AppID` and `AppSecret` into the form, click `Enable`, then click `Save`.
5. Back on the QQ Open Platform page, click `Scan QR Code to Chat` next to your bot, then scan with your mobile QQ to start chatting.
To use the bot in group chats, refer to the `Allow Bot in Channel / Group / Private Chat` section below.
---
## Apply for a Bot
> [!WARNING]
> 1. QQ Official Bot currently requires an IP whitelist.
> 2. It supports group chat, private chat, channel chat, and channel private chat.
> 3. Tencent is phasing out Websockets access, so this method is no longer recommended. Please use [Webhook](/en/platform/qqofficial/webhook) instead.
Open [QQ Official Bot](https://q.qq.com) and sign in.
Create a bot, fill in name/description/avatar, then submit for review. After security verification passes, creation is complete.
Open the created bot to enter its management page:
![image](https://files.astrbot.app/docs/source/images/qqofficial/image.png)
## Allow Bot in Channel / Group / Private Chat
Open `Sandbox Configuration` to set a sandbox channel / QQ group / QQ private chat (up to 20 members).
Then configure QQ groups, private chat QQ accounts, and QQ channels as needed.
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-1.png)
## Get `appid` and `secret`
After adding the bot where you need it, open `Development -> Development Settings`, then copy `appid` and `secret`.
## Add IP Whitelist
Open `Development -> Development Settings`, find IP whitelist, and add your server IP.
![image](https://files.astrbot.app/docs/source/images/qqofficial/image-3.png)
> [!TIP]
> If you do not know your server IP, run `curl ifconfig.me` or check [ip138.com](https://ip138.com/).
>
> In NAT environments without a public IP, the observed IP may change depending on your carrier. Use proxy/tunnel if needed.
## Configure in AstrBot
1. Open AstrBot Dashboard.
2. Click `Bots` in the left sidebar.
3. Click `+ Create Bot`.
4. Select `qq_official`.
Fill in:
- ID (`id`): any unique identifier.
- Enable (`enable`): checked.
- `appid`: from QQ Official Bot platform.
- `secret`: from QQ Official Bot platform.
Click `Save`.
## Done
AstrBot should now be connected. Send `/help` to the bot in QQ private chat to verify.
+73
View File
@@ -0,0 +1,73 @@
# Connect LLTwoBot (Satori)
> [!TIP]
> LLTwoBot is a multi-protocol implementation based on QQNT (OneBot v11 + Satori), allowing AstrBot to communicate with QQ via Satori.
> [!TIP]
> - Please control message frequency responsibly.
> - This project must not be used for illegal purposes.
## Preparation
First complete basic setup using official LLTwoBot documentation:
[LLTwoBot Docs](https://llonebot.com/guide/getting-started)
Make sure you have:
1. Installed LLTwoBot.
2. Logged into a QQ account successfully.
## Configure Satori in LLTwoBot
After QQ login succeeds, open LLTwoBot WebUI:
> Default WebUI URL: <http://localhost:3080/>
In the WebUI sidebar, open the `Satori` tab and configure:
1. Enable Satori protocol.
2. Port defaults to `5600`.
3. Set Satori token if needed.
4. Click Save.
![image](https://files.astrbot.app/docs/source/images/satori/2025-10-10_15-52-32.png)
## Configure Satori Adapter in AstrBot
1. Open AstrBot Dashboard.
2. Click `Bots`.
3. Click `+ Create Bot`.
4. Select `satori`.
Fill in:
- Bot ID (`id`): `LLTwoBot`
- Enable (`enable`): checked
- Satori API endpoint (`satori_api_base_url`): `http://localhost:5600/v1`
- Satori WebSocket endpoint (`satori_endpoint`): `ws://localhost:5600/v1/events`
- Satori token (`satori_token`): from LLTwoBot config if set
> [!NOTE]
> - LLTwoBot Satori service defaults to port `5600`.
> - The complete API base path is `http://localhost:5600/v1`.
> - If your Satori service runs on another port/path, adjust these values.
![image](https://files.astrbot.app/docs/source/images/satori/2025-10-10_16-10-54.png)
Click `Save`.
## Done
AstrBot should now be connected to LLTwoBot via Satori.
Send `/help` in QQ to verify.
## Troubleshooting
If connection fails, check:
1. LLTwoBot is running.
2. Satori service is enabled.
3. Port/path are configured correctly.
4. Token matches (if configured).
+65
View File
@@ -0,0 +1,65 @@
# Connect server-satori (Koishi)
> [!TIP]
> `server-satori` is a Koishi plugin that exposes Koishi as a Satori server, so AstrBot can connect to Koishi through Satori.
## Preparation
Make sure you already have a running Koishi instance.
If not, follow official docs first:
- Koishi starter docs: <https://koishi.chat/zh-CN/manual/starter/windows.html>
- Koishi community: <https://koishi.chat/zh-CN/about/contact.html>
## Enable `server-satori` in Koishi
1. Open Koishi admin panel.
2. Go to `Plugin Config`.
3. Install and enable `server-satori` (defaults usually work).
After enabling, `server-satori` serves Satori API under `/satori`.
![image](https://files.astrbot.app/docs/source/images/satori/2025-09-07_17-14-55.png)
## Configure Satori Adapter in AstrBot
1. Open AstrBot Dashboard.
2. Click `Bots`.
3. Click `+ Create Bot`.
4. Select `satori`.
Fill in:
- Bot ID (`id`): `server-satori`
- Enable (`enable`): checked
- Satori API endpoint (`satori_api_base_url`): `http://localhost:5140/satori/v1`
- Satori WebSocket endpoint (`satori_endpoint`): `ws://localhost:5140/satori/v1/events`
- Satori token (`satori_token`): usually empty unless configured in Koishi
> [!NOTE]
> - Koishi default port is `5140`.
> - `server-satori` default path is `/satori`.
> - So the full API base is `http://localhost:5140/satori/v1`.
> - If your Koishi runs on different host/port/path, change accordingly.
![image](https://files.astrbot.app/docs/source/images/satori/2025-10-10_16-16-25.png)
Click `Save`.
## Done
AstrBot should now be connected to Koishi via `server-satori`.
Test by sending an AstrBot command (for example `/help`) in Koishi sandbox.
![image](https://files.astrbot.app/docs/source/images/satori/2025-09-07_17-19-04.png)
## Troubleshooting
If connection fails, check:
1. Koishi is running.
2. `server-satori` is installed and enabled.
3. Port/path are configured correctly.
4. Firewall is not blocking related ports.
+94
View File
@@ -0,0 +1,94 @@
# Connecting to Slack
## Create AstrBot Slack Platform Adapter
Navigate to the `Bots` page, click `+ Create Bot`, find Slack and click to enter the Slack configuration page.
![image](https://files.astrbot.app/docs/source/images/slack/image-1.png)
In the configuration dialog that appears, click `Enable`.
## Create an App in Slack
Slack supports two connection methods: `Webhook` and `Socket`. If you don't have a public server and your message volume is relatively small, we recommend using the `socket` method. If you have a public server (or have technical knowledge about setting up tunnels, such as Cloudflare Tunnel), you can choose the `webhook` method. The `socket` method is relatively simpler to deploy.
1. Create a [Slack](https://slack.com/signin) account and a Workspace.
2. Go to [Apps Management](https://api.slack.com/apps), click "Create New App" -> "From Scratch", enter the `App Name` and the workspace to add it to, then click "Create App".
3. (Webhook only) Obtain the `Signing Secret`. In the Basic Information page on the left sidebar, find `Signing Secret` under App Credentials, click Show and copy it to the signing_secret field in the platform adapter configuration.
![image](https://files.astrbot.app/docs/source/images/slack/image.png)
4. In the Basic Information page on the left sidebar, find App-Level Tokens and click "Generate Token and Scopes". Enter any Token Name, click Add Scope, select `connections:write`, then click "Generate". Click Copy and paste the result into the app_token field on the AstrBot configuration page.
![image](https://files.astrbot.app/docs/source/images/slack/image-2.png)
5. In the OAuth & Permissions page on the left sidebar, add the following permissions under Bot Token Scopes:
- channels:history
- channels:read
- channels:write.invites
- chat:write
- chat:write.customize
- chat:write.public
- files:read
- files:write
- groups:history
- groups:read
- groups:write
- im:history
- im:read
- im:write
- reactions:read
- reactions:write
- users:read
6. In the OAuth & Permissions page on the left sidebar, click `Install to xxx` under OAuth Token (where xxx is your workspace name). Then copy the generated Bot User OAuth Token to the bot_token field in the platform adapter configuration.
7. (Socket only) In the Socket Mode page on the left sidebar, enable Socket Mode.
![image](https://files.astrbot.app/docs/source/images/slack/image-3.png)
## Start the Platform Adapter
The configuration is now complete. If you're using Socket mode, simply click the Save button in the bottom right corner of the configuration.
If you're using Webhook mode, please keep `Unified Webhook Mode (unified_webhook_mode)` enabled.
> [!TIP]
> Before v4.8.0, there is no `Unified Webhook Mode`. You need to fill in the following configuration items:
> Slack Webhook Host, Slack Webhook Port, and Slack Webhook Path
## Enable Event Subscriptions
After successfully creating the platform adapter, return to the Slack settings. In the Event Subscriptions page on the left sidebar, click Enable Events to enable event reception.
If you're using Webhook mode:
- If `Unified Webhook Mode` is enabled, after clicking save, AstrBot will automatically generate a unique Webhook callback URL for you. You can find it in the logs or on the bot card in the WebUI's Bots page. Enter this URL in the `Request URL` field.
![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png)
- If `Unified Webhook Mode` is not enabled, enter `https://your-domain/astrbot-slack-webhook/callback` in the `Request URL` field.
> [!TIP]
> In Webhook mode, you need to first set up your domain with your DNS provider, then use reverse proxy software to forward requests to port `6185` on the AstrBot server (if Unified Webhook Mode is enabled) or the port specified in your configuration (if Unified Webhook Mode is not enabled). Alternatively, you can use Cloudflare Tunnel. For detailed tutorials, please refer to online resources; this tutorial will not cover these in detail.
After enabling, under Subscribe to bot events below, click Add Bot User Event and add the following events:
1. channel_created
2. channel_deleted
3. channel_left
4. member_joined_channel
5. member_left_channel
6. message.channels
7. message.groups
8. message.im
9. reaction_added
10. reaction_removed
11. team_join
## Test the Connection
Enter the Slack workspace you just added, navigate to the channel where you want to use the bot, then @ mention the app you just created. Click the Add button in the message subsequently sent by Slackbot to add it to the workspace. Then, @ mention the app and type `/help`. If it responds successfully, the test is successful.
If you have any questions, please [submit an Issue](https://github.com/AstrBotDevs/AstrBot/issues).
+6
View File
@@ -0,0 +1,6 @@
# Messaging Platforms
AstrBot supports integration with many mainstream instant messaging platforms, so you can use AstrBot on the IM platform your team already uses.
In WebUI, click **Bots** in the left sidebar to open the messaging platform integration page.
Then click **Create Bot** in the top-right corner, choose the platform you want to connect, and follow the platform-specific guide in the left sidebar of this documentation.
+55
View File
@@ -0,0 +1,55 @@
# Connecting to Telegram
## Supported Message Types
> Version v4.15.0.
| Message Type | Receive Support | Send Support | Notes |
| --- | --- | --- | --- |
| Text | Yes | Yes | |
| Image | Yes | Yes | |
| Voice | Yes | Yes | |
| Video | Yes | Yes | |
| File | Yes | Yes | |
Proactive message push: Supported.
## 1. Create a Telegram Bot
First, open Telegram and search for `BotFather`. Click `Start`, then send `/newbot` and follow the prompts to enter your bot's name and username.
After successful creation, `BotFather` will provide you with a `token`. Please keep it secure.
If you need to use the bot in group chats, you must disable the bot's [Privacy mode](https://core.telegram.org/bots/features#privacy-mode). Send the `/setprivacy` command to `BotFather`, select your bot, and then choose `Disable`.
## 2. Configure AstrBot
1. Enter the AstrBot admin panel
2. Click `Bots` in the left sidebar
3. In the interface on the right, click `+ Create Bot`
4. Select `telegram`
Fill in the configuration fields that appear:
- ID: Enter any value to distinguish between different messaging platform instances.
- Enable: Check this option.
- Bot Token: Your Telegram bot's `token`.
Please ensure your network environment can access Telegram. You may need to configure a proxy using `Configuration -> Other Settings -> HTTP Proxy`.
## Streaming Output
The Telegram platform supports streaming output. Enable the "Streaming Output" switch in "AI Configuration" -> "Other Settings".
### Private Chat Streaming
In private chats, AstrBot uses the `sendMessageDraft` API (added in Telegram Bot API v9.3) for streaming output. This displays a "typing" draft preview animation in the chat interface, creating a more natural "typewriter" effect. It avoids issues with the traditional approach such as message flickering, push notification interference, and API edit frequency limits.
### Group Chat Streaming
In group chats, since the `sendMessageDraft` API only supports private chats, AstrBot automatically falls back to the traditional `send_message` + `edit_message_text` approach.
:::warning
`sendMessageDraft` requires `python-telegram-bot>=22.6`.
:::
+44
View File
@@ -0,0 +1,44 @@
# Connect to VoceChat
> [!TIP]
> AstrBot does not include this adapter by default. Install [astrbot_plugin_vocechat](https://github.com/HikariFroya/astrbot_plugin_vocechat), developed by [HikariFroya](https://github.com/HikariFroya).
> [!WARNING]
> This adapter is community-maintained and not officially maintained by AstrBot.
## Deploy VoceChat
VoceChat is an open-source instant messaging platform with simple multi-platform deployment.
See deployment methods on the [VoceChat official website](https://voce.chat/en-US).
## Install `astrbot_plugin_vocechat`
In AstrBot Dashboard Plugin Market, search for `astrbot_plugin_vocechat` and install it.
![image](https://files.astrbot.app/docs/source/images/vocechat/image.png)
After installation, go to `Bots` -> `+ Create Bot` -> `VoceChat`.
If VoceChat is missing, restart AstrBot or verify plugin installation.
Enable the adapter in the configuration dialog.
## Configuration
- `vocechat_server_url` (required): full VoceChat server URL, e.g. `http://localhost:3009` or `https://your.vocechat.domain` (no trailing `/`).
- `api_key` (required): API key generated for the bot account in VoceChat.
- `webhook_path` (recommended default/custom): webhook path used by AstrBot to receive VoceChat messages, e.g. `/vocechat_webhook`.
- `webhook_listen_host` (usually `0.0.0.0`): listen host for AstrBot webhook server.
- `webhook_port` (required): listen port for AstrBot webhook server, e.g. `8080`.
- `get_user_nickname_from_api` (boolean, default `true`): fetch nickname via VoceChat API.
- `send_plain_as_markdown` (boolean, default `false`): send plain text in markdown format.
- `default_bot_self_uid` (required): UID of your VoceChat bot account.
After configuration, click Save and test in VoceChat.
## Issue Reporting
If needed, report issues to:
- Plugin repo: <https://github.com/HikariFroya/astrbot_plugin_vocechat/issues>
- AstrBot repo: <https://github.com/AstrBotDevs/AstrBot/issues/new?template=bug-report.yml>
+137
View File
@@ -0,0 +1,137 @@
# Connect AstrBot to WeCom
AstrBot supports both WeCom Applications and WeCom Customer Service.
## Supported Basic Message Types
> Version v4.15.0.
| Message Type | Receive | Send | Notes |
| --- | --- | --- | --- |
| Text | Yes | Yes | |
| Image | Yes | Yes | |
| Voice | Yes | Yes | |
| Video | No | Yes | |
| File | No | Yes | |
Proactive message push: Supported for WeCom Application. Not fully tested for WeCom Customer Service.
## Before You Start
1. Open AstrBot Dashboard.
2. Click `Bots` in the left sidebar.
3. Click `+ Create Bot`.
4. Select `wecom`.
A configuration dialog will appear. Keep it open and continue with the steps below.
## Method 1: WeCom Customer Service
> [!NOTE]
> 1. Requires AstrBot >= v3.5.7.
> 2. This method works directly inside WeChat.
1. Open [WeCom Customer Service Console](https://kf.weixin.qq.com/) and sign in with WeCom QR login.
2. Create a customer service account in `Customer Service Account`, then copy its **name** (not account ID) to AstrBot field `wechat_kf_account_name`.
3. Go to [WeCom Enterprise Info](https://work.weixin.qq.com/wework_admin/frame#profile), copy `Corpid`, and fill AstrBot `corpid`.
4. Configure callback verification:
- If this is your first customer service bot, open `Development Configuration`, click `Start` next to internal access.
- If you used it before, open `Callback Configuration` directly and click edit.
![image](https://files.astrbot.app/docs/source/images/wecom/8287fd9fec5823847e6b590dc3f0f545.png)
5. Click random generation buttons to get `Token` and `EncodingAESKey`, then fill AstrBot `token` and `encoding_aes_key`.
6. Keep `Unified Webhook Mode (unified_webhook_mode)` enabled, click `Save`, and wait for adapter reload.
For callback URL:
- If unified mode is enabled, AstrBot generates a unique webhook callback URL after save. Copy it from logs or bot card in WebUI.
- If unified mode is disabled, use `http://<your-public-server-ip>:6195/callback/command`.
![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png)
> If unified mode is enabled, forward external requests to AstrBot port `6185`; otherwise forward to configured adapter port (default `6195`).
Back in WeCom Customer Service callback settings, click `Complete`. If successful, status shows completed.
7. In `Development Configuration`, get `Secret`, edit your WeCom adapter in AstrBot, set `secret`, then save again.
> [!TIP]
> Based on [#571](https://github.com/Soulter/AstrBot/issues/571), for newly registered enterprises, `corp_id` may take about 30 minutes to become valid.
Then open AstrBot `Console`, you should see logs asking you to open a WeChat scan link.
```txt
Please open the following link and scan with WeChat ...
```
![image](https://files.astrbot.app/docs/source/images/wecom/image-13.png)
Open the link, scan with WeChat, then send `help` in the customer service chat to test connectivity.
## Method 2: WeCom Application
Open: <https://work.weixin.qq.com/wework_admin/frame#apps>
1. Click `My Company`, copy enterprise ID (`Corpid`), and fill AstrBot `corpid`.
> [!TIP]
> For newly registered enterprises, `corp_id` may take time to become valid. See [#571](https://github.com/Soulter/AstrBot/issues/571).
![image](https://files.astrbot.app/docs/source/images/wecom/image-5.png)
2. Create a custom app (`Custom App`) and fill name/avatar/visibility scope.
3. Open the app, copy `Secret`, and fill AstrBot `secret`.
![image](https://files.astrbot.app/docs/source/images/wecom/image-4.png)
4. In app settings, find `Receive Messages`, click `Set API Receive`.
![image](https://files.astrbot.app/docs/source/images/wecom/image-6.png)
![image](https://files.astrbot.app/docs/source/images/wecom/image-9.png)
5. Generate `Token` and `EncodingAESKey`, fill AstrBot `token` and `encoding_aes_key`.
6. Keep `Unified Webhook Mode (unified_webhook_mode)` enabled (recommended), then click Save in AstrBot and wait for restart.
For callback URL:
- If unified mode is enabled, use the generated unique callback URL from logs or bot card.
- If unified mode is disabled, use `http://<your-public-server-ip>:6195/callback/command`.
![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png)
> If unified mode is enabled, forward to port `6185`; otherwise forward to configured adapter port (default `6195`).
7. Configure trusted enterprise IP in WeCom.
![image](https://files.astrbot.app/docs/source/images/wecom/image-10.png)
Add your public IP and confirm.
![image](https://files.astrbot.app/docs/source/images/wecom/image-12.png)
After AstrBot restart, return to API receive page and click save. If you see callback verification errors, re-check all required fields.
If save succeeds, AstrBot can receive messages from WeCom.
## Test
In WeCom Workbench, open the app you just created and send `/help`.
If AstrBot replies, integration is successful.
## Reverse Proxy (Custom API Base)
AstrBot supports custom WeCom endpoint (`api_base_url`) for environments without stable public IP.
Set your custom endpoint in `api_base_url`.
## Voice Input
Install `ffmpeg` for voice input support.
- Linux: `apt install ffmpeg`
- Windows: download from [FFmpeg website](https://ffmpeg.org/download.html)
- macOS: `brew install ffmpeg`
+87
View File
@@ -0,0 +1,87 @@
# Connect to WeCom AI Bot Platform
WeCom AI Bot is an official AI-friendly bot platform by WeCom. It can be used directly in one-on-one chats and internal group chats, and supports streaming responses.
AstrBot supports this platform since v4.3.5.
## Supported Basic Message Types
| Message Type | Receive | Send | Notes |
| --- | --- | --- | --- |
| Text | Yes | Yes | |
| Image | Yes | Yes | Requires message push Webhook URL to be configured. |
| Voice | No | Yes | Requires message push Webhook URL to be configured. |
| Video | No | Yes | Requires message push Webhook URL to be configured. |
| File | No | Yes | Requires message push Webhook URL to be configured. |
Proactive message push: Supported, but requires a message push Webhook URL.
## Configure WeCom AI Bot
1. Sign in to [WeCom Admin Console](https://work.weixin.qq.com/wework_admin).
2. In the left sidebar, open `Management Tools` -> `AI Bot`, then click Create Bot.
![Management Tools - AI Bot](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-1.png)
3. On the create page, choose `Create via API Mode`. Fill bot name/avatar and other basic info.
Generate `Token` and `EncodingAESKey` using random generation, but do not click Create yet.
![Create AI Bot Account](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image.png)
## Configure AstrBot
1. Open AstrBot Dashboard, click `Messaging Platforms`, then click `+ Add Adapter`, choose `WeCom AI Bot`.
![Add Adapter](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-2.png)
2. Fill AstrBot fields with values from the WeCom AI Bot create page:
- Bot name
- `token`
- `encoding_aes_key`
- `id` (any unique value)
- `port` (default `6198`, change if needed)
Keep `Unified Webhook Mode (unified_webhook_mode)` enabled and click `Save`.
3. Return to WeCom AI Bot create page and set `URL`:
- If unified mode is enabled, AstrBot generates a unique callback URL after save. Copy it from logs or bot card in WebUI.
- If unified mode is disabled, use `http://IP:port/webhook/wecom-ai-bot`.
![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png)
> It is recommended to use a domain + reverse proxy + HTTPS. You can also use [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/).
4. Click `Create`. If successful, you will enter bot details page.
If you see `Service did not respond correctly`, re-check AstrBot config and firewall rules.
![Bot Details](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-3.png)
5. Optional (recommended): Configure WeCom message push Webhook URL.
By default, WeCom AI Bot replies only when users send messages first. Configuring message push enables proactive notifications.
6. Optional (recommended): Enable `Send messages via Webhook only` for richer multi-message output and to bypass single-bubble reply limits.
This option requires the message push Webhook URL from step 5.
## Use the Bot
### Add Bot to Group Chat
In WeCom client internal group chat, click Add Member -> AI Bot, select the bot you created, and add it.
![Add Member](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-4.png)
![Added Successfully](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-5.png)
### Chat with the Bot
Send a message in private chat or group chat to talk to the bot.
If you need typing-like streaming effect, enable `Streaming Reply` in AstrBot.
![Streaming Reply](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-6.png)
## Help & Support
If you have issues during setup/use or need enterprise support, contact: [community@astrbot.app](mailto:community@astrbot.app).

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