Compare commits
22 Commits
dev
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479a737a03 | ||
|
|
e324b69bf1 | ||
|
|
df86913fbf | ||
|
|
ca77cd6a83 | ||
|
|
02a9769b35 | ||
|
|
7640f11bfc | ||
|
|
9fa44dbcfa | ||
|
|
2cae941bae | ||
|
|
bc0784f41d | ||
|
|
c57d75e01a | ||
|
|
73edeae013 | ||
|
|
7d46314dc8 | ||
|
|
d5a53a89eb | ||
|
|
a85bc510dd | ||
|
|
2beea7d218 | ||
|
|
a93cd3dd5f | ||
|
|
db4d02c2e2 | ||
|
|
fd7811402b | ||
|
|
eb0325e627 | ||
|
|
8b4b04ec09 | ||
|
|
9f32c9280f | ||
|
|
4fcd09cfa8 |
31
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md
vendored
31
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.md
vendored
@@ -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
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml
vendored
Normal file
56
.github/ISSUE_TEMPLATE/PLUGIN_PUBLISH.yml
vendored
Normal 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
.github/copilot-instructions.md
vendored
Normal file
63
.github/copilot-instructions.md
vendored
Normal 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.
|
||||||
4
.github/workflows/auto_release.yml
vendored
4
.github/workflows/auto_release.yml
vendored
@@ -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
|
||||||
|
|||||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -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
|
||||||
|
|||||||
2
.github/workflows/coverage_test.yml
vendored
2
.github/workflows/coverage_test.yml
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/dashboard_ci.yml
vendored
2
.github/workflows/dashboard_ci.yml
vendored
@@ -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: |
|
||||||
|
|||||||
2
.github/workflows/docker-image.yml
vendored
2
.github/workflows/docker-image.yml
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
85
README.md
85
README.md
@@ -27,57 +27,50 @@ _✨ 易上手的多平台 LLM 聊天机器人及开发框架 ✨_
|
|||||||
|
|
||||||
AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用的插件系统和完善的大语言模型(LLM)接入功能的聊天机器人及开发框架。
|
AstrBot 是一个松耦合、异步、支持多消息平台部署、具有易用的插件系统和完善的大语言模型(LLM)接入功能的聊天机器人及开发框架。
|
||||||
|
|
||||||
|
|
||||||
<!-- [](https://codecov.io/gh/Soulter/AstrBot)
|
|
||||||
-->
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
>
|
|
||||||
> 请务必修改默认密码以及保证 AstrBot 版本 >= 3.5.13。
|
|
||||||
|
|
||||||
## ✨ 近期更新
|
|
||||||
|
|
||||||
<details><summary>1. AstrBot 现已自带知识库能力</summary>
|
|
||||||
|
|
||||||
📚 详见[文档](https://astrbot.app/use/knowledge-base.html)
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
</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 已由雨云官方上架至云应用平台,可一键部署。
|
||||||
|
|
||||||
|
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
||||||
|
|
||||||
|
#### 在 Replit 上部署
|
||||||
|
|
||||||
|
社区贡献的部署方式。
|
||||||
|
|
||||||
|
[](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 上部署
|
|
||||||
|
|
||||||
[](https://repl.it/github/Soulter/AstrBot)
|
|
||||||
|
|
||||||
#### 在 雨云 上部署
|
|
||||||
|
|
||||||
[](https://app.rainyun.com/apps/rca/store/5994?ref=NjU1ODg0)
|
|
||||||
|
|
||||||
## ⚡ 消息平台支持情况
|
## ⚡ 消息平台支持情况
|
||||||
|
|
||||||
| 平台 | 支持性 |
|
| 平台 | 支持性 |
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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,7 +674,9 @@ CONFIG_METADATA_2 = {
|
|||||||
"timeout": 120,
|
"timeout": 120,
|
||||||
"model_config": {
|
"model_config": {
|
||||||
"model": "gemini-1.5-flash",
|
"model": "gemini-1.5-flash",
|
||||||
|
"temperature": 0.4,
|
||||||
},
|
},
|
||||||
|
"enable_google_search": False,
|
||||||
},
|
},
|
||||||
"Gemini": {
|
"Gemini": {
|
||||||
"id": "gemini_default",
|
"id": "gemini_default",
|
||||||
@@ -690,6 +689,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 +714,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 +725,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 +738,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 +752,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 +777,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 +805,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 +824,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 +856,7 @@ CONFIG_METADATA_2 = {
|
|||||||
"model": "whisper-1",
|
"model": "whisper-1",
|
||||||
},
|
},
|
||||||
"Whisper(本地加载)": {
|
"Whisper(本地加载)": {
|
||||||
"whisper_hint": "(不用修改我)",
|
"hint": "启用前请 pip 安装 openai-whisper 库(N卡用户大约下载 2GB,主要是 torch 和 cuda,CPU 用户大约下载 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 +865,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 +887,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",
|
||||||
@@ -1556,6 +1577,11 @@ CONFIG_METADATA_2 = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"hint": "API Base URL 请在模型提供商处获得。如出现 404 报错,尝试在地址末尾加上 /v1",
|
"hint": "API Base URL 请在模型提供商处获得。如出现 404 报错,尝试在地址末尾加上 /v1",
|
||||||
},
|
},
|
||||||
|
"enable_google_search": {
|
||||||
|
"description": "启用 Google 搜索",
|
||||||
|
"type": "bool",
|
||||||
|
"hint": "启用后,Gemini(OpenAI兼容) 提供商将支持 googleSearch 函数工具调用,用于网页搜索功能。",
|
||||||
|
},
|
||||||
"model_config": {
|
"model_config": {
|
||||||
"description": "模型配置",
|
"description": "模型配置",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
@@ -1686,6 +1712,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"]
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -81,6 +81,17 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
|
|
||||||
async def _query(self, payloads: dict, tools: FuncCall) -> LLMResponse:
|
async def _query(self, payloads: dict, tools: FuncCall) -> LLMResponse:
|
||||||
if tools:
|
if tools:
|
||||||
|
# Check if we need to add googleSearch function for Gemini(OpenAI Compatible)
|
||||||
|
if (
|
||||||
|
self.provider_config.get("enable_google_search", False)
|
||||||
|
and self.provider_config.get("api_base", "").find(
|
||||||
|
"generativelanguage.googleapis.com"
|
||||||
|
)
|
||||||
|
!= -1
|
||||||
|
):
|
||||||
|
# Add googleSearch function as alias to web_search
|
||||||
|
await self._add_google_search_tool(tools)
|
||||||
|
|
||||||
model = payloads.get("model", "").lower()
|
model = payloads.get("model", "").lower()
|
||||||
omit_empty_param_field = "gemini" in model
|
omit_empty_param_field = "gemini" in model
|
||||||
tool_list = tools.get_func_desc_openai_style(
|
tool_list = tools.get_func_desc_openai_style(
|
||||||
@@ -99,6 +110,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
|
||||||
)
|
)
|
||||||
@@ -119,6 +135,17 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
) -> AsyncGenerator[LLMResponse, None]:
|
) -> AsyncGenerator[LLMResponse, None]:
|
||||||
"""流式查询API,逐步返回结果"""
|
"""流式查询API,逐步返回结果"""
|
||||||
if tools:
|
if tools:
|
||||||
|
# Check if we need to add googleSearch function for Gemini(OpenAI Compatible)
|
||||||
|
if (
|
||||||
|
self.provider_config.get("enable_google_search", False)
|
||||||
|
and self.provider_config.get("api_base", "").find(
|
||||||
|
"generativelanguage.googleapis.com"
|
||||||
|
)
|
||||||
|
!= -1
|
||||||
|
):
|
||||||
|
# Add googleSearch function as alias to web_search
|
||||||
|
await self._add_google_search_tool(tools)
|
||||||
|
|
||||||
model = payloads.get("model", "").lower()
|
model = payloads.get("model", "").lower()
|
||||||
omit_empty_param_field = "gemini" in model
|
omit_empty_param_field = "gemini" in model
|
||||||
tool_list = tools.get_func_desc_openai_style(
|
tool_list = tools.get_func_desc_openai_style(
|
||||||
@@ -176,7 +203,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 +237,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}。")
|
||||||
|
|
||||||
@@ -548,3 +575,35 @@ class ProviderOpenAIOfficial(Provider):
|
|||||||
image_bs64 = base64.b64encode(f.read()).decode("utf-8")
|
image_bs64 = base64.b64encode(f.read()).decode("utf-8")
|
||||||
return "data:image/jpeg;base64," + image_bs64
|
return "data:image/jpeg;base64," + image_bs64
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
async def _add_google_search_tool(self, tools: FuncCall) -> None:
|
||||||
|
"""Add googleSearch function as an alias to web_search for Gemini(OpenAI Compatible)"""
|
||||||
|
# Check if googleSearch is already added
|
||||||
|
for func in tools.func_list:
|
||||||
|
if func.name == "googleSearch":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if web_search exists
|
||||||
|
web_search_func = None
|
||||||
|
for func in tools.func_list:
|
||||||
|
if func.name == "web_search":
|
||||||
|
web_search_func = func
|
||||||
|
break
|
||||||
|
|
||||||
|
if web_search_func is None:
|
||||||
|
# If web_search is not available, don't add googleSearch
|
||||||
|
return
|
||||||
|
|
||||||
|
# Add googleSearch as an alias to web_search with English description
|
||||||
|
tools.add_func(
|
||||||
|
name="googleSearch",
|
||||||
|
func_args=[
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"name": "query",
|
||||||
|
"description": "The most relevant search keywords for the user's question, used to search on Google.",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
desc="Search the internet to answer user questions using Google search. Call this tool when users need to search the web for real-time information.",
|
||||||
|
handler=web_search_func.handler,
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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__
|
||||||
|
|||||||
10
changelogs/v3.5.24.md
Normal file
10
changelogs/v3.5.24.md
Normal 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">
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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。",
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ class Main(star.Star):
|
|||||||
websearch = self.context.get_config()["provider_settings"]["web_search"]
|
websearch = self.context.get_config()["provider_settings"]["web_search"]
|
||||||
if websearch:
|
if websearch:
|
||||||
self.context.activate_llm_tool("web_search")
|
self.context.activate_llm_tool("web_search")
|
||||||
|
self.context.activate_llm_tool("googleSearch")
|
||||||
self.context.activate_llm_tool("fetch_url")
|
self.context.activate_llm_tool("fetch_url")
|
||||||
else:
|
else:
|
||||||
self.context.deactivate_llm_tool("web_search")
|
self.context.deactivate_llm_tool("web_search")
|
||||||
|
self.context.deactivate_llm_tool("googleSearch")
|
||||||
self.context.deactivate_llm_tool("fetch_url")
|
self.context.deactivate_llm_tool("fetch_url")
|
||||||
|
|
||||||
async def _tidy_text(self, text: str) -> str:
|
async def _tidy_text(self, text: str) -> str:
|
||||||
@@ -70,12 +72,14 @@ class Main(star.Star):
|
|||||||
self.context.get_config()["provider_settings"]["web_search"] = True
|
self.context.get_config()["provider_settings"]["web_search"] = True
|
||||||
self.context.get_config().save_config()
|
self.context.get_config().save_config()
|
||||||
self.context.activate_llm_tool("web_search")
|
self.context.activate_llm_tool("web_search")
|
||||||
|
self.context.activate_llm_tool("googleSearch")
|
||||||
self.context.activate_llm_tool("fetch_url")
|
self.context.activate_llm_tool("fetch_url")
|
||||||
event.set_result(MessageEventResult().message("已开启网页搜索功能"))
|
event.set_result(MessageEventResult().message("已开启网页搜索功能"))
|
||||||
elif oper == "off":
|
elif oper == "off":
|
||||||
self.context.get_config()["provider_settings"]["web_search"] = False
|
self.context.get_config()["provider_settings"]["web_search"] = False
|
||||||
self.context.get_config().save_config()
|
self.context.get_config().save_config()
|
||||||
self.context.deactivate_llm_tool("web_search")
|
self.context.deactivate_llm_tool("web_search")
|
||||||
|
self.context.deactivate_llm_tool("googleSearch")
|
||||||
self.context.deactivate_llm_tool("fetch_url")
|
self.context.deactivate_llm_tool("fetch_url")
|
||||||
event.set_result(MessageEventResult().message("已关闭网页搜索功能"))
|
event.set_result(MessageEventResult().message("已关闭网页搜索功能"))
|
||||||
else:
|
else:
|
||||||
@@ -139,6 +143,16 @@ class Main(star.Star):
|
|||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
@llm_tool("googleSearch")
|
||||||
|
async def google_search_alias(self, event: AstrMessageEvent, query: str) -> str:
|
||||||
|
"""Search the internet to answer user questions using Google search. Call this tool when users need to search the web for real-time information.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query(string): The most relevant search keywords for the user's question, used to search on Google.
|
||||||
|
"""
|
||||||
|
# This is an alias for web_search to provide better OpenAI API compatibility
|
||||||
|
return await self.search_from_search_engine(event, query)
|
||||||
|
|
||||||
@llm_tool("fetch_url")
|
@llm_tool("fetch_url")
|
||||||
async def fetch_website_content(self, event: AstrMessageEvent, url: str) -> str:
|
async def fetch_website_content(self, event: AstrMessageEvent, url: str) -> str:
|
||||||
"""fetch the content of a website with the given web url
|
"""fetch the content of a website with the given web url
|
||||||
|
|||||||
@@ -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",
|
||||||
|
]
|
||||||
|
|||||||
216
uv.lock
generated
216
uv.lock
generated
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user