Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a663d6509b | ||
|
|
9ec8839efa | ||
|
|
a7a0350eb2 | ||
|
|
39a7a0d960 | ||
|
|
7740e1e131 | ||
|
|
9dce1ed47e | ||
|
|
e84a00d3a5 | ||
|
|
88a944cb57 | ||
|
|
20c32e72cc | ||
|
|
4788c20816 | ||
|
|
e83fc570a4 | ||
|
|
e841b6af88 | ||
|
|
ea6f209557 | ||
|
|
9bfa726107 | ||
|
|
d24902c66d | ||
|
|
72aea2d3f3 | ||
|
|
dc9612d564 | ||
|
|
1770556d56 | ||
|
|
888fb84aee | ||
|
|
d597fd056d | ||
|
|
dea0ab3974 | ||
|
|
da6facd7d7 | ||
|
|
bb8ab5f173 | ||
|
|
ac8a541059 | ||
|
|
0e66771f0e | ||
|
|
d3a295a801 | ||
|
|
f2df771771 | ||
|
|
7b72cd87a5 | ||
|
|
9431efc6d1 |
51
.github/PULL_REQUEST_TEMPLATE.md
vendored
51
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,19 +1,46 @@
|
||||
<!-- 如果有的话,指定这个 PR 要解决的 ISSUE -->
|
||||
解决了 #XYZ
|
||||
<!-- 如果有的话,请指定此 PR 旨在解决的 ISSUE 编号。 -->
|
||||
<!-- If applicable, please specify the ISSUE number this PR aims to resolve. -->
|
||||
|
||||
### Motivation
|
||||
fixes #XYZ
|
||||
|
||||
<!--解释为什么要改动-->
|
||||
---
|
||||
|
||||
### Modifications
|
||||
### Motivation / 动机
|
||||
|
||||
<!--简单解释你的改动-->
|
||||
<!--请描述此项更改的动机:它解决了什么问题?(例如:修复了 XX 错误,添加了 YY 功能)-->
|
||||
<!--Please describe the motivation for this change: What problem does it solve? (e.g., Fixes XX bug, adds YY feature)-->
|
||||
|
||||
### Check
|
||||
### Modifications / 改动点
|
||||
|
||||
<!--如果分支被合并,您的代码将服务于数万名用户!在提交前,请核查一下几点内容-->
|
||||
<!--请总结你的改动:哪些核心文件被修改了?实现了什么功能?-->
|
||||
<!--Please summarize your changes: What core files were modified? What functionality was implemented?-->
|
||||
|
||||
- [ ] 😊 我的 Commit Message 符合良好的[规范](https://www.conventionalcommits.org/en/v1.0.0/#summary)
|
||||
- [ ] 👀 我的更改经过良好的测试
|
||||
- [ ] 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 `requirements.txt` 和 `pyproject.toml` 文件相应位置。
|
||||
- [ ] 😮 我的更改没有引入恶意代码
|
||||
### Verification Steps / 验证步骤
|
||||
|
||||
<!--请为审查者 (Reviewer) 提供清晰、可复现的验证步骤(例如:1. 导航到... 2. 点击...)。-->
|
||||
<!--Please provide clear and reproducible verification steps for the Reviewer (e.g., 1. Navigate to... 2. Click...).-->
|
||||
|
||||
### Screenshots or Test Results / 运行截图或测试结果
|
||||
|
||||
<!--请粘贴截图、GIF 或测试日志,作为执行“验证步骤”的证据,证明此改动有效。-->
|
||||
<!--Please paste screenshots, GIFs, or test logs here as evidence of executing the "Verification Steps" to prove this change is effective.-->
|
||||
|
||||
### Compatibility & Breaking Changes / 兼容性与破坏性变更
|
||||
|
||||
<!--请说明此变更的兼容性:哪些是破坏性变更?哪些地方做了向后兼容处理?是否提供了数据迁移方法?-->
|
||||
<!--Please explain the compatibility of this change: What are the breaking changes? What backward-compatible measures were taken? Are data migration paths provided?-->
|
||||
|
||||
- [ ] 这是一个破坏性变更 (Breaking Change)。/ This is a breaking change.
|
||||
- [ ] 这不是一个破坏性变更。/ This is NOT a breaking change.
|
||||
|
||||
---
|
||||
|
||||
### Checklist / 检查清单
|
||||
|
||||
<!--如果分支被合并,您的代码将服务于数万名用户!在提交前,请核查一下几点内容。-->
|
||||
<!--If merged, your code will serve tens of thousands of users! Please double-check the following items before submitting.-->
|
||||
|
||||
- [ ] 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
|
||||
- [ ] 👀 我的更改经过了良好的测试,**并已在上方提供了“验证步骤”和“运行截图”**。/ My changes have been well-tested, **and "Verification Steps" and "Screenshots" have been provided above**.
|
||||
- [ ] 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 `requirements.txt` 和 `pyproject.toml` 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in `requirements.txt` and `pyproject.toml`.
|
||||
- [ ] 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.
|
||||
|
||||
36
.github/auto_assign.yml
vendored
Normal file
36
.github/auto_assign.yml
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Set to true to add reviewers to pull requests
|
||||
addReviewers: true
|
||||
|
||||
# Set to true to add assignees to pull requests
|
||||
addAssignees: false
|
||||
|
||||
# A list of reviewers to be added to pull requests (GitHub user name)
|
||||
reviewers:
|
||||
- Soulter
|
||||
- Raven95676
|
||||
- Larch-C
|
||||
- anka-afk
|
||||
- advent259141
|
||||
# - zouyonghe
|
||||
|
||||
# A number of reviewers added to the pull request
|
||||
# Set 0 to add all the reviewers (default: 0)
|
||||
numberOfReviewers: 2
|
||||
|
||||
# A list of assignees, overrides reviewers if set
|
||||
# assignees:
|
||||
# - assigneeA
|
||||
|
||||
# A number of assignees to add to the pull request
|
||||
# Set to 0 to add all of the assignees.
|
||||
# Uses numberOfReviewers if unset.
|
||||
# numberOfAssignees: 2
|
||||
|
||||
# A list of keywords to be skipped the process that add reviewers if pull requests include it
|
||||
skipKeywords:
|
||||
- wip
|
||||
- draft
|
||||
|
||||
# A list of users to be skipped by both the add reviewers and add assignees processes
|
||||
# skipUsers:
|
||||
# - dependabot[bot]
|
||||
2
.github/workflows/auto_release.yml
vendored
2
.github/workflows/auto_release.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
|
||||
34
.github/workflows/code-format.yml
vendored
Normal file
34
.github/workflows/code-format.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Code Format Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
format-check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install UV
|
||||
run: pip install uv
|
||||
|
||||
- name: Install dependencies
|
||||
run: uv sync
|
||||
|
||||
- name: Check code formatting with ruff
|
||||
run: |
|
||||
uv run ruff format --check .
|
||||
|
||||
- name: Check code style with ruff
|
||||
run: |
|
||||
uv run ruff check .
|
||||
2
.github/workflows/coverage_test.yml
vendored
2
.github/workflows/coverage_test.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
1
.github/workflows/dashboard_ci.yml
vendored
1
.github/workflows/dashboard_ci.yml
vendored
@@ -37,6 +37,7 @@ jobs:
|
||||
!dist/**/*.md
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.event_name == 'push'
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
tag: release-${{ github.sha }}
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'Stale issue message'
|
||||
|
||||
15
README.md
15
README.md
@@ -14,7 +14,6 @@
|
||||
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=wtbaNx7EioxeaqS9z7RQWVXPIxg2zYr7&jump_from=webapi&authKey=vlqnv/AV2DbJEvGIcxdlNSpfxVy+8vVqijgreRdnVKOaydpc+YSw4MctmEbr0k5"><img alt="QQ_community" src="https://img.shields.io/badge/QQ群-775869627-purple?style=for-the-badge&color=76bad9"></a>
|
||||
<a href="https://t.me/+hAsD2Ebl5as3NmY1"><img alt="Telegram_community" src="https://img.shields.io/badge/Telegram-AstrBot-purple?style=for-the-badge&color=76bad9"></a>
|
||||
[](https://wakatime.com/badge/user/915e5316-99c6-4563-a483-ef186cf000c9/project/018e705a-a1a7-409a-a849-3013485e6c8e)
|
||||

|
||||

|
||||
|
||||
<a href="https://github.com/Soulter/AstrBot/blob/master/README_en.md">English</a> |
|
||||
@@ -100,7 +99,7 @@ uv run main.py
|
||||
- 3 群:630166526
|
||||
- 5 群:822130018
|
||||
- 6 群:753075035
|
||||
- 开发者群:753075035
|
||||
- 开发者群:975206796
|
||||
- 开发者群(备份):295657329
|
||||
|
||||
### Telegram 群组
|
||||
@@ -182,10 +181,18 @@ pre-commit install
|
||||
<img src="https://contrib.rocks/image?repo=AstrBotDevs/AstrBot" />
|
||||
</a>
|
||||
|
||||
此外,本项目的诞生离不开以下开源项目:
|
||||
此外,本项目的诞生离不开以下开源项目的帮助:
|
||||
|
||||
- [NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ) - 伟大的猫猫框架
|
||||
- [wechatpy/wechatpy](https://github.com/wechatpy/wechatpy)
|
||||
|
||||
另外,一些同类型其他的活跃开源 Bot 项目:
|
||||
|
||||
- [nonebot/nonebot2](https://github.com/nonebot/nonebot2) - 扩展性极强的 Bot 框架
|
||||
- [koishijs/koishi](https://github.com/koishijs/koishi) - 扩展性极强的 Bot 框架
|
||||
- [MaiM-with-u/MaiBot](https://github.com/MaiM-with-u/MaiBot) - 注重拟人功能的 ChatBot
|
||||
- [langbot-app/LangBot](https://github.com/langbot-app/LangBot) - 功能丰富的 Bot 平台
|
||||
- [LroMiose/nekro-agent](https://github.com/KroMiose/nekro-agent) - 注重 Agent 的 ChatBot
|
||||
- [zhenxun-org/zhenxun_bot](https://github.com/zhenxun-org/zhenxun_bot) - 功能完善的 ChatBot
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from astrbot.core.star.register import (
|
||||
register_permission_type as permission_type,
|
||||
register_custom_filter as custom_filter,
|
||||
register_on_astrbot_loaded as on_astrbot_loaded,
|
||||
register_on_platform_loaded as on_platform_loaded,
|
||||
register_on_llm_request as on_llm_request,
|
||||
register_on_llm_response as on_llm_response,
|
||||
register_llm_tool as llm_tool,
|
||||
@@ -41,6 +42,7 @@ __all__ = [
|
||||
"custom_filter",
|
||||
"PermissionType",
|
||||
"on_astrbot_loaded",
|
||||
"on_platform_loaded",
|
||||
"on_llm_request",
|
||||
"llm_tool",
|
||||
"on_decorating_result",
|
||||
|
||||
@@ -124,15 +124,17 @@ def build_plug_list(plugins_dir: Path) -> list:
|
||||
if metadata and all(
|
||||
k in metadata for k in ["name", "desc", "version", "author", "repo"]
|
||||
):
|
||||
result.append({
|
||||
"name": str(metadata.get("name", "")),
|
||||
"desc": str(metadata.get("desc", "")),
|
||||
"version": str(metadata.get("version", "")),
|
||||
"author": str(metadata.get("author", "")),
|
||||
"repo": str(metadata.get("repo", "")),
|
||||
"status": PluginStatus.INSTALLED,
|
||||
"local_path": str(plugin_dir),
|
||||
})
|
||||
result.append(
|
||||
{
|
||||
"name": str(metadata.get("name", "")),
|
||||
"desc": str(metadata.get("desc", "")),
|
||||
"version": str(metadata.get("version", "")),
|
||||
"author": str(metadata.get("author", "")),
|
||||
"repo": str(metadata.get("repo", "")),
|
||||
"status": PluginStatus.INSTALLED,
|
||||
"local_path": str(plugin_dir),
|
||||
}
|
||||
)
|
||||
|
||||
# 获取在线插件列表
|
||||
online_plugins = []
|
||||
@@ -142,15 +144,17 @@ def build_plug_list(plugins_dir: Path) -> list:
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
for plugin_id, plugin_info in data.items():
|
||||
online_plugins.append({
|
||||
"name": str(plugin_id),
|
||||
"desc": str(plugin_info.get("desc", "")),
|
||||
"version": str(plugin_info.get("version", "")),
|
||||
"author": str(plugin_info.get("author", "")),
|
||||
"repo": str(plugin_info.get("repo", "")),
|
||||
"status": PluginStatus.NOT_INSTALLED,
|
||||
"local_path": None,
|
||||
})
|
||||
online_plugins.append(
|
||||
{
|
||||
"name": str(plugin_id),
|
||||
"desc": str(plugin_info.get("desc", "")),
|
||||
"version": str(plugin_info.get("version", "")),
|
||||
"author": str(plugin_info.get("author", "")),
|
||||
"repo": str(plugin_info.get("repo", "")),
|
||||
"status": PluginStatus.NOT_INSTALLED,
|
||||
"local_path": None,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
click.echo(f"获取在线插件列表失败: {e}", err=True)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from dataclasses import dataclass
|
||||
import typing as T
|
||||
from astrbot.core.message.message_event_result import MessageChain
|
||||
|
||||
|
||||
class AgentResponseData(T.TypedDict):
|
||||
chain: MessageChain
|
||||
|
||||
|
||||
@@ -14,4 +14,5 @@ class ContextWrapper(Generic[TContext]):
|
||||
context: TContext
|
||||
event: AstrMessageEvent
|
||||
|
||||
|
||||
NoContext = ContextWrapper[None]
|
||||
|
||||
@@ -6,7 +6,7 @@ import os
|
||||
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
VERSION = "4.0.0"
|
||||
VERSION = "4.1.2"
|
||||
DB_PATH = os.path.join(get_astrbot_data_path(), "data_v4.db")
|
||||
|
||||
# 默认配置
|
||||
@@ -56,7 +56,7 @@ DEFAULT_CONFIG = {
|
||||
"wake_prefix": "",
|
||||
"web_search": False,
|
||||
"websearch_provider": "default",
|
||||
"websearch_tavily_key": "",
|
||||
"websearch_tavily_key": [],
|
||||
"web_search_link": False,
|
||||
"display_reasoning_text": False,
|
||||
"identifier": False,
|
||||
@@ -599,6 +599,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "https://api.openai.com/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
"hint": "也兼容所有与 OpenAI API 兼容的服务。",
|
||||
},
|
||||
@@ -613,6 +614,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "",
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "gpt-4o-mini", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"xAI": {
|
||||
@@ -625,6 +627,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "https://api.x.ai/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "grok-2-latest", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"Anthropic": {
|
||||
@@ -654,6 +657,7 @@ CONFIG_METADATA_2 = {
|
||||
"key": ["ollama"], # ollama 的 key 默认是 ollama
|
||||
"api_base": "http://localhost:11434/v1",
|
||||
"model_config": {"model": "llama3.1-8b", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"LM Studio": {
|
||||
@@ -667,6 +671,7 @@ CONFIG_METADATA_2 = {
|
||||
"model_config": {
|
||||
"model": "llama-3.1-8b",
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"Gemini(OpenAI兼容)": {
|
||||
@@ -682,6 +687,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "gemini-1.5-flash",
|
||||
"temperature": 0.4,
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"Gemini": {
|
||||
@@ -722,6 +728,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "https://api.deepseek.com/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "deepseek-chat", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"302.AI": {
|
||||
@@ -734,6 +741,7 @@ CONFIG_METADATA_2 = {
|
||||
"api_base": "https://api.302.ai/v1",
|
||||
"timeout": 120,
|
||||
"model_config": {"model": "gpt-4.1-mini", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"硅基流动": {
|
||||
@@ -749,6 +757,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "deepseek-ai/DeepSeek-V3",
|
||||
"temperature": 0.4,
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"PPIO派欧云": {
|
||||
@@ -764,6 +773,7 @@ CONFIG_METADATA_2 = {
|
||||
"model": "deepseek/deepseek-r1",
|
||||
"temperature": 0.4,
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
},
|
||||
"优云智算": {
|
||||
"id": "compshare",
|
||||
@@ -777,6 +787,7 @@ CONFIG_METADATA_2 = {
|
||||
"model_config": {
|
||||
"model": "moonshotai/Kimi-K2-Instruct",
|
||||
},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"Kimi": {
|
||||
@@ -789,6 +800,7 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": 120,
|
||||
"api_base": "https://api.moonshot.cn/v1",
|
||||
"model_config": {"model": "moonshot-v1-8k", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"智谱 AI": {
|
||||
@@ -847,6 +859,7 @@ CONFIG_METADATA_2 = {
|
||||
"timeout": 120,
|
||||
"api_base": "https://api-inference.modelscope.cn/v1",
|
||||
"model_config": {"model": "Qwen/Qwen3-32B", "temperature": 0.4},
|
||||
"custom_extra_body": {},
|
||||
"modalities": ["text", "image", "tool_use"],
|
||||
},
|
||||
"FastGPT": {
|
||||
@@ -858,6 +871,7 @@ CONFIG_METADATA_2 = {
|
||||
"key": [],
|
||||
"api_base": "https://api.fastgpt.in/api/v1",
|
||||
"timeout": 60,
|
||||
"custom_extra_body": {},
|
||||
},
|
||||
"Whisper(API)": {
|
||||
"id": "whisper",
|
||||
@@ -1102,6 +1116,12 @@ CONFIG_METADATA_2 = {
|
||||
"render_type": "checkbox",
|
||||
"hint": "模型支持的模态。如所填写的模型不支持图像,请取消勾选图像。",
|
||||
},
|
||||
"custom_extra_body": {
|
||||
"description": "自定义请求体参数",
|
||||
"type": "dict",
|
||||
"items": {},
|
||||
"hint": "此处添加的键值对将被合并到发送给 API 的 extra_body 中。值可以是字符串、数字或布尔值。",
|
||||
},
|
||||
"provider": {
|
||||
"type": "string",
|
||||
"invisible": True,
|
||||
@@ -1938,7 +1958,9 @@ CONFIG_METADATA_3 = {
|
||||
},
|
||||
"provider_settings.websearch_tavily_key": {
|
||||
"description": "Tavily API Key",
|
||||
"type": "string",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
"hint": "可添加多个 Key 进行轮询。",
|
||||
"condition": {
|
||||
"provider_settings.websearch_provider": "tavily",
|
||||
},
|
||||
@@ -2108,41 +2130,41 @@ CONFIG_METADATA_3 = {
|
||||
"description": "内容安全",
|
||||
"type": "object",
|
||||
"items": {
|
||||
"platform_settings.content_safety.also_use_in_response": {
|
||||
"content_safety.also_use_in_response": {
|
||||
"description": "同时检查模型的响应内容",
|
||||
"type": "bool",
|
||||
},
|
||||
"platform_settings.content_safety.baidu_aip.enable": {
|
||||
"content_safety.baidu_aip.enable": {
|
||||
"description": "使用百度内容安全审核",
|
||||
"type": "bool",
|
||||
"hint": "您需要手动安装 baidu-aip 库。",
|
||||
},
|
||||
"platform_settings.content_safety.baidu_aip.app_id": {
|
||||
"content_safety.baidu_aip.app_id": {
|
||||
"description": "App ID",
|
||||
"type": "string",
|
||||
"condition": {
|
||||
"platform_settings.content_safety.baidu_aip.enable": True,
|
||||
"content_safety.baidu_aip.enable": True,
|
||||
},
|
||||
},
|
||||
"platform_settings.content_safety.baidu_aip.api_key": {
|
||||
"content_safety.baidu_aip.api_key": {
|
||||
"description": "API Key",
|
||||
"type": "string",
|
||||
"condition": {
|
||||
"platform_settings.content_safety.baidu_aip.enable": True,
|
||||
"content_safety.baidu_aip.enable": True,
|
||||
},
|
||||
},
|
||||
"platform_settings.content_safety.baidu_aip.secret_key": {
|
||||
"content_safety.baidu_aip.secret_key": {
|
||||
"description": "Secret Key",
|
||||
"type": "string",
|
||||
"condition": {
|
||||
"platform_settings.content_safety.baidu_aip.enable": True,
|
||||
"content_safety.baidu_aip.enable": True,
|
||||
},
|
||||
},
|
||||
"platform_settings.content_safety.internal_keywords.enable": {
|
||||
"content_safety.internal_keywords.enable": {
|
||||
"description": "关键词检查",
|
||||
"type": "bool",
|
||||
},
|
||||
"platform_settings.content_safety.internal_keywords.extra_keywords": {
|
||||
"content_safety.internal_keywords.extra_keywords": {
|
||||
"description": "额外关键词",
|
||||
"type": "list",
|
||||
"items": {"type": "string"},
|
||||
|
||||
@@ -53,7 +53,7 @@ async def do_migration_v4(
|
||||
await migration_webchat_data(db_helper, platform_id_map)
|
||||
|
||||
# 执行偏好设置迁移
|
||||
await migration_preferences(db_helper,platform_id_map)
|
||||
await migration_preferences(db_helper, platform_id_map)
|
||||
|
||||
# 执行平台统计表迁移
|
||||
await migration_platform_table(db_helper, platform_id_map)
|
||||
|
||||
@@ -5,6 +5,7 @@ from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
_VT = TypeVar("_VT")
|
||||
|
||||
|
||||
class SharedPreferences:
|
||||
def __init__(self, path=None):
|
||||
if path is None:
|
||||
@@ -42,4 +43,5 @@ class SharedPreferences:
|
||||
self._data.clear()
|
||||
self._save_preferences()
|
||||
|
||||
|
||||
sp = SharedPreferences()
|
||||
|
||||
@@ -4,6 +4,7 @@ from astrbot.core.db.po import Platform, Stats
|
||||
from typing import Tuple, List, Dict, Any
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Conversation:
|
||||
"""LLM 对话存储
|
||||
@@ -76,7 +77,7 @@ PRAGMA encoding = 'UTF-8';
|
||||
"""
|
||||
|
||||
|
||||
class SQLiteDatabase():
|
||||
class SQLiteDatabase:
|
||||
def __init__(self, db_path: str) -> None:
|
||||
super().__init__()
|
||||
self.db_path = db_path
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from .vec_db import FaissVecDB
|
||||
|
||||
__all__ = ["FaissVecDB"]
|
||||
__all__ = ["FaissVecDB"]
|
||||
|
||||
@@ -113,7 +113,8 @@ class FaissVecDB(BaseVecDB):
|
||||
reranked_results, key=lambda x: x.relevance_score, reverse=True
|
||||
)
|
||||
top_k_results = [
|
||||
top_k_results[reranked_result.index] for reranked_result in reranked_results
|
||||
top_k_results[reranked_result.index]
|
||||
for reranked_result in reranked_results
|
||||
]
|
||||
|
||||
return top_k_results
|
||||
|
||||
@@ -22,6 +22,7 @@ class InitialLoader:
|
||||
self.db = db
|
||||
self.logger = logger
|
||||
self.log_broker = log_broker
|
||||
self.webui_dir: str | None = None
|
||||
|
||||
async def start(self):
|
||||
core_lifecycle = AstrBotCoreLifecycle(self.log_broker, self.db)
|
||||
@@ -35,8 +36,10 @@ class InitialLoader:
|
||||
|
||||
core_task = core_lifecycle.start()
|
||||
|
||||
webui_dir = self.webui_dir
|
||||
|
||||
self.dashboard_server = AstrBotDashboard(
|
||||
core_lifecycle, self.db, core_lifecycle.dashboard_shutdown_event
|
||||
core_lifecycle, self.db, core_lifecycle.dashboard_shutdown_event, webui_dir
|
||||
)
|
||||
task = asyncio.gather(
|
||||
core_task, self.dashboard_server.run()
|
||||
|
||||
@@ -77,7 +77,7 @@ async def call_event_hook(
|
||||
|
||||
Returns:
|
||||
bool: 如果事件被终止,返回 True
|
||||
# """
|
||||
#"""
|
||||
handlers = star_handlers_registry.get_handlers_by_event_type(
|
||||
hook_type, plugins_name=event.plugins_name
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ from astrbot.core.provider.entities import ProviderRequest
|
||||
from astrbot.core.utils.metrics import Metric
|
||||
from .astrbot_message import AstrBotMessage, Group
|
||||
from .platform_metadata import PlatformMetadata
|
||||
from .message_session import MessageSession, MessageSesion # noqa
|
||||
from .message_session import MessageSession, MessageSesion # noqa
|
||||
|
||||
|
||||
class AstrMessageEvent(abc.ABC):
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import List
|
||||
from asyncio import Queue
|
||||
from .register import platform_cls_map
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.star.star_handler import star_handlers_registry, star_map, EventType
|
||||
from .sources.webchat.webchat_adapter import WebChatAdapter
|
||||
|
||||
|
||||
@@ -66,18 +67,24 @@ class PlatformManager:
|
||||
WeChatPadProAdapter, # noqa: F401
|
||||
)
|
||||
case "lark":
|
||||
from .sources.lark.lark_adapter import LarkPlatformAdapter # noqa: F401
|
||||
from .sources.lark.lark_adapter import (
|
||||
LarkPlatformAdapter, # noqa: F401
|
||||
)
|
||||
case "dingtalk":
|
||||
from .sources.dingtalk.dingtalk_adapter import (
|
||||
DingtalkPlatformAdapter, # noqa: F401
|
||||
)
|
||||
case "telegram":
|
||||
from .sources.telegram.tg_adapter import TelegramPlatformAdapter # noqa: F401
|
||||
from .sources.telegram.tg_adapter import (
|
||||
TelegramPlatformAdapter, # noqa: F401
|
||||
)
|
||||
case "wecom":
|
||||
from .sources.wecom.wecom_adapter import WecomPlatformAdapter # noqa: F401
|
||||
from .sources.wecom.wecom_adapter import (
|
||||
WecomPlatformAdapter, # noqa: F401
|
||||
)
|
||||
case "weixin_official_account":
|
||||
from .sources.weixin_official_account.weixin_offacc_adapter import (
|
||||
WeixinOfficialAccountPlatformAdapter, # noqa
|
||||
WeixinOfficialAccountPlatformAdapter, # noqa: F401
|
||||
)
|
||||
case "discord":
|
||||
from .sources.discord.discord_platform_adapter import (
|
||||
@@ -86,7 +93,9 @@ class PlatformManager:
|
||||
case "slack":
|
||||
from .sources.slack.slack_adapter import SlackAdapter # noqa: F401
|
||||
case "satori":
|
||||
from .sources.satori.satori_adapter import SatoriPlatformAdapter # noqa: F401
|
||||
from .sources.satori.satori_adapter import (
|
||||
SatoriPlatformAdapter, # noqa: F401
|
||||
)
|
||||
except (ImportError, ModuleNotFoundError) as e:
|
||||
logger.error(
|
||||
f"加载平台适配器 {platform_config['type']} 失败,原因:{e}。请检查依赖库是否安装。提示:可以在 管理面板->控制台->安装Pip库 中安装依赖库。"
|
||||
@@ -115,6 +124,17 @@ class PlatformManager:
|
||||
)
|
||||
)
|
||||
)
|
||||
handlers = star_handlers_registry.get_handlers_by_event_type(
|
||||
EventType.OnPlatformLoadedEvent
|
||||
)
|
||||
for handler in handlers:
|
||||
try:
|
||||
logger.info(
|
||||
f"hook(on_platform_loaded) -> {star_map[handler.handler_module_path].name} - {handler.handler_name}"
|
||||
)
|
||||
await handler.handler()
|
||||
except Exception:
|
||||
logger.error(traceback.format_exc())
|
||||
|
||||
async def _task_wrapper(self, task: asyncio.Task):
|
||||
try:
|
||||
|
||||
@@ -321,7 +321,9 @@ class AiocqhttpAdapter(Platform):
|
||||
user_id=int(m["data"]["qq"]),
|
||||
no_cache=False,
|
||||
)
|
||||
nickname = at_info.get("nick", "") or at_info.get("nickname", "")
|
||||
nickname = at_info.get("nick", "") or at_info.get(
|
||||
"nickname", ""
|
||||
)
|
||||
is_at_self = str(m["data"]["qq"]) in {abm.self_id, "all"}
|
||||
|
||||
abm.message.append(
|
||||
|
||||
@@ -54,9 +54,9 @@ class DingtalkMessageEvent(AstrMessageEvent):
|
||||
logger.debug(f"send image: {ret}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"钉钉图片处理失败: {e}")
|
||||
logger.warning(f"跳过图片发送: {image_path}")
|
||||
logger.warning(f"钉钉图片处理失败: {e}, 跳过图片发送")
|
||||
continue
|
||||
|
||||
async def send(self, message: MessageChain):
|
||||
await self.send_with_client(self.client, message)
|
||||
await super().send(message)
|
||||
|
||||
@@ -41,7 +41,8 @@ class DiscordBotClient(discord.Bot):
|
||||
await self.on_ready_once_callback()
|
||||
except Exception as e:
|
||||
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:
|
||||
"""从 discord.Message 创建数据字典"""
|
||||
@@ -90,7 +91,6 @@ class DiscordBotClient(discord.Bot):
|
||||
message_data = self._create_message_data(message)
|
||||
await self.on_message_received(message_data)
|
||||
|
||||
|
||||
def _extract_interaction_content(self, interaction: discord.Interaction) -> str:
|
||||
"""从交互中提取内容"""
|
||||
interaction_type = interaction.type
|
||||
|
||||
@@ -79,9 +79,12 @@ class DiscordButton(BaseMessageComponent):
|
||||
self.url = url
|
||||
self.disabled = disabled
|
||||
|
||||
|
||||
class DiscordReference(BaseMessageComponent):
|
||||
"""Discord引用组件"""
|
||||
|
||||
type: str = "discord_reference"
|
||||
|
||||
def __init__(self, message_id: str, channel_id: str):
|
||||
self.message_id = message_id
|
||||
self.channel_id = channel_id
|
||||
@@ -98,7 +101,6 @@ class DiscordView(BaseMessageComponent):
|
||||
self.components = components or []
|
||||
self.timeout = timeout
|
||||
|
||||
|
||||
def to_discord_view(self) -> discord.ui.View:
|
||||
"""转换为Discord View对象"""
|
||||
view = discord.ui.View(timeout=self.timeout)
|
||||
|
||||
@@ -53,7 +53,13 @@ class DiscordPlatformEvent(AstrMessageEvent):
|
||||
|
||||
# 解析消息链为 Discord 所需的对象
|
||||
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:
|
||||
logger.error(f"[Discord] 解析消息链时失败: {e}", exc_info=True)
|
||||
return
|
||||
@@ -206,8 +212,7 @@ class DiscordPlatformEvent(AstrMessageEvent):
|
||||
if await asyncio.to_thread(path.exists):
|
||||
file_bytes = await asyncio.to_thread(path.read_bytes)
|
||||
files.append(
|
||||
discord.File(BytesIO(file_bytes),
|
||||
filename=i.name)
|
||||
discord.File(BytesIO(file_bytes), filename=i.name)
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
|
||||
@@ -94,10 +94,15 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
plain_text,
|
||||
image_base64,
|
||||
image_path,
|
||||
record_file_path
|
||||
record_file_path,
|
||||
) = await QQOfficialMessageEvent._parse_to_qqofficial(self.send_buffer)
|
||||
|
||||
if not plain_text and not image_base64 and not image_path and not record_file_path:
|
||||
if (
|
||||
not plain_text
|
||||
and not image_base64
|
||||
and not image_path
|
||||
and not record_file_path
|
||||
):
|
||||
return
|
||||
|
||||
payload = {
|
||||
@@ -118,7 +123,7 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if record_file_path: # group record msg
|
||||
if record_file_path: # group record msg
|
||||
media = await self.upload_group_and_c2c_record(
|
||||
record_file_path, 3, group_openid=source.group_openid
|
||||
)
|
||||
@@ -134,9 +139,9 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
if record_file_path: # c2c record
|
||||
if record_file_path: # c2c record
|
||||
media = await self.upload_group_and_c2c_record(
|
||||
record_file_path, 3, openid = source.author.user_openid
|
||||
record_file_path, 3, openid=source.author.user_openid
|
||||
)
|
||||
payload["media"] = media
|
||||
payload["msg_type"] = 7
|
||||
@@ -190,58 +195,55 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
return await self.bot.api._http.request(route, json=payload)
|
||||
|
||||
async def upload_group_and_c2c_record(
|
||||
self,
|
||||
file_source: str,
|
||||
file_type: int,
|
||||
srv_send_msg: bool = False,
|
||||
**kwargs
|
||||
self, file_source: str, file_type: int, srv_send_msg: bool = False, **kwargs
|
||||
) -> Optional[Media]:
|
||||
"""
|
||||
上传媒体文件
|
||||
"""
|
||||
# 构建基础payload
|
||||
payload = {
|
||||
"file_type": file_type,
|
||||
"srv_send_msg": srv_send_msg
|
||||
}
|
||||
|
||||
payload = {"file_type": file_type, "srv_send_msg": srv_send_msg}
|
||||
|
||||
# 处理文件数据
|
||||
if os.path.exists(file_source):
|
||||
# 读取本地文件
|
||||
async with aiofiles.open(file_source, 'rb') as f:
|
||||
async with aiofiles.open(file_source, "rb") as f:
|
||||
file_content = await f.read()
|
||||
# use base64 encode
|
||||
payload["file_data"] = base64.b64encode(file_content).decode('utf-8')
|
||||
payload["file_data"] = base64.b64encode(file_content).decode("utf-8")
|
||||
else:
|
||||
# 使用URL
|
||||
payload["url"] = file_source
|
||||
|
||||
|
||||
# 添加接收者信息和确定路由
|
||||
if "openid" in kwargs:
|
||||
payload["openid"] = kwargs["openid"]
|
||||
route = Route("POST", "/v2/users/{openid}/files", openid=kwargs["openid"])
|
||||
elif "group_openid" in kwargs:
|
||||
payload["group_openid"] =kwargs["group_openid"]
|
||||
route = Route("POST", "/v2/groups/{group_openid}/files", group_openid=kwargs["group_openid"])
|
||||
payload["group_openid"] = kwargs["group_openid"]
|
||||
route = Route(
|
||||
"POST",
|
||||
"/v2/groups/{group_openid}/files",
|
||||
group_openid=kwargs["group_openid"],
|
||||
)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
try:
|
||||
# 使用底层HTTP请求
|
||||
result = await self.bot.api._http.request(route, json=payload)
|
||||
|
||||
|
||||
if result:
|
||||
return Media(
|
||||
file_uuid=result.get("file_uuid"),
|
||||
file_info=result.get("file_info"),
|
||||
ttl=result.get("ttl", 0),
|
||||
file_id=result.get("id", "")
|
||||
file_id=result.get("id", ""),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"上传请求错误: {e}")
|
||||
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def post_c2c_message(
|
||||
self,
|
||||
openid: str,
|
||||
@@ -286,19 +288,23 @@ class QQOfficialMessageEvent(AstrMessageEvent):
|
||||
image_base64 = image_base64.removeprefix("base64://")
|
||||
elif isinstance(i, Record):
|
||||
if i.file:
|
||||
record_wav_path = await i.convert_to_file_path() # wav 路径
|
||||
record_wav_path = await i.convert_to_file_path() # wav 路径
|
||||
temp_dir = os.path.join(get_astrbot_data_path(), "temp")
|
||||
record_tecent_silk_path = os.path.join(temp_dir, f"{uuid.uuid4()}.silk")
|
||||
record_tecent_silk_path = os.path.join(
|
||||
temp_dir, f"{uuid.uuid4()}.silk"
|
||||
)
|
||||
try:
|
||||
duration = await wav_to_tencent_silk(record_wav_path, record_tecent_silk_path)
|
||||
duration = await wav_to_tencent_silk(
|
||||
record_wav_path, record_tecent_silk_path
|
||||
)
|
||||
if duration > 0:
|
||||
record_file_path = record_tecent_silk_path
|
||||
else:
|
||||
record_file_path = None
|
||||
record_file_path = None
|
||||
logger.error("转换音频格式时出错:音频时长不大于0")
|
||||
except Exception as e:
|
||||
logger.error(f"处理语音时出错: {e}")
|
||||
record_file_path = None
|
||||
record_file_path = None
|
||||
else:
|
||||
logger.debug(f"qq_official 忽略 {i.type}")
|
||||
return plain_text, image_base64, image_file_path, record_file_path
|
||||
|
||||
@@ -308,7 +308,9 @@ class SlackAdapter(Platform):
|
||||
base64_content = base64.b64encode(content).decode("utf-8")
|
||||
return base64_content
|
||||
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}")
|
||||
|
||||
async def run(self) -> Awaitable[Any]:
|
||||
|
||||
@@ -75,7 +75,13 @@ class SlackMessageEvent(AstrMessageEvent):
|
||||
"text": {"type": "mrkdwn", "text": "文件上传失败"},
|
||||
}
|
||||
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:
|
||||
return {"type": "section", "text": {"type": "mrkdwn", "text": str(segment)}}
|
||||
|
||||
|
||||
@@ -66,7 +66,9 @@ class TelegramPlatformEvent(AstrMessageEvent):
|
||||
return chunks
|
||||
|
||||
@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
|
||||
|
||||
has_reply = False
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
class WebChatQueueMgr:
|
||||
def __init__(self) -> None:
|
||||
self.queues = {}
|
||||
@@ -30,4 +31,5 @@ class WebChatQueueMgr:
|
||||
"""Check if a queue exists for the given conversation ID"""
|
||||
return conversation_id in self.queues
|
||||
|
||||
|
||||
webchat_queue_mgr = WebChatQueueMgr()
|
||||
|
||||
@@ -213,10 +213,10 @@ class WeChatPadProAdapter(Platform):
|
||||
def _extract_auth_key(self, data):
|
||||
"""Helper method to extract auth_key from response data."""
|
||||
if isinstance(data, dict):
|
||||
auth_keys = data.get("authKeys") # 新接口
|
||||
auth_keys = data.get("authKeys") # 新接口
|
||||
if isinstance(auth_keys, list) and auth_keys:
|
||||
return auth_keys[0]
|
||||
elif isinstance(data, list) and data: # 旧接口
|
||||
elif isinstance(data, list) and data: # 旧接口
|
||||
return data[0]
|
||||
return None
|
||||
|
||||
@@ -234,7 +234,9 @@ class WeChatPadProAdapter(Platform):
|
||||
try:
|
||||
async with session.post(url, params=params, json=payload) as response:
|
||||
if response.status != 200:
|
||||
logger.error(f"生成授权码失败: {response.status}, {await response.text()}")
|
||||
logger.error(
|
||||
f"生成授权码失败: {response.status}, {await response.text()}"
|
||||
)
|
||||
return
|
||||
|
||||
response_data = await response.json()
|
||||
@@ -245,7 +247,9 @@ class WeChatPadProAdapter(Platform):
|
||||
if self.auth_key:
|
||||
logger.info("成功获取授权码")
|
||||
else:
|
||||
logger.error(f"生成授权码成功但未找到授权码: {response_data}")
|
||||
logger.error(
|
||||
f"生成授权码成功但未找到授权码: {response_data}"
|
||||
)
|
||||
else:
|
||||
logger.error(f"生成授权码失败: {response_data}")
|
||||
except aiohttp.ClientConnectorError as e:
|
||||
|
||||
@@ -48,7 +48,12 @@ class WeChatKF(BaseWeChatAPI):
|
||||
注意:可能会出现返回条数少于limit的情况,需结合返回的has_more字段判断是否继续请求。
|
||||
: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)
|
||||
|
||||
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)
|
||||
|
||||
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")
|
||||
|
||||
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}
|
||||
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
|
||||
|
||||
|
||||
class WeChatKFMessage(BaseWeChatAPI):
|
||||
"""
|
||||
发送微信客服消息
|
||||
@@ -125,35 +126,55 @@ class WeChatKFMessage(BaseWeChatAPI):
|
||||
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(
|
||||
user_id,
|
||||
open_kfid,
|
||||
msgid,
|
||||
msg={
|
||||
"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(
|
||||
user_id,
|
||||
open_kfid,
|
||||
msgid,
|
||||
msg={
|
||||
"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(
|
||||
user_id,
|
||||
open_kfid,
|
||||
msgid,
|
||||
msg={
|
||||
"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
|
||||
await self.convert_message(msg, future)
|
||||
# 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}")
|
||||
self.wexin_event_workers.pop(msg.id, None)
|
||||
return result # xml. see weixin_offacc_event.py
|
||||
|
||||
@@ -150,7 +150,6 @@ class WeixinOfficialAccountPlatformEvent(AstrMessageEvent):
|
||||
return
|
||||
logger.info(f"微信公众平台上传语音返回: {response}")
|
||||
|
||||
|
||||
if active_send_mode:
|
||||
self.client.message.send_voice(
|
||||
message_obj.sender.user_id,
|
||||
|
||||
@@ -297,6 +297,7 @@ class LLMResponse:
|
||||
)
|
||||
return ret
|
||||
|
||||
|
||||
@dataclass
|
||||
class RerankResult:
|
||||
index: int
|
||||
|
||||
@@ -366,7 +366,10 @@ class ProviderManager:
|
||||
if not self.curr_provider_inst:
|
||||
self.curr_provider_inst = inst
|
||||
|
||||
elif provider_metadata.provider_type in [ProviderType.EMBEDDING, ProviderType.RERANK]:
|
||||
elif provider_metadata.provider_type in [
|
||||
ProviderType.EMBEDDING,
|
||||
ProviderType.RERANK,
|
||||
]:
|
||||
inst = provider_metadata.cls_type(
|
||||
provider_config, self.provider_settings
|
||||
)
|
||||
@@ -388,6 +391,7 @@ class ProviderManager:
|
||||
|
||||
# 和配置文件保持同步
|
||||
config_ids = [provider["id"] for provider in self.providers_config]
|
||||
logger.debug(f"providers in user's config: {config_ids}")
|
||||
for key in list(self.inst_map.keys()):
|
||||
if key not in config_ids:
|
||||
await self.terminate_provider(key)
|
||||
|
||||
@@ -98,7 +98,7 @@ class ProviderFishAudioTTSAPI(TTSProvider):
|
||||
|
||||
# FishAudio的reference_id通常是32位十六进制字符串
|
||||
# 例如: 626bb6d3f3364c9cbc3aa6a67300a664
|
||||
pattern = r'^[a-fA-F0-9]{32}$'
|
||||
pattern = r"^[a-fA-F0-9]{32}$"
|
||||
return bool(re.match(pattern, reference_id.strip()))
|
||||
|
||||
async def _generate_request(self, text: str) -> dict:
|
||||
|
||||
@@ -99,12 +99,15 @@ class ProviderOpenAIOfficial(Provider):
|
||||
for key in to_del:
|
||||
del payloads[key]
|
||||
|
||||
model = payloads.get("model", "")
|
||||
# 针对 qwen3 非 thinking 模型的特殊处理:非流式调用必须设置 enable_thinking=false
|
||||
if "qwen3" in model.lower() and "thinking" not in model.lower():
|
||||
extra_body["enable_thinking"] = False
|
||||
# 读取并合并 custom_extra_body 配置
|
||||
custom_extra_body = self.provider_config.get("custom_extra_body", {})
|
||||
if isinstance(custom_extra_body, dict):
|
||||
extra_body.update(custom_extra_body)
|
||||
|
||||
model = payloads.get("model", "").lower()
|
||||
|
||||
# 针对 deepseek 模型的特殊处理:deepseek-reasoner调用必须移除 tools ,否则将被切换至 deepseek-chat
|
||||
elif model == "deepseek-reasoner" and "tools" in payloads:
|
||||
if model == "deepseek-reasoner" and "tools" in payloads:
|
||||
del payloads["tools"]
|
||||
|
||||
completion = await self.client.chat.completions.create(
|
||||
@@ -137,6 +140,12 @@ class ProviderOpenAIOfficial(Provider):
|
||||
|
||||
# 不在默认参数中的参数放在 extra_body 中
|
||||
extra_body = {}
|
||||
|
||||
# 读取并合并 custom_extra_body 配置
|
||||
custom_extra_body = self.provider_config.get("custom_extra_body", {})
|
||||
if isinstance(custom_extra_body, dict):
|
||||
extra_body.update(custom_extra_body)
|
||||
|
||||
to_del = []
|
||||
for key in payloads.keys():
|
||||
if key not in self.default_params:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import aiohttp
|
||||
from astrbot import logger
|
||||
from ..provider import RerankProvider
|
||||
from ..register import register_provider_adapter
|
||||
from ..entities import ProviderType, RerankResult
|
||||
@@ -44,6 +45,11 @@ class VLLMRerankProvider(RerankProvider):
|
||||
response_data = await response.json()
|
||||
results = response_data.get("results", [])
|
||||
|
||||
if not results:
|
||||
logger.warning(
|
||||
f"Rerank API 返回了空的列表数据。原始响应: {response_data}"
|
||||
)
|
||||
|
||||
return [
|
||||
RerankResult(
|
||||
index=result["index"],
|
||||
|
||||
@@ -113,8 +113,7 @@ class CommandGroupFilter(HandlerFilter):
|
||||
+ self.print_cmd_tree(self.sub_command_filters, event=event, cfg=cfg)
|
||||
)
|
||||
raise ValueError(
|
||||
f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n"
|
||||
+ tree
|
||||
f"参数不足。{self.group_name} 指令组下有如下指令,请参考:\n" + tree
|
||||
)
|
||||
|
||||
# complete_command_names = [name + " " for name in complete_command_names]
|
||||
|
||||
@@ -8,6 +8,7 @@ from .star_handler import (
|
||||
register_permission_type,
|
||||
register_custom_filter,
|
||||
register_on_astrbot_loaded,
|
||||
register_on_platform_loaded,
|
||||
register_on_llm_request,
|
||||
register_on_llm_response,
|
||||
register_llm_tool,
|
||||
@@ -26,6 +27,7 @@ __all__ = [
|
||||
"register_permission_type",
|
||||
"register_custom_filter",
|
||||
"register_on_astrbot_loaded",
|
||||
"register_on_platform_loaded",
|
||||
"register_on_llm_request",
|
||||
"register_on_llm_response",
|
||||
"register_llm_tool",
|
||||
|
||||
@@ -267,6 +267,18 @@ def register_on_astrbot_loaded(**kwargs):
|
||||
return decorator
|
||||
|
||||
|
||||
def register_on_platform_loaded(**kwargs):
|
||||
"""
|
||||
当平台加载完成时
|
||||
"""
|
||||
|
||||
def decorator(awaitable):
|
||||
_ = get_handler_or_create(awaitable, EventType.OnPlatformLoadedEvent, **kwargs)
|
||||
return awaitable
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def register_on_llm_request(**kwargs):
|
||||
"""当有 LLM 请求时的事件
|
||||
|
||||
@@ -376,9 +388,11 @@ def register_llm_tool(name: str = None, **kwargs):
|
||||
# print(f"Registering tool {llm_tool_name} for agent", registering_agent._agent.name)
|
||||
if registering_agent._agent.tools is None:
|
||||
registering_agent._agent.tools = []
|
||||
registering_agent._agent.tools.append(llm_tools.spec_to_func(
|
||||
llm_tool_name, args, docstring.description.strip(), awaitable
|
||||
))
|
||||
registering_agent._agent.tools.append(
|
||||
llm_tools.spec_to_func(
|
||||
llm_tool_name, args, docstring.description.strip(), awaitable
|
||||
)
|
||||
)
|
||||
|
||||
return awaitable
|
||||
|
||||
@@ -421,7 +435,7 @@ def register_agent(
|
||||
run_hooks=run_hooks or BaseAgentRunHooks[AstrAgentContext](),
|
||||
)
|
||||
handoff_tool = HandoffTool(agent=agent)
|
||||
handoff_tool.handler=awaitable
|
||||
handoff_tool.handler = awaitable
|
||||
llm_tools.func_list.append(handoff_tool)
|
||||
return RegisteringAgent(agent)
|
||||
|
||||
|
||||
@@ -84,7 +84,10 @@ class SessionPluginManager:
|
||||
session_config["disabled_plugins"] = disabled_plugins
|
||||
session_plugin_config[session_id] = session_config
|
||||
sp.put(
|
||||
"session_plugin_config", session_plugin_config, scope="umo", scope_id=session_id
|
||||
"session_plugin_config",
|
||||
session_plugin_config,
|
||||
scope="umo",
|
||||
scope_id=session_id,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
|
||||
@@ -34,19 +34,26 @@ class StarHandlerRegistry(Generic[T]):
|
||||
) -> List[StarHandlerMetadata]:
|
||||
handlers = []
|
||||
for handler in self._handlers:
|
||||
# 过滤事件类型
|
||||
if handler.event_type != event_type:
|
||||
continue
|
||||
# 过滤启用状态
|
||||
if only_activated:
|
||||
plugin = star_map.get(handler.handler_module_path)
|
||||
if not (plugin and plugin.activated):
|
||||
continue
|
||||
# 过滤插件白名单
|
||||
if plugins_name is not None and plugins_name != ["*"]:
|
||||
plugin = star_map.get(handler.handler_module_path)
|
||||
if not plugin:
|
||||
continue
|
||||
if (
|
||||
plugin.name not in plugins_name
|
||||
and event_type != EventType.OnAstrBotLoadedEvent
|
||||
and event_type
|
||||
not in (
|
||||
EventType.OnAstrBotLoadedEvent,
|
||||
EventType.OnPlatformLoadedEvent,
|
||||
)
|
||||
and not plugin.reserved
|
||||
):
|
||||
continue
|
||||
@@ -90,6 +97,7 @@ class EventType(enum.Enum):
|
||||
"""
|
||||
|
||||
OnAstrBotLoadedEvent = enum.auto() # AstrBot 加载完成
|
||||
OnPlatformLoadedEvent = enum.auto() # 平台加载完成
|
||||
|
||||
AdapterMessageEvent = enum.auto() # 收到适配器发来的消息
|
||||
OnLLMRequestEvent = enum.auto() # 收到 LLM 请求(可以是用户也可以是插件)
|
||||
|
||||
@@ -791,11 +791,11 @@ class PluginManager:
|
||||
if star_metadata.star_cls is None:
|
||||
return
|
||||
|
||||
if '__del__' in star_metadata.star_cls_type.__dict__:
|
||||
if "__del__" in star_metadata.star_cls_type.__dict__:
|
||||
asyncio.get_event_loop().run_in_executor(
|
||||
None, star_metadata.star_cls.__del__
|
||||
)
|
||||
elif 'terminate' in star_metadata.star_cls_type.__dict__:
|
||||
elif "terminate" in star_metadata.star_cls_type.__dict__:
|
||||
await star_metadata.star_cls.terminate()
|
||||
|
||||
async def turn_on_plugin(self, plugin_name: str):
|
||||
|
||||
@@ -30,8 +30,13 @@ from astrbot.core.platform.astr_message_event import MessageSesion
|
||||
from astrbot.core.star.context import Context
|
||||
from astrbot.core.star.star import star_map
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import AiocqhttpMessageEvent
|
||||
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_platform_adapter import AiocqhttpAdapter
|
||||
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import (
|
||||
AiocqhttpMessageEvent,
|
||||
)
|
||||
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_platform_adapter import (
|
||||
AiocqhttpAdapter,
|
||||
)
|
||||
|
||||
|
||||
class StarTools:
|
||||
"""
|
||||
@@ -77,7 +82,11 @@ class StarTools:
|
||||
|
||||
@classmethod
|
||||
async def send_message_by_id(
|
||||
cls, type: str, id: str, message_chain: MessageChain, platform: str = "aiocqhttp"
|
||||
cls,
|
||||
type: str,
|
||||
id: str,
|
||||
message_chain: MessageChain,
|
||||
platform: str = "aiocqhttp",
|
||||
):
|
||||
"""
|
||||
根据 id(例如qq号, 群号等) 直接, 主动地发送消息
|
||||
@@ -92,7 +101,9 @@ class StarTools:
|
||||
raise ValueError("StarTools not initialized")
|
||||
platforms = cls._context.platform_manager.get_insts()
|
||||
if platform == "aiocqhttp":
|
||||
adapter = next((p for p in platforms if isinstance(p, AiocqhttpAdapter)), None)
|
||||
adapter = next(
|
||||
(p for p in platforms if isinstance(p, AiocqhttpAdapter)), None
|
||||
)
|
||||
if adapter is None:
|
||||
raise ValueError("未找到适配器: AiocqhttpAdapter")
|
||||
await AiocqhttpMessageEvent.send_message(
|
||||
@@ -115,7 +126,7 @@ class StarTools:
|
||||
message_str: str,
|
||||
message_id: str = "",
|
||||
raw_message: object = None,
|
||||
group_id: str = ""
|
||||
group_id: str = "",
|
||||
) -> AstrBotMessage:
|
||||
"""
|
||||
创建一个AstrBot消息对象
|
||||
@@ -152,7 +163,6 @@ class StarTools:
|
||||
@classmethod
|
||||
async def create_event(
|
||||
cls, abm: AstrBotMessage, platform: str = "aiocqhttp", is_wake: bool = True
|
||||
|
||||
) -> None:
|
||||
"""
|
||||
创建并提交事件到指定平台
|
||||
@@ -167,7 +177,9 @@ class StarTools:
|
||||
raise ValueError("StarTools not initialized")
|
||||
platforms = cls._context.platform_manager.get_insts()
|
||||
if platform == "aiocqhttp":
|
||||
adapter = next((p for p in platforms if isinstance(p, AiocqhttpAdapter)), None)
|
||||
adapter = next(
|
||||
(p for p in platforms if isinstance(p, AiocqhttpAdapter)), None
|
||||
)
|
||||
if adapter is None:
|
||||
raise ValueError("未找到适配器: AiocqhttpAdapter")
|
||||
event = AiocqhttpMessageEvent(
|
||||
@@ -277,7 +289,9 @@ class StarTools:
|
||||
if not plugin_name:
|
||||
raise ValueError("无法获取插件名称")
|
||||
|
||||
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:
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.css" integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/path/to/styles/default.min.css">
|
||||
<script src="/path/to/highlight.min.js"></script>
|
||||
<script>hljs.highlightAll();</script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/katex.min.js" integrity="sha384-hIoBPJpTUs74ddyc4bFZSM1TVlQDA60VBbJS0oA934VSz82sBx1X7kSx2ATBDIyd" crossorigin="anonymous"></script>
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.10/dist/contrib/auto-render.min.js" integrity="sha384-43gviWU0YVjaDtb/GhzOouOXtZMP/7XUzwPTstBeZFe/+rCMvRwr4yROQP43s0Xk" crossorigin="anonymous"
|
||||
onload="renderMathInElement(document.getElementById('content'),{delimiters: [{left: '$$', right: '$$', display: true},{left: '$', right: '$', display: false}]});"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div style="background-color: #3276dc; color: #fff; font-size: 64px; ">
|
||||
<span style="font-weight: bold; margin-left: 16px"># AstrBot</span>
|
||||
<span>{{ version }}</span>
|
||||
</div>
|
||||
<article style="margin-top: 32px" id="content"></article>
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script>
|
||||
document.getElementById('content').innerHTML = marked.parse(`{{ text | safe}}`);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<style>
|
||||
#content {
|
||||
min-width: 200px;
|
||||
max-width: 85%;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1em 1em;
|
||||
}
|
||||
|
||||
body {
|
||||
word-break: break-word;
|
||||
line-height: 1.75;
|
||||
font-weight: 400;
|
||||
font-size: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
color: #333;
|
||||
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1.5;
|
||||
margin-top: 35px;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child, h6:first-child {
|
||||
margin-top: -1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h1::before, h2::before, h3::before, h4::before, h5::before, h6::before {
|
||||
content: "#";
|
||||
display: inline-block;
|
||||
color: #3eaf7c;
|
||||
padding-right: 0.23em;
|
||||
}
|
||||
h1 {
|
||||
position: relative;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
h1::before {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
h2 {
|
||||
padding-bottom: 0.5rem;
|
||||
font-size: 2.2rem;
|
||||
border-bottom: 1px solid #ececec;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
h4 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
h6 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
p {
|
||||
line-height: inherit;
|
||||
margin-top: 22px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
strong {
|
||||
color: #3eaf7c;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
margin: auto;
|
||||
border: 3px solid rgba(62, 175, 124, 0.2);
|
||||
}
|
||||
hr {
|
||||
border-top: 1px solid #3eaf7c;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
margin-top: 32px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
code {
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
word-break: break-word;
|
||||
overflow-x: auto;
|
||||
padding: 0.2rem 0.5rem;
|
||||
margin: 0;
|
||||
color: #3eaf7c;
|
||||
font-size: 0.85em;
|
||||
background-color: rgba(27, 31, 35, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
pre {
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
line-height: 1.75;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #3eaf7c;
|
||||
}
|
||||
pre > code {
|
||||
font-size: 12px;
|
||||
padding: 15px 12px;
|
||||
margin: 0;
|
||||
word-break: normal;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
color: #333;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
a {
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
color: #3eaf7c;
|
||||
}
|
||||
a:hover, a:active {
|
||||
border-bottom: 1.5px solid #3eaf7c;
|
||||
}
|
||||
a:before {
|
||||
content: "⇲";
|
||||
}
|
||||
table {
|
||||
display: inline-block !important;
|
||||
font-size: 12px;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
overflow: auto;
|
||||
border: solid 1px #3eaf7c;
|
||||
}
|
||||
thead {
|
||||
background: #3eaf7c;
|
||||
color: #fff;
|
||||
text-align: left;
|
||||
}
|
||||
tr:nth-child(2n) {
|
||||
background-color: rgba(62, 175, 124, 0.2);
|
||||
}
|
||||
th, td {
|
||||
padding: 12px 7px;
|
||||
line-height: 24px;
|
||||
}
|
||||
td {
|
||||
min-width: 120px;
|
||||
}
|
||||
blockquote {
|
||||
color: #666;
|
||||
padding: 1px 23px;
|
||||
margin: 22px 0;
|
||||
border-left: 0.5rem solid rgba(62, 175, 124, 0.6);
|
||||
border-color: #42b983;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
blockquote::after {
|
||||
display: block;
|
||||
content: "";
|
||||
}
|
||||
blockquote > p {
|
||||
margin: 10px 0;
|
||||
}
|
||||
details {
|
||||
border: none;
|
||||
outline: none;
|
||||
border-left: 4px solid #3eaf7c;
|
||||
padding-left: 10px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
details summary {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: white;
|
||||
margin: 0px -17px;
|
||||
}
|
||||
details summary::-webkit-details-marker {
|
||||
color: #3eaf7c;
|
||||
}
|
||||
ol, ul {
|
||||
padding-left: 28px;
|
||||
}
|
||||
ol li, ul li {
|
||||
margin-bottom: 0;
|
||||
list-style: inherit;
|
||||
}
|
||||
ol li .task-list-item, ul li .task-list-item {
|
||||
list-style: none;
|
||||
}
|
||||
ol li .task-list-item ul, ul li .task-list-item ul, ol li .task-list-item ol, ul li .task-list-item ol {
|
||||
margin-top: 0;
|
||||
}
|
||||
ol ul, ul ul, ol ol, ul ol {
|
||||
margin-top: 3px;
|
||||
}
|
||||
ol li {
|
||||
padding-left: 6px;
|
||||
}
|
||||
ol li::marker {
|
||||
color: #3eaf7c;
|
||||
}
|
||||
ul li {
|
||||
list-style: none;
|
||||
}
|
||||
ul li:before {
|
||||
content: "•";
|
||||
margin-right: 4px;
|
||||
color: #3eaf7c;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -2,94 +2,111 @@
|
||||
|
||||
import os
|
||||
import shutil
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_path
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path, get_astrbot_path
|
||||
|
||||
|
||||
class TemplateManager:
|
||||
"""
|
||||
负责管理 t2i HTML 模板的 CRUD 和重置操作。
|
||||
采用“用户覆盖内置”策略:用户模板存储在 data 目录中,并优先于内置模板加载。
|
||||
所有创建、更新、删除操作仅影响用户目录,以确保更新框架时用户数据安全。
|
||||
"""
|
||||
|
||||
CORE_TEMPLATES = ["base.html", "astrbot_powershell.html"]
|
||||
|
||||
def __init__(self):
|
||||
# 修正路径拼接,加入缺失的 'astrbot' 目录
|
||||
self.template_dir = os.path.join(
|
||||
self.builtin_template_dir = os.path.join(
|
||||
get_astrbot_path(), "astrbot", "core", "utils", "t2i", "template"
|
||||
)
|
||||
self.backup_template_path = os.path.join(
|
||||
self.template_dir, "default_template.html.bak"
|
||||
)
|
||||
# 确保模板目录存在
|
||||
os.makedirs(self.template_dir, exist_ok=True)
|
||||
self.user_template_dir = os.path.join(get_astrbot_data_path(), "t2i_templates")
|
||||
|
||||
# 检查模板目录中是否有 .html 文件
|
||||
html_files = [f for f in os.listdir(self.template_dir) if f.endswith(".html")]
|
||||
if not html_files and os.path.exists(self.backup_template_path):
|
||||
self.reset_default_template()
|
||||
os.makedirs(self.user_template_dir, exist_ok=True)
|
||||
self._initialize_user_templates()
|
||||
|
||||
def _get_template_path(self, name: str) -> str:
|
||||
"""获取模板的完整路径,防止路径遍历漏洞。"""
|
||||
def _copy_core_templates(self, overwrite: bool = False):
|
||||
"""从内置目录复制核心模板到用户目录。"""
|
||||
for filename in self.CORE_TEMPLATES:
|
||||
src = os.path.join(self.builtin_template_dir, filename)
|
||||
dst = os.path.join(self.user_template_dir, filename)
|
||||
if os.path.exists(src) and (overwrite or not os.path.exists(dst)):
|
||||
shutil.copyfile(src, dst)
|
||||
|
||||
def _initialize_user_templates(self):
|
||||
"""如果用户目录下缺少核心模板,则进行复制。"""
|
||||
self._copy_core_templates(overwrite=False)
|
||||
|
||||
def _get_user_template_path(self, name: str) -> str:
|
||||
"""获取用户模板的完整路径,防止路径遍历漏洞。"""
|
||||
if ".." in name or "/" in name or "\\" in name:
|
||||
raise ValueError("模板名称包含非法字符。")
|
||||
return os.path.join(self.template_dir, f"{name}.html")
|
||||
return os.path.join(self.user_template_dir, f"{name}.html")
|
||||
|
||||
def list_templates(self) -> list[dict]:
|
||||
"""列出所有可用的模板。"""
|
||||
templates = []
|
||||
for filename in os.listdir(self.template_dir):
|
||||
if filename.endswith(".html"):
|
||||
templates.append(
|
||||
{
|
||||
"name": os.path.splitext(filename)[0],
|
||||
"is_default": filename == "base.html",
|
||||
}
|
||||
)
|
||||
return templates
|
||||
|
||||
def get_template(self, name: str) -> str:
|
||||
"""获取指定模板的内容。"""
|
||||
path = self._get_template_path(name)
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError("模板不存在。")
|
||||
def _read_file(self, path: str) -> str:
|
||||
"""读取文件内容。"""
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def list_templates(self) -> list[dict]:
|
||||
"""
|
||||
列出所有可用模板。
|
||||
该列表是内置模板和用户模板的合并视图,用户模板将覆盖同名的内置模板。
|
||||
"""
|
||||
dirs_to_scan = [self.builtin_template_dir, self.user_template_dir]
|
||||
all_names = {
|
||||
os.path.splitext(f)[0]
|
||||
for d in dirs_to_scan
|
||||
for f in os.listdir(d)
|
||||
if f.endswith(".html")
|
||||
}
|
||||
return [
|
||||
{"name": name, "is_default": name == "base"} for name in sorted(all_names)
|
||||
]
|
||||
|
||||
def get_template(self, name: str) -> str:
|
||||
"""
|
||||
获取指定模板的内容。
|
||||
优先从用户目录加载,如果不存在则回退到内置目录。
|
||||
"""
|
||||
user_path = self._get_user_template_path(name)
|
||||
if os.path.exists(user_path):
|
||||
return self._read_file(user_path)
|
||||
|
||||
builtin_path = os.path.join(self.builtin_template_dir, f"{name}.html")
|
||||
if os.path.exists(builtin_path):
|
||||
return self._read_file(builtin_path)
|
||||
|
||||
raise FileNotFoundError("模板不存在。")
|
||||
|
||||
def create_template(self, name: str, content: str):
|
||||
"""创建一个新的模板文件。"""
|
||||
path = self._get_template_path(name)
|
||||
"""在用户目录中创建一个新的模板文件。"""
|
||||
path = self._get_user_template_path(name)
|
||||
if os.path.exists(path):
|
||||
raise FileExistsError("同名模板已存在。")
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
def update_template(self, name: str, content: str):
|
||||
"""更新一个已存在的模板文件。"""
|
||||
path = self._get_template_path(name)
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError("模板不存在。")
|
||||
"""
|
||||
更新一个模板。此操作始终写入用户目录。
|
||||
如果更新的是一个内置模板,此操作实际上会在用户目录中创建一个修改后的副本,
|
||||
从而实现对内置模板的“覆盖”。
|
||||
"""
|
||||
path = self._get_user_template_path(name)
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
def delete_template(self, name: str):
|
||||
"""删除一个模板文件。"""
|
||||
if name == "base":
|
||||
raise ValueError("不能删除默认的 base 模板。")
|
||||
path = self._get_template_path(name)
|
||||
"""
|
||||
仅删除用户目录中的模板文件。
|
||||
如果删除的是一个覆盖了内置模板的用户模板,这将有效地“恢复”到内置版本。
|
||||
"""
|
||||
path = self._get_user_template_path(name)
|
||||
if not os.path.exists(path):
|
||||
raise FileNotFoundError("模板不存在。")
|
||||
raise FileNotFoundError("用户模板不存在,无法删除。")
|
||||
os.remove(path)
|
||||
|
||||
def backup_default_template_if_not_exist(self):
|
||||
"""如果备份不存在,则创建默认模板的备份。"""
|
||||
default_path = os.path.join(self.template_dir, "base.html")
|
||||
if not os.path.exists(self.backup_template_path) and os.path.exists(
|
||||
default_path
|
||||
):
|
||||
shutil.copyfile(default_path, self.backup_template_path)
|
||||
|
||||
def reset_default_template(self):
|
||||
"""重置默认模板。"""
|
||||
if not os.path.exists(self.backup_template_path):
|
||||
raise FileNotFoundError("默认模板的备份文件不存在,无法重置。")
|
||||
|
||||
default_path = os.path.join(self.template_dir, "base.html")
|
||||
shutil.copyfile(self.backup_template_path, default_path)
|
||||
"""
|
||||
将核心模板从内置目录强制重置到用户目录。
|
||||
"""
|
||||
self._copy_core_templates(overwrite=True)
|
||||
|
||||
@@ -157,7 +157,11 @@ class ChatRoute(Route):
|
||||
|
||||
if type == "end":
|
||||
break
|
||||
elif (streaming and type == "complete") or not streaming:
|
||||
elif (
|
||||
(streaming and type == "complete")
|
||||
or not streaming
|
||||
or type == "break"
|
||||
):
|
||||
# append bot message
|
||||
new_his = {"type": "bot", "message": result_text}
|
||||
await self.platform_history_mgr.insert(
|
||||
@@ -197,6 +201,7 @@ class ChatRoute(Route):
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
)
|
||||
response.timeout = None # fix SSE auto disconnect issue
|
||||
return response
|
||||
|
||||
async def _get_webchat_conv_id_from_conv_id(self, conversation_id: str) -> str:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import typing
|
||||
import traceback
|
||||
import os
|
||||
import copy
|
||||
from .route import Route, Response, RouteContext
|
||||
from astrbot.core.provider.entities import ProviderType
|
||||
from quart import request
|
||||
|
||||
@@ -10,7 +10,9 @@ class LogRoute(Route):
|
||||
super().__init__(context)
|
||||
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/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 stream():
|
||||
@@ -48,9 +50,15 @@ class LogRoute(Route):
|
||||
"""获取日志历史"""
|
||||
try:
|
||||
logs = list(self.log_broker.log_cache)
|
||||
return Response().ok(data={
|
||||
"logs": logs,
|
||||
}).__dict__
|
||||
return (
|
||||
Response()
|
||||
.ok(
|
||||
data={
|
||||
"logs": logs,
|
||||
}
|
||||
)
|
||||
.__dict__
|
||||
)
|
||||
except BaseException as e:
|
||||
logger.error(f"获取日志历史失败: {e}")
|
||||
return Response().error(f"获取日志历史失败: {e}").__dict__
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from astrbot.core import logger
|
||||
from astrbot.core.config.astrbot_config import AstrBotConfig
|
||||
from dataclasses import dataclass
|
||||
from quart import Quart
|
||||
|
||||
@@ -32,10 +32,6 @@ class T2iRoute(Route):
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
# 应用启动时,确保备份存在
|
||||
self.manager.backup_default_template_if_not_exist()
|
||||
|
||||
self.register_routes()
|
||||
|
||||
async def list_templates(self):
|
||||
@@ -89,6 +85,7 @@ class T2iRoute(Route):
|
||||
)
|
||||
response.status_code = 400
|
||||
return response
|
||||
name = name.strip()
|
||||
|
||||
self.manager.create_template(name, content)
|
||||
response = jsonify(
|
||||
@@ -118,6 +115,7 @@ class T2iRoute(Route):
|
||||
async def update_template(self, name: str):
|
||||
"""更新一个已存在的T2I模板"""
|
||||
try:
|
||||
name = name.strip()
|
||||
data = await request.json
|
||||
content = data.get("content")
|
||||
if content is None:
|
||||
@@ -126,17 +124,16 @@ class T2iRoute(Route):
|
||||
return response
|
||||
|
||||
self.manager.update_template(name, content)
|
||||
return jsonify(
|
||||
asdict(
|
||||
Response().ok(
|
||||
data={"name": name}, message="Template updated successfully."
|
||||
)
|
||||
)
|
||||
)
|
||||
except FileNotFoundError:
|
||||
response = jsonify(asdict(Response().error("Template not found.")))
|
||||
response.status_code = 404
|
||||
return response
|
||||
|
||||
# 检查更新的是否为当前激活的模板,如果是,则热重载
|
||||
active_template = self.config.get("t2i_active_template", "base")
|
||||
if name == active_template:
|
||||
await self.core_lifecycle.reload_pipeline_scheduler("default")
|
||||
message = f"模板 '{name}' 已更新并重新加载。"
|
||||
else:
|
||||
message = f"模板 '{name}' 已更新。"
|
||||
|
||||
return jsonify(asdict(Response().ok(data={"name": name}, message=message)))
|
||||
except ValueError as e:
|
||||
response = jsonify(asdict(Response().error(str(e))))
|
||||
response.status_code = 400
|
||||
@@ -149,6 +146,7 @@ class T2iRoute(Route):
|
||||
async def delete_template(self, name: str):
|
||||
"""删除一个T2I模板"""
|
||||
try:
|
||||
name = name.strip()
|
||||
self.manager.delete_template(name)
|
||||
return jsonify(
|
||||
asdict(Response().ok(message="Template deleted successfully."))
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import traceback
|
||||
|
||||
import aiohttp
|
||||
from quart import request
|
||||
|
||||
from astrbot.core import logger
|
||||
|
||||
@@ -29,10 +29,19 @@ class AstrBotDashboard:
|
||||
core_lifecycle: AstrBotCoreLifecycle,
|
||||
db: BaseDatabase,
|
||||
shutdown_event: asyncio.Event,
|
||||
webui_dir: str | None = None,
|
||||
) -> None:
|
||||
self.core_lifecycle = core_lifecycle
|
||||
self.config = core_lifecycle.astrbot_config
|
||||
self.data_path = os.path.abspath(os.path.join(get_astrbot_data_path(), "dist"))
|
||||
|
||||
# 参数指定webui目录
|
||||
if webui_dir and os.path.exists(webui_dir):
|
||||
self.data_path = os.path.abspath(webui_dir)
|
||||
else:
|
||||
self.data_path = os.path.abspath(
|
||||
os.path.join(get_astrbot_data_path(), "dist")
|
||||
)
|
||||
|
||||
self.app = Quart("dashboard", static_folder=self.data_path, static_url_path="/")
|
||||
APP = self.app # noqa
|
||||
self.app.config["MAX_CONTENT_LENGTH"] = (
|
||||
|
||||
15
changelogs/v4.1.0.md
Normal file
15
changelogs/v4.1.0.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# What's Changed
|
||||
|
||||
> 如果已经使用自定义文转图模板,此次升级之后将会被覆盖,请提前备份。路径在 `astrbot/core/utils/t2i/template` 目录下。
|
||||
|
||||
0. ‼️‼️‼️ 修复 LLM 仍会调用已禁用的工具的问题 ([#2729](https://github.com/Soulter/AstrBot/issues/2729))
|
||||
1. ‼️ 修复 WebChat 下,Agent 长时任务时,SSE 连接自动断开的问题
|
||||
2. ‼️ 修复自定义文转图模板更新版本后会被覆盖的问题 ([#2677](https://github.com/Soulter/AstrBot/issues/2677))
|
||||
3. 修复 Satori 适配器教程链接 ([#2668](https://github.com/Soulter/AstrBot/issues/2668))
|
||||
4. 修复插件页表格视图中,点击状态字段表头排序不起作用的问题 ([#2714](https://github.com/Soulter/AstrBot/issues/2714))
|
||||
5. 修复工具调用时的 content 内容在重新加载后没有显示在 webchat 的问题 ([#2727](https://github.com/Soulter/AstrBot/issues/2727))
|
||||
6. 允许添加多个 tavily API Key 进行轮询 ([#2725](https://github.com/Soulter/AstrBot/issues/2725))
|
||||
7. 添加 --webui-dir 启动参数以支持指定 WebUI 构建文件目录 ([#2680](https://github.com/Soulter/AstrBot/issues/2680))
|
||||
8. 兼容指令名和第一个参数之间没有空格的情况 ([#2650](https://github.com/Soulter/AstrBot/issues/2650))
|
||||
9. 支持在 WebUI 自定义 OpenAI API extra_body 参数 ([#2719](https://github.com/Soulter/AstrBot/issues/2719))
|
||||
10. 增加 on_platform_loaded 钩子以在消息平台适配器实例化完成后触发 ([#2651](https://github.com/Soulter/AstrBot/issues/2651))
|
||||
5
changelogs/v4.1.1.md
Normal file
5
changelogs/v4.1.1.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# What's Changed
|
||||
|
||||
修复了 v4.1.0 `model referenced before assignment` 的错误。
|
||||
|
||||
> 如果已经使用自定义文转图模板,此次升级之后将会被覆盖,请提前备份。路径在 `astrbot/core/utils/t2i/template` 目录下。
|
||||
9
changelogs/v4.1.2.md
Normal file
9
changelogs/v4.1.2.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# What's Changed
|
||||
|
||||
0. ‼️‼️‼️ fix: 修复 4.1.1 版本下,指令调用异常的问题
|
||||
1. ‼️‼️ fix: 修复多配置文件配置的不同人格无法生效的问题 ([#2739](https://github.com/AstrBotDevs/AstrBot/issues/2739))
|
||||
2. ‼️‼️ fix: 修复人格所选择的工具无法应用的问题 ([#2739](https://github.com/AstrBotDevs/AstrBot/issues/2739))
|
||||
3. ‼️‼️ fix: 修复平台配置下的「内容安全」组无法生效 ([#2751](https://github.com/AstrBotDevs/AstrBot/issues/2751))
|
||||
4. perf: 检查服务提供商可用性时跳过未启用的提供商,解决部分 `provider with id xxx not found` 的问题
|
||||
|
||||
fixes: [#2724](https://github.com/AstrBotDevs/AstrBot/issues/2724)
|
||||
@@ -2,6 +2,7 @@
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { ref, computed } from 'vue'
|
||||
import ListConfigItem from './ListConfigItem.vue'
|
||||
import ObjectEditor from './ObjectEditor.vue'
|
||||
import ProviderSelector from './ProviderSelector.vue'
|
||||
import PersonaSelector from './PersonaSelector.vue'
|
||||
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue'
|
||||
@@ -80,7 +81,7 @@ function shouldShowItem(itemMeta, itemKey) {
|
||||
|
||||
function hasVisibleItemsAfter(items, currentIndex) {
|
||||
const itemEntries = Object.entries(items)
|
||||
|
||||
|
||||
// 检查当前索引之后是否还有可见的配置项
|
||||
for (let i = currentIndex + 1; i < itemEntries.length; i++) {
|
||||
const [itemKey, itemValue] = itemEntries[i]
|
||||
@@ -89,7 +90,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
@@ -130,7 +131,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Regular Property -->
|
||||
<template v-else>
|
||||
<v-row v-if="!metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)" class="config-row">
|
||||
@@ -145,7 +146,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-list-item-title>
|
||||
|
||||
<v-list-item-subtitle class="property-hint">
|
||||
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint"
|
||||
<span v-if="metadata[metadataKey].items[key]?.obvious_hint && metadata[metadataKey].items[key]?.hint"
|
||||
class="important-hint">‼️</span>
|
||||
{{ metadata[metadataKey].items[key]?.hint }}
|
||||
</v-list-item-subtitle>
|
||||
@@ -153,10 +154,10 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="1" class="d-flex align-center type-indicator">
|
||||
<v-chip v-if="!metadata[metadataKey].items[key]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
size="x-small"
|
||||
<v-chip v-if="!metadata[metadataKey].items[key]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
size="x-small"
|
||||
variant="flat">
|
||||
{{ metadata[metadataKey].items[key]?.type || 'string' }}
|
||||
</v-chip>
|
||||
@@ -166,35 +167,35 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<div v-if="metadata[metadataKey].items[key]" class="w-100">
|
||||
<!-- Special handling for specific metadata types -->
|
||||
<div v-if="metadata[metadataKey].items[key]?._special === 'select_provider'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="iterable[key]"
|
||||
:provider-type="'chat_completion'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_provider_stt'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="iterable[key]"
|
||||
:provider-type="'speech_to_text'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_provider_tts'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="iterable[key]"
|
||||
:provider-type="'text_to_speech'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_persona'">
|
||||
<PersonaSelector
|
||||
<PersonaSelector
|
||||
v-model="iterable[key]"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="metadata[metadataKey].items[key]?._special === 'select_knowledgebase'">
|
||||
<KnowledgeBaseSelector
|
||||
<KnowledgeBaseSelector
|
||||
v-model="iterable[key]"
|
||||
/>
|
||||
</div>
|
||||
<!-- List item with options-->
|
||||
<div v-else-if="metadata[metadataKey].items[key]?.type === 'list' && metadata[metadataKey].items[key]?.options && !metadata[metadataKey].items[key]?.invisible && metadata[metadataKey].items[key]?.render_type === 'checkbox'"
|
||||
<div v-else-if="metadata[metadataKey].items[key]?.type === 'list' && metadata[metadataKey].items[key]?.options && !metadata[metadataKey].items[key]?.invisible && metadata[metadataKey].items[key]?.render_type === 'checkbox'"
|
||||
class="d-flex flex-wrap gap-20">
|
||||
<v-checkbox
|
||||
v-for="(option, index) in metadata[metadataKey].items[key]?.options"
|
||||
@@ -233,10 +234,10 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
|
||||
<!-- Code Editor with Full Screen Option -->
|
||||
<div v-else-if="metadata[metadataKey].items[key]?.editor_mode && !metadata[metadataKey].items[key]?.invisible" class="editor-container">
|
||||
<VueMonacoEditor
|
||||
:theme="metadata[metadataKey].items[key]?.editor_theme || 'vs-light'"
|
||||
<VueMonacoEditor
|
||||
:theme="metadata[metadataKey].items[key]?.editor_theme || 'vs-light'"
|
||||
:language="metadata[metadataKey].items[key]?.editor_language || 'json'"
|
||||
style="min-height: 100px; flex-grow: 1; border: 1px solid rgba(0, 0, 0, 0.1);"
|
||||
style="min-height: 100px; flex-grow: 1; border: 1px solid rgba(0, 0, 0, 0.1);"
|
||||
v-model:value="iterable[key]"
|
||||
>
|
||||
</VueMonacoEditor>
|
||||
@@ -252,7 +253,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<v-icon>mdi-fullscreen</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- String input -->
|
||||
<v-text-field
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'string' && !metadata[metadataKey].items[key]?.invisible"
|
||||
@@ -262,7 +263,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
|
||||
<!-- Numeric input -->
|
||||
<v-text-field
|
||||
v-else-if="(metadata[metadataKey].items[key]?.type === 'int' || metadata[metadataKey].items[key]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
@@ -273,7 +274,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
|
||||
<!-- Text area -->
|
||||
<v-textarea
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'text' && !metadata[metadataKey].items[key]?.invisible"
|
||||
@@ -283,7 +284,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-textarea>
|
||||
|
||||
|
||||
<!-- Boolean switch -->
|
||||
<v-switch
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'bool' && !metadata[metadataKey].items[key]?.invisible"
|
||||
@@ -293,20 +294,27 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-switch>
|
||||
|
||||
|
||||
<!-- List item -->
|
||||
<ListConfigItem
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'list' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
class="config-field"
|
||||
/>
|
||||
|
||||
<!-- Dict item (key-value editor) -->
|
||||
<ObjectEditor
|
||||
v-else-if="metadata[metadataKey].items[key]?.type === 'dict' && !metadata[metadataKey].items[key]?.invisible"
|
||||
v-model="iterable[key]"
|
||||
class="config-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Fallback for unknown metadata -->
|
||||
<div v-else class="w-100">
|
||||
<v-text-field
|
||||
v-model="iterable[key]"
|
||||
:label="key"
|
||||
<v-text-field
|
||||
v-model="iterable[key]"
|
||||
:label="key"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
class="config-field"
|
||||
@@ -316,14 +324,14 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider
|
||||
<v-divider
|
||||
v-if="hasVisibleItemsAfter(filteredIterable, index) && !metadata[metadataKey].items[key]?.invisible && shouldShowItem(metadata[metadataKey].items[key], key)"
|
||||
class="config-divider"
|
||||
></v-divider>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Simple Value Configuration -->
|
||||
<div v-else class="simple-config">
|
||||
<v-row class="config-row">
|
||||
@@ -342,9 +350,9 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" sm="1" class="d-flex align-center type-indicator">
|
||||
<v-chip v-if="!metadata[metadataKey]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
<v-chip v-if="!metadata[metadataKey]?.invisible"
|
||||
color="primary"
|
||||
label
|
||||
size="x-small"
|
||||
variant="flat">
|
||||
{{ metadata[metadataKey]?.type }}
|
||||
@@ -364,7 +372,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-select>
|
||||
|
||||
|
||||
<!-- String input -->
|
||||
<v-text-field
|
||||
v-else-if="metadata[metadataKey]?.type === 'string' && !metadata[metadataKey]?.invisible"
|
||||
@@ -374,7 +382,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
|
||||
<!-- Numeric input -->
|
||||
<v-text-field
|
||||
v-else-if="(metadata[metadataKey]?.type === 'int' || metadata[metadataKey]?.type === 'float') && !metadata[metadataKey]?.invisible"
|
||||
@@ -385,7 +393,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
type="number"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
|
||||
|
||||
<!-- Text area -->
|
||||
<v-textarea
|
||||
v-else-if="metadata[metadataKey]?.type === 'text' && !metadata[metadataKey]?.invisible"
|
||||
@@ -396,7 +404,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
class="config-field"
|
||||
hide-details
|
||||
></v-textarea>
|
||||
|
||||
|
||||
<!-- Boolean switch -->
|
||||
<v-switch
|
||||
v-else-if="metadata[metadataKey]?.type === 'bool' && !metadata[metadataKey]?.invisible"
|
||||
@@ -406,9 +414,9 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
density="compact"
|
||||
hide-details
|
||||
></v-switch>
|
||||
|
||||
|
||||
<!-- List item -->
|
||||
<ListConfigItem
|
||||
<ListConfigItem
|
||||
v-else-if="metadata[metadataKey]?.type === 'list' && !metadata[metadataKey]?.invisible"
|
||||
v-model="iterable[metadataKey]"
|
||||
class="config-field"
|
||||
@@ -435,9 +443,9 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
</v-toolbar-items>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pa-0">
|
||||
<VueMonacoEditor
|
||||
<VueMonacoEditor
|
||||
:theme="currentEditingTheme"
|
||||
:language="currentEditingLanguage"
|
||||
:language="currentEditingLanguage"
|
||||
style="height: calc(100vh - 64px);"
|
||||
v-model:value="currentEditingKeyIterable[currentEditingKey]"
|
||||
>
|
||||
@@ -567,11 +575,11 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
.nested-object {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
|
||||
.config-row {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
|
||||
.property-info, .type-indicator, .config-input {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { VueMonacoEditor } from '@guolao/vue-monaco-editor'
|
||||
import { ref, computed } from 'vue'
|
||||
import ListConfigItem from './ListConfigItem.vue'
|
||||
import ObjectEditor from './ObjectEditor.vue'
|
||||
import ProviderSelector from './ProviderSelector.vue'
|
||||
import PersonaSelector from './PersonaSelector.vue'
|
||||
import KnowledgeBaseSelector from './KnowledgeBaseSelector.vue'
|
||||
@@ -102,7 +103,7 @@ function shouldShowItem(itemMeta, itemKey) {
|
||||
|
||||
function hasVisibleItemsAfter(items, currentIndex) {
|
||||
const itemEntries = Object.entries(items)
|
||||
|
||||
|
||||
// 检查当前索引之后是否还有可见的配置项
|
||||
for (let i = currentIndex + 1; i < itemEntries.length; i++) {
|
||||
const [itemKey, itemMeta] = itemEntries[i]
|
||||
@@ -110,7 +111,7 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
</script>
|
||||
@@ -188,13 +189,20 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
color="primary" inset density="compact" hide-details style="display: flex; justify-content: end;"></v-switch>
|
||||
|
||||
<!-- List item for JSON selector -->
|
||||
<ListConfigItem
|
||||
<ListConfigItem
|
||||
v-else-if="itemMeta?.type === 'list'"
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
button-text="修改"
|
||||
class="config-field"
|
||||
/>
|
||||
|
||||
<!-- Object editor for JSON selector -->
|
||||
<ObjectEditor
|
||||
v-else-if="itemMeta?.type === 'dict'"
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
class="config-field"
|
||||
/>
|
||||
|
||||
<!-- Fallback for JSON selector -->
|
||||
<v-text-field v-else v-model="createSelectorModel(itemKey).value" density="compact" variant="outlined"
|
||||
class="config-field" hide-details></v-text-field>
|
||||
@@ -202,48 +210,48 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
|
||||
<!-- Special handling for specific metadata types -->
|
||||
<div v-else-if="itemMeta?._special === 'select_provider'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'chat_completion'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_provider_stt'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'speech_to_text'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_provider_tts'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'text_to_speech'"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'provider_pool'">
|
||||
<ProviderSelector
|
||||
<ProviderSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
:provider-type="'chat_completion'"
|
||||
button-text="选择提供商池..."
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_persona'">
|
||||
<PersonaSelector
|
||||
<PersonaSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'persona_pool'">
|
||||
<PersonaSelector
|
||||
<PersonaSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
button-text="选择人格池..."
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_knowledgebase'">
|
||||
<KnowledgeBaseSelector
|
||||
<KnowledgeBaseSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="itemMeta?._special === 'select_plugin_set'">
|
||||
<PluginSetSelector
|
||||
<PluginSetSelector
|
||||
v-model="createSelectorModel(itemKey).value"
|
||||
/>
|
||||
</div>
|
||||
@@ -261,12 +269,12 @@ function hasVisibleItemsAfter(items, currentIndex) {
|
||||
<small class="text-grey">已选择的插件:</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap ga-2 mt-2">
|
||||
<v-chip
|
||||
v-for="plugin in (createSelectorModel(itemKey).value || [])"
|
||||
:key="plugin"
|
||||
size="small"
|
||||
label
|
||||
color="primary"
|
||||
<v-chip
|
||||
v-for="plugin in (createSelectorModel(itemKey).value || [])"
|
||||
:key="plugin"
|
||||
size="small"
|
||||
label
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
>
|
||||
{{ plugin === '*' ? '所有插件' : plugin }}
|
||||
|
||||
282
dashboard/src/components/shared/ObjectEditor.vue
Normal file
282
dashboard/src/components/shared/ObjectEditor.vue
Normal file
@@ -0,0 +1,282 @@
|
||||
<template>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div>
|
||||
<span v-if="!modelValue || Object.keys(modelValue).length === 0" style="color: rgb(var(--v-theme-primaryText));">
|
||||
暂无项目
|
||||
</span>
|
||||
<div v-else class="d-flex flex-wrap ga-2">
|
||||
<v-chip v-for="key in displayKeys" :key="key" size="x-small" label color="primary">
|
||||
{{ key.length > 20 ? key.slice(0, 20) + '...' : key }}
|
||||
</v-chip>
|
||||
<v-chip v-if="Object.keys(modelValue).length > maxDisplayItems" size="x-small" label color="grey-lighten-1">
|
||||
+{{ Object.keys(modelValue).length - maxDisplayItems }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<v-btn size="small" color="primary" variant="tonal" @click="openDialog">
|
||||
{{ buttonText }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Key-Value Management Dialog -->
|
||||
<v-dialog v-model="dialog" max-width="600px">
|
||||
<v-card>
|
||||
<v-card-title class="text-h3 py-4" style="font-weight: normal;">
|
||||
{{ dialogTitle }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text class="pa-4" style="max-height: 400px; overflow-y: auto;">
|
||||
<div v-if="localKeyValuePairs.length > 0">
|
||||
<div v-for="(pair, index) in localKeyValuePairs" :key="index" class="key-value-pair">
|
||||
<v-row no-gutters align="center" class="mb-2">
|
||||
<v-col cols="4">
|
||||
<v-text-field
|
||||
v-model="pair.key"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
placeholder="键名"
|
||||
@blur="updateKey(index, pair.key)"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="7" class="pl-2 d-flex align-center justify-end">
|
||||
<v-text-field
|
||||
v-if="pair.type === 'string'"
|
||||
v-model="pair.value"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
placeholder="字符串值"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-else-if="pair.type === 'number'"
|
||||
v-model.number="pair.value"
|
||||
type="number"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
placeholder="数值"
|
||||
></v-text-field>
|
||||
<v-switch
|
||||
v-else-if="pair.type === 'boolean'"
|
||||
v-model="pair.value"
|
||||
density="compact"
|
||||
hide-details
|
||||
color="primary"
|
||||
></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="1" class="pl-2">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
color="error"
|
||||
@click="removeKeyValuePair(index)"
|
||||
>
|
||||
<v-icon>mdi-delete</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-center py-8">
|
||||
<v-icon size="64" color="grey-lighten-1">mdi-code-json</v-icon>
|
||||
<p class="text-grey mt-4">暂无参数</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<!-- Add new key-value pair section -->
|
||||
<v-card-text class="pa-4">
|
||||
<div class="d-flex align-center ga-2">
|
||||
<v-text-field
|
||||
v-model="newKey"
|
||||
label="新键名"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
class="flex-grow-1"
|
||||
></v-text-field>
|
||||
<v-select
|
||||
v-model="newValueType"
|
||||
:items="['string', 'number', 'boolean']"
|
||||
label="值类型"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
style="max-width: 120px;"
|
||||
></v-select>
|
||||
<v-btn @click="addKeyValuePair" variant="tonal" color="primary">
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
添加
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn variant="text" @click="cancelDialog">取消</v-btn>
|
||||
<v-btn color="primary" @click="confirmDialog">确认</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useI18n } from '@/i18n/composables'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '修改'
|
||||
},
|
||||
dialogTitle: {
|
||||
type: String,
|
||||
default: '修改键值对'
|
||||
},
|
||||
maxDisplayItems: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const dialog = ref(false)
|
||||
const localKeyValuePairs = ref([])
|
||||
const originalKeyValuePairs = ref([])
|
||||
const newKey = ref('')
|
||||
const newValueType = ref('string')
|
||||
|
||||
// 计算要显示的键名
|
||||
const displayKeys = computed(() => {
|
||||
return Object.keys(props.modelValue).slice(0, props.maxDisplayItems)
|
||||
})
|
||||
|
||||
// 监听 modelValue 变化,主要用于初始化
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
// This watch is primarily for initialization or external changes
|
||||
// The dialog-based editing handles internal updates
|
||||
}, { immediate: true })
|
||||
|
||||
function initializeLocalKeyValuePairs() {
|
||||
localKeyValuePairs.value = []
|
||||
for (const [key, value] of Object.entries(props.modelValue)) {
|
||||
localKeyValuePairs.value.push({
|
||||
key: key,
|
||||
value: value,
|
||||
type: typeof value // Store the original type
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
initializeLocalKeyValuePairs()
|
||||
originalKeyValuePairs.value = JSON.parse(JSON.stringify(localKeyValuePairs.value)) // Deep copy
|
||||
newKey.value = ''
|
||||
newValueType.value = 'string'
|
||||
dialog.value = true
|
||||
}
|
||||
|
||||
function addKeyValuePair() {
|
||||
const key = newKey.value.trim()
|
||||
if (key !== '') {
|
||||
const isKeyExists = localKeyValuePairs.value.some(pair => pair.key === key)
|
||||
if (isKeyExists) {
|
||||
alert('键名已存在')
|
||||
return
|
||||
}
|
||||
|
||||
let defaultValue
|
||||
switch (newValueType.value) {
|
||||
case 'number':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'boolean':
|
||||
defaultValue = false
|
||||
break
|
||||
default: // string
|
||||
defaultValue = ""
|
||||
break
|
||||
}
|
||||
|
||||
localKeyValuePairs.value.push({
|
||||
key: key,
|
||||
value: defaultValue,
|
||||
type: newValueType.value
|
||||
})
|
||||
newKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function removeKeyValuePair(index) {
|
||||
localKeyValuePairs.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function updateKey(index, newKey) {
|
||||
const originalKey = localKeyValuePairs.value[index].key
|
||||
// 如果键名没有改变,则不执行任何操作
|
||||
if (originalKey === newKey) return
|
||||
|
||||
// 检查新键名是否已存在
|
||||
const isKeyExists = localKeyValuePairs.value.some((pair, i) => i !== index && pair.key === newKey)
|
||||
if (isKeyExists) {
|
||||
// 如果键名已存在,提示用户并恢复原值
|
||||
alert('键名已存在')
|
||||
// 将键名恢复为修改前的原始值
|
||||
localKeyValuePairs.value[index].key = originalKey
|
||||
return
|
||||
}
|
||||
|
||||
// 更新本地副本
|
||||
localKeyValuePairs.value[index].key = newKey
|
||||
}
|
||||
|
||||
function confirmDialog() {
|
||||
const updatedValue = {}
|
||||
for (const pair of localKeyValuePairs.value) {
|
||||
let convertedValue = pair.value
|
||||
// 根据声明的类型进行转换
|
||||
switch (pair.type) {
|
||||
case 'number':
|
||||
// 尝试转换为数字,如果失败则保持原值(或设为默认值0)
|
||||
convertedValue = Number(pair.value)
|
||||
// 可选:检查是否为有效数字,无效则设为0或报错
|
||||
// if (isNaN(convertedValue)) convertedValue = 0;
|
||||
break
|
||||
case 'boolean':
|
||||
// 布尔值通常由 v-switch 正确处理,但为保险起见可以显式转换
|
||||
// 注意:在 JavaScript 中,只有严格的 false, 0, "", null, undefined, NaN 会被转换为 false
|
||||
// 这里直接赋值 pair.value 应该是安全的,因为 v-model 绑定的就是布尔值
|
||||
// convertedValue = Boolean(pair.value)
|
||||
break
|
||||
case 'string':
|
||||
default:
|
||||
// 默认转换为字符串
|
||||
convertedValue = String(pair.value)
|
||||
break
|
||||
}
|
||||
updatedValue[pair.key] = convertedValue
|
||||
}
|
||||
emit('update:modelValue', updatedValue)
|
||||
dialog.value = false
|
||||
}
|
||||
|
||||
function cancelDialog() {
|
||||
// Reset to original state
|
||||
localKeyValuePairs.value = JSON.parse(JSON.stringify(originalKeyValuePairs.value))
|
||||
dialog.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.key-value-pair {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -78,7 +78,7 @@ const pluginHeaders = computed(() => [
|
||||
{ title: tm('table.headers.description'), key: 'desc', maxWidth: '250px' },
|
||||
{ title: tm('table.headers.version'), key: 'version', width: '100px' },
|
||||
{ title: tm('table.headers.author'), key: 'author', width: '100px' },
|
||||
{ title: tm('table.headers.status'), key: 'status', width: '80px' },
|
||||
{ title: tm('table.headers.status'), key: 'activated', width: '100px' },
|
||||
{ title: tm('table.headers.actions'), key: 'actions', sortable: false, width: '220px' }
|
||||
]);
|
||||
|
||||
@@ -660,7 +660,7 @@ onMounted(async () => {
|
||||
<div class="text-body-2">{{ item.author }}</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.status="{ item }">
|
||||
<template v-slot:item.activated="{ item }">
|
||||
<v-chip :color="item.activated ? 'success' : 'error'" size="small" class="font-weight-medium"
|
||||
:variant="item.activated ? 'flat' : 'outlined'">
|
||||
{{ item.activated ? tm('status.enabled') : tm('status.disabled') }}
|
||||
|
||||
@@ -318,20 +318,20 @@ export default {
|
||||
|
||||
getTutorialLink(platform_type) {
|
||||
let tutorial_map = {
|
||||
"qq_official_webhook": "https://astrbot.app/deploy/platform/qqofficial/webhook.html",
|
||||
"qq_official": "https://astrbot.app/deploy/platform/qqofficial/websockets.html",
|
||||
"aiocqhttp": "https://astrbot.app/deploy/platform/aiocqhttp/napcat.html",
|
||||
"wecom": "https://astrbot.app/deploy/platform/wecom.html",
|
||||
"lark": "https://astrbot.app/deploy/platform/lark.html",
|
||||
"telegram": "https://astrbot.app/deploy/platform/telegram.html",
|
||||
"dingtalk": "https://astrbot.app/deploy/platform/dingtalk.html",
|
||||
"wechatpadpro": "https://astrbot.app/deploy/platform/wechat/wechatpadpro.html",
|
||||
"weixin_official_account": "https://astrbot.app/deploy/platform/weixin-official-account.html",
|
||||
"discord": "https://astrbot.app/deploy/platform/discord.html",
|
||||
"slack": "https://astrbot.app/deploy/platform/slack.html",
|
||||
"kook": "https://astrbot.app/deploy/platform/kook.html",
|
||||
"vocechat": "https://astrbot.app/deploy/platform/vocechat.html",
|
||||
"satori": "https://astrbot.app/deploy/platform/satori.html", // TODO
|
||||
"qq_official_webhook": "https://docs.astrbot.app/deploy/platform/qqofficial/webhook.html",
|
||||
"qq_official": "https://docs.astrbot.app/deploy/platform/qqofficial/websockets.html",
|
||||
"aiocqhttp": "https://docs.astrbot.app/deploy/platform/aiocqhttp/napcat.html",
|
||||
"wecom": "https://docs.astrbot.app/deploy/platform/wecom.html",
|
||||
"lark": "https://docs.astrbot.app/deploy/platform/lark.html",
|
||||
"telegram": "https://docs.astrbot.app/deploy/platform/telegram.html",
|
||||
"dingtalk": "https://docs.astrbot.app/deploy/platform/dingtalk.html",
|
||||
"wechatpadpro": "https://docs.astrbot.app/deploy/platform/wechat/wechatpadpro.html",
|
||||
"weixin_official_account": "https://docs.astrbot.app/deploy/platform/weixin-official-account.html",
|
||||
"discord": "https://docs.astrbot.app/deploy/platform/discord.html",
|
||||
"slack": "https://docs.astrbot.app/deploy/platform/slack.html",
|
||||
"kook": "https://docs.astrbot.app/deploy/platform/kook.html",
|
||||
"vocechat": "https://docs.astrbot.app/deploy/platform/vocechat.html",
|
||||
"satori": "https://docs.astrbot.app/deploy/platform/satori/llonebot.html",
|
||||
}
|
||||
return tutorial_map[platform_type] || "https://docs.astrbot.app";
|
||||
},
|
||||
|
||||
@@ -712,6 +712,19 @@ export default {
|
||||
|
||||
// 2. 为每个provider创建一个并发的测试请求
|
||||
const promises = this.config_data.provider.map(p => {
|
||||
if (!p.enable) {
|
||||
const index = this.providerStatuses.findIndex(s => s.id === p.id);
|
||||
if (index !== -1) {
|
||||
const disabledStatus = {
|
||||
...this.providerStatuses[index],
|
||||
status: 'unavailable',
|
||||
error: '该提供商未被用户启用'
|
||||
};
|
||||
this.providerStatuses.splice(index, 1, disabledStatus);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return axios.get(`/api/config/provider/check_one?id=${p.id}`)
|
||||
.then(res => {
|
||||
if (res.data && res.data.status === 'ok') {
|
||||
@@ -887,4 +900,9 @@ export default {
|
||||
.v-window {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -718,10 +718,10 @@ export default {
|
||||
|
||||
createCollection(name, emoji, description) {
|
||||
// 如果 this.newKB.embedding_provider_id 是 Object
|
||||
if (typeof this.newKB.embedding_provider_id === 'object') {
|
||||
if (this.newKB.embedding_provider_id && typeof this.newKB.embedding_provider_id === 'object') {
|
||||
this.newKB.embedding_provider_id = this.newKB.embedding_provider_id.id || '';
|
||||
}
|
||||
if (typeof this.newKB.rerank_provider_id === 'object') {
|
||||
if (this.newKB.rerank_provider_id && typeof this.newKB.rerank_provider_id === 'object') {
|
||||
this.newKB.rerank_provider_id = this.newKB.rerank_provider_id.id || '';
|
||||
}
|
||||
axios.post('/api/plug/alkaid/kb/create_collection', {
|
||||
|
||||
45
main.py
45
main.py
@@ -2,11 +2,13 @@ import os
|
||||
import asyncio
|
||||
import sys
|
||||
import mimetypes
|
||||
import argparse
|
||||
from astrbot.core.initial_loader import InitialLoader
|
||||
from astrbot.core import db_helper
|
||||
from astrbot.core import logger, LogManager, LogBroker
|
||||
from astrbot.core.config.default import VERSION
|
||||
from astrbot.core.utils.io import download_dashboard, get_dashboard_version
|
||||
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
|
||||
|
||||
# add parent path to sys.path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
@@ -37,19 +39,28 @@ def check_env():
|
||||
mimetypes.add_type("application/json", ".json")
|
||||
|
||||
|
||||
async def check_dashboard_files():
|
||||
async def check_dashboard_files(webui_dir: str | None = None):
|
||||
"""下载管理面板文件"""
|
||||
|
||||
v = await get_dashboard_version()
|
||||
if v is not None:
|
||||
# has file
|
||||
if v == f"v{VERSION}":
|
||||
logger.info("WebUI 版本已是最新。")
|
||||
# 指定webui目录
|
||||
if webui_dir:
|
||||
if os.path.exists(webui_dir):
|
||||
logger.info(f"使用指定的 WebUI 目录: {webui_dir}")
|
||||
return webui_dir
|
||||
else:
|
||||
logger.warning(
|
||||
f"检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符。"
|
||||
)
|
||||
return
|
||||
logger.warning(f"指定的 WebUI 目录 {webui_dir} 不存在,将使用默认逻辑。")
|
||||
|
||||
data_dist_path = os.path.join(get_astrbot_data_path(), "dist")
|
||||
if os.path.exists(data_dist_path):
|
||||
v = await get_dashboard_version()
|
||||
if v is not None:
|
||||
# has file
|
||||
if v == f"v{VERSION}":
|
||||
logger.info("WebUI 版本已是最新。")
|
||||
else:
|
||||
logger.warning(
|
||||
f"检测到 WebUI 版本 ({v}) 与当前 AstrBot 版本 (v{VERSION}) 不符。"
|
||||
)
|
||||
return data_dist_path
|
||||
|
||||
logger.info(
|
||||
"开始下载管理面板文件...高峰期(晚上)可能导致较慢的速度。如多次下载失败,请前往 https://github.com/Soulter/AstrBot/releases/latest 下载 dist.zip,并将其中的 dist 文件夹解压至 data 目录下。"
|
||||
@@ -59,12 +70,19 @@ async def check_dashboard_files():
|
||||
await download_dashboard(version=f"v{VERSION}", latest=False)
|
||||
except Exception as e:
|
||||
logger.critical(f"下载管理面板文件失败: {e}。")
|
||||
return
|
||||
return None
|
||||
|
||||
logger.info("管理面板下载完成。")
|
||||
return data_dist_path
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="AstrBot")
|
||||
parser.add_argument(
|
||||
"--webui-dir", type=str, help="指定 WebUI 静态文件目录路径", default=None
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
check_env()
|
||||
|
||||
# start log broker
|
||||
@@ -72,7 +90,7 @@ if __name__ == "__main__":
|
||||
LogManager.set_queue_handler(logger, log_broker)
|
||||
|
||||
# check dashboard files
|
||||
asyncio.run(check_dashboard_files())
|
||||
webui_dir = asyncio.run(check_dashboard_files(args.webui_dir))
|
||||
|
||||
db = db_helper
|
||||
|
||||
@@ -80,4 +98,5 @@ if __name__ == "__main__":
|
||||
logger.info(logo_tmpl)
|
||||
|
||||
core_lifecycle = InitialLoader(db, log_broker)
|
||||
core_lifecycle.webui_dir = webui_dir
|
||||
asyncio.run(core_lifecycle.start())
|
||||
|
||||
@@ -35,7 +35,9 @@ class LongTermMemory:
|
||||
else False
|
||||
)
|
||||
image_caption_prompt = cfg["provider_settings"]["image_caption_prompt"]
|
||||
image_caption_provider_id = cfg["provider_settings"]["default_image_caption_provider_id"]
|
||||
image_caption_provider_id = cfg["provider_settings"][
|
||||
"default_image_caption_provider_id"
|
||||
]
|
||||
active_reply = cfg["provider_ltm_settings"]["active_reply"]
|
||||
enable_active_reply = active_reply.get("enable", False)
|
||||
ar_method = active_reply["method"]
|
||||
|
||||
@@ -1232,11 +1232,13 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
|
||||
if req.conversation:
|
||||
# persona inject
|
||||
persona_id = req.conversation.persona_id
|
||||
persona_id = req.conversation.persona_id or cfg.get("default_personality")
|
||||
if not persona_id and persona_id != "[%None]": # [%None] 为用户取消人格
|
||||
persona_id = self.context.persona_manager.selected_default_persona_v3[
|
||||
"name"
|
||||
]
|
||||
default_persona = (
|
||||
self.context.persona_manager.selected_default_persona_v3
|
||||
)
|
||||
if default_persona:
|
||||
persona_id = default_persona["name"]
|
||||
persona = next(
|
||||
builtins.filter(
|
||||
lambda persona: persona["name"] == persona_id,
|
||||
@@ -1255,11 +1257,14 @@ UID: {user_id} 此 ID 可用于设置管理员。
|
||||
if (persona and persona.get("tools") is None) or not persona:
|
||||
# select all
|
||||
toolset = tmgr.get_full_tool_set()
|
||||
for tool in toolset:
|
||||
if not tool.active:
|
||||
toolset.remove_tool(tool.name)
|
||||
else:
|
||||
toolset = ToolSet()
|
||||
for tool_name in persona["tools"]:
|
||||
tool = tmgr.get_func(tool_name)
|
||||
if tool:
|
||||
if tool and tool.active:
|
||||
toolset.add_tool(tool)
|
||||
req.func_tool = toolset
|
||||
logger.debug(f"Tool set for persona {persona_id}: {toolset.names()}")
|
||||
|
||||
@@ -5,7 +5,7 @@ import astrbot.api.star as star
|
||||
import astrbot.api.event.filter as filter
|
||||
from astrbot.api.event import AstrMessageEvent, MessageEventResult
|
||||
from astrbot.api.provider import ProviderRequest
|
||||
from astrbot.api import llm_tool, agent, logger, AstrBotConfig
|
||||
from astrbot.api import llm_tool, logger, AstrBotConfig
|
||||
from astrbot.core.provider.func_tool_manager import FunctionToolManager
|
||||
from .engines import SearchResult
|
||||
from .engines.bing import Bing
|
||||
@@ -26,6 +26,23 @@ class Main(star.Star):
|
||||
|
||||
def __init__(self, context: star.Context) -> None:
|
||||
self.context = context
|
||||
self.tavily_key_index = 0
|
||||
self.tavily_key_lock = asyncio.Lock()
|
||||
|
||||
# 将 str 类型的 key 迁移至 list[str],并保存
|
||||
cfg = self.context.get_config()
|
||||
provider_settings = cfg.get("provider_settings")
|
||||
if provider_settings:
|
||||
tavily_key = provider_settings.get("websearch_tavily_key")
|
||||
if isinstance(tavily_key, str):
|
||||
logger.info(
|
||||
"检测到旧版 websearch_tavily_key (字符串格式),自动迁移为列表格式并保存。"
|
||||
)
|
||||
if tavily_key:
|
||||
provider_settings["websearch_tavily_key"] = [tavily_key]
|
||||
else:
|
||||
provider_settings["websearch_tavily_key"] = []
|
||||
cfg.save_config()
|
||||
|
||||
self.bing_search = Bing()
|
||||
self.sogo_search = Sogo()
|
||||
@@ -94,13 +111,22 @@ class Main(star.Star):
|
||||
|
||||
return results
|
||||
|
||||
async def _get_tavily_key(self, cfg: AstrBotConfig) -> str:
|
||||
"""并发安全的从列表中获取并轮换Tavily API密钥。"""
|
||||
tavily_keys = cfg.get("provider_settings", {}).get("websearch_tavily_key", [])
|
||||
if not tavily_keys:
|
||||
raise ValueError("错误:Tavily API密钥未在AstrBot中配置。")
|
||||
|
||||
async with self.tavily_key_lock:
|
||||
key = tavily_keys[self.tavily_key_index]
|
||||
self.tavily_key_index = (self.tavily_key_index + 1) % len(tavily_keys)
|
||||
return key
|
||||
|
||||
async def _web_search_tavily(
|
||||
self, cfg: AstrBotConfig, payload: dict
|
||||
) -> list[SearchResult]:
|
||||
"""使用 Tavily 搜索引擎进行搜索"""
|
||||
tavily_key = cfg.get("provider_settings", {}).get("websearch_tavily_key", None)
|
||||
if not tavily_key:
|
||||
raise ValueError("Error: Tavily API key is not configured in AstrBot.")
|
||||
tavily_key = await self._get_tavily_key(cfg)
|
||||
url = "https://api.tavily.com/search"
|
||||
header = {
|
||||
"Authorization": f"Bearer {tavily_key}",
|
||||
@@ -128,9 +154,7 @@ class Main(star.Star):
|
||||
|
||||
async def _extract_tavily(self, cfg: AstrBotConfig, payload: dict) -> list[dict]:
|
||||
"""使用 Tavily 提取网页内容"""
|
||||
tavily_key = cfg.get("provider_settings", {}).get("websearch_tavily_key", None)
|
||||
if not tavily_key:
|
||||
raise ValueError("Error: Tavily API key is not configured in AstrBot.")
|
||||
tavily_key = await self._get_tavily_key(cfg)
|
||||
url = "https://api.tavily.com/extract"
|
||||
header = {
|
||||
"Authorization": f"Bearer {tavily_key}",
|
||||
@@ -235,8 +259,7 @@ class Main(star.Star):
|
||||
logger.info(f"web_searcher - search_from_tavily: {query}")
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
websearch_link = cfg["provider_settings"].get("web_search_link", False)
|
||||
tavily_key = cfg.get("provider_settings", {}).get("websearch_tavily_key", None)
|
||||
if not tavily_key:
|
||||
if not cfg.get("provider_settings", {}).get("websearch_tavily_key", []):
|
||||
raise ValueError("Error: Tavily API key is not configured in AstrBot.")
|
||||
|
||||
# build payload
|
||||
@@ -288,8 +311,7 @@ class Main(star.Star):
|
||||
extract_depth(string): Optional. The depth of the extraction, must be one of 'basic', 'advanced'. Default is "basic".
|
||||
"""
|
||||
cfg = self.context.get_config(umo=event.unified_msg_origin)
|
||||
tavily_key = cfg.get("provider_settings", {}).get("websearch_tavily_key", None)
|
||||
if not tavily_key:
|
||||
if not cfg.get("provider_settings", {}).get("websearch_tavily_key", []):
|
||||
raise ValueError("Error: Tavily API key is not configured in AstrBot.")
|
||||
|
||||
if not url:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "AstrBot"
|
||||
version = "4.0.0"
|
||||
version = "4.1.2"
|
||||
description = "易上手的多平台 LLM 聊天机器人及开发框架"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user