Compare commits

...

23 Commits

Author SHA1 Message Date
copilot-swe-agent[bot] c6b6eef8c4 Complete Docker compatibility fix with enhanced documentation
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2025-08-16 16:03:54 +00:00
copilot-swe-agent[bot] 50cf263076 Implement CLI Docker compatibility fix and login-info command
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2025-08-16 16:01:36 +00:00
copilot-swe-agent[bot] 2554548088 Initial commit: fix formatting and explore codebase
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2025-08-16 15:53:37 +00:00
copilot-swe-agent[bot] aa4a2d10e2 Initial plan 2025-08-16 15:48:10 +00:00
Soulter 02a9769b35 fix: 补充工具调用轮数上限配置 2025-08-14 23:51:22 +08:00
Soulter 7640f11bfc docs: update readme 2025-08-14 17:28:51 +08:00
Soulter 9fa44dbcfa docs: update readme 2025-08-14 14:16:22 +08:00
Copilot 2cae941bae Fix incomplete Gemini streaming responses in chat history (#2429)
* Initial plan

* Fix incomplete Gemini streaming responses in chat history

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>
Co-authored-by: Raven95676 <Raven95676@gmail.com>
2025-08-14 11:56:50 +08:00
Copilot bc0784f41d fix: enable_thinking parameter for qwen3 models in non-streaming calls (#2424)
* Initial plan

* Fix ModelScope enable_thinking parameter for non-streaming calls

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* Tighten enable_thinking condition to only Qwen/Qwen3 models

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* qwen3 model handle

* Update astrbot/core/provider/sources/openai_source.py

Co-authored-by: Copilot <175728472+Copilot@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>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-14 11:18:29 +08:00
Copilot c57d75e01a feat: add comprehensive GitHub Copilot instructions for AstrBot development (#2426)
* Initial plan

* Initial progress - completed repository exploration and dependency installation

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* Complete copilot-instructions.md with comprehensive development guide

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* Update copilot-instructions.md

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
2025-08-13 23:31:28 +08:00
RC-CHN 73edeae013 perf: 优化hint渲染方式,为部分类型供应商添加默认的温度选项 (#2321)
* feat:为webchat页面添加一个手动上传文件按钮(目前只处理图片)

* fix:上传后清空value,允许触发change事件以多次上传同一张图片

* perf:webchat页面消息发送后清空图片预览缩略图,维持与文本信息行为一致

* perf:将文件输入的值重置为空字符串以提升浏览器兼容性

* feat:webchat文件上传按钮支持多选文件上传

* fix:释放blob URL以防止内存泄漏

* perf:并行化sendMessage中的图片获取逻辑

* perf:优化hint渲染方式,为部分类型供应商添加默认的温度选项
2025-08-12 21:53:06 +08:00
MUKAPP 7d46314dc8 fix: 修复注册文件时由于 file:/// 前缀,导致文件被误判为不存在的问题 (#2325)
fixes #2222
2025-08-12 21:47:31 +08:00
你们的饺子 d5a53a89eb fix: 修复插件的 terminate 无法被正常调用的问题 (#2352) 2025-08-12 21:41:19 +08:00
dependabot[bot] a85bc510dd chore(deps): bump actions/checkout in the github-actions group (#2400)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


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

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  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>
2025-08-12 15:15:28 +08:00
Soulter 2beea7d218 📦 release: v3.5.24 2025-08-07 20:36:59 +08:00
Soulter a93cd3dd5f feat: compshare provider 2025-08-07 20:25:45 +08:00
Soulter db4d02c2e2 docs: add 1panel deployment method 2025-08-02 19:01:49 +08:00
Soulter fd7811402b fix: 添加对 metadata 中 description 字段的支持,确保元数据完整性
fixes: #2245
2025-08-02 16:01:10 +08:00
你们的饺子 eb0325e627 fix: 修复了 OpenAI 类型的 LLM 空内容响应导致的无法解析 completion 的错误。 (#2279) 2025-08-02 15:46:11 +08:00
IGCrystal 8b4b04ec09 fix(i18n): add missing noTemplates key (#2292) 2025-08-02 14:16:59 +08:00
Larch-C 9f32c9280f chore: update and rename PLUGIN_PUBLISH.md to PLUGIN_PUBLISH.yml (#2289) 2025-08-02 14:16:19 +08:00
yrk111222 4fcd09cfa8 feat: add ModelScope API support (#2230)
* add ModelScope API support

* update
2025-08-02 14:14:08 +08:00
Misaka Mikoto 7a8d65d37d feat: add plugins local cache and remote file MD5 validation (#2211)
* 修改openai的嵌入模型默认维度为1024

* 为插件市场添加本地缓存
- 优先使用api获取,获取失败时则使用本地缓存
- 每次获取后会更新本地缓存
- 如果获取结果为空,判定为获取失败,使用本地缓存
- 前端页面添加刷新按钮,用于手动刷新本地缓存

* feat: 增强插件市场缓存机制,支持MD5校验以确保数据有效性

---------

Co-authored-by: Soulter <905617992@qq.com>
2025-08-02 14:03:53 +08:00
54 changed files with 997 additions and 240 deletions
-31
View File
@@ -1,31 +0,0 @@
---
name: '🥳 发布插件'
title: "[Plugin] 插件名"
about: 提交插件到插件市场
labels: [ "plugin-publish" ]
assignees: ''
---
欢迎发布插件到插件市场!
## 插件基本信息
请将插件信息填写到下方的 Json 代码块中。`tags`(插件标签)和 `social_link`(社交链接)选填。
```json
{
"name": "插件名",
"desc": "插件介绍",
"author": "作者名",
"repo": "插件仓库链接",
"tags": [],
"social_link": ""
}
```
## 检查
- [ ] 我的插件经过完整的测试
- [ ] 我的插件不包含恶意代码
- [ ] 我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
+56
View File
@@ -0,0 +1,56 @@
name: 🥳 发布插件
description: 提交插件到插件市场
title: "[Plugin] 插件名"
labels: ["plugin-publish"]
assignees: []
body:
- type: markdown
attributes:
value: |
欢迎发布插件到插件市场!
- type: markdown
attributes:
value: |
## 插件基本信息
请将插件信息填写到下方的 JSON 代码块中。其中 `tags`(插件标签)和 `social_link`(社交链接)选填。
不熟悉 JSON ?现在可以从 [这里](https://plugins.astrbot.app/#/submit) 获取你的 JSON 啦!获取到了记得复制粘贴过来哦!
- type: textarea
id: plugin-info
attributes:
label: 插件信息
description: 请在下方代码块中填写您的插件信息,确保反引号包裹了JSON
value: |
```json
{
"name": "插件名",
"desc": "插件介绍",
"author": "作者名",
"repo": "插件仓库链接",
"tags": [],
"social_link": ""
}
```
validations:
required: true
- type: markdown
attributes:
value: |
## 检查
- type: checkboxes
id: checks
attributes:
label: 插件检查清单
description: 请确认以下所有项目
options:
- label: 我的插件经过完整的测试
required: true
- label: 我的插件不包含恶意代码
required: true
- label: 我已阅读并同意遵守该项目的 [行为准则](https://docs.github.com/zh/site-policy/github-terms/github-community-code-of-conduct)。
required: true
+63
View File
@@ -0,0 +1,63 @@
# AstrBot Development Instructions
AstrBot is a multi-platform LLM chatbot and development framework written in Python with a Vue.js dashboard. It supports multiple messaging platforms (QQ, Telegram, Discord, etc.) and various LLM providers (OpenAI, Anthropic, Google Gemini, etc.).
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
## Working Effectively
### Bootstrap and Install Dependencies
- **Python 3.10+ required** - Check `.python-version` file
- Install UV package manager: `pip install uv`
- Install project dependencies: `uv sync` -- takes 6-7 minutes. NEVER CANCEL. Set timeout to 10+ minutes.
- Create required directories: `mkdir -p data/plugins data/config data/temp`
### Running the Application
- Run main application: `uv run main.py` -- starts in ~3 seconds
- Application creates WebUI on http://localhost:6185 (default credentials: `astrbot`/`astrbot`)
- Application loads plugins automatically from `packages/` and `data/plugins/` directories
### Dashboard Build (Vue.js/Node.js)
- **Prerequisites**: Node.js 20+ and npm 10+ required
- Navigate to dashboard: `cd dashboard`
- Install dashboard dependencies: `npm install` -- takes 2-3 minutes. NEVER CANCEL. Set timeout to 5+ minutes.
- Build dashboard: `npm run build` -- takes 25-30 seconds. NEVER CANCEL.
- Dashboard creates optimized production build in `dashboard/dist/`
### Testing
- Do not generate test files for now.
### Code Quality and Linting
- Install ruff linter: `uv add --dev ruff`
- Check code style: `uv run ruff check .` -- takes <1 second
- Check formatting: `uv run ruff format --check .` -- takes <1 second
- Fix formatting: `uv run ruff format .`
- **ALWAYS** run `uv run ruff check .` and `uv run ruff format .` before committing changes
### Plugin Development
- Plugins load from `packages/` (built-in) and `data/plugins/` (user-installed)
- Plugin system supports function tools and message handlers
- Key plugins: python_interpreter, web_searcher, astrbot, reminder, session_controller
### Common Issues and Workarounds
- **Dashboard download fails**: Known issue with "division by zero" error - application still works
- **Import errors in tests**: Ensure `uv run` is used to run tests in proper environment
=- **Build timeouts**: Always set appropriate timeouts (10+ minutes for uv sync, 5+ minutes for npm install)
## CI/CD Integration
- GitHub Actions workflows in `.github/workflows/`
- Docker builds supported via `Dockerfile`
- Pre-commit hooks enforce ruff formatting and linting
## Docker Support
- Primary deployment method: `docker run soulter/astrbot:latest`
- Compose file available: `compose.yml`
- Exposes ports: 6185 (WebUI), 6195 (WeChat), 6199 (QQ), etc.
- Volume mount required: `./data:/AstrBot/data`
## Multi-language Support
- Documentation in Chinese (README.md), English (README_en.md), Japanese (README_ja.md)
- UI supports internationalization
- Default language is Chinese
Remember: This is a production chatbot framework with real users. Always test thoroughly and ensure changes don't break existing functionality.
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Dashboard Build - name: Dashboard Build
run: | run: |
@@ -70,7 +70,7 @@ jobs:
needs: build-and-publish-to-github-release needs: build-and-publish-to-github-release
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
+1 -1
View File
@@ -56,7 +56,7 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 fetch-depth: 0
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: npm install, build - name: npm install, build
run: | run: |
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Pull The Codes - name: Pull The Codes
uses: actions/checkout@v4 uses: actions/checkout@v5
with: with:
fetch-depth: 0 # Must be 0 so we can fetch tags fetch-depth: 0 # Must be 0 so we can fetch tags
+31 -54
View File
@@ -27,57 +27,50 @@ _✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用的插件系统和完善的大语言模型(LLM)接入功能的聊天机器人及开发框架。 AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用的插件系统和完善的大语言模型(LLM)接入功能的聊天机器人及开发框架。
<!-- [![codecov](https://img.shields.io/codecov/c/github/soulter/astrbot?style=for-the-badge)](https://codecov.io/gh/Soulter/AstrBot)
-->
> [!WARNING]
>
> 请务必修改默认密码以及保证 AstrBot 版本 >= 3.5.13。
## ✨ 近期更新
<details><summary>1. AstrBot 现已自带知识库能力</summary>
📚 详见[文档](https://astrbot.app/use/knowledge-base.html)
![image](https://github.com/user-attachments/assets/28b639b0-bb5c-4958-8e94-92ae8cfd1ab4)
</details>
2. AstrBot 现已支持接入 [MCP](https://modelcontextprotocol.io/) 服务器!
## ✨ 主要功能 ## ✨ 主要功能
> [!NOTE] 1. **大模型对话**。支持接入多种大模型服务。支持多模态、工具调用、MCP、原生知识库、人设等功能。
> 🪧 我们正基于前沿科研成果,设计并实现适用于角色扮演和情感陪伴的长短期记忆模型及情绪控制模型,旨在提升对话的真实性与情感表达能力。敬请期待 `v3.6.0` 版本! 2. **多消息平台支持**。支持接入 QQ、企业微信、微信公众号、飞书、Telegram、钉钉、Discord、KOOK 等平台。支持速率限制、白名单、百度内容审核。
3. **Agent**。完善适配的 Agentic 能力。支持多轮工具调用、内置沙盒代码执行器、网页搜索等功能。
1. **大语言模型对话**。支持各种大语言模型,包括 OpenAI API、Google Gemini、Llama、Deepseek、ChatGLM 等,支持接入本地部署的大模型,通过 Ollama、LLMTuner。具有多轮对话、人格情境、多模态能力,支持图片理解、语音转文字(Whisper) 4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,社区插件生态丰富
2. **多消息平台接入**。支持接入 QQ(OneBot、QQ 官方机器人平台)、QQ 频道、企业微信、微信公众号、飞书、Telegram、钉钉、Discord、KOOK、VoceChat。支持速率限制、白名单、关键词过滤、百度内容审核 5. **WebUI**。可视化配置和管理机器人,功能齐全
3. **Agent**。原生支持部分 Agent 能力,如代码执行器、自然语言待办、网页搜索。对接 [Dify 平台](https://dify.ai/),便捷接入 Dify 智能助手、知识库和 Dify 工作流。
4. **插件扩展**。深度优化的插件机制,支持[开发插件](https://astrbot.app/dev/plugin.html)扩展功能,极简开发。已支持安装多个插件。
5. **可视化管理面板**。支持可视化修改配置、插件管理、日志查看等功能,降低配置难度。集成 WebChat,可在面板上与大模型对话。
6. **高稳定性、高模块化**。基于事件总线和流水线的架构设计,高度模块化,低耦合。
> [!TIP]
> WebUI 在线体验 Demo: [https://demo.astrbot.app/](https://demo.astrbot.app/)
>
> 用户名: `astrbot`, 密码: `astrbot`。
## ✨ 使用方式 ## ✨ 使用方式
#### Docker 部署 #### Docker 部署
推荐使用 Docker / Docker Compose 方式部署 AstrBot。
请参阅官方文档 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) 。 请参阅官方文档 [使用 Docker 部署 AstrBot](https://astrbot.app/deploy/astrbot/docker.html#%E4%BD%BF%E7%94%A8-docker-%E9%83%A8%E7%BD%B2-astrbot) 。
#### 宝塔面板部署
AstrBot 与宝塔面板合作,已上架至宝塔面板。
请参阅官方文档 [宝塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.html) 。
#### 1Panel 部署
AstrBot 已由 1Panel 官方上架至 1Panel 面板。
请参阅官方文档 [1Panel 部署](https://astrbot.app/deploy/astrbot/1panel.html) 。
#### 在 雨云 上部署
AstrBot 已由雨云官方上架至云应用平台,可一键部署。
[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
#### 在 Replit 上部署
社区贡献的部署方式。
[![Run on Repl.it](https://repl.it/badge/github/Soulter/AstrBot)](https://repl.it/github/Soulter/AstrBot)
#### Windows 一键安装器部署 #### Windows 一键安装器部署
请参阅官方文档 [使用 Windows 一键安装器部署 AstrBot](https://astrbot.app/deploy/astrbot/windows.html) 。 请参阅官方文档 [使用 Windows 一键安装器部署 AstrBot](https://astrbot.app/deploy/astrbot/windows.html) 。
#### 宝塔面板部署
请参阅官方文档 [宝塔面板部署](https://astrbot.app/deploy/astrbot/btpanel.html) 。
#### CasaOS 部署 #### CasaOS 部署
社区贡献的部署方式。 社区贡献的部署方式。
@@ -101,24 +94,8 @@ git clone https://github.com/AstrBotDevs/AstrBot && cd AstrBot
uv run main.py uv run main.py
``` ```
或者,直接通过 uvx 安装 AstrBot
```bash
mkdir astrbot && cd astrbot
uvx astrbot init
# uvx astrbot run
```
或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。 或者请参阅官方文档 [通过源码部署 AstrBot](https://astrbot.app/deploy/astrbot/cli.html) 。
#### 在 Replit 上部署
[![Run on Repl.it](https://repl.it/badge/github/Soulter/AstrBot)](https://repl.it/github/Soulter/AstrBot)
#### 在 雨云 上部署
[![Deploy on RainYun](https://rainyun-apps.cn-nb1.rains3.com/materials/deploy-on-rainyun-en.svg)](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
## ⚡ 消息平台支持情况 ## ⚡ 消息平台支持情况
| 平台 | 支持性 | | 平台 | 支持性 |
View File
+49
View File
@@ -139,6 +139,14 @@ def conf():
- dashboard.password: Dashboard 密码 - dashboard.password: Dashboard 密码
- callback_api_base: 回调接口基址 - callback_api_base: 回调接口基址
可用子命令:
- set: 设置配置项值
- get: 获取配置项值
- login-info: 显示 Web 管理面板登录信息
""" """
pass pass
@@ -204,3 +212,44 @@ def get_config(key: str = None):
click.echo(f" {key}: {value}") click.echo(f" {key}: {value}")
except (KeyError, TypeError): except (KeyError, TypeError):
pass pass
@conf.command(name="login-info")
def get_login_info():
"""显示 Web 管理面板的登录信息
在 Docker 环境中使用示例:
docker exec -e ASTRBOT_ROOT=/AstrBot astrbot-container astrbot conf login-info
"""
config = _load_config()
try:
username = _get_nested_item(config, "dashboard.username")
# 注意:我们不显示实际的MD5哈希密码,而是提示用户如何重置
click.echo("🔐 Web 管理面板登录信息:")
click.echo(f" 用户名: {username}")
click.echo(" 密码: [已加密存储]")
click.echo()
click.echo("💡 如需重置密码,请使用以下命令:")
click.echo(" astrbot conf set dashboard.password <新密码>")
click.echo()
click.echo("🌐 访问地址:")
# 尝试获取端口信息
try:
port = _get_nested_item(config, "dashboard.port")
click.echo(f" http://localhost:{port}")
click.echo(f" http://your-server-ip:{port}")
except (KeyError, TypeError):
click.echo(" http://localhost:6185 (默认端口)")
click.echo(" http://your-server-ip:6185 (默认端口)")
click.echo()
click.echo("📋 Docker 环境使用说明:")
click.echo(" 如果在 Docker 中运行,请使用以下命令格式:")
click.echo(" docker exec -e ASTRBOT_ROOT=/AstrBot <容器名> astrbot conf login-info")
except KeyError:
click.echo("❌ 无法找到登录配置,请先运行 'astrbot init' 初始化")
except Exception as e:
raise click.UsageError(f"获取登录信息失败: {str(e)}")
+7 -1
View File
@@ -16,7 +16,13 @@ def check_astrbot_root(path: str | Path) -> bool:
def get_astrbot_root() -> Path: def get_astrbot_root() -> Path:
"""获取Astrbot根目录路径""" """获取Astrbot根目录路径"""
return Path.cwd() import os
# 使用与core应用相同的路径解析逻辑,优先使用ASTRBOT_ROOT环境变量
if path := os.environ.get("ASTRBOT_ROOT"):
return Path(path)
else:
return Path.cwd()
async def check_dashboard(astrbot_root: Path) -> None: async def check_dashboard(astrbot_root: Path) -> None:
+25 -18
View File
@@ -117,19 +117,24 @@ def build_plug_list(plugins_dir: Path) -> list:
# 从 metadata.yaml 加载元数据 # 从 metadata.yaml 加载元数据
metadata = load_yaml_metadata(plugin_dir) metadata = load_yaml_metadata(plugin_dir)
if "desc" not in metadata and "description" in metadata:
metadata["desc"] = metadata["description"]
# 如果成功加载元数据,添加到结果列表 # 如果成功加载元数据,添加到结果列表
if metadata and all( if metadata and all(
k in metadata for k in ["name", "desc", "version", "author", "repo"] k in metadata for k in ["name", "desc", "version", "author", "repo"]
): ):
result.append({ result.append(
"name": str(metadata.get("name", "")), {
"desc": str(metadata.get("desc", "")), "name": str(metadata.get("name", "")),
"version": str(metadata.get("version", "")), "desc": str(metadata.get("desc", "")),
"author": str(metadata.get("author", "")), "version": str(metadata.get("version", "")),
"repo": str(metadata.get("repo", "")), "author": str(metadata.get("author", "")),
"status": PluginStatus.INSTALLED, "repo": str(metadata.get("repo", "")),
"local_path": str(plugin_dir), "status": PluginStatus.INSTALLED,
}) "local_path": str(plugin_dir),
}
)
# 获取在线插件列表 # 获取在线插件列表
online_plugins = [] online_plugins = []
@@ -139,15 +144,17 @@ def build_plug_list(plugins_dir: Path) -> list:
resp.raise_for_status() resp.raise_for_status()
data = resp.json() data = resp.json()
for plugin_id, plugin_info in data.items(): for plugin_id, plugin_info in data.items():
online_plugins.append({ online_plugins.append(
"name": str(plugin_id), {
"desc": str(plugin_info.get("desc", "")), "name": str(plugin_id),
"version": str(plugin_info.get("version", "")), "desc": str(plugin_info.get("desc", "")),
"author": str(plugin_info.get("author", "")), "version": str(plugin_info.get("version", "")),
"repo": str(plugin_info.get("repo", "")), "author": str(plugin_info.get("author", "")),
"status": PluginStatus.NOT_INSTALLED, "repo": str(plugin_info.get("repo", "")),
"local_path": None, "status": PluginStatus.NOT_INSTALLED,
}) "local_path": None,
}
)
except Exception as e: except Exception as e:
click.echo(f"获取在线插件列表失败: {e}", err=True) click.echo(f"获取在线插件列表失败: {e}", err=True)
+49 -25
View File
@@ -6,7 +6,7 @@ import os
from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.core.utils.astrbot_path import get_astrbot_data_path
VERSION = "3.5.23" VERSION = "3.5.24"
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db") DB_PATH = os.path.join(get_astrbot_data_path(), "data_v3.db")
# 默认配置 # 默认配置
@@ -65,6 +65,7 @@ DEFAULT_CONFIG = {
"show_tool_use_status": False, "show_tool_use_status": False,
"streaming_segmented": False, "streaming_segmented": False,
"separate_provider": True, "separate_provider": True,
"max_agent_step": 30,
}, },
"provider_stt_settings": { "provider_stt_settings": {
"enable": False, "enable": False,
@@ -597,9 +598,8 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.openai.com/v1", "api_base": "https://api.openai.com/v1",
"timeout": 120, "timeout": 120,
"model_config": { "model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
"model": "gpt-4o-mini", "hint": "也兼容所有与OpenAI API兼容的服务。",
},
}, },
"Azure OpenAI": { "Azure OpenAI": {
"id": "azure", "id": "azure",
@@ -611,9 +611,7 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "", "api_base": "",
"timeout": 120, "timeout": 120,
"model_config": { "model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
"model": "gpt-4o-mini",
},
}, },
"xAI": { "xAI": {
"id": "xai", "id": "xai",
@@ -624,11 +622,10 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.x.ai/v1", "api_base": "https://api.x.ai/v1",
"timeout": 120, "timeout": 120,
"model_config": { "model_config": {"model": "grok-2-latest", "temperature": 0.4},
"model": "grok-2-latest",
},
}, },
"Anthropic": { "Anthropic": {
"hint": "注意Claude系列模型的温度调节范围为0到1.0,超出可能导致报错",
"id": "claude", "id": "claude",
"provider": "anthropic", "provider": "anthropic",
"type": "anthropic_chat_completion", "type": "anthropic_chat_completion",
@@ -640,9 +637,11 @@ CONFIG_METADATA_2 = {
"model_config": { "model_config": {
"model": "claude-3-5-sonnet-latest", "model": "claude-3-5-sonnet-latest",
"max_tokens": 4096, "max_tokens": 4096,
"temperature": 0.2,
}, },
}, },
"Ollama": { "Ollama": {
"hint": "启用前请确保已正确安装并运行 Ollama 服务端,Ollama默认不带鉴权,无需修改key",
"id": "ollama_default", "id": "ollama_default",
"provider": "ollama", "provider": "ollama",
"type": "openai_chat_completion", "type": "openai_chat_completion",
@@ -650,9 +649,7 @@ CONFIG_METADATA_2 = {
"enable": True, "enable": True,
"key": ["ollama"], # ollama 的 key 默认是 ollama "key": ["ollama"], # ollama 的 key 默认是 ollama
"api_base": "http://localhost:11434/v1", "api_base": "http://localhost:11434/v1",
"model_config": { "model_config": {"model": "llama3.1-8b", "temperature": 0.4},
"model": "llama3.1-8b",
},
}, },
"LM Studio": { "LM Studio": {
"id": "lm_studio", "id": "lm_studio",
@@ -677,6 +674,7 @@ CONFIG_METADATA_2 = {
"timeout": 120, "timeout": 120,
"model_config": { "model_config": {
"model": "gemini-1.5-flash", "model": "gemini-1.5-flash",
"temperature": 0.4,
}, },
}, },
"Gemini": { "Gemini": {
@@ -690,6 +688,7 @@ CONFIG_METADATA_2 = {
"timeout": 120, "timeout": 120,
"model_config": { "model_config": {
"model": "gemini-2.0-flash-exp", "model": "gemini-2.0-flash-exp",
"temperature": 0.4,
}, },
"gm_resp_image_modal": False, "gm_resp_image_modal": False,
"gm_native_search": False, "gm_native_search": False,
@@ -714,9 +713,7 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.deepseek.com/v1", "api_base": "https://api.deepseek.com/v1",
"timeout": 120, "timeout": 120,
"model_config": { "model_config": {"model": "deepseek-chat", "temperature": 0.4},
"model": "deepseek-chat",
},
}, },
"302.AI": { "302.AI": {
"id": "302ai", "id": "302ai",
@@ -727,9 +724,7 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"api_base": "https://api.302.ai/v1", "api_base": "https://api.302.ai/v1",
"timeout": 120, "timeout": 120,
"model_config": { "model_config": {"model": "gpt-4.1-mini", "temperature": 0.4},
"model": "gpt-4.1-mini",
},
}, },
"硅基流动": { "硅基流动": {
"id": "siliconflow", "id": "siliconflow",
@@ -742,6 +737,7 @@ CONFIG_METADATA_2 = {
"api_base": "https://api.siliconflow.cn/v1", "api_base": "https://api.siliconflow.cn/v1",
"model_config": { "model_config": {
"model": "deepseek-ai/DeepSeek-V3", "model": "deepseek-ai/DeepSeek-V3",
"temperature": 0.4,
}, },
}, },
"PPIO派欧云": { "PPIO派欧云": {
@@ -755,6 +751,20 @@ CONFIG_METADATA_2 = {
"timeout": 120, "timeout": 120,
"model_config": { "model_config": {
"model": "deepseek/deepseek-r1", "model": "deepseek/deepseek-r1",
"temperature": 0.4,
},
},
"优云智算": {
"id": "compshare",
"provider": "compshare",
"type": "openai_chat_completion",
"provider_type": "chat_completion",
"enable": True,
"key": [],
"api_base": "https://api.modelverse.cn/v1",
"timeout": 120,
"model_config": {
"model": "moonshotai/Kimi-K2-Instruct",
}, },
}, },
"Kimi": { "Kimi": {
@@ -766,9 +776,7 @@ CONFIG_METADATA_2 = {
"key": [], "key": [],
"timeout": 120, "timeout": 120,
"api_base": "https://api.moonshot.cn/v1", "api_base": "https://api.moonshot.cn/v1",
"model_config": { "model_config": {"model": "moonshot-v1-8k", "temperature": 0.4},
"model": "moonshot-v1-8k",
},
}, },
"智谱 AI": { "智谱 AI": {
"id": "zhipu_default", "id": "zhipu_default",
@@ -796,6 +804,7 @@ CONFIG_METADATA_2 = {
"dify_query_input_key": "astrbot_text_query", "dify_query_input_key": "astrbot_text_query",
"variables": {}, "variables": {},
"timeout": 60, "timeout": 60,
"hint": "请确保你在 AstrBot 里设置的 APP 类型和 Dify 里面创建的应用的类型一致!",
}, },
"阿里云百炼应用": { "阿里云百炼应用": {
"id": "dashscope", "id": "dashscope",
@@ -814,6 +823,17 @@ CONFIG_METADATA_2 = {
"variables": {}, "variables": {},
"timeout": 60, "timeout": 60,
}, },
"ModelScope": {
"id": "modelscope",
"provider": "modelscope",
"type": "openai_chat_completion",
"provider_type": "chat_completion",
"enable": True,
"key": [],
"timeout": 120,
"api_base": "https://api-inference.modelscope.cn/v1",
"model_config": {"model": "Qwen/Qwen3-32B", "temperature": 0.4},
},
"FastGPT": { "FastGPT": {
"id": "fastgpt", "id": "fastgpt",
"provider": "fastgpt", "provider": "fastgpt",
@@ -835,7 +855,7 @@ CONFIG_METADATA_2 = {
"model": "whisper-1", "model": "whisper-1",
}, },
"Whisper(本地加载)": { "Whisper(本地加载)": {
"whisper_hint": "(不用修改我)", "hint": "启用前请 pip 安装 openai-whisper 库(N卡用户大约下载 2GB,主要是 torch 和 cudaCPU 用户大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
"provider": "openai", "provider": "openai",
"type": "openai_whisper_selfhost", "type": "openai_whisper_selfhost",
"provider_type": "speech_to_text", "provider_type": "speech_to_text",
@@ -844,7 +864,7 @@ CONFIG_METADATA_2 = {
"model": "tiny", "model": "tiny",
}, },
"SenseVoice(本地加载)": { "SenseVoice(本地加载)": {
"sensevoice_hint": "(不用修改我)", "hint": "启用前请 pip 安装 funasr、funasr_onnx、torchaudio、torch、modelscope、jieba 库(默认使用CPU,大约下载 1 GB),并且安装 ffmpeg。否则将无法正常转文字。",
"type": "sensevoice_stt_selfhost", "type": "sensevoice_stt_selfhost",
"provider": "sensevoice", "provider": "sensevoice",
"provider_type": "speech_to_text", "provider_type": "speech_to_text",
@@ -866,7 +886,7 @@ CONFIG_METADATA_2 = {
"timeout": "20", "timeout": "20",
}, },
"Edge TTS": { "Edge TTS": {
"edgetts_hint": "提示:使用这个服务前需要安装有 ffmpeg,并且可以直接在终端调用 ffmpeg 指令。", "hint": "提示:使用这个服务前需要安装有 ffmpeg,并且可以直接在终端调用 ffmpeg 指令。",
"id": "edge_tts", "id": "edge_tts",
"provider": "microsoft", "provider": "microsoft",
"type": "edge_tts", "type": "edge_tts",
@@ -1686,6 +1706,10 @@ CONFIG_METADATA_2 = {
"type": "bool", "type": "bool",
"hint": "启用后,若平台不支持流式回复,会分段输出。目前仅支持 aiocqhttp 两个平台,不支持或无需使用流式分段输出的平台会静默忽略此选项", "hint": "启用后,若平台不支持流式回复,会分段输出。目前仅支持 aiocqhttp 两个平台,不支持或无需使用流式分段输出的平台会静默忽略此选项",
}, },
"max_agent_step": {
"description": "工具调用轮数上限",
"type": "int",
},
}, },
}, },
"persona": { "persona": {
@@ -1,3 +1,3 @@
from .vec_db import FaissVecDB from .vec_db import FaissVecDB
__all__ = ["FaissVecDB"] __all__ = ["FaissVecDB"]
+29 -5
View File
@@ -2,6 +2,8 @@ import asyncio
import os import os
import uuid import uuid
import time import time
from urllib.parse import urlparse, unquote
import platform
class FileTokenService: class FileTokenService:
@@ -15,7 +17,9 @@ class FileTokenService:
async def _cleanup_expired_tokens(self): async def _cleanup_expired_tokens(self):
"""清理过期的令牌""" """清理过期的令牌"""
now = time.time() now = time.time()
expired_tokens = [token for token, (_, expire) in self.staged_files.items() if expire < now] expired_tokens = [
token for token, (_, expire) in self.staged_files.items() if expire < now
]
for token in expired_tokens: for token in expired_tokens:
self.staged_files.pop(token, None) self.staged_files.pop(token, None)
@@ -32,15 +36,35 @@ class FileTokenService:
Raises: Raises:
FileNotFoundError: 当路径不存在时抛出 FileNotFoundError: 当路径不存在时抛出
""" """
# 处理 file:///
try:
parsed_uri = urlparse(file_path)
if parsed_uri.scheme == "file":
local_path = unquote(parsed_uri.path)
if platform.system() == "Windows" and local_path.startswith("/"):
local_path = local_path[1:]
else:
# 如果没有 file:/// 前缀,则认为是普通路径
local_path = file_path
except Exception:
# 解析失败时,按原路径处理
local_path = file_path
async with self.lock: async with self.lock:
await self._cleanup_expired_tokens() await self._cleanup_expired_tokens()
if not os.path.exists(file_path): if not os.path.exists(local_path):
raise FileNotFoundError(f"文件不存在: {file_path}") raise FileNotFoundError(
f"文件不存在: {local_path} (原始输入: {file_path})"
)
file_token = str(uuid.uuid4()) file_token = str(uuid.uuid4())
expire_time = time.time() + (timeout if timeout is not None else self.default_timeout) expire_time = time.time() + (
self.staged_files[file_token] = (file_path, expire_time) timeout if timeout is not None else self.default_timeout
)
# 存储转换后的真实路径
self.staged_files[file_token] = (local_path, expire_time)
return file_token return file_token
async def handle_file(self, file_token: str) -> str: async def handle_file(self, file_token: str) -> str:
+1
View File
@@ -128,6 +128,7 @@ class Plain(BaseMessageComponent):
async def to_dict(self): async def to_dict(self):
return {"type": "text", "data": {"text": self.text}} return {"type": "text", "data": {"text": self.text}}
class Face(BaseMessageComponent): class Face(BaseMessageComponent):
type: ComponentType = "Face" type: ComponentType = "Face"
id: int id: int
@@ -8,10 +8,11 @@ from enum import Enum, auto
class AgentState(Enum): class AgentState(Enum):
"""Agent 状态枚举""" """Agent 状态枚举"""
IDLE = auto() # 初始状态
RUNNING = auto() # 运行中 IDLE = auto() # 初始状态
DONE = auto() # 完成 RUNNING = auto() # 运行中
ERROR = auto() # 错误状态 DONE = auto() # 完成
ERROR = auto() # 错误状态
class AgentResponseData(T.TypedDict): class AgentResponseData(T.TypedDict):
@@ -57,6 +57,7 @@ class DingtalkMessageEvent(AstrMessageEvent):
logger.error(f"钉钉图片处理失败: {e}") logger.error(f"钉钉图片处理失败: {e}")
logger.warning(f"跳过图片发送: {image_path}") logger.warning(f"跳过图片发送: {image_path}")
continue continue
async def send(self, message: MessageChain): async def send(self, message: MessageChain):
await self.send_with_client(self.client, message) await self.send_with_client(self.client, message)
await super().send(message) await super().send(message)
@@ -41,7 +41,8 @@ class DiscordBotClient(discord.Bot):
await self.on_ready_once_callback() await self.on_ready_once_callback()
except Exception as e: except Exception as e:
logger.error( logger.error(
f"[Discord] on_ready_once_callback 执行失败: {e}", exc_info=True) f"[Discord] on_ready_once_callback 执行失败: {e}", exc_info=True
)
def _create_message_data(self, message: discord.Message) -> dict: def _create_message_data(self, message: discord.Message) -> dict:
"""从 discord.Message 创建数据字典""" """从 discord.Message 创建数据字典"""
@@ -90,7 +91,6 @@ class DiscordBotClient(discord.Bot):
message_data = self._create_message_data(message) message_data = self._create_message_data(message)
await self.on_message_received(message_data) await self.on_message_received(message_data)
def _extract_interaction_content(self, interaction: discord.Interaction) -> str: def _extract_interaction_content(self, interaction: discord.Interaction) -> str:
"""从交互中提取内容""" """从交互中提取内容"""
interaction_type = interaction.type interaction_type = interaction.type
@@ -79,9 +79,12 @@ class DiscordButton(BaseMessageComponent):
self.url = url self.url = url
self.disabled = disabled self.disabled = disabled
class DiscordReference(BaseMessageComponent): class DiscordReference(BaseMessageComponent):
"""Discord引用组件""" """Discord引用组件"""
type: str = "discord_reference" type: str = "discord_reference"
def __init__(self, message_id: str, channel_id: str): def __init__(self, message_id: str, channel_id: str):
self.message_id = message_id self.message_id = message_id
self.channel_id = channel_id self.channel_id = channel_id
@@ -98,7 +101,6 @@ class DiscordView(BaseMessageComponent):
self.components = components or [] self.components = components or []
self.timeout = timeout self.timeout = timeout
def to_discord_view(self) -> discord.ui.View: def to_discord_view(self) -> discord.ui.View:
"""转换为Discord View对象""" """转换为Discord View对象"""
view = discord.ui.View(timeout=self.timeout) view = discord.ui.View(timeout=self.timeout)
@@ -53,7 +53,13 @@ class DiscordPlatformEvent(AstrMessageEvent):
# 解析消息链为 Discord 所需的对象 # 解析消息链为 Discord 所需的对象
try: try:
content, files, view, embeds, reference_message_id = await self._parse_to_discord(message) (
content,
files,
view,
embeds,
reference_message_id,
) = await self._parse_to_discord(message)
except Exception as e: except Exception as e:
logger.error(f"[Discord] 解析消息链时失败: {e}", exc_info=True) logger.error(f"[Discord] 解析消息链时失败: {e}", exc_info=True)
return return
@@ -206,8 +212,7 @@ class DiscordPlatformEvent(AstrMessageEvent):
if await asyncio.to_thread(path.exists): if await asyncio.to_thread(path.exists):
file_bytes = await asyncio.to_thread(path.read_bytes) file_bytes = await asyncio.to_thread(path.read_bytes)
files.append( files.append(
discord.File(BytesIO(file_bytes), discord.File(BytesIO(file_bytes), filename=i.name)
filename=i.name)
) )
else: else:
logger.warning( logger.warning(
@@ -308,7 +308,9 @@ class SlackAdapter(Platform):
base64_content = base64.b64encode(content).decode("utf-8") base64_content = base64.b64encode(content).decode("utf-8")
return base64_content return base64_content
else: else:
logger.error(f"Failed to download slack file: {resp.status} {await resp.text()}") logger.error(
f"Failed to download slack file: {resp.status} {await resp.text()}"
)
raise Exception(f"下载文件失败: {resp.status}") raise Exception(f"下载文件失败: {resp.status}")
async def run(self) -> Awaitable[Any]: async def run(self) -> Awaitable[Any]:
@@ -75,7 +75,13 @@ class SlackMessageEvent(AstrMessageEvent):
"text": {"type": "mrkdwn", "text": "文件上传失败"}, "text": {"type": "mrkdwn", "text": "文件上传失败"},
} }
file_url = response["files"][0]["permalink"] file_url = response["files"][0]["permalink"]
return {"type": "section", "text": {"type": "mrkdwn", "text": f"文件: <{file_url}|{segment.name or '文件'}>"}} return {
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"文件: <{file_url}|{segment.name or '文件'}>",
},
}
else: else:
return {"type": "section", "text": {"type": "mrkdwn", "text": str(segment)}} return {"type": "section", "text": {"type": "mrkdwn", "text": str(segment)}}
@@ -66,7 +66,9 @@ class TelegramPlatformEvent(AstrMessageEvent):
return chunks return chunks
@classmethod @classmethod
async def send_with_client(cls, client: ExtBot, message: MessageChain, user_name: str): async def send_with_client(
cls, client: ExtBot, message: MessageChain, user_name: str
):
image_path = None image_path = None
has_reply = False has_reply = False
@@ -1,5 +1,6 @@
import asyncio import asyncio
class WebChatQueueMgr: class WebChatQueueMgr:
def __init__(self) -> None: def __init__(self) -> None:
self.queues = {} self.queues = {}
@@ -30,4 +31,5 @@ class WebChatQueueMgr:
"""Check if a queue exists for the given conversation ID""" """Check if a queue exists for the given conversation ID"""
return conversation_id in self.queues return conversation_id in self.queues
webchat_queue_mgr = WebChatQueueMgr() webchat_queue_mgr = WebChatQueueMgr()
@@ -213,10 +213,10 @@ class WeChatPadProAdapter(Platform):
def _extract_auth_key(self, data): def _extract_auth_key(self, data):
"""Helper method to extract auth_key from response data.""" """Helper method to extract auth_key from response data."""
if isinstance(data, dict): if isinstance(data, dict):
auth_keys = data.get("authKeys") # 新接口 auth_keys = data.get("authKeys") # 新接口
if isinstance(auth_keys, list) and auth_keys: if isinstance(auth_keys, list) and auth_keys:
return auth_keys[0] return auth_keys[0]
elif isinstance(data, list) and data: # 旧接口 elif isinstance(data, list) and data: # 旧接口
return data[0] return data[0]
return None return None
@@ -234,7 +234,9 @@ class WeChatPadProAdapter(Platform):
try: try:
async with session.post(url, params=params, json=payload) as response: async with session.post(url, params=params, json=payload) as response:
if response.status != 200: if response.status != 200:
logger.error(f"生成授权码失败: {response.status}, {await response.text()}") logger.error(
f"生成授权码失败: {response.status}, {await response.text()}"
)
return return
response_data = await response.json() response_data = await response.json()
@@ -245,7 +247,9 @@ class WeChatPadProAdapter(Platform):
if self.auth_key: if self.auth_key:
logger.info("成功获取授权码") logger.info("成功获取授权码")
else: else:
logger.error(f"生成授权码成功但未找到授权码: {response_data}") logger.error(
f"生成授权码成功但未找到授权码: {response_data}"
)
else: else:
logger.error(f"生成授权码失败: {response_data}") logger.error(f"生成授权码失败: {response_data}")
except aiohttp.ClientConnectorError as e: except aiohttp.ClientConnectorError as e:
@@ -48,7 +48,12 @@ class WeChatKF(BaseWeChatAPI):
注意:可能会出现返回条数少于limit的情况,需结合返回的has_more字段判断是否继续请求。 注意:可能会出现返回条数少于limit的情况,需结合返回的has_more字段判断是否继续请求。
:return: 接口调用结果 :return: 接口调用结果
""" """
data = {"token": token, "cursor": cursor, "limit": limit, "open_kfid": open_kfid} data = {
"token": token,
"cursor": cursor,
"limit": limit,
"open_kfid": open_kfid,
}
return self._post("kf/sync_msg", data=data) return self._post("kf/sync_msg", data=data)
def get_service_state(self, open_kfid, external_userid): def get_service_state(self, open_kfid, external_userid):
@@ -72,7 +77,9 @@ class WeChatKF(BaseWeChatAPI):
} }
return self._post("kf/service_state/get", data=data) return self._post("kf/service_state/get", data=data)
def trans_service_state(self, open_kfid, external_userid, service_state, servicer_userid=""): def trans_service_state(
self, open_kfid, external_userid, service_state, servicer_userid=""
):
""" """
变更会话状态 变更会话状态
@@ -180,7 +187,9 @@ class WeChatKF(BaseWeChatAPI):
""" """
return self._get("kf/customer/get_upgrade_service_config") return self._get("kf/customer/get_upgrade_service_config")
def upgrade_service(self, open_kfid, external_userid, service_type, member=None, groupchat=None): def upgrade_service(
self, open_kfid, external_userid, service_type, member=None, groupchat=None
):
""" """
为客户升级为专员或客户群服务 为客户升级为专员或客户群服务
@@ -246,7 +255,9 @@ class WeChatKF(BaseWeChatAPI):
data = {"open_kfid": open_kfid, "start_time": start_time, "end_time": end_time} data = {"open_kfid": open_kfid, "start_time": start_time, "end_time": end_time}
return self._post("kf/get_corp_statistic", data=data) return self._post("kf/get_corp_statistic", data=data)
def get_servicer_statistic(self, start_time, end_time, open_kfid=None, servicer_userid=None): def get_servicer_statistic(
self, start_time, end_time, open_kfid=None, servicer_userid=None
):
""" """
获取「客户数据统计」接待人员明细数据 获取「客户数据统计」接待人员明细数据
@@ -26,6 +26,7 @@ from optionaldict import optionaldict
from wechatpy.client.api.base import BaseWeChatAPI from wechatpy.client.api.base import BaseWeChatAPI
class WeChatKFMessage(BaseWeChatAPI): class WeChatKFMessage(BaseWeChatAPI):
""" """
发送微信客服消息 发送微信客服消息
@@ -125,35 +126,55 @@ class WeChatKFMessage(BaseWeChatAPI):
msg={"msgtype": "news", "link": {"link": articles_data}}, msg={"msgtype": "news", "link": {"link": articles_data}},
) )
def send_msgmenu(self, user_id, open_kfid, head_content, menu_list, tail_content, msgid=""): def send_msgmenu(
self, user_id, open_kfid, head_content, menu_list, tail_content, msgid=""
):
return self.send( return self.send(
user_id, user_id,
open_kfid, open_kfid,
msgid, msgid,
msg={ msg={
"msgtype": "msgmenu", "msgtype": "msgmenu",
"msgmenu": {"head_content": head_content, "list": menu_list, "tail_content": tail_content}, "msgmenu": {
"head_content": head_content,
"list": menu_list,
"tail_content": tail_content,
},
}, },
) )
def send_location(self, user_id, open_kfid, name, address, latitude, longitude, msgid=""): def send_location(
self, user_id, open_kfid, name, address, latitude, longitude, msgid=""
):
return self.send( return self.send(
user_id, user_id,
open_kfid, open_kfid,
msgid, msgid,
msg={ msg={
"msgtype": "location", "msgtype": "location",
"msgmenu": {"name": name, "address": address, "latitude": latitude, "longitude": longitude}, "msgmenu": {
"name": name,
"address": address,
"latitude": latitude,
"longitude": longitude,
},
}, },
) )
def send_miniprogram(self, user_id, open_kfid, appid, title, thumb_media_id, pagepath, msgid=""): def send_miniprogram(
self, user_id, open_kfid, appid, title, thumb_media_id, pagepath, msgid=""
):
return self.send( return self.send(
user_id, user_id,
open_kfid, open_kfid,
msgid, msgid,
msg={ msg={
"msgtype": "miniprogram", "msgtype": "miniprogram",
"msgmenu": {"appid": appid, "title": title, "thumb_media_id": thumb_media_id, "pagepath": pagepath}, "msgmenu": {
"appid": appid,
"title": title,
"thumb_media_id": thumb_media_id,
"pagepath": pagepath,
},
}, },
) )
@@ -160,7 +160,9 @@ class WeixinOfficialAccountPlatformAdapter(Platform):
self.wexin_event_workers[msg.id] = future self.wexin_event_workers[msg.id] = future
await self.convert_message(msg, future) await self.convert_message(msg, future)
# I love shield so much! # I love shield so much!
result = await asyncio.wait_for(asyncio.shield(future), 60) # wait for 60s result = await asyncio.wait_for(
asyncio.shield(future), 60
) # wait for 60s
logger.debug(f"Got future result: {result}") logger.debug(f"Got future result: {result}")
self.wexin_event_workers.pop(msg.id, None) self.wexin_event_workers.pop(msg.id, None)
return result # xml. see weixin_offacc_event.py return result # xml. see weixin_offacc_event.py
@@ -150,7 +150,6 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
return return
logger.info(f"微信公众平台上传语音返回: {response}") logger.info(f"微信公众平台上传语音返回: {response}")
if active_send_mode: if active_send_mode:
self.client.message.send_voice( self.client.message.send_voice(
message_obj.sender.user_id, message_obj.sender.user_id,
+26 -8
View File
@@ -470,6 +470,10 @@ class ProviderGoogleGenAI(Provider):
raise raise
continue continue
# Accumulate the complete response text for the final response
accumulated_text = ""
final_response = None
async for chunk in result: async for chunk in result:
llm_response = LLMResponse("assistant", is_chunk=True) llm_response = LLMResponse("assistant", is_chunk=True)
@@ -481,23 +485,37 @@ class ProviderGoogleGenAI(Provider):
chunk, llm_response chunk, llm_response
) )
yield llm_response yield llm_response
break return
if chunk.text: if chunk.text:
accumulated_text += chunk.text
llm_response.result_chain = MessageChain(chain=[Comp.Plain(chunk.text)]) llm_response.result_chain = MessageChain(chain=[Comp.Plain(chunk.text)])
yield llm_response yield llm_response
if chunk.candidates[0].finish_reason: if chunk.candidates[0].finish_reason:
llm_response = LLMResponse("assistant", is_chunk=False) # Process the final chunk for potential tool calls or other content
if not chunk.candidates[0].content.parts: if chunk.candidates[0].content.parts:
llm_response.result_chain = MessageChain(chain=[Comp.Plain(" ")]) final_response = LLMResponse("assistant", is_chunk=False)
else: final_response.result_chain = self._process_content_parts(
llm_response.result_chain = self._process_content_parts( chunk, final_response
chunk, llm_response
) )
yield llm_response
break break
# Yield final complete response with accumulated text
if not final_response:
final_response = LLMResponse("assistant", is_chunk=False)
# Set the complete accumulated text in the final response
if accumulated_text:
final_response.result_chain = MessageChain(
chain=[Comp.Plain(accumulated_text)]
)
elif not final_response.result_chain:
# If no text was accumulated and no final response was set, provide empty space
final_response.result_chain = MessageChain(chain=[Comp.Plain(" ")])
yield final_response
async def text_chat( async def text_chat(
self, self,
prompt: str, prompt: str,
@@ -99,6 +99,11 @@ class ProviderOpenAIOfficial(Provider):
for key in to_del: for key in to_del:
del payloads[key] del payloads[key]
# 针对 qwen3 模型的特殊处理:非流式调用必须设置 enable_thinking=false
model = payloads.get("model", "")
if "qwen3" in model.lower():
extra_body["enable_thinking"] = False
completion = await self.client.chat.completions.create( completion = await self.client.chat.completions.create(
**payloads, stream=False, extra_body=extra_body **payloads, stream=False, extra_body=extra_body
) )
@@ -176,7 +181,7 @@ class ProviderOpenAIOfficial(Provider):
raise Exception("API 返回的 completion 为空。") raise Exception("API 返回的 completion 为空。")
choice = completion.choices[0] choice = completion.choices[0]
if choice.message.content: if choice.message.content is not None:
# text completion # text completion
completion_text = str(choice.message.content).strip() completion_text = str(choice.message.content).strip()
llm_response.result_chain = MessageChain().message(completion_text) llm_response.result_chain = MessageChain().message(completion_text)
@@ -210,7 +215,7 @@ class ProviderOpenAIOfficial(Provider):
"API 返回的 completion 由于内容安全过滤被拒绝(非 AstrBot)。" "API 返回的 completion 由于内容安全过滤被拒绝(非 AstrBot)。"
) )
if not llm_response.completion_text and not llm_response.tools_call_args: if llm_response.completion_text is None and not llm_response.tools_call_args:
logger.error(f"API 返回的 completion 无法解析:{completion}") logger.error(f"API 返回的 completion 无法解析:{completion}")
raise Exception(f"API 返回的 completion 无法解析:{completion}") raise Exception(f"API 返回的 completion 无法解析:{completion}")
+6 -1
View File
@@ -2,7 +2,12 @@ from asyncio import Queue
from typing import List, Union from typing import List, Union
from astrbot.core import sp from astrbot.core import sp
from astrbot.core.provider.provider import Provider, TTSProvider, STTProvider, EmbeddingProvider from astrbot.core.provider.provider import (
Provider,
TTSProvider,
STTProvider,
EmbeddingProvider,
)
from astrbot.core.provider.entities import ProviderType from astrbot.core.provider.entities import ProviderType
from astrbot.core.db import BaseDatabase from astrbot.core.db import BaseDatabase
from astrbot.core.config.astrbot_config import AstrBotConfig from astrbot.core.config.astrbot_config import AstrBotConfig
+1 -2
View File
@@ -113,8 +113,7 @@ class CommandGroupFilter(HandlerFilter):
+ self.print_cmd_tree(self.sub_command_filters, event=event, cfg=cfg) + self.print_cmd_tree(self.sub_command_filters, event=event, cfg=cfg)
) )
raise ValueError( raise ValueError(
f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n" f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n" + tree
+ tree
) )
# complete_command_names = [name + " " for name in complete_command_names] # complete_command_names = [name + " " for name in complete_command_names]
+4 -1
View File
@@ -7,6 +7,7 @@ from .star import star_map
T = TypeVar("T", bound="StarHandlerMetadata") T = TypeVar("T", bound="StarHandlerMetadata")
class StarHandlerRegistry(Generic[T]): class StarHandlerRegistry(Generic[T]):
def __init__(self): def __init__(self):
self.star_handlers_map: Dict[str, StarHandlerMetadata] = {} self.star_handlers_map: Dict[str, StarHandlerMetadata] = {}
@@ -49,7 +50,8 @@ class StarHandlerRegistry(Generic[T]):
self, module_name: str self, module_name: str
) -> List[StarHandlerMetadata]: ) -> List[StarHandlerMetadata]:
return [ return [
handler for handler in self._handlers handler
for handler in self._handlers
if handler.handler_module_path == module_name if handler.handler_module_path == module_name
] ]
@@ -67,6 +69,7 @@ class StarHandlerRegistry(Generic[T]):
def __len__(self): def __len__(self):
return len(self._handlers) return len(self._handlers)
star_handlers_registry = StarHandlerRegistry() star_handlers_registry = StarHandlerRegistry()
+2 -2
View File
@@ -809,11 +809,11 @@ class PluginManager:
if star_metadata.star_cls is None: if star_metadata.star_cls is None:
return return
if hasattr(star_metadata.star_cls, "__del__"): if "__del__" in star_metadata.star_cls_type.__dict__:
asyncio.get_event_loop().run_in_executor( asyncio.get_event_loop().run_in_executor(
None, star_metadata.star_cls.__del__ None, star_metadata.star_cls.__del__
) )
elif hasattr(star_metadata.star_cls, "terminate"): elif "terminate" in star_metadata.star_cls_type.__dict__:
await star_metadata.star_cls.terminate() await star_metadata.star_cls.terminate()
async def turn_on_plugin(self, plugin_name: str): async def turn_on_plugin(self, plugin_name: str):
+3 -1
View File
@@ -182,7 +182,9 @@ class StarTools:
plugin_name = metadata.name plugin_name = metadata.name
data_dir = Path(os.path.join(get_astrbot_data_path(), "plugin_data", plugin_name)) data_dir = Path(
os.path.join(get_astrbot_data_path(), "plugin_data", plugin_name)
)
try: try:
data_dir.mkdir(parents=True, exist_ok=True) data_dir.mkdir(parents=True, exist_ok=True)
+1 -3
View File
@@ -56,9 +56,7 @@ class AstrBotUpdator(RepoZipUpdator):
try: try:
if "astrbot" in os.path.basename(sys.argv[0]): # 兼容cli if "astrbot" in os.path.basename(sys.argv[0]): # 兼容cli
if os.name == "nt": if os.name == "nt":
args = [ args = [f'"{arg}"' if " " in arg else arg for arg in sys.argv[1:]]
f'"{arg}"' if " " in arg else arg for arg in sys.argv[1:]
]
else: else:
args = sys.argv[1:] args = sys.argv[1:]
os.execl(sys.executable, py, "-m", "astrbot.cli.__main__", *args) os.execl(sys.executable, py, "-m", "astrbot.cli.__main__", *args)
+1
View File
@@ -5,6 +5,7 @@ from .astrbot_path import get_astrbot_data_path
_VT = TypeVar("_VT") _VT = TypeVar("_VT")
class SharedPreferences: class SharedPreferences:
def __init__(self, path=None): def __init__(self, path=None):
if path is None: if path is None:
+91 -30
View File
@@ -210,11 +210,16 @@ class ConfigRoute(Route):
response = await asyncio.wait_for( response = await asyncio.wait_for(
provider.text_chat(prompt="REPLY `PONG` ONLY"), timeout=45.0 provider.text_chat(prompt="REPLY `PONG` ONLY"), timeout=45.0
) )
logger.debug(f"Received response from {status_info['name']}: {response}") logger.debug(
f"Received response from {status_info['name']}: {response}"
)
if response is not None: if response is not None:
status_info["status"] = "available" status_info["status"] = "available"
response_text_snippet = "" response_text_snippet = ""
if hasattr(response, "completion_text") and response.completion_text: if (
hasattr(response, "completion_text")
and response.completion_text
):
response_text_snippet = ( response_text_snippet = (
response.completion_text[:70] + "..." response.completion_text[:70] + "..."
if len(response.completion_text) > 70 if len(response.completion_text) > 70
@@ -233,29 +238,48 @@ class ConfigRoute(Route):
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'" f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{response_text_snippet}'"
) )
else: else:
status_info["error"] = "Test call returned None, but expected an LLMResponse object." status_info["error"] = (
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None.") "Test call returned None, but expected an LLMResponse object."
)
logger.warning(
f"Provider {status_info['name']} (ID: {status_info['id']}) test call returned None."
)
except asyncio.TimeoutError: except asyncio.TimeoutError:
status_info["error"] = "Connection timed out after 45 seconds during test call." status_info["error"] = (
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) timed out.") "Connection timed out after 45 seconds during test call."
)
logger.warning(
f"Provider {status_info['name']} (ID: {status_info['id']}) timed out."
)
except Exception as e: except Exception as e:
error_message = str(e) error_message = str(e)
status_info["error"] = error_message status_info["error"] = error_message
logger.warning(f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}") logger.warning(
logger.debug(f"Traceback for {status_info['name']}:\n{traceback.format_exc()}") f"Provider {status_info['name']} (ID: {status_info['id']}) is unavailable. Error: {error_message}"
)
logger.debug(
f"Traceback for {status_info['name']}:\n{traceback.format_exc()}"
)
elif provider_capability_type == ProviderType.EMBEDDING: elif provider_capability_type == ProviderType.EMBEDDING:
try: try:
# For embedding, we can call the get_embedding method with a short prompt. # For embedding, we can call the get_embedding method with a short prompt.
embedding_result = await provider.get_embedding("health_check") embedding_result = await provider.get_embedding("health_check")
if isinstance(embedding_result, list) and (not embedding_result or isinstance(embedding_result[0], float)): if isinstance(embedding_result, list) and (
not embedding_result or isinstance(embedding_result[0], float)
):
status_info["status"] = "available" status_info["status"] = "available"
else: else:
status_info["status"] = "unavailable" status_info["status"] = "unavailable"
status_info["error"] = f"Embedding test failed: unexpected result type {type(embedding_result)}" status_info["error"] = (
f"Embedding test failed: unexpected result type {type(embedding_result)}"
)
except Exception as e: except Exception as e:
logger.error(f"Error testing embedding provider {provider_name}: {e}", exc_info=True) logger.error(
f"Error testing embedding provider {provider_name}: {e}",
exc_info=True,
)
status_info["status"] = "unavailable" status_info["status"] = "unavailable"
status_info["error"] = f"Embedding test failed: {str(e)}" status_info["error"] = f"Embedding test failed: {str(e)}"
@@ -267,41 +291,71 @@ class ConfigRoute(Route):
status_info["status"] = "available" status_info["status"] = "available"
else: else:
status_info["status"] = "unavailable" status_info["status"] = "unavailable"
status_info["error"] = f"TTS test failed: unexpected result type {type(audio_result)}" status_info["error"] = (
f"TTS test failed: unexpected result type {type(audio_result)}"
)
except Exception as e: except Exception as e:
logger.error(f"Error testing TTS provider {provider_name}: {e}", exc_info=True) logger.error(
f"Error testing TTS provider {provider_name}: {e}", exc_info=True
)
status_info["status"] = "unavailable" status_info["status"] = "unavailable"
status_info["error"] = f"TTS test failed: {str(e)}" status_info["error"] = f"TTS test failed: {str(e)}"
elif provider_capability_type == ProviderType.SPEECH_TO_TEXT: elif provider_capability_type == ProviderType.SPEECH_TO_TEXT:
try: try:
logger.debug(f"Sending health check audio to provider: {status_info['name']}") logger.debug(
sample_audio_path = os.path.join(get_astrbot_path(), "samples", "stt_health_check.wav") f"Sending health check audio to provider: {status_info['name']}"
)
sample_audio_path = os.path.join(
get_astrbot_path(), "samples", "stt_health_check.wav"
)
if not os.path.exists(sample_audio_path): if not os.path.exists(sample_audio_path):
status_info["status"] = "unavailable" status_info["status"] = "unavailable"
status_info["error"] = "STT test failed: sample audio file not found." status_info["error"] = (
logger.warning(f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}") "STT test failed: sample audio file not found."
)
logger.warning(
f"STT test for {status_info['name']} failed: sample audio file not found at {sample_audio_path}"
)
else: else:
text_result = await provider.get_text(sample_audio_path) text_result = await provider.get_text(sample_audio_path)
if isinstance(text_result, str) and text_result: if isinstance(text_result, str) and text_result:
status_info["status"] = "available" status_info["status"] = "available"
snippet = text_result[:70] + "..." if len(text_result) > 70 else text_result snippet = (
logger.info(f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'") text_result[:70] + "..."
if len(text_result) > 70
else text_result
)
logger.info(
f"Provider {status_info['name']} (ID: {status_info['id']}) is available. Response snippet: '{snippet}'"
)
else: else:
status_info["status"] = "unavailable" status_info["status"] = "unavailable"
status_info["error"] = f"STT test failed: unexpected result type {type(text_result)}" status_info["error"] = (
logger.warning(f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}") f"STT test failed: unexpected result type {type(text_result)}"
)
logger.warning(
f"STT test for {status_info['name']} failed: unexpected result type {type(text_result)}"
)
except Exception as e: except Exception as e:
logger.error(f"Error testing STT provider {provider_name}: {e}", exc_info=True) logger.error(
f"Error testing STT provider {provider_name}: {e}", exc_info=True
)
status_info["status"] = "unavailable" status_info["status"] = "unavailable"
status_info["error"] = f"STT test failed: {str(e)}" status_info["error"] = f"STT test failed: {str(e)}"
else: else:
logger.debug(f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}") logger.debug(
f"Provider {provider_name} is not a Chat Completion or Embedding provider. Marking as available without test. Meta: {meta}"
)
status_info["status"] = "available" status_info["status"] = "available"
status_info["error"] = "This provider type is not tested and is assumed to be available." status_info["error"] = (
"This provider type is not tested and is assumed to be available."
)
return status_info return status_info
def _error_response(self, message: str, status_code: int = 500, log_fn=logger.error): def _error_response(
self, message: str, status_code: int = 500, log_fn=logger.error
):
log_fn(message) log_fn(message)
# 记录更详细的traceback信息,但只在是严重错误时 # 记录更详细的traceback信息,但只在是严重错误时
if status_code == 500: if status_code == 500:
@@ -312,7 +366,9 @@ class ConfigRoute(Route):
"""API: check a single LLM Provider's status by id""" """API: check a single LLM Provider's status by id"""
provider_id = request.args.get("id") provider_id = request.args.get("id")
if not provider_id: if not provider_id:
return self._error_response("Missing provider_id parameter", 400, logger.warning) return self._error_response(
"Missing provider_id parameter", 400, logger.warning
)
logger.info(f"API call: /config/provider/check_one id={provider_id}") logger.info(f"API call: /config/provider/check_one id={provider_id}")
try: try:
@@ -320,16 +376,21 @@ class ConfigRoute(Route):
target = prov_mgr.inst_map.get(provider_id) target = prov_mgr.inst_map.get(provider_id)
if not target: if not target:
logger.warning(f"Provider with id '{provider_id}' not found in provider_manager.") logger.warning(
return Response().error(f"Provider with id '{provider_id}' not found").__dict__ f"Provider with id '{provider_id}' not found in provider_manager."
)
return (
Response()
.error(f"Provider with id '{provider_id}' not found")
.__dict__
)
result = await self._test_single_provider(target) result = await self._test_single_provider(target)
return Response().ok(result).__dict__ return Response().ok(result).__dict__
except Exception as e: except Exception as e:
return self._error_response( return self._error_response(
f"Critical error checking provider {provider_id}: {e}", f"Critical error checking provider {provider_id}: {e}", 500
500
) )
async def get_configs(self): async def get_configs(self):
+12 -4
View File
@@ -10,7 +10,9 @@ class LogRoute(Route):
super().__init__(context) super().__init__(context)
self.log_broker = log_broker self.log_broker = log_broker
self.app.add_url_rule("/api/live-log", view_func=self.log, methods=["GET"]) self.app.add_url_rule("/api/live-log", view_func=self.log, methods=["GET"])
self.app.add_url_rule("/api/log-history", view_func=self.log_history, methods=["GET"]) self.app.add_url_rule(
"/api/log-history", view_func=self.log_history, methods=["GET"]
)
async def log(self): async def log(self):
async def stream(): async def stream():
@@ -48,9 +50,15 @@ class LogRoute(Route):
"""获取日志历史""" """获取日志历史"""
try: try:
logs = list(self.log_broker.log_cache) logs = list(self.log_broker.log_cache)
return Response().ok(data={ return (
"logs": logs, Response()
}).__dict__ .ok(
data={
"logs": logs,
}
)
.__dict__
)
except BaseException as e: except BaseException as e:
logger.error(f"获取日志历史失败: {e}") logger.error(f"获取日志历史失败: {e}")
return Response().error(f"获取日志历史失败: {e}").__dict__ return Response().error(f"获取日志历史失败: {e}").__dict__
+134 -5
View File
@@ -1,6 +1,8 @@
import traceback import traceback
import aiohttp import aiohttp
import os import os
import json
from datetime import datetime
import ssl import ssl
import certifi import certifi
@@ -75,15 +77,33 @@ class PluginRoute(Route):
async def get_online_plugins(self): async def get_online_plugins(self):
custom = request.args.get("custom_registry") custom = request.args.get("custom_registry")
force_refresh = request.args.get("force_refresh", "false").lower() == "true"
cache_file = "data/plugins.json"
if custom: if custom:
urls = [custom] urls = [custom]
else: else:
urls = ["https://api.soulter.top/astrbot/plugins"] urls = [
"https://api.soulter.top/astrbot/plugins",
"https://github.com/AstrBotDevs/AstrBot_Plugins_Collection/raw/refs/heads/main/plugin_cache_original.json",
]
# 新增:创建 SSL 上下文,使用 certifi 提供的根证书 # 如果不是强制刷新,先检查缓存是否有效
cached_data = None
if not force_refresh:
# 先检查MD5是否匹配,如果匹配则使用缓存
if await self._is_cache_valid(cache_file):
cached_data = self._load_plugin_cache(cache_file)
if cached_data:
logger.debug("缓存MD5匹配,使用缓存的插件市场数据")
return Response().ok(cached_data).__dict__
# 尝试获取远程数据
remote_data = None
ssl_context = ssl.create_default_context(cafile=certifi.where()) ssl_context = ssl.create_default_context(cafile=certifi.where())
connector = aiohttp.TCPConnector(ssl=ssl_context) connector = aiohttp.TCPConnector(ssl=ssl_context)
for url in urls: for url in urls:
try: try:
async with aiohttp.ClientSession( async with aiohttp.ClientSession(
@@ -91,14 +111,123 @@ class PluginRoute(Route):
) as session: ) as session:
async with session.get(url) as response: async with session.get(url) as response:
if response.status == 200: if response.status == 200:
result = await response.json() remote_data = await response.json()
return Response().ok(result).__dict__
# 检查远程数据是否为空
if not remote_data or (
isinstance(remote_data, dict) and len(remote_data) == 0
):
logger.warning(f"远程插件市场数据为空: {url}")
continue # 继续尝试其他URL或使用缓存
logger.info("成功获取远程插件市场数据")
# 获取最新的MD5并保存到缓存
current_md5 = await self._get_remote_md5()
self._save_plugin_cache(
cache_file, remote_data, current_md5
)
return Response().ok(remote_data).__dict__
else: else:
logger.error(f"请求 {url} 失败,状态码:{response.status}") logger.error(f"请求 {url} 失败,状态码:{response.status}")
except Exception as e: except Exception as e:
logger.error(f"请求 {url} 失败,错误:{e}") logger.error(f"请求 {url} 失败,错误:{e}")
return Response().error("获取插件列表失败").__dict__ # 如果远程获取失败,尝试使用缓存数据
if not cached_data:
cached_data = self._load_plugin_cache(cache_file)
if cached_data:
logger.warning("远程插件市场数据获取失败,使用缓存数据")
return Response().ok(cached_data, "使用缓存数据,可能不是最新版本").__dict__
return Response().error("获取插件列表失败,且没有可用的缓存数据").__dict__
async def _is_cache_valid(self, cache_file: str) -> bool:
"""检查缓存是否有效(基于MD5"""
try:
if not os.path.exists(cache_file):
return False
# 加载缓存文件
with open(cache_file, "r", encoding="utf-8") as f:
cache_data = json.load(f)
cached_md5 = cache_data.get("md5")
if not cached_md5:
logger.debug("缓存文件中没有MD5信息")
return False
# 获取远程MD5
remote_md5 = await self._get_remote_md5()
if not remote_md5:
logger.warning("无法获取远程MD5,将使用缓存")
return True # 如果无法获取远程MD5,认为缓存有效
is_valid = cached_md5 == remote_md5
logger.debug(
f"插件数据MD5: 本地={cached_md5}, 远程={remote_md5}, 有效={is_valid}"
)
return is_valid
except Exception as e:
logger.warning(f"检查缓存有效性失败: {e}")
return False
async def _get_remote_md5(self) -> str:
"""获取远程插件数据的MD5"""
try:
ssl_context = ssl.create_default_context(cafile=certifi.where())
connector = aiohttp.TCPConnector(ssl=ssl_context)
async with aiohttp.ClientSession(
trust_env=True, connector=connector
) as session:
async with session.get(
"https://api.soulter.top/astrbot/plugins-md5"
) as response:
if response.status == 200:
data = await response.json()
return data.get("md5", "")
else:
logger.error(f"获取MD5失败,状态码:{response.status}")
return ""
except Exception as e:
logger.error(f"获取远程MD5失败: {e}")
return ""
def _load_plugin_cache(self, cache_file: str):
"""加载本地缓存的插件市场数据"""
try:
if os.path.exists(cache_file):
with open(cache_file, "r", encoding="utf-8") as f:
cache_data = json.load(f)
# 检查缓存是否有效
if "data" in cache_data and "timestamp" in cache_data:
logger.debug(
f"加载缓存文件: {cache_file}, 缓存时间: {cache_data['timestamp']}"
)
return cache_data["data"]
except Exception as e:
logger.warning(f"加载插件市场缓存失败: {e}")
return None
def _save_plugin_cache(self, cache_file: str, data, md5: str = None):
"""保存插件市场数据到本地缓存"""
try:
# 确保目录存在
os.makedirs(os.path.dirname(cache_file), exist_ok=True)
cache_data = {
"timestamp": datetime.now().isoformat(),
"data": data,
"md5": md5 or "",
}
with open(cache_file, "w", encoding="utf-8") as f:
json.dump(cache_data, f, ensure_ascii=False, indent=2)
logger.debug(f"插件市场数据已缓存到: {cache_file}, MD5: {md5}")
except Exception as e:
logger.warning(f"保存插件市场缓存失败: {e}")
async def get_plugins(self): async def get_plugins(self):
_plugin_resp = [] _plugin_resp = []
+10
View File
@@ -0,0 +1,10 @@
# What's Changed
> 新版本预告: v4.0.0 即将发布。
1. 新增: 添加对 ModelScope、Compshare(优云智算)的模版支持。
2. 优化: 增加插件数据缓存,优化插件市场数据获取时的稳定性。
其他更新:
1. 现已支持在 1Panel 平台通过应用商城快捷部署 AstrBot。详见:[在 1Panel 部署 AstrBot](https://docs.astrbot.app/deploy/astrbot/1panel.html)
@@ -1,10 +1,10 @@
<script setup> <script setup>
import { VueMonacoEditor } from '@guolao/vue-monaco-editor' import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
import { ref } from 'vue' import { ref, computed } from 'vue'
import ListConfigItem from './ListConfigItem.vue' import ListConfigItem from './ListConfigItem.vue'
import { useI18n } from '@/i18n/composables' import { useI18n } from '@/i18n/composables'
defineProps({ const props = defineProps({
metadata: { metadata: {
type: Object, type: Object,
required: true required: true
@@ -16,11 +16,21 @@ defineProps({
metadataKey: { metadataKey: {
type: String, type: String,
required: true required: true
},
isEditing: {
type: Boolean,
default: false
} }
}) })
const { t } = useI18n() const { t } = useI18n()
const filteredIterable = computed(() => {
if (!props.iterable) return {}
const { hint, ...rest } = props.iterable
return rest
})
const dialog = ref(false) const dialog = ref(false)
const currentEditingKey = ref('') const currentEditingKey = ref('')
const currentEditingLanguage = ref('json') const currentEditingLanguage = ref('json')
@@ -54,7 +64,19 @@ function saveEditedContent() {
<v-card-text class="px-0 py-1"> <v-card-text class="px-0 py-1">
<!-- Object Type Configuration --> <!-- Object Type Configuration -->
<div v-if="metadata[metadataKey]?.type === 'object' || metadata[metadataKey]?.config_template" class="object-config"> <div v-if="metadata[metadataKey]?.type === 'object' || metadata[metadataKey]?.config_template" class="object-config">
<div v-for="(val, key, index) in iterable" :key="key" class="config-item"> <!-- Provider-level hint -->
<v-alert
v-if="iterable.hint && !isEditing"
type="info"
variant="tonal"
class="mb-4"
border="start"
density="compact"
>
{{ iterable.hint }}
</v-alert>
<div v-for="(val, key, index) in filteredIterable" :key="key" class="config-item">
<!-- Nested Object --> <!-- Nested Object -->
<div v-if="metadata[metadataKey].items[key]?.type === 'object'" class="nested-object"> <div v-if="metadata[metadataKey].items[key]?.type === 'object'" class="nested-object">
<div v-if="metadata[metadataKey].items[key] && !metadata[metadataKey].items[key]?.invisible" class="nested-container"> <div v-if="metadata[metadataKey].items[key] && !metadata[metadataKey].items[key]?.invisible" class="nested-container">
@@ -32,7 +32,8 @@
"cancel": "Cancel", "cancel": "Cancel",
"actions": "Actions", "actions": "Actions",
"back": "Back", "back": "Back",
"selectFile": "Select File" "selectFile": "Select File",
"refresh": "Refresh"
}, },
"status": { "status": {
"enabled": "Enabled", "enabled": "Enabled",
@@ -24,6 +24,7 @@
"addPlatform": "Add Platform Adapter", "addPlatform": "Add Platform Adapter",
"connectTitle": "Connect {name}", "connectTitle": "Connect {name}",
"viewTutorial": "View Tutorial", "viewTutorial": "View Tutorial",
"noTemplates": "No platform templates available",
"idConflict": { "idConflict": {
"title": "ID Conflict Warning", "title": "ID Conflict Warning",
"message": "Detected duplicate ID \"{id}\". Please use a new ID.", "message": "Detected duplicate ID \"{id}\". Please use a new ID.",
@@ -32,7 +32,8 @@
"cancel": "取消", "cancel": "取消",
"actions": "操作", "actions": "操作",
"back": "返回", "back": "返回",
"selectFile": "选择文件" "selectFile": "选择文件",
"refresh": "刷新"
}, },
"status": { "status": {
"enabled": "启用", "enabled": "启用",
@@ -24,6 +24,7 @@
"addPlatform": "添加平台适配器", "addPlatform": "添加平台适配器",
"connectTitle": "接入 {name}", "connectTitle": "接入 {name}",
"viewTutorial": "查看接入教程", "viewTutorial": "查看接入教程",
"noTemplates": "暂无平台模板",
"idConflict": { "idConflict": {
"title": "ID 冲突警告", "title": "ID 冲突警告",
"message": "检测到 ID \"{id}\" 重复。请使用一个新的 ID。", "message": "检测到 ID \"{id}\" 重复。请使用一个新的 ID。",
+5 -1
View File
@@ -159,7 +159,11 @@ export const useCommonStore = defineStore({
if (!force && this.pluginMarketData.length > 0) { if (!force && this.pluginMarketData.length > 0) {
return Promise.resolve(this.pluginMarketData); return Promise.resolve(this.pluginMarketData);
} }
return axios.get('/api/plugin/market_list')
// 如果是强制刷新,添加 force_refresh 参数
const url = force ? '/api/plugin/market_list?force_refresh=true' : '/api/plugin/market_list';
return axios.get(url)
.then((res) => { .then((res) => {
let data = [] let data = []
for (let key in res.data.data) { for (let key in res.data.data) {
+34 -2
View File
@@ -71,6 +71,7 @@ const uploadTab = ref('file');
const showPluginFullName = ref(false); const showPluginFullName = ref(false);
const marketSearch = ref(""); const marketSearch = ref("");
const filterKeys = ['name', 'desc', 'author']; const filterKeys = ['name', 'desc', 'author'];
const refreshingMarket = ref(false);
const plugin_handler_info_headers = computed(() => [ const plugin_handler_info_headers = computed(() => [
{ title: tm('table.headers.eventType'), key: 'event_type_h' }, { title: tm('table.headers.eventType'), key: 'event_type_h' },
@@ -560,6 +561,25 @@ const newExtension = async () => {
} }
}; };
//
const refreshPluginMarket = async () => {
refreshingMarket.value = true;
try {
//
const data = await commonStore.getPluginCollections(true);
pluginMarketData.value = data;
trimExtensionName();
checkAlreadyInstalled();
checkUpdate();
toast(tm('messages.refreshSuccess'), "success");
} catch (err) {
toast(tm('messages.refreshFailed') + " " + err, "error");
} finally {
refreshingMarket.value = false;
}
};
// //
onMounted(async () => { onMounted(async () => {
await getExtensions(); await getExtensions();
@@ -851,8 +871,20 @@ onMounted(async () => {
<div class="mt-4"> <div class="mt-4">
<div class="d-flex align-center mb-2" style="justify-content: space-between;"> <div class="d-flex align-center mb-2" style="justify-content: space-between;">
<h2>{{ tm('market.allPlugins') }}</h2> <h2>{{ tm('market.allPlugins') }}</h2>
<v-switch v-model="showPluginFullName" :label="tm('market.showFullName')" hide-details density="compact" <div class="d-flex align-center">
style="margin-left: 12px" /> <v-btn
variant="tonal"
size="small"
@click="refreshPluginMarket"
:loading="refreshingMarket"
class="mr-2"
>
<v-icon>mdi-refresh</v-icon>
{{ tm('buttons.refresh') }}
</v-btn>
<v-switch v-model="showPluginFullName" :label="tm('market.showFullName')" hide-details density="compact"
style="margin-left: 12px" />
</div>
</div> </div>
<v-col cols="12" md="12" style="padding: 0px;"> <v-col cols="12" md="12" style="padding: 0px;">
+2
View File
@@ -233,6 +233,7 @@
:iterable="newSelectedProviderConfig" :iterable="newSelectedProviderConfig"
:metadata="metadata['provider_group']?.metadata" :metadata="metadata['provider_group']?.metadata"
metadataKey="provider" metadataKey="provider"
:is-editing="updatingMode"
/> />
</v-card-text> </v-card-text>
@@ -549,6 +550,7 @@ export default {
'ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg', 'ollama': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/ollama.svg',
'google': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg', 'google': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/gemini-color.svg',
'deepseek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg', 'deepseek': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/deepseek.svg',
'modelscope': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/modelscope.svg',
'zhipu': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg', 'zhipu': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/zhipu.svg',
'siliconflow': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg', 'siliconflow': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/siliconcloud.svg',
'moonshot': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg', 'moonshot': 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons/kimi.svg',
+9 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "AstrBot" name = "AstrBot"
version = "3.5.23" version = "3.5.24"
description = "易上手的多平台 LLM 聊天机器人及开发框架" description = "易上手的多平台 LLM 聊天机器人及开发框架"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -76,3 +76,11 @@ lint.select = [
"Q", # flake8-quotes "Q", # flake8-quotes
] ]
target-version = "py310" target-version = "py310"
[dependency-groups]
dev = [
"pytest>=8.4.1",
"pytest-asyncio>=1.1.0",
"pytest-cov>=6.2.1",
"ruff>=0.12.8",
]
Generated
+214 -2
View File
@@ -1,5 +1,5 @@
version = 1 version = 1
revision = 2 revision = 3
requires-python = ">=3.10" requires-python = ">=3.10"
[[package]] [[package]]
@@ -204,7 +204,7 @@ wheels = [
[[package]] [[package]]
name = "astrbot" name = "astrbot"
version = "3.5.23" version = "3.5.24"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiocqhttp" }, { name = "aiocqhttp" },
@@ -250,6 +250,14 @@ dependencies = [
{ name = "wechatpy" }, { name = "wechatpy" },
] ]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "ruff" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "aiocqhttp", specifier = ">=1.4.4" }, { name = "aiocqhttp", specifier = ">=1.4.4" },
@@ -295,6 +303,14 @@ requires-dist = [
{ name = "wechatpy", specifier = ">=1.8.18" }, { name = "wechatpy", specifier = ">=1.8.18" },
] ]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=8.4.1" },
{ name = "pytest-asyncio", specifier = ">=1.1.0" },
{ name = "pytest-cov", specifier = ">=6.2.1" },
{ name = "ruff", specifier = ">=0.12.8" },
]
[[package]] [[package]]
name = "async-timeout" name = "async-timeout"
version = "5.0.1" version = "5.0.1"
@@ -313,6 +329,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
] ]
[[package]]
name = "backports-asyncio-runner"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
]
[[package]] [[package]]
name = "beautifulsoup4" name = "beautifulsoup4"
version = "4.13.4" version = "4.13.4"
@@ -513,6 +538,96 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" }, { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" },
] ]
[[package]]
name = "coverage"
version = "7.10.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f4/2c/253cc41cd0f40b84c1c34c5363e0407d73d4a1cae005fed6db3b823175bd/coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619", size = 822936, upload-time = "2025-08-10T21:27:39.968Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/44/e14576c34b37764c821866909788ff7463228907ab82bae188dab2b421f1/coverage-7.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53808194afdf948c462215e9403cca27a81cf150d2f9b386aee4dab614ae2ffe", size = 215964, upload-time = "2025-08-10T21:25:22.828Z" },
{ url = "https://files.pythonhosted.org/packages/e6/15/f4f92d9b83100903efe06c9396ee8d8bdba133399d37c186fc5b16d03a87/coverage-7.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4d1b837d1abf72187a61645dbf799e0d7705aa9232924946e1f57eb09a3bf00", size = 216361, upload-time = "2025-08-10T21:25:25.603Z" },
{ url = "https://files.pythonhosted.org/packages/e9/3a/c92e8cd5e89acc41cfc026dfb7acedf89661ce2ea1ee0ee13aacb6b2c20c/coverage-7.10.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2a90dd4505d3cc68b847ab10c5ee81822a968b5191664e8a0801778fa60459fa", size = 243115, upload-time = "2025-08-10T21:25:27.09Z" },
{ url = "https://files.pythonhosted.org/packages/23/53/c1d8c2778823b1d95ca81701bb8f42c87dc341a2f170acdf716567523490/coverage-7.10.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d52989685ff5bf909c430e6d7f6550937bc6d6f3e6ecb303c97a86100efd4596", size = 244927, upload-time = "2025-08-10T21:25:28.77Z" },
{ url = "https://files.pythonhosted.org/packages/79/41/1e115fd809031f432b4ff8e2ca19999fb6196ab95c35ae7ad5e07c001130/coverage-7.10.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdb558a1d97345bde3a9f4d3e8d11c9e5611f748646e9bb61d7d612a796671b5", size = 246784, upload-time = "2025-08-10T21:25:30.195Z" },
{ url = "https://files.pythonhosted.org/packages/c7/b2/0eba9bdf8f1b327ae2713c74d4b7aa85451bb70622ab4e7b8c000936677c/coverage-7.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c9e6331a8f09cb1fc8bda032752af03c366870b48cce908875ba2620d20d0ad4", size = 244828, upload-time = "2025-08-10T21:25:31.785Z" },
{ url = "https://files.pythonhosted.org/packages/1f/cc/74c56b6bf71f2a53b9aa3df8bc27163994e0861c065b4fe3a8ac290bed35/coverage-7.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:992f48bf35b720e174e7fae916d943599f1a66501a2710d06c5f8104e0756ee1", size = 242844, upload-time = "2025-08-10T21:25:33.37Z" },
{ url = "https://files.pythonhosted.org/packages/b6/7b/ac183fbe19ac5596c223cb47af5737f4437e7566100b7e46cc29b66695a5/coverage-7.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c5595fc4ad6a39312c786ec3326d7322d0cf10e3ac6a6df70809910026d67cfb", size = 243721, upload-time = "2025-08-10T21:25:34.939Z" },
{ url = "https://files.pythonhosted.org/packages/57/96/cb90da3b5a885af48f531905234a1e7376acfc1334242183d23154a1c285/coverage-7.10.3-cp310-cp310-win32.whl", hash = "sha256:9e92fa1f2bd5a57df9d00cf9ce1eb4ef6fccca4ceabec1c984837de55329db34", size = 218481, upload-time = "2025-08-10T21:25:36.935Z" },
{ url = "https://files.pythonhosted.org/packages/15/67/1ba4c7d75745c4819c54a85766e0a88cc2bff79e1760c8a2debc34106dc2/coverage-7.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b96524d6e4a3ce6a75c56bb15dbd08023b0ae2289c254e15b9fbdddf0c577416", size = 219382, upload-time = "2025-08-10T21:25:38.267Z" },
{ url = "https://files.pythonhosted.org/packages/87/04/810e506d7a19889c244d35199cbf3239a2f952b55580aa42ca4287409424/coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397", size = 216075, upload-time = "2025-08-10T21:25:39.891Z" },
{ url = "https://files.pythonhosted.org/packages/2e/50/6b3fbab034717b4af3060bdaea6b13dfdc6b1fad44b5082e2a95cd378a9a/coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85", size = 216476, upload-time = "2025-08-10T21:25:41.137Z" },
{ url = "https://files.pythonhosted.org/packages/c7/96/4368c624c1ed92659812b63afc76c492be7867ac8e64b7190b88bb26d43c/coverage-7.10.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1af4461b25fe92889590d438905e1fc79a95680ec2a1ff69a591bb3fdb6c7157", size = 246865, upload-time = "2025-08-10T21:25:42.408Z" },
{ url = "https://files.pythonhosted.org/packages/34/12/5608f76070939395c17053bf16e81fd6c06cf362a537ea9d07e281013a27/coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54", size = 248800, upload-time = "2025-08-10T21:25:44.098Z" },
{ url = "https://files.pythonhosted.org/packages/ce/52/7cc90c448a0ad724283cbcdfd66b8d23a598861a6a22ac2b7b8696491798/coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a", size = 250904, upload-time = "2025-08-10T21:25:45.384Z" },
{ url = "https://files.pythonhosted.org/packages/e6/70/9967b847063c1c393b4f4d6daab1131558ebb6b51f01e7df7150aa99f11d/coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84", size = 248597, upload-time = "2025-08-10T21:25:47.059Z" },
{ url = "https://files.pythonhosted.org/packages/2d/fe/263307ce6878b9ed4865af42e784b42bb82d066bcf10f68defa42931c2c7/coverage-7.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0dc69c60224cda33d384572da945759756e3f06b9cdac27f302f53961e63160", size = 246647, upload-time = "2025-08-10T21:25:48.334Z" },
{ url = "https://files.pythonhosted.org/packages/8e/27/d27af83ad162eba62c4eb7844a1de6cf7d9f6b185df50b0a3514a6f80ddd/coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124", size = 247290, upload-time = "2025-08-10T21:25:49.945Z" },
{ url = "https://files.pythonhosted.org/packages/28/83/904ff27e15467a5622dbe9ad2ed5831b4a616a62570ec5924d06477dff5a/coverage-7.10.3-cp311-cp311-win32.whl", hash = "sha256:54e409dd64e5302b2a8fdf44ec1c26f47abd1f45a2dcf67bd161873ee05a59b8", size = 218521, upload-time = "2025-08-10T21:25:51.208Z" },
{ url = "https://files.pythonhosted.org/packages/b8/29/bc717b8902faaccf0ca486185f0dcab4778561a529dde51cb157acaafa16/coverage-7.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:30c601610a9b23807c5e9e2e442054b795953ab85d525c3de1b1b27cebeb2117", size = 219412, upload-time = "2025-08-10T21:25:52.494Z" },
{ url = "https://files.pythonhosted.org/packages/7b/7a/5a1a7028c11bb589268c656c6b3f2bbf06e0aced31bbdf7a4e94e8442cc0/coverage-7.10.3-cp311-cp311-win_arm64.whl", hash = "sha256:dabe662312a97958e932dee056f2659051d822552c0b866823e8ba1c2fe64770", size = 218091, upload-time = "2025-08-10T21:25:54.102Z" },
{ url = "https://files.pythonhosted.org/packages/b8/62/13c0b66e966c43d7aa64dadc8cd2afa1f5a2bf9bb863bdabc21fb94e8b63/coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42", size = 216262, upload-time = "2025-08-10T21:25:55.367Z" },
{ url = "https://files.pythonhosted.org/packages/b5/f0/59fdf79be7ac2f0206fc739032f482cfd3f66b18f5248108ff192741beae/coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294", size = 216496, upload-time = "2025-08-10T21:25:56.759Z" },
{ url = "https://files.pythonhosted.org/packages/34/b1/bc83788ba31bde6a0c02eb96bbc14b2d1eb083ee073beda18753fa2c4c66/coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7", size = 247989, upload-time = "2025-08-10T21:25:58.067Z" },
{ url = "https://files.pythonhosted.org/packages/0c/29/f8bdf88357956c844bd872e87cb16748a37234f7f48c721dc7e981145eb7/coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437", size = 250738, upload-time = "2025-08-10T21:25:59.406Z" },
{ url = "https://files.pythonhosted.org/packages/ae/df/6396301d332b71e42bbe624670af9376f63f73a455cc24723656afa95796/coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587", size = 251868, upload-time = "2025-08-10T21:26:00.65Z" },
{ url = "https://files.pythonhosted.org/packages/91/21/d760b2df6139b6ef62c9cc03afb9bcdf7d6e36ed4d078baacffa618b4c1c/coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea", size = 249790, upload-time = "2025-08-10T21:26:02.009Z" },
{ url = "https://files.pythonhosted.org/packages/69/91/5dcaa134568202397fa4023d7066d4318dc852b53b428052cd914faa05e1/coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613", size = 247907, upload-time = "2025-08-10T21:26:03.757Z" },
{ url = "https://files.pythonhosted.org/packages/38/ed/70c0e871cdfef75f27faceada461206c1cc2510c151e1ef8d60a6fedda39/coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb", size = 249344, upload-time = "2025-08-10T21:26:05.11Z" },
{ url = "https://files.pythonhosted.org/packages/5f/55/c8a273ed503cedc07f8a00dcd843daf28e849f0972e4c6be4c027f418ad6/coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a", size = 218693, upload-time = "2025-08-10T21:26:06.534Z" },
{ url = "https://files.pythonhosted.org/packages/94/58/dd3cfb2473b85be0b6eb8c5b6d80b6fc3f8f23611e69ef745cef8cf8bad5/coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5", size = 219501, upload-time = "2025-08-10T21:26:08.195Z" },
{ url = "https://files.pythonhosted.org/packages/56/af/7cbcbf23d46de6f24246e3f76b30df099d05636b30c53c158a196f7da3ad/coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571", size = 218135, upload-time = "2025-08-10T21:26:09.584Z" },
{ url = "https://files.pythonhosted.org/packages/0a/ff/239e4de9cc149c80e9cc359fab60592365b8c4cbfcad58b8a939d18c6898/coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a", size = 216298, upload-time = "2025-08-10T21:26:10.973Z" },
{ url = "https://files.pythonhosted.org/packages/56/da/28717da68f8ba68f14b9f558aaa8f3e39ada8b9a1ae4f4977c8f98b286d5/coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a", size = 216546, upload-time = "2025-08-10T21:26:12.616Z" },
{ url = "https://files.pythonhosted.org/packages/de/bb/e1ade16b9e3f2d6c323faeb6bee8e6c23f3a72760a5d9af102ef56a656cb/coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46", size = 247538, upload-time = "2025-08-10T21:26:14.455Z" },
{ url = "https://files.pythonhosted.org/packages/ea/2f/6ae1db51dc34db499bfe340e89f79a63bd115fc32513a7bacdf17d33cd86/coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4", size = 250141, upload-time = "2025-08-10T21:26:15.787Z" },
{ url = "https://files.pythonhosted.org/packages/4f/ed/33efd8819895b10c66348bf26f011dd621e804866c996ea6893d682218df/coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a", size = 251415, upload-time = "2025-08-10T21:26:17.535Z" },
{ url = "https://files.pythonhosted.org/packages/26/04/cb83826f313d07dc743359c9914d9bc460e0798da9a0e38b4f4fabc207ed/coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3", size = 249575, upload-time = "2025-08-10T21:26:18.921Z" },
{ url = "https://files.pythonhosted.org/packages/2d/fd/ae963c7a8e9581c20fa4355ab8940ca272554d8102e872dbb932a644e410/coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c", size = 247466, upload-time = "2025-08-10T21:26:20.263Z" },
{ url = "https://files.pythonhosted.org/packages/99/e8/b68d1487c6af370b8d5ef223c6d7e250d952c3acfbfcdbf1a773aa0da9d2/coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21", size = 249084, upload-time = "2025-08-10T21:26:21.638Z" },
{ url = "https://files.pythonhosted.org/packages/66/4d/a0bcb561645c2c1e21758d8200443669d6560d2a2fb03955291110212ec4/coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0", size = 218735, upload-time = "2025-08-10T21:26:23.009Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c3/78b4adddbc0feb3b223f62761e5f9b4c5a758037aaf76e0a5845e9e35e48/coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c", size = 219531, upload-time = "2025-08-10T21:26:24.474Z" },
{ url = "https://files.pythonhosted.org/packages/70/1b/1229c0b2a527fa5390db58d164aa896d513a1fbb85a1b6b6676846f00552/coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87", size = 218162, upload-time = "2025-08-10T21:26:25.847Z" },
{ url = "https://files.pythonhosted.org/packages/fc/26/1c1f450e15a3bf3eaecf053ff64538a2612a23f05b21d79ce03be9ff5903/coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84", size = 217003, upload-time = "2025-08-10T21:26:27.231Z" },
{ url = "https://files.pythonhosted.org/packages/29/96/4b40036181d8c2948454b458750960956a3c4785f26a3c29418bbbee1666/coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e", size = 217238, upload-time = "2025-08-10T21:26:28.83Z" },
{ url = "https://files.pythonhosted.org/packages/62/23/8dfc52e95da20957293fb94d97397a100e63095ec1e0ef5c09dd8c6f591a/coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f", size = 258561, upload-time = "2025-08-10T21:26:30.475Z" },
{ url = "https://files.pythonhosted.org/packages/59/95/00e7fcbeda3f632232f4c07dde226afe3511a7781a000aa67798feadc535/coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5", size = 260735, upload-time = "2025-08-10T21:26:32.333Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4c/f4666cbc4571804ba2a65b078ff0de600b0b577dc245389e0bc9b69ae7ca/coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8", size = 262960, upload-time = "2025-08-10T21:26:33.701Z" },
{ url = "https://files.pythonhosted.org/packages/c1/a5/8a9e8a7b12a290ed98b60f73d1d3e5e9ced75a4c94a0d1a671ce3ddfff2a/coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1", size = 260515, upload-time = "2025-08-10T21:26:35.16Z" },
{ url = "https://files.pythonhosted.org/packages/86/11/bb59f7f33b2cac0c5b17db0d9d0abba9c90d9eda51a6e727b43bd5fce4ae/coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256", size = 258278, upload-time = "2025-08-10T21:26:36.539Z" },
{ url = "https://files.pythonhosted.org/packages/cc/22/3646f8903743c07b3e53fded0700fed06c580a980482f04bf9536657ac17/coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b", size = 259408, upload-time = "2025-08-10T21:26:37.954Z" },
{ url = "https://files.pythonhosted.org/packages/d2/5c/6375e9d905da22ddea41cd85c30994b8b6f6c02e44e4c5744b76d16b026f/coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e", size = 219396, upload-time = "2025-08-10T21:26:39.426Z" },
{ url = "https://files.pythonhosted.org/packages/33/3b/7da37fd14412b8c8b6e73c3e7458fef6b1b05a37f990a9776f88e7740c89/coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c", size = 220458, upload-time = "2025-08-10T21:26:40.905Z" },
{ url = "https://files.pythonhosted.org/packages/28/cc/59a9a70f17edab513c844ee7a5c63cf1057041a84cc725b46a51c6f8301b/coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098", size = 218722, upload-time = "2025-08-10T21:26:42.362Z" },
{ url = "https://files.pythonhosted.org/packages/2d/84/bb773b51a06edbf1231b47dc810a23851f2796e913b335a0fa364773b842/coverage-7.10.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bce8b8180912914032785850d8f3aacb25ec1810f5f54afc4a8b114e7a9b55de", size = 216280, upload-time = "2025-08-10T21:26:44.132Z" },
{ url = "https://files.pythonhosted.org/packages/92/a8/4d8ca9c111d09865f18d56facff64d5fa076a5593c290bd1cfc5dceb8dba/coverage-7.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07790b4b37d56608536f7c1079bd1aa511567ac2966d33d5cec9cf520c50a7c8", size = 216557, upload-time = "2025-08-10T21:26:45.598Z" },
{ url = "https://files.pythonhosted.org/packages/fe/b2/eb668bfc5060194bc5e1ccd6f664e8e045881cfee66c42a2aa6e6c5b26e8/coverage-7.10.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e79367ef2cd9166acedcbf136a458dfe9a4a2dd4d1ee95738fb2ee581c56f667", size = 247598, upload-time = "2025-08-10T21:26:47.081Z" },
{ url = "https://files.pythonhosted.org/packages/fd/b0/9faa4ac62c8822219dd83e5d0e73876398af17d7305968aed8d1606d1830/coverage-7.10.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:419d2a0f769f26cb1d05e9ccbc5eab4cb5d70231604d47150867c07822acbdf4", size = 250131, upload-time = "2025-08-10T21:26:48.65Z" },
{ url = "https://files.pythonhosted.org/packages/4e/90/203537e310844d4bf1bdcfab89c1e05c25025c06d8489b9e6f937ad1a9e2/coverage-7.10.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee221cf244757cdc2ac882e3062ab414b8464ad9c884c21e878517ea64b3fa26", size = 251485, upload-time = "2025-08-10T21:26:50.368Z" },
{ url = "https://files.pythonhosted.org/packages/b9/b2/9d894b26bc53c70a1fe503d62240ce6564256d6d35600bdb86b80e516e7d/coverage-7.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c2079d8cdd6f7373d628e14b3357f24d1db02c9dc22e6a007418ca7a2be0435a", size = 249488, upload-time = "2025-08-10T21:26:52.045Z" },
{ url = "https://files.pythonhosted.org/packages/b4/28/af167dbac5281ba6c55c933a0ca6675d68347d5aee39cacc14d44150b922/coverage-7.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bd8df1f83c0703fa3ca781b02d36f9ec67ad9cb725b18d486405924f5e4270bd", size = 247419, upload-time = "2025-08-10T21:26:53.533Z" },
{ url = "https://files.pythonhosted.org/packages/f4/1c/9a4ddc9f0dcb150d4cd619e1c4bb39bcf694c6129220bdd1e5895d694dda/coverage-7.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b4e25e0fa335c8aa26e42a52053f3786a61cc7622b4d54ae2dad994aa754fec", size = 248917, upload-time = "2025-08-10T21:26:55.11Z" },
{ url = "https://files.pythonhosted.org/packages/92/27/c6a60c7cbe10dbcdcd7fc9ee89d531dc04ea4c073800279bb269954c5a9f/coverage-7.10.3-cp314-cp314-win32.whl", hash = "sha256:d7c3d02c2866deb217dce664c71787f4b25420ea3eaf87056f44fb364a3528f5", size = 218999, upload-time = "2025-08-10T21:26:56.637Z" },
{ url = "https://files.pythonhosted.org/packages/36/09/a94c1369964ab31273576615d55e7d14619a1c47a662ed3e2a2fe4dee7d4/coverage-7.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:9c8916d44d9e0fe6cdb2227dc6b0edd8bc6c8ef13438bbbf69af7482d9bb9833", size = 219801, upload-time = "2025-08-10T21:26:58.207Z" },
{ url = "https://files.pythonhosted.org/packages/23/59/f5cd2a80f401c01cf0f3add64a7b791b7d53fd6090a4e3e9ea52691cf3c4/coverage-7.10.3-cp314-cp314-win_arm64.whl", hash = "sha256:1007d6a2b3cf197c57105cc1ba390d9ff7f0bee215ced4dea530181e49c65ab4", size = 218381, upload-time = "2025-08-10T21:26:59.707Z" },
{ url = "https://files.pythonhosted.org/packages/73/3d/89d65baf1ea39e148ee989de6da601469ba93c1d905b17dfb0b83bd39c96/coverage-7.10.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ebc8791d346410d096818788877d675ca55c91db87d60e8f477bd41c6970ffc6", size = 217019, upload-time = "2025-08-10T21:27:01.242Z" },
{ url = "https://files.pythonhosted.org/packages/7d/7d/d9850230cd9c999ce3a1e600f85c2fff61a81c301334d7a1faa1a5ba19c8/coverage-7.10.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f4e4d8e75f6fd3c6940ebeed29e3d9d632e1f18f6fb65d33086d99d4d073241", size = 217237, upload-time = "2025-08-10T21:27:03.442Z" },
{ url = "https://files.pythonhosted.org/packages/36/51/b87002d417202ab27f4a1cd6bd34ee3b78f51b3ddbef51639099661da991/coverage-7.10.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:24581ed69f132b6225a31b0228ae4885731cddc966f8a33fe5987288bdbbbd5e", size = 258735, upload-time = "2025-08-10T21:27:05.124Z" },
{ url = "https://files.pythonhosted.org/packages/1c/02/1f8612bfcb46fc7ca64a353fff1cd4ed932bb6e0b4e0bb88b699c16794b8/coverage-7.10.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec151569ddfccbf71bac8c422dce15e176167385a00cd86e887f9a80035ce8a5", size = 260901, upload-time = "2025-08-10T21:27:06.68Z" },
{ url = "https://files.pythonhosted.org/packages/aa/3a/fe39e624ddcb2373908bd922756384bb70ac1c5009b0d1674eb326a3e428/coverage-7.10.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ae8e7c56290b908ee817200c0b65929b8050bc28530b131fe7c6dfee3e7d86b", size = 263157, upload-time = "2025-08-10T21:27:08.398Z" },
{ url = "https://files.pythonhosted.org/packages/5e/89/496b6d5a10fa0d0691a633bb2b2bcf4f38f0bdfcbde21ad9e32d1af328ed/coverage-7.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb742309766d7e48e9eb4dc34bc95a424707bc6140c0e7d9726e794f11b92a0", size = 260597, upload-time = "2025-08-10T21:27:10.237Z" },
{ url = "https://files.pythonhosted.org/packages/b6/a6/8b5bf6a9e8c6aaeb47d5fe9687014148efc05c3588110246d5fdeef9b492/coverage-7.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c65e2a5b32fbe1e499f1036efa6eb9cb4ea2bf6f7168d0e7a5852f3024f471b1", size = 258353, upload-time = "2025-08-10T21:27:11.773Z" },
{ url = "https://files.pythonhosted.org/packages/c3/6d/ad131be74f8afd28150a07565dfbdc86592fd61d97e2dc83383d9af219f0/coverage-7.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d48d2cb07d50f12f4f18d2bb75d9d19e3506c26d96fffabf56d22936e5ed8f7c", size = 259504, upload-time = "2025-08-10T21:27:13.254Z" },
{ url = "https://files.pythonhosted.org/packages/ec/30/fc9b5097092758cba3375a8cc4ff61774f8cd733bcfb6c9d21a60077a8d8/coverage-7.10.3-cp314-cp314t-win32.whl", hash = "sha256:dec0d9bc15ee305e09fe2cd1911d3f0371262d3cfdae05d79515d8cb712b4869", size = 219782, upload-time = "2025-08-10T21:27:14.736Z" },
{ url = "https://files.pythonhosted.org/packages/72/9b/27fbf79451b1fac15c4bda6ec6e9deae27cf7c0648c1305aa21a3454f5c4/coverage-7.10.3-cp314-cp314t-win_amd64.whl", hash = "sha256:424ea93a323aa0f7f01174308ea78bde885c3089ec1bef7143a6d93c3e24ef64", size = 220898, upload-time = "2025-08-10T21:27:16.297Z" },
{ url = "https://files.pythonhosted.org/packages/d1/cf/a32bbf92869cbf0b7c8b84325327bfc718ad4b6d2c63374fef3d58e39306/coverage-7.10.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f5983c132a62d93d71c9ef896a0b9bf6e6828d8d2ea32611f58684fba60bba35", size = 218922, upload-time = "2025-08-10T21:27:18.22Z" },
{ url = "https://files.pythonhosted.org/packages/84/19/e67f4ae24e232c7f713337f3f4f7c9c58afd0c02866fb07c7b9255a19ed7/coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1", size = 207921, upload-time = "2025-08-10T21:27:38.254Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "44.0.3" version = "44.0.3"
@@ -933,6 +1048,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
] ]
[[package]]
name = "iniconfig"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
]
[[package]] [[package]]
name = "itsdangerous" name = "itsdangerous"
version = "2.2.0" version = "2.2.0"
@@ -1537,6 +1661,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/29/a2/d40fb2460e883eca5199c62cfc2463fd261f760556ae6290f88488c362c0/pip-25.1.1-py3-none-any.whl", hash = "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", size = 1825227, upload-time = "2025-05-02T15:13:59.102Z" }, { url = "https://files.pythonhosted.org/packages/29/a2/d40fb2460e883eca5199c62cfc2463fd261f760556ae6290f88488c362c0/pip-25.1.1-py3-none-any.whl", hash = "sha256:2913a38a2abf4ea6b64ab507bd9e967f3b53dc1ede74b01b0931e1ce548751af", size = 1825227, upload-time = "2025-05-02T15:13:59.102Z" },
] ]
[[package]]
name = "pluggy"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
[[package]] [[package]]
name = "priority" name = "priority"
version = "2.0.0" version = "2.0.0"
@@ -1828,6 +1961,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" },
] ]
[[package]]
name = "pygments"
version = "2.19.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.10.1" version = "2.10.1"
@@ -1837,6 +1979,51 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
] ]
[[package]]
name = "pytest"
version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
]
[[package]]
name = "pytest-asyncio"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" },
]
[[package]]
name = "pytest-cov"
version = "6.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage", extra = ["toml"] },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" },
]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@@ -2011,6 +2198,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
] ]
[[package]]
name = "ruff"
version = "0.12.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/da/5bd7565be729e86e1442dad2c9a364ceeff82227c2dece7c29697a9795eb/ruff-0.12.8.tar.gz", hash = "sha256:4cb3a45525176e1009b2b64126acf5f9444ea59066262791febf55e40493a033", size = 5242373, upload-time = "2025-08-07T19:05:47.268Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/1e/c843bfa8ad1114fab3eb2b78235dda76acd66384c663a4e0415ecc13aa1e/ruff-0.12.8-py3-none-linux_armv6l.whl", hash = "sha256:63cb5a5e933fc913e5823a0dfdc3c99add73f52d139d6cd5cc8639d0e0465513", size = 11675315, upload-time = "2025-08-07T19:05:06.15Z" },
{ url = "https://files.pythonhosted.org/packages/24/ee/af6e5c2a8ca3a81676d5480a1025494fd104b8896266502bb4de2a0e8388/ruff-0.12.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a9bbe28f9f551accf84a24c366c1aa8774d6748438b47174f8e8565ab9dedbc", size = 12456653, upload-time = "2025-08-07T19:05:09.759Z" },
{ url = "https://files.pythonhosted.org/packages/99/9d/e91f84dfe3866fa648c10512904991ecc326fd0b66578b324ee6ecb8f725/ruff-0.12.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2fae54e752a3150f7ee0e09bce2e133caf10ce9d971510a9b925392dc98d2fec", size = 11659690, upload-time = "2025-08-07T19:05:12.551Z" },
{ url = "https://files.pythonhosted.org/packages/fe/ac/a363d25ec53040408ebdd4efcee929d48547665858ede0505d1d8041b2e5/ruff-0.12.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0acbcf01206df963d9331b5838fb31f3b44fa979ee7fa368b9b9057d89f4a53", size = 11896923, upload-time = "2025-08-07T19:05:14.821Z" },
{ url = "https://files.pythonhosted.org/packages/58/9f/ea356cd87c395f6ade9bb81365bd909ff60860975ca1bc39f0e59de3da37/ruff-0.12.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae3e7504666ad4c62f9ac8eedb52a93f9ebdeb34742b8b71cd3cccd24912719f", size = 11477612, upload-time = "2025-08-07T19:05:16.712Z" },
{ url = "https://files.pythonhosted.org/packages/1a/46/92e8fa3c9dcfd49175225c09053916cb97bb7204f9f899c2f2baca69e450/ruff-0.12.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cb82efb5d35d07497813a1c5647867390a7d83304562607f3579602fa3d7d46f", size = 13182745, upload-time = "2025-08-07T19:05:18.709Z" },
{ url = "https://files.pythonhosted.org/packages/5e/c4/f2176a310f26e6160deaf661ef60db6c3bb62b7a35e57ae28f27a09a7d63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dbea798fc0065ad0b84a2947b0aff4233f0cb30f226f00a2c5850ca4393de609", size = 14206885, upload-time = "2025-08-07T19:05:21.025Z" },
{ url = "https://files.pythonhosted.org/packages/87/9d/98e162f3eeeb6689acbedbae5050b4b3220754554526c50c292b611d3a63/ruff-0.12.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:49ebcaccc2bdad86fd51b7864e3d808aad404aab8df33d469b6e65584656263a", size = 13639381, upload-time = "2025-08-07T19:05:23.423Z" },
{ url = "https://files.pythonhosted.org/packages/81/4e/1b7478b072fcde5161b48f64774d6edd59d6d198e4ba8918d9f4702b8043/ruff-0.12.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ac9c570634b98c71c88cb17badd90f13fc076a472ba6ef1d113d8ed3df109fb", size = 12613271, upload-time = "2025-08-07T19:05:25.507Z" },
{ url = "https://files.pythonhosted.org/packages/e8/67/0c3c9179a3ad19791ef1b8f7138aa27d4578c78700551c60d9260b2c660d/ruff-0.12.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:560e0cd641e45591a3e42cb50ef61ce07162b9c233786663fdce2d8557d99818", size = 12847783, upload-time = "2025-08-07T19:05:28.14Z" },
{ url = "https://files.pythonhosted.org/packages/4e/2a/0b6ac3dd045acf8aa229b12c9c17bb35508191b71a14904baf99573a21bd/ruff-0.12.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:71c83121512e7743fba5a8848c261dcc454cafb3ef2934a43f1b7a4eb5a447ea", size = 11702672, upload-time = "2025-08-07T19:05:30.413Z" },
{ url = "https://files.pythonhosted.org/packages/9d/ee/f9fdc9f341b0430110de8b39a6ee5fa68c5706dc7c0aa940817947d6937e/ruff-0.12.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:de4429ef2ba091ecddedd300f4c3f24bca875d3d8b23340728c3cb0da81072c3", size = 11440626, upload-time = "2025-08-07T19:05:32.492Z" },
{ url = "https://files.pythonhosted.org/packages/89/fb/b3aa2d482d05f44e4d197d1de5e3863feb13067b22c571b9561085c999dc/ruff-0.12.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a2cab5f60d5b65b50fba39a8950c8746df1627d54ba1197f970763917184b161", size = 12462162, upload-time = "2025-08-07T19:05:34.449Z" },
{ url = "https://files.pythonhosted.org/packages/18/9f/5c5d93e1d00d854d5013c96e1a92c33b703a0332707a7cdbd0a4880a84fb/ruff-0.12.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:45c32487e14f60b88aad6be9fd5da5093dbefb0e3e1224131cb1d441d7cb7d46", size = 12913212, upload-time = "2025-08-07T19:05:36.541Z" },
{ url = "https://files.pythonhosted.org/packages/71/13/ab9120add1c0e4604c71bfc2e4ef7d63bebece0cfe617013da289539cef8/ruff-0.12.8-py3-none-win32.whl", hash = "sha256:daf3475060a617fd5bc80638aeaf2f5937f10af3ec44464e280a9d2218e720d3", size = 11694382, upload-time = "2025-08-07T19:05:38.468Z" },
{ url = "https://files.pythonhosted.org/packages/f6/dc/a2873b7c5001c62f46266685863bee2888caf469d1edac84bf3242074be2/ruff-0.12.8-py3-none-win_amd64.whl", hash = "sha256:7209531f1a1fcfbe8e46bcd7ab30e2f43604d8ba1c49029bb420b103d0b5f76e", size = 12740482, upload-time = "2025-08-07T19:05:40.391Z" },
{ url = "https://files.pythonhosted.org/packages/cb/5c/799a1efb8b5abab56e8a9f2a0b72d12bd64bb55815e9476c7d0a2887d2f7/ruff-0.12.8-py3-none-win_arm64.whl", hash = "sha256:c90e1a334683ce41b0e7a04f41790c429bf5073b62c1ae701c9dc5b3d14f0749", size = 11884718, upload-time = "2025-08-07T19:05:42.866Z" },
]
[[package]] [[package]]
name = "silk-python" name = "silk-python"
version = "0.2.6" version = "0.2.6"