mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-20 10:57:22 +08:00
* feat: add bocha web search tool (#4902) * add bocha web search tool * Revert "add bocha web search tool" This reverts commit1b36d75a17. * add bocha web search tool * fix: correct temporary_cache spelling and update supported tools for web search * ruff --------- Co-authored-by: Soulter <905617992@qq.com> * fix: messages[x] assistant content must contain at least one part (#4928) * fix: messages[x] assistant content must contain at least one part fixes: #4876 * ruff format * chore: bump version to 4.14.5 (#4930) * feat: implement feishu / lark media file handling utilities for file, audio and video processing (#4938) * feat: implement media file handling utilities for audio and video processing * feat: refactor file upload handling for audio and video in LarkMessageEvent * feat: add cleanup for failed audio and video conversion outputs in media_utils * feat: add utility methods for sending messages and uploading files in LarkMessageEvent * fix: correct spelling of 'temporary' in SharedPreferences class * perf: optimize webchat and wecom ai queue lifecycle (#4941) * perf: optimize webchat and wecom ai queue lifecycle * perf: enhance webchat back queue management with conversation ID support * fix: localize provider source config UI (#4933) * fix: localize provider source ui * feat: localize provider metadata keys * chore: add provider metadata translations * chore: format provider i18n changes * fix: preserve metadata fields in i18n conversion * fix: internationalize platform config and dialog * fix: add Weixin official account platform icon --------- Co-authored-by: Soulter <905617992@qq.com> * chore: bump version to 4.14.6 * feat: add provider-souce-level proxy (#4949) * feat: 添加 Provider 级别代理支持及请求失败日志 * refactor: simplify provider source configuration structure * refactor: move env proxy fallback logic to log_connection_failure * refactor: update client proxy handling and add terminate method for cleanup * refactor: update no_proxy configuration to remove redundant subnet --------- Co-authored-by: Soulter <905617992@qq.com> * feat(ComponentPanel): implement permission management for dashboard (#4887) * feat(backend): add permission update api * feat(useCommandActions): add updatePermission action and translations * feat(dashboard): implement permission editing ui * style: fix import sorting in command.py * refactor(backend): extract permission update logic to service * feat(i18n): add success and failure messages for command updates --------- Co-authored-by: Soulter <905617992@qq.com> * feat: 允许 LLM 预览工具返回的图片并自主决定是否发送 (#4895) * feat: 允许 LLM 预览工具返回的图片并自主决定是否发送 * 复用 send_message_to_user 替代独立的图片发送工具 * feat: implement _HandleFunctionToolsResult class for improved tool response handling * docs: add path handling guidelines to AGENTS.md --------- Co-authored-by: Soulter <905617992@qq.com> * feat(telegram): 添加媒体组(相册)支持 / add media group (album) support (#4893) * feat(telegram): 添加媒体组(相册)支持 / add media group (album) support ## 功能说明 支持 Telegram 的媒体组消息(相册),将多张图片/视频合并为一条消息处理,而不是分散成多条消息。 ## 主要改动 ### 1. 初始化媒体组缓存 (__init__) - 添加 `media_group_cache` 字典存储待处理的媒体组消息 - 使用 2.5 秒超时收集媒体组消息(基于社区最佳实践) - 最大等待时间 10 秒(防止永久等待) ### 2. 消息处理流程 (message_handler) - 检测 `media_group_id` 判断是否为媒体组消息 - 媒体组消息走特殊处理流程,避免分散处理 ### 3. 媒体组消息缓存 (handle_media_group_message) - 缓存收到的媒体组消息 - 使用 APScheduler 实现防抖(debounce)机制 - 每收到新消息时重置超时计时器 - 超时后触发统一处理 ### 4. 媒体组合并处理 (process_media_group) - 从缓存中取出所有媒体项 - 使用第一条消息作为基础(保留文本、回复等信息) - 依次添加所有图片、视频、文档到消息链 - 将合并后的消息发送到处理流程 ## 技术方案论证 Telegram Bot API 在处理媒体组时的设计限制: 1. 将媒体组的每个消息作为独立的 update 发送 2. 每个 update 带有相同的 `media_group_id` 3. **不提供**组的总数、结束标志或一次性完整组的机制 因此,bot 必须自行收集消息,并通过硬编码超时(timeout/delay)等待可能延迟到达的消息。 这是目前唯一可靠的方案,被官方实现、主流框架和开发者社区广泛采用。 ### 官方和社区证据: - **Telegram Bot API 服务器实现(tdlib)**:明确指出缺少结束标志或总数信息 https://github.com/tdlib/telegram-bot-api/issues/643 - **Telegram Bot API 服务器 issue**:讨论媒体组处理的不便性,推荐使用超时机制 https://github.com/tdlib/telegram-bot-api/issues/339 - **Telegraf(Node.js 框架)**:专用媒体组中间件使用 timeout 控制等待时间 https://github.com/DieTime/telegraf-media-group - **StackOverflow 讨论**:无法一次性获取媒体组所有文件,必须手动收集 https://stackoverflow.com/questions/50180048/telegram-api-get-all-uploaded-photos-by-media-group-id - **python-telegram-bot 社区**:确认媒体组消息单独到达,需手动处理 https://github.com/python-telegram-bot/python-telegram-bot/discussions/3143 - **Telegram Bot API 官方文档**:仅定义 `media_group_id` 为可选字段,不提供获取完整组的接口 https://core.telegram.org/bots/api#message ## 实现细节 - 使用 2.5 秒超时收集媒体组消息(基于社区最佳实践) - 最大等待时间 10 秒(防止永久等待) - 采用防抖(debounce)机制:每收到新消息重置计时器 - 利用 APScheduler 实现延迟处理和任务调度 ## 测试验证 - ✅ 发送 5 张图片相册,成功合并为一条消息 - ✅ 保留原始文本说明和回复信息 - ✅ 支持图片、视频、文档混合的媒体组 - ✅ 日志显示 Processing media group <media_group_id> with 5 items ## 代码变更 - 文件:astrbot/core/platform/sources/telegram/tg_adapter.py - 新增代码:124 行 - 新增方法:handle_media_group_message(), process_media_group() Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * refactor(telegram): 优化媒体组处理性能和可靠性 根据代码审查反馈改进: 1. 实现 media_group_max_wait 防止无限延迟 - 跟踪媒体组创建时间,超过最大等待时间立即处理 - 最坏情况下 10 秒内必定处理,防止消息持续到达导致无限延迟 2. 移除手动 job 查找优化性能 - 删除 O(N) 的 get_jobs() 循环扫描 - 依赖 replace_existing=True 自动替换任务 3. 重用 convert_message 减少代码重复 - 统一所有媒体类型转换逻辑 - 未来添加新媒体类型只需修改一处 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(telegram): handle missing message in media group processing and improve logging messages --------- Co-authored-by: Ubuntu <ubuntu@localhost.localdomain> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: Soulter <905617992@qq.com> * feat: add welcome feature with localized content and onboarding steps * fix: correct height attribute to max-height for dialog component * feat: supports electron app (#4952) * feat: add desktop wrapper with frontend-only packaging * docs: add desktop build docs and track dashboard lockfile * fix: track desktop lockfile for npm ci * fix: allow custom install directory for windows installer * chore: migrate desktop workflow to pnpm * fix(desktop): build AppImage only on Linux * fix(desktop): harden packaged startup and backend bundling * fix(desktop): adapt packaged restart and plugin dependency flow * fix(desktop): prevent backend respawn race on quit * fix(desktop): prefer pyproject version for desktop packaging * fix(desktop): improve startup loading UX and reduce flicker * ci: add desktop multi-platform release workflow * ci: fix desktop release build and mac runner labels * ci: disable electron-builder auto publish in desktop build * ci: avoid electron-builder publish path in build matrix * ci: normalize desktop release artifact names * ci: exclude blockmap files from desktop release assets * ci: prefix desktop release assets with AstrBot and purge blockmaps * feat: add electron bridge types and expose backend control methods in preload script * Update startup screen assets and styles - Changed the icon from PNG to SVG format for better scalability. - Updated the border color from #d0d0d0 to #eeeeee for a softer appearance. - Adjusted the width of the startup screen from 460px to 360px for improved responsiveness. * Update .gitignore to include package.json * chore: remove desktop gitkeep ignore exceptions * docs: update desktop troubleshooting for current runtime behavior * refactor(desktop): modularize runtime and harden startup flow --------- Co-authored-by: Soulter <905617992@qq.com> Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * fix: dedupe preset messages (#4961) * feat: enhance package.json with resource filters and compression settings * chore: update Python version requirements to 3.12 (#4963) * chore: bump version to 4.14.7 * feat: refactor release workflow and add special update handling for electron app (#4969) * chore: bump version to 4.14.8 and bump faiss-cpu version up to date * chore: auto ann fix by ruff (#4903) * chore: auto fix by ruff * refactor: 统一修正返回类型注解为 None/bool 以匹配实现 * refactor: 将 _get_next_page 改为异步并移除多余的请求错误抛出 * refactor: 将 get_client 的返回类型改为 object * style: 为 LarkMessageEvent 的相关方法添加返回类型注解 None --------- Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * fix: prepare OpenSSL via vcpkg for Windows ARM64 * ci: change ghcr namespace * chore: update pydantic dependency version (#4980) * feat: add delete button to persona management dialog (#4978) * Initial plan * feat: add delete button to persona management dialog - Added delete button to PersonaForm dialog (only visible when editing) - Implemented deletePersona method with confirmation dialog - Connected delete event to PersonaManager for proper handling - Button positioned on left side of dialog actions for clear separation - Uses existing i18n translations for delete button and messages Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * fix: use finally block to ensure saving state is reset - Moved `this.saving = false` to finally block in deletePersona - Ensures UI doesn't stay in saving state after errors - Follows best practices for state management Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * feat: enhance Dingtalk adapter with active push message and image, video, audio message type (#4986) * fix: handle pip install execution in frozen runtime (#4985) * fix: handle pip install execution in frozen runtime * fix: harden pip subprocess fallback handling * fix: collect certifi data in desktop backend build (#4995) * feat: 企业微信应用 支持主动消息推送,并优化企微应用、微信公众号、微信客服音频相关的处理 (#4998) * feat: 企业微信智能机器人支持主动消息推送以及发送视频、文件等消息类型支持 (#4999) * feat: enhance WecomAIBotAdapter and WecomAIBotMessageEvent for improved streaming message handling (#5000) fixes: #3965 * feat: enhance persona tool management and update UI localization for subagent orchestration (#4990) * feat: enhance persona tool management and update UI localization for subagent orchestration * fix: remove debug logging for final ProviderRequest in build_main_agent function * perf: 稳定源码与 Electron 打包环境下的 pip 安装行为,并修复非 Electron 环境下点击 WebUI 更新按钮时出现跳转对话框的问题 (#4996) * fix: handle pip install execution in frozen runtime * fix: harden pip subprocess fallback handling * fix: scope global data root to packaged electron runtime * refactor: inline frozen runtime check for electron guard * fix: prefer current interpreter for source pip installs * fix: avoid resolving venv python symlink for pip * refactor: share runtime environment detection utilities * fix: improve error message when pip module is unavailable * fix: raise ImportError when pip module is unavailable * fix: preserve ImportError semantics for missing pip * fix: 修复非electron app环境更新时仍然显示electron更新对话框的问题 --------- Co-authored-by: Soulter <905617992@qq.com> * fix: 'HandoffTool' object has no attribute 'agent' (#5005) * fix: 移动agent的位置到super().__init__之后 * add: 添加一行注释 * chore(deps): bump the github-actions group with 2 updates (#5006) Bumps the github-actions group with 2 updates: [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) and [actions/download-artifact](https://github.com/actions/download-artifact). Updates `astral-sh/setup-uv` from 6 to 7 - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v6...v7) Updates `actions/download-artifact` from 6 to 7 - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v6...v7) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: actions/download-artifact dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix: stabilize packaged runtime pip/ssl behavior and mac font fallback (#5007) * fix: patch pip distlib finder for frozen electron runtime * fix: use certifi CA bundle for runtime SSL requests * fix: configure certifi CA before core imports * fix: improve mac font fallback for dashboard text * fix: harden frozen pip patch and unify TLS connector * refactor: centralize dashboard CJK font fallback stacks * perf: reuse TLS context and avoid repeated frozen pip patch * refactor: bootstrap TLS setup before core imports * fix: use async confirm dialog for provider deletions * fix: replace native confirm dialogs in dashboard - Add shared confirm helper in dashboard/src/utils/confirmDialog.ts for async dialog usage with safe fallback. - Migrate provider, chat, config, session, platform, persona, MCP, backup, and knowledge-base delete/close confirmations to use the shared helper. - Remove scattered inline confirm handling to keep behavior consistent and avoid native blocking dialog focus/caret issues in Electron. * fix: capture runtime bootstrap logs after logger init - Add bootstrap record buffer in runtime_bootstrap for early TLS patch logs before logger is ready. - Flush buffered bootstrap logs to astrbot logger at process startup in main.py. - Include concrete exception details for TLS bootstrap failures to improve diagnosis. * fix: harden runtime bootstrap and unify confirm handling - Simplify bootstrap log buffering and add a public initialize hook for non-main startup paths. - Guard aiohttp TLS patching with feature/type checks and keep graceful fallback when internals are unavailable. - Standardize dashboard confirmation flow via shared confirm helpers across composition and options API components. * refactor: simplify runtime tls bootstrap and tighten confirm typing * refactor: align ssl helper namespace and confirm usage * fix: 修复 Windows 打包版后端重启失败问题 (#5009) * fix: patch pip distlib finder for frozen electron runtime * fix: use certifi CA bundle for runtime SSL requests * fix: configure certifi CA before core imports * fix: improve mac font fallback for dashboard text * fix: harden frozen pip patch and unify TLS connector * refactor: centralize dashboard CJK font fallback stacks * perf: reuse TLS context and avoid repeated frozen pip patch * refactor: bootstrap TLS setup before core imports * fix: use async confirm dialog for provider deletions * fix: replace native confirm dialogs in dashboard - Add shared confirm helper in dashboard/src/utils/confirmDialog.ts for async dialog usage with safe fallback. - Migrate provider, chat, config, session, platform, persona, MCP, backup, and knowledge-base delete/close confirmations to use the shared helper. - Remove scattered inline confirm handling to keep behavior consistent and avoid native blocking dialog focus/caret issues in Electron. * fix: capture runtime bootstrap logs after logger init - Add bootstrap record buffer in runtime_bootstrap for early TLS patch logs before logger is ready. - Flush buffered bootstrap logs to astrbot logger at process startup in main.py. - Include concrete exception details for TLS bootstrap failures to improve diagnosis. * fix: harden runtime bootstrap and unify confirm handling - Simplify bootstrap log buffering and add a public initialize hook for non-main startup paths. - Guard aiohttp TLS patching with feature/type checks and keep graceful fallback when internals are unavailable. - Standardize dashboard confirmation flow via shared confirm helpers across composition and options API components. * refactor: simplify runtime tls bootstrap and tighten confirm typing * refactor: align ssl helper namespace and confirm usage * fix: avoid frozen restart crash from multiprocessing import * fix: include missing frozen dependencies for windows backend * fix: use execv for stable backend reboot args * Revert "fix: use execv for stable backend reboot args" This reverts commit9cc27becff. * Revert "fix: include missing frozen dependencies for windows backend" This reverts commit52554bea1f. * Revert "fix: avoid frozen restart crash from multiprocessing import" This reverts commit10548645b0. * fix: reset pyinstaller onefile env before reboot * fix: unify electron restart path and tray-exit backend cleanup * fix: stabilize desktop restart detection and frozen reboot args * fix: make dashboard restart wait detection robust * fix: revert dashboard restart waiting interaction tweaks * fix: pass auth token for desktop graceful restart * fix: avoid false failure during graceful restart wait * fix: start restart waiting before electron restart call * fix: harden restart waiting and reboot arg parsing * fix: parse start_time as numeric timestamp * fix: 修复app内重启异常,修复app内点击重启不能立刻提示重启,以及在后端就绪时及时刷新界面的问题 (#5013) * fix: patch pip distlib finder for frozen electron runtime * fix: use certifi CA bundle for runtime SSL requests * fix: configure certifi CA before core imports * fix: improve mac font fallback for dashboard text * fix: harden frozen pip patch and unify TLS connector * refactor: centralize dashboard CJK font fallback stacks * perf: reuse TLS context and avoid repeated frozen pip patch * refactor: bootstrap TLS setup before core imports * fix: use async confirm dialog for provider deletions * fix: replace native confirm dialogs in dashboard - Add shared confirm helper in dashboard/src/utils/confirmDialog.ts for async dialog usage with safe fallback. - Migrate provider, chat, config, session, platform, persona, MCP, backup, and knowledge-base delete/close confirmations to use the shared helper. - Remove scattered inline confirm handling to keep behavior consistent and avoid native blocking dialog focus/caret issues in Electron. * fix: capture runtime bootstrap logs after logger init - Add bootstrap record buffer in runtime_bootstrap for early TLS patch logs before logger is ready. - Flush buffered bootstrap logs to astrbot logger at process startup in main.py. - Include concrete exception details for TLS bootstrap failures to improve diagnosis. * fix: harden runtime bootstrap and unify confirm handling - Simplify bootstrap log buffering and add a public initialize hook for non-main startup paths. - Guard aiohttp TLS patching with feature/type checks and keep graceful fallback when internals are unavailable. - Standardize dashboard confirmation flow via shared confirm helpers across composition and options API components. * refactor: simplify runtime tls bootstrap and tighten confirm typing * refactor: align ssl helper namespace and confirm usage * fix: avoid frozen restart crash from multiprocessing import * fix: include missing frozen dependencies for windows backend * fix: use execv for stable backend reboot args * Revert "fix: use execv for stable backend reboot args" This reverts commit9cc27becff. * Revert "fix: include missing frozen dependencies for windows backend" This reverts commit52554bea1f. * Revert "fix: avoid frozen restart crash from multiprocessing import" This reverts commit10548645b0. * fix: reset pyinstaller onefile env before reboot * fix: unify electron restart path and tray-exit backend cleanup * fix: stabilize desktop restart detection and frozen reboot args * fix: make dashboard restart wait detection robust * fix: revert dashboard restart waiting interaction tweaks * fix: pass auth token for desktop graceful restart * fix: avoid false failure during graceful restart wait * fix: start restart waiting before electron restart call * fix: harden restart waiting and reboot arg parsing * fix: parse start_time as numeric timestamp * fix: preserve windows frozen reboot argv quoting * fix: align restart waiting with electron restart timing * fix: tighten graceful restart and unmanaged kill safety * chore: bump version to 4.15.0 (#5003) * fix: add reminder for v4.14.8 users regarding manual redeployment due to a bug * fix: harden plugin dependency loading in frozen app runtime (#5015) * fix: compare plugin versions semantically in market updates * fix: prioritize plugin site-packages for in-process pip * fix: reload starlette from plugin target site-packages * fix: harden plugin dependency import precedence in frozen runtime * fix: improve plugin dependency conflict handling * refactor: simplify plugin conflict checks and version utils * fix: expand transitive plugin dependencies for conflict checks * fix: recover conflicting plugin dependencies during module prefer * fix: reuse renderer restart flow for tray backend restart * fix: add recoverable plugin dependency conflict handling * revert: remove plugin version comparison changes * fix: add missing tray restart backend labels * feat: adding support for media and quoted message attachments for feishu (#5018) * docs: add AUR installation method (#4879) * docs: sync system package manager installation instructions to all languages * Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update README.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * fix/typo * refactor: update system package manager installation instructions for Arch Linux across multiple language README files * feat: add installation command for AstrBot in multiple language README files --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> Co-authored-by: Soulter <905617992@qq.com> * fix(desktop): 为 Electron 与后端日志增加按大小轮转 (#5029) * fix(desktop): rotate electron and backend logs * refactor(desktop): centralize log rotation defaults and debug fs errors * fix(desktop): harden rotation fs ops and buffer backend log writes * refactor(desktop): extract buffered logger and reduce sync stat calls * refactor(desktop): simplify rotation flow and harden logger config * fix(desktop): make app logging async and flush-safe * fix: harden app log path switching and debug-gated rotation errors * fix: cap buffered log chunk size during path switch * feat: add first notice feature with multilingual support and UI integration * fix: 提升打包版桌面端启动稳定性并优化插件依赖处理 (#5031) * fix(desktop): rotate electron and backend logs * refactor(desktop): centralize log rotation defaults and debug fs errors * fix(desktop): harden rotation fs ops and buffer backend log writes * refactor(desktop): extract buffered logger and reduce sync stat calls * refactor(desktop): simplify rotation flow and harden logger config * fix(desktop): make app logging async and flush-safe * fix: harden app log path switching and debug-gated rotation errors * fix: cap buffered log chunk size during path switch * fix: avoid redundant plugin reinstall and upgrade electron * fix: stop webchat tasks cleanly and bind packaged backend to localhost * fix: unify platform shutdown and await webchat listener cleanup * fix: improve startup logs for dashboard and onebot listeners * fix: revert extra startup service logs * fix: harden plugin import recovery and webchat listener cleanup * fix: pin dashboard ci node version to 24.13.0 * fix: avoid duplicate webchat listener cleanup on terminate * refactor: clarify platform task lifecycle management * fix: continue platform shutdown when terminate fails * feat: temporary file handling and introduce TempDirCleaner (#5026) * feat: temporary file handling and introduce TempDirCleaner - Updated various modules to use `get_astrbot_temp_path()` instead of `get_astrbot_data_path()` for temporary file storage. - Renamed temporary files for better identification and organization. - Introduced `TempDirCleaner` to manage the size of the temporary directory, ensuring it does not exceed a specified limit by deleting the oldest files. - Added configuration option for maximum temporary directory size in the dashboard. - Implemented tests for `TempDirCleaner` to verify cleanup functionality and size management. * ruff * fix: close unawaited reset coroutine on early return (#5033) When an OnLLMRequestEvent hook stops event propagation, the reset_coro created by build_main_agent was never awaited, causing a RuntimeWarning. Close the coroutine explicitly before returning. Fixes #5032 Co-authored-by: Limitless2023 <limitless@users.noreply.github.com> * fix: update error logging message for connection failures * docs: clean and sync README (#5014) * fix: close missing div in README * fix: sync README_zh-TW with README * fix: sync README * fix: correct typo correct url in README_en README_fr README_ru * docs: sync README_en with README * Update README_en.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * fix: provider extra param dialog key display error * chore: ruff format * feat: add send_chat_action for Telegram platform adapter (#5037) * feat: add send_chat_action for Telegram platform adapter Add typing/upload indicator when sending messages via Telegram. - Added _send_chat_action helper method for sending chat actions - Send appropriate action (typing, upload_photo, upload_document, upload_voice) before sending different message types - Support streaming mode with typing indicator - Support supergroup with message_thread_id * refactor(telegram): extract chat action helpers and add throttling - Add ACTION_BY_TYPE mapping for message type to action priority - Add _get_chat_action_for_chain() to determine action from message chain - Add _send_media_with_action() for upload → send → restore typing pattern - Add _ensure_typing() helper for typing status - Add chat action throttling (0.5s) in streaming mode to avoid rate limits - Update type annotation to ChatAction | str for better static checking * feat(telegram): implement send_typing method for Telegram platform --------- Co-authored-by: Soulter <905617992@qq.com> * fix: 修复更新日志、官方文档弹窗双滚动条问题 (#5060) * docs: sync and fix readme typo (#5055) * docs: fix index typo * docs: fix typo in README_en.md - 移除英文README中意外出现的俄语,并替换为英语 * docs: fix html typo - remove unused '</p>' * docs: sync table with README * docs: sync README header format - keep the README header format consistent * doc: sync key features * style: format files - Fix formatting issues from previous PR * fix: correct md anchor link * docs: correct typo in README_fr.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * docs: correct typo in README_zh-TW.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * fix: 修复备份时缺失的人格文件夹映射 (#5042) * feat: QQ 官方机器人平台支持主动推送消息、私聊场景下支持接收文件 (#5066) * feat: QQ 官方机器人平台支持主动推送消息、私聊场景下支持接收文件 * feat: enhance QQOfficialWebhook to remember session scenes for group, channel, and friend messages * perf: 优化分段回复间隔时间的初始化逻辑 (#5068) fixes: #5059 * fix: chunk err when using openrouter deepseek (#5069) * feat: add i18n supports for custom platform adapters (#5045) * Feat: 为插件提供的适配器的元数据&i18n提供数据通路 * chore: update docstrings with pull request references Added references to pull request 5045 in docstrings. --------- Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * fix: 完善转发引用解析与图片回退并支持配置化控制 (#5054) * feat: support fallback image parsing for quoted messages * fix: fallback parse quoted images when reply chain has placeholders * style: format network utils with ruff * test: expand quoted parser coverage and improve fallback diagnostics * fix: fallback to text-only retry when image requests fail * fix: tighten image fallback and resolve nested quoted forwards * refactor: simplify quoted message extraction and dedupe images * fix: harden quoted parsing and openai error candidates * fix: harden quoted image ref normalization * refactor: organize quoted parser settings and logging * fix: cap quoted fallback images and avoid retry loops * refactor: split quoted message parser into focused modules * refactor: share onebot segment parsing logic * refactor: unify quoted message parsing flow * feat: move quoted parser tuning to provider settings * fix: add missing i18n metadata for quoted parser settings * chore: refine forwarded message setting labels * fix: add config tabs and routing for normal and system configurations * chore: bump version to 4.16.0 (#5074) * feat: add LINE platform support with adapter and configuration (#5085) * fix-correct-FIRST_NOTICE.md-locale-path-resolution (#5083) (#5082) * fix:修改配置文件目录 * fix:添加备选的FIRST_NOTICE.zh-CN.md用于兼容 * fix: remove unnecessary frozen flag from requirements export in Dockerfile fixes: #5089 * fix #5089: add uv lock step in Dockerfile before export (#5091) Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * feat: support hot reload after plugin load failure (#5043) * add :Support hot reload after plugin load failure * Apply suggestions from code review Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * fix:reformat code * fix:reformat code --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * feat: add fallback chat model chain in tool loop runner (#5109) * feat: implement fallback provider support for chat models and update configuration * feat: enhance provider selection display with count and chips for selected providers * feat: update fallback chat providers to use provider settings and add warning for non-list fallback models * feat: add Afdian support card to resources section in WelcomePage * feat: replace colorlog with loguru for enhanced logging support (#5115) * feat: add SSL configuration options for WebUI and update related logging (#5117) * chore: bump version to 4.17.0 * fix: handle list format content from OpenAI-compatible APIs (#5128) * fix: handle list format content from OpenAI-compatible APIs Some LLM providers (e.g., GLM-4.5V via SiliconFlow) return content as list[dict] format like [{'type': 'text', 'text': '...'}] instead of plain string. This causes the raw list representation to be displayed to users. Changes: - Add _normalize_content() helper to extract text from various content formats - Use json.loads instead of ast.literal_eval for safer parsing - Add size limit check (8KB) before attempting JSON parsing - Only convert lists that match OpenAI content-part schema (has 'type': 'text') to avoid collapsing legitimate list-literal replies like ['foo', 'bar'] - Add strip parameter to preserve whitespace in streaming chunks - Clean up orphan </think> tags that may leak from some models Fixes #5124 * fix: improve content normalization safety - Try json.loads first, fallback to ast.literal_eval for single-quoted Python literals to avoid corrupting apostrophes (e.g., "don't") - Coerce text values to str to handle null or non-string text fields * fix: update retention logic in LogManager to handle backup count correctly * chore: bump version to 4.17.1 * docs: Added instructions for deploying AstrBot using AstrBot Launcher. (#5136) Added instructions for deploying AstrBot using AstrBot Launcher. * fix: add MCP tools to function tool set in _plugin_tool_fix (#5144) * fix: add support for collecting data from builtin stars in electron pyinstaller build (#5145) * chore: bump version to 4.17.1 * chore: ruff format * fix: prevent updates for AstrBot launched via launcher * fix(desktop): include runtime deps for builtin plugins in backend build (#5146) * fix: 'Plain' object has no attribute 'text' when using python 3.14 (#5154) * fix: enhance plugin metadata handling by injecting attributes before instantiation (#5155) * fix: enhance handle_result to support event context and webchat image sending * chore: bump version to 4.17.3 * chore: ruff format * feat: add NVIDIA provider template (#5157) fixes: #5156 * feat: enhance provider sources panel with styled menu and mobile support * fix: improve permission denied message for local execution in Python and shell tools * feat: enhance PersonaForm component with responsive design and improved styling (#5162) fix: #5159 * ui(CronJobPage): fix action column buttons overlapping in CronJobPage (#5163) - 修改前:操作列容器仅使用 `d-flex`,在页面宽度变窄时,子元素(开关和删除按钮)会因为宽度挤压而发生视觉重叠,甚至堆叠在一起。 - 修改后: 1. 为容器添加了 `flex-nowrap`,强制禁止子元素换行。 2. 设置了 `min-width: 140px`,确保该列拥有固定的保护空间,防止被其他长文本列挤压。 3. 增加了 `gap: 12px` 间距,提升了操作辨识度并优化了点击体验。 * feat: add unsaved changes notice to configuration page and update messages * feat: implement search functionality in configuration components and update UI (#5168) * feat: add FAQ link to vertical sidebar and update navigation for localization * feat: add announcement section to WelcomePage and localize announcement title * chore: bump version to 4.17.4 * feat: supports send markdown message in qqofficial (#5173) * feat: supports send markdown message in qqofficial closes: #1093 #918 #4180 #4264 * ruff format * fix: prevent duplicate error message when all LLM providers fail (#5183) * fix: 修复选择配置文件进入配置文件管理弹窗直接关闭弹窗显示的配置文件不正确 (#5174) * feat: add MarketPluginCard component and integrate random plugin feature in ExtensionPage (#5190) * feat: add MarketPluginCard component and integrate random plugin feature in ExtensionPage * feat: update random plugin selection logic to use pluginMarketData and refresh on relevant events * feat: supports aihubmix * docs: update readme * chore: ruff format * feat: add LINE support to multiple language README files * feat(core): add plugin error hook for custom error routing (#5192) * feat(core): add plugin error hook for custom error routing * fix(core): align plugin error suppression with event stop state * refactor: extract Voice_messages_forbidden fallback into shared helper with typed BadRequest exception (#5204) - Add _send_voice_with_fallback helper to deduplicate voice forbidden handling - Catch telegram.error.BadRequest instead of bare Exception with string matching - Add text field to Record component to preserve TTS source text - Store original text in Record during TTS conversion for use as document caption - Skip _send_chat_action when chat_id is empty to avoid unnecessary warnings * chore: bump version to 4.17.5 * feat: add admin permission checks for Python and Shell execution (#5214) * fix: 改进微信公众号被动回复处理机制,引入缓冲与分片回复,并优化超时行为 (#5224) * 修复wechat official 被动回复功能 * ruff format --------- Co-authored-by: Soulter <905617992@qq.com> * fix: 修复仅发送 JSON 消息段时的空消息回复报错 (#5208) * Fix Register_Stage · 补全 JSON 消息判断,修复发送 JSON 消息时遇到 “消息为空,跳过发送阶段” 的问题。 · 顺带补全其它消息类型判断。 Co-authored-by: Pizero <zhaory200707@outlook.com> * Fix formatting and comments in stage.py * Format stage.py --------- Co-authored-by: Pizero <zhaory200707@outlook.com> * docs: update related repo links * fix(core): terminate active events on reset/new/del to prevent stale responses (#5225) * fix(core): terminate active events on reset/new/del to prevent stale responses Closes #5222 * style: fix import sorting in scheduler.py * chore: remove Electron desktop pipeline and switch to tauri repo (#5226) * ci: remove Electron desktop build from release pipeline * chore: remove electron desktop and switch to tauri release trigger * ci: remove desktop workflow dispatch trigger * refactor: migrate data paths to astrbot_path helpers * fix: point desktop update prompt to AstrBot-desktop releases * fix: update feature request template for clarity and consistency in English and Chinese * Feat/config leave confirm (#5249) * feat: 配置文件增加未保存提示弹窗 * fix: 移除unsavedChangesDialog插件使用组件方式实现弹窗 * feat: add support for plugin astrbot-version and platform requirement checks (#5235) * feat: add support for plugin astrbot-version and platform requirement checks * fix: remove unsupported platform and version constraints from metadata.yaml * fix: remove restriction on 'v' in astrbot_version specification format * ruff format * feat: add password confirmation when changing password (#5247) * feat: add password confirmation when changing password Fixes #5177 Adds a password confirmation field to prevent accidental password typos. Changes: - Backend: validate confirm_password matches new_password - Frontend: add confirmation input with validation - i18n: add labels and error messages for password mismatch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(auth): improve error message for password confirmation mismatch * fix(auth): update password hashing logic and improve confirmation validation --------- Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(provider): 修复 dict 格式 content 导致的 JSON 残留问题 (#5250) * fix(provider): 修复 dict 格式 content 导致的 JSON 残留问题 修复 _normalize_content 函数未处理 dict 类型 content 的问题。 当 LLM 返回 {"type": "text", "text": "..."} 格式的 content 时, 现在会正确提取 text 字段而非直接转为字符串。 同时改进 fallback 行为,对 None 值返回空字符串。 Fixes #5244 * Update warning message for unexpected dict format --------- Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> * chore: remove outdated heihe.md documentation file * fix: all mcp tools exposed to main agent (#5252) * fix: enhance PersonaForm layout and improve tool selection display * fix: update tool status display and add localization for inactive tools * fix: remove additionalProperties from tool schema properties (#5253) fixes: #5217 * fix: simplify error messages for account edit validation * fix: streamline error response for empty new username and password in account edit * chore: bump vertion to 4.17.6 * feat: add OpenRouter provider support and icon * chore: ruff format * refactor(dashboard): replace legacy isElectron bridge fields with isDesktop (#5269) * refactor dashboard desktop bridge fields from isElectron to isDesktop * refactor dashboard runtime detection into shared helper * fix: update contributor avatar image URL to include max size and columns (#5268) * feat: astrbot http api (#5280) * feat: astrbot http api * Potential fix for code scanning alert no. 34: Use of a broken or weak cryptographic hashing algorithm on sensitive data Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix: improve error handling for missing attachment path in file upload * feat: implement paginated retrieval of platform sessions for creators * feat: refactor attachment directory handling in ChatRoute * feat: update API endpoint paths for file and message handling * feat: add documentation link to API key management section in settings * feat: update API key scopes and related configurations in API routes and tests * feat: enhance API key expiration options and add warning for permanent keys * feat: add UTC normalization and serialization for API key timestamps * feat: implement chat session management and validation for usernames * feat: ignore session_id type chunks in message processing --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * feat(dashboard): improve plugin platform support display and mobile accessibility (#5271) * feat(dashboard): improve plugin platform support display and mobile accessibility - Replace hover-based tooltips with interactive click menus for platform support information. - Fix mobile touch issues by introducing explicit state control for status capsules. - Enhance UI aesthetics with platform-specific icons and a structured vertical list layout. - Add dynamic chevron icons to provide clear visual cues for expandable content. * refactor(dashboard): refactor market card with computed properties for performance * refactor(dashboard): unify plugin platform support UI with new reusable chip component - Create shared 'PluginPlatformChip' component to encapsulate platform meta display. - Fix mobile interaction bugs by simplifying menu triggers and event handling. - Add stacked platform icon previews and dynamic chevron indicators within capsules. - Improve information hierarchy using structured vertical lists for platform details. - Optimize rendering efficiency with computed properties across both card views. * fix: qq official guild message send error (#5287) * fix: qq official guild message send error * Update astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * 更新readme文档,补充桌面app说明,并向前移动位置 (#5297) * docs: update desktop deployment section in README * docs: refine desktop and launcher deployment descriptions * Update README.md * feat: add Anthropic Claude Code OAuth provider and adaptive thinking support (#5209) * feat: add Anthropic Claude Code OAuth provider and adaptive thinking support * fix: add defensive guard for metadata overrides and align budget condition with docs * refactor: adopt sourcery-ai suggestions for OAuth provider - Use use_api_key=False in OAuth subclass to avoid redundant API-key client construction before replacing with auth_token client - Generalize metadata override helper to merge all dict keys instead of only handling 'limit', improving extensibility * Feat/telegram command alias register #5233 (#5234) * feat: support registering command aliases for Telegram Now when registering commands with aliases, all aliases will be registered as Telegram bot commands in addition to the main command. Example: @register_command(command_name="draw", alias={"画", "gen"}) Now /draw, /画, and /gen will all appear in the Telegram command menu. * feat(telegram): add duplicate command name warning when registering commands Log a warning when duplicate command names are detected during Telegram command registration to help identify configuration conflicts. * refactor: remove Anthropic OAuth provider implementation and related metadata overrides * fix: 修复新建对话时因缺少会话ID导致配置绑定失败的问题 (#5292) * fix:尝试修改 * fix:添加详细日志 * fix:进行详细修改,并添加日志 * fix:删除所有日志 * fix: 增加安全访问函数 - 给 localStorage 访问加了 try/catch + 可用性判断:dashboard/src/utils/chatConfigBinding.ts:13 - 新增 getFromLocalStorage/setToLocalStorage(在受限存储/无痕模式下异常时回退/忽略) - getStoredDashboardUsername() / getStoredSelectedChatConfigId() 改为走安全读取:dashboard/src/utils/chatConfigBinding.ts:36 - 新增 setStoredSelectedChatConfigId(),写入失败静默忽略:dashboard/src/utils/chatConfigBinding.ts:44 - 把 ConfigSelector.vue 里直接 localStorage.getItem/setItem 全部替换为上述安全方法:dashboard/src/components/chat/ConfigSelector.vue:81 - 已重新跑过 pnpm run typecheck,通过。 * rm:删除个人用的文档文件 * Revert "rm:删除个人用的文档文件" This reverts commit0fceee0543. * rm:删除个人用的文档文件 * rm:删除个人用的文档文件 * chore: bump version to 4.18.0 * fix(SubAgentPage): 当中间的介绍文本非常长时,Flex 布局会自动挤压右侧的控制按钮区域 (#5306) * fix: 修复新版本插件市场出现插件显示为空白的 bug;纠正已安装插件卡片的排版,统一大小 (#5309) * fix(ExtensionCard): 解决插件卡片大小不统一的问题 * fix(MarketPluginCard): 解决插件市场不加载插件的问题 (#5303) * feat: supports spawn subagent as a background task that not block the main agent workflow (#5081) * feat:为subagent添加后台任务参数 * ruff * fix: update terminology from 'handoff mission' to 'background task' and refactor related logic * fix: update terminology from 'background_mission' to 'background_task' in HandoffTool and related logic * fix(HandoffTool): update background_task description for clarity on usage --------- Co-authored-by: Soulter <905617992@qq.com> * cho * fix: 修复 aiohttp 版本过新导致 qq-botpy 报错的问题 (#5316) * chore: ruff format * fix: remove hard-coded 6s timeout from tavily request * fix: remove changelogs directory from .dockerignore * feat(dashboard): make release redirect base URL configurable (#5330) * feat(dashboard): make desktop release base URL configurable * refactor(dashboard): use generic release base URL env with upstream default * fix(dashboard): guard release base URL normalization when env is unset * refactor(dashboard): use generic release URL helpers and avoid latest suffix duplication * feat: add stop functionality for active agent sessions and improve handling of stop requests (#5380) * feat: add stop functionality for active agent sessions and improve handling of stop requests * feat: update stop button icon and tooltip in ChatInput component * fix: correct indentation in tool call handling within ChatRoute class * fix: chatui cannot persist file segment (#5386) * fix(plugin): update plugin directory handling for reserved plugins (#5369) * fix(plugin): update plugin directory handling for reserved plugins * fix(plugin): add warning logs for missing plugin name, object, directory, and changelog * chore(README): updated with README.md (#5375) * chore(README): updated with README.md * Update README_fr.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * Update README_zh-TW.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --------- Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> * feat: add image urls / paths supports for subagent (#5348) * fix: 修复5081号PR在子代理执行后台任务时,未正确使用系统配置的流式/非流请求的问题(#5081) * feat:为子代理增加远程图片URL参数支持 * fix: update description for image_urls parameter in HandoffTool to clarify usage in multimodal tasks * ruff format --------- Co-authored-by: Soulter <905617992@qq.com> * feat: add hot reload when failed to load plugins (#5334) * feat:add hot reload when failed to load plugins * apply bot suggestions * fix(chatui): add copy rollback path and error message. (#5352) * fix(chatui): add copy rollback path and error message. * fix(chatui): fixed textarea leak in the copy button. * fix(chatui): use color styles from the component library. * fix: 处理配置文件中的 UTF-8 BOM 编码问题 (#5376) * fix(config): handle UTF-8 BOM in configuration file loading Problem: On Windows, some text editors (like Notepad) automatically add UTF-8 BOM to JSON files when saving. This causes json.decoder.JSONDecodeError: "Unexpected UTF-8 BOM" and AstrBot fails to start when cmd_config.json contains BOM. Solution: Add defensive check to strip UTF-8 BOM (\ufeff) if present before parsing JSON configuration file. Impact: - Improves robustness and cross-platform compatibility - No breaking changes to existing functionality - Fixes startup failure when configuration file has UTF-8 BOM encoding Relates-to: Windows editor compatibility issues * style: fix code formatting with ruff Fix single quote to double quote to comply with project code style. * feat: add plugin load&unload hook (#5331) * 添加了插件的加载完成和卸载完成的钩子事件 * 添加了插件的加载完成和卸载完成的钩子事件 * format code with ruff * ruff format --------- Co-authored-by: Soulter <905617992@qq.com> * test: enhance test framework with comprehensive fixtures and mocks (#5354) * test: enhance test framework with comprehensive fixtures and mocks - Add shared mock builders for aiocqhttp, discord, telegram - Add test helpers for platform configs and mock objects - Expand conftest.py with test profile support - Update coverage test workflow configuration Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(tests): 移动并重构模拟 LLM 响应和消息组件函数 * fix(tests): 优化 pytest_runtest_setup 中的标记检查逻辑 --------- Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add comprehensive tests for message event handling (#5355) * test: add comprehensive tests for message event handling - Add AstrMessageEvent unit tests (688 lines) - Add AstrBotMessage unit tests - Enhance smoke tests with message event scenarios Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: improve message type handling and add defensive tests --------- Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add support for showing tool call results in agent execution (#5388) closes: #5329 * fix: resolve pipeline and star import cycles (#5353) * fix: resolve pipeline and star import cycles - Add bootstrap.py and stage_order.py to break circular dependencies - Export Context, PluginManager, StarTools from star module - Update pipeline __init__ to defer imports - Split pipeline initialization into separate bootstrap module Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add logging for get_config() failure in Star class * fix: reorder logger initialization in base.py --------- Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: enable computer-use tools for subagent handoff (#5399) * fix: enforce admin guard for sandbox file transfer tools (#5402) * fix: enforce admin guard for sandbox file transfer tools * refactor: deduplicate computer tools admin permission checks * fix: add missing space in permission error message * fix(core): 优化 File 组件处理逻辑并增强 OneBot 驱动层路径兼容性 (#5391) * fix(core): 优化 File 组件处理逻辑并增强 OneBot 驱动层路径兼容性 原因 (Necessity): 1. 内核一致性:AstrBot 内核的 Record 和 Video 组件均具备识别 `file:///` 协议头的逻辑,但 File 组件此前缺失此功能,导致行为不统一。 2. OneBot 协议合规:OneBot 11 标准要求本地文件路径必须使用 `file:///` 协议头。此前驱动层未对裸路径进行自动转换,导致发送本地文件时常触发 retcode 1200 (识别URL失败) 错误。 3. 容器环境适配:在 Docker 等路径隔离环境下,裸路径更容易因驱动或协议端的解析歧义而失效。 更改 (Changes): - [astrbot/core/message/components.py]: - 在 File.get_file() 中增加对 `file:///` 前缀的识别与剥离逻辑,使其与 Record/Video 组件行为对齐。 - [astrbot/core/platform/sources/aiocqhttp/aiocqhttp_message_event.py]: - 在发送文件前增加自动修正逻辑:若路径为绝对路径且未包含协议头,驱动层将自动补全 `file:///` 前缀。 - 对 http、base64 及已有协议头,确保不干扰原有的正常传输逻辑。 影响 (Impact): - 以完全兼容的方式增强了文件发送的鲁棒性。 - 解决了插件在发送日志等本地生成的压缩包时,因路径格式不规范导致的发送失败问题。 * refactor(core): 根据 cr 建议,规范化文件 URI 生成与解析逻辑,优化跨平台兼容性 原因 (Necessity): 1. 修复原生路径与 URI 转换在 Windows 下的不对称问题。 2. 规范化 file: 协议头处理,确保符合 RFC 标准并能在 Linux/Windows 间稳健切换。 3. 增强协议判定准确度,防止对普通绝对路径的误处理。 更改 (Changes): - [astrbot/core/platform/sources/aiocqhttp]: - 弃用手动拼接,改用 `pathlib.Path.as_uri()` 生成标准 URI。 - 将协议检测逻辑从前缀匹配优化为包含性检测 ("://")。 - [astrbot/core/message/components]: - 重构 `File.get_file` 解析逻辑,支持对称处理 2/3 斜杠格式。 - 针对 Windows 环境增加了对 `file:///C:/` 格式的自动修正,避免 `os.path` 识别失效。 - [data/plugins/astrbot_plugin_logplus]: - 在直接 API 调用中同步应用 URI 规范化处理。 影响 (Impact): - 解决 Docker 环境中因路径不规范导致的 "识别URL失败" 报错。 - 提升了本体框架在 Windows 系统下的文件操作鲁棒性。 * i18n(SubAgentPage): complete internationalization for subagent orchestration page (#5400) * i18n: complete internationalization for subagent orchestration page - Replace hardcoded English strings in [SubAgentPage.vue] with i18n keys. - Update `en-US` and `zh-CN` locales with missing hints, validation messages, and empty state translations. - Fix translation typos and improve consistency across the SubAgent orchestration UI. * fix(bug_risk): 避免在模板中的翻译调用上使用 || 'Close' 作为回退值。 * fix(aiocqhttp): enhance shutdown process for aiocqhttp adapter (#5412) * fix: pass embedding dimensions to provider apis (#5411) * fix(context): log warning when platform not found for session * fix(context): improve logging for platform not found in session * chore: bump version to 4.18.2 * chore: bump version to 4.18.2 * chore: bump version to 4.18.2 * fix: Telegram voice message format (OGG instead of WAV) causing issues with OpenAI STT API (#5389) * chore: ruff format * feat(dashboard): add generic desktop app updater bridge (#5424) * feat(dashboard): add generic desktop app updater bridge * fix(dashboard): address updater bridge review feedback * fix(dashboard): unify updater bridge types and error logging * fix(dashboard): consolidate updater bridge typings * fix(conversation): retain existing persona_id when updating conversation * fix(dashboard): 修复设置页新建 API Key 后复制失败问题 (#5439) * Fix: GitHub proxy not displaying correctly in WebUI (#5438) * fix(dashboard): preserve custom GitHub proxy setting on reload * fix(dashboard): keep github proxy selection persisted in settings * fix(persona): enhance persona resolution logic for conversations and sessions * fix: ensure tool call/response pairing in context truncation (#5417) * fix: ensure tool call/response pairing in context truncation * refactor: simplify fix_messages to single-pass state machine * perf(cron): enhance future task session isolation fixes: #5392 * feat: add useExtensionPage composable for managing plugin extensions - Implemented a new composable `useExtensionPage` to handle various functionalities related to plugin management, including fetching extensions, handling updates, and managing UI states. - Added support for conflict checking, plugin installation, and custom source management. - Integrated search and filtering capabilities for plugins in the market. - Enhanced user experience with dialogs for confirmations and notifications. - Included pagination and sorting features for better plugin visibility. * fix: clear markdown field when sending media messages via QQ Official Platform (#5445) * fix: clear markdown field when sending media messages via QQ Official API * refactor: use pop() to remove markdown key instead of setting None * fix: cannot automatically get embedding dim when create embedding provider (#5442) * fix(dashboard): 强化 API Key 复制临时节点清理逻辑 * fix(embedding): 自动检测改为探测 OpenAI embedding 最大可用维度 * fix: normalize openai embedding base url and add hint key * i18n: add embedding_api_base hint translations * i18n: localize provider embedding/proxy metadata hints * fix: show provider-specific embedding API Base URL hint as field subtitle * fix(embedding): cap OpenAI detect_dim probes with early short-circuit * fix(dashboard): return generic error on provider adapter import failure * 回退检测逻辑 * fix: 修复Pyright静态类型检查报错 (#5437) * refactor: 修正 Sqlite 查询、下载回调、接口重构与类型调整 * feat: 为 OneBotClient 增加 CallAction 协议与异步调用支持 * fix(telegram): avoid duplicate message_thread_id in streaming (#5430) * perf: batch metadata query in KB retrieval to fix N+1 problem (#5463) * perf: batch metadata query in KB retrieval to fix N+1 problem Replace N sequential get_document_with_metadata() calls with a single get_documents_with_metadata_batch() call using SQL IN clause. Benchmark results (local SQLite): - 10 docs: 10.67ms → 1.47ms (7.3x faster) - 20 docs: 26.00ms → 2.68ms (9.7x faster) - 50 docs: 63.87ms → 2.79ms (22.9x faster) * refactor: use set[str] param type and chunk IN clause for SQLite safety Address review feedback: - Change doc_ids param from list[str] to set[str] to avoid unnecessary conversion - Chunk IN clause into batches of 900 to stay under SQLite's 999 parameter limit - Remove list() wrapping at call site, pass set directly * fix:fix the issue where incomplete cleanup of residual plugins occurs… (#5462) * fix:fix the issue where incomplete cleanup of residual plugins occurs in the failed loading of plugins * fix:ruff format,apply bot suggestions * Apply suggestion from @gemini-code-assist[bot] Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * chore: 为类型检查添加 TYPE_CHECKING 的导入与阶段类型引用 (#5474) * fix(line): line adapter does not appear in the add platform dialog fixes: #5477 * [bug]查看介入教程line前往错误界面的问题 (#5479) Fixes #5478 * chore: bump version to 4.18.3 * feat: implement follow-up message handling in ToolLoopAgentRunner (#5484) * feat: implement follow-up message handling in ToolLoopAgentRunner * fix: correct import path for follow-up module in InternalAgentSubStage * feat: implement websockets transport mode selection for chat (#5410) * feat: implement websockets transport mode selection for chat - Added transport mode selection (SSE/WebSocket) in the chat component. - Updated conversation sidebar to include transport mode options. - Integrated transport mode handling in message sending logic. - Refactored message sending functions to support both SSE and WebSocket. - Enhanced WebSocket connection management and message handling. - Updated localization files for transport mode labels. - Configured Vite to support WebSocket proxying. * feat(webchat): refactor message parsing logic and integrate new parsing function * feat(chat): add websocket API key extraction and scope validation * Revert "可选后端,实现前后端分离" (#5536) --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: can <51474963+weijintaocode@users.noreply.github.com> Co-authored-by: Soulter <905617992@qq.com> Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com> Co-authored-by: letr <123731298+letr007@users.noreply.github.com> Co-authored-by: 搁浅 <id6543156918@gmail.com> Co-authored-by: Helian Nuits <sxp20061207@163.com> Co-authored-by: Gao Jinzhe <2968474907@qq.com> Co-authored-by: DD斩首 <155905740+DDZS987@users.noreply.github.com> Co-authored-by: Ubuntu <ubuntu@localhost.localdomain> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com> Co-authored-by: エイカク <62183434+zouyonghe@users.noreply.github.com> Co-authored-by: 鸦羽 <Raven95676@gmail.com> Co-authored-by: Dt8333 <25431943+Dt8333@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Li-shi-ling <114913764+Li-shi-ling@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> Co-authored-by: Limitless <127183162+Limitless2023@users.noreply.github.com> Co-authored-by: Limitless2023 <limitless@users.noreply.github.com> Co-authored-by: evpeople <54983536+evpeople@users.noreply.github.com> Co-authored-by: SnowNightt <127504703+SnowNightt@users.noreply.github.com> Co-authored-by: xzj0898 <62733743+xzj0898@users.noreply.github.com> Co-authored-by: stevessr <89645372+stevessr@users.noreply.github.com> Co-authored-by: Waterwzy <2916963017@qq.com> Co-authored-by: NayukiMeko <MekoNayuki@outlook.com> Co-authored-by: 時壹 <137363396+KBVsent@users.noreply.github.com> Co-authored-by: sanyekana <Clhikari@qq.com> Co-authored-by: Chiu Chun-Hsien <95356121+911218sky@users.noreply.github.com> Co-authored-by: Dream Tokenizer <60459821+Trance-0@users.noreply.github.com> Co-authored-by: NanoRocky <76585834+NanoRocky@users.noreply.github.com> Co-authored-by: Pizero <zhaory200707@outlook.com> Co-authored-by: 雪語 <167516635+YukiRa1n@users.noreply.github.com> Co-authored-by: whatevertogo <1879483647@qq.com> Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com> Co-authored-by: 香草味的纳西妲喵 <151599587+VanillaNahida@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Lovely Moe Moli <44719954+moemoli@users.noreply.github.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Minidoracat <minidora0702@gmail.com> Co-authored-by: Chen <42998804+a61995987@users.noreply.github.com> Co-authored-by: hanbings <hanbings@hanbings.io> Co-authored-by: tangsenfei <155090747+tangsenfei@users.noreply.github.com> Co-authored-by: PyuraMazo <1605025385@qq.com> Co-authored-by: Axi404 <118950647+Axi404@users.noreply.github.com> Co-authored-by: 氕氙 <2014440212@qq.com> Co-authored-by: Yunhao Cao <18230652+realquantumcookie@users.noreply.github.com> Co-authored-by: exynos <110159911+exynos967@users.noreply.github.com> Co-authored-by: Luna_Dol <86590429+Luna-channel@users.noreply.github.com> Co-authored-by: CCCCCCTV <64309817+CCCCCCTV@users.noreply.github.com> Co-authored-by: CAICAII <3360776475@qq.com> Co-authored-by: 圣达生物多 <qq3258819795@163.com>
1122 lines
41 KiB
Python
1122 lines
41 KiB
Python
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import copy
|
||
import datetime
|
||
import json
|
||
import os
|
||
import zoneinfo
|
||
from collections.abc import Coroutine
|
||
from dataclasses import dataclass, field
|
||
|
||
from astrbot.core import logger
|
||
from astrbot.core.agent.handoff import HandoffTool
|
||
from astrbot.core.agent.mcp_client import MCPTool
|
||
from astrbot.core.agent.message import TextPart
|
||
from astrbot.core.agent.tool import ToolSet
|
||
from astrbot.core.astr_agent_context import AgentContextWrapper, AstrAgentContext
|
||
from astrbot.core.astr_agent_hooks import MAIN_AGENT_HOOKS
|
||
from astrbot.core.astr_agent_run_util import AgentRunner
|
||
from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor
|
||
from astrbot.core.astr_main_agent_resources import (
|
||
CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT,
|
||
EXECUTE_SHELL_TOOL,
|
||
FILE_DOWNLOAD_TOOL,
|
||
FILE_UPLOAD_TOOL,
|
||
KNOWLEDGE_BASE_QUERY_TOOL,
|
||
LIVE_MODE_SYSTEM_PROMPT,
|
||
LLM_SAFETY_MODE_SYSTEM_PROMPT,
|
||
LOCAL_EXECUTE_SHELL_TOOL,
|
||
LOCAL_PYTHON_TOOL,
|
||
PYTHON_TOOL,
|
||
SANDBOX_MODE_PROMPT,
|
||
SEND_MESSAGE_TO_USER_TOOL,
|
||
TOOL_CALL_PROMPT,
|
||
TOOL_CALL_PROMPT_SKILLS_LIKE_MODE,
|
||
retrieve_knowledge_base,
|
||
)
|
||
from astrbot.core.conversation_mgr import Conversation
|
||
from astrbot.core.message.components import File, Image, Reply
|
||
from astrbot.core.platform.astr_message_event import AstrMessageEvent
|
||
from astrbot.core.provider import Provider
|
||
from astrbot.core.provider.entities import ProviderRequest
|
||
from astrbot.core.skills.skill_manager import SkillManager, build_skills_prompt
|
||
from astrbot.core.star.context import Context
|
||
from astrbot.core.star.star_handler import star_map
|
||
from astrbot.core.tools.cron_tools import (
|
||
CREATE_CRON_JOB_TOOL,
|
||
DELETE_CRON_JOB_TOOL,
|
||
LIST_CRON_JOBS_TOOL,
|
||
)
|
||
from astrbot.core.utils.file_extract import extract_file_moonshotai
|
||
from astrbot.core.utils.llm_metadata import LLM_METADATAS
|
||
from astrbot.core.utils.quoted_message.settings import (
|
||
SETTINGS as DEFAULT_QUOTED_MESSAGE_SETTINGS,
|
||
)
|
||
from astrbot.core.utils.quoted_message.settings import (
|
||
QuotedMessageParserSettings,
|
||
)
|
||
from astrbot.core.utils.quoted_message_parser import (
|
||
extract_quoted_message_images,
|
||
extract_quoted_message_text,
|
||
)
|
||
from astrbot.core.utils.string_utils import normalize_and_dedupe_strings
|
||
|
||
|
||
@dataclass(slots=True)
|
||
class MainAgentBuildConfig:
|
||
"""The main agent build configuration.
|
||
Most of the configs can be found in the cmd_config.json"""
|
||
|
||
tool_call_timeout: int
|
||
"""The timeout (in seconds) for a tool call.
|
||
When the tool call exceeds this time,
|
||
a timeout error as a tool result will be returned.
|
||
"""
|
||
tool_schema_mode: str = "full"
|
||
"""The tool schema mode, can be 'full' or 'skills-like'."""
|
||
provider_wake_prefix: str = ""
|
||
"""The wake prefix for the provider. If the user message does not start with this prefix,
|
||
the main agent will not be triggered."""
|
||
streaming_response: bool = True
|
||
"""Whether to use streaming response."""
|
||
sanitize_context_by_modalities: bool = False
|
||
"""Whether to sanitize the context based on the provider's supported modalities.
|
||
This will remove unsupported message types(e.g. image) from the context to prevent issues."""
|
||
kb_agentic_mode: bool = False
|
||
"""Whether to use agentic mode for knowledge base retrieval.
|
||
This will inject the knowledge base query tool into the main agent's toolset to allow dynamic querying."""
|
||
file_extract_enabled: bool = False
|
||
"""Whether to enable file content extraction for uploaded files."""
|
||
file_extract_prov: str = "moonshotai"
|
||
"""The file extraction provider."""
|
||
file_extract_msh_api_key: str = ""
|
||
"""The API key for Moonshot AI file extraction provider."""
|
||
context_limit_reached_strategy: str = "truncate_by_turns"
|
||
"""The strategy to handle context length limit reached."""
|
||
llm_compress_instruction: str = ""
|
||
"""The instruction for compression in llm_compress strategy."""
|
||
llm_compress_keep_recent: int = 6
|
||
"""The number of most recent turns to keep during llm_compress strategy."""
|
||
llm_compress_provider_id: str = ""
|
||
"""The provider ID for the LLM used in context compression."""
|
||
max_context_length: int = -1
|
||
"""The maximum number of turns to keep in context. -1 means no limit.
|
||
This enforce max turns before compression"""
|
||
dequeue_context_length: int = 1
|
||
"""The number of oldest turns to remove when context length limit is reached."""
|
||
llm_safety_mode: bool = True
|
||
"""This will inject healthy and safe system prompt into the main agent,
|
||
to prevent LLM output harmful information"""
|
||
safety_mode_strategy: str = "system_prompt"
|
||
computer_use_runtime: str = "local"
|
||
"""The runtime for agent computer use: none, local, or sandbox."""
|
||
sandbox_cfg: dict = field(default_factory=dict)
|
||
add_cron_tools: bool = True
|
||
"""This will add cron job management tools to the main agent for proactive cron job execution."""
|
||
provider_settings: dict = field(default_factory=dict)
|
||
subagent_orchestrator: dict = field(default_factory=dict)
|
||
timezone: str | None = None
|
||
max_quoted_fallback_images: int = 20
|
||
"""Maximum number of images injected from quoted-message fallback extraction."""
|
||
|
||
|
||
@dataclass(slots=True)
|
||
class MainAgentBuildResult:
|
||
agent_runner: AgentRunner
|
||
provider_request: ProviderRequest
|
||
provider: Provider
|
||
reset_coro: Coroutine | None = None
|
||
|
||
|
||
def _select_provider(
|
||
event: AstrMessageEvent, plugin_context: Context
|
||
) -> Provider | None:
|
||
"""Select chat provider for the event."""
|
||
sel_provider = event.get_extra("selected_provider")
|
||
if sel_provider and isinstance(sel_provider, str):
|
||
provider = plugin_context.get_provider_by_id(sel_provider)
|
||
if not provider:
|
||
logger.error("未找到指定的提供商: %s。", sel_provider)
|
||
if not isinstance(provider, Provider):
|
||
logger.error(
|
||
"选择的提供商类型无效(%s),跳过 LLM 请求处理。", type(provider)
|
||
)
|
||
return None
|
||
return provider
|
||
try:
|
||
return plugin_context.get_using_provider(umo=event.unified_msg_origin)
|
||
except ValueError as exc:
|
||
logger.error("Error occurred while selecting provider: %s", exc)
|
||
return None
|
||
|
||
|
||
async def _get_session_conv(
|
||
event: AstrMessageEvent, plugin_context: Context
|
||
) -> Conversation:
|
||
conv_mgr = plugin_context.conversation_manager
|
||
umo = event.unified_msg_origin
|
||
cid = await conv_mgr.get_curr_conversation_id(umo)
|
||
if not cid:
|
||
cid = await conv_mgr.new_conversation(umo, event.get_platform_id())
|
||
conversation = await conv_mgr.get_conversation(umo, cid)
|
||
if not conversation:
|
||
cid = await conv_mgr.new_conversation(umo, event.get_platform_id())
|
||
conversation = await conv_mgr.get_conversation(umo, cid)
|
||
if not conversation:
|
||
raise RuntimeError("无法创建新的对话。")
|
||
return conversation
|
||
|
||
|
||
async def _apply_kb(
|
||
event: AstrMessageEvent,
|
||
req: ProviderRequest,
|
||
plugin_context: Context,
|
||
config: MainAgentBuildConfig,
|
||
) -> None:
|
||
if not config.kb_agentic_mode:
|
||
if req.prompt is None:
|
||
return
|
||
try:
|
||
kb_result = await retrieve_knowledge_base(
|
||
query=req.prompt,
|
||
umo=event.unified_msg_origin,
|
||
context=plugin_context,
|
||
)
|
||
if not kb_result:
|
||
return
|
||
if req.system_prompt is not None:
|
||
req.system_prompt += (
|
||
f"\n\n[Related Knowledge Base Results]:\n{kb_result}"
|
||
)
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.error("Error occurred while retrieving knowledge base: %s", exc)
|
||
else:
|
||
if req.func_tool is None:
|
||
req.func_tool = ToolSet()
|
||
req.func_tool.add_tool(KNOWLEDGE_BASE_QUERY_TOOL)
|
||
|
||
|
||
async def _apply_file_extract(
|
||
event: AstrMessageEvent,
|
||
req: ProviderRequest,
|
||
config: MainAgentBuildConfig,
|
||
) -> None:
|
||
file_paths = []
|
||
file_names = []
|
||
for comp in event.message_obj.message:
|
||
if isinstance(comp, File):
|
||
file_paths.append(await comp.get_file())
|
||
file_names.append(comp.name)
|
||
elif isinstance(comp, Reply) and comp.chain:
|
||
for reply_comp in comp.chain:
|
||
if isinstance(reply_comp, File):
|
||
file_paths.append(await reply_comp.get_file())
|
||
file_names.append(reply_comp.name)
|
||
if not file_paths:
|
||
return
|
||
if not req.prompt:
|
||
req.prompt = "总结一下文件里面讲了什么?"
|
||
if config.file_extract_prov == "moonshotai":
|
||
if not config.file_extract_msh_api_key:
|
||
logger.error("Moonshot AI API key for file extract is not set")
|
||
return
|
||
file_contents = await asyncio.gather(
|
||
*[
|
||
extract_file_moonshotai(
|
||
file_path,
|
||
config.file_extract_msh_api_key,
|
||
)
|
||
for file_path in file_paths
|
||
]
|
||
)
|
||
else:
|
||
logger.error("Unsupported file extract provider: %s", config.file_extract_prov)
|
||
return
|
||
|
||
for file_content, file_name in zip(file_contents, file_names):
|
||
req.contexts.append(
|
||
{
|
||
"role": "system",
|
||
"content": (
|
||
"File Extract Results of user uploaded files:\n"
|
||
f"{file_content}\nFile Name: {file_name or 'Unknown'}"
|
||
),
|
||
},
|
||
)
|
||
|
||
|
||
def _apply_prompt_prefix(req: ProviderRequest, cfg: dict) -> None:
|
||
prefix = cfg.get("prompt_prefix")
|
||
if not prefix:
|
||
return
|
||
if "{{prompt}}" in prefix:
|
||
req.prompt = prefix.replace("{{prompt}}", req.prompt)
|
||
else:
|
||
req.prompt = f"{prefix}{req.prompt}"
|
||
|
||
|
||
def _apply_local_env_tools(req: ProviderRequest) -> None:
|
||
if req.func_tool is None:
|
||
req.func_tool = ToolSet()
|
||
req.func_tool.add_tool(LOCAL_EXECUTE_SHELL_TOOL)
|
||
req.func_tool.add_tool(LOCAL_PYTHON_TOOL)
|
||
|
||
|
||
async def _ensure_persona_and_skills(
|
||
req: ProviderRequest,
|
||
cfg: dict,
|
||
plugin_context: Context,
|
||
event: AstrMessageEvent,
|
||
) -> None:
|
||
"""Ensure persona and skills are applied to the request's system prompt or user prompt."""
|
||
if not req.conversation:
|
||
return
|
||
|
||
(
|
||
persona_id,
|
||
persona,
|
||
_,
|
||
use_webchat_special_default,
|
||
) = await plugin_context.persona_manager.resolve_selected_persona(
|
||
umo=event.unified_msg_origin,
|
||
conversation_persona_id=req.conversation.persona_id,
|
||
platform_name=event.get_platform_name(),
|
||
provider_settings=cfg,
|
||
)
|
||
|
||
if persona:
|
||
# Inject persona system prompt
|
||
if prompt := persona["prompt"]:
|
||
req.system_prompt += f"\n# Persona Instructions\n\n{prompt}\n"
|
||
if begin_dialogs := copy.deepcopy(persona.get("_begin_dialogs_processed")):
|
||
req.contexts[:0] = begin_dialogs
|
||
elif use_webchat_special_default:
|
||
req.system_prompt += CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT
|
||
|
||
# Inject skills prompt
|
||
runtime = cfg.get("computer_use_runtime", "local")
|
||
skill_manager = SkillManager()
|
||
skills = skill_manager.list_skills(active_only=True, runtime=runtime)
|
||
|
||
if skills:
|
||
if persona and persona.get("skills") is not None:
|
||
if not persona["skills"]:
|
||
skills = []
|
||
else:
|
||
allowed = set(persona["skills"])
|
||
skills = [skill for skill in skills if skill.name in allowed]
|
||
if skills:
|
||
req.system_prompt += f"\n{build_skills_prompt(skills)}\n"
|
||
if runtime == "none":
|
||
req.system_prompt += (
|
||
"User has not enabled the Computer Use feature. "
|
||
"You cannot use shell or Python to perform skills. "
|
||
"If you need to use these capabilities, ask the user to enable Computer Use in the AstrBot WebUI -> Config."
|
||
)
|
||
tmgr = plugin_context.get_llm_tool_manager()
|
||
|
||
# inject toolset in the persona
|
||
if (persona and persona.get("tools") is None) or not persona:
|
||
persona_toolset = tmgr.get_full_tool_set()
|
||
for tool in list(persona_toolset):
|
||
if not tool.active:
|
||
persona_toolset.remove_tool(tool.name)
|
||
else:
|
||
persona_toolset = ToolSet()
|
||
if persona["tools"]:
|
||
for tool_name in persona["tools"]:
|
||
tool = tmgr.get_func(tool_name)
|
||
if tool and tool.active:
|
||
persona_toolset.add_tool(tool)
|
||
if not req.func_tool:
|
||
req.func_tool = persona_toolset
|
||
else:
|
||
req.func_tool.merge(persona_toolset)
|
||
|
||
# sub agents integration
|
||
orch_cfg = plugin_context.get_config().get("subagent_orchestrator", {})
|
||
so = plugin_context.subagent_orchestrator
|
||
if orch_cfg.get("main_enable", False) and so:
|
||
remove_dup = bool(orch_cfg.get("remove_main_duplicate_tools", False))
|
||
|
||
assigned_tools: set[str] = set()
|
||
agents = orch_cfg.get("agents", [])
|
||
if isinstance(agents, list):
|
||
for a in agents:
|
||
if not isinstance(a, dict):
|
||
continue
|
||
if a.get("enabled", True) is False:
|
||
continue
|
||
persona_tools = None
|
||
pid = a.get("persona_id")
|
||
if pid:
|
||
persona_tools = next(
|
||
(
|
||
p.get("tools")
|
||
for p in plugin_context.persona_manager.personas_v3
|
||
if p["name"] == pid
|
||
),
|
||
None,
|
||
)
|
||
tools = a.get("tools", [])
|
||
if persona_tools is not None:
|
||
tools = persona_tools
|
||
if tools is None:
|
||
assigned_tools.update(
|
||
[
|
||
tool.name
|
||
for tool in tmgr.func_list
|
||
if not isinstance(tool, HandoffTool)
|
||
]
|
||
)
|
||
continue
|
||
if not isinstance(tools, list):
|
||
continue
|
||
for t in tools:
|
||
name = str(t).strip()
|
||
if name:
|
||
assigned_tools.add(name)
|
||
|
||
if req.func_tool is None:
|
||
req.func_tool = ToolSet()
|
||
|
||
# add subagent handoff tools
|
||
for tool in so.handoffs:
|
||
req.func_tool.add_tool(tool)
|
||
|
||
# check duplicates
|
||
if remove_dup:
|
||
handoff_names = {tool.name for tool in so.handoffs}
|
||
for tool_name in assigned_tools:
|
||
if tool_name in handoff_names:
|
||
continue
|
||
req.func_tool.remove_tool(tool_name)
|
||
|
||
router_prompt = (
|
||
plugin_context.get_config()
|
||
.get("subagent_orchestrator", {})
|
||
.get("router_system_prompt", "")
|
||
).strip()
|
||
if router_prompt:
|
||
req.system_prompt += f"\n{router_prompt}\n"
|
||
try:
|
||
event.trace.record(
|
||
"sel_persona",
|
||
persona_id=persona_id,
|
||
persona_toolset=persona_toolset.names(),
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
async def _request_img_caption(
|
||
provider_id: str,
|
||
cfg: dict,
|
||
image_urls: list[str],
|
||
plugin_context: Context,
|
||
) -> str:
|
||
prov = plugin_context.get_provider_by_id(provider_id)
|
||
if prov is None:
|
||
raise ValueError(
|
||
f"Cannot get image caption because provider `{provider_id}` is not exist.",
|
||
)
|
||
if not isinstance(prov, Provider):
|
||
raise ValueError(
|
||
f"Cannot get image caption because provider `{provider_id}` is not a valid Provider, it is {type(prov)}.",
|
||
)
|
||
|
||
img_cap_prompt = cfg.get(
|
||
"image_caption_prompt",
|
||
"Please describe the image.",
|
||
)
|
||
logger.debug("Processing image caption with provider: %s", provider_id)
|
||
llm_resp = await prov.text_chat(
|
||
prompt=img_cap_prompt,
|
||
image_urls=image_urls,
|
||
)
|
||
return llm_resp.completion_text
|
||
|
||
|
||
async def _ensure_img_caption(
|
||
req: ProviderRequest,
|
||
cfg: dict,
|
||
plugin_context: Context,
|
||
image_caption_provider: str,
|
||
) -> None:
|
||
try:
|
||
caption = await _request_img_caption(
|
||
image_caption_provider,
|
||
cfg,
|
||
req.image_urls,
|
||
plugin_context,
|
||
)
|
||
if caption:
|
||
req.extra_user_content_parts.append(
|
||
TextPart(text=f"<image_caption>{caption}</image_caption>")
|
||
)
|
||
req.image_urls = []
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.error("处理图片描述失败: %s", exc)
|
||
|
||
|
||
def _append_quoted_image_attachment(req: ProviderRequest, image_path: str) -> None:
|
||
req.extra_user_content_parts.append(
|
||
TextPart(text=f"[Image Attachment in quoted message: path {image_path}]")
|
||
)
|
||
|
||
|
||
def _get_quoted_message_parser_settings(
|
||
provider_settings: dict[str, object] | None,
|
||
) -> QuotedMessageParserSettings:
|
||
if not isinstance(provider_settings, dict):
|
||
return DEFAULT_QUOTED_MESSAGE_SETTINGS
|
||
overrides = provider_settings.get("quoted_message_parser")
|
||
if not isinstance(overrides, dict):
|
||
return DEFAULT_QUOTED_MESSAGE_SETTINGS
|
||
return DEFAULT_QUOTED_MESSAGE_SETTINGS.with_overrides(overrides)
|
||
|
||
|
||
async def _process_quote_message(
|
||
event: AstrMessageEvent,
|
||
req: ProviderRequest,
|
||
img_cap_prov_id: str,
|
||
plugin_context: Context,
|
||
quoted_message_settings: QuotedMessageParserSettings = DEFAULT_QUOTED_MESSAGE_SETTINGS,
|
||
) -> None:
|
||
quote = None
|
||
for comp in event.message_obj.message:
|
||
if isinstance(comp, Reply):
|
||
quote = comp
|
||
break
|
||
if not quote:
|
||
return
|
||
|
||
content_parts = []
|
||
sender_info = f"({quote.sender_nickname}): " if quote.sender_nickname else ""
|
||
message_str = (
|
||
await extract_quoted_message_text(
|
||
event,
|
||
quote,
|
||
settings=quoted_message_settings,
|
||
)
|
||
or quote.message_str
|
||
or "[Empty Text]"
|
||
)
|
||
content_parts.append(f"{sender_info}{message_str}")
|
||
|
||
image_seg = None
|
||
if quote.chain:
|
||
for comp in quote.chain:
|
||
if isinstance(comp, Image):
|
||
image_seg = comp
|
||
break
|
||
|
||
if image_seg:
|
||
try:
|
||
prov = None
|
||
if img_cap_prov_id:
|
||
prov = plugin_context.get_provider_by_id(img_cap_prov_id)
|
||
if prov is None:
|
||
prov = plugin_context.get_using_provider(event.unified_msg_origin)
|
||
|
||
if prov and isinstance(prov, Provider):
|
||
llm_resp = await prov.text_chat(
|
||
prompt="Please describe the image content.",
|
||
image_urls=[await image_seg.convert_to_file_path()],
|
||
)
|
||
if llm_resp.completion_text:
|
||
content_parts.append(
|
||
f"[Image Caption in quoted message]: {llm_resp.completion_text}"
|
||
)
|
||
else:
|
||
logger.warning("No provider found for image captioning in quote.")
|
||
except BaseException as exc:
|
||
logger.error("处理引用图片失败: %s", exc)
|
||
|
||
quoted_content = "\n".join(content_parts)
|
||
quoted_text = f"<Quoted Message>\n{quoted_content}\n</Quoted Message>"
|
||
req.extra_user_content_parts.append(TextPart(text=quoted_text))
|
||
|
||
|
||
def _append_system_reminders(
|
||
event: AstrMessageEvent,
|
||
req: ProviderRequest,
|
||
cfg: dict,
|
||
timezone: str | None,
|
||
) -> None:
|
||
system_parts: list[str] = []
|
||
if cfg.get("identifier"):
|
||
user_id = event.message_obj.sender.user_id
|
||
user_nickname = event.message_obj.sender.nickname
|
||
system_parts.append(f"User ID: {user_id}, Nickname: {user_nickname}")
|
||
|
||
if cfg.get("group_name_display") and event.message_obj.group_id:
|
||
if not event.message_obj.group:
|
||
logger.error(
|
||
"Group name display enabled but group object is None. Group ID: %s",
|
||
event.message_obj.group_id,
|
||
)
|
||
else:
|
||
group_name = event.message_obj.group.group_name
|
||
if group_name:
|
||
system_parts.append(f"Group name: {group_name}")
|
||
|
||
if cfg.get("datetime_system_prompt"):
|
||
current_time = None
|
||
if timezone:
|
||
try:
|
||
now = datetime.datetime.now(zoneinfo.ZoneInfo(timezone))
|
||
current_time = now.strftime("%Y-%m-%d %H:%M (%Z)")
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.error("时区设置错误: %s, 使用本地时区", exc)
|
||
if not current_time:
|
||
current_time = (
|
||
datetime.datetime.now().astimezone().strftime("%Y-%m-%d %H:%M (%Z)")
|
||
)
|
||
system_parts.append(f"Current datetime: {current_time}")
|
||
|
||
if system_parts:
|
||
system_content = (
|
||
"<system_reminder>" + "\n".join(system_parts) + "</system_reminder>"
|
||
)
|
||
req.extra_user_content_parts.append(TextPart(text=system_content))
|
||
|
||
|
||
async def _decorate_llm_request(
|
||
event: AstrMessageEvent,
|
||
req: ProviderRequest,
|
||
plugin_context: Context,
|
||
config: MainAgentBuildConfig,
|
||
) -> None:
|
||
cfg = config.provider_settings or plugin_context.get_config(
|
||
umo=event.unified_msg_origin
|
||
).get("provider_settings", {})
|
||
|
||
_apply_prompt_prefix(req, cfg)
|
||
|
||
if req.conversation:
|
||
await _ensure_persona_and_skills(req, cfg, plugin_context, event)
|
||
|
||
img_cap_prov_id: str = cfg.get("default_image_caption_provider_id") or ""
|
||
if img_cap_prov_id and req.image_urls:
|
||
await _ensure_img_caption(
|
||
req,
|
||
cfg,
|
||
plugin_context,
|
||
img_cap_prov_id,
|
||
)
|
||
|
||
img_cap_prov_id = cfg.get("default_image_caption_provider_id") or ""
|
||
quoted_message_settings = _get_quoted_message_parser_settings(cfg)
|
||
await _process_quote_message(
|
||
event,
|
||
req,
|
||
img_cap_prov_id,
|
||
plugin_context,
|
||
quoted_message_settings,
|
||
)
|
||
|
||
tz = config.timezone
|
||
if tz is None:
|
||
tz = plugin_context.get_config().get("timezone")
|
||
_append_system_reminders(event, req, cfg, tz)
|
||
|
||
|
||
def _modalities_fix(provider: Provider, req: ProviderRequest) -> None:
|
||
if req.image_urls:
|
||
provider_cfg = provider.provider_config.get("modalities", ["image"])
|
||
if "image" not in provider_cfg:
|
||
logger.debug(
|
||
"Provider %s does not support image, using placeholder.", provider
|
||
)
|
||
image_count = len(req.image_urls)
|
||
placeholder = " ".join(["[图片]"] * image_count)
|
||
if req.prompt:
|
||
req.prompt = f"{placeholder} {req.prompt}"
|
||
else:
|
||
req.prompt = placeholder
|
||
req.image_urls = []
|
||
if req.func_tool:
|
||
provider_cfg = provider.provider_config.get("modalities", ["tool_use"])
|
||
if "tool_use" not in provider_cfg:
|
||
logger.debug(
|
||
"Provider %s does not support tool_use, clearing tools.", provider
|
||
)
|
||
req.func_tool = None
|
||
|
||
|
||
def _sanitize_context_by_modalities(
|
||
config: MainAgentBuildConfig,
|
||
provider: Provider,
|
||
req: ProviderRequest,
|
||
) -> None:
|
||
if not config.sanitize_context_by_modalities:
|
||
return
|
||
if not isinstance(req.contexts, list) or not req.contexts:
|
||
return
|
||
modalities = provider.provider_config.get("modalities", None)
|
||
if not modalities or not isinstance(modalities, list):
|
||
return
|
||
supports_image = bool("image" in modalities)
|
||
supports_tool_use = bool("tool_use" in modalities)
|
||
if supports_image and supports_tool_use:
|
||
return
|
||
|
||
sanitized_contexts: list[dict] = []
|
||
removed_image_blocks = 0
|
||
removed_tool_messages = 0
|
||
removed_tool_calls = 0
|
||
|
||
for msg in req.contexts:
|
||
if not isinstance(msg, dict):
|
||
continue
|
||
role = msg.get("role")
|
||
if not role:
|
||
continue
|
||
|
||
new_msg = msg
|
||
if not supports_tool_use:
|
||
if role == "tool":
|
||
removed_tool_messages += 1
|
||
continue
|
||
if role == "assistant" and "tool_calls" in new_msg:
|
||
if "tool_calls" in new_msg:
|
||
removed_tool_calls += 1
|
||
new_msg.pop("tool_calls", None)
|
||
new_msg.pop("tool_call_id", None)
|
||
|
||
if not supports_image:
|
||
content = new_msg.get("content")
|
||
if isinstance(content, list):
|
||
filtered_parts: list = []
|
||
removed_any_image = False
|
||
for part in content:
|
||
if isinstance(part, dict):
|
||
part_type = str(part.get("type", "")).lower()
|
||
if part_type in {"image_url", "image"}:
|
||
removed_any_image = True
|
||
removed_image_blocks += 1
|
||
continue
|
||
filtered_parts.append(part)
|
||
if removed_any_image:
|
||
new_msg["content"] = filtered_parts
|
||
|
||
if role == "assistant":
|
||
content = new_msg.get("content")
|
||
has_tool_calls = bool(new_msg.get("tool_calls"))
|
||
if not has_tool_calls:
|
||
if not content:
|
||
continue
|
||
if isinstance(content, str) and not content.strip():
|
||
continue
|
||
|
||
sanitized_contexts.append(new_msg)
|
||
|
||
if removed_image_blocks or removed_tool_messages or removed_tool_calls:
|
||
logger.debug(
|
||
"sanitize_context_by_modalities applied: "
|
||
"removed_image_blocks=%s, removed_tool_messages=%s, removed_tool_calls=%s",
|
||
removed_image_blocks,
|
||
removed_tool_messages,
|
||
removed_tool_calls,
|
||
)
|
||
req.contexts = sanitized_contexts
|
||
|
||
|
||
def _plugin_tool_fix(event: AstrMessageEvent, req: ProviderRequest) -> None:
|
||
"""根据事件中的插件设置,过滤请求中的工具列表。
|
||
|
||
注意:没有 handler_module_path 的工具(如 MCP 工具)会被保留,
|
||
因为它们不属于任何插件,不应被插件过滤逻辑影响。
|
||
"""
|
||
if event.plugins_name is not None and req.func_tool:
|
||
new_tool_set = ToolSet()
|
||
for tool in req.func_tool.tools:
|
||
if isinstance(tool, MCPTool):
|
||
# 保留 MCP 工具
|
||
new_tool_set.add_tool(tool)
|
||
continue
|
||
mp = tool.handler_module_path
|
||
if not mp:
|
||
continue
|
||
plugin = star_map.get(mp)
|
||
if not plugin:
|
||
continue
|
||
if plugin.name in event.plugins_name or plugin.reserved:
|
||
new_tool_set.add_tool(tool)
|
||
req.func_tool = new_tool_set
|
||
|
||
|
||
async def _handle_webchat(
|
||
event: AstrMessageEvent, req: ProviderRequest, prov: Provider
|
||
) -> None:
|
||
from astrbot.core import db_helper
|
||
|
||
chatui_session_id = event.session_id.split("!")[-1]
|
||
user_prompt = req.prompt
|
||
session = await db_helper.get_platform_session_by_id(chatui_session_id)
|
||
|
||
if not user_prompt or not chatui_session_id or not session or session.display_name:
|
||
return
|
||
|
||
llm_resp = await prov.text_chat(
|
||
system_prompt=(
|
||
"You are a conversation title generator. "
|
||
"Generate a concise title in the same language as the user’s input, "
|
||
"no more than 10 words, capturing only the core topic."
|
||
"If the input is a greeting, small talk, or has no clear topic, "
|
||
"(e.g., “hi”, “hello”, “haha”), return <None>. "
|
||
"Output only the title itself or <None>, with no explanations."
|
||
),
|
||
prompt=f"Generate a concise title for the following user query:\n{user_prompt}",
|
||
)
|
||
if llm_resp and llm_resp.completion_text:
|
||
title = llm_resp.completion_text.strip()
|
||
if not title or "<None>" in title:
|
||
return
|
||
logger.info(
|
||
"Generated chatui title for session %s: %s", chatui_session_id, title
|
||
)
|
||
await db_helper.update_platform_session(
|
||
session_id=chatui_session_id,
|
||
display_name=title,
|
||
)
|
||
|
||
|
||
def _apply_llm_safety_mode(config: MainAgentBuildConfig, req: ProviderRequest) -> None:
|
||
if config.safety_mode_strategy == "system_prompt":
|
||
req.system_prompt = (
|
||
f"{LLM_SAFETY_MODE_SYSTEM_PROMPT}\n\n{req.system_prompt or ''}"
|
||
)
|
||
else:
|
||
logger.warning(
|
||
"Unsupported llm_safety_mode strategy: %s.",
|
||
config.safety_mode_strategy,
|
||
)
|
||
|
||
|
||
def _apply_sandbox_tools(
|
||
config: MainAgentBuildConfig, req: ProviderRequest, session_id: str
|
||
) -> None:
|
||
if req.func_tool is None:
|
||
req.func_tool = ToolSet()
|
||
if config.sandbox_cfg.get("booter") == "shipyard":
|
||
ep = config.sandbox_cfg.get("shipyard_endpoint", "")
|
||
at = config.sandbox_cfg.get("shipyard_access_token", "")
|
||
if not ep or not at:
|
||
logger.error("Shipyard sandbox configuration is incomplete.")
|
||
return
|
||
os.environ["SHIPYARD_ENDPOINT"] = ep
|
||
os.environ["SHIPYARD_ACCESS_TOKEN"] = at
|
||
req.func_tool.add_tool(EXECUTE_SHELL_TOOL)
|
||
req.func_tool.add_tool(PYTHON_TOOL)
|
||
req.func_tool.add_tool(FILE_UPLOAD_TOOL)
|
||
req.func_tool.add_tool(FILE_DOWNLOAD_TOOL)
|
||
req.system_prompt += f"\n{SANDBOX_MODE_PROMPT}\n"
|
||
|
||
|
||
def _proactive_cron_job_tools(req: ProviderRequest) -> None:
|
||
if req.func_tool is None:
|
||
req.func_tool = ToolSet()
|
||
req.func_tool.add_tool(CREATE_CRON_JOB_TOOL)
|
||
req.func_tool.add_tool(DELETE_CRON_JOB_TOOL)
|
||
req.func_tool.add_tool(LIST_CRON_JOBS_TOOL)
|
||
|
||
|
||
def _get_compress_provider(
|
||
config: MainAgentBuildConfig, plugin_context: Context
|
||
) -> Provider | None:
|
||
if not config.llm_compress_provider_id:
|
||
return None
|
||
if config.context_limit_reached_strategy != "llm_compress":
|
||
return None
|
||
provider = plugin_context.get_provider_by_id(config.llm_compress_provider_id)
|
||
if provider is None:
|
||
logger.warning(
|
||
"未找到指定的上下文压缩模型 %s,将跳过压缩。",
|
||
config.llm_compress_provider_id,
|
||
)
|
||
return None
|
||
if not isinstance(provider, Provider):
|
||
logger.warning(
|
||
"指定的上下文压缩模型 %s 不是对话模型,将跳过压缩。",
|
||
config.llm_compress_provider_id,
|
||
)
|
||
return None
|
||
return provider
|
||
|
||
|
||
def _get_fallback_chat_providers(
|
||
provider: Provider, plugin_context: Context, provider_settings: dict
|
||
) -> list[Provider]:
|
||
fallback_ids = provider_settings.get("fallback_chat_models", [])
|
||
if not isinstance(fallback_ids, list):
|
||
logger.warning(
|
||
"fallback_chat_models setting is not a list, skip fallback providers."
|
||
)
|
||
return []
|
||
|
||
provider_id = str(provider.provider_config.get("id", ""))
|
||
seen_provider_ids: set[str] = {provider_id} if provider_id else set()
|
||
fallbacks: list[Provider] = []
|
||
|
||
for fallback_id in fallback_ids:
|
||
if not isinstance(fallback_id, str) or not fallback_id:
|
||
continue
|
||
if fallback_id in seen_provider_ids:
|
||
continue
|
||
fallback_provider = plugin_context.get_provider_by_id(fallback_id)
|
||
if fallback_provider is None:
|
||
logger.warning("Fallback chat provider `%s` not found, skip.", fallback_id)
|
||
continue
|
||
if not isinstance(fallback_provider, Provider):
|
||
logger.warning(
|
||
"Fallback chat provider `%s` is invalid type: %s, skip.",
|
||
fallback_id,
|
||
type(fallback_provider),
|
||
)
|
||
continue
|
||
fallbacks.append(fallback_provider)
|
||
seen_provider_ids.add(fallback_id)
|
||
return fallbacks
|
||
|
||
|
||
async def build_main_agent(
|
||
*,
|
||
event: AstrMessageEvent,
|
||
plugin_context: Context,
|
||
config: MainAgentBuildConfig,
|
||
provider: Provider | None = None,
|
||
req: ProviderRequest | None = None,
|
||
apply_reset: bool = True,
|
||
) -> MainAgentBuildResult | None:
|
||
"""构建主对话代理(Main Agent),并且自动 reset。
|
||
|
||
If apply_reset is False, will not call reset on the agent runner.
|
||
"""
|
||
provider = provider or _select_provider(event, plugin_context)
|
||
if provider is None:
|
||
logger.info("未找到任何对话模型(提供商),跳过 LLM 请求处理。")
|
||
return None
|
||
|
||
if req is None:
|
||
if event.get_extra("provider_request"):
|
||
req = event.get_extra("provider_request")
|
||
assert isinstance(req, ProviderRequest), (
|
||
"provider_request 必须是 ProviderRequest 类型。"
|
||
)
|
||
if req.conversation:
|
||
req.contexts = json.loads(req.conversation.history)
|
||
else:
|
||
req = ProviderRequest()
|
||
req.prompt = ""
|
||
req.image_urls = []
|
||
if sel_model := event.get_extra("selected_model"):
|
||
req.model = sel_model
|
||
if config.provider_wake_prefix and not event.message_str.startswith(
|
||
config.provider_wake_prefix
|
||
):
|
||
return None
|
||
|
||
req.prompt = event.message_str[len(config.provider_wake_prefix) :]
|
||
|
||
# media files attachments
|
||
for comp in event.message_obj.message:
|
||
if isinstance(comp, Image):
|
||
image_path = await comp.convert_to_file_path()
|
||
req.image_urls.append(image_path)
|
||
req.extra_user_content_parts.append(
|
||
TextPart(text=f"[Image Attachment: path {image_path}]")
|
||
)
|
||
elif isinstance(comp, File):
|
||
file_path = await comp.get_file()
|
||
file_name = comp.name or os.path.basename(file_path)
|
||
req.extra_user_content_parts.append(
|
||
TextPart(
|
||
text=f"[File Attachment: name {file_name}, path {file_path}]"
|
||
)
|
||
)
|
||
# quoted message attachments
|
||
reply_comps = [
|
||
comp for comp in event.message_obj.message if isinstance(comp, Reply)
|
||
]
|
||
quoted_message_settings = _get_quoted_message_parser_settings(
|
||
config.provider_settings
|
||
)
|
||
fallback_quoted_image_count = 0
|
||
for comp in reply_comps:
|
||
has_embedded_image = False
|
||
if comp.chain:
|
||
for reply_comp in comp.chain:
|
||
if isinstance(reply_comp, Image):
|
||
has_embedded_image = True
|
||
image_path = await reply_comp.convert_to_file_path()
|
||
req.image_urls.append(image_path)
|
||
_append_quoted_image_attachment(req, image_path)
|
||
elif isinstance(reply_comp, File):
|
||
file_path = await reply_comp.get_file()
|
||
file_name = reply_comp.name or os.path.basename(file_path)
|
||
req.extra_user_content_parts.append(
|
||
TextPart(
|
||
text=(
|
||
f"[File Attachment in quoted message: "
|
||
f"name {file_name}, path {file_path}]"
|
||
)
|
||
)
|
||
)
|
||
|
||
# Fallback quoted image extraction for reply-id-only payloads, or when
|
||
# embedded reply chain only contains placeholders (e.g. [Forward Message], [Image]).
|
||
if not has_embedded_image:
|
||
try:
|
||
fallback_images = normalize_and_dedupe_strings(
|
||
await extract_quoted_message_images(
|
||
event,
|
||
comp,
|
||
settings=quoted_message_settings,
|
||
)
|
||
)
|
||
remaining_limit = max(
|
||
config.max_quoted_fallback_images
|
||
- fallback_quoted_image_count,
|
||
0,
|
||
)
|
||
if remaining_limit <= 0 and fallback_images:
|
||
logger.warning(
|
||
"Skip quoted fallback images due to limit=%d for umo=%s",
|
||
config.max_quoted_fallback_images,
|
||
event.unified_msg_origin,
|
||
)
|
||
continue
|
||
if len(fallback_images) > remaining_limit:
|
||
logger.warning(
|
||
"Truncate quoted fallback images for umo=%s, reply_id=%s from %d to %d",
|
||
event.unified_msg_origin,
|
||
getattr(comp, "id", None),
|
||
len(fallback_images),
|
||
remaining_limit,
|
||
)
|
||
fallback_images = fallback_images[:remaining_limit]
|
||
for image_ref in fallback_images:
|
||
if image_ref in req.image_urls:
|
||
continue
|
||
req.image_urls.append(image_ref)
|
||
fallback_quoted_image_count += 1
|
||
_append_quoted_image_attachment(req, image_ref)
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.warning(
|
||
"Failed to resolve fallback quoted images for umo=%s, reply_id=%s: %s",
|
||
event.unified_msg_origin,
|
||
getattr(comp, "id", None),
|
||
exc,
|
||
exc_info=True,
|
||
)
|
||
|
||
conversation = await _get_session_conv(event, plugin_context)
|
||
req.conversation = conversation
|
||
req.contexts = json.loads(conversation.history)
|
||
event.set_extra("provider_request", req)
|
||
|
||
if isinstance(req.contexts, str):
|
||
req.contexts = json.loads(req.contexts)
|
||
req.image_urls = normalize_and_dedupe_strings(req.image_urls)
|
||
|
||
if config.file_extract_enabled:
|
||
try:
|
||
await _apply_file_extract(event, req, config)
|
||
except Exception as exc: # noqa: BLE001
|
||
logger.error("Error occurred while applying file extract: %s", exc)
|
||
|
||
if not req.prompt and not req.image_urls:
|
||
if not event.get_group_id() and req.extra_user_content_parts:
|
||
req.prompt = "<attachment>"
|
||
else:
|
||
return None
|
||
|
||
await _decorate_llm_request(event, req, plugin_context, config)
|
||
|
||
await _apply_kb(event, req, plugin_context, config)
|
||
|
||
if not req.session_id:
|
||
req.session_id = event.unified_msg_origin
|
||
|
||
_modalities_fix(provider, req)
|
||
_plugin_tool_fix(event, req)
|
||
_sanitize_context_by_modalities(config, provider, req)
|
||
|
||
if config.llm_safety_mode:
|
||
_apply_llm_safety_mode(config, req)
|
||
|
||
if config.computer_use_runtime == "sandbox":
|
||
_apply_sandbox_tools(config, req, req.session_id)
|
||
elif config.computer_use_runtime == "local":
|
||
_apply_local_env_tools(req)
|
||
|
||
agent_runner = AgentRunner()
|
||
astr_agent_ctx = AstrAgentContext(
|
||
context=plugin_context,
|
||
event=event,
|
||
)
|
||
|
||
if config.add_cron_tools:
|
||
_proactive_cron_job_tools(req)
|
||
|
||
if event.platform_meta.support_proactive_message:
|
||
if req.func_tool is None:
|
||
req.func_tool = ToolSet()
|
||
req.func_tool.add_tool(SEND_MESSAGE_TO_USER_TOOL)
|
||
|
||
if provider.provider_config.get("max_context_tokens", 0) <= 0:
|
||
model = provider.get_model()
|
||
if model_info := LLM_METADATAS.get(model):
|
||
provider.provider_config["max_context_tokens"] = model_info["limit"][
|
||
"context"
|
||
]
|
||
|
||
if event.get_platform_name() == "webchat":
|
||
asyncio.create_task(_handle_webchat(event, req, provider))
|
||
|
||
if req.func_tool and req.func_tool.tools:
|
||
tool_prompt = (
|
||
TOOL_CALL_PROMPT
|
||
if config.tool_schema_mode == "full"
|
||
else TOOL_CALL_PROMPT_SKILLS_LIKE_MODE
|
||
)
|
||
req.system_prompt += f"\n{tool_prompt}\n"
|
||
|
||
action_type = event.get_extra("action_type")
|
||
if action_type == "live":
|
||
req.system_prompt += f"\n{LIVE_MODE_SYSTEM_PROMPT}\n"
|
||
|
||
reset_coro = agent_runner.reset(
|
||
provider=provider,
|
||
request=req,
|
||
run_context=AgentContextWrapper(
|
||
context=astr_agent_ctx,
|
||
tool_call_timeout=config.tool_call_timeout,
|
||
),
|
||
tool_executor=FunctionToolExecutor(),
|
||
agent_hooks=MAIN_AGENT_HOOKS,
|
||
streaming=config.streaming_response,
|
||
llm_compress_instruction=config.llm_compress_instruction,
|
||
llm_compress_keep_recent=config.llm_compress_keep_recent,
|
||
llm_compress_provider=_get_compress_provider(config, plugin_context),
|
||
truncate_turns=config.dequeue_context_length,
|
||
enforce_max_turns=config.max_context_length,
|
||
tool_schema_mode=config.tool_schema_mode,
|
||
fallback_providers=_get_fallback_chat_providers(
|
||
provider, plugin_context, config.provider_settings
|
||
),
|
||
)
|
||
|
||
if apply_reset:
|
||
await reset_coro
|
||
|
||
return MainAgentBuildResult(
|
||
agent_runner=agent_runner,
|
||
provider_request=req,
|
||
provider=provider,
|
||
reset_coro=reset_coro if not apply_reset else None,
|
||
)
|