From 357e199162c25ab47855427945792c0df98132a8 Mon Sep 17 00:00:00 2001 From: xunxiing <110362831+xunxiing@users.noreply.github.com> Date: Mon, 27 Apr 2026 08:37:04 +0000 Subject: [PATCH] docs: automated sync from main repo --- docs/snapshots/index.md | 5 + docs/snapshots/v4.23.6/.gitignore | 6 + docs/snapshots/v4.23.6/.vitepress/config.mjs | 708 +++++++ .../v4.23.6/.vitepress/config/head.ts | 47 + .../theme/components/ArticleShare.vue | 194 ++ .../theme/components/HomeFeaturesAfter.vue | 7 + .../.vitepress/theme/components/Layout.vue | 131 ++ .../.vitepress/theme/components/NotFound.vue | 73 + .../theme/components/SectionTabs.vue | 121 ++ .../v4.23.6/.vitepress/theme/index.js | 21 + .../.vitepress/theme/styles/custom-block.css | 185 ++ .../v4.23.6/.vitepress/theme/styles/font.css | 5 + .../v4.23.6/.vitepress/theme/styles/style.css | 358 ++++ docs/snapshots/v4.23.6/README.md | 10 + docs/snapshots/v4.23.6/SKILL.md | 130 ++ .../v4.23.6/Storage & Utils/file_storage.md | 29 + .../v4.23.6/Storage & Utils/kv_storage.md | 21 + .../v4.23.6/Storage & Utils/text_to_image.md | 109 ++ docs/snapshots/v4.23.6/agent/Invoke-llm.md | 86 + .../v4.23.6/agent/agent-related-hooks.md | 88 + docs/snapshots/v4.23.6/agent/agent-runner.md | 17 + .../v4.23.6/agent/context-compression.md | 50 + docs/snapshots/v4.23.6/agent/conversation.md | 52 + docs/snapshots/v4.23.6/agent/cron.md | 57 + docs/snapshots/v4.23.6/agent/index.md | 56 + .../v4.23.6/agent/offical-tool-list/tools.md | 43 + .../v4.23.6/agent/persona-resolution.md | 53 + docs/snapshots/v4.23.6/agent/persona-sets.md | 72 + docs/snapshots/v4.23.6/agent/registe tools.md | 111 ++ docs/snapshots/v4.23.6/agent/sandbox.md | 48 + docs/snapshots/v4.23.6/agent/subagents.md | 70 + .../design_standards/architecture_overview.md | 21 + .../design_standards/best_practices.md | 43 + .../v4.23.6/design_standards/context_usage.md | 30 + .../v4.23.6/design_standards/core_concepts.md | 128 ++ .../v4.23.6/design_standards/event_flow.md | 20 + .../v4.23.6/design_standards/sandbox.md | 20 + .../v4.23.6/design_standards/visual_utils.md | 89 + docs/snapshots/v4.23.6/en/community.md | 34 + .../v4.23.6/en/config/model-config.md | 12 + .../v4.23.6/en/deploy/astrbot/1panel.md | 27 + .../v4.23.6/en/deploy/astrbot/btpanel.md | 48 + .../v4.23.6/en/deploy/astrbot/casaos.md | 39 + .../v4.23.6/en/deploy/astrbot/cli.md | 92 + .../en/deploy/astrbot/community-deployment.md | 52 + .../v4.23.6/en/deploy/astrbot/compshare.md | 92 + .../v4.23.6/en/deploy/astrbot/docker.md | 91 + .../v4.23.6/en/deploy/astrbot/kubernetes.md | 197 ++ .../v4.23.6/en/deploy/astrbot/launcher.md | 98 + .../en/deploy/astrbot/other-deployments.md | 5 + .../v4.23.6/en/deploy/astrbot/package.md | 24 + .../v4.23.6/en/deploy/astrbot/sys-pm.md | 41 + .../v4.23.6/en/deploy/when-deployed.md | 16 + .../v4.23.6/en/dev/astrbot-config.md | 576 ++++++ docs/snapshots/v4.23.6/en/dev/openapi.md | 51 + .../v4.23.6/en/dev/plugin-platform-adapter.md | 185 ++ docs/snapshots/v4.23.6/en/dev/plugin.md | 1 + .../v4.23.6/en/dev/star/guides/ai.md | 489 +++++ .../v4.23.6/en/dev/star/guides/env.md | 48 + .../v4.23.6/en/dev/star/guides/html-to-pic.md | 66 + .../dev/star/guides/listen-message-event.md | 436 +++++ .../en/dev/star/guides/plugin-config.md | 219 +++ .../en/dev/star/guides/send-message.md | 131 ++ .../en/dev/star/guides/session-control.md | 113 ++ .../v4.23.6/en/dev/star/guides/simple.md | 58 + .../v4.23.6/en/dev/star/guides/storage.md | 32 + .../v4.23.6/en/dev/star/plugin-new.md | 128 ++ .../v4.23.6/en/dev/star/plugin-publish.md | 9 + docs/snapshots/v4.23.6/en/faq.md | 79 + docs/snapshots/v4.23.6/en/index.md | 31 + docs/snapshots/v4.23.6/en/ospp/2025.md | 31 + .../v4.23.6/en/others/self-host-t2i.md | 28 + .../v4.23.6/en/platform/aiocqhttp.md | 44 + .../snapshots/v4.23.6/en/platform/dingtalk.md | 65 + docs/snapshots/v4.23.6/en/platform/discord.md | 74 + docs/snapshots/v4.23.6/en/platform/kook.md | 46 + docs/snapshots/v4.23.6/en/platform/lark.md | 121 ++ docs/snapshots/v4.23.6/en/platform/line.md | 79 + docs/snapshots/v4.23.6/en/platform/matrix.md | 20 + .../v4.23.6/en/platform/mattermost.md | 139 ++ docs/snapshots/v4.23.6/en/platform/misskey.md | 113 ++ .../v4.23.6/en/platform/qqofficial.md | 8 + .../v4.23.6/en/platform/qqofficial/webhook.md | 93 + .../en/platform/qqofficial/websockets.md | 87 + .../v4.23.6/en/platform/satori/guide.md | 32 + .../en/platform/satori/server-satori.md | 65 + docs/snapshots/v4.23.6/en/platform/slack.md | 94 + docs/snapshots/v4.23.6/en/platform/start.md | 6 + .../snapshots/v4.23.6/en/platform/telegram.md | 55 + .../snapshots/v4.23.6/en/platform/vocechat.md | 44 + docs/snapshots/v4.23.6/en/platform/wecom.md | 137 ++ .../v4.23.6/en/platform/wecom_ai_bot.md | 87 + .../en/platform/weixin-official-account.md | 78 + .../v4.23.6/en/platform/weixin_oc.md | 74 + docs/snapshots/v4.23.6/en/providers/302ai.md | 21 + .../v4.23.6/en/providers/agent-runners.md | 19 + .../agent-runners/astrbot-agent-runner.md | 8 + .../en/providers/agent-runners/coze.md | 65 + .../en/providers/agent-runners/dashscope.md | 52 + .../en/providers/agent-runners/deerflow.md | 53 + .../en/providers/agent-runners/dify.md | 82 + .../v4.23.6/en/providers/aihubmix.md | 70 + docs/snapshots/v4.23.6/en/providers/coze.md | 1 + .../v4.23.6/en/providers/dashscope.md | 1 + docs/snapshots/v4.23.6/en/providers/dify.md | 1 + docs/snapshots/v4.23.6/en/providers/llm.md | 13 + docs/snapshots/v4.23.6/en/providers/newapi.md | 40 + docs/snapshots/v4.23.6/en/providers/ppio.md | 43 + .../v4.23.6/en/providers/provider-lmstudio.md | 38 + .../v4.23.6/en/providers/provider-ollama.md | 46 + .../v4.23.6/en/providers/siliconflow.md | 15 + docs/snapshots/v4.23.6/en/providers/start.md | 41 + .../v4.23.6/en/providers/tokenpony.md | 23 + docs/snapshots/v4.23.6/en/use/agent-runner.md | 52 + .../v4.23.6/en/use/astrbot-agent-sandbox.md | 389 ++++ .../v4.23.6/en/use/astrbot-sandbox.md | 0 .../v4.23.6/en/use/code-interpreter.md | 96 + docs/snapshots/v4.23.6/en/use/command.md | 110 ++ docs/snapshots/v4.23.6/en/use/computer.md | 139 ++ .../v4.23.6/en/use/context-compress.md | 40 + docs/snapshots/v4.23.6/en/use/custom-rules.md | 17 + .../v4.23.6/en/use/function-calling.md | 54 + .../v4.23.6/en/use/knowledge-base.md | 59 + docs/snapshots/v4.23.6/en/use/mcp.md | 102 + docs/snapshots/v4.23.6/en/use/plugin.md | 7 + .../v4.23.6/en/use/proactive-agent.md | 52 + docs/snapshots/v4.23.6/en/use/skills.md | 37 + docs/snapshots/v4.23.6/en/use/subagent.md | 56 + .../v4.23.6/en/use/unified-webhook.md | 28 + docs/snapshots/v4.23.6/en/use/websearch.md | 41 + docs/snapshots/v4.23.6/en/use/webui.md | 96 + docs/snapshots/v4.23.6/en/what-is-astrbot.md | 29 + docs/snapshots/v4.23.6/index.md | 16 + docs/snapshots/v4.23.6/messages/components.md | 41 + docs/snapshots/v4.23.6/messages/events.md | 33 + docs/snapshots/v4.23.6/messages/model.md | 31 + docs/snapshots/v4.23.6/messages/umo.md | 22 + docs/snapshots/v4.23.6/package.json | 13 + .../platform_adapters/adapter_interface.md | 374 ++++ .../platform_adapters/message_conversion.md | 29 + .../platform_adapters/telegram_media_group.md | 40 + .../v4.23.6/plugin-development-workflow.md | 133 ++ .../plugin_config/command_management.md | 34 + .../v4.23.6/plugin_config/decorators.md | 30 + .../v4.23.6/plugin_config/file_config.md | 30 + docs/snapshots/v4.23.6/plugin_config/hooks.md | 93 + .../v4.23.6/plugin_config/lifecycle.md | 32 + .../snapshots/v4.23.6/plugin_config/schema.md | 81 + .../v4.23.6/plugin_config/session_control.md | 31 + docs/snapshots/v4.23.6/pnpm-lock.yaml | 1554 +++++++++++++++ docs/snapshots/v4.23.6/public/404-seio.png | Bin 0 -> 195194 bytes docs/snapshots/v4.23.6/public/logo.png | Bin 0 -> 1467 bytes docs/snapshots/v4.23.6/public/logo_prod.png | Bin 0 -> 80735 bytes docs/snapshots/v4.23.6/public/openapi.json | 727 +++++++ docs/snapshots/v4.23.6/public/scalar.html | 25 + .../v4.23.6/scripts/sync_docs_to_wiki.py | 644 ++++++ .../scripts/upload-doc-images-to-r2.sh | 5 + .../scripts/upload_doc_images_to_r2.py | 344 ++++ docs/snapshots/v4.23.6/scripts/usage.md | 8 + .../v4.23.6/tests/test_sync_docs_to_wiki.py | 491 +++++ docs/snapshots/v4.23.6/vercel.json | 12 + .../v4.23.6/zh/community-events/ospp-2025.md | 31 + ...onggujiyu-astrbot-plugin-reward-program.md | 13 + docs/snapshots/v4.23.6/zh/community.md | 42 + .../v4.23.6/zh/deploy/astrbot/1panel.md | 27 + .../v4.23.6/zh/deploy/astrbot/btpanel.md | 48 + .../v4.23.6/zh/deploy/astrbot/casaos.md | 39 + .../v4.23.6/zh/deploy/astrbot/cli.md | 92 + .../zh/deploy/astrbot/community-deployment.md | 52 + .../v4.23.6/zh/deploy/astrbot/compshare.md | 89 + .../v4.23.6/zh/deploy/astrbot/desktop.md | 33 + .../v4.23.6/zh/deploy/astrbot/docker.md | 105 + .../v4.23.6/zh/deploy/astrbot/kubernetes.md | 197 ++ .../v4.23.6/zh/deploy/astrbot/launcher.md | 101 + .../zh/deploy/astrbot/other-deployments.md | 5 + .../v4.23.6/zh/deploy/astrbot/package.md | 24 + .../v4.23.6/zh/deploy/astrbot/rainyun.md | 44 + .../v4.23.6/zh/deploy/astrbot/sys-pm.md | 39 + .../v4.23.6/zh/deploy/when-deployed.md | 24 + .../v4.23.6/zh/dev/astrbot-config.md | 576 ++++++ docs/snapshots/v4.23.6/zh/dev/openapi.md | 150 ++ .../v4.23.6/zh/dev/plugin-platform-adapter.md | 185 ++ docs/snapshots/v4.23.6/zh/dev/plugin.md | 1 + .../v4.23.6/zh/dev/star/guides/ai.md | 553 ++++++ .../v4.23.6/zh/dev/star/guides/env.md | 48 + .../v4.23.6/zh/dev/star/guides/html-to-pic.md | 66 + .../dev/star/guides/listen-message-event.md | 452 +++++ .../v4.23.6/zh/dev/star/guides/other.md | 52 + .../zh/dev/star/guides/plugin-config.md | 218 +++ .../zh/dev/star/guides/send-message.md | 131 ++ .../zh/dev/star/guides/session-control.md | 113 ++ .../v4.23.6/zh/dev/star/guides/simple.md | 41 + .../v4.23.6/zh/dev/star/guides/storage.md | 32 + .../v4.23.6/zh/dev/star/plugin-new.md | 130 ++ .../v4.23.6/zh/dev/star/plugin-publish.md | 9 + docs/snapshots/v4.23.6/zh/dev/star/plugin.md | 1725 +++++++++++++++++ docs/snapshots/v4.23.6/zh/faq.md | 116 ++ docs/snapshots/v4.23.6/zh/index.md | 31 + .../v4.23.6/zh/others/github-proxy.md | 32 + docs/snapshots/v4.23.6/zh/others/ipv6.md | 35 + .../v4.23.6/zh/others/self-host-t2i.md | 27 + .../v4.23.6/zh/platform/aiocqhttp.md | 83 + .../snapshots/v4.23.6/zh/platform/dingtalk.md | 65 + docs/snapshots/v4.23.6/zh/platform/discord.md | 72 + docs/snapshots/v4.23.6/zh/platform/kook.md | 48 + docs/snapshots/v4.23.6/zh/platform/lark.md | 123 ++ docs/snapshots/v4.23.6/zh/platform/line.md | 78 + docs/snapshots/v4.23.6/zh/platform/matrix.md | 45 + .../v4.23.6/zh/platform/mattermost.md | 139 ++ docs/snapshots/v4.23.6/zh/platform/misskey.md | 112 ++ .../v4.23.6/zh/platform/qqofficial.md | 8 + .../v4.23.6/zh/platform/qqofficial/webhook.md | 108 ++ .../zh/platform/qqofficial/websockets.md | 90 + .../v4.23.6/zh/platform/satori/guide.md | 32 + .../zh/platform/satori/server-satori.md | 68 + docs/snapshots/v4.23.6/zh/platform/slack.md | 93 + docs/snapshots/v4.23.6/zh/platform/start.md | 8 + .../snapshots/v4.23.6/zh/platform/telegram.md | 56 + .../snapshots/v4.23.6/zh/platform/vocechat.md | 43 + docs/snapshots/v4.23.6/zh/platform/wecom.md | 148 ++ .../v4.23.6/zh/platform/wecom_ai_bot.md | 77 + .../zh/platform/weixin-official-account.md | 83 + .../v4.23.6/zh/platform/weixin_oc.md | 80 + .../v4.23.6/zh/platform/weixin_qr_entry.png | Bin 0 -> 46799 bytes docs/snapshots/v4.23.6/zh/providers/302ai.md | 21 + .../v4.23.6/zh/providers/agent-runners.md | 19 + .../agent-runners/astrbot-agent-runner.md | 8 + .../zh/providers/agent-runners/coze.md | 64 + .../zh/providers/agent-runners/dashscope.md | 51 + .../zh/providers/agent-runners/deerflow.md | 57 + .../zh/providers/agent-runners/dify.md | 81 + .../v4.23.6/zh/providers/aihubmix.md | 68 + docs/snapshots/v4.23.6/zh/providers/coze.md | 1 + .../v4.23.6/zh/providers/dashscope.md | 1 + docs/snapshots/v4.23.6/zh/providers/dify.md | 1 + docs/snapshots/v4.23.6/zh/providers/llm.md | 13 + docs/snapshots/v4.23.6/zh/providers/newapi.md | 38 + docs/snapshots/v4.23.6/zh/providers/ppio.md | 43 + .../v4.23.6/zh/providers/provider-lmstudio.md | 38 + .../v4.23.6/zh/providers/provider-ollama.md | 48 + .../v4.23.6/zh/providers/siliconflow.md | 19 + docs/snapshots/v4.23.6/zh/providers/start.md | 41 + .../v4.23.6/zh/providers/tokenpony.md | 23 + docs/snapshots/v4.23.6/zh/use/agent-runner.md | 52 + .../v4.23.6/zh/use/astrbot-agent-sandbox.md | 391 ++++ .../v4.23.6/zh/use/code-interpreter.md | 96 + docs/snapshots/v4.23.6/zh/use/command.md | 106 + docs/snapshots/v4.23.6/zh/use/computer.md | 137 ++ .../v4.23.6/zh/use/context-compress.md | 41 + docs/snapshots/v4.23.6/zh/use/custom-rules.md | 16 + .../v4.23.6/zh/use/function-calling.md | 52 + .../v4.23.6/zh/use/knowledge-base-old.md | 49 + .../v4.23.6/zh/use/knowledge-base.md | 60 + docs/snapshots/v4.23.6/zh/use/mcp.md | 101 + docs/snapshots/v4.23.6/zh/use/plugin.md | 7 + .../v4.23.6/zh/use/proactive-agent.md | 53 + docs/snapshots/v4.23.6/zh/use/skills.md | 38 + docs/snapshots/v4.23.6/zh/use/subagent.md | 56 + .../v4.23.6/zh/use/unified-webhook.md | 32 + docs/snapshots/v4.23.6/zh/use/websearch.md | 40 + docs/snapshots/v4.23.6/zh/use/webui.md | 96 + docs/snapshots/v4.23.6/zh/what-is-astrbot.md | 37 + scripts/state.json | 4 +- 263 files changed, 24847 insertions(+), 2 deletions(-) create mode 100644 docs/snapshots/index.md create mode 100644 docs/snapshots/v4.23.6/.gitignore create mode 100644 docs/snapshots/v4.23.6/.vitepress/config.mjs create mode 100644 docs/snapshots/v4.23.6/.vitepress/config/head.ts create mode 100644 docs/snapshots/v4.23.6/.vitepress/theme/components/ArticleShare.vue create mode 100644 docs/snapshots/v4.23.6/.vitepress/theme/components/HomeFeaturesAfter.vue create mode 100644 docs/snapshots/v4.23.6/.vitepress/theme/components/Layout.vue create mode 100644 docs/snapshots/v4.23.6/.vitepress/theme/components/NotFound.vue create mode 100644 docs/snapshots/v4.23.6/.vitepress/theme/components/SectionTabs.vue create mode 100644 docs/snapshots/v4.23.6/.vitepress/theme/index.js create mode 100644 docs/snapshots/v4.23.6/.vitepress/theme/styles/custom-block.css create mode 100644 docs/snapshots/v4.23.6/.vitepress/theme/styles/font.css create mode 100644 docs/snapshots/v4.23.6/.vitepress/theme/styles/style.css create mode 100644 docs/snapshots/v4.23.6/README.md create mode 100644 docs/snapshots/v4.23.6/SKILL.md create mode 100644 docs/snapshots/v4.23.6/Storage & Utils/file_storage.md create mode 100644 docs/snapshots/v4.23.6/Storage & Utils/kv_storage.md create mode 100644 docs/snapshots/v4.23.6/Storage & Utils/text_to_image.md create mode 100644 docs/snapshots/v4.23.6/agent/Invoke-llm.md create mode 100644 docs/snapshots/v4.23.6/agent/agent-related-hooks.md create mode 100644 docs/snapshots/v4.23.6/agent/agent-runner.md create mode 100644 docs/snapshots/v4.23.6/agent/context-compression.md create mode 100644 docs/snapshots/v4.23.6/agent/conversation.md create mode 100644 docs/snapshots/v4.23.6/agent/cron.md create mode 100644 docs/snapshots/v4.23.6/agent/index.md create mode 100644 docs/snapshots/v4.23.6/agent/offical-tool-list/tools.md create mode 100644 docs/snapshots/v4.23.6/agent/persona-resolution.md create mode 100644 docs/snapshots/v4.23.6/agent/persona-sets.md create mode 100644 docs/snapshots/v4.23.6/agent/registe tools.md create mode 100644 docs/snapshots/v4.23.6/agent/sandbox.md create mode 100644 docs/snapshots/v4.23.6/agent/subagents.md create mode 100644 docs/snapshots/v4.23.6/design_standards/architecture_overview.md create mode 100644 docs/snapshots/v4.23.6/design_standards/best_practices.md create mode 100644 docs/snapshots/v4.23.6/design_standards/context_usage.md create mode 100644 docs/snapshots/v4.23.6/design_standards/core_concepts.md create mode 100644 docs/snapshots/v4.23.6/design_standards/event_flow.md create mode 100644 docs/snapshots/v4.23.6/design_standards/sandbox.md create mode 100644 docs/snapshots/v4.23.6/design_standards/visual_utils.md create mode 100644 docs/snapshots/v4.23.6/en/community.md create mode 100644 docs/snapshots/v4.23.6/en/config/model-config.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/1panel.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/btpanel.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/casaos.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/cli.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/community-deployment.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/compshare.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/docker.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/kubernetes.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/launcher.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/other-deployments.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/package.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/astrbot/sys-pm.md create mode 100644 docs/snapshots/v4.23.6/en/deploy/when-deployed.md create mode 100644 docs/snapshots/v4.23.6/en/dev/astrbot-config.md create mode 100644 docs/snapshots/v4.23.6/en/dev/openapi.md create mode 100644 docs/snapshots/v4.23.6/en/dev/plugin-platform-adapter.md create mode 100644 docs/snapshots/v4.23.6/en/dev/plugin.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/guides/ai.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/guides/env.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/guides/html-to-pic.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/guides/listen-message-event.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/guides/plugin-config.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/guides/send-message.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/guides/session-control.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/guides/simple.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/guides/storage.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/plugin-new.md create mode 100644 docs/snapshots/v4.23.6/en/dev/star/plugin-publish.md create mode 100644 docs/snapshots/v4.23.6/en/faq.md create mode 100644 docs/snapshots/v4.23.6/en/index.md create mode 100644 docs/snapshots/v4.23.6/en/ospp/2025.md create mode 100644 docs/snapshots/v4.23.6/en/others/self-host-t2i.md create mode 100644 docs/snapshots/v4.23.6/en/platform/aiocqhttp.md create mode 100644 docs/snapshots/v4.23.6/en/platform/dingtalk.md create mode 100644 docs/snapshots/v4.23.6/en/platform/discord.md create mode 100644 docs/snapshots/v4.23.6/en/platform/kook.md create mode 100644 docs/snapshots/v4.23.6/en/platform/lark.md create mode 100644 docs/snapshots/v4.23.6/en/platform/line.md create mode 100644 docs/snapshots/v4.23.6/en/platform/matrix.md create mode 100644 docs/snapshots/v4.23.6/en/platform/mattermost.md create mode 100644 docs/snapshots/v4.23.6/en/platform/misskey.md create mode 100644 docs/snapshots/v4.23.6/en/platform/qqofficial.md create mode 100644 docs/snapshots/v4.23.6/en/platform/qqofficial/webhook.md create mode 100644 docs/snapshots/v4.23.6/en/platform/qqofficial/websockets.md create mode 100644 docs/snapshots/v4.23.6/en/platform/satori/guide.md create mode 100644 docs/snapshots/v4.23.6/en/platform/satori/server-satori.md create mode 100644 docs/snapshots/v4.23.6/en/platform/slack.md create mode 100644 docs/snapshots/v4.23.6/en/platform/start.md create mode 100644 docs/snapshots/v4.23.6/en/platform/telegram.md create mode 100644 docs/snapshots/v4.23.6/en/platform/vocechat.md create mode 100644 docs/snapshots/v4.23.6/en/platform/wecom.md create mode 100644 docs/snapshots/v4.23.6/en/platform/wecom_ai_bot.md create mode 100644 docs/snapshots/v4.23.6/en/platform/weixin-official-account.md create mode 100644 docs/snapshots/v4.23.6/en/platform/weixin_oc.md create mode 100644 docs/snapshots/v4.23.6/en/providers/302ai.md create mode 100644 docs/snapshots/v4.23.6/en/providers/agent-runners.md create mode 100644 docs/snapshots/v4.23.6/en/providers/agent-runners/astrbot-agent-runner.md create mode 100644 docs/snapshots/v4.23.6/en/providers/agent-runners/coze.md create mode 100644 docs/snapshots/v4.23.6/en/providers/agent-runners/dashscope.md create mode 100644 docs/snapshots/v4.23.6/en/providers/agent-runners/deerflow.md create mode 100644 docs/snapshots/v4.23.6/en/providers/agent-runners/dify.md create mode 100644 docs/snapshots/v4.23.6/en/providers/aihubmix.md create mode 100644 docs/snapshots/v4.23.6/en/providers/coze.md create mode 100644 docs/snapshots/v4.23.6/en/providers/dashscope.md create mode 100644 docs/snapshots/v4.23.6/en/providers/dify.md create mode 100644 docs/snapshots/v4.23.6/en/providers/llm.md create mode 100644 docs/snapshots/v4.23.6/en/providers/newapi.md create mode 100644 docs/snapshots/v4.23.6/en/providers/ppio.md create mode 100644 docs/snapshots/v4.23.6/en/providers/provider-lmstudio.md create mode 100644 docs/snapshots/v4.23.6/en/providers/provider-ollama.md create mode 100644 docs/snapshots/v4.23.6/en/providers/siliconflow.md create mode 100644 docs/snapshots/v4.23.6/en/providers/start.md create mode 100644 docs/snapshots/v4.23.6/en/providers/tokenpony.md create mode 100644 docs/snapshots/v4.23.6/en/use/agent-runner.md create mode 100644 docs/snapshots/v4.23.6/en/use/astrbot-agent-sandbox.md create mode 100644 docs/snapshots/v4.23.6/en/use/astrbot-sandbox.md create mode 100644 docs/snapshots/v4.23.6/en/use/code-interpreter.md create mode 100644 docs/snapshots/v4.23.6/en/use/command.md create mode 100644 docs/snapshots/v4.23.6/en/use/computer.md create mode 100644 docs/snapshots/v4.23.6/en/use/context-compress.md create mode 100644 docs/snapshots/v4.23.6/en/use/custom-rules.md create mode 100644 docs/snapshots/v4.23.6/en/use/function-calling.md create mode 100644 docs/snapshots/v4.23.6/en/use/knowledge-base.md create mode 100644 docs/snapshots/v4.23.6/en/use/mcp.md create mode 100644 docs/snapshots/v4.23.6/en/use/plugin.md create mode 100644 docs/snapshots/v4.23.6/en/use/proactive-agent.md create mode 100644 docs/snapshots/v4.23.6/en/use/skills.md create mode 100644 docs/snapshots/v4.23.6/en/use/subagent.md create mode 100644 docs/snapshots/v4.23.6/en/use/unified-webhook.md create mode 100644 docs/snapshots/v4.23.6/en/use/websearch.md create mode 100644 docs/snapshots/v4.23.6/en/use/webui.md create mode 100644 docs/snapshots/v4.23.6/en/what-is-astrbot.md create mode 100644 docs/snapshots/v4.23.6/index.md create mode 100644 docs/snapshots/v4.23.6/messages/components.md create mode 100644 docs/snapshots/v4.23.6/messages/events.md create mode 100644 docs/snapshots/v4.23.6/messages/model.md create mode 100644 docs/snapshots/v4.23.6/messages/umo.md create mode 100644 docs/snapshots/v4.23.6/package.json create mode 100644 docs/snapshots/v4.23.6/platform_adapters/adapter_interface.md create mode 100644 docs/snapshots/v4.23.6/platform_adapters/message_conversion.md create mode 100644 docs/snapshots/v4.23.6/platform_adapters/telegram_media_group.md create mode 100644 docs/snapshots/v4.23.6/plugin-development-workflow.md create mode 100644 docs/snapshots/v4.23.6/plugin_config/command_management.md create mode 100644 docs/snapshots/v4.23.6/plugin_config/decorators.md create mode 100644 docs/snapshots/v4.23.6/plugin_config/file_config.md create mode 100644 docs/snapshots/v4.23.6/plugin_config/hooks.md create mode 100644 docs/snapshots/v4.23.6/plugin_config/lifecycle.md create mode 100644 docs/snapshots/v4.23.6/plugin_config/schema.md create mode 100644 docs/snapshots/v4.23.6/plugin_config/session_control.md create mode 100644 docs/snapshots/v4.23.6/pnpm-lock.yaml create mode 100644 docs/snapshots/v4.23.6/public/404-seio.png create mode 100644 docs/snapshots/v4.23.6/public/logo.png create mode 100644 docs/snapshots/v4.23.6/public/logo_prod.png create mode 100644 docs/snapshots/v4.23.6/public/openapi.json create mode 100644 docs/snapshots/v4.23.6/public/scalar.html create mode 100644 docs/snapshots/v4.23.6/scripts/sync_docs_to_wiki.py create mode 100755 docs/snapshots/v4.23.6/scripts/upload-doc-images-to-r2.sh create mode 100755 docs/snapshots/v4.23.6/scripts/upload_doc_images_to_r2.py create mode 100644 docs/snapshots/v4.23.6/scripts/usage.md create mode 100644 docs/snapshots/v4.23.6/tests/test_sync_docs_to_wiki.py create mode 100644 docs/snapshots/v4.23.6/vercel.json create mode 100644 docs/snapshots/v4.23.6/zh/community-events/ospp-2025.md create mode 100644 docs/snapshots/v4.23.6/zh/community-events/tonggujiyu-astrbot-plugin-reward-program.md create mode 100644 docs/snapshots/v4.23.6/zh/community.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/1panel.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/btpanel.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/casaos.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/cli.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/community-deployment.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/compshare.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/desktop.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/docker.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/kubernetes.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/launcher.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/other-deployments.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/package.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/rainyun.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/astrbot/sys-pm.md create mode 100644 docs/snapshots/v4.23.6/zh/deploy/when-deployed.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/astrbot-config.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/openapi.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/plugin-platform-adapter.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/plugin.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/guides/ai.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/guides/env.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/guides/html-to-pic.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/guides/listen-message-event.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/guides/other.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/guides/plugin-config.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/guides/send-message.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/guides/session-control.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/guides/simple.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/guides/storage.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/plugin-new.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/plugin-publish.md create mode 100644 docs/snapshots/v4.23.6/zh/dev/star/plugin.md create mode 100644 docs/snapshots/v4.23.6/zh/faq.md create mode 100644 docs/snapshots/v4.23.6/zh/index.md create mode 100644 docs/snapshots/v4.23.6/zh/others/github-proxy.md create mode 100644 docs/snapshots/v4.23.6/zh/others/ipv6.md create mode 100644 docs/snapshots/v4.23.6/zh/others/self-host-t2i.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/aiocqhttp.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/dingtalk.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/discord.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/kook.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/lark.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/line.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/matrix.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/mattermost.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/misskey.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/qqofficial.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/qqofficial/webhook.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/qqofficial/websockets.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/satori/guide.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/satori/server-satori.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/slack.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/start.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/telegram.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/vocechat.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/wecom.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/wecom_ai_bot.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/weixin-official-account.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/weixin_oc.md create mode 100644 docs/snapshots/v4.23.6/zh/platform/weixin_qr_entry.png create mode 100644 docs/snapshots/v4.23.6/zh/providers/302ai.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/agent-runners.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/agent-runners/astrbot-agent-runner.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/agent-runners/coze.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/agent-runners/dashscope.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/agent-runners/deerflow.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/agent-runners/dify.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/aihubmix.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/coze.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/dashscope.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/dify.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/llm.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/newapi.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/ppio.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/provider-lmstudio.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/provider-ollama.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/siliconflow.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/start.md create mode 100644 docs/snapshots/v4.23.6/zh/providers/tokenpony.md create mode 100644 docs/snapshots/v4.23.6/zh/use/agent-runner.md create mode 100644 docs/snapshots/v4.23.6/zh/use/astrbot-agent-sandbox.md create mode 100644 docs/snapshots/v4.23.6/zh/use/code-interpreter.md create mode 100644 docs/snapshots/v4.23.6/zh/use/command.md create mode 100644 docs/snapshots/v4.23.6/zh/use/computer.md create mode 100644 docs/snapshots/v4.23.6/zh/use/context-compress.md create mode 100644 docs/snapshots/v4.23.6/zh/use/custom-rules.md create mode 100644 docs/snapshots/v4.23.6/zh/use/function-calling.md create mode 100644 docs/snapshots/v4.23.6/zh/use/knowledge-base-old.md create mode 100644 docs/snapshots/v4.23.6/zh/use/knowledge-base.md create mode 100644 docs/snapshots/v4.23.6/zh/use/mcp.md create mode 100644 docs/snapshots/v4.23.6/zh/use/plugin.md create mode 100644 docs/snapshots/v4.23.6/zh/use/proactive-agent.md create mode 100644 docs/snapshots/v4.23.6/zh/use/skills.md create mode 100644 docs/snapshots/v4.23.6/zh/use/subagent.md create mode 100644 docs/snapshots/v4.23.6/zh/use/unified-webhook.md create mode 100644 docs/snapshots/v4.23.6/zh/use/websearch.md create mode 100644 docs/snapshots/v4.23.6/zh/use/webui.md create mode 100644 docs/snapshots/v4.23.6/zh/what-is-astrbot.md diff --git a/docs/snapshots/index.md b/docs/snapshots/index.md new file mode 100644 index 0000000..70ab9ae --- /dev/null +++ b/docs/snapshots/index.md @@ -0,0 +1,5 @@ +# 文档快照 + +这里存放按 AstrBot Tag 归档的文档快照。 + +- [v4.23.6](/snapshots/v4.23.6/) diff --git a/docs/snapshots/v4.23.6/.gitignore b/docs/snapshots/v4.23.6/.gitignore new file mode 100644 index 0000000..3562259 --- /dev/null +++ b/docs/snapshots/v4.23.6/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +venv/ +.DS_Store +node_modules/ +.vitepress/cache +*dist diff --git a/docs/snapshots/v4.23.6/.vitepress/config.mjs b/docs/snapshots/v4.23.6/.vitepress/config.mjs new file mode 100644 index 0000000..7fc6ea0 --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/config.mjs @@ -0,0 +1,708 @@ +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: [ + // Skill-specific pages that don't exist in upstream docs + '/use/mcp', + '/use/knowledge-base', + '/use/websearch', + '/use/astrbot-agent-sandbox', + '/use/skills', + '/use/agent-runner', + '/deploy/astrbot/docker', + '/deploy/astrbot/kubernetes', + '/deploy/astrbot/launcher', + '/deploy/astrbot/rainyun', + '/platform/qqofficial/webhook', + '/platform/qqofficial/websockets', + '/dev/star/plugin', + '/providers/agent-runners/astrbot-agent-runner', + '/providers/agent-runners/dify', + '/providers/agent-runners/coze', + '/providers/agent-runners/dashscope', + '/providers/agent-runners/deerflow', + '/providers/start', + // Relative links in provider pages + './../agent-runners/coze', + './../agent-runners/dashscope', + './../agent-runners/dify', + ], + + 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: "Skill 开发", + base: "/design_standards", + collapsed: true, + items: [ + { text: "核心概念", link: "/core_concepts" }, + { text: "架构总览", link: "/architecture_overview" }, + { text: "最佳实践", link: "/best_practices" }, + { text: "上下文使用", link: "/context_usage" }, + { text: "事件流", link: "/event_flow" }, + { text: "沙盒", link: "/sandbox" }, + { text: "视觉工具", link: "/visual_utils" }, + ], + }, + { + text: "Agent", + base: "/agent", + collapsed: true, + items: [ + { text: "Agent 概览", link: "/" }, + { text: "Agent Runner", link: "/agent-runner" }, + { text: "对话", link: "/conversation" }, + { text: "上下文压缩", link: "/context-compression" }, + { text: "定时任务", link: "/cron" }, + { text: "调用 LLM", link: "/Invoke-llm" }, + { text: "人格解析", link: "/persona-resolution" }, + { text: "人格集合", link: "/persona-sets" }, + { text: "沙盒", link: "/sandbox" }, + { text: "子智能体", link: "/subagents" }, + { text: "Agent 相关钩子", link: "/agent-related-hooks" }, + { text: "官方工具列表", link: "/offical-tool-list/tools" }, + ], + }, + { + text: "消息", + base: "/messages", + collapsed: true, + items: [ + { text: "组件", link: "/components" }, + { text: "事件", link: "/events" }, + { text: "模型", link: "/model" }, + { text: "UMO", link: "/umo" }, + ], + }, + { + text: "平台适配器", + base: "/platform_adapters", + collapsed: true, + items: [ + { text: "适配器接口", link: "/adapter_interface" }, + { text: "消息转换", link: "/message_conversion" }, + { text: "Telegram 媒体组", link: "/telegram_media_group" }, + ], + }, + { + text: "插件配置", + base: "/plugin_config", + collapsed: true, + items: [ + { text: "命令管理", link: "/command_management" }, + { text: "装饰器", link: "/decorators" }, + { text: "文件配置", link: "/file_config" }, + { text: "钩子", link: "/hooks" }, + { text: "生命周期", link: "/lifecycle" }, + { text: "Schema", link: "/schema" }, + { text: "会话控制", link: "/session_control" }, + ], + }, + { + text: "存储与工具", + base: "/Storage & Utils", + collapsed: true, + items: [ + { text: "文件存储", link: "/file_storage" }, + { text: "KV 存储", link: "/kv_storage" }, + { text: "文转图", link: "/text_to_image" }, + ], + }, + { + 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", + link: "/aiocqhttp" + }, + { text: "企微应用", link: "/wecom" }, + { text: "企微智能机器人", link: "/wecom_ai_bot" }, + { text: "微信公众号", link: "/weixin-official-account" }, + { text: "个人微信", link: "/weixin_oc" }, + { text: "飞书", link: "/lark" }, + { text: "钉钉", link: "/dingtalk" }, + { text: "Telegram", link: "/telegram" }, + { text: "LINE", link: "/line" }, + { text: "Slack", link: "/slack" }, + { text: "Mattermost", link: "/mattermost" }, + { text: "Misskey", link: "/misskey" }, + { text: "Discord", link: "/discord" }, + { text: "KOOK", link: "/kook" }, + { + text: "Satori", + base: "/platform/satori", + collapsed: true, + items: [ + { text: "接入 Satori", link: "/guide" }, + { 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: "使用电脑能力", link: "/computer" }, + { 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: "/community-events", + collapsed: false, + items: [ + { text: "开源之夏 2025", link: "/ospp-2025" }, + { text: "桐谷霁屿 x AstrBot 插件奖励活动", link: "/tonggujiyu-astrbot-plugin-reward-program" }, + ], + }, + ], + outline: { + level: 'deep', + label: '目录', + }, + darkModeSwitchLabel: '切换日光/暗黑模式', + sidebarMenuLabel: '文章', + returnToTopLabel: '返回顶部', + docFooter: { + prev: '上一篇', + next: '下一篇' + }, + editLink: { + pattern: 'https://github.com/AstrBotdevs/AstrBot/edit/master/docs/:path', + text: '发现文档有问题?在 GitHub 上编辑此页', + }, + logo: '/logo_prod.png', + socialLinks: [ + { icon: "github", link: "https://github.com/AstrBotDevs/AstrBot" }, + ], + footer: { + message: 'Deployed on ' + + '' + + 'Rainyun Logo' + + '', + } + } + }, + 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: "Skill Development", + base: "/design_standards", + collapsed: true, + items: [ + { text: "Core Concepts", link: "/core_concepts" }, + { text: "Architecture Overview", link: "/architecture_overview" }, + { text: "Best Practices", link: "/best_practices" }, + { text: "Context Usage", link: "/context_usage" }, + { text: "Event Flow", link: "/event_flow" }, + { text: "Sandbox", link: "/sandbox" }, + { text: "Visual Utils", link: "/visual_utils" }, + ], + }, + { + text: "Agent", + base: "/agent", + collapsed: true, + items: [ + { text: "Agent Overview", link: "/" }, + { text: "Agent Runner", link: "/agent-runner" }, + { text: "Conversation", link: "/conversation" }, + { text: "Context Compression", link: "/context-compression" }, + { text: "Cron", link: "/cron" }, + { text: "Invoke LLM", link: "/Invoke-llm" }, + { text: "Persona Resolution", link: "/persona-resolution" }, + { text: "Persona Sets", link: "/persona-sets" }, + { text: "Sandbox", link: "/sandbox" }, + { text: "Subagents", link: "/subagents" }, + { text: "Agent Related Hooks", link: "/agent-related-hooks" }, + { text: "Official Tool List", link: "/offical-tool-list/tools" }, + ], + }, + { + text: "Messages", + base: "/messages", + collapsed: true, + items: [ + { text: "Components", link: "/components" }, + { text: "Events", link: "/events" }, + { text: "Model", link: "/model" }, + { text: "UMO", link: "/umo" }, + ], + }, + { + text: "Platform Adapters", + base: "/platform_adapters", + collapsed: true, + items: [ + { text: "Adapter Interface", link: "/adapter_interface" }, + { text: "Message Conversion", link: "/message_conversion" }, + { text: "Telegram Media Group", link: "/telegram_media_group" }, + ], + }, + { + text: "Plugin Config", + base: "/plugin_config", + collapsed: true, + items: [ + { text: "Command Management", link: "/command_management" }, + { text: "Decorators", link: "/decorators" }, + { text: "File Config", link: "/file_config" }, + { text: "Hooks", link: "/hooks" }, + { text: "Lifecycle", link: "/lifecycle" }, + { text: "Schema", link: "/schema" }, + { text: "Session Control", link: "/session_control" }, + ], + }, + { + text: "Storage & Utils", + base: "/Storage & Utils", + collapsed: true, + items: [ + { text: "File Storage", link: "/file_storage" }, + { text: "KV Storage", link: "/kv_storage" }, + { text: "Text to Image", link: "/text_to_image" }, + ], + }, + { + 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", + link: "/aiocqhttp", + }, + { text: "WeCom Application", link: "/wecom" }, + { text: "WeCom AI Bot", link: "/wecom_ai_bot" }, + { text: "WeChat Official Account", link: "/weixin-official-account" }, + { text: "Personal WeChat", link: "/weixin_oc" }, + { text: "Lark", link: "/lark" }, + { text: "DingTalk", link: "/dingtalk" }, + { text: "Telegram", link: "/telegram" }, + { text: "LINE", link: "/line" }, + { text: "Slack", link: "/slack" }, + { text: "Mattermost", link: "/mattermost" }, + { text: "Misskey", link: "/misskey" }, + { text: "Discord", link: "/discord" }, + { + text: "Satori", + base: "/en/platform/satori", + collapsed: true, + items: [ + { text: "Connect Satori", link: "/guide" }, + { 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: "Computer Use", link: "/computer" }, + { 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/edit/master/docs/:path', + text: 'Edit this page on GitHub', + }, + logo: '/logo_prod.png', + socialLinks: [ + { icon: "github", link: "https://github.com/AstrBotDevs/AstrBot" }, + ], + footer: { + message: 'Deployed on ' + + '' + + 'Rainyun Logo' + + '', + } + } + }, + }, + + themeConfig: { + search: { + provider: "local", + options: { + locales: { + root: { + translations: { + button: { + buttonText: "搜索文档", + buttonAriaLabel: "搜索文档", + }, + modal: { + noResultsText: "无法找到相关结果", + resetButtonTitle: "清除查询条件", + footer: { + selectText: "选择", + navigateText: "切换", + closeText: "关闭", + }, + }, + }, + }, + }, + }, + }, + } +}); diff --git a/docs/snapshots/v4.23.6/.vitepress/config/head.ts b/docs/snapshots/v4.23.6/.vitepress/config/head.ts new file mode 100644 index 0000000..efa645e --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/config/head.ts @@ -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" }], +]; \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/.vitepress/theme/components/ArticleShare.vue b/docs/snapshots/v4.23.6/.vitepress/theme/components/ArticleShare.vue new file mode 100644 index 0000000..35115e5 --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/theme/components/ArticleShare.vue @@ -0,0 +1,194 @@ + + + + + \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/.vitepress/theme/components/HomeFeaturesAfter.vue b/docs/snapshots/v4.23.6/.vitepress/theme/components/HomeFeaturesAfter.vue new file mode 100644 index 0000000..2033a72 --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/theme/components/HomeFeaturesAfter.vue @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/.vitepress/theme/components/Layout.vue b/docs/snapshots/v4.23.6/.vitepress/theme/components/Layout.vue new file mode 100644 index 0000000..e8ea0cb --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/theme/components/Layout.vue @@ -0,0 +1,131 @@ + + + + + diff --git a/docs/snapshots/v4.23.6/.vitepress/theme/components/NotFound.vue b/docs/snapshots/v4.23.6/.vitepress/theme/components/NotFound.vue new file mode 100644 index 0000000..f3b01e2 --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/theme/components/NotFound.vue @@ -0,0 +1,73 @@ + + + + + \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/.vitepress/theme/components/SectionTabs.vue b/docs/snapshots/v4.23.6/.vitepress/theme/components/SectionTabs.vue new file mode 100644 index 0000000..80b9988 --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/theme/components/SectionTabs.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/docs/snapshots/v4.23.6/.vitepress/theme/index.js b/docs/snapshots/v4.23.6/.vitepress/theme/index.js new file mode 100644 index 0000000..02131c6 --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/theme/index.js @@ -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) + }) + } +} diff --git a/docs/snapshots/v4.23.6/.vitepress/theme/styles/custom-block.css b/docs/snapshots/v4.23.6/.vitepress/theme/styles/custom-block.css new file mode 100644 index 0000000..951f1d0 --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/theme/styles/custom-block.css @@ -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; +} \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/.vitepress/theme/styles/font.css b/docs/snapshots/v4.23.6/.vitepress/theme/styles/font.css new file mode 100644 index 0000000..9b37c44 --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/theme/styles/font.css @@ -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; +} diff --git a/docs/snapshots/v4.23.6/.vitepress/theme/styles/style.css b/docs/snapshots/v4.23.6/.vitepress/theme/styles/style.css new file mode 100644 index 0000000..6c1e155 --- /dev/null +++ b/docs/snapshots/v4.23.6/.vitepress/theme/styles/style.css @@ -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; +} diff --git a/docs/snapshots/v4.23.6/README.md b/docs/snapshots/v4.23.6/README.md new file mode 100644 index 0000000..10cc7ac --- /dev/null +++ b/docs/snapshots/v4.23.6/README.md @@ -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) + diff --git a/docs/snapshots/v4.23.6/SKILL.md b/docs/snapshots/v4.23.6/SKILL.md new file mode 100644 index 0000000..a6e5862 --- /dev/null +++ b/docs/snapshots/v4.23.6/SKILL.md @@ -0,0 +1,130 @@ +--- +name: skill-astrbot-dev +description: Reference + workflow notes for AstrBot plugin development (messages, platform adapters, plugin config, agent system). +metadata: + short-description: AstrBot dev reference +--- +# skill-astrbot-dev + +This skill is the source-of-truth index for AstrBot developer docs in this repo (`docs/`). + +Goal: when this skill is selected, immediately ground on the minimum required docs + code entrypoints, +avoid duplicated reading, and always prefer code as the final authority. + +## When to use + +Use this skill when you ask for help with: + +- AstrBot plugin structure, decorators/hooks, lifecycle, schema, sessions +- Message model/event flow and message-chain conversion +- Platform adapter interface and message conversion patterns +- Agent topics (tools/providers/personas/subagents/sandbox/cron/context compression) + +## Mandatory workflow (use this every time) + +1. Start from a single entrypoint (avoid broad loading): + - Site index: `docs/index.md` + - Core concepts: `docs/design_standards/core_concepts.md` +2. Pick one topic folder and stay focused: + - Agent system: `docs/agent/` + - Plugin config: `docs/plugin_config/` + - Messages: `docs/messages/` + - Platform adapters: `docs/platform_adapters/` +3. For Agent Runner (v4.7.0+): `docs/agent/agent-runner.md` +3. If the user targets a specific AstrBot version, cross-check: + - `docs/snapshots//` +4. If docs and code disagree, treat code as truth: + - Core code lives under `astrbotcore/astrbot/core/` (read only the needed files) + +## STRONGLY ADVISED: use AstrBot SDK while writing plugins + +When writing plugin code, strongly advised to install AstrBot SDK locally and use it for API reference, +signature lookup, and IDE auto-completion. + +```powershell +python -m pip install -U astrbot +``` + +Use SDK symbols first when implementing hooks, provider/context calls, and agent runner integration. +This helps reduce guesswork and signature mismatch. + +If AstrBot source code in this repo is available, still treat repo code as higher priority than package docs. + +## Plugin project structure (strongly advised) + +A standard AstrBot plugin project should include: + +- `main.py`: entrypoint. Implement plugin startup and primary features here. +- `metadata.yaml`: plugin metadata (name, version, author, repo, description). +- `README.md`: installation, usage, feature overview, and dev links. +- `.gitignore`: ignore Python cache (`__pycache__`) and IDE config files. +- `LICENSE`: open-source license file. + +## `metadata.yaml` minimal template + +```yaml +name: astrbot_plugin_helloworld # 插件唯一识别名,最好以 astrbot_plugin_ 前缀开头 +display_name: helloworld # 展示名(v4.5.0+) +desc: AstrBot 插件示例。 # 插件简短描述 +version: v1.3.0 # 版本号:v1.1.1 或 v1.1 +author: Soulter # 作者 +repo: https://github.com/Soulter/helloworld # 插件的仓库地址 +``` + +## Code rules for plugin implementation + +- Use `async def` for handlers/hooks/tool functions. +- Keep `main.py` focused on plugin entry and orchestration; extract complex logic into submodules. +- Add type hints for public methods and hook signatures. +- Do not hardcode provider IDs or secrets; expose configurable fields in `_conf_schema.json`. +- Prefer small, testable functions over large monolithic handler bodies. +- Keep README and metadata consistent with actual plugin behavior and version. +-If you are writing AstrBot core code instead of plugins, you must submit a PR to https://github.com/AstrBotDevs/AstrBot-docs if the changes require doc updates (for instance: new hooks, new APIs, new features, platform adapter changes, and so on). If you don't see the docs repo, please remind the user to clone the docs-repo and add it to the workspace. +## Hooks: avoid missing / outdated references + +There are two different "hook" layers you must not mix up: + +- Plugin event hooks (decorators): `docs/plugin_config/hooks.md` +- Agent runner hooks (`BaseAgentRunHooks`): `docs/agent/agent-related-hooks.md` + +If you need a complete hook inventory (because context may be truncated), generate it locally: + +```powershell +python scripts/generate_hook_inventory.py +``` + +This writes to `docs/.tmp/hook_inventory/` (gitignored). Use it as a scratchpad for writing/updating docs; +do not reference `.tmp` paths as public documentation URLs. + +## High-signal code entrypoints (open only when needed) + +- Event hooks registration + signatures: `astrbotcore/astrbot/core/star/register/star_handler.py` +- Event types: `astrbotcore/astrbot/core/star/star_handler.py` +- Agent runners + hook call order: `astrbotcore/astrbot/core/agent/runners/` +- Agent hook interface: `astrbotcore/astrbot/core/agent/hooks.py` +- Main agent build (sandbox/cron/tools): `astrbotcore/astrbot/core/astr_main_agent.py` +- Skills system (AstrBot runtime skills): `astrbotcore/astrbot/core/skills/skill_manager.py` +- Subagents config loading: `astrbotcore/astrbot/core/subagent_orchestrator.py` + +## v4.5.7+ New Tool Definition Pattern + +推荐使用 dataclass 模式定义 Tool(见 `docs/design_standards/core_concepts.md` 第7节): + +```python +from pydantic.dataclasses import dataclass +from astrbot.core.agent.tool import FunctionTool + +@dataclass +class MyTool(FunctionTool): + name: str = "my_tool" + description: str = "工具描述" + parameters: dict = {...} + + async def call(self, context, **kwargs) -> str: + return "结果" +``` + +注册:`self.context.add_llm_tools(MyTool())` + +装饰器方式仍然支持,但推荐新项目使用 dataclass 模式。 + diff --git a/docs/snapshots/v4.23.6/Storage & Utils/file_storage.md b/docs/snapshots/v4.23.6/Storage & Utils/file_storage.md new file mode 100644 index 0000000..a248d0d --- /dev/null +++ b/docs/snapshots/v4.23.6/Storage & Utils/file_storage.md @@ -0,0 +1,29 @@ +--- +category: storage +--- + +# 文件存储规范 + +对于大文件、日志或插件特有的资源文件,AstrBot 建议遵循以下存储规范。 + +### 目录规范 + +所有插件特有的文件应存储在以下目录: +`data/plugin_data/{plugin_name}/` + +### 获取存储路径 + +建议在插件中使用以下方式获取路径,以确保兼容性: + +```python +from astrbot.core.utils.astrbot_path import get_astrbot_data_path + +# 获取插件专属数据目录 +plugin_data_path = get_astrbot_data_path() / "plugin_data" / self.name +plugin_data_path.mkdir(parents=True, exist_ok=True) # 确保目录存在 +``` + +### 注意事项 + +- 不要将大文件直接存储在 `docs/` 或插件根目录下。 +- 建议定期清理不再使用的临时文件。 diff --git a/docs/snapshots/v4.23.6/Storage & Utils/kv_storage.md b/docs/snapshots/v4.23.6/Storage & Utils/kv_storage.md new file mode 100644 index 0000000..62e98f3 --- /dev/null +++ b/docs/snapshots/v4.23.6/Storage & Utils/kv_storage.md @@ -0,0 +1,21 @@ +--- +category: storage +--- + +# 键值对存储 (KV Storage) + +AstrBot 为插件提供了简单易用的 KV 存储接口,适合存储配置、轻量级状态或用户数据。 + +### 核心接口 (>= v4.9.2) + +这些方法在插件类(继承自 `Star`)中可以直接调用: + +- `await self.put_kv_data(key: str, value: Any)`: 存储数据。 +- `await self.get_kv_data(key: str, default: Any = None) -> Any`: 获取数据。 +- `await self.delete_kv_data(key: str)`: 删除数据。 + +### 特点 + +- **隔离性**: 数据按插件 ID 隔离,不同插件之间的 Key 不会冲突。 +- **持久化**: 数据会自动持久化到 `data/metadata/kv_storage.db`(或相应目录)。 +- **异步**: 接口均为异步方法。 diff --git a/docs/snapshots/v4.23.6/Storage & Utils/text_to_image.md b/docs/snapshots/v4.23.6/Storage & Utils/text_to_image.md new file mode 100644 index 0000000..a3203a9 --- /dev/null +++ b/docs/snapshots/v4.23.6/Storage & Utils/text_to_image.md @@ -0,0 +1,109 @@ +# 文转图 (Text to Image) + +将文本或 HTML 模板渲染为图片。 + +## 插件方法(Star) + +### `text_to_image` + +```python +async def text_to_image(self, text: str, return_url: bool = True) -> str +``` + +- 内部调用:`html_renderer.render_t2i(...)` +- 使用当前激活模板:`t2i_active_template` +- `return_url=True` 返回可发送的 URL;`False` 返回本地文件路径 +- **网络渲染失败会自动 fallback 到本地渲染** + +```python +url = await self.text_to_image("你好,AstrBot") +yield event.image_result(url) +``` + +### `html_render` + +```python +async def html_render(self, tmpl: str, data: dict, return_url: bool = True, options: dict | None = None) -> str +``` + +- 内部调用:`html_renderer.render_custom_template(...)` +- 适合自定义 HTML + Jinja2 模板渲染 + +```python +tmpl = """ +
+

{{ title }}

+ +
+""" +url = await self.html_render(tmpl, {"title": "Todo", "items": ["吃饭", "睡觉"]}) +yield event.image_result(url) +``` + +## SDK 方法(`html_renderer`) + +```python +from astrbot.api import html_renderer +``` + +### 初始化 + +```python +await html_renderer.initialize() +``` + +### 默认文转图 + +```python +await html_renderer.render_t2i( + text: str, + use_network: bool = True, + return_url: bool = False, + template_name: str | None = None, +) +``` + +- `use_network=True` 先走网络渲染;失败时 fallback 到本地渲染 +- `return_url=False` 时返回本地路径 + +### 自定义模板渲染 + +```python +await html_renderer.render_custom_template( + tmpl_str: str, + tmpl_data: dict, + return_url: bool = False, + options: dict | None = None, +) +``` + +## 渲染选项(`html_render` / `render_custom_template`) + +`options` 透传给截图参数(Playwright 风格): + +- `timeout` +- `type`: `"jpeg" | "png"` +- `quality`(仅 jpeg) +- `omit_background`(仅 png) +- `full_page` +- `clip` +- `animations`: `"allow" | "disabled"` +- `caret`: `"hide" | "initial"` +- `scale`: `"css" | "device"` + +默认值(未传 `options` 时): + +```python +{"full_page": True, "type": "jpeg", "quality": 40} +``` + +## 模板管理方法 + +`TemplateManager` 提供模板 CRUD: + +- `list_templates()` +- `get_template(name)` +- `create_template(name, content)` +- `update_template(name, content)` +- `delete_template(name)` +- `reset_default_template()` diff --git a/docs/snapshots/v4.23.6/agent/Invoke-llm.md b/docs/snapshots/v4.23.6/agent/Invoke-llm.md new file mode 100644 index 0000000..6229426 --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/Invoke-llm.md @@ -0,0 +1,86 @@ +Provider 是模型能力入口(Chat/STT/TTS/Embedding) + +### 获取当前会话使用的 Chat Provider ID + +```python +prov_id = await ctx.get_current_chat_provider_id(umo) +``` +- `await get_current_chat_provider_id(umo: str) -> str`:返回当前会话的 chat provider ID + +### 简化 LLM 调用 + +```python +llm_resp = await ctx.llm_generate( + chat_provider_id=prov_id, + prompt="Hello!", + system_prompt="You are a helpful assistant.", +) +print(llm_resp.completion_text) +``` +- `await llm_generate(chat_provider_id, prompt, contexts=None, image_urls=None, system_prompt=None, tools=None) -> LLMResponse`: 简化的 LLM 调用接口,不自动执行 tool call + +### 工具循环 Agent + +```python +llm_resp = await ctx.tool_loop_agent( + event=event, + chat_provider_id=prov_id, + prompt="搜索 AstrBot 相关信息", + tools=ToolSet([SearchTool()]), + max_steps=30, + tool_call_timeout=60, +) +``` +- `await tool_loop_agent(event, chat_provider_id, prompt, contexts=None, image_urls=None, tools=None, system_prompt=None, max_steps=30, tool_call_timeout=120, **kwargs) -> LLMResponse` + - `event`: AstrMessageEvent,会话上下文来源 + - `chat_provider_id`: chat provider ID + - `prompt`: 用户 prompt + - `contexts`: 消息历史上下文(可选,追加到 prompt 后) + - `image_urls`: 图片 URL 列表(追加到 prompt) + - `tools`: ToolSet,AI 可调用的工具集 + - `system_prompt`: 系统提示(插到上下文最前面) + - `max_steps`: 最大 tool call 轮次,默认 30 + - `tool_call_timeout`: 单次工具调用超时(秒),默认 120 + - **`**kwargs`**: 扩展参数: + - `stream: bool` — 是否流式输出 + - `agent_hooks: BaseAgentRunHooks` — Agent 运行期钩子 + - `agent_context: AstrAgentContext` — 复用已有 agent 上下文 + - 其他 kwargs — 直接透传给 `runner.reset()` + +## 传统方法 + +### 当前会话正在使用的 Provider + +- `get_using_provider(umo: str | None = None) -> Provider | None`: 拿 chat provider 实例 +- `get_using_stt_provider(umo: str | None = None) -> STTProvider | None` +- `get_using_tts_provider(umo: str | None = None) -> TTSProvider | None` + +### 按 ID 读取 Provider + +- `get_provider_by_id(provider_id: str)`: 按 ID 获取 provider(可能是 chat/stt/tts/embedding/rerank) + +```python +prov = ctx.get_provider_by_id("your_provider_id") +``` +### 列表查询(用于配置页或校验) + +- `get_all_providers() -> list[Provider]` +- `get_all_stt_providers() -> list[STTProvider]` +- `get_all_tts_providers() -> list[TTSProvider]` +- `get_all_embedding_providers() -> list[EmbeddingProvider]` + +## Agent Runner 相关 + +```python +# 获取当前会话使用的 Agent Runner +runner = ctx.get_using_agent_runner(umo=event.unified_msg_origin) + +# 或者通过 ID 获取 +runner = ctx.get_agent_runner_by_id(runner_id="your_runner_id") +``` +```## + +- 会话内调用必须优先传 `umo`,否则会回退到默认配置,可能拿到错误 provider +- `get_provider_by_id` 返回的不一定是 chat provider,传给 `tool_loop_agent` 前要确保是 chat provider id +- 不要把 provider id 硬编码在代码里,优先从 `_conf_schema.json` 配置读取 +``` diff --git a/docs/snapshots/v4.23.6/agent/agent-related-hooks.md b/docs/snapshots/v4.23.6/agent/agent-related-hooks.md new file mode 100644 index 0000000..6db27f7 --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/agent-related-hooks.md @@ -0,0 +1,88 @@ +--- +category: agent +--- +# Agent Related Hooks + + Agent 请求/工具循环直接相关的 hooks。 + +## Plugin Hooks + +### LLM 请求阶段 + +- `@filter.on_waiting_llm_request()` +- `@filter.on_llm_request()` +- `@filter.on_llm_response()` + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.api.provider import ProviderRequest, LLMResponse + +@filter.on_waiting_llm_request() +async def on_waiting(self, event: AstrMessageEvent) -> None: ... + +@filter.on_llm_request() +async def on_req(self, event: AstrMessageEvent, request: ProviderRequest) -> None: ... + +@filter.on_llm_response() +async def on_resp(self, event: AstrMessageEvent, response: LLMResponse) -> None: ... +``` + +### Tool 调用阶段 + +- `@filter.on_using_llm_tool()` +- `@filter.on_llm_tool_respond()` + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.core.agent.tool import FunctionTool +from mcp.types import CallToolResult + +@filter.on_using_llm_tool() +async def on_tool_start(self, event: AstrMessageEvent, tool: FunctionTool, tool_args: dict | None) -> None: ... + +@filter.on_llm_tool_respond() +async def on_tool_end(self, event: AstrMessageEvent, tool: FunctionTool, tool_args: dict | None, tool_result: CallToolResult | None) -> None: ... +``` + +### 结果发送阶段 + +- `@filter.on_decorating_result()` +- `@filter.after_message_sent()` + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.on_decorating_result() +async def on_decorating(self, event: AstrMessageEvent) -> None: ... + +@filter.after_message_sent() +async def after_sent(self, event: AstrMessageEvent) -> None: ... +``` + +## Agent Runner Hooks + +用于 `context.tool_loop_agent(..., agent_hooks=...)` 的运行期扩展。 + +```python +from astrbot.core.agent.hooks import BaseAgentRunHooks +from astrbot.core.agent.run_context import ContextWrapper +from astrbot.core.agent.tool import FunctionTool +from astrbot.core.provider.entities import LLMResponse +import mcp + +class MyAgentHooks(BaseAgentRunHooks): + async def on_agent_begin(self, run_context: ContextWrapper) -> None: ... + async def on_tool_start(self, run_context: ContextWrapper, tool: FunctionTool, tool_args: dict | None) -> None: ... + async def on_tool_end(self, run_context: ContextWrapper, tool: FunctionTool, tool_args: dict | None, tool_result: mcp.types.CallToolResult | None) -> None: ... + async def on_agent_done(self, run_context: ContextWrapper, llm_response: LLMResponse) -> None: ... +``` + +## 主 Agent 默认映射关系 + +- `on_tool_start` -> `@filter.on_using_llm_tool()` +- `on_tool_end` -> `@filter.on_llm_tool_respond()` +- `on_agent_done` -> `@filter.on_llm_response()` + +## MUST + +- Hook 处理函数必须使用 `async def`。 diff --git a/docs/snapshots/v4.23.6/agent/agent-runner.md b/docs/snapshots/v4.23.6/agent/agent-runner.md new file mode 100644 index 0000000..803b389 --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/agent-runner.md @@ -0,0 +1,17 @@ +Agent Runner 是 AstrBot 中用于执行 Agent 的组件。 + +## 插件侧使用 + +```python +# 获取当前会话使用的 Agent Runner +runner = self.context.get_using_agent_runner(umo=event.unified_msg_origin) + +# 或者通过 provider_id 获取 +runner = self.context.get_agent_runner_by_id(runner_id="your_runner_id") +``` + +## 注意事项 + +- Agent Runner 会调用 Chat Provider 接口 +- 切换 Agent Runner 后,部分 AstrBot 功能(MCP、知识库、网页搜索)可能不可用(取决于 Runner 实现) +- AstrBot 内置 Agent Runner 支持全部功能 diff --git a/docs/snapshots/v4.23.6/agent/context-compression.md b/docs/snapshots/v4.23.6/agent/context-compression.md new file mode 100644 index 0000000..086ff43 --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/context-compression.md @@ -0,0 +1,50 @@ + +# 上下文控制与压缩 + + +## 1 `context.tool_loop_agent(...)` + +用于运行 Agent 工具循环,同时传入上下文压缩参数。 + +```python +await self.context.tool_loop_agent(event=event, chat_provider_id=prov_id, prompt="...", enforce_max_turns=20, truncate_turns=2, llm_compress_keep_recent=6) +``` + +可用压缩参数(都可选): + +- `enforce_max_turns: int`:最多保留多少轮对话(`-1` 不限制)。 +- `truncate_turns: int`:触发截断时一次丢弃多少轮。 +- `llm_compress_instruction: str | None`:LLM 压缩时的摘要指令。 +- `llm_compress_keep_recent: int`:LLM 压缩时保留最近多少条消息不摘要。 +- `llm_compress_provider: Provider | None`:用于压缩摘要的模型 provider。 +- `custom_token_counter: TokenCounter | None`:自定义 token 计数器。 +- `custom_compressor: ContextCompressor | None`:自定义压缩器。 + +## 2`context.get_provider_by_id(provider_id)` + +用于拿到压缩模型实例,再传给 `llm_compress_provider`。 + +```python +compress_prov = self.context.get_provider_by_id("openai/gpt-4o-mini") +``` + +## 3 `context.get_current_chat_provider_id(umo)` + +用于获取当前会话正在使用的对话 provider id,常用于给 `tool_loop_agent` 传 `chat_provider_id`。 + +```python +chat_provider_id = await self.context.get_current_chat_provider_id(event.unified_msg_origin) +``` + +## 4 `context.get_config(umo)` +用于读取当前会话配置,按需决定压缩参数。 +```python +cfg = self.context.get_config(event.unified_msg_origin) +``` +## 示例 +```python +umo = event.unified_msg_origin +chat_prov = await self.context.get_current_chat_provider_id(umo) +compress_prov = self.context.get_provider_by_id("your_compress_provider_id") +resp = await self.context.tool_loop_agent(event=event, chat_provider_id=chat_prov, prompt="总结最近讨论并给出下一步", enforce_max_turns=24, truncate_turns=2, llm_compress_instruction="保留任务结论、待办、关键约束", llm_compress_keep_recent=8, llm_compress_provider=compress_prov) +``` \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/agent/conversation.md b/docs/snapshots/v4.23.6/agent/conversation.md new file mode 100644 index 0000000..2da96ab --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/conversation.md @@ -0,0 +1,52 @@ +--- +category: agent +--- + +# 会话与对话分支(Conversation) + +插件侧通过 `self.context.conversation_manager` 管理会话分支;会话标识使用 `event.unified_msg_origin`(`umo`)。 + +## 插件可用入口 + +```python +conv_mgr = self.context.conversation_manager +umo = event.unified_msg_origin +``` + +## ConversationManager 可用方法 + +- `register_on_session_deleted(callback: Callable[[str], Awaitable[None]]) -> None`:注册会话删除后的级联清理回调。 +- `new_conversation(unified_msg_origin: str, platform_id: str | None = None, content: list[dict] | None = None, title: str | None = None, persona_id: str | None = None) -> str`:新建分支并切换为当前分支。 +- `switch_conversation(unified_msg_origin: str, conversation_id: str) -> None`:切换当前分支。 +- `delete_conversation(unified_msg_origin: str, conversation_id: str | None = None) -> None`:删除指定分支;不传 `conversation_id` 时删除当前分支。 +- `delete_conversations_by_user_id(unified_msg_origin: str) -> None`:删除该会话下全部分支。 +- `get_curr_conversation_id(unified_msg_origin: str) -> str | None`:读取当前分支 ID。 +- `get_conversation(unified_msg_origin: str, conversation_id: str, create_if_not_exists: bool = False) -> Conversation | None`:读取分支对象。 +- `get_conversations(unified_msg_origin: str | None = None, platform_id: str | None = None) -> list[Conversation]`:列出分支。 +- `get_filtered_conversations(page: int = 1, page_size: int = 20, platform_ids: list[str] | None = None, search_query: str = "", **kwargs) -> tuple[list[Conversation], int]`:分页 + 条件过滤。 +- `update_conversation(unified_msg_origin: str, conversation_id: str | None = None, history: list[dict] | None = None, title: str | None = None, persona_id: str | None = None, token_usage: int | None = None) -> None`:更新历史/标题/persona/token_usage。 +- `add_message_pair(cid: str, user_message: UserMessageSegment | dict, assistant_message: AssistantMessageSegment | dict) -> None`:向指定分支追加一组 user/assistant 消息。 +- `get_human_readable_context(unified_msg_origin: str, conversation_id: str, page: int = 1, page_size: int = 10) -> tuple[list[str], int]`:获取分页后的可读上下文。 + +## 最小示例 + +```python +cid = await self.context.conversation_manager.get_curr_conversation_id(event.unified_msg_origin) +``` + +```python +cid = await self.context.conversation_manager.new_conversation(event.unified_msg_origin, title="新分支") +``` + +```python +await self.context.conversation_manager.update_conversation(event.unified_msg_origin, conversation_id=cid, title="重命名", persona_id="assistant_default") +``` + +```python +contexts, total_pages = await self.context.conversation_manager.get_human_readable_context(event.unified_msg_origin, cid, page=1, page_size=10) +``` + +## MUST + +- 所有分支操作必须使用当前会话的 `umo`,不要跨会话复用 `conversation_id`。 +- 更新历史时必须传 OpenAI 风格 `list[dict]` 消息结构。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/agent/cron.md b/docs/snapshots/v4.23.6/agent/cron.md new file mode 100644 index 0000000..e9ead65 --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/cron.md @@ -0,0 +1,57 @@ +--- +category: agent +--- + +# Cron + +定时执行逻辑或唤醒 AI。AI 任务触发生成 `CronMessageEvent`(继承自 AstrMessageEvent)。 + +通过 `self.context.cron_manager` 调用。 + +## 注册 Python 函数(Basic Job) + +```python +await cron_mgr.add_basic_job( + name="任务名", + cron_expression="*/5 * * * *", + handler=self.your_method, + payload={"key": "value"}, + persistent=False, + description="任务描述", + handler_params={"extra": "data"}, + enabled=True, +) +``` + +- `name: str`: 任务唯一标识名 +- `cron_expression: str`: 标准 cron 表达式(5 段,`分 时 日 月 周`) +- `handler: Callable`: Python 异步处理函数 +- `payload: dict`: 传给 handler 的上下文数据 +- `persistent: bool`: 是否持久化(重启后保留,依赖 DB) +- `description: str`: 任务描述(v4.22.2 新增) +- `handler_params: dict`: 额外参数,合并到 payload(v4.22.2 新增) +- `enabled: bool`: 是否启用(v4.22.2 新增) + +## 注册 AI 唤醒(Active Agent Job) + +```python +await cron_mgr.add_active_job( + name="AI 定时任务", + cron_expression="0 8 * * *", + payload={"session": "UMO", "note": "指令"}, + run_once=False, + description="每日早报", +) +``` + +- `name: str`: 任务唯一标识名 +- `cron_expression: str`: 标准 cron 表达式 +- `payload: dict`: 包含 `session`(UMO)、`note`(唤醒指令) +- `run_once: bool`: 是否只执行一次 +- `description: str`: 任务描述(v4.22.2 新增) + +## 维护方法 + +- `delete_job(job_id: str)`: 删除任务 +- `list_jobs(job_type: str = None) -> list[CronJob]`: 列出任务(可选过滤 basic/active) +- `update_job(job_id: str, **kwargs) -> CronJob | None`: 更新任务(只支持部分字段) diff --git a/docs/snapshots/v4.23.6/agent/index.md b/docs/snapshots/v4.23.6/agent/index.md new file mode 100644 index 0000000..1c80977 --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/index.md @@ -0,0 +1,56 @@ +--- +category: agent +--- + +# Agent 系统概览 + +在 AstrBot 中,"Agent"指的是:**指令/系统提示(instructions)+ 工具(tools)+ 模型提供商(providers)+ 运行时能力(上下文管理 / 子智能体 / 沙盒 / 定时任务)** 的组合。 + +本目录把原先以 "LLM" 为中心的内容重组为 "Agent" 视角:LLM/VLM/Embedding 等都被视为 Provider 能力的一部分,工具与运行时能力决定了 Agent 的上限与安全边界。 + +## 你大概率会从这里开始 + +- 需要让模型调用工具:`docs/agent/registe tools.md` +- 需要选模型/Embedding/STT/TTS:`docs/agent/providers.md` +- 需要控制上下文与压缩:`docs/agent/context-compression.md` +- 需要 Hook(事件钩子/Agent 钩子):`docs/agent/agent-related-hooks.md` +- 需要子智能体:`docs/agent/subagents.md` +- 需要代码方式注册子智能体:`docs/agent/agent-registration.md` +- 需要沙盒(computer use):`docs/agent/sandbox.md` +- 需要定时任务(主动能力):`docs/agent/cron.md` +- **v4.7.0+ Agent Runner 架构(Dify/Coze/DeerFlow)**:`docs/agent/agent-runner.md` + +## 最短示例:工具循环 Agent + +```python +llm_resp = await self.context.tool_loop_agent( + event=event, + chat_provider_id=prov_id, + prompt="把这段需求拆成 3 个可执行步骤,并给出每步输出。", + tools=ToolSet([MyTool()]), + max_steps=10, + tool_call_timeout=60, + system_prompt="你是一个严谨的工程助手。", +) +``` + +### 关键参数(只记这几个就够用) + +- `chat_provider_id`:对话模型 provider id(LLM/VLM 的入口通常在这里) +- `tools`:可用工具集合(`FunctionTool` / handoff tool / 运行时注入工具) +- `max_steps`:限制循环次数,避免无限工具调用 +- `tool_call_timeout`:单个工具调用超时 +- `system_prompt`:定义 Agent 角色、边界与输出格式 + +### v4.22.2 扩展参数 + +`tool_loop_agent` 的 `**kwargs` 支持: +- `stream: bool` — 流式输出 +- `agent_hooks: BaseAgentRunHooks` — Agent 运行期钩子 +- `agent_context: AstrAgentContext` — 复用已有 agent 上下文 + +## 相关源码入口(以代码为准) + +- Agent runner(工具循环):`astrbotcore/astrbot/core/agent/runners/tool_loop_agent_runner.py` +- Agent hooks 接口:`astrbotcore/astrbot/core/agent/hooks.py` +- 主 Agent 构建(沙盒/定时工具注入/安全模式等):`astrbotcore/astrbot/core/astr_main_agent.py` diff --git a/docs/snapshots/v4.23.6/agent/offical-tool-list/tools.md b/docs/snapshots/v4.23.6/agent/offical-tool-list/tools.md new file mode 100644 index 0000000..595d9cd --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/offical-tool-list/tools.md @@ -0,0 +1,43 @@ +--- +category: agent +--- +# AstrBot 官方 Tool 列表 + + AstrBot Core 内置工具 + +## Computer Use + +- `astrbot_execute_shell`(`computer_use_runtime=sandbox|local`):执行 Shell 命令。 + - 示例参数:`{"command":"pwd","background":false}` +- `astrbot_execute_ipython`(`computer_use_runtime=sandbox`):在沙盒 IPython 执行代码。 + - 示例参数:`{"code":"print(1+1)","silent":false}` +- `astrbot_execute_python`(`computer_use_runtime=local`):在本地 Python 执行代码(仅管理员)。 + - 示例参数:`{"code":"print(1+1)","silent":false}` +- `astrbot_upload_file`(`computer_use_runtime=sandbox`):上传本地文件到沙盒。 + - 示例参数:`{"local_path":"C:/tmp/a.txt"}` +- `astrbot_download_file`(`computer_use_runtime=sandbox`):从沙盒下载文件。 + - 示例参数:`{"remote_path":"/workspace/out.txt","also_send_to_user":true}` + +## Knowledge Base + +- `astr_kb_search`(`kb_agentic_mode=true`):检索知识库内容。 + - 示例参数:`{"query":"AstrBot provider isolation"}` + +## Cron / Proactive Task + +- `create_future_task`(`add_cron_tools=true`):创建未来任务(周期或一次性)。 + - 示例参数:`{"note":"明早提醒我同步日报","cron_expression":"0 9 * * *"}` +- `delete_future_task`(`add_cron_tools=true`):删除未来任务。 + - 示例参数:`{"job_id":"cron_xxx"}` +- `list_future_tasks`(`add_cron_tools=true`):列出未来任务。 + - 示例参数:`{"job_type":"active_agent"}` + +## Proactive Message + +- `send_message_to_user`(平台支持主动消息时注入):主动向用户发送消息。 + - 示例参数:`{"messages":[{"type":"plain","text":"任务已完成"}]}` + +## Dynamic Handoff Tool + +- `transfer_to_`(`subagent_orchestrator.main_enable=true`):将任务移交给子智能体。 + - 示例参数:`{"input":"请处理这段文本并给出结构化结论"}` diff --git a/docs/snapshots/v4.23.6/agent/persona-resolution.md b/docs/snapshots/v4.23.6/agent/persona-resolution.md new file mode 100644 index 0000000..4a83a36 --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/persona-resolution.md @@ -0,0 +1,53 @@ +--- +category: agent +--- + +# 人格解析与优先级(Persona Resolution) +系统按以下顺序解析 `persona_id`,命中即停止: + +1. 会话级:`session_service_config.persona_id`(`umo` 作用域) +2. 对话分支级:`conversation.persona_id` +3. 全局默认:`provider_settings.default_personality` + +## 插件可用入口 + +```python +umo = event.unified_msg_origin +conv_mgr = self.context.conversation_manager +``` + +## 1设置会话级 persona(最高优先级) + +使用 SDK `sp` 读写 `session_service_config`: + +```python +from astrbot.api import sp + +cfg = await sp.get_async(scope="umo", scope_id=umo, key="session_service_config", default={}) or {} +cfg["persona_id"] = "assistant_default" +await sp.put_async(scope="umo", scope_id=umo, key="session_service_config", value=cfg) +``` + +## 2设置对话分支级 persona + +```python +cid = await conv_mgr.get_curr_conversation_id(umo) +await conv_mgr.update_conversation(umo, conversation_id=cid, persona_id="assistant_default") +``` + +## 3 显式禁用人格注入 + +将 `persona_id` 设置为 `"[%None]"`(会话级或分支级都可): + +```python +await conv_mgr.update_conversation(umo, conversation_id=cid, persona_id="[%None]") +``` + +## 运行时行为要点 + +- 命中 persona 后会注入: + - `persona.prompt` -> `system_prompt` + - `persona._begin_dialogs_processed` -> 上下文前置消息 +- `webchat` 平台下,若未命中 persona 且 `persona_id != "[%None]"`,会追加 ChatUI 默认人格提示词。 +- 读写 `session_service_config` 时必须先读后改再写回,避免覆盖掉同键下其他字段(如 `llm_enabled` / `tts_enabled`)。 +- 会话操作必须使用当前 `umo`,不要跨会话复用 `conversation_id`。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/agent/persona-sets.md b/docs/snapshots/v4.23.6/agent/persona-sets.md new file mode 100644 index 0000000..12de20d --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/persona-sets.md @@ -0,0 +1,72 @@ +--- +category: agent +--- + +# Persona 集管理(插件可用) + +Persona 用于控制系统提示词、开场对话、可用 tools/skills。插件侧入口:`self.context.persona_manager`。 + +## 快速入口 + +```python +pm = self.context.persona_manager +umo = event.unified_msg_origin +``` + +## 核心操作(高频) + +### 读取 + +- `get_persona(persona_id: str)`:读取单个 persona。 +- `get_all_personas() -> list[Persona]`:列出全部 persona。 +- `get_default_persona_v3(umo: str | MessageSession | None = None) -> Personality`:读取默认 persona(按会话配置解析)。 + +### 新建 + +- `create_persona(persona_id, system_prompt, begin_dialogs=None, tools=None, skills=None, folder_id=None, sort_order=0) -> Persona` + +```python +await pm.create_persona( + persona_id="astrbot_plugin_writer", + system_prompt="你是一个技术写作助手。", + begin_dialogs=["你是谁?", "我是你的写作助手。"], + tools=None, + skills=None, +) +``` + +### 更新 + +- `update_persona(persona_id, system_prompt=None, begin_dialogs=None, tools=None, skills=None)` + +```python +# 只改 prompt 时,先读旧值再回填 tools/skills,避免被重置 +old = await pm.get_persona("astrbot_plugin_writer") +await pm.update_persona( + persona_id="astrbot_plugin_writer", + system_prompt="你是一个精炼的技术写作助手。", + tools=old.tools, + skills=old.skills, +) +``` + +### 删除 + +- `delete_persona(persona_id: str) -> None` + +## 文件夹管理(按需) + +- `create_folder / get_folder / get_folders / get_all_folders` +- `update_folder / delete_folder / get_folder_tree` +- `move_persona_to_folder / get_personas_by_folder / batch_update_sort_order` + + + + +## tips + +- `create_persona` 和 `update_persona` 参数不完全一致: + - `create` 有 `folder_id`、`sort_order`;`update` 没有。 +- `tools` / `skills` 语义:`None` = 全部可用,`[]` = 全部禁用 +- `begin_dialogs` 必须是偶数条 +- `_conf_schema.json`中有选择人设的配置 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/agent/registe tools.md b/docs/snapshots/v4.23.6/agent/registe tools.md new file mode 100644 index 0000000..539a0f9 --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/registe tools.md @@ -0,0 +1,111 @@ +--- +category: agent +--- + +# Tools(函数调用) + +Tool 是让大语言模型调用外部能力(检索、计算、执行命令、文件处理)的机制。 + +## 两种定义方式 + +- 类方式:继承 FunctionTool +- 装饰器方式:@filter.llm_tool(...) + +## 方式一:类定义 Tool(推荐,v4.5.7+) + +`python +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" + description: str = "搜索 Bilibili 视频" + parameters: dict = Field( + default_factory=lambda: { + "type": "object", + "properties": { + "keywords": { + "type": "string", + "description": "搜索关键词", + } + }, + "required": ["keywords"], + } + ) + + async def call( + self, + context: ContextWrapper[AstrAgentContext], + **kwargs, + ) -> ToolExecResult: + return ToolExecResult(result="搜索结果...") +` + +**ToolExecResult 返回值格式(v4.22.2):** + +`python +from astrbot.core.agent.tool import ToolExecResult + +return ToolExecResult(result="文本结果") +return ToolExecResult(is_error=True, result="错误信息") +return ToolExecResult(result="", image_url="https://...") # 图片结果 +` + +## 注册到全局工具池 + +`python +class MyPlugin(Star): + def __init__(self, context: Context): + super().__init__(context) + self.context.add_llm_tools(BilibiliTool()) +` + +注册后主对话模型自动感知并调用该 Tool。 + +## 方式二:装饰器(兼容旧版) + +`python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.llm_tool(name="get_weather") +async def get_weather(self, event: AstrMessageEvent, location: str): + """获取天气信息。 + + Args: + location(string): 地点 + """ + resp = self.get_weather_from_api(location) + yield event.plain_result("天气信息: " + resp) +` + +Docstring 中 Args 格式必须是 参数名(类型): 描述。 + +支持的类型:string、 +umber、object、oolean、rray、rray[string](v4.5.7+)。 + +## 内部 Tool(不注册全局) + +仅在单次 ool_loop_agent 调用中可见,不进入全局工具池: + +`python +from astrbot.core.agent.tool import ToolSet + +llm_resp = await self.context.tool_loop_agent( + event=event, + chat_provider_id=await self.context.get_current_chat_provider_id(event.unified_msg_origin), + prompt="请调用 bilibili_videos 工具搜索 AstrBot 教程", + tools=ToolSet([BilibiliTool()]), +) +` + +## Tips + +- parameters 必须是合法 JSON Schema +- 装饰器方式必须写规范 docstring(尤其 Args),否则 schema 解析失败 +- 推荐新项目使用类定义方式,参数类型检查更严格 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/agent/sandbox.md b/docs/snapshots/v4.23.6/agent/sandbox.md new file mode 100644 index 0000000..ba1e833 --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/sandbox.md @@ -0,0 +1,48 @@ +--- +category: agent +--- + +# Sandbox(插件可用) + +Sandbox 是 Agent 的计算机使用运行时(shell/python/文件上传下载)。 + +## 快速入口 + +```python +ctx = self.context +umo = event.unified_msg_origin +``` +## 底层方法(给 booter/工具实现使用) + +- `get_booter(context, session_id)` +- `get_local_booter()` +- `booter.shell.exec(command, cwd=None, env=None, timeout=30, shell=True, background=False)` +- `booter.python.exec(code, kernel_id=None, timeout=30, silent=False)` +- `booter.upload_file(path, file_name)` +- `booter.download_file(remote_path, local_path)` +- `booter.available()` + +## UMO 与当前 Sandbox 的绑定规则 + +- 当前会话标识使用 `event.unified_msg_origin`。 +- 工具执行时用 `event.unified_msg_origin` 调用 `get_booter(...)` 获取当前会话 booter。 +- `get_booter` 内部按 `session_id` 缓存:`session_booter[session_id]`。 +- 若缓存实例 `available()` 为 false,会先移除再重建。 +- `get_booter` 会读取 `context.get_config(umo=session_id)`,因此会话级配置可生效。 + +## 配置键(常用) + +- `provider_settings.computer_use_runtime`: `none | local | sandbox` +- `provider_settings.sandbox.booter`: `shipyard | boxlite` +- `provider_settings.sandbox.shipyard_endpoint` +- `provider_settings.sandbox.shipyard_access_token` +- `provider_settings.sandbox.shipyard_ttl` +- `provider_settings.sandbox.shipyard_max_sessions` + +## 易翻车点 + +- `shipyard_endpoint` 或 `shipyard_access_token` 缺失时,sandbox 工具不会注入。 +- `astrbot_execute_shell` 要求 `admin` 角色,否则返回 permission denied。 +- `astrbot_download_file(..., also_send_to_user=True)` 会发送后删除本地临时文件。 +- `local` 与 `sandbox` 是两套运行时:`local` 走 `get_local_booter()`,`sandbox` 走 `get_booter(..., umo)`。 + diff --git a/docs/snapshots/v4.23.6/agent/subagents.md b/docs/snapshots/v4.23.6/agent/subagents.md new file mode 100644 index 0000000..2ad5ee3 --- /dev/null +++ b/docs/snapshots/v4.23.6/agent/subagents.md @@ -0,0 +1,70 @@ +--- +category: agent +--- + +# Subagents(子智能体 / Handoff) + +Subagent 是给主 Agent 使用的 handoff 工具。主模型通过 `transfer_to_` 把任务转交给子智能体执行。 +`from astrbot.api import agent`(详见 `docs/agent/agent-registration.md`) + +## 配置式(推荐) + +### 最小配置 + +```json +{ + "subagent_orchestrator": { + "main_enable": true, + "remove_main_duplicate_tools": false, + "router_system_prompt": "You are a task router...", + "agents": [ + { + "enabled": true, + "name": "writer", + "public_description": "负责技术文档整理与重写", + "persona_id": null, + "system_prompt": "你是文档子智能体,输出精简且结构化。", + "provider_id": "openai_gpt4o_mini", + "tools": ["search_docs", "rewrite_text"] + } + ] + } +} +``` + +### `agents[]` 字段(源码对齐) + +- `enabled`: 是否启用 +- `name`: 子智能体名;工具名会生成为 `transfer_to_` +- `public_description`: 暴露给主模型的工具描述(决定主模型是否愿意调用) +- `persona_id`: 可选;存在时优先使用 persona 的 `system_prompt/begin_dialogs/tools` +- `system_prompt`: 未命中 persona 时使用 +- `provider_id`: 可选;子智能体专用 chat provider 覆盖 +- `tools`: 子智能体可用工具名列表(字符串) + +## 运行规则 + +- `main_enable=true` 时,主 Agent 会把所有 handoff 工具加入工具集。 +- `remove_main_duplicate_tools=true` 时,会把“已分配给子智能体”的同名工具从主 Agent 工具集移除。 +- `router_system_prompt` 会拼接到主 Agent 的 `system_prompt`。 +- `provider_id` 不为空时,handoff 执行优先用该 provider;否则回退当前会话 provider。 + +## SDK/代码式(高级) + +```python +from astrbot.api import agent + +@agent(name="writer", instruction="你是写作子智能体。") +async def writer_agent(event): + return None +``` + +> 代码式注册、`run_hooks`、专属工具挂载见:`docs/agent/agent-registration.md` + +## MUST + +- `name` 必须非空,且在同一实例中保持唯一。 +- `public_description` 必须写“适用任务”,不要写空泛人设。 +- `tools` 必须显式写成字符串列表(不要依赖隐式行为)。 + + diff --git a/docs/snapshots/v4.23.6/design_standards/architecture_overview.md b/docs/snapshots/v4.23.6/design_standards/architecture_overview.md new file mode 100644 index 0000000..4c6eec1 --- /dev/null +++ b/docs/snapshots/v4.23.6/design_standards/architecture_overview.md @@ -0,0 +1,21 @@ +--- +category: design_standards +--- + +# 核心架构综述 + +AstrBot 采用基于**插件化 (Plugin-based)** 和 **事件驱动 (Event-driven)** 的架构。其核心(Core)负责协调各个管理器(Manager),并通过 `Context` 对象向插件(Star)暴露能力。 + +### 核心管理器分工 + +- **`PluginManager`**: 负责插件的加载、卸载、重载以及元数据管理。 +- **`PlatformManager`**: 管理所有已接入的消息平台适配器,负责分发事件。 +- **`ProviderManager`**: 管理大语言模型(LLM)、语音识别(STT)、语音合成(TTS)等服务提供商。 +- **`ConversationManager`**: 管理用户会话历史、上下文存储及切换。 +- **`PersonaManager`**: 管理人格设定(Persona),包括系统提示词(System Prompt)和工具配置。 + +### 核心设计原则 + +1. **解耦**: 核心系统与平台适配器、AI 提供商、插件之间高度解耦。 +2. **统一模型**: 所有的平台消息都被转化为统一的 `AstrBotMessage` 模型。 +3. **插件化**: 功能尽可能通过插件实现,核心仅提供基础调度能力。 diff --git a/docs/snapshots/v4.23.6/design_standards/best_practices.md b/docs/snapshots/v4.23.6/design_standards/best_practices.md new file mode 100644 index 0000000..845cc5e --- /dev/null +++ b/docs/snapshots/v4.23.6/design_standards/best_practices.md @@ -0,0 +1,43 @@ +--- +category: design_standards +--- + +# AI 插件开发最佳实践 + +为了确保插件的稳定性、安全性和易用性,建议遵循以下实践方案。 + +### 1. 异常处理 + +务必捕获可能的异常,并给用户明确的反馈。 + +```python +try: + # 逻辑代码 +except TimeoutError: + yield event.plain_result("⌛ 会话已超时,请重新开始。") +except Exception as e: + logger.error(f"插件执行出错: {e}") + yield event.plain_result(f"❌ 发生错误: {e}") +finally: + event.stop_event() # 已经处理过错误,通常建议停止事件继续传播 +``` + +### 2. 平台差异化 + +虽然 AstrBot 提供了统一模型,但在调用底层 SDK 功能(如 `call_action`)时,需进行环境检查: + +```python +if event.get_platform_name() == "aiocqhttp": + # 调用 OneBot 特有 API + pass +``` + +### 3. 工具 (Tools) 开发 + +- 推荐使用 `agent-as-tool` 模式。 +- 完善 Docstring,这直接决定了大模型对工具的理解能力。 +- 尽量保持工具功能的单一性。 + +### 4. 资源清理 + +在插件卸载时,应在 `terminate()` 方法中清理定时器、数据库连接或文件句柄。 diff --git a/docs/snapshots/v4.23.6/design_standards/context_usage.md b/docs/snapshots/v4.23.6/design_standards/context_usage.md new file mode 100644 index 0000000..22dc222 --- /dev/null +++ b/docs/snapshots/v4.23.6/design_standards/context_usage.md @@ -0,0 +1,30 @@ +--- +category: design_standards +--- + +# Context 对象使用规范 + +`Context` 对象是 AstrBot 的能力中枢,在插件初始化时通过 `__init__` 注入。它是插件与系统核心交互的唯一桥梁。 + +### 重要属性 + +通过 `self.context` 可以访问各个管理器: + +- `self.context.conversation_manager`: 会话管理器。 +- `self.context.persona_manager`: 人格管理器。 +- `self.context.platform_manager`: 平台管理器。 +- `self.context.provider_manager`: 提供商管理器。 + +### 核心方法 + +#### 消息与平台相关 +- `send_message(umo: str, message_chain: MessageChain)`: 向指定源主动发送消息。 +- `get_platform(platform_type: PlatformAdapterType)`: 获取指定类型的平台实例。 + +#### AI 与工具相关 +- `add_llm_tools(*tools)`: 动态注册函数工具。 +- `get_using_provider(umo)`: 获取当前使用的 LLM 提供商。 + +#### 配置与插件 +- `get_config(umo=None)`: 获取当前配置。 +- `get_all_stars()`: 获取所有已加载插件的元数据。 diff --git a/docs/snapshots/v4.23.6/design_standards/core_concepts.md b/docs/snapshots/v4.23.6/design_standards/core_concepts.md new file mode 100644 index 0000000..58549b4 --- /dev/null +++ b/docs/snapshots/v4.23.6/design_standards/core_concepts.md @@ -0,0 +1,128 @@ +# AstrBot 核心概念 API 清单 + +本文档仅列出 AstrBot 插件开发的核心功能 API。 + +### 1. 装饰器 (Decorators) + +- `@register(id, author, description, version, repo_url)`: 注册插件。 +- `@filter.command(name, alias, priority)`: 注册指令。支持带参函数。 +- `@filter.command_group(name)`: 注册指令组。 +- `@filter.event_message_type(type)`: 过滤消息类型 (`ALL`, `PRIVATE_MESSAGE`, `GROUP_MESSAGE`)。 +- `@filter.platform_adapter_type(type)`: 过滤平台类型 (如 `AIOCQHTTP`, `TELEGRAM`)。 +- `@filter.permission_type(type)`: 校验权限 (如 `ADMIN`)。 +- `@filter.regex(pattern)`: 正则匹配。 +- `@filter.llm_tool(name)`: 注册为 AI 可调用的工具。 +- `@session_waiter(timeout, record_history_chains)`: 等待下一条用户消息。 + +### 2. 消息组件 (Message Components) + +- `Plain(text)`: 纯文本。 +- `At(user_id)`: 提及用户。 +- `Image.fromFileSystem(path)` / `Image.fromURL(url)`: 图片。 +- `Record.fromFileSystem(path)`: 语音。 +- `Video.fromFileSystem(path)` / `Video.fromURL(url)`: 视频。 +- `File.fromFileSystem(path, name)`: 文件。 +- `Face(id)`: 系统表情。 +- `Reply(message_id)`: 回复特定消息。 +- `Node(uin, name, content)` / `Nodes(nodes)`: 合并转发节点 (部分平台支持)。 + +### 3. 核心对象与方法 + +**AstrMessageEvent (事件对象)** + +- 消息事件对象,包含消息内容、发送者信息、群组信息等。 +- 提供消息发送和结果构建方法。 + +**Context (核心枢纽)** + +- `context.send_message(umo, chain)`: 向指定源主动发送消息。 +- `context.get_platform(type)`: 获取指定类型的平台实例。 +- `context.get_using_provider(umo)`: 获取当前 LLM 提供商。 +- `context.add_llm_tools(*tools)`: 动态注册 AI 工具。 + +**v4.5.7+ 新增 LLM API** + +```python +umo = event.unified_msg_origin +prov_id = await self.context.get_current_chat_provider_id(umo) +``` + +- `await context.get_current_chat_provider_id(umo) -> str`: 获取当前会话使用的 chat provider ID。 +- `await context.llm_generate(chat_provider_id, prompt, contexts=None, system_prompt=None, tools=None) -> LLMResponse`: 简化的 LLM 调用。 +- `await context.tool_loop_agent(event, chat_provider_id, prompt, tools, system_prompt=None, max_steps=30, tool_call_timeout=60) -> LLMResponse`: 工具循环 Agent。 + +**MessageChain (消息链构建器)** + +- `MessageChain().message(text)`: 添加文本。 +- `MessageChain().file_image(path)`: 添加图片文件。 +- `MessageChain().at(user_id)`: 添加 At。 + +### 4. 存储与工具 (Storage & Utils) + +- `await self.get_kv_data(key, default)`: 获取插件隔离的 KV 数据。 +- `await self.put_kv_data(key, value)`: 存储插件隔离的 KV 数据。 +- `await self.delete_kv_data(key)`: 删除 KV 数据。 +- `await self.html_render(html_text=None, url=None, data=None, options=None)`: 将 HTML 字符串或网页渲染为图片。基于 Playwright。 +- `text_to_image(text)`: 将文字转为图片。 + +### 5. 系统钩子 (Hooks) + +Hooks 分为两层,不建议在"概念清单"里重复列举具体 hook 名单(容易过时): + +- 插件事件钩子(`@filter.on_*`):见 `docs/plugin_config/hooks.md` +- Agent 运行钩子(`BaseAgentRunHooks`):见 `docs/agent/agent-related-hooks.md` + +### 6. Agent 智能体 + +- Agent 相关能力(tools / providers / persona / sandbox / cron / subagents):见 `docs/agent/` +- `context.tool_loop_agent(...)`: 调用工具循环 Agent(可结合子智能体 handoff) +- v4.7.0+ Agent Runner 架构:见 `docs/agent/agent-runner.md` + +### 7. Tool 定义 (v4.5.7+ 推荐) + +推荐使用 dataclass 模式定义 Tool: + +```python +from pydantic import Field +from pydantic.dataclasses import dataclass +from astrbot.core.agent.tool import FunctionTool +from astrbot.core.agent.run_context import ContextWrapper +from astrbot.core.astr_agent_context import AstrAgentContext + +@dataclass +class MyTool(FunctionTool[AstrAgentContext]): + name: str = "my_tool" + description: str = "工具描述" + parameters: dict = Field(default_factory=lambda: { + "type": "object", + "properties": {"query": {"type": "string", "description": "参数描述"}}, + "required": ["query"], + }) + + async def call(self, context: ContextWrapper[AstrAgentContext], **kwargs) -> str: + return "结果" +``` + +### 8. 多智能体 (Multi-Agent) v4.5.7+ + +使用 agent-as-tool 模式实现多智能体: + +```python +@dataclass +class SubAgentTool(FunctionTool[AstrAgentContext]): + name: str = "sub_agent" + description: str = "子智能体描述" + parameters: dict = Field(default_factory=lambda: {...}) + + async def call(self, context: ContextWrapper[AstrAgentContext], **kwargs) -> str: + ctx = context.context.context + event = context.context.event + 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([SomeTool()]), + max_steps=30, + ) + return llm_resp.completion_text +``` diff --git a/docs/snapshots/v4.23.6/design_standards/event_flow.md b/docs/snapshots/v4.23.6/design_standards/event_flow.md new file mode 100644 index 0000000..16389ba --- /dev/null +++ b/docs/snapshots/v4.23.6/design_standards/event_flow.md @@ -0,0 +1,20 @@ +--- +category: design_standards +--- + +# 消息流转模型 + +AstrBot 的消息处理遵循一个清晰的流转过程。 + +### 核心流程图 + +1. **接收**: 平台适配器(Platform)接收原始消息。 +2. **转换**: 调用 `convert_message` 将其封装为 `AstrBotMessage`。 +3. **提交**: 封装为 `AstrMessageEvent` 后通过 `self.commit_event(event)` 提交到事件队列。 +4. **分发**: `PlatformManager` 按优先级将事件分发给所有插件的 Handler。 +5. **处理**: 插件执行业务逻辑。 + - 若调用 `event.stop_event()`,流程在此终止。 +6. **LLM 交互**: 若消息未被拦截,且符合 AI 触发条件,调用配置的 LLM。 +7. **结果装饰**: 发送前调用 `on_decorating_result` 钩子。 +8. **回复**: 调用 `event.send()` 或 `yield`,触发适配器的 `send` 方法。 +9. **发送**: 适配器调用平台 SDK 发送消息。 diff --git a/docs/snapshots/v4.23.6/design_standards/sandbox.md b/docs/snapshots/v4.23.6/design_standards/sandbox.md new file mode 100644 index 0000000..faf01d3 --- /dev/null +++ b/docs/snapshots/v4.23.6/design_standards/sandbox.md @@ -0,0 +1,20 @@ +--- +title: Sandbox 存储挂载与文件共享 (Sandbox Storage Mounting & File Sharing) +type: improvement +status: stable +last_updated: 2025-02-10 +related_base: agent/sandbox.md +--- + +## 概述 +在基于 Shipyard 的 Sandbox 运行时中,系统通过 Docker Volume 建立了宿主机与沙盒环境之间的共享临时目录。这一变更明确了文件在宿主机与沙盒之间流转的物理路径契约,是 `astrbot_upload_file` 等文件操作工具正常运行的基础设施保障。 + +## 存储映射契约 +为了实现宿主机与沙盒环境的高效文件交换,系统建立了以下挂载关系: +- **宿主机源路径**: `${PWD}/data/temp` (即 AstrBot 运行根目录下的临时文件夹) +- **沙盒目标路径**: `/AstrBot/data/temp` (沙盒环境内的绝对路径) + +## 变更影响分析 +1. **文件访问一致性**:AI 开发者在编写涉及沙盒文件操作的工具(Tools)时,应知晓 `/AstrBot/data/temp` 是预设的共享交换区。上传到宿主机临时目录的文件将直接映射至此路径,无需通过网络流重复传输。 +2. **底层实现透明化**:此变更解释了 `ShipyardBooter` 如何在物理层面处理文件可见性。如果开发者在非标准 Docker 环境下部署,需手动配置类似的卷挂载以维持 `astrbot_upload_file` 和 `astrbot_download_file` 的功能兼容性。 +3. **边界情况**:宿主机对 `data/temp` 的清理操作会同步反映在沙盒内。在执行长时间运行的 Agent 任务时,需注意临时文件的生命周期管理,避免因宿主机清理导致沙盒内路径失效。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/design_standards/visual_utils.md b/docs/snapshots/v4.23.6/design_standards/visual_utils.md new file mode 100644 index 0000000..1a18162 --- /dev/null +++ b/docs/snapshots/v4.23.6/design_standards/visual_utils.md @@ -0,0 +1,89 @@ +--- +category: design_standards +--- + +# 视觉与渲染工具 (Visual Utils) + +AstrBot 提供了一些工具函数,帮助插件实现更丰富的视觉表现,如 HTML 渲染图片。 + +### 1. HTML 渲染 (html_render) + +AstrBot 内置了基于 **Playwright** 的 HTML 渲染引擎,支持将 HTML 字符串(支持 Jinja2 模板)或远程网页渲染为图片。 + +#### `await self.html_render(html_text: str = None, url: str = None, data: dict = None, options: dict = None) -> str` + +- **参数**: + - `html_text`: Jinja2 格式的 HTML 模板字符串。 + - `url`: 目标网页的 URL。如果提供了 `url`,将优先使用 `url` 而忽略 `html_text`。 + - `data`: 传入模板的变量字典(仅在提供 `html_text` 时有效)。 + - `options`: 渲染选项(映射自 Playwright API)。 + - `viewport`: 视口大小,例如 `{"width": 800, "height": 600}`。 + - `selector`: 等待并截图指定的 CSS 选择器对应的元素。 + - `wait_until`: 等待页面加载的状态。可选值:`"commit"`, `"domcontentloaded"`, `"load"`, `"networkidle"` (默认)。 + - `timeout`: 截图超时时间(毫秒)。 + - `type`: 图片格式,`"jpeg"` 或 `"png"`。 + - `quality`: 仅 JPEG 有效 (0-100)。 + - `omit_background`: 是否透明背景 (仅 PNG)。 + - `full_page`: 是否截取整页 (默认为 True,如果指定了 `selector` 则失效)。 + - `clip`: 裁切区域 `{"x": 0, "y": 0, "width": 100, "height": 100}`。 + - `animations`: `"allow"` 或 `"disabled"`。 + - `scale`: `"css"` 或 `"device"`。 +- **返回值**: 渲染后的图片本地路径。 + +#### 使用示例 + +##### 渲染 HTML 模板 + +```python +TMPL = """ +
+

Hello {{ name }}!

+

This is rendered via AstrBot HTML Render.

+
+""" + +@filter.command("hello_render") +async def hello_render(self, event: AstrMessageEvent): + # 渲染 HTML 字符串并传入数据 + image_path = await self.html_render(html_text=TMPL, data={"name": event.get_sender_id()}) + + # 将结果作为图片发送 + yield event.image_result(image_path) +``` + +##### 渲染远程网页 + +```python +@filter.command("screenshot") +async def screenshot(self, event: AstrMessageEvent, site_url: str): + # 渲染指定 URL,并设置视口大小 + image_path = await self.html_render( + url=site_url, + options={"viewport": {"width": 1280, "height": 720}, "wait_until": "networkidle"} + ) + yield event.image_result(image_path) +``` + +#### 转换为 Image 组件 + +`html_render` 返回的是图片的本地路径。你可以使用 `event.image_result(path)` 快速发送,也可以手动构建 `Image` 组件: + +```python +from astrbot.api.message_components import Image + +image_path = await self.html_render(url="...") +image_comp = Image.fromFileSystem(image_path) + +# 放入消息链发送 +# yield event.chain_result([Plain("这是截图:"), image_comp]) +``` + +### 2. 文字转图片 (text_to_image) + +#### `text_to_image(text: str, return_url: bool = True) -> str` + +- **说明**: 简单的文字转图片工具。 +- **参数**: + - `text`: 要转换的文字内容。 + - `return_url`: 是否返回 URL 格式。 +- **返回值**: 图片的路径或 URL。 diff --git a/docs/snapshots/v4.23.6/en/community.md b/docs/snapshots/v4.23.6/en/community.md new file mode 100644 index 0000000..7aeb334 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/community.md @@ -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 + + + +### GitHub + +Welcome to submit Issues or Pull Requests: + +- [AstrBotDevs/AstrBot](https://github.com/AstrBotDevs/AstrBot) + +### Tencent QQ Groups + +- Group 12: 916228568 (New) +- Group 9: 1076659624 (Full) +- Group 10: 1078079676 (Full) +- Group 11: 704659519 (Full) +- Group 1: 322154837 (Full) +- Group 3: 630166526 (Full) +- Group 4: 1077826412 (Full) +- Group 5: 822130018 (Full) +- Group 6: 753075035 (Full) +- Group 7: 743746109 (Full) +- Group 8: 1030353265 (Full) +- **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! diff --git a/docs/snapshots/v4.23.6/en/config/model-config.md b/docs/snapshots/v4.23.6/en/config/model-config.md new file mode 100644 index 0000000..5bc3d57 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/config/model-config.md @@ -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` 中添加新的参数即可。 + +具体的参数请参看对应的提供商的文档。 diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/1panel.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/1panel.md new file mode 100644 index 0000000..9c0b6fa --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/1panel.md @@ -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. diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/btpanel.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/btpanel.md new file mode 100644 index 0000000..750fd61 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/btpanel.md @@ -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. + diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/casaos.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/casaos.md new file mode 100644 index 0000000..dd02fc4 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/casaos.md @@ -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. \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/cli.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/cli.md new file mode 100644 index 0000000..857e0d6 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/cli.md @@ -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. diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/community-deployment.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/community-deployment.md new file mode 100644 index 0000000..17602ce --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/community-deployment.md @@ -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) \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/compshare.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/compshare.md new file mode 100644 index 0000000..c985830 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/compshare.md @@ -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://: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 ` 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). diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/docker.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/docker.md new file mode 100644 index 0000000..7ce973a --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/docker.md @@ -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. diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/kubernetes.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/kubernetes.md new file mode 100644 index 0000000..39f51f1 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/kubernetes.md @@ -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 `:/TCP`. For example, if you see `8080:30185/TCP`, the access address is `http://: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 `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 `:/TCP`. For example, if you see `8080:30185/TCP`, the access address is `http://: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`. diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/launcher.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/launcher.md new file mode 100644 index 0000000..87ef896 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/launcher.md @@ -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 + +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. diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/other-deployments.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/other-deployments.md new file mode 100644 index 0000000..c42d2c8 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/other-deployments.md @@ -0,0 +1,5 @@ +# Other Deployments + +- [CasaOS Deployment](./casaos.md) +- [Compshare GPU Deployment](./compshare.md) +- [Community Deployments](./community-deployment.md) diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/package.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/package.md new file mode 100644 index 0000000..c921f20 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/package.md @@ -0,0 +1,24 @@ +# 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: + + +`uv` supports Linux, Windows, and macOS. + +## Important Notes + +> [!WARNING] +> AstrBot deployed via `uv` **does not support upgrading through the WebUI**. To update, run `uv tool upgrade astrbot --python 3.12` from the command line. + +AstrBot requires Python 3.12 or later. Use `--python 3.12` to ensure that `uv` creates the tool environment with Python 3.12; if Python downloads are enabled, `uv` will download Python 3.12 automatically when it is missing. + +## Install and Start + +```bash +uv tool install astrbot --python 3.12 +astrbot +``` diff --git a/docs/snapshots/v4.23.6/en/deploy/astrbot/sys-pm.md b/docs/snapshots/v4.23.6/en/deploy/astrbot/sys-pm.md new file mode 100644 index 0000000..b89080c --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/astrbot/sys-pm.md @@ -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 +``` diff --git a/docs/snapshots/v4.23.6/en/deploy/when-deployed.md b/docs/snapshots/v4.23.6/en/deploy/when-deployed.md new file mode 100644 index 0000000..d302450 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/deploy/when-deployed.md @@ -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. + diff --git a/docs/snapshots/v4.23.6/en/dev/astrbot-config.md b/docs/snapshots/v4.23.6/en/dev/astrbot-config.md new file mode 100644 index 0000000..e27111e --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/astrbot-config.md @@ -0,0 +1,576 @@ +--- +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": "tavily", + "websearch_tavily_key": [], + "websearch_bocha_key": [], + "websearch_brave_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": 120, + }, + "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": ["✍️"]}, + }, + "discord": { + "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'', 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'', '', 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 `tavily`. Currently supports `tavily`, `bocha`, `baidu_ai_search`, and `brave`. + +- `tavily`: Uses the Tavily search engine. +- `bocha`: Uses the BoCha search engine. +- `baidu_ai_search`: Uses Baidu AI Search (MCP). +- `brave`: Uses Brave Search API. + +#### `provider_settings.websearch_tavily_key` + +API Key list for the Tavily search engine. Required when using `tavily` as the web search provider. + +#### `provider_settings.websearch_bocha_key` + +API Key list for the BoCha search engine. Required when using `bocha` as the web search provider. + +#### `provider_settings.websearch_brave_key` + +API Key list for the Brave search engine. Required when using `brave` 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..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). + +##### discord + +- `enable`: Whether to enable pre-reply emojis for Discord messages. Default is `false`. +- `emojis`: List of pre-reply emojis. Default is `["🤔"]`. Refer to [Discord Reaction FAQ](https://support.discord.com/hc/en-us/articles/12102061808663-Reactions-and-Super-Reactions-FAQ). + +### `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 `["*"]`. diff --git a/docs/snapshots/v4.23.6/en/dev/openapi.md b/docs/snapshots/v4.23.6/en/dev/openapi.md new file mode 100644 index 0000000..c6e9dd3 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/openapi.md @@ -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 diff --git a/docs/snapshots/v4.23.6/en/dev/plugin-platform-adapter.md b/docs/snapshots/v4.23.6/en/dev/plugin-platform-adapter.md new file mode 100644 index 0000000..8e65528 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/plugin-platform-adapter.md @@ -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) + + +有任何疑问欢迎加群询问~ \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/en/dev/plugin.md b/docs/snapshots/v4.23.6/en/dev/plugin.md new file mode 100644 index 0000000..8e2eaf2 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/plugin.md @@ -0,0 +1 @@ +This page has moved to [AstrBot Plugin Development Guide](/en/dev/star/plugin-new). diff --git a/docs/snapshots/v4.23.6/en/dev/star/guides/ai.md b/docs/snapshots/v4.23.6/en/dev/star/guides/ai.md new file mode 100644 index 0000000..361ac55 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/guides/ai.md @@ -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=120, # 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""" +``` + +::: diff --git a/docs/snapshots/v4.23.6/en/dev/star/guides/env.md b/docs/snapshots/v4.23.6/en/dev/star/guides/env.md new file mode 100644 index 0000000..7dd0480 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/guides/env.md @@ -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/)。 diff --git a/docs/snapshots/v4.23.6/en/dev/star/guides/html-to-pic.md b/docs/snapshots/v4.23.6/en/dev/star/guides/html-to-pic.md new file mode 100644 index 0000000..b04e5c1 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/guides/html-to-pic.md @@ -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 = ''' +
+

Todo List

+ +
    +{% for item in items %} +
  • {{ item }}
  • +{% endfor %} +
+''' + +@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. diff --git a/docs/snapshots/v4.23.6/en/dev/star/guides/listen-message-event.md b/docs/snapshots/v4.23.6/en/dev/star/guides/listen-message-event.md new file mode 100644 index 0000000..a789bb1 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/guides/listen-message-event.md @@ -0,0 +1,436 @@ + +# 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. + +#### On Agent Begin + +> Requires AstrBot version > v4.23.1 + +When the Agent starts running, the `on_agent_begin` hook is triggered. + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.core.agent.run_context import ContextWrapper +from astrbot.core.astr_agent_context import AstrAgentContext + +@filter.on_agent_begin() +async def on_agent_begin(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext]): # Note there are three parameters + print("Agent started") +``` + +> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly. + +#### Before LLM Tool Call + +> Requires AstrBot version > v4.23.1 + +When the Agent is about to call an LLM tool, the `on_using_llm_tool` hook is triggered. + +You can obtain the `FunctionTool` object and tool call arguments. + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.core.agent.tool import FunctionTool + +@filter.on_using_llm_tool() +async def on_using_llm_tool( + self, + event: AstrMessageEvent, + tool: FunctionTool, + tool_args: dict | None, +): + print(tool.name, tool_args) +``` + +> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly. + +#### After LLM Tool Call + +> Requires AstrBot version > v4.23.1 + +After the LLM tool call completes, the `on_llm_tool_respond` hook is triggered. + +You can obtain the `FunctionTool` object, tool call arguments, and tool call result. + +```python +from mcp.types import CallToolResult + +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.core.agent.tool import FunctionTool + +@filter.on_llm_tool_respond() +async def on_llm_tool_respond( + self, + event: AstrMessageEvent, + tool: FunctionTool, + tool_args: dict | None, + tool_result: CallToolResult | None, +): + print(tool.name, tool_args, tool_result) +``` + +> You cannot use yield to send messages here. If you need to send, please use the `event.send()` method directly. + +#### On Agent Done + +> Requires AstrBot version > v4.23.1 + +After the Agent finishes running, the `on_agent_done` hook is triggered. This hook is triggered after `on_llm_response`. + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.api.provider import LLMResponse +from astrbot.core.agent.run_context import ContextWrapper +from astrbot.core.astr_agent_context import AstrAgentContext + +@filter.on_agent_done() +async def on_agent_done(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext], resp: LLMResponse): # Note there are four 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. diff --git a/docs/snapshots/v4.23.6/en/dev/star/guides/plugin-config.md b/docs/snapshots/v4.23.6/en/dev/star/guides/plugin-config.md new file mode 100644 index 0000000..3bb6811 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/guides/plugin-config.md @@ -0,0 +1,219 @@ + +# 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. Common values include `select_provider`, `select_provider_tts`, `select_provider_stt`, `select_persona`, and `select_knowledgebase`, allowing users to quickly select model providers, personas, knowledge bases, and other data already configured in the WebUI. + +- `select_provider`, `select_provider_tts`, `select_provider_stt`, and `select_persona` return strings. +- `select_knowledgebase` returns a `list` and supports multiple selection, so the corresponding config item should use `type: list` with a default value of `[]`. + +> [!NOTE] +> For reference, AstrBot Core also uses other internal `_special` values, such as `select_providers`, `provider_pool`, `persona_pool`, `select_plugin_set`, `t2i_template`, `get_embedding_dim`, and `select_agent_runner_provider:*` (where `*` is a placeholder for the runner type). These are internal implementations and may change at any time — please avoid using them in plugins. + +Using `select_provider` as an example, it will display as follows: + +![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 + } +] +``` + +image + + +## 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/_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. \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/en/dev/star/guides/send-message.md b/docs/snapshots/v4.23.6/en/dev/star/guides/send-message.md new file mode 100644 index 0000000..417b60e --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/guides/send-message.md @@ -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) diff --git a/docs/snapshots/v4.23.6/en/dev/star/guides/session-control.md b/docs/snapshots/v4.23.6/en/dev/star/guides/session-control.md new file mode 100644 index 0000000..e08bae7 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/guides/session-control.md @@ -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! diff --git a/docs/snapshots/v4.23.6/en/dev/star/guides/simple.md b/docs/snapshots/v4.23.6/en/dev/star/guides/simple.md new file mode 100644 index 0000000..f2dc11b --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/guides/simple.md @@ -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`。 + +所有的处理函数都需写在插件类中。为了精简内容,在之后的章节中,我们可能会忽略插件类的定义。 diff --git a/docs/snapshots/v4.23.6/en/dev/star/guides/storage.md b/docs/snapshots/v4.23.6/en/dev/star/guides/storage.md new file mode 100644 index 0000000..286d238 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/guides/storage.md @@ -0,0 +1,32 @@ +# 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 pathlib import Path +from astrbot.core.utils.astrbot_path import get_astrbot_data_path + +plugin_data_path = 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. +``` diff --git a/docs/snapshots/v4.23.6/en/dev/star/plugin-new.md b/docs/snapshots/v4.23.6/en/dev/star/plugin-new.md new file mode 100644 index 0000000..3c59718 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/plugin-new.md @@ -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 +``` + +Then, use `VSCode` to open the `AstrBot` project. Navigate to the `data/plugins/` 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). diff --git a/docs/snapshots/v4.23.6/en/dev/star/plugin-publish.md b/docs/snapshots/v4.23.6/en/dev/star/plugin-publish.md new file mode 100644 index 0000000..29864fd --- /dev/null +++ b/docs/snapshots/v4.23.6/en/dev/star/plugin-publish.md @@ -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) diff --git a/docs/snapshots/v4.23.6/en/faq.md b/docs/snapshots/v4.23.6/en/faq.md new file mode 100644 index 0000000..3955f54 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/faq.md @@ -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 `` + +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. diff --git a/docs/snapshots/v4.23.6/en/index.md b/docs/snapshots/v4.23.6/en/index.md new file mode 100644 index 0000000..add09ac --- /dev/null +++ b/docs/snapshots/v4.23.6/en/index.md @@ -0,0 +1,31 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: >- + Soulter%2FAstrBot | Trendshift + 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. +--- diff --git a/docs/snapshots/v4.23.6/en/ospp/2025.md b/docs/snapshots/v4.23.6/en/ospp/2025.md new file mode 100644 index 0000000..b451136 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/ospp/2025.md @@ -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) diff --git a/docs/snapshots/v4.23.6/en/others/self-host-t2i.md b/docs/snapshots/v4.23.6/en/others/self-host-t2i.md new file mode 100644 index 0000000..85638c6 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/others/self-host-t2i.md @@ -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://:8999`. + +> If you deployed on the same machine as AstrBot, the URL should be `http://localhost:8999`. + +image + diff --git a/docs/snapshots/v4.23.6/en/platform/aiocqhttp.md b/docs/snapshots/v4.23.6/en/platform/aiocqhttp.md new file mode 100644 index 0000000..a05a31d --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/aiocqhttp.md @@ -0,0 +1,44 @@ +# Connect OneBot v11 Protocol Implementations + +OneBot is a standardized bot application interface designed to unify bot development across different chat platforms, so developers can write business logic once and use it on multiple platforms. + +AstrBot supports all client implementations that implement OneBot v11 reverse WebSocket (AstrBot acts as the server). + +Common OneBot v11 implementation projects are listed below: + +- [NapCat](https://github.com/NapNeko/NapCatQQ) +- [OneDisc](https://github.com/ITCraftDevelopmentTeam/OneDisc) +- [Tele-KiraLink](https://github.com/Echomirix/Tele-KiraLink) + +Please refer to each implementation project's deployment documentation. + +## 1. Configure OneBot v11 + +1. Open AstrBot's WebUI +2. Click `Bots` in the left sidebar +3. In the right panel, click `+ Create Bot` +4. Select `OneBot v11` + +Fill in the form: + +- ID (`id`): any value, used only to distinguish instances of different platforms. +- Enable (`enable`): check it. +- Reverse WebSocket host: fill your machine IP, usually `0.0.0.0`. +- Reverse WebSocket port: choose any port, default is `6199`. +- Reverse WebSocket token: fill this only when NapCat network configuration has a token set. + +Click `Save`. + +## 2. Configure the protocol implementation side + +Please refer to each protocol implementation project's deployment documentation. + +Notes: + +1. The implementation must support `Reverse WebSocket`, with AstrBot acting as the server and the implementation client as the client. +2. The reverse WebSocket URL is `ws(s)://:6199/ws`. + +## 3. Verify + +Go to AstrBot WebUI `Console`. If a blue log appears saying `aiocqhttp(OneBot v11) adapter connected.`, the connection is successful. +If after a few seconds you see `aiocqhttp adapter has been closed`, it means the connection timed out (failed). Please double-check your configuration. diff --git a/docs/snapshots/v4.23.6/en/platform/dingtalk.md b/docs/snapshots/v4.23.6/en/platform/dingtalk.md new file mode 100644 index 0000000..9b21f7c --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/dingtalk.md @@ -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. diff --git a/docs/snapshots/v4.23.6/en/platform/discord.md b/docs/snapshots/v4.23.6/en/platform/discord.md new file mode 100644 index 0000000..4298ce3 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/discord.md @@ -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). diff --git a/docs/snapshots/v4.23.6/en/platform/kook.md b/docs/snapshots/v4.23.6/en/platform/kook.md new file mode 100644 index 0000000..9671456 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/kook.md @@ -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 diff --git a/docs/snapshots/v4.23.6/en/platform/lark.md b/docs/snapshots/v4.23.6/en/platform/lark.md new file mode 100644 index 0000000..b6e4fe0 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/lark.md @@ -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) diff --git a/docs/snapshots/v4.23.6/en/platform/line.md b/docs/snapshots/v4.23.6/en/platform/line.md new file mode 100644 index 0000000..884a841 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/line.md @@ -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. diff --git a/docs/snapshots/v4.23.6/en/platform/matrix.md b/docs/snapshots/v4.23.6/en/platform/matrix.md new file mode 100644 index 0000000..17d673d --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/matrix.md @@ -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). diff --git a/docs/snapshots/v4.23.6/en/platform/mattermost.md b/docs/snapshots/v4.23.6/en/platform/mattermost.md new file mode 100644 index 0000000..feb0716 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/mattermost.md @@ -0,0 +1,139 @@ +# Connecting to Mattermost + +The Mattermost adapter connects to your Mattermost server through a Bot Token and WebSocket. After finishing the two parts below, AstrBot can send and receive messages in Mattermost channels and direct messages. + +## Create the AstrBot Mattermost Platform Adapter + +Go to the `Bots` page, click `+ Create Bot`, and choose `Mattermost`. + +On the configuration page, enable it first, then fill in: + +- `Mattermost URL`: your Mattermost server URL, for example `https://chat.example.com` +- `Mattermost Bot Token`: the access token generated after creating a bot account in Mattermost +- `Mattermost Reconnect Delay`: how long AstrBot waits before reconnecting after a WebSocket disconnect, default `5` + +Then click save. + +## Deploy Mattermost + +If you do not have a Mattermost server yet, use the official Mattermost Docker Compose repository: + +- Official docs: https://docs.mattermost.com/deployment-guide/server/containers/install-docker.html +- Official repository: https://github.com/mattermost/docker + +The current quick-start flow recommended by Mattermost is: + +```bash +git clone https://github.com/mattermost/docker +cd docker +cp env.example .env +``` + +Then update at least these values in `.env`: + +- `DOMAIN` +- `MATTERMOST_IMAGE_TAG` +- It is also recommended to set `MM_SUPPORTSETTINGS_SUPPORTEMAIL` + +Create the data directories and set ownership: + +```bash +mkdir -p ./volumes/app/mattermost/{config,data,logs,plugins,client/plugins,bleve-indexes} +sudo chown -R 2000:2000 ./volumes/app/mattermost +``` + +Choose one startup mode: + +Without the bundled NGINX: + +```bash +docker compose -f docker-compose.yml -f docker-compose.without-nginx.yml up -d +``` + +With the bundled NGINX: + +```bash +docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d +``` + +Access URLs: + +- Without NGINX: `http://your-domain:8065` +- With NGINX: `https://your-domain` + +> [!TIP] +> Mattermost currently states that production Docker support is Linux-only. macOS and Windows are better suited for development or testing. + +## Create a Bot in Mattermost + +### 1. Enable Bot Account Creation + +Open the Mattermost system console: + +`System Console > Integrations > Bot Accounts` + +Enable `Enable Bot Account Creation`. + +### 2. Create the Bot Account + +Go to: + +`Product menu > Integrations > Bot Accounts` + +Click `Add Bot Account` and fill in: + +- `Username` +- `Display Name` +- `Description` + +After creation, copy the generated Bot Token. It is shown only once. Paste it into AstrBot's `Mattermost Bot Token` field. + +### 3. Add the Bot to a Channel + +Add the bot to the channel where AstrBot should work. Otherwise the bot will not be able to properly receive and send messages in that channel. + +## How to Fill in Mattermost URL + +`Mattermost URL` should be the external URL of your Mattermost server, without a trailing slash. For example: + +```text +https://chat.example.com +``` + +If you are only testing locally, you can also use: + +```text +http://127.0.0.1:8065 +``` + +If both AstrBot and Mattermost run in containers, prefer an address reachable from the AstrBot container, such as the Mattermost service name on the same Docker network. + +## Start and Verify + +After saving the AstrBot platform adapter configuration: + +1. Make sure the AstrBot logs do not show Mattermost authentication or WebSocket connection errors. +2. Send a message in a channel that includes the bot, or send the bot a direct message. +3. If AstrBot replies normally, the integration is working. + +## Common Issues + +### Invalid Token Errors + +Usually one of these: + +- You copied a user token instead of the bot token +- The token contains extra spaces +- The bot account was deleted or the token was regenerated + +### Connected but No Channel Messages Arrive + +Check these first: + +- The bot has been added to the target channel +- `Mattermost URL` points to an address AstrBot can actually reach +- Your Mattermost reverse proxy forwards WebSocket traffic correctly + +### Mattermost Opens in Browser but AstrBot Still Cannot Connect + +If AstrBot runs in a container while `Mattermost URL` is set to `localhost` or `127.0.0.1`, AstrBot will connect to itself instead of the Mattermost service. In that case, switch to an address reachable inside the Docker network. diff --git a/docs/snapshots/v4.23.6/en/platform/misskey.md b/docs/snapshots/v4.23.6/en/platform/misskey.md new file mode 100644 index 0000000..801dd57 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/misskey.md @@ -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) diff --git a/docs/snapshots/v4.23.6/en/platform/qqofficial.md b/docs/snapshots/v4.23.6/en/platform/qqofficial.md new file mode 100644 index 0000000..b4237a0 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/qqofficial.md @@ -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) diff --git a/docs/snapshots/v4.23.6/en/platform/qqofficial/webhook.md b/docs/snapshots/v4.23.6/en/platform/qqofficial/webhook.md new file mode 100644 index 0000000..ebd136e --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/qqofficial/webhook.md @@ -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.19.6. + +| Message Type | Receive | Send | Notes | +| --- | --- | --- | --- | +| Text | Yes | Yes | | +| Image | Yes | Yes | | +| Voice | Yes | Yes | | +| Video | Yes | Yes | | +| File | Yes | Yes | | + +Proactive message push: 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 `/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: +2. Configure reverse proxy: + +Caddy can automatically apply TLS certificates for Webhook access. diff --git a/docs/snapshots/v4.23.6/en/platform/qqofficial/websockets.md b/docs/snapshots/v4.23.6/en/platform/qqofficial/websockets.md new file mode 100644 index 0000000..9b64576 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/qqofficial/websockets.md @@ -0,0 +1,87 @@ +# Connect QQ via QQ Official Bot (Websockets) + +## Supported Basic Message Types + +> Version v4.19.6. + +| Message Type | Receive | Send | Notes | +| --- | --- | --- | --- | +| Text | Yes | Yes | | +| Image | Yes | Yes | | +| Voice | Yes | Yes | | +| Video | Yes | Yes | | +| File | Yes | Yes | | + +Proactive message push: 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. diff --git a/docs/snapshots/v4.23.6/en/platform/satori/guide.md b/docs/snapshots/v4.23.6/en/platform/satori/guide.md new file mode 100644 index 0000000..7c9b0b0 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/satori/guide.md @@ -0,0 +1,32 @@ +# Connect to Satori Protocol + +## Satori protocol overview + +> Excerpt from: https://satori.chat/introduction.html + +Satori is a unified chat protocol. It aims to reduce differences between chat platforms and let developers build cross-platform, extensible, high-performance chat applications with lower cost. + +The protocol is named after [Komeiji Satori](https://satori.js.org) in Touhou Project. The idea is that Satori can serve as a bridge between chat platforms, as Komeiji Satori communicates telepathically. + +The development team behind Satori has long worked on bot development and is familiar with the communication patterns of many platforms. After about 4 years, Satori now has a mature design and implementation. The official project currently provides adapters for more than 15 platforms, covering major messaging services worldwide such as QQ, Discord, WeCom, KOOK, and others. + +## 1. Configure the protocol server side + +Please refer to the deployment documentation of the chosen implementation project. + +## 2. Configure Satori protocol in AstrBot + +1. Open AstrBot WebUI. +2. Click `Bots` in the left sidebar. +3. In the right panel, click `+ Create Bot`. +4. Select `satori`. + +Fill in the form: + +- Bot ID (`id`): e.g. `satori` (any value is fine). +- Enable (`enable`): check it. +- Satori API base URL (`satori_api_base_url`): `http://localhost:5600/v1` (same port as the protocol implementation). +- Satori WebSocket endpoint (`satori_endpoint`): `ws://localhost:5600/v1/events` (same port as the protocol implementation). +- Satori token (`satori_token`): fill according to implementation settings. + +Click `Save`. diff --git a/docs/snapshots/v4.23.6/en/platform/satori/server-satori.md b/docs/snapshots/v4.23.6/en/platform/satori/server-satori.md new file mode 100644 index 0000000..bcf5bee --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/satori/server-satori.md @@ -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: +- Koishi community: + +## 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. diff --git a/docs/snapshots/v4.23.6/en/platform/slack.md b/docs/snapshots/v4.23.6/en/platform/slack.md new file mode 100644 index 0000000..28b6b0f --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/slack.md @@ -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). diff --git a/docs/snapshots/v4.23.6/en/platform/start.md b/docs/snapshots/v4.23.6/en/platform/start.md new file mode 100644 index 0000000..3dbfce9 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/start.md @@ -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. diff --git a/docs/snapshots/v4.23.6/en/platform/telegram.md b/docs/snapshots/v4.23.6/en/platform/telegram.md new file mode 100644 index 0000000..68ae143 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/telegram.md @@ -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`. +::: diff --git a/docs/snapshots/v4.23.6/en/platform/vocechat.md b/docs/snapshots/v4.23.6/en/platform/vocechat.md new file mode 100644 index 0000000..bb03266 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/vocechat.md @@ -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: +- AstrBot repo: diff --git a/docs/snapshots/v4.23.6/en/platform/wecom.md b/docs/snapshots/v4.23.6/en/platform/wecom.md new file mode 100644 index 0000000..a102c56 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/wecom.md @@ -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://: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: + +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://: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` diff --git a/docs/snapshots/v4.23.6/en/platform/wecom_ai_bot.md b/docs/snapshots/v4.23.6/en/platform/wecom_ai_bot.md new file mode 100644 index 0000000..51635b4 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/wecom_ai_bot.md @@ -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). diff --git a/docs/snapshots/v4.23.6/en/platform/weixin-official-account.md b/docs/snapshots/v4.23.6/en/platform/weixin-official-account.md new file mode 100644 index 0000000..2ca2783 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/weixin-official-account.md @@ -0,0 +1,78 @@ +# Connect AstrBot to WeChat Official Account Platform + +AstrBot supports WeChat Official Account integration (version >= v3.5.8). After setup, you can chat with AstrBot directly in the WeChat Official Account chat interface. + +## Before You Start + +1. Open AstrBot Dashboard. +2. Click `Bots` in the left sidebar. +3. Click `+ Create Bot`. +4. Select `weixin_official_account`. + +A configuration dialog will appear. Keep it open and continue. + +## Create / Sign In to WeChat Official Account Platform + +Open [WeChat Official Account Platform](https://mp.weixin.qq.com/). + +- If you already have an account, sign in. +- If not, register a new account and choose `Official Account`. + +> [!NOTE] +> A newly registered account may require 1-2 days for review before it can be used. + +## Configure Callback Service + +Open `Settings & Development` -> `Development Interface Management`. + +![Development Interface Management](https://files.astrbot.app/docs/source/images/weixin-official-account/image.png) + +Copy AppID and AppSecret from WeChat platform to AstrBot fields `appid` and `secret`. + +Open IP whitelist and add your public IP(s), one per line if multiple. + +In server configuration, click modify. + +- `Token`: create any string with length 3-32, and fill the same value in AstrBot `token`. +- `EncodingAESKey`: click random generate and fill AstrBot `encoding_aes_key`. + +Keep `Unified Webhook Mode (unified_webhook_mode)` enabled (recommended), then save AstrBot config and wait for restart. + +For `URL`: + +- If unified mode is enabled, use the unique callback URL generated by AstrBot (from logs or bot card). +- If unified mode is disabled, use `http:///callback/command`. + +![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png) + +> [!NOTE] +> WeChat Official Account callback supports only ports 80 or 443. You usually need a domain and reverse proxy: +> - Unified mode enabled: forward to AstrBot port `6185` +> - Unified mode disabled: forward to adapter port `6194` + +Set message encryption mode to `Security Mode`. + +Wait a moment and click `Submit`. If configuration is correct, you will see success. + +## Test + +In WeChat Official Account platform, open account profile and find your QR code. + +Scan it with WeChat, send `help`, and check whether AstrBot replies. + +If it replies, integration is successful. + +> [!NOTE] +> If console shows `ip xxxxx not in whitelist`, your public IP is not in WeChat whitelist yet. Add it and wait a few minutes for WeChat to refresh. + +## Reverse Proxy (Custom API Base) + +AstrBot supports custom endpoint via `api_base_url` for environments without stable public IP. + +## 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` diff --git a/docs/snapshots/v4.23.6/en/platform/weixin_oc.md b/docs/snapshots/v4.23.6/en/platform/weixin_oc.md new file mode 100644 index 0000000..2068c02 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/platform/weixin_oc.md @@ -0,0 +1,74 @@ +# Connect Personal WeChat + +> Introduced in v4.22.0. + +AstrBot supports connecting a personal WeChat account through the `Personal WeChat` adapter. This adapter is implemented on top of Tencent's official `openclaw-weixin` interface, uses QR-code login plus long polling, and does not require a Webhook callback URL. + +> [!NOTE] +> Please upgrade your mobile WeChat to a recent version. +> +> **iOS**: >= 4.0.70 + +## Supported Message Types + +| Message Type | Receive | Send | Notes | +| --- | --- | --- | --- | +| Text | Yes | Yes | | +| Image | Yes | Yes | Downloaded and decrypted into the local temp directory on receive | +| Voice | Yes* | No | *WeChat cloud-side transcription is used, so no local transcription is required | +| Video | Yes | Yes | Downloaded and decrypted into the local temp directory on receive | +| File | Yes | Yes | Downloaded and decrypted into the local temp directory on receive | + +## Create the Bot + +1. Open AstrBot WebUI. +2. Click `Bots` in the left sidebar. +3. Click `+ Create Bot` in the upper-right corner. +4. Select `Personal WeChat`. + +## Configuration Notes + +In most cases, you only need to pay attention to these fields: + +- `ID(id)`: Any value you like, used to distinguish different bot instances. +- `Enable(enable)`: Turn it on. + +Leave the remaining options at their default values unless you explicitly know you need to change them: + +- `QR Poll Interval (weixin_oc_qr_poll_interval)` +- `Long Poll Timeout (weixin_oc_long_poll_timeout_ms)` +- `API Timeout (weixin_oc_api_timeout_ms)` + +> [!TIP] +> `token` and `account_id` are saved automatically by AstrBot after QR login succeeds. You normally do not need to fill them manually. + +## QR Login + +1. Fill in the configuration and click `Save`. +2. Return to the bot list. AstrBot will automatically request a login QR code from WeChat. +3. On the bot card, click `View QR Code` to open the QR dialog. +4. Scan it with WeChat on your phone, then confirm the login inside WeChat. + +After login succeeds, AstrBot will automatically persist the login state. On later restarts, if the session is still valid, you usually do not need to scan again. + +> [!NOTE] +> If the QR code expires, AstrBot will automatically request a new one. Please scan the refreshed QR code instead of the old one. + +## Verification + +After login succeeds, send a message from WeChat. If AstrBot replies normally, the integration is working. + +You can also watch the `Console` page in WebUI to confirm that the adapter has completed login and started polling messages. + +## Media File Storage + +Received images, videos, files, and voice messages are downloaded and decrypted into AstrBot's local temporary directory: + +`data/temp` + +These files are temporary cached files and can be further used by plugins, agents, or the file service. + +## Notes + +- This adapter logs in by scanning a QR code with a personal WeChat account, so its setup flow is different from WeChat Official Account and WeCom. +- No public callback URL is required, and Unified Webhook Mode is not needed. diff --git a/docs/snapshots/v4.23.6/en/providers/302ai.md b/docs/snapshots/v4.23.6/en/providers/302ai.md new file mode 100644 index 0000000..50a3abe --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/302ai.md @@ -0,0 +1,21 @@ +# 接入 302.AI + +302.AI 是企业级 AI 应用平台,支持快捷接入全球各类 AI 模型。 + +## 使用 + +点击[此链接](https://share.302.ai/rr1M3l) 注册账户。 + +注册完毕之后,点击[此链接](https://302.ai/apis/)选择需要接入的模型。 + +根据需求,进入[此链接](https://dash.302.ai/charge) 充值对应的金额。 + +## 接入 + +打开 AstrBot 控制台 -> 服务提供商页面,点击新增提供商,找到并点击 `302.AI`(需要版本 >= 3.5.18) + +修改 ID,并将 API Key 和模型名称填入对话框表单,点击保存,即可完成创建。 + +## 使用 + +对机器人输入 `/provider` 指令,将提供商切换到刚刚添加的 302.AI 提供商,即可使用。 diff --git a/docs/snapshots/v4.23.6/en/providers/agent-runners.md b/docs/snapshots/v4.23.6/en/providers/agent-runners.md new file mode 100644 index 0000000..3f982bc --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/agent-runners.md @@ -0,0 +1,19 @@ +# Agent Runners + +## What Is an Agent Runner? + +An Agent Runner is the component in AstrBot that executes Agent capabilities and handles AI-related workflows. + +AstrBot includes a powerful built-in Agent Runner. You can also integrate third-party Agent Runner services like Dify, Coze, Alibaba Bailian, and DeerFlow, or build your own. + +If you already have a model provider that handles single requests, you still need an execution layer for multi-turn conversations, tool calling, and orchestration. That is exactly what an Agent Runner does. + +For more details, see [Usage · Agent Runner](/en/use/agent-runner). + +## Quick Links + +- [Built-in Agent Runner](/en/providers/agent-runners/astrbot-agent-runner) +- [Dify](/en/providers/agent-runners/dify) +- [Coze](/en/providers/agent-runners/coze) +- [Alibaba Bailian](/en/providers/agent-runners/dashscope) +- [DeerFlow](/en/providers/agent-runners/deerflow) diff --git a/docs/snapshots/v4.23.6/en/providers/agent-runners/astrbot-agent-runner.md b/docs/snapshots/v4.23.6/en/providers/agent-runners/astrbot-agent-runner.md new file mode 100644 index 0000000..74873d7 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/agent-runners/astrbot-agent-runner.md @@ -0,0 +1,8 @@ +# Built-in Agent Runner + +By default, AstrBot uses the built-in Agent Runner as the default executor. You don't need to configure anything to use AstrBot's powerful built-in Agent Runner. + +![image](https://files.astrbot.app/docs/source/images/astrbot-agent-runner/image.png) + +With the built-in Agent Runner, you can use AstrBot's [MCP Server](/use/mcp), [Knowledge Base](/use/knowledge-base), [Web Search](/use/websearch), and persona features. + diff --git a/docs/snapshots/v4.23.6/en/providers/agent-runners/coze.md b/docs/snapshots/v4.23.6/en/providers/agent-runners/coze.md new file mode 100644 index 0000000..3ef4e86 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/agent-runners/coze.md @@ -0,0 +1,65 @@ +# Connect to Coze + +AstrBot v4.2.1 and later versions support connecting to [Coze](https://www.coze.cn/) Agent service. + +## Preparation: Get API Key + +First, register and log in to your [Coze](https://www.coze.cn/) account, then go to the [API Key Management Page](https://www.coze.cn/open/oauth/pats) to create a new API Key. + +You can follow the steps in the image to reach the API Key management page, or click the link above to go directly. + +![Create API Key](https://files.astrbot.app/docs/source/images/coze/image_1.png) + +Then, click "Create", fill in your API Key name on the following page, select an expiration time (permanent tokens are not recommended), click "Select All" under "Permissions", select a workspace, and then click "Confirm". + +![Create Token](https://files.astrbot.app/docs/source/images/coze/image_2.png) + +After that, we will get a new API Key. Please copy and save it, as it will be needed later. + +![New API Key](https://files.astrbot.app/docs/source/images/coze/image_3.png) + +## Preparation: Configure the Agent + +Go to the [Project Development](https://www.coze.cn/space/develop) page, click "+Project" in the upper right corner to create a new project, and select to create an agent. + +![Create Project](https://files.astrbot.app/docs/source/images/coze/image_4.png) + +![Create Project](https://files.astrbot.app/docs/source/images/coze/image_5.png) + +**Note**: After creating the agent, you must first click the **Publish** button in the upper right corner to publish the agent. In the "Select Publishing Platform" section, check all API options, then click "Publish". + +> If you don't publish or don't check the API options during publishing, you won't be able to call the agent via API. + +![Publish Agent](https://files.astrbot.app/docs/source/images/coze/image_6.png) + +After clicking publish, the agent creation is complete. You can see the publish history on the left side of the publish button on the agent development page to confirm the agent has been published successfully. + +Next, note the URL on the agent development page: + +![Agent Development](https://files.astrbot.app/docs/source/images/coze/image_7.png) + +For example, if the URL in the example is: "https://www.coze.cn/space/7553214941005004863/bot/7553248674860826660" + +Then the `bot_id` is the string of numbers after `bot/` in the URL: `7553248674860826660` + +We need to record the `bot_id` for later use. + +## Configure Coze in AstrBot + +After completing all the preparation work, we can now configure Coze in AstrBot. + +Go to AstrBot Admin Panel -> Service Provider -> Add Service Provider -> Coze to enter the configuration page. + +![Coze Provider](https://files.astrbot.app/docs/source/images/coze/image_8.png) + +Fill in the API Key and bot_id you just created, then click Save. + +> Other configuration notes: +> +> - API Base URL: Generally no modification is needed. If you are using the international version of Coze, change this to: "https://api.coze.com" +> - Let Coze manage conversation history: As described. + +## Select Agent Runner + +Go to the Configuration page in the left sidebar, click "Agent Execution Method", select "Coze", then select the ID of the Coze Agent Runner you just created in the new configuration options that appear below, and click "Save" in the bottom right corner to complete the configuration. + diff --git a/docs/snapshots/v4.23.6/en/providers/agent-runners/dashscope.md b/docs/snapshots/v4.23.6/en/providers/agent-runners/dashscope.md new file mode 100644 index 0000000..6323824 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/agent-runners/dashscope.md @@ -0,0 +1,52 @@ +# Connect to Alibaba Cloud Bailian Application + +Since v3.4.30, AstrBot supports connecting to Alibaba Cloud Bailian Application. + +## Configure Alibaba Cloud Bailian Application in AstrBot + +On the [Alibaba Cloud Bailian Application](https://bailian.console.aliyun.com/app-center#/app-center) website, click to add a new application. Create an agent application, workflow application, or agent orchestration application according to your needs, and build the agent or workflow as required. + +Record the Application ID: + +![image](https://files.astrbot.app/docs/source/images/dashscope/image-1.png) + +Click to enter the application, click Publishing Channel -> API Call -> API KEY, create and copy the API KEY: + +![alt text](https://files.astrbot.app/docs/source/images/dashscope/image-2.png) + +In the WebUI, click "Model Provider" -> "Add Provider", select "Agent Runner", select "Alibaba Cloud Bailian Application", and enter the Alibaba Cloud Bailian Application configuration page. + +According to Alibaba Cloud Bailian Application, there are four application types: + +- Agent Application (agent) +- Task Workflow Application (task-workflow) +- Dialog Workflow Application (dialog-workflow) +- Agent Orchestration Application (agent-arrange) + +> [!TIP] +> Multi-turn conversations are only supported for agent applications and dialog workflow applications. AstrBot will automatically attach conversation history for these two types of applications to support multi-turn conversations. + +Please ensure that the `Application Type` configured in AstrBot matches the application type created in Alibaba Cloud Bailian Application. + +Then fill in the Application ID in `dashscope_app_id` and the API KEY in `dashscope_api_key`. + +After filling in these three items, click Save. + +## Select Agent Runner + +Go to the Configuration page in the left sidebar, click "Agent Execution Method", select "Alibaba Cloud Bailian Application", then select the ID of the Alibaba Cloud Bailian Application Agent Runner you just created in the new configuration options that appear below, and click "Save" in the bottom right corner to complete the configuration. + +## Appendix: Dynamically Set Workflow Input Variables During Chat (Optional) + +For the two workflow applications, you can dynamically set input variables in the chat area. + +Use the `/set` command to dynamically set input variables, as shown in the figure below: + +![alt text](https://files.astrbot.app/docs/source/images/dify/image-5.png) + +After setting variables, AstrBot will attach the variables you set in the next request to Alibaba Cloud Bailian Application, flexibly adapting to your Workflow. + +Of course, you can use the `/unset` command to cancel the variables you set. For example, `/unset name` + +Variables are permanently valid in the current session. + diff --git a/docs/snapshots/v4.23.6/en/providers/agent-runners/deerflow.md b/docs/snapshots/v4.23.6/en/providers/agent-runners/deerflow.md new file mode 100644 index 0000000..04b6fc8 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/agent-runners/deerflow.md @@ -0,0 +1,53 @@ +# Connect to DeerFlow + +Starting from v4.19.2, AstrBot supports connecting to the [DeerFlow](https://github.com/bytedance/deer-flow) Agent Runner. + +## Preparation: Deploy DeerFlow + +If you have not deployed DeerFlow yet, please complete installation and startup by following the official DeerFlow documentation: + +- [DeerFlow GitHub Repository](https://github.com/bytedance/deer-flow) +- [DeerFlow Official Website](https://deerflow.tech/) +- [DeerFlow Configuration Guide](https://github.com/bytedance/deer-flow/blob/main/backend/docs/CONFIGURATION.md) + +Make sure DeerFlow is running properly and that AstrBot can reach the DeerFlow gateway. By default, the DeerFlow gateway address is `http://127.0.0.1:2026`. + +> [!TIP] +> - `API Base URL` must start with `http://` or `https://`. +> - If AstrBot and DeerFlow run in different containers or on different hosts, replace `127.0.0.1` with the actual reachable LAN address, hostname, or domain of your DeerFlow service. + +## Configure DeerFlow in AstrBot + +In the WebUI, click "Model Provider" -> "Add Provider", select "Agent Runner", select "DeerFlow", and enter the DeerFlow configuration page. + +Fill in the following fields: + +- `API Base URL`: DeerFlow API gateway URL. Default: `http://127.0.0.1:2026` +- `DeerFlow API Key`: Optional. Fill this if your DeerFlow gateway is protected by Bearer auth +- `Authorization Header`: Optional. Custom Authorization header value. This takes precedence over `DeerFlow API Key` +- `Assistant ID`: Maps to LangGraph `assistant_id`. Default: `lead_agent` +- `Model name override`: Optional. Overrides the default model configured in DeerFlow +- `Enable thinking mode`: Whether to enable DeerFlow thinking mode +- `Enable plan mode`: Maps to DeerFlow `is_plan_mode` +- `Enable subagent`: Maps to DeerFlow `subagent_enabled` +- `Max concurrent subagents`: Maps to `max_concurrent_subagents`. Effective only when subagents are enabled. Default: `3` +- `Recursion limit`: Maps to LangGraph `recursion_limit`. Default: `1000` + +After filling in the configuration, click Save. + +> [!TIP] +> - If DeerFlow already has a default model configured on its side, you can leave `Model name override` empty. +> - Only enable `plan mode` or `subagent` related options when the corresponding DeerFlow capabilities are already configured on the DeerFlow side. + +## Select Agent Runner + +Go to the Configuration page in the left sidebar, click "Agent Execution Method", select "DeerFlow", then select the ID of the DeerFlow Agent Runner you just created in the new configuration option below, and click "Save" in the bottom right corner to complete the configuration. + +## Common Checks + +If requests are not being executed through DeerFlow correctly, check the following first: + +- whether the DeerFlow service is running properly +- whether `API Base URL` is reachable from the AstrBot environment +- whether the authentication settings are correct +- whether `Assistant ID` matches an actual available assistant in DeerFlow diff --git a/docs/snapshots/v4.23.6/en/providers/agent-runners/dify.md b/docs/snapshots/v4.23.6/en/providers/agent-runners/dify.md new file mode 100644 index 0000000..8b17143 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/agent-runners/dify.md @@ -0,0 +1,82 @@ +# Connect to Dify + +## Install Dify + +If you haven't installed Dify yet, please refer to the [Dify Installation Documentation](https://docs.dify.ai/getting-started/install-self-hosted) to install it. + +## Configure Dify in AstrBot + +In the WebUI, click "Model Provider" -> "Add Provider", select "Agent Runner", select "Dify", and enter the Dify configuration page. + +![image](https://files.astrbot.app/docs/source/images/dify/image.png) + +In Dify, one `API Key` uniquely corresponds to one Dify application. Therefore, you can create multiple Providers to adapt to multiple Dify applications. + +According to the current Dify project, there are three types: + +- chat +- agent +- workflow + +>[!TIP] +>Please ensure that the APP type you set in AstrBot matches the application type created in Dify. +>![image](https://files.astrbot.app/docs/source/images/dify/image-3.png) + + +### Chat and Agent Applications + +Create your Dify Chat and Agent application keys as shown in the figure below: + +![image](https://files.astrbot.app/docs/source/images/dify/chat-agent-api-key.png) + +![image](https://files.astrbot.app/docs/source/images/dify/chat-agent-api-key-2.png) + +Copy the key and paste it into the `API Key` field in the configuration, then click "Save". + +### Workflow Applications + +#### Configure Input and Output Variable Names + +Workflow applications receive input variables, execute the workflow, and output the results. + +![image](https://files.astrbot.app/docs/source/images/dify/workflow-io-key.png) + +For Workflow applications, AstrBot will attach two variables with each request: + +- `astrbot_text_query`: Input variable name. This is the text content entered by the user. +- `astrbot_session_id`: Session ID + +You can customize the input variable name in the configuration, which is the "Prompt Input Variable Name" shown in the figure above. + +You need to modify the input variable name of your Workflow to adapt to AstrBot's input. + +Finally, the Workflow will output a result. You can customize the variable name of this result, which is the "Dify Workflow Output Variable Name" in the configuration above, with a default value of `astrbot_wf_output`. You need to configure this variable name in the output node of the Dify Workflow, otherwise AstrBot cannot parse it correctly. + +#### Create API Key + +Create your Dify Workflow application's API Key as shown in the figure below: + +Click the Publish button in the upper right corner -> Access API -> click API Key in the upper right corner -> Create Key, then copy the API Key. + +![image](https://files.astrbot.app/docs/source/images/dify/workflow-api-key.png) + +Copy the key and paste it into the `API Key` field in the configuration, then click "Save". + +### Select Agent Runner + +Go to the Configuration page in the left sidebar, click "Agent Execution Method", select "Dify", then select the ID of the Dify Agent Runner you just created in the new configuration options that appear below, and click "Save" in the bottom right corner to complete the configuration. + +## Appendix: Dynamically Set Workflow Input Variables During Chat (Optional) + +You can use the `/set` command to dynamically set input variables, as shown in the figure below: + +![alt text](https://files.astrbot.app/docs/source/images/dify/image-5.png) + +After setting variables, AstrBot will attach the variables you set in the next request to Dify, flexibly adapting to your Workflow. + +![alt text](https://files.astrbot.app/docs/source/images/dify/image-4.png) + +Of course, you can use the `/unset` command to cancel the variables you set. + +Variables are permanently valid in the current session. + diff --git a/docs/snapshots/v4.23.6/en/providers/aihubmix.md b/docs/snapshots/v4.23.6/en/providers/aihubmix.md new file mode 100644 index 0000000..535f629 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/aihubmix.md @@ -0,0 +1,70 @@ +# Connect AIHubMix + +[AIHubMix](https://aihubmix.com/?aff=4bfH) is a multi-model AI API gateway that provides unified access to OpenAI, Claude, Gemini, DeepSeek, Kimi and more through a single API key. Beyond LLM, it also supports speech, embedding, reranking and other capabilities. + +Fully compatible with the OpenAI API format — just change the API Base and Key to get started. **Some models are completely free for development and testing.** + +## Get an API Key + +1. Sign up at [AIHubMix](https://aihubmix.com/?aff=4bfH) +2. Go to Console → API Keys to create a new key + +![Get an API Key](https://github.com/user-attachments/assets/d717f21b-2805-4aff-ac90-f5c98f17cb79) + +## Configure in AstrBot + +Open the AstrBot dashboard , click **Providers → Add Provider → OpenAI**. + +Fill in the following: + +| Field | Value | +|-------|-------| +| API Base URL | `https://aihubmix.com/v1` | +| API Key | Your AIHubMix key | + +After saving, click the provider card to add models. + +![Configure in AstrBot](https://github.com/user-attachments/assets/ee2fb8ba-652c-4e97-a781-42a9082ad7eb) + +## Recommended Models + +### Free Models 🆓 + +These models are completely free, great for development and testing: + +| Model ID | Description | +|----------|-------------| +| `gpt-4.1-free` | GPT-4.1 free tier | +| `gemini-3-flash-preview-free` | Gemini 3 Flash free tier | +| `coding-glm-5-free` | GLM-5 coding model, free | +| `coding-minimax-m2.5-free` | MiniMax M2.5 coding model, free | + +### Paid Models (Popular) + +| Model ID | Provider | Description | +|----------|----------|-------------| +| `gpt-5.4` | OpenAI | Latest flagship model | +| `claude-sonnet-4-6` | Anthropic | Great for reasoning and code | +| `gpt-5.3-chat-latest` | OpenAI | High-performance chat | +| `deepseek-v3.2` | DeepSeek | Cost-effective | +| `kimi-k2.5` | Moonshot | Long context | +| `gemini-3.1-pro-preview` | Google | Multimodal | + +> See the full model list at [AIHubMix Docs](https://doc.aihubmix.com). + +## More Than Chat Models + +AIHubMix also supports the following capabilities, all configurable in AstrBot: + +| Capability | AstrBot Config Location | +|------------|------------------------| +| Speech-to-Text (STT) | Providers → Speech to Text | +| Text-to-Speech (TTS) | Providers → Text to Speech | +| Embedding | Providers → Embedding | +| Reranking | Providers → Rerank | + +All capabilities use the same API Key and API Base — no extra setup needed. + +## Set as Default + +Go to **Settings → Provider Settings**, set "Default Chat Model Provider" to your AIHubMix provider, and save. diff --git a/docs/snapshots/v4.23.6/en/providers/coze.md b/docs/snapshots/v4.23.6/en/providers/coze.md new file mode 100644 index 0000000..942b6c9 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/coze.md @@ -0,0 +1 @@ +This page is deprecated. Please refer to [Coze Agent Runner](../agent-runners/coze.md). diff --git a/docs/snapshots/v4.23.6/en/providers/dashscope.md b/docs/snapshots/v4.23.6/en/providers/dashscope.md new file mode 100644 index 0000000..a2d2443 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/dashscope.md @@ -0,0 +1 @@ +This page is deprecated. Please refer to [Alibaba Cloud Bailian Application Agent Runner](../agent-runners/dashscope.md). diff --git a/docs/snapshots/v4.23.6/en/providers/dify.md b/docs/snapshots/v4.23.6/en/providers/dify.md new file mode 100644 index 0000000..de0f902 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/dify.md @@ -0,0 +1 @@ +This page is deprecated. Please refer to [Dify Agent Runner](../agent-runners/dify.md). diff --git a/docs/snapshots/v4.23.6/en/providers/llm.md b/docs/snapshots/v4.23.6/en/providers/llm.md new file mode 100644 index 0000000..80f1a1f --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/llm.md @@ -0,0 +1,13 @@ +# 大语言模型提供商 + +你可在管理面板->服务提供商->+新增服务提供商 处配置各种大语言模型服务。 + +> [!TIP] +> 如果没有你希望接入的模型服务,你可以试着查看您希望接入的服务提供商处是否支持 兼容 OpenAI API,如果支持,那么你可以选择上面截图中的第一项 `OpenAI` 然后通过修改 API Base URL 的方式接入。 + +![image](https://files.astrbot.app/docs/source/images/llm/image.png) + +![image](https://files.astrbot.app/docs/source/images/llm/image-1.png) + + +> 相应的配置保存在 `data/cmd_config.json` 的 `provider` 字段中。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/en/providers/newapi.md b/docs/snapshots/v4.23.6/en/providers/newapi.md new file mode 100644 index 0000000..2fa55e9 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/newapi.md @@ -0,0 +1,40 @@ +# NewAPI + +[NewAPI](http://newapi.ai/) is a next-generation LLM gateway and AI asset management system built on top of One API. It provides a unified interface for managing and using multiple AI model services, including OpenAI, Anthropic, Gemini, Midjourney, and more. + +AstrBot can integrate with NewAPI as a model provider, so you can access those model services through AstrBot. + +## Setup Steps + +### 1. Create a NewAPI API Key + +After registering and signing in to NewAPI, open `Console` in the top navigation bar, go to `Token Management`, then click `Add Token` to create a new API key with appropriate permissions. + +![create-api-key](https://files.astrbot.app/docs/source/images/newapi/image.png) + +After creation, copy the generated API key. + +![copy-api-key](https://files.astrbot.app/docs/source/images/newapi/image-1.png) + +### 2. Configure NewAPI in AstrBot + +Open AstrBot WebUI, go to `Service Providers`, and click `Add Provider`. + +NewAPI fully supports OpenAI Chat Completion and Responses APIs, so select `OpenAI` and open its provider configuration. + +Set `API Base URL` to your NewAPI endpoint: + +- Self-hosted NewAPI example: `http://localhost:3000/v1` +- Hosted service example: `https://api.example.com/v1` + +Then paste your API key into `API Key` and click `Save`. + +![astrbot-provider-config](https://files.astrbot.app/docs/source/images/newapi/image-2.png) + +### 3. Apply the Provider + +Go to `Configuration`, find the model section, set `Default Chat Model` to the NewAPI-based provider you just created, and click `Save`. + +![apply](https://files.astrbot.app/docs/source/images/newapi/image-3.png) + +You have now successfully configured NewAPI as an AstrBot model provider. diff --git a/docs/snapshots/v4.23.6/en/providers/ppio.md b/docs/snapshots/v4.23.6/en/providers/ppio.md new file mode 100644 index 0000000..fceb61d --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/ppio.md @@ -0,0 +1,43 @@ +# 接入 PPIO 派欧云 + +PPIO 派欧云是中国领先的独立分布式云计算服务商,您可以在派欧云上使用稳定、低价甚至免费的模型服务。 + +## 准备 + +打开 [PPIO 派欧云官网](https://ppio.cn/user/register?invited_by=AIOONE),并注册账户(通过此链接注册的账户将会获得 15 元人民币的代金券)。 + +进入 [模型 API 服务](https://ppio.cn/model-api/console),找到你想接入的模型。你可以通过筛选器选择不同厂商或者免费的模型。 + +![image](https://files.astrbot.app/docs/source/images/ppio/image-1.png) + +找到你想要接入的模型后,点击模型卡片,侧边会展开一个模型详情卡片,找到下方的 API 接入指南,如果您还没创建过 Key 可以点击创建。 + +![image](https://files.astrbot.app/docs/source/images/ppio/image-3.png) + +打开 AstrBot 控制台 -> 服务提供商页面,点击新增提供商,找到并点击 `PPIO派欧云`(需要版本 >= 3.5.10,旧版本也可使用,见下文)。 + +![image](https://files.astrbot.app/docs/source/images/ppio/image.png) + +将 API Key 和模型名称填入对话框表单,点击保存,即可完成创建。 + +> [!TIP] +> 如果您是 AstrBot 旧版本(< 3.5.10)的用户,请打开 AstrBot 控制台 -> 服务提供商页面,点击新增提供商,找到 `OpenAI`,点击进入。 +> 1. 将 ID 命名为 `ppio`(随意) +> 2. 然后将 `API Base URL` 设置为 `https://api.ppinfra.com/v3/openai` +> 3. 然后将 API Key 和模型名称填入对话框表单,点击保存,即可完成创建。 + + +## 使用 + +对机器人输入 `/provider` 指令,将提供商切换到刚刚添加的 PPIO 派欧云提供商,即可使用。 + +## 常见问题 + +#### 显示 `400` 错误 + +```log +Error code: 400 - {'code': 400, 'message': '"auto" tool choice requires --enable-auto-tool-choice and --tool-call-parser to be set', 'type': 'BadRequestError'} +``` + + +请暂时使用 `/tool off_all` 禁用所有的函数调用工具即可使用,或者换用其他模型。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/en/providers/provider-lmstudio.md b/docs/snapshots/v4.23.6/en/providers/provider-lmstudio.md new file mode 100644 index 0000000..969f7d7 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/provider-lmstudio.md @@ -0,0 +1,38 @@ +# 接入 LM Studio 使用 DeepSeek-R1 等模型 + +LMStudio 允许在本地电脑上部署模型(需要电脑硬件配置符合要求) + +### 下载并安装 LMStudio + +https://lmstudio.ai/download + +### 下载并运行模型 + +https://lmstudio.ai/models + +跟随 LMStudio 下载并运行想要的模型,如 deepseek-r1-qwen-7b: + +```bash +lms get deepseek-r1-qwen-7b +``` + +### 配置 AstrBot + +在 AstrBot 上: + +点击 配置->服务提供商配置->加号->openai + +API Base URL 填写 `http://localhost:1234/v1` + +API Key 填写 `lm-studio` + +> 对于 Mac/Windows 使用 Docker Desktop 部署 AstrBot 部署的用户,API Base URL 请填写为 `http://host.docker.internal:1234/v1`。 +> 对于 Linux 使用 Docker 部署 AstrBot 部署的用户,API Base URL 请填写为 `http://172.17.0.1:1234/v1`,或者将 `172.17.0.1` 替换为你的公网 IP IP(确保宿主机系统放行了 1234 端口)。 + +如果 LM Studio 使用了 Docker 部署,请确保 1234 端口已经映射到宿主机。 + +模型名填写上一步选好的 + +保存配置即可。 + +> 输入 /provider 查看 AstrBot 配置的模型 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/en/providers/provider-ollama.md b/docs/snapshots/v4.23.6/en/providers/provider-ollama.md new file mode 100644 index 0000000..b3fc172 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/provider-ollama.md @@ -0,0 +1,46 @@ +# Integrating Ollama + +🦙 Ollama is a free, open-source tool that lets you run large language models (LLMs) on your own computer. (hardware must meet requirements) + +## Download and Install Ollama + +You can download Ollama from [https://ollama.com](https://ollama.com/download). + +## Select and Pull a Model + +Choose the model you want to use at [https://ollama.com/search](https://ollama.com/search). + +In the terminal (PowerShell on Windows), enter `ollama pull ` to download the model. + +model_name format: `:`. For example, `deepseek-r1:8b`. +> The 8b parameter model requires at least 16GB of video memory (VRAM). Refer to other documentation for detailed information on configurations and parameter sizes. + +After pulling is complete, use `ollama list` to view the models you have pulled. + +Then use `ollama run ` to run the model. + +## Configure AstrBot + +Open the AstrBot WebUI, locate Service Provider Management, click on Add Provider, find and click on `Ollama`. +![image](https://files.astrbot.app/docs/source/images/ollama/image.png) + +Save the configuration. + +::: tip + +For Mac/Windows users deploying AstrBot with Docker Desktop, enter `http://host.docker.internal:11434/v1` for the API Base URL.\ +For Linux users deploying AstrBot with Docker, enter `http://172.17.0.1:11434/v1` for the API Base URL, or replace `172.17.0.1` with your public IP address (ensure that port 11434 is allowed by the host system).\ +If Ollama is deployed using Docker, ensure that port 11434 is mapped to the host. + +::: + +## FAQ + +Error: +``` +AstrBot request failed. +Error type: NotFoundError +Error message: Error code: 404 - {'error': {'message': 'model "llama3.1-8b" not found, try pulling it first', 'type': 'api_error', 'param': None, 'code': None}} + +``` +Please refer to the instructions above and use `ollama pull ` to pull the model, then use `ollama run ` to run the model. diff --git a/docs/snapshots/v4.23.6/en/providers/siliconflow.md b/docs/snapshots/v4.23.6/en/providers/siliconflow.md new file mode 100644 index 0000000..b986b02 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/siliconflow.md @@ -0,0 +1,15 @@ +# Connecting to SiliconFlow + +SiliconFlow leverages its proprietary inference engine to deliver efficient acceleration for large language model inference. It provides high-performance, cost-effective API services for a wide range of large models with pay-as-you-go pricing, making application development a breeze. + +## Configuring the Chat Model + +Navigate to the SiliconFlow [API Keys](https://cloud.siliconflow.cn/me/account/ak) page and create a new API Key. Save it for later use. + +Visit the SiliconFlow [Models page](https://cloud.siliconflow.cn/me/models) to select your desired model. Note down the model name for later use. + +Open the AstrBot WebUI, click `Service Providers` in the left sidebar -> `Add Provider` -> select `SiliconFlow`. + +Paste the `API Key` and `Model Name` you obtained earlier, then click Save to complete the setup. You can click the `Refresh` button under `Service Provider Availability` to verify whether the configuration is successful. + +![Configuring Chat Model](https://files.astrbot.app/docs/source/images/siliconflow/image.png) diff --git a/docs/snapshots/v4.23.6/en/providers/start.md b/docs/snapshots/v4.23.6/en/providers/start.md new file mode 100644 index 0000000..3cc5fe9 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/start.md @@ -0,0 +1,41 @@ +# Connecting Model Services + +AstrBot supports the native API formats of OpenAI, Google GenAI, and Anthropic. You can connect any model service provider that conforms to one of these three API formats. + +> [!NOTE] +> If you are located in mainland China, we strongly recommend using **official model providers** or compliant providers that follow local laws and regulations, for example: +> +> - [MoonshotAI](https://moonshot.cn/) +> - [GLM](https://bigmodel.cn/) +> - [MiniMax](https://www.minimax.io/) +> - [Qwen](https://qwen.ai/apiplatform) +> - [DeepSeek](https://deepseek.com/) +> +> These providers support the OpenAI API format. You can find the API Base URL and API Key from their documentation and fill them into AstrBot provider settings. +> +> Please note that using non-compliant third-party model services may introduce availability, privacy, or legal risks. For details, see the [EULA](https://github.com/AstrBotDevs/AstrBot/blob/master/EULA.md). + +For example, you may choose to connect model services provided by (but not limited to): + +- Official OpenAI model services ([OpenAI](https://openai.com/)) +- Official Anthropic model services ([Anthropic](https://www.anthropic.com/)) +- Google's Gemini model services via Google Cloud ([Google Cloud](https://cloud.google.com/)) +- OpenRouter model services ([OpenRouter](https://openrouter.ai/)) + +## Integration Steps Using DeepSeek as an Example + +Using DeepSeek as an example, assuming you have registered and logged in to a DeepSeek account, the steps to connect are: + +1. Go to the DeepSeek Console (https://platform.deepseek.com/). +2. Click the "API Keys" menu in the left sidebar, create a new API Key, and copy the key. +3. Click the "API Documentation" link near the bottom of the left sidebar to open the API documentation page. +4. On the API documentation page, find the section about the "OpenAI-compatible interface" and note the API Base URL, for example `https://api.deepseek.com/v1`. (If there is no `/v1`, please add `/v1`.) +5. Open the AstrBot Console -> Service Providers page, click Add Provider, find and click `OpenAI` (if the provider type you want to connect is listed, prefer clicking that type; for some providers like DeepSeek we provide optimized adapter support). Paste the API Key into the `API Key` field of the form and paste the API Base URL into the `API Base URL` field. +6. Click Get Model List, find the model you want to use, click the + button on the right, then toggle the switch that appears on the right to enable it. +7. Go to the Configuration page, find the conversational model, click the selection button on the right, choose the provider and model you just added, then click the Save Configuration button at the bottom-right of the screen. + +## Using Environment Variables to Load Keys + +> Introduced in v4.13.0. + +You can use environment variables to load provider API keys. In the provider configuration page, set the API Key field to `$ENV_VARIABLE_NAME`, for example: `$DEESEEK_API_KEY`. diff --git a/docs/snapshots/v4.23.6/en/providers/tokenpony.md b/docs/snapshots/v4.23.6/en/providers/tokenpony.md new file mode 100644 index 0000000..4a65ce1 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/providers/tokenpony.md @@ -0,0 +1,23 @@ +# Connecting to TokenPony + +## Configuring the Chat Model + +Register and log in to [TokenPony](https://www.tokenpony.cn/3YPyf). + +Navigate to the TokenPony [API Keys](https://www.tokenpony.cn/#/user/keys) page and create a new API Key. Save it for later use. + +Visit the TokenPony [Models page](https://www.tokenpony.cn/#/model) to select your desired model. Note down the model name for later use. + +Open the AstrBot WebUI, click `Service Providers` in the left sidebar -> `Add Provider` -> select `TokenPony` (requires version >= 4.3.3) + +![Configuring Chat Model](https://files.astrbot.app/docs/source/images/tokenpony/image.png) + +> If you don't see the `TokenPony` option, you can also click `Connect to OpenAI` as shown in the image and change the `API Base URL` to `https://api.tokenpony.cn/v1`. + +Paste the `API Key` and `Model Name` you obtained earlier, then click Save to complete the setup. You can click the `Refresh` button under `Service Provider Availability` to verify whether the configuration is successful. + +## Applying the Chat Model + +In the AstrBot WebUI, click `Configuration` in the left sidebar, find `Default Chat Model` under AI Configuration, select the `tokenpony` (TokenPony) provider you just created, and click Save. + +![Configuring Chat Model 2](https://files.astrbot.app/docs/source/images/tokenpony/image_1.png) diff --git a/docs/snapshots/v4.23.6/en/use/agent-runner.md b/docs/snapshots/v4.23.6/en/use/agent-runner.md new file mode 100644 index 0000000..95a85a9 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/agent-runner.md @@ -0,0 +1,52 @@ +# Agent Runner + +The Agent Runner is a component in AstrBot used to execute Agents. + +Starting from version v4.7.0, we have migrated three providers—Dify, Coze, and Alibaba Cloud Bailian Application—to the Agent Runner layer, reducing some conflicts with AstrBot's existing features. Rest assured, if you upgrade from an older version to v4.7.0, you don't need to take any action as AstrBot will automatically migrate for you. Later versions also added DeerFlow support as an Agent Runner provider. + +AstrBot currently supports five Agent Runners: + +- AstrBot Built-in Agent Runner +- Dify Agent Runner +- Coze Agent Runner +- Alibaba Cloud Bailian Application Agent Runner +- DeerFlow Agent Runner + +By default, the AstrBot Built-in Agent Runner is the default runner. + +## Why Abstract the Agent Runner + +In earlier versions, platforms with "built-in Agent capabilities" like Dify, Coze, and Alibaba Cloud Bailian Application were integrated into AstrBot as regular Chat Providers. In practice, we found that they are fundamentally different from traditional Chat Providers that "only handle text completion". Forcing them into the same layer caused many design and usage conflicts. Therefore, starting from v4.7.0, we abstracted them into independent Agent Runners. + +From an architectural perspective, you can understand it as: + +- Chat Provider is responsible for "talking"; +- Agent Runner is responsible for "thinking + doing". + +The Agent Runner calls the Chat Provider's interface and, based on the Chat Provider's response, performs multi-turn "perceive → plan → execute action → observe result → re-plan" loops. + +A Chat Provider is essentially a `single-turn completion interface`, taking prompt + conversation history + tool list as input and outputting model responses (text, tool call instructions, etc.). + +An Agent Runner is typically a `loop` that receives user intent, context, and environment state, makes plans based on strategy/model (Plan), selects and invokes tools (Act), reads results from the environment (Observe), understands the results again, updates internal state, decides the next action, and repeats this process until the task is completed or times out. + +![image](https://files.astrbot.app/docs/source/images/use/agent-runner/agent-arch.svg) + +Platforms like Dify, Coze, Bailian Application, and DeerFlow have this loop built-in. If you treat them as regular Chat Providers, it will conflict with AstrBot's built-in Agent Runner functionality. + +## Usage + +By default, the AstrBot Built-in Agent Runner is the default runner. Using the default runner can already meet most needs, and you can use AstrBot's MCP, knowledge base, web search, and other features. + +If you need to use the capabilities of platforms like Dify, Coze, Bailian Application, or DeerFlow, you can create an Agent Runner and select the corresponding provider. + +## Creating an Agent Runner + +![image](https://files.astrbot.app/docs/source/images/use/agent-runner/image-1.png) + +In the WebUI, click "Model Provider" -> "Add Provider", select "Agent Runner", choose the platform or runner type you want to connect to, and fill in the relevant information. + +## Changing the Default Agent Runner + +![image](https://files.astrbot.app/docs/source/images/use/agent-runner/image.png) + +In the WebUI, click "Configuration" -> "Agent Execution Method", change the runner type to the Agent Runner type you just created, then select `XX Agent Runner Provider ID` as the ID of the Agent Runner provider you just created, and click save. diff --git a/docs/snapshots/v4.23.6/en/use/astrbot-agent-sandbox.md b/docs/snapshots/v4.23.6/en/use/astrbot-agent-sandbox.md new file mode 100644 index 0000000..775418c --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/astrbot-agent-sandbox.md @@ -0,0 +1,389 @@ +# Agent Sandbox Environment ⛵️ + +> [!TIP] +> This feature is currently in technical preview and may have some bugs. If you encounter any issues, please submit an issue on [GitHub](https://github.com/AstrBotDevs/AstrBot/issues). + +Starting from version `v4.12.0`, AstrBot introduced the Agent sandbox environment to replace the previous code executor functionality. The sandbox environment provides Agents with safer and more flexible code execution and automation capabilities. + +![](https://files.astrbot.app/docs/source/images/astrbot-agent-sandbox/image.png) + +## Enabling the Sandbox Environment + +AstrBot currently supports the following sandbox drivers: + +- `Shipyard Neo` (recommended) +- `Shipyard` (legacy option, still supported) + +In the current AstrBot console, go to **AI Settings** -> **Agent Computer Use** and select: + +- `Computer Use Runtime` = `sandbox` +- `Sandbox Driver` = `Shipyard Neo` or `Shipyard` + +`Shipyard Neo` is now the default driver. It consists of Bay, Ship, and Gull: + +- **Bay**: the control-plane API responsible for creating and managing sandboxes +- **Ship**: provides Python / Shell / filesystem capabilities +- **Gull**: provides browser automation capabilities + +For `Shipyard Neo`, the workspace root is fixed at `/workspace`. When using filesystem tools in AstrBot, you should pass **paths relative to the workspace root**, for example `reports/result.txt`, not `/workspace/reports/result.txt`. + +> [!TIP] +> Browser capability is not available in every `Shipyard Neo` profile. AstrBot only mounts browser-related tools when the selected profile supports the `browser` capability. A typical example is `browser-python`. + +## Performance Requirements + +AstrBot limits each sandbox instance to at most 1 CPU and 512 MB of memory. + +We recommend that your host machine have at least 2 CPUs, 4 GB of memory, and swap enabled, so multiple sandbox instances can run more reliably. + +## Recommended: Use Shipyard Neo + +### Deploy Shipyard Neo Separately (Recommended) + +If you plan to use `Shipyard Neo` for the long term, it is generally better to **deploy it separately on a machine with more resources**, such as your homelab, a LAN server, or a dedicated cloud host, and then let AstrBot connect to Bay remotely. + +The reason is that `Shipyard Neo` can become fairly resource-heavy when browser capability is enabled, because it needs to run a full browser runtime. On resource-constrained cloud servers, deploying AstrBot and `Shipyard Neo` on the same machine usually puts significant pressure on CPU and memory, which can negatively affect both stability and overall experience. + +A basic deployment flow looks like this: + +```bash +git clone https://github.com/AstrBotDevs/shipyard-neo +cd shipyard-neo/deploy/docker +# Modify the key settings in config.yaml, such as security.api_key +docker compose up -d +``` + +After deployment: + +- Bay listens on `http://:8114` by default +- In the AstrBot console, choose the `Shipyard Neo` driver +- Set `Shipyard Neo API Endpoint` to the corresponding address, for example `http://:8114` +- Set `Shipyard Neo Access Token` to the Bay API key; if AstrBot can access Bay's `credentials.json`, you may also leave it empty and let AstrBot auto-discover it + +### Reference: Full `config.yaml` Example (with Notes) + +If you want to customize the deployment parameters of `Shipyard Neo`, you can refer to the complete example below, adapted from [`deploy/docker/config.yaml`](https://github.com/AstrBotDevs/shipyard-neo/blob/main/deploy/docker/config.yaml). It keeps the default structure and adds explanatory notes to make each option easier to understand. + +> [!TIP] +> The minimum required change is `security.api_key`. If you are not sure what the other options do, it is usually best to keep the defaults first and only adjust profiles, resource limits, and warm pool settings as needed. + +```yaml +# Bay Production Config - Docker Compose (container_network mode) +# +# Bay runs inside Docker and communicates with Ship/Gull containers +# through a shared Docker network. +# In this mode, sandbox containers do not need to expose ports to the host. +# +# At minimum, update: +# 1. security.api_key — set a strong random secret + +server: + # Bay API listen address + host: "0.0.0.0" + # Bay API listen port + port: 8114 + +database: + # SQLite is the default for single-node deployment. + # For multi-instance / HA deployments, you can switch to PostgreSQL, for example: + # url: "postgresql+asyncpg://user:pass@db-host:5432/bay" + url: "sqlite+aiosqlite:///./data/bay.db" + echo: false + +driver: + # Docker is the default driver + type: docker + + # Whether to pull images when creating new sandboxes. + # In production, always is usually recommended so you get the latest images. + image_pull_policy: always + + docker: + # Docker Socket endpoint + socket: "unix:///var/run/docker.sock" + + # When Bay, Ship, and Gull all run in containers, + # container_network is recommended for direct container-network communication. + connect_mode: container_network + + # Shared network name; must match the network in docker-compose.yaml + network: "bay-network" + + # Whether to expose sandbox container ports to the host. + # Disabling this is generally recommended in production. + publish_ports: false + host_port: null + +cargo: + # Cargo storage root path on the Bay side + root_path: "/var/lib/bay/cargos" + # Default workspace size limit (MB) + default_size_limit_mb: 1024 + # Path mounted inside the sandbox. This is AstrBot/Neo's workspace root. + mount_path: "/workspace" + +security: + # Required: set a strong random secret, for example openssl rand -hex 32 + api_key: "CHANGE-ME" + # Whether anonymous access is allowed. false is recommended for production. + allow_anonymous: false + +# Proxy environment variable injection for containers. +# When enabled, Bay injects HTTP(S)_PROXY and NO_PROXY into sandbox containers. +proxy: + enabled: false + # http_proxy: "http://proxy.example.com:7890" + # https_proxy: "http://proxy.example.com:7890" + # no_proxy: "my-internal.service" + +# Warm Pool: keep standby sandboxes pre-warmed to reduce cold-start latency. +# When a user creates a sandbox, Bay will first try to claim a pre-warmed instance. +warm_pool: + enabled: true + # Number of warmup queue workers + warmup_queue_workers: 2 + # Maximum warmup queue size + warmup_queue_max_size: 256 + # Policy when the queue is full + warmup_queue_drop_policy: "drop_newest" + # Useful threshold for operational alerts + warmup_queue_drop_alert_threshold: 50 + # Warm pool maintenance interval (seconds) + interval_seconds: 30 + # Whether to start warm-pool maintenance when Bay starts + run_on_startup: true + +profiles: + # ── Standard Python sandbox ──────────────────────── + - id: python-default + description: "Standard Python sandbox with filesystem and shell access" + image: "ghcr.io/astrbotdevs/shipyard-neo-ship:latest" + runtime_type: ship + runtime_port: 8123 + resources: + cpus: 1.0 + memory: "1g" + capabilities: + - filesystem # includes upload/download + - shell + - python + # Idle timeout (seconds) + idle_timeout: 1800 + # Keep 1 warm instance ready + warm_pool_size: 1 + env: {} + # Optional profile-level proxy override + # proxy: + # enabled: false + + # ── Data-science sandbox (more resources) ────────── + - id: python-data + description: "Data science sandbox with extra CPU and memory" + image: "ghcr.io/astrbotdevs/shipyard-neo-ship:latest" + runtime_type: ship + runtime_port: 8123 + resources: + cpus: 2.0 + memory: "4g" + capabilities: + - filesystem # includes upload/download + - shell + - python + idle_timeout: 1800 + warm_pool_size: 1 + env: {} + + # ── Browser + Python multi-container sandbox ─────── + - id: browser-python + description: "Browser automation with Python backend" + containers: + - name: ship + image: "ghcr.io/astrbotdevs/shipyard-neo-ship:latest" + runtime_type: ship + runtime_port: 8123 + resources: + cpus: 1.0 + memory: "1g" + capabilities: + - python + - shell + - filesystem # includes upload/download + # These capabilities are primarily handled by the ship container + primary_for: + - filesystem + - python + - shell + env: {} + - name: browser + image: "ghcr.io/astrbotdevs/shipyard-neo-gull:latest" + runtime_type: gull + runtime_port: 8115 + resources: + cpus: 1.0 + memory: "2g" + capabilities: + - browser + env: {} + idle_timeout: 1800 + warm_pool_size: 1 + +gc: + # Automatic GC is recommended in production + enabled: true + run_on_startup: true + # GC interval (seconds) + interval_seconds: 300 + + # Must be unique in multi-instance deployments + instance_id: "bay-prod" + + idle_session: + enabled: true + expired_sandbox: + enabled: true + orphan_cargo: + enabled: true + orphan_container: + # Recommended in production to clean up leaked containers + enabled: true +``` + +A practical way to think about this file: + +- **Minimum required change**: `security.api_key` +- **Most commonly adjusted options**: resource limits, `warm_pool_size`, and `idle_timeout` under `profiles` +- **If you need browser capability**: use or customize the `browser-python` profile +- **If you want to reduce cold-start time**: keep `warm_pool.enabled: true` and increase `warm_pool_size` for frequently used profiles +- **If resources are limited**: reduce `warm_pool_size`, or even disable `warm_pool` +- **If outbound proxy access is needed**: configure the top-level `proxy`, or override it per profile + +### About Shipyard Neo Reuse and Persistence + +`Shipyard Neo` has several important concepts: + +- **Sandbox**: the stable, externally visible resource unit +- **Session**: the actual running container session, which may be stopped or rebuilt +- **Cargo**: the persistent workspace volume mounted at `/workspace` + +From AstrBot's perspective, the current implementation caches the sandbox booter by request `session_id`; in the default main-agent flow, this `session_id` usually equals the message-session identifier `unified_msg_origin`. As a result, follow-up requests from the same message session will usually continue using the same Neo sandbox; if the sandbox becomes unavailable, it will be rebuilt automatically. + +For more detailed explanations of TTL and persistence behavior, see the later sections on “`Shipyard Neo Sandbox TTL`” and “Data Persistence in the Sandbox Environment”. + +## Legacy Option: Shipyard + +The following content describes the older `Shipyard` driver. It is kept for compatibility with existing legacy deployments. + +### Deploying AstrBot and Shipyard with Docker Compose + +If you have not deployed AstrBot yet, or want to use the older recommended deployment method with sandbox support, you can still deploy AstrBot with Docker Compose using the following commands: + +```bash +git clone https://github.com/AstrBotDevs/AstrBot +cd AstrBot +# Modify the environment variables in compose-with-shipyard.yml, such as the Shipyard access token +docker compose -f compose-with-shipyard.yml up -d +docker pull soulter/shipyard-ship:latest +``` + +This starts a Docker Compose stack containing the AstrBot main program and the sandbox environment. + +### Deploying Shipyard Separately + +If AstrBot is already deployed but the sandbox environment is not, you can deploy Shipyard separately. + +```bash +mkdir astrbot-shipyard +cd astrbot-shipyard +wget https://raw.githubusercontent.com/AstrBotDevs/shipyard/refs/heads/main/pkgs/bay/docker-compose.yml -O docker-compose.yml +# Modify the environment variables in docker-compose.yml, such as the Shipyard access token +docker compose -f docker-compose.yml up -d +docker pull soulter/shipyard-ship:latest +``` + +After successful deployment, Shipyard listens on `http://:8156` by default. + +> [!TIP] +> If you deploy AstrBot with Docker, you can also place Shipyard on the same Docker network as AstrBot so you do not need to expose Shipyard's port to the host. + +## Configuring AstrBot to Use the Sandbox Environment + +> [!TIP] +> Please make sure your AstrBot version is `v4.12.0` or later. + +In the AstrBot console, go to **AI Settings** -> **Agent Computer Use**. + +1. Set `Computer Use Runtime` to `sandbox` +2. Select `Shipyard Neo` or `Shipyard` as the sandbox driver +3. Fill in the corresponding configuration values for the selected driver +4. Click **Save** + +### Configuring Shipyard Neo + +If you choose `Shipyard Neo`, the main configuration items are: + +- `Shipyard Neo API Endpoint` + - For a separated deployment, use the actual address, such as `http://:8114` +- `Shipyard Neo Access Token` + - Fill in the Bay API key + - If AstrBot can access Bay's `credentials.json`, you may leave it empty and let AstrBot auto-discover it +- `Shipyard Neo Profile` + - For example `python-default` or `browser-python` + - If not explicitly specified, AstrBot will try to choose a profile with richer capabilities, preferring one that includes the `browser` capability, and fall back to `python-default` if needed +- `Shipyard Neo Sandbox TTL` + - The upper lifetime limit of the sandbox, defaulting to 3600 seconds (1 hour) + +### Configuring Shipyard (Legacy) + +If you choose the legacy `Shipyard` driver, the relevant configuration items are: + +- `Shipyard API Endpoint` + - If you use the Docker Compose deployment above, set it to `http://shipyard:8156` + - If Shipyard is deployed separately, use the corresponding address, such as `http://:8156` +- `Shipyard Access Token` + - Fill in the access token you configured when deploying Shipyard +- `Shipyard Ship Lifetime (seconds)` + - Defines the lifetime of each sandbox instance, default 3600 seconds (1 hour) +- `Shipyard Ship Session Reuse Limit` + - Defines the maximum number of sessions that can reuse the same sandbox instance, default 10 + +## About `Shipyard Neo Sandbox TTL` + +In `Shipyard Neo`: + +- TTL represents the upper lifetime bound of the sandbox +- The selected profile also defines a separate idle timeout (`idle_timeout`) +- Capability calls from AstrBot usually refresh the idle timeout, rather than directly extending the TTL +- `keepalive` only extends the idle timeout; it does not automatically start a new session and does not extend the TTL + +## About `Shipyard Ship Lifetime (seconds)` + +The following explanation applies only to the legacy `Shipyard` driver: + +The lifetime of a sandbox instance defines the maximum amount of time that instance can exist before being destroyed. This value should be chosen according to your use case and available resources. + +- When a new session joins an existing sandbox instance, the instance automatically extends its lifetime to the TTL requested by that session +- When an operation is performed on a sandbox instance, the instance automatically extends its lifetime to the current time plus TTL + +## About Data Persistence in the Sandbox Environment + +### Shipyard Neo + +The workspace root of `Shipyard Neo` is fixed at `/workspace`. + +Persistence is provided by Cargo: + +- Filesystem data is stored in Cargo and mounted at `/workspace` +- Even if the underlying Session is stopped or rebuilt, the data in Cargo is usually retained +- For profiles with browser capability, browser state may also be persisted together, for example under `/workspace/.browser/profile/` + +### Shipyard (Legacy) + +Shipyard allocates a working directory for each session under `/home/`. + +Shipyard automatically mounts the `/home` directory from the sandbox environment to `${PWD}/data/shipyard/ship_mnt_data` on the host. When a sandbox instance is destroyed and a session later requests the sandbox again, Shipyard recreates a new instance and remounts the previously persisted data to preserve continuity. + +## Other Community Plugins + +### luosheng520qaq/astrobot_plugin_code_executor + +If your resources are limited and you do not want to use the sandbox environment for code execution, you can try the [astrobot_plugin_code_executor](https://github.com/luosheng520qaq/astrobot_plugin_code_executor) plugin developed by luosheng520qaq. This plugin executes code directly on the host machine. It tries to improve safety as much as possible, but you should still pay close attention to code-execution security. diff --git a/docs/snapshots/v4.23.6/en/use/astrbot-sandbox.md b/docs/snapshots/v4.23.6/en/use/astrbot-sandbox.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/snapshots/v4.23.6/en/use/code-interpreter.md b/docs/snapshots/v4.23.6/en/use/code-interpreter.md new file mode 100644 index 0000000..4b7a550 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/code-interpreter.md @@ -0,0 +1,96 @@ +# Docker-based Code Interpreter + +> [!WARNING] +> Deprecated, please refer to the latest [Agent Sandbox Environment](/en/use/astrbot-agent-sandbox.md) documentation. This feature will be unavailable after v4.12.0. + +Starting from version `v3.4.2`, AstrBot supports a code interpreter to enhance LLM capabilities and enable various automated operations. + +> [!TIP] +> This feature is currently in experimental stage and may have some issues. If you encounter any problems, please submit an issue on [GitHub](https://github.com/AstrBotDevs/AstrBot/issues). Join our discussion group: [322154837](https://qm.qq.com/cgi-bin/qm/qr?k=EYGsuUTfe00_iOu9JTXS7_TEpMkXOvwv&jump_from=webapi&authKey=uUEMKCROfsseS+8IzqPjzV3y1tzy4AkykwTib2jNkOFdzezF9s9XknqnIaf3CDft). + +To use this feature, ensure that `Docker` is installed on your machine. This feature requires a dedicated Docker sandbox environment to execute code and prevent malicious code generated by the LLM from harming your machine. + + +## Running AstrBot with Docker on Linux + +If you've deployed AstrBot using Docker, some additional setup is required. + +1. When starting the Docker container, mount `/var/run/docker.sock` inside the container. This allows AstrBot to launch sandbox containers. + +```bash +sudo docker run -itd -p 6180-6200:6180-6200 -p 11451:11451 -v $PWD/data:/AstrBot/data -v /var/run/docker.sock:/var/run/docker.sock --name astrbot soulter/astrbot:latest +``` + +2. Use the `/pi absdir ` command during chat to set the absolute path of AstrBot's data directory on your host machine. + +Example: + +![image](https://files.astrbot.app/docs/source/images/code-interpreter/image-4.png) + +## Running AstrBot from Source on Linux + +**If your Docker commands require sudo privileges**, you need to start AstrBot with `sudo`, otherwise the code interpreter won't be able to invoke Docker due to insufficient permissions. + +```bash +sudo -E python3 main.py +``` + +## Usage + +This feature uses the `soulter/astrbot-code-interpreter-sandbox` image. You can view detailed information about the image on [Docker Hub](https://hub.docker.com/r/soulter/astrbot-code-interpreter-sandbox). + +The image includes commonly used Python libraries: + +- Pillow +- requests +- numpy +- matplotlib +- scipy +- scikit-learn +- beautifulsoup4 +- pandas +- opencv-python +- python-docx +- python-pptx +- pymupdf +- mplfonts + +Tasks that can be accomplished include: + +- Image editing +- Web scraping +- Data analysis and simple machine learning +- Document processing, such as reading and writing Word, PPT, PDF files +- Mathematical calculations, such as plotting graphs and solving equations + +Since Docker Hub is inaccessible from mainland China, if you're in that region, use `/pi mirror` to view/set the mirror source. For example, as of this writing, you can use `cjie.eu.org` as the mirror source by setting `/pi mirror cjie.eu.org`. + +When the code interpreter is triggered for the first time, AstrBot will automatically pull the image, which may take some time. Please be patient. + +The image may be updated periodically to provide more features, so check for updates regularly. If you need to update the image, use the `/pi repull` command to re-pull it. + +> [!TIP] +> If the feature doesn't start properly initially, after successful startup, execute `/tool on python_interpreter` to enable this feature. +> You can use `/tool ls` to view all tools and their enabled status. + +![image](https://files.astrbot.app/docs/source/images/code-interpreter/image-3.png) + +## Image and File Input + +In addition to recognizing and processing images and text tasks, the code interpreter can also recognize files you send and send files back. + +After v3.4.34, use the `/pi file` command to start uploading files. After uploading, you can use `/pi list` to view your uploaded files and `/pi clean` to clear your uploaded files. + +Uploaded files will be used as input for the code interpreter. + +For example, if you want to add rounded corners to an image, you can upload the image using `/pi file`, then ask: `Please run code to add rounded corners to this image`. + +## Demo + +![image](https://files.astrbot.app/docs/source/images/code-interpreter/a3cd3a0e-aca5-41b2-aa52-66b568bd955b.png) + +![alt text](https://files.astrbot.app/docs/source/images/code-interpreter/image.png) + +![image](https://files.astrbot.app/docs/source/images/code-interpreter/image-1.png) + +![image](https://files.astrbot.app/docs/source/images/code-interpreter/image-2.png) diff --git a/docs/snapshots/v4.23.6/en/use/command.md b/docs/snapshots/v4.23.6/en/use/command.md new file mode 100644 index 0000000..74c9e0c --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/command.md @@ -0,0 +1,110 @@ +# Built-in Commands + +AstrBot commands are registered through the plugin system. To keep the core lightweight, only a small set of basic commands are loaded with AstrBot itself. Other management and extended commands have been moved into a separate plugin. + +Use `/help` to view currently enabled commands. + +> [!NOTE] +> 1. `/help`, `/set`, and `/unset` are not shown in the `/help` command list by default, but they are still available. +> 2. If you change the wake prefix and remove the default `/`, commands must use the new wake prefix as well. For example, after changing the wake prefix to `!`, use `!help` and `!reset` instead of `/help` and `/reset`. + +## Core Built-in Commands + +The following commands are shipped with AstrBot and loaded by default: + +- `/help`: View currently enabled commands and AstrBot version information. +- `/sid`: View current message source information, including UMO, user ID, platform ID, message type, and session ID. This is commonly used when configuring admins, allowlists, or routing rules. +- `/reset`: Reset the current conversation's LLM context. +- `/stop`: Stop Agent tasks currently running in the current session. +- `/new`: Create and switch to a new conversation. +- `/dashboard_update`: Update AstrBot WebUI. This command requires admin permission. +- `/set`: Set a session variable, commonly used for Agent Runner input variables such as Dify, Coze, or DashScope. +- `/unset`: Remove a session variable. + +These commands are located in: + +```text +astrbot/builtin_stars/builtin_commands +``` + +## Core Command Details + +### `/sid` + +`/sid` shows information about the current message source. It mainly returns: + +- `UMO`: The unified message origin of the current message. It is commonly used for allowlists and per-session config routing. +- `UID`: The sender's user ID. It is commonly used when adding AstrBot admins. +- `Bot ID`: The platform instance ID of the current bot. +- `Message Type`: The message type, such as private chat or group chat. +- `Session ID`: The platform-side session ID. + +In group chats, if `unique_session` is enabled, `/sid` also shows the current group ID. This group ID can be used to allowlist the entire group. + +Common uses: + +- Add an admin: run `/sid` to get the `UID`, then add it in WebUI under `Config -> Other Config -> Admin ID`. +- Configure allowlists: use `UMO` or group ID to control which sessions can use the bot. +- Configure routing rules: use `UMO` to distinguish different platforms, groups, or private chats. + +### `/reset` + +`/reset` resets the LLM context of the current session. + +For AstrBot's built-in Agent Runner, it: + +- Stops running tasks in the current session. +- Clears the context messages of the current conversation. +- Notifies long-term memory to clear the current session state. + +For third-party Agent Runners such as `dify`, `coze`, `dashscope`, and `deerflow`, it: + +- Stops running tasks in the current session. +- Removes the saved third-party conversation ID for this session, so the next turn starts a new conversation. + +Permission notes: + +- In private chat, regular users can use it by default. +- In group chat with `unique_session` enabled, regular users can use it by default. +- In group chat without `unique_session`, admin permission is required by default. +- If command permission settings have been customized, the actual configuration takes precedence. + +### `/stop` + +`/stop` stops Agent tasks currently running in the current session. + +It does not clear conversation history and does not create a new conversation. It only sends a stop request to tasks currently executing in this session. + +For the built-in Agent Runner, `/stop` asks the Agent Runner to stop the current task. +For third-party Agent Runners such as `dify`, `coze`, `dashscope`, and `deerflow`, `/stop` directly stops registered running tasks in the current session. + +If there are no running tasks in the current session, AstrBot will report that no task is running. + +## Built-in Commands Extension + +Other commands that were previously shipped with the core have been moved to a separate plugin: + +- [builtin_commands_extension](https://github.com/AstrBotDevs/builtin_commands_extension) + +This plugin provides extended commands for plugin management, Provider management, model switching, Persona management, and conversation management. Examples include: + +- `/plugin`: View, enable, disable, or install plugins. +- `/op`, `/deop`: Add or remove admins. +- `/provider`: View or switch LLM Providers. +- `/model`: View or switch models. +- `/history`: View current conversation history. +- `/ls`: View the conversation list. +- `/groupnew`: Create a new conversation for a specified group. +- `/switch`: Switch to a specified conversation. +- `/rename`: Rename the current conversation. +- `/del`: Delete the current conversation. +- `/persona`: View or switch Persona. +- `/llm`: Enable or disable LLM chat. + +Install or enable the `builtin_commands_extension` plugin if you need these extended commands. + +## Permission Notes + +Some commands require AstrBot admin permission, such as `/dashboard_update`, `/op`, `/deop`, `/provider`, `/model`, and `/persona`. + +You can use `/sid` to get a user ID, then add it in WebUI under `Config -> Other Config -> Admin ID`. diff --git a/docs/snapshots/v4.23.6/en/use/computer.md b/docs/snapshots/v4.23.6/en/use/computer.md new file mode 100644 index 0000000..09fcd87 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/computer.md @@ -0,0 +1,139 @@ +# Computer Use + +Computer Use controls whether an Agent can execute code, access files, run Shell commands. + +## Mode Selection + +In WebUI, open: + +- `Config -> General Config -> Use Computer Capabilities` + +The key option is `Computer Use Runtime`: + +- `none`: disables Computer Use; Shell, Python, filesystem, and related tools are not mounted. +- `local`: executes on the host machine where AstrBot is running. Use this when the Agent needs local files, command-line tools, or local dependencies. +- `sandbox`: executes inside an isolated sandbox. Use this when you want to reduce host risk or provide automation capabilities to multiple users. + +If you are not sure which mode to choose, prefer `sandbox`. Use `local` only when direct host access is required. + +## Local Mode + +`local` mode mounts Computer Use tools into the host environment where AstrBot runs. The Agent can call the host Shell, host Python, and host filesystem tools. + +This means the Agent's boundary is close to the AstrBot process itself. What it can access depends on the system permissions, runtime user, working directory, and operating-system restrictions of the AstrBot process. + +### Workspace + +In `local` mode, AstrBot prepares a workspace for each session: + +```text +data/workspaces/{normalized_umo} +``` + +`{normalized_umo}` is derived from the current session's `unified_msg_origin`; characters unsuitable for filenames are replaced with `_`. + +Relative paths passed to local filesystem tools are resolved under this workspace. For example: + +```text +notes/todo.txt +``` + +is resolved as: + +```text +data/workspaces/{normalized_umo}/notes/todo.txt +``` + +The local Shell tool also runs with this workspace as its current working directory. + +> [!NOTE] +> The local Python tool executes code through AstrBot's current Python environment. When Python code reads or writes files, use explicit absolute paths or prepare files through filesystem tools in the workspace first. + +### Local Tools + +`local` mode mainly provides: + +- `Shell`: executes host shell commands. Windows follows `cmd.exe` semantics; Linux/macOS follow Unix-like shell semantics. +- `Python`: executes Python code in AstrBot's current Python environment. +- `File read`: reads text, image, spreadsheet, and other supported files. +- `File write`: writes UTF-8 text files; relative paths default to the current workspace. +- `File edit`: replaces exact text in files. +- `Grep search`: searches file contents through ripgrep. + +`local` mode does not mount sandbox upload/download tools, and it does not provide browser automation. Browser automation belongs to the sandbox runtime and requires a sandbox profile with the `browser` capability. + +The local Shell tool includes basic blocking for dangerous commands such as `rm -rf`, `sudo`, `shutdown`, `reboot`, and `kill -9`. This is not a complete security sandbox and should not be treated as one. + +### Permission Model + +Computer Use has a separate option: + +- `Require AstrBot admin permission` + +This option is enabled by default. + +When enabled: + +- Admin users can use Shell, Python, file read, file write, file edit, and Grep search in `local` mode. +- Non-admin users cannot use Shell or Python. +- Non-admin users can only use file read, write, edit, and search inside restricted directories. + +Allowed directories for non-admin users in `local` mode include: + +- `data/skills` +- Current session's `data/workspaces/{normalized_umo}` +- AstrBot temporary directories +- `.astrbot` under the system temporary directory + +If `Require AstrBot admin permission` is disabled, regular users behave much closer to admins for Computer Use tools. Do not disable it unless you understand the risk. + +Admin IDs can be configured in: + +- `Config -> Other Config -> Admin ID` + +Users can get their own ID with `/sid`. + +## Sandbox Mode + +`sandbox` mode runs execution actions inside an isolated environment instead of directly on the AstrBot host. + +Inside the sandbox, the Agent can still use Shell, Python, and filesystem tools. If the selected sandbox profile supports the `browser` capability, AstrBot also mounts browser automation tools. + +With Shipyard Neo, the sandbox workspace root is usually: + +```text +/workspace +``` + +Filesystem tools should usually receive relative paths, for example: + +```text +result.txt +``` + +instead of: + +```text +/workspace/result.txt +``` + +For sandbox deployment, profiles, TTL, persistence, and browser capabilities, see [Agent Sandbox Environment](/en/use/astrbot-agent-sandbox). + +> [!NOTE] +> Even in `sandbox` mode, `Require AstrBot admin permission` still affects access to Shell, Python, browser, upload/download, and related tools. The exact behavior depends on your configuration. + +## Skills + +Skills are reusable instruction bundles for Agents. They are usually stored under `data/skills`, and each Skill contains a `SKILL.md`. + +The relationship between Skills and Computer Use is: + +- Skills tell the Agent what to do. +- Computer Use decides whether the Agent can execute those steps. + +For example, a Skill may ask the Agent to read files, run scripts, and generate a report. If `Computer Use Runtime` is `none`, the Agent may see the Skill instructions, but it cannot call Shell or Python to execute them. + +In `local` mode, the Agent reads local Skills. +In `sandbox` mode, AstrBot attempts to sync local Skills into the sandbox so the Agent can execute them there. + +For more details, see [Anthropic Skills](/en/use/skills). diff --git a/docs/snapshots/v4.23.6/en/use/context-compress.md b/docs/snapshots/v4.23.6/en/use/context-compress.md new file mode 100644 index 0000000..c5664e2 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/context-compress.md @@ -0,0 +1,40 @@ +# Context Compression + +Starting from v4.11.0, AstrBot introduced an automatic context compression feature. + +![alt text](https://files.astrbot.app/docs/source/images/context-compress/image.png) + +AstrBot automatically compresses the context when the conversation context **reaches 82% of the maximum context window length of the conversation model being used**, ensuring that as much conversation content as possible is retained without losing key information. + +## Compression Strategies + +There are currently two compression strategies: + +1. Truncate by conversation rounds. This strategy simply removes the earliest conversation content until the context length meets the requirements. You can specify the number of conversation rounds to discard at once, with a default of 1 round. This is the **default strategy**. +2. LLM-based context compression. This strategy calls the model itself to summarize and compress the conversation content, thereby retaining more key information. You can specify the conversation model to use for compression; if not selected, it will automatically fall back to the "truncate by conversation rounds" strategy. You can set the number of recent conversation rounds to retain during compression, with a default of 4. You can also customize the prompt used during compression. The default prompt is: + +``` +Based on our full conversation history, produce a concise summary of key takeaways and/or project progress. +1. Systematically cover all core topics discussed and the final conclusion/outcome for each; clearly highlight the latest primary focus. +2. If any tools were used, summarize tool usage (total call count) and extract the most valuable insights from tool outputs. +3. If there was an initial user goal, state it first and describe the current progress/status. +4. Write the summary in the user's language. +``` + +After one round of compression, AstrBot will perform a secondary check to verify if the current context length meets the requirements. If it still doesn't meet the requirements, it will adopt a halving strategy, cutting the current context content in half until the requirements are met. + +- AstrBot will invoke the compressor for checking before each conversation request. +- In the current version, AstrBot does not perform context compression during tool invocations. We will support this feature in the future, so stay tuned. + +## ‼️ Important: Model Context Window Settings + +By default, when you add a model, AstrBot automatically retrieves the model's context window size from the API provided by [MODELS.DEV](https://models.dev/) based on the model's ID. However, due to the wide variety of models and the fact that some providers even modify the model IDs, AstrBot cannot automatically infer the context window size for all models you add. + +You can manually set the model's context window size in the model configuration, as shown in the image below: + +![alt text](https://files.astrbot.app/docs/source/images/context-compress/image1.png) + +> [!NOTE] +> If you don't see the configuration option shown in the image above, please delete the model and re-add it. + +When the model context window size is set to 0, AstrBot will still automatically retrieve the model's context window size from MODELS.DEV for each request. If it remains 0, context compression will not be enabled for that request. diff --git a/docs/snapshots/v4.23.6/en/use/custom-rules.md b/docs/snapshots/v4.23.6/en/use/custom-rules.md new file mode 100644 index 0000000..7545da1 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/custom-rules.md @@ -0,0 +1,17 @@ +# Custom Rules + +> [!NOTE] +> The "unified message origin" mentioned below refers to UMO. A UMO uniquely identifies a specific conversation on a messaging platform. + +Since version v4.7.0, we have refactored AstrBot's original "Session Management" feature into the "Custom Rules" feature to reduce conflicts with configuration files. + +You can think of custom rules as more flexible, mandatory processing rules for specified message sources, which have higher priority than configuration files. + +For example, if a messaging platform originally uses the "default" configuration file, all conversations under this platform are processed according to the rules in the configuration file. If you want to apply special processing to a specific session source A, previously you would need to create a separate configuration file and bind A to it. Now, you simply need to create a custom rule in the WebUI's Custom Rules page and select message source A. You can define the following rules: + +1. Whether to enable message processing for this unified message origin. If disabled, the effect is equivalent to blacklisting this unified message origin. +2. Whether to enable LLM for messages from this unified message origin. If disabled, AI capabilities will not be used. +3. Whether to enable TTS for messages from this unified message origin. If disabled, TTS capabilities will not be used. +4. Configure specific chat models, speech recognition models (STT), and text-to-speech models (TTS) for this unified message origin. +5. Configure a specific persona for this unified message origin. + diff --git a/docs/snapshots/v4.23.6/en/use/function-calling.md b/docs/snapshots/v4.23.6/en/use/function-calling.md new file mode 100644 index 0000000..1526cba --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/function-calling.md @@ -0,0 +1,54 @@ +--- +outline: deep +--- + +# Function Calling + +## Introduction + +Function calling aims to provide large language models with **the ability to invoke external tools**, enabling various Agentic functionalities. + +For example, when you ask the LLM: "Help me search for information about cats", the model will call external search tools, such as search engines, and return the search results. + +Here is the revised text, updated to reflect your new content while maintaining a formal documentation tone: + +Currently, supported models include but are not limited to: + +- GPT-5.x series +- Gemini 3.x series +- Claude 4.x series +- DeepSeek v3.2 (deepseek-chat) +- Qwen 3.x series + +Mainstream models released after 2025 typically support function calling. + +Commonly unsupported models include older models such as DeepSeek-R1 and Gemini 2.0 thinking-type models. + +In AstrBot, web search, todo reminders, and code interpreter tools are provided by default. Many plugins, such as: + +- astrbot_plugin_cloudmusic +- astrbot_plugin_bilibili +- ... + +In addition to providing traditional command invocation, also offer function calling capabilities. + +Related commands: + +- `/tool ls` - View the list of available tools +- `/tool on` - Enable a specific tool +- `/tool off` - Disable a specific tool +- `/tool off_all` - Disable all tools + +Some models may not support function calling and will return errors such as `tool call is not supported`, `function calling is not supported`, `tool use is not supported`, etc. In most cases, AstrBot can detect these errors and automatically remove function calling tools for you. If you find that a model doesn't support function calling, you can also use the `/tool off_all` command to disable all tools and try again, or switch to a model that supports function calling. + + +Below are some common tool calling demos: + +![image](https://files.astrbot.app/docs/source/images/function-calling/image.png) + +![image](https://files.astrbot.app/docs/source/images/function-calling/image-1.png) + + +## MCP + +Please refer to this documentation: [AstrBot - MCP](/use/mcp). diff --git a/docs/snapshots/v4.23.6/en/use/knowledge-base.md b/docs/snapshots/v4.23.6/en/use/knowledge-base.md new file mode 100644 index 0000000..b1f9e1d --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/knowledge-base.md @@ -0,0 +1,59 @@ + +# AstrBot Knowledge Base + +> [!TIP] +> Requires AstrBot version >= 4.5.0. + +![Knowledge Base Preview](https://files.astrbot.app/docs/en/use/image-3.png) + +## Configuring Embedding Model + +Open the service provider page, click "Add Service Provider", and select Embedding. + +Currently, AstrBot supports embedding vector services compatible with OpenAI API and Gemini API. + +Click on the provider card above to enter the configuration page and fill in the configuration. + +After completing the configuration, click Save. + +## Configuring Reranker Model (Optional) + +A reranker model can improve the precision of final retrieval results to some extent. + +Similar to configuring the embedding model, open the service provider page, click "Add Service Provider", and select Reranker. For more information about reranker models, please refer to online resources. + +## Creating a Knowledge Base + +AstrBot supports multiple knowledge base management. During chat, you can **freely specify which knowledge base to use**. + +Enter the knowledge base page and click "Create Knowledge Base", as shown below: + +![image](https://files.astrbot.app/docs/source/images/knowledge-base/image.png) + +Fill in the relevant information. In the embedding model dropdown menu, you will see the embedding model and reranker model you just created (reranker model is optional). + +> [!TIP] +> Once you've selected an embedding model for a knowledge base, do not modify the **model** or **vector dimension information** of that provider, as this will **seriously affect** the retrieval accuracy of the knowledge base or even **cause errors**. + +## Uploading Files + +After creating a knowledge base, you can upload documents to it. Up to 10 files can be uploaded simultaneously, with a maximum size of 128 MB per file. + +![Upload Files](https://files.astrbot.app/docs/en/use/image-4.png) + +## Using the Knowledge Base + +In the configuration file, you can specify different knowledge bases for different configuration profiles. + +## Appendix 2: Applying for Free Embedding Models + +### PPIO Cloud + +1. Open the [PPIO Cloud website](https://ppio.cn/user/register?invited_by=AIOONE) and register an account (accounts registered through this link will receive a 15 RMB voucher). +2. Go to the [Model Marketplace](https://ppio.cn/model-api/console) and click on Embedding Models. +3. Click on BAAI:BGE-M3 (as of 2025-06-02, this model is free on this platform). +4. Find the API integration guide and apply for a Key. +5. Fill in the AstrBot OpenAI Embedding model provider configuration: + 1. API Key is the PPIO API Key you just applied for + 2. embedding api base: enter `https://api.ppinfra.com/v3/openai` + 3. model: enter the model you selected, in this example `baai/bge-m3`. diff --git a/docs/snapshots/v4.23.6/en/use/mcp.md b/docs/snapshots/v4.23.6/en/use/mcp.md new file mode 100644 index 0000000..1681d31 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/mcp.md @@ -0,0 +1,102 @@ + +# MCP + +MCP (Model Context Protocol) is a new open standard protocol for establishing secure bidirectional connections between large language models and data sources. Simply put, it extracts function tools as independent services, allowing AstrBot to remotely invoke these function tools via the MCP protocol, which then return results to AstrBot. + +![image](https://files.astrbot.app/docs/source/images/function-calling/image3.png) + +AstrBot v3.5.0 supports the MCP protocol, enabling you to add multiple MCP servers and use function tools from MCP servers. + +![image](https://files.astrbot.app/docs/source/images/function-calling/image2.png) + +## Initial Configuration + +MCP servers are typically launched using `uv` or `npm`, so you need to install these two tools. + +For `uv`, you can install it directly via pip. Quick installation via AstrBot WebUI: + +![image](https://files.astrbot.app/docs/en/use/image.png) + +Just enter `uv`. + +If you're deploying AstrBot with Docker, you can also execute the following command for quick installation: + +```bash +docker exec astrbot python -m pip install uv +``` + +If you're deploying AstrBot from source, please install it within the created virtual environment. + +For `npm`, you need to install `node`. + +If you're deploying AstrBot from source or using one-click installation, please refer to [Download Node.js](https://nodejs.org/en/download) to download to your local machine. + +If you're using Docker to deploy AstrBot, you need to install `node` in the container (future AstrBot Docker images will include `node` by default). Please execute the following commands: + +```bash +sudo docker exec -it astrbot /bin/bash +apt update && apt install curl -y +export NVM_NODEJS_ORG_MIRROR=http://nodejs.org/dist +# Download and install nvm: +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.2/install.sh | bash +\. "$HOME/.nvm/nvm.sh" +nvm install 22 +# Verify version: +node -v +nvm current +npm -v +npx -v +``` + +After installing `node`, you need to restart `AstrBot` to apply the new environment variables. + +## Installing MCP Servers + +If you're deploying AstrBot with Docker, please install MCP servers in the data directory. + +### An Example + +I want to install an MCP server for querying papers on Arxiv and found this repository: [arxiv-mcp-server](https://github.com/blazickjp/arxiv-mcp-server). Referring to its README, + +We extract the necessary information: + +```json +{ + "command": "uv", + "args": [ + "tool", + "run", + "arxiv-mcp-server", + "--storage-path", "data/arxiv" + ] +} +``` + +If the MCP server you need requires environment variables to configure something (e.g. access token), you could use the command-line tool `env`: + +```json +{ + "command": "env", + "args": [ + "XXX_RESOURCE_FROM=local", + "XXX_API_URL=https://xxx.com", + "XXX_API_TOKEN=sk-xxxxx", + "uv", + "tool", + "run", + "xxx-mcp-server", + "--storage-path", "data/res" + ] +} +``` + +Configure it in the AstrBot WebUI: + +![image](https://files.astrbot.app/docs/en/use/image-2.png) + +That's it. + +Reference links: + +1. Learn how to use MCP here: [Model Context Protocol](https://modelcontextprotocol.io/introduction) +2. Get commonly used MCP servers here: [awesome-mcp-servers](https://github.com/punkpeye/awesome-mcp-servers/blob/main/README-zh.md#what-is-mcp), [Model Context Protocol servers](https://github.com/modelcontextprotocol/servers), [MCP.so](https://mcp.so) diff --git a/docs/snapshots/v4.23.6/en/use/plugin.md b/docs/snapshots/v4.23.6/en/use/plugin.md new file mode 100644 index 0000000..194c6b4 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/plugin.md @@ -0,0 +1,7 @@ +# AstrBot Star + +Starting from version `3.4.0`, AstrBot renamed plugins to `Star`. AstrBot is a highly modular project, and plugins leverage this modularity to implement various functionalities. + +Use `/plugin` to view all plugins. You can also manage installed plugins in the admin panel. + +If you want to develop your own plugin, see [AstrBot Plugin Development Guide](/en/dev/star/plugin-new). diff --git a/docs/snapshots/v4.23.6/en/use/proactive-agent.md b/docs/snapshots/v4.23.6/en/use/proactive-agent.md new file mode 100644 index 0000000..72ff9cb --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/proactive-agent.md @@ -0,0 +1,52 @@ +# Proactive Capabilities + +AstrBot introduces a Proactive Agent system, enabling AstrBot to not only respond passively to users but also schedule future tasks and proactively execute them at specified times, delivering results (text, images, files, etc.) to users. + +![](https://files.astrbot.app/docs/source/images/proactive-agent/image.png) + +Introduced in v4.14.0, this is currently an **experimental feature** and not yet stable. + +## Future Tasks (FutureTask) + +The Main Agent can now manage a global **Cron Job List**, setting tasks for its future self. + +### Features + +- **Self-Wakeup**: AstrBot automatically wakes up at the scheduled time to execute tasks. +- **Task Feedback**: After execution, AstrBot reports the results back to the task creator. +- **WebUI Management**: You can view, edit, or delete scheduled tasks in the "Future Tasks" page of the WebUI. + +### How to Use + +> [!TIP] +> First, ensure that "Proactive Capabilities" is enabled in the configuration. + +The Main Agent has the ability to manage scheduled tasks. You can tell it: +- "Remind me to have a meeting at 8 AM tomorrow." +- "Summarize this week's work log every Friday at 5 PM." +- "Set a timer for 10 minutes." + +The Main Agent will call built-in scheduling tools to arrange these plans. + +You can view and manage all future tasks by clicking **Future Tasks** in the left navigation bar of the AstrBot WebUI. + +![](https://files.astrbot.app/docs/source/images/proactive-agent/image-1.png) + +### Supported Platforms + +Scheduling tasks is supported on all platforms. However, due to some platforms not providing APIs for proactive message pushing, only the following platforms support AstrBot proactively pushing results to users: +- Telegram +- OneBot (QQ) +- Slack +- Feishu (Lark) +- Discord +- Misskey +- Satori + +## Sending Multimedia Messages + +To make it easier for Agents to send images, audio, video, and other files directly to users, AstrBot provides a `send_message_to_user` tool by default. + +### Features +- **Direct Sending**: Agents can send generated or retrieved multimedia files directly to users without complex text conversions. +- **Multiple Formats**: Supports images, files, audio, video, etc. diff --git a/docs/snapshots/v4.23.6/en/use/skills.md b/docs/snapshots/v4.23.6/en/use/skills.md new file mode 100644 index 0000000..a57a5b1 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/skills.md @@ -0,0 +1,37 @@ +# Anthropic Skills + +Anthropic's Agent Skills are a modular extension standard designed to turn Claude from a "general-purpose chatbot" into a "task executor" with domain-specific expertise. A Skill is a structured folder containing instructions, scripts, metadata, and reference resources. It is more than just a prompt—it functions like a specialized "operation manual" that is dynamically loaded only when the Agent needs to perform a specific task. A Tool is the model's concrete interface for interacting with the outside world (APIs/functions), while a Skill standardizes the combination of instructions, templates, and tools into a reusable task execution guide. Traditional Tools require all API definitions to be injected into the prompt at conversation start. If there are more than 50 tools, tens of thousands of tokens can be consumed before any conversation begins, making responses slower and costlier. + +Support for Anthropic Skills was introduced in AstrBot starting from v4.13.0, allowing users to easily integrate and use various predefined skill modules to improve the Agent's performance on specific tasks. + +## Key Features + +- Progressive Disclosure: The model initially loads only skill names and short descriptions. Detailed `SKILL.md` instructions are loaded only when a task matches, saving context window space and reducing cost. +- Highly Reusable: Skills can be used across different Claude API projects, Claude Code, or Claude.ai. +- Executable Capability: Skills can include executable code scripts that, together with Anthropic's code execution environment, can directly generate or process files. + +## Uploading Skills to AstrBot + +Open the AstrBot admin panel, navigate to the `Plugins` page, and find `Skills`. + +![Skills](https://files.astrbot.app/docs/source/images/skills/image.png) + +You can upload Skills with the following requirements: + +1. The upload must be a `.zip` archive. +2. **After extraction, it must contain a single Skill folder. The folder name will be used as the identifier for the Skill in AstrBot—please name it using English characters.** +3. The Skill folder must include a file named `SKILL.md`, and its contents should preferably follow the Anthropic Skills specification. You can refer to Anthropic's documentation: https://code.claude.com/docs/zh-CN/skills + +## Using Skills in AstrBot + +Skills serve as operation manuals for Agents and often include executable Python snippets and scripts. Therefore, an Agent requires an **execution environment**. + +Currently, AstrBot provides two execution environments: + +- Local — The Agent runs in your AstrBot runtime environment. **Use with caution: this allows the Agent to execute arbitrary code in your environment, which may pose security risks.** +- Sandbox — The Agent runs inside an isolated sandbox environment. **You must enable AstrBot sandbox mode first.** See: /use/astrbot-agent-sandbox. If sandbox mode is not enabled, Skills will not be passed to the Agent. + +You can select the default execution environment on the `Config` page under "Computer Use". + +> [!NOTE] +> Please note: if you select `Local` as the execution environment, AstrBot currently only allows **AstrBot administrators** to request that the Agent operate on your local environment. Regular users are prohibited from doing so. The Agent will be prevented from executing code locally via Shell, Python, or other tools and will receive a permission restriction message such as `Sorry, I cannot execute code on your local environment due to permission restrictions.`. diff --git a/docs/snapshots/v4.23.6/en/use/subagent.md b/docs/snapshots/v4.23.6/en/use/subagent.md new file mode 100644 index 0000000..da6e5a7 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/subagent.md @@ -0,0 +1,56 @@ +# Agent Handoff and SubAgent + +SubAgent Orchestration is an advanced agent organization method provided by AstrBot. It allows you to decompose complex tasks into multiple specialized SubAgents, reducing the Main Agent's prompt length and improving task execution success rates. + +v4.14.0 introduced this feature, which is currently an **experimental feature** and not yet stable. + +![](https://files.astrbot.app/docs/source/images/subagent/image.png) + +## Motivation + +In traditional architectures, all tools are directly mounted on the Main Agent. When there are many tools, several issues arise: +1. **Prompt Bloat**: The Main Agent must include descriptions for all tools in its System Prompt, consuming excessive context. +2. **Execution Errors**: With a large number of tools, the LLM may confuse tool purposes or generate incorrect parameters. +3. **Complexity**: The Main Agent is overburdened with both conversation and the organization/invocation of numerous tools. + +With SubAgent Orchestration, the Main Agent is only responsible for user interaction and **task delegation**. Actual tool execution is handled by specialized SubAgents. + +## How It Works + +1. **Main Agent Delegation**: When SubAgent mode is enabled, the Main Agent only sees a series of delegation tools named `transfer_to_`. +2. **Task Handoff**: When the Main Agent determines a task needs execution, it calls the corresponding delegation tool, passing the task description to the SubAgent. +3. **SubAgent Execution**: The SubAgent receives the task, performs operations using its assigned tools, and returns the organized results to the Main Agent. +4. **Feedback**: The Main Agent receives the results and continues the conversation with the user. + +![](https://files.astrbot.app/docs/source/images/subagent/1.png) + +## Configuration + +In the AstrBot WebUI, click **SubAgents** in the left navigation bar. + +### 1. Enable SubAgent Mode + +Toggle "Enable SubAgent Orchestration" at the top of the page. + +### 2. Create a SubAgent + +Click the "Add SubAgent" button: + +- **Agent Name**: Used to generate the delegation tool name (e.g., `transfer_to_weather`). Use lowercase and underscores. +- **Select Persona**: Choose a preset Persona, which defines the SubAgent's basic character, behavioral guidance, and the Tools collection it can use. You can create and manage Personas on the "Persona Settings" page. +- **Description for Main LLM**: This description tells the Main Agent what this SubAgent is good at, ensuring accurate delegation. +- **Assign Tools**: Select the tools this SubAgent can invoke. +- **Provider Override (Optional)**: You can specify different model providers for specific SubAgents. For example, the Main Agent could use GPT-4o, while a simple query SubAgent uses GPT-4o-mini to save costs. + +## Best Practices + +- **Single Responsibility**: Each SubAgent should handle one category of related tasks (e.g., search, file processing, smart home control). +- **Clear Descriptions**: Descriptions for the Main Agent should be concise and highlight the SubAgent's core capabilities. +- **Layered Management**: For extremely complex tasks, consider multi-level delegation if necessary. + +## Known Issues + +SubAgent orchestration is currently an **experimental feature** and not yet stable. + +1. Skills of personas cannot be isolated at this time. +2. SubAgent conversation histories are not currently saved. diff --git a/docs/snapshots/v4.23.6/en/use/unified-webhook.md b/docs/snapshots/v4.23.6/en/use/unified-webhook.md new file mode 100644 index 0000000..63bdbff --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/unified-webhook.md @@ -0,0 +1,28 @@ +# Unified Webhook Mode + +Starting from v4.8.0, AstrBot supports Unified Webhook Mode (unified_webhook_mode). When this mode is enabled, all platform adapters that support it will use the same Webhook callback endpoint, simplifying reverse proxy and domain configuration. You no longer need to configure separate ports, domains, and reverse proxies for each bot adapter. + +Platform adapters that support Unified Webhook Mode include: + +- Slack Webhook Mode +- WeChat Official Account +- WeCom Application +- WeCom AI Bot +- WeChat Customer Service Bot +- QQ Official Bot Webhook Mode +- ... + +## How to Use Unified Webhook Mode + +1. Have a domain (e.g., example.com) and a server with a public IP +2. Configure DNS resolution (e.g., astrbot.example.com) +3. Configure reverse proxy to forward requests from port 80 or 443 of your domain to AstrBot's WebUI port (default is 6185) +4. Go to AstrBot's `Configuration` page, click `System`, and set the `Externally Reachable Callback URL` to your configured URL (e.g., https://astrbot.example.com). Click save and wait for restart. + +When configuring each platform adapter afterwards, enable `Unified Webhook Mode (unified_webhook_mode)`. + +![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook-config.png) + +Once this mode is enabled, AstrBot will generate a unique Webhook callback URL for you. You just need to fill this URL into each platform's callback address field. + +![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png) diff --git a/docs/snapshots/v4.23.6/en/use/websearch.md b/docs/snapshots/v4.23.6/en/use/websearch.md new file mode 100644 index 0000000..119166d --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/websearch.md @@ -0,0 +1,41 @@ + +# Web Search + +The web search feature gives large language models internet retrieval capability for recent information, which can improve response accuracy and reduce hallucinations to some extent. + +AstrBot's built-in web search functionality relies on the large language model's `function calling` capability. If you're not familiar with function calling, please refer to: [Function Calling](/use/websearch). + +When using a large language model that supports function calling with the web search feature enabled, you can try saying: + +- `Help me search for xxx` +- `Help me summarize this link: https://soulter.top` +- `Look up xxx` +- `Recent xxxx` + +And other prompts with search intent to trigger the model to invoke the search tool. + +AstrBot currently supports 4 web search providers: `Tavily`, `BoCha`, `Baidu AI Search`, and `Brave`. + +![image](https://files.astrbot.app/docs/source/images/websearch/image.png) + +Go to `Configuration`, scroll down to find Web Search, where you can select `Tavily`, `BoCha`, `Baidu AI Search`, or `Brave`. + +### Tavily + +Go to [Tavily](https://app.tavily.com/home) to get an API Key, then fill it in the corresponding configuration item. + +### BoCha + +Get an API Key from the BoCha platform, then fill it in the corresponding configuration item. + +### Baidu AI Search + +Get an API Key from Baidu Qianfan APP Builder, then fill it in the corresponding configuration item. + +### Brave + +Get an API Key from Brave Search, then fill it in the corresponding configuration item. + +If you use Tavily as your web search source, you will get a better experience optimization on AstrBot ChatUI, including citation source display and more: + +![](https://files.astrbot.app/docs/source/images/websearch/image1.png) diff --git a/docs/snapshots/v4.23.6/en/use/webui.md b/docs/snapshots/v4.23.6/en/use/webui.md new file mode 100644 index 0000000..dd17994 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/use/webui.md @@ -0,0 +1,96 @@ +# WebUI + +The AstrBot admin panel features plugin management, log viewing, visual configuration, statistics viewing, and more. + +![image](https://files.astrbot.app/docs/source/images/webui/image-4.png) + +## Accessing the Admin Panel + +After starting AstrBot, you can access the admin panel by visiting `http://localhost:6185` in your browser. + +> [!TIP] +> - If you're deploying AstrBot on a cloud server, replace `localhost` with your server's IP address. + +## Login + +The default username and password are both `astrbot`. + +## ChatUI + +AstrBot includes a built-in ChatUI for talking to configured models directly in your browser. + +ChatUI supports these common workflows: + +- Create, rename, and delete conversations, and switch previous conversations from the sidebar. +- Select the config profile, model provider, and model on the chat page; if Provider session separation is enabled, you can also choose a model for the current session only. +- Send text, images, files, and voice input; uploaded attachments show previews and use file signature checks to help identify file types. +- View model thinking, tool-call status, knowledge-base or web-search references, and per-message token and latency statistics. +- Copy or regenerate existing replies, including regenerating with another model. +- Edit a user message and continue generation from that point, or start a thread from a specific excerpt. +- Switch between streaming/normal response modes and SSE/WebSocket transport modes. + +> [!NOTE] +> To keep message delivery ordered, keep only one ChatUI page open for the same browser session. If you open chat in multiple tabs, the system may ask you to reconnect. + +## Visual Configuration + +In the admin panel, you can configure AstrBot's plugins through visual configuration. Click `Configuration` in the left sidebar to enter the configuration page. + +![image](https://files.astrbot.app/docs/source/images/webui/image-3.png) + +After modifying the configuration, you need to click the `Save` button in the bottom right corner to successfully save the configuration. + +Use the first circular button in the bottom right corner to switch to `Code Edit Configuration`. In `Code Edit Configuration`, you can directly edit the configuration file. + +After editing, first click `Apply This Configuration`, which will apply the configuration to the visual configuration, then click the `Save` button in the bottom right corner to save the configuration. If you don't click `Apply This Configuration`, your modifications won't take effect. + +![alt text](https://files.astrbot.app/docs/source/images/webui/image-5.png) + +## Plugins + +In the admin panel, you can view installed plugins and install new plugins through the `Plugins` section in the left sidebar. + +Click the Plugin Market tab to browse plugins officially listed by AstrBot. + +![image](https://files.astrbot.app/docs/source/images/webui/image-1.png) + +You can also click the + button in the bottom right corner to manually install plugins via URL or file upload. + +> Due to the plugin update mechanism, the AstrBot Team cannot fully guarantee the security of plugins in the plugin market. Please carefully verify them. The AstrBot Team is not responsible for any losses caused by plugins. + +### Handling Plugin Load Failures + +If a plugin fails to load, the admin panel will display the error message and provide a **"Try one-click reload fix"** button. This allows you to quickly reload the plugin after fixing the environment (e.g., installing missing dependencies) or modifying the code, without having to restart the entire application. + +## Command Management + +Use the `Command Management` menu on the left to centrally manage all registered commands; system plugins are hidden by default. + +Filter by plugin, type (command / command group / subcommand), permission, and status, and combine with the search box for quick lookup. Command group rows can expand to show subcommands, badges display the subcommand count, and subcommand rows are indented to indicate hierarchy. + +You can enable/disable and rename each command. + +## Trace + +In the `Trace` page of the admin panel, you can view the real-time execution trace of AstrBot. This is useful for debugging model call paths, tool invocation processes, etc. + +You can enable or disable trace recording using the switch at the top of the page. + +> [!NOTE] +> Currently only recording partial model call paths from AstrBot main Agent. More coverage will be added. + +## Updating the Admin Panel + +When AstrBot starts, it automatically checks if the admin panel needs updating. If it does, the first log entry (in yellow) will prompt you. + +Use the `/dashboard_update` command to manually update the admin panel (admin command). + +Admin panel files are located in the data/dist directory. If you need to manually replace them, download `dist.zip` from https://github.com/AstrBotDevs/AstrBot/releases/ and extract it to the data directory. + +## Customizing WebUI Port + +Modify the `port` in the `dashboard` configuration in the data/cmd_config.json file. + +## Forgot Password + +Modify the `password` in the `dashboard` configuration in the data/cmd_config.json file and delete the entire password key-value pair. diff --git a/docs/snapshots/v4.23.6/en/what-is-astrbot.md b/docs/snapshots/v4.23.6/en/what-is-astrbot.md new file mode 100644 index 0000000..57ce878 --- /dev/null +++ b/docs/snapshots/v4.23.6/en/what-is-astrbot.md @@ -0,0 +1,29 @@ +--- +outline: deep +--- + +# 👋 I'm AstrBot + +## Introduction + +AstrBot is an open-source, all-in-one Agentic assistant for personal and group chats. It can be deployed across dozens of mainstream instant messaging platforms, such as QQ, Telegram, WeCom, Lark, DingTalk, and Slack. It also includes a lightweight built-in ChatUI (similar to OpenWebUI), providing reliable and extensible conversational AI infrastructure for individuals, developers, and teams. Whether you are building a personal AI companion, an intelligent customer service assistant, an automation bot, or an enterprise knowledge base, AstrBot helps you build AI applications directly inside your IM workflows. + +## Documentation Overview + +This documentation is divided into the following sections: + +- **Deployment**: multiple ways to quickly deploy AstrBot on local machines or cloud servers. +- **Messaging Platform Integration**: integration guides for 18+ mainstream instant messaging platforms. +- **AI Provider Integration**: connect to model providers, use AstrBot's built-in Agent Runner, or integrate third-party Agent Runner services such as Dify, Coze, Alibaba Bailian, and DeerFlow. +- **Usage Guides**: practical guides for features such as plugins, tool calling, knowledge base, MCP, Skills, and Agent sandbox. + +## Quick Start + +- Deploy AstrBot: Read the Deployment Guide to quickly deploy AstrBot on your local machine or cloud server. +- Connect to IM platforms: Follow the instructions to connect AstrBot to your preferred IM platforms such as Discord, Telegram, Slack, etc. +- Configure AI models: AstrBot supports various AI models. See [Connecting Model Services](/en/providers/start) + +## Notice + +1. AstrBot is a non-profit project under the AstrBotDevs organization, maintained by open-source contributors worldwide, and protected by the [AGPL-v3](https://www.chinasona.org/gnu/agpl-3.0-cn.html) license. If you modify AstrBot and use it to provide commercial network services, you must open-source your modifications. For details, contact [community@astrbot.app](mailto:community@astrbot.app). +2. Before using this project, please read the End User License Agreement (EULA): [End User License Agreement](https://github.com/AstrBotDevs/AstrBot/blob/master/EULA.md). If you do not agree to any terms of the agreement, do not use this project. diff --git a/docs/snapshots/v4.23.6/index.md b/docs/snapshots/v4.23.6/index.md new file mode 100644 index 0000000..39e00da --- /dev/null +++ b/docs/snapshots/v4.23.6/index.md @@ -0,0 +1,16 @@ +# AstrBot 开发文档 + +本仓库的文档用于给 **AI 开发者 / RAG** 提供结构化的上下文。 + +## 快速入口 + +- [核心概念](/design_standards/core_concepts) +- [架构总览](/design_standards/architecture_overview) +- [Agent(工具 / 子智能体 / 沙盒 / 定时任务)](/agent/) +- [v4.7.0+ Agent Runner 架构](/agent/agent-runner) +- [消息模型](/messages/model) +- [插件配置 Schema](/plugin_config/schema) +- [事件钩子(Hooks)](/plugin_config/hooks) +- [平台适配器接口](/platform_adapters/adapter_interface) + + diff --git a/docs/snapshots/v4.23.6/messages/components.md b/docs/snapshots/v4.23.6/messages/components.md new file mode 100644 index 0000000..ce7f2dd --- /dev/null +++ b/docs/snapshots/v4.23.6/messages/components.md @@ -0,0 +1,41 @@ +--- +category: messages +--- + +# 消息链组件 (Message Components) + +AstrBot 使用消息链(MessageChain)来描述消息结构,它是一个由多个消息段(MessagePart/Component)组成的有序列表。 + +### 核心组件及其兼容性 + +| 组件类型 | 描述 | 参数示例 | 平台兼容性建议 | +| :--- | :--- | :--- | :--- | +| `Plain` | 纯文本 | `text="Hello"` | 所有平台支持。 | +| `At` | 提及/艾特 | `user_id="xxx"` | 大多数平台支持。 | +| `Image` | 图片 | `fromFileSystem(path)`, `fromURL(url)` | 所有平台支持。URL 必须以 `http` 或 `https` 开头。 | +| `Record` | 语音 | `file="path/to/wav"` | 广泛支持。目前主要支持 `wav` 格式。 | +| `Video` | 视频 | `fromFileSystem(path)`, `fromURL(url)` | 广泛支持。常用格式为 `mp4` | +| `File` | 文件 | `file="path"`, `name="a.txt"` | 部分平台不支持。 | +| `Face` | 表情 | `id="123"` | 主要在 OneBot v11 (QQ) 平台支持。 | +| `Node/Nodes` | 合并转发节点 | `uin`, `name`, `content` | 仅 OneBot v11 支持。 | +| `Poke` | 戳一戳 | - | 主要在 OneBot v11 支持。 | +| `Reply` | 回复特定消息 | `message_id="xxx"` | 广泛支持。 | + +### 消息构建示例 + +```python +import astrbot.api.message_components as Comp + +# 方式 1:手动构建列表 +chain = [ + Comp.At(user_id=event.get_sender_id()), + Comp.Plain(" 来看这张图:"), + Comp.Image.fromURL("https://example.com/image.jpg") +] +yield event.chain_result(chain) + +# 方式 2:使用 MessageChain 流式构建 +from astrbot.api.event import MessageChain +message_chain = MessageChain().message("Hello!").file_image("path/to/image.jpg") +await self.context.send_message(event.unified_msg_origin, message_chain) +``` diff --git a/docs/snapshots/v4.23.6/messages/events.md b/docs/snapshots/v4.23.6/messages/events.md new file mode 100644 index 0000000..c8681f9 --- /dev/null +++ b/docs/snapshots/v4.23.6/messages/events.md @@ -0,0 +1,33 @@ +--- +title: 消息事件 (AstrMessageEvent) +type: improvement +status: stable +last_updated: 2024-05-22 +related_base: messages/events.md +--- + +## 概述 +`AstrMessageEvent` 是插件处理逻辑的核心上下文对象。在最新版本中,该对象的会话标识属性(`session_id` 与 `unified_msg_origin`)已重构为基于 `MessageSession` 对象的动态属性(Property),增强了会话管理的一致性。 + +## 核心属性与 Setter 契约 + +这些属性现在不仅支持读取,还支持通过 Setter 进行动态修改,且修改会自动同步到底层的 `MessageSession` 状态: + +- **`event.unified_msg_origin` (UMO)**: + - **Getter**: 返回格式为 `platform_name:message_type:session_id` 的统一标识符。 + - **Setter**: 允许通过赋值 UMO 字符串来重置事件的会话上下文。内部通过 `MessageSession.from_str(value)` 重新解析并覆盖当前 session 对象。 +- **`event.session_id`**: + - **Getter**: 获取当前会话的唯一 ID。 + - **Setter**: 直接修改当前会话 ID,此变更会立即反映在 `unified_msg_origin` 的输出中。 + +## 内部实现逻辑 + +`AstrMessageEvent` 不再在 `__init__` 中静态存储 `session_id` 和 `unified_msg_origin` 字符串,而是统一维护一个 `self.session` (`MessageSession` 类实例)。 +- **初始化**: 修正了 `MessageSession` 的拼写错误并确保其在事件创建时被正确初始化。 +- **响应式更新**: 通过 Python `@property` 装饰器,确保了 UMO 和 Session ID 始终指向同一个数据源,消除了状态不一致的风险。 + +## 变更影响分析 + +1. **动态会话切换**: 插件开发者现在可以在事件处理过程中,通过修改 `event.unified_msg_origin` 动态地将事件“重定向”到另一个会话上下文。这对于实现跨群指令触发或会话劫持逻辑至关重要。 +2. **副作用警示**: 修改 `unified_msg_origin` 会导致底层的 `platform_name` 和 `message_type` 同时发生变化。如果仅需修改用户 ID,应优先使用 `event.session_id` 的 setter。 +3. **最佳实践**: 在编写需要持久化或比对会话的逻辑时,应始终依赖 `event.unified_msg_origin` 属性,因为它现在是经过 `MessageSession` 校验的权威来源。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/messages/model.md b/docs/snapshots/v4.23.6/messages/model.md new file mode 100644 index 0000000..4394c9d --- /dev/null +++ b/docs/snapshots/v4.23.6/messages/model.md @@ -0,0 +1,31 @@ +--- +category: messages +--- + +# 消息模型 (AstrBotMessage) + +`AstrBotMessage` 是适配器层生成的标准化消息对象,它屏蔽了不同平台(QQ、飞书等)的差异,使插件可以“一次编写,到处运行”。 + +### AstrBotMessage 结构 + +在适配器中,必须填充 `AstrBotMessage` 的以下字段: + +```python +class AstrBotMessage: + type: MessageType # 消息类型(GROUP_MESSAGE 或 FRIEND_MESSAGE) + self_id: str # 机器人 ID + session_id: str # 会话 ID,决定了上下文隔离 + message_id: str # 消息 ID + group_id: str # 群组 ID(如果是私聊则为空) + sender: MessageMember # 发送者信息(含 user_id 和 nickname) + message: List[BaseMessageComponent] # 消息链(组件列表) + message_str: str # 纯文本汇总内容 + raw_message: object # 原始平台消息对象(用于 Debug 或特殊处理) + timestamp: int # 时间戳 +``` + +### 属性详解 + +- **`session_id`**: 核心字段,用于决定 LLM 对话的上下文隔离。 +- **`message_str`**: 插件处理逻辑中常用的纯文本内容。 +- **`message`**: 结构化消息内容,由各种消息组件组成。 diff --git a/docs/snapshots/v4.23.6/messages/umo.md b/docs/snapshots/v4.23.6/messages/umo.md new file mode 100644 index 0000000..3c432a9 --- /dev/null +++ b/docs/snapshots/v4.23.6/messages/umo.md @@ -0,0 +1,22 @@ +--- +category: messages +--- + +# 统一消息源 (Unified Message Origin) + +统一消息源(Unified Message Origin,简称 **UMO**)是 AstrBot 识别跨平台会话的核心标识。 + +### 格式 + +UMO 是一个格式如下的字符串: +`platform_id:message_type:session_id` + +- **`platform_id`**: 平台 ID(如 `aiocqhttp`, `qqofficial`)。 +- **`message_type`**: 消息类型(`group` 或 `private`)。 +- **`session_id`**: 会话 ID(群号或用户 ID)。 + +### 用途 + +1. **会话识别**: 唯一标识一个消息来源。 +2. **主动发送**: 通过 `context.send_message(umo, message_chain)` 向指定的源发送消息。 +3. **获取方式**: 在插件中通过 `event.unified_msg_origin` 获取。 diff --git a/docs/snapshots/v4.23.6/package.json b/docs/snapshots/v4.23.6/package.json new file mode 100644 index 0000000..8d39e96 --- /dev/null +++ b/docs/snapshots/v4.23.6/package.json @@ -0,0 +1,13 @@ +{ + "scripts": { + "docs:dev": "vitepress dev --host", + "docs:build": "vitepress build", + "docs:preview": "vitepress preview" + }, + "devDependencies": { + "vitepress": "^1.6.4" + }, + "dependencies": { + "vue": "^3.5.17" + } +} diff --git a/docs/snapshots/v4.23.6/platform_adapters/adapter_interface.md b/docs/snapshots/v4.23.6/platform_adapters/adapter_interface.md new file mode 100644 index 0000000..ca0b211 --- /dev/null +++ b/docs/snapshots/v4.23.6/platform_adapters/adapter_interface.md @@ -0,0 +1,374 @@ +# Platform Adapter + +平台适配器将外部消息平台接入 AstrBot。插件可注册自定义适配器。 + +## 注册适配器 + +`@register_platform_adapter(adapter_name="id", desc="描述", default_config_tmpl={"key": "value"}, adapter_display_name="显示名", logo_path="logo.png", support_streaming_message=True)` + +## Platform 基类 + +继承 `Platform` 并实现以下方法: + +### 必须实现 + +- `run() -> Coroutine`: 异步阻塞方法,启动客户端 SDK 并持续监听消息。 +- `meta() -> PlatformMetadata`: 返回适配器元数据。 +- `send_by_session(session: MessageSession, message_chain: MessageChain)`: 通过会话发送消息。 + +### 可选重写 + +- `terminate()`: 终止平台运行。 +- `get_client() -> object`: 获取平台客户端对象。 +- `webhook_callback(request) -> Any`: 统一 Webhook 回调入口。 + +### 辅助方法 + +- `commit_event(event: AstrMessageEvent)`: 提交事件到事件队列。 +- `unified_webhook() -> bool`: 是否使用统一 Webhook 模式。 +- `get_stats() -> dict`: 获取平台统计信息。 +- `record_error(message: str, traceback_str: str | None)`: 记录错误。 +- `clear_errors()`: 清除错误记录。 + +### 属性 + +- `config: dict`: 平台配置(用户填写的 default_config_tmpl)。 +- `status: PlatformStatus`: 运行状态(PENDING/RUNNING/ERROR/STOPPED)。 +- `errors: list[PlatformError]`: 错误列表。 +- `last_error: PlatformError | None`: 最近错误。 + +## PlatformMetadata + +```python +PlatformMetadata( + name="adapter_id", # 平台类型标识 + description="适配器描述", + id="adapter_id", # 唯一标识符 + default_config_tmpl={}, # 默认配置模板 + adapter_display_name="显示名", # WebUI 显示名称 + logo_path="logo.png", # Logo 路径(相对于插件目录) + support_streaming_message=True, # 是否支持流式消息 + support_proactive_message=True, # 是否支持主动消息 +) +``` + +## AstrBotMessage + +适配器必须填充以下字段: + +```python +AstrBotMessage( + type=MessageType.GROUP_MESSAGE, # GROUP_MESSAGE / FRIEND_MESSAGE / OTHER_MESSAGE + self_id="bot_id", # 机器人 ID + session_id="session_id", # 会话 ID(决定上下文隔离) + message_id="msg_id", # 消息 ID + group=Group(group_id="123"), # 群组信息(私聊为 None) + sender=MessageMember(user_id="uid", nickname="昵称"), + message=[Plain(text="内容")], # 消息链 + message_str="纯文本内容", # 纯文本汇总 + raw_message=original_data, # 原始平台消息 + timestamp=1234567890, # 时间戳 +) +``` + +### 属性 + +- `group_id: str`: 群组 ID(私聊返回空字符串)。 + +## MessageType 枚举 + +- `MessageType.GROUP_MESSAGE`: 群组消息 +- `MessageType.FRIEND_MESSAGE`: 私聊消息 +- `MessageType.OTHER_MESSAGE`: 其他消息 + +## MessageMember + +```python +MessageMember(user_id="uid", nickname="昵称") +``` + +## Group + +```python +Group( + group_id="123", + group_name="群名", + group_avatar="头像URL", + group_owner="群主ID", + group_admins=["admin1", "admin2"], + members=[MessageMember(...)], +) +``` + +## MessageSession + +```python +MessageSession( + platform_name="adapter_id", # 平台 ID + message_type=MessageType.GROUP_MESSAGE, + session_id="session_id", +) +# 字符串格式: "platform_id:message_type:session_id" +``` + +### 方法 + +- `MessageSession.from_str(session_str)`: 从字符串解析。 + +## AstrMessageEvent + +事件基类,平台适配器需继承并实现 `send()` 方法。 + +### 核心属性 + +- `message_str: str`: 纯文本消息。 +- `message_obj: AstrBotMessage`: 完整消息对象。 +- `platform_meta: PlatformMetadata`: 平台元数据。 +- `session: MessageSession`: 会话对象。 +- `unified_msg_origin: str`: UMO(格式: `platform_id:message_type:session_id`)。 +- `session_id: str`: 会话 ID。 +- `role: str`: 用户角色("member" / "admin")。 +- `is_wake: bool`: 是否唤醒。 +- `is_at_or_wake_command: bool`: 是否 At 或唤醒词。 +- `call_llm: bool`: 是否调用 LLM。 + +### 获取信息方法 + +- `get_platform_name() -> str`: 获取平台类型。 +- `get_platform_id() -> str`: 获取平台 ID。 +- `get_message_str() -> str`: 获取消息文本。 +- `get_message_outline() -> str`: 获取消息概要(图片转 `[图片]`)。 +- `get_messages() -> list[BaseMessageComponent]`: 获取消息链。 +- `get_message_type() -> MessageType`: 获取消息类型。 +- `get_session_id() -> str`: 获取会话 ID。 +- `get_group_id() -> str`: 获取群组 ID。 +- `get_self_id() -> str`: 获取机器人 ID。 +- `get_sender_id() -> str`: 获取发送者 ID。 +- `get_sender_name() -> str`: 获取发送者昵称。 +- `is_private_chat() -> bool`: 是否私聊。 +- `is_wake_up() -> bool`: 是否唤醒。 +- `is_admin() -> bool`: 是否管理员。 + +### 消息发送方法 + +- `send(message: MessageChain)`: 发送消息到平台。 +- `send_streaming(generator: AsyncGenerator, use_fallback: bool)`: 发送流式消息。 +- `react(emoji: str)`: 添加表情回应。 +- `get_group(group_id: str | None) -> Group | None`: 获取群组数据。 + +### 结果设置方法 + +- `set_result(result: MessageEventResult | str)`: 设置事件结果。 +- `stop_event()`: 终止事件传播。 +- `continue_event()`: 继续事件传播。 +- `is_stopped() -> bool`: 是否已终止。 +- `should_call_llm(call_llm: bool)`: 是否调用 LLM。 +- `get_result() -> MessageEventResult | None`: 获取结果。 +- `clear_result()`: 清除结果。 + +### 快捷构建结果 + +- `make_result() -> MessageEventResult`: 创建空结果。 +- `plain_result(text: str) -> MessageEventResult`: 文本结果。 +- `image_result(url_or_path: str) -> MessageEventResult`: 图片结果。 +- `chain_result(chain: list) -> MessageEventResult`: 消息链结果。 + +### LLM 请求 + +- `request_llm(prompt: str, func_tool_manager=None, tool_set=None, session_id="", image_urls=None, contexts=None, system_prompt="", conversation=None) -> ProviderRequest`: 创建 LLM 请求。 + +### 额外信息 + +- `set_extra(key, value)`: 设置额外信息。 +- `get_extra(key: str | None, default=None) -> Any`: 获取额外信息。 +- `clear_extra()`: 清除额外信息。 + +## MessageChain + +消息链,用于构建和发送消息。 + +### 构建方法 + +- `message(text: str)`: 添加文本。 +- `at(name: str, qq: str | int)`: 添加 At。 +- `at_all()`: 添加 AtAll。 +- `url_image(url: str)`: 添加网络图片。 +- `file_image(path: str)`: 添加本地图片。 +- `base64_image(base64_str: str)`: 添加 base64 图片。 +- `use_t2i(use_t2i: bool)`: 设置是否使用文本转图片。 + +### 工具方法 + +- `get_plain_text(with_other_comps_mark: bool) -> str`: 获取纯文本。 +- `squash_plain()`: 合并所有 Plain 消息段。 + +## MessageEventResult + +继承 MessageChain,增加事件控制。 + +### 方法 + +- `stop_event()`: 终止事件传播。 +- `continue_event()`: 继续事件传播。 +- `is_stopped() -> bool`: 是否终止。 +- `set_async_stream(stream: AsyncGenerator)`: 设置异步流。 +- `set_result_content_type(typ: ResultContentType)`: 设置结果类型。 +- `is_llm_result() -> bool`: 是否 LLM 结果。 + +## 消息组件 + +### Plain + +`Plain(text="文本内容")` + +### Image + +```python +Image.fromURL("https://example.com/img.jpg") +Image.fromFileSystem("/path/to/image.jpg") +Image.fromBase64("base64_data") +Image.fromBytes(bytes_data) +``` + +- `convert_to_file_path() -> str`: 转换为本地路径。 +- `convert_to_base64() -> str`: 转换为 base64。 +- `register_to_file_service() -> str`: 注册到文件服务。 + +### Record + +```python +Record.fromFileSystem("/path/to/audio.wav") +Record.fromURL("https://example.com/audio.wav") +Record.fromBase64("base64_data") +``` + +- `convert_to_file_path() -> str`: 转换为本地路径。 +- `convert_to_base64() -> str`: 转换为 base64。 +- `register_to_file_service() -> str`: 注册到文件服务。 + +### Video + +```python +Video.fromFileSystem("/path/to/video.mp4") +Video.fromURL("https://example.com/video.mp4") +``` + +- `convert_to_file_path() -> str`: 转换为本地路径。 +- `register_to_file_service() -> str`: 注册到文件服务。 + +### File + +`File(name="文件名", file="/path/to/file", url="https://...")` + +- `get_file(allow_return_url: bool) -> str`: 异步获取文件。 +- `register_to_file_service() -> str`: 注册到文件服务。 + +### At / AtAll + +```python +At(qq="user_id", name="昵称") +AtAll() +``` + +### Reply + +`Reply(id="message_id", chain=[...], sender_id="uid", sender_nickname="昵称", time=timestamp, message_str="文本")` + +### Face + +`Face(id=123)` + +### Node / Nodes + +```python +Node(uin="qq号", name="昵称", content=[Plain("内容")]) +Nodes(nodes=[Node(...), Node(...)]) +``` + +### Forward + +`Forward(id="forward_id")` + +### Poke + +`Poke(type="poke_type")` + +### Json + +`Json(data={"key": "value"})` + +### WechatEmoji + +`WechatEmoji(md5="md5值", md5_len=长度, cdnurl="CDN链接")` + +## 完整示例 + +```python +from astrbot.api.platform import ( + Platform, AstrBotMessage, MessageMember, MessageType, + PlatformMetadata, register_platform_adapter +) +from astrbot.api.event import AstrMessageEvent, MessageChain +from astrbot.core.platform.astr_message_event import MessageSesion + +@register_platform_adapter("myplatform", "我的平台适配器", default_config_tmpl={ + "token": "", + "enable": False, +}) +class MyPlatformAdapter(Platform): + def __init__(self, platform_config: dict, platform_settings: dict, event_queue: asyncio.Queue): + super().__init__(platform_config, event_queue) + self.settings = platform_settings + + def meta(self) -> PlatformMetadata: + return PlatformMetadata(name="myplatform", description="我的平台", id=self.config.get("id", "myplatform")) + + async def run(self): + async def on_message(data): + abm = await self.convert_message(data) + await self.handle_msg(abm) + # 启动客户端监听... + + async def convert_message(self, data: dict) -> AstrBotMessage: + abm = AstrBotMessage() + abm.type = MessageType.GROUP_MESSAGE + abm.session_id = data["session_id"] + abm.message_id = data["message_id"] + abm.sender = MessageMember(user_id=data["user_id"], nickname=data["nickname"]) + abm.message_str = data["content"] + abm.message = [Plain(text=data["content"])] + abm.raw_message = data + return abm + + async def handle_msg(self, message: AstrBotMessage): + event = MyPlatformEvent( + message_str=message.message_str, + message_obj=message, + platform_meta=self.meta(), + session_id=message.session_id, + client=self.client, + ) + self.commit_event(event) + + async def send_by_session(self, session: MessageSesion, message_chain: MessageChain): + # 实现发送逻辑... + await super().send_by_session(session, message_chain) + +class MyPlatformEvent(AstrMessageEvent): + def __init__(self, message_str, message_obj, platform_meta, session_id, client): + super().__init__(message_str, message_obj, platform_meta, session_id) + self.client = client + + async def send(self, message: MessageChain): + for comp in message.chain: + if isinstance(comp, Plain): + await self.client.send_text(self.get_sender_id(), comp.text) + await super().send(message) +``` + +## 注意事项 + +- `run()` 必须是阻塞方法,持续监听消息。 +- `convert_message()` 必须正确设置 `session_id`,它决定 LLM 上下文隔离。 +- `commit_event()` 用于提交事件到队列,不可遗漏。 +- 事件类必须实现 `send()` 方法,并在最后调用 `await super().send(message)`。 diff --git a/docs/snapshots/v4.23.6/platform_adapters/message_conversion.md b/docs/snapshots/v4.23.6/platform_adapters/message_conversion.md new file mode 100644 index 0000000..4d56d3c --- /dev/null +++ b/docs/snapshots/v4.23.6/platform_adapters/message_conversion.md @@ -0,0 +1,29 @@ +--- +category: platform_adapters +--- + +# 消息转换逻辑 (Message Conversion) + +`convert_message` 是适配器中最关键的方法,它负责将平台原始的消息格式映射到 AstrBot 的统一模型。 + +### 转换要求 + +在 `convert_message` 中,必须填充 `AstrBotMessage` 的以下核心字段: + +1. **`type`**: 识别是 `GROUP_MESSAGE` 还是 `FRIEND_MESSAGE`。 +2. **`session_id`**: 设置会话隔离。 +3. **`message_str`**: 提取纯文本内容。 +4. **`message`**: 将平台各段消息(如图片、表情)映射为 AstrBot 的 `MessageComponent` 列表。 +5. **`sender`**: 提取发送者的 ID 和昵称。 +6. **`raw_message`**: 保存原始对象。 + +### 提交事件 + +转换完成后,需将其封装为 `AstrMessageEvent` 并提交: + +```python +async def handle_raw_message(self, data): + bot_msg = self.convert_message(data) + event = AstrMessageEvent(bot_msg, self) # 或子类 + self.commit_event(event) +``` diff --git a/docs/snapshots/v4.23.6/platform_adapters/telegram_media_group.md b/docs/snapshots/v4.23.6/platform_adapters/telegram_media_group.md new file mode 100644 index 0000000..823c09e --- /dev/null +++ b/docs/snapshots/v4.23.6/platform_adapters/telegram_media_group.md @@ -0,0 +1,40 @@ +--- +title: Telegram 媒体组处理机制 (Telegram Media Group Handling) +type: feature +status: stable +last_updated: 2025-02-08 +related_base: platform_adapters/adapter_interface.md +--- + +## 概述 + +Telegram 平台在发送包含多张图片或视频的“相册”(Media Group)时,会将其拆分为多个独立的 Update 发送。AstrBot 的 Telegram 适配器实现了缓存与防抖机制,将这些碎片化的消息合并为单个 `AstrMessageEvent`,从而保证插件逻辑的一致性。 + +## 核心逻辑与参数 + +### 1. 收集与防抖机制 +适配器通过 `media_group_id` 识别属于同一相册的消息,并使用 `APScheduler` 进行异步调度处理: +- **`telegram_media_group_timeout` (默认 2.5s)**: 防抖延迟。每收到该组内的一条新消息,计时器都会重置。这是收集所有媒体项的窗口期。 +- **`telegram_media_group_max_wait` (默认 10.0s)**: 硬性超时上限。防止因消息流持续不断导致的无限延迟,达到此时间后将强制触发合并处理。 + +### 2. 消息合并策略 +在 `process_media_group` 方法中,系统执行以下合并逻辑: +- **基础元数据**: 以媒体组的第一条消息作为基准,保留其 `message_str`(通常是相册的 Caption)、回复关系(Reply Chain)和会话上下文。 +- **组件聚合**: 遍历组内所有后续消息,调用 `convert_message` 提取其媒体组件(如 `Image`, `Video`, `File`),并将其 `extend` 到基准消息的 `message` 列表(MessageChain)中。 +- **事件分发**: 合并完成后,仅提交一个封装了完整 `MessageChain` 的 `AstrMessageEvent` 到事件循环。 + +## 关键方法签名 + +- `handle_media_group_message(update, context)`: 拦截带有 `media_group_id` 的消息并管理缓存与调度任务。 +- `process_media_group(media_group_id)`: 核心合并函数,负责从缓存提取数据、重组 `AstrBotMessage` 并触发 `handle_msg`。 + +## 变更影响分析 + +- **插件开发者**: + - **事件密度变化**: 针对 Telegram 平台的相册消息,插件现在只会接收到一个 `AstrMessageEvent`。开发者应预期 `event.message` 列表中可能包含多个 `Image` 或 `Video` 组件。 + - **响应延迟**: 处理 Telegram 相册消息时会有至少 2.5s 的固有延迟,这是为了确保媒体收集完整,属于预期行为。 +- **适配器开发者**: + - 此机制展示了处理“流式/碎片化”平台消息的标准范式:`缓存 -> 防抖调度 -> 组件合并 -> 统一分发`。在接入类似具有媒体组概念的平台(如 Discord)时应参考此实现。 +- **边界情况**: + - 如果相册中的不同图片带有不同的文字说明(虽然 Telegram UI 通常只允许一个 Caption),目前逻辑仅保留第一条消息的文本。 + - 超过 `max_wait` 后到达的消息将被视为独立消息处理。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/plugin-development-workflow.md b/docs/snapshots/v4.23.6/plugin-development-workflow.md new file mode 100644 index 0000000..dc33541 --- /dev/null +++ b/docs/snapshots/v4.23.6/plugin-development-workflow.md @@ -0,0 +1,133 @@ +# Plugin Development Workflow + +## 1. Scaffold or Inspect + +Expected plugin layout: + +```text +astrbot_plugin_example/ +├── main.py +├── metadata.yaml +├── _conf_schema.json # optional but recommended for settings/secrets +├── requirements.txt # optional dependencies +├── README.md +├── LICENSE # recommended for publishable plugins +├── .gitignore # ignore __pycache__, venvs, IDE state, logs +└── tools/ # optional LLM FunctionTool classes +``` + +Minimum `metadata.yaml` fields: + +```yaml +name: astrbot_plugin_example +display_name: Example +desc: Short user-facing description. +version: v0.1.0 +author: YourName +repo: https://github.com/owner/astrbot_plugin_example +``` + +Optional metadata: `support_platforms: [...]`, `tags: [...]`, `social_link: ...`, `astrbot_version: ">=4.5.0"`. + +Before scaffolding from memory, skim the structured reference entrypoint `references/offline/xunxiing-AstrBot-Skill/docs/REFERENCE.md` and the core concept map `references/offline/xunxiing-AstrBot-Skill/docs/design_standards/core_concepts.md`. + +## 2. Implement the Plugin Class + +```python +from astrbot.api import logger +from astrbot.api.event import AstrMessageEvent, filter +from astrbot.api.star import Context, Star + +class ExamplePlugin(Star): + def __init__(self, context: Context): + super().__init__(context) + + @filter.command("hello") + async def hello(self, event: AstrMessageEvent): + """Say hello to the sender.""" + logger.info("hello command triggered") + yield event.plain_result(f"Hello, {event.get_sender_name()}!") + + async def terminate(self): + """Clean up background tasks/resources when unloaded.""" +``` + +Rules: +- Handler methods live on the `Star` subclass and include `self` plus `event` unless the hook explicitly documents otherwise. +- Use `async def` for handlers and hooks; avoid blocking network/file calls. +- Include short docstrings for commands/tools because AstrBot surfaces them to users and agents. + +## 3. Listen to Events + +Common filters: +- `@filter.command("name")` for slash-style commands. +- `@filter.command_group("group")` then `@group.command("sub")` for grouped commands. +- `@filter.on_full_match(...)`, `@filter.on_prefix(...)`, `@filter.on_keyword(...)`, `@filter.on_regex(...)` for text triggers. +- `@filter.event_message_type(...)` to restrict private/group/all message types. +- `@filter.permission_type(...)` for permission-gated non-tool handlers. + +Special hooks such as `on_llm_request`, `on_llm_response`, `on_decorating_result`, and `after_message_sent` should send with `await event.send(...)` instead of yielding results. + +Do not mix hook layers: +- Plugin event hooks/decorators: `references/offline/xunxiing-AstrBot-Skill/docs/plugin_config/hooks.md`. +- Agent runner hooks (`BaseAgentRunHooks`): `references/offline/xunxiing-AstrBot-Skill/docs/agent/agent-related-hooks.md`. + +## 4. Work With Messages + +- Plain text input: `event.message_str`. +- Full message chain: `event.message_obj.message`. +- Raw platform payload: `event.message_obj.raw_message` for debugging and adapter-specific details. +- Components: import `astrbot.api.message_components as Comp` and build chains such as `Comp.Plain(...)`, `Comp.At(...)`, `Comp.Image(...)`, `Comp.Record(...)`, `Comp.Video(...)`, `Comp.Reply(...)`. + +Passive replies usually yield a result: + +```python +yield event.plain_result("done") +``` + +For richer output, return/yield message-chain results or call `event.chain_result([...])` when available in the target version. Active sends require a platform/session target; inspect existing plugin patterns or official docs before implementing. + +## 5. Add Configuration + +Use `_conf_schema.json` for editable plugin config. Prefer schema fields for API keys, feature toggles, limits, prompt templates, and provider/tool names. In code, read the config through the plugin/context pattern used by the target AstrBot version; never hard-code secrets or deployment-specific IDs. + +Use `StarTools.get_data_dir()` for persistent plugin files. Treat the returned value as a `Path`. + +## 6. Session and Conversation Control + +- Use `event.unified_msg_origin` to identify the current conversation origin when working with `conversation_manager`. +- Check `platform_settings.unique_session` behavior if the plugin relies on exact session IDs. +- Use the official `SessionController` patterns for custom session grouping; avoid inventing incompatible IDs. +- For conversation history, access `self.context.conversation_manager` and await its async methods. + +## 7. AI Calls and Tools + +Provider calls: +- Prefer provider/context abstractions documented for the target AstrBot version. +- Pass the relevant tools if the call should use plugin/MCP tools; get the LLM tool manager from context when needed. +- Surface provider/model selection as config when users may have multiple providers. + +Function tools: +- Prefer dataclass/class-based tools by subclassing `FunctionTool` and registering via `self.context.add_llm_tools(...)` on supported versions. +- For v4.5.7+ targets, cross-check the dataclass pattern in `references/offline/xunxiing-AstrBot-Skill/docs/design_standards/core_concepts.md`. +- For `@filter.llm_tool`, include a parseable docstring and typed parameters matching the documented JSON parameter schema. +- Do not combine `@filter.permission_type` with `@filter.llm_tool`; it is ineffective. + +## 8. HTML-to-Image + +Use AstrBot's text-to-image/HTML rendering helpers documented in the plugin guide when replies are too long or need layout. Keep templates local to the plugin, sanitize user-provided HTML/text, and provide a plain-text fallback for platforms that cannot send images. + +## 9. MCP and Skill Integration Basics + +- MCP tools are managed by AstrBot and can appear in the LLM tool manager; do not reimplement an MCP server inside a plugin unless the user asks. +- If a plugin depends on MCP tools, document setup commands and required environment variables in `README.md` and config schema. +- AstrBot Skills are uploaded as zipped skill folders and may execute in Local or Sandbox environments; plugins should treat them as agent capabilities, not as trusted local code. + +## 10. Debug and Validate + +- Log through `astrbot.api.logger`. +- During message parsing bugs, inspect both `event.message_obj.message` and `event.message_obj.raw_message`. +- Test command registration, config defaults, permission behavior, reload/unload via `terminate`, and platform-specific component support. +- Run `scripts/check_astrbot_plugin.py ` for static structure checks. + +When writing README/API docs for AI consumption, use `references/offline/xunxiing-AstrBot-Skill/docs4agent/REFERENCE.md`: keep docs minimal, code-first, structured, and focused on exact callable APIs. diff --git a/docs/snapshots/v4.23.6/plugin_config/command_management.md b/docs/snapshots/v4.23.6/plugin_config/command_management.md new file mode 100644 index 0000000..912cf14 --- /dev/null +++ b/docs/snapshots/v4.23.6/plugin_config/command_management.md @@ -0,0 +1,34 @@ +--- +title: 动态指令管理与权限覆盖 (Dynamic Command Management) +type: feature +status: stable +last_updated: 2024-05-22 +related_base: plugin_config/decorators.md +--- + +## 概述 +AstrBot 引入了动态指令管理机制,允许在运行时通过 Dashboard 或 API 修改指令的元数据(如名称、启用状态)和执行权限。这意味着插件代码中通过装饰器(如 `@filter.permission_type`)定义的静态配置现在仅作为“初始默认值”,系统支持持久化的运行时覆盖。 + +## 核心逻辑与 API + +### 1. 权限动态更新 (`update_command_permission`) +该服务函数负责修改特定指令的处理权限。其核心流程如下: +- **定位处理函数**:通过 `handler_full_name` 检索指令描述符。 +- **持久化配置**:将权限变更写入全局 KV 存储中的 `alter_cmd` 字典。存储结构为 `alter_cmd -> {plugin_name} -> {handler_name} -> { "permission": "admin" | "member" }`。 +- **运行时滤镜注入**: + - 遍历指令关联的 `event_filters`。 + - 如果存在 `PermissionTypeFilter`,则直接更新其 `permission_type` 属性。 + - 如果不存在,则在滤镜列表首位插入一个新的 `PermissionTypeFilter` 实例。 + +### 2. 权限类型映射 +- `admin`: 映射为 `PermissionType.ADMIN`。 +- `member`: 映射为 `PermissionType.MEMBER`。 + +## 数据流向 +1. **配置加载**:插件加载时,系统会读取 `alter_cmd` 中的持久化设置并应用到指令实例。 +2. **实时生效**:通过 Dashboard 修改权限后,后端会同步更新内存中的 `handler.event_filters`,无需重启插件即可生效。 + +## 变更影响分析 +- **权限判定优先级**:动态配置(`alter_cmd`) > 装饰器静态定义。AI 开发者在调试权限问题时,应优先检查全局配置而非仅查看源码中的 `@filter.permission_type`。 +- **滤镜链可变性**:指令的 `event_filters` 列表现在是动态可变的。插件开发者不应假设滤镜列表在插件生命周期内保持不变。 +- **副作用**:修改权限会直接影响 `AstrMessageEvent` 的分发逻辑。如果一个指令被动态设为 `ADMIN`,则非管理员发送的消息将在 `PermissionTypeFilter` 阶段被拦截,不会进入插件业务逻辑。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/plugin_config/decorators.md b/docs/snapshots/v4.23.6/plugin_config/decorators.md new file mode 100644 index 0000000..489dbb0 --- /dev/null +++ b/docs/snapshots/v4.23.6/plugin_config/decorators.md @@ -0,0 +1,30 @@ +--- +category: plugin_config +--- + +# 常用装饰器 (Decorators) + +AstrBot 提供了一系列基于 `astrbot.api.event.filter` 的装饰器,用于注册插件和控制消息处理逻辑。 + +### 注册装饰器 + +- **`@register(id, author, description, version, repo_url)`** + - 标记插件类,提供基础元数据(若存在 `metadata.yaml` 则优先级较低)。 + +### 消息过滤器装饰器 + +过滤器遵循 **AND 逻辑**,即所有条件均满足时才触发。 + +| 装饰器 | 参数说明 | +| :--- | :--- | +| `@filter.command(name, alias, priority)` | 注册指令。支持带参函数,如 `def add(self, event, a: int, b: int)`。 | +| `@filter.command_group(name)` | 注册指令组。子指令通过 `@组名.command` 注册。 | +| `@filter.event_message_type(type)` | 过滤消息来源类型(`ALL`, `PRIVATE_MESSAGE`, `GROUP_MESSAGE`)。 | +| `@filter.platform_adapter_type(type)` | 过滤平台类型(`AIOCQHTTP`, `TELEGRAM` 等)。支持按位或 `|`。 | +| `@filter.permission_type(type)` | 校验权限(如 `ADMIN`)。 | +| `@filter.regex(pattern)` | 正则表达式匹配。 | + +### 注意事项 + +- 指令名不应包含空格。 +- 优先级 `priority` 默认为 0,数值越大优先级越高。 diff --git a/docs/snapshots/v4.23.6/plugin_config/file_config.md b/docs/snapshots/v4.23.6/plugin_config/file_config.md new file mode 100644 index 0000000..8337071 --- /dev/null +++ b/docs/snapshots/v4.23.6/plugin_config/file_config.md @@ -0,0 +1,30 @@ +--- +title: 插件文件配置系统 (Plugin File Config) +type: feature +status: stable +last_updated: 2024-05-22 +related_base: plugin_config/schema.md +--- + +## 概述 +AstrBot 引入了原生的文件配置支持,允许插件开发者在 `_conf_schema.json` 中定义文件类型的配置项。该系统集成了 Dashboard 文件上传、路径安全清洗及自动化存储管理,使插件能够以标准化的方式管理模型权重、静态资源或本地数据库文件。 + +## Schema 定义 +在插件的 `_conf_schema.json` 中,可以通过以下字段定义文件配置项: +- **`type`**: 必须设置为 `"file"`。 +- **`file_types`**: (可选) 字符串数组,定义允许的文件后缀名白名单(例如 `[".jpg", ".png", ".onnx"]`)。 +- **`description`**: 配置项描述,将显示在 Dashboard 的上传控件旁。 + +## 存储与路径逻辑 +1. **物理存储**: 上传的文件统一存储在 `data/plugins//files//` 目录下。其中 `` 是配置项在 Schema 中的点号路径(dot-path),确保了不同配置项间的文件隔离。 +2. **路径安全**: 系统通过 `sanitize_filename` 强制清洗文件名以防止路径穿越攻击,并使用 `normalize_rel_path` 确保跨平台路径的一致性。 +3. **配置引用**: 存入插件 `config.json` 的值为相对于插件数据目录的规范化路径(始终以 `files/` 开头)。 + +## 核心校验机制 +- **`validate_config`**: 在配置保存前,核心系统会强制执行校验逻辑,确保所有 `file` 类型的路径均指向合法的插件内部存储区域,并符合后缀名白名单。 +- **`MAX_FILE_BYTES`**: 系统级限制上传文件大小,默认为 500MB。 + +## 变更影响分析 +- **资源管理标准化**: 开发者不再需要手动编写文件上传接口或处理复杂的 `multipart/form-data` 逻辑,只需通过 `self.config` 即可获取已就绪的本地文件路径。 +- **安全性增强**: 统一的路径清洗和校验机制消除了插件开发者自行处理文件路径时可能引入的任意文件读写漏洞。 +- **AI 开发者适配**: 在为 AstrBot 编写插件 Schema 时,AI 助手应优先推荐使用 `type: "file"` 来处理外部资源依赖,而非要求用户手动填写绝对路径。注意,保存配置时必须通过 `validate_config` 校验,否则配置将无法持久化。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/plugin_config/hooks.md b/docs/snapshots/v4.23.6/plugin_config/hooks.md new file mode 100644 index 0000000..0ae6d6b --- /dev/null +++ b/docs/snapshots/v4.23.6/plugin_config/hooks.md @@ -0,0 +1,93 @@ +--- +category: plugin_config +--- + +# 事件钩子 (Hooks) + +事件钩子用于在 AstrBot 核心执行流程的关键节点介入(例如:LLM 请求前后、工具调用前后、发送消息前后)。 + +> 事件钩子是“插件事件系统”的一部分,和 Agent 运行钩子(`BaseAgentRunHooks`)不是同一套机制。 +> Agent 运行钩子见:`docs/agent/agent-related-hooks.md` + +## 使用方式 + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.on_astrbot_loaded() +async def on_loaded(self): + ... +``` + +### 重要限制与最佳实践 + +- 事件钩子通常不支持与指令/过滤器混用(例如:`@filter.command`、`@filter.command_group`、`@filter.event_message_type`、`@filter.platform_adapter_type`、`@filter.permission_type`)。 +- 大多数事件钩子不建议用 `yield` 发送消息:如果要发消息,请直接调用 `await event.send(...)`。 +- 事件钩子应当短小、幂等、可失败(失败只影响当前 hook,不应导致整个机器人崩溃)。 + +## 核心钩子清单 + +下面列出对外暴露的常用事件钩子(按执行流程排序)。签名写法以核心实现为准(参考源码:`astrbotcore/astrbot/core/star/register/star_handler.py`)。 + +### 1) 生命周期 + +- `@filter.on_astrbot_loaded()` + - 触发:AstrBot 加载完成 + - 建议签名:`async def on_astrbot_loaded(self) -> None` + +- `@filter.on_platform_loaded()` + - 触发:平台加载完成 + - 建议签名:`async def on_platform_loaded(self) -> None` + +### 2) LLM 请求前后 + +- `@filter.on_waiting_llm_request()` + - 触发:确定要调用 LLM,但尚未获取会话锁之前(适合提示“思考中/排队中”) + - 建议签名:`async def on_waiting_llm(self, event: AstrMessageEvent) -> None` + +- `@filter.on_llm_request()` + - 触发:LLM 请求发送前(可修改请求内容) + - 建议签名:`async def on_llm_request(self, event: AstrMessageEvent, request: ProviderRequest) -> None` + - 常见用途: + - 注入/调整 `system_prompt` + - 根据平台/会话动态切换模型或工具策略(请保持可解释、可回滚) + +- `@filter.on_llm_response()` + - 触发:LLM 请求完成后(可读取/修饰返回结果) + - 建议签名:`async def on_llm_response(self, event: AstrMessageEvent, response: LLMResponse) -> None` + +### 3) 工具调用前后(Function Calling / Tools Use) + +- `@filter.on_using_llm_tool()` + - 触发:函数工具调用前 + - 建议签名:`async def on_using_tool(self, event: AstrMessageEvent, tool: FunctionTool, tool_args: dict | None) -> None` + - 常见用途: + - 记录审计日志 / 埋点 + - 根据会话状态拒绝某些工具(需配合工具层做硬限制) + +- `@filter.on_llm_tool_respond()` + - 触发:函数工具调用后 + - 建议签名:`async def on_tool_respond(self, event: AstrMessageEvent, tool: FunctionTool, tool_args: dict | None, tool_result: CallToolResult | None) -> None` + - 常见用途: + - 对工具结果做脱敏/裁剪 + - 追加提示,让 LLM 更好地总结工具输出 + +### 4) 发送消息前后 + +- `@filter.on_decorating_result()` + - 触发:发送消息前(用于“装饰”即将发送的消息链) + - 建议签名:`async def on_decorating_result(self, event: AstrMessageEvent) -> None` + - 常见用途: + - 文转图、追加后缀、统一格式化输出 + - 注意:这里的职责是“改 `event.get_result().chain`”,不是发消息;如需主动发送请使用 `event.send()`。 + +- `@filter.after_message_sent()` + - 触发:消息成功发送到平台后 + - 建议签名:`async def after_message_sent(self, event: AstrMessageEvent) -> None` + +## 相关文档与源码 + +- Agent 运行钩子:`docs/agent/agent-related-hooks.md` +- `filter` 对外导出位置:`astrbotcore/astrbot/api/event/filter/__init__.py` +- Hook 注册实现:`astrbotcore/astrbot/core/star/register/star_handler.py` +- 事件类型枚举:`astrbotcore/astrbot/core/star/star_handler.py` diff --git a/docs/snapshots/v4.23.6/plugin_config/lifecycle.md b/docs/snapshots/v4.23.6/plugin_config/lifecycle.md new file mode 100644 index 0000000..67807d3 --- /dev/null +++ b/docs/snapshots/v4.23.6/plugin_config/lifecycle.md @@ -0,0 +1,32 @@ +--- +category: plugin_config +--- + +# 插件生命周期 (Lifecycle) + +AstrBot 的插件系统基于**运行时注入**和**动态加载**机制。 + +### 插件结构规范 + +一个标准的 AstrBot 插件(Star)通常包含: +- `main.py`: 插件入口,包含继承自 `Star` 的类。 +- `metadata.yaml`: 插件元数据(ID、名称、版本、作者等)。 +- `_conf_schema.json`: 可选,配置 Schema。 +- `requirements.txt`: 可选,依赖定义。 +- `logo.png`: 可选,图标。 + +### 加载流程 + +1. **扫描**: 扫描 `data/plugins` 目录。 +2. **解析**: 读取元数据。 +3. **依赖校验**: 检查并提示缺失依赖。 +4. **实例化**: + - 解析 `_conf_schema.json` 并加载配置实体 `AstrBotConfig`。 + - 实例化插件类,注入 `Context` 和 `AstrBotConfig`。 +5. **注册**: 扫描 `@filter` 装饰器方法并注册到事件分发中心。 + +### 卸载与重载 + +- 调用插件实例的 `terminate()` 异步方法。 +- 清除事件分发中心中该插件的所有 Handler。 +- 允许在不重启 Bot 的情况下动态重载。 diff --git a/docs/snapshots/v4.23.6/plugin_config/schema.md b/docs/snapshots/v4.23.6/plugin_config/schema.md new file mode 100644 index 0000000..9daca24 --- /dev/null +++ b/docs/snapshots/v4.23.6/plugin_config/schema.md @@ -0,0 +1,81 @@ +--- +category: plugin_config +--- + +# 配置 Schema (`_conf_schema.json`) + +AstrBot 通过 Schema 实现配置的自动解析与 WebUI 可视化渲染。 + +### 配置定义 + +在插件目录下添加 `_conf_schema.json` 文件,定义配置项的 Schema。 + +### 字段说明 + +| 字段名 | 说明 | +| :--- | :--- | +| `type` | **必填**。支持 `string`, `text`, `int`, `float`, `bool`, `object`, `list`, `dict`, `template_list` | +| `description` | 配置描述 | +| `hint` | 提示语,右侧问号图标悬浮显示 | +| `obvious_hint` | 是否醒目显示 | +| `default` | 默认值 | +| `options` | 下拉列表可选项 | +| `items` | `object` 类型的子 Schema | +| `editor_mode` | 启用代码编辑器 (Monaco Editor) | +| `editor_language` | 代码编辑器语言,默认 `json` | +| `editor_theme` | 代码编辑器主题,`vs-light` 或 `vs-dark` | +| `_special` | 调用内置数据:`select_provider`, `select_provider_tts`, `select_provider_stt`, `select_persona` | +| `invisible` | 是否隐藏,默认 `false` | + +### 高级类型 + +- **`text`**: 多行文本输入 +- **`dict`**: 键值对配置,支持 `template_schema` 定义子项 +- **`template_list`**: 多组重复配置(v4.10.4+) + +### `template_list` 类型 + +用于保存多组重复配置,如多个 API 供应商或多套人设。 + +```json +{ + "providers": { + "type": "template_list", + "description": "API 供应商列表", + "templates": { + "openai": { + "name": "OpenAI", + "items": { + "api_key": {"description": "API Key", "type": "string", "default": "sk-xxxx"}, + "model": {"description": "模型名称", "type": "string", "default": "gpt-3.5-turbo"} + } + } + } + } +} +``` + +存储格式(包含 `__template_key` 字段): + +```json +{ + "providers": [ + {"__template_key": "openai", "api_key": "sk-xxxx", "model": "gpt-3.5-turbo"} + ] +} +``` + +### 在插件中使用 + +```python +from astrbot.api import AstrBotConfig + +@register("config", "Soulter", "一个配置示例", "1.0.0") +class ConfigPlugin(Star): + def __init__(self, context: Context, config: AstrBotConfig): + super().__init__(context) + self.config = config + # self.config.save_config() # 保存配置 +``` + +配置更新时,AstrBot 会自动添加缺失的默认值、移除不存在的配置项。 diff --git a/docs/snapshots/v4.23.6/plugin_config/session_control.md b/docs/snapshots/v4.23.6/plugin_config/session_control.md new file mode 100644 index 0000000..bdf93f3 --- /dev/null +++ b/docs/snapshots/v4.23.6/plugin_config/session_control.md @@ -0,0 +1,31 @@ +--- +category: plugin_config +--- + +# 会话控制 (Session Control) + +`session_waiter` 是实现多轮对话状态机的核心机制,常用于问答、验证或连续操作。 + +### 核心装饰器 + +- **`@session_waiter(timeout: float, record_history_chains: bool = False)`** + - 用于定义一个等待用户进一步输入的异步函数。 + - 超时会抛出 `TimeoutError`。 + +### SessionController 接口 + +Waiter 函数通常接收一个 `controller: SessionController` 参数: + +- `keep(timeout, reset_timeout)`: 保持会话拦截。`reset_timeout=True` 将重置计时。 +- `stop()`: 立即终止拦截,恢复正常消息分发。 +- `get_history_chains()`: 获取拦截期间的消息历史。 + +### SessionFilter (自定义会话隔离) + +通过继承 `SessionFilter` 并重写 `filter` 方法,可以自定义拦截的范围(如按群组拦截): + +```python +class GroupFilter(SessionFilter): + def filter(self, event: AstrMessageEvent) -> str: + return event.get_group_id() or event.unified_msg_origin +``` diff --git a/docs/snapshots/v4.23.6/pnpm-lock.yaml b/docs/snapshots/v4.23.6/pnpm-lock.yaml new file mode 100644 index 0000000..acc6202 --- /dev/null +++ b/docs/snapshots/v4.23.6/pnpm-lock.yaml @@ -0,0 +1,1554 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + vue: + specifier: ^3.5.17 + version: 3.5.17 + devDependencies: + vitepress: + specifier: ^1.6.4 + version: 1.6.4(@algolia/client-search@5.31.0)(postcss@8.5.6)(search-insights@2.17.3) + +packages: + + '@algolia/autocomplete-core@1.17.7': + resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7': + resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.7': + resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.7': + resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.31.0': + resolution: {integrity: sha512-J+wZq5uotbisEsbKmXv79dsENI/AW6IZWIvfTqebE6QcH/S2yGDeNh6b4qa4koJ1eQx7+wKkLMfZ+nOZpBWclA==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.31.0': + resolution: {integrity: sha512-zxz9ooi6HsMG7gS7xCG9NkUlWkpwMT/oYr8+cojchB98pEmn3OqHA7KaY1w8GKqKXNM4MiQD15N2/aZhDa9b9g==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.31.0': + resolution: {integrity: sha512-lO6oZLEPiCgtUcUHIFyfrRvcS8iB3Je1LqW3c04anjrCO7dqhkccXHC/5XuH0fIW4l7V5AtbPS2tpJGtRp1NJw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.31.0': + resolution: {integrity: sha512-gwWTW4CMM6pov3aJv2a+Ex4v7fWG9wtey43qWBq5rABk3p3uYYFkzfylrht18rcq1zA99Wxo8UEireExHuzs2w==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.31.0': + resolution: {integrity: sha512-3G8ZpoLCgrcuILTQGVU9WXxUmK4R8uUmAiU31Qqd/pkta/9J8DHQjNh+Fs/i27ls2YxQq36GqXvVM2eoQFmFJw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.31.0': + resolution: {integrity: sha512-+YIHy+n+x2/DqRdnrPv2Eck2pbZ4Q5Lu1mWpwOUZ2u2XG6JVQx0goePomtYl8evsDGspDRZJPpGD+CFJboe0gQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.31.0': + resolution: {integrity: sha512-2I79ICkuTqbXeK5RGSmzCN1Uj86NghWxaWt41lIcFk1OXuUWhyXTxC2fN5M8ASRBf/qWSeXr6AzL8jb3opya3g==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.31.0': + resolution: {integrity: sha512-HiBWdO7ztzgFoR+SnbHq0iBQtDUusRZPSVMkPIR/MNbNJrH/OhrCsxk6Y7dUvQAIjypKmFl38raf1XEKz9fdUA==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.31.0': + resolution: {integrity: sha512-ifrQ3BMg7Z4EGBPouUINd7xVU2ySTrJ2FtuAoiRHaZ7rT1Kp56JW40kuHiCvmDI4ZBaIzrQuGxWYKUZ29QWR6g==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.31.0': + resolution: {integrity: sha512-dA94TKQ9FiZ8E1BlpfAMVKC3XimhDBjNFLPR3w5eRgSXymJbbK93xr/LrhyCWHbJPxtUcJvaO+Xg0pFKP+HZvw==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.31.0': + resolution: {integrity: sha512-akbqE63Scw3dttQatKhjiHdFXpqihCCpcAciIHpdebw3/zWfb+e/Tkf6tDv/05AGcG5BHC365dp8LIl9+NchSA==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.31.0': + resolution: {integrity: sha512-qYOEOCIqXvbVKNTabgKmPFltpNxB1U38hhrMEbypyOc/X9zjdxnVi/dqZ+jKsYY4X7MSQTtowLK4AR++OdMD/g==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.31.0': + resolution: {integrity: sha512-eq8uTVUc/E7YIOqTVfXgGQ3ZSsAWqZZHy5ntuwm6WxnvdcAyhyzRo0sncX1zWFkFpNGvJ8xyONDWq/Ef2e31Tg==} + engines: {node: '>= 14.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.0': + resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + engines: {node: '>=6.9.0'} + + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} + + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} + + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@iconify-json/simple-icons@1.2.42': + resolution: {integrity: sha512-G/EED0hUV1wMNUsWaFdQYLibm6SO7rP2GZP1+CvhszB5WAFYYibD3zoWp3X96xSIWpYQFvccvE17ewpd0Q1hWQ==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@rollup/rollup-android-arm-eabi@4.44.2': + resolution: {integrity: sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.44.2': + resolution: {integrity: sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.44.2': + resolution: {integrity: sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.44.2': + resolution: {integrity: sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.44.2': + resolution: {integrity: sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.44.2': + resolution: {integrity: sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.44.2': + resolution: {integrity: sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.44.2': + resolution: {integrity: sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.44.2': + resolution: {integrity: sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.44.2': + resolution: {integrity: sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.44.2': + resolution: {integrity: sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': + resolution: {integrity: sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.44.2': + resolution: {integrity: sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.44.2': + resolution: {integrity: sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.44.2': + resolution: {integrity: sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.44.2': + resolution: {integrity: sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.44.2': + resolution: {integrity: sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.44.2': + resolution: {integrity: sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.44.2': + resolution: {integrity: sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.44.2': + resolution: {integrity: sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==} + cpu: [x64] + os: [win32] + + '@shikijs/core@2.5.0': + resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} + + '@shikijs/engine-javascript@2.5.0': + resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} + + '@shikijs/engine-oniguruma@2.5.0': + resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} + + '@shikijs/langs@2.5.0': + resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} + + '@shikijs/themes@2.5.0': + resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} + + '@shikijs/transformers@2.5.0': + resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} + + '@shikijs/types@2.5.0': + resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.17': + resolution: {integrity: sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==} + + '@vue/compiler-dom@3.5.17': + resolution: {integrity: sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==} + + '@vue/compiler-sfc@3.5.17': + resolution: {integrity: sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==} + + '@vue/compiler-ssr@3.5.17': + resolution: {integrity: sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==} + + '@vue/devtools-api@7.7.7': + resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} + + '@vue/devtools-kit@7.7.7': + resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} + + '@vue/devtools-shared@7.7.7': + resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} + + '@vue/reactivity@3.5.17': + resolution: {integrity: sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==} + + '@vue/runtime-core@3.5.17': + resolution: {integrity: sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==} + + '@vue/runtime-dom@3.5.17': + resolution: {integrity: sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==} + + '@vue/server-renderer@3.5.17': + resolution: {integrity: sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==} + peerDependencies: + vue: 3.5.17 + + '@vue/shared@3.5.17': + resolution: {integrity: sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==} + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + + '@vueuse/integrations@12.8.2': + resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + + algoliasearch@5.31.0: + resolution: {integrity: sha512-LBpwGyNPOcprdu1OnRtgaWeKLjnDR3T+vp64WRiQEgHYACIXgU+djAvj88m3OQc+6MfWbw7rKUjXtdRMLfU7Aw==} + engines: {node: '>= 14.0.0'} + + birpc@2.4.0: + resolution: {integrity: sha512-5IdNxTyhXHv2UlgnPHQ0h+5ypVmkrYHzL8QT+DwFZ//2N/oNV8Ch+BCRmTJ3x6/z9Axo/cXYBc9eprsUVK/Jsg==} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + focus-trap@7.6.5: + resolution: {integrity: sha512-7Ke1jyybbbPZyZXFxEftUtxFGLMpE2n6A+z//m4CRDlj0hW+o3iYSmh8nFlYMurOiJVDmJRilUQtJr08KfIxlg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + mdast-util-to-hast@13.2.0: + resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + minisearch@7.1.2: + resolution: {integrity: sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + oniguruma-to-es@3.1.1: + resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.26.9: + resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.0.1: + resolution: {integrity: sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rollup@4.44.2: + resolution: {integrity: sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + shiki@2.5.0: + resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + unist-util-is@6.0.0: + resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.1: + resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + + unist-util-visit@5.0.0: + resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + + vfile-message@4.0.2: + resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@5.4.19: + resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitepress@1.6.4: + resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + + vue@3.5.17: + resolution: {integrity: sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.31.0)(algoliasearch@5.31.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.31.0)(algoliasearch@5.31.0)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.31.0)(algoliasearch@5.31.0) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.31.0)(algoliasearch@5.31.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.31.0)(algoliasearch@5.31.0) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.31.0)(algoliasearch@5.31.0)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.31.0)(algoliasearch@5.31.0) + '@algolia/client-search': 5.31.0 + algoliasearch: 5.31.0 + + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.31.0)(algoliasearch@5.31.0)': + dependencies: + '@algolia/client-search': 5.31.0 + algoliasearch: 5.31.0 + + '@algolia/client-abtesting@5.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + '@algolia/requester-browser-xhr': 5.31.0 + '@algolia/requester-fetch': 5.31.0 + '@algolia/requester-node-http': 5.31.0 + + '@algolia/client-analytics@5.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + '@algolia/requester-browser-xhr': 5.31.0 + '@algolia/requester-fetch': 5.31.0 + '@algolia/requester-node-http': 5.31.0 + + '@algolia/client-common@5.31.0': {} + + '@algolia/client-insights@5.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + '@algolia/requester-browser-xhr': 5.31.0 + '@algolia/requester-fetch': 5.31.0 + '@algolia/requester-node-http': 5.31.0 + + '@algolia/client-personalization@5.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + '@algolia/requester-browser-xhr': 5.31.0 + '@algolia/requester-fetch': 5.31.0 + '@algolia/requester-node-http': 5.31.0 + + '@algolia/client-query-suggestions@5.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + '@algolia/requester-browser-xhr': 5.31.0 + '@algolia/requester-fetch': 5.31.0 + '@algolia/requester-node-http': 5.31.0 + + '@algolia/client-search@5.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + '@algolia/requester-browser-xhr': 5.31.0 + '@algolia/requester-fetch': 5.31.0 + '@algolia/requester-node-http': 5.31.0 + + '@algolia/ingestion@1.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + '@algolia/requester-browser-xhr': 5.31.0 + '@algolia/requester-fetch': 5.31.0 + '@algolia/requester-node-http': 5.31.0 + + '@algolia/monitoring@1.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + '@algolia/requester-browser-xhr': 5.31.0 + '@algolia/requester-fetch': 5.31.0 + '@algolia/requester-node-http': 5.31.0 + + '@algolia/recommend@5.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + '@algolia/requester-browser-xhr': 5.31.0 + '@algolia/requester-fetch': 5.31.0 + '@algolia/requester-node-http': 5.31.0 + + '@algolia/requester-browser-xhr@5.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + + '@algolia/requester-fetch@5.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + + '@algolia/requester-node-http@5.31.0': + dependencies: + '@algolia/client-common': 5.31.0 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.0 + + '@babel/types@7.28.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@docsearch/css@3.8.2': {} + + '@docsearch/js@3.8.2(@algolia/client-search@5.31.0)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 3.8.2(@algolia/client-search@5.31.0)(search-insights@2.17.3) + preact: 10.26.9 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + + '@docsearch/react@3.8.2(@algolia/client-search@5.31.0)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.31.0)(algoliasearch@5.31.0)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.31.0)(algoliasearch@5.31.0) + '@docsearch/css': 3.8.2 + algoliasearch: 5.31.0 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@iconify-json/simple-icons@1.2.42': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@rollup/rollup-android-arm-eabi@4.44.2': + optional: true + + '@rollup/rollup-android-arm64@4.44.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.44.2': + optional: true + + '@rollup/rollup-darwin-x64@4.44.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.44.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.44.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.44.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.44.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.44.2': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.44.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.44.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.44.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.44.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.44.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.44.2': + optional: true + + '@shikijs/core@2.5.0': + dependencies: + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 3.1.1 + + '@shikijs/engine-oniguruma@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/themes@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/transformers@2.5.0': + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/types': 2.5.0 + + '@shikijs/types@2.5.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.0': {} + + '@vitejs/plugin-vue@5.2.4(vite@5.4.19)(vue@3.5.17)': + dependencies: + vite: 5.4.19 + vue: 3.5.17 + + '@vue/compiler-core@3.5.17': + dependencies: + '@babel/parser': 7.28.0 + '@vue/shared': 3.5.17 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.17': + dependencies: + '@vue/compiler-core': 3.5.17 + '@vue/shared': 3.5.17 + + '@vue/compiler-sfc@3.5.17': + dependencies: + '@babel/parser': 7.28.0 + '@vue/compiler-core': 3.5.17 + '@vue/compiler-dom': 3.5.17 + '@vue/compiler-ssr': 3.5.17 + '@vue/shared': 3.5.17 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.17': + dependencies: + '@vue/compiler-dom': 3.5.17 + '@vue/shared': 3.5.17 + + '@vue/devtools-api@7.7.7': + dependencies: + '@vue/devtools-kit': 7.7.7 + + '@vue/devtools-kit@7.7.7': + dependencies: + '@vue/devtools-shared': 7.7.7 + birpc: 2.4.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.7': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.17': + dependencies: + '@vue/shared': 3.5.17 + + '@vue/runtime-core@3.5.17': + dependencies: + '@vue/reactivity': 3.5.17 + '@vue/shared': 3.5.17 + + '@vue/runtime-dom@3.5.17': + dependencies: + '@vue/reactivity': 3.5.17 + '@vue/runtime-core': 3.5.17 + '@vue/shared': 3.5.17 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.17(vue@3.5.17)': + dependencies: + '@vue/compiler-ssr': 3.5.17 + '@vue/shared': 3.5.17 + vue: 3.5.17 + + '@vue/shared@3.5.17': {} + + '@vueuse/core@12.8.2': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.17 + transitivePeerDependencies: + - typescript + + '@vueuse/integrations@12.8.2(focus-trap@7.6.5)': + dependencies: + '@vueuse/core': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.17 + optionalDependencies: + focus-trap: 7.6.5 + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.8.2': {} + + '@vueuse/shared@12.8.2': + dependencies: + vue: 3.5.17 + transitivePeerDependencies: + - typescript + + algoliasearch@5.31.0: + dependencies: + '@algolia/client-abtesting': 5.31.0 + '@algolia/client-analytics': 5.31.0 + '@algolia/client-common': 5.31.0 + '@algolia/client-insights': 5.31.0 + '@algolia/client-personalization': 5.31.0 + '@algolia/client-query-suggestions': 5.31.0 + '@algolia/client-search': 5.31.0 + '@algolia/ingestion': 1.31.0 + '@algolia/monitoring': 1.31.0 + '@algolia/recommend': 5.31.0 + '@algolia/requester-browser-xhr': 5.31.0 + '@algolia/requester-fetch': 5.31.0 + '@algolia/requester-node-http': 5.31.0 + + birpc@2.4.0: {} + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + comma-separated-tokens@2.0.3: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + csstype@3.1.3: {} + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + emoji-regex-xs@1.0.0: {} + + entities@4.5.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + estree-walker@2.0.2: {} + + focus-trap@7.6.5: + dependencies: + tabbable: 6.2.0 + + fsevents@2.3.3: + optional: true + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + html-void-elements@3.0.0: {} + + is-what@4.1.16: {} + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + mark.js@8.11.1: {} + + mdast-util-to-hast@13.2.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.0.0 + vfile: 6.0.3 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + minisearch@7.1.2: {} + + mitt@3.0.1: {} + + nanoid@3.3.11: {} + + oniguruma-to-es@3.1.1: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 6.0.1 + regex-recursion: 6.0.2 + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.26.9: {} + + property-information@7.1.0: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.0.1: + dependencies: + regex-utilities: 2.3.0 + + rfdc@1.4.1: {} + + rollup@4.44.2: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.44.2 + '@rollup/rollup-android-arm64': 4.44.2 + '@rollup/rollup-darwin-arm64': 4.44.2 + '@rollup/rollup-darwin-x64': 4.44.2 + '@rollup/rollup-freebsd-arm64': 4.44.2 + '@rollup/rollup-freebsd-x64': 4.44.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.2 + '@rollup/rollup-linux-arm-musleabihf': 4.44.2 + '@rollup/rollup-linux-arm64-gnu': 4.44.2 + '@rollup/rollup-linux-arm64-musl': 4.44.2 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.2 + '@rollup/rollup-linux-riscv64-gnu': 4.44.2 + '@rollup/rollup-linux-riscv64-musl': 4.44.2 + '@rollup/rollup-linux-s390x-gnu': 4.44.2 + '@rollup/rollup-linux-x64-gnu': 4.44.2 + '@rollup/rollup-linux-x64-musl': 4.44.2 + '@rollup/rollup-win32-arm64-msvc': 4.44.2 + '@rollup/rollup-win32-ia32-msvc': 4.44.2 + '@rollup/rollup-win32-x64-msvc': 4.44.2 + fsevents: 2.3.3 + + search-insights@2.17.3: {} + + shiki@2.5.0: + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/langs': 2.5.0 + '@shikijs/themes': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + tabbable@6.2.0: {} + + trim-lines@3.0.1: {} + + unist-util-is@6.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.1: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + + unist-util-visit@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + + vfile-message@4.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.2 + + vite@5.4.19: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.44.2 + optionalDependencies: + fsevents: 2.3.3 + + vitepress@1.6.4(@algolia/client-search@5.31.0)(postcss@8.5.6)(search-insights@2.17.3): + dependencies: + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.31.0)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.42 + '@shikijs/core': 2.5.0 + '@shikijs/transformers': 2.5.0 + '@shikijs/types': 2.5.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 5.2.4(vite@5.4.19)(vue@3.5.17) + '@vue/devtools-api': 7.7.7 + '@vue/shared': 3.5.17 + '@vueuse/core': 12.8.2 + '@vueuse/integrations': 12.8.2(focus-trap@7.6.5) + focus-trap: 7.6.5 + mark.js: 8.11.1 + minisearch: 7.1.2 + shiki: 2.5.0 + vite: 5.4.19 + vue: 3.5.17 + optionalDependencies: + postcss: 8.5.6 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + + vue@3.5.17: + dependencies: + '@vue/compiler-dom': 3.5.17 + '@vue/compiler-sfc': 3.5.17 + '@vue/runtime-dom': 3.5.17 + '@vue/server-renderer': 3.5.17(vue@3.5.17) + '@vue/shared': 3.5.17 + + zwitch@2.0.4: {} diff --git a/docs/snapshots/v4.23.6/public/404-seio.png b/docs/snapshots/v4.23.6/public/404-seio.png new file mode 100644 index 0000000000000000000000000000000000000000..7c9774fc781700a290f3f89993b13bcb645662a4 GIT binary patch literal 195194 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ēb2IIhigVv-a}u^ewR1$w71&1cW?^gA1n>=Poa$f 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 DBwj~toiQ*-hO=Hy0Mw9|})MKannz3va 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 literal 0 HcmV?d00001 diff --git a/docs/snapshots/v4.23.6/public/logo.png b/docs/snapshots/v4.23.6/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..88d34417d88a6ab314a2871b6ff1837d5a24a1ac GIT binary patch literal 1467 zcmV;s1w{IZP)S$=AZ)#?G`uIf(!<`y|h`5`LsRlp}psFTP|H6*5u~gd-^@!@8|P9 z-}61c2zl{);ep(EprWGUSVu=kv)OF!F_}zf4&#CD_V)IZm6esR=*Fs{q2XP(+x?p? z%b`dlLUZxRvMjk=u95os`gaaOq;Pn6*cypMp5+3xz{WGH)%q17q##owh2!Jn*91y) z(hIz8x7$5J2r1M|!e+C<^En5qW^-_G@C!`@5cAJDY!n?Q%kpk*ZS7n8k{BBsbEt^wQD(by>>P+&5d&WRc#Y%Y5O6yVSPxsm{fZ`-X^`rt_@ z0Wutk(BA2!%(-bFMV&}@CiO<6@w8}OC@`DNeIjKy9&RN-!}F=n_jev80M5n3hnKml z(^xPV44-C200R~=&x2y`pG+;&o2y$10++b(VKTqm4JQ5s8i6BfJ-{nh8w0!H z2tC6>Y$DlH7q0vBhrqYD9!7ftp67|-on@b>=O?`M6c=tQI%FeVdH@3!F@JOQNg4r` zVp-_$<;cRp!$=nbsa|K_J93yJm+90BefB(8XZbZ&hotJJCs0pxT5_;D_Nc5rgz#GCGN=}#U6+N zKwp|m@w5Use&8knFYOu7$@*BPLeKm(AE5iq7g-Yk_*A3OZ)6%SL=>>EL>Qb%>R32IU=V7A!hi)OYdz;!hxPO0b~f|@vEL7?VDkl zMg&wuL{#skpU?OxW^11ccwjgX0Y1&4M_{o3x?j0x1RmylR1TU#SL-D@HTZN zz$?ajACpvHVh#}XOw7~2F0N7kKAz_F^Y6O#0I!t2W0FEP;l0eK)AO_b{F}%acvfEZ z#CB^Sk(y57c_zoo*?%|F_7m(srSedF00!q%(=C@OmBOZHDi&_var+4+;H>V@31F5^ z?AQQ4PT-&lG4W5k|>nwKtlX_)R_b6Nq_)4Fiwu zD!#8Rt*fq5)i(9mQffL@(Lw?%RR+$kvL9wL8H|B0;ikT|we>?S;IzmfjIc1n+D2`0 z);}AY$(>8-DOSl&b93`aA%WuJ;!;VHB&CMg)U7<0=>0A6!VEe$q+N=b=R-_n3)o6a zON&JWaNu>+>2&7p;jZ0o$Kmc#(a9X@3+w9Y-p|+L_3G;CxA(i~z*SDs(9lrcU2s?| zmM?)#EqqwdyhI2&;c~fLqFBszy~E*f023hu*!KOQ7hWcWygo26@Qp0X;ame|_HZj> zu~@z)gp|ZFX};_`5^r##rlzK0baeE0Ns@AQ&5CSex7$ans;b^2gdC4!0j7+8Xn+y_ zDm+k9Qu1a~Q`5&?U0uBfgQ35>yZeHk4|Ey~hO>>0jUN>i6}=JnI&1@jX3wWGL_=I} zBBXG8evA-;D;{_SF{$SRozM*%7_0ye+gXF>$v_Eu1PU>b4)cKx*wO_!Pa-eh{|5j` VH~KS_fMWmv002ovPDHLkV1jM)!Gr(+ literal 0 HcmV?d00001 diff --git a/docs/snapshots/v4.23.6/public/logo_prod.png b/docs/snapshots/v4.23.6/public/logo_prod.png new file mode 100644 index 0000000000000000000000000000000000000000..c1ec6bb6e71da5421489f18f4f918157d0b4c909 GIT binary patch literal 80735 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 jx is also accepted." + } + }, + "responses": { + "Unauthorized": { + "description": "Unauthorized" + }, + "Forbidden": { + "description": "Forbidden" + } + }, + "schemas": { + "ApiResponseEmpty": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + }, + "message": { + "type": [ + "string", + "null" + ] + }, + "data": { + "type": "object", + "additionalProperties": true + } + } + }, + "ApiResponseBotList": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + }, + "message": { + "type": [ + "string", + "null" + ] + }, + "data": { + "type": "object", + "properties": { + "bot_ids": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "ApiResponseUpload": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + }, + "message": { + "type": [ + "string", + "null" + ] + }, + "data": { + "type": "object", + "properties": { + "attachment_id": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + } + }, + "ApiResponseChatSessions": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + }, + "message": { + "type": [ + "string", + "null" + ] + }, + "data": { + "type": "object", + "properties": { + "sessions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatSessionItem" + } + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + } + } + }, + "ChatSessionItem": { + "type": "object", + "properties": { + "session_id": { + "type": "string" + }, + "platform_id": { + "type": "string" + }, + "creator": { + "type": "string" + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "is_group": { + "type": "integer" + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + } + } + }, + "MessagePart": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "plain", + "reply", + "image", + "record", + "file", + "video" + ] + }, + "text": { + "type": "string" + }, + "message_id": { + "type": [ + "string", + "integer" + ] + }, + "selected_text": { + "type": "string" + }, + "attachment_id": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": [ + "type" + ] + }, + "ChatSendRequest": { + "type": "object", + "required": [ + "message", + "username" + ], + "properties": { + "message": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessagePart" + } + } + ] + }, + "session_id": { + "type": "string", + "description": "Optional chat session ID. If omitted (and conversation_id is also omitted), server creates a UUID automatically." + }, + "conversation_id": { + "type": "string", + "description": "Alias of session_id." + }, + "username": { + "type": "string", + "description": "Target username." + }, + "selected_provider": { + "type": "string" + }, + "selected_model": { + "type": "string" + }, + "enable_streaming": { + "type": "boolean", + "default": true + }, + "config_id": { + "type": "string", + "description": "Optional AstrBot config file ID. If provided, the chat session will use this config file. Use \"default\" to reset to default config." + }, + "config_name": { + "type": "string", + "description": "Optional AstrBot config file name. Used only when config_id is not provided." + } + } + }, + "SendMessageRequest": { + "type": "object", + "required": [ + "umo", + "message" + ], + "properties": { + "umo": { + "type": "string", + "description": "Unified message origin. Format: platform:message_type:session_id" + }, + "message": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "$ref": "#/components/schemas/MessagePart" + } + } + ] + } + } + }, + "ChatConfigFile": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "is_default": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "path", + "is_default" + ] + }, + "ApiResponseChatConfigList": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "ok" + }, + "message": { + "type": [ + "string", + "null" + ] + }, + "data": { + "type": "object", + "properties": { + "configs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ChatConfigFile" + } + } + } + } + } + } + } + } +} diff --git a/docs/snapshots/v4.23.6/public/scalar.html b/docs/snapshots/v4.23.6/public/scalar.html new file mode 100644 index 0000000..607421d --- /dev/null +++ b/docs/snapshots/v4.23.6/public/scalar.html @@ -0,0 +1,25 @@ + + + + + + AstrBot OpenAPI - Scalar + + + +

+ + + + diff --git a/docs/snapshots/v4.23.6/scripts/sync_docs_to_wiki.py b/docs/snapshots/v4.23.6/scripts/sync_docs_to_wiki.py new file mode 100644 index 0000000..f717f1f --- /dev/null +++ b/docs/snapshots/v4.23.6/scripts/sync_docs_to_wiki.py @@ -0,0 +1,644 @@ +from __future__ import annotations + +import argparse +import posixpath +import re +from dataclasses import dataclass +from pathlib import Path, PurePosixPath + +TITLE_RE = re.compile(r"^#\s+(.+)$", re.MULTILINE) +FENCED_BLOCK_RE = re.compile( + r"(^```.*?$.*?^```$|^~~~.*?$.*?^~~~$)", + re.MULTILINE | re.DOTALL, +) +INLINE_CODE_RE = re.compile(r"(`[^`]*`)") +MANIFEST_NAME = ".astrbot-wiki-sync-manifest" +SOURCE_ALIASES = { + "zh/config/providers/start.md": "zh/providers/start.md", + "en/config/providers/start.md": "en/providers/start.md", +} +LANG_CONFIG = { + "zh": { + "index_title": "# AstrBot 中文文档", + "index_intro": "该页面由 `AstrBot-docs` 自动同步到 GitHub Wiki。", + "index_links": [ + ("关于 AstrBot", "zh-what-is-astrbot"), + ("社区", "zh-community"), + ("常见问题", "zh-faq"), + ], + "home_intro": "该 Wiki 由 `AstrBot-docs` 自动同步生成。", + "home_links": [ + ("中文文档入口", "zh-index"), + ("English Docs", "Home-en"), + ], + "sidebar_language_label": "Chinese", + "sidebar_home_label": "首页", + "sidebar_home_target": "Home", + "sidebar_docs_entry_label": "文档入口", + }, + "en": { + "index_title": "# AstrBot English Documentation", + "index_intro": "This page is synchronized automatically from `AstrBot-docs` to the GitHub wiki.", + "index_links": [ + ("What is AstrBot", "en-what-is-astrbot"), + ("Community", "en-community"), + ("FAQ", "en-faq"), + ], + "home_intro": "This wiki is synchronized automatically from `AstrBot-docs`.", + "home_links": [ + ("English docs entry", "en-index"), + ("中文文档入口", "Home"), + ], + "sidebar_language_label": "English", + "sidebar_home_label": "Home", + "sidebar_home_target": "Home-en", + "sidebar_docs_entry_label": "Docs Entry", + }, +} + + +@dataclass +class PageInfo: + source_path: str + page_name: str + title: str + content: str + language: str + group: str + is_index: bool + + +@dataclass +class ResolutionResult: + resolved_path: str | None + ambiguous_matches: tuple[str, ...] = () + + +@dataclass +class MarkdownLink: + start: int + end: int + prefix: str + target: str + suffix: str + + +@dataclass +class Segment: + kind: str + text: str + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[1] + + +def discover_source_pages(source_root: str) -> tuple[str, ...]: + root = Path(source_root) + pages = [] + for language in ("zh", "en"): + language_root = root / language + if not language_root.exists(): + continue + for path in language_root.rglob("*.md"): + pages.append(path.relative_to(root).as_posix()) + return tuple(sorted(pages)) + + +def find_label_end(content: str, label_start: int) -> int: + index = label_start + 1 + while index < len(content): + close = content.find("]", index) + if close == -1: + return -1 + if close > label_start and content[close - 1] == "\\": + index = close + 1 + continue + lookahead = close + 1 + while lookahead < len(content) and content[lookahead].isspace(): + lookahead += 1 + if lookahead < len(content) and content[lookahead] == "(": + return close + index = close + 1 + return -1 + + +def find_target_end(content: str, target_start: int) -> int: + depth = 0 + index = target_start + while index < len(content): + character = content[index] + if character == "\\": + index += 2 + continue + if character == "(": + depth += 1 + elif character == ")": + if depth == 0: + return index + depth -= 1 + index += 1 + return -1 + + +def iter_markdown_links(content: str): + """Yield inline Markdown links only. + + This scanner intentionally handles inline `[]()` links used in the docs tree. + It does not parse reference-style links or arbitrary HTML. + """ + + index = 0 + while index < len(content): + label_start = content.find("[", index) + if label_start == -1: + break + + link_start = ( + label_start - 1 + if label_start > 0 and content[label_start - 1] == "!" + else label_start + ) + label_end = find_label_end(content, label_start) + if label_end == -1: + index = label_start + 1 + continue + + target_start = label_end + 1 + while target_start < len(content) and content[target_start].isspace(): + target_start += 1 + if target_start >= len(content) or content[target_start] != "(": + index = label_end + 1 + continue + target_start += 1 + target_end = find_target_end(content, target_start) + if target_end == -1: + index = label_end + 1 + continue + + yield MarkdownLink( + start=link_start, + end=target_end + 1, + prefix=content[link_start:target_start], + target=content[target_start:target_end], + suffix=")", + ) + index = target_end + 1 + + +def split_anchor(target: str) -> tuple[str, str]: + if "#" not in target: + return target, "" + base, anchor = target.split("#", 1) + return base, f"#{anchor}" + + +def prepare_candidate_path(path: PurePosixPath) -> PurePosixPath: + if not path.suffix: + path = path.with_suffix(".md") + + normalized = PurePosixPath(posixpath.normpath(path.as_posix())) + normalized_text = normalized.as_posix() + aliased = SOURCE_ALIASES.get(normalized_text, normalized_text) + return PurePosixPath(aliased) + + +def language_for_source(source_path: str) -> str: + return PurePosixPath(source_path).parts[0] + + +def parse_doc_target(target: str) -> tuple[str, str] | None: + if target.startswith(("http://", "https://", "mailto:", "#")): + return None + + base_target, anchor = split_anchor(target) + if not base_target: + return None + + suffix = PurePosixPath(base_target).suffix.lower() + if suffix and suffix != ".md": + return None + + return base_target, anchor + + +def find_existing_source_path( + candidate: PurePosixPath, + source_root: Path, + source_pages: tuple[str, ...], +) -> ResolutionResult: + candidate_text = candidate.as_posix() + if (source_root / candidate_text).exists(): + return ResolutionResult(resolved_path=candidate_text) + + language = candidate.parts[0] if candidate.parts else "" + suffix = ( + PurePosixPath(*candidate.parts[1:]).as_posix() + if len(candidate.parts) > 1 + else "" + ) + if not suffix: + return ResolutionResult(resolved_path=None) + + prefix = f"{language}/" + full_suffix = f"{language}/{suffix}" + matches = [ + page + for page in source_pages + if page.startswith(prefix) + and (page == full_suffix or page.endswith(f"/{suffix}")) + ] + if len(matches) == 1: + return ResolutionResult(resolved_path=matches[0]) + if len(matches) > 1: + return ResolutionResult( + resolved_path=None, + ambiguous_matches=tuple(sorted(matches)), + ) + return ResolutionResult(resolved_path=None) + + +def resolve_link_path( + base_target: str, + source_path: str, + source_root: Path, + source_pages: tuple[str, ...], +) -> ResolutionResult: + source_language = language_for_source(source_path) + + if base_target.startswith("/"): + target = base_target.lstrip("/") + if not target: + candidate = PurePosixPath(source_language) / "index.md" + elif target in {"en", "en/"}: + candidate = PurePosixPath("en") / "index.md" + elif target in {"zh", "zh/"}: + candidate = PurePosixPath("zh") / "index.md" + elif target.startswith(("en/", "zh/")): + candidate = PurePosixPath(target) + else: + language_root = source_language if source_language == "en" else "zh" + candidate = PurePosixPath(language_root) / target + else: + candidate = PurePosixPath(source_path).parent / base_target + + candidate = prepare_candidate_path(candidate) + return find_existing_source_path(candidate, source_root, source_pages) + + +class LinkResolver: + def __init__(self, source_root: Path): + self.source_root = Path(source_root) + self.source_pages = discover_source_pages(str(self.source_root)) + + def resolve_base_target( + self, base_target: str, source_path: str + ) -> ResolutionResult: + return resolve_link_path( + base_target=base_target, + source_path=source_path, + source_root=self.source_root, + source_pages=self.source_pages, + ) + + def resolve_markdown_target( + self, target: str, source_path: str + ) -> tuple[str | None, str]: + parsed_target = parse_doc_target(target) + if parsed_target is None: + return None, "" + + base_target, anchor = parsed_target + result = self.resolve_base_target(base_target, source_path) + return result.resolved_path, anchor + + +def rewrite_link_target(target: str, source_path: str, resolver: LinkResolver) -> str: + resolved, anchor = resolver.resolve_markdown_target(target, source_path) + if resolved is None: + return target + + return f"{page_name_for_source(resolved)}{anchor}" + + +def rewrite_links_in_segment( + segment: str, + source_path: str, + resolver: LinkResolver, +) -> str: + links = list(iter_markdown_links(segment)) + if not links: + return segment + + result: list[str] = [] + previous_end = 0 + for link in links: + result.append(segment[previous_end : link.start]) + result.append( + f"{link.prefix}{rewrite_link_target(link.target, source_path, resolver)}{link.suffix}", + ) + previous_end = link.end + result.append(segment[previous_end:]) + return "".join(result) + + +def iter_segments(content: str): + last_end = 0 + for fenced in FENCED_BLOCK_RE.finditer(content): + before = content[last_end : fenced.start()] + if before: + last_inline_end = 0 + for inline in INLINE_CODE_RE.finditer(before): + if inline.start() > last_inline_end: + yield Segment("text", before[last_inline_end : inline.start()]) + yield Segment("inline_code", inline.group(0)) + last_inline_end = inline.end() + if last_inline_end < len(before): + yield Segment("text", before[last_inline_end:]) + + yield Segment("code_block", fenced.group(0)) + last_end = fenced.end() + + tail = content[last_end:] + if not tail: + return + + last_inline_end = 0 + for inline in INLINE_CODE_RE.finditer(tail): + if inline.start() > last_inline_end: + yield Segment("text", tail[last_inline_end : inline.start()]) + yield Segment("inline_code", inline.group(0)) + last_inline_end = inline.end() + if last_inline_end < len(tail): + yield Segment("text", tail[last_inline_end:]) + + +def rewrite_links( + content: str, + source_path: str, + resolver: LinkResolver, +) -> str: + output: list[str] = [] + for segment in iter_segments(content): + if segment.kind == "text": + output.append( + rewrite_links_in_segment( + segment.text, + source_path=source_path, + resolver=resolver, + ) + ) + continue + + output.append(segment.text) + + return "".join(output) + + +def find_unresolved_doc_links(source_root: Path) -> list[str]: + unresolved: list[str] = [] + root = Path(source_root) + resolver = LinkResolver(root) + + for source_path in resolver.source_pages: + content = (root / source_path).read_text(encoding="utf-8") + for link in iter_markdown_links(content): + resolved_path, _ = resolver.resolve_markdown_target( + link.target, source_path + ) + if resolved_path is not None: + continue + parsed_target = parse_doc_target(link.target) + if parsed_target is None: + continue + base_target, _ = parsed_target + resolution = resolver.resolve_base_target(base_target, source_path) + if resolution.ambiguous_matches: + unresolved.append( + f"{source_path} -> {link.target} (ambiguous: {', '.join(resolution.ambiguous_matches)})", + ) + continue + unresolved.append(f"{source_path} -> {link.target}") + + return unresolved + + +def check_unresolved_doc_links(source_root: Path) -> None: + unresolved = find_unresolved_doc_links(source_root) + if not unresolved: + return + + issues = "\n".join(f"- {item}" for item in unresolved) + raise ValueError(f"Unresolved internal doc links found:\n{issues}") + + +def page_name_for_source(source_path: str) -> str: + if not source_path.endswith(".md"): + raise ValueError(f"Unsupported source path: {source_path}") + return source_path[:-3].replace("/", "-") + + +def strip_frontmatter(content: str) -> str: + if not content.startswith("---\n"): + return content + + closing = content.find("\n---\n", 4) + if closing == -1: + return content + + return content[closing + 5 :].lstrip("\n") + + +def normalize_content(content: str) -> str: + stripped = content.rstrip() + if not stripped: + return "" + return f"{stripped}\n" + + +def default_title_for_source(source_path: str) -> str: + stem = PurePosixPath(source_path).stem + return stem.replace("-", " ") + + +def extract_title(content: str, source_path: str) -> str: + match = TITLE_RE.search(content) + if match: + return match.group(1).strip() + return default_title_for_source(source_path) + + +def build_language_index(language: str, page_names: set[str]) -> str: + config = LANG_CONFIG[language] + lines = [config["index_title"], "", config["index_intro"], ""] + + for label, page_name in config["index_links"]: + if page_name in page_names: + lines.append(f"- [{label}]({page_name})") + + return normalize_content("\n".join(lines)) + + +def build_home_page(language: str) -> str: + config = LANG_CONFIG[language] + lines = ["# AstrBot Wiki", "", config["home_intro"], ""] + for label, target in config["home_links"]: + lines.append(f"- [{label}]({target})") + return normalize_content("\n".join(lines)) + + +def build_sidebar(page_infos: list[PageInfo]) -> str: + lines: list[str] = [] + + for language in ("zh", "en"): + config = LANG_CONFIG[language] + infos = [ + info + for info in page_infos + if info.language == language and not info.is_index + ] + infos.sort(key=lambda info: info.source_path) + + lines.append(f"### {config['sidebar_language_label']}") + lines.append("") + lines.append( + f"- [{config['sidebar_home_label']}]({config['sidebar_home_target']})", + ) + lines.append( + f"- [{config['sidebar_docs_entry_label']}]({language}-index)", + ) + + grouped: dict[str, list[PageInfo]] = {} + for info in infos: + grouped.setdefault(info.group, []).append(info) + + for group_name in sorted(grouped): + lines.append(f"- {group_name}") + for info in grouped[group_name]: + lines.append(f" - [{info.title}]({info.page_name})") + + lines.append("") + + return normalize_content("\n".join(lines)) + + +def build_page_info( + source_root: Path, source_path: str, resolver: LinkResolver +) -> PageInfo: + source_file = source_root / source_path + content = source_file.read_text(encoding="utf-8") + content = strip_frontmatter(content) + content = rewrite_links(content, source_path=source_path, resolver=resolver) + content = normalize_content(content) + + relative = PurePosixPath(source_path) + parts = relative.parts + group = "Top Level" if len(parts) <= 2 else parts[1].replace("-", " ") + + return PageInfo( + source_path=source_path, + page_name=page_name_for_source(source_path), + title=extract_title(content, source_path), + content=content, + language=language_for_source(source_path), + group=group, + is_index=relative.name == "index.md", + ) + + +def read_manifest(wiki_root: Path) -> set[str]: + manifest_path = wiki_root / MANIFEST_NAME + if not manifest_path.exists(): + return set() + return { + line.strip() + for line in manifest_path.read_text(encoding="utf-8").splitlines() + if line.strip() + } + + +def write_manifest(wiki_root: Path, file_names: set[str]) -> None: + manifest_path = wiki_root / MANIFEST_NAME + content = "\n".join(sorted(file_names)) + if content: + content = f"{content}\n" + manifest_path.write_text(content, encoding="utf-8") + + +def write_file(path: Path, content: str) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + + +def sync_docs_to_wiki(source_root: Path, wiki_root: Path) -> None: + source_root = Path(source_root) + wiki_root = Path(wiki_root) + wiki_root.mkdir(parents=True, exist_ok=True) + resolver = LinkResolver(source_root) + + page_infos = [ + build_page_info(source_root, source_path, resolver) + for source_path in resolver.source_pages + ] + page_names = {info.page_name for info in page_infos} + + for info in page_infos: + if info.is_index and not info.content.strip(): + generated = build_language_index(info.language, page_names) + info.content = generated + info.title = extract_title(generated, info.source_path) + + desired_files = {f"{info.page_name}.md": info.content for info in page_infos} + desired_files["Home.md"] = build_home_page("zh") + desired_files["Home-en.md"] = build_home_page("en") + desired_files["_Sidebar.md"] = build_sidebar(page_infos) + + previously_managed = read_manifest(wiki_root) + for existing_name in previously_managed - set(desired_files): + existing_path = wiki_root / existing_name + if existing_path.exists(): + existing_path.unlink() + + for file_name, content in desired_files.items(): + write_file(wiki_root / file_name, content) + + managed_files = set(desired_files) + write_manifest(wiki_root, managed_files) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Sync AstrBot docs content to GitHub wiki pages." + ) + parser.add_argument( + "--source-root", + default=str(repo_root()), + help="Path to the AstrBot-docs repository root.", + ) + parser.add_argument( + "--wiki-root", + help="Path to the checked out wiki repository.", + ) + parser.add_argument( + "--check-links-only", + action="store_true", + help="Validate internal doc links without writing wiki files.", + ) + args = parser.parse_args() + + if not args.check_links_only and not args.wiki_root: + parser.error("--wiki-root is required unless --check-links-only is set") + + check_unresolved_doc_links(Path(args.source_root)) + + if args.check_links_only: + return 0 + + sync_docs_to_wiki( + source_root=Path(args.source_root), wiki_root=Path(args.wiki_root) + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/snapshots/v4.23.6/scripts/upload-doc-images-to-r2.sh b/docs/snapshots/v4.23.6/scripts/upload-doc-images-to-r2.sh new file mode 100755 index 0000000..e7a6a8d --- /dev/null +++ b/docs/snapshots/v4.23.6/scripts/upload-doc-images-to-r2.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +exec python3 "$SCRIPT_DIR/upload_doc_images_to_r2.py" "$@" diff --git a/docs/snapshots/v4.23.6/scripts/upload_doc_images_to_r2.py b/docs/snapshots/v4.23.6/scripts/upload_doc_images_to_r2.py new file mode 100755 index 0000000..7db614d --- /dev/null +++ b/docs/snapshots/v4.23.6/scripts/upload_doc_images_to_r2.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import sys +import tempfile +from collections.abc import Iterable, Sequence +from pathlib import Path +from urllib.parse import quote + +IMAGE_EXTS = { + ".png", + ".jpg", + ".jpeg", + ".gif", + ".webp", + ".svg", + ".avif", + ".bmp", + ".ico", + ".tif", + ".tiff", +} + +MD_IMAGE_RE = re.compile(r"!\[[^\]]*\]\(([^)]+)\)") +HTML_IMG_RE = re.compile( + r"]*\bsrc\s*=\s*([\"'])([^\"']+)\1[^>]*>", re.IGNORECASE +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Upload all locally referenced images from Markdown docs to Cloudflare R2 using rclone." + ) + parser.add_argument("--remote", required=True, help="rclone remote name, e.g. r2") + parser.add_argument("--bucket", default="", help="bucket name in remote path") + parser.add_argument( + "--prefix", + default="docs-images", + help="destination prefix inside bucket/remote (default: docs-images)", + ) + parser.add_argument( + "--docs-root", + default=".", + help="docs root to scan for .md files (default: current directory)", + ) + parser.add_argument( + "--dry-run", action="store_true", help="preview uploads without sending files" + ) + parser.add_argument( + "--list-only", action="store_true", help="only print matched image files" + ) + parser.add_argument( + "--rewrite-markdown", + action="store_true", + help="rewrite local image links in markdown/html to public URL after upload", + ) + parser.add_argument( + "--public-base-url", + default="", + help="public URL base used for replacement, e.g. https://cdn.example.com/docs", + ) + parser.add_argument( + "--backup-ext", + default=".bak", + help="backup extension used when rewriting markdown (default: .bak)", + ) + return parser.parse_args() + + +def is_local_ref(ref: str) -> bool: + lower = ref.lower() + return not ( + lower.startswith("http://") + or lower.startswith("https://") + or lower.startswith("//") + or lower.startswith("data:") + or lower.startswith("mailto:") + ) + + +def parse_md_ref(raw: str) -> str: + ref = raw.strip() + if ref.startswith("<") and ">" in ref: + ref = ref[1 : ref.find(">")] + else: + ref = re.split(r"\s+", ref, maxsplit=1)[0] + ref = ref.split("#", 1)[0].split("?", 1)[0] + return ref.strip() + + +def clean_ref(raw: str) -> str: + ref = raw.strip().strip("<>") + ref = ref.split("#", 1)[0].split("?", 1)[0] + return ref.strip() + + +def resolve_local_ref(md_file: Path, ref: str, root: Path) -> Path | None: + if not ref: + return None + if ref.startswith("/"): + candidate = root / ref.lstrip("/") + else: + candidate = (md_file.parent / ref).resolve() + + try: + resolved = candidate.resolve() + except FileNotFoundError: + return None + + if not resolved.is_file(): + return None + + try: + resolved.relative_to(root) + except ValueError: + return None + + if resolved.suffix.lower() not in IMAGE_EXTS: + return None + + return resolved + + +def find_markdown_files(root: Path) -> list[Path]: + files: list[Path] = [] + for path in root.rglob("*.md"): + if "node_modules" in path.parts: + continue + files.append(path) + return sorted(files) + + +def collect_images( + root: Path, md_files: Sequence[Path] +) -> tuple[set[Path], list[tuple[Path, str]]]: + images: set[Path] = set() + missing: list[tuple[Path, str]] = [] + + for md_file in md_files: + text = md_file.read_text(encoding="utf-8") + + for m in MD_IMAGE_RE.finditer(text): + ref = parse_md_ref(m.group(1)) + if not ref or not is_local_ref(ref): + continue + resolved = resolve_local_ref(md_file, ref, root) + if resolved: + images.add(resolved) + else: + missing.append((md_file, ref)) + + for m in HTML_IMG_RE.finditer(text): + ref = clean_ref(m.group(2)) + if not ref or not is_local_ref(ref): + continue + resolved = resolve_local_ref(md_file, ref, root) + if resolved: + images.add(resolved) + else: + missing.append((md_file, ref)) + + return images, missing + + +def build_target(remote: str, bucket: str, prefix: str) -> str: + target = f"{remote}:" + if bucket: + target = f"{remote}:{bucket}" + + p = prefix.strip("/") + if p: + target = f"{target}/{p}" + + return target + + +def rel_object_path(root: Path, image_path: Path, prefix: str) -> str: + rel = image_path.relative_to(root).as_posix() + p = prefix.strip("/") + return f"{p}/{rel}" if p else rel + + +def build_public_url(base: str, object_path: str) -> str: + base = base.rstrip("/") + encoded_path = quote(object_path, safe="/-._~") + return f"{base}/{encoded_path}" + + +def run_rclone_upload( + root: Path, target: str, rel_files: Iterable[str], dry_run: bool +) -> None: + if shutil.which("rclone") is None: + raise RuntimeError("rclone not found in PATH") + + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", delete=False) as tmp: + tmp_path = Path(tmp.name) + for rel in rel_files: + tmp.write(f"{rel}\n") + + try: + cmd = [ + "rclone", + "copy", + str(root), + target, + "--files-from", + str(tmp_path), + "--create-empty-src-dirs", + ] + if dry_run: + cmd.append("--dry-run") + + print() + if dry_run: + print("Dry-run:", " ".join(cmd)) + else: + print(f"Uploading to: {target}") + + subprocess.run(cmd, check=True) + finally: + tmp_path.unlink(missing_ok=True) + + +def rewrite_markdown_files( + root: Path, + md_files: Sequence[Path], + image_set: set[Path], + prefix: str, + public_base_url: str, + backup_ext: str, +) -> int: + changed_count = 0 + + def to_url(md_file: Path, raw_ref: str, is_markdown: bool) -> str | None: + ref = parse_md_ref(raw_ref) if is_markdown else clean_ref(raw_ref) + if not ref or not is_local_ref(ref): + return None + resolved = resolve_local_ref(md_file, ref, root) + if not resolved or resolved not in image_set: + return None + obj = rel_object_path(root, resolved, prefix) + return build_public_url(public_base_url, obj) + + for md_file in md_files: + text = md_file.read_text(encoding="utf-8") + + def md_repl(match: re.Match[str]) -> str: + raw = match.group(1) + url = to_url(md_file, raw, is_markdown=True) + if not url: + return match.group(0) + return match.group(0).replace(raw, url, 1) + + def html_repl(match: re.Match[str]) -> str: + quote_ch = match.group(1) + raw = match.group(2) + url = to_url(md_file, raw, is_markdown=False) + if not url: + return match.group(0) + return match.group(0).replace( + f"src={quote_ch}{raw}{quote_ch}", f"src={quote_ch}{url}{quote_ch}", 1 + ) + + updated = MD_IMAGE_RE.sub(md_repl, text) + updated = HTML_IMG_RE.sub(html_repl, updated) + + if updated != text: + if backup_ext: + backup_path = md_file.with_suffix(md_file.suffix + backup_ext) + backup_path.write_text(text, encoding="utf-8") + md_file.write_text(updated, encoding="utf-8") + changed_count += 1 + + return changed_count + + +def main() -> int: + args = parse_args() + + if args.rewrite_markdown and not args.public_base_url: + print( + "Error: --public-base-url is required when using --rewrite-markdown", + file=sys.stderr, + ) + return 1 + + root = Path(args.docs_root).resolve() + if not root.is_dir(): + print(f"Error: docs root not found: {args.docs_root}", file=sys.stderr) + return 1 + + if shutil.which("rg") is None: + print("Error: rg (ripgrep) not found in PATH", file=sys.stderr) + return 1 + + md_files = find_markdown_files(root) + images, missing = collect_images(root, md_files) + + if not images: + print("No local image references found in Markdown docs.") + return 0 + + rel_files = sorted(p.relative_to(root).as_posix() for p in images) + + print(f"Found {len(rel_files)} image files:") + for rel in rel_files: + print(rel) + + if missing: + print(file=sys.stderr) + print( + f"Warning: {len(missing)} referenced files were not found (showing up to 20):", + file=sys.stderr, + ) + for md, ref in missing[:20]: + print(f"{md}\t{ref}", file=sys.stderr) + + if args.list_only: + return 0 + + target = build_target(args.remote, args.bucket, args.prefix) + run_rclone_upload(root, target, rel_files, dry_run=args.dry_run) + + if args.rewrite_markdown and not args.dry_run: + changed = rewrite_markdown_files( + root=root, + md_files=md_files, + image_set=images, + prefix=args.prefix, + public_base_url=args.public_base_url, + backup_ext=args.backup_ext, + ) + print(f"Rewrote {changed} markdown files.") + + print("Done.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/snapshots/v4.23.6/scripts/usage.md b/docs/snapshots/v4.23.6/scripts/usage.md new file mode 100644 index 0000000..768e41f --- /dev/null +++ b/docs/snapshots/v4.23.6/scripts/usage.md @@ -0,0 +1,8 @@ +```bash +bash scripts/upload-doc-images-to-r2.sh \ + --remote astrbot-docs-s3 \ + --bucket astrbot \ + --prefix docs \ + --rewrite-markdown \ + --public-base-url https://files.astrbot.app +``` \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/tests/test_sync_docs_to_wiki.py b/docs/snapshots/v4.23.6/tests/test_sync_docs_to_wiki.py new file mode 100644 index 0000000..4bcecc0 --- /dev/null +++ b/docs/snapshots/v4.23.6/tests/test_sync_docs_to_wiki.py @@ -0,0 +1,491 @@ +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path +import sys +from tempfile import TemporaryDirectory +import unittest + + +def load_sync_module(): + script_path = ( + Path(__file__).resolve().parents[1] / "scripts" / "sync_docs_to_wiki.py" + ) + spec = spec_from_file_location("sync_docs_to_wiki", script_path) + if spec is None or spec.loader is None: + raise ImportError(f"Unable to load module from {script_path}") + module = module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +class SyncDocsHelpersTest(unittest.TestCase): + def test_page_name_for_nested_markdown_source(self): + module = load_sync_module() + + self.assertEqual( + module.page_name_for_source("zh/deploy/astrbot/docker.md"), + "zh-deploy-astrbot-docker", + ) + + def test_strip_frontmatter_removes_leading_block(self): + module = load_sync_module() + + source = "---\nlayout: home\n---\n\n# Title\n" + + self.assertEqual(module.strip_frontmatter(source), "# Title\n") + + def test_module_does_not_expose_removed_wrapper_helpers(self): + module = load_sync_module() + + self.assertFalse(hasattr(module, "get_link_resolver")) + self.assertFalse(hasattr(module, "resolve_source_path")) + self.assertFalse(hasattr(module, "compute_managed_files")) + self.assertFalse(hasattr(module, "MANAGED_FILENAMES")) + self.assertFalse(hasattr(module, "find_candidates_by_suffix")) + + def test_module_exposes_consolidated_helper_names(self): + module = load_sync_module() + + self.assertTrue(hasattr(module, "prepare_candidate_path")) + self.assertTrue(hasattr(module, "resolve_link_path")) + self.assertTrue(hasattr(module, "LANG_CONFIG")) + self.assertTrue(hasattr(module, "Segment")) + self.assertTrue(hasattr(module, "iter_segments")) + + def test_parse_doc_target_returns_base_and_anchor(self): + module = load_sync_module() + + self.assertEqual( + module.parse_doc_target("/deploy/guide#intro"), + ("/deploy/guide", "#intro"), + ) + self.assertIsNone(module.parse_doc_target("https://example.com/guide")) + self.assertIsNone(module.parse_doc_target("../images/diagram.png")) + self.assertIsNone(module.parse_doc_target("#intro")) + + def test_iter_markdown_links_handles_whitespace_before_target(self): + module = load_sync_module() + + links = list(module.iter_markdown_links("See [Guide]\n(guide.md).\n")) + + self.assertEqual([link.target for link in links], ["guide.md"]) + + def test_iter_segments_splits_text_inline_and_fenced_code(self): + module = load_sync_module() + + segments = list( + module.iter_segments( + "Start [Guide](/guide) `code [Guide](/guide)`\n\n```md\n[Guide](/guide)\n```\nTail\n" + ) + ) + + self.assertEqual( + [(segment.kind, segment.text) for segment in segments], + [ + ("text", "Start [Guide](/guide) "), + ("inline_code", "`code [Guide](/guide)`"), + ("text", "\n\n"), + ("code_block", "```md\n[Guide](/guide)\n```"), + ("text", "\nTail\n"), + ], + ) + + def test_rewrite_links_handles_absolute_same_language_links(self): + module = load_sync_module() + + resolver = module.LinkResolver(Path(__file__).resolve().parents[1]) + + content = "See [Docker](/deploy/astrbot/docker).\n" + + self.assertEqual( + module.rewrite_links( + content, + source_path="zh/what-is-astrbot.md", + resolver=resolver, + ), + "See [Docker](zh-deploy-astrbot-docker).\n", + ) + + def test_rewrite_links_handles_relative_links(self): + module = load_sync_module() + + resolver = module.LinkResolver(Path(__file__).resolve().parents[1]) + + content = "Use [Dify](../agent-runners/dify.md).\n" + + self.assertEqual( + module.rewrite_links( + content, + source_path="zh/providers/dify.md", + resolver=resolver, + ), + "Use [Dify](zh-providers-agent-runners-dify).\n", + ) + + def test_rewrite_links_handles_rewritten_root_paths(self): + module = load_sync_module() + + resolver = module.LinkResolver(Path(__file__).resolve().parents[1]) + + content = "See [Connecting Model Services](/config/providers/start).\n" + + self.assertEqual( + module.rewrite_links( + content, + source_path="zh/what-is-astrbot.md", + resolver=resolver, + ), + "See [Connecting Model Services](zh-providers-start).\n", + ) + + def test_rewrite_links_handles_internal_links_with_parentheses(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh").mkdir(parents=True) + (source_root / "zh" / "index.md").write_text( + "See [Guide](/guide(test)).\n", + encoding="utf-8", + ) + (source_root / "zh" / "guide(test).md").write_text( + "# Guide\n", + encoding="utf-8", + ) + resolver = module.LinkResolver(source_root) + + self.assertEqual( + module.rewrite_links( + "See [Guide](/guide(test)).\n", + source_path="zh/index.md", + resolver=resolver, + ), + "See [Guide](zh-guide(test)).\n", + ) + + def test_rewrite_links_leaves_local_asset_links_unchanged(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh" / "use").mkdir(parents=True) + (source_root / "zh" / "images").mkdir(parents=True) + (source_root / "zh" / "use" / "guide.md").write_text( + "# Guide\n", encoding="utf-8" + ) + (source_root / "zh" / "images" / "diagram.png").write_bytes(b"png") + resolver = module.LinkResolver(source_root) + + content = "![Diagram](../images/diagram.png)\n" + + self.assertEqual( + module.rewrite_links( + content, + source_path="zh/use/guide.md", + resolver=resolver, + ), + content, + ) + + def test_rewrite_links_skips_fenced_code_blocks(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh").mkdir(parents=True) + (source_root / "zh" / "index.md").write_text("# Home\n", encoding="utf-8") + (source_root / "zh" / "guide.md").write_text("# Guide\n", encoding="utf-8") + resolver = module.LinkResolver(source_root) + + content = "```md\n[Guide](/guide)\n```\n\nSee [Guide](/guide).\n" + + self.assertEqual( + module.rewrite_links( + content, + source_path="zh/index.md", + resolver=resolver, + ), + "```md\n[Guide](/guide)\n```\n\nSee [Guide](zh-guide).\n", + ) + + def test_rewrite_links_skips_inline_code(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh").mkdir(parents=True) + (source_root / "zh" / "index.md").write_text("# Home\n", encoding="utf-8") + (source_root / "zh" / "guide.md").write_text("# Guide\n", encoding="utf-8") + resolver = module.LinkResolver(source_root) + + content = "Use `[Guide](/guide)` literally, then See [Guide](/guide).\n" + + self.assertEqual( + module.rewrite_links( + content, + source_path="zh/index.md", + resolver=resolver, + ), + "Use `[Guide](/guide)` literally, then See [Guide](zh-guide).\n", + ) + + def test_link_resolver_resolves_source_paths(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh" / "deploy").mkdir(parents=True) + (source_root / "zh" / "index.md").write_text("# Home\n", encoding="utf-8") + (source_root / "zh" / "deploy" / "guide.md").write_text( + "# Guide\n", encoding="utf-8" + ) + + resolver = module.LinkResolver(source_root) + + self.assertEqual( + resolver.resolve_markdown_target("/deploy/guide#intro", "zh/index.md"), + ("zh/deploy/guide.md", "#intro"), + ) + + def test_resolve_link_path_resolves_relative_target(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh" / "providers").mkdir(parents=True) + (source_root / "zh" / "agent-runners").mkdir(parents=True) + (source_root / "zh" / "providers" / "dify.md").write_text( + "# Dify\n", + encoding="utf-8", + ) + (source_root / "zh" / "agent-runners" / "dify.md").write_text( + "# Agent Runner\n", + encoding="utf-8", + ) + + self.assertEqual( + module.resolve_link_path( + base_target="../agent-runners/dify.md", + source_path="zh/providers/dify.md", + source_root=source_root, + source_pages=module.discover_source_pages(str(source_root)), + ).resolved_path, + "zh/agent-runners/dify.md", + ) + + def test_build_home_page_uses_language_config(self): + module = load_sync_module() + + self.assertIn( + module.LANG_CONFIG["zh"]["home_intro"], module.build_home_page("zh") + ) + self.assertIn( + module.LANG_CONFIG["en"]["home_intro"], module.build_home_page("en") + ) + + def test_prepare_candidate_path_normalizes_suffix_and_alias(self): + module = load_sync_module() + + self.assertEqual( + module.prepare_candidate_path( + module.PurePosixPath("zh/config/providers/../providers/start") + ), + module.PurePosixPath("zh/providers/start.md"), + ) + + def test_find_existing_source_path_matches_language_bounded_suffixes(self): + module = load_sync_module() + + self.assertEqual( + module.find_existing_source_path( + candidate=module.PurePosixPath("zh/bar/guide.md"), + source_root=Path("/tmp/nonexistent"), + source_pages=( + "zh/bar/guide.md", + "zh/foo/bar/guide.md", + "zh/foobar/guide.md", + "en/bar/guide.md", + ), + ).ambiguous_matches, + ("zh/bar/guide.md", "zh/foo/bar/guide.md"), + ) + + def test_build_page_info_returns_page_info_dataclass(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh").mkdir(parents=True) + (source_root / "zh" / "index.md").write_text( + "# 中文首页\n", encoding="utf-8" + ) + + resolver = module.LinkResolver(source_root) + page_info = module.build_page_info( + source_root=source_root, + source_path="zh/index.md", + resolver=resolver, + ) + + self.assertIsInstance(page_info, module.PageInfo) + self.assertEqual(page_info.page_name, "zh-index") + + def test_build_page_info_uses_display_ready_group(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh" / "agent-runners").mkdir(parents=True) + (source_root / "zh" / "agent-runners" / "guide.md").write_text( + "# Guide\n", + encoding="utf-8", + ) + + resolver = module.LinkResolver(source_root) + page_info = module.build_page_info( + source_root=source_root, + source_path="zh/agent-runners/guide.md", + resolver=resolver, + ) + + self.assertEqual(page_info.group, "agent runners") + + def test_sync_writes_pages_and_sidebar(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + wiki_root = Path(temp_dir) / "wiki" + (source_root / "zh").mkdir(parents=True) + (source_root / "en").mkdir(parents=True) + + (source_root / "zh" / "index.md").write_text( + "---\nlayout: home\n---\n\n# 中文首页\n\nSee [Guide](/deploy/guide).\n", + encoding="utf-8", + ) + (source_root / "zh" / "deploy").mkdir(parents=True) + (source_root / "zh" / "deploy" / "guide.md").write_text( + "# 部署指南\n", + encoding="utf-8", + ) + (source_root / "en" / "index.md").write_text( + "# English Home\n\nSee [Guide](/en/deploy/guide).\n", + encoding="utf-8", + ) + (source_root / "en" / "deploy").mkdir(parents=True) + (source_root / "en" / "deploy" / "guide.md").write_text( + "# Deployment Guide\n", + encoding="utf-8", + ) + + module.sync_docs_to_wiki(source_root=source_root, wiki_root=wiki_root) + + self.assertTrue((wiki_root / "Home.md").exists()) + self.assertTrue((wiki_root / "Home-en.md").exists()) + self.assertTrue((wiki_root / "_Sidebar.md").exists()) + self.assertTrue((wiki_root / "zh-index.md").exists()) + self.assertTrue((wiki_root / "en-index.md").exists()) + self.assertIn( + "[Guide](zh-deploy-guide)", + (wiki_root / "zh-index.md").read_text(encoding="utf-8"), + ) + + def test_sync_preserves_unknown_wiki_pages(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + wiki_root = Path(temp_dir) / "wiki" + (source_root / "zh").mkdir(parents=True) + (source_root / "en").mkdir(parents=True) + + (source_root / "zh" / "index.md").write_text( + "# 中文首页\n", encoding="utf-8" + ) + (source_root / "en" / "index.md").write_text( + "# English Home\n", encoding="utf-8" + ) + + wiki_root.mkdir(parents=True) + handwritten = wiki_root / "zh-handwritten.md" + handwritten.write_text("# Keep me\n", encoding="utf-8") + + module.sync_docs_to_wiki(source_root=source_root, wiki_root=wiki_root) + + self.assertTrue(handwritten.exists()) + + def test_find_unresolved_doc_links_reports_ambiguous_matches(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh" / "foo").mkdir(parents=True) + (source_root / "zh" / "bar").mkdir(parents=True) + (source_root / "zh" / "index.md").write_text( + "See [Guide](/guide).\n", + encoding="utf-8", + ) + (source_root / "zh" / "foo" / "guide.md").write_text( + "# Foo\n", encoding="utf-8" + ) + (source_root / "zh" / "bar" / "guide.md").write_text( + "# Bar\n", encoding="utf-8" + ) + + unresolved = module.find_unresolved_doc_links(source_root) + + self.assertEqual( + unresolved, + [ + "zh/index.md -> /guide (ambiguous: zh/bar/guide.md, zh/foo/guide.md)", + ], + ) + + def test_resolver_does_not_match_partial_path_segments(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh" / "foobar").mkdir(parents=True) + (source_root / "zh" / "index.md").write_text( + "See [Guide](/bar/guide).\n", + encoding="utf-8", + ) + (source_root / "zh" / "foobar" / "guide.md").write_text( + "# Guide\n", + encoding="utf-8", + ) + + resolver = module.LinkResolver(source_root) + + self.assertEqual( + resolver.resolve_markdown_target("/bar/guide", "zh/index.md"), + (None, ""), + ) + + def test_live_docs_have_no_unresolved_internal_doc_links(self): + module = load_sync_module() + + unresolved = module.find_unresolved_doc_links( + source_root=Path(__file__).resolve().parents[1], + ) + + self.assertEqual(unresolved, []) + + def test_check_unresolved_doc_links_raises_for_bad_docs(self): + module = load_sync_module() + + with TemporaryDirectory() as temp_dir: + source_root = Path(temp_dir) / "docs" + (source_root / "zh").mkdir(parents=True) + (source_root / "zh" / "index.md").write_text( + "See [Missing](/missing).\n", + encoding="utf-8", + ) + + with self.assertRaises(ValueError): + module.check_unresolved_doc_links(source_root) + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/snapshots/v4.23.6/vercel.json b/docs/snapshots/v4.23.6/vercel.json new file mode 100644 index 0000000..41e7af0 --- /dev/null +++ b/docs/snapshots/v4.23.6/vercel.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": null, + "buildCommand": "npm run docs:build", + "outputDirectory": ".vitepress/dist", + "cleanUrls": true, + "trailingSlash": false, + "routes": [ + { "handle": "filesystem" }, + { "src": "/.*", "dest": "/404.html", "status": 404 } + ] +} diff --git a/docs/snapshots/v4.23.6/zh/community-events/ospp-2025.md b/docs/snapshots/v4.23.6/zh/community-events/ospp-2025.md new file mode 100644 index 0000000..b451136 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/community-events/ospp-2025.md @@ -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) diff --git a/docs/snapshots/v4.23.6/zh/community-events/tonggujiyu-astrbot-plugin-reward-program.md b/docs/snapshots/v4.23.6/zh/community-events/tonggujiyu-astrbot-plugin-reward-program.md new file mode 100644 index 0000000..d682e34 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/community-events/tonggujiyu-astrbot-plugin-reward-program.md @@ -0,0 +1,13 @@ +# 桐谷霁屿团队 x AstrBot 插件奖励活动 + +> 发布于 2026/03/30 + +为进一步促进 AstrBot 社区生态发展,鼓励优质插件的创作与分享,桐谷霁屿团队邀请到了 AstrBot 社区发起本次社区合作计划。 + +本计划采用「社区投票 + 评审筛选」的方式:参与插件将进入公开投票环节,由社区选出得票数最高的五个插件;若当期无主动报名项目,将从官方插件库中选取优质插件补充入选;随后由评审团基于实用性、创新性与社区价值等维度进行综合评估,最终确定本期获得支持的插件。 + +对于入选项目,我们将联系作者提供一定形式的支持,包括「周四 50 元」赞助、社区曝光或其他资源协助等。该计划为社区之间的合作尝试,并非商业性评选,旨在以轻量、持续的方式推动优秀项目的成长。 + +本次活动地址:https://abponsor.tongujiyu.cn/ + +欢迎开发者参与,也欢迎社区成员积极投票与推荐优秀插件。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/zh/community.md b/docs/snapshots/v4.23.6/zh/community.md new file mode 100644 index 0000000..bb2279f --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/community.md @@ -0,0 +1,42 @@ +# 社区 + +## 社区渠道 + +本文档可能没有完全覆盖所有的功能,如果你有关于 AstrBot 与本文档的任何问题或建议,欢迎通过下面的社区渠道联系我们。 + +### QQ 群 + +- 12 群: 916228568 (新) +- 9 群: 1076659624 (人满) +- 10 群: 1078079676 (人满) +- 11 群: 704659519 (人满) +- 1 群: 322154837 (人满) +- 3 群: 630166526 (人满) +- 4 群: 1077826412 (人满) +- 5 群: 822130018 (人满) +- 6 群: 753075035 (人满) +- 7 群: 743746109 (人满) +- 8 群: 1030353265 (人满) +- **AstrBot 核心开发交流群: 975206796**(AstrBot 开发成员通常活跃于此,欢迎任何对编程/AI 技术感兴趣的同学加入~) + +### Discord + +https://discord.gg/hAVk6tgV36 + +### Astrbook + +- [Astrbook](https://book.astrbot.app/) - 专为 AI Agent 打造的社交社区,你可以在这里看到机器人们的日常动态,也可以将你的 Bot 接入其中。 + +### 玖帕喵 Prompt Market + +- [玖帕喵](https://jiupamiao.asia/) - AI 人设与 Prompt 分享市场,在这里发现和分享高质量的 Prompts。玖帕喵,喵喵喵喵,喵! + +### GitHub + +欢迎提交 Issue 或 Pull Request: + +- [AstrBotDevs/AstrBot](https://github.com/AstrBotDevs/AstrBot) + +## 成为 AstrBot 组织成员 + +欢迎加入我们! diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/1panel.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/1panel.md new file mode 100644 index 0000000..53699ae --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/1panel.md @@ -0,0 +1,27 @@ +# 在 1Panel 部署 AstrBot + +[1Panel](https://1panel.cn/) 是开源的新一代 Linux 服务器运维管理面板。 + +AstrBot 已经由 1Panel 团队上架至 [1Panel 应用商店](https://apps.fit2cloud.com/1panel),用户可以直接通过 1Panel 快速部署使用。 + +## 安装 1Panel + +如果您还没有安装 1Panel 面板,请参考 [1Panel 官网](https://1panel.cn/) 一键安装。 + +> International users can refer to the [1Panel official site](https://github.com/1Panel-dev/1Panel) for tutorials. + +## 安装 AstrBot + +打开 1Panel 面板,进入 1Panel 应用商店,搜索 `AstrBot`,如下图所示。 + +![image](https://files.astrbot.app/docs/source/images/1panel/image.png) + +点击 `安装`,等待安装成功。 + +安装成功后,在 1Panel 系统-防火墙页面放行对应的 AstrBot 端口(默认是 6185 端口)。 + +如果您正在使用 AWS、阿里云、腾讯云等厂商的云服务器,请确保其安全组也放行了 6185 端口。 + +## 访问 AstrBot + +访问 `http://IP:6185` 即可访问 AstrBot 的管理面板。 diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/btpanel.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/btpanel.md new file mode 100644 index 0000000..9bf26df --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/btpanel.md @@ -0,0 +1,48 @@ +# 在 宝塔面板 部署 AstrBot + +[宝塔面板](https://www.bt.cn/new/index.html)是一个安全高效、生产可用的 Linux/Windows 服务器运维面板。 + +AstrBot 已经上架至宝塔的 Docker 应用商店,支持一键安装。 + +## 安装宝塔面板 + +如果您还没有安装宝塔面板,请参考 [安装宝塔产品](https://www.bt.cn/new/download.html) 一键安装。 + +## 设置加速 URL(国内服务器用户) + +进入宝塔面板页面后,点击左侧的 `Docker`,点击设置,修改`加速 URL`。 + +![alt text](https://files.astrbot.app/docs/source/images/btpanel/image-1.png) + +## 安装 AstrBot + +进入 Docker 的应用商店,搜索 `AstrBot`,如下图所示。 + +![image](https://files.astrbot.app/docs/source/images/btpanel/image.png) + +点击安装,等待安装成功。 + +安装成功后,点击左侧 `安全`,放行对应的 AstrBot 端口(默认是 6185 端口)。 + +如果您正在使用 AWS、阿里云、腾讯云等厂商的云服务器,请确保其安全组也放行了对应的端口。 + +## 访问 AstrBot + +访问 `http://IP:6185` 即可访问 AstrBot 的管理面板。 + +> [!TIP] +> 默认情况下,上述方法只会放行一个 6185 端口。如果需要部署消息平台,需要额外放行对应的端口。点击上栏 `容器`,找到 AstrBot 容器,点击 `管理`,点击 `编辑容器`,添加对应的端口即可。 +> +> ![image](https://files.astrbot.app/docs/source/images/btpanel/image-2.png) +> +> 具体的消息平台对应端口可以参考下表: +> +>| 端口 | 描述 | 类型 +>| -------- | ------- | ------- | +>| 6185 | AstrBot WebUI `默认` 端口 | 需要 | +>| 6195 | 企业微信 `默认` 端口 | 可选 | +>| 6199 | QQ 个人号(aiocqhttp) `默认` 端口 | 可选 | +>| 6196 | QQ 官方接口(Webhook) `默认` 端口 | 可选 | +> +> 没有列举的平台表示不需要额外放行端口。 + diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/casaos.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/casaos.md new file mode 100644 index 0000000..14ec39e --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/casaos.md @@ -0,0 +1,39 @@ +# 在 CasaOS 部署 AstrBot + +## 安装 CasaOS + +```bash +curl -fsSL https://get.casaos.io | sudo bash +``` + +## 添加 CasaOS-AppStore-Play 应用商店源 + +![image](https://files.astrbot.app/docs/source/images/casaos/image.png) + +点击 `更多应用`,然后输入: + +```txt +https://play.cuse.eu.org/Cp0204-AppStore-Play.zip +``` + +并添加,等待添加完成。 + +如果您的网络环境在国内,请先搜索并添加 `dkTurbo`,否则可能无法拉取 AstrBot 镜像。 + +![image](https://files.astrbot.app/docs/source/images/casaos/image-1.png) + +输入 `Astrbot` 即可找到 AstrBot。 + +![image](https://files.astrbot.app/docs/source/images/casaos/image-2.png) + +点击图标(不是安装按钮),然后悬浮到`安装`按钮上,点击自定义安装。 + +![image](https://files.astrbot.app/docs/source/images/casaos/image-3.png) + +在网络一栏选择 `host`。 + +![image](https://files.astrbot.app/docs/source/images/casaos/image-4.png) + +然后点击`安装`开始安装。 + +安装完成后,主界面会出现 AstrBot APP,点击即可打开管理面板。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/cli.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/cli.md new file mode 100644 index 0000000..623eb58 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/cli.md @@ -0,0 +1,92 @@ +# 通过源码部署 AstrBot + +> [!WARNING] +> 你正在直接通过源码来部署本项目,该教程需要您具有一定的技术基础。 +> +> 以下教程默认您的设备上已经安装 Python,并且版本 `>=3.10` + + +## 下载/克隆仓库 + +如果你的电脑上安装了 `git`,你可以通过以下命令来下载源码: + +```bash +git clone https://github.com/AstrBotDevs/AstrBot.git +# 上面的代码默认会拉取最新的提交的源码,如果你需要拉取最新稳定发行版本的源码,可以使用以下命令: +# 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 +``` + +如果你没有安装 `git`,请先下载安装。 + +或者,直接从 GitHub 上下载源码解压: + +![image](https://files.astrbot.app/docs/source/images/cli/image.png) + +## 安装依赖并运行 + +::: details 【🥳推荐】使用 `uv` 管理依赖 + +> 如果没安装 `uv`,请参考 [Installing uv](https://docs.astral.sh/uv/getting-started/installation/) 安装。 + +2. 在终端执行(AstrBot 目录下) +```bash +uv sync +uv run main.py +``` + +如果您安装了一些插件,建议后续启动附上 `--no-sync` 参数,以避免插件依赖库被重复安装。我们正在努力解决这个问题,敬请期待。 + +```bash +uv run --no-sync main.py +``` +::: + +::: details Python 内置 venv 安装依赖 + +在 AstrBot 源码目录下,使用终端运行以下命令: + +> 如果是 Windows,直接下载源码解压的,请打开解压的文件夹,在地址栏输入: +> ![image](https://files.astrbot.app/docs/source/images/cli/image-1.png) + +```bash +python3 -m venv ./venv +``` + +> 也可能是 `python` 而不是 `python3` + +以上步骤会创建一个虚拟环境并激活(以免打乱您设备本地的 Python 环境)。 + +接下来,通过以下命令安装依赖文件,这可能需要花费一些时间: + +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 +``` + +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 +``` +::: + + +## 🎉 大功告成! + +如果一切顺利,你会看到 AstrBot 打印出的日志。 + +如果没有报错,你会看到一条日志显示类似 `🌈 管理面板已启动,可访问` 并附带了几条链接。打开其中一个链接即可访问 AstrBot 管理面板。链接是 `http://localhost:6185`。 + +> [!TIP] +> 如果你正在服务器上部署 AstrBot,需要将 `localhost` 替换为你的服务器 IP 地址。 +> +> 默认用户名和密码是 `astrbot` 和 `astrbot`。 + + +接下来,你需要部署任何一个消息平台,才能够实现在消息平台上使用 AstrBot。 diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/community-deployment.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/community-deployment.md new file mode 100644 index 0000000..4042d86 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/community-deployment.md @@ -0,0 +1,52 @@ +# 社区提供的部署方式 + +> [!WARNING] +> AstrBot 官方不保证这些部署方式的安全性和稳定性。 + +## Linux 一键部署脚本 + +使用 `curl` 去下载脚本并且使用 `bash` 执行脚本: + +```bash +bash <(curl -sSL https://raw.githubusercontent.com/zhende1113/Antlia/refs/heads/main/Script/AstrBot/Antlia.sh) +``` + +如果你的系统没有 `curl`,你可以使用 `wget`: + +```bash +wget -qO- https://raw.githubusercontent.com/zhende1113/Antlia/refs/heads/main/Script/AstrBot/Antlia.sh | bash +``` + +仓库地址:[zhende1113/Antlia](https://github.com/zhende1113/Antlia/) + +## Linux 一键部署脚本(基于Docker) + +支持 AstrBot / NapCat + +> [!TIP] +> 权限不足时请使用 `sudo` 提权 + +### 使用 `curl` + +```bash +curl -sSL https://raw.githubusercontent.com/railgun19457/AstrbotScript/main/AstrbotScript.sh -o AstrbotScript.sh +chmod +x AstrbotScript.sh +sudo ./AstrbotScript.sh +``` + +### 使用 `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 (可选禁用彩色输出)` + +__仓库地址:[railgun19457/AstrbotScript](https://github.com/railgun19457/AstrbotScript)__ + +## AstrBot Android 部署 + +参考 [zz6zz666/AstrBot-Android-App](https://github.com/zz6zz666/AstrBot-Android-App) diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/compshare.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/compshare.md new file mode 100644 index 0000000..f6c4d80 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/compshare.md @@ -0,0 +1,89 @@ +# 通过优云智算部署 + +优云智算是 UCloud 旗下的 GPU 算力租赁和大模型 API 调用平台,致力于为 AI、深度学习、科学计算相关客户提供丰富多样的算力资源。 + +AstrBot 在优云智算发布了 Ollama + AstrBot 一键自部署镜像,并且接入了优云智算 LLM API。 + +## 使用 Ollama + AstrBot 一键自部署镜像 + +> 镜像默认参数为:RTX 3090 24GB + Intel 16核 + 64GB RAM + 200GB 系统盘。采用按量付费的方式,请留意您的余额使用情况。 + +1. 通过 [此链接](https://passport.compshare.cn/register?referral_code=FV7DcGowN4hB5UuXKgpE74) 注册优云智算账户。 +1. 打开 [AstrBot 镜像链接](https://www.compshare.cn/images/0oX7xoGrzfre),点击创建实例。 +2. 部署成功后,在[控制台](https://console.compshare.cn/light-gpu/console/resources)中打开「JupyterLab」 +3. 进入JupyterLab后,新建一个终端 Terminal,在终端中粘贴以下指令 + +```bash +cd +./astrbot_booter.sh +``` + +指令运行结果如下所示即说明启动成功。 + +```txt +(py312) root@f8396035c96d:/workspace# cd +./astrbot_booter.sh +Starting AstrBot... +Starting ollama... +Both services started in the background. +``` + +启动成功后,在浏览器中输入 `http://实例的外网IP:6185` 即可访问 AstrBot 的界面。外网 IP 可以在 控制台->基础网络(外网)中获取。 + +> 可能需要等待半分钟左右。 + +![WebUI 界面](https://www-s.ucloud.cn/2025/07/7e9fc6edc1dfa916abc069f4cecc24cf_1753940381771.png) + +使用用户名:astrbot 和密码 astrbot 进行登录。 + + +登录成功后,可以重新设置密码,并进入 AstrBot 的页面。 + +实例默认会导入 Ollama-DeepSeek-R1-32B 模型。 + +## 使用其他模型 + +### 使用 Ollama 拉取模型 + +镜像原生部署了 Ollama,您可以通过 Ollama 指令自行拉取想要的模型,将模型本地部署在实例。 + +1. 在 [Ollama](https://ollama.com/search) 模型列表找到想部署的模型。 +2. 通过 SSH 进入到实例的终端(进入优云智算平台的控制台页面->实例列表->控制台指令和密码) +3. 通过 `ollama pull 模型名` 拉取模型,等待拉取成功。 +4. 在 AstrBot 面板的 服务提供商页面找到 `ollama_deepseek-r1`,点击编辑,更新模型名称,点击保存。 + +![image](https://files.astrbot.app/docs/source/images/compshare/image-1.png) + +### 使用优云智算提供的模型 API + +AstrBot 支持接入优云智算提供的模型 API。 + +1. 在 [优云智算](https://console.compshare.cn/light-gpu/model-center) 找到想要接入的模型 +2. 在 AstrBot 面板的 服务提供商页面点击「+ 新增服务提供商」,点击优云智算(如果没有,点击“接入 OpenAI”,并且修改下一步弹出窗口的 API Base URL 为 `https://api.modelverse.cn/v1`)。在模型配置-模型名称输入模型名,点击保存。 + +### 测试 + +在 AstrBot 面板左侧点击 `聊天`,输入 `/provider`,可以查看和切换您当前接入的提供商。 + +您可以直接聊天来测试模型是否正常。 + +![image](https://files.astrbot.app/docs/source/images/compshare/image-2.png) + + +## 接入到消息平台 + +- 飞书:[接入到飞书](https://docs.astrbot.app/deploy/platform/lark.html) +- LINE:[接入到 LINE](https://docs.astrbot.app/deploy/platform/line.html) +- 钉钉:[接入到钉钉](https://docs.astrbot.app/deploy/platform/dingtalk.html) +- 企业微信:[接入到企业微信应用](https://docs.astrbot.app/deploy/platform/wecom.html) +- 微信客服:[接入到微信客服](https://docs.astrbot.app/deploy/platform/wecom.html) +- 微信公众平台:[接入到微信公众平台](https://docs.astrbot.app/deploy/platform/weixin-official-account.html) +- QQ 官方机器人平台:[接入到 QQ 机器人](https://docs.astrbot.app/deploy/platform/qqofficial/webhook.html) +- KOOK:[接入到 KOOK](https://docs.astrbot.app/deploy/platform/kook.html) +- Slack:[接入到 Slack](https://docs.astrbot.app/deploy/platform/slack.html) +- Discord:[接入到 Discord](https://docs.astrbot.app/deploy/platform/discord.html) +- 更多接入方式参考 [AstrBot 官方文档](https://docs.astrbot.app/what-is-astrbot.html) + +## 更多功能 + +更多功能请参考 [AstrBot 官方文档](https://docs.astrbot.app)。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/desktop.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/desktop.md new file mode 100644 index 0000000..8d41800 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/desktop.md @@ -0,0 +1,33 @@ +# 使用 AstrBot 桌面客户端部署 + +`AstrBot-desktop` 适合在本地电脑快速部署和使用 AstrBot,支持 Windows、macOS、Linux。 + +在多种部署方式中,桌面客户端更适合个人本地快速使用,不建议用于服务器长期运行或生产环境;如需生产部署,建议优先考虑 [Docker 部署](/deploy/astrbot/docker) 或 [Kubernetes 部署](/deploy/astrbot/kubernetes)。 + +相比命令行或容器方案,桌面客户端更偏向「开箱即用」体验,适合希望少折腾环境、直接开始使用的用户。 + +仓库地址:[AstrBotDevs/AstrBot-desktop](https://github.com/AstrBotDevs/AstrBot-desktop) + +## 适合谁 + +- 想快速本地部署,优先使用图形化界面的用户。 +- 不想手动维护 Docker / Python 运行环境的新手用户。 +- 个人设备长期在线,主要用于个人或小团队日常使用的场景。 + +## 主要特点 + +- 多平台安装包,下载后可直接安装使用。 +- 图形化界面配置,降低首次部署成本。 +- 适合作为本地常驻客户端。 + +## 下载并安装 + +1. 打开 [AstrBot-desktop Releases](https://github.com/AstrBotDevs/AstrBot-desktop/releases)。 +2. 下载与你系统对应的安装包(如 `.exe`、`.dmg`、`.rpm`、`.deb`)。 +3. 安装完成后启动桌面客户端,按向导完成初始化。 + +## 与启动器部署的区别 + +- 桌面客户端:更偏向开箱即用的 GUI 体验。 +- 启动器部署:更偏向自动化脚本拉起,适合希望保持传统部署流程的用户。 +- 参考 [启动器部署](/deploy/astrbot/launcher)。 diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/docker.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/docker.md new file mode 100644 index 0000000..69b0de6 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/docker.md @@ -0,0 +1,105 @@ +# 使用 Docker 部署 AstrBot + +> [!WARNING] +> 通过 Docker 可以方便地将 AstrBot 部署到 Windows, Mac, Linux 上。 +> +> 以下教程默认您的环境已安装 Docker。如果没有安装,请参考 [Docker 官方文档](https://docs.docker.com/get-docker/) 进行安装。 + +## 通过 Docker Compose 部署 + +::: details 只部署 AstrBot(通用方式) + +首先,需要 Clone AstrBot 仓库到本地: + +```bash +git clone https://github.com/AstrBotDevs/AstrBot +cd AstrBot +``` + +然后,运行 Compose: + +```bash +sudo docker compose up -d +``` + +> [!TIP] +> 如果您的网络环境在中国大陆境内,上述命令将无法正常拉取。您可能需要修改 compose.yml 文件,将其中的 `image: soulter/astrbot:latest` 替换为 `image: m.daocloud.io/docker.io/soulter/astrbot:latest`。 +::: + +::: details 带 Agent 沙盒环境的部署 + +支持原生的 Python 代码执行、Shell 代码执行等功能。 + +部署方式如下: + +```bash +git clone https://github.com/AstrBotDevs/AstrBot +cd AstrBot +# 修改 compose-with-shipyard.yml 文件中的环境变量配置,例如 Shipyard 的 access token 等 +docker compose -f compose-with-shipyard.yml up -d +docker pull soulter/shipyard-ship:latest +``` + +配置和使用详见 [Agent 沙盒环境](/use/astrbot-agent-sandbox.md) 文档。 +::: + +::: details 和 NapCat 一起部署 + +如果您想对接 NapCat,使用这种方式可以同时部署 AstrBot 和 NapCat。 + +```bash +mkdir astrbot +cd astrbot +wget https://raw.githubusercontent.com/NapNeko/NapCat-Docker/main/compose/astrbot.yml +sudo docker compose -f astrbot.yml up -d +``` + +::: + + +## 通过 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] +> 如果您的网络环境在中国大陆境内,上述命令将无法正常拉取。请使用以下命令拉取镜像: +> +> ```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 +> ``` +> +> (感谢 DaoCloud ❤️) +> +> Windows 下不需要加 sudo,下同 +> +Windows 同步 Host Time(需要WSL2) + +``` +-v \\wsl.localhost\(your-wsl-os)\etc\timezone:/etc/timezone:ro +-v \\wsl.localhost\(your-wsl-os)\etc\localtime:/etc/localtime:ro +``` + +通过以下命令查看 AstrBot 的日志: + +```bash +sudo docker logs -f astrbot +``` + +## 🎉 大功告成 + +如果一切顺利,你会看到 AstrBot 打印出的日志。 + +如果没有报错,你会看到一条日志显示类似 `🌈 管理面板已启动,可访问` 并附带了几条链接。打开其中一个链接即可访问 AstrBot 管理面板。 + +> [!TIP] +> 由于 Docker 隔离了网络环境,所以不能使用 `localhost` 访问管理面板。 +> +> 默认用户名和密码是 `astrbot` 和 `astrbot`。 +> +> 如果部署在云服务器上,需要在相应厂商控制台里放行对应端口。 + +接下来,你需要部署任何一个消息平台,才能够实现在消息平台上使用 AstrBot。 diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/kubernetes.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/kubernetes.md new file mode 100644 index 0000000..180b1ea --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/kubernetes.md @@ -0,0 +1,197 @@ +# 使用 Kubernetes 部署 AstrBot + +> [!WARNING] +> 通过 Kubernetes (K8s) 可以将 AstrBot 以高可用的方式部署在集群环境中,当出现故障时可以自动拉起恢复。 +> +> 由于 AstrBot 当前使用 SQLite 数据库,此部署方案不支持多副本水平扩展。同时,若采用 Sidecar 模式,NapCat 的登录状态持久化需要您特别关注。 +> +> 以下教程默认您的环境已安装并配置好 `kubectl`,且能够连接到您的 K8s 集群。 + +## 准备工作 + +在开始之前,请确保您的 Kubernetes 集群满足以下条件: + +1. **拥有默认的 StorageClass**:用于动态创建 `PersistentVolumeClaim` (PVC)。您可以通过 `kubectl get sc` 查看。如果没有,您需要手动创建 `PersistentVolume` (PV) 或安装相应的存储插件 (如 `nfs-client-provisioner`)。 +2. **网络访问**:确保您的集群节点可以从 `docker.io` 或您指定的镜像仓库拉取镜像。 + +## 部署方式 + +我们提供两种部署方案: + +* **集成部署 (Sidecar 模式)**:将 AstrBot 和 NapCat 部署在同一个 Pod 中,推荐用于 QQ 个人号。 +* **独立部署**:只部署 AstrBot,适用于其他平台或您希望独立管理 NapCat 的场景。 + +--- + +### 方式一:和 NapCatQQ 一起部署 (Sidecar) + +此方式位于 `k8s/astrbot_with_napcat` 目录。 + +#### 1. 部署 + +```bash +# 1. 创建命名空间 +kubectl apply -f k8s/astrbot_with_napcat/00-namespace.yaml + +# 2. 创建持久化存储卷 +# 注意:astrbot-data-shared-pvc 需要 ReadWriteMany (RWX) 访问模式。 +# 如果您的集群不支持 RWX,您需要配置 NFS 等共享存储,并修改 01-pvc.yaml 中的 storageClassName。 +kubectl apply -f k8s/astrbot_with_napcat/01-pvc.yaml + +# 3. 部署应用 +kubectl apply -f k8s/astrbot_with_napcat/02-deployment.yaml +``` + +#### 2. 暴露服务 (二选一) + +* **方式 A: NodePort** + + ```bash + kubectl apply -f k8s/astrbot_with_napcat/03-service-nodeport.yaml + ``` + + 服务将通过节点 IP 和一个由 Kubernetes 自动分配的端口暴露。您可以通过以下命令查看端口: + + ```bash + kubectl get svc -n astrbot-ns + ``` + + 在输出中找到 `astrbot-webui-svc` 和 `napcat-web-svc` 的 `PORT(S)` 列,格式为 `<内部端口>:/TCP`。例如 `8080:30185/TCP`,则访问地址为 `http://:30185`。 + +* **方式 B: LoadBalancer** + + 如果您的集群支持 `LoadBalancer` 类型的服务 (通常在云厂商的 K8s 服务中提供),可以使用此方式。 + + ```bash + kubectl apply -f k8s/astrbot_with_napcat/04-service-loadbalancer.yaml + ``` + + 执行后,通过 `kubectl get svc -n astrbot-ns` 查看分配到的外部 IP (EXTERNAL-IP)。 + +#### 3. 配置连接 + +由于 AstrBot 和 NapCat 在同一个 Pod 中,它们可以通过 `localhost` 直接通信。 + +1. **在 AstrBot 中添加消息平台:** + * 进入 AstrBot WebUI,选择 `机器人` -> `添加`。 + * **选择消息平台类别**: `aiocqhttp` + * **机器人名称**: `napcat` (或自定义) + * **反向 Websocket 主机**: `0.0.0.0` + * **反向 Websocket 端口**: `6199` + * 保存配置。 + + +2. **在 NapCat 中配置 Websocket Client:** + * 进入 NapCat WebUI,选择 `设置` -> `反向WS` -> `添加`。 + * **启用**: 开启 + * **URL**: `ws://localhost:6199/ws` + * **消息格式**: `Array` + * 保存配置。 + + +--- + +### 方式二:只部署 AstrBot (通用方式) + +此方式位于 `k8s/astrbot` 目录。 + +#### 1. 部署 + +```bash +# 1. 创建命名空间 +kubectl apply -f k8s/astrbot/00-namespace.yaml + +# 2. 创建持久化存储卷 +kubectl apply -f k8s/astrbot/01-pvc.yaml + +# 3. 部署应用 +kubectl apply -f k8s/astrbot/02-deployment.yaml +``` + +#### 2. 暴露服务 (二选一) + +* **方式 A: NodePort** + + ```bash + kubectl apply -f k8s/astrbot/03-service-nodeport.yaml + ``` + + 服务将通过节点 IP 和一个由 Kubernetes 自动分配的端口暴露。您可以通过以下命令查看端口: + + ```bash + kubectl get svc -n astrbot-standalone-ns + ``` + + 在输出中找到 `astrbot-webui-svc` 的 `PORT(S)` 列,格式为 `<内部端口>:/TCP`。例如 `8080:30185/TCP`,则访问地址为 `http://:30185`。 + +* **方式 B: LoadBalancer** + + ```bash + kubectl apply -f k8s/astrbot/04-service-loadbalancer.yaml + ``` + + 执行后,通过 `kubectl get svc -n astrbot-standalone-ns` 查看分配到的外部 IP (EXTERNAL-IP)。 + +--- + +## 高级配置 + +### 镜像加速 (中国大陆用户) + +如果拉取 `soulter/astrbot:latest` 或 `mlikiowa/napcat-docker:latest` 镜像困难,可以手动修改对应的 `02-deployment.yaml` 文件,将 `image` 字段替换为国内的镜像加速地址,例如: + +```yaml +# 示例: +# image: soulter/astrbot:latest +# 替换为 +image: m.daocloud.io/docker.io/soulter/astrbot:latest +``` + +### 启用 Docker 沙箱代码执行器 + +如果您需要使用沙箱代码执行器,需要将 Docker 的 socket 文件挂载到 Pod 中。 + +编辑 `02-deployment.yaml` 文件,在 `spec.template.spec` 下添加 `volumes` 和 `volumeMounts`: + +1. **在 `astrbot` 容器的 `volumeMounts` 列表下添加以下内容:** + + ```yaml + - name: docker-sock + mountPath: /var/run/docker.sock + ``` + +2. **在 `spec.template.spec.volumes` 列表下添加以下内容:** + + ```yaml + - name: docker-sock + hostPath: + path: /var/run/docker.sock + type: Socket + ``` + +> [!WARNING] +> 将 Docker socket 挂载到 Pod 中存在安全风险,请确保您了解其影响。 + +## 查看日志 + +* **Sidecar 部署模式:** + + ```bash + # 查看 AstrBot 日志 + kubectl logs -f -n astrbot-ns deployment/astrbot-stack -c astrbot + + # 查看 NapCat 日志 + kubectl logs -f -n astrbot-ns deployment/astrbot-stack -c napcat + ``` + +* **独立部署模式:** + + ```bash + kubectl logs -f -n astrbot-standalone-ns deployment/astrbot-standalone + ``` + +## 🎉 大功告成 + +部署并暴露服务后,您就可以通过相应的 IP 和端口访问 AstrBot 管理面板了。 + +> 默认用户名和密码是 `astrbot` 和 `astrbot`。 diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/launcher.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/launcher.md new file mode 100644 index 0000000..33c7182 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/launcher.md @@ -0,0 +1,101 @@ +# 使用 AstrBot 启动器部署 AstrBot + +## AstrBot 一键启动器 + +AstrBot 一键启动器支持 Windows、MacOS、Linux 等多端部署。 + +0. 打开 [AstrBotDevs/astrbot-launcher](https://github.com/AstrBotDevs/astrbot-launcher) +1. **(可选但推荐)** 给本项目点个 [**Star ⭐**](https://github.com/AstrBotDevs/astrbot-launcher),你的支持是作者更新和维护的动力! +2. 找到右边的 Releases,点击最新版本的 Release,在新的页面的 Assets 中下载对应你系统的安装器。 + +如,Windows X86 的用户应该下载 `AstrBot.Launcher_0.2.1_x64-setup.exe`,Windows on Arm 的用户应该下载 `AstrBot.Launcher_0.2.1_arm64-setup.exe`,MacOS M 芯片的用户下载 `AstrBot.Launcher_0.2.1_aarch64.dmg`。 + +MacOS 用户下载安装好后,可能会遇到 "已损坏,无法打开" 的提示。这是因为 MacOS 的安全机制阻止了未认证的应用运行。解决方法如下: + +1. 打开终端 +2. 输入以下命令并回车: + `xattr -dr com.apple.quarantine /Applications/AstrBot\ Launcher.app` +3. 重新尝试打开 AstrBot Launcher 应用 + +## 旧版本 Windows 安装器(不推荐) + + +> [!WARNING] +> 需要您的电脑上预先安装好 Python 环境(3.10 - 3.13),并且将 Python 添加到环境变量中,否则安装器将无法正常工作。 + + +推荐使用上面提到的 AstrBot 一键启动器来部署 AstrBot,因为它更简单、更自动化、更现代化,适合大多数用户。 + +安装器是一个使用 `Powershell` 编写的脚本,体积小巧,<20KB。需要您的电脑上安装有 `Powershell`,一般 `Windows 10` 及以上版本的设备都会自带这个工具。 + + +### 下载安装器 + +打开 https://github.com/AstrBotDevs/AstrBotLauncher/releases/latest + +下载 `Source code (zip)` 并解压到您的电脑。 + +### 运行安装器 + +> 视频和此处不一致,请参考此处!!!如果部署不了,请参阅其他两个部署方式:Docker 部署和 手动部署。 + +解压后,打开文件夹, + +地址栏输入 Powershell 并打开: + +![image](https://files.astrbot.app/docs/source/images/windows/image-4.png) + +将 `launcher_astrbot_en.bat` 批处理文件拖进去回车运行。 + +> [!WARNING] +> - 这个脚本没有病毒。如果提示 `Windows 已保护您的电脑`,请点击 `更多信息`,然后点击 `仍要运行`。 +> +> - 脚本默认使用 `python` 指令来执行代码,如果你想指定 Python 解释器器路径或者指令,请修改 `launcher_astrbot_en.bat` 文件。找到 `set PYTHON_CMD=python` 这一行,将 `python` 改为你的 Python 解释器路径或指令。 +> + +如果没有检测到 Python 环境,脚本将会提示并退出。 + +脚本将自动检测目录下是否有 `AstrBot` 文件夹,如果没有,将会从 [GitHub](https://github.com/AstrBotDevs/AstrBot/releases/latest) 自动下载最新的 AstrBot 源码。下载好后,会自动安装 AstrBot 的依赖并运行。 + +## 🎉 大功告成! + +如果一切顺利,你会看到 AstrBot 打印出的日志。 + +如果没有报错,你会看到一条日志显示类似 `🌈 管理面板已启动,可访问` 并附带了几条链接。打开其中一个链接即可访问 AstrBot 管理面板。 + +> [!TIP] +> 默认用户名和密码是 `astrbot` 和 `astrbot`。 +> +> **当管理面板打开时遇到 404 错误:** +> 在 [release](https://github.com/AstrBotDevs/AstrBot/releases) 页面下载dist.zip,解压拖到 AstrBot/data 下。还不行请重启电脑(来自群里的反馈) + +接下来,你需要部署任何一个消息平台,才能够实现在消息平台上使用 AstrBot。 + + +> [!TIP] +> 如果部署不了,请参阅其他两个部署方式:Docker 部署和 手动部署。 + + +## 报错:Python is not installed + +如果提示 Python is not installed,并且已经安装 Python,并且**也已经重启并仍报这个错误**,说明环境变量不对,有两个方法解决: + +**方法 1:** + +windows 搜索 Python,打开文件位置: + +![image](https://files.astrbot.app/docs/source/images/windows/image.png) + +右键下面这个快捷方式,打开文件所在位置: + +![alt text](https://files.astrbot.app/docs/source/images/windows/image-1.png) + +复制文件地址: + +![image](https://files.astrbot.app/docs/source/images/windows/image-2.png) + +回到 `launcher_astrbot_en.bat` 文件,右键点击 `在记事本中编辑`,找到 `set PYTHON_CMD=python` 这一行,将 `python` 改为你的 Python 解释器路径或指令,路径两端的双引号不要删。 + +**方法 2:** + +重装 python,并且在安装时勾选 `Add Python to PATH`,然后重启电脑。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/other-deployments.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/other-deployments.md new file mode 100644 index 0000000..5c5dcd6 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/other-deployments.md @@ -0,0 +1,5 @@ +# 其他部署方式 + +- [CasaOS 部署](./casaos.md) +- [优云智算 GPU 部署](./compshare.md) +- [社区提供的部署方式](./community-deployment.md) diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/package.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/package.md new file mode 100644 index 0000000..96a50f5 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/package.md @@ -0,0 +1,24 @@ +# 包管理器部署(uv) + +使用 `uv` 可以快速安装并启动 AstrBot。 + +## 前置条件 + +如果尚未安装 `uv`,请先按照官方文档安装: + +`uv` 支持 Linux、Windows、macOS。 + +## 注意事项 + +> [!WARNING] +> 通过 `uv` 部署的 AstrBot **不支持在 WebUI 中进行版本升级**。如需更新,请在命令行中执行 `uv tool upgrade astrbot --python 3.12`。 + +AstrBot 需要 Python 3.12 或更高版本。使用 `--python 3.12` 可以确保 `uv` 使用 Python 3.12 创建 tool 环境;如果启用了 Python 自动下载,`uv` 会在缺少 Python 3.12 时自动下载。 + +## 安装并启动 + +```bash +uv tool install astrbot --python 3.12 +astrbot init # 只需要在第一次部署时执行,后续启动不需要执行 +astrbot run +``` diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/rainyun.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/rainyun.md new file mode 100644 index 0000000..818785e --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/rainyun.md @@ -0,0 +1,44 @@ +# 通过 雨云 一键部署 + +[雨云](https://www.rainyun.com/about)成立于 2018 年,是具有自主知识产权的国产云计算服务提供商,具有可靠的营业资质和实体办公场所。 + +AstrBot 已经上架至雨云的预装软件列表,支持**一键安装** AstrBot 并提供高性能的云计算资源,保证 `AstrBot` 24 小时在线。 + +目前有两种部署方式:云服务器部署和云应用部署。 + +## 云服务器 + +1. 打开 [雨云官网](https://www.rainyun.com/NjU1ODg0_)。 +2. 根据你的喜好和预算,选择一个合适的服务器配置。建议选择 至少 2 核 CPU、4GB 内存的服务器,以确保 AstrBot 的流畅运行。 +3. 在下面的 `系统和软件安装` 一节,选中 `AstrBot`,然后点击 `立即购买`。 +4. 如果您的余额不足,将会跳转至充值页面。充值完成后再返回点击 `立即购买` 即可。 + +![AstrBot - 系统和软件安装](https://files.astrbot.app/docs/source/images/rainyun/image.png) + +接下来,雨云会自动帮您安装好系统和 `AstrBot` 软件。 + +如果有疑问,请: + +1. 点击雨云官网右下角 `咨询` 提交工单 +2. 点击雨云官网上方 `交流社区` 添加雨云 QQ 群。 + +## 云应用 + +雨云支持更加优惠的云应用部署方式来一键部署 AstrBot。点击以下图标来部署: + +[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0) + +## 附录: 配置端口映射 + +> [!NOTE] +> 只有当您购买的是 `江苏宿迁` 的服务器时,才需要配置端口映射。 + +通过 `我的云服务器` 进入 `云服务器` 页面,可以看到 `NAT端口映射管理` 卡片,如下图所示: + +![NAT端口映射管理](https://files.astrbot.app/docs/source/images/rainyun/image-1.png) + +点击 `+端口设置` -> `新建规则`,如下图所示: + +![创建NAT端口映射规则](https://files.astrbot.app/docs/source/images/rainyun/image-2.png) + +然后,内网端口填写 `6185`,点击 `创建映射规则`,这样就可以通过 `http://IP:上面设置好的外网端口` 访问 AstrBot 的管理面板了。如果无法打开,请点击`备用地址`,通过备用地址访问管理面板。 diff --git a/docs/snapshots/v4.23.6/zh/deploy/astrbot/sys-pm.md b/docs/snapshots/v4.23.6/zh/deploy/astrbot/sys-pm.md new file mode 100644 index 0000000..48bf94b --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/astrbot/sys-pm.md @@ -0,0 +1,39 @@ +# 通过系统包管理器安装 +> [!WARNING] +> 目前仅提供AUR版本 +> 如果你是windows用户/macos用户,建议通过uv来安装 +> 如果你是Linux用户,强烈建议通过包管理器来安装 + +# 准备步骤 + +## AUR 是什么? +AUR允许用户从社区维护的软件仓库中安装软件。AUR的包通常是由社区成员维护的,而不是官方维护的。 +常见的AUR助手有yay,paru。 +以下教程以paru为例,yay同理,仅需将paru替换为yay。 + +# 安装过程 + +## AUR +```bash +paru -S astrbot-git +# 提示: +# 开始审阅步骤,按q可退出审阅,继续安装 +# 安装后数据目录固定在:~/.local/share/astrbot +``` +# 启动 +>[!TIP] +> 你可以直接使用 astrbot init (首次运行)初始化 +> 使用astrbot run运行 +> 但是更加推荐使用systemctl启动,拥有自动重启,日志轮转等功能 + +```bash +systemctl --user start astrbot.service +``` + +# 开机自启 +```bash +# 处于安全考虑,设计为以用户身份执行 +systemctl --user enable astrbot.service +# 如果需要立即启动,加上--now +# systemctl --user enable --now astrbot.service +``` diff --git a/docs/snapshots/v4.23.6/zh/deploy/when-deployed.md b/docs/snapshots/v4.23.6/zh/deploy/when-deployed.md new file mode 100644 index 0000000..b4a1795 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/deploy/when-deployed.md @@ -0,0 +1,24 @@ +# 支持我们 + +我们是开源免费项目,AstrBot 的持续开发和维护离不开社区的支持。如果你觉得这个项目对你有帮助,欢迎通过以下几种方式支持我们: + +1. 在 GitHub 上给 [AstrBot](https://github.com/AstrBotDevs/AstrBot) 点一个 Star ⭐️。 +2. 通过 [爱发电平台](https://afdian.com/a/astrbot_team) 支持我们。 +3. 通过这个链接「[雨云官网](https://www.rainyun.com/NjU1ODg0_)」购买云服务器或云应用部署 AstrBot。如果你正好需要云服务器(例如用于部署 AstrBot),非常推荐使用雨云的一键部署方案。雨云是具有自主知识产权的国产云计算服务提供商,拥有可靠的营业资质和实体办公场所。 +4. 如果您是企业用户,可以联系我们获取定制化、文档/项目首页赞助广告位等服务支持。 + +如果你愿意支持我们,那我会非常非常感谢你,也会继续用更优秀的产品回应你的信任。🌟 + +## Wakatime + +AstrBot 主仓库:[![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 文档:[![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) + +❤️ 非常欢迎您提交贡献到这个项目中,比如提交 Issue、PR。 + +## 正文 + +当你看到这里,说明已经成功部署好消息平台并且实现了第一条指令的收发。接下来,你可以配置大语言模型,或者添加插件。请参看 `配置-接入大模型服务` 一节。 diff --git a/docs/snapshots/v4.23.6/zh/dev/astrbot-config.md b/docs/snapshots/v4.23.6/zh/dev/astrbot-config.md new file mode 100644 index 0000000..40a75ee --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/astrbot-config.md @@ -0,0 +1,576 @@ +--- +outline: deep +--- + +# AstrBot 配置文件 + +## data/cmd_config.json + +AstrBot 的配置文件是一个 JSON 格式的文件。AstrBot 会在启动时读取这个文件,并根据文件中的配置来初始化 AstrBot,其路径位于 `data/cmd_config.json`。 + +> 在 AstrBot v4.0.0 版本及之后,我们引入了[多配置文件](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` 作为默认配置文件 `default`。其他您在 WebUI 新建的配置文件会存储在 `data/config/` 目录下,以 `abconf_` 开头。 + +AstrBot 默认配置如下: + +```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": ["*"], # "*" 表示使用所有可用的提供者 + "wake_prefix": "", + "web_search": False, + "websearch_provider": "tavily", + "websearch_tavily_key": [], + "websearch_bocha_key": [], + "websearch_brave_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": 120, + }, + "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": { + # 平台特异配置:按平台分类,平台下按功能分组 + "lark": { + "pre_ack_emoji": {"enable": False, "emojis": ["Typing"]}, + }, + "telegram": { + "pre_ack_emoji": {"enable": False, "emojis": ["✍️"]}, + }, + "discord": { + "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": "", # 默认知识库名称 + "plugin_set": ["*"], # "*" 表示使用所有可用的插件, 空列表表示不使用任何插件 +} +``` + +## 字段详解 + +### `config_version` + +配置文件版本,请勿修改。 + +### `platform_settings` + +消息平台适配器的通用设置。 + +#### `platform_settings.unique_session` + +是否启用会话隔离。默认为 `false`。启用后,在群组或者频道中,每个人的对话的上下文都是独立的。 + +#### `platform_settings.rate_limit` + +当消息速率超过限制时的处理策略。`time` 为时间窗口,`count` 为消息数量,`strategy` 为限制策略。`stall` 为等待,`discard` 为丢弃。 + +#### `platform_settings.reply_prefix` + +回复消息时的固定前缀字符串。默认为空。 + +#### `platform_settings.forward_threshold` + +> 目前仅 QQ 平台适配器适用。 + +消息转发阈值。当回复内容超过一定字数后,机器人会将消息折叠成 QQ 群聊的 “转发消息”,以防止刷屏。 + +#### `platform_settings.enable_id_white_list` + +是否启用 ID 白名单。默认为 `true`。启用后,只有在白名单中的 ID 发来的消息才会被处理。 + +#### `platform_settings.id_whitelist` + +ID 白名单。填写后,将只处理所填写的 ID 发来的消息事件。为空时表示不启用白名单过滤。可以使用 `/sid` 指令获取在某个平台上的会话 ID。 + +也可在 AstrBot 日志内获取会话 ID,当一条消息没通过白名单时,会输出 INFO 级别的日志,格式类似 `aiocqhttp:GroupMessage:547540978` + +#### `platform_settings.id_whitelist_log` + +是否打印未通过 ID 白名单的消息日志。默认为 `true`。 + +#### `platform_settings.wl_ignore_admin_on_group` & `platform_settings.wl_ignore_admin_on_friend` + +- `wl_ignore_admin_on_group`: 是否管理员发送的群组消息无视 ID 白名单。默认为 `true`。 + +- `wl_ignore_admin_on_friend`: 是否管理员发送的私聊消息无视 ID 白名单。默认为 `true`。 + +#### `platform_settings.reply_with_mention` + +是否在回复消息时 @ 提到用户。默认为 `false`。 + +#### `platform_settings.reply_with_quote` + +是否在回复消息时引用用户的消息。默认为 `false`。 + +#### `platform_settings.path_mapping` + +*该配置项已经在 v4.0.0 版本之后被废弃。* + +路径映射列表。用于将消息中的文件路径进行替换。每个映射项包含 `from` 和 `to` 两个字段,表示将消息中的 `from` 路径替换为 `to` 路径。 + +#### `platform_settings.segmented_reply` + +分段回复设置。 + +- `enable`: 是否启用分段回复。默认为 `false`。 +- `only_llm_result`: 是否仅对 LLM 生成的回复进行分段。默认为 `true`。 +- `interval_method`: 分段间隔方法。可选值为 `random` 和 `log`。默认为 `random`。 +- `interval`: 分段间隔时间。对于 `random` 方法,填写两个逗号分隔的数字,表示最小和最大间隔时间(单位:秒)。对于 `log` 方法,填写一个数字,表示对数基底。默认为 `"1.5,3.5"`。 +- `log_base`: 对数基底,仅在 `interval_method` 为 `log` 时适用。默认为 `2.6`。 +- `words_count_threshold`: 分段回复的字数上限。只有字数小于此值的消息才会被分段,超过此值的长消息将直接发送(不分段)。默认为 `150`。 +- `regex`: 用于分隔一段消息。默认情况下会根据句号、问号等标点符号分隔。`re.findall(r'', text)`。默认值为 `".*?[。?!~…]+|.+$"`。 +- `content_cleanup_rule`: 移除分段后的内容中的指定的内容。支持正则表达式。如填写 `[。?!]` 将移除所有的句号、问号、感叹号。`re.sub(r'', '', text)`。 + +#### `platform_settings.no_permission_reply` + +是否在用户没有权限时回复无权限的提示消息。默认为 `true`。 + +#### `platform_settings.empty_mention_waiting` + +是否启用空 @ 等待机制。默认为 `true`。启用后,当用户发送一条仅包含 @ 机器人的消息时,机器人会等待用户在 60 秒内发送下一条消息,并将两条消息合并后进行处理。这在某些平台不支持 @ 和语音/图片等消息同时发送时特别有用。 + +#### `platform_settings.empty_mention_waiting_need_reply` + +在上面一个配置项(`empty_mention_waiting`)中,如果启用了触发等待,启用此项后,机器人会立即使用 LLM 生成一条回复。否则,将不回复而只是等待。默认为 `true`。 + +#### `platform_settings.friend_message_needs_wake_prefix` + +是否在消息平台的私聊消息中需要唤醒前缀。默认为 `false`。启用后,在私聊消息中,用户需要使用唤醒前缀才能触发机器人的响应。 + +#### `platform_settings.ignore_bot_self_message` + +是否忽略机器人自己发送的消息。默认为 `false`。启用后,机器人将不会处理自己发送的消息,在某些平台可以防止死循环。 + +#### `platform_settings.ignore_at_all` + +是否忽略 @ 全体成员的消息。默认为 `false`。启用后,机器人将不会响应包含 @ 全体成员的消息。 + +### `provider` + +> 此配置项仅在 `data/cmd_config.json` 中生效,AstrBot 不会读取 `data/config/` 目录下的配置文件中的此项。 + +已配置的模型服务提供商的配置列表。 + +### `provider_settings` + +大语言模型提供商的通用设置。 + +#### `provider_settings.enable` + +是否启用大语言模型聊天。默认为 `true`。 + +#### `provider_settings.default_provider_id` + +默认的对话模型提供商 ID。必须是 `provider` 列表中已配置的提供商 ID。如果为空,则使用配置列表中的第一个对话模型提供商。 + +#### `provider_settings.default_image_caption_provider_id` + +默认的图像描述模型提供商 ID。必须是 `provider` 列表中已配置的提供商 ID。如果为空,则代表不使用图像描述功能。 + +此配置项的意思是,当用户发送一张图片时,AstrBot 会使用此提供商来生成对图片的描述文本,并将描述文本作为对话的上下文之一。这在对话模型不支持多模态输入时特别有用。 + +#### `provider_settings.image_caption_prompt` + +图像描述的提示词模板。默认为 `"Please describe the image using Chinese."`。 + +#### `provider_settings.provider_pool` + +*此配置项尚未实际使用* + +#### `provider_settings.wake_prefix` + +使用 LLM 聊天额外的触发条件。如填写 `chat`,则需要发送消息时要以 `/chat` 才能触发 LLM 聊天。其中 `/` 是机器人的唤醒前缀。是一个防止滥用的手段。 + +#### `provider_settings.web_search` + +是否启用 AstrBot 自带的网页搜索能力。默认为 `false`。启用后,LLM 可能会自动搜索网页并根据内容回答。 + +#### `provider_settings.websearch_provider` + +网页搜索提供商类型。默认为 `tavily`。目前支持 `tavily`、`bocha`、`baidu_ai_search`、`brave`。 + +- `tavily`:使用 Tavily 搜索引擎。 +- `bocha`:使用 BoCha 搜索引擎。 +- `baidu_ai_search`:使用百度 AI Search(MCP)。 +- `brave`:使用 Brave Search API。 + +#### `provider_settings.websearch_tavily_key` + +Tavily 搜索引擎的 API Key 列表。使用 `tavily` 作为网页搜索提供商时需要填写。 + +#### `provider_settings.websearch_bocha_key` + +BoCha 搜索引擎的 API Key 列表。使用 `bocha` 作为网页搜索提供商时需要填写。 + +#### `provider_settings.websearch_brave_key` + +Brave 搜索引擎的 API Key 列表。使用 `brave` 作为网页搜索提供商时需要填写。 + +#### `provider_settings.web_search_link` + +是否在回复中提示模型附上搜索结果的链接。默认为 `false`。 + +#### `provider_settings.display_reasoning_text` + +是否在回复中显示模型的推理过程。默认为 `false`。 + +#### `provider_settings.identifier` + +是否在 Prompt 前加上群成员的名字以让模型更好地了解群聊状态。默认为 `false`。启用将略微增加 token 开销。 + +#### `provider_settings.group_name_display` + +是否在提示模型了解所在群的名称。默认为 `false`。此配置项目前仅在 QQ 平台适配器中生效。 + +#### `provider_settings.datetime_system_prompt` + +是否在系统提示词中加上当前机器的日期时间。默认为 `true`。 + +#### `provider_settings.default_personality` + +默认使用的人格的 ID。请在 WebUI 配置人格。 + +#### `provider_settings.persona_pool` + +*此配置项尚未实际使用* + +#### `provider_settings.prompt_prefix` + +用户提示词。可使用 `{{prompt}}` 作为用户输入的占位符。如果不输入占位符则代表添加在用户输入的前面。 + +#### `provider_settings.max_context_length` + +当对话上下文超出这个数量时丢弃最旧的部分,一轮聊天记为 1 条。-1 为不限制。 + +#### `provider_settings.dequeue_context_length` + +当触发上面提到的 `max_context_length` 限制时,每次丢弃的对话轮数。 + +#### `provider_settings.streaming_response` + +是否启用流式响应。默认为 `false`。启用后,模型的回复会实时类似打字机的效果发送给用户。此配置项仅在 WebChat、Telegram、飞书平台生效。 + +#### `provider_settings.show_tool_use_status` + +是否显示工具使用状态。默认为 `false`。启用后,模型在使用工具时会显示工具的名称和输入参数。 + +#### `provider_settings.streaming_segmented` + +不支持流式响应的消息平台是否降级为使用分段回复。默认为 `false`。意思是,如果启用了流式响应,但当前消息平台不支持流式响应,那么是否使用分段多次回复来代替。 + +#### `provider_settings.max_agent_step` + +Agent 最大步骤数限制。默认为 `30`。模型的每次工具调用算作一步。 + +#### `provider_settings.tool_call_timeout` + +Added in `v4.3.5` + +工具调用的最大超时时间(秒),默认为 `60` 秒。 + +#### `provider_stt_settings` + +语音转文本服务提供商的通用设置。 + +#### `provider_stt_settings.enable` + +是否启用语音转文本服务。默认为 `false`。 + +#### `provider_stt_settings.provider_id` + +语音转文本服务提供商 ID。必须是 `provider` 列表中已配置的 STT 提供商 ID。 + +#### `provider_tts_settings` + +文本转语音服务提供商的通用设置。 + +#### `provider_tts_settings.enable` + +是否启用文本转语音服务。默认为 `false`。 + +#### `provider_tts_settings.provider_id` + +文本转语音服务提供商 ID。必须是 `provider` 列表中已配置的 TTS 提供商 ID。 + +#### `provider_tts_settings.dual_output` + +是否启用双输出。默认为 `false`。启用后,机器人会同时发送文本和语音消息。 + +#### `provider_tts_settings.use_file_service` + +是否启用文件服务。默认为 `false`。启用后,机器人会将输出的语音文件以 HTTP 文件外链的形式提供给消息平台。此配置项依赖于 `callback_api_base` 的配置。 + +#### `provider_ltm_settings` + +群聊上下文感知服务提供商的通用设置。 + +#### `provider_ltm_settings.group_icl_enable` + +是否启用群聊上下文感知。默认为 `false`。启用后,机器人会记录群聊中的对话内容,以便更好地理解群聊的上下文。 + +上下文的内容会被放在对话的系统提示词中。 + +#### `provider_ltm_settings.group_message_max_cnt` + +群聊消息的最大记录数量。默认为 `100`。超过此数量的消息将被丢弃。 + +#### `provider_ltm_settings.image_caption` + +是否记录群聊中的图片,并自动使用图像描述模型生成图片的描述文本。默认为 `false`。此配置项依赖于 `provider_settings.default_image_caption_provider_id` 的配置。请谨慎使用,因为这可能会增加大量的 API 调用和 token 开销。 + +#### `provider_ltm_settings.active_reply` + +- `enable`: 是否启用主动回复。默认为 `false`。 +- `method`: 主动回复的方法。可选值为 `possibility_reply`。 +- `possibility_reply`: 主动回复的概率。默认为 `0.1`。仅在 `method` 为 `possibility_reply` 时适用。 +- `whitelist`: 主动回复的 ID 白名单。仅在此列表中的 ID 才会触发主动回复。为空时表示不启用白名单过滤。可以使用 `/sid` 指令获取在某个平台上的会话 ID。 + +### `content_safety` + +内容安全设置。 + +#### `content_safety.also_use_in_response` + +是否在 LLM 回复中也进行内容安全检查。默认为 `false`。启用后,机器人生成的回复也会经过内容安全检查,以防止生成不当内容。 + +#### `content_safety.internal_keywords` + +内部关键词检测设置。 + +- `enable`: 是否启用内部关键词检测。默认为 `true`。 +- `extra_keywords`: 额外的关键词列表,支持正则表达式。默认为空。 + +#### `content_safety.baidu_aip` + +百度 AI 内容审核设置。 + +- `enable`: 是否启用百度 AI 内容审核。默认为 `false`。 +- `app_id`: 百度 AI 内容审核的 App ID。 +- `api_key`: 百度 AI 内容审核的 API Key。 +- `secret_key`: 百度 AI 内容审核的 Secret Key。 + +> [!TIP] +> 如果要启用百度 AI 内容审核,请先 `pip install baidu-aip`。 + +### `admins_id` + +管理员 ID 列表。此外,还可以使用 `/op`, `/deop` 指令来添加或删除管理员。 + +### `t2i` + +是否启用文本转图像功能。默认为 `false`。启用后,当用户发送的消息超过一定字数时,机器人会将消息渲染成图片发送给用户,以提高可读性并防止刷屏。支持 Markdown 渲染。 + +### `t2i_word_threshold` + +文本转图像的字数阈值。默认为 `150`。当用户发送的消息超过此字数时,机器人会将消息渲染成图片发送给用户。 + +### `t2i_strategy` + +文本转图像的渲染策略。可选值为 `local` 和 `remote`。默认为 `remote`。 + +- `local`: 使用 AstrBot 本地的文本转图像服务进行渲染。效果较差,但不依赖外部服务。 +- `remote`: 使用远程的文本转图像服务进行渲染。默认使用 AstrBot 官方提供的服务,效果较好。 + +### `t2i_endpoint` + +AstrBot API 的地址。用于渲染 Markdown 图片。当 `t2i_strategy` 为 `remote` 时生效。默认为空,表示使用 AstrBot 官方提供的服务。 + +### `t2i_use_file_service` + +是否启用文件服务。默认为 `false`。启用后,机器人会将渲染的图片以 HTTP 文件外链的形式提供给消息平台。此配置项依赖于 `callback_api_base` 的配置。 + +### `http_proxy` + +HTTP 代理。如 `http://localhost:7890`。 + +### `no_proxy` + +不使用代理的地址列表。如 `["localhost", "127.0.0.1"]`。 + +### `dashboard` + +AstrBot WebUI 配置。 + +请不要随意修改 `password` 的值。它是一个经过 `md5` 编码的密码。请在控制面板修改密码。 + +- `enable`: 是否启用 AstrBot WebUI。默认为 `true`。 +- `username`: AstrBot WebUI 的用户名。默认为 `astrbot`。 +- `password`: AstrBot WebUI 的密码。默认为 `astrbot` 的 `md5` 编码值。请勿直接修改,除非您知道自己在做什么。 +- `jwt_secret`: JWT 的密钥。AstrBot 会在初始化时随机生成。请勿修改,除非您知道自己在做什么。 +- `host`: AstrBot WebUI 监听的地址。默认为 `0.0.0.0`。 +- `port`: AstrBot WebUI 监听的端口。默认为 `6185`。 + +### `platform` + +> 此配置项仅在 `data/cmd_config.json` 中生效,AstrBot 不会读取 `data/config/` 目录下的配置文件中的此项。 + +已配置的 AstrBot 消息平台适配器的配置列表。 + +### `platform_specific` + +平台特异配置。按平台分类,平台下按功能分组。 + +#### `platform_specific..pre_ack_emoji` + +启用后,当请求 LLM 前,AstrBot 会先发送一个预回复的表情以告知用户正在处理请求。此功能目前仅在飞书平台适配器和 Telegram 中生效。 + +##### lark (飞书) + +- `enable`: 是否启用飞书消息预回复表情。默认为 `false`。 +- `emojis`: 预回复的表情列表。默认为 `["Typing"]`。表情枚举名参考:[表情文案说明](https://open.feishu.cn/document/server-docs/im-v1/message-reaction/emojis-introduce) + +##### telegram + +- `enable`: 是否启用 Telegram 消息预回复表情。默认为 `false`。 +- `emojis`: 预回复的表情列表。默认为 `["✍️"]`。Telegram 仅支持固定反应集合,参考:[reactions.txt](https://gist.github.com/Soulter/3f22c8e5f9c7e152e967e8bc28c97fc9) + +##### discord + +- `enable`: 是否启用 Discord 消息预回复表情。默认为 `false`。 +- `emojis`: 预回复的表情列表。默认为 `["🤔"]`。Discord反应支持参考:[Discord Reaction FAQ](https://support.discord.com/hc/en-us/articles/12102061808663-Reactions-and-Super-Reactions-FAQ) + +### `wake_prefix` + +唤醒前缀。默认为 `/`。当消息以 `/` 开头时,AstrBot 会被唤醒。 + +> [!TIP] +> 如果唤醒的会话不在 ID 白名单中,AstrBot 将不会响应。 + +### `log_level` + +日志级别。默认为 `INFO`。可以设置为 `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`。 + +### `trace_enable` + +是否启用追踪记录。默认为 `false`。启用后,AstrBot 会记录运行追踪信息,可以在管理面板的 Trace 页面查看。 + +### `pip_install_arg` + +`pip install` 的参数。如 `-i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple`。 + +### `pypi_index_url` + +PyPI 镜像源地址。默认为 `https://mirrors.aliyun.com/pypi/simple/`。 + +### `persona` + +*此配置项已经在 v4.0.0 版本之后被废弃。请使用 WebUI 来配置人格。* + +已配置的人格列表。每个人格包含 `id`, `name`, `description`, `system_prompt` 四个字段。 + +### `timezone` + +时区设置。请填写 IANA 时区名称, 如 Asia/Shanghai, 为空时使用系统默认时区。所有时区请查看: [IANA Time Zone Database](https://data.iana.org/time-zones/tzdb-2021a/zone1970.tab)。 + +### `callback_api_base` + +AstrBot API 的基础地址。用于文件服务和插件回调等功能。如 `http://example.com:6185`。默认为空,表示不启用文件服务和插件回调功能。 + +### `default_kb_collection` + +默认知识库名称。用于 RAG 功能。如果为空,则不使用知识库。 + +### `plugin_set` + +已启用的插件列表。`*` 表示启用所有可用的插件。默认为 `["*"]`。 diff --git a/docs/snapshots/v4.23.6/zh/dev/openapi.md b/docs/snapshots/v4.23.6/zh/dev/openapi.md new file mode 100644 index 0000000..4ac8f84 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/openapi.md @@ -0,0 +1,150 @@ +--- +outline: deep +--- + +# AstrBot HTTP API + +从 v4.18.0 开始,AstrBot 提供基于 API Key 的 HTTP API,开发者可以通过标准 HTTP 请求访问核心能力。 + +## 快速开始 + +1. 在 WebUI - 设置中创建 API Key。 +2. 在请求头中携带 API Key: + +```http +Authorization: Bearer abk_xxx +``` + +也支持: + +```http +X-API-Key: abk_xxx +``` + +3. 对于对话接口,`username` 为必填参数: + +- `POST /api/v1/chat`:请求体必须包含 `username` +- `GET /api/v1/chat/sessions`:查询参数必须包含 `username` + +## Scope 权限说明 + +创建 API Key 时可配置 `scopes`。每个 scope 控制可访问的接口范围: + +| Scope | 作用 | 可访问接口 | +| --- | --- | --- | +| `chat` | 调用对话能力、查询对话会话 | `POST /api/v1/chat`、`GET /api/v1/chat/sessions` | +| `config` | 获取可用配置文件列表 | `GET /api/v1/configs` | +| `file` | 上传附件文件,获取 `attachment_id` | `POST /api/v1/file` | +| `im` | 主动发 IM 消息、查询 bot/platform 列表 | `POST /api/v1/im/message`、`GET /api/v1/im/bots` | + +如果 API Key 未包含目标接口所需 scope,请求会返回 `403 Insufficient API key scope`。 + +## 常用接口 + +**对话类** + +调用 AstrBot 内建的 Agent 进行对话交互。支持插件调用、工具调用等能力,与 IM 端对话能力一致。 + +- `POST /api/v1/chat`:发送对话消息(SSE 流式返回,不传 `session_id` 会自动创建 UUID) +- `GET /api/v1/chat/sessions`:分页获取指定 `username` 的会话 +- `GET /api/v1/configs`:获取可用配置文件列表 + +**文件上传** + +- `POST /api/v1/file`:上传附件 + +**IM 消息发送** + +- `POST /api/v1/im/message`:按 UMO 主动发消息 +- `GET /api/v1/im/bots`:获取 bot/platform ID 列表 + +## `message` 字段格式(重点) + +`POST /api/v1/chat` 和 `POST /api/v1/im/message` 的 `message` 字段支持两种格式: + +1. 字符串:纯文本消息 +2. 数组:消息段(message chain) + +### 1. 纯文本格式 + +```json +{ + "message": "Hello" +} +``` + +### 2. 消息段数组格式 + +```json +{ + "message": [ + { "type": "plain", "text": "请看这个文件" }, + { "type": "file", "attachment_id": "9a2f8c72-e7af-4c0e-b352-111111111111" } + ] +} +``` + +支持的 `type`: + +| type | 必填字段 | 可选字段 | 说明 | +| --- | --- | --- | --- | +| `plain` | `text` | - | 文本段 | +| `reply` | `message_id` | `selected_text` | 引用回复某条消息 | +| `image` | `attachment_id` | - | 图片附件段 | +| `record` | `attachment_id` | - | 音频附件段 | +| `file` | `attachment_id` | - | 通用文件段 | +| `video` | `attachment_id` | - | 视频附件段 | + +* reply 消息段目前仅适配 `/api/v1/chat`,不适用于 `POST /api/v1/im/message`。 + + +说明: + +- `attachment_id` 来自 `POST /api/v1/file` 上传结果。 +- `reply` 不能单独作为唯一内容,至少需要一个有实际内容的段(如 `plain/image/file/...`)。 +- 仅 `reply` 或空内容会返回错误。 + +### Chat API 的 `message` 用法 + +`POST /api/v1/chat` 额外需要 `username`,可选 `session_id`(不传会自动创建 UUID)。 + +```json +{ + "username": "alice", + "session_id": "my_session_001", + "message": [ + { "type": "plain", "text": "帮我总结这个 PDF" }, + { "type": "file", "attachment_id": "9a2f8c72-e7af-4c0e-b352-111111111111" } + ], + "enable_streaming": true +} +``` + +### IM Message API 的 `message` 用法 + +`POST /api/v1/im/message` 需要 `umo` + `message`。 + +```json +{ + "umo": "webchat:FriendMessage:openapi_probe", + "message": [ + { "type": "plain", "text": "这是主动消息" }, + { "type": "image", "attachment_id": "9a2f8c72-e7af-4c0e-b352-222222222222" } + ] +} +``` + +## 示例 + +```bash +curl -N 'http://localhost:6185/api/v1/chat' \ + -H 'Authorization: Bearer abk_xxx' \ + -H 'Content-Type: application/json' \ + -d '{"message":"Hello","username":"alice"}' +``` + +## 完整 API 文档 + +交互式 API 文档请查看: + +- https://docs.astrbot.app/scalar.html diff --git a/docs/snapshots/v4.23.6/zh/dev/plugin-platform-adapter.md b/docs/snapshots/v4.23.6/zh/dev/plugin-platform-adapter.md new file mode 100644 index 0000000..8e65528 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/plugin-platform-adapter.md @@ -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) + + +有任何疑问欢迎加群询问~ \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/zh/dev/plugin.md b/docs/snapshots/v4.23.6/zh/dev/plugin.md new file mode 100644 index 0000000..d929443 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/plugin.md @@ -0,0 +1 @@ +本页面已经迁移至 [插件基础开发](/dev/star/plugin)。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/zh/dev/star/guides/ai.md b/docs/snapshots/v4.23.6/zh/dev/star/guides/ai.md new file mode 100644 index 0000000..549275a --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/guides/ai.md @@ -0,0 +1,553 @@ + +# AI + +AstrBot 内置了对多种大语言模型(LLM)提供商的支持,并且提供了统一的接口,方便插件开发者调用各种 LLM 服务。 + +您可以使用 AstrBot 提供的 LLM / Agent 接口来实现自己的智能体。 + +我们在 `v4.5.7` 版本之后对 LLM 提供商的调用方式进行了较大调整,推荐使用新的调用方式。新的调用方式更加简洁,并且支持更多的功能。当然,您仍然可以使用[旧的调用方式](/dev/star/plugin#ai)。 + +## 获取当前会话使用的聊天模型 ID + +> [!TIP] +> 在 v4.5.7 时加入 + +```py +umo = event.unified_msg_origin +provider_id = await self.context.get_current_chat_provider_id(umo=umo) +``` + +## 调用大模型 + +> [!TIP] +> 在 v4.5.7 时加入 + +```py +llm_resp = await self.context.llm_generate( + chat_provider_id=provider_id, # 聊天模型 ID + prompt="Hello, world!", +) +# print(llm_resp.completion_text) # 获取返回的文本 +``` + +## 定义 Tool + +Tool 是大语言模型调用外部工具的能力。 + +```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" # 工具名称 + description: str = "A tool to fetch Bilibili videos." # 工具描述 + 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. 视频标题:如何使用AstrBot\n视频链接:xxxxxx" +``` + +## 注册 Tool 到 AstrBot + +在上面定义好 Tool 之后,如果你需要实现的功能是让用户在使用 AstrBot 进行对话时自动调用该 Tool,那么你需要在插件的 __init__ 方法中将 Tool 注册到 AstrBot 中: + +```py +class MyPlugin(Star): + def __init__(self, context: Context): + super().__init__(context) + # >= v4.5.1 使用: + self.context.add_llm_tools(BilibiliTool(), SecondTool(), ...) + + # < v4.5.1 之前使用: + tool_mgr = self.context.provider_manager.llm_tools + tool_mgr.func_list.append(BilibiliTool()) +``` + +### 通过装饰器定义 Tool 和注册 Tool + +除了上述的通过 `@dataclass` 定义 Tool 的方式之外,你也可以使用装饰器的方式注册 tool 到 AstrBot。如果请务必按照以下格式编写一个工具(包括函数注释,AstrBot 会解析该函数注释,请务必将注释格式写对) + +```py{3,4,5,6,7} +@filter.llm_tool(name="get_weather") # 如果 name 不填,将使用函数名 +async def get_weather(self, event: AstrMessageEvent, location: str) -> MessageEventResult: + '''获取天气信息。 + + Args: + location(string): 地点 + ''' + resp = self.get_weather_from_api(location) + yield event.plain_result("天气信息: " + resp) +``` + +在 `location(string): 地点` 中,`location` 是参数名,`string` 是参数类型,`地点` 是参数描述。 + +支持的参数类型有 `string`, `number`, `object`, `boolean`, `array`。在 v4.5.7 之后,支持对 `array` 类型参数指定子类型,例如 `array[string]`。 + +## 调用 Agent + +> [!TIP] +> 在 v4.5.7 时加入 + +Agent 可以被定义为 system_prompt + tools + llm 的结合体,可以实现更复杂的智能体行为。 + +在上面定义好 Tool 之后,可以通过以下方式调用 Agent: + +```py +llm_resp = await self.context.tool_loop_agent( + event=event, + chat_provider_id=prov_id, + prompt="搜索一下 bilibili 上关于 AstrBot 的相关视频。", + tools=ToolSet([BilibiliTool()]), + max_steps=30, # Agent 最大执行步骤 + tool_call_timeout=60, # 工具调用超时时间 +) +# print(llm_resp.completion_text) # 获取返回的文本 +``` + +`tool_loop_agent()` 方法会自动处理工具调用和大模型请求的循环,直到大模型不再调用工具或者达到最大步骤数为止。 + +## Multi-Agent + +> [!TIP] +> 在 v4.5.7 时加入 + +Multi-Agent(多智能体)系统将复杂应用分解为多个专业化智能体,它们协同解决问题。不同于依赖单个智能体处理每一步,多智能体架构允许将更小、更专注的智能体组合成协调的工作流程。我们使用 `agent-as-tool` 模式来实现多智能体系统。 + +在下面的例子中,我们定义了一个主智能体(Main Agent),它负责根据用户查询将任务分配给不同的子智能体(Sub-Agents)。每个子智能体专注于特定任务,例如获取天气信息。 + +![multi-agent-example-1](https://files.astrbot.app/docs/zh/dev/star/guides/multi-agent-example-1.svg) + +定义 Tools: + +```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 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 + ) -> ToolExecResult: + # 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 + ) -> ToolExecResult: + 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 + ) -> ToolExecResult: + 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 + ) -> ToolExecResult: + return "I am useless :(, you shouldn't call me :(" +``` + +然后,同样地,通过 `tool_loop_agent()` 方法调用 Agent: + +```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) +``` + +## 对话管理器 + +### 获取会话当前的 LLM 对话历史 `get_conversation` + +```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.""" +``` + +::: + +### 快速添加 LLM 记录到对话 `add_message_pair` + +```py +from astrbot.core.agent.message import ( + AssistantMessageSegment, + UserMessageSegment, + TextPart, +) + +curr_cid = await conv_mgr.get_curr_conversation_id(event.unified_msg_origin) +user_msg = UserMessageSegment(content=[TextPart(text="hi")]) +llm_resp = await self.context.llm_generate( + chat_provider_id=provider_id, # 聊天模型 ID + contexts=[user_msg], # 当未指定 prompt 时,使用 contexts 作为输入;同时指定 prompt 和 contexts 时,prompt 会被添加到 LLM 输入的最后 +) +await conv_mgr.add_message_pair( + cid=curr_cid, + user_message=user_msg, + assistant_message=AssistantMessageSegment( + content=[TextPart(text=llm_resp.completion_text)] + ), +) +``` + +### 主要方法 + +#### `new_conversation` + +- __Usage__ + 在当前会话中新建一条对话,并自动切换为该对话。 +- __Arguments__ + - `unified_msg_origin: str` – 形如 `platform_name:message_type:session_id` + - `platform_id: str | None` – 平台标识,默认从 `unified_msg_origin` 解析 + - `content: list[dict] | None` – 初始历史消息 + - `title: str | None` – 对话标题 + - `persona_id: str | None` – 绑定的 persona ID +- __Returns__ + `str` – 新生成的 UUID 对话 ID + +#### `switch_conversation` + +- __Usage__ + 将会话切换到指定的对话。 +- __Arguments__ + - `unified_msg_origin: str` + - `conversation_id: str` +- __Returns__ + `None` + +#### `delete_conversation` + +- __Usage__ + 删除会话中的某条对话;若 `conversation_id` 为 `None`,则删除当前对话。 +- __Arguments__ + - `unified_msg_origin: str` + - `conversation_id: str | None` +- __Returns__ + `None` + +#### `get_curr_conversation_id` + +- __Usage__ + 获取当前会话正在使用的对话 ID。 +- __Arguments__ + - `unified_msg_origin: str` +- __Returns__ + `str | None` – 当前对话 ID,不存在时返回 `None` + +#### `get_conversation` + +- __Usage__ + 获取指定对话的完整对象;若不存在且 `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__ + 拉取用户或平台下的全部对话列表。 +- __Arguments__ + - `unified_msg_origin: str | None` – 为 `None` 时不过滤用户 + - `platform_id: str | None` +- __Returns__ + `List[Conversation]` + +#### `update_conversation` + +- __Usage__ + 更新对话的标题、历史记录或 persona_id。 +- __Arguments__ + - `unified_msg_origin: str` + - `conversation_id: str | None` – 为 `None` 时使用当前对话 + - `history: list[dict] | None` + - `title: str | None` + - `persona_id: str | None` +- __Returns__ + `None` + +## 人格设定管理器 + +`PersonaManager` 负责统一加载、缓存并提供所有人格(Persona)的增删改查接口,同时兼容 AstrBot 4.x 之前的旧版人格格式(v3)。 +初始化时会自动从数据库读取全部人格,并生成一份 v3 兼容数据,供旧代码无缝使用。 + +```py +persona_mgr = self.context.persona_manager +``` + +### 主要方法 + +#### `get_persona` + +- __Usage__ + 获取根据人格 ID 获取人格数据。 +- __Arguments__ + - `persona_id: str` – 人格 ID +- __Returns__ + `Persona` – 人格数据,若不存在则返回 None +- __Raises__ + `ValueError` – 当不存在时抛出 + +#### `get_all_personas` + +- __Usage__ + 一次性获取数据库中所有人格。 +- __Returns__ + `list[Persona]` – 人格列表,可能为空 + +#### `create_persona` + +- __Usage__ + 新建人格并立即写入数据库,成功后自动刷新本地缓存。 +- __Arguments__ + - `persona_id: str` – 新人格 ID(唯一) + - `system_prompt: str` – 系统提示词 + - `begin_dialogs: list[str]` – 可选,开场对话(偶数条,user/assistant 交替) + - `tools: list[str]` – 可选,允许使用的工具列表;`None`=全部工具,`[]`=禁用全部 +- __Returns__ + `Persona` – 新建后的人格对象 +- __Raises__ + `ValueError` – 若 `persona_id` 已存在 + +#### `update_persona` + +- __Usage__ + 更新现有人格的任意字段,并同步到数据库与缓存。 +- __Arguments__ + - `persona_id: str` – 待更新的人格 ID + - `system_prompt: str` – 可选,新的系统提示词 + - `begin_dialogs: list[str]` – 可选,新的开场对话 + - `tools: list[str]` – 可选,新的工具列表;语义同 `create_persona` +- __Returns__ + `Persona` – 更新后的人格对象 +- __Raises__ + `ValueError` – 若 `persona_id` 不存在 + +#### `delete_persona` + +- __Usage__ + 删除指定人格,同时清理数据库与缓存。 +- __Arguments__ + - `persona_id: str` – 待删除的人格 ID +- __Raises__ + `Valueable` – 若 `persona_id` 不存在 + +#### `get_default_persona_v3` + +- __Usage__ + 根据当前会话配置,获取应使用的默认人格(v3 格式)。 + 若配置未指定或指定的人格不存在,则回退到 `DEFAULT_PERSONALITY`。 +- __Arguments__ + - `umo: str | MessageSession | None` – 会话标识,用于读取用户级配置 +- __Returns__ + `Personality` – v3 格式的默认人格对象 + +::: 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 人格类。 + + 在 v4.0.0 版本及之后,推荐使用上面的 Persona 类。并且, mood_imitation_dialogs 字段已被废弃。 + """ + + prompt: str + name: str + begin_dialogs: list[str] + mood_imitation_dialogs: list[str] + """情感模拟对话预设。在 v4.0.0 版本及之后,已被废弃。""" + tools: list[str] | None + """工具列表。None 表示使用所有工具,空列表表示不使用任何工具""" +``` + +::: diff --git a/docs/snapshots/v4.23.6/zh/dev/star/guides/env.md b/docs/snapshots/v4.23.6/zh/dev/star/guides/env.md new file mode 100644 index 0000000..7dd0480 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/guides/env.md @@ -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/)。 diff --git a/docs/snapshots/v4.23.6/zh/dev/star/guides/html-to-pic.md b/docs/snapshots/v4.23.6/zh/dev/star/guides/html-to-pic.md new file mode 100644 index 0000000..6249f2d --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/guides/html-to-pic.md @@ -0,0 +1,66 @@ + +# 文转图 + +> [!TIP] +> 为了方便开发,您可以使用 [AstrBot Text2Image Playground](https://t2i-playground.astrbot.app/) 在线可视化编辑和测试 HTML 模板。 + +## 基本 + +AstrBot 支持将文字渲染成图片。 + +```python +@filter.command("image") # 注册一个 /image 指令,接收 text 参数。 +async def on_aiocqhttp(self, event: AstrMessageEvent, text: str): + url = await self.text_to_image(text) # text_to_image() 是 Star 类的一个方法。 + # path = await self.text_to_image(text, return_url = False) # 如果你想保存图片到本地 + yield event.image_result(url) + +``` + +![image](https://files.astrbot.app/docs/source/images/plugin/image-3.png) + +## 自定义(基于 HTML) + +如果你觉得上面渲染出来的图片不够美观,你可以使用自定义的 HTML 模板来渲染图片。 + +AstrBot 支持使用 `HTML + Jinja2` 的方式来渲染文转图模板。 + +```py{7} +# 自定义的 Jinja2 模板,支持 CSS +TMPL = ''' +
+

Todo List

+ +
    +{% for item in items %} +
  • {{ item }}
  • +{% endfor %} +
+''' + +@filter.command("todo") +async def custom_t2i_tmpl(self, event: AstrMessageEvent): + options = {} # 可选择传入渲染选项。 + url = await self.html_render(TMPL, {"items": ["吃饭", "睡觉", "玩原神"]}, options=options) # 第二个参数是 Jinja2 的渲染数据 + yield event.image_result(url) +``` + +返回的结果: + +![image](https://files.astrbot.app/docs/source/images/plugin/fcc2dcb472a91b12899f617477adc5c7.png) + +这只是一个简单的例子。得益于 HTML 和 DOM 渲染器的强大性,你可以进行更复杂和更美观的的设计。除此之外,Jinja2 支持循环、条件等语法以适应列表、字典等数据结构。你可以从网上了解更多关于 Jinja2 的知识。 + +**图片渲染选项(options)**: + +请参考 Playwright 的 [screenshot](https://playwright.dev/python/docs/api/class-page#page-screenshot) API。 + +- `timeout` (float, optional): 截图超时时间. +- `type` (Literal["jpeg", "png"], optional): 截图图片类型. +- `quality` (int, optional): 截图质量,仅适用于 JPEG 格式图片. +- `omit_background` (bool, optional): 是否允许隐藏默认的白色背景,这样就可以截透明图了,仅适用于 PNG 格式 +- `full_page` (bool, optional): 是否截整个页面而不是仅设置的视口大小,默认为 True. +- `clip` (dict, optional): 截图后裁切的区域。参考 Playwright screenshot API。 +- `animations`: (Literal["allow", "disabled"], optional): 是否允许播放 CSS 动画. +- `caret`: (Literal["hide", "initial"], optional): 当设置为 hide 时,截图时将隐藏文本插入符号,默认为 hide. +- `scale`: (Literal["css", "device"], optional): 页面缩放设置. 当设置为 css 时,则将设备分辨率与 CSS 中的像素一一对应,在高分屏上会使得截图变小. 当设置为 device 时,则根据设备的屏幕缩放设置或当前 Playwright 的 Page/Context 中的 device_scale_factor 参数来缩放. diff --git a/docs/snapshots/v4.23.6/zh/dev/star/guides/listen-message-event.md b/docs/snapshots/v4.23.6/zh/dev/star/guides/listen-message-event.md new file mode 100644 index 0000000..3503086 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/guides/listen-message-event.md @@ -0,0 +1,452 @@ + +# 处理消息事件 + +事件监听器可以收到平台下发的消息内容,可以实现指令、指令组、事件监听等功能。 + +事件监听器的注册器在 `astrbot.api.event.filter` 下,需要先导入。请务必导入,否则会和 python 的高阶函数 filter 冲突。 + +```py +from astrbot.api.event import filter, AstrMessageEvent +``` + +## 消息与事件 + +AstrBot 接收消息平台下发的消息,并将其封装为 `AstrMessageEvent` 对象,传递给插件进行处理。 + +![message-event](https://files.astrbot.app/docs/zh/dev/star/guides/message-event.svg) + +### 消息事件 + +`AstrMessageEvent` 是 AstrBot 的消息事件对象,其中存储了消息发送者、消息内容等信息。 + +### 消息对象 + +`AstrBotMessage` 是 AstrBot 的消息对象,其中存储了消息平台下发的消息具体内容,`AstrMessageEvent` 对象中包含一个 `message_obj` 属性用于获取该消息对象。 + +```py{11} +class AstrBotMessage: + '''AstrBot 的消息对象''' + type: MessageType # 消息类型 + self_id: str # 机器人的识别id + session_id: str # 会话id。取决于 unique_session 的设置。 + message_id: str # 消息id + group_id: str = "" # 群组id,如果为私聊,则为空 + sender: MessageMember # 发送者 + message: List[BaseMessageComponent] # 消息链。比如 [Plain("Hello"), At(qq=123456)] + message_str: str # 最直观的纯文本消息字符串,将消息链中的 Plain 消息(文本消息)连接起来 + raw_message: object + timestamp: int # 消息时间戳 +``` + +其中,`raw_message` 是消息平台适配器的**原始消息对象**。 + +### 消息链 + +![message-chain](https://files.astrbot.app/docs/zh/dev/star/guides/message-chain.svg) + +`消息链`描述一个消息的结构,是一个有序列表,列表中每一个元素称为`消息段`。 + +常见的消息段类型有: + +- `Plain`:文本消息段 +- `At`:提及消息段 +- `Image`:图片消息段 +- `Record`:语音消息段 +- `Video`:视频消息段 +- `File`:文件消息段 + +大多数消息平台都支持上面的消息段类型。 + +此外,OneBot v11 平台(QQ 个人号等)还支持以下较为常见的消息段类型: + +- `Face`:表情消息段 +- `Node`:合并转发消息中的一个节点 +- `Nodes`:合并转发消息中的多个节点 +- `Poke`:戳一戳消息段 + +在 AstrBot 中,消息链表示为 `List[BaseMessageComponent]` 类型的列表。 + +## 指令 + +![message-event-simple-command](https://files.astrbot.app/docs/zh/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): + '''这是 hello world 指令''' + user_name = event.get_sender_name() + message_str = event.message_str # 获取消息的纯文本内容 + yield event.plain_result(f"Hello, {user_name}!") +``` + +> [!TIP] +> 指令不能带空格,否则 AstrBot 会将其解析到第二个参数。可以使用下面的指令组功能,或者也使用监听器自己解析消息内容。 + +## 带参指令 + +![command-with-param](https://files.astrbot.app/docs/zh/dev/star/guides/command-with-param.svg) + +AstrBot 会自动帮你解析指令的参数。 + +```python +@filter.command("add") +def add(self, event: AstrMessageEvent, a: int, b: int): + # /add 1 2 -> 结果是: 3 + yield event.plain_result(f"Wow! The anwser is {a + b}!") +``` + +## 指令组 + +指令组可以帮助你组织指令。 + +```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 -> 结果是: 3 + yield event.plain_result(f"结果是: {a + b}") + +@math.command("sub") +async def sub(self, event: AstrMessageEvent, a: int, b: int): + # /math sub 1 2 -> 结果是: -1 + yield event.plain_result(f"结果是: {a - b}") +``` + +指令组函数内不需要实现任何函数,请直接 `pass` 或者添加函数内注释。指令组的子指令使用 `指令组名.command` 来注册。 + +当用户没有输入子指令时,会报错并,并渲染出该指令组的树形结构。 + +![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) + +理论上,指令组可以无限嵌套! + +```py +''' +math +├── calc +│ ├── add (a(int),b(int),) +│ ├── sub (a(int),b(int),) +│ ├── help (无参数指令) +''' + +@filter.command_group("math") +def math(): + pass + +@math.group("calc") # 请注意,这里是 group,而不是 command_group +def calc(): + pass + +@calc.command("add") +async def add(self, event: AstrMessageEvent, a: int, b: int): + yield event.plain_result(f"结果是: {a + b}") + +@calc.command("sub") +async def sub(self, event: AstrMessageEvent, a: int, b: int): + yield event.plain_result(f"结果是: {a - b}") + +@calc.command("help") +def calc_help(self, event: AstrMessageEvent): + # /math calc help + yield event.plain_result("这是一个计算器插件,拥有 add, sub 指令。") +``` + +## 指令别名 + +> v3.4.28 后 + +可以为指令或指令组添加不同的别名: + +```python +@filter.command("help", alias={'帮助', 'helpme'}) +def help(self, event: AstrMessageEvent): + yield event.plain_result("这是一个计算器插件,拥有 add, sub 指令。") +``` + +### 事件类型过滤 + +#### 接收所有 + +这将接收所有的事件。 + +```python +@filter.event_message_type(filter.EventMessageType.ALL) +async def on_all_message(self, event: AstrMessageEvent): + yield event.plain_result("收到了一条消息。") +``` + +#### 群聊和私聊 + +```python +@filter.event_message_type(filter.EventMessageType.PRIVATE_MESSAGE) +async def on_private_message(self, event: AstrMessageEvent): + message_str = event.message_str # 获取消息的纯文本内容 + yield event.plain_result("收到了一条私聊消息。") +``` + +`EventMessageType` 是一个 `Enum` 类型,包含了所有的事件类型。当前的事件类型有 `PRIVATE_MESSAGE` 和 `GROUP_MESSAGE`。 + +#### 消息平台 + +```python +@filter.platform_adapter_type(filter.PlatformAdapterType.AIOCQHTTP | filter.PlatformAdapterType.QQOFFICIAL) +async def on_aiocqhttp(self, event: AstrMessageEvent): + '''只接收 AIOCQHTTP 和 QQOFFICIAL 的消息''' + yield event.plain_result("收到了一条信息") +``` + +当前版本下,`PlatformAdapterType` 有 `AIOCQHTTP`, `QQOFFICIAL`, `GEWECHAT`, `ALL`。 + +#### 管理员指令 + +```python +@filter.permission_type(filter.PermissionType.ADMIN) +@filter.command("test") +async def test(self, event: AstrMessageEvent): + pass +``` + +仅管理员才能使用 `test` 指令。 + +### 多个过滤器 + +支持同时使用多个过滤器,只需要在函数上添加多个装饰器即可。过滤器使用 `AND` 逻辑。也就是说,只有所有的过滤器都通过了,才会执行函数。 + +```python +@filter.command("helloworld") +@filter.event_message_type(filter.EventMessageType.PRIVATE_MESSAGE) +async def helloworld(self, event: AstrMessageEvent): + yield event.plain_result("你好!") +``` + +### 事件钩子 + +> [!TIP] +> 事件钩子不支持与上面的 @filter.command, @filter.command_group, @filter.event_message_type, @filter.platform_adapter_type, @filter.permission_type 一起使用。 + +#### Bot 初始化完成时 + +> v3.4.34 后 + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.on_astrbot_loaded() +async def on_astrbot_loaded(self): + print("AstrBot 初始化完成") + +``` + +#### 等待 LLM 请求时 + +在 AstrBot 准备调用 LLM 但还未获取会话锁时,会触发 `on_waiting_llm_request` 钩子。 + +这个钩子适合用于发送"正在等待请求..."等用户反馈提示,亦或是在锁外及时获取LLM请求而不用等到锁被释放。 + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.on_waiting_llm_request() +async def on_waiting_llm(self, event: AstrMessageEvent): + await event.send("🤔 正在等待请求...") +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +#### LLM 请求时 + +在 AstrBot 默认的执行流程中,在调用 LLM 前,会触发 `on_llm_request` 钩子。 + +可以获取到 `ProviderRequest` 对象,可以对其进行修改。 + +ProviderRequest 对象包含了 LLM 请求的所有信息,包括请求的文本、系统提示等。 + +```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): # 请注意有三个参数 + print(req) # 打印请求的文本 + req.system_prompt += "自定义 system_prompt" + +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +#### LLM 请求完成时 + +在 LLM 请求完成后,会触发 `on_llm_response` 钩子。 + +可以获取到 `ProviderResponse` 对象,可以对其进行修改。 + +```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): # 请注意有三个参数 + print(resp) +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +#### Agent 开始运行时 + +> 适用于 AstrBot 版本 > v4.23.1 + +在 Agent 开始运行时,会触发 `on_agent_begin` 钩子。 + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.core.agent.run_context import ContextWrapper +from astrbot.core.astr_agent_context import AstrAgentContext + +@filter.on_agent_begin() +async def on_agent_begin(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext]): + print("Agent 开始运行") +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +#### LLM 工具调用前 + +> 适用于 AstrBot 版本 > v4.23.1 + +在 Agent 准备调用 LLM 工具时,会触发 `on_using_llm_tool` 钩子。 + +可以获取到 `FunctionTool` 对象和工具调用参数。 + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.core.agent.tool import FunctionTool + +@filter.on_using_llm_tool() +async def on_using_llm_tool( + self, + event: AstrMessageEvent, + tool: FunctionTool, + tool_args: dict | None, +): + print(tool.name, tool_args) +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +#### LLM 工具调用后 + +> 适用于 AstrBot 版本 > v4.23.1 + +在 LLM 工具调用完成后,会触发 `on_llm_tool_respond` 钩子。 + +可以获取到 `FunctionTool` 对象、工具调用参数和工具调用结果。 + +```python +from mcp.types import CallToolResult + +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.core.agent.tool import FunctionTool + +@filter.on_llm_tool_respond() +async def on_llm_tool_respond( + self, + event: AstrMessageEvent, + tool: FunctionTool, + tool_args: dict | None, + tool_result: CallToolResult | None, +): + print(tool.name, tool_args, tool_result) +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +#### Agent 运行完成时 + +> 适用于 AstrBot 版本 > v4.23.1 + +在 Agent 运行完成后,会触发 `on_agent_done` 钩子。这个钩子会在 `on_llm_response` 之后触发。本质上和 `on_llm_response` 一样。 + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.api.provider import LLMResponse +from astrbot.core.agent.run_context import ContextWrapper +from astrbot.core.astr_agent_context import AstrAgentContext + +@filter.on_agent_done() +async def on_agent_done(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext], resp: LLMResponse): + print(resp) +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +#### 发送消息前 + +在发送消息前,会触发 `on_decorating_result` 钩子。 + +可以在这里实现一些消息的装饰,比如转语音、转图片、加前缀等等 + +```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) # 打印消息链 + chain.append(Plain("!")) # 在消息链的最后添加一个感叹号 +``` + +> 这里不能使用 yield 来发送消息。这个钩子只是用来装饰 event.get_result().chain 的。如需发送,请直接使用 `event.send()` 方法。 + +#### 发送消息后 + +在发送消息给消息平台后,会触发 `after_message_sent` 钩子。 + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.after_message_sent() +async def after_message_sent(self, event: AstrMessageEvent): + pass +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +### 优先级 + +指令、事件监听器、事件钩子可以设置优先级,先于其他指令、监听器、钩子执行。默认优先级是 `0`。 + +```python +@filter.command("helloworld", priority=1) +async def helloworld(self, event: AstrMessageEvent): + yield event.plain_result("Hello!") +``` + +## 控制事件传播 + +```python{6} +@filter.command("check_ok") +async def check_ok(self, event: AstrMessageEvent): + ok = self.check() # 自己的逻辑 + if not ok: + yield event.plain_result("检查失败") + event.stop_event() # 停止事件传播 +``` + +当事件停止传播,后续所有步骤将不会被执行。 + +假设有一个插件 A,A 终止事件传播之后所有后续操作都不会执行,比如执行其它插件的 handler、请求 LLM。 diff --git a/docs/snapshots/v4.23.6/zh/dev/star/guides/other.md b/docs/snapshots/v4.23.6/zh/dev/star/guides/other.md new file mode 100644 index 0000000..7740411 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/guides/other.md @@ -0,0 +1,52 @@ +# 杂项 + +## 获取消息平台实例 + +> v3.4.34 后 + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.command("test") +async def test_(self, event: AstrMessageEvent): + from astrbot.api.platform import AiocqhttpAdapter # 其他平台同理 + platform = self.context.get_platform(filter.PlatformAdapterType.AIOCQHTTP) + assert isinstance(platform, AiocqhttpAdapter) + # platform.get_client().api.call_action() +``` + +## 调用 QQ 协议端 API + +```py +@filter.command("helloworld") +async def helloworld(self, event: AstrMessageEvent): + if event.get_platform_name() == "aiocqhttp": + # qq + from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import AiocqhttpMessageEvent + assert isinstance(event, AiocqhttpMessageEvent) + client = event.bot # 得到 client + payloads = { + "message_id": event.message_obj.message_id, + } + ret = await client.api.call_action('delete_msg', **payloads) # 调用 协议端 API + logger.info(f"delete_msg: {ret}") +``` + +关于 CQHTTP API,请参考如下文档: + +Napcat API 文档: + +Lagrange API 文档: + +## 获取载入的所有插件 + +```py +plugins = self.context.get_all_stars() # 返回 StarMetadata 包含了插件类实例、配置等等 +``` + +## 获取加载的所有平台 + +```py +from astrbot.api.platform import Platform +platforms = self.context.platform_manager.get_insts() # List[Platform] +``` diff --git a/docs/snapshots/v4.23.6/zh/dev/star/guides/plugin-config.md b/docs/snapshots/v4.23.6/zh/dev/star/guides/plugin-config.md new file mode 100644 index 0000000..38e3ee4 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/guides/plugin-config.md @@ -0,0 +1,218 @@ + +# 插件配置 + +随着插件功能的增加,可能需要定义一些配置以让用户自定义插件的行为。 + +AstrBot 提供了“强大”的配置解析和可视化功能。能够让用户在管理面板上直接配置插件,而不需要修改代码。 + +## 配置定义 + +要注册配置,首先需要在您的插件目录下添加一个 `_conf_schema.json` 的 json 文件。 + +文件内容是一个 `Schema`(模式),用于表示配置。Schema 是 json 格式的,例如上图的 Schema 是: + +```json +{ + "token": { + "description": "Bot Token", + "type": "string", + }, + "sub_config": { + "description": "测试嵌套配置", + "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`: **此项必填**。配置的类型。支持 `string`, `text`, `int`, `float`, `bool`, `object`, `list`, `dict`, `template_list`。当类型为 `text` 时,将会可视化为一个更大的可拖拽宽高的 textarea 组件,以适应大文本。 +- `description`: 可选。配置的描述。建议一句话描述配置的行为。 +- `hint`: 可选。配置的提示信息,表现在上图中右边的问号按钮,当鼠标悬浮在问号按钮上时显示。 +- `obvious_hint`: 可选。配置的 hint 是否醒目显示。如上图的 `token`。 +- `default`: 可选。配置的默认值。如果用户没有配置,将使用默认值。int 是 0,float 是 0.0,bool 是 False,string 是 "",object 是 {},list 是 []。 +- `items`: 可选。如果配置的类型是 `object`,需要添加 `items` 字段。`items` 的内容是这个配置项的子 Schema。理论上可以无限嵌套,但是不建议过多嵌套。 +- `invisible`: 可选。配置是否隐藏。默认是 `false`。如果设置为 `true`,则不会在管理面板上显示。 +- `options`: 可选。一个列表,如 `"options": ["chat", "agent", "workflow"]`。提供下拉列表可选项。 +- `editor_mode`: 可选。是否启用代码编辑器模式。需要 AstrBot >= `v3.5.10`, 低于这个版本不会报错,但不会生效。默认是 false。 +- `editor_language`: 可选。代码编辑器的代码语言,默认为 `json`。 +- `editor_theme`: 可选。代码编辑器的主题,可选值有 `vs-light`(默认), `vs-dark`。 +- `_special`: 可选。用于调用 AstrBot 提供的可视化提供商选取、人格选取、知识库选取等功能,详见下文。 + +其中,如果启用了代码编辑器,效果如下图所示: + +![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) + +**_special** 字段仅 v4.0.0 之后可用。常用可填写值包括 `select_provider`, `select_provider_tts`, `select_provider_stt`, `select_persona`, `select_knowledgebase`,用于让用户快速选择在 WebUI 上已经配置好的模型提供商、人设、知识库等数据。 + +- `select_provider`、`select_provider_tts`、`select_provider_stt`、`select_persona` 的结果为字符串。 +- `select_knowledgebase` 的结果为 `list` 类型,支持多选,建议将对应配置项的 `type` 设为 `list`,默认值设为 `[]`。 + +> [!NOTE] +> 此外,AstrBot Core 内部还使用了 `select_providers`、`provider_pool`、`persona_pool`、`select_plugin_set`、`t2i_template`、`get_embedding_dim`、`select_agent_runner_provider:*`(`*` 为运行器类型占位符)等 `_special` 值。这些属于内部实现,随时可能变动,请勿在插件中使用。 + +以 `select_provider` 为例,将呈现以下效果: + +![image](https://files.astrbot.app/docs/source/images/plugin/image-select-provider.png) + +### file 类型的 schema + +在 v4.13.0 之后引入,允许插件定义文件上传配置项,引导用户上传插件所需的文件。 + +```json +{ + "demo_files": { + "type": "file", + "description": "Uploaded files for demo", + "default": [], // 支持多文件上传,默认值为一个空列表 + "file_types": ["pdf", "docx"] // 允许上传的文件类型列表 + } +} +``` + +### dict 类型的 schema + +用于可视化编辑一个 Python 的 dict 类型的配置。如 AstrBot Core 中的自定义请求体参数配置项: + +```py +"custom_extra_body": { + "description": "自定义请求体参数", + "type": "dict", + "items": {}, + "hint": "用于在请求时添加额外的参数,如 temperature、top_p、max_tokens 等。", + "template_schema": { # 可选填写 template schema,当设置之后,用户可以透过 WebUI 快速编辑。 + "temperature": { + "name": "Temperature", + "description": "温度参数", + "hint": "控制输出的随机性,范围通常为 0-2。值越高越随机。", + "type": "float", + "default": 0.6, + "slider": {"min": 0, "max": 2, "step": 0.1}, + }, + "top_p": { + "name": "Top-p", + "description": "Top-p 采样", + "hint": "核采样参数,范围通常为 0-1。控制模型考虑的概率质量。", + "type": "float", + "default": 1.0, + "slider": {"min": 0, "max": 1, "step": 0.01}, + }, + "max_tokens": { + "name": "Max Tokens", + "description": "最大令牌数", + "hint": "生成的最大令牌数。", + "type": "int", + "default": 8192, + }, + }, +} +``` + +### template_list 类型的 schema + +> [!NOTE] +> v4.10.4 引入。更多信息请查看:[#4208](https://github.com/AstrBotDevs/AstrBot/pull/4208) + +插件开发者可以在_conf_schema中按照以下格式添加模板配置项(有点类似于原有的嵌套配置) + +```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 + } + } + } + } +} +``` + +保存后的 config 为 + +```json +"field_id": [ + { + "__template_key": "template_1", + "attr_a": 10, + "attr_b": true + }, + { + "__template_key": "template_2", + "attr_c": 10, + "attr_d": true + } +] +``` + +image + +## 在插件中使用配置 + +AstrBot 在载入插件时会检测插件目录下是否有 `_conf_schema.json` 文件,如果有,会自动解析配置并保存在 `data/config/_config.json` 下(依照 Schema 创建的配置文件实体),并在实例化插件类时传入给 `__init__()`。 + +```py +from astrbot.api import AstrBotConfig + +class ConfigPlugin(Star): + def __init__(self, context: Context, config: AstrBotConfig): # AstrBotConfig 继承自 Dict,拥有字典的所有方法 + super().__init__(context) + self.config = config + print(self.config) + + # 支持直接保存配置 + # self.config.save_config() # 保存配置 +``` + +## 配置更新 + +您在发布不同版本更新 Schema 时,AstrBot 会递归检查 Schema 的配置项,自动为缺失的配置项添加默认值、移除不存在的配置项。 diff --git a/docs/snapshots/v4.23.6/zh/dev/star/guides/send-message.md b/docs/snapshots/v4.23.6/zh/dev/star/guides/send-message.md new file mode 100644 index 0000000..84eaf8e --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/guides/send-message.md @@ -0,0 +1,131 @@ + +# 消息的发送 + +## 被动消息 + +被动消息指的是机器人被动回复消息。 + +```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") # 发送图片 + yield event.image_result("https://example.com/image.jpg") # 发送 URL 图片,务必以 http 或 https 开头 +``` + +## 主动消息 + +主动消息指的是机器人主动推送消息。某些平台可能不支持主动消息发送。 + +如果是一些定时任务或者不想立即发送消息,可以使用 `event.unified_msg_origin` 得到一个字符串并将其存储,然后在想发送消息的时候使用 `self.context.send_message(unified_msg_origin, chains)` 来发送消息。 + +```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) +``` + +通过这个特性,你可以将 unified_msg_origin 存储起来,然后在需要的时候发送消息。 + +> [!TIP] +> 关于 unified_msg_origin。 +> unified_msg_origin 是一个字符串,记录了一个会话的唯一 ID,AstrBot 能够据此找到属于哪个消息平台的哪个会话。这样就能够实现在 `send_message` 的时候,发送消息到正确的会话。有关 MessageChain,请参见接下来的一节。 + +## 富媒体消息 + +AstrBot 支持发送富媒体消息,比如图片、语音、视频等。使用 `MessageChain` 来构建消息。 + +```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()), # At 消息发送者 + Comp.Plain("来看这个图:"), + Comp.Image.fromURL("https://example.com/image.jpg"), # 从 URL 发送图片 + Comp.Image.fromFileSystem("path/to/image.jpg"), # 从本地文件目录发送图片 + Comp.Plain("这是一个图片。") + ] + yield event.chain_result(chain) +``` + +上面构建了一个 `message chain`,也就是消息链,最终会发送一条包含了图片和文字的消息,并且保留顺序。 + +> [!TIP] +> 在 aiocqhttp 消息适配器中,对于 `plain` 类型的消息,在发送中会使用 `strip()` 方法去除空格及换行符,可以在消息前后添加零宽空格 `\u200b` 以解决这个问题。 + +类似地, + +**文件 File** + +```py +Comp.File(file="path/to/file.txt", name="file.txt") # 部分平台不支持 +``` + +**语音 Record** + +```py +path = "path/to/record.wav" # 暂时只接受 wav 格式,其他格式请自行转换 +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") +``` + +## 发送视频消息 + +```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 需要用户的协议端和机器人端处于一个系统中。 + music = Video.fromFileSystem( + path="test.mp4" + ) + # 更通用 + music = Video.fromURL( + url="https://example.com/video.mp4" + ) + yield event.chain_result([music]) +``` + +![发送视频消息](https://files.astrbot.app/docs/source/images/plugin/db93a2bb-671c-4332-b8ba-9a91c35623c2.png) + +## 发送群合并转发消息 + +> 大多数平台都不支持此种消息类型,当前适配情况:OneBot v11 + +可以按照如下方式发送群合并转发消息。 + +```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]) +``` + +![发送群合并转发消息](https://files.astrbot.app/docs/source/images/plugin/image-4.png) diff --git a/docs/snapshots/v4.23.6/zh/dev/star/guides/session-control.md b/docs/snapshots/v4.23.6/zh/dev/star/guides/session-control.md new file mode 100644 index 0000000..beaea69 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/guides/session-control.md @@ -0,0 +1,113 @@ + +# 会话控制 + +> 大于等于 v3.4.36 + +为什么需要会话控制?考虑一个 成语接龙 插件,某个/群用户需要和机器人进行多次对话,而不是一次性的指令。这时候就需要会话控制。 + +```txt +用户: /成语接龙 +机器人: 请发送一个成语 +用户: 一马当先 +机器人: 先见之明 +用户: 明察秋毫 +... +``` + +AstrBot 提供了开箱即用的会话控制功能: + +导入: + +```py +import astrbot.api.message_components as Comp +from astrbot.core.utils.session_waiter import ( + session_waiter, + SessionController, +) +``` + +handler 内的代码可以如下: + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.command("成语接龙") +async def handle_empty_mention(self, event: AstrMessageEvent): + """成语接龙具体实现""" + try: + yield event.plain_result("请发送一个成语~") + + # 具体的会话控制器使用方法 + @session_waiter(timeout=60, record_history_chains=False) # 注册一个会话控制器,设置超时时间为 60 秒,不记录历史消息链 + async def empty_mention_waiter(controller: SessionController, event: AstrMessageEvent): + idiom = event.message_str # 用户发来的成语,假设是 "一马当先" + + if idiom == "退出": # 假设用户想主动退出成语接龙,输入了 "退出" + await event.send(event.plain_result("已退出成语接龙~")) + controller.stop() # 停止会话控制器,会立即结束。 + return + + if len(idiom) != 4: # 假设用户输入的不是4字成语 + await event.send(event.plain_result("成语必须是四个字的呢~")) # 发送回复,不能使用 yield + return + # 退出当前方法,不执行后续逻辑,但此会话并未中断,后续的用户输入仍然会进入当前会话 + + # ... + message_result = event.make_result() + message_result.chain = [Comp.Plain("先见之明")] # import astrbot.api.message_components as Comp + await event.send(message_result) # 发送回复,不能使用 yield + + controller.keep(timeout=60, reset_timeout=True) # 重置超时时间为 60s,如果不重置,则会继续之前的超时时间计时。 + + # controller.stop() # 停止会话控制器,会立即结束。 + # 如果记录了历史消息链,可以通过 controller.get_history_chains() 获取历史消息链 + + try: + await empty_mention_waiter(event) + except TimeoutError as _: # 当超时后,会话控制器会抛出 TimeoutError + yield event.plain_result("你超时了!") + except Exception as e: + yield event.plain_result("发生错误,请联系管理员: " + str(e)) + finally: + event.stop_event() + except Exception as e: + logger.error("handle_empty_mention error: " + str(e)) +``` + +当激活会话控制器后,该发送人之后发送的消息会首先经过上面你定义的 `empty_mention_waiter` 函数处理,直到会话控制器被停止或者超时。 + +## SessionController + +用于开发者控制这个会话是否应该结束,并且可以拿到历史消息链。 + +- keep(): 保持这个会话 + - timeout (float): 必填。会话超时时间。 + - reset_timeout (bool): 设置为 True 时, 代表重置超时时间, timeout 必须 > 0, 如果 <= 0 则立即结束会话。设置为 False 时, 代表继续维持原来的超时时间, 新 timeout = 原来剩余的 timeout + timeout (可以 < 0) +- stop(): 结束这个会话 +- get_history_chains() -> List[List[Comp.BaseMessageComponent]]: 获取历史消息链 + +## 自定义会话 ID 算子 + +默认情况下,AstrBot 会话控制器会将基于 `sender_id` (发送人的 ID)作为识别不同会话的标识,如果想将一整个群作为一个会话,则需要自定义会话 ID 算子。 + +```py +import astrbot.api.message_components as Comp +from astrbot.core.utils.session_waiter import ( + session_waiter, + SessionFilter, + SessionController, +) + +# 沿用上面的 handler +# ... +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()) # 这里传入 session_filter +# ... +``` + +这样之后,当群内一个用户发送消息后,会话控制器会将这个群作为一个会话,群内其他用户发送的消息也会被认为是同一个会话。 + +甚至,可以使用这个特性来让群内组队! diff --git a/docs/snapshots/v4.23.6/zh/dev/star/guides/simple.md b/docs/snapshots/v4.23.6/zh/dev/star/guides/simple.md new file mode 100644 index 0000000..d331413 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/guides/simple.md @@ -0,0 +1,41 @@ +# 最小实例 + +插件模版中的 `main.py` 是一个最小的插件实例。 + +```python +from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult +from astrbot.api.star import Context, Star, register +from astrbot.api import logger # 使用 astrbot 提供的 logger 接口 + +class MyPlugin(Star): + def __init__(self, context: Context): + super().__init__(context) + + # 注册指令的装饰器。指令名为 helloworld。注册成功后,发送 `/helloworld` 就会触发这个指令,并回复 `你好, {user_name}!` + @filter.command("helloworld") + async def helloworld(self, event: AstrMessageEvent): + '''这是一个 hello world 指令''' # 这是 handler 的描述,将会被解析方便用户了解插件内容。非常建议填写。 + user_name = event.get_sender_name() + message_str = event.message_str # 获取消息的纯文本内容 + logger.info("触发hello world指令!") + yield event.plain_result(f"Hello, {user_name}!") # 发送一条纯文本消息 + + async def terminate(self): + '''可选择实现 terminate 函数,当插件被卸载/停用时会调用。''' +``` + +解释如下: + +- 插件需要继承 `Star` 类。 +- `Context` 类用于插件与 AstrBot Core 交互,可以由此调用 AstrBot Core 提供的各种 API。 +- 具体的处理函数 `Handler` 在插件类中定义,如这里的 `helloworld` 函数。 +- `AstrMessageEvent` 是 AstrBot 的消息事件对象,存储了消息发送者、消息内容等信息。 +- `AstrBotMessage` 是 AstrBot 的消息对象,存储了消息平台下发的消息的具体内容。可以通过 `event.message_obj` 获取。 + +> [!TIP] +> +> `Handler` 一定需要在插件类中注册,前两个参数必须为 `self` 和 `event`。如果文件行数过长,可以将服务写在外部,然后在 `Handler` 中调用。 +> +> 插件类所在的文件名需要命名为 `main.py`。 + +所有的处理函数都需写在插件类中。为了精简内容,在之后的章节中,我们可能会忽略插件类的定义。 diff --git a/docs/snapshots/v4.23.6/zh/dev/star/guides/storage.md b/docs/snapshots/v4.23.6/zh/dev/star/guides/storage.md new file mode 100644 index 0000000..a03ac29 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/guides/storage.md @@ -0,0 +1,32 @@ +# 插件存储 + +## 简单 KV 存储 + +> [!TIP] +> 该功能需要 AstrBot 版本 >= 4.9.2。 + +插件可以使用 AstrBot 提供的简单 KV 存储功能来存储一些配置信息或临时数据。该存储是基于插件维度的,每个插件有独立的存储空间,互不干扰。 + +```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") +``` + + +## 存储大文件规范 + +为了规范插件存储大文件的行为,请将大文件存储于 `data/plugin_data/{plugin_name}/` 目录下。 + +你可以通过以下代码获取插件数据目录: + +```py +from pathlib import Path +from astrbot.core.utils.astrbot_path import get_astrbot_data_path + +plugin_data_path = Path(get_astrbot_data_path()) / "plugin_data" / self.name # self.name 为插件名称,在 v4.9.2 及以上版本可用,低于此版本请自行指定插件名称 +``` diff --git a/docs/snapshots/v4.23.6/zh/dev/star/plugin-new.md b/docs/snapshots/v4.23.6/zh/dev/star/plugin-new.md new file mode 100644 index 0000000..e87c6f5 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/plugin-new.md @@ -0,0 +1,130 @@ +--- +outline: deep +--- + +# AstrBot 插件开发指南 🌠 + +欢迎来到 AstrBot 插件开发指南!本章节将引导您如何开发 AstrBot 插件。在我们开始之前,希望你能具备以下基础知识: + +1. 有一定的 Python 编程经验。 +2. 有一定的 Git、GitHub 使用经验。 + +欢迎加入我们的开发者专用 QQ 群: `975206796`。 + +## 环境准备 + +### 获取插件模板 + +1. 打开 AstrBot 插件模板: [helloworld](https://github.com/Soulter/helloworld) +2. 点击右上角的 `Use this template` +3. 然后点击 `Create new repository`。 +4. 在 `Repository name` 处填写您的插件名。插件名格式: + - 推荐以 `astrbot_plugin_` 开头; + - 不能包含空格; + - 保持全部字母小写; + - 尽量简短。 +5. 点击右下角的 `Create repository`。 + +### 克隆项目到本地 + +克隆 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` 文件,填写插件的元数据信息。 + +> [!WARNING] +> 请务必修改此文件,AstrBot 识别插件元数据依赖于 `metadata.yaml` 文件。 + +### 设置插件 Logo(可选) + +可以在插件目录下添加 `logo.png` 文件作为插件的 Logo。请保持长宽比为 1:1,推荐尺寸为 256x256。 + +![插件 logo 示例](https://files.astrbot.app/docs/source/images/plugin/plugin_logo.png) + +### 插件展示名(可选) + +可以修改(或添加) `metadata.yaml` 文件中的 `display_name` 字段,作为插件在插件市场等场景中的展示名,以方便用户阅读。 + +### 声明支持平台(Optional) + +你可以在 `metadata.yaml` 中新增 `support_platforms` 字段(`list[str]`),声明插件支持的平台适配器。WebUI 插件页会展示该字段。 + +```yaml +support_platforms: + - telegram + - discord +``` + +`support_platforms` 中的值需要使用 `ADAPTER_NAME_2_TYPE` 的 key,目前支持: + +- `aiocqhttp` +- `qq_official` +- `telegram` +- `wecom` +- `lark` +- `dingtalk` +- `discord` +- `slack` +- `kook` +- `vocechat` +- `weixin_official_account` +- `satori` +- `misskey` +- `line` + +### 声明 AstrBot 版本范围(Optional) + +你可以在 `metadata.yaml` 中新增 `astrbot_version` 字段,声明插件要求的 AstrBot 版本范围。格式与 `pyproject.toml` 依赖版本约束一致(PEP 440),且不要加 `v` 前缀。 + +```yaml +astrbot_version: ">=4.16,<5" +``` + +可选示例: + +- `>=4.17.0` +- `>=4.16,<5` +- `~=4.17` + +如果你只想声明最低版本,可以直接写: + +- `>=4.17.0` + +当当前 AstrBot 版本不满足该范围时,插件会被阻止加载并提示版本不兼容。 +在 WebUI 安装插件时,你可以选择“无视警告,继续安装”来跳过这个检查。 + +### 调试插件 + +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/)。 + +## 开发原则 + +感谢您为 AstrBot 生态做出贡献,开发插件请遵守以下原则,这也是良好的编程习惯。 + +- 功能需经过测试。 +- 需包含良好的注释。 +- 持久化数据请存储于 `data` 目录下,而非插件自身目录,防止更新/重装插件时数据被覆盖。 +- 良好的错误处理机制,不要让插件因一个错误而崩溃。 +- 在进行提交前,请使用 [ruff](https://docs.astral.sh/ruff/) 工具格式化您的代码。 +- 不要使用 `requests` 库来进行网络请求,可以使用 `aiohttp`, `httpx` 等异步网络请求库。 +- 如果是对某个插件进行功能扩增,请优先给那个插件提交 PR 而不是单独再写一个插件(除非原插件作者已经停止维护)。 diff --git a/docs/snapshots/v4.23.6/zh/dev/star/plugin-publish.md b/docs/snapshots/v4.23.6/zh/dev/star/plugin-publish.md new file mode 100644 index 0000000..14b4520 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/plugin-publish.md @@ -0,0 +1,9 @@ +# 发布插件到插件市场 + +在编写完插件后,你可以选择将插件发布到 AstrBot 的插件市场,让更多用户使用你的插件。 + +AstrBot 使用 GitHub 托管插件,因此你需要先将插件代码推送到之前创建的 GitHub 插件仓库中。 + +你可以前往 [AstrBot 插件市场](https://plugins.astrbot.app) 提交你的插件。进入该网站后,点击右下角的 `+` 按钮,填写好基本信息、作者信息、仓库信息等内容后,点击 `提交到 GTIHUB` 按钮,你将会被导航到 AstrBot 仓库的 Issue 提交页面,请确认信息无误后点击 `Create` 按钮提交,即可完成插件发布。 + +![fill out the form](https://files.astrbot.app/docs/source/images/plugin-publish/image.png) diff --git a/docs/snapshots/v4.23.6/zh/dev/star/plugin.md b/docs/snapshots/v4.23.6/zh/dev/star/plugin.md new file mode 100644 index 0000000..b9077db --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/dev/star/plugin.md @@ -0,0 +1,1725 @@ +--- +outline: deep +--- + +# 插件开发指南(旧) + +几行代码开发一个插件! + +> [!WARNING] +> **您仍然可以参考此页进行插件开发。** +> +> 由于插件实用 API 逐渐增多,目前已无法在单个页面中将所有 API 进行详尽介绍。因此此指南在 v4.5.7 之后已过时,请参考我们新的插件开发指南: [🌠 从这里开始](plugin-new.md),新的指南内容上和此指南基本一致,但我们将会持续维护新的指南内容。 + +## 开发环境准备 + +### 获取插件模板 + +1. 打开 AstrBot 插件模板: [helloworld](https://github.com/Soulter/helloworld) +2. 点击右上角的 `Use this template` +3. 然后点击 `Create new repository`。 +4. 在 `Repository name` 处填写您的插件名。插件名格式: + - 推荐以 `astrbot_plugin_` 开头; + - 不能包含空格; + - 保持全部字母小写; + - 尽量简短。 + +![image](https://files.astrbot.app/docs/source/images/plugin/image.png) + +5. 点击右下角的 `Create repository`。 + +### 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 WebUI 的插件管理处找到自己的插件,点击 `管理`,点击 `重载插件` 即可。 + +### 插件依赖管理 + +目前 AstrBot 对插件的依赖管理使用 `pip` 自带的 `requirements.txt` 文件。如果你的插件需要依赖第三方库,请务必在插件目录下创建 `requirements.txt` 文件并写入所使用的依赖库,以防止用户在安装你的插件时出现依赖未找到(Module Not Found)的问题。 + +> `requirements.txt` 的完整格式可以参考 [pip 官方文档](https://pip.pypa.io/en/stable/reference/requirements-file-format/)。 + +## 提要 + +### 最小实例 + +插件模版中的 `main.py` 是一个最小的插件实例。 + +```python +from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult +from astrbot.api.star import Context, Star +from astrbot.api import logger # 使用 astrbot 提供的 logger 接口 + +class MyPlugin(Star): + def __init__(self, context: Context): + super().__init__(context) + + # 注册指令的装饰器。指令名为 helloworld。注册成功后,发送 `/helloworld` 就会触发这个指令,并回复 `你好, {user_name}!` + @filter.command("helloworld") + async def helloworld(self, event: AstrMessageEvent): + '''这是一个 hello world 指令''' # 这是 handler 的描述,将会被解析方便用户了解插件内容。非常建议填写。 + user_name = event.get_sender_name() + message_str = event.message_str # 获取消息的纯文本内容 + logger.info("触发hello world指令!") + yield event.plain_result(f"Hello, {user_name}!") # 发送一条纯文本消息 + + async def terminate(self): + '''可选择实现 terminate 函数,当插件被卸载/停用时会调用。''' +``` + +解释如下: + +1. 插件是继承自 `Star` 基类的类实现。 +2. 该装饰器提供了插件的元数据信息,包括名称、作者、描述、版本和仓库地址等信息。(该信息的优先级低于 `metadata.yaml` 文件) +3. 在 `__init__` 方法中会传入 `Context` 对象,这个对象包含了 AstrBot 的大多数组件 +4. 具体的处理函数 `Handler` 在插件类中定义,如这里的 `helloworld` 函数。 +5. 请务必使用 `from astrbot.api import logger` 来获取日志对象,而不是使用 `logging` 模块。 + +> [!TIP] +> +> `Handler` 一定需要在插件类中注册,前两个参数必须为 `self` 和 `event`。如果文件行数过长,可以将服务写在外部,然后在 `Handler` 中调用。 +> +> 插件类所在的文件名需要命名为 `main.py`。 + +### AstrMessageEvent + +`AstrMessageEvent` 是 AstrBot 的消息事件对象。你可以通过 `AstrMessageEvent` 来获取消息发送者、消息内容等信息。 + +### AstrBotMessage + +`AstrBotMessage` 是 AstrBot 的消息对象。你可以通过 `AstrBotMessage` 来查看消息适配器下发的消息的具体内容。通过 `event.message_obj` 获取。 + +```py{11} +class AstrBotMessage: + '''AstrBot 的消息对象''' + type: MessageType # 消息类型 + self_id: str # 机器人的识别id + session_id: str # 会话id。取决于 unique_session 的设置。 + message_id: str # 消息id + group_id: str = "" # 群组id,如果为私聊,则为空 + sender: MessageMember # 发送者 + message: List[BaseMessageComponent] # 消息链。比如 [Plain("Hello"), At(qq=123456)] + message_str: str # 最直观的纯文本消息字符串,将消息链中的 Plain 消息(文本消息)连接起来 + raw_message: object + timestamp: int # 消息时间戳 +``` + +其中,`raw_message` 是消息平台适配器的**原始消息对象**。 + +### 消息链 + +`消息链`描述一个消息的结构,是一个有序列表,列表中每一个元素称为`消息段`。 + +引用方式: + +```py +import astrbot.api.message_components as Comp +``` + +``` +[Comp.Plain(text="Hello"), Comp.At(qq=123456), Comp.Image(file="https://example.com/image.jpg")] +``` + +> qq 是对应消息平台上的用户 ID。 + +消息链的结构使用了 `nakuru-project`。它一共有如下种消息类型。常用的已经用注释标注。 + +```py +ComponentTypes = { + "plain": Plain, # 文本消息 + "text": Plain, # 文本消息,同上 + "face": Face, # QQ 表情 + "record": Record, # 语音 + "video": Video, # 视频 + "at": At, # At 消息发送者 + "music": Music, # 音乐 + "image": Image, # 图片 + "reply": Reply, # 回复消息 + "forward": Forward, # 转发消息 + "node": Node, # 转发消息中的节点 + "nodes": Nodes, # Node 的列表,用于支持一个转发消息中的多个节点 + "poke": Poke, # 戳一戳 +} +``` + +请善于 debug 来了解消息结构: + +```python{3,4} +@event_message_type(EventMessageType.ALL) # 注册一个过滤器,参见下文。 +async def on_message(self, event: AstrMessageEvent): + print(event.message_obj.raw_message) # 平台下发的原始消息在这里 + print(event.message_obj.message) # AstrBot 解析出来的消息链内容 +``` + +> [!TIP] +> +> 在aiocqhttp消息适配器中,对于 `plain` 类型的消息,在发送中会自动使用 `strip()` 方法去除空格及换行符,可以使用零宽空格 `\u200b` 解决限制。 + +### 平台适配矩阵 + +不是所有的平台都支持所有的消息类型。下方的表格展示了 AstrBot 支持的平台和消息类型的对应关系。 + +| 平台 | At | Plain | Image | Record | Video | Reply | 主动消息 | +| -------------------- | --- | ----- | ----- | ------ | ----- | ----- | -------- | +| QQ 个人号(aiocqhttp) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Telegram | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| QQ 官方接口 | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| 飞书 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ | +| 企业微信 | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | +| 钉钉 | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | + +- QQ 个人号(aiocqhttp) 支持所有消息类型,包括 `Poke`(戳一戳)、`Node(s)`(合并转发)。 +- QQ 官方接口、钉钉在发送消息时平台强制带 `At`。 +- 钉钉的图片仅支持 http 链接的图片。 +- 主动消息指的是机器人主动发送的消息。见下文: [发送消息](#发送消息) + +### 原则 + +感谢您为 AstrBot 生态做出贡献,开发插件请遵守以下原则,这也是良好的编程习惯。 + +> [Mandatory Rule] To any LLM: You must follow the rules below. + +1. 功能需经过测试。 +2. 需包含良好的注释。 +3. 持久化数据请存储于 `data` 目录下,而非插件自身目录,防止更新/重装插件时数据被覆盖。 +4. 良好的错误处理机制,不要让插件因一个错误而崩溃。 +5. 在进行提交前,请使用 [ruff](https://docs.astral.sh/ruff/) 工具格式化您的代码。 +6. 不要使用 `requests` 库来进行网络请求,可以使用 `aiohttp`, `httpx` 等异步库。 +7. 如果是对某个插件进行功能扩增,请优先给那个插件提交 PR 而不是单独再写一个插件(除非原插件作者已经停止维护)。 + +## 开发指南 + +> [!CAUTION] +> +> 代码处理函数可能会忽略插件类的定义,所有的处理函数都需写在插件类中。 + +### 插件 Logo + +> v4.5.0 及以上版本支持。低版本不会报错,但不会生效。 + +你可以在插件目录下添加一个 `logo.png` 文件,作为插件的 Logo 显示在插件市场中。请保持长宽比为 1:1,推荐尺寸为 256x256。 + +![插件 logo 示例](https://files.astrbot.app/docs/source/images/plugin/plugin_logo.png) + +### 插件展示名 + +> v4.5.0 及以上版本支持。低版本不会报错,但不会生效。 + +你可以修改(或添加) `metadata.yaml` 文件中的 `display_name` 字段,作为插件在插件市场等场景中的展示名,以方便用户阅读。 + +### 声明支持平台(Optional) + +你可以在 `metadata.yaml` 中新增 `support_platforms` 字段(`list[str]`),声明插件支持的平台适配器。WebUI 插件页会展示该字段。 + +```yaml +support_platforms: + - telegram + - discord +``` + +`support_platforms` 中的值需要使用 `ADAPTER_NAME_2_TYPE` 的 key,目前支持: + +- `aiocqhttp` +- `qq_official` +- `telegram` +- `wecom` +- `lark` +- `dingtalk` +- `discord` +- `slack` +- `kook` +- `vocechat` +- `weixin_official_account` +- `satori` +- `misskey` +- `line` + +### 声明 AstrBot 版本范围(Optional) + +你可以在 `metadata.yaml` 中新增 `astrbot_version` 字段,声明插件要求的 AstrBot 版本范围。格式与 `pyproject.toml` 依赖版本约束一致(PEP 440),且不要加 `v` 前缀。 + +```yaml +astrbot_version: ">=4.16,<5" +``` + +可选示例: + +- `>=4.17.0` +- `>=4.16,<5` +- `~=4.17` + +如果你只想声明最低版本,可以直接写: + +- `>=4.17.0` + +当当前 AstrBot 版本不满足该范围时,插件会被阻止加载并提示版本不兼容。 +在 WebUI 安装插件时,你可以选择“无视警告,继续安装”来跳过这个检查。 + +### 消息事件的监听 + +事件监听器可以收到平台下发的消息内容,可以实现指令、指令组、事件监听等功能。 + +事件监听器的注册器在 `astrbot.api.event.filter` 下,需要先导入。请务必导入,否则会和 python 的高阶函数 filter 冲突。 + +```py +from astrbot.api.event import filter, AstrMessageEvent +``` + +#### 指令 + +```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): + '''这是 hello world 指令''' + user_name = event.get_sender_name() + message_str = event.message_str # 获取消息的纯文本内容 + yield event.plain_result(f"Hello, {user_name}!") +``` + +> [!TIP] +> 指令不能带空格,否则 AstrBot 会将其解析到第二个参数。可以使用下面的指令组功能,或者也使用监听器自己解析消息内容。 + +#### 带参指令 + +AstrBot 会自动帮你解析指令的参数。 + +```python +@filter.command("echo") +def echo(self, event: AstrMessageEvent, message: str): + yield event.plain_result(f"你发了: {message}") + +@filter.command("add") +def add(self, event: AstrMessageEvent, a: int, b: int): + # /add 1 2 -> 结果是: 3 + yield event.plain_result(f"结果是: {a + b}") +``` + +#### 指令组 + +指令组可以帮助你组织指令。 + +```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 -> 结果是: 3 + yield event.plain_result(f"结果是: {a + b}") + +@math.command("sub") +async def sub(self, event: AstrMessageEvent, a: int, b: int): + # /math sub 1 2 -> 结果是: -1 + yield event.plain_result(f"结果是: {a - b}") +``` + +指令组函数内不需要实现任何函数,请直接 `pass` 或者添加函数内注释。指令组的子指令使用 `指令组名.command` 来注册。 + +当用户没有输入子指令时,会报错并,并渲染出该指令组的树形结构。 + +![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) + +理论上,指令组可以无限嵌套! + +```py +''' +math +├── calc +│ ├── add (a(int),b(int),) +│ ├── sub (a(int),b(int),) +│ ├── help (无参数指令) +''' + +@filter.command_group("math") +def math(): + pass + +@math.group("calc") # 请注意,这里是 group,而不是 command_group +def calc(): + pass + +@calc.command("add") +async def add(self, event: AstrMessageEvent, a: int, b: int): + yield event.plain_result(f"结果是: {a + b}") + +@calc.command("sub") +async def sub(self, event: AstrMessageEvent, a: int, b: int): + yield event.plain_result(f"结果是: {a - b}") + +@calc.command("help") +def calc_help(self, event: AstrMessageEvent): + # /math calc help + yield event.plain_result("这是一个计算器插件,拥有 add, sub 指令。") +``` + +#### 指令别名 + +> v3.4.28 后 + +可以为指令或指令组添加不同的别名: + +```python +@filter.command("help", alias={'帮助', 'helpme'}) +def help(self, event: AstrMessageEvent): + yield event.plain_result("这是一个计算器插件,拥有 add, sub 指令。") +``` + +#### 事件类型过滤 + +##### 接收所有 + +这将接收所有的事件。 + +```python +@filter.event_message_type(filter.EventMessageType.ALL) +async def on_all_message(self, event: AstrMessageEvent): + yield event.plain_result("收到了一条消息。") +``` + +##### 群聊和私聊 + +```python +@filter.event_message_type(filter.EventMessageType.PRIVATE_MESSAGE) +async def on_private_message(self, event: AstrMessageEvent): + message_str = event.message_str # 获取消息的纯文本内容 + yield event.plain_result("收到了一条私聊消息。") +``` + +`EventMessageType` 是一个 `Enum` 类型,包含了所有的事件类型。当前的事件类型有 `PRIVATE_MESSAGE` 和 `GROUP_MESSAGE`。 + +##### 消息平台 + +```python +@filter.platform_adapter_type(filter.PlatformAdapterType.AIOCQHTTP | filter.PlatformAdapterType.QQOFFICIAL) +async def on_aiocqhttp(self, event: AstrMessageEvent): + '''只接收 AIOCQHTTP 和 QQOFFICIAL 的消息''' + yield event.plain_result("收到了一条信息") +``` + +当前版本下,`PlatformAdapterType` 有 `AIOCQHTTP`, `QQOFFICIAL`, `GEWECHAT`, `ALL`。 + +##### 管理员指令 + +```python +@filter.permission_type(filter.PermissionType.ADMIN) +@filter.command("test") +async def test(self, event: AstrMessageEvent): + pass +``` + +仅管理员才能使用 `test` 指令。 + +#### 多个过滤器 + +支持同时使用多个过滤器,只需要在函数上添加多个装饰器即可。过滤器使用 `AND` 逻辑。也就是说,只有所有的过滤器都通过了,才会执行函数。 + +```python +@filter.command("helloworld") +@filter.event_message_type(filter.EventMessageType.PRIVATE_MESSAGE) +async def helloworld(self, event: AstrMessageEvent): + yield event.plain_result("你好!") +``` + +#### 事件钩子 + +> [!TIP] +> 事件钩子不支持与上面的 @filter.command, @filter.command_group, @filter.event_message_type, @filter.platform_adapter_type, @filter.permission_type 一起使用。 + +##### Bot 初始化完成时 + +> v3.4.34 后 + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.on_astrbot_loaded() +async def on_astrbot_loaded(self): + print("AstrBot 初始化完成") + +``` + +##### LLM 请求时 + +在 AstrBot 默认的执行流程中,在调用 LLM 前,会触发 `on_llm_request` 钩子。 + +可以获取到 `ProviderRequest` 对象,可以对其进行修改。 + +ProviderRequest 对象包含了 LLM 请求的所有信息,包括请求的文本、系统提示等。 + +```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): # 请注意有三个参数 + print(req) # 打印请求的文本 + req.system_prompt += "自定义 system_prompt" + +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +##### LLM 请求完成时 + +在 LLM 请求完成后,会触发 `on_llm_response` 钩子。 + +可以获取到 `ProviderResponse` 对象,可以对其进行修改。 + +```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): # 请注意有三个参数 + print(resp) +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +##### Agent 开始运行时 + +> 适用于 AstrBot 版本 > v4.23.1 + +在 Agent 开始运行时,会触发 `on_agent_begin` 钩子。 + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.core.agent.run_context import ContextWrapper +from astrbot.core.astr_agent_context import AstrAgentContext + +@filter.on_agent_begin() +async def on_agent_begin(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext]): # 请注意有三个参数 + print("Agent 开始运行") +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +##### LLM 工具调用前 + +> 适用于 AstrBot 版本 > v4.23.1 + +在 Agent 准备调用 LLM 工具时,会触发 `on_using_llm_tool` 钩子。 + +可以获取到 `FunctionTool` 对象和工具调用参数。 + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.core.agent.tool import FunctionTool + +@filter.on_using_llm_tool() +async def on_using_llm_tool( + self, + event: AstrMessageEvent, + tool: FunctionTool, + tool_args: dict | None, +): + print(tool.name, tool_args) +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +##### LLM 工具调用后 + +> 适用于 AstrBot 版本 > v4.23.1 + +在 LLM 工具调用完成后,会触发 `on_llm_tool_respond` 钩子。 + +可以获取到 `FunctionTool` 对象、工具调用参数和工具调用结果。 + +```python +from mcp.types import CallToolResult + +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.core.agent.tool import FunctionTool + +@filter.on_llm_tool_respond() +async def on_llm_tool_respond( + self, + event: AstrMessageEvent, + tool: FunctionTool, + tool_args: dict | None, + tool_result: CallToolResult | None, +): + print(tool.name, tool_args, tool_result) +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +##### Agent 运行完成时 + +> 适用于 AstrBot 版本 > v4.23.1 + +在 Agent 运行完成后,会触发 `on_agent_done` 钩子。这个钩子会在 `on_llm_response` 之后触发。 + +可以获取到 `LLMResponse` 对象,可以对其进行修改。 + +```python +from astrbot.api.event import filter, AstrMessageEvent +from astrbot.api.provider import LLMResponse +from astrbot.core.agent.run_context import ContextWrapper +from astrbot.core.astr_agent_context import AstrAgentContext + +@filter.on_agent_done() +async def on_agent_done(self, event: AstrMessageEvent, run_context: ContextWrapper[AstrAgentContext], resp: LLMResponse): # 请注意有四个参数 + print(resp) +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +##### 发送消息前 + +在发送消息前,会触发 `on_decorating_result` 钩子。 + +可以在这里实现一些消息的装饰,比如转语音、转图片、加前缀等等 + +```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) # 打印消息链 + chain.append(Plain("!")) # 在消息链的最后添加一个感叹号 +``` + +> 这里不能使用 yield 来发送消息。这个钩子只是用来装饰 event.get_result().chain 的。如需发送,请直接使用 `event.send()` 方法。 + +##### 发送消息后 + +在发送消息给消息平台后,会触发 `after_message_sent` 钩子。 + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.after_message_sent() +async def after_message_sent(self, event: AstrMessageEvent): + pass +``` + +> 这里不能使用 yield 来发送消息。如需发送,请直接使用 `event.send()` 方法。 + +#### 优先级 + +> 大于等于 v3.4.21。 + +指令、事件监听器、事件钩子可以设置优先级,先于其他指令、监听器、钩子执行。默认优先级是 `0`。 + +```python +@filter.command("helloworld", priority=1) +async def helloworld(self, event: AstrMessageEvent): + yield event.plain_result("Hello!") +``` + +### 消息的发送 + +#### 被动消息 + +被动消息指的是机器人被动回复消息。 + +```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") # 发送图片 + yield event.image_result("https://example.com/image.jpg") # 发送 URL 图片,务必以 http 或 https 开头 +``` + +#### 主动消息 + +主动消息指的是机器人主动推送消息。某些平台可能不支持主动消息发送。 + +如果是一些定时任务或者不想立即发送消息,可以使用 `event.unified_msg_origin` 得到一个字符串并将其存储,然后在想发送消息的时候使用 `self.context.send_message(unified_msg_origin, chains)` 来发送消息。 + +```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) +``` + +通过这个特性,你可以将 unified_msg_origin 存储起来,然后在需要的时候发送消息。 + +> [!TIP] +> 关于 unified_msg_origin。 +> unified_msg_origin 是一个字符串,记录了一个会话的唯一 ID,AstrBot 能够据此找到属于哪个消息平台的哪个会话。这样就能够实现在 `send_message` 的时候,发送消息到正确的会话。有关 MessageChain,请参见接下来的一节。 + +#### 富媒体消息 + +AstrBot 支持发送富媒体消息,比如图片、语音、视频等。使用 `MessageChain` 来构建消息。 + +```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()), # At 消息发送者 + Comp.Plain("来看这个图:"), + Comp.Image.fromURL("https://example.com/image.jpg"), # 从 URL 发送图片 + Comp.Image.fromFileSystem("path/to/image.jpg"), # 从本地文件目录发送图片 + Comp.Plain("这是一个图片。") + ] + yield event.chain_result(chain) +``` + +上面构建了一个 `message chain`,也就是消息链,最终会发送一条包含了图片和文字的消息,并且保留顺序。 + +类似地, + +**文件 File** + +```py +Comp.File(file="path/to/file.txt", name="file.txt") # 部分平台不支持 +``` + +**语音 Record** + +```py +path = "path/to/record.wav" # 暂时只接受 wav 格式,其他格式请自行转换 +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") +``` + +#### 发送群合并转发消息 + +> 当前适配情况:aiocqhttp + +可以按照如下方式发送群合并转发消息。 + +```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]) +``` + +![发送群合并转发消息](https://files.astrbot.app/docs/source/images/plugin/image-4.png) + +#### 发送视频消息 + +> 当前适配情况:aiocqhttp + +```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 需要用户的协议端和机器人端处于一个系统中。 + music = Video.fromFileSystem( + path="test.mp4" + ) + # 更通用 + music = Video.fromURL( + url="https://example.com/video.mp4" + ) + yield event.chain_result([music]) +``` + +![发送视频消息](https://files.astrbot.app/docs/source/images/plugin/db93a2bb-671c-4332-b8ba-9a91c35623c2.png) + +#### 发送 QQ 表情 + +> 当前适配情况:仅 aiocqhttp + +QQ 表情 ID 参考: + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.command("test") +async def test(self, event: AstrMessageEvent): + from astrbot.api.message_components import Face, Plain + yield event.chain_result([Face(id=21), Plain("你好呀")]) +``` + +![发送 QQ 表情](https://files.astrbot.app/docs/source/images/plugin/image-5.png) + +### 控制事件传播 + +```python{6} +@filter.command("check_ok") +async def check_ok(self, event: AstrMessageEvent): + ok = self.check() # 自己的逻辑 + if not ok: + yield event.plain_result("检查失败") + event.stop_event() # 停止事件传播 +``` + +当事件停止传播,后续所有步骤将不会被执行。 + +假设有一个插件 A,A 终止事件传播之后所有后续操作都不会执行,比如执行其它插件的 handler、请求 LLM。 + +### 插件配置 + +> 大于等于 v3.4.15 + +随着插件功能的增加,可能需要定义一些配置以让用户自定义插件的行为。 + +AstrBot 提供了”强大“的配置解析和可视化功能。能够让用户在管理面板上直接配置插件,而不需要修改代码。 + +![image](https://files.astrbot.app/docs/source/images/plugin/QQ_1738149538737.png) + +**Schema 介绍** + +要注册配置,首先需要在您的插件目录下添加一个 `_conf_schema.json` 的 json 文件。 + +文件内容是一个 `Schema`(模式),用于表示配置。Schema 是 json 格式的,例如上图的 Schema 是: + +```json +{ + "token": { + "description": "Bot Token", + "type": "string", + "hint": "测试醒目提醒", + "obvious_hint": true + }, + "sub_config": { + "description": "测试嵌套配置", + "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`: **此项必填**。配置的类型。支持 `string`, `text`, `int`, `float`, `bool`, `object`, `list`。当类型为 `text` 时,将会可视化为一个更大的可拖拽宽高的 textarea 组件,以适应大文本。 +- `description`: 可选。配置的描述。建议一句话描述配置的行为。 +- `hint`: 可选。配置的提示信息,表现在上图中右边的问号按钮,当鼠标悬浮在问号按钮上时显示。 +- `obvious_hint`: 可选。配置的 hint 是否醒目显示。如上图的 `token`。 +- `default`: 可选。配置的默认值。如果用户没有配置,将使用默认值。int 是 0,float 是 0.0,bool 是 False,string 是 "",object 是 {},list 是 []。 +- `items`: 可选。如果配置的类型是 `object`,需要添加 `items` 字段。`items` 的内容是这个配置项的子 Schema。理论上可以无限嵌套,但是不建议过多嵌套。 +- `invisible`: 可选。配置是否隐藏。默认是 `false`。如果设置为 `true`,则不会在管理面板上显示。 +- `options`: 可选。一个列表,如 `"options": ["chat", "agent", "workflow"]`。提供下拉列表可选项。 +- `editor_mode`: 可选。是否启用代码编辑器模式。需要 AstrBot >= `v3.5.10`, 低于这个版本不会报错,但不会生效。默认是 false。 +- `editor_language`: 可选。代码编辑器的代码语言,默认为 `json`。 +- `editor_theme`: 可选。代码编辑器的主题,可选值有 `vs-light`(默认), `vs-dark`。 +- `_special`: 可选。用于调用 AstrBot 提供的可视化提供商选取、人格选取、知识库选取等功能,详见下文。 + +其中,如果启用了代码编辑器,效果如下图所示: + +![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) + +**_special** 字段仅 v4.0.0 之后可用。目前支持填写 `select_provider`, `select_provider_tts`, `select_provider_stt`, `select_persona`,用于让用户快速选择用户在 WebUI 上已经配置好的模型提供商、人设等数据。结果均为字符串。以 select_provider 为例,将呈现以下效果: + +![image](https://files.astrbot.app/docs/source/images/plugin/image.png) + +**使用配置** + +AstrBot 在载入插件时会检测插件目录下是否有 `_conf_schema.json` 文件,如果有,会自动解析配置并保存在 `data/config/_config.json` 下(依照 Schema 创建的配置文件实体),并在实例化插件类时传入给 `__init__()`。 + +```py +from astrbot.api import AstrBotConfig + +class ConfigPlugin(Star): + def __init__(self, context: Context, config: AstrBotConfig): # AstrBotConfig 继承自 Dict,拥有字典的所有方法 + super().__init__(context) + self.config = config + print(self.config) + + # 支持直接保存配置 + # self.config.save_config() # 保存配置 +``` + +**配置版本管理** + +如果您在发布不同版本时更新了 Schema,请注意,AstrBot 会递归检查 Schema 的配置项,如果发现配置文件中缺失了某个配置项,会自动添加默认值。但是 AstrBot 不会删除配置文件中**多余的**配置项,即使这个配置项在新的 Schema 中不存在(您在新的 Schema 中删除了这个配置项)。 + +### 文转图 + +#### 基本 + +AstrBot 支持将文字渲染成图片。 + +```python +@filter.command("image") # 注册一个 /image 指令,接收 text 参数。 +async def on_aiocqhttp(self, event: AstrMessageEvent, text: str): + url = await self.text_to_image(text) # text_to_image() 是 Star 类的一个方法。 + # path = await self.text_to_image(text, return_url = False) # 如果你想保存图片到本地 + yield event.image_result(url) + +``` + +![image](https://files.astrbot.app/docs/source/images/plugin/image-3.png) + +#### 自定义(基于 HTML) + +如果你觉得上面渲染出来的图片不够美观,你可以使用自定义的 HTML 模板来渲染图片。 + +AstrBot 支持使用 `HTML + Jinja2` 的方式来渲染文转图模板。 + +```py{7} +# 自定义的 Jinja2 模板,支持 CSS +TMPL = ''' +
+

Todo List

+ +
    +{% for item in items %} +
  • {{ item }}
  • +{% endfor %} +
+''' + +@filter.command("todo") +async def custom_t2i_tmpl(self, event: AstrMessageEvent): + options = {} # 可选择传入渲染选项。 + url = await self.html_render(TMPL, {"items": ["吃饭", "睡觉", "玩原神"]}, options=options) # 第二个参数是 Jinja2 的渲染数据 + yield event.image_result(url) +``` + +返回的结果: + +![image](https://files.astrbot.app/docs/source/images/plugin/fcc2dcb472a91b12899f617477adc5c7.png) + +这只是一个简单的例子。得益于 HTML 和 DOM 渲染器的强大性,你可以进行更复杂和更美观的的设计。除此之外,Jinja2 支持循环、条件等语法以适应列表、字典等数据结构。你可以从网上了解更多关于 Jinja2 的知识。 + +**图片渲染选项(options)**: + +请参考 Playwright 的 [screenshot](https://playwright.dev/python/docs/api/class-page#page-screenshot) API。 + +- `timeout` (float, optional): 截图超时时间. +- `type` (Literal["jpeg", "png"], optional): 截图图片类型. +- `quality` (int, optional): 截图质量,仅适用于 JPEG 格式图片. +- `omit_background` (bool, optional): 是否允许隐藏默认的白色背景,这样就可以截透明图了,仅适用于 PNG 格式 +- `full_page` (bool, optional): 是否截整个页面而不是仅设置的视口大小,默认为 True. +- `clip` (dict, optional): 截图后裁切的区域。参考 Playwright screenshot API。 +- `animations`: (Literal["allow", "disabled"], optional): 是否允许播放 CSS 动画. +- `caret`: (Literal["hide", "initial"], optional): 当设置为 hide 时,截图时将隐藏文本插入符号,默认为 hide. +- `scale`: (Literal["css", "device"], optional): 页面缩放设置. 当设置为 css 时,则将设备分辨率与 CSS 中的像素一一对应,在高分屏上会使得截图变小. 当设置为 device 时,则根据设备的屏幕缩放设置或当前 Playwright 的 Page/Context 中的 device_scale_factor 参数来缩放. +- `mask` (List["Locator"]], optional): 指定截图时的遮罩的 Locator。元素将被一颜色为 #FF00FF 的框覆盖. + +### 会话控制 + +> 大于等于 v3.4.36 + +为什么需要会话控制?考虑一个 成语接龙 插件,某个/群用户需要和机器人进行多次对话,而不是一次性的指令。这时候就需要会话控制。 + +```txt +用户: /成语接龙 +机器人: 请发送一个成语 +用户: 一马当先 +机器人: 先见之明 +用户: 明察秋毫 +... +``` + +AstrBot 提供了开箱即用的会话控制功能: + +导入: + +```py +import astrbot.api.message_components as Comp +from astrbot.core.utils.session_waiter import ( + session_waiter, + SessionController, +) +``` + +handler 内的代码可以如下: + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.command("成语接龙") +async def handle_empty_mention(self, event: AstrMessageEvent): + """成语接龙具体实现""" + try: + yield event.plain_result("请发送一个成语~") + + # 具体的会话控制器使用方法 + @session_waiter(timeout=60, record_history_chains=False) # 注册一个会话控制器,设置超时时间为 60 秒,不记录历史消息链 + async def empty_mention_waiter(controller: SessionController, event: AstrMessageEvent): + idiom = event.message_str # 用户发来的成语,假设是 "一马当先" + + if idiom == "退出": # 假设用户想主动退出成语接龙,输入了 "退出" + await event.send(event.plain_result("已退出成语接龙~")) + controller.stop() # 停止会话控制器,会立即结束。 + return + + if len(idiom) != 4: # 假设用户输入的不是4字成语 + await event.send(event.plain_result("成语必须是四个字的呢~")) # 发送回复,不能使用 yield + return + # 退出当前方法,不执行后续逻辑,但此会话并未中断,后续的用户输入仍然会进入当前会话 + + # ... + message_result = event.make_result() + message_result.chain = [Comp.Plain("先见之明")] # import astrbot.api.message_components as Comp + await event.send(message_result) # 发送回复,不能使用 yield + + controller.keep(timeout=60, reset_timeout=True) # 重置超时时间为 60s,如果不重置,则会继续之前的超时时间计时。 + + # controller.stop() # 停止会话控制器,会立即结束。 + # 如果记录了历史消息链,可以通过 controller.get_history_chains() 获取历史消息链 + + try: + await empty_mention_waiter(event) + except TimeoutError as _: # 当超时后,会话控制器会抛出 TimeoutError + yield event.plain_result("你超时了!") + except Exception as e: + yield event.plain_result("发生错误,请联系管理员: " + str(e)) + finally: + event.stop_event() + except Exception as e: + logger.error("handle_empty_mention error: " + str(e)) +``` + +当激活会话控制器后,该发送人之后发送的消息会首先经过上面你定义的 `empty_mention_waiter` 函数处理,直到会话控制器被停止或者超时。 + +#### SessionController + +用于开发者控制这个会话是否应该结束,并且可以拿到历史消息链。 + +- keep(): 保持这个会话 + - timeout (float): 必填。会话超时时间。 + - reset_timeout (bool): 设置为 True 时, 代表重置超时时间, timeout 必须 > 0, 如果 <= 0 则立即结束会话。设置为 False 时, 代表继续维持原来的超时时间, 新 timeout = 原来剩余的 timeout + timeout (可以 < 0) +- stop(): 结束这个会话 +- get_history_chains() -> List[List[Comp.BaseMessageComponent]]: 获取历史消息链 + +#### 自定义会话 ID 算子 + +默认情况下,AstrBot 会话控制器会将基于 `sender_id` (发送人的 ID)作为识别不同会话的标识,如果想将一整个群作为一个会话,则需要自定义会话 ID 算子。 + +```py +import astrbot.api.message_components as Comp +from astrbot.core.utils.session_waiter import ( + session_waiter, + SessionFilter, + SessionController, +) + +# 沿用上面的 handler +# ... +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()) # 这里传入 session_filter +# ... +``` + +这样之后,当群内一个用户发送消息后,会话控制器会将这个群作为一个会话,群内其他用户发送的消息也会被认为是同一个会话。 + +甚至,可以使用这个特性来让群内组队! + +### AI + +#### 通过提供商调用 LLM + +获取提供商有以下几种方式: + +- 获取当前使用的大语言模型提供商: `self.context.get_using_provider(umo=event.unified_msg_origin)`。 +- 根据 ID 获取大语言模型提供商: `self.context.get_provider_by_id(provider_id="xxxx")`。 +- 获取所有大语言模型提供商: `self.context.get_all_providers()`。 + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.command("test") +async def test(self, event: AstrMessageEvent): + # func_tools_mgr = self.context.get_llm_tool_manager() + prov = self.context.get_using_provider(umo=event.unified_msg_origin) + if prov: + llm_resp = await provider.text_chat( + prompt="Hi!", + context=[ + {"role": "user", "content": "balabala"}, + {"role": "assistant", "content": "response balabala"} + ], + system_prompt="You are a helpful assistant." + ) + print(llm_resp) +``` + +`Provider.text_chat()` 用于请求 LLM。其返回 `LLMResponse` 方法。除了上面的三个参数,其还支持: + +- `func_tool`(ToolSet): 可选。用于传入函数工具。参考 [函数工具](#函数工具)。 +- `image_urls`(List[str]): 可选。用于传入请求中带有的图片 URL 列表。支持文件路径。 +- `model`(str): 可选。用于强制指定使用的模型。默认使用这个提供商默认配置的模型。 +- `tool_calls_result`(dict): 可选。用于传入工具调用的结果。 + +::: details LLMResponse 类型定义 + +```py + +@dataclass +class LLMResponse: + role: str + """角色, assistant, tool, err""" + result_chain: MessageChain = None + """返回的消息链""" + tools_call_args: List[Dict[str, any]] = field(default_factory=list) + """工具调用参数""" + tools_call_name: List[str] = field(default_factory=list) + """工具调用名称""" + tools_call_ids: List[str] = field(default_factory=list) + """工具调用 ID""" + + raw_completion: ChatCompletion = None + _new_record: Dict[str, any] = None + + _completion_text: str = "" + + is_chunk: bool = False + """是否是流式输出的单个 Chunk""" + + def __init__( + self, + role: str, + completion_text: str = "", + result_chain: MessageChain = None, + tools_call_args: List[Dict[str, any]] = None, + tools_call_name: List[str] = None, + tools_call_ids: List[str] = None, + raw_completion: ChatCompletion = None, + _new_record: Dict[str, any] = None, + is_chunk: bool = False, + ): + """初始化 LLMResponse + + Args: + role (str): 角色, assistant, tool, err + completion_text (str, optional): 返回的结果文本,已经过时,推荐使用 result_chain. Defaults to "". + result_chain (MessageChain, optional): 返回的消息链. Defaults to None. + tools_call_args (List[Dict[str, any]], optional): 工具调用参数. Defaults to None. + tools_call_name (List[str], optional): 工具调用名称. Defaults to None. + raw_completion (ChatCompletion, optional): 原始响应, OpenAI 格式. Defaults to None. + """ + if tools_call_args is None: + tools_call_args = [] + if tools_call_name is None: + tools_call_name = [] + if tools_call_ids is None: + tools_call_ids = [] + + self.role = role + self.completion_text = completion_text + self.result_chain = result_chain + self.tools_call_args = tools_call_args + self.tools_call_name = tools_call_name + self.tools_call_ids = tools_call_ids + self.raw_completion = raw_completion + self._new_record = _new_record + self.is_chunk = is_chunk + + @property + def completion_text(self): + if self.result_chain: + return self.result_chain.get_plain_text() + return self._completion_text + + @completion_text.setter + def completion_text(self, value): + if self.result_chain: + self.result_chain.chain = [ + comp + for comp in self.result_chain.chain + if not isinstance(comp, Comp.Plain) + ] # 清空 Plain 组件 + self.result_chain.chain.insert(0, Comp.Plain(value)) + else: + self._completion_text = value + + def to_openai_tool_calls(self) -> List[Dict]: + """将工具调用信息转换为 OpenAI 格式""" + ret = [] + for idx, tool_call_arg in enumerate(self.tools_call_args): + ret.append( + { + "id": self.tools_call_ids[idx], + "function": { + "name": self.tools_call_name[idx], + "arguments": json.dumps(tool_call_arg), + }, + "type": "function", + } + ) + return ret +``` + +::: + +#### 获取其他类型的提供商 + +> 嵌入、重排序 没有 “当前使用”。这两个提供商主要用于知识库。 + +- 获取当前使用的语音识别提供商(STTProvider): `self.context.get_using_stt_provider(umo=event.unified_msg_origin)`。 +- 获取当前使用的语音合成提供商(TTSProvider): `self.context.get_using_tts_provider(umo=event.unified_msg_origin)`。 +- 获取所有语音识别提供商: `self.context.get_all_stt_providers()`。 +- 获取所有语音合成提供商: `self.context.get_all_tts_providers()`。 +- 获取所有嵌入提供商: `self.context.get_all_embedding_providers()`。 + +::: details STTProvider / TTSProvider / EmbeddingProvider 类型定义 + +```py +class TTSProvider(AbstractProvider): + def __init__(self, provider_config: dict, provider_settings: dict) -> None: + super().__init__(provider_config) + self.provider_config = provider_config + self.provider_settings = provider_settings + + @abc.abstractmethod + async def get_audio(self, text: str) -> str: + """获取文本的音频,返回音频文件路径""" + raise NotImplementedError() + + +class EmbeddingProvider(AbstractProvider): + def __init__(self, provider_config: dict, provider_settings: dict) -> None: + super().__init__(provider_config) + self.provider_config = provider_config + self.provider_settings = provider_settings + + @abc.abstractmethod + async def get_embedding(self, text: str) -> list[float]: + """获取文本的向量""" + ... + + @abc.abstractmethod + async def get_embeddings(self, text: list[str]) -> list[list[float]]: + """批量获取文本的向量""" + ... + + @abc.abstractmethod + def get_dim(self) -> int: + """获取向量的维度""" + ... + +class STTProvider(AbstractProvider): + def __init__(self, provider_config: dict, provider_settings: dict) -> None: + super().__init__(provider_config) + self.provider_config = provider_config + self.provider_settings = provider_settings + + @abc.abstractmethod + async def get_text(self, audio_url: str) -> str: + """获取音频的文本""" + raise NotImplementedError() +``` + +::: + +#### 函数工具 + +函数工具给了大语言模型调用外部工具的能力。在 AstrBot 中,函数工具有多种定义方式。 + +##### 以类的形式(推荐) + +推荐在插件目录下新建 `tools` 文件夹,然后在其中编写工具类: + +`tools/search.py`: + +```py +from astrbot.api import FunctionTool +from astrbot.api.event import AstrMessageEvent +from dataclasses import dataclass, field + +@dataclass +class HelloWorldTool(FunctionTool): + name: str = "hello_world" # 工具名称 + description: str = "Say hello to the world." # 工具描述 + parameters: dict = field( + default_factory=lambda: { + "type": "object", + "properties": { + "greeting": { + "type": "string", + "description": "The greeting message.", + }, + }, + "required": ["greeting"], + } + ) # 工具参数定义,见 OpenAI 官网或 https://json-schema.org/understanding-json-schema/ + + async def run( + self, + event: AstrMessageEvent, # 必须包含此 event 参数在前面,用于获取上下文 + greeting: str, # 工具参数,必须与 parameters 中定义的参数名一致 + ): + return f"{greeting}, World!" # 也支持 mcp.types.CallToolResult 类型 +``` + +要将上述工具注册到 AstrBot,可以在插件主文件的 `__init__.py` 中添加以下代码: + +```py +from .tools.search import SearchTool + +class MyPlugin(Star): + def __init__(self, context: Context): + super().__init__(context) + # >= v4.5.1 使用: + self.context.add_llm_tools(HelloWorldTool(), SecondTool(), ...) + + # < v4.5.1 之前使用: + tool_mgr = self.context.provider_manager.llm_tools + tool_mgr.func_list.append(HelloWorldTool()) +``` + +##### 以装饰器的形式 + +这个形式定义的工具函数会被自动加载到 AstrBot Core 中,在 Core 请求大模型时会被自动带上。 + +请务必按照以下格式编写一个工具(包括**函数注释**,AstrBot 会解析该函数注释,请务必将注释格式写对) + +```py{3,4,5,6,7} +@filter.llm_tool(name="get_weather") # 如果 name 不填,将使用函数名 +async def get_weather(self, event: AstrMessageEvent, location: str) -> MessageEventResult: + '''获取天气信息。 + + Args: + location(string): 地点 + ''' + resp = self.get_weather_from_api(location) + yield event.plain_result("天气信息: " + resp) +``` + +在 `location(string): 地点` 中,`location` 是参数名,`string` 是参数类型,`地点` 是参数描述。 + +支持的参数类型有 `string`, `number`, `object`, `boolean`。 + +> [!NOTE] +> 对于装饰器注册的 llm_tool,如果需要调用 Provider.text_chat(),func_tool(ToolSet 类型) 可以通过以下方式获取: +> +> ```py +> func_tool = self.context.get_llm_tool_manager() # 获取 AstrBot 的 LLM Tool Manager,包含了所有插件和 MCP 注册的 Tool +> tool = func_tool.get_func("xxx") +> if tool: +> tool_set = ToolSet() +> tool_set.add_tool(tool) +> ``` + +#### 对话管理器 ConversationManager + +**获取会话当前的 LLM 对话历史** + +```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: + """LLM 对话类 + + 对于 WebChat,history 存储了包括指令、回复、图片等在内的所有消息。 + 对于其他平台的聊天,不存储非 LLM 的回复(因为考虑到已经存储在各自的平台上)。 + + 在 v4.0.0 版本及之后,WebChat 的历史记录被迁移至 `PlatformMessageHistory` 表中, + """ + + platform_id: str + user_id: str + cid: str + """对话 ID, 是 uuid 格式的字符串""" + history: str = "" + """字符串格式的对话列表。""" + title: str | None = "" + persona_id: str | None = "" + """对话当前使用的人格 ID""" + created_at: int = 0 + updated_at: int = 0 +``` + +::: + +**所有方法** + +##### `new_conversation` + +- **Usage** + 在当前会话中新建一条对话,并自动切换为该对话。 +- **Arguments** + - `unified_msg_origin: str` – 形如 `platform_name:message_type:session_id` + - `platform_id: str | None` – 平台标识,默认从 `unified_msg_origin` 解析 + - `content: list[dict] | None` – 初始历史消息 + - `title: str | None` – 对话标题 + - `persona_id: str | None` – 绑定的 persona ID +- **Returns** + `str` – 新生成的 UUID 对话 ID + +##### `switch_conversation` + +- **Usage** + 将会话切换到指定的对话。 +- **Arguments** + - `unified_msg_origin: str` + - `conversation_id: str` +- **Returns** + `None` + +##### `delete_conversation` + +- **Usage** + 删除会话中的某条对话;若 `conversation_id` 为 `None`,则删除当前对话。 +- **Arguments** + - `unified_msg_origin: str` + - `conversation_id: str | None` +- **Returns** + `None` + +##### `get_curr_conversation_id` + +- **Usage** + 获取当前会话正在使用的对话 ID。 +- **Arguments** + - `unified_msg_origin: str` +- **Returns** + `str | None` – 当前对话 ID,不存在时返回 `None` + +##### `get_conversation` + +- **Usage** + 获取指定对话的完整对象;若不存在且 `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** + 拉取用户或平台下的全部对话列表。 +- **Arguments** + - `unified_msg_origin: str | None` – 为 `None` 时不过滤用户 + - `platform_id: str | None` +- **Returns** + `List[Conversation]` + +##### `get_filtered_conversations` + +- **Usage** + 分页 + 关键词搜索对话。 +- **Arguments** + - `page: int = 1` + - `page_size: int = 20` + - `platform_ids: list[str] | None` + - `search_query: str = ""` + - `**kwargs` – 透传其他过滤条件 +- **Returns** + `tuple[list[Conversation], int]` – 对话列表与总数 + +##### `update_conversation` + +- **Usage** + 更新对话的标题、历史记录或 persona_id。 +- **Arguments** + - `unified_msg_origin: str` + - `conversation_id: str | None` – 为 `None` 时使用当前对话 + - `history: list[dict] | None` + - `title: str | None` + - `persona_id: str | None` +- **Returns** + `None` + +##### `get_human_readable_context` + +- **Usage** + 生成分页后的人类可读对话上下文,方便展示或调试。 +- **Arguments** + - `unified_msg_origin: str` + - `conversation_id: str` + - `page: int = 1` + - `page_size: int = 10` +- **Returns** + `tuple[list[str], int]` – 当前页文本列表与总页数 + +```py +import json + +context = json.loads(conversation.history) +``` + +#### 人格设定管理器 PersonaManager + +`PersonaManager` 负责统一加载、缓存并提供所有人格(Persona)的增删改查接口,同时兼容 AstrBot 4.x 之前的旧版人格格式(v3)。 +初始化时会自动从数据库读取全部人格,并生成一份 v3 兼容数据,供旧代码无缝使用。 + +```py +persona_mgr = self.context.persona_manager +``` + +##### `get_persona` + +- **Usage** + 获取根据人格 ID 获取人格数据。 +- **Arguments** + - `persona_id: str` – 人格 ID +- **Returns** + `Persona` – 人格数据,若不存在则返回 None +- **Raises** + `ValueError` – 当不存在时抛出 + +##### `get_all_personas` + +- **Usage** + 一次性获取数据库中所有人格。 +- **Returns** + `list[Persona]` – 人格列表,可能为空 + +##### `create_persona` + +- **Usage** + 新建人格并立即写入数据库,成功后自动刷新本地缓存。 +- **Arguments** + - `persona_id: str` – 新人格 ID(唯一) + - `system_prompt: str` – 系统提示词 + - `begin_dialogs: list[str]` – 可选,开场对话(偶数条,user/assistant 交替) + - `tools: list[str]` – 可选,允许使用的工具列表;`None`=全部工具,`[]`=禁用全部 +- **Returns** + `Persona` – 新建后的人格对象 +- **Raises** + `ValueError` – 若 `persona_id` 已存在 + +##### `update_persona` + +- **Usage** + 更新现有人格的任意字段,并同步到数据库与缓存。 +- **Arguments** + - `persona_id: str` – 待更新的人格 ID + - `system_prompt: str` – 可选,新的系统提示词 + - `begin_dialogs: list[str]` – 可选,新的开场对话 + - `tools: list[str]` – 可选,新的工具列表;语义同 `create_persona` +- **Returns** + `Persona` – 更新后的人格对象 +- **Raises** + `ValueError` – 若 `persona_id` 不存在 + +##### `delete_persona` + +- **Usage** + 删除指定人格,同时清理数据库与缓存。 +- **Arguments** + - `persona_id: str` – 待删除的人格 ID +- **Raises** + `Valueable` – 若 `persona_id` 不存在 + +##### `get_default_persona_v3` + +- **Usage** + 根据当前会话配置,获取应使用的默认人格(v3 格式)。 + 若配置未指定或指定的人格不存在,则回退到 `DEFAULT_PERSONALITY`。 +- **Arguments** + - `umo: str | MessageSession | None` – 会话标识,用于读取用户级配置 +- **Returns** + `Personality` – v3 格式的默认人格对象 + +::: 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 人格类。 + + 在 v4.0.0 版本及之后,推荐使用上面的 Persona 类。并且, mood_imitation_dialogs 字段已被废弃。 + """ + + prompt: str + name: str + begin_dialogs: list[str] + mood_imitation_dialogs: list[str] + """情感模拟对话预设。在 v4.0.0 版本及之后,已被废弃。""" + tools: list[str] | None + """工具列表。None 表示使用所有工具,空列表表示不使用任何工具""" +``` + +::: + +### 其他 + +#### 配置文件 + +##### 默认配置文件 + +```py +config = self.context.get_config() +``` + +不建议修改默认配置文件,建议只读取。 + +##### 会话配置文件 + +v4.0.0 后,AstrBot 支持会话粒度的多配置文件。 + +```py +umo = event.unified_msg_origin +config = self.context.get_config(umo=umo) +``` + +#### 获取消息平台实例 + +> v3.4.34 后 + +```python +from astrbot.api.event import filter, AstrMessageEvent + +@filter.command("test") +async def test_(self, event: AstrMessageEvent): + from astrbot.api.platform import AiocqhttpAdapter # 其他平台同理 + platform = self.context.get_platform(filter.PlatformAdapterType.AIOCQHTTP) + assert isinstance(platform, AiocqhttpAdapter) + # platform.get_client().api.call_action() +``` + +#### 调用 QQ 协议端 API + +```py +@filter.command("helloworld") +async def helloworld(self, event: AstrMessageEvent): + if event.get_platform_name() == "aiocqhttp": + # qq + from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import AiocqhttpMessageEvent + assert isinstance(event, AiocqhttpMessageEvent) + client = event.bot # 得到 client + payloads = { + "message_id": event.message_obj.message_id, + } + ret = await client.api.call_action('delete_msg', **payloads) # 调用 协议端 API + logger.info(f"delete_msg: {ret}") +``` + +关于 CQHTTP API,请参考如下文档: + +Napcat API 文档: + +Lagrange API 文档: + +#### 载入的所有插件 + +```py +plugins = self.context.get_all_stars() # 返回 StarMetadata 包含了插件类实例、配置等等 +``` + +#### 注册一个异步任务 + +直接在 **init**() 中使用 `asyncio.create_task()` 即可。 + +```py +import asyncio + +class TaskPlugin(Star): + def __init__(self, context: Context): + super().__init__(context) + asyncio.create_task(self.my_task()) + + async def my_task(self): + await asyncio.sleep(1) + print("Hello") +``` + +#### 获取加载的所有平台 + +```py +from astrbot.api.platform import Platform +platforms = self.context.platform_manager.get_insts() # List[Platform] +``` diff --git a/docs/snapshots/v4.23.6/zh/faq.md b/docs/snapshots/v4.23.6/zh/faq.md new file mode 100644 index 0000000..f965788 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/faq.md @@ -0,0 +1,116 @@ +# FAQ + +## 管理面板相关 + +### 当管理面板打开时遇到 404 错误 + +在 [release](https://github.com/AstrBotDevs/AstrBot/releases) 页面下载 `dist.zip`,解压拖到 `AstrBot/data` 下。还不行请重启电脑(来自群里的反馈) + +### 管理面板的密码忘记了 + +如果你忘记了 AstrBot 管理面板的密码,你可以在 `AstrBot/data/cmd_config.json` 配置文件中找到 `"dashboard"` 字段进行修改,其中 `"username"` 是你的用户名,`"password"` 是你的密码(经过 MD5 加密)。 + +如果想要修改账号密码,你可以这样做: + +1. 修改 `"username"` 字段,注意保留 `""`;如果不想修改用户名,可以不修改 +2. 进入网站:[在线 MD5 生成](https://www.metools.info/code/c26.html) +3. 在转换前文本框输入你的新密码 +4. 选择 MD5 加密(32 位),请确认选择 32 位选项 +5. 将转换后的字符粘贴至配置文件,注意保留 `""`, 且字母使用小写 + +## AstrBot 使用相关 + +### 如何让 AstrBot 控制我的 Mac / Windows / Linux 电脑? + +1. 在 AstrBot WebUI 的 `配置 -> 普通配置` 中,找到 `使用电脑能力`,运行环境选择 `local`。 +2. 在 `配置 -> 其他配置` 中,找到 `管理员 ID 列表`,添加你的用户 ID(可以通过 `/sid` 指令获取)。 +3. 右下角保存配置 + +> [!TIP] +> AstrBot 为了安全起见,运行环境选择 `local` 时,默认仅允许 AstrBot 管理员使用电脑能力。 +> 运行环境可以选择 `sandbox`,此时所有用户都可以使用电脑能力(在一个隔离的沙箱中)。详情请看 [AstrBot 沙箱环境](/use/astrbot-agent-sandbox.md) + +### 通过 AstrBot 桌面客户端安装的 AstrBot,data 目录在哪? + +在家目录下的 `.astrbot` 目录下。 + +- Windows: `C:\Users\你的用户名\.astrbot` +- MacOS / Linux: `/Users/你的用户名/.astrbot` 或者 `/home/你的用户名/.astrbot` + +### 通过 AstrBot Launcher 安装的 AstrBot,data 目录在哪? + +如果是旧版本的 AstrBot Launcher(Powershell),data 目录就在 Launcher bat 脚本的同级目录下。 + +如果是新版本的 AstrBot Launcher(可视化),data 目录在家目录下的 `.astrbot_launcher` 目录下。 + +- Windows: `C:\Users\你的用户名\.astrbot_launcher` +- MacOS / Linux: `/Users/你的用户名/.astrbot_launcher` 或者 `/home/你的用户名/.astrbot_launcher` + +### 机器人在群聊无法聊天 + +1. 群聊情况下,由于防止消息泛滥,不会对每条监听到的消息都回复,请尝试 @ 机器人或者使用唤醒词来聊天,比如默认的 `/`,输入 `/你好`。 + +### 没有权限操作管理员指令 + +1. `/reset, /persona, /dashboard_update, /op, /deop, /wl, /dewl` 是默认的管理员指令。可以通过 `/sid` 指令得到用户的 ID,然后在 `配置` -> `其他配置` 中添加到管理员 ID 名单中。 + +### 本地渲染 Markdown 图片(t2i)时中文乱码 + +可以自定义字体。详见 -> [#957](https://github.com/AstrBotDevs/AstrBot/issues/957#issuecomment-2749981802) + +推荐 [Maple Mono](https://github.com/subframe7536/maple-font) 字体。 + +### API 返回的 completion 无法解析 + +这是由于供应商的 API 返回了空文本,尝试以下步骤: + +1. 检查 API Key 是否仍然有效 +2. 检查是否达到 API 调用限制或配额 +3. 检查网络连接 +4. 尝试 `reset` +5. 降低最大对话次数设置 +6. 切换使用同一供应商的其他模型,或不同供应商的模型 + +## 插件相关 + +### 插件安装不上 + +1. 插件通过 GitHub 安装,在国内访问 GitHub 确实有时候连不上。可以挂代理,然后进入 `其他配置` -> `HTTP 代理` 设置代理,或者直接下载插件压缩包后上传。 + +### 安装插件后报错 `No module named 'xxx'` + +![image](https://files.astrbot.app/docs/source/images/faq/image.png) + +这个是因为插件依赖的库没有被正常安装。一般情况下,AstrBot 会在安装好插件后自动为插件安装依赖库,如果出现了以下情况可能造成安装失败: + +1. 网络问题导致依赖库无法下载 +2. 插件作者没有填写 `requirements.txt` 文件 +3. Python 版本不兼容 + +解决方法: + +结合报错信息,参考插件的 README 手动安装依赖库。你可以在 AstrBot WebUI 的 `平台日志` -> `安装 Pip 库` 中安装依赖库。 + +![image](https://files.astrbot.app/docs/source/images/faq/image-1.png) + +如果发现插件作者没有填写 `requirements.txt` 文件,请在插件仓库提交 Issue,提醒作者补充。 + + +## OneBot v11 实现端 NapCat 连接相关 + +### 我明明按照文档的步骤做了,为什么 NapCat 连不上 Astrbot? + +1. 如果你两个**全都**是使用 Docker 部署,请尝试在终端运行: + +```bash +sudo docker network create newnet # 创建新网络 +sudo docker network connect newnet astrbot +sudo docker network connect newnet napcat # 让两个容器连到一起 +sudo docker restart astrbot +sudo docker restart napcat # 重启容器 +``` + +运行无报错则回到 NapCat 的 WebUI,网络配置中,将你之前填写的 `ws://127.0.0.1:6199/ws` 修改为 `ws://astrbot:6199/ws`。 + +2. 如果只有 NapCat 是 Docker 部署,请将 NapCat 的 WebUI 网络配置中的 `ws://127.0.0.1:6199/ws` 修改为 `ws://宿主机IP:6199/ws`(宿主机 IP 请自行搜索如何查看)。 +3. 如果都不是 Docker 部署,则请将 NapCat 的 WebUI 网络配置中的 `ws://127.0.0.1:6199/ws` 修改为 `ws://localhost:6199/ws` 或 `ws://127.0.0.1:6199/ws`。 diff --git a/docs/snapshots/v4.23.6/zh/index.md b/docs/snapshots/v4.23.6/zh/index.md new file mode 100644 index 0000000..a62caef --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/index.md @@ -0,0 +1,31 @@ +--- +# https://vitepress.dev/reference/default-theme-home-page +layout: home + +hero: + name: >- +
Soulter%2FAstrBot | Trendshift + text: "Agentic AI 助手,服务个人与群聊" + tagline: 连接 IM / 1000+ 插件扩展 / 通用 Agent 能力编排 + actions: + - theme: brand + text: 快速开始 + link: /what-is-astrbot + - theme: alt + text: GitHub 仓库 + link: https://github.com/AstrBotDevs/AstrBot + +features: + - icon: ✨ + title: 多平台支持 + details: 可集成到 QQ、企业微信、飞书、Telegram、Discord 等多个聊天平台 + - icon: 😌 + title: 方便易用 + details: 支持多种方式部署,无需复杂的配置,配备高度可视化的管理面板 + - icon: 🧩 + title: 高扩展性 + details: 灵活易用的插件系统。 + - icon: 🌟 + title: AI + details: 支持 OpenAI、Anthropic、Gemini 等多种大模型接入,内置知识库和 Agent 智能体 +--- diff --git a/docs/snapshots/v4.23.6/zh/others/github-proxy.md b/docs/snapshots/v4.23.6/zh/others/github-proxy.md new file mode 100644 index 0000000..88daea1 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/others/github-proxy.md @@ -0,0 +1,32 @@ +# 自建 GitHub 加速服务 + +如果发现升级 AstrBot、安装/更新插件时总是因为网络问题安装失败,您可以通过自建 GitHub 加速服务来实现高速访问。 + +![image](https://files.astrbot.app/docs/source/images/github-proxy/image.png) + +## 使用 `lxfight/astrbot2github` 自建加速服务 + +> 预计部署用时: `2` 分钟 + +0. 打开 [lxfight/astrbot2github](https://github.com/lxfight/astrbot2github) +1. **(可选但推荐)** 给本项目点个 [**Star ⭐**](https://github.com/lxfight/astrbot2github),你的支持是作者更新和维护的动力! +2. **Fork 本项目**: 点击页面右上角的 [**Fork**](https://github.com/lxfight/astrbot2github/fork) 按钮,将此项目复刻到你自己的 GitHub 账号下。 +3. **登录 Deno Deploy**: 访问 [Deno Deploy](https://dash.deno.com/) 并使用你的 GitHub 账号登录。 +4. **创建新项目**: + * 点击 **New Project** (或 **新建项目**)。 + * 选择 **Deploy from GitHub repository** (带有 GitHub 图标的那个选项)。 + * 授权 Deno Deploy 访问你的 GitHub 仓库。 +5. **选择仓库**: 在仓库列表中,选择刚刚 Fork 的 `astrbot2github` 项目。 +6. **配置部署**: + * **Production Branch**: 保持默认 (`main`) 即可。 + * **Entrypoint**: **这是关键步骤!** 点击下拉框,找到并选择 `deno_index.ts` 文件作为入口点。 + * **Project Name**: Deno 会自动生成一个项目名称,这将是你的服务地址的一部分。你可以保留自动生成的名称 (例如 `fluffy-donkey-12`),也可以自定义名称 (例如 `my-astrbot-proxy`)。 +7. **开始部署**: 确认设置无误后,点击 **Link** 或 **Deploy** 按钮。稍等片刻即可完成。 +8. **获取服务地址**: 部署成功后,页面会显示你的服务地址,格式为 `https://<第6步设置的项目名>.deno.dev`。复制这个地址。 +9. **配置 AstrBot**: + * 回到你的 AstrBot WebUI。 + * 进入 **设置 (Settings)** 页面。 + * 找到 **GitHub 加速地址 (GitHub Proxy)** + * 将**第 8 步**复制的 Deno 服务地址完整粘贴进去。 + +🎉 **完成!** 现在 AstrBot 在访问插件市场和下载插件时,将会通过你刚刚部署的 Deno 服务进行代理。 diff --git a/docs/snapshots/v4.23.6/zh/others/ipv6.md b/docs/snapshots/v4.23.6/zh/others/ipv6.md new file mode 100644 index 0000000..29aabb7 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/others/ipv6.md @@ -0,0 +1,35 @@ +# IPv6支持 + +目前ipv6普及度很高,很多家庭宽带都支持ipv6,且具有公网ipv6地址,本教程将介绍如何在astrbot中充分利用ipv6。 + +# 准备 + +如果你是服务器环境,可以直接跳过以下内容,因为无需过多配置即可通过指定host,从而通过公网ipv6访问astrbot服务 + +如果你是家庭宽带环境,处于安全考虑,从外部无法直接访问,需按照以下步骤修改 +这里以中国电信天翼宽带为例 + +进入光猫后台面板 +你可以试试192.168.1.1 + +如图所示: +![image](https://files.astrbot.app/docs/source/images/ipv6/index.png) +这里超级管理员密码是随机生成的,需要用到一点社会工程学手段搞到这个超级密码 +当然你也可以用漏洞搞到 +如果你可以联系到当时给你家安装宽带的师傅,给他打个电话就可以要到 + +进入后菜单如下 +![image](https://files.astrbot.app/docs/source/images/ipv6/index.png) + +依此点击:安全-防火墙 +![image](https://files.astrbot.app/docs/source/images/ipv6/firewall.png) +将防火墙等级设置为低 +同时将启用IPV6 SESSEION关闭(此选项开启后将无法从外部访问) + +# 启动服务 +```bash +# 新版本默认0.0.0.0改成了::,默认启用了双栈支持,如果使用的旧版,需要手动修改配置文件,将host修改为[::] +astrbot run +# 不出意外,你可以在输出里面看到24开头,一长串的ipv6链接 +# http://[ipv6地址]:6185 +``` diff --git a/docs/snapshots/v4.23.6/zh/others/self-host-t2i.md b/docs/snapshots/v4.23.6/zh/others/self-host-t2i.md new file mode 100644 index 0000000..cb18acf --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/others/self-host-t2i.md @@ -0,0 +1,27 @@ +# 自行部署文转图服务 + +AstrBot 使用 [AstrBotDevs/astrbot-t2i-service](https://github.com/AstrBotDevs/astrbot-t2i-service) 项目作为默认的文本转图像服务。默认使用的文转图服务接口是 + +```plain +https://t2i.soulter.top/text2img +https://t2i.rcfortress.site/text2img +``` + +此接口能够保障大部分时间正常响应。但是由于部署在国外的(纽约)服务器,因此响应速度可能会比较慢。 + +> [!TIP] +> 欢迎通过 [爱发电](https://afdian.com/a/astrbot_team) 支持我们,以帮助我们支付服务器费用。 + +您可以选择自行部署文转图服务,以提升响应速度。 + +```bash +docker run -itd -p 8999:8999 soulter/astrbot-t2i-service:latest +``` + +在部署完成后,前往 AstrBot 仪表盘 -> 配置文件 -> 系统,修改 `文本转图像服务 API 地址` 为你部署好的 url(如下图所示) + +>如果你是使用本文档的 Docker教程 部署的 AstrBot ,url应为 `http://文转图服务容器名:8999`。 + +>如果部署在与 AstrBot 相同的机器上,url 应为 `http://localhost:8999`。 + +image diff --git a/docs/snapshots/v4.23.6/zh/platform/aiocqhttp.md b/docs/snapshots/v4.23.6/zh/platform/aiocqhttp.md new file mode 100644 index 0000000..0d20337 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/aiocqhttp.md @@ -0,0 +1,83 @@ +# 接入 OneBot v11 协议实现 + +OneBot 是一个**聊天机器人应用接口标准**,旨在统一不同聊天平台上的机器人应用开发接口。 + +AstrBot 支持接入所有适配了 OneBotv11 反向 Websockets(AstrBot 做服务器端)的机器人协议端。 + +下文给出一些常见的 OneBot v11 协议实现端项目。 + +- [NapCat](https://github.com/NapNeko/NapCatQQ) (连接到 QQ) +- [OneDisc](https://github.com/ITCraftDevelopmentTeam/OneDisc) (连接到 Discord) +- [Tele-KiraLink](https://github.com/Echomirix/Tele-KiraLink) (连接到 Telegram) + +请参阅对应的协议实现端项目的部署文档。 + +对于 Napcat 项目,请参考下文的 `附录:部署 Napcat` + +## 1. 配置 OneBot v11 + +1. 进入 AstrBot 的 WebUI +2. 点击左边栏 `机器人` +3. 然后在右边的界面中,点击 `+ 创建机器人` +4. 选择 `OneBot v11` + +在出现的表单中,填写: + +- ID(id):随意填写,仅用于区分不同的消息平台实例。 +- 启用(enable): 勾选。 +- 反向 WebSocket 主机地址:请填写你的机器的 IP 地址,一般情况下请直接填写 `0.0.0.0` +- 反向 WebSocket 端口:填写一个端口,默认为 `6199`。 +- 反向 Websocket Token:只有当 NapCat 网络配置中配置了 token 才需填写。 + +点击 `保存`。 + +## 2. 配置协议实现端 + +请参阅对应的协议实现端项目的部署文档。 + +一些注意点: + +1. 协议实现端需要支持 `反向 WebSocket` 实现,及 AstrBot 端作为服务端,实现端作为客户端。 +2. `反向 WebSocket` 的 URL 为 `ws(s)://:6199/ws`。 + +## 3. 验证 + +前往 AstrBot WebUI `控制台`,如果出现 ` aiocqhttp(OneBot v11) 适配器已连接。` 蓝色的日志,说明连接成功。如果没有,若干秒后出现` aiocqhttp 适配器已被关闭` 则为连接超时(失败),请检查配置是否正确。 + +## 附录:部署 Napcat + +### 通过一键启动脚本部署 + +推荐采用这种方式部署。 + +#### Windows + +看这篇文章:[NapCat.Shell - Win手动启动教程](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 + +看这篇文章:[NapCat.Installer - Linux一键使用脚本(支持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] +> **Napcat WebUI 在哪打开**: +> 在 napcat 的日志里会显示 WebUI 链接。 +> +> 如果是 linux 命令行一键部署的napcat:`docker log <账号>`。 +> +> Docker部署的 NapCat:`docker logs napcat`。 + +## 通过 Docker Compose 部署 + +1. 下载或复制 [astrbot.yml](https://github.com/NapNeko/NapCat-Docker/blob/main/compose/astrbot.yml) 内容 +2. 将刚刚下载的文件重命名为 `astrbot.yml` +3. 编辑 `astrbot.yml`,将 `# - "6199:6199"` 修改为 `- "6199:6199"`,移除开头的 `#` +4. 在 `astrbot.yml` 文件所在目录执行: + +```bash +NAPCAT_UID=$(id -u) NAPCAT_GID=$(id -g) docker compose -f ./astrbot.yml up -d +``` + +部署完毕之后,可以去 Napcat 的 WebUI(默认端口 6099)中新增 OneBot 连接实例:点击`网络配置->新建->WebSockets客户端`,在新弹出的窗口中:勾选`启用`, +URL 填写 `ws://宿主机IP:端口/ws`。如 `ws://127.0.0.1:6199/ws`。如果采用上面的 Docker Compose 部署,可以填写 `ws://astrbot:6199/ws`(参考本文档的 Docker 脚本)。心跳间隔和重连间隔可以改为 `1000`(1 秒)。点击保存,然后去 AstrBot WebUI 的控制台中检查是否连接成功,出现 `aiocqhttp(OneBot v11) 适配器已连接` 日志即代表成功。 + +如果您对部署、网络配置不了解,请千万不要在公网暴露 Napcat 的端口。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/zh/platform/dingtalk.md b/docs/snapshots/v4.23.6/zh/platform/dingtalk.md new file mode 100644 index 0000000..dcc1410 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/dingtalk.md @@ -0,0 +1,65 @@ +# 接入钉钉 DingTalk + +## 支持的基本消息类型 + +> 版本 v4.15.0。 + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| --- | --- | --- | --- | +| 文本 | 是 | 是 | | +| 图片 | 是 | 是 | | +| 语音 | 否 | 是 | | +| 视频 | 否 | 是 | | +| 文件 | 否 | 是 | | + +主动消息推送:支持。 + +## 创建和配置应用 + +前往 [钉钉开放平台](https://open-dev.dingtalk.com/fe/app),点击创建应用: + +![image](https://files.astrbot.app/docs/source/images/dingtalk/image-4.png) + +创建好之后,添加应用能力,选择机器人: + +![image](https://files.astrbot.app/docs/source/images/dingtalk/image-5.png) + +点击机器人配置,填写填写机器人相关信息: + +![image](https://files.astrbot.app/docs/source/images/dingtalk/image-7.png) + +确认无误后,点击下面的发布按钮。 + +点击凭证与基础信息,将 `ClientID` 和 `ClientSecret` 复制下来。 + +## 开始连接 + +打开 AstrBot 管理面板 -> `机器人` -> `+ 创建机器人`,创建一个钉钉适配器。 + +将刚刚复制的 `ClientID` 和 `ClientSecret` 填入,点击保存,AstrBot 将会自动向钉钉开放平台请求。 + +回到钉钉开放平台,点击事件订阅,选择 `Stream 模式推送`,点击保存,如果没有意外情况,将会看到 连接接入成功 字样。 + +![image](https://files.astrbot.app/docs/source/images/dingtalk/image-8.png) + +点击保存即可。 + +## 发布版本 + +点击边栏的 版本管理与发布,创建一个新版本。 + +填写应用版本号、版本描述、应用可见范围(选择全部员工或者按照您的需求),点击保存,确认发布。 + +![alt text](https://files.astrbot.app/docs/source/images/dingtalk/image-11.png) + +找到一个钉钉群聊,点击右上角的设置: + +![image](https://files.astrbot.app/docs/source/images/dingtalk/image-12.png) + +下拉找到添加机器人,然后找到刚刚创建的机器人,点击添加即可: + +![image](https://files.astrbot.app/docs/source/images/dingtalk/image-9.png) + +## 🎉 大功告成 + +在群聊中 @ 机器人后附带 `/help` 指令,如果机器人回复了,那么说明接入成功。 diff --git a/docs/snapshots/v4.23.6/zh/platform/discord.md b/docs/snapshots/v4.23.6/zh/platform/discord.md new file mode 100644 index 0000000..8e50fc2 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/discord.md @@ -0,0 +1,72 @@ +# 接入 Discord + +## 创建 AstrBot Discord 平台适配器 + +进入机器人,点击新增适配器,找到 Discord 并点击进入 Discord 配置页。 +> 旧版本`机器人`为`消息平台` +![点击创建机器人,选择discord类型](https://files.astrbot.app/docs/source/images/discord/image.png) + +![选项从上到下依次是 1.机器人名称 2. 启用 3. Bot token 4. Discord 代理地址 5. 是否自动将插件指令注册为 Discord 斜杠指令 6. discord_guild_id_for_debug 7.Discord 活动名称](https://files.astrbot.app/docs/source/images/discord/image-3.png) +> 本次教程只用管1,2,3,5项 + +- 机器人名称:自定义,方便区分不同适配器 +- 启用:勾选后启用该适配器 +- Bot Token:在 Discord 创建 App 后获取的 Token(见下文) +- Discord 代理地址:如果你需要使用代理访问 Discord,可以在这里填写代理地址(可选) +- 是否自动将插件指令注册为 Discord 斜杠指令:勾选后,AstrBot 会自动将已安装插件中的指令注册为 Discord 斜杠指令,方便用户使用。 + +## 在 Discord 创建 App + +1. 前往 [Discord](https://discord.com/developers/applications),点击右上角蓝色按钮,输入应用名字,创建应用。 + +![创建bot(输入名字)](https://files.astrbot.app/docs/source/images/discord/image-1.png) + +2. 点击左边栏的 Bot,点击 Reset Token 按钮,创建好 Token 后,点击 Copy 按钮,将 Token 填入配置中的 Discord Bot Token 处。 + +![token选项](https://files.astrbot.app/docs/source/images/discord/image-4.png) +4. 下滑找到这三个选项全开启 + +![Presence Intent,Server Members Intent,Message Content Intent截图](https://files.astrbot.app/docs/source/images/discord/image-2.png) + +- Presence Intent:允许机器人获取用户在线状态 +- Server Members Intent:允许机器人获取服务器成员信息 +- Message Content Intent:允许机器人读取消息内容 + +5. 点击左边栏的 OAuth2,在 OAuth2 URL Generator 中选中 `Bot` +也就是这样 +![OAuth2 URL Generator](https://files.astrbot.app/docs/source/images/discord/image-6.png) +然后在下方出现的 Bot 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 +如果你觉得麻烦也可以直接使用administrator权限,但仍然建议在使用环境中使用上文的配置权限(或您自己需要的权限) +> 记住,权限越高,风险越大。 + +6. 复制下方出现的 Generated URL。打开这个 URL,将 Bot 添加到所需要的服务器。 +![Generated URL位置](https://files.astrbot.app/docs/source/images/discord/image-5.png) + +7. 进入 Discord 服务器,你的机器人应该已经提示在线了 +![机器人在线](https://files.astrbot.app/docs/source/images/discord/image-7.png) +@ 刚刚创建的机器人(也可以不 @),输入 `/help`,如果成功返回,则测试成功。 + +## 预回应表情 + +Discord 支持预回应表情功能。启用后,机器人在处理消息时会先添加一个表情反应,让用户知道机器人正在处理消息。 + +在管理面板的「配置」页面中,找到 `平台特定配置 -> Discord -> 预回应表情`: + +- **启用预回应表情**:开启后,机器人收到消息时会自动添加表情反应 +- **表情列表**:填写 Unicode 表情符号,例如:👍、🤔、⏳。可填写多个,机器人会随机选择一个使用 + +# 故障排除 + +- 如果卡在最后的步骤,机器人不在线请确定自己的服务器可以直接连接discord + +如果有疑问,请[提交 Issue](https://github.com/AstrBotDevs/AstrBot/issues)。 diff --git a/docs/snapshots/v4.23.6/zh/platform/kook.md b/docs/snapshots/v4.23.6/zh/platform/kook.md new file mode 100644 index 0000000..b585ab8 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/kook.md @@ -0,0 +1,48 @@ +# 接入 Kook + +## 支持的基本消息类型 + +> 版本 v4.19.2 + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| ------------ | ------------ | ------------ | ---------------------------------------------- | +| 文本 | 是 | 是 | 支持官方[kmarkdown]语法 | +| 图片 | 是 | 是 | 支持外链,图片类型仅支持`jpeg`, `gif`, `png` | +| 语音 | 是 | 是 | 支持外链 | +| 视频 | 是 | 是 | 支持外链,视频仅支持`mp4`,`mov` | +| 文件 | 是 | 是 | 支持外链 | +| 卡片(JSON) | 是 | 是 | 参见[Kook文档-卡片消息] | + +主动消息推送:支持 + +消息接收模式:WebSocket + +## 在 Kook 创建机器人 + +1. 点击跳转 [Kook 开发者平台] ,完成以下步骤: +2. 登录账号并完成实名认证; +3. 点击「新建应用」,自定义 Bot 昵称; +4. 进入应用后台,选择「机器人」模块,开启 **WebSocket 连接模式**,注意保存生成的 **Token**,后续配置Astrbot需要使用; +5. 在左边栏「机器人」页面下点击「邀请链接」,设置角色权限(建议赋予全权限,确保功能完整)。 +6. 设置好角色权限后,点击上方邀请链接的复制按钮复制链接,在浏览器中打开复制出来的邀请链接,将机器人加入到所需的服务器。 + + ![image](https://files.astrbot.app/docs/source/images/kook/image-1.png) + +## 在 AstrBot 配置 + +1. 进入 AstrBot 的管理面板 +2. 点击左边栏 `机器人` +3. 然后在右边的界面中,点击 `+ 创建机器人` +4. 选择 `kook` 适配器 +5. 弹出的配置项填写: + + - ID(id):随意填写,用于区分不同的消息平台实例。 + - 启用(enable): 勾选。 + - 机器人 Token: 填写在 [Kook 开发者平台] 中创建机器人时生成的 Token。 + +6. 完成适配器配置填写后,点击 `保存`。 +7. 最后,在kook服务器频道(若没有属于自己的服务器频道,请先创建一个服务器频道)中,@ 刚刚创建的机器人,输入 `/sid`,如果机器人成功回复,则测试成功。 + +[Kook 开发者平台]: https://developer.kookapp.cn/app +[kmarkdown]: https://developer.kookapp.cn/doc/kmarkdown +[Kook文档-卡片消息]: https://developer.kookapp.cn/doc/cardmessage diff --git a/docs/snapshots/v4.23.6/zh/platform/lark.md b/docs/snapshots/v4.23.6/zh/platform/lark.md new file mode 100644 index 0000000..206e654 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/lark.md @@ -0,0 +1,123 @@ +# 接入飞书 + +## 支持的基本消息类型 + +> 版本 v4.15.0。 + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| --- | --- | --- | --- | +| 文本 | 是 | 是 | | +| 图片 | 是 | 是 | | +| 语音 | 否 | 是 | | +| 视频 | 否 | 是 | | +| 文件 | 否 | 是 | | + +主动消息推送:支持。 + +流式输出:支持。需要在飞书开发者后台为应用开通 `创建与更新卡片(cardkit:card:write)` 权限。 + +飞书客户端版本需 >= 7.20。低版本客户端将只显示标题和升级提示。 + +## 创建机器人 + +前往 [开发者后台](https://open.feishu.cn/app) ,创建企业自建应用。 + +![创建企业自建应用](https://files.astrbot.app/docs/source/images/lark/image.png) + +添加应用能力——机器人。 + +![添加应用能力](https://files.astrbot.app/docs/source/images/lark/image-1.png) + +点击凭证与基础信息,获取 app_id 和 app_secret。 + +![获取 app_id 和 app_secret](https://files.astrbot.app/docs/source/images/lark/image-4.png) + +## 配置 AstrBot + +1. 进入 AstrBot 的管理面板 +2. 点击左边栏 `机器人` +3. 然后在右边的界面中,点击 `+ 创建机器人` +4. 选择 `lark(飞书)` + +弹出的配置项填写: + +- ID(id):随意填写,用于区分不同的消息平台实例。 +- 启用(enable): 勾选。 +- app_id: 获取的 app_id +- app_secret: 获取的 app_secret +- 飞书机器人的名字 + +对于 domain,如果您使用国内版飞书,保持默认即可;如果您正在用国际版飞书,请设置为 `https://open.larksuite.com`;如果您使用企业自部署飞书,请填写您的飞书实例的域名。 + +对于订阅方式,`socket` 代表使用「长连接」订阅方式,`webhook` 代表「将事件发送至开发者服务器」的订阅方式,后者需要您拥有公网服务器。一般来说使用 `socket` 即可,如果您使用国际版飞书或者企业自部署飞书,请选择 `webhook`。相应地,接下来的配置也会有所不同。 + +如果您选择了 `webhook` 方式,选择了之后,前往飞书的开发者后台,点击事件与回调,点击加密策略,填写 Encrypt Key。这不是必须的,AstrBot 十分注重你的数据安全,所以请务必填写。填写后复制 `Encrypt Key` 和 `Verification Token` 到 AstrBot 配置的 `encrypt_key` 和 `verification_token` 处。 + +点击 `保存`。 + +## 设置回调和权限 + +对于上面选择的订阅方式,接下来的步骤有所不同,请你根据实际选择的方式,跳转到对应的章节。 + +### `socket` 长连接方式 + +接下来,点击事件与回调,使用长连接接收事件,点击保存。**如果上一步没有成功启动,那么这里将无法保存。** + +![设置事件与回调](https://files.astrbot.app/docs/source/images/lark/image-6.png) + +### `webhook` 将事件发送至开发者服务器方式 + +> [!TIP] +> 为了更好地使用这种方式,请先参考 [统一 Webhook 模式](/zh/use/unified-webhook.md#如何使用统一-webhook-模式) 做好相关配置。 + +在点击 `保存` 后,机器人卡片会显示「查看 Webhook 链接」,点击查看,复制回调 URL。 + +![](https://files.astrbot.app/docs/source/images/lark/webhook.png) + +接下来,回到飞书的事件与回调页,点击「事件配置」,选择「将事件发送至开发者服务器」,将“请求地址”填写为刚刚复制的回调 URL,点击保存。如果一切无误将不会报错。 + +### 设置事件 + +上一步事件配置完成后,点击添加事件,消息与群组,下拉找到 `接收消息`,添加。 + +![添加事件](https://files.astrbot.app/docs/source/images/lark/image-7.png) + +点击开通以下权限。 + +![开通权限](https://files.astrbot.app/docs/source/images/lark/image-8.png) + +再点击上面的`保存`按钮。 + +接下来,点击权限管理,点击开通权限,输入 `im:message,im:message:send_as_bot`。添加筛选到的权限。 + +再次输入 `im:resource:upload,im:resource` 开通上传图片相关的权限。 + +如果需要在群聊里使用,请额外开通 `im:message.group_at_msg:readonly` 和 `im:message.group_msg` 权限。 + +如果需要使用流式输出,请额外开通 `创建与更新卡片(cardkit:card:write)` 权限。 + +最终开通的权限如下图: + +![最终开通的权限](https://files.astrbot.app/docs/source/images/lark/image-11.png) + +## 创建版本 + +创建版本。 + +![创建版本](https://files.astrbot.app/docs/source/images/lark/image-2.png) + +填写版本号,更新说明,可见范围后点击保存,确认发布。 + +## 拉入机器人到群组 + +进入飞书 APP(网页版飞书无法添加机器人),点进群聊,点击右上角按钮->群机器人->添加机器人。 + +搜索刚刚创建的机器人的名字。比如教程创建了 `AstrBot` 机器人: + +![添加机器人](https://files.astrbot.app/docs/source/images/lark/image-9.png) + +## 🎉 大功告成 + +在群内发送一个 `/help` 指令,机器人将做出响应。 + +![成功](https://files.astrbot.app/docs/source/images/lark/image-13.png) diff --git a/docs/snapshots/v4.23.6/zh/platform/line.md b/docs/snapshots/v4.23.6/zh/platform/line.md new file mode 100644 index 0000000..32b7ded --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/line.md @@ -0,0 +1,78 @@ +# 接入 LINE + +## 支持的基本消息类型 + +> 版本 v4.17.0。 + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| --- | --- | --- | --- | +| 文本 | 是 | 是 | | +| 图片 | 是 | 是 | | +| 语音 | 是 | 是 | | +| 视频 | 是 | 是 | | +| 文件 | 是 | 是 | | +| 贴纸 | 是 | 否 | | + +主动消息推送:支持。 + +## 创建 LINE Messaging API Channel + +1. 打开 [LINE Developers Console](https://developers.line.biz/console/) +2. 创建或选择一个 Provider +3. 创建一个 `Messaging API` Channel (不是 `LINE Login` Channel) +4. 在 `Messaging API` 页面中,完成机器人初始化 + +## 获取凭据 + +你需要以下配置项: + +- `channel_secret` +- `channel_access_token` + +获取方式: + +1. 进入对应 Channel 的设置页面 +2. 在 `Basic settings` 获取 `Channel secret` +3. 在 `Messaging API` 页面签发 `Channel access token` + +![](https://files.astrbot.app/docs/source/images/line/7ecee0a9102f191245330f8408eb0493.png) + +## 配置 AstrBot + +1. 进入 AstrBot 管理面板 +2. 点击左侧 `机器人` +3. 点击 `+ 创建机器人` +4. 选择 `line` + +填写配置: + +- `ID(id)`:自定义,区分多个平台实例 +- `启用(enable)`:勾选 +- `LINE Channel Access Token`:填入 `channel_access_token` +- `LINE Channel Secret`:填入 `channel_secret` + +点击保存。 + +## 配置回调地址(统一 Webhook) + +LINE 适配器仅支持 AstrBot 统一 Webhook 模式。 + +保存后,在机器人卡片里点击「查看 Webhook 链接」,复制 URL。 + +然后到 LINE Developers Console: + +1. 打开 `Messaging API` 页面 +2. 在 `Webhook settings` 中粘贴 `Webhook URL` +3. 点击 `Verify` +4. 打开 `Use webhook` + +> [!TIP] +> 如果你的 AstrBot 不在公网,请先配置好可公网访问的域名与反向代理,确保 LINE 可以访问该 Webhook URL。 + +## 测试 + +1. 用 LINE 添加该官方账号为好友(通过二维码即可添加) +2. 给机器人发送一条消息(例如 `hi`) +3. 若能收到回复,即接入成功 + +如果要在群内使用,请先将该官方账号拉入群组后再测试。 diff --git a/docs/snapshots/v4.23.6/zh/platform/matrix.md b/docs/snapshots/v4.23.6/zh/platform/matrix.md new file mode 100644 index 0000000..5a1ae0b --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/matrix.md @@ -0,0 +1,45 @@ +# 接入 Matrix + +> [!TIP] +> 该平台适配器由社区([stevessr](https://github.com/stevessr)) 维护。如果您觉得有帮助,请支持开发者,给该仓库点一个 Star。❤️ + +## 部署 Matrix 服务器 + +Matrix 是一个 IM 协议,有着丰富的服务端实现。 + +请在 [Matrix Server](https://matrix.org/ecosystem/servers/)查看可用的服务端。 + + + +## 支持的基本消息类型 + + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| ------------ | ------------ | ------------ | ---------------------------------------------- | +| 文本 | 是 | 是 | | +| 图片* | 是 | 是 | | +| 语音* | 是 | 是 | | +| 视频* | 是 | 是 | | +| 文件* | 是 | 是 | | +| 投票 | 是 | 否 | | +*: 会持久化到本地,插件会按配置清理,在发送前会进行上传操作,超过服务器允许大小的上传将会失败 + +## 安装 astrbot_plugin_matrix_adapter 插件 + +进入 AstrBot WebUI 的插件市场,搜索 `astrbot_plugin_matrix_adapter`,点击安装。 + +安装完成后,前往 机器人(旧版本为 `消息平台`) → 新增适配器 → 选择 Matrix(若选项缺失,尝试重启 AstrBot 或检查插件安装状态)。 + +在弹出的配置对话框中点击 `启用`。 + +## 配置 + +- **`matrix_homeserver` (必填)`**: 你的 matrix 服务器实例的完整URL地址,支持域名委托自动探测。例如官方实例`https://matrix.org` +- **`matrix_user_id`**: 你的 matrix 完整用户名。如 `@username:homeserver.com` +- **`matrix_auth_method` (必填)** : 你的登陆方式,可选`password`,`token`,`oauth2`,`qr`推荐使用`password`或`oauth2/qr`模式 (oauth2/qr 模式下请确保用于认证/扫码的设备回调可以访问到 astrbot 配置的公开地址) + +更多请参考该仓库的 [README.md](https://github.com/stevessr/astrbot_plugin_matrix_adapter?tab=readme-ov-file#astrbot-matrix-adapter-%E6%8F%92%E4%BB%B6) 进行配置。 + +## 问题提交 + +如有疑问,请提交 issue 至[插件仓库](https://github.com/stevessr/astrbot_plugin_matrix_adapter/issues)。 diff --git a/docs/snapshots/v4.23.6/zh/platform/mattermost.md b/docs/snapshots/v4.23.6/zh/platform/mattermost.md new file mode 100644 index 0000000..be67494 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/mattermost.md @@ -0,0 +1,139 @@ +# 接入 Mattermost + +Mattermost 适配器通过 Bot Token 和 WebSocket 连接到 Mattermost 服务器。完成下面两部分配置后,AstrBot 就可以在 Mattermost 频道和私聊中收发消息。 + +## 创建 AstrBot Mattermost 平台适配器 + +进入 `机器人` 页面,点击 `+ 创建机器人`,选择 `Mattermost`。 + +在配置页中先打开 `启用`,然后填写以下字段: + +- `Mattermost URL`:你的 Mattermost 服务地址,例如 `https://chat.example.com` +- `Mattermost Bot Token`:在 Mattermost 中创建 Bot 账户后生成的访问令牌 +- `Mattermost 重连延迟`:WebSocket 断开后的重连等待时间,默认 `5` + +填写完成后点击保存。 + +## 部署 Mattermost + +如果你还没有 Mattermost 服务,建议直接使用 Mattermost 官方提供的 Docker Compose 仓库: + +- 官方文档:https://docs.mattermost.com/deployment-guide/server/containers/install-docker.html +- 官方仓库:https://github.com/mattermost/docker + +官方当前推荐的快速部署步骤如下: + +```bash +git clone https://github.com/mattermost/docker +cd docker +cp env.example .env +``` + +然后至少修改 `.env` 中的: + +- `DOMAIN` +- `MATTERMOST_IMAGE_TAG` +- 建议补充 `MM_SUPPORTSETTINGS_SUPPORTEMAIL` + +接着创建数据目录并设置权限: + +```bash +mkdir -p ./volumes/app/mattermost/{config,data,logs,plugins,client/plugins,bleve-indexes} +sudo chown -R 2000:2000 ./volumes/app/mattermost +``` + +启动方式二选一: + +不使用内置 NGINX: + +```bash +docker compose -f docker-compose.yml -f docker-compose.without-nginx.yml up -d +``` + +使用内置 NGINX: + +```bash +docker compose -f docker-compose.yml -f docker-compose.nginx.yml up -d +``` + +访问地址: + +- 不使用 NGINX:`http://你的域名:8065` +- 使用 NGINX:`https://你的域名` + +> [!TIP] +> Mattermost 官方当前说明中,Docker 生产支持仅限 Linux。macOS 和 Windows 更适合开发或测试用途。 + +## 在 Mattermost 中创建 Bot + +### 1. 开启 Bot 账户创建 + +进入 Mattermost 的系统控制台: + +`System Console > Integrations > Bot Accounts` + +开启 `Enable Bot Account Creation`。 + +### 2. 创建 Bot 账户 + +进入: + +`Product menu(左上角的图标) > Integrations > Bot Accounts` + +点击 `Add Bot Account`,填写: + +- `Username` +- `Display Name` +- `Description` + +创建完成后复制生成的 Bot Token。这个 Token 只会展示一次,随后填写到 AstrBot 的 `Mattermost Bot Token` 中。 + +### 3. 将 Bot 加入频道 + +把刚创建的 Bot 添加到你准备让 AstrBot 工作的频道中,否则机器人无法在该频道正常收发消息。 + +## Mattermost URL 如何填写 + +`Mattermost URL` 填 Mattermost 的外部访问地址,不要带结尾斜杠。例如: + +```text +https://chat.example.com +``` + +如果你当前只是在本机测试,也可以填写: + +```text +http://127.0.0.1:8065 +``` + +如果 AstrBot 和 Mattermost 都在 Docker 中运行,请优先填写 AstrBot 容器可访问到的地址,例如同一 Docker 网络中的服务名地址。 + +## 启动并验证 + +保存 AstrBot 平台适配器配置后: + +1. 确保 AstrBot 日志中没有出现 Mattermost 认证失败或 WebSocket 连接失败。 +2. 在 Mattermost 中向 Bot 所在频道发送消息,或直接给 Bot 发私聊。 +3. 如果 AstrBot 正常回复,说明接入成功。 + +## 常见问题 + +### 提示 Token 无效 + +通常是以下原因: + +- 复制的不是 Bot Token +- Token 复制时带了空格 +- Bot 账户被删除或重新生成了 Token + +### 连接成功但收不到频道消息 + +优先检查: + +- Bot 是否已经加入目标频道 +- Mattermost URL 是否填写为 AstrBot 实际可访问的地址 +- Mattermost 反向代理是否正确转发了 WebSocket 请求 + +### 本机部署能打开页面,但 AstrBot 连接不到 + +如果 AstrBot 运行在容器里,而 Mattermost URL 填的是 `localhost` 或 `127.0.0.1`,那么 AstrBot 实际连接到的是它自己的容器,而不是 Mattermost。此时应改为 Docker 网络内可访问的地址。 diff --git a/docs/snapshots/v4.23.6/zh/platform/misskey.md b/docs/snapshots/v4.23.6/zh/platform/misskey.md new file mode 100644 index 0000000..955c434 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/misskey.md @@ -0,0 +1,112 @@ +# 接入 Misskey 平台 + +> [!WARNING] +> 1. 我们建议您在非您参与管理的 Misskey 实例上部署 Bot 前请先查看实例规则或征求实例管理组或检察组的同意,并在部署后为机器人账号开启`Bot`标识。 +> 2. 本项目严禁用于任何违反法律法规的用途。若您意图将 AstrBot 应用于非法产业或活动,我们明确反对并拒绝您使用本项目。 + +## 创建 AstrBot Misskey 平台适配器 + +进入消息平台,点击新增适配器,找到 Misskey 并单击进入 Misskey 配置页。 + +![创建 Misskey 平台适配器](https://files.astrbot.app/docs/source/images/misskey/create.png) + +## 配置平台适配器设置 + +在 AstrBot Misskey 的平台适配器配置页,我们需要填写 Misskey 的接入信息和配置适配器的部分行为。 + +::: tip 注意 +别忘了退出保存前先点击`启用`以启用 Misskey 平台配置器! +::: + +获取 Misskey 接入信息的方式见下文介绍。 + +![Misskey 平台适配器配置](https://files.astrbot.app/docs/source/images/misskey/config.png) + +## Misskey 实例 URL + +就是你的 Bot 所处账号的 Misskey 实例前端地址,格式为标准域名。例如`https://misskey.example`。 + +## 获取 Bot 账号 Access Token + +1. 首先打开 Misskey Web 前端页面,在前端页面侧边栏找到并打开`设置 > 连接服务`页面。 + +![打开 Misskey 连接服务页面](https://files.astrbot.app/docs/source/images/misskey/pat-1.png) + +2. 单击“生成访问令牌”以生成账号接入访问令牌。 + +![生成 Misskey 账号令牌](https://files.astrbot.app/docs/source/images/misskey/pat-2.png) + +3. 在弹出的访问令牌配置页面,我们为令牌起一个名字,比如`AstrBot`。 + +4. 然后我们需要为令牌配置相关权限让 Bot 能够与 Misskey 实例交互。 + +::: tip 注意 +如果你使用的 AstrBot 第三方插件需要额外权限,请参考其文档增加相应权限。若你完全信任 Bot 的部署环境,也可以临时开启全部权限以简化调试,但仍建议您在生产环境使用时限制 Bot 的相关权限。 +::: + +![配置访问令牌权限](https://files.astrbot.app/docs/source/images/misskey/pat-3.png) + +**默认需要开启的权限** + +| 权限名称 | 说明 | 用途 | +|---|---:|---| +| 读取账户信息 | 查看账户的基本信息 | 获取 Bot 自身的用户信息和账号 ID | +| 撰写或删除帖子 | 创建、编辑和删除笔记内容 | 发送消息回复和发布内容 | +| 撰写或删除消息 | 创建、编辑和删除私信内容 | 处理私信对话 | +| 查看通知 | 接收系统通知和提醒 | 获取提及、回复等通知信息 | +| 查看消息 | 读取私信和聊天记录 | 接收和处理用户私信 | +| 查看回应 | 查看帖子的回复和反应 | 处理用户对 Bot 消息的回应 | + +5. 权限配置完成后,单击“完成”以查看账号访问令牌。把获取到的令牌复制并粘贴到 AstrBot 配置页面 Access Token 输入框内。 + +![查看账号令牌](https://files.astrbot.app/docs/source/images/misskey/pat-4.png) + +## 默认帖文可见性 + +修改机器人发帖时的默认可见性 + +| 名称 | 说明 | +|---|---| +| public | 任何人都可以看到 Bot 的帖文 | +| home | 公开 Bot 帖文于实例主页时间线 | +| followers | 只有关注了 Bot 账号的用户才能在主页时间线看到 Bot 帖文 | + +## 仅限本站(不参与联合) + +开启后,Bot 发送的所有帖文都不会参与 Fediverse 联合,非常适用于仅想在自己实例使用和传播 Bot 的帖子的需求。 + +## 启用聊天信息响应 + +::: tip 注意 +Misskey 的“聊天”组件特性并不受所有 Misskey Fork 版本支持!无法跨实例互联。 + +Misskey 在`v2025.4.0`及以后的版本中为加入“聊天”组件支持,且仅受其 Web 前端支持,并未受到第三方 App 良好的支持。 +::: + +默认开启,开启后 Bot 会响应 Misskey 聊天内用户发送的私聊内容并进行回复。 + +## 历史记录 + +聊天和贴文单个用户的对话历史在 AstrBot 的 WebUI 控制台“对话历史”会以`chat:UserID`的 id 记录,传统贴文则是以`note:UserID`的 id 记录。 + +::: tip Misskey 用户的 UserID 在哪里? +位于用户个人页面部分的`Raw`页面内可以查询,UserID 是单个实例中 Misskey 用户唯一的关键身份标识。 +::: + +![UserID](https://files.astrbot.app/docs/source/images/misskey/userid.png) + +## 测试成功性 + +配置完成并启用后,前往 Misskey 新建帖文并在发送中引用 Bot (@mention)测试效果。如果 Bot 账号能够成功触发回复,说明配置成功。 + +![效果示例](https://files.astrbot.app/docs/source/images/misskey/demo.png) + +## 杂谈 + +我们建议您为 Bot 账号开启 Misskey `Bot` 标识以尊重 Misskey 各实例的相关规定和速率限制等,也能有效帮助 Misskey 实例管理员管理和识别 Bot 的使用情况。 + +**开启方式** + +在 Bot 账号个人资料页面的高级设置中开启“这是一个机器人账号”即可。 + +![这是一个机器人账号](https://files.astrbot.app/docs/source/images/misskey/botset.png) diff --git a/docs/snapshots/v4.23.6/zh/platform/qqofficial.md b/docs/snapshots/v4.23.6/zh/platform/qqofficial.md new file mode 100644 index 0000000..c1ddbfa --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/qqofficial.md @@ -0,0 +1,8 @@ +# 接入 QQ 官方机器人平台 + +QQ 官方机器人平台是腾讯官方提供的一个机器人接入平台,允许开发者通过官方接口将机器人接入 QQ 群聊和个人聊天中。 + +目前主要通过 Webhook 方式接入。 + +- [Webhook 方式](/platform/qqofficial/webhook) +- [Websockets 方式](/platform/qqofficial/websockets) diff --git a/docs/snapshots/v4.23.6/zh/platform/qqofficial/webhook.md b/docs/snapshots/v4.23.6/zh/platform/qqofficial/webhook.md new file mode 100644 index 0000000..b7c5f4f --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/qqofficial/webhook.md @@ -0,0 +1,108 @@ + +# 通过 QQ官方机器人 接入 QQ (Webhook) + +> [!WARNING] +> +> 1. 截至目前,QQ 官方机器人需要设置 IP 白名单。 +> 2. 支持群聊、私聊、频道聊天、频道私聊。 +> +> **需要**一台带有公网 IP 的服务器和域名(如果没备案,需要服务器在海外或者中国港澳台地区) + +## 支持的基本消息类型 + +> 版本 v4.19.6。 + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| --- | --- | --- | --- | +| 文本 | 是 | 是 | | +| 图片 | 是 | 是 | | +| 语音 | 是 | 是 | | +| 视频 | 是 | 是 | | +| 文件 | 是 | 是 | | + +主动消息推送:支持。 + +## 申请一个机器人 + +首先,打开 [QQ官方机器人](https://q.qq.com) 并登录。 + +然后,点击创建机器人,填写名称、简介、头像等信息。然后点击下一步、提交审核。等待安全校验通过后,创建成功。 + +点击创建好的机器人,然后你将会被导航到机器人的管理页面。如下图所示: + +![image](https://files.astrbot.app/docs/source/images/qqofficial/image.png) + +## 允许机器人加入频道/群/私聊 + +点击`沙箱配置`,这允许你立即设置一个沙箱频道/QQ群/QQ私聊,用于拉入机器人(需要小于等于20个人)。 + +然后你将会看到 QQ 群配置、消息列表配置和 QQ 频道配置。根据你的需求来选择QQ群、允许私聊的QQ号、QQ频道。 + +![image](https://files.astrbot.app/docs/source/images/qqofficial/image-1.png) + +## 获取 appid、secret + +添加机器人到你想用的地方后。 + +点击 `开发->开发设置`,找到 appid、secret。复制并保存它们。 + +## 添加 IP 白名单 + +点击 `开发->开发设置`,找到 IP 白名单。添加你的服务器 IP 地址。 + +![image](https://files.astrbot.app/docs/source/images/qqofficial/image-3.png) + +## 在 AstrBot 配置 + +1. 进入 AstrBot 的管理面板 +2. 点击左边栏 `机器人` +3. 然后在右边的界面中,点击 `+ 创建机器人` +4. 选择 `qq_official_webhook` + +弹出的配置项填写: + +- ID(id):随意填写,用于区分不同的消息平台实例。 +- 启用(enable): 勾选。 +- appid: QQ 官方机器人中获取的 appid。 +- secret: QQ 官方机器人中获取的 secret。 +- 统一 Webhook 模式 (unified_webhook_mode): 保持开启。 + +点击 `保存`。 + +## 反向代理 + +保存之后,请根据你的服务器环境,配置域名 DNS 解析和反向代理,将请求转发到 AstrBot 所在服务器的 `6185` 端口 (如果没有开启统一 Webhook 模式,将请求转发到上一步配置指定的端口)。 + +## 设置回调地址 + +在 `开发->回调配置` 处,配置回调地址。 + +上一步点击保存之后,AstrBot 将会自动为你生成唯一的 Webhook 回调链接,你可以在日志中或者 WebUI 的机器人页的卡片上找到。 + +![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png) + +将请求地址填写为该地址。 + +> [!TIP] +> v4.8.0 之前没有 `统一 Webhook 模式`,则请求地址填写 `<你的域名>/astrbot-qo-webhook/callback`。 + +填写好之后,添加事件,四个事件类型都全选:单聊事件、群事件、频道事件等,如下图。 + +![image](https://files.astrbot.app/docs/source/images/webhook/image.png) + +输入完成后,将光标挪出输入框,将会发送一次验证请求。如果没问题,右边的确定配置按钮将可点击,点击即可。 + +接着重启 AstrBot。 + +## 🎉 大功告成 + +此时,你的 AstrBot 应该已经连接成功。如果发送消息没有反应,请等待一两分钟后重启 AstrBot 再进行确认(测试时发现回调地址不会立即生效)。 + +## 附录:如何配置反向代理 + +如果你还没有相关经验,这里推荐使用 Caddy 作为反向代理的工具,请参考: + +1. 安装 Caddy: +2. 设置反向代理: + +Caddy 将自动为您申请 TLS 证书,以达到接入 Webhook 的目的。 diff --git a/docs/snapshots/v4.23.6/zh/platform/qqofficial/websockets.md b/docs/snapshots/v4.23.6/zh/platform/qqofficial/websockets.md new file mode 100644 index 0000000..3913cdd --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/qqofficial/websockets.md @@ -0,0 +1,90 @@ + +# 通过 QQ官方机器人 接入 QQ (Websockets) + +## 支持的基本消息类型 + +> 版本 v4.19.6。 + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| --- | --- | --- | --- | +| 文本 | 是 | 是 | | +| 图片 | 是 | 是 | | +| 语音 | 是 | 是 | | +| 视频 | 是 | 是 | | +| 文件 | 是 | 是 | | + +主动消息推送:支持。 + +## 快速部署通道 + +> 更新自: `2026/03/06`。该方法仅支持 `私聊`。 + +1. 打开 [QQ 开放平台](https://q.qq.com/qqbot/openclaw/)。如果没注册,需要先注册。 +2. 点击右侧 `创建机器人` 按钮。 +3. 获取 `AppID` 和 `AppSecret`。 +4. 进入 AstrBot 的 WebUI,点击左边栏 `机器人`,然后在右边的界面中,点击 `+ 创建机器人`,选择 `QQ 官方机器人(WebSocket)`,将之前得到的的 `AppID` 和 `AppSecret` 复制到这里的表单中,然后 `启用`,然后点击保存。 +5. 回到 QQ 开放平台页面,点击机器人右边的 `扫码聊天`。用手机 QQ 扫码即可聊天。 + +如果要在群聊中使用,参考下面文档的 `允许机器人加入频道/群/私聊` 一节。 + +--- + +## 申请一个机器人 + +> [!WARNING] +> +> 1. 截至目前,QQ 官方机器人需要设置 IP 白名单。 +> 2. 支持群聊、私聊、频道聊天、频道私聊。 + +首先,打开 [QQ官方机器人](https://q.qq.com) 并登录。 + +然后,点击创建机器人,填写名称、简介、头像等信息。然后点击下一步、提交审核。等待安全校验通过后,创建成功。 + +点击创建好的机器人,然后你将会被导航到机器人的管理页面。如下图所示: + +![image](https://files.astrbot.app/docs/source/images/qqofficial/image.png) + +## 允许机器人加入频道/群/私聊 + +点击`沙箱配置`,这允许你立即设置一个沙箱频道/QQ群/QQ私聊,用于拉入机器人(需要小于等于20个人)。 + +然后你将会看到 QQ 群配置、消息列表配置和 QQ 频道配置。根据你的需求来选择QQ群、允许私聊的QQ号、QQ频道。 + +![image](https://files.astrbot.app/docs/source/images/qqofficial/image-1.png) + +## 获取 appid、secret + +添加机器人到你想用的地方后。 + +点击 `开发->开发设置`,找到 appid、secret。复制并保存它们。 + +## 添加 IP 白名单(可选) + +点击 `开发->开发设置`,找到 IP 白名单。添加你的服务器 IP 地址。 + +![image](https://files.astrbot.app/docs/source/images/qqofficial/image-3.png) + +> [!TIP] +> 如果你不知道你的服务器 IP 地址,可以在终端中输入 `curl ifconfig.me` 来获取。或者登录 [ip138.com](https://ip138.com/) 查看。 +> +> 如果你在没有公网 IP 的环境下,你看到的 IP 是运营商 NAT 的 IP,这个 IP 根据你的运营商的情况可能会随时变化。如有必要,可以配置代理。 + +## 在 AstrBot 配置 + +1. 进入 AstrBot 的管理面板 +2. 点击左边栏 `机器人` +3. 然后在右边的界面中,点击 `+ 创建机器人` +4. 选择 `QQ 官方机器人(WebSocket)` + +弹出的配置项填写: + +- ID(id):随意填写,用于区分不同的消息平台实例。 +- 启用(enable): 勾选。 +- appid: QQ 官方机器人中获取的 appid。 +- secret: QQ 官方机器人中获取的 secret。 + +点击 `保存`。 + +## 🎉 大功告成 + +此时,你的 AstrBot 应该已经成功连接 QQ 官方接口。使用 `私聊` 的方式在 QQ 对机器人发送 `/help` 以检查是否连接成功。 diff --git a/docs/snapshots/v4.23.6/zh/platform/satori/guide.md b/docs/snapshots/v4.23.6/zh/platform/satori/guide.md new file mode 100644 index 0000000..c60380d --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/satori/guide.md @@ -0,0 +1,32 @@ +# 接入 Satori 协议 + +## Satori 协议简介 + +> 摘录自:https://satori.chat/zh-CN/introduction.html + +Satori 是一个通用的聊天协议。Satori 协议希望能够抹平不同聊天平台之间的差异,让开发者以更低的成本开发出跨平台、可扩展、高性能的聊天应用。 + +Satori 的名称来源于游戏东方 Project 中的角色 [古明地觉 (Komeiji Satori)](https://zh.touhouwiki.net/wiki/%E5%8F%A4%E6%98%8E%E5%9C%B0%E8%A7%89)。古明地觉能够以心灵感应的方式与各种动物交流,取这个名字是希望 Satori 能够成为各个聊天平台之间的桥梁。 + +Satori 的开发团队长期从事聊天机器人开发,熟悉各种聊天平台的通信方式。经过长达 4 年的发展,Satori 有了健全的设计和完善的实现。目前,Satori 官方提供了超过 15 个聊天平台的适配器,完全覆盖了世界上主流的聊天平台,如 QQ、Discord、企业微信、KOOK 等等。 + +## 1. 配置协议实现端 + +请参阅对应的协议实现端项目的部署文档。 + +## 2. 配置 Satori 协议 + +1. 进入 AstrBot 的 WebUI +2. 点击左边栏 `机器人` +3. 然后在右边的界面中,点击 `+ 创建机器人` +4. 选择 `Satori` + +弹出的配置项填写: + +- 机器人名称 (id): `satori` (随意) +- 启用 (enable): 勾选 +- Satori API 终结点 (satori_api_base_url):`http://localhost:5600/v1`(端口和上面配置的协议端端口一致) +- Satori WebSocket 终结点 (satori_endpoint):`ws://localhost:5600/v1/events`(端口和上面配置的协议端端口一致) +- Satori 令牌 (satori_token):根据协议端配置情况选择填写 + +点击 `保存`。 diff --git a/docs/snapshots/v4.23.6/zh/platform/satori/server-satori.md b/docs/snapshots/v4.23.6/zh/platform/satori/server-satori.md new file mode 100644 index 0000000..b957bd0 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/satori/server-satori.md @@ -0,0 +1,68 @@ +# 接入 server-satori (基于 Koishi) + +> [!TIP] +> server-satori 是 Koishi 平台的一个插件,可以将 Koishi 作为 Satori 协议的服务端,让 AstrBot 通过 Satori 协议接入 koishi 响应消息。 + +## 准备工作 + +确保你已经有一个运行中的 Koishi 实例。 + +如果没有,请先参考 [Koishi 官方文档](https://koishi.chat/zh-CN/manual/starter/windows.html) 完成安装和基础配置。 + +> 安装过程中遇到任何问题,欢迎前往 [Koishi 社区](https://koishi.chat/zh-CN/about/contact.html) 社区讨论。 + +## 在 Koishi 中启用 server-satori 插件 + +1. 打开 Koishi 管理界面 +2. 进入`插件配置` 页面 +3. 启用该插件(通常不需要额外配置,使用默认设置即可) + +安装并启用插件后,server-satori 会自动在 Koishi 的 `/satori` 路径下提供 Satori 协议服务。 + +![image](https://files.astrbot.app/docs/source/images/satori/2025-09-07_17-14-55.png) + +## 在 AstrBot 中配置 Satori 适配器 + +1. 进入 AstrBot 的管理面板 +2. 点击左边栏 `机器人` +3. 然后在右边的界面中,点击 `+ 创建机器人` +4. 选择 `satori` + +弹出的配置项填写: + +- 机器人名称 (id): `server-satori` +- 启用 (enable): 勾选 +- Satori API 终结点 (satori_api_base_url):`http://localhost:5140/satori/v1` +- Satori WebSocket 终结点 (satori_endpoint):`ws://localhost:5140/satori/v1/events` +- Satori Token (satori_token):通常留空(除非在 Koishi 中特别配置了 Token) + +> [!NOTE] +> +> - Koishi 默认运行在 5140 端口 +> - server-satori 插件默认在 `/satori` 路径下提供服务 +> - 因此完整的 URL 路径为 `http://localhost:5140/satori/v1` +> +> 如果你的 koishi 运行在其他端口或路由下,**请根据实际情况修改对应的配置!** + +![image](https://files.astrbot.app/docs/source/images/satori/2025-10-10_16-16-25.png) + +点击右下角 `保存` 完成配置。 + +## 🎉 大功告成 + +此时,你的 AstrBot 应该已经通过 Satori 协议成功连接到了 Koishi 的 server-satori 插件。 + +在 Koishi 的沙盒里 向机器人发送 AstrBot的指令(例如:`/help`)进行测试, + +如果成功回复,则配置成功。 + +![image](https://files.astrbot.app/docs/source/images/satori/2025-09-07_17-19-04.png) + +## 常见问题 + +如果遇到连接问题,请检查: + +1. Koishi 是否正常运行 +2. server-satori 插件是否已正确安装并启用 +3. 端口和路径配置是否正确 +4. 防火墙是否阻止了相关端口的访问 diff --git a/docs/snapshots/v4.23.6/zh/platform/slack.md b/docs/snapshots/v4.23.6/zh/platform/slack.md new file mode 100644 index 0000000..de5b31c --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/slack.md @@ -0,0 +1,93 @@ +# 接入 Slack + +## 创建 AstrBot Slack 平台适配器 + +进入 `机器人` 页,点击 `+ 创建机器人`,找到 Slack 并点击进入 Slack 配置页。 + +![image](https://files.astrbot.app/docs/source/images/slack/image-1.png) + +在弹出的配置对话框中点击 `启用`。 + +## 在 Slack 创建 App + +Slack 支持两种接入方式:`Webhook` 与 `Socket`。如果您没有公网服务器并且消息业务量的规模较小,我们建议您使用 `socket` 方式。如果您有公网服务器(或者有一定的技术背景,了解如何设置 Tunnel,如 Cloudflare Tunnel),可以选择 `webhook` 方式。`socket` 方式部署相对简单。 + +1. 创建 [Slack](https://slack.com/signin) 账号和一个工作区(Workspace)。 +2. 前往 [应用后台](https://api.slack.com/apps),点击「Create New App」->「From Scratch」,输入 `应用名称` 和要添加到的工作区,然后点击「Create App」。 +3. (仅 Webhook 需要)获取 `Signing Secret`,在左边栏 Basic Information 页下,找到 App Credentials 的 `Signing Secret`,点击 Show 并且复制到平台适配器配置的 signing_secret 处。 + +![image](https://files.astrbot.app/docs/source/images/slack/image.png) + +4. 在左边栏 Basic Information 页下,找到 App-Level Tokens,点击 「Generate Token and Scopes」。Token Name 任意输入,点击 Add Scope,选择 `connections:write`,然后点击 「Generate」,点击 Copy 将结果复制到 AstrBot 配置页的 app_token 处。 + +![image](https://files.astrbot.app/docs/source/images/slack/image-2.png) + +5. 在左边栏 OAuth & Permissions 页下,在 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. 在左边栏 OAuth & Permissions 页下,在 Oauth Token 处点击 `Install to xxx`(xxx 是您工作区的名字)。然后复制生成的 Bot User OAuth Token 到平台适配器配置的 bot_token 处。 + +7. (仅 Socket 需要)在左边栏 Socket Mode 页下,开启 Enable Socket Mode。 + +![image](https://files.astrbot.app/docs/source/images/slack/image-3.png) + +## 启动平台适配器 + +现在,配置已经完成。如果您使用的是 Socket 模式,那么直接点击配置的右下角的保存按钮即可。 + +如果您使用的是 Webhook 模式,请保持 `统一 Webhook 模式 (unified_webhook_mode)` 为开启状态。 + +> [!TIP] +> v4.8.0 之前没有 `统一 Webhook 模式`,请填写以下配置项: +> Slack Webhook Host、Slack Webhook Port 和 Slack Webhook Path + +## 开启事件接收 + +新建平台适配器成功后,返回到 Slack 设置,在左边栏 Event Subscriptions 页下,点击 Enable Events 启用事件接收。 + +如果您使用的是 Webhook 模式: + +- 如果开启了 `统一 Webhook 模式`,点击保存之后,AstrBot 将会自动为你生成唯一的 Webhook 回调链接,你可以在日志中或者 WebUI 的机器人页的卡片上找到,将该链接填入 `Request URL` 输入框中。 + +![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png) + +- 如果没有开启 `统一 Webhook 模式`,请在 `Request URL` 输入框中输入 `https://您的域名/astrbot-slack-webhook/callback`。 + +> [!TIP] +> Webhook 模式下,您需要先在 DNS 服务商处设置好域名,然后使用反向代理软件将请求转发到 AstrBot 所在服务器的 `6185` 端口(如果开启了统一 Webhook 模式)或配置指定的端口(如果没有开启统一 Webhook 模式)。或者您可以使用 Cloudflare Tunnel。具体教程请参考网络资源,本教程不赘述。 + +启用后,在下方的 Subscribe to bot events 处,点击 Add Bot User Event,添加如下事件: + +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 + +## 测试成功性 + +进入您刚刚添加的 Slack 工作区,进入需要用到 Bot 的频道,然后 @ 您刚刚创建的应用。然后点击 Slackbot 随后发送的消息中的 添加 按钮来添加到工作区中。然后,@ 应用,输入 `/help`,如果能够成功回复,说明测试成功。 + +如果有疑问,请[提交 Issue](https://github.com/AstrBotDevs/AstrBot/issues)。 diff --git a/docs/snapshots/v4.23.6/zh/platform/start.md b/docs/snapshots/v4.23.6/zh/platform/start.md new file mode 100644 index 0000000..6e48d5d --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/start.md @@ -0,0 +1,8 @@ +# 接入消息平台 + +AstrBot 支持接入众多主流即时通讯软件平台,帮助您在自己喜欢的 IM 平台上使用 AstrBot 的强大功能。 + +在 WebUI 中,点击侧边栏的**机器人**,即可进入消息平台接入界面。点击右上角的**创建机器人**,选择您想要接入的平台,按照本文档左侧提供的接入指南进行操作,即可完成接入。 + +> [!TIP] +> 建议在部署前预先安装 `ffmpeg`(并确保支持 `amr`),否则媒体类文件可能无法正常收发。对于微信类平台接入,强烈建议安装。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/zh/platform/telegram.md b/docs/snapshots/v4.23.6/zh/platform/telegram.md new file mode 100644 index 0000000..0156b66 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/telegram.md @@ -0,0 +1,56 @@ + +# 接入 Telegram + +## 支持的基本消息类型 + +> 版本 v4.15.0。 + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| --- | --- | --- | --- | +| 文本 | 是 | 是 | | +| 图片 | 是 | 是 | | +| 语音 | 是 | 是 | | +| 视频 | 是 | 是 | | +| 文件 | 是 | 是 | | + + +主动消息推送:支持。 + +## 1. 创建 Telegram Bot + +首先,打开 Telegram,搜索 `BotFather`,点击 `Start`,然后发送 `/newbot`,按照提示输入你的机器人名字和用户名。 + +创建成功后,`BotFather` 会给你一个 `token`,请妥善保存。 + +如果需要在群聊中使用,需要关闭Bot的 [Privacy mode](https://core.telegram.org/bots/features#privacy-mode),对 `BotFather` 发送 `/setprivacy` 命令,然后选择bot, 再选择 `Disable`。 + +## 2. 配置 AstrBot + +1. 进入 AstrBot 的管理面板 +2. 点击左边栏 `机器人` +3. 然后在右边的界面中,点击 `+ 创建机器人` +4. 选择 `telegram` + +弹出的配置项填写: + +- ID(id):随意填写,用于区分不同的消息平台实例。 +- 启用(enable): 勾选。 +- Bot Token: 你的 Telegram 机器人的 `token`。 + +请确保你的网络环境可以访问 Telegram。你可能需要使用 `配置页->其他配置->HTTP 代理` 来设置代理。 + +## 流式输出 + +Telegram 平台支持流式输出。需要在「AI 配置」->「其他配置」中开启「流式输出」开关。 + +### 私聊流式输出 + +在私聊中,AstrBot 使用 Telegram Bot API v9.3 新增的 `sendMessageDraft` API 实现流式输出。这种方式会在私聊界面展示一个「正在输入」的草稿预览动画,体验更接近「打字机」效果,且避免了传统方案的消息闪烁、推送通知干扰和 API 编辑频率限制等问题。 + +### 群聊流式输出 + +在群聊中,由于 `sendMessageDraft` API 仅支持私聊,AstrBot 会自动回退到传统的 `send_message` + `edit_message_text` 方案。 + +:::warning +`sendMessageDraft` 功能需要 `python-telegram-bot>=22.6`。 +::: diff --git a/docs/snapshots/v4.23.6/zh/platform/vocechat.md b/docs/snapshots/v4.23.6/zh/platform/vocechat.md new file mode 100644 index 0000000..67bd7e2 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/vocechat.md @@ -0,0 +1,43 @@ +# 接入 VoceChat + +> [!TIP] +> AstrBot 未自带这个适配器,需要安装 [astrbot_plugin_vocechat](https://github.com/HikariFroya/astrbot_plugin_vocechat) 插件。该插件由 [HikariFroya](https://github.com/HikariFroya) 开发 ❤️。 +> **如果您觉得有帮助,请支持开发者,给该仓库点一个 Star。** + +> [!WARNING] +> 这个适配器目前不由 AstrBot 官方维护,因此稳定性未知。 + +## 部署 VoceChat + +VoceChat 是一个开源的支持多平台、搭建简单的即时通讯平台。 + +请在 [VoceChat 官方网站](https://voce.chat/zh-CN)查看部署方式。 + +## 安装 astrbot_plugin_vocechat 插件 + +进入 AstrBot 仪表盘的插件市场,搜索 `astrbot_plugin_vocechat`,点击安装。 + +![image](https://files.astrbot.app/docs/source/images/vocechat/image.png) + +安装完成后,前往 `机器人` → `+ 创建机器人` → 选择 VoceChat(若选项缺失,尝试重启 AstrBot 或检查插件安装状态)。 + +在弹出的配置对话框中点击 `启用`。 + +## 配置 + +- **`vocechat_server_url` (必填)**: 您的 VoceChat 服务器的完整 URL 地址。例如: `http://localhost:3009` 或 `https://your.vocechat.domain`。请确保末尾没有 `/`。 +- **`api_key` (必填)**: 您在 VoceChat 后台为该机器人账号生成的 API Key。 +- **`webhook_path` (建议保留默认或自定义)**: AstrBot 用于接收 VoceChat 推送消息的 Webhook 路径。例如: `/vocechat_webhook`。您需要在 VoceChat 机器人设置中填写的 Webhook URL 将是 `http://<你的AstrBot可访问地址>:`。 +- **`webhook_listen_host` (通常为 `0.0.0.0`)**: AstrBot Webhook 服务器监听的IP地址。`0.0.0.0` 表示监听所有可用的网络接口。 +- **`webhook_port` (必填)**: AstrBot Webhook 服务器监听的端口号。例如: `8080`。请确保此端口未被其他应用占用,并且如果您的 AstrBot 服务器在防火墙后,此端口需要被允许访问。 +- **`get_user_nickname_from_api` (布尔值, 默认: `true`)**: 是否尝试通过 VoceChat API 获取用户昵称。如果为 `false`,将使用 `VoceChatUser_UID` 作为默认昵称。 +- **`send_plain_as_markdown` (布尔值, 默认: `false`)**: 如果为 `true`,发送纯文本消息时会使用 Markdown 格式(可能会影响部分纯文本的显示,但能更好地支持一些特殊字符)。通常建议保持 `false`,除非有特定需求。 +- **`default_bot_self_uid` (必填)**: 您要连接的这个 VoceChat 机器人账号在 VoceChat 中的用户 ID (UID)。 + +全部配置好后,点击右下角的保存按钮。然后前往 VoceChat 中测试。 + +## 问题提交 + +如有疑问,请提交 issue 至[插件仓库](https://github.com/HikariFroya/astrbot_plugin_vocechat/issues) 以及 [AstrBot 仓库](https://github.com/AstrBotDevs/AstrBot/issues/new?template=bug-report.yml)。 + +**如果您觉得有帮助,请支持开发者,给 [astrbot_plugin_vocechat](https://github.com/HikariFroya/astrbot_plugin_vocechat) 仓库点一个 Star。** diff --git a/docs/snapshots/v4.23.6/zh/platform/wecom.md b/docs/snapshots/v4.23.6/zh/platform/wecom.md new file mode 100644 index 0000000..0774a05 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/wecom.md @@ -0,0 +1,148 @@ +# AstrBot 接入企业微信 + +AstrBot 支持接入企业微信应用和微信客服。 + +## 支持的基本消息类型 + +> 版本 v4.15.0。 + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| --- | --- | --- | --- | +| 文本 | 是 | 是 | | +| 图片 | 是 | 是 | | +| 语音 | 是 | 是 | | +| 视频 | 否 | 是 | | +| 文件 | 否 | 是 | | + +主动消息推送:企业微信应用支持,未测试企业微信客服。 + +## 准备接入 + +步骤: + +1. 进入 AstrBot 的管理面板 +2. 点击左边栏 `机器人` +3. 然后在右边的界面中,点击 `+ 创建机器人` +4. 选择 `wecom` + +这将弹出一个对话框。接下来,不要关闭页面,转移到下一步。 + +## 接入方式一:微信客服 + +> [!NOTE] +> +> 1. 需要 >= v3.5.7 +> 2. 以这种方式接入,支持在微信内使用。 + +1. 进入 [微信客服后台](https://kf.weixin.qq.com/),使用企业微信扫码登录。 + +2. **得到客服账号名。** 在 `客服账号` 中创建一个客服账号,记录下名称,填入 AstrBot 配置的 `微信客服账号名` 中(不是账号 ID)。 + +3. **得到企业 ID。** 在 [企业微信 - 企业信息](https://work.weixin.qq.com/wework_admin/frame#profile) 得到企业 ID(`Corpid`),复制到 AstrBot 配置的 `corpid` 处。 + +4. **回调服务器验证。** 如果您之前没有使用过微信客服机器人,那么请在 `开发配置` 中点击企业内部接入右侧的 `开始使用` 按钮,您应该会看到回调配置的页面。 + +![image](https://files.astrbot.app/docs/source/images/wecom/8287fd9fec5823847e6b590dc3f0f545.png) + +如果您之前使用过微信客服机器人,那么在 `开发配置` 中直接找到 `回调配置`,点击修改。 + +点击下方的两个随机获取,得到 `Token` 和 `EncodingAESKey`,复制到 AstrBot 配置的 `token` 和 `encoding_aes_key` 处。请保持 `统一 Webhook 模式 (unified_webhook_mode)` 为开启状态。然后点击保存配置,等待适配器加载完成。 + +回调 URL 填写: + +- 如果开启了 `统一 Webhook 模式`,点击保存之后,AstrBot 将会自动为你生成唯一的 Webhook 回调链接,你可以在日志中或者 WebUI 的机器人页的卡片上找到,将该链接填入回调 URL 处。 + +![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png) + +- 如果没有开启 `统一 Webhook 模式`,填写 `http://你的带公网地址的服务器ip:6195/callback/command`。 + +> 请注意放行端口。如果开启了统一 Webhook 模式,需要将请求转发到 AstrBot 所在服务器的 `6185` 端口;如果没有开启,则转发到配置指定的端口(默认 `6195`)。 + +回到微信客服 `回调配置`,点击 `完成`。如果一切无误,将会显示 `已完成`(否则会显示类似 `openapi 回调不通过` 类似的文本)。 + +1. **获取 Secret。** 之后,在 `开发配置` 中得到 Secret,找到复制到刚刚创建的企业微信适配器,点击编辑,然后修改配置中的 `secret`。然后再次保存配置,等待适配器加载完成。 + +> [!TIP] +> 根据 [#571](https://github.com/Soulter/AstrBot/issues/571) 的反馈,对于新注册的企业,`corp_id` 可能要注册一段时间后才生效(前后大概过了半个小时)。 + +然后,打开 `控制台` 页,你应该会看到如下日志: + +```txt +请打开以下链接,在微信扫码以获取客服微信 ... +``` + +![image](https://files.astrbot.app/docs/source/images/wecom/image-13.png) + +打开链接,用微信扫码,然后即可打开微信客服聊天页,输入 `help` 测试是否正常连通。 + +## 接入方式二:企业微信应用 + +进入 https://work.weixin.qq.com/wework_admin/frame#apps + +点击 `我的企业`,查看并得到企业 ID(`Corpid`),复制到 AstrBot 配置的 `corpid` 处。 + +> [!TIP] +> 根据 [#571](https://github.com/Soulter/AstrBot/issues/571) 的反馈,对于新注册的企业,`corp_id` 可能要注册一段时间后才生效(前后大概过了半个小时)。 + +![image](https://files.astrbot.app/docs/source/images/wecom/image-5.png) + +点击下面的 `自建应用`,然后点击 `创建应用`,填写好应用名称、头像、应用可见范围等信息。 + +进入应用,查看并得到机器人的 `Secret`,复制到 AstrBot 配置的 `secret` 处。 + +![image](https://files.astrbot.app/docs/source/images/wecom/image-4.png) + +在下方,找到 `接收消息`,点击 `设置 API 接收`,进入 API 接收页面。 + +![image](https://files.astrbot.app/docs/source/images/wecom/image-6.png) + +![image](https://files.astrbot.app/docs/source/images/wecom/image-9.png) + +并且点击下方的两个随机获取,得到 `Token` 和 `EncodingAESKey`,复制到 AstrBot 配置的 `token` 和 `encoding_aes_key` 处。建议保持 `统一 Webhook 模式 (unified_webhook_mode)` 为开启状态。 + +现在应该已经填完 AstrBot 连接到企业微信的所有配置项。点击 AstrBot 配置页右下角保存,等待 AstrBot 重启。 + +在 URL 处填入回调地址: + +- 如果开启了 `统一 Webhook 模式`,点击保存之后,AstrBot 将会自动为你生成唯一的 Webhook 回调链接,你可以在日志中或者 WebUI 的机器人页的卡片上找到,将该链接填入 URL 处。 + +![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png) + +- 如果没有开启 `统一 Webhook 模式`,填入 `http://你的带公网地址的服务器ip:6195/callback/command`。 + +> 请注意放行端口。如果开启了统一 Webhook 模式,需要将请求转发到 AstrBot 所在服务器的 `6185` 端口;如果没有开启,则转发到配置指定的端口(默认 `6195`)。 + +接下来配置企业可信 IP。 + +![image](https://files.astrbot.app/docs/source/images/wecom/image-10.png) + +将你的 公网 IP 地址填写到此处,点击确定。 + + +重启成功后,回到API 接收页面,点击下面的保存,看是否能\够保存成功。如果出现 `openapi 请求回调地址不通过` 说明配置有问题,请检查四个配置项是否填写正确。 + +如果能够保存成功,AstrBot 就已经能够接收信息。 + +## 测试 + +在企业微信-工作台中,找到刚刚创建的应用,发送 `/help`,看看 AstrBot 是否能够回复。 + +![image](https://files.astrbot.app/docs/source/images/wecom/3dc9fa61145ab0dd8f56a10295affec8_720.png) + +## 反向代理(自定义 API BASE) + +AstrBot 支持自定义企业微信的终结点以适应家庭 ip 没有固定的公网 IP 问题。 + +只需要将您的自定义地址填入 `api_base_url` 即可。 + +> 如果您没有公网 ip 当然也可以购买一台服务器,推荐 阿里云 的 99 元/年的服务器。 + +## 语音输入 + +为了语音输入,需要你的电脑上安装有 `ffmpeg`。 + +linux 用户可以使用 `apt install ffmpeg` 安装。 + +windows 用户可以在 [ffmpeg 官网](https://ffmpeg.org/download.html) 下载安装。 + +mac 用户可以使用 `brew install ffmpeg` 安装。 diff --git a/docs/snapshots/v4.23.6/zh/platform/wecom_ai_bot.md b/docs/snapshots/v4.23.6/zh/platform/wecom_ai_bot.md new file mode 100644 index 0000000..55aca1b --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/wecom_ai_bot.md @@ -0,0 +1,77 @@ +# 接入企业微信智能机器人平台 + +企业微信智能机器人是企业微信官方推出的 AI 友好的机器人平台,可在单聊或群聊(企业微信内部群)中直接使用,并且支持流式传输。 + +## 支持的基本消息类型 + +> 版本 v4.15.0。 + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| --- | --- | --- | --- | +| 文本 | 是 | 是 | | +| 图片 | 是 | 是 | 仅限配置了消息推送 Webhook URL。| +| 语音 | 否 | 是 | 仅限配置了消息推送 Webhook URL。| +| 视频 | 否 | 是 | 仅限配置了消息推送 Webhook URL。| +| 文件 | 否 | 是 | 仅限配置了消息推送 Webhook URL。| + +主动消息推送:支持,但需要配置消息推送 Webhook URL。 + +## 配置智能机器人 + +1. 登录到[企业微信后台](https://work.weixin.qq.com/wework_admin)。 + +2. 在左侧导航栏中,点击 `管理工具`,找到 `智能机器人`,点击进入,然后点击创建机器人。 + +![管理工具-智能机器人](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-1.png) + +3. 在创建智能机器人页面下方找到并点击 `API模式创建`。填写机器人名称、头像等基本信息。Token、EncodingAESKey 请点击 `随机获取` 按钮生成。生成之后,先不要点击创建,接下来将配置 AstrBot。 + +![创建智能机器人账号](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image.png) + +## 配置 AstrBot + +1. 进入 AstrBot 的管理面板,点击左侧栏 `机器人`(旧版本为 `消息平台`),然后在右侧的界面中,点击 `+ 新增适配器`,选择 `企业微信智能机器人`,进入配置页面。 + +![新增适配器](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-2.png) + +2. 在弹出的配置项中将 `企业微信智能机器人的名字`、`token`、`encoding_aes_key` 从上一步创建智能机器人时填写的值复制粘贴到对应的输入框中。ID 可以随意填写,用于区分不同的消息平台实例。`port` 默认为 `6198`,可以根据需要修改,但请确保该端口未被占用。请保持 `统一 Webhook 模式 (unified_webhook_mode)` 为开启状态。点击 `保存`。 + +3. 回到企业微信智能机器人创建页面,填写 `URL`: + + - 如果开启了 `统一 Webhook 模式`,点击保存之后,AstrBot 将会自动为你生成唯一的 Webhook 回调链接,你可以在日志中或者 WebUI 的机器人页的卡片上找到,将该链接填入 `URL` 处。 + + ![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png) + + - 如果没有开启 `统一 Webhook 模式`,填写 `http://IP:port/webhook/wecom-ai-bot`,其中 `IP` 替换为你的 AstrBot 服务器的公网 IP 地址,`port` 替换为上一步填写的端口号。 + +> 建议有能力的用户自行配置域名和反向代理,将请求转发到 AstrBot 所在服务器的 `6185` 端口(如果开启了统一 Webhook 模式)或配置指定的端口(如果没有开启统一 Webhook 模式),并使用 HTTPS 协议。如果没有域名,也可以使用 [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/)。 + +4. 点击 `创建` 按钮,如果一切无误,将进入智能机器人详情页面。如果报错 `服务没有正确响应,请确认后重试`,请检查 AstrBot 的配置、服务器防火墙端口放行规则等。 + +![创建智能机器人详情页面](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-3.png) + +5. [可选,推荐] 配置企业微信消息推送 Webhook URL。默认情况下,企业微信智能机器人只能在用户主动发送消息时被动回复消息。如果希望实现机器人主动消息推送功能,可以配置企业微信的消息推送 Webhook URL。只需要在企业微信内部群中,点击群设置 -消息推送,创建一个推送机器人,然后将下方生成的 Webhook URL 填入配置中即可。要求 AstrBot 版本不低于 v4.15.0。企业微信智能机器人之支持图片和文本消息类型,如果配置了该选项,在发送其他类型消息(如视频、音频、文件)时,AstrBot 将会调用消息推送的接口去发送消息。**强烈建议配置该选项以获得更完整的消息类型支持。** + +6. [可选,推荐] 企业微信智能机器人只支持对用户的一个消息回复最多一个消息气泡。如果您希望机器人发送更复杂的消息(例如连续发送多条消息、包含图片或文件的消息等),您可打开 「仅使用 Webhook 发送消息」。这将仅使用 Webhook 方式发送消息,绕过企业微信智能机器人的回复限制。**如果您不需要类似企业微信智能机器人那样的打字机效果,强烈建议您打开此选项。**此选项需要您配置第 5 步中的消息推送 Webhook URL。 + +## 使用智能机器人 + +### 将机器人添加到群聊 + +在企业微信客户端的企业内部群中,点击添加成员,点击智能机器人,找到刚刚创建的智能机器人,点击添加即可。 + +![点击添加成员](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-4.png) + +![添加成功](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-5.png) + +### 使用机器人 + +在单聊或群聊中,直接发送消息即可与机器人进行对话。 + +如果您需要类似实时打字机的效果,请确保在 AstrBot 中开启了 `流式回复` 功能。 + +![流式回复](https://files.astrbot.app/docs/source/images/wecom_ai_bot/image-6.png) + +## 帮助与支持 + +如您在配置或使用过程中遇到问题,或需要其他企业支持服务,可发送邮件至 [community@astrbot.app](mailto://community@astrbot.app)。 diff --git a/docs/snapshots/v4.23.6/zh/platform/weixin-official-account.md b/docs/snapshots/v4.23.6/zh/platform/weixin-official-account.md new file mode 100644 index 0000000..90c4fc1 --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/weixin-official-account.md @@ -0,0 +1,83 @@ +# AstrBot 接入微信公众平台 + +AstrBot 支持接入微信公众平台(版本 >= v3.5.8),并以微信公众号的形式接入,届时,您可以直接在微信公众号聊天界面中与 AstrBot 进行交互。 + +## 准备接入 + +步骤: + +1. 进入 AstrBot 的管理面板 +2. 点击左边栏 `机器人` +3. 然后在右边的界面中,点击 `+ 创建机器人` +4. 选择 `weixin_official_account(微信公众平台)` + +这将弹出一个对话框。接下来,不要关闭页面,转移到下一步。 + +## 创建/登入微信公众平台 + +进入[微信公众平台](https://mp.weixin.qq.com/),如果您需要接入现有的公众号请直接登录即可,如果没有,请点击立即注册然后选择 `公众号` 并填写相关信息注册。 + +> [!NOTE] +> 新注册的公众号需要花费 1-2 天审核,期间不能使用。 + +## 设置回调服务 + +点击 `设置与开发` -> `开发接口管理`。界面如下: + +![开发接口管理](https://files.astrbot.app/docs/source/images/weixin-official-account/image.png) + +记录开发者 ID(AppID) 和开发者密码(AppSecret),分别填入 AstrBot 配置的 `appid` 和 `secret` 处。 + +找到 IP白名单,点击查看,然后添加你的公网 IP 地址。如果有多个公网 IP 地址,换行分隔。 + +找到下方的服务器配置,然后点击修改配置。 + + +`Token` 由自己填写,请随意填写一个字符串,长度 3-32 位。并同样填入 AstrBot 配置的 `token` 处(一定要相同)。 + +`EncodingAESKey` 请点击随机生成,然后复制到 AstrBot 配置的 `encoding_aes_key` 处。 + +建议保持 `统一 Webhook 模式 (unified_webhook_mode)` 为开启状态。 + +现在应该已经填完 AstrBot 连接到微信公众平台的所有配置项。点击 AstrBot 配置页右下角保存,等待 AstrBot 重启。 + +`URL` 填写: + +- 如果开启了 `统一 Webhook 模式`,点击保存之后,AstrBot 将会自动为你生成唯一的 Webhook 回调链接,你可以在日志中或者 WebUI 的机器人页的卡片上找到,将该链接填入 URL 处。 + +![unified_webhook](https://files.astrbot.app/docs/source/images/use/unified-webhook.png) + +- 如果没有开启 `统一 Webhook 模式`,请填入 `http://你的域名/callback/command`。 + +> 注意⚠️:仅支持 80 或者 443 端口。您可能需要购买域名,然后反向代理流量到 AstrBot 所在服务器的 `6185` 端口(如果开启了统一 Webhook 模式)或 `6194` 端口(如果没有开启统一 Webhook 模式),或者将端口改成 80 端口(注意服务器需要没有软件在占用 80 端口)。 + +消息加解密方式请选中 `安全模式`。 + +等待片刻,点击 `提交`。如果一切无误,会显示 `提交成功`。 + +## 测试 + +点击左下角你的账号头像,点击账号详情,找到 `二维码`,扫码进入到公众号聊天界面,发送 `help`,看看 AstrBot 是否能够回复。 + +如果可以回复,说明接入成功。 + +> [!NOTE] +> 如果没有回复,并且控制台报错 `ip xxxxx not in whitelist`,说明你没有添加公网 IP 地址到微信公众平台的 IP 白名单中。如果确认添加了,那请等待若干分钟以让微信服务器更新。 + +## 反向代理(自定义 API BASE) + +AstrBot 支持自定义企业微信的终结点以适应家庭 ip 没有固定的公网 IP 问题。 + +只需要将您的自定义地址填入 `api_base_url` 即可。 + +> 如果您没有公网 ip 当然也可以购买一台服务器,推荐 阿里云 的 99 元/年的服务器。 + +## 语音输入 + +为了语音输入,需要你的电脑上安装有 `ffmpeg`。 + +linux 用户可以使用 `apt install ffmpeg` 安装。 + +windows 用户可以在 [ffmpeg 官网](https://ffmpeg.org/download.html) 下载安装。 + +mac 用户可以使用 `brew install ffmpeg` 安装。 \ No newline at end of file diff --git a/docs/snapshots/v4.23.6/zh/platform/weixin_oc.md b/docs/snapshots/v4.23.6/zh/platform/weixin_oc.md new file mode 100644 index 0000000..7fc64db --- /dev/null +++ b/docs/snapshots/v4.23.6/zh/platform/weixin_oc.md @@ -0,0 +1,80 @@ +# 接入个人微信 + +> v4.22.0 引入。 + +AstrBot 支持通过 `个人微信` 适配器接入微信个人号。该适配器基于**腾讯微信官方** `openclaw-weixin` 接口实现,使用扫码登录和长轮询收发消息,不需要配置 Webhook 回调地址。 + +> [!NOTE] +> 需要升级到最新的手机微信版本:iOS >= 8.0.70,Android >= 8.0.69,并确保微信中包含 ClawBot 插件 + +## 支持的消息类型 + +| 消息类型 | 是否支持接收 | 是否支持发送 | 备注 | +| --- | --- | --- | --- | +| 文本 | 是 | 是 | | +| 图片 | 是 | 是 | 接收时会下载并解密到本地临时目录 | +| 语音 | 是* | 否 | *微信云端会自动转录成文本,无需本地转录 | +| 视频 | 是 | 是 | 接收时会下载并解密到本地临时目录 | +| 文件 | 是 | 是 | 接收时会下载并解密到本地临时目录 | + +## 创建机器人 + +1. 进入 AstrBot WebUI。 +2. 点击左侧栏 `机器人`。 +3. 点击右上角 `+ 创建机器人`。 +4. 选择 `个人微信`。 + +## 配置项说明 + +通常只需要关注以下几个配置: + +- `ID(id)`:随意填写,用于区分不同的机器人实例。 +- `启用(enable)`:勾选。 + +其余配置**保持默认即可**,一般无需修改,除非您明确知道用途: + +- `二维码轮询间隔(weixin_oc_qr_poll_interval)` +- `长轮询超时(weixin_oc_long_poll_timeout_ms)` +- `API 超时(weixin_oc_api_timeout_ms)` + +> [!TIP] +> `token` 和 `account_id` 会在扫码登录成功后由 AstrBot 自动保存,通常不需要手动填写。 + +## 扫码登录 + +1. 填好配置后点击 `保存`。 +2. 返回机器人列表,AstrBot 会自动向微信接口申请登录二维码。 +3. 在**机器人卡片**中点击 “查看二维码” 按钮,会弹出二维码对话框。(点击保存之后可能需要等 5 到 10 秒左右才会出现这个按钮) +4. 使用手机微信扫码,并在微信内确认登录。 + +![微信二维码入口](weixin_qr_entry.png) + +登录成功后,AstrBot 会自动保存登录态。后续重启时,如果登录态仍有效,通常不需要再次扫码。 + +> [!NOTE] +> 1. 如果二维码过期,AstrBot 会自动重新申请新的二维码。刷新后请使用新的二维码重新扫码。 +> 2. 如果 WebUI 没看到 “查看二维码” 按钮,可以前往终端或者 WebUI 控制台,找到 `请使用手机微信扫码登录,二维码有效期 5 分钟,过期后会自动刷新。` 对应的日志,附近会显示二维码扫码链接和终端直接输出的二维码,直选择一种方式扫码即可。 + +## 验证 + +登录成功后,用微信发送一条消息。如果 AstrBot 能正常回复,说明接入成功。 + +也可以在 WebUI `控制台` 中观察日志,确认适配器已经完成登录并开始轮询消息。 + +## 修改头像和备注名 + +进入微信聊天会话,点击右上角的齿轮图标,再点击右上角的「...」图标,即可修改头像和备注名。 + +## 多媒体文件保存位置 + +接收到的图片、视频、文件、语音会被 AstrBot 下载并解密后保存到本地临时目录: + +`data/temp` + +这些文件属于 AstrBot 的临时缓存文件,后续插件、Agent 或文件服务可以继续读取和处理。 + +## 已知说明 + +- 该适配器通过扫码登录个人微信,接入方式与微信公众号、企业微信不同。 +- 不需要配置公网回调地址,也不需要开启统一 Webhook 模式。 + diff --git a/docs/snapshots/v4.23.6/zh/platform/weixin_qr_entry.png b/docs/snapshots/v4.23.6/zh/platform/weixin_qr_entry.png new file mode 100644 index 0000000000000000000000000000000000000000..b376bce488fd519ec087bf7b1f710eeb334f0c88 GIT binary patch literal 46799 zcmd?Qg;$ha_dg6n_fW$S!q8o!gdp7@2ndYy&`NiA4Jk+q2nL~a58Z=^gi3b^2t!Ku zZ@lmC^XT*b3-4OjS}<3fv*YZu_t|@YVjpX%kPy-lVqjp9sHrOIVqgG}7#Nsy__*jh z<3o<)=s%bqx+?M*6~hdh7#I)?HAT56zGmBbc%f`6UN?(BNE%ycPTZ5V&c$)C~lkZfbg5y0u$r-r+B(!cG)lT(LX=Eth0MkJ-_PRzzjS{gFW|-D#QK{z0?#;JB^KtFowqLu?w6cZUYL}-C#(G>lP%_C zRn>Z3>hhv5BjZUKhSl|8P6`6GXUvXUFmkRRPhln_XC6#{qznl=$6o)EP>ZYL7Bhx51hxJ3^&r2bbkvc(jS&8P{;*^{lF%O#ZwX>m}f z1A8)kC%h1P8|0Ttf0oA`1=%LUC#C8TL(BCD1~nM5aBj4qa1h6XbvBsFf3?vZk8!VB z=L1(5Lneg;EJ2lWMwfvG?#THURh{yH8FvPn#Z^@TEBuuwFEcD(L;t8?lpDLwK}iBP zxIcv)S%kvAj+IAK#y^WC8OE7+-ma zS^eFTW;qg;^~ywMyhiR>X#cHlSxDRs(uo|YdPuxz3Hn>DyX+X|t6YUIoa2RtZFT1* zv8Fu!ek=j#dr3}9$4`UirN{Ei{~DxD4PB!N*zTJx3)$aeVp7Pa#pz1W8LVayT3vuV ze*MuV2Ei18V&C&QU6`hWur~HzGhs?%M!N8(^E%;uzY8NiCBV1(-yy3gLxrMco$a1w z3)#-Cc$12}c=Sj0UpatL2qZrx((@hs#M&D5M;Qfq*l(A#zEZ#vAXM4+tY{iNY7VCU z2oLB5fNOo#a*4~pU$tgoes)2l5+qO)^yh_91HhWQ5|7gpBtJRwNYEL@u-f%Uun-XT z%#NKVj3NG6xAMzBYj%V3Cn4-hluw>a%Cy2CChgN?TWFT*do6Q^T$fGXQAS<aTwZ z!U*7fV7&MB9p5YN1k@e7Zzp9@#0K*Tf{^uYZVcWYz+Nb|8^n2}O- z(4{ipfW61#WmKK%y9%26yYGZ5(!q^?ahU}yQKGseSTWlAs5Cm$czoGN9W<3;MYx)! z>G-$uo-+f-z8RN7Dkk%~wf843+HA>-#D9xC+HJOhVyeL@XM3tzLA~&0nR$nqmehfw zTA{9!Q|k2+B_(Bz%^+yH#ix?bxIr^X=Ej$nkum8B{WSO=Nrnw`K7PSx5?p(6C(8YJ z+;Gsa{TH=F;8_*L-lnI~#pUIijbK1_>z01yOHB^SDBBv6e+6?#t3XF*n=O1Nq^Bc- z&>C)hZADY#43H>sZ6LJ)|K^%hMq*{5El^spGHAzPElF}Wd2@5qZ+>9S-F?oje?Z^R zl%3bG!qLGn8v)hZw#`;7l@qbK5)(41B z@rVU}hQr@>C)jw;MUL;2^NMJ;<}m-K@3Z1g#-(UEk_vUyX4|(tCmXePwq)LyxjmHm z6-E{)`_v+jP^&`bJXI#;v~}a5)2e|$=9$qvg zyDfb{lFZ62Ro-k>{=kzt3)^@j<}|}rHPuksS$x(s613aO`K-7h@F3p&3E)-K?^RC6 zzK_1I)#Thn4PVg?{*{`@7=78FAPap9HI69{2j`PL0BP69NPaP|5N2RRJ9`33gPZOoe_lfprSEbitaNEWpcX!=k=}X5jnL6VxHNsNwCm=%690C;DRP6;Y z30K#DC0gyk;Fdi?>vJ4+;_a8!ZcP`%Snd{YOBCI}I4iz=vIb`RXVmT&&2svM`al$1 zZwJ^e=Cof5reReaI@`9BK{ao# zWts~xT~75sxkvJkbF@#xh;n;}E$MPw&^T8L#*+7+6lnYlj4*3E(! zvH!9?LCWsCuHSGGj6I=z1=?bOh_$MW^0T*UGeBEam2%ecy(kQ=;l;h2O<5ekmQ+qVOg$Dd}@&$Cb_bi`z4@>{qW|IdIB1w4L<{W{9;PB%1h6rqOn* zs})ERJO=x0AX<(eXzCD`3h%Zpwpn#t*$$Ryg1W0ITIz#z1lYADVbvyX>*{DqCRV)R zR)kisjZ-*7;Z&EWdzqJe9p>JU6@AYrQH1wu(qu&`LkRDZrCO-Y`vLKphXA1k4pk-=IrujMJ zI@R6xBNuy&J=rk-eWe5WO~>n&1zD9OSIRz?mVUxtQ_!cM%fI$|n1+kD|EPmCjjkBH zNev&X<*GDuLq`&Mhi~2)g-6+|Br&8Y#e)VqJ7ral`PK8n@MF$wg$aK8iHVDs)R2m{ zKybuC<}9%qEsG{gZ^|;LHhn+!8k25Eh|iY$e#*_x*6C-UqkGxj-fm0w!tT0U+~+9J z5qX7<1yDx>*Gnozaqb(#tTmUaW`u=tgh8NI29i{7&-{1l(*s6jH=^|Uh;S?YAuh^& z_JzB;X}a-B)y`sbJQabT>84w4Q<_De$(04Yh~U7jt|>j9QHC)DuHUyA^l3wxzWz$Q za8x8VfJ~$}eUTVt*SO$cYBvic0^cB<*E()5JJvFco!zeJD7RRd&f$BHw!`oklk=~) z?S73akbT&8v9huXr@zl3>>S$D)8oEf-TxVf#ss46|1A};*s0S$yKo{pa&vubetVpI zE5HJk`t^zR^{l!30DJpEWXEaS-auvWG4F`~&GyKRCQyMEhn%yb?ZjwuM_cCFGacS* zR%PKb+u&4j3H!S6J^#k@_#qU61(}Hgx~V=)q!*mBOj_DFTYZsP{BY-4*tw{Ut{C)L zxu(#q@ab;T#_J}(7$TnD$*u8HYO>yJq=FGEPOM3C#uZJ+S(N)`msE_5MBss|6x=PL zZ+$3d{OZB^k5nus(v3vJpXCb+X7#?`jf6%zZm;(SBgmvnOcGsR=Z4&zSM)39ELXnB z-hhVIh9t#MAALenT?i5 zO{}~FH1X)KJqW9EoT}x)E&IM!GgsTeB~q*QU`FZrxP`%j*Rt@p#nf!hgz_6?J>tn@ z@T~IPt?WIs!&j3fIpxM-Tv6igyn=ztMCsb4hUc>#*T#N*yk^VI1B2!_?oFtQ7KV

pI)TpDHpJDOZcP*ZI$}dWD>c#hYA3MsZRp;#V!Edmq{D%+&uvJ6!Ghfsr8$ z0fJz1PKaeaeweEKwiPsD?(;|e(kE<0jeMR!T0y@7jj_bJa;Y_L?Uaz4jSvt%h>DN& zsxjnJY;wmWYZ?2AFm#6$yK=%jUKx4>_>*eI(VrjX(b z^EI0<4noSaMzJ1;%=FZ!RV~1Z1u^wQ+QGJDDa7 z_8W^-`VNFZS(LzZ&OFbTuT;PR9qv6OVkLek1J?!F(YFi_JXNKSKWKjl>A8ptUX|(! z?Dew|%<4D4_?DSw5s@4N={<*Onbu)2;54K&0NfrmMZX-v0g{acuT z)+4Vje@zp;qdA<)W7K3R9$4`W0Yw}8Pl=eDkekDN!|Gw!G9K2xyzwY5swtwL$R*@* zcDyHmALo$t=>s?ZaL9y5?*k?`zAq#R>=Sod5A*h(px8PGP|HjvLZ{nN-G~hGxZMU| zUq`ULViDom1NzMsWnl+5?>dlS%0LCnI3b&x(nnnE8fHlqjbBr~9yBvJn{03OO=|qI zrEZVbJPfR^MKz6+9mtX5w)`tupjF7bv0kT}HS-&#n`Ne(u4DX=Bb5-gy^!0>J01Ox z4stz)b;O=k0#GA%(ucJDk6a^syD47_3BC;XQkZ+coxv0MB8S&m6Me{zJFD+9;^a76 z>5E?*&nm)&tD_SV0CIzl7z^*v3l}~SG!_G+kD#`7x7gPZ?OS}kA|mT zkLhXWNh-F9aVwU|J~wOxXpk%3+3F_|!?(kyl^ShbCUJReDdfzKPl->MK-pV8sgS%Y ziDXzI0_nd8lRYON?DgSq<|G-Sh7<*WYEyaD0W%(Z<3fb3UN`B){Bp zYRA_XqpPQ1a^Zlq{^5FzaTxS=O)#Ws2?e>f3n%L!ciB62%|H7(e@O@Iv+ro3k9wx$ zz0V(Ak1$FhOsm!*SKzs;7g~=PbMLThB>y?WvG$PW4(lLZ00{BKzGD>voAbP;LV~{O zG=h6{Q8whkE0p!L1X$V;!26XieI$tIWfwn1o@zfJqjbYMY&5gD4Rf{*Ivfl{`kDAa>Tq+`I#4oLH zS;$Gt$M5C!gTHADIK4=!^}LLilJl4jr}5m zL#z$$^6UPH%Tlvr9zT@v;#MrtBmw^3;`Odi1rn!{(A5cfO!J13JHa74JFj}D{&Y9h ziS>}l)nWH(xieL5IJAknl-LKZs*^EqWiu@?!O?G-snxMmXXAeGp+_C;5Mt%9W#35b zS2A4T!gdD%V_O~JCKc+;jxAM^8JsgIJlKJb7reuAH#^G1Lr*D z0&Z6XZ{%GmBki5)EG&Cw0>SJ-jS$Ti`J$HBO4g@Qcq5;d3-?u|@H!?Vqf6_7@?@}alrT2)ddK96 zTt6_wrBxz)ZF*}1)>E7+lUV4ZtD;Xeb#Fu{Az+Sk*W^ARF|E>1bR#(LFIn{_)t+E&)K5R|bu_(|$ z?IEysO6W%g);@m{sAD{W)^D$`pXd7BD*ftAs6)y4bH_%8cQjg)9}8BpDyYb_@O;-u zWJHR?!+Lp}DYd2Cx+eEYPJU@O>|!{KQyZF87mz|7nAP(L%S;w;P8v7PX?m_PCf+GK zW1m>TUiQ>uzU{ifcw8xjBsoAY$hdd(aB1nO*FFn=pT)ppUrQ_7joP}sP&uhYQ5A!G z9Hx~*&Yu3^#SN}vMuw#)r<6!o9oEE%cHj!-XCphoD$>r+Ck%se#Z6+EUs2OWj;UZ1`bN#L-F|p6A$J9fHgy(d@K&@`Hm;IViBw>g`C? z?r-aCSq&HO@DNDw3rA6ajvmZEKA2b;xxE@mmvIvzigFX#Y^dCI*q!3PM9xiiV>@K0 z3Z3A8Eh_6PqQ?O(a_gAz4=OGsUy_1JiO$07-##@iq+oFgIC+3Rf)>|>8K)7J0=+2) zCIW(!8+5^VZ^nK&p^pVFp8^&R87JR$-gWe8?6d@(t>DyiG@lb%g$)gfEpGcd!m(O8 z#&Dc*UFY{}oGpwMl{-HNLanPW=y&N+j!5LPaU%+t9SG+#QVh=-zYhvO``e!hEXIv?S2zNt#n=BNcm zi8;?a-(J^}i?7D}RdNY_x9RH?pKRhh0XnPgW|}ZsGQ-3quUo<;^&xcV8X^GEipAp@ zJniXtoQV#SgP!ZWG7cN3US8Y(46Y15xBzDu!+qg;(8%h?Bju295p6fhk-|w4LQupT zEA|ew6A6dd2y*L2N zkzfz92R=q^ru^OD3Kr-buJk@pUj#{@y0DM%UMrzMiLdeg7dvSWXAyixK4@>N8!z-; z6rOUfB$&vg05%Y{M&$y9P_*$4sahpf*JV^i39n1d732~QYak{q1U7NPpt8}ZQIz?u z|Kxs{Yo9#$E{X(iGRaQna{BcK%?w6}GomQh{WCWY-&}jpWg33}EKlW6!6};6I0qUK zmQnnoL~&!^jW2IZ3}!2L=hTuv2ClEhk5dB-t0>`GP?>or;9*B!lttps5^jWpkV)tg zYZ9J*<$d6Ul@7S^f^h6=b-v-cYOdkoR8O{#!*fa`HG497$&Kd=tOjEx+!mEraC-AX&`E$q{t;^8lA;+f+#6K%K@-G>=f=XBCezQ5ksO zg~-UluWN{ALctWmQiT{)OIE;GL4J(b?=hqyg0*6EfPQq{-C<*_mNAj9DnRZiW2c1S zC>!LrD@O@W1#d_$>g-~h;Q%mKIzKGkzx(!auJ_Ed3jMSB_w?1^)gcTd9g$br^bG`Q zArW18=j}r2)%@ah-W56kgN<%}o7f2_v}$b&y-yxaU|^2_1nLJPZ{2IZ9P5_o$eP%{ z-=G{NhmeVk_n?##X96BzFSl;%aJVuqcaiNX@0 zxX@~y(aPT`uWAB&v5RLeGQ%1shPmB@Z&~VSgc1@M*h2s4A+ z%vJ+??*CL^x=1#nT++Ar5Ixlh>hK_)UEwESv|lbJww-B?Jm_OFjeK>~lC&~I{Ckbl z<5cb^Oo61oRe4%zx;o~zAQ(5_GvQ*CZ@-2k}GpRG*C!qI}?{ft|^;tgpI#hb)vA{JM<1=0|s_W{ez3mO;HES<=Yb zS{_o6)jjGr#dFR@z*qP!)~0n4IR*281b^29uK=6``UipQo?+wN{F=ZrdxSkIHnPgG zDi<-2Q5{NZ@St+(49Eu*F(Xj3h>akM5lBP_KUg}XUz2Vo^dRfYm7CNa)Bm^zWFN*M zK>o|l`)EXH){*~1Vuw7vm!bZ?_X6@E-QX0_ec7dqYLY`-OQP=SL3Y4|B45K8S*Y0t z^94yqu;VJV9Ss?;FWj>hx+;Z?05SS{5%46Cj{}@Sp*=kq>oM&D1%fmD*zJ|7{wq%) zq&ofdi)Tnbvd>c>5ByziXB?KHSI@|I!DboS_%OJg2~#fUjy%^2Gp>NC87LSLI5lF9 z)<;k$mhiA?V?t<^?`mSM;U~qq`HQ7kJbV)-pPiYt&jbm}>zMhDjbGA$>%K%W4yA_Z zH2w*7@$qrUj*p~uGg!XWf)ln))d42jsiVgUjAw?EC_F5oReriVXP)AIA6*L(Oig}a z)(q1EJGhn6EW?g+YnGSy>cA~ub`bbha1*#`ad=nXBhvO3uB9?W?Zb|nRbktA zIo!yR(9!7l@*FQBOC3SY9!pW|Uq0??vR|wsSN6X?{!cH!BljA{Zl=Rm4i(v~YiluZ zO^pdva1|(^twQC?93;NQBJtNR?nq$dPXc3b1ocH^@Yy-lmQ@d$5US1G>$rdAe1J{9 zW_V@du>W{1y=JEJ#vpi)GvS~L5Vn@eA8m{h4a?JFrx-y#@RE@=^&73#7go^g+PsFa zsbh7$@>4k~5*b6L%liX+xjj(kJN4#ag9IkpbA-+nz^Mnj_dz@lY`}H9WDdd;MrYJ; zhhCFYbs25gwu*4@V#sFZZhULRDTjTDC=@&rK}0pt+?QY+y2w3F@Bg$IKF#4Ff6_}$z<7fxs&B?FTWB6Mllh; za|!+jY$C#5X0s7V{^*_+7QUM5qG6@y#t4slElX2Cg62pYVdbC)3e;v+c-*R`#Jh6X zS^r2dL3F7eVcvy;!ucClygiOf8}BTlW5mXv_*za;LXrC4Q80X~7WuD%|tQ8+?6T;1_KxvP(=y+d2A#c&kM5>I+Do#4AaP;&&`{ZM z;6VTh{n79EyP>5HanK(@tQc4>Hfj69?%^?7cv2Am<8gn@1QEV3W(F2#`K$ayHAELp zYGdD-ih~3YOW}T&T;Y9JS>c+f;;#t<+j&yG$4#P-&X40((;#GTg=Hj zm0w(^7*jaIrJ(YkdX#zxN5B9Ia>nr29#H7^+D##@9^a0}=a&5X=fmmU%@|}GuTBIfR=^LP?qvu^xkc!Se3`F?_i+>8C1DZ` z^Eb}Db=InSCC+~}8jhJ^3iZv4JHb(o#7D7+p&FotE+O@B!x-PFsqOH|^ojTEFd{3V z8kIBGElS^>9X5D9(dDE=+W}=5`f#@B+TnhEJXxxw>|B`2htyiGox32B!~eQkkD;xy z9h&T^@o2ZdV`5LR?dY4|S0?nKa8B%ZI`vgx=vR)jij`)74|%K7$9}pb56nq+DOqTr z@WeykwxP};g8mba0a;0A_<_)t&2-E%#+##X!qozh9v>mHx-Cv09AGNxbq9gFN@A7_ zIM_{(O6eBI@AGv!oQ)y-d>z0+=jbV97qViJlP~6ZK|iGBIrRS-w^#imV>@rzjFaq8=$O?;o~| zaZ)*#slEbQ%Tb_9Hmo6@AssO|mw-o=tgHNZ!!(s|ujQCOL*BLpMv#-Rz;VW2;LcG! ze;SDAwpq_Lit-hYu@F}IMV>pmpV?&)0oZj15Cu>Ro+4n7GVZ-Qf8H zb+U59?AcT-3z^9B?nh+7z2f9m$$i|NGN9?sUkS1|TC~em-uO$}4hu z{;d58r9c>g6?7O#@ex!iR_6J$aq%tW2;DXCHHAzdft-xSpal{?3hbdHmu!vG@3j!7 z1*JDpBUthRe+`iMfZ`X*Qnar4kW-@e^7xI&Wm|ZR{FN*fYb*hXQ~oEjNa5n!C-^1^ zZTnrQs>36UAD$+SPmWHUgmEYMaYe`n|0E#cjW1#&Y) zetsXAMp4mDmP@D-Fo}<#YGNP$O7rAwcMFaMuiZ(PXIRF)2IY<`yru4*nh}taS|y>z zz8efg0w+xQw!J7n`FezZx`&U7Udfc8oLwRT@wGWu2Ue5t9f3V6J;$n`tR^|*E{zKw z6*@3tXV<2KUopye8Ku(B&m&m@WOIeB-=3k%10KpsA^EdD3@OHqO?KfVV-Xkx(rJBA zo7U12598~S5-K1v8eI66N`u|czDDKqxcxcpsU797i9^uf+l9GYA>@4w`K)g7QDV0R zF6VA=dynris!p7kD55HSW)m0i#^PA`*hrXkP@uQ^I|+~lxS6tpQ~BV4ZhEIER>=dn z4UgG+QpI%jv(-6xYOjasX1{fO4(;9M9y6b{Sw{Ik5`O0gO@p1AhN#Bj0slMYim-;r4iu$9 z;o2`fymYP!w}f&XZckjPX5Fz*(r96d;m!FrIL|j16#M28@&23beda7cwx;|F(W z8IbT5xy5-T=>Oc8>2(Q^Yk`!s)l7870fbk6=)vikoSV&Va#YjfgQ4Xb=<~=C6G*x; zt+tAg)i~R@i-3TrC6y?Cp?D$ZUetm7R?7sVn#*s~N9~t@{p!17@otBdB48`6~}q4;9i^BwWbJ8OOkc z59#4Qdg$@2%J>U&XA15@sRcP5Xm3Vjhj~IoQ8@6njgjrK7^9hIGfH`+34O*rIbDr( zT55;}|AbD4;)xH@BS{dYUHF(f+Z)~TFq73sGRir7;Ov+AqCXr=a)c@fLG$`gq^#cE zZ8@uWqHC}k8_qmSh?VlzXDvIZ#tq>UCKTNVp0pH%nYfgQdNFZFC8t+uy^NJ#4Z8{? zyCVimAZ;ov?30yz`qC{z*~qwbguOw*{+*E}8S4P1z=U(DXxp6Bs53MMVo+sIvH~`^ z4~18c;)y*j&Kj7&^o$c7EfI>2A`&wfdlNqO9%_NN+~Tq*J$W7(LlD_rD{xcotO?r= z;K5yhz=bB#T~+Km~U@!&t@qf})lj=5Kx4WsL7Ypa;H;)i>dU3@@U~utR<8O2$jZiDKGX4wRn8 z#5J)(1j`#?em86-i4iTFmVFU%Zz3_EYUOq&cCj$%%LK|}Ma z7b9pYVtuG*q@l`RMhVTu!s*j(&FYHFP~#rONKU zX#Ch|S!N059TbifmZnz+8v-jH@ptp8S3iE)|LbXX$qBZkXKLdEo2T+-b$%&F$63x) zGnS5IleUc@F}jb5-G~7!W0&Jkk8wj*3i=o+_4hV>R;wm&hke?=A0k|pWMA1sS6T_3 z0Z^03VhJexV3k^c$m@>JoZIIPolF0%3wm_J=^9jg!wV}sik=L(pBD-FRkN^!OQMnU zy;MhL;utAh#H&tTde_(Q)7g|A(FAiK+WL?JLKXs13Q86|ve+q>d-zI8?c%wj~+-Ef|_ zYEg`JWR)8pnWP|jAiI@z^=`pKfEbp5$A#!Yl&`wJsz+3>UaW|zY+s_&rY*R=UZs{z zEM(iXm3j=9F^(cp)g?ks;~$xEEolvHt!R7FySdZ~pS;wq=K8Lr(2$8+$g^{Nf0#|l z%tKjgf^RpQvQIq3_1cvJ`2ZSv-yVY>$!Mp@z>f^xe6us}MX8N|;lzfOqAqVvI|OsY zTuxfsA#%7NE}VLA>?Ww(7$k$!3^vQ@(bf&V?AoBq@N5L21h2T4nMiaFe|I~z)p+rW zGc$|lh967@q@L6!p=r*Rt9OaO6Fyscz@HRu(NpStHgeInUa|J6Ds5$%OhDlT9Rn%P zXPt^`U(Zz5s>x|y66oJ)A7!i{4F~ViPiO|$ySmMSCIMeU5UvtRkPoN1L<-<{q|hK3 zSBY4H*<)oYq?}rxk0gB6G^TiM_>Ri+Q;CzOp?Gq{G*)`xS(>FgUd4~wk@!1M)dAJ| zHUa99x#VOIu_JYD=Qs6JLzW3&PbrXcD>NutMZ-A!*BVvIkh-i%0G&fEY-g_S+4u0* zo!tQ^({_>Z1^T4FG7`8hqTXw)(I)nca5d`vb=K1I(B634WNXhXG4wL|{OgWo zEFOD;2*@;AXGtET!yGET^9HCBGSv;<;Fx<0R6}Yr;B7LQ!~#E(O4n-PkR3wpqg|b0 z)VcA#;%>&R273}T8A8erp&nsnvBA>c%ruumwyEP@bc>hKQt$XL83>#54Jur6SP$L| z(8)LXUD7(h%I+?9`IoJLS3RZ^?1<$Yv*PzXBjc{uq|w$_k2AFnOWAPz^4TJPQ~FJa zXNhR>55@^>CVNlT3CnG5*j&b&Q<)M&e|yw|)=l3<0KOxiW=#>)Thg6hjS@l!JO&WC z(Wz0AbSOY0tAy|jjEi+&2t`-*MMO=8KX46mZ4fxZ5-hvd?*OZu%;U^;4Og#sFb&k- zdo-hs{mrt~)u8o!$i={gZ{I@Cp(~5|7r~W6+uMW!vo=-2Y{+$>TH6dJ4C|DDkI_Se zlA%FJIbh#{c+mv^mywC)c2L(_b7(}0+jrg};bE~z+X(ZH{KYC7L598G4NGykF!Lq* z`AW2Iac_sb{O#k9pI(2ZotAl>sX&R7 z&6U;{q?XYri9_MvX_;{QB$=MhnTopSNJN13E8ZNpg@nLx-0I5$5+r8qZQl_x#D)uyHg7GyYwXbQA!-(5QiKc~4DK_A$gLcw65 zU5RL(0LcMY^(A_%4Xr7Q&xw{3x4nH;6?7>Cb*717WqrakWHP3LjVz6aWXvoF1-=tc z?Qkq{2f4+&P8$qpuyPGm!Qo0_@T%35sfb1@Nl#rDI8y$8KM6W$le|hj6z`9**|YI1($-u9zkaG9*Mw zwlrT}&Q_iJe7xeLV~%}d#{n`d&cxMGH{&EH9BLfL99fSp(H4S`4?d^>Z&!>Ig}J_4 zGJHKG29W5mw1P0+Y*yXt%p5Jpe)=gUHtIbtG(k_JD>j;X;uH39y!n+pfpxKvN3#}G zdZ{^7d#`53=9{TC*XT}_;R6!PbXlE7dHkXkKPYUzCbT=jyWe_8z6r6O5n>71=0wyEYt!er zXy(P@Sz~5Nqeo{VHPLxh0a+RF9IOA7B3Sh?_9RG3wzHh~6L=i$i}@3Dm?Cl4@h2>n z-q1Tg(?SBi;2a%((-733ltq+S+~|TEx3<1z_q*K%Y<4^ocDznJk7E)!%sY=bkMp3( ze`xB>J za6~{y2zGi|B8x{6uE)HtWUiEP!Ivx85vpPJ_{(qzohxf^sXe~x9W5fKs18VQxY4%j zT~_r_!>*wxlQCBQ^N~7a&aDXMCTa%CN~JbS0V{KS+(;sYuh(9sloOTp4wC|Bbj&ra zimk_BGh&Nr#gn^@;qmdQQFAVivj;(sVF?YJy;NBNz<{iPEUYXm-%%`?g!@Q>_P#`_ zIdNBp>vtOx>r!U7tkw)~KQ4u7j~qe(f}i{dyApq}BvO45snd_C#p2|H^fbjEQcaTQ zmck!kig6x&Ah$(SZ=nFes|=VTfk@v_>a1n)v#x`PU(bK!){69ENh`J*b~RD{SjGqk zn2-;XRItN;g^eu5`C&Hvlw5ybK_*pYTE5}jHkqru4m?fd2t%ji`6msmu1+ZWjo3;Z z7Btm%=|6npztq$H$RUrDtoCG0jhAFvDmU%TLc#-1PpGJauV{xo6|7mOAfgT&^WaMv zF(;Q6$2HYQJ+ZCWNAh1;L#xq6QiJk`9>zbP#tStt>KRLpGD-c|oOB7Em@%47+$o;` z1x{3+l|55OeQ4Cb{CTLi$WTA>nzR_?Eh*G-m@bZ>jLzD3#u@3{1tLx|g| z3o_8rC&cgSgc6@6sX7tV|2fZ6G!P<_QQVQ>|n zoj~PhR~KE-@vApeWz#!#g;2X@k6-)^NFn8S-BlnxaLnD-vPkTeM;HRk$7lJXGY*jg z-$6bOrpM@h{MnxC-;ME*3T$-J#xEGjm|<1=SQMv*c@;;2R8!w(5GwDj#bJXi8M;M? z>8Fsy^SF0E);Bofv=+k4hkb{`sJoqaDJPVCw4N1kCtwXPAOzZprXD&g14t-4E&G1A zK9Nl7Ip9D&e^7a$0TBi-=U)GAW(Jn}E>k?@kL*$jX)sRQai*RLFXQhwF#lmWo%|u3 z`P^OqQ6O8Yn^5ujT-8wUyalCuUQ^$MjmbGzI>EJ zw0T6g zB?i<#q>Aap{M`{UB8$paGAyYCA9oD5K4a%)m z@>OwAr-SkCDS_m2%js9WJt7sTdE_KI{AY; z#tY>I8z$lpw$XYyi;|4;N_jorTt>4$J%2x~Ph8kRLW^z8jz59fv)s~9@MA>%6wj;j z*`>V)&ca*X>RYgl$xkeXMhQKy63b$+E!9K2#fz}|ZGU50La{K#+#IQU{pkbn#Y$Bh z2(9n5soo-eCW3YhtXk^P854f@dPu`Aenn;VTfxBz?ngQt^? zAumBwX#Ap{8cJl|CNy&4g0c$$kn<|H$W(!v4GtXY*&Mz)UM~z5i#yT~&U+grNN!tO z_^1{b0DiZT=@(TP&{%UTu3!bgp2QivzdI+k?38`Lx zM%@5J&bTKmdixNd7VA)v#!yi)!&qe#F0KU09X^W?)dDTRr_m_l*m40?y`40tly1a0 zYu74T{hlnCGMR)&(~lTd_q>@bQ5^?Q_guWbRv9N8xRi!e=`)~444AA-UA-y#nx%^m z2o34-{YS(JwZ2Sm)cBiX*(v`zCn>vqyzfDf?zGT z)u3T#1IyB-dZeH?#WJk3Sco)&1%*Zp;wN_g?)36o3d1u&1YO(Djo-}Cc&KkD5jb$CzE@_2$}rak|rC8(^sh6?4+ogjJc zXtAgYiN-$JTNI3v@2X!@Wql|l1nhZTJ@C(fi~za7B(&y;Y{YzsE>E-`et)a4lDuC- zXdG2_eL!r~!@2H9I9bIFhxkTkA}C zH5gH?BG7TZa@y=)khd=czGzvD)?bp%cGLz}rc9w4E^!@*D)rd{u7?I$7#c1ky`X3~ zf%L$1;2+QrzIx{)h8BWVNJyizRsG3~jAX23DMmxc-dM41oFxM~VE(DO;{BVIf3Hzy z?5nbVt?;QG7rNMr@4V;HIqcJFkBxQ2jDaY0t>5Ppww>7TF7JP@0uVs1tl!(gH94qN zu`E@n!65ifywtNK6wwiwRMZbmz>gPBfpQLL0tCTH6Q*W(h(30m>%@s9&0F&7yIcwv@P?y zWv9mZ58rU%umYB*1p#F_V&hpYn8}{1yUd@bKUn~<*dHaa==83@tKMy%KcKODIiZH( zFKn44zg#>MCQao)vHX`bObR$Z8rm>LdWZghuwxwPn?lcO1pW*U37bNL%#wCStC#-} zEen95Z~p&e$f(7A_d*`!LY8P9>UTW49}LD{Jn4}EIc&=U2vV$B-URCsq&(qa{$Q>9 zZ9oyvJHzx}G5=IVZ|!;FAMYQ1aD4LK{>gjmCxyBn)a#A(AAtR{qOiBvlU3oeq@0kn zRDyx`6fYmKv*)Q#7_*k$BBPt}tA$AtbuetT#KdH!arr={6wFJ1Eb~%|Is4XPfXS>>0G92R+Pwe`NE0$C>bj_Wv2u?@?(qvH3pc zaA)A`Qr@HR=?d*HiFd6IHCA#F9a8carV|rQ|7}JO($TLLCv7`+EZr4;%2%pN$;SrN z2f!_=xpE8yK>tCujtZcs-eFk`U+tEc$`<^diNGoIgChw#PknW?UlASBsSMElYm{4T zvM>`)M9Chm#sRjH+gMEOPAz$;IOESZYgv^40{sdeqW9U^p0*DvQQ8M*nz8ZQuhg^C z#o{qQYa}POKRxF7I?MjW`gQ)M4o`~|Zi&#TN|rStu#jWy75)!qx$Y(UDetq!B`E%Y zsHu>VD;r}t8j4vUm}$w?xXxm`^Y8K(MU#H#9(!`Dm19wFvg4h5Me}pik?!>28aUro zeGczgEdJgo3JTFg8OOCW;k_eL@;I+KnV>AtP(o!N!her4I)r{IX??V;M+}>Lw)T2{ zwZ>*KQp)a7o%(aBKF(&1O0fnShXqmLi2wC|HA9^z+kSlc&I^&dI>{L&lN1i*bsr@_%{(Y~QFJI;f29g}82* zHQjV!{o2*gN?8sw8J%Bmi0F(ePy{)?W8oA*1=Lz-cpPlBVFNC*Er5%%*lGR8C! zQQ`q6DZdC!YU1-Lvb{{JjN?Iv{NP*%k+#1g z7H#hby?D>$`oV(v|Ln*1xBZANgT}}fEumr&X)kqB$jpK|R*M9EyXhoQQ-wtB5v~_= zBaPy_hnyQOX^B18a-?k*IXkT1YeK#}bwwq*%m*v(&OQ{#lyNI3zJMlZn|DC{oid)N zHjP{zElWt-syo6>uMD54N?r+uoi#PpS#%kOi1N2J)$#3&e2VL0q-Ff{IWCM*h1bdS zPNfxR-6g9G!kptK1l)ewW>I^zv2HvPa=8x=ygrp&OW$x#KGb3`Sh+Q?m}QwIdo&Ou zAmi0_(oAr6GuSb+KJ(V(0e48i^y&|n_MjJ304d+L<#0-yRaf&n504gX+?awA6=FUU z{_CbdzyhJ&P&HwFu0c&tUEs@U~u-p5JR_n}$UdgnDYxX*QPh=j`uv!BQ-;Yp_L*-G8>g50MPnJfA}rsuW(=KWmcO`LXs=eM#~VZx*&p(-5&X zR_@4@{nBOe3TjGxq~(xt@`-m?0(l6|zZvNHRhJsQ#;$E7c(Q(VSa19z1&i|68(yuv z+KL6laq`t9iR2IMkwq?Taqn2G`52CPG*zrylC9JTpVO?|pgG;AGx_=^aVp zu;{rQTZ0f|H9sE`o%Zv!)dDXfY!rjN_kyQey;aSw-g|j;gID(3GM#jLrH@l%#!wf^ zkp~OatqiaD_`sO(Uk-cooWQG6np2+mgC`n08Y{u_Y~DXLoUcSU!x+0RZv^o^D;n8C$ z@pZ+n_1REpP3Sv|h0b*BwXIp7!Mz5Cm#1y}Zl9k^$E#hqKl{+V;a~I~X?-sH!dmZ; ziK^Z8+Q85H_I=<5+3Bhn3E zS6#fKyn{Vrd(TB_t# z8H9BEm}lPN)>39V$2l@rCx7s}5d5LTm}U)g$GV-*HKt0yETFH6{zt-bJPpIvyIL2V z1+Pu+q?HpDpS|=&mxp{IUR?x4S8X!;Z2{?CcbsX4c;YW>zeG>+A^y+9$o_^Zo-2md zldW*tR}-$LM@X<2p_@B+^j>=xx>6#=k+1rS3dw`}f%S>_R`a&_h`gAhwe|9tAH$|r zHQs73O#G2_Pg~v_Ry0|?FbP-;K5rQmI;%i|QsgA(?PzJDIiB(tQRB3175;rF?ULV8O|Ksed!s2S8ErHzSb_)lgz2R7&%N_H&)wfSb*gr)y|&aU_xh*bH(P$u94{Rv zg}*=N3y|XFb*kpM&%1(vmN->RO@im+St7sL!c~VEaUSjXiqj^Am4iO%55hkC^Pk^r zjiz9$u6_F4>OKWr)ptM0oV?$7eKfL9g1c87|7_$QZY=VNQKFdC>2=iL7-cA5V92k~ zs95=%;=;mkSYBLf!aog!;e5~@n;Jte((XeF`;} zJSCo_d);Imh*gJT`v!wOV^?SGX&GCJazHS)VMz^E%BkvHC zr(%5?ohrRn+H+*jDK~qmzO0P-PAmJ&n{l=o!k_Jr~$X(F07lo)s6li#+Y+SP~j5r*7oEgAX zRc_m-Y_z^l!qP&eR+K7}vmF57?wB-u`|STxfP8DQG{~dvgHfq~r+teNwBPZ$E_sqD zKpmg&_F5>Q&A@TXTYKwnGv$ZQ%mA$Y&iYln(-wslQIj@ki}z|g)EX3DjhqmtWe9oo z*s3)}XLQLl#ZP#H0T-mv^2Zk5?V^&5+0sOQIZ;oRSx7GT4WKJ}`DpT9;s&8@#1GvL z{-dVdnvh}QYp+Y!r&j+4dqV;0Hb=fa z5v!UUyP2Ae`0;qLYZ$+|a^O!Qo{}fe=}XY5@S2)4ZRqyV)`yzq-j&8*c>WC%t%RAW zw{lPBYr=}RY`KcSE=C@S+AgQQZv#6VN*mPquaBBIP7msG)fNwduTO;-JSG->joiiN zsx$||CJfT1P#(w+xy8BDd&QQN#k|8Mz(0c|PmMUK_@R`}dC9#l8tj z4I^5$rFH%rroJDWk^3AT1^$I;i(;VE7LhH+x)rp_d)wI&?dtoldS7NuGx8dAB?xB^ zmK`EAEIAC8na)z&l(fsL=cO+ocA}|qs3VO7nQcKZrY}KL-Xd(Bpopiz&D~A*Wx&D(%S&F#qj8kkhdU3qc(+AZh*SU|w}^N0#fE`YJPD6GFSVZ=Yu77{YP z-o1|Z9)HbV?!(pB4Wbl1A^}pTz6W!iR%*I6_4$RLU5@SuNjnlPR9+R&o|{R3WAlQO zKiVh0<4H?3=}>ehe0?9=)?s>!meIA*rO|~u`Td75y>7k6{#1J&7ZZq8m-m!1k-yF*euxfzo(wN>ff7iRb%*tu@)Ir8Zl1Gpz_zvy;7wTJcA$v(aZLoxq zu+^VX{IPwRHrAvPP>jy#8s`tyYdqTzG zI}l&85y@U^c5w9c6z}z#WPB{Xj@Q@=hGIb?wkfF(XRaL^!$(8n;&+uU?FtvVgJayL zmqv-dl-fsfV3Ex1&LxxDrsV#Jop;{nWi2i->RBOY9wP~Ag5F)xwA1#5ON!C%SA;3Tj;S-I2n(Pg zRpj4G#bH&WpX})x)3{@Mkz8xGRDNtG6&(_G+M@|1iM0Y+@Hl)O9L555^Wxz54jK+V ze3wF-R65_Hi})OKb*fO6iUe82Uv2nFZH_4zo1Qns`QtM75 z?KL6p1jDh+1jvgO`UbZ!8@6y$jWG!wF9)BUud>jdq~BGBc~M{NEc@?1+y`BDo6pm9 zeB2;;+;Z^I4|74+mw|2F_X4ZkpuMA=#YW`|Uj zr`EAs-EvhDCkK=K;MZ_;e4(@=K)L`UW_TaFXTEp! zJKRMW|K2?13gn^i%NZY5{Lg>r>4&jwA`3o}vCR+0OzVCor8^Z&&X{>QHQ}*X3amEZ z6{>065V=GTiimPY*!$C*R`+7rA)~uAs>wUd0k_K&|q9hsCx-f{3bb@ZGl##5W zvFsRmIIt!W)sxlSeYTtOy>+glv92p1zlu5BZe&_)%MqKZZZt$ywc~@n>nLrj+FAhaTD66RpRr~2E|rkPi*>DrzC3^_jW(s@J6M1;-`EJVPe=-em?hk^fgMK z?7^52et4(dHRQ>MapSoe5605xzgRl8^BlEuA`+v4-*?tQRe|6C`nyWX;zwatN9rsW zKP#zF$B%M!RV&)P0 zlxv(ZEYO0Z$fB!xHK-UAPfa!hNFm;M+ef`V2xcXaV|s7uTu-kAdH6Mwm3|+R;7kl> z2lF0o-fWH6cc{^W4NY zW-wk~9^YBW7;&v8nhN!MmB-5>w z+pdQHvE3pl`>E8(QNzDeaquAnjp*Vd3;YN4NlE9lhkm3l(1i7i+ih0*cZ69<0>vO9 z6;3j?(&%Uc{?#1Km`g@MzZP=x!cdl7nW0?Kj3{3j{|_sIUYqMdUNLVmzCh8m*GTBa zHYhWt$X6;mYd?G*YI(eUgX`PA0Z_b&F#2G7NN|ikNiZRQEZumx^1=VV+@af_OqVgM zFvNfqw=}5o`F!VYQa#jibOu=beP!oS9zg257C4ekxWBsW0MZ;1e7$C9Ise}Ai{KsZ zB%2b8pG|Hw?x^#MaCry70;H8Bz*vhd?aZ2mc0P)^?-_~H<2pV?4!`yDwa;g1wG0zM zhbJ`!W%6miWGST4wLr?dN#?+VKZ`4sSu&hY+vj7@yHWK4Wi-a+62i#o%kM}DjQvA* zy7;e4VN{jlXybtoOWTef*rWJ-;$09KjVdaRoOaW`2n)uBGLfz^(dP< z)jB94=VNF8Q#rsMZJCWeY#nJVd~pcD)=oF^`#^iq!x5&bQxAdm`&0e=5mXE#>vUoD z+hZH)7l6PuUM-RqKAGA!h?<>8iy-YSETeo0W4bnT-&@A8P;eUC<-7R3=J#xIV63Q# zdPlv2OE28?xpX<^=Qmgk^SO88jRJ-@23?W|WuV}(RB5Y#R8Jlo*!b&P3FfX?&ox%N zuyeq!6dD3%UqKxDMQJq2SRH<%oT3rgyNsN|ok9BgM5{MtNNCsX)iGdP=mOz_N57}o z>7XrK5N1X&=MW6}6x0B`$xlt=Un7Hq3RQ6GsS$vK*7s7WOSc`6^NMg4b;}Ouj;QIz zOxwER6pu?GWVF!lMdx5>-0!j4t-gUyr24{L>Q$&yWXaQO-$XnSukzIrjWWlF^bj1Z z2f@VEkHUR)K$usw4|%$nh4n!EPZZGlPZh#)-ofPS1wt<3V~amh&UUic&H&^mV-}Ro zNTdz2AldwgZ;mp10;o_fh}&kNTOk|bbcn3vqWpU*^3Psea;>Bq+^R5Q9_i`h3H*!rl+i$=_b!+4B!2KC z-I4l)yDNcDm9tC< zG~1JyY+ls&fIZO{GSKV!TOFzh8Z0op?J9)>_F7S((Db<=LpQ|^1thJi-jR5a|4nuEitySBS9w1bX5gGMT}fVVAjbMEWuPM7*QIa}&WOgumAYj@;HCZiK`;Zp{`fVFH|7%SET88w(_~oExVMYQR znzuMKX;r~}4T-|5(3nx7=JS8$%w$y1BfezZf=a!A>gJ$~7B_!-Lp6dazAJuqyM9Hq ziTczw&O&2#Z>eGY!D?&wTs?%Gs7|m6fg%-FvMj}t(*qh+DDG~00sz9fdXV5~$!Vm|pdOwK>Hj%E4lALsa6xor63 zCt^%Y$^PXi|JEn76+n^VTBwfD{}lKVh3WrzbJNUK>Hl4@11G*smjA=xKaWO7`jdyi zr{w%QoB!d~8VH?p6f$rBN2(G6VK^AHhVMzgfY zUu!rq8ec;!R#v0(fuU)8;?&*Rqb2%v>K2eBoo%x%K^`>|Rq~DL;gfs`d75-3@vYD; zS4lY6>!>J{${%aF#V~#TZ}6~C=>62_eH6vyBhim#{-l8Kg7LD-SS$s3r%8xY~E^Gi{iNZn7?p-8;sOOaJ0g$o-$JEjDc(ztlf z|5G3RLIE+LSBuZ8lbuXpCP|A!pBa*0-wN*$4=|LzDozF6g;E;I% znxr2|7vOmGM|1Vxg}G2AFg{CyQ-26h7bM{`%(x2*sbwX`*k6!8+z>miKL`Vcf;V%{ zlO&G(brW+WJDX_Bn9r*(&7Hu8O)3eo%Voqku5BY4Em^U*iz_+PHC@K>lT?0gCk*tm&_GZI|-PrI;b1&p#Nn9P+p2b?S#d8$#o?d`Q9 z1sJREljvM4Vv#N3N|?UNtwh7e%2efmysqsoKhfVA6Fx-%9A-vr| ztnkAgB?&U;0DnWS90aCk89$j6qM2%S>Y>tRhnT|&0j4Zf30GQ*g@AtQ3d#^@zoHaR z34+Co7Zo7VO-eops^V+uoVJhGI;J*!ozqW^h{TBjfy`(tv36v!b6 zV=HA?eIIO4!S*;3SN^If&6t&T&?5)qlAy!;hi6#D)A@iyn^^#4Kw(jx_y{*Siku?u^FT<0Ss27UMM90cC@|kNfr1|?!}6F_-DI9dtpt#Oa8vUJ zwd%t>eXaxugqucFdcZ&jAJmV%Np)4MT5DSP9ti=|&X9ASW@0&U(p~{ljCTb|gl=-TPqpjDV>QxT#f4di>qG);$zFQVb}`rNpEc44klF!I8Mc(=tD-WO;IP zYCh_aEXdm|iY?GS+!OG(2d8v};h|}lq8qV?+A?kO#30r+G|2yWGH8Gw$W9XCA6G$o z!M+gx+M0q!5n?ka52XFoX;p2yS(j(ipc^AyHk)EiHcbVeKr+t6!kpkvs{$4%uVMh) zbwr6i%c1BTK9F&nj|7AV)rSV7Bc3dI2%tX`@pI;ahs*+nJ_L6aQDSV|V9?S4Z1-5f zl8!ub>L%w}yrtho1ks#YvN3lsj}htqx7Zd)i@L3A9+h%=&S`-CfN=5V-3m1Gy>;Z99h| zXj$mcywX(jv^Xx5NlEIgH1JrkNL;um=~dyWv=k!cJjIoMC3^tU8ssVeK_$Fj@>HtH z@+FI+oczt1zvz)nk&%0C!IT#p=-JxM^RgV~bfa z?cu<1FbUruW?IcnWP%qRifsO*aTRILT0C52G?%`V#O0){nOCvFnD!m3U$pBN z8(EVhoJv!9HY1E!774-lUBw~nbx-uKM`7-CS_=Xi!uWa#{nV=A#!b^Bg?m~%&l;F> zoJeZcWFdO#Y0RGF#H|krh&!4}uSF~Q=hhfuE*`OLJ~DqH2TutWOqMI47NOv{3Z0A$(D|q8 zk@(T+DnsW$N}#WiJkj&|#=Z-p2<}2gjY&Fay{z@&-bX zauQ||By}exm^&Gh7|!ukCyR$_0OHZ`q%69*2{4d4Y#0m|+EgDEBQ$_%ji)&TQ3W3f z1?0=a!aR}sG5tUkc#fE?HSzBz0YF2k9oX=2yHPhHoj=X@&<<-Q!dSO_3nV`8%_`TP zz-nwqn0s4)f$sFC$m%^;mDoHqTAc>b0Fg>kVSW zL=?x5+tFnM5mm^$+)_Vxs0v5jHGaCvfDSh1z&0b%foR2`h*id)F&5;&_v<37AZG+0 zB2c=KCtf;yd_@$&4zWx%EhX>zeAm+R8aWwzVey4l3bB|r+>L#Vhh#{>n{Jjq@2fUb zmlsq;`4ZpzGjsFn-63|!8l+CT-h8u(@b|u< zN3x{%b`MERKM6FW6ts(EK%kSoH>mk9)DngWb_RL(;;?dEj4z6$*YD^nS}3o}Vdm~8 z#?~2QBO1jT*}2r^n(Wryi`Xe5t|G|jVlO3)6$0G!Ep4Gj;tW6`yPGQ*bUx5S4!3{h zYf_4aO%@VgF-6|q(D0kgp^R-{M#0^4XR~e{@uVm1T|B2s%{umJx3U?_b94ELALg+YE$iLyG!so{SOpX@iXRj zJvU&;a0zP9jFwV$BJUyLEK#S!_w9frqLtV&7wNQ_(BV^QEm{tz z%Ln5~k0}a$og%Um-+w888!c9$YRd=B4*`gGVH=zuFfhMg^rz9<*j93yMZlS9&zj>| z7o25r1bOIKxCDiNUq}^bPV{Ugx$a1T!YPT6hWIv#27Q5h3X=g9A&76}YOA#HNo|N$ zP$fH6?D1~Q_Kuj3t?aV%_tNhi*O>G_tyDa&MBtrMum%lC{o^s@JJ5!U0j2|uTNEJn zmr)<=H4yF19H3n`xsVi;m{?MM`YwG-UCG*k_ASL)z7X|6G)4>@i8!&&*EUtx3VUm0 zZ53{i^Bme4*Zs$R#UTdq&)-Cv5auTUk%cxWI!I(Fj(<|o@1hTDYe&ww&_;>}vwy2dG_cwo z21GlQGRNV<~k}E9`NS9A) zJ&n~?G6v~;wk@<$(R-j%vg$&6>X@|b+u6No$qM1bQRIr`Ma!Cqd*yNP8)9G*$P7u} zIaJg{J=S!4PL8{0VF98fPpUo1h{gipfbcp7(4pz*Nnr5XuO*a+LPSvB^E?Do^r|sv zA?=#N=s)OSvgj2jnv)U=sNJv8$FvWBPR&$#XUIA6@*ACmD-mh}K%^WR{1NXZAnF#A zrbrQYCf-6KA%vNudmfkfWr_S62%cfq18? z5{z*U?E<<<0vA2`kg9s`NL*GYVaJgKJ#8r(^Yy@1CpLO+Uq1Z0rqh~gJ;a^n_XC5> z!MoTy3M}-ydOs?ORKW7cOXw?6%SKNAg;Au1oBYXrL#>CPi;xG%7kqmE-&_Eku+`rl za>t1OfXr$U)!kamfi>e+5#O*VYDI8gq;&*N(KcV&6X&ccJ^YHsY%hc1BkM5NY^)=6E*Dsm;t50)k!rN!gvf&}PtbfV1*&1MZW0v9D*09cCy@l-?=8in zhwY&;3=(v;%SjV5Rp>AD7!GrO5&w2zJY{?$4kTDP#K*kjLDhFli%%OZ=?9m-Ii#YK5I*`}f+191fp^wsk73)up zbWS=1r52D_Sb;~GgDtLsrFcbnJ3HuiXlS~}Y@yCDcP0y})n*opx&gCiKlXoCYjf6& z%Ph^vlh&}3k#4>dq)KU~hdVfK0g+IAY@L66&>a5s(`HSw(5xw87Kx3inEVk)TWufU zoFPr2aH$=`p%Kv)B7yJ79ciyxnO>@^g1P#V@0u1FzTN5Crff>!$ALAlpG5f69u4CgsTiRz$x0Hi-Erm+P##2~GlnZZm0BIE@vd zTN{2tAiO0Zl4epBy|9KtebrsU?68#pMY^EukY?76{0h9=rT$CI#8@*WRjSjKaoM(f z%h$mCk1rk2Cy-$-%!-@QU`|B|f^#!gWe%=|DfyL*b2Uv<^D?pV761$?I{MIfbSBe} zQe_(2ZIcHK5APsh$+wsCv{j@Z0Os#axfkXJz4k$;RDQ)8zy9$C1(P6e=&>G{<3Ef( z5YzN#KIbTCs9)b4wr-2|opUMaqpS#}teP!GE`e$d7X465T+>m8eR!xCy_NVGGnxJ( zaZPb6?y#91;+|(LmL8b$q9;Q5#l8uSp0GrBjG6b79j}naYPt;gm5pY{JnY=7ul#!` ztrCN;&!kUq`MHOHji7g22ec$~bgVHT{*uWS1P-r?;ZHMt_jDU@PVk( zMrB;iB?$1Q;%R}@j2b{tI%^YmzQuBXN}UvaLg45VvL!B!kog0Jhg%`@`S&?1aG&@a zgwyPGvI9h4sbUP>sy7&19dc#NVkJ?9Q-N0zBPRiU!6R;evO1#W9L3ZY;iuPPoc97R zi94E*eP;uw1%P#_HX!JlhyI1|ci=F8I0%Amd&DGlbKR4Dci7lc*ur-~XKV8=kOM#K zkWx+!s7g4dY^OrN(q04b8aIA>j^jrg=H7;bxaauJBCo@8rN(=kE{PE6$D751$5-alNgE6U(mra6_}APa9^F6i zLV}Z@UW=Xd7I%BpJXuF4`<I(9i_C#;^-?;bA#(*vd$g@=dC4#3Ff6Hb6sv*R^ypxb+g<5E^|Q zW!ICMtDN=lqb|s5nlUT_KwIOtk9$V?gh7Yif1tb>P%{#RZn>Q)LIEG)YF6 z+UAxhO54gSVmEok2rCkcXF{1tY;IB?$afbMnL{E@&0=IpD`kQM$^@vt9Qov&Hu&~L-iv>Lh{^DCmb8pNZq z+=G)$x!S7W%_aq7Q1VMOr!+_E;fHW~kP!EZ$Sfkj=4J>0EX{SM_>Irc^_QL{+S&*< zhh;AYSlKFa7L$%N8qsg=%xmI!X^U~q5n2n6Ba_@u7r4F+b2&K0u>xl%l_4M}*bMUz zHPhztIT#`IlNguS-!mj@*@s*C*5plCvV0;MaQ03uYM0*ZnNz3@ZC<_7(8WMUG;`t? z`Sh>en1mpmcPVTOPj3A>OR6KrtZH9RlPXRqe}RHe z(&FS=vY`92Qge6t!4WnhW#Y89Ht|nMPC8V$4?NWP-}s#_j(+Q?Q70?PmMUx0U-K&g z)0}hmGo@y+%j@tzBO>m}de(AFf#U!#jB!8+(Sc%85py2ifT$BN?aUgLlXN$SCror+ zzZ0`35smSsUzHlv3Tdcnd+f`Bvj@7bMPFwI&r0quGgk%#CEZXzsfl45!?@;bA7o<= zD+IyfXA3&uZOOWjEK)~lqwx%Vz+zI#G-`PVOxL<;!bexlKU zR;ES|zfb6xerT*$4pT?~op>rdFH^)dhc@=cYb5#A-j>og9Iec zMrTy81yGOyFo1=N%;EFJv2QbC8$W!Sq{7Lm&3dsLArV)lcJ&%@lwfrWX0;8+;3Q+_ zc;8u@z-MI*d@m*_7Ui7b1>8~iS?X&Ik1x3RV4F&f+dyMC4^Vlk#=IttJgW}Le6J<6 z^h0zdb8>SO%^dca-{2)=u)SoPe3j=flMKXzj|7~=-uF*O1I*}PATg}Yb~GQXF6cFh zmFLOuW#ihZXW1Xv)if-To`acg-NW= zZp@kFf_h0ii85Cmzd@8g(rfQxLDgLt>32O*g~&`3F2FP|W!PnPr02iTfCD{o$m2>@ zA5pfQV!ASD?}J?;wXsjRh2#K18`g1uE}^~K=L7N5c8jUhSN;sI36s|8%#!`ou58oV zZg-z{o%gLdPAO>An78h2hjC?s!YyIhKL{`LetQ3KBM!UtEx#lJ@0)0Hfa4p3D8UT0 z01fg^0;-yMEYUCU1cd~3M~XKjwM8T6>`~Tj5xN-cFG$&eB^t51#rV>wGO$;{Pqh1% zQH{%R$0|STX!cue>e9HVa-)c}ibr?z=BkTE*@W1T3=jGFKn{b7dkb7JQ}%*(wTK)W z4U3!V^W81Gvi@Q*`u3%h_`S&lLGD(Y_UT&Zo?oqG%#bB|&A@jUiI{8l;n5nlEd$Li z9+M3sTQx;&B!^^G5;zsLtmc@RKJk79dW5zwJS#qhCwV8Jn=RZZM*dwNSs5JIt(Yz* z?ZS6d#9WS;3It^-jjFB;Kids1;y zV?-G-^48jzjv9*CBen*YhUA}I{p#maJ+U!#s>|kScE4IEz-1}o%h84r4(C*LNj5C^ zRrpiKPKsoifdeMbXyJ3f7CfxM?cyzVvX?yN4}@-V)?X*iP7k<0d;<>Be#!p^UTe2| z?=f4ZO`3>_)d*FhqT*J`@Q!zSX2dMJHDW51s*FeA@@OBXNt&Fd<$mJ_J4v9d@M+ty zs5~K7rX4@=jVqv9u~b$5hcun8`ZI0JCoJ zNDRIlMc-`$V+PqMj|+WK9}9ES>&K3KLc~2+tRJ3W2+AY5u#}bPIBVk%G2X&Tl0Kbx z-iV#+WuXh-+7E;i{IIXjHYnjLoGAV9n$l+>gYY7d8CL$Q z`Vse)5feM28Ph4$53qawh8Uzj7XtmH_NG<~+6|^33N~$Jn>G$g?JKo7f3%ZJZ9&9F zO?w4=Ww&S1JJVlUwUip?`xm)|?<5Essn$NzK6>paMD63Xh3f*QNer#AM-5kDrokk? zp9AXAcdziKe3B=lmLzkmKO2)(b={*g{W|8Hq1bJxEW`L1Ha!%Vk$z;8$%bk!_mIJ^ zS!1|Z{VXLlI*b$JF4>4ap2QG+G+9yQN1e(vr6G5ylu7q2RqQ>u?xM~QiGlRyh-A3N zQwK)rAb6u$A!pwqMnR7!#@If)v@A4ZUyP}Q2Y6hEhQOy`sghNNOJz9K$!Q>Yzo|*_ z%))xUcZQhFE-I$ZJQ*2C}3-Moi5%D*|%XTbs=)9)G-K^emGX z8O3m(v5S2)%xFk!(ZPphJLkVyen7 z>Z;wdv@hY;4`0NyTA+{q9n&j~@Fw&9kg_eb`|_D|6|5RtszM(2SY`$C{pcdayaP`w zuQ6?yKzGVk<0U-@bR5L`5QE!#imIRn_GoFzWG|R_(8dSC!(EoYmqt+`oT>LAA&b0o zK4}i%vyGz(u&(njM`xi=I*-KA#C244)G<6z!cDn)E=d{G<}Leu(n~!(qZ~p=nJSPt zSoxL8@R`E2Qk(qO84Wg%A%s#+ad1nO!)L3-wS?W1bgJBs|(p zPf<6@L`Eo4l)?}@HF?@pG{WWqofYaBB4^6zP8ho3fu4%ZCQa#+N|K`!*IL4za?FVr z=yt>{e?@M-zY7YP0UwDkyG}DM!dj*wzZp6z9MZU&y&0;b)vXmJTZgpwvPm&dOM~#3 z*#Ix66XDB!RRqYJ(jSTD;K{zXT+~}m6p^ecRL!m~(7-Wu0o{i%*shv~KMlT)w%`=n zHEFMzw>i}npJ&=B5I!@KXGu+~+l6mWfL5tXHPmH|m==}$4Wz<*`szz50}64IQ(Avi zut>qTk5HH#sVnm#M;K{7@&j-mLN^voDkc=>QA_k2A4F49R`TuZz9=F3iIX^yZG!x= z6oC|bwMml3M?gV_vtv%d43#z$3Q zQ-x|b6;~a8BZN}j3fR7#p?PPY3fN4wK9K7f9F75Ftu#PN7zibIbXeGo=j$-pN9QVS zD`W0$@@cvxE#lJjrC<4XZh0$)8|Q~fGC6$%{IG_q2cCcTnNnaTidnk$%fEQz<>YRZ zg*LRSjPLM)sIG72*F3lB_vTH9s#;W{|7*&WE1+E?uIyvi%z$kkm;3MSv}r4_!vlcJ zBd6Tnm>{2+_N3e;=!BxTNbm6x7kjjJ_s2IdXs5{wv&QCD*Hmx9mf}lm0;T1u&Nufc zcJ6G@wpH-8t`<(?I-FFR2A86g@w=k)L;j)Ko1pIP%S@c1OxK#kC!3{>62j(G@;p#b zRpl`=*}{srhnaHsyjjy>uzS$p1GcvWbmdwc9k{QT{v~Xxk2T(Ua`gl%+lx@dEPO$E z;zmnSQ&k>TUkwS=Xk>raMh&i>MSR-(^S1t7(%B5c{&JiuTNiFMnZwW&b&A3&fi!a$ zfw==+huY@b?{gLhZ=Kg&GG%xEuMsIbK*Oqv0e`e-kC@cd!72xG#!-OcvgxB zW|6BS=WA<97717wX$;-LcE=pmZ;C$JfoQ>?c4vs_TOIe2B<6U!;k%d-B2+f*+a~rw zDB{wBu0kcmz2sesAK>{*fxjVt#%S8rc+bMgL7XnJQ7DpA?>*6gYciEQ`_f4LDKNZ% zKv=50u{(&ds#n(@R&CoYyVHUkytfy~zsZ{NMFC!Ktof(}8eMjrkS9bP?6J_E| zYE$)LAG$4%|8xQP=R!&th><}H`%FN6e!R~hE?0n&WMdBz!#7{%Q_RdrW@&>1p@F7h%0}gaI4GF>Jqjg%8pu z`ikp>|C*C45h4Q`;D<&4h`&0Cy^4HZvVlQyn}M*b=;RSqNgm1{*$ z))kbOm8ox315d_?biqwLHkJWk6Z>0dnb{FL+{Ju+=~Zl15B;5~Z?9aNC5qtJ;biA` z`yH-LD)?P8`DXtt83i&bv6es&jVDlxe;^xcFdG)1d53EX1%rd<8x_wNR6=tN=hjUO zT?L%S@=FP-%vOsgo$5G`xF0$hK8hOG+zgFj){&VzG!;CK!UfFkA-rS%T6B4#XsF_* zJw1|C;jePx;KEd0;rLaSzee4shX_2pD&Dd24HpXx8Qt_;*`CAb_Yv6mCL^6ieb;M%jH$ z**(}VfFeu$!FiJJ5c)rBm|3AzDaLO&p4eBYfW z+5Hp;ZAk`jlF%Jun5EKC{dCr=`XUhKLQ~$T1YSf531L|hk5QjgbcSvl+P8$PqO$04 z3IPMbo>!T)s#U);G(bI=`_(mwPTD$h8f8sQ$vK}icz|BH{KOrEtZ%H5TI3@wCp`tC z1k-j0B%yLBzr_s_J@TmHUwi%6=Ldr+ZAA~_`Z$@qiIX^YHk5CJZ!f9{cLzy_!{qhE zoqtOy(*GbG!<0#X6GECU!9Q7d@YBsA*G+5xJS_U~!0ppPUwE0=ydb$P|C)lac4;~? z4hyfrxUH8%DSF}{(b)hl^p`-YGLRy0lmqoS`Fve3wB0SO_Q}fN(MdQPd!#&RxP2$9 zT&k_V(2X**q~Dv^@0<_m^dU|oxED?>4lV;;&0HH3{nO?<<^@+U*FrIOl+Bwb%Mb<& zoG^2A>~moffO`| z*#11El*AuU-raNxy+3sPpKL`to}{= zb=TKZxL62VUyR#zZPL-DtzpqxzO^w^;-%iXYu)^^Uf6VTkS0*P*4wZKE%%~8FY_Aq z(en;0v-?d{D|6_q28=Xa^!0-?_|tkiP~PWoz?iYo_6c$!loF#|Hcr-@ZCsWp$bro6 zvj#bTiu~aDx%hF`Sd!;1MjCICl%2`qGO2fe1iHYtH?zK2XCa8f`pa?>&y)+O$?3)O zL46)iKFl{U`GCvSAzFu`J2ENIn!#x> zo|)ner?g2S?C*%geszPBbbOAJt%E;ng*z7dNFQ6KiFQi)&Ac)4^ZOEqOZ3~}R9Z_+ zOo6)`*g^j{;nSOY(QECr`Q`AvzlQ@Gc~dB7D)up?rXqP=gw{r@fiCi0{2B;M9OLDe zrkgJ|%k&*14m-HFw>vGM%s|WgEOuhM+Y|hkOAYCsC(@}h%|)U|9cYiu_mU2|V^-(q zH=mMB$n$@rN&35yo&F|*Zqo9=SySUmVbyy*P1WdC*N}G|jRfGpN~4?0I3j+}^|byP~Kv2_n6I*DE?2 zuIr+Vm%vUSchIOzbJ&4KHjr{i-1XccUlU`2->03LpevyNKocUul}4jo%HC3^?{vhv;S@2fK`LKCCjYQ&m$byt8+G8lhuGfb=OKHW8htB-Hf}4ndLJL= zix9d*-zOI6RzIX{C85u!HaN+GEZOEI?W+kv(S$MFmbwxRbB#in)Vz>U)o3LV%*jLy z-rJ}3tnkulU1fm}xDf&b3}wuB7jBClk1&x!H-XXET)8hFGZ`F+@@nosTi&}DzXmX> zB<)@B-HBgoP1Bu}NMW`DX)NgkAt!ihQ5aCE(svik63CSep=>lz>fBy@VbF9^GkQ0Q zZQ!#+nxS*}&!z_>8#ZPp*!Nc|E8jYS^I%{jok&QKRVH$wG~g1C}m!HGN+IhK#qN4lL;COipiV3Mrul zEN3{g1j8D}@}w?l&by9H4+Q62zlXv1{4$Wwr(PzqV3X#*0(dhCXmx0?=X+~T_{EQW z_GCAi^DEN!;T97D!T|P*XI!FqntU*+VIe0*&XrJ%r=r#H@!pjGc%D_C?vX8I9?F~P z`^~@Q9mgwRUwG?6fs2-|t$&$r&U^VkY5eEpR~yEW&!Jko4VH@6j;ruTEis)7`7o6m#VLi}JYBbglS-hiz}Eequl zu1E%{rZH{?5^cNSgmk%-UZ>H(P^#WstxO9nqZO1z4D-a(5(QNP#u;T)qNmjoR4npx zX1IxWJkHLNvNV3M@1kub^#07N14?=5f9#=6SDm7)LxeN4DO1MAIwpKdPK^$aBvz$J zh=Z2y-+&%&RgkqoD+wQ?0{WUc{nXxR!eZGZv=trS^BQ?q7dzN~;m0`fo~&&AD z>t1{9d#!a{*FxCD&LSK5{WC;?%_CM(C}(o?N6OxO{+|z3W9A_Oko7I`K*zk2g54M( zn_XOep+D`hUTo}AC3QB>_R`hF(Ejyc_qH8GeY!&AoI!jZCIUU*s|Il`V6oy!8nuzb zR@8F&BFGI_&0*d1wRpfJE05e}`_r2MU6NGg>(1^`c|YGG^reP*qqD7{D32!l^Zw($ zU1M4nQn^mk{)~B+$)ixiJvmcue~~7uj5D8i`XzfLBs9K!AzPn-gzJpT$lMkv^ivf! z?r3mg`BSvZ+(nSyUzBls7UH+%a{TOqr&GlfA>85(MVapWk0={dxsV|&gg&P}^**5XJeP;4_x746T=l1!b zWL)j4?YvB9!VfAlR1iPmOpY;_5j3&%T|3zzuEb$}k-l561Fh21K%YAl%^{Bnm{I&% z$~TtpX5;F62RqFs*ZV0yyTIhMu7-_jQso~bUWOWZ_fdh(z413cGPbU>`H)W1{iAwJjny4GO-3;2MZID31p3=v=Mr58sqboVP!JX6KI@<)_^m%( z{wQKc(1yHo%U5uy9=IS6)bBxyH$dmjzxC~+t8uk|!N*U0WiUo|1t&Xo&+e?V>MV-A zN3Z^E&;8^*mc$03>cB1yR>qY51}8l=@Ft3Lzk0IZ(}4o9q1;|Wne$#Zayd9nKU$=U z3iUhVdb)bEXAL7*_Hb*+0!R>ku&0Qt*77cl5T zpXS{I`01)MeBLdD@*`N#l-j_q#f{sV+B~UwbH|?2Fc8u?5o!LB+WX>1(lK!>%EPnr zP^#IY(=o@omIJXpj>MXp`KGajs(g!{G-$#-8*>rb{OY46`Jqn}X`9`v7}Lu|N3`Nw z@P5BNp{w!9@bu{3=Le%T<0Hv=@4su&KgrhWJ4^B=!vt!{FUjB>iP(Xr zu*!Mw$Ve{qv;!FYhqe%jJ&}`3{W7$KJ#W!z%sMUInwL1|%W2%JeR4*ci$2<}UT}`2-1z{nNE=k>*c5Cxlp?0oom$WG4VQ zviWI`?$_@;GQ}}zgysp0r~t2a5;crpRlpi}EU8`J*(OPgqq%RFT+HSSfLrr)@0ToF z-9+V;3J>D_XwTK2b(~J+C;MK|IcIvnd6SP$_(Ct39QAgS2?vyXy`N373x{@&LIfm; z(p6ZuTMl)I9%e5_j`-N4ygEIQ;?+b#nj)(M{LU)*7^9%r#%DSZR9$Q5EN||NN#lkr zKYjZ^3b$Epo_PwnIxVL+S|qlG6cG5L z!oE&aC=-lVk_Mn`JYH{B?UuzA1lzAXwBNW0A24u|bGTfMg`3=5-8c2@KD6Kt;MP9u zdljK=fJLta4UZyIRES11_IgFbnmI`U^FgtPA~&+6DKjeCJ(ydSOUU@MGOerp!19@F9AB}&|>(HEjho5v_Pc=_fF(guY_kK6na`H0MUKtR6tkga2 zT{YTH5$hg0^5bDQCi`|kZY6NLBrklz;K?rdOwktXL_;enbY5?hgc7n*BJXgua$5o- zb<9!I2i;V)VArsGB3DwsoK&N3hxyV$r+^(9&h1|Ig!?{Y7{%Qh;>RRqAtL5_w-X*e zU*d7M=ye}^>;Bv}c5R3u7q(7o`)cPNB3;zD`=cUvkcBEMk*Z$Heib1-is^|_=UMDa6Rbk8gg zF4PtYD)wmPcq#ePA zM}GgLA?4@dtchI8^;*WT6Q@s#-BAS2`(aI9`uy{eKBF_oVIZEDHoHl`(u+1c^2q&( zuV(MOSd9otkEs52amoZwMxUH3I3k_WusrA_IP}QBNQIy0>A*DqZL;Nazyu})61T<0 zZO1VwH#K_}7yNg2qeU#0yjCXKV6vDFQI5Sjx21NRrG_L?8QJHcQqZK)&T7|}&sM)c zDla*Hm87Yv`2_sA%Qx`9R15Q29a}#Kpf)H(0c9sK#T;zo(_9-2E3mAP87H;83({5f z9R#Qq6n9_@EJC>#{fO|QH_)o>n8{`6-SNPl7AHo%PxuJ^oa7p#($}JRp!k5Rs+OYY zfE{CeLc*r4Bzi%4?tNB{(r#|qVBik@5Q0O^xPu3^TK7HK^Q#_^s>U=rN*Z$?e5K;v z==HQ&tkV#+hYM>FGv1V?$9)j$fCfw@j{(<9b^;&#e*5K{~f+%Eu%e~Y~ zOD5)WrPC$QZs}}KPAgGZAQV0{etKE|Sr@@rTs%!YH=07R5wP1TQHB$6ur*yE>HdUI z2}Z~t{Jx-qfdeLL>|}uZ3|VJc2@_~ZTdJUSzN%nb3qy4DTuB>hJ z@d|V^xaIp&_B2!M{&0+-&x4I`dfc5Y(H#TTJ>7rEh~CDOmb|xw2hAg)e+G^3QW?$7 z3MfLV@5L~|!d32<*0+>lLcG7aboe`+JlTQ>i_&nTzP_P#;>^}gWCdV98R+phgS|=5 z!$PKcon8-O4O}PdR zJCvZNdj`}0Cmms zDJI3)7biq>zBhqoT|=kf<;EQ2n*j~;+bq`54cdE&DTclP>f{DaqUM?c(V%LwhRU*5 z$D8VGElgs$qgDZslRPu_i2ZAgwt#K}wW>h_HCN+7a~J-HjyG#%z~1S)FZAG{h$DG< zeHNaxV)r9+O5`xXdqyGja3-ZCYXI?J_3=j+`8UEo4^=)Jajs0Dyc$6m=Rc1+B+=eN zK4|n7L4&p0cGao_v&i;r^qzEo+Q;g69%O~YG#72hYlb&akHd7b8Yz;-kb>X5jBZsZ zB9R^5KXEa;VRU&3*MyN@i1C`avSOXKu)Cx(aAgH`8|h2(D>{9yTFz}AmlC17XY#^7 zUrO}w*GwM*uOU6USfQ$$ov5ESpfCORpqdp-tJ}H9#7|e;E0$cq&f${rq+>n#mVn!L zwM6S3lUl;i@SYE+_KegNQ&q<$epDejS^a$Sg58?i06AAJsUWFsK4Fwf3{-^ z9r`K38h)-1x%TSNG^mljKw5!a4bYI#LJm;o&EH=RnWB-eQ#2&zoUzdpv3g&)$%PZQCc_K4@{Ois@@9H!#qbr*YRP zLo0_XZ7|qD7^$O(pfS`3P9xX8%GC~+^PS)|Wb%HMzGqi2G$bDqm4-!zRO_7p?@G$2 zKo{qXCW04Ye^&YYF0(*K-mGn=Qup?ysxgbQG+AU=Qv14B){X}9rs&1tn6_R&vxq4v zg0<$17%UJcKAnuh&FQKe3=Y<6O!z=;0!^+XlP}!6JEHN0CnRfvrvQr-OtJJKnxx=A z^NCK_iDKdg9h9jZ!ub)i&M-tN-6!=vDCD#w6d1)N#!}R85Y@^Kh-{>~>5mu;;U8c# zH(@{RxY3{o&b&VVl7GY_M;>G=_z?=}Bu#@fOjg=HhX+a&pT|W}7i-lUz!#cy&dAje zq&2sr$Zx%30aevx#g2AwzP2LdyWPDizUKKSJqAxklh6(p)`)o+yq=3d|HKJ1CHS(P{zX>8In zwGN2gEZK0lU=&j{uWDt0-7~7}?9g&0>dAdLHUuYOEk7 z?|~E9c;zPA$+f9xJzhnrj(l@^kgMZ_vypVbvyevzmtDH?gM5h(#T1nrR(=RQimSvg zZ>mE#Ez)4we!VqD`$HSf@wwy&I8>nk9Gj=s^dj%JWdrmhM5ejPM|;RQE=DTMbQqq= zMxoQa+T5Ebzg3sF-lP-zTv~wqg;!Z9At20wm1v-j^qtIIE;8>G(LDLJK!woO`>W>f zmA?_Q`ov=g%ns{ z`z;S9@hneeFW2SN(%MlmQ_sF=bO}^gzUs2k75dcob#yOrqj#@^b0)-_DdqAqrb-jU zm+Yu@c&vV;A6PVlYN)wvgM@8zKv;!1BRY7f7iC2yGC>nRsM z5B%zjf#ZNop_*~`;12n&WvVDr8d2{Xk#sfP`}fN|7+CFwfb`MMx96<8BMmZS0H`=e z8bO+Di5T5fkt(Ll&(g8ddo@t0fg8)Eyp-YObSPehZ1v}ab0i0V%?63<%M%)aMJRUW z4JDVFAu(M|OY8;Z0xNq_Pfb@~n8Ego7TYa1mLu%^;2-&0QgS(G1>;mLqe~N>hvqUk z1GuQK*obTlbqvSi?lF;G5MLN~S=nkhCCAU?yFFk&*sXq;J7d$FG_|?cyGmx$@qpm+ zu$e1$oC|+`ydB*=2H(2**5u^UrtKyUSE(>f8aTqxx5-pX&XE-SV*N1t*xnUGFGkAt zJ7JzP#w~<`a-Mm@| zHO26CubWq;?Z8bgtzMo)`!ZLV24fmX)Z0Qs!Rkc5bBz*8I1rM{47TLOr3wIK^J6HA zGw@_9IcYQS>dH(g-qfOOr$vwDX^QMe&O+vIayeW*m#(;Cy*r4)CCBFI&> zUYY~%pVEdfd=+CLRWemD#cq5;`OeoJBTJtv3f|4OH#_T4*2@6f%XTrRudYR|-7?)` z`AnH_SF-3-{9zBhgGgoJ>M%~oeu$HRz_G@jA^8;}9-mkPQf9A0!z|K(Q#9Aro#7iJ z4r4}RmZoVl4v|w1GSp?`hcC|JUu(_rxc~sy`>t%ntW^3SwBmU*HO%t z=GQE$sNSPRLJ?SY95a^B-3~hLfCfu1WVUH&03Zp^uO@{$tf*&89fXQTSJkV2XxufQ zX@7jlP3AO(V7%H=O!(#hqOC-6QL6%pFjK`dNzjkx)$_tQbn_ z=<)4ycaUcX^l}HlFosD$a;c5Yk3YW2R%gT;JZi@GJx9^_v5sh_4iJpiGgHD)g|wB8s5b(MsM8&SG1-Er&^h?hV1~jIZ&&Oh zn6ZcdWC-%(={X1(L-G z_6Vkt<2-;iqv@E{zhD zxXIFe^$RZbg8LnHt?J*+|M`bNML}9Z3;%+ek|f_(xc;3qs%fo_&_S0SJqvi`71(8b zbay=3d(iQsdo71tNHJRewBp{vniX5A6Xkc^h2x1r^^jcbvsZ`yS@5Bl1c(V=R;YSf zSZ6-O5@%RqCAp9ScE6>|b2xyB=2_Hk-a?jR-QJX0D@gNSpPR-yj;*;JibxRI_)^Lq zeFmO(9|P^;^bEx~mBQFD{t37zU3EY~0I-|}UBx_Dd(OT)(@l;SNA9R_)t_r7W;=jV zx|frlIY4-EkpEdsJ`e(CI&uvz{R?yw%eq3jCSoqY4+T9P`Iarr!bN&HwwbC)s$D9{w zwLocpuSG)*=K@!RX-PSvX0T6ACJ8IZv z?LwPEgv_#P3E3teDb~)|!SKWcfbIV%CfGd6e8&WoxowTcgDpYVeKb~gr}Nk=M>+9h z@@TFVLWdi>@0X@1R|@XJ>Be%8PVLJY5N!@fj|y+`?aCq9bx4IS|1o}d>~ zc}|4Ve;Fm-?)}WZNk;o>`OdOyXt8CJW?*FS>fA~D3C`HL#M$+v)5DH@$1#hyQgSZz zZj`U({nNW`rl|WJSA?cj96LgT%A0VpW5RO-kGQ&*Hf>j4$M&@@3N2Ch&={ht2Xm$# z0~W~Un!8?-4x3!tITm{fip$7It6W%c8p;Z?;h|36#d%~E6i?5=CD%U&v2RCL%5Dq?@x^4< z!{s;~N7FIUcT5vAlIH=q~!G+~Es93+rCZ5KJ-)R#TF2<6_ov67U9Rvm;s z;mnLvx-K?dmEA|Fu4vPRj|WRa$NliDyMqiQjwZFG_BB~~w+^W>Cbm6z-xA^dZDyrGq8jCJHXVUI^)v9XZkM89CAC z6S>%$96Y+X>X^?oqUDb9XLhp2Ea0qC+Wk($PnC9x{-bIAqL|T@BdU>Hq0(t*DqH1p zd1W9&M!lQHlu4hjjBh_G`5x~PE4?BVZ1Ub94|`4Olh4`^+6sQu13#TSk+LwEe~!FU zps|91Rnddv#WE<={8n=Qz2gUthn3Q3oJ4EUg!R;f`n(lWUJhARv8mEy^lJ8^Q99z(J@q zZ^(6kB8C-t+ z7r@SA%$oN&7XaFpT1l-y;FH{NDE^usx@fPSbTn73jRO{V%ICUDIn&Mm$h*`F>3=mu zn*ii*THJ2uSj|ki6bpxyKrKikT#k{DbzR`HR!e6G!(tG3d-5AbU^&D2vzRW3_Zj03 zVS**Y{75Jq(et}>yWBpvQc&;osd}1V`PSrOPAIL9jhQ4- z>{LIkKzI4%O3-CzaJ5#l&^)mY(OdBChscd{XGVcKdt6->1V_Um;*068VK1As*&L@- zds63~En?IPpWGICA7gEc21Q}tg{YyLGWO+4=}0hZza)qUv+mGXM->bvzLBdMvwU#; zr<&NfzQF~t{rC$|{Wm7;WB&P3XVT?{FejA8>V6BKE|O$aC(>JE&e`B~$?y?fri5>G zr#_in_zz{_zx{jGN+LpS{`#{r0U5Q`MGsJ3P?5HqHfX*$NOXm zwOCRg=qhS$`R4LlOd`u^CcAw6r2`tF$ z`b192o@=mu$0O~7!;MT@CYt%7$GvmhI__vxkQd3<9pB{g{>iWM=$W-8p$CF%GiN|2 zGbH#5{*t=x@pkg6Uz2ZP3_mj7dz6@2bEZYtsVc;mR*ww-!~i35?boCyV4^u3KYM&( zUY1~0F+x2&vrTCt;%IFmxqhcgb!zdmyo9{2YfSEvp=$7Dul?29JhZn{IuMH8fMRA) z7=fR5lpYxtDJ4e|xU+U*b)}D3SClsezyuX@bew?J>%^gAndId>Bubene1JVGdg%MD zmzC=*R}v6)je+Pbj)gGkI(%u|)Rbc8pvqgGHvs*}l4!<=KUsPwMLlVfKSR9}k<+aJ z5Nz?%yO5C4lZs+*sqOou1xNj4+!c3a*jQ;UVb@^?=ZA!8iZ%(9pdjcx?vvz16gpoS zw9%tMnx48W$^K(Ar0Z*WTJJBaRPXw=qg|@(^k_%*1tlw)^bE#6_ADs1rH`4Me|^K@ z-s{3ak~ZP_;Fh!p|K-#`{M;oyuA)Ua)Q|8B>H@=?kw2pya4I z_M1WjeV!9^B&&)YEGW5&cvLh6ifAqJHf-F~!YY)DtJxqOUQb0hT|ee3;$YiKcoo#S z$&sez6DfA(7Ub$jm^RLjl_vHwC~HxMH&b%eja__Q3=4Eu